kiku-agent 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -12
- package/package.json +2 -1
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/modules/evaluation/runner.js +4 -14
- package/server/agent/eval/modules/evaluation/scoring.js +15 -3
- package/server/agent/eval/modules/server/evalApi.js +5 -5
- package/server/agent/eval/modules/ui/evalUi.js +7 -4
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentFollowUps.test.js +128 -2
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +1 -1
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/bilibiliEmbed.test.js +45 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/clarifyingQuestions.test.js +33 -0
- package/server/agent/tests/cliAutomation.test.js +38 -1
- package/server/agent/tests/cloudProviders.test.js +91 -3
- package/server/agent/tests/commands.test.js +92 -3
- package/server/agent/tests/contextIntelligence.test.js +50 -3
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/envCommand.test.js +69 -0
- package/server/agent/tests/evalMetrics.test.js +8 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/hybridWebSearch.test.js +2 -0
- package/server/agent/tests/lanDiscovery.test.js +5 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +32 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/nextGenArchitecture.test.js +4 -2
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +10 -5
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +16 -9
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +4 -4
- package/server/agent/tests/sessionAndCopy.test.js +39 -4
- package/server/agent/tests/sidebarControls.test.js +34 -0
- package/server/agent/tests/statefulMemory.test.js +8 -1
- package/server/agent/tests/textProtocolLoop.test.js +71 -0
- package/server/agent/tests/textToolParsing.test.js +48 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
- package/server/agent/tests/unifiedEndpoints.test.js +13 -6
- package/server/agent/tests/v110Features.test.js +1 -1
- package/server/agent/tests/vramEstimator.test.js +75 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +256 -5
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webExtract.test.js +23 -0
- package/server/agent/tests/webSources.test.js +46 -12
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +14 -8
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +18 -18
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
buildSystemPrompt,
|
|
5
|
+
buildOpenDomainSystemPrompt,
|
|
6
|
+
buildInDomainSystemPrompt,
|
|
7
|
+
buildDynamicRoutingDirective
|
|
8
|
+
} from '../prompts.js';
|
|
9
|
+
import {
|
|
10
|
+
classifyQuerySpectrum,
|
|
11
|
+
classifyQueryDomain,
|
|
12
|
+
isQueryInDomain
|
|
13
|
+
} from '../catalogEntityResolver.js';
|
|
14
|
+
import { KikuAgent } from '../agentCore.js';
|
|
15
|
+
|
|
16
|
+
test('buildOpenDomainSystemPrompt includes coding, math, science, and awakening directives', () => {
|
|
17
|
+
const prompt = buildOpenDomainSystemPrompt(['web_search', 'search_encyclopedia']);
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
assert.ok(prompt.includes('SOFTWARE ENGINEERING & CODING EXCELLENCE'));
|
|
21
|
+
assert.ok(prompt.includes('markdown fenced code blocks'));
|
|
22
|
+
assert.ok(prompt.includes('time complexity $O(N)$'));
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
assert.ok(prompt.includes('MATHEMATICS & SCIENTIFIC RIGOR'));
|
|
26
|
+
assert.ok(prompt.includes('LaTeX / KaTeX'));
|
|
27
|
+
assert.ok(prompt.includes('$ ... $'));
|
|
28
|
+
assert.ok(prompt.includes('$$ ... $$'));
|
|
29
|
+
assert.ok(prompt.includes('step-by-step reasoning'));
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
assert.ok(prompt.includes('CONTEXT AWAKENING PROTOCOL'));
|
|
33
|
+
assert.ok(prompt.includes('ตื่นรู้เข้าสู่บริบท'));
|
|
34
|
+
assert.ok(prompt.includes('search_works'));
|
|
35
|
+
assert.ok(prompt.includes('search_discography'));
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
assert.ok(prompt.includes('OPEN-DOMAIN TOOL RETRIEVAL & INTEGRITY'));
|
|
39
|
+
assert.ok(prompt.includes('web_search'));
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
assert.ok(prompt.includes('Fibonacci'));
|
|
43
|
+
assert.ok(prompt.includes('แคลคูลัส'));
|
|
44
|
+
assert.ok(prompt.includes('เทอร์โมไดนามิกส์'));
|
|
45
|
+
assert.ok(prompt.includes('[CONTEXT AWAKENING ACTIVATED]'));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('buildInDomainSystemPrompt retains full Ju Jingyi concierge persona', () => {
|
|
49
|
+
const prompt = buildInDomainSystemPrompt(['search_works', 'search_discography']);
|
|
50
|
+
|
|
51
|
+
assert.ok(prompt.includes('KIKU AGENT (菊)'));
|
|
52
|
+
assert.ok(prompt.includes('authoritative AI encyclopedia agent and concierge for international artist Ju Jingyi'));
|
|
53
|
+
assert.ok(prompt.includes('MANDATORY RETRIEVAL RULE'));
|
|
54
|
+
assert.ok(prompt.includes('DUAL-TIER RETRIEVAL ARCHITECTURE'));
|
|
55
|
+
assert.ok(prompt.includes('ENTITY & UNIT DISAMBIGUATION'));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('buildSystemPrompt dispatcher honors options and maintains backward compatibility', () => {
|
|
59
|
+
|
|
60
|
+
const defaultPrompt = buildSystemPrompt();
|
|
61
|
+
assert.ok(defaultPrompt.includes('concierge for international artist Ju Jingyi'));
|
|
62
|
+
|
|
63
|
+
const inDomainPrompt = buildSystemPrompt([], { inDomain: true });
|
|
64
|
+
assert.ok(inDomainPrompt.includes('concierge for international artist Ju Jingyi'));
|
|
65
|
+
|
|
66
|
+
const inDomainExplicit = buildSystemPrompt([], { domain: 'IN_DOMAIN' });
|
|
67
|
+
assert.ok(inDomainExplicit.includes('concierge for international artist Ju Jingyi'));
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
const openDomainPrompt = buildSystemPrompt([], { inDomain: false });
|
|
71
|
+
assert.ok(openDomainPrompt.includes('OPEN-DOMAIN TECHNICAL & PROBLEM-SOLVING MODE'));
|
|
72
|
+
assert.ok(openDomainPrompt.includes('CONTEXT AWAKENING PROTOCOL'));
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
const openDomainExplicit = buildSystemPrompt([], { domain: 'OPEN_DOMAIN' });
|
|
76
|
+
assert.ok(openDomainExplicit.includes('OPEN-DOMAIN TECHNICAL & PROBLEM-SOLVING MODE'));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('buildDynamicRoutingDirective includes open-domain and general tools', () => {
|
|
80
|
+
const directive = buildDynamicRoutingDirective([
|
|
81
|
+
'web_search',
|
|
82
|
+
'search_encyclopedia',
|
|
83
|
+
'get_weather',
|
|
84
|
+
'search_location_map',
|
|
85
|
+
'read_local_file',
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
assert.ok(directive.includes('Web Search & News -> web_search'));
|
|
89
|
+
assert.ok(directive.includes('Encyclopedia & General Knowledge -> search_encyclopedia'));
|
|
90
|
+
assert.ok(directive.includes('Real-time Weather & Location -> get_weather'));
|
|
91
|
+
assert.ok(directive.includes('Maps & Navigation -> search_location_map'));
|
|
92
|
+
assert.ok(directive.includes('System & Files -> read_local_file'));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('classifyQuerySpectrum identifies coding, mathematics, and science as OPEN_DOMAIN', () => {
|
|
96
|
+
const testCases = [
|
|
97
|
+
|
|
98
|
+
'ช่วยเขียนโค้ด Python เชื่อมต่อ WebSocket ให้หน่อย',
|
|
99
|
+
'สอนเขียนโปรแกรม Golang จัดการ Goroutines',
|
|
100
|
+
'แก้บั๊ก React useEffect infinite loop',
|
|
101
|
+
'เขียนฟังก์ชัน TypeScript สำหรับ debounce',
|
|
102
|
+
'Write a quicksort algorithm in C++',
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
'ช่วยแก้โจทย์คณิตศาสตร์หน่อย อินทิกรัล 2x dx',
|
|
106
|
+
'แก้สมการ x^2 - 5x + 6 = 0 ให้ดูทีละขั้นตอน',
|
|
107
|
+
'ช่วยคำนวณแคลคูลัสหาอนุพันธ์ของ f(x) = sin(x)*e^x',
|
|
108
|
+
'อธิบายทฤษฎีบทพีทาโกรัสและวิธีพิสูจน์',
|
|
109
|
+
'Calculate the eigenvalues of a 2x2 matrix',
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
'อธิบายกฎข้อที่ 2 ของเทอร์โมไดนามิกส์',
|
|
113
|
+
'กระบวนการสังเคราะห์ด้วยแสงในพืช light reaction กับ dark reaction แตกต่างกันอย่างไร',
|
|
114
|
+
'ทำไมน้ำถึงมีพันธะไฮโดรเจนและมีความจุความร้อนสูง',
|
|
115
|
+
'Explain the structure of an atom and electron orbitals',
|
|
116
|
+
'ความแตกต่างระหว่างแรงโน้มถ่วงและแรงแม่เหล็กไฟฟ้า'
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
for (const query of testCases) {
|
|
120
|
+
const domain = classifyQueryDomain(query);
|
|
121
|
+
const inDomain = isQueryInDomain(query);
|
|
122
|
+
assert.strictEqual(domain, 'OPEN_DOMAIN', `Expected OPEN_DOMAIN for query: "${query}"`);
|
|
123
|
+
assert.strictEqual(inDomain, false, `Expected inDomain=false for query: "${query}"`);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('classifyQuerySpectrum awakens to IN_DOMAIN when Ju Jingyi or catalog entities are mentioned', () => {
|
|
128
|
+
const awakeningCases = [
|
|
129
|
+
|
|
130
|
+
'ช่วยเขียนโค้ด Python ดึงรายชื่อผลงานซีรีส์ของจวีจิ้งอี',
|
|
131
|
+
'คำนวณว่าจวีจิ้งอีเดบิวต์ในวงการมากี่ปีแล้ว',
|
|
132
|
+
'เพลง 0.2s ของจวีจิ้งอีมีความยาวกี่นาที',
|
|
133
|
+
'ซีรีส์เรื่อง In Blossom จวีจิ้งอีแสดงเป็นตัวละครอะไร',
|
|
134
|
+
'จวีจิ้งอีเป็นแอมบาสเดอร์ของแบรนด์ FRED Paris ตั้งแต่ปีไหน',
|
|
135
|
+
'ยอดขายกล่องของขวัญ TuTuCare ของจวีจิ้งอีขายหมดในกี่วินาที',
|
|
136
|
+
'เพลง Dewdrops ประกอบซีรีส์เรื่องอะไร',
|
|
137
|
+
'What dramas has Ju Jingyi starred in?'
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
for (const query of awakeningCases) {
|
|
141
|
+
const domain = classifyQueryDomain(query);
|
|
142
|
+
const inDomain = isQueryInDomain(query);
|
|
143
|
+
assert.strictEqual(domain, 'IN_DOMAIN', `Expected IN_DOMAIN awakening for query: "${query}"`);
|
|
144
|
+
assert.strictEqual(inDomain, true, `Expected inDomain=true for query: "${query}"`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('KikuAgent dynamically adapts systemPrompt between open-domain and in-domain turns', async () => {
|
|
149
|
+
const agent = new KikuAgent();
|
|
150
|
+
await agent.ready();
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
const inDomainPrompt = agent.buildSystemPrompt(null, { inDomain: true });
|
|
154
|
+
assert.ok(inDomainPrompt.includes('concierge for international artist Ju Jingyi'));
|
|
155
|
+
|
|
156
|
+
const openDomainPrompt = agent.buildSystemPrompt(null, { inDomain: false });
|
|
157
|
+
assert.ok(openDomainPrompt.includes('OPEN-DOMAIN TECHNICAL & PROBLEM-SOLVING MODE'));
|
|
158
|
+
assert.ok(openDomainPrompt.includes('CONTEXT AWAKENING PROTOCOL'));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('KikuAgent ask() triggers Context Awakening in multi-turn transition', async () => {
|
|
162
|
+
const capturedMessagesList = [];
|
|
163
|
+
const mockClient = {
|
|
164
|
+
model: 'mock-kiku',
|
|
165
|
+
baseUrl: 'http://127.0.0.1:1234',
|
|
166
|
+
provider: 'lmstudio',
|
|
167
|
+
providerName: 'lmstudio',
|
|
168
|
+
chat: async ({ messages }) => {
|
|
169
|
+
capturedMessagesList.push([...messages]);
|
|
170
|
+
return {
|
|
171
|
+
content: 'Response from mock LLM'
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const agent = new KikuAgent({ client: mockClient });
|
|
177
|
+
await agent.ready();
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
await agent.ask('ช่วยเขียนฟังก์ชัน Python สำหรับคำนวณ Fibonacci หน่อย');
|
|
181
|
+
assert.strictEqual(capturedMessagesList.length, 1);
|
|
182
|
+
const turn1Messages = capturedMessagesList[0];
|
|
183
|
+
const turn1SystemPrompt = turn1Messages[0].content;
|
|
184
|
+
assert.ok(turn1SystemPrompt.includes('OPEN-DOMAIN TECHNICAL & PROBLEM-SOLVING MODE'), 'Turn 1 must use open-domain system prompt');
|
|
185
|
+
const turn1OpenDirective = turn1Messages.find(m => typeof m.content === 'string' && m.content.includes('OPEN-DOMAIN EXPERT DIRECTIVE'));
|
|
186
|
+
assert.ok(turn1OpenDirective, 'Turn 1 must include OPEN-DOMAIN EXPERT DIRECTIVE');
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
await agent.ask('แล้วจวีจิ้งอีมีผลงานซีรีส์เรื่องล่าสุดอะไรบ้าง?');
|
|
190
|
+
assert.strictEqual(capturedMessagesList.length, 2);
|
|
191
|
+
const turn2Messages = capturedMessagesList[1];
|
|
192
|
+
const turn2SystemPrompt = turn2Messages[0].content;
|
|
193
|
+
assert.ok(turn2SystemPrompt.includes('concierge for international artist Ju Jingyi'), 'Turn 2 must awaken to Ju Jingyi concierge persona');
|
|
194
|
+
const turn2AwakeningDirective = turn2Messages.find(m => typeof m.content === 'string' && m.content.includes('CONTEXT AWAKENING ACTIVATED'));
|
|
195
|
+
assert.ok(turn2AwakeningDirective, 'Turn 2 must include CONTEXT AWAKENING ACTIVATED directive');
|
|
196
|
+
});
|
|
197
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
classifyQueryDomain,
|
|
5
|
+
isQueryInDomain,
|
|
6
|
+
QUERY_DOMAINS,
|
|
7
|
+
getContextualizedSearchQuery
|
|
8
|
+
} from '../catalogEntityResolver.js';
|
|
9
|
+
import { dynamicToolRouter } from '../toolRouter.js';
|
|
10
|
+
import { buildImageSearchQuery } from '../webSearch.js';
|
|
11
|
+
import { createCatalogMatcher, toolHandlers } from '../tools.js';
|
|
12
|
+
|
|
13
|
+
test('classifyQueryDomain and isQueryInDomain correctly classify in-domain and open-domain queries', () => {
|
|
14
|
+
|
|
15
|
+
const inDomainQueries = [
|
|
16
|
+
'จวีจิ้งอี มีผลงานอะไรบ้าง',
|
|
17
|
+
'Ju Jingyi dramas 2026',
|
|
18
|
+
'鞠婧祎 花间令',
|
|
19
|
+
'เพลง 0.2s ใครร้อง',
|
|
20
|
+
'เสี่ยวจวี เป็นแบรนด์แอมบาสเดอร์อะไรบ้าง',
|
|
21
|
+
'Veil of Shadows พระเอกคือใคร',
|
|
22
|
+
'ตำนานรักนางพญางูขาว เล่นเป็นใคร',
|
|
23
|
+
'Kiku fanclub',
|
|
24
|
+
'แบรนด์ FRED Paris จวีจิ้งอี'
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
for (const q of inDomainQueries) {
|
|
28
|
+
assert.equal(
|
|
29
|
+
isQueryInDomain(q),
|
|
30
|
+
true,
|
|
31
|
+
`Expected "${q}" to be recognized as in-domain`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
const openDomainQueries = [
|
|
37
|
+
'วิธีทำต้มยำกุ้งน้ำข้น',
|
|
38
|
+
'สภาพอากาศ โตเกียว วันนี้',
|
|
39
|
+
'What is quantum computing?',
|
|
40
|
+
'Who won the 2022 FIFA World Cup?',
|
|
41
|
+
'Bitcoin price history and future outlook',
|
|
42
|
+
'Taylor Swift The Eras Tour setlist',
|
|
43
|
+
'ทฤษฎีสัมพัทธภาพของไอน์สไตน์คืออะไร',
|
|
44
|
+
'ประวัติศาสตร์อยุธยาตอนต้น',
|
|
45
|
+
'How to fix TypeError in JavaScript',
|
|
46
|
+
'สูตรทำข้าวมันไก่'
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
for (const q of openDomainQueries) {
|
|
50
|
+
assert.equal(
|
|
51
|
+
classifyQueryDomain(q),
|
|
52
|
+
QUERY_DOMAINS.OPEN_DOMAIN,
|
|
53
|
+
`Expected "${q}" to be classified as OPEN_DOMAIN`
|
|
54
|
+
);
|
|
55
|
+
assert.equal(
|
|
56
|
+
isQueryInDomain(q),
|
|
57
|
+
false,
|
|
58
|
+
`Expected "${q}" to NOT be in-domain`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('getContextualizedSearchQuery preserves open-domain queries without injecting artist tokens', () => {
|
|
64
|
+
const openQuery = 'วิธีทำแกงเขียวหวานไก่';
|
|
65
|
+
const result = getContextualizedSearchQuery(openQuery, 'chinese_web');
|
|
66
|
+
assert.equal(result, openQuery, 'Open-domain query should not have artist injected');
|
|
67
|
+
assert.equal(result.includes('鞠婧祎'), false);
|
|
68
|
+
assert.equal(result.includes('จวีจิ้งอี'), false);
|
|
69
|
+
|
|
70
|
+
const inDomainQuery = 'ผลงานเพลง 0.2s';
|
|
71
|
+
const inDomainResult = getContextualizedSearchQuery(inDomainQuery, 'chinese_web');
|
|
72
|
+
assert.ok(
|
|
73
|
+
inDomainResult.includes('鞠婧祎') || inDomainResult.includes('0.2s') || inDomainResult.includes('0.2秒'),
|
|
74
|
+
'In-domain query should be contextualized'
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('buildImageSearchQuery does not prepend artist tokens for open-domain queries', () => {
|
|
79
|
+
const openQuery = 'Eiffel Tower at night';
|
|
80
|
+
const imgQuery = buildImageSearchQuery(openQuery);
|
|
81
|
+
assert.equal(imgQuery, 'Eiffel Tower at night');
|
|
82
|
+
assert.equal(imgQuery.includes('鞠婧祎'), false);
|
|
83
|
+
|
|
84
|
+
const inDomainQuery = 'In Blossom drama poster';
|
|
85
|
+
const inDomainImgQuery = buildImageSearchQuery(inDomainQuery);
|
|
86
|
+
assert.ok(inDomainImgQuery.includes('鞠婧祎'), 'In-domain query should include artist name');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('toolRouter detects OPEN_DOMAIN intent and routes general web/encyclopedia search tools', () => {
|
|
90
|
+
const openQuery = 'ทำไมท้องฟ้าถึงเป็นสีฟ้า';
|
|
91
|
+
const intents = dynamicToolRouter.detectIntents(openQuery).map(p => p.type);
|
|
92
|
+
assert.ok(
|
|
93
|
+
intents.includes('OPEN_DOMAIN'),
|
|
94
|
+
'Should detect OPEN_DOMAIN intent for general science query'
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const tools = dynamicToolRouter.routeToolsForQuery(openQuery);
|
|
98
|
+
const toolNames = tools.map(t => t.function?.name || t.name);
|
|
99
|
+
assert.ok(toolNames.includes('web_search'), 'Should include web_search for open-domain queries');
|
|
100
|
+
assert.ok(
|
|
101
|
+
!toolNames.includes('search_discography'),
|
|
102
|
+
'Should not force search_discography on open-domain science question'
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('search_works matches plot concepts and synopses in Thai', async () => {
|
|
107
|
+
|
|
108
|
+
const res = await toolHandlers.search_works({ query: 'แฟนตาซี' });
|
|
109
|
+
assert.equal(res.status, 'success');
|
|
110
|
+
assert.ok(res.count > 0);
|
|
111
|
+
assert.ok(
|
|
112
|
+
res.works.some(w => (w.synopsis || '').includes('แฟนตาซี') || (w.titleEn || '').includes('Veil of Shadows'))
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('createCatalogMatcher handles unspaced Thai queries with stop phrases', () => {
|
|
117
|
+
const matcher = createCatalogMatcher('อยากดูซีรีส์แนวสืบคดี');
|
|
118
|
+
|
|
119
|
+
const haystack = 'ซีรีส์แฟนตาซีฟอร์มยักษ์ นำไปสู่การสืบคดีสไตล์หักเหลี่ยมซ่อนเงื่อน';
|
|
120
|
+
assert.equal(matcher(haystack), true);
|
|
121
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import {
|
|
5
|
+
isPidAlive,
|
|
6
|
+
killProcessTreeSync,
|
|
7
|
+
registerBackgroundProcess,
|
|
8
|
+
unregisterBackgroundProcess,
|
|
9
|
+
getBackgroundProcess,
|
|
10
|
+
listBackgroundProcesses,
|
|
11
|
+
stopBackgroundProcess,
|
|
12
|
+
stopAllBackgroundProcessesSync,
|
|
13
|
+
initProcessExitHooks,
|
|
14
|
+
clearProcessRegistry,
|
|
15
|
+
killWslProcessByName,
|
|
16
|
+
} from '../processManager.js';
|
|
17
|
+
|
|
18
|
+
test('isPidAlive correctly identifies running and dead processes', () => {
|
|
19
|
+
assert.equal(isPidAlive(process.pid), true, 'Current node process should be alive');
|
|
20
|
+
assert.equal(isPidAlive(0), false, 'PID 0 should not be alive');
|
|
21
|
+
assert.equal(isPidAlive(-1), false, 'Negative PID should not be alive');
|
|
22
|
+
assert.equal(isPidAlive(null), false, 'Null PID should return false');
|
|
23
|
+
assert.equal(isPidAlive(99999999), false, 'Arbitrary high PID should not be alive');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('processManager registers, queries, and unregisters background processes', () => {
|
|
27
|
+
clearProcessRegistry();
|
|
28
|
+
|
|
29
|
+
const dummyPid = process.pid;
|
|
30
|
+
const rec = registerBackgroundProcess('test-proc', dummyPid, {
|
|
31
|
+
command: 'node',
|
|
32
|
+
args: ['test.js'],
|
|
33
|
+
metadata: { test: true },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
assert.equal(rec.name, 'test-proc');
|
|
37
|
+
assert.equal(rec.pid, dummyPid);
|
|
38
|
+
assert.equal(rec.status, 'running');
|
|
39
|
+
|
|
40
|
+
const fetched = getBackgroundProcess('test-proc');
|
|
41
|
+
assert.ok(fetched);
|
|
42
|
+
assert.equal(fetched.name, 'test-proc');
|
|
43
|
+
assert.equal(fetched.pid, dummyPid);
|
|
44
|
+
assert.equal(fetched.isAlive, true);
|
|
45
|
+
|
|
46
|
+
const list = listBackgroundProcesses();
|
|
47
|
+
assert.ok(list.some(p => p.name === 'test-proc' && p.pid === dummyPid));
|
|
48
|
+
|
|
49
|
+
const removed = unregisterBackgroundProcess('test-proc');
|
|
50
|
+
assert.equal(removed, true);
|
|
51
|
+
assert.equal(getBackgroundProcess('test-proc'), null);
|
|
52
|
+
|
|
53
|
+
clearProcessRegistry();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('processManager tracks real child process and terminates it cleanly via tree kill', async () => {
|
|
57
|
+
clearProcessRegistry();
|
|
58
|
+
|
|
59
|
+
const child = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 20000)'], {
|
|
60
|
+
stdio: 'ignore',
|
|
61
|
+
windowsHide: true,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
assert.ok(child.pid > 0);
|
|
65
|
+
assert.equal(isPidAlive(child.pid), true);
|
|
66
|
+
|
|
67
|
+
registerBackgroundProcess('sleep-job', child, {
|
|
68
|
+
command: process.execPath,
|
|
69
|
+
metadata: { job: 'timeout' },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const procInfo = getBackgroundProcess('sleep-job');
|
|
73
|
+
assert.equal(procInfo.isAlive, true);
|
|
74
|
+
assert.equal(procInfo.pid, child.pid);
|
|
75
|
+
|
|
76
|
+
const stopResult = stopBackgroundProcess('sleep-job');
|
|
77
|
+
assert.equal(stopResult.ok, true);
|
|
78
|
+
assert.equal(stopResult.status, 'stopped');
|
|
79
|
+
|
|
80
|
+
await new Promise(r => setTimeout(r, 200));
|
|
81
|
+
assert.equal(isPidAlive(child.pid), false, 'Child process should be terminated');
|
|
82
|
+
|
|
83
|
+
clearProcessRegistry();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('processManager validates input arguments defensively', () => {
|
|
87
|
+
assert.throws(() => registerBackgroundProcess('', 1234), /must be a non-empty string/);
|
|
88
|
+
assert.throws(() => registerBackgroundProcess('bad-pid', -5), /Invalid PID/);
|
|
89
|
+
assert.throws(() => registerBackgroundProcess('bad-null', null), /Invalid PID/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('initProcessExitHooks is idempotent and safe', () => {
|
|
93
|
+
assert.doesNotThrow(() => {
|
|
94
|
+
initProcessExitHooks();
|
|
95
|
+
initProcessExitHooks();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('killWslProcessByName handles safe fallback', () => {
|
|
100
|
+
assert.equal(killWslProcessByName(null, 'test'), false);
|
|
101
|
+
assert.equal(killWslProcessByName('Ubuntu', null), false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('stopAllBackgroundProcessesSync cleanly stops all registered processes', () => {
|
|
105
|
+
clearProcessRegistry();
|
|
106
|
+
|
|
107
|
+
const child1 = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 20000)'], { stdio: 'ignore' });
|
|
108
|
+
const child2 = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 20000)'], { stdio: 'ignore' });
|
|
109
|
+
|
|
110
|
+
registerBackgroundProcess('worker-1', child1);
|
|
111
|
+
registerBackgroundProcess('worker-2', child2);
|
|
112
|
+
|
|
113
|
+
const stopped = stopAllBackgroundProcessesSync();
|
|
114
|
+
assert.equal(stopped.length, 2);
|
|
115
|
+
|
|
116
|
+
assert.equal(isPidAlive(child1.pid), false);
|
|
117
|
+
assert.equal(isPidAlive(child2.pid), false);
|
|
118
|
+
|
|
119
|
+
clearProcessRegistry();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('stopAllBackgroundProcessesSync preserves persistOnExit processes on exit unless force is passed', () => {
|
|
123
|
+
clearProcessRegistry();
|
|
124
|
+
|
|
125
|
+
const persistentChild = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 20000)'], { stdio: 'ignore' });
|
|
126
|
+
const ephemeralChild = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 20000)'], { stdio: 'ignore' });
|
|
127
|
+
|
|
128
|
+
registerBackgroundProcess('daemon-service', persistentChild, { persistOnExit: true });
|
|
129
|
+
registerBackgroundProcess('worker-job', ephemeralChild);
|
|
130
|
+
|
|
131
|
+
const normalExitStopped = stopAllBackgroundProcessesSync({ force: false });
|
|
132
|
+
assert.equal(normalExitStopped.length, 1);
|
|
133
|
+
assert.equal(normalExitStopped[0].name, 'worker-job');
|
|
134
|
+
assert.equal(isPidAlive(ephemeralChild.pid), false);
|
|
135
|
+
assert.equal(isPidAlive(persistentChild.pid), true, 'Persistent daemon should remain alive on standard exit');
|
|
136
|
+
|
|
137
|
+
const forcedStopped = stopAllBackgroundProcessesSync({ force: true });
|
|
138
|
+
assert.equal(forcedStopped.length, 1);
|
|
139
|
+
assert.equal(forcedStopped[0].name, 'daemon-service');
|
|
140
|
+
assert.equal(isPidAlive(persistentChild.pid), false, 'Persistent daemon stops when forced');
|
|
141
|
+
|
|
142
|
+
clearProcessRegistry();
|
|
143
|
+
});
|
|
144
|
+
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
1
|
+
import test, { after } from 'node:test';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import assert from 'node:assert/strict';
|
|
5
|
+
const isolatedSettingsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kiku-provider-test-'));
|
|
6
|
+
process.env.KIKU_DATA_DIR = isolatedSettingsDir;
|
|
7
|
+
after(() => fs.rmSync(isolatedSettingsDir, { recursive: true, force: true }));
|
|
3
8
|
import fs from 'node:fs';
|
|
4
|
-
|
|
5
|
-
|
|
9
|
+
const { SETTINGS_FILE } = await import("../appPaths.js");
|
|
10
|
+
const {
|
|
6
11
|
savePersistedActiveProvider,
|
|
7
12
|
savePersistedProviderModel,
|
|
8
13
|
getPersistedActiveProvider,
|
|
9
14
|
loadPersistedSettings,
|
|
10
15
|
CLOUD_PROVIDERS,
|
|
11
|
-
}
|
|
12
|
-
|
|
16
|
+
} = await import("../config.js");
|
|
17
|
+
const { LLMClient } = await import("../llmClient.js");
|
|
13
18
|
|
|
14
19
|
test('Provider and Model settings persist across restarts', () => {
|
|
15
20
|
const originalGroqModel = CLOUD_PROVIDERS.groq.defaultModel;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
|
+
import { getWebChatHtml } from '../../webChatHtml.js';
|
|
7
|
+
import { handleChatRoute } from '../../web-chat/routes/chatRoute.js';
|
|
8
|
+
|
|
9
|
+
test('Web Chat HTML contains timeline-modal, research-modal, and verify-modal', () => {
|
|
10
|
+
const html = getWebChatHtml();
|
|
11
|
+
assert.match(html, /id="timeline-modal"/, 'timeline-modal must exist');
|
|
12
|
+
assert.match(html, /id="timeline-body"/, 'timeline-body must exist inside modal');
|
|
13
|
+
assert.match(html, /id="research-modal"/, 'research-modal must exist');
|
|
14
|
+
assert.match(html, /id="research-modal-input"/, 'research-modal-input textarea must exist');
|
|
15
|
+
assert.match(html, /id="verify-modal"/, 'verify-modal must exist');
|
|
16
|
+
assert.match(html, /id="verify-modal-input"/, 'verify-modal-input textarea must exist');
|
|
17
|
+
|
|
18
|
+
// Verify quick action menu items call modal openers
|
|
19
|
+
assert.match(html, /onclick="openResearchModal\(\); closeQuickActions\(\)"/);
|
|
20
|
+
assert.match(html, /onclick="openVerifyModal\(\); closeQuickActions\(\)"/);
|
|
21
|
+
assert.match(html, /onclick="openTimelineModal\(\); closeQuickActions\(\)"/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('client.js parses timeline data correctly whether wrapped in data array or milestones', () => {
|
|
25
|
+
const src = readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8');
|
|
26
|
+
|
|
27
|
+
// Verify loadTimelineData parses array from json.data
|
|
28
|
+
assert.ok(src.includes('const items = Array.isArray(json)'));
|
|
29
|
+
assert.ok(src.includes('Array.isArray(json.data) ? json.data'));
|
|
30
|
+
|
|
31
|
+
// Test parsing logic with actual API responses
|
|
32
|
+
const apiFormat = {
|
|
33
|
+
status: 'success',
|
|
34
|
+
code: 200,
|
|
35
|
+
data: [
|
|
36
|
+
{ id: '1', year: '2013', era: 'snh48', title: 'Debut' },
|
|
37
|
+
{ id: '2', year: '2024', era: 'solo', title: 'In Blossom' }
|
|
38
|
+
]
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const parseItems = (json) => {
|
|
42
|
+
return Array.isArray(json)
|
|
43
|
+
? json
|
|
44
|
+
: (Array.isArray(json.data) ? json.data : (json.data?.milestones || json.milestones || []));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const parsed = parseItems(apiFormat);
|
|
48
|
+
assert.equal(parsed.length, 2);
|
|
49
|
+
assert.equal(parsed[0].title, 'Debut');
|
|
50
|
+
assert.equal(parsed[1].title, 'In Blossom');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('chatRoute streams /research queries via agent.runResearch with subagent_progress SSE', async () => {
|
|
54
|
+
const progressEvents = [];
|
|
55
|
+
let calledQuery = '';
|
|
56
|
+
let calledOptions = null;
|
|
57
|
+
|
|
58
|
+
const mockAgent = {
|
|
59
|
+
async runResearch(question, options = {}) {
|
|
60
|
+
calledQuery = question;
|
|
61
|
+
calledOptions = options;
|
|
62
|
+
options.onProgress?.({ type: 'worker-start', id: 'worker-1', label: 'Local Catalog Explorer' });
|
|
63
|
+
options.onProgress?.({ type: 'worker-complete', id: 'worker-1', label: 'Local Catalog Explorer' });
|
|
64
|
+
return {
|
|
65
|
+
id: 'res_test_123',
|
|
66
|
+
answer: 'จวีจิ้งอีได้รับรางวัลมากมายตั้งแต่ยุค SNH48 จนถึงศิลปินเดี่ยว',
|
|
67
|
+
sources: ['https://weibo.com/jujingyi'],
|
|
68
|
+
reports: [{ toolCalls: [{ name: 'get_awards', args: {}, result: { count: 5 }, duration: 12 }] }]
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
session: {
|
|
72
|
+
createUserMessage: () => {},
|
|
73
|
+
createAssistantMessage: () => {}
|
|
74
|
+
},
|
|
75
|
+
conversationHistory: []
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const mockResolve = async () => ({
|
|
79
|
+
ready: true,
|
|
80
|
+
agent: mockAgent,
|
|
81
|
+
client: { provider: 'lmstudio' },
|
|
82
|
+
provider: 'lmstudio'
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const req = Readable.from([JSON.stringify({ query: '/research รางวัลทั้งหมดของจวีจิ้งอี', stream: true })]);
|
|
86
|
+
req.headers = { accept: 'text/event-stream' };
|
|
87
|
+
req.socket = { remoteAddress: '127.0.0.1' };
|
|
88
|
+
|
|
89
|
+
const res = new EventEmitter();
|
|
90
|
+
res.chunks = '';
|
|
91
|
+
res.writeHead = (code, headers) => { res.statusCode = code; res.headers = headers; };
|
|
92
|
+
res.write = (chunk) => { res.chunks += chunk; };
|
|
93
|
+
res.end = (chunk = '') => { res.chunks += chunk; res.writableEnded = true; };
|
|
94
|
+
|
|
95
|
+
await handleChatRoute(req, res, new URL('http://localhost/api/chat'), { resolveConnection: mockResolve });
|
|
96
|
+
|
|
97
|
+
assert.equal(calledQuery, 'รางวัลทั้งหมดของจวีจิ้งอี');
|
|
98
|
+
assert.equal(calledOptions?.mode, 'research');
|
|
99
|
+
|
|
100
|
+
// Verify SSE stream contains subagent_progress and final answer
|
|
101
|
+
assert.ok(res.chunks.includes('subagent_progress'));
|
|
102
|
+
assert.ok(res.chunks.includes('Local Catalog Explorer'));
|
|
103
|
+
assert.ok(res.chunks.includes('จวีจิ้งอีได้รับรางวัลมากมาย'));
|
|
104
|
+
assert.ok(res.chunks.includes('"type":"final"'));
|
|
105
|
+
assert.ok(res.chunks.includes('[DONE]'));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('chatRoute streams /verify queries via agent.runResearch with mode: verify', async () => {
|
|
109
|
+
let calledQuery = '';
|
|
110
|
+
let calledOptions = null;
|
|
111
|
+
|
|
112
|
+
const mockAgent = {
|
|
113
|
+
async runResearch(claim, options = {}) {
|
|
114
|
+
calledQuery = claim;
|
|
115
|
+
calledOptions = options;
|
|
116
|
+
options.onProgress?.({ type: 'worker-start', id: 'worker-verify', label: 'Fact Verification' });
|
|
117
|
+
return {
|
|
118
|
+
id: 'verify_test_123',
|
|
119
|
+
answer: 'VERIFIED: จวีจิ้งอีเป็น Brand Ambassador ของ FRED Paris จริง',
|
|
120
|
+
sources: ['https://fred.com'],
|
|
121
|
+
reports: []
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
session: {
|
|
125
|
+
createUserMessage: () => {},
|
|
126
|
+
createAssistantMessage: () => {}
|
|
127
|
+
},
|
|
128
|
+
conversationHistory: []
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const mockResolve = async () => ({
|
|
132
|
+
ready: true,
|
|
133
|
+
agent: mockAgent,
|
|
134
|
+
client: { provider: 'lmstudio' },
|
|
135
|
+
provider: 'lmstudio'
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const req = Readable.from([JSON.stringify({ query: '/verify จวีจิ้งอีเป็นแบรนด์แอมบาสเดอร์ของ FRED Paris', stream: true })]);
|
|
139
|
+
req.headers = { accept: 'text/event-stream' };
|
|
140
|
+
req.socket = { remoteAddress: '127.0.0.1' };
|
|
141
|
+
|
|
142
|
+
const res = new EventEmitter();
|
|
143
|
+
res.chunks = '';
|
|
144
|
+
res.writeHead = () => {};
|
|
145
|
+
res.write = (chunk) => { res.chunks += chunk; };
|
|
146
|
+
res.end = (chunk = '') => { res.chunks += chunk; res.writableEnded = true; };
|
|
147
|
+
|
|
148
|
+
await handleChatRoute(req, res, new URL('http://localhost/api/chat'), { resolveConnection: mockResolve });
|
|
149
|
+
|
|
150
|
+
assert.equal(calledQuery, 'จวีจิ้งอีเป็นแบรนด์แอมบาสเดอร์ของ FRED Paris');
|
|
151
|
+
assert.equal(calledOptions?.mode, 'verify');
|
|
152
|
+
assert.ok(res.chunks.includes('VERIFIED'));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('chatRoute returns guidance when /research or /verify is sent with empty argument', async () => {
|
|
156
|
+
const mockAgent = {
|
|
157
|
+
runResearch: async () => assert.fail('should not call runResearch when empty')
|
|
158
|
+
};
|
|
159
|
+
const mockResolve = async () => ({
|
|
160
|
+
ready: true,
|
|
161
|
+
agent: mockAgent,
|
|
162
|
+
client: { provider: 'lmstudio' },
|
|
163
|
+
provider: 'lmstudio'
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const req = Readable.from([JSON.stringify({ query: '/research', stream: true })]);
|
|
167
|
+
req.headers = { accept: 'text/event-stream' };
|
|
168
|
+
req.socket = { remoteAddress: '127.0.0.1' };
|
|
169
|
+
|
|
170
|
+
const res = new EventEmitter();
|
|
171
|
+
res.chunks = '';
|
|
172
|
+
res.writeHead = () => {};
|
|
173
|
+
res.write = (chunk) => { res.chunks += chunk; };
|
|
174
|
+
res.end = (chunk = '') => { res.chunks += chunk; res.writableEnded = true; };
|
|
175
|
+
|
|
176
|
+
await handleChatRoute(req, res, new URL('http://localhost/api/chat'), { resolveConnection: mockResolve });
|
|
177
|
+
|
|
178
|
+
assert.ok(res.chunks.includes('ระบบวิจัยเชิงลึก'));
|
|
179
|
+
assert.ok(res.chunks.includes('3 Subagent Workers'));
|
|
180
|
+
});
|