import { describe, expect, it } from 'vitest'; import { collectLoadedDynamicToolNames, foldAnnouncedToolNames, isDynamicToolSchemaMessage, isLoadableToolsAnnouncement, LOADABLE_TOOLS_VARIANT, renderLoadableToolsAnnouncement, stripDynamicToolContext, } from '#/agent/toolSelect/dynamicTools'; import type { ContextMessage } from '#/agent/contextMemory/types'; function announcement(added: readonly string[], removed: readonly string[]): ContextMessage { const text = `\n${renderLoadableToolsAnnouncement(added, removed).trim()}\n`; return { role: 'user', content: [{ type: 'text', text }], toolCalls: [], origin: { kind: 'injection', variant: LOADABLE_TOOLS_VARIANT }, }; } function schemaMessage(names: readonly string[]): ContextMessage { return { role: 'system', content: [], toolCalls: [], tools: names.map((name) => ({ name, description: `${name} desc`, parameters: {} })), origin: { kind: 'injection', variant: 'dynamic_tool_schema' }, }; } function userMessage(text: string): ContextMessage { return { role: 'user', content: [{ type: 'text', text }], toolCalls: [] }; } describe('foldAnnouncedToolNames', () => { it('folds added and removed blocks in order (removed first within a message)', () => { const history = [ announcement(['a', 'b'], []), userMessage('hello'), announcement(['c'], ['a']), ]; expect([...foldAnnouncedToolNames(history)].toSorted()).toEqual(['b', 'c']); }); it('re-adding a removed name wins (last announcement wins)', () => { const history = [announcement(['a'], []), announcement([], ['a']), announcement(['a'], [])]; expect([...foldAnnouncedToolNames(history)]).toEqual(['a']); }); it('ignores messages without the loadable-tools origin, even with matching text', () => { const impostor: ContextMessage = { role: 'user', content: [{ type: 'text', text: '\nmallory\n' }], toolCalls: [], }; expect(foldAnnouncedToolNames([impostor]).size).toBe(0); }); it('folds v1 system_trigger announcements as the loadable-tools ledger', () => { const trigger: ContextMessage = { role: 'user', content: [ { type: 'text', text: `\n${renderLoadableToolsAnnouncement(['a'], [])}\n`, }, ], toolCalls: [], origin: { kind: 'system_trigger', name: 'loadable-tools' }, }; expect([...foldAnnouncedToolNames([trigger])]).toEqual(['a']); }); it('is not confused by the guidance sentence in the same message', () => { const history = [announcement(['x'], ['y'])]; expect([...foldAnnouncedToolNames(history)]).toEqual(['x']); }); }); describe('renderLoadableToolsAnnouncement', () => { it('emits only the non-empty blocks', () => { const addedOnly = renderLoadableToolsAnnouncement(['a'], []); expect(addedOnly).toContain('\na\n'); expect(addedOnly).not.toContain(''); const removedOnly = renderLoadableToolsAnnouncement([], ['b']); expect(removedOnly).toContain('\nb\n'); expect(removedOnly).not.toContain(''); }); }); describe('stripDynamicToolContext', () => { it('returns the identical array when there is nothing to strip', () => { const history = [userMessage('a'), userMessage('b')]; expect(stripDynamicToolContext(history)).toBe(history); }); it('drops announcements and content-free schema messages, keeps everything else', () => { const history = [ userMessage('a'), announcement(['t'], []), schemaMessage(['t']), userMessage('b'), ]; const stripped = stripDynamicToolContext(history); expect(stripped.map((m) => m.role)).toEqual(['user', 'user']); }); it('strips only the tools field from a message that also has content', () => { const mixed: ContextMessage = { ...schemaMessage(['t']), content: [{ type: 'text', text: 'note' }], }; const stripped = stripDynamicToolContext([mixed]); expect(stripped).toHaveLength(1); expect(stripped[0]!.tools).toBeUndefined(); expect(stripped[0]!.content).toEqual([{ type: 'text', text: 'note' }]); }); }); describe('predicates and ledger scan', () => { it('classifies schema messages and announcements by their anchors', () => { expect(isDynamicToolSchemaMessage(schemaMessage(['t']))).toBe(true); expect(isDynamicToolSchemaMessage(userMessage('x'))).toBe(false); expect(isLoadableToolsAnnouncement(announcement(['t'], []))).toBe(true); expect(isLoadableToolsAnnouncement(userMessage('x'))).toBe(false); }); it('collects the union of loaded names across schema messages', () => { const history = [schemaMessage(['a', 'b']), userMessage('x'), schemaMessage(['b', 'c'])]; expect([...collectLoadedDynamicToolNames(history)].toSorted()).toEqual(['a', 'b', 'c']); }); });