Skip to content

Commit a519152

Browse files
author
Amaad Martin
committed
Fix: un-skip the workflow/loop_001 conformance test
ExitLoopTool is implemented and now resolved from YAML configs, so the skip reason no longer holds. Add a TestRunner suite that replays an in-memory equivalent of the case (LoopAgent -> LlmAgent calling exit_loop) as executable proof, since the conformance corpus itself is not vendored in this repo.
1 parent a73f149 commit a519152

2 files changed

Lines changed: 140 additions & 1 deletion

File tree

dev/src/integration/test_runner.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ const SKIPPED_TESTS = [
3030
name: 'tool/example_tool_001',
3131
reason: 'ExampleTool is not implemented yet.',
3232
},
33-
{name: 'workflow/loop_001', reason: 'ExitLoopTool is not implemented yet.'},
3433
{
3534
name: 'core/multi_005',
3635
reason: 'Suspected broken test. Need to re-evaluate.',
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {createEvent, createEventActions, createSession} from '@google/adk';
8+
import {beforeEach, describe, expect, it} from 'vitest';
9+
import {AgentRegistry} from '../../src/integration/agent_registry.js';
10+
import {YamlAgentConfig} from '../../src/integration/agent_types.js';
11+
import {IntegrationRegistry} from '../../src/integration/integration_registry.js';
12+
import {TestRunner} from '../../src/integration/test_runner.js';
13+
import {TestInfo} from '../../src/integration/test_types.js';
14+
15+
const ROOT_AGENT = 'loop_root_agent';
16+
const SUB_AGENT = 'refiner_agent';
17+
const USER_MESSAGE = 'Refine the poem.';
18+
19+
/**
20+
* Registers the in-memory equivalent of the `workflow/loop_001` conformance
21+
* corpus: a LoopAgent whose only sub-agent calls `exit_loop`.
22+
*/
23+
function registerLoopAgents(registry: AgentRegistry) {
24+
registry.registerAgentConfig('loop_test/refiner_agent', {
25+
name: SUB_AGENT,
26+
model: 'gemini-2.5-flash',
27+
description: 'Refines a poem.',
28+
instruction: 'Refine the poem, then call exit_loop.',
29+
agentClass: 'LlmAgent',
30+
tools: [{name: 'exit_loop'}],
31+
} as unknown as YamlAgentConfig);
32+
33+
registry.registerAgentConfig('loop_test/root_agent', {
34+
name: ROOT_AGENT,
35+
model: 'gemini-2.5-flash',
36+
description: 'Loops until the refiner exits.',
37+
instruction: '',
38+
agentClass: 'LoopAgent',
39+
maxIterations: '3',
40+
isRootAgent: true,
41+
subAgents: [{configPath: 'loop_test/refiner_agent'}],
42+
} as unknown as YamlAgentConfig);
43+
}
44+
45+
/**
46+
* The session the harness must reproduce: the refiner agent calls `exit_loop`
47+
* once and the LoopAgent stops, so there is exactly one iteration. The final
48+
* function-response event carries `escalate` / `skipSummarization`, which only
49+
* happens when the `exit_loop` tool is resolved and its side effects replayed.
50+
*
51+
* The fixture keeps the shape of a recorded `generated-session.yaml`, but note
52+
* that `filterPartFields` strips `functionCall` / `functionResponse` from every
53+
* part before the comparison, so those payloads are documentation rather than
54+
* assertions.
55+
*/
56+
function expectedSession() {
57+
return createSession({
58+
id: 'expected-session',
59+
appName: 'test-runner',
60+
events: [
61+
createEvent({
62+
author: 'user',
63+
content: {role: 'user', parts: [{text: USER_MESSAGE}]},
64+
}),
65+
createEvent({
66+
author: SUB_AGENT,
67+
content: {
68+
role: 'model',
69+
parts: [{functionCall: {name: 'exit_loop', args: {}}}],
70+
},
71+
}),
72+
createEvent({
73+
author: SUB_AGENT,
74+
content: {
75+
role: 'user',
76+
parts: [
77+
{functionResponse: {name: 'exit_loop', response: {result: null}}},
78+
],
79+
},
80+
actions: {
81+
...createEventActions(),
82+
escalate: true,
83+
skipSummarization: true,
84+
},
85+
}),
86+
],
87+
});
88+
}
89+
90+
function loopTestInfo(): TestInfo {
91+
return {
92+
name: 'workflow/loop_001',
93+
spec: {
94+
description: 'The refiner agent exits the loop on the first iteration.',
95+
agent: 'loop_test',
96+
userMessages: [{text: USER_MESSAGE}],
97+
},
98+
recordings: {
99+
recordings: [
100+
{
101+
userMessageIndex: 0,
102+
agentName: SUB_AGENT,
103+
llmRecording: {
104+
llmResponse: {
105+
content: {
106+
role: 'model',
107+
parts: [{functionCall: {name: 'exit_loop', args: {}}}],
108+
},
109+
},
110+
},
111+
},
112+
{
113+
userMessageIndex: 0,
114+
agentName: SUB_AGENT,
115+
toolRecording: {
116+
toolCall: {name: 'exit_loop'},
117+
toolResponse: {response: {result: null}},
118+
},
119+
},
120+
],
121+
},
122+
session: expectedSession(),
123+
};
124+
}
125+
126+
describe('TestRunner', () => {
127+
let registry: AgentRegistry;
128+
let testRunner: TestRunner;
129+
130+
beforeEach(() => {
131+
registry = new AgentRegistry(new IntegrationRegistry());
132+
testRunner = new TestRunner(registry);
133+
});
134+
135+
it('replays a loop agent that exits via the exit_loop built-in tool', async () => {
136+
registerLoopAgents(registry);
137+
138+
await expect(testRunner.run(loopTestInfo(), false)).resolves.toBe(false);
139+
});
140+
});

0 commit comments

Comments
 (0)