File size: 5,650 Bytes
c056ab3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
 * 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);