kiku-agent 1.3.0 → 1.3.2

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 (155) hide show
  1. package/README.md +7 -7
  2. package/package.json +4 -7
  3. package/server/agent/agentCore.js +2 -2
  4. package/server/agent/catalogEntityResolver.js +8 -0
  5. package/server/agent/cli.js +4 -0
  6. package/server/agent/eval/AGENTS.md +228 -0
  7. package/server/agent/eval/analyze.py +684 -0
  8. package/server/agent/eval/gates.json +15 -0
  9. package/server/agent/eval/modules/analysis/compare.js +198 -0
  10. package/server/agent/eval/modules/analysis/gates.js +72 -0
  11. package/server/agent/eval/modules/analysis/judge.js +70 -0
  12. package/server/agent/eval/modules/core/cli.js +453 -0
  13. package/server/agent/eval/modules/core/config.js +60 -0
  14. package/server/agent/eval/modules/core/store.js +236 -0
  15. package/server/agent/eval/modules/evaluation/cases.js +689 -0
  16. package/server/agent/eval/modules/evaluation/runner.js +298 -0
  17. package/server/agent/eval/modules/evaluation/scoring.js +351 -0
  18. package/server/agent/eval/modules/evaluation/worker.js +122 -0
  19. package/server/agent/eval/modules/reporting/benchmark.js +265 -0
  20. package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
  21. package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
  22. package/server/agent/eval/modules/server/evalApi.js +1099 -0
  23. package/server/agent/eval/modules/ui/evalUi.js +4796 -0
  24. package/server/agent/eval/modules/utils/smokes.js +119 -0
  25. package/server/agent/ink/App.jsx +348 -0
  26. package/server/agent/ink/Panel.jsx +54 -0
  27. package/server/agent/ink/PromptInput.jsx +321 -0
  28. package/server/agent/ink/TaskPlanPanel.jsx +88 -0
  29. package/server/agent/ink/ThinkingPanel.jsx +68 -0
  30. package/server/agent/ink/ToolResult.jsx +133 -0
  31. package/server/agent/ink/main.jsx +195 -0
  32. package/server/agent/ink/useEventBus.js +218 -0
  33. package/server/agent/memory/contextAllocator.js +214 -0
  34. package/server/agent/memory/hierarchicalStore.js +523 -0
  35. package/server/agent/memory/reflectionWorker.js +110 -0
  36. package/server/agent/prompts.js +1 -1
  37. package/server/agent/telemetry/hardwareTiers.js +332 -0
  38. package/server/agent/telemetry/runtimeWatchdog.js +66 -0
  39. package/server/agent/telemetry/techPowerUpClient.js +682 -0
  40. package/server/agent/telemetry/techpowerup-mcp.json +22 -0
  41. package/server/agent/tests/agentBubbleSystem.test.js +65 -0
  42. package/server/agent/tests/agentFollowUps.test.js +127 -0
  43. package/server/agent/tests/apiRouter.test.js +108 -0
  44. package/server/agent/tests/architectureFixes.test.js +81 -0
  45. package/server/agent/tests/architectureRefactoring.test.js +194 -0
  46. package/server/agent/tests/autoResearch.test.js +64 -0
  47. package/server/agent/tests/bilibiliEmbed.test.js +251 -0
  48. package/server/agent/tests/catalogSync.test.js +130 -0
  49. package/server/agent/tests/citations.test.js +180 -0
  50. package/server/agent/tests/clarificationAnalytics.test.js +219 -0
  51. package/server/agent/tests/clarifyingQuestions.test.js +594 -0
  52. package/server/agent/tests/cliAutomation.test.js +121 -0
  53. package/server/agent/tests/cloudProviders.test.js +234 -0
  54. package/server/agent/tests/commandInput.test.js +24 -0
  55. package/server/agent/tests/commands.test.js +417 -0
  56. package/server/agent/tests/contextIntelligence.test.js +162 -0
  57. package/server/agent/tests/conversationSafety.test.js +53 -0
  58. package/server/agent/tests/decisionAndSearchEnhancements.test.js +135 -0
  59. package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
  60. package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
  61. package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
  62. package/server/agent/tests/edgeCasesScenarios.test.js +49 -0
  63. package/server/agent/tests/entertainmentMetrics.test.js +126 -0
  64. package/server/agent/tests/envBootstrap.test.js +87 -0
  65. package/server/agent/tests/envCommand.test.js +215 -0
  66. package/server/agent/tests/errorHandling.test.js +80 -0
  67. package/server/agent/tests/errors.test.js +13 -0
  68. package/server/agent/tests/evalAnalyze.test.js +54 -0
  69. package/server/agent/tests/evalCommand.test.js +54 -0
  70. package/server/agent/tests/evalCorpus.test.js +126 -0
  71. package/server/agent/tests/evalHarness.test.js +62 -0
  72. package/server/agent/tests/evalJudgeContext.test.js +38 -0
  73. package/server/agent/tests/evalMetrics.test.js +312 -0
  74. package/server/agent/tests/evalStore.test.js +190 -0
  75. package/server/agent/tests/evalStudio.test.js +179 -0
  76. package/server/agent/tests/feedbackLearning.test.js +186 -0
  77. package/server/agent/tests/firstRun.test.js +44 -0
  78. package/server/agent/tests/gemma4eOptimization.test.js +95 -0
  79. package/server/agent/tests/hardwareTiers.test.js +206 -0
  80. package/server/agent/tests/hybridWebSearch.test.js +192 -0
  81. package/server/agent/tests/judgeService.test.js +251 -0
  82. package/server/agent/tests/lanDiscovery.test.js +319 -0
  83. package/server/agent/tests/mcpClient.test.js +201 -0
  84. package/server/agent/tests/modelManager.test.js +109 -0
  85. package/server/agent/tests/multilingualScenarios.test.js +69 -0
  86. package/server/agent/tests/newCommands.test.js +347 -0
  87. package/server/agent/tests/newCommandsTui.test.js +141 -0
  88. package/server/agent/tests/newTools.test.js +113 -0
  89. package/server/agent/tests/newsIntelligence.test.js +178 -0
  90. package/server/agent/tests/nextGenArchitecture.test.js +99 -0
  91. package/server/agent/tests/offlineInput.test.js +217 -0
  92. package/server/agent/tests/packagingGuard.test.js +57 -0
  93. package/server/agent/tests/presentation.test.js +61 -0
  94. package/server/agent/tests/providerPersistence.test.js +39 -0
  95. package/server/agent/tests/queryBenchmarks.test.js +83 -0
  96. package/server/agent/tests/queryDetails.test.js +126 -0
  97. package/server/agent/tests/realWorldValidation.test.js +391 -0
  98. package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
  99. package/server/agent/tests/runtimeWatchdog.test.js +32 -0
  100. package/server/agent/tests/sanitizer.test.js +37 -0
  101. package/server/agent/tests/scenariosData.js +378 -0
  102. package/server/agent/tests/schemaRouting.test.js +54 -0
  103. package/server/agent/tests/securityAuditRemediation.test.js +214 -0
  104. package/server/agent/tests/securityHardening.test.js +146 -0
  105. package/server/agent/tests/securitySanitizer.test.js +38 -0
  106. package/server/agent/tests/sessionAndCopy.test.js +232 -0
  107. package/server/agent/tests/sidebarControls.test.js +101 -0
  108. package/server/agent/tests/statefulMemory.test.js +270 -0
  109. package/server/agent/tests/subagent.test.js +216 -0
  110. package/server/agent/tests/subagentProgress.test.js +109 -0
  111. package/server/agent/tests/subagentTextFallback.test.js +115 -0
  112. package/server/agent/tests/suggestions.test.js +199 -0
  113. package/server/agent/tests/suggestionsTui.test.js +251 -0
  114. package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
  115. package/server/agent/tests/terminalImage.test.js +80 -0
  116. package/server/agent/tests/terminalMarkdown.test.js +166 -0
  117. package/server/agent/tests/terminalText.test.js +133 -0
  118. package/server/agent/tests/textProtocolLoop.test.js +126 -0
  119. package/server/agent/tests/textToolParsing.test.js +139 -0
  120. package/server/agent/tests/theme.test.js +26 -0
  121. package/server/agent/tests/toolRegistryParity.test.js +53 -0
  122. package/server/agent/tests/toolRoutingRegressions.test.js +119 -0
  123. package/server/agent/tests/tools.test.js +259 -0
  124. package/server/agent/tests/unifiedEndpoints.test.js +152 -0
  125. package/server/agent/tests/v110Features.test.js +262 -0
  126. package/server/agent/tests/versionBump.test.js +69 -0
  127. package/server/agent/tests/vramEstimator.test.js +520 -0
  128. package/server/agent/tests/webChatInspection.test.js +300 -0
  129. package/server/agent/tests/webChatMemory.test.js +112 -0
  130. package/server/agent/tests/webChatSecurity.test.js +169 -0
  131. package/server/agent/tests/webExtract.test.js +261 -0
  132. package/server/agent/tests/webSources.test.js +247 -0
  133. package/server/agent/websearch/adapters/duckduckgo.js +97 -0
  134. package/server/agent/websearch/adapters/jina.js +77 -0
  135. package/server/agent/websearch/adapters/searxng.js +57 -0
  136. package/server/agent/websearch/core/canonical.js +89 -0
  137. package/server/agent/websearch/core/fusion.js +81 -0
  138. package/server/agent/websearch/core/types.js +29 -0
  139. package/server/agent/websearch/extract.js +605 -0
  140. package/server/agent/websearch/index.js +43 -0
  141. package/server/agent/websearch/orchestrator.js +266 -0
  142. package/server/agent/websearch/resilience/breaker.js +97 -0
  143. package/server/agent/websearch/resilience/cache.js +59 -0
  144. package/server/agent/websearch/resilience/limiter.js +93 -0
  145. package/server/agent/websearch/security/filter.js +36 -0
  146. package/server/agent/websearch/security/ssrf.js +289 -0
  147. package/server/agent/websearch/telemetry.js +71 -0
  148. package/server/apiRouter.js +7 -11
  149. package/server/eval/judgeRubrics.js +110 -0
  150. package/server/eval/judgeService.js +290 -0
  151. package/server/eval/runBenchmark.js +182 -0
  152. package/server/mcp/examples/librechat.example.yaml +16 -0
  153. package/server/mcp/tests/connection.test.js +156 -0
  154. package/server/routes/agent.js +55 -0
  155. package/server/supabase_schema.sql +63 -0
@@ -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,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
+ });