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,391 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawn } from 'node:child_process';
4
+ import { createServer } from 'node:http';
5
+ import { resolve, dirname } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ import { estimateTokens, calculateTokenCost, formatCostUsd, formatTokenCount, MODEL_PRICING } from '../telemetry.js';
9
+ import { getProfile, listProfiles, BUILTIN_PROFILES } from '../profiles.js';
10
+ import { extractCitations, formatCitationsBlock, resolveCitationReference } from '../citations.js';
11
+ import { handleWebChatRequest } from '../../web-chat/router.js';
12
+ import { checkChatRateLimit, applySecurityHeaders, resolveAllowedOrigin, extractApiKey } from '../../web-chat/core/security.js';
13
+ import {
14
+ getUnifiedProfile,
15
+ getUnifiedWorks,
16
+ getUnifiedDiscography,
17
+ getUnifiedAwards,
18
+ getUnifiedBrands,
19
+ getUnifiedTimeline
20
+ } from '../../mcp/dataAdapter.js';
21
+ import { worksData as richWorks } from '../../../src/data/worksData.js';
22
+ import { worksData as mcpWorks } from '../../mcp/data/works.js';
23
+
24
+ const __filename = fileURLToPath(import.meta.url);
25
+ const __dirname = dirname(__filename);
26
+ const rootDir = resolve(__dirname, '../../../');
27
+ const cliPath = resolve(rootDir, 'server/agent/cli.js');
28
+ const mcpPath = resolve(rootDir, 'server/mcp/stdio.js');
29
+
30
+
31
+ function runProcessWithTimeout(cmd, args, { input = null, timeoutMs = 10000 } = {}) {
32
+ return new Promise((resolve) => {
33
+ const proc = spawn(cmd, args, {
34
+ cwd: rootDir,
35
+ env: { ...process.env, NODE_ENV: 'test', NO_COLOR: '1' },
36
+ stdio: ['pipe', 'pipe', 'pipe']
37
+ });
38
+
39
+ let stdout = '';
40
+ let stderr = '';
41
+ let timedOut = false;
42
+
43
+ const timer = setTimeout(() => {
44
+ timedOut = true;
45
+ try { proc.kill('SIGKILL'); } catch {}
46
+ resolve({
47
+ code: -1,
48
+ stdout,
49
+ stderr,
50
+ timedOut: true,
51
+ error: new Error(`Process timed out after ${timeoutMs}ms`)
52
+ });
53
+ }, timeoutMs);
54
+ timer.unref();
55
+
56
+ proc.stdout.on('data', d => { stdout += d.toString(); });
57
+ proc.stderr.on('data', d => { stderr += d.toString(); });
58
+
59
+ if (input) {
60
+ proc.stdin.write(input);
61
+ }
62
+ proc.stdin.end();
63
+
64
+ proc.on('close', code => {
65
+ clearTimeout(timer);
66
+ if (!timedOut) {
67
+ resolve({ code, stdout, stderr, timedOut: false });
68
+ }
69
+ });
70
+
71
+ proc.on('error', err => {
72
+ clearTimeout(timer);
73
+ resolve({ code: -1, stdout, stderr, timedOut: false, error: err });
74
+ });
75
+ });
76
+ }
77
+
78
+
79
+
80
+ test('Phase 1 Real-World: CLI --version exits cleanly in < 1s with exit code 0', async () => {
81
+
82
+ const res = await runProcessWithTimeout(process.execPath, [cliPath, '--version'], { timeoutMs: 8000 });
83
+ assert.equal(res.timedOut, false, 'CLI must not hang on --version');
84
+ assert.equal(res.code, 0);
85
+ assert.match(res.stdout.trim(), /^\d+\.\d+\.\d+/);
86
+ });
87
+
88
+ test('Phase 1 Real-World: CLI --help --plain produces zero ANSI garbage and exits code 0', async () => {
89
+ const res = await runProcessWithTimeout(process.execPath, [cliPath, '--help', '--plain'], { timeoutMs: 8000 });
90
+ assert.equal(res.timedOut, false, 'CLI must not hang on --help');
91
+ assert.equal(res.code, 0);
92
+
93
+ assert.equal(/\x1b\[[0-9;]*[a-zA-Z]/.test(res.stdout), false, 'Output must not contain ANSI escape codes in plain mode');
94
+ assert.ok(res.stdout.includes('CLI Flags & Options'));
95
+ assert.ok(res.stdout.includes('--profile'));
96
+ assert.ok(res.stdout.includes('--print'));
97
+ });
98
+
99
+ test('Phase 1 Real-World: CLI exits with code 3 on unknown flags without crashing', async () => {
100
+ const res = await runProcessWithTimeout(process.execPath, [cliPath, '--non-existent-flag-xyz'], { timeoutMs: 8000 });
101
+ assert.equal(res.timedOut, false);
102
+ assert.equal(res.code, 3);
103
+ assert.ok(res.stderr.includes('Unknown option') || res.stdout.includes('Unknown option'));
104
+ });
105
+
106
+ test('Phase 1 Real-World: CLI piped stdin handles EOF gracefully without hanging', async () => {
107
+ const res = await runProcessWithTimeout(
108
+ process.execPath,
109
+ [cliPath, '--url', 'http://127.0.0.1:9999/v1', '--output', 'json'],
110
+ { input: 'Who is Ju Jingyi?', timeoutMs: 4000 }
111
+ );
112
+ assert.equal(res.timedOut, false, 'CLI piped input must not hang');
113
+ assert.equal(res.code, 2, 'Unreachable server exits code 2');
114
+
115
+ const payload = JSON.parse(res.stdout);
116
+ assert.equal(payload.status, 'error');
117
+ assert.equal(payload.query, 'Who is Ju Jingyi?');
118
+ assert.ok(payload.metrics);
119
+ assert.equal(typeof payload.metrics.costUsd, 'number');
120
+ });
121
+
122
+
123
+
124
+ test('Phase 2 Real-World: Security headers and CORS origins are strictly defended', () => {
125
+ assert.equal(resolveAllowedOrigin('http://localhost:3000'), 'http://localhost:3000');
126
+ assert.equal(resolveAllowedOrigin('http://127.0.0.1:3000'), 'http://127.0.0.1:3000');
127
+ assert.equal(resolveAllowedOrigin('https://jujingyi-kikuuu.vercel.app'), 'https://jujingyi-kikuuu.vercel.app');
128
+
129
+ const origEnv = process.env.WEBCHAT_CORS_ORIGINS;
130
+ try {
131
+ process.env.WEBCHAT_CORS_ORIGINS = 'https://jujingyi-kikuuu.vercel.app, https://portal.kiku.com';
132
+ assert.equal(resolveAllowedOrigin('https://malicious-attacker.com'), null);
133
+ assert.equal(resolveAllowedOrigin('https://portal.kiku.com'), 'https://portal.kiku.com');
134
+ } finally {
135
+ if (origEnv !== undefined) process.env.WEBCHAT_CORS_ORIGINS = origEnv;
136
+ else delete process.env.WEBCHAT_CORS_ORIGINS;
137
+ }
138
+
139
+ const mockHeaders = {};
140
+ const mockRes = {
141
+ setHeader: (k, v) => { mockHeaders[k] = v; }
142
+ };
143
+ applySecurityHeaders({ headers: { origin: 'http://localhost:3000' } }, mockRes);
144
+ assert.equal(mockHeaders['X-Content-Type-Options'], 'nosniff');
145
+ assert.equal(mockHeaders['X-Frame-Options'], 'SAMEORIGIN');
146
+ assert.equal(mockHeaders['Referrer-Policy'], 'strict-origin-when-cross-origin');
147
+ assert.equal(mockHeaders['Access-Control-Allow-Origin'], 'http://localhost:3000');
148
+ });
149
+
150
+ test('Phase 2 Real-World: Rate limiter handles burst traffic and rejects on limit without leaking memory', () => {
151
+ const testIp = '198.51.100.99';
152
+ let allowedCount = 0;
153
+ let rejectedCount = 0;
154
+
155
+ for (let i = 0; i < 70; i++) {
156
+ const check = checkChatRateLimit(testIp);
157
+ if (check.allowed) allowedCount++;
158
+ else rejectedCount++;
159
+ }
160
+
161
+ assert.equal(allowedCount, 60, 'Should allow exactly 60 requests per minute');
162
+ assert.equal(rejectedCount, 10, 'Should reject requests exceeding the 60 threshold');
163
+ });
164
+
165
+ test('Phase 2 Real-World: Real HTTP server routes /api/status with security headers without errors', async () => {
166
+ const server = createServer(async (req, res) => {
167
+ await handleWebChatRequest(req, res);
168
+ });
169
+
170
+ await new Promise(r => server.listen(0, '127.0.0.1', r));
171
+ const port = server.address().port;
172
+
173
+ try {
174
+ const res = await fetch(`http://127.0.0.1:${port}/api/status`);
175
+ assert.equal(res.status, 200);
176
+ assert.equal(res.headers.get('x-content-type-options'), 'nosniff');
177
+ assert.equal(res.headers.get('x-frame-options'), 'SAMEORIGIN');
178
+ const data = await res.json();
179
+ assert.equal(data.status, 'ok');
180
+ assert.equal(data.server, 'Kiku Web Chatbot');
181
+ assert.ok(data.capabilities);
182
+ } finally {
183
+ server.close();
184
+ }
185
+ });
186
+
187
+
188
+
189
+ test('Phase 3 Real-World: Token heuristics handle Thai, CJK, English, and null edge cases safely', () => {
190
+ assert.equal(estimateTokens(''), 0);
191
+ assert.equal(estimateTokens(null), 0);
192
+ assert.equal(estimateTokens(undefined), 0);
193
+
194
+ const enCount = estimateTokens('Ju Jingyi is a talented Chinese singer and actress.');
195
+ assert.ok(enCount >= 10 && enCount <= 20);
196
+
197
+ const cnCount = estimateTokens('鞠婧祎是中国内地流行乐女歌手、影视演员。');
198
+ assert.ok(cnCount >= 15 && cnCount <= 35);
199
+
200
+ const thCount = estimateTokens('จวีจิ้งอี เป็นนักแสดงและนักร้องชาวจีนที่มีชื่อเสียง');
201
+ assert.ok(thCount >= 20 && thCount <= 75);
202
+
203
+ const mixedCount = estimateTokens('Ju Jingyi (鞠婧祎) แสดงในเรื่อง ตำนานหยุนซี (Legend of Yunxi) 2018');
204
+ assert.ok(mixedCount > 0 && !Number.isNaN(mixedCount));
205
+ });
206
+
207
+ test('Phase 3 Real-World: calculateTokenCost produces exact values across providers and local LM Studio', () => {
208
+ const localCost = calculateTokenCost({ prompt: 5000, completion: 2000, total: 7000 }, 'lmstudio', 'google/gemma-4-e4b');
209
+ assert.equal(localCost, 0);
210
+ assert.equal(formatCostUsd(localCost), '$0.00 (Local)');
211
+
212
+ const groqCost = calculateTokenCost({ prompt: 1_000_000, completion: 1_000_000, total: 2_000_000 }, 'groq');
213
+ assert.ok(groqCost > 0);
214
+
215
+ const deepseekCost = calculateTokenCost({ prompt: 1_000_000, completion: 1_000_000, total: 2_000_000 }, 'deepseek');
216
+ assert.ok(deepseekCost > 0);
217
+
218
+ assert.equal(formatCostUsd(0.00001), '<$0.0001');
219
+ assert.equal(formatCostUsd(0.01234), '$0.0123');
220
+ assert.equal(formatTokenCount(1500), '1,500');
221
+ assert.equal(formatTokenCount(1200000), '1,200,000');
222
+ });
223
+
224
+ test('Phase 3 Real-World: All built-in profiles are valid and have required runtime keys', () => {
225
+ const profiles = listProfiles();
226
+ assert.ok(profiles.length >= 5);
227
+
228
+ for (const p of profiles) {
229
+ assert.ok(p.id, 'Profile must have an id');
230
+ assert.ok(p.name, 'Profile must have a human-readable name');
231
+ assert.ok(p.provider, 'Profile must specify a provider');
232
+ assert.ok(p.model, 'Profile must specify a model');
233
+ assert.ok(typeof p.temperature === 'number', 'Profile must have numeric temperature');
234
+ assert.ok(typeof p.description === 'string', 'Profile must have a description');
235
+ }
236
+
237
+ const geminiProfile = getProfile('gemini');
238
+ assert.equal(geminiProfile.provider, 'gemini');
239
+ assert.equal(geminiProfile.model, 'gemini-3.7-flash');
240
+ });
241
+
242
+
243
+ test('Phase 4 Real-World: Citations extraction handles realistic multi-turn responses without corruption', () => {
244
+ const tools = [
245
+ {
246
+ name: 'search_works',
247
+ result: {
248
+ works: [
249
+ {
250
+ id: 'in-blossom',
251
+ titleTh: 'ฮวาเจียนลิ่ง ในม่านเมฆ (In Blossom)',
252
+ titleCn: '花间令',
253
+ titleEn: 'In Blossom',
254
+ year: 2024,
255
+ role: 'หยางไฉ่เวย / ซ่างกวนจื่อ',
256
+ category: 'ซีรีส์สืบสวนโบราณ'
257
+ },
258
+ {
259
+ id: 'veil-of-shadows',
260
+ titleTh: '月鳞绮纪 (Veil of Shadows)',
261
+ titleCn: '月鳞绮纪',
262
+ titleEn: 'Veil of Shadows',
263
+ year: 2026,
264
+ role: 'ลู่หวู่อี (Lu Wuyi)',
265
+ category: 'ซีรีส์แฟนตาซีโบราณ'
266
+ }
267
+ ]
268
+ }
269
+ }
270
+ ];
271
+
272
+ const answer = 'ผลงานเด่นในปี 2024 คือเรื่อง In Blossom (花间令) รับบทเป็น หยางไฉ่เวย และปี 2026 มีเรื่อง Veil of Shadows';
273
+ const citations = extractCitations(answer, tools);
274
+
275
+ assert.equal(citations.length, 2);
276
+ assert.equal(citations[0].id, 1);
277
+ assert.equal(citations[1].id, 2);
278
+
279
+
280
+ const block = formatCitationsBlock(citations, { lang: 'th' });
281
+ assert.ok(block.includes('📖 แหล่งข้อมูลและหลักฐานอ้างอิง'));
282
+ assert.ok(block.includes('[1]'));
283
+ assert.ok(block.includes('[2]'));
284
+ assert.ok(!block.includes('undefined'));
285
+ assert.ok(!block.includes('null'));
286
+ });
287
+
288
+ test('Phase 4 Real-World: resolveCitationReference is immune to crash on edge case queries', () => {
289
+ const mockCitations = [
290
+ { id: 1, title: 'ตำนานหยุนซี (芸汐传)', year: 2018 },
291
+ { id: 2, title: 'ฮวาเจียนลิ่ง ในม่านเมฆ (In Blossom)', year: 2024 }
292
+ ];
293
+
294
+
295
+ assert.equal(resolveCitationReference('', mockCitations), null);
296
+ assert.equal(resolveCitationReference(' ', mockCitations), null);
297
+ assert.equal(resolveCitationReference(null, mockCitations), null);
298
+ assert.equal(resolveCitationReference('!@#$%^&*()_+', mockCitations), null);
299
+
300
+
301
+ const matchBracket = resolveCitationReference('เล่าเนื้อเรื่องของ [2] ให้ฟังหน่อย', mockCitations);
302
+ assert.equal(matchBracket?.id, 2);
303
+
304
+ const matchHash = resolveCitationReference('ขอประวัติตัวละครใน #1', mockCitations);
305
+ assert.equal(matchHash?.id, 1);
306
+
307
+ const matchTitle = resolveCitationReference('หยุนซี ออกอากาศปีไหน', mockCitations);
308
+ assert.equal(matchTitle?.id, 1);
309
+
310
+ const matchEnglishTitle = resolveCitationReference('Tell me more about In Blossom', mockCitations);
311
+ assert.equal(matchEnglishTitle?.id, 2);
312
+ });
313
+
314
+
315
+ test('Phase 5 Real-World: 100% of 18 catalog works match with valid schema and release metadata', () => {
316
+ assert.equal(richWorks.length, 18, 'Expected exactly 18 works in rich catalog');
317
+
318
+ for (const w of richWorks) {
319
+ assert.ok(w.id, 'Work must have an id');
320
+ assert.ok(w.titleCn, `Work ${w.id} must have titleCn`);
321
+ assert.ok(w.titleEn, `Work ${w.id} must have titleEn`);
322
+ assert.ok(typeof w.year === 'number' && w.year >= 2013 && w.year <= 2026, `Work ${w.id} has invalid year ${w.year}`);
323
+
324
+
325
+ if (w.themeSong?.youtubeId) {
326
+ assert.equal(w.themeSong.youtubeId.length, 11, `YouTube ID for ${w.id} theme song should be 11 characters`);
327
+ }
328
+
329
+
330
+ const mcpMatch = mcpWorks.find(m => m.id === w.id || m.alias === w.id);
331
+ assert.ok(mcpMatch, `Work ${w.id} must exist in MCP worksData`);
332
+ assert.equal(mcpMatch.year, w.year);
333
+ }
334
+ });
335
+
336
+ test('Phase 5 Real-World: Unified DataAdapter models are structurally complete', () => {
337
+ const profile = getUnifiedProfile();
338
+ assert.equal(profile.nameEn, 'Ju Jingyi');
339
+ assert.equal(profile.nameCn, '鞠婧祎');
340
+ assert.ok(profile.bioSummary);
341
+
342
+ const works = getUnifiedWorks();
343
+ assert.equal(works.length, 18);
344
+
345
+ const discography = getUnifiedDiscography();
346
+ assert.ok(discography.length >= 25);
347
+
348
+ const ids = new Set();
349
+ for (const d of discography) {
350
+ assert.ok(d.id, 'Song must have id');
351
+ ids.add(d.id);
352
+ }
353
+ assert.equal(ids.size, discography.length, 'Unified discography must not have duplicate IDs');
354
+
355
+ const awards = getUnifiedAwards();
356
+ assert.ok(awards.length >= 10);
357
+
358
+ const brands = getUnifiedBrands();
359
+ assert.ok(brands.length >= 10);
360
+ assert.ok(brands.some(b => b.isGlobal && b.id === 'prsr'));
361
+ assert.ok(brands.some(b => b.isGlobal && b.id === 'versace'));
362
+
363
+ const timeline = getUnifiedTimeline();
364
+ assert.ok(timeline.length >= 10);
365
+ });
366
+
367
+ test('Phase 5 Real-World: MCP stdio server initializes properly over child process stdio', async () => {
368
+
369
+ const res = await runProcessWithTimeout(
370
+ process.execPath,
371
+ [mcpPath],
372
+ {
373
+ input: JSON.stringify({
374
+ jsonrpc: '2.0',
375
+ id: 1,
376
+ method: 'initialize',
377
+ params: {
378
+ protocolVersion: '2024-11-05',
379
+ capabilities: {},
380
+ clientInfo: { name: 'real-world-tester', version: '1.0.0' }
381
+ }
382
+ }) + '\n',
383
+ timeoutMs: 4000
384
+ }
385
+ );
386
+
387
+ assert.equal(res.timedOut, false, 'MCP server must not hang on initialize');
388
+
389
+ assert.ok(res.stdout.includes('"jsonrpc":"2.0"'), 'MCP must return valid JSON-RPC');
390
+ assert.ok(res.stdout.includes('"capabilities"'), 'MCP must advertise capabilities');
391
+ });
@@ -0,0 +1,117 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { mapConcurrent } from '../suggestions.js';
5
+ import { LLMClient } from '../llmClient.js';
6
+ import { routeCommand } from '../commands.js';
7
+
8
+ test('mapConcurrent limits active concurrency strictly to specified limit', async () => {
9
+ const items = [1, 2, 3, 4, 5, 6];
10
+ let activeWorkers = 0;
11
+ let maxObservedActive = 0;
12
+ const executionOrder = [];
13
+
14
+ const results = await mapConcurrent(items, 2, async (item) => {
15
+ activeWorkers++;
16
+ maxObservedActive = Math.max(maxObservedActive, activeWorkers);
17
+ await new Promise((r) => setTimeout(r, 25));
18
+ executionOrder.push(item);
19
+ activeWorkers--;
20
+ return item * 10;
21
+ });
22
+
23
+ assert.equal(maxObservedActive, 2, 'Max active concurrent workers must not exceed limit 2');
24
+ assert.equal(results.length, 6);
25
+ assert.deepEqual(results.map(r => r.value), [10, 20, 30, 40, 50, 60]);
26
+ });
27
+
28
+ test('mapConcurrent with limit 1 processes strictly sequentially for local models', async () => {
29
+ const items = ['task1', 'task2', 'task3'];
30
+ let activeWorkers = 0;
31
+ let maxObservedActive = 0;
32
+
33
+ const results = await mapConcurrent(items, 1, async (item) => {
34
+ activeWorkers++;
35
+ maxObservedActive = Math.max(maxObservedActive, activeWorkers);
36
+ await new Promise((r) => setTimeout(r, 15));
37
+ activeWorkers--;
38
+ return `${item}_done`;
39
+ });
40
+
41
+ assert.equal(maxObservedActive, 1, 'Local inference concurrency must stay strictly 1');
42
+ assert.deepEqual(results.map(r => r.value), ['task1_done', 'task2_done', 'task3_done']);
43
+ });
44
+
45
+ test('LLMClient retries on transient 500 error and recovers', async () => {
46
+ let callCount = 0;
47
+ const originalFetch = globalThis.fetch;
48
+
49
+ try {
50
+ globalThis.fetch = async () => {
51
+ callCount++;
52
+ if (callCount === 1) {
53
+ return {
54
+ ok: false,
55
+ status: 500,
56
+ text: async () => 'Internal Server Error',
57
+ };
58
+ }
59
+ return {
60
+ ok: true,
61
+ status: 200,
62
+ json: async () => ({
63
+ choices: [
64
+ {
65
+ message: {
66
+ role: 'assistant',
67
+ content: 'Recovered after retry',
68
+ },
69
+ },
70
+ ],
71
+ }),
72
+ };
73
+ };
74
+
75
+ const client = new LLMClient({
76
+ provider: 'openai',
77
+ apiKey: 'test-key',
78
+ model: 'gpt-4o-mini',
79
+ });
80
+
81
+ const response = await client.chat({
82
+ messages: [{ role: 'user', content: 'Hello' }],
83
+ stream: false,
84
+ });
85
+ assert.equal(response.content, 'Recovered after retry');
86
+ assert.equal(callCount, 2, 'Should have retried once after 500 error');
87
+ } finally {
88
+ globalThis.fetch = originalFetch;
89
+ }
90
+ });
91
+
92
+ test('Commands populate recordQueryDiagnostics for inspection via /details', async () => {
93
+ let recorded = null;
94
+ const ctx = {
95
+ sub: 'Ju Jingyi 2026',
96
+ agent: {
97
+ runSubagent: async (task) => ({
98
+ answer: 'Ju Jingyi is starring in upcoming 2026 drama.',
99
+ thinking: 'Gathering drama data.',
100
+ toolCalls: [{ name: 'search_catalog', args: { query: '2026' }, result: { count: 2 }, duration: 120 }],
101
+ iterations: 1,
102
+ }),
103
+ },
104
+ setIsProcessing: () => {},
105
+ recordQueryDiagnostics: (details) => {
106
+ recorded = details;
107
+ },
108
+ };
109
+
110
+ const res = await routeCommand('/subagent Ju Jingyi 2026', ctx);
111
+ assert.equal(res, 'HANDLED');
112
+ assert.ok(recorded, 'Diagnostics should have been recorded for subagent run');
113
+ assert.equal(recorded.prompt, 'Ju Jingyi 2026');
114
+ assert.equal(recorded.answer, 'Ju Jingyi is starring in upcoming 2026 drama.');
115
+ assert.equal(recorded.tools.length, 1);
116
+ assert.equal(recorded.tools[0].name, 'search_catalog');
117
+ });
@@ -0,0 +1,32 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { RuntimeWatchdog } from '../telemetry/runtimeWatchdog.js';
4
+
5
+ test('RuntimeWatchdog: samples telemetry and tracks spillover thresholds', async () => {
6
+ const watchdog = new RuntimeWatchdog({ spilloverThresholdMb: 10.0 });
7
+ const sample = await watchdog.sampleTelemetry();
8
+
9
+ assert.ok(typeof sample.sharedMemoryUsedMb === 'number');
10
+ assert.ok(typeof sample.hasSpillover === 'boolean');
11
+ assert.equal(watchdog.metricsHistory.length, 1);
12
+ });
13
+
14
+ test('RuntimeWatchdog: triggers self-healing context eviction of 50% older messages', () => {
15
+ const watchdog = new RuntimeWatchdog();
16
+ const fakeAgent = {
17
+ conversationHistory: [
18
+ { role: 'system', content: 'system persona' },
19
+ { role: 'user', content: 'q1' },
20
+ { role: 'assistant', content: 'a1' },
21
+ { role: 'user', content: 'q2' },
22
+ { role: 'assistant', content: 'a2' },
23
+ { role: 'user', content: 'q3' },
24
+ { role: 'assistant', content: 'a3' },
25
+ ]
26
+ };
27
+
28
+ const evicted = watchdog.selfHealContext(fakeAgent);
29
+ assert.ok(evicted >= 3);
30
+ assert.equal(fakeAgent.conversationHistory[0].role, 'system');
31
+ assert.ok(fakeAgent.conversationHistory.some(m => m.content?.includes('WATCHDOG SELF-HEALING')));
32
+ });
@@ -0,0 +1,37 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import path from 'node:path';
4
+
5
+ import { sanitizeFilePath, sanitizeToolArgs, sanitizeUrl } from '../sanitizer.js';
6
+
7
+ const projectRoot = path.resolve('C:/workspace/project');
8
+
9
+ test('dangerous file extensions are rejected case-insensitively', () => {
10
+ const executable = sanitizeFilePath('scripts/launch.EXE', projectRoot);
11
+ const script = sanitizeFilePath('scripts/setup.sh', projectRoot);
12
+ const text = sanitizeFilePath('notes/readme.txt', projectRoot);
13
+
14
+ assert.equal(executable.safe, false);
15
+ assert.match(executable.reason, /dangerous file extension/i);
16
+ assert.equal(script.safe, false);
17
+ assert.equal(text.safe, true);
18
+ });
19
+
20
+ test('directory validation does not reject a directory name that resembles a script', () => {
21
+ const result = sanitizeToolArgs('list_directory', { path: 'fixtures.js' });
22
+ assert.equal(result.safe, true);
23
+ });
24
+
25
+ test('IPv6 loopback and private literals are rejected after bracket normalization', () => {
26
+ for (const url of [
27
+ 'http://[::1]/admin',
28
+ 'http://[0:0:0:0:0:0:0:1]/admin',
29
+ 'http://[::ffff:127.0.0.1]/admin',
30
+ 'http://[fd00::1]/admin',
31
+ ]) {
32
+ const result = sanitizeUrl(url);
33
+ assert.equal(result.safe, false, `${url} should be blocked`);
34
+ }
35
+
36
+ assert.equal(sanitizeUrl('https://[2606:4700:4700::1111]/').safe, true);
37
+ });