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,551 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { readFileSync } from 'node:fs';
4
+ import { getWebChatHtml } from '../../webChatHtml.js';
5
+ import { executeDirectCatalogKnowledge, makeInspectableToolPayload } from '../../webChatServer.js';
6
+
7
+ test('web chat tool payloads redact secrets and bound large values', () => {
8
+ const payload = makeInspectableToolPayload({
9
+ query: 'In Blossom',
10
+ apiKey: 'must-not-leak',
11
+ nested: { token: 'must-not-leak-either' },
12
+ values: Array.from({ length: 5 }, (_, index) => index)
13
+ }, { maxArrayItems: 2 });
14
+
15
+ assert.equal(payload.query, 'In Blossom');
16
+ assert.equal(payload.apiKey, '···');
17
+ assert.equal(payload.nested.token, '···');
18
+ assert.deepEqual(payload.values, [0, 1, '… ย่ออีก 3 รายการ']);
19
+ });
20
+
21
+ test('direct catalog fallback retains inspectable tool input and result', async () => {
22
+ const response = await executeDirectCatalogKnowledge('ผลงานซีรีส์ In Blossom');
23
+ const tool = response.tools.find(item => item.name === 'search_works');
24
+
25
+ assert.ok(tool);
26
+ assert.equal(typeof tool.input, 'object');
27
+ assert.equal(typeof tool.result, 'object');
28
+ assert.ok(Number.isFinite(tool.durationMs));
29
+ });
30
+
31
+ test('agent bare URLs render as clickable _blank links without double-wrapping', () => {
32
+ const src = readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8');
33
+ const start = src.indexOf('function linkifyBareUrls(segment)');
34
+ assert.ok(start >= 0);
35
+ let depth = 0;
36
+ let end = -1;
37
+ for (let i = src.indexOf('{', start); i < src.length; i++) {
38
+ if (src[i] === '{') depth++;
39
+ else if (src[i] === '}') {
40
+ depth--;
41
+ if (depth === 0) {
42
+ end = i + 1;
43
+ break;
44
+ }
45
+ }
46
+ }
47
+ assert.ok(end > start);
48
+ const escapeHtml = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
49
+ const linkifyBareUrls = new Function('escapeHtml', src.slice(start, end) + '; return linkifyBareUrls;')(escapeHtml);
50
+
51
+ const linked = linkifyBareUrls('ชมได้ที่ https://www.youtube.com/watch?v=OMLpNyp8T7Q จบ.');
52
+ assert.match(linked, /<a href="https:\/\/www\.youtube\.com\/watch\?v=OMLpNyp8T7Q" target="_blank" rel="noopener noreferrer" class="md-link">/);
53
+
54
+ const intact = linkifyBareUrls('<a href="https://a.test">https://a.test</a>');
55
+ assert.equal((intact.match(/<a /g) || []).length, 1);
56
+
57
+ assert.equal(linkifyBareUrls('no link here'), 'no link here');
58
+ });
59
+
60
+ test('web chat loads KaTeX assets for math typesetting', () => {
61
+ const html = getWebChatHtml();
62
+ assert.match(html, /katex@0\.16\.11\/dist\/katex\.min\.css/);
63
+ assert.match(html, /katex@0\.16\.11\/dist\/katex\.min\.js/);
64
+ assert.match(html, /contrib\/auto-render\.min\.js/);
65
+ assert.match(html, /function typesetMathInBubble\(/);
66
+ assert.match(html, /renderMathInElement/);
67
+ assert.match(html, /KIKU_CLIENT_BUILD/);
68
+ });
69
+
70
+ test('math segments are shielded from markdown and restored as KaTeX targets', () => {
71
+ const src = readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8');
72
+ const loadFn = (name) => {
73
+ const start = src.indexOf('function ' + name + '(');
74
+ assert.ok(start >= 0, name + ' must exist');
75
+ let depth = 0;
76
+ let end = -1;
77
+ for (let i = src.indexOf('{', start); i < src.length; i++) {
78
+ if (src[i] === '{') depth++;
79
+ else if (src[i] === '}') {
80
+ depth--;
81
+ if (depth === 0) {
82
+ end = i + 1;
83
+ break;
84
+ }
85
+ }
86
+ }
87
+ assert.ok(end > start);
88
+ return new Function(src.slice(start, end) + '; return ' + name + ';')();
89
+ };
90
+ const extractMathSegments = loadFn('extractMathSegments');
91
+ const restoreMathSegments = loadFn('restoreMathSegments');
92
+
93
+ const store = [];
94
+ const shielded = extractMathSegments('โจทย์ $\\lim_{x \\to 0} a * b$ และ $$x^2$$ จบ ส่วน `$y$` ในโค้ดไม่แตะ', store);
95
+ assert.equal(store.length, 2);
96
+ assert.ok(shielded.includes('§§§MATH0§§§'));
97
+ assert.ok(shielded.includes('§§§MATH1§§§'));
98
+ assert.ok(shielded.includes('`$y$`'), 'math inside inline code must stay untouched');
99
+ assert.ok(!shielded.includes('\\lim'), 'raw math must not leak into markdown processing');
100
+
101
+ const restored = restoreMathSegments(shielded, store);
102
+ assert.match(restored, /<span class="math-inline">\$\\lim/);
103
+ assert.match(restored, /<div class="math-display">\$\$x\^2\$\$<\/div>/);
104
+
105
+ assert.equal(extractMathSegments('ราคา $100 บาท', []), 'ราคา $100 บาท');
106
+ assert.equal(extractMathSegments('no math here', []).includes('§§§MATH'), false);
107
+ });
108
+
109
+ test('latexToUnicode fallback covers calculus symbols without KaTeX', () => {
110
+ const src = readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8');
111
+ const loadBlock = (prefix) => {
112
+ const start = src.indexOf(prefix);
113
+ assert.ok(start >= 0, prefix + ' must exist');
114
+ let depth = 0;
115
+ let end = -1;
116
+ for (let i = src.indexOf('{', start); i < src.length; i++) {
117
+ if (src[i] === '{') depth++;
118
+ else if (src[i] === '}') {
119
+ depth--;
120
+ if (depth === 0) {
121
+ end = i + 1;
122
+ break;
123
+ }
124
+ }
125
+ }
126
+ assert.ok(end > start);
127
+ return src.slice(start, end);
128
+ };
129
+ const sandbox = loadBlock('const LATEX_SYMBOL_MAP =') + '\n' +
130
+ loadBlock('const LATEX_SUP_MAP =') + '\n' +
131
+ loadBlock('const LATEX_SUB_MAP =') + '\n' +
132
+ src.slice(src.indexOf('function latexReadBrace('), src.indexOf('function latexMapChars(')) + '\n' +
133
+ src.slice(src.indexOf('function latexMapChars('), src.indexOf('function latexToUnicode(')) + '\n' +
134
+ src.slice(src.indexOf('function latexToUnicode('), src.indexOf('window.latexToUnicode')) +
135
+ '\nreturn latexToUnicode;';
136
+ const latexToUnicode = new Function(sandbox)();
137
+ assert.equal(latexToUnicode('\\frac{1}{2}'), '(1)/(2)');
138
+ assert.equal(latexToUnicode('L = \\frac{1}{\\sqrt{e}}'), 'L = (1)/(√(e))');
139
+ assert.equal(latexToUnicode('1^\\infty'), '1^∞');
140
+ assert.equal(latexToUnicode('\\lim_{x \\to 0} \\cos x'), 'lim_(x → 0) cos x');
141
+ assert.equal(latexToUnicode('x_0 + x_{10}'), 'x₀ + x₁₀');
142
+ assert.match(readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8'), /fallbackTypesetMath\(bubble\)/);
143
+ });
144
+
145
+ test('latexToUnicode fallback covers advanced math: matrices, accents, aliases', () => {
146
+ const src = readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8');
147
+ const sandbox = src.slice(src.indexOf('const LATEX_SYMBOL_MAP ='), src.indexOf('window.latexToUnicode')) +
148
+ '\nreturn latexToUnicode;';
149
+ const latexToUnicode = new Function(sandbox)();
150
+ assert.equal(latexToUnicode('\\begin{matrix}a&b\\\\c&d\\end{matrix}'), 'a b c d');
151
+ assert.equal(latexToUnicode('\\binom{n}{k}'), '(n choose k)');
152
+ assert.equal(latexToUnicode('\\hat{\\theta}+\\vec{v}+\\bar{z}+\\dot{x}'), 'θ+v+z+x');
153
+ assert.equal(latexToUnicode('x\\le 0, y\\ge 1, a\\ne b'), 'x≤ 0, y≥ 1, a≠ b');
154
+ assert.equal(latexToUnicode('\\tag{1} x=1'), '(1) x=1');
155
+ assert.equal(latexToUnicode('\\sqrt{\\frac{1}{2}}'), '√((1)/(2))');
156
+ assert.equal(latexToUnicode('\\dfrac{a}{b}'), '(a)/(b)');
157
+ assert.equal(latexToUnicode('\\boxed{42} + \\cancel{x}'), '[42] + x');
158
+ assert.equal(latexToUnicode('\\text{อนันต์} + \\mathrm{d}x'), 'อนันต์ + dx');
159
+ assert.equal(latexToUnicode('x \\in \\mathbb{R}^n'), 'x ∈ ℝⁿ');
160
+ const noRaw = (s) => !/[\\][a-zA-Z]+/.test(s) && !/[{}]/.test(s);
161
+ for (const tex of [
162
+ '\\sum_{n=1}^{\\infty} n + \\prod_{i=1}^n i + \\int_0^1 x\\,dx',
163
+ '\\forall x \\exists y: x \\subset y \\cup z \\approx w',
164
+ '\\alpha\\beta\\gamma\\Delta\\Omega + \\clubsuit\\sharp\\flat',
165
+ '\\left( \\frac{a}{b} \\right) + \\big| x \\big| + A \\overset{!}{=} B',
166
+ ]) {
167
+ assert.ok(noRaw(latexToUnicode(tex)), 'raw latex left in: ' + latexToUnicode(tex));
168
+ }
169
+ });
170
+
171
+ test('math extractor shields bare display environments and auto-render knows them', () => {
172
+ const src = readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8');
173
+ const loadFn = (name) => {
174
+ const start = src.indexOf('function ' + name + '(');
175
+ assert.ok(start >= 0, name + ' must exist');
176
+ const endMarker = name === 'extractMathSegments' ? 'function restoreMathSegments(' : 'const LATEX_SYMBOL_MAP =';
177
+ return new Function(src.slice(start, src.indexOf(endMarker)) + '; return ' + name + ';')();
178
+ };
179
+ const extractMathSegments = loadFn('extractMathSegments');
180
+ const restoreMathSegments = loadFn('restoreMathSegments');
181
+ const store = [];
182
+ const shielded = extractMathSegments('ผลคือ\n\\begin{align}x &= 1 \\\\ y &= 2\\end{align}\nจบ', store);
183
+ assert.equal(store.length, 1);
184
+ assert.equal(store[0].display, true);
185
+ assert.ok(shielded.includes('§§§MATH0§§§'));
186
+ const restored = restoreMathSegments(shielded, store);
187
+ assert.match(restored, /<div class="math-display">\\begin\{align\}/);
188
+ const html = getWebChatHtml();
189
+ for (const env of ['equation', 'align', 'gather', 'multline']) {
190
+ assert.ok(html.includes('\\\\begin{' + env + '}'), 'auto-render must know ' + env);
191
+ }
192
+ });
193
+
194
+ test('web chat HTML isolates answer text and renders expandable tool details', () => {
195
+ const html = getWebChatHtml();
196
+
197
+ assert.match(html, /class="answer-content"/);
198
+ assert.match(html, /class="tool-inspector"/);
199
+
200
+ assert.match(html, /renderToolParamChips\(t\)/);
201
+ assert.match(html, /summarizeToolResult\(t\)/);
202
+ assert.match(html, /tool-call-line/);
203
+ assert.match(html, /tool-result-strip tool-tone-/);
204
+ assert.match(html, /tool-param-chip/);
205
+ assert.match(html, /function renderJsonTree\(/);
206
+ assert.match(html, /function copyToolJson\(/);
207
+ assert.match(html, /ข้อมูลดิบ \(JSON\)/);
208
+ assert.match(html, /\.tool-raw-json\s*\{[^}]*max-width:\s*100%/);
209
+ assert.match(html, /\.tool-raw-json\s*\{[^}]*box-sizing:\s*border-box/);
210
+ assert.match(html, /\.tool-raw-json\s*\{[^}]*overflow:\s*hidden/);
211
+ assert.match(html, /\.tool-json-tree\s*\{[^}]*word-break:\s*break-word/);
212
+ assert.match(html, /\.tool-view-switch-row\s*\{[^}]*flex-wrap:\s*wrap/);
213
+
214
+ assert.doesNotMatch(html, /tool-raw-json[^\n]*106\.38/);
215
+ assert.doesNotMatch(html, /\.tool-inspector:has\(\.tool-raw-json\[open\]\)\s*\{\s*overflow:\s*visible/);
216
+
217
+ assert.doesNotMatch(html, /class="tool-payload"/);
218
+ assert.doesNotMatch(html, /toggleInspectorJsonView/);
219
+ assert.match(html, /extractQuoteFromAnswer\(answerRoot\)/);
220
+ assert.match(html, /restoreInspectorState\(bubble/);
221
+ assert.match(html, /--tool-accent:\s*#0369a1/);
222
+ assert.match(html, /sessionId:\s*currentSessionId/);
223
+ assert.match(html, /chatHistory\.slice\(0, -1\)\.slice\(-24\)/);
224
+ assert.doesNotMatch(html, /Deep Reasoning Trace/);
225
+ });
226
+
227
+ test('generated web chat inline scripts remain syntactically valid', () => {
228
+ const html = getWebChatHtml();
229
+ const scripts = Array.from(html.matchAll(/<script(?:\s[^>]*)?>([\s\S]*?)<\/script>/gi), match => match[1]);
230
+
231
+ assert.ok(scripts.length > 0);
232
+ scripts.forEach((script) => {
233
+ assert.doesNotThrow(() => new Function(script));
234
+ });
235
+ });
236
+
237
+ test('drama cards use matching OST video ids and hide unavailable tracks', () => {
238
+ const html = getWebChatHtml();
239
+
240
+ assert.match(html, /youtubeId: 'oEgxqij58LQ',[\s\S]*?ostTitle: '叹云兮 \(Tan Yun Xi\) OST'/);
241
+ assert.match(html, /youtubeId: '-vf6jU96WI4',[\s\S]*?ostTitle: '千年等一回 \(A Millennium Wait\)'/);
242
+ assert.match(html, /youtubeId: '9ke3efb9U7A',[\s\S]*?ostTitle: '芙蓉 \(Fu Rong\) OST'/);
243
+ assert.match(html, /youtubeId: 'lzDP2MfP3eA',[\s\S]*?ostTitle: '如纱 \(Ru Sha\) OST'/);
244
+ assert.match(html, /drama\.youtubeId && drama\.ostTitle/);
245
+ assert.doesNotMatch(html, /youtubeId: 'qjA7K9pM_U8'/);
246
+ });
247
+
248
+ test('web chat html registers sw.js and sw.js is accessible', () => {
249
+ const html = getWebChatHtml();
250
+ assert.match(html, /navigator\.serviceWorker\.register\('\/sw\.js'\)/);
251
+ });
252
+
253
+ test('floating mini player contains disc dock, progress bar, video toggle, and jukebox controls', () => {
254
+ const html = getWebChatHtml();
255
+ assert.match(html, /id="mini-player-dock"/);
256
+ assert.match(html, /id="mini-player-card"/);
257
+ assert.match(html, /id="btn-mini-disc"/);
258
+ assert.match(html, /id="mini-disc-progress-circle"/);
259
+ assert.match(html, /id="mini-player-progressbar"/);
260
+ assert.match(html, /onclick="toggleMiniPlayerShuffle\(\)"/);
261
+ assert.match(html, /onclick="toggleMiniPlayerRepeat\(\)"/);
262
+ assert.match(html, /onclick="toggleMiniPlayerVideo\(\)"/);
263
+ assert.match(html, /id="mini-player-video-frame-container"/);
264
+ assert.match(html, /onclick="nextMiniPlayerTrack\(\)"/);
265
+ assert.match(html, /onclick="prevMiniPlayerTrack\(\)"/);
266
+ assert.match(html, /JU_JINGYI_PLAYLIST/);
267
+ });
268
+
269
+ test('renderFollowUpSuggestions creates intelligent contextual follow-up topic chips', () => {
270
+ const html = getWebChatHtml();
271
+ assert.match(html, /function renderFollowUpSuggestions\(botGroup, answerText, userQuery, serverSuggestions\)/);
272
+ assert.match(html, /follow-up-suggestions/);
273
+ assert.match(html, /typesetMathInBubble\(chip\)/, 'renderFollowUpSuggestions must typeset math in chips');
274
+ assert.match(html, /detectSpecializedOpenDomainFollowUps/, 'client must have specialized open-domain follow-up detector');
275
+ assert.match(html, /💎 บทบาท Versace/);
276
+ assert.match(html, /👗 แบรนด์อื่นๆ ทั้งหมด/);
277
+ assert.match(html, /🌸 ตัวตนกับรูปลักษณ์/);
278
+ assert.match(html, /🌿 เสน่ห์หานหยุนซี/);
279
+ });
280
+
281
+ test('song queries render embedded iframes and tool visual mini iframes', () => {
282
+ const html = getWebChatHtml();
283
+ assert.match(html, /function extractSongEmbedsFromContext/);
284
+ assert.match(html, /function renderSongEmbedCard/);
285
+ assert.match(html, /chat-rich-songs-container/);
286
+ assert.match(html, /chat-song-iframe/);
287
+ assert.match(html, /https:\/\/www\.youtube-nocookie\.com\/embed\//);
288
+ assert.match(html, /tool-card-iframe-wrap/);
289
+ assert.match(html, /tool-card-mini-iframe/);
290
+ });
291
+
292
+ test('agent interactive question cards detect questions and render action pills', () => {
293
+ const html = getWebChatHtml();
294
+ assert.match(html, /function detectAgentInteractiveQuestion/);
295
+ assert.match(html, /function renderAgentQuestionCard/);
296
+ assert.match(html, /agent-interactive-question-card/);
297
+ assert.match(html, /agent-q-btn btn-primary-pill/);
298
+ assert.match(html, /submitCustomAgentContinuation/);
299
+ });
300
+
301
+ test('luxury Voice HUD renders live orb, visualizer canvas, and direct action controls', () => { const html = getWebChatHtml();
302
+ assert.match(html, /id="voice-hud"/);
303
+ assert.match(html, /class="voice-live-orb"/);
304
+ assert.match(html, /id="voice-canvas"/);
305
+ assert.match(html, /id="voice-hud-text"/);
306
+ assert.match(html, /id="voice-lang-badge"/);
307
+ assert.match(html, /onclick="sendVoiceQueryDirectly\(\)"/);
308
+ assert.match(html, /onclick="cancelSpeechRecognition\(\)"/);
309
+ assert.match(html, /function startVoiceAudioVisualizer/);
310
+ assert.match(html, /function stopVoiceAudioVisualizer/);
311
+ });
312
+
313
+ test('song playback timestamp syncs and mutually pauses between chat embed and mini player', () => {
314
+ const html = getWebChatHtml();
315
+ assert.match(html, /function openMiniPlayerFromCard\(btnEl, videoIdOrUrl, title\)/);
316
+ assert.match(html, /function pauseAllChatSongIframes\(\)/);
317
+ assert.match(html, /function pauseMiniPlayer\(\)/);
318
+ assert.match(html, /function handleChatIframeLoad\(iframe\)/);
319
+ assert.match(html, /handleChatIframeLoad\(this\)/);
320
+ assert.match(html, /openMiniPlayerFromCard\(this/);
321
+ assert.match(html, /chatIframeTimes/);
322
+ assert.match(html, /&start=/);
323
+ assert.match(html, /sendMiniPlayerCommand\('seekTo'/);
324
+ });
325
+
326
+ test('message bubble actions work without disruptive toasts and modal elements exist', () => {
327
+ const html = getWebChatHtml();
328
+ assert.match(html, /id="quote-card-modal"/);
329
+ assert.match(html, /id="quote-canvas"/);
330
+ assert.match(html, /id="quote-text-editor"/);
331
+ assert.match(html, /id="share-card-modal"/);
332
+ assert.match(html, /function copyBubbleText\(btn\)/);
333
+ assert.match(html, /function fallbackCopy\(text\)/);
334
+ assert.match(html, /function regenerateMessage\(target\)/);
335
+ assert.match(html, /function makeQuoteCardFromBubble\(btn\)/);
336
+ assert.match(html, /function toggleBookmark\(btn\)/);
337
+ assert.match(html, /function forkConversation\(target\)/);
338
+ assert.match(html, /function openShareCardFromAction\(btn\)/);
339
+ assert.match(html, /btn\.closest\('\.msg-group'\)/);
340
+ assert.doesNotMatch(html, /function speakMessage/);
341
+ assert.doesNotMatch(html, /aria-label="อ่านออกเสียง"/);
342
+ assert.doesNotMatch(html, /showToast\('คัดลอกข้อความแล้ว'\)/);
343
+ assert.doesNotMatch(html, /showToast\('บันทึก Bookmark เรียบร้อย ⭐'\)/);
344
+ assert.doesNotMatch(html, /id="toast-container"/);
345
+ assert.doesNotMatch(html, /对话已导出/);
346
+ assert.doesNotMatch(html, /\.toast-container/);
347
+ assert.doesNotMatch(html, /\.toast-pill/);
348
+ });
349
+
350
+ test('follow-up suggestions persist and re-render upon page refresh and loadActiveSession', () => {
351
+ const html = getWebChatHtml();
352
+ assert.match(html, /function loadActiveSession\(\)/);
353
+ assert.match(html, /renderFollowUpSuggestions\(lastBotGroup, lastBotMessage\.text, userQ, serverSugg\)/);
354
+ assert.match(html, /serverSuggestions:\s*serverSuggestions\s*\|\|\s*\[\]/);
355
+ assert.match(html, /userQuery:\s*query/);
356
+ });
357
+
358
+ test('web-chat displays Sponsor Spotlight ads with sidebar card, modal, and /ads in-chat card', () => {
359
+ const html = getWebChatHtml();
360
+ assert.match(html, /id="sidebar-ad-card"/);
361
+ assert.match(html, /id="sponsors-modal"/);
362
+ assert.match(html, /id="sponsors-modal-body"/);
363
+ assert.match(html, /function renderSidebarAd\(\)/);
364
+ assert.match(html, /function cycleSidebarAd\(/);
365
+ assert.match(html, /function openSponsorsModal\(/);
366
+ assert.match(html, /function showInChatSponsorCard\(/);
367
+ assert.match(html, /modal\.classList\.add\('open'\)/);
368
+ assert.match(html, /body\.sidebar-collapsed\s+\.sidebar-sponsors\s*\{[^}]*display:\s*none\s*!important/);
369
+ assert.match(html, /class="sidebar-ad-cta"/);
370
+ assert.match(html, /class="sidebar-ad-direct-link"/);
371
+ assert.match(html, /width:\s*300px\s*!important/);
372
+ assert.match(html, /width:\s*310px;\s*max-width:\s*86vw/);
373
+ assert.match(html, /\.sponsors-modal-dialog/);
374
+ assert.match(html, /\.sponsor-url-code/);
375
+ assert.match(html, /DashSecurity/);
376
+ assert.match(html, /ThemeHackers/);
377
+ assert.match(html, /https:\/\/dashsecurity\.kpltgroup\.com\//);
378
+ assert.match(html, /https:\/\/jujingyi-kikuuu\.vercel\.app\//);
379
+ assert.match(html, /https:\/\/github\.com\/Themehackers\//);
380
+ assert.match(html, /lowerQuery === '\/ads'/);
381
+ assert.match(html, /cmd_ads/);
382
+ });
383
+
384
+ test('btn-quick-actions renders complete plus sign (+) icon with horizontal and vertical lines', () => {
385
+ const html = getWebChatHtml();
386
+ assert.match(html, /id="btn-quick-actions"/);
387
+ assert.match(html, /<line x1="12" y1="5" x2="12" y2="19"\/><line x1="5" y1="12" x2="19" y2="12"\/>/);
388
+ });
389
+
390
+ test('user bubble renders dedicated renderUserContent with high contrast white text and Kiku rose theme', () => {
391
+ const html = getWebChatHtml();
392
+ assert.match(html, /function renderUserContent\(/);
393
+ assert.match(html, /user-content-body/);
394
+ assert.match(html, /--user-msg:\s*linear-gradient\(135deg,\s*#e11d48/);
395
+ assert.match(html, /\.bubble-user\s*,\s*\.bubble-user\s*\*\s*,\s*\.bubble-user\s*p/);
396
+ assert.match(html, /color:\s*#ffffff\s*!important/);
397
+ });
398
+
399
+ test('user bubble renders math formulas with typesetMathInBubble and dedicated KaTeX styles', () => {
400
+ const html = getWebChatHtml();
401
+ assert.match(html, /renderUserContent\(bubble,\s*text,\s*details\s*=\s*\{\}\)\s*\{[\s\S]*?typesetMathInBubble\(bubble\)/);
402
+ assert.match(html, /\.user-content-body\s*\.katex\s*,\s*\.bubble-user\s*\.katex/);
403
+ assert.match(html, /\.bubble-user\s*\.math-inline/);
404
+ assert.match(html, /\.bubble-user\s*\.math-display/);
405
+ });
406
+
407
+ test('clarifying questions and interactive question cards are stably pinned without stream flickering', () => {
408
+ const html = getWebChatHtml();
409
+ assert.match(html, /\.clarify-slot/);
410
+ assert.match(html, /clarifySlot\.className = 'clarify-slot'/);
411
+ assert.match(html, /msgGroup\.insertBefore\(clarifySlot,\s*botBubble(?:\.nextSibling)?\)/);
412
+ assert.match(html, /dismissAgentInteractiveQuestion/);
413
+ assert.match(html, /dismissedAgentQuestions/);
414
+ assert.match(html, /!details\.isStreaming/);
415
+ });
416
+
417
+ test('subagent progress renders a dedicated live card with evidence report', () => {
418
+ const html = getWebChatHtml();
419
+ assert.match(html, /function accumulateSubagentProgress\(/);
420
+ assert.match(html, /function finalizeSubagentFromToolResult\(/);
421
+ assert.match(html, /function renderSubagentCards\(/);
422
+ assert.match(html, /event\.type === 'subagent_progress'/);
423
+ assert.match(html, /subagentsAccum/);
424
+ assert.match(html, /subagents: subagentsAccum/);
425
+ assert.match(html, /class="subagents-container"/);
426
+ assert.match(html, /subagent-card/);
427
+ assert.match(html, /subagent-report/);
428
+ assert.match(html, /\.subagent-card\s*\{/);
429
+ assert.match(html, /subagent-pulse/);
430
+ assert.match(html, /run_deep_research: '▸/);
431
+ });
432
+
433
+ test('error handling renders dedicated retry error card and interactive retry badge', () => {
434
+ const html = getWebChatHtml();
435
+ assert.match(html, /function renderErrorCardWithRetry\(/);
436
+ assert.match(html, /class="chat-error-card"/);
437
+ assert.match(html, /class="btn-retry-chat"/);
438
+ assert.match(html, /onclick="regenerateMessage\(this\)"/);
439
+ assert.match(html, /data-retry-query=/);
440
+ assert.match(html, /badge\.dataset\.status = status;/);
441
+ assert.match(html, /if \(status === STATUS\.ERROR\)/);
442
+ assert.match(html, /globalScope\.regenerateMessage\(group\)/);
443
+ assert.match(html, /\.agent-status-badge\[data-status='error'\]\s*\{/);
444
+ assert.match(html, /\.btn-retry-chat\s*\{/);
445
+ assert.match(html, /btn-retry-chat:hover/);
446
+ assert.match(html, /bubble button:not\(\.btn-retry-chat\)/);
447
+ });
448
+
449
+ test('incomplete answer renders styled resume bar with continue button and CSS styles', () => {
450
+ const html = getWebChatHtml();
451
+ assert.match(html, /resumeWrap\.className = 'resume-incomplete-bar'/);
452
+ assert.match(html, /class="resume-incomplete-info"/);
453
+ assert.match(html, /class="resume-incomplete-badge"/);
454
+ assert.match(html, /class="resume-incomplete-title"/);
455
+ assert.match(html, /class="btn-resume-continue btn-mini-ctrl"/);
456
+ assert.match(html, /resumeIncompleteAnswer\(group, m\)/);
457
+ assert.match(html, /\.resume-incomplete-bar\s*\{/);
458
+ assert.match(html, /\.btn-resume-continue/);
459
+ assert.match(html, /\.btn-mini-ctrl/);
460
+ assert.match(html, /resumeBarArrive/);
461
+ });
462
+ test('input box enhancements: slash commands menu, model chip, relocated attachment tray, and quick action icons', () => {
463
+ const html = getWebChatHtml();
464
+ assert.match(html, /id="slash-commands-menu"/);
465
+ assert.match(html, /class="slash-commands-menu"/);
466
+ assert.match(html, /id="input-model-chip"/);
467
+ assert.match(html, /id="input-model-chip-label"/);
468
+ assert.match(html, /id="input-box-container"[\s\S]*?id="attachment-tray"/);
469
+ assert.match(html, /btn-send-muted/);
470
+ assert.match(html, /SLASH_COMMANDS/);
471
+ assert.match(html, /syncInputModelChip/);
472
+ assert.match(html, /updateSendButtonState/);
473
+ assert.match(html, /drag-over/);
474
+ });
475
+
476
+ test('file upload system: attachments are validated, sent to /api/chat, and rendered with lightbox preview', () => {
477
+ const html = getWebChatHtml();
478
+ assert.match(html, /function safeRasterDataUrl\(/);
479
+ assert.match(html, /function addAttachmentFile\(/);
480
+ assert.match(html, /function renderAttachmentTray\(/);
481
+ assert.match(html, /function handleImageUpload\(/);
482
+ assert.match(html, /chat-attached-images-wrap/);
483
+ assert.match(html, /chat-attached-image/);
484
+ assert.match(html, /openImageLightbox/);
485
+ assert.match(html, /attachments:\s*attachmentsToSend/);
486
+ assert.match(html, /svg\+xml/);
487
+ assert.match(html, /avif/);
488
+ });
489
+
490
+ test('welcome hero gallery image picker and drag-and-drop system exist and are wired', () => {
491
+ const html = getWebChatHtml();
492
+ assert.match(html, /id="gallery-picker-modal"/);
493
+ assert.match(html, /id="gallery-picker-grid"/);
494
+ assert.match(html, /id="gallery-picker-search"/);
495
+ assert.match(html, /data-hero-slot="primary"/);
496
+ assert.match(html, /data-hero-slot="secondary"/);
497
+ assert.match(html, /data-hero-slot="poster"/);
498
+ assert.match(html, /function openHeroGalleryPicker\(/);
499
+ assert.match(html, /function closeHeroGalleryPicker\(/);
500
+ assert.match(html, /function filterGalleryPickerCategory\(/);
501
+ assert.match(html, /function handleGallerySearchInput\(/);
502
+ assert.match(html, /function applyHeroImage\(/);
503
+ assert.match(html, /function resetHeroImages\(/);
504
+ assert.match(html, /function scoreGalleryItem\(/);
505
+ assert.match(html, /function initWelcomeHeroGallery\(/);
506
+ assert.match(html, /kiku_hero_images/);
507
+ assert.match(html, /application\/x-kiku-hero-img/);
508
+ assert.match(html, /\.gallery-picker-dialog/);
509
+ assert.match(html, /\.gallery-picker-grid/);
510
+ assert.match(html, /\.hero-media-frame \.hero-slot-action/);
511
+ });
512
+
513
+ test('all gallery images exist on disk and staticRoute serves them without 404', async () => {
514
+ const { handleApiRequest } = await import('../../apiRouter.js');
515
+ const { handleStaticRoute } = await import('../../web-chat/routes/staticRoute.js');
516
+ const stream = await import('node:stream');
517
+
518
+ let body = '';
519
+ const req = {
520
+ method: 'GET',
521
+ url: '/api/v1/gallery?scope=all&limit=200',
522
+ headers: { host: 'localhost:3000' }
523
+ };
524
+ const res = {
525
+ writeHead: () => {},
526
+ end: (chunk) => { if (chunk) body += chunk; },
527
+ setHeader: () => {}
528
+ };
529
+ await handleApiRequest(req, res);
530
+ const json = JSON.parse(body);
531
+ assert.ok(Array.isArray(json.data) && json.data.length >= 70);
532
+
533
+ for (const item of json.data) {
534
+ const encodedUrl = encodeURI(item.image);
535
+ const mockReq = { url: encodedUrl };
536
+ let handledCode = 0;
537
+ const mockRes = new stream.Writable({
538
+ write(chunk, encoding, callback) { callback(); }
539
+ });
540
+ mockRes.writeHead = (code) => { handledCode = code; };
541
+ const u = new URL(mockReq.url, 'http://localhost:3002');
542
+ const handled = handleStaticRoute(mockReq, mockRes, u.pathname, 3002);
543
+ assert.strictEqual(handled, true, `Route must handle image ${item.image}`);
544
+ assert.strictEqual(handledCode, 200, `Image must return 200 OK for ${item.image}`);
545
+ }
546
+
547
+ const html = getWebChatHtml();
548
+ assert.match(html, /onerror="this\.onerror=null;const it=this\.closest/);
549
+ });
550
+
551
+
@@ -0,0 +1,94 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import http from 'node:http';
4
+ import { handleWebChatRequest } from '../../web-chat/router.js';
5
+
6
+ test('Web Chat MCP: GET /mcp returns protocol discovery JSON', async () => {
7
+ const server = http.createServer(async (req, res) => {
8
+ await handleWebChatRequest(req, res, { port: 3002 });
9
+ });
10
+
11
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
12
+ const port = server.address().port;
13
+
14
+ try {
15
+ const res = await fetch(`http://127.0.0.1:${port}/mcp`);
16
+ assert.equal(res.status, 200);
17
+ const data = await res.json();
18
+ assert.equal(data.protocol, 'mcp');
19
+ assert.equal(data.name, 'kiku');
20
+ assert.ok(data.capabilities?.toolsCount > 0);
21
+ } finally {
22
+ server.close();
23
+ }
24
+ });
25
+
26
+ test('Web Chat MCP: GET /api/mcp/status returns tools ecosystem and configs', async () => {
27
+ const server = http.createServer(async (req, res) => {
28
+ await handleWebChatRequest(req, res, { port: 3002 });
29
+ });
30
+
31
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
32
+ const port = server.address().port;
33
+
34
+ try {
35
+ const res = await fetch(`http://127.0.0.1:${port}/api/mcp/status`);
36
+ assert.equal(res.status, 200);
37
+ const data = await res.json();
38
+ assert.equal(data.status, 'ok');
39
+ assert.equal(data.mcpServer?.protocolVersion, '2025-11-25');
40
+ assert.ok(data.mcpServer?.supportedProtocolVersions?.includes('2024-11-05'));
41
+ assert.ok(data.mcpServer?.toolsCount >= 60);
42
+ assert.ok(data.totalTools >= 68);
43
+ assert.ok(data.categories?.ALL >= 68);
44
+ assert.ok(data.categories?.CATALOG > 0);
45
+ assert.ok(data.categories?.RESEARCH > 0);
46
+ assert.ok(Array.isArray(data.tools));
47
+ assert.ok(data.tools.length >= 68);
48
+ assert.ok(data.mcpServer?.clientConfigs?.claudeDesktop);
49
+ assert.ok(data.mcpServer?.clientConfigs?.cursor);
50
+ assert.ok(data.mcpServer?.clientConfigs?.windsurf);
51
+ assert.ok(data.mcpServer?.clientConfigs?.vscode);
52
+ assert.ok(data.mcpServer?.clientConfigs?.antigravity);
53
+ assert.ok(data.mcpServer?.clientConfigs?.cline);
54
+ assert.ok(data.mcpServer?.clientConfigs?.claudeCode);
55
+ assert.ok(data.mcpServer?.clientConfigs?.geminiCli);
56
+ assert.ok(data.mcpServer?.clientConfigs?.librechat);
57
+ assert.ok(data.mcpServer?.clientConfigs?.opencode);
58
+
59
+
60
+ assert.ok(data.mcpServer.clientConfigs.antigravity.http?.mcpServers?.kiku?.url);
61
+ assert.ok(data.mcpServer.clientConfigs.antigravity.stdio?.mcpServers?.kiku?.command);
62
+ assert.ok(data.mcpServer.clientConfigs.vscode.http?.servers?.kiku?.url);
63
+ assert.ok(data.mcpServer.clientConfigs.librechat.httpYaml?.includes('streamable-http'));
64
+ assert.ok(data.mcpServer.clientConfigs.opencode.http?.$schema);
65
+
66
+ const profileTool = data.tools.find(t => t.name === 'get_profile_and_fandom');
67
+ assert.ok(profileTool);
68
+ assert.equal(profileTool.category, 'CATALOG');
69
+ assert.equal(profileTool.isMcpExposed, true);
70
+ } finally {
71
+ server.close();
72
+ }
73
+ });
74
+
75
+ test('Web Chat MCP: GET /api/status includes mcp server capability', async () => {
76
+ const server = http.createServer(async (req, res) => {
77
+ await handleWebChatRequest(req, res, { port: 3002 });
78
+ });
79
+
80
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
81
+ const port = server.address().port;
82
+
83
+ try {
84
+ const res = await fetch(`http://127.0.0.1:${port}/api/status?light=1`);
85
+ assert.equal(res.status, 200);
86
+ const data = await res.json();
87
+ assert.equal(data.status, 'ok');
88
+ assert.ok(data.mcp);
89
+ assert.equal(data.mcp.endpoint, '/mcp');
90
+ assert.equal(data.mcp.online, true);
91
+ } finally {
92
+ server.close();
93
+ }
94
+ });