kiku-agent 1.3.1 → 1.3.3

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.
Files changed (232) hide show
  1. package/README.md +69 -12
  2. package/package.json +5 -12
  3. package/server/agent/agentCore.js +319 -46
  4. package/server/agent/appPaths.js +31 -0
  5. package/server/agent/audioPlayer.js +339 -12
  6. package/server/agent/autoContext.js +193 -0
  7. package/server/agent/catalogEntityResolver.js +170 -28
  8. package/server/agent/clarifyingQuestions.js +137 -35
  9. package/server/agent/cli.js +177 -48
  10. package/server/agent/cloudAdapters.js +116 -0
  11. package/server/agent/commandRegistry.js +409 -80
  12. package/server/agent/commands.js +1056 -125
  13. package/server/agent/config.js +1088 -690
  14. package/server/agent/contextCompactor.js +213 -21
  15. package/server/agent/cpuMetrics.js +199 -0
  16. package/server/agent/engineService.js +591 -0
  17. package/server/agent/eval/AGENTS.md +228 -0
  18. package/server/agent/eval/analyze.py +684 -0
  19. package/server/agent/eval/gates.json +15 -0
  20. package/server/agent/eval/modules/analysis/compare.js +198 -0
  21. package/server/agent/eval/modules/analysis/gates.js +72 -0
  22. package/server/agent/eval/modules/analysis/judge.js +70 -0
  23. package/server/agent/eval/modules/core/cli.js +453 -0
  24. package/server/agent/eval/modules/core/config.js +60 -0
  25. package/server/agent/eval/modules/core/store.js +236 -0
  26. package/server/agent/eval/modules/evaluation/cases.js +689 -0
  27. package/server/agent/eval/modules/evaluation/runner.js +288 -0
  28. package/server/agent/eval/modules/evaluation/scoring.js +363 -0
  29. package/server/agent/eval/modules/evaluation/worker.js +122 -0
  30. package/server/agent/eval/modules/reporting/benchmark.js +265 -0
  31. package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
  32. package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
  33. package/server/agent/eval/modules/server/evalApi.js +1099 -0
  34. package/server/agent/eval/modules/ui/evalUi.js +4799 -0
  35. package/server/agent/eval/modules/utils/smokes.js +119 -0
  36. package/server/agent/eventBus.js +1 -0
  37. package/server/agent/ink/main.jsx +196 -191
  38. package/server/agent/jobManager.js +11 -1
  39. package/server/agent/lanDiscovery.js +721 -23
  40. package/server/agent/llmClient.js +317 -71
  41. package/server/agent/lmStudioWakeup.js +517 -65
  42. package/server/agent/locationService.js +797 -0
  43. package/server/agent/mcpQuizData.js +509 -0
  44. package/server/agent/memory/contextAllocator.js +19 -7
  45. package/server/agent/memory.js +3 -1
  46. package/server/agent/modelCompatibility.js +352 -0
  47. package/server/agent/modelDefaults.js +1 -0
  48. package/server/agent/modelManager.js +46 -2
  49. package/server/agent/newsWatch.js +9 -3
  50. package/server/agent/processManager.js +229 -0
  51. package/server/agent/prompts.js +174 -8
  52. package/server/agent/routingInterceptor.js +9 -1
  53. package/server/agent/sanitizer.js +41 -6
  54. package/server/agent/searchQueryPlanner.js +202 -0
  55. package/server/agent/session.js +59 -4
  56. package/server/agent/tests/agentBubbleSystem.test.js +65 -0
  57. package/server/agent/tests/agentFollowUps.test.js +253 -0
  58. package/server/agent/tests/agentMusicControl.test.js +245 -0
  59. package/server/agent/tests/apiRouter.test.js +108 -0
  60. package/server/agent/tests/architectureFixes.test.js +81 -0
  61. package/server/agent/tests/architectureRefactoring.test.js +194 -0
  62. package/server/agent/tests/autoContext.test.js +176 -0
  63. package/server/agent/tests/autoResearch.test.js +64 -0
  64. package/server/agent/tests/bilibiliEmbed.test.js +296 -0
  65. package/server/agent/tests/catalogSync.test.js +130 -0
  66. package/server/agent/tests/chatHardening.test.js +121 -0
  67. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  68. package/server/agent/tests/citations.test.js +180 -0
  69. package/server/agent/tests/clarificationAnalytics.test.js +219 -0
  70. package/server/agent/tests/clarifyingQuestions.test.js +627 -0
  71. package/server/agent/tests/cliAutomation.test.js +158 -0
  72. package/server/agent/tests/cloudProviders.test.js +322 -0
  73. package/server/agent/tests/commandInput.test.js +24 -0
  74. package/server/agent/tests/commands.test.js +506 -0
  75. package/server/agent/tests/contextIntelligence.test.js +209 -0
  76. package/server/agent/tests/conversationSafety.test.js +53 -0
  77. package/server/agent/tests/cpuMetrics.test.js +101 -0
  78. package/server/agent/tests/decisionAndSearchEnhancements.test.js +167 -0
  79. package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
  80. package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
  81. package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
  82. package/server/agent/tests/edgeCasesScenarios.test.js +131 -0
  83. package/server/agent/tests/engineService.test.js +436 -0
  84. package/server/agent/tests/engineUnload.test.js +94 -0
  85. package/server/agent/tests/entertainmentMetrics.test.js +126 -0
  86. package/server/agent/tests/envBootstrap.test.js +87 -0
  87. package/server/agent/tests/envCommand.test.js +284 -0
  88. package/server/agent/tests/errorHandling.test.js +80 -0
  89. package/server/agent/tests/errors.test.js +13 -0
  90. package/server/agent/tests/evalAnalyze.test.js +54 -0
  91. package/server/agent/tests/evalCommand.test.js +54 -0
  92. package/server/agent/tests/evalCorpus.test.js +126 -0
  93. package/server/agent/tests/evalHarness.test.js +62 -0
  94. package/server/agent/tests/evalJudgeContext.test.js +38 -0
  95. package/server/agent/tests/evalMetrics.test.js +320 -0
  96. package/server/agent/tests/evalStore.test.js +190 -0
  97. package/server/agent/tests/evalStudio.test.js +179 -0
  98. package/server/agent/tests/feedbackLearning.test.js +186 -0
  99. package/server/agent/tests/fetchApi.test.js +99 -0
  100. package/server/agent/tests/firstRun.test.js +44 -0
  101. package/server/agent/tests/gemma4eOptimization.test.js +95 -0
  102. package/server/agent/tests/hardwareTiers.test.js +206 -0
  103. package/server/agent/tests/hybridWebSearch.test.js +194 -0
  104. package/server/agent/tests/judgeService.test.js +251 -0
  105. package/server/agent/tests/lanDiscovery.test.js +324 -0
  106. package/server/agent/tests/lanInterfaces.test.js +510 -0
  107. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  108. package/server/agent/tests/locationTool.test.js +278 -0
  109. package/server/agent/tests/mcpClient.test.js +201 -0
  110. package/server/agent/tests/memoryRoute.test.js +173 -0
  111. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  112. package/server/agent/tests/modelCompatibility.test.js +148 -0
  113. package/server/agent/tests/modelManager.test.js +141 -0
  114. package/server/agent/tests/multilingualScenarios.test.js +69 -0
  115. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  116. package/server/agent/tests/newCommands.test.js +347 -0
  117. package/server/agent/tests/newCommandsTui.test.js +141 -0
  118. package/server/agent/tests/newTools.test.js +113 -0
  119. package/server/agent/tests/newsIntelligence.test.js +178 -0
  120. package/server/agent/tests/nextGenArchitecture.test.js +101 -0
  121. package/server/agent/tests/offlineInput.test.js +217 -0
  122. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  123. package/server/agent/tests/openDomainSearch.test.js +121 -0
  124. package/server/agent/tests/packagingGuard.test.js +57 -0
  125. package/server/agent/tests/presentation.test.js +61 -0
  126. package/server/agent/tests/processManager.test.js +144 -0
  127. package/server/agent/tests/providerPersistence.test.js +44 -0
  128. package/server/agent/tests/queryBenchmarks.test.js +83 -0
  129. package/server/agent/tests/queryDetails.test.js +126 -0
  130. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  131. package/server/agent/tests/realWorldValidation.test.js +391 -0
  132. package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
  133. package/server/agent/tests/runtimeWatchdog.test.js +32 -0
  134. package/server/agent/tests/sanitizer.test.js +37 -0
  135. package/server/agent/tests/scenariosData.js +378 -0
  136. package/server/agent/tests/schemaRouting.test.js +54 -0
  137. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  138. package/server/agent/tests/securityAuditRemediation.test.js +221 -0
  139. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  140. package/server/agent/tests/securityHardening.test.js +146 -0
  141. package/server/agent/tests/securitySanitizer.test.js +38 -0
  142. package/server/agent/tests/sessionAndCopy.test.js +267 -0
  143. package/server/agent/tests/sidebarControls.test.js +135 -0
  144. package/server/agent/tests/statefulMemory.test.js +277 -0
  145. package/server/agent/tests/subagent.test.js +216 -0
  146. package/server/agent/tests/subagentProgress.test.js +109 -0
  147. package/server/agent/tests/subagentTextFallback.test.js +115 -0
  148. package/server/agent/tests/suggestions.test.js +199 -0
  149. package/server/agent/tests/suggestionsTui.test.js +251 -0
  150. package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
  151. package/server/agent/tests/terminalImage.test.js +80 -0
  152. package/server/agent/tests/terminalMarkdown.test.js +166 -0
  153. package/server/agent/tests/terminalText.test.js +133 -0
  154. package/server/agent/tests/textProtocolLoop.test.js +197 -0
  155. package/server/agent/tests/textToolParsing.test.js +187 -0
  156. package/server/agent/tests/theme.test.js +26 -0
  157. package/server/agent/tests/toolRegistryParity.test.js +53 -0
  158. package/server/agent/tests/toolRoutingRegressions.test.js +160 -0
  159. package/server/agent/tests/tools.test.js +259 -0
  160. package/server/agent/tests/unifiedEndpoints.test.js +159 -0
  161. package/server/agent/tests/v110Features.test.js +262 -0
  162. package/server/agent/tests/versionBump.test.js +69 -0
  163. package/server/agent/tests/vramEstimator.test.js +595 -0
  164. package/server/agent/tests/webChatDocs.test.js +199 -0
  165. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  166. package/server/agent/tests/webChatInspection.test.js +551 -0
  167. package/server/agent/tests/webChatMcp.test.js +94 -0
  168. package/server/agent/tests/webChatMemory.test.js +112 -0
  169. package/server/agent/tests/webChatSecurity.test.js +169 -0
  170. package/server/agent/tests/webExtract.test.js +284 -0
  171. package/server/agent/tests/webSources.test.js +281 -0
  172. package/server/agent/textToolProtocol.js +173 -46
  173. package/server/agent/toolRouter.js +318 -44
  174. package/server/agent/tools.js +877 -96
  175. package/server/agent/ui.js +39 -34
  176. package/server/agent/vramService.js +35 -54
  177. package/server/agent/webSearch.js +441 -162
  178. package/server/agent/websearch/orchestrator.js +15 -5
  179. package/server/agent/websearch/resilience/cache.js +9 -2
  180. package/server/agent/websearch/security/ssrf.js +41 -10
  181. package/server/agent/youtubeAuth.js +5 -20
  182. package/server/apiRouter.js +5558 -4422
  183. package/server/eval/judgeRubrics.js +110 -0
  184. package/server/eval/judgeService.js +290 -0
  185. package/server/eval/runBenchmark.js +182 -0
  186. package/server/mcp/data/discography.js +2 -2
  187. package/server/mcp/examples/antigravity.example.json +17 -0
  188. package/server/mcp/examples/librechat.example.yaml +16 -0
  189. package/server/mcp/handler.js +8 -4
  190. package/server/mcp/security.js +220 -21
  191. package/server/mcp/server.js +2 -1
  192. package/server/mcp/tests/connection.test.js +162 -0
  193. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  194. package/server/mcp/tools.js +7 -1
  195. package/server/supabase_migration_prefix.sql +102 -0
  196. package/server/supabase_schema.sql +63 -0
  197. package/server/web-chat/core/connection.js +179 -218
  198. package/server/web-chat/core/conversationContext.js +9 -2
  199. package/server/web-chat/core/directCatalogEngine.js +52 -28
  200. package/server/web-chat/core/dramaKnowledge.js +33 -22
  201. package/server/web-chat/core/followUpEngine.js +735 -81
  202. package/server/web-chat/core/futureEventGuard.js +5 -1
  203. package/server/web-chat/core/musicControlAction.js +70 -0
  204. package/server/web-chat/core/security.js +32 -21
  205. package/server/web-chat/core/toolInspector.js +6 -6
  206. package/server/web-chat/core/urlGuard.js +45 -0
  207. package/server/web-chat/router.js +204 -157
  208. package/server/web-chat/routes/chatRoute.js +885 -488
  209. package/server/web-chat/routes/engineRoute.js +270 -0
  210. package/server/web-chat/routes/exportRoute.js +1 -1
  211. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  212. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  213. package/server/web-chat/routes/mcpRoute.js +386 -0
  214. package/server/web-chat/routes/memoryRoute.js +253 -0
  215. package/server/web-chat/routes/providerRoute.js +49 -16
  216. package/server/web-chat/routes/staticRoute.js +306 -17
  217. package/server/web-chat/routes/statusRoute.js +194 -73
  218. package/server/web-chat/routes/suggestRoute.js +11 -3
  219. package/server/web-chat/routes/verifyRoute.js +26 -4
  220. package/server/web-chat/server.js +7 -6
  221. package/server/web-chat/ui/docsBuilder.js +3558 -0
  222. package/server/web-chat/ui/htmlBuilder.js +12 -0
  223. package/server/web-chat/ui/scripts/client.js +3401 -1302
  224. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  225. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  226. package/server/web-chat/ui/styles/lan-card.css +165 -0
  227. package/server/web-chat/ui/styles/main.css +4289 -447
  228. package/server/web-chat/ui/sw.js +64 -0
  229. package/server/web-chat/ui/templates/body.html +407 -281
  230. package/src/data/awardsData.js +146 -146
  231. package/src/data/musicVideoData.js +6 -4
  232. package/src/lib/vram/estimator.js +195 -19
@@ -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
+
@@ -0,0 +1,44 @@
1
+ import test, { after } from 'node:test';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
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 }));
8
+ import fs from 'node:fs';
9
+ const { SETTINGS_FILE } = await import("../appPaths.js");
10
+ const {
11
+ savePersistedActiveProvider,
12
+ savePersistedProviderModel,
13
+ getPersistedActiveProvider,
14
+ loadPersistedSettings,
15
+ CLOUD_PROVIDERS,
16
+ } = await import("../config.js");
17
+ const { LLMClient } = await import("../llmClient.js");
18
+
19
+ test('Provider and Model settings persist across restarts', () => {
20
+ const originalGroqModel = CLOUD_PROVIDERS.groq.defaultModel;
21
+ const originalLmStudioModel = CLOUD_PROVIDERS.lmstudio.defaultModel;
22
+
23
+ try {
24
+ savePersistedActiveProvider('groq', 'openai/gpt-oss-120b');
25
+ savePersistedProviderModel('groq', 'llama-3.3-70b-versatile');
26
+
27
+ assert.equal(getPersistedActiveProvider(), 'groq');
28
+ const settings = loadPersistedSettings();
29
+ assert.equal(settings.activeProvider, 'groq');
30
+ assert.equal(settings.providerModels?.groq, 'llama-3.3-70b-versatile');
31
+
32
+ const client = new LLMClient();
33
+ assert.equal(client.provider, 'groq');
34
+ assert.equal(client.model, 'llama-3.3-70b-versatile');
35
+
36
+ client.switchProvider('openrouter', { model: 'openrouter/auto' });
37
+ assert.equal(getPersistedActiveProvider(), 'openrouter');
38
+ } finally {
39
+ try { if (fs.existsSync(SETTINGS_FILE)) fs.unlinkSync(SETTINGS_FILE); } catch {}
40
+ CLOUD_PROVIDERS.groq.defaultModel = originalGroqModel || 'openai/gpt-oss-120b';
41
+ CLOUD_PROVIDERS.lmstudio.defaultModel = originalLmStudioModel || 'google/gemma-4-e4b';
42
+ loadPersistedSettings();
43
+ }
44
+ });
@@ -0,0 +1,83 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { TEST_SCENARIOS } from './scenariosData.js';
4
+ import { DynamicToolRouter } from '../toolRouter.js';
5
+ import { detectDominantLanguage } from '../agentCore.js';
6
+ import { executeTool } from '../tools.js';
7
+ import { AgentMemory } from '../memory.js';
8
+
9
+ test('Benchmark: Test scenarios contain comprehensive valid entries', () => {
10
+ assert.ok(TEST_SCENARIOS.length >= 20);
11
+ const categories = new Set(TEST_SCENARIOS.map(s => s.category));
12
+ assert.ok(categories.has('works'));
13
+ assert.ok(categories.has('music'));
14
+ assert.ok(categories.has('brand'));
15
+ assert.ok(categories.has('awards'));
16
+ assert.ok(categories.has('profile'));
17
+ assert.ok(categories.has('timeline'));
18
+ assert.ok(categories.has('research'));
19
+ assert.ok(categories.has('system'));
20
+ });
21
+
22
+ test('Benchmark: DynamicToolRouter routes all test scenario prompts correctly', () => {
23
+ const router = new DynamicToolRouter();
24
+
25
+ for (const scenario of TEST_SCENARIOS) {
26
+ const routedTools = router.routeToolsForQuery(scenario.query);
27
+ const routedNames = new Set(routedTools.map(t => t.function.name));
28
+
29
+ assert.ok(routedTools.length > 0, `Scenario ${scenario.id} returned empty routed tools`);
30
+ assert.ok(routedTools.length <= 15, `Scenario ${scenario.id} exceeded tool limit`);
31
+
32
+ const hasExpectedTool = scenario.expectedTools.some(toolName => routedNames.has(toolName));
33
+ assert.ok(hasExpectedTool, `Scenario ${scenario.id} did not include any expected tools: ${scenario.expectedTools.join(', ')}`);
34
+ }
35
+ });
36
+
37
+ test('Benchmark: Language detection accurately identifies scenario query languages', () => {
38
+ for (const scenario of TEST_SCENARIOS) {
39
+ const detected = detectDominantLanguage(scenario.query);
40
+ assert.equal(detected, scenario.language, `Scenario ${scenario.id} expected ${scenario.language} but got ${detected}`);
41
+ }
42
+ });
43
+
44
+ test('Benchmark: Direct tool execution verifies domain facts across categories', async () => {
45
+ const worksRes = await executeTool('search_works', { query: 'In Blossom' });
46
+ assert.equal(worksRes.status, 'success');
47
+ assert.ok(worksRes.count > 0);
48
+
49
+ const discoRes = await executeTool('search_discography', { query: 'FIND THE WAY' });
50
+ assert.equal(discoRes.status, 'success');
51
+ assert.ok(discoRes.count > 0);
52
+
53
+ const brandRes = await executeTool('get_brand_endorsements', {});
54
+ assert.equal(brandRes.status, 'success');
55
+ assert.ok(brandRes.brands.length > 0);
56
+
57
+ const profileRes = await executeTool('get_profile_and_fandom', {});
58
+ assert.equal(profileRes.status, 'success');
59
+ assert.ok(profileRes.profile.nameCn.includes('鞠婧祎'));
60
+
61
+ const awardsRes = await executeTool('get_awards', {});
62
+ assert.equal(awardsRes.status, 'success');
63
+ assert.ok(awardsRes.awards.length > 0);
64
+
65
+ const statsRes = await executeTool('get_system_stats', {});
66
+ assert.equal(statsRes.status, 'success');
67
+ assert.ok(statsRes.stats.totalWorks > 0);
68
+ });
69
+
70
+ test('Benchmark: Memory query-aware ranking retrieves top relevant scenario facts', () => {
71
+ const memory = new AgentMemory();
72
+ for (const scenario of TEST_SCENARIOS) {
73
+ memory.addFact(`Ju Jingyi scenario fact for ${scenario.id}: ${scenario.expectedKeywords.join(' ')}`, {
74
+ category: scenario.category,
75
+ tags: scenario.expectedKeywords,
76
+ confidence: 'high'
77
+ });
78
+ }
79
+
80
+ const retrieved = memory.getRelevantFacts('In Blossom drama Yang Caiwei', 3);
81
+ assert.ok(retrieved.length > 0);
82
+ assert.ok(retrieved.some(f => f.content.includes('drama_in_blossom_detail')));
83
+ });
@@ -0,0 +1,126 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import {
5
+ classifyToolResult,
6
+ formatDetailDuration,
7
+ parseDetailsRequest,
8
+ serializeDetailValue,
9
+ summarizeQueryDetails,
10
+ getCopyableContent,
11
+ copyToClipboard,
12
+ formatQueryDetails,
13
+ } from '../queryDetails.js';
14
+
15
+ test('details request supports latest, query, view, markdown, and raw tool selection', () => {
16
+ assert.deepEqual(parseDetailsRequest(''), {
17
+ error: null, queryNumber: null, view: 'answer', toolIndex: null,
18
+ });
19
+ assert.deepEqual(parseDetailsRequest('Q12 tools'), {
20
+ error: null, queryNumber: 12, view: 'tools', toolIndex: null,
21
+ });
22
+ assert.deepEqual(parseDetailsRequest('latest thinking'), {
23
+ error: null, queryNumber: null, view: 'thinking', toolIndex: null,
24
+ });
25
+ assert.deepEqual(parseDetailsRequest('3 tool 2'), {
26
+ error: null, queryNumber: 3, view: 'tool', toolIndex: 2,
27
+ });
28
+ assert.deepEqual(parseDetailsRequest('Q1 all'), {
29
+ error: null, queryNumber: 1, view: 'all', toolIndex: null,
30
+ });
31
+ assert.deepEqual(parseDetailsRequest('Q2 markdown'), {
32
+ error: null, queryNumber: 2, view: 'markdown', toolIndex: null,
33
+ });
34
+ assert.match(parseDetailsRequest('tool').error, /positive tool number/);
35
+ assert.match(parseDetailsRequest('Q1 unknown').error, /Unknown details view/);
36
+ });
37
+
38
+ test('query details summary separates successful, empty, and failed tools', () => {
39
+ const details = {
40
+ answer: 'answer',
41
+ thinking: 'thought',
42
+ tools: [
43
+ { duration: 10, result: { status: 'ok', count: 2 } },
44
+ { duration: 20, result: { status: 'not_found' } },
45
+ { duration: 30, result: { status: 'error' } },
46
+ ],
47
+ };
48
+
49
+ assert.deepEqual(summarizeQueryDetails(details), {
50
+ toolCount: 3,
51
+ successfulTools: 1,
52
+ failedTools: 1,
53
+ emptyTools: 1,
54
+ toolDurationMs: 60,
55
+ thinkingCharacters: 7,
56
+ answerCharacters: 6,
57
+ });
58
+ assert.equal(classifyToolResult({ count: 0 }).state, 'not_found');
59
+ });
60
+
61
+ test('detail formatting is bounded and handles non-serializable values', () => {
62
+ assert.equal(formatDetailDuration(45), '45ms');
63
+ assert.equal(formatDetailDuration(1250), '1.25s');
64
+ assert.equal(serializeDetailValue({ long: 'x'.repeat(100) }, 30).truncated, true);
65
+ assert.equal(serializeDetailValue({ long: 'x'.repeat(100) }).truncated, false);
66
+
67
+ const cyclic = {};
68
+ cyclic.self = cyclic;
69
+ assert.equal(serializeDetailValue(cyclic).truncated, false);
70
+ });
71
+
72
+ test('getCopyableContent extracts untruncated answers, thinking, tools, and markdown', () => {
73
+ const details = {
74
+ number: 1,
75
+ status: 'complete',
76
+ prompt: 'Who is Ju Jingyi?',
77
+ answer: 'Ju Jingyi (鞠婧祎) is a famous Chinese singer and actress.',
78
+ thinking: 'Analyzing profile catalog and works...',
79
+ tools: [
80
+ {
81
+ name: 'get_profile',
82
+ duration: 15,
83
+ args: {},
84
+ result: { status: 'success', nameCn: '鞠婧祎' },
85
+ },
86
+ ],
87
+ };
88
+
89
+ const ans = getCopyableContent(details, { view: 'answer' });
90
+ assert.equal(ans.text, 'Ju Jingyi (鞠婧祎) is a famous Chinese singer and actress.');
91
+ assert.equal(ans.type, 'answer');
92
+
93
+ const think = getCopyableContent(details, { view: 'thinking' });
94
+ assert.equal(think.text, 'Analyzing profile catalog and works...');
95
+ assert.equal(think.type, 'thinking');
96
+
97
+ const tool1 = getCopyableContent(details, { view: 'tool', toolIndex: 1 });
98
+ assert.match(tool1.text, /get_profile/);
99
+ assert.equal(tool1.type, 'tool');
100
+
101
+ const md = getCopyableContent(details, { view: 'markdown' });
102
+ assert.match(md.text, /# Question 1/);
103
+ assert.match(md.text, /## Thinking/);
104
+ assert.match(md.text, /## Answer/);
105
+ assert.equal(md.type, 'markdown');
106
+ });
107
+
108
+ test('copyToClipboard safely executes cross-platform child process', () => {
109
+ const result = copyToClipboard('test copy');
110
+ assert.equal(typeof result, 'boolean');
111
+ });
112
+
113
+ test('formatQueryDetails formats diagnostics without throwing', () => {
114
+ const formatted = formatQueryDetails({
115
+ number: 1,
116
+ status: 'complete',
117
+ prompt: 'test prompt',
118
+ answer: 'test answer',
119
+ durationMs: 50,
120
+ });
121
+ assert.match(formatted, /=== Answer • Q1 ===/);
122
+ assert.match(formatted, /test answer/);
123
+
124
+ assert.equal(formatQueryDetails(null), 'No query diagnostics recorded.');
125
+ });
126
+
@@ -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
+ });