Skip to content

Commit 33935a4

Browse files
authored
add unit tests for uncovered patterns (#616)
1 parent 27915f7 commit 33935a4

File tree

4 files changed

+412
-0
lines changed

4 files changed

+412
-0
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2+
3+
import {
4+
createAgentSpan,
5+
createFunctionSpan,
6+
createGuardrailSpan,
7+
createSpeechSpan,
8+
createMCPListToolsSpan,
9+
withAgentSpan,
10+
withFunctionSpan,
11+
} from '../src/tracing/createSpans';
12+
import {
13+
getCurrentSpan,
14+
setTraceProcessors,
15+
setTracingDisabled,
16+
withTrace,
17+
} from '../src/tracing';
18+
import type { TraceProvider } from '../src/tracing/provider';
19+
import type { Span } from '../src/tracing/spans';
20+
import * as providerModule from '../src/tracing/provider';
21+
import { defaultProcessor, TracingProcessor } from '../src/tracing/processor';
22+
import type { Trace } from '../src/tracing/traces';
23+
24+
class RecordingProcessor implements TracingProcessor {
25+
tracesStarted: Trace[] = [];
26+
tracesEnded: Trace[] = [];
27+
spansStarted: Span<any>[] = [];
28+
spansEnded: Span<any>[] = [];
29+
30+
async onTraceStart(trace: Trace): Promise<void> {
31+
this.tracesStarted.push(trace);
32+
}
33+
async onTraceEnd(trace: Trace): Promise<void> {
34+
this.tracesEnded.push(trace);
35+
}
36+
async onSpanStart(span: Span<any>): Promise<void> {
37+
this.spansStarted.push(span);
38+
}
39+
async onSpanEnd(span: Span<any>): Promise<void> {
40+
this.spansEnded.push(span);
41+
}
42+
async shutdown(): Promise<void> {
43+
/* noop */
44+
}
45+
async forceFlush(): Promise<void> {
46+
/* noop */
47+
}
48+
reset() {
49+
this.tracesStarted.length = 0;
50+
this.tracesEnded.length = 0;
51+
this.spansStarted.length = 0;
52+
this.spansEnded.length = 0;
53+
}
54+
}
55+
56+
describe('create*Span helpers', () => {
57+
const createSpanMock = vi.fn();
58+
let providerSpy: ReturnType<typeof vi.spyOn> | undefined;
59+
const fakeSpan = { spanId: 'span', traceId: 'trace' } as Span<any>;
60+
61+
beforeEach(() => {
62+
createSpanMock.mockReturnValue(fakeSpan);
63+
providerSpy = vi.spyOn(providerModule, 'getGlobalTraceProvider');
64+
providerSpy.mockReturnValue({
65+
createSpan: createSpanMock,
66+
} as unknown as TraceProvider);
67+
});
68+
69+
afterEach(() => {
70+
createSpanMock.mockReset();
71+
providerSpy?.mockRestore();
72+
});
73+
74+
it('createAgentSpan falls back to the default name when not provided', () => {
75+
createAgentSpan();
76+
77+
expect(createSpanMock).toHaveBeenCalledWith(
78+
expect.objectContaining({
79+
data: expect.objectContaining({ type: 'agent', name: 'Agent' }),
80+
}),
81+
undefined,
82+
);
83+
});
84+
85+
it('createFunctionSpan populates default input/output values', () => {
86+
createFunctionSpan({ data: { name: 'call' } });
87+
88+
const calls = createSpanMock.mock.calls;
89+
const [options] = calls[calls.length - 1];
90+
expect(options.data).toMatchObject({
91+
type: 'function',
92+
name: 'call',
93+
input: '',
94+
output: '',
95+
});
96+
});
97+
98+
it('createGuardrailSpan enforces a non-triggered default state', () => {
99+
createGuardrailSpan({ data: { name: 'moderation' } });
100+
101+
const calls = createSpanMock.mock.calls;
102+
const [options] = calls[calls.length - 1];
103+
expect(options.data).toMatchObject({
104+
type: 'guardrail',
105+
name: 'moderation',
106+
triggered: false,
107+
});
108+
});
109+
110+
it('createSpeechSpan forwards the provided payload with the expected type', () => {
111+
createSpeechSpan({
112+
data: { output: { data: 'pcm-data', format: 'pcm' } },
113+
});
114+
115+
const calls = createSpanMock.mock.calls;
116+
const [options] = calls[calls.length - 1];
117+
expect(options.data).toMatchObject({
118+
type: 'speech',
119+
output: { data: 'pcm-data', format: 'pcm' },
120+
});
121+
});
122+
123+
it('createMCPListToolsSpan stamps the span type', () => {
124+
createMCPListToolsSpan();
125+
126+
const calls = createSpanMock.mock.calls;
127+
const [options] = calls[calls.length - 1];
128+
expect(options.data).toMatchObject({ type: 'mcp_tools' });
129+
});
130+
});
131+
132+
describe('with*Span helpers', () => {
133+
const processor = new RecordingProcessor();
134+
135+
beforeEach(() => {
136+
processor.reset();
137+
setTraceProcessors([processor]);
138+
setTracingDisabled(false);
139+
});
140+
141+
afterEach(() => {
142+
setTraceProcessors([defaultProcessor()]);
143+
setTracingDisabled(true);
144+
});
145+
146+
it('records errors and restores the previous span when the callback throws', async () => {
147+
const failingError = Object.assign(new Error('boom'), {
148+
data: { reason: 'bad input' },
149+
});
150+
151+
await withTrace('workflow', async () => {
152+
await withAgentSpan(async (outerSpan) => {
153+
await expect(
154+
withFunctionSpan(
155+
async () => {
156+
expect(getCurrentSpan()).toBeDefined();
157+
throw failingError;
158+
},
159+
{ data: { name: 'inner' } },
160+
),
161+
).rejects.toThrow('boom');
162+
163+
expect(getCurrentSpan()).toBe(outerSpan);
164+
});
165+
166+
expect(getCurrentSpan()).toBeNull();
167+
});
168+
169+
const functionSpan = processor.spansEnded.find(
170+
(span) => span.spanData.type === 'function',
171+
);
172+
expect(functionSpan?.error).toMatchObject({
173+
message: 'boom',
174+
data: { reason: 'bad input' },
175+
});
176+
});
177+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { beforeEach, describe, expect, test, vi } from 'vitest';
2+
import {
3+
OPENAI_DEFAULT_MODEL_ENV_VARIABLE_NAME,
4+
getDefaultModel,
5+
getDefaultModelSettings,
6+
gpt5ReasoningSettingsRequired,
7+
isGpt5Default,
8+
} from '../src/defaultModel';
9+
import { loadEnv } from '../src/config';
10+
vi.mock('../src/config', () => ({
11+
loadEnv: vi.fn(),
12+
}));
13+
const mockedLoadEnv = vi.mocked(loadEnv);
14+
beforeEach(() => {
15+
mockedLoadEnv.mockReset();
16+
mockedLoadEnv.mockReturnValue({});
17+
});
18+
describe('gpt5ReasoningSettingsRequired', () => {
19+
test('detects GPT-5 models while ignoring chat latest', () => {
20+
expect(gpt5ReasoningSettingsRequired('gpt-5.1-mini')).toBe(true);
21+
expect(gpt5ReasoningSettingsRequired('gpt-5-pro')).toBe(true);
22+
expect(gpt5ReasoningSettingsRequired('gpt-5-chat-latest')).toBe(false);
23+
});
24+
test('returns false for non GPT-5 models', () => {
25+
expect(gpt5ReasoningSettingsRequired('gpt-4o')).toBe(false);
26+
});
27+
});
28+
describe('getDefaultModel', () => {
29+
test('falls back to gpt-4.1 when env var missing', () => {
30+
mockedLoadEnv.mockReturnValue({});
31+
expect(getDefaultModel()).toBe('gpt-4.1');
32+
});
33+
test('lowercases provided env value', () => {
34+
mockedLoadEnv.mockReturnValue({
35+
[OPENAI_DEFAULT_MODEL_ENV_VARIABLE_NAME]: 'GPT-5-CHAT',
36+
});
37+
expect(getDefaultModel()).toBe('gpt-5-chat');
38+
});
39+
});
40+
describe('isGpt5Default', () => {
41+
test('returns true only when env points to GPT-5', () => {
42+
mockedLoadEnv.mockReturnValue({
43+
[OPENAI_DEFAULT_MODEL_ENV_VARIABLE_NAME]: 'gpt-5-preview',
44+
});
45+
expect(isGpt5Default()).toBe(true);
46+
mockedLoadEnv.mockReturnValue({
47+
[OPENAI_DEFAULT_MODEL_ENV_VARIABLE_NAME]: 'gpt-4o-mini',
48+
});
49+
expect(isGpt5Default()).toBe(false);
50+
});
51+
});
52+
describe('getDefaultModelSettings', () => {
53+
test('returns reasoning defaults for GPT-5 models', () => {
54+
expect(getDefaultModelSettings('gpt-5.1-mini')).toEqual({
55+
reasoning: { effort: 'low' },
56+
text: { verbosity: 'low' },
57+
});
58+
});
59+
test('returns empty settings for non GPT-5 models', () => {
60+
expect(getDefaultModelSettings('gpt-4o')).toEqual({});
61+
});
62+
});

packages/agents-core/test/extensions/handoffFilters.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,48 @@
11
import { describe, test, expect } from 'vitest';
22
import { removeAllTools } from '../../src/extensions';
3+
import {
4+
RunHandoffCallItem,
5+
RunHandoffOutputItem,
6+
RunMessageOutputItem,
7+
RunToolCallItem,
8+
RunToolCallOutputItem,
9+
} from '../../src/items';
10+
import type { AgentInputItem } from '../../src/types';
11+
import type * as protocol from '../../src/types/protocol';
12+
import {
13+
TEST_AGENT,
14+
TEST_MODEL_FUNCTION_CALL,
15+
fakeModelMessage,
16+
} from '../stubs';
17+
18+
const functionCallResult: protocol.FunctionCallResultItem = {
19+
type: 'function_call_result',
20+
callId: 'call-1',
21+
name: 'tool',
22+
status: 'completed',
23+
output: { type: 'text', text: 'done' },
24+
};
25+
26+
const computerCall: protocol.ComputerUseCallItem = {
27+
type: 'computer_call',
28+
callId: 'computer-call',
29+
status: 'completed',
30+
action: { type: 'screenshot' },
31+
};
32+
33+
const computerCallResult: protocol.ComputerCallResultItem = {
34+
type: 'computer_call_result',
35+
callId: 'computer-call',
36+
output: { type: 'computer_screenshot', data: 'image-data' },
37+
};
38+
39+
const hostedToolCall: protocol.HostedToolCallItem = {
40+
type: 'hosted_tool_call',
41+
name: 'web_search_call',
42+
arguments: '{"q":"openai"}',
43+
status: 'completed',
44+
output: 'results',
45+
};
346

447
describe('removeAllTools', () => {
548
test('should be available', () => {
@@ -14,4 +57,59 @@ describe('removeAllTools', () => {
1457
newItems: [],
1558
});
1659
});
60+
61+
test('removes run tool and handoff items from run collections', () => {
62+
const message = new RunMessageOutputItem(
63+
fakeModelMessage('ok'),
64+
TEST_AGENT,
65+
);
66+
const anotherMessage = new RunMessageOutputItem(
67+
fakeModelMessage('still here'),
68+
TEST_AGENT,
69+
);
70+
71+
const result = removeAllTools({
72+
inputHistory: 'keep me',
73+
preHandoffItems: [
74+
new RunHandoffCallItem(TEST_MODEL_FUNCTION_CALL, TEST_AGENT),
75+
message,
76+
new RunToolCallItem(TEST_MODEL_FUNCTION_CALL, TEST_AGENT),
77+
],
78+
newItems: [
79+
new RunToolCallOutputItem(functionCallResult, TEST_AGENT, 'ok'),
80+
new RunHandoffOutputItem(functionCallResult, TEST_AGENT, TEST_AGENT),
81+
anotherMessage,
82+
],
83+
});
84+
85+
expect(result.inputHistory).toBe('keep me');
86+
expect(result.preHandoffItems).toStrictEqual([message]);
87+
expect(result.newItems).toStrictEqual([anotherMessage]);
88+
});
89+
90+
test('filters out tool typed input history entries', () => {
91+
const userMessage = {
92+
type: 'message',
93+
role: 'user',
94+
content: [{ type: 'input_text', text: 'hello' }],
95+
} as AgentInputItem;
96+
const history: AgentInputItem[] = [
97+
userMessage,
98+
TEST_MODEL_FUNCTION_CALL,
99+
functionCallResult,
100+
computerCall,
101+
computerCallResult,
102+
hostedToolCall,
103+
];
104+
105+
const result = removeAllTools({
106+
inputHistory: history,
107+
preHandoffItems: [],
108+
newItems: [],
109+
});
110+
111+
expect(result.inputHistory).toStrictEqual([userMessage]);
112+
expect(history).toHaveLength(6);
113+
expect((result.inputHistory as AgentInputItem[])[0]).toBe(userMessage);
114+
});
17115
});

0 commit comments

Comments
 (0)