InfoLens1 / client /src /tests /chat /toolConfig.test.ts
dqy08's picture
支持多轮工具调用模拟;Causal Flow支持tool_call→tool_response 合成归因边;新增demo;其它改进
c056ab3
Raw
History Blame Contribute Delete
5.65 kB
/**
* Tool config ↔ catalog entry 互转与校验
* 运行: cd client/src && npx tsx tests/chat/toolConfig.test.ts
*/
import { TOOL_CATALOG } from '../../features/chat/toolCatalog';
import { resolveMockTool } from '../../features/chat/mockExecutor';
import {
buildToolConfigFromCheckedJson,
createBlankToolEntry,
entriesToToolConfig,
EMPTY_TOOL_CONFIG,
isToolConfigEmpty,
nextAvailableNewToolName,
normalizeToolConfig,
parseToolCatalogEntryJson,
toolConfigCacheKeyFields,
toolConfigFingerprint,
toolConfigToEntries,
} from '../../features/chat/toolConfig';
let passed = 0;
let failed = 0;
function assert(label: string, cond: boolean): void {
if (cond) {
passed++;
console.log(` ✓ ${label}`);
} else {
failed++;
console.log(` ✗ ${label}`);
}
}
const sampleEntry = TOOL_CATALOG[0]!;
const sampleJson = JSON.stringify(sampleEntry, null, 2);
const webSearchEntry = TOOL_CATALOG.find((e) => e.function.name === 'web_search')!;
console.log('toolConfig round-trip');
{
const config = entriesToToolConfig([sampleEntry]);
const back = toolConfigToEntries(config);
assert('entriesToToolConfig → toolConfigToEntries', back[0]!.function.name === sampleEntry.function.name);
assert(
'mock response preserved',
back[0]!.mock_results[0]!.response.temperature ===
sampleEntry.mock_results[0]!.response.temperature,
);
}
console.log('parseToolCatalogEntryJson');
{
const ok = parseToolCatalogEntryJson(sampleJson);
assert('valid entry parses', 'entry' in ok && ok.entry.function.name === sampleEntry.function.name);
const bad = parseToolCatalogEntryJson('{');
assert('invalid JSON rejected', 'error' in bad);
const webJson = JSON.stringify(webSearchEntry, null, 2);
const webParsed = parseToolCatalogEntryJson(webJson);
assert(
'web_search keeps all mock candidates',
'entry' in webParsed && webParsed.entry.mock_results.length === 2,
);
}
console.log('legacy tool config migration');
{
const legacy = {
tools_schema: [{ function: sampleEntry.function }],
mock_results: {
get_current_temperature: JSON.stringify(sampleEntry.mock_results[0]!.response),
},
};
const migrated = normalizeToolConfig(legacy);
assert('legacy migrates to entries', migrated?.entries.length === 1);
assert(
'legacy mock preserved',
migrated?.entries[0]!.mock_results[0]!.response.temperature ===
sampleEntry.mock_results[0]!.response.temperature,
);
}
console.log('resolveMockTool trigger_keyword');
{
const config = entriesToToolConfig([webSearchEntry]);
const ipo = resolveMockTool(config, 'web_search', { query: 'SpaceX IPO 最新消息' });
assert('IPO keyword matches', ipo?.includes('750亿美元') === true);
const boss = resolveMockTool(config, 'web_search', { query: 'SpaceX 老板是谁' });
assert('老板 keyword matches', boss?.includes('埃隆·马斯克') === true);
const miss = resolveMockTool(config, 'web_search', { query: 'SpaceX 火箭发射' });
assert('no match returns null', miss === null);
const weatherConfig = entriesToToolConfig([sampleEntry]);
const weather = resolveMockTool(weatherConfig, 'get_current_temperature', { location: '北京' });
assert('no-keyword tool uses first candidate', weather?.includes('25') === true);
}
console.log('resolveMockTool keyword fallback');
{
const entry = {
function: webSearchEntry.function,
mock_results: [
{ trigger_keyword: 'IPO', response: { content: 'ipo hit' } },
{ response: { content: 'default hit' } },
],
};
const config = entriesToToolConfig([entry]);
const matched = resolveMockTool(config, 'web_search', { query: 'IPO news' });
assert('keyword branch wins', matched === JSON.stringify({ content: 'ipo hit' }));
const fallback = resolveMockTool(config, 'web_search', { query: 'other topic' });
assert('non-keyword fallback', fallback === JSON.stringify({ content: 'default hit' }));
}
console.log('buildToolConfigFromCheckedJson');
{
const built = buildToolConfigFromCheckedJson([{ label: 't1', text: sampleJson }]);
assert('single tool builds', 'config' in built && built.config.entries.length === 1);
const dup = buildToolConfigFromCheckedJson([
{ label: 'a', text: sampleJson },
{ label: 'b', text: sampleJson },
]);
assert('duplicate name rejected', 'error' in dup);
const empty = buildToolConfigFromCheckedJson([]);
assert('empty selection → empty config', 'config' in empty && isToolConfigEmpty(empty.config));
}
console.log('EMPTY_TOOL_CONFIG');
assert('empty config has no entries', isToolConfigEmpty(EMPTY_TOOL_CONFIG));
console.log('createBlankToolEntry / nextAvailableNewToolName');
{
const blank = createBlankToolEntry('new_tool');
assert('blank entry has name', blank.function.name === 'new_tool');
assert(
'next name skips used',
nextAvailableNewToolName(['new_tool', 'new_tool_2']) === 'new_tool_3',
);
}
console.log('toolConfigCacheKeyFields');
{
const config = entriesToToolConfig([sampleEntry]);
assert('single-turn omits fingerprint', toolConfigCacheKeyFields(false, config).toolConfigFingerprint === undefined);
const multi = toolConfigCacheKeyFields(true, config);
assert(
'multi-turn includes fingerprint',
multi.toolConfigFingerprint === toolConfigFingerprint(config),
);
}
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);