Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions packages/agents-core/test/createSpans.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';

import {
createAgentSpan,
createFunctionSpan,
createGuardrailSpan,
createSpeechSpan,
createMCPListToolsSpan,
withAgentSpan,
withFunctionSpan,
} from '../src/tracing/createSpans';
import {
getCurrentSpan,
setTraceProcessors,
setTracingDisabled,
withTrace,
} from '../src/tracing';
import type { TraceProvider } from '../src/tracing/provider';
import type { Span } from '../src/tracing/spans';
import * as providerModule from '../src/tracing/provider';
import { defaultProcessor, TracingProcessor } from '../src/tracing/processor';
import type { Trace } from '../src/tracing/traces';

class RecordingProcessor implements TracingProcessor {
tracesStarted: Trace[] = [];
tracesEnded: Trace[] = [];
spansStarted: Span<any>[] = [];
spansEnded: Span<any>[] = [];

async onTraceStart(trace: Trace): Promise<void> {
this.tracesStarted.push(trace);
}
async onTraceEnd(trace: Trace): Promise<void> {
this.tracesEnded.push(trace);
}
async onSpanStart(span: Span<any>): Promise<void> {
this.spansStarted.push(span);
}
async onSpanEnd(span: Span<any>): Promise<void> {
this.spansEnded.push(span);
}
async shutdown(): Promise<void> {
/* noop */
}
async forceFlush(): Promise<void> {
/* noop */
}
reset() {
this.tracesStarted.length = 0;
this.tracesEnded.length = 0;
this.spansStarted.length = 0;
this.spansEnded.length = 0;
}
}

describe('create*Span helpers', () => {
const createSpanMock = vi.fn();
let providerSpy: ReturnType<typeof vi.spyOn> | undefined;
const fakeSpan = { spanId: 'span', traceId: 'trace' } as Span<any>;

beforeEach(() => {
createSpanMock.mockReturnValue(fakeSpan);
providerSpy = vi.spyOn(providerModule, 'getGlobalTraceProvider');
providerSpy.mockReturnValue({
createSpan: createSpanMock,
} as unknown as TraceProvider);
});

afterEach(() => {
createSpanMock.mockReset();
providerSpy?.mockRestore();
});

it('createAgentSpan falls back to the default name when not provided', () => {
createAgentSpan();

expect(createSpanMock).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ type: 'agent', name: 'Agent' }),
}),
undefined,
);
});

it('createFunctionSpan populates default input/output values', () => {
createFunctionSpan({ data: { name: 'call' } });

const calls = createSpanMock.mock.calls;
const [options] = calls[calls.length - 1];
expect(options.data).toMatchObject({
type: 'function',
name: 'call',
input: '',
output: '',
});
});

it('createGuardrailSpan enforces a non-triggered default state', () => {
createGuardrailSpan({ data: { name: 'moderation' } });

const calls = createSpanMock.mock.calls;
const [options] = calls[calls.length - 1];
expect(options.data).toMatchObject({
type: 'guardrail',
name: 'moderation',
triggered: false,
});
});

it('createSpeechSpan forwards the provided payload with the expected type', () => {
createSpeechSpan({
data: { output: { data: 'pcm-data', format: 'pcm' } },
});

const calls = createSpanMock.mock.calls;
const [options] = calls[calls.length - 1];
expect(options.data).toMatchObject({
type: 'speech',
output: { data: 'pcm-data', format: 'pcm' },
});
});

it('createMCPListToolsSpan stamps the span type', () => {
createMCPListToolsSpan();

const calls = createSpanMock.mock.calls;
const [options] = calls[calls.length - 1];
expect(options.data).toMatchObject({ type: 'mcp_tools' });
});
});

describe('with*Span helpers', () => {
const processor = new RecordingProcessor();

beforeEach(() => {
processor.reset();
setTraceProcessors([processor]);
setTracingDisabled(false);
});

afterEach(() => {
setTraceProcessors([defaultProcessor()]);
setTracingDisabled(true);
});

it('records errors and restores the previous span when the callback throws', async () => {
const failingError = Object.assign(new Error('boom'), {
data: { reason: 'bad input' },
});

await withTrace('workflow', async () => {
await withAgentSpan(async (outerSpan) => {
await expect(
withFunctionSpan(
async () => {
expect(getCurrentSpan()).toBeDefined();
throw failingError;
},
{ data: { name: 'inner' } },
),
).rejects.toThrow('boom');

expect(getCurrentSpan()).toBe(outerSpan);
});

expect(getCurrentSpan()).toBeNull();
});

const functionSpan = processor.spansEnded.find(
(span) => span.spanData.type === 'function',
);
expect(functionSpan?.error).toMatchObject({
message: 'boom',
data: { reason: 'bad input' },
});
});
});
62 changes: 62 additions & 0 deletions packages/agents-core/test/defaultModel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import {
OPENAI_DEFAULT_MODEL_ENV_VARIABLE_NAME,
getDefaultModel,
getDefaultModelSettings,
gpt5ReasoningSettingsRequired,
isGpt5Default,
} from '../src/defaultModel';
import { loadEnv } from '../src/config';
vi.mock('../src/config', () => ({
loadEnv: vi.fn(),
}));
const mockedLoadEnv = vi.mocked(loadEnv);
beforeEach(() => {
mockedLoadEnv.mockReset();
mockedLoadEnv.mockReturnValue({});
});
describe('gpt5ReasoningSettingsRequired', () => {
test('detects GPT-5 models while ignoring chat latest', () => {
expect(gpt5ReasoningSettingsRequired('gpt-5.1-mini')).toBe(true);
expect(gpt5ReasoningSettingsRequired('gpt-5-pro')).toBe(true);
expect(gpt5ReasoningSettingsRequired('gpt-5-chat-latest')).toBe(false);
});
test('returns false for non GPT-5 models', () => {
expect(gpt5ReasoningSettingsRequired('gpt-4o')).toBe(false);
});
});
describe('getDefaultModel', () => {
test('falls back to gpt-4.1 when env var missing', () => {
mockedLoadEnv.mockReturnValue({});
expect(getDefaultModel()).toBe('gpt-4.1');
});
test('lowercases provided env value', () => {
mockedLoadEnv.mockReturnValue({
[OPENAI_DEFAULT_MODEL_ENV_VARIABLE_NAME]: 'GPT-5-CHAT',
});
expect(getDefaultModel()).toBe('gpt-5-chat');
});
});
describe('isGpt5Default', () => {
test('returns true only when env points to GPT-5', () => {
mockedLoadEnv.mockReturnValue({
[OPENAI_DEFAULT_MODEL_ENV_VARIABLE_NAME]: 'gpt-5-preview',
});
expect(isGpt5Default()).toBe(true);
mockedLoadEnv.mockReturnValue({
[OPENAI_DEFAULT_MODEL_ENV_VARIABLE_NAME]: 'gpt-4o-mini',
});
expect(isGpt5Default()).toBe(false);
});
});
describe('getDefaultModelSettings', () => {
test('returns reasoning defaults for GPT-5 models', () => {
expect(getDefaultModelSettings('gpt-5.1-mini')).toEqual({
reasoning: { effort: 'low' },
text: { verbosity: 'low' },
});
});
test('returns empty settings for non GPT-5 models', () => {
expect(getDefaultModelSettings('gpt-4o')).toEqual({});
});
});
98 changes: 98 additions & 0 deletions packages/agents-core/test/extensions/handoffFilters.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
import { describe, test, expect } from 'vitest';
import { removeAllTools } from '../../src/extensions';
import {
RunHandoffCallItem,
RunHandoffOutputItem,
RunMessageOutputItem,
RunToolCallItem,
RunToolCallOutputItem,
} from '../../src/items';
import type { AgentInputItem } from '../../src/types';
import type * as protocol from '../../src/types/protocol';
import {
TEST_AGENT,
TEST_MODEL_FUNCTION_CALL,
fakeModelMessage,
} from '../stubs';

const functionCallResult: protocol.FunctionCallResultItem = {
type: 'function_call_result',
callId: 'call-1',
name: 'tool',
status: 'completed',
output: { type: 'text', text: 'done' },
};

const computerCall: protocol.ComputerUseCallItem = {
type: 'computer_call',
callId: 'computer-call',
status: 'completed',
action: { type: 'screenshot' },
};

const computerCallResult: protocol.ComputerCallResultItem = {
type: 'computer_call_result',
callId: 'computer-call',
output: { type: 'computer_screenshot', data: 'image-data' },
};

const hostedToolCall: protocol.HostedToolCallItem = {
type: 'hosted_tool_call',
name: 'web_search_call',
arguments: '{"q":"openai"}',
status: 'completed',
output: 'results',
};

describe('removeAllTools', () => {
test('should be available', () => {
Expand All @@ -14,4 +57,59 @@ describe('removeAllTools', () => {
newItems: [],
});
});

test('removes run tool and handoff items from run collections', () => {
const message = new RunMessageOutputItem(
fakeModelMessage('ok'),
TEST_AGENT,
);
const anotherMessage = new RunMessageOutputItem(
fakeModelMessage('still here'),
TEST_AGENT,
);

const result = removeAllTools({
inputHistory: 'keep me',
preHandoffItems: [
new RunHandoffCallItem(TEST_MODEL_FUNCTION_CALL, TEST_AGENT),
message,
new RunToolCallItem(TEST_MODEL_FUNCTION_CALL, TEST_AGENT),
],
newItems: [
new RunToolCallOutputItem(functionCallResult, TEST_AGENT, 'ok'),
new RunHandoffOutputItem(functionCallResult, TEST_AGENT, TEST_AGENT),
anotherMessage,
],
});

expect(result.inputHistory).toBe('keep me');
expect(result.preHandoffItems).toStrictEqual([message]);
expect(result.newItems).toStrictEqual([anotherMessage]);
});

test('filters out tool typed input history entries', () => {
const userMessage = {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'hello' }],
} as AgentInputItem;
const history: AgentInputItem[] = [
userMessage,
TEST_MODEL_FUNCTION_CALL,
functionCallResult,
computerCall,
computerCallResult,
hostedToolCall,
];

const result = removeAllTools({
inputHistory: history,
preHandoffItems: [],
newItems: [],
});

expect(result.inputHistory).toStrictEqual([userMessage]);
expect(history).toHaveLength(6);
expect((result.inputHistory as AgentInputItem[])[0]).toBe(userMessage);
});
});
Loading