Skip to content

Commit 26769a5

Browse files
authored
Feat/tool registry discovery (google-gemini#28113)
1 parent e977b6b commit 26769a5

4 files changed

Lines changed: 752 additions & 0 deletions

File tree

scripts/tests/eval-analysis.test.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,235 @@ describe('eval-analysis', () => {
279279
'Could not statically resolve eval case object for evalTest call.',
280280
]);
281281
});
282+
283+
describe('tool reference extraction', () => {
284+
it('extracts tool from waitForToolCall string literal', () => {
285+
const analysis = analyzeEvalSource(`
286+
import { evalTest } from './test-helper.js';
287+
evalTest('USUALLY_PASSES', {
288+
name: 'grep test',
289+
prompt: 'find something',
290+
assert: async (rig) => {
291+
await rig.waitForToolCall('grep_search');
292+
},
293+
});
294+
`);
295+
296+
expect(analysis.cases[0].toolReferences).toEqual(['grep_search']);
297+
});
298+
299+
it('extracts tool from toolRequest.name comparison', () => {
300+
const analysis = analyzeEvalSource(`
301+
import { evalTest } from './test-helper.js';
302+
evalTest('USUALLY_PASSES', {
303+
name: 'shell test',
304+
prompt: 'run a command',
305+
assert: async (rig) => {
306+
const logs = rig.readToolLogs();
307+
const calls = logs.filter(
308+
(log) => log.toolRequest.name === 'run_shell_command',
309+
);
310+
},
311+
});
312+
`);
313+
314+
expect(analysis.cases[0].toolReferences).toEqual(['run_shell_command']);
315+
});
316+
317+
it('extracts multiple tools from array includes', () => {
318+
const analysis = analyzeEvalSource(`
319+
import { evalTest } from './test-helper.js';
320+
evalTest('USUALLY_PASSES', {
321+
name: 'edit test',
322+
prompt: 'edit a file',
323+
assert: async (rig) => {
324+
const logs = rig.readToolLogs();
325+
const editCalls = logs.filter(
326+
(log) => ['write_file', 'replace'].includes(log.toolRequest.name),
327+
);
328+
},
329+
});
330+
`);
331+
332+
expect(analysis.cases[0].toolReferences).toEqual([
333+
'replace',
334+
'write_file',
335+
]);
336+
});
337+
338+
it('extracts tool from imported constant', () => {
339+
const analysis = analyzeEvalSource(`
340+
import { TRACKER_CREATE_TASK_TOOL_NAME } from '@google/gemini-cli-core';
341+
import { evalTest } from './test-helper.js';
342+
evalTest('USUALLY_PASSES', {
343+
name: 'tracker test',
344+
prompt: 'create a task',
345+
assert: async (rig) => {
346+
await rig.waitForToolCall(TRACKER_CREATE_TASK_TOOL_NAME);
347+
},
348+
});
349+
`);
350+
351+
expect(analysis.cases[0].toolReferences).toEqual(['tracker_create_task']);
352+
});
353+
354+
it('deduplicates references within a case', () => {
355+
const analysis = analyzeEvalSource(`
356+
import { evalTest } from './test-helper.js';
357+
evalTest('USUALLY_PASSES', {
358+
name: 'dedup test',
359+
prompt: 'search twice',
360+
assert: async (rig) => {
361+
await rig.waitForToolCall('grep_search');
362+
const logs = rig.readToolLogs();
363+
const calls = logs.filter(
364+
(log) => log.toolRequest.name === 'grep_search',
365+
);
366+
},
367+
});
368+
`);
369+
370+
expect(analysis.cases[0].toolReferences).toEqual(['grep_search']);
371+
});
372+
373+
it('sorts references alphabetically', () => {
374+
const analysis = analyzeEvalSource(`
375+
import { evalTest } from './test-helper.js';
376+
evalTest('USUALLY_PASSES', {
377+
name: 'sorted test',
378+
prompt: 'do things',
379+
assert: async (rig) => {
380+
await rig.waitForToolCall('write_file');
381+
await rig.waitForToolCall('grep_search');
382+
await rig.waitForToolCall('glob');
383+
},
384+
});
385+
`);
386+
387+
expect(analysis.cases[0].toolReferences).toEqual([
388+
'glob',
389+
'grep_search',
390+
'write_file',
391+
]);
392+
});
393+
394+
it('returns empty array when no tool refs found', () => {
395+
const analysis = analyzeEvalSource(`
396+
import { evalTest } from './test-helper.js';
397+
evalTest('USUALLY_PASSES', {
398+
name: 'no tools',
399+
prompt: 'just answer',
400+
assert: async (rig, result) => {
401+
expect(result).toContain('hello');
402+
},
403+
});
404+
`);
405+
406+
expect(analysis.cases[0].toolReferences).toEqual([]);
407+
});
408+
409+
it('aggregates file-level toolReferences across cases', () => {
410+
const analysis = analyzeEvalSource(`
411+
import { evalTest } from './test-helper.js';
412+
evalTest('USUALLY_PASSES', {
413+
name: 'case 1',
414+
prompt: 'first',
415+
assert: async (rig) => {
416+
await rig.waitForToolCall('grep_search');
417+
},
418+
});
419+
evalTest('USUALLY_PASSES', {
420+
name: 'case 2',
421+
prompt: 'second',
422+
assert: async (rig) => {
423+
await rig.waitForToolCall('write_file');
424+
},
425+
});
426+
`);
427+
428+
expect(analysis.toolReferences).toEqual(['grep_search', 'write_file']);
429+
});
430+
431+
it('deduplicates file-level toolReferences', () => {
432+
const analysis = analyzeEvalSource(`
433+
import { evalTest } from './test-helper.js';
434+
evalTest('USUALLY_PASSES', {
435+
name: 'case 1',
436+
prompt: 'first',
437+
assert: async (rig) => {
438+
await rig.waitForToolCall('grep_search');
439+
},
440+
});
441+
evalTest('USUALLY_PASSES', {
442+
name: 'case 2',
443+
prompt: 'second',
444+
assert: async (rig) => {
445+
await rig.waitForToolCall('grep_search');
446+
},
447+
});
448+
`);
449+
450+
expect(analysis.toolReferences).toEqual(['grep_search']);
451+
});
452+
453+
it('handles aliased constant imports', () => {
454+
const analysis = analyzeEvalSource(`
455+
import { TRACKER_CREATE_TASK_TOOL_NAME as CREATE_TOOL } from '@google/gemini-cli-core';
456+
import { evalTest } from './test-helper.js';
457+
evalTest('USUALLY_PASSES', {
458+
name: 'alias test',
459+
prompt: 'create task',
460+
assert: async (rig) => {
461+
await rig.waitForToolCall(CREATE_TOOL);
462+
},
463+
});
464+
`);
465+
466+
expect(analysis.cases[0].toolReferences).toEqual(['tracker_create_task']);
467+
});
468+
469+
it('handles reversed toolRequest.name comparison', () => {
470+
const analysis = analyzeEvalSource(`
471+
import { evalTest } from './test-helper.js';
472+
evalTest('USUALLY_PASSES', {
473+
name: 'reversed compare',
474+
prompt: 'do something',
475+
assert: async (rig) => {
476+
const logs = rig.readToolLogs();
477+
const calls = logs.filter(
478+
(log) => 'replace' === log.toolRequest.name,
479+
);
480+
},
481+
});
482+
`);
483+
484+
expect(analysis.cases[0].toolReferences).toEqual(['replace']);
485+
});
486+
487+
it('extracts tools from real grep_search eval pattern', () => {
488+
const analysis = analyzeEvalSource(
489+
`
490+
import { describe, expect } from 'vitest';
491+
import { evalTest, TestRig } from './test-helper.js';
492+
493+
describe('grep_search_functionality', () => {
494+
evalTest('USUALLY_PASSES', {
495+
suiteName: 'default',
496+
suiteType: 'behavioral',
497+
name: 'should find a simple string in a file',
498+
files: { 'test.txt': 'hello world' },
499+
prompt: 'Find "world" in test.txt',
500+
assert: async (rig: TestRig, result: string) => {
501+
await rig.waitForToolCall('grep_search');
502+
},
503+
});
504+
});
505+
`,
506+
{ filePath: '/repo/evals/grep_search.eval.ts', repoRoot: '/repo' },
507+
);
508+
509+
expect(analysis.cases[0].toolReferences).toEqual(['grep_search']);
510+
expect(analysis.toolReferences).toEqual(['grep_search']);
511+
});
512+
});
282513
});
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, expect, it } from 'vitest';
8+
import {
9+
buildToolRegistry,
10+
resolveToolName,
11+
getToolsByCategory,
12+
type ToolCategory,
13+
} from '../utils/tool-registry.js';
14+
15+
describe('tool-registry', () => {
16+
const registry = buildToolRegistry();
17+
18+
describe('buildToolRegistry', () => {
19+
it('includes all canonical built-in tools', () => {
20+
expect(registry.totalTools).toBeGreaterThanOrEqual(26);
21+
});
22+
23+
it('every tool has a valid category', () => {
24+
for (const [name, entry] of registry.tools) {
25+
expect(entry.category).toBeTruthy();
26+
expect(entry.name).toBe(name);
27+
}
28+
});
29+
30+
it('byCategory entries match tools map', () => {
31+
let categoryTotal = 0;
32+
for (const [, entries] of registry.byCategory) {
33+
for (const entry of entries) {
34+
expect(registry.tools.get(entry.name)).toBe(entry);
35+
}
36+
categoryTotal += entries.length;
37+
}
38+
expect(categoryTotal).toBe(registry.totalTools);
39+
});
40+
41+
it('aliasLookup covers every canonical name', () => {
42+
for (const name of registry.tools.keys()) {
43+
expect(registry.aliasLookup.get(name)).toBe(name);
44+
}
45+
});
46+
47+
it('aliasLookup covers every legacy alias', () => {
48+
for (const [, entry] of registry.tools) {
49+
for (const alias of entry.aliases) {
50+
expect(registry.aliasLookup.get(alias)).toBe(entry.name);
51+
}
52+
}
53+
});
54+
55+
it('is deterministic across calls', () => {
56+
const second = buildToolRegistry();
57+
expect([...second.tools.keys()]).toEqual([...registry.tools.keys()]);
58+
expect(second.totalTools).toBe(registry.totalTools);
59+
});
60+
});
61+
62+
describe('resolveToolName', () => {
63+
it('resolves canonical names to themselves', () => {
64+
expect(resolveToolName(registry, 'grep_search')).toBe('grep_search');
65+
expect(resolveToolName(registry, 'run_shell_command')).toBe(
66+
'run_shell_command',
67+
);
68+
});
69+
70+
it('resolves legacy alias to canonical name', () => {
71+
expect(resolveToolName(registry, 'search_file_content')).toBe(
72+
'grep_search',
73+
);
74+
});
75+
76+
it('returns undefined for unknown tool names', () => {
77+
expect(resolveToolName(registry, 'nonexistent_tool')).toBeUndefined();
78+
});
79+
80+
it('returns undefined for empty string', () => {
81+
expect(resolveToolName(registry, '')).toBeUndefined();
82+
});
83+
});
84+
85+
describe('getToolsByCategory', () => {
86+
it('returns file-system tools', () => {
87+
const tools = getToolsByCategory(registry, 'file-system');
88+
const names = tools.map((t) => t.name);
89+
expect(names).toContain('glob');
90+
expect(names).toContain('grep_search');
91+
expect(names).toContain('read_file');
92+
expect(names).toContain('write_file');
93+
expect(names).toContain('replace');
94+
});
95+
96+
it('returns task-tracker tools', () => {
97+
const tools = getToolsByCategory(registry, 'task-tracker');
98+
const names = tools.map((t) => t.name);
99+
expect(names).toContain('tracker_create_task');
100+
expect(names).toContain('tracker_update_task');
101+
expect(names).toContain('tracker_get_task');
102+
expect(names).toContain('tracker_list_tasks');
103+
expect(names).toContain('tracker_add_dependency');
104+
expect(names).toContain('tracker_visualize');
105+
expect(names).toHaveLength(6);
106+
});
107+
108+
it('returns agent tools', () => {
109+
const tools = getToolsByCategory(registry, 'agent');
110+
const names = tools.map((t) => t.name);
111+
expect(names).toContain('invoke_agent');
112+
expect(names).toContain('complete_task');
113+
expect(names).toContain('update_topic');
114+
});
115+
116+
it('returns empty array for unknown category', () => {
117+
expect(
118+
getToolsByCategory(registry, 'nonexistent' as ToolCategory),
119+
).toEqual([]);
120+
});
121+
122+
it('every defined category has at least one tool', () => {
123+
const expectedCategories: ToolCategory[] = [
124+
'file-system',
125+
'shell',
126+
'web',
127+
'planning',
128+
'user-interaction',
129+
'skills',
130+
'task-tracker',
131+
'agent',
132+
'mcp',
133+
];
134+
for (const cat of expectedCategories) {
135+
expect(getToolsByCategory(registry, cat).length).toBeGreaterThan(0);
136+
}
137+
});
138+
});
139+
});

0 commit comments

Comments
 (0)