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']);
});
});