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,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
+ });
@@ -0,0 +1,378 @@
1
+ export const TEST_SCENARIOS = Object.freeze([
2
+ {
3
+ id: 'drama_2026_veil_of_shadows',
4
+ category: 'works',
5
+ language: 'th',
6
+ query: 'จวีจิ้งอี มีผลงานซีรีส์เรื่องอะไรบ้างในปี 2026',
7
+ expectedTools: ['search_works', 'get_work_detail'],
8
+ expectedKeywords: ['Veil of Shadows', 'The Eternal Fragrance', 'Wanhua Sin', '2026']
9
+ },
10
+ {
11
+ id: 'drama_eternal_fragrance_role',
12
+ category: 'works',
13
+ language: 'th',
14
+ query: 'ซีรีส์ พันกลิ่นรัญจวน (The Eternal Fragrance / 千香) จวีจิ้งอี รับบทเป็นใคร คู่กับใคร',
15
+ expectedTools: ['search_works', 'get_work_detail', 'get_character_roles'],
16
+ expectedKeywords: ['The Eternal Fragrance', '千香', 'Song Song', 'Jiang Li']
17
+ },
18
+ {
19
+ id: 'drama_wanhua_sin',
20
+ category: 'works',
21
+ language: 'zh',
22
+ query: '鞠婧祎新剧《万华世界》(Wanhua Sin) 的题材和角色',
23
+ expectedTools: ['search_works', 'get_work_detail'],
24
+ expectedKeywords: ['Wanhua Sin', '万华世界']
25
+ },
26
+ {
27
+ id: 'drama_in_blossom_detail',
28
+ category: 'works',
29
+ language: 'th',
30
+ query: 'เล่าเรื่องย่อและบทบาทของจวีจิ้งอีในเรื่อง ฮวาเจียนลิ่ง (In Blossom) ให้ฟังหน่อย',
31
+ expectedTools: ['search_works', 'get_work_detail', 'get_character_roles'],
32
+ expectedKeywords: ['In Blossom', 'Yang Caiwei', 'Shangguan Zhi', 'ฮวาเจียนลิ่ง']
33
+ },
34
+ {
35
+ id: 'drama_legend_of_yunxi',
36
+ category: 'works',
37
+ language: 'zh',
38
+ query: '鞠婧祎在《芸汐传》中饰演什么角色?',
39
+ expectedTools: ['search_works', 'get_character_roles'],
40
+ expectedKeywords: ['韩芸汐', '芸汐传', 'Legend of Yun Xi']
41
+ },
42
+ {
43
+ id: 'drama_white_snake_en',
44
+ category: 'works',
45
+ language: 'en',
46
+ query: 'Tell me about Ju Jingyi role in The Legend of White Snake 2019',
47
+ expectedTools: ['search_works', 'get_work_detail'],
48
+ expectedKeywords: ['Bai Suzhen', 'White Snake', '2019']
49
+ },
50
+ {
51
+ id: 'drama_sword_fairy_4',
52
+ category: 'works',
53
+ language: 'th',
54
+ query: 'ซีรีส์ เซียนกระบี่พิชิตมาร 4 (Sword and Fairy 4) จวีจิ้งอี รับบทเป็นใคร',
55
+ expectedTools: ['search_works', 'get_character_roles'],
56
+ expectedKeywords: ['Han Lingsha', 'หานหลิงซา', 'Sword and Fairy 4']
57
+ },
58
+ {
59
+ id: 'drama_ruyi_pavilion',
60
+ category: 'works',
61
+ language: 'th',
62
+ query: 'ซีรีส์ กรุ่นรักกลิ่นบุปผา (The Blooms at Ruyi Pavilion) จวีจิ้งอี แสดงร่วมกับใครและรับบทอะไร',
63
+ expectedTools: ['search_works', 'get_work_detail'],
64
+ expectedKeywords: ['Fu Rong', 'Ruyi Pavilion', '如意芳霏']
65
+ },
66
+ {
67
+ id: 'drama_rebirth_for_you',
68
+ category: 'works',
69
+ language: 'th',
70
+ query: 'เจียหนานจ้วน (Rebirth For You) จวีจิ้งอี รับบทเป็นท่านหญิงชื่ออะไร',
71
+ expectedTools: ['search_works', 'get_character_roles'],
72
+ expectedKeywords: ['Jiang Baoning', 'Rebirth For You', '嘉南传']
73
+ },
74
+ {
75
+ id: 'drama_pair_of_wings',
76
+ category: 'works',
77
+ language: 'zh',
78
+ query: '民国悬疑剧《请赐我一双翅膀》鞠婧祎饰演的角色叫什么',
79
+ expectedTools: ['search_works', 'get_character_roles'],
80
+ expectedKeywords: ['林九歌', 'Lin Jiuge', 'Wings']
81
+ },
82
+ {
83
+ id: 'drama_love_full_moon',
84
+ category: 'works',
85
+ language: 'en',
86
+ query: 'What is the plot of modern fantasy romance drama Love Under the Full Moon starring Ju Jingyi?',
87
+ expectedTools: ['search_works', 'get_work_detail'],
88
+ expectedKeywords: ['Lei Chuxia', 'Full Moon', 'Love Under the Full Moon']
89
+ },
90
+ {
91
+ id: 'drama_novoland_castle',
92
+ category: 'works',
93
+ language: 'th',
94
+ query: 'จวีจิ้งอี เคยเล่นเรื่อง ตำนานอาณาจักรเหนือเวหา (Novoland: The Castle in the Sky) รับบทเป็นใคร',
95
+ expectedTools: ['search_works', 'get_character_roles'],
96
+ expectedKeywords: ['Xue Feishuang', 'Novoland', 'องค์หญิงหิมะ']
97
+ },
98
+ {
99
+ id: 'music_ruyi_ost',
100
+ category: 'music',
101
+ language: 'th',
102
+ query: 'เพลงประกอบซีรีส์ กรุ่นรักกลิ่นบุปผา (The Blooms at Ruyi Pavilion) มีเพลงอะไรบ้าง',
103
+ expectedTools: ['search_discography', 'get_media_links'],
104
+ expectedKeywords: ['Fu Rong', 'Ruyi Pavilion', '如意芳霏']
105
+ },
106
+ {
107
+ id: 'music_ginkgo_play',
108
+ category: 'music',
109
+ language: 'th',
110
+ query: 'เปิดเพลง Rain of Ginkgo (银杏树下的雨) ให้หน่อย',
111
+ expectedTools: ['play_music', 'search_discography'],
112
+ expectedKeywords: ['Rain of Ginkgo', '银杏树下的雨']
113
+ },
114
+ {
115
+ id: 'music_solo_singles_en',
116
+ category: 'music',
117
+ language: 'en',
118
+ query: 'What are the top solo singles released by Ju Jingyi?',
119
+ expectedTools: ['search_discography', 'get_music_videos'],
120
+ expectedKeywords: ['Every Day', 'Rain of Ginkgo', 'Don\'t Touch', 'Single']
121
+ },
122
+ {
123
+ id: 'music_luohua_chengni',
124
+ category: 'music',
125
+ language: 'zh',
126
+ query: '《落花成泥》是哪部电视剧的主题曲?',
127
+ expectedTools: ['search_discography', 'get_work_detail'],
128
+ expectedKeywords: ['芸汐传', 'Legend of Yun Xi', '落花成泥']
129
+ },
130
+ {
131
+ id: 'music_qiannian_deng_yihui',
132
+ category: 'music',
133
+ language: 'th',
134
+ query: 'เพลง พันปีรอคอยหนึ่งหน (千年等一回) จากเรื่อง นางพญางูขาว จวีจิ้งอี ร้องเวอร์ชันไหน',
135
+ expectedTools: ['search_discography', 'get_media_links'],
136
+ expectedKeywords: ['千年等一回', 'White Snake', 'OST']
137
+ },
138
+ {
139
+ id: 'music_dont_touch_mv',
140
+ category: 'music',
141
+ language: 'en',
142
+ query: 'Tell me about Ju Jingyi iconic dance track Don\'t Touch and its music video',
143
+ expectedTools: ['search_discography', 'get_music_videos'],
144
+ expectedKeywords: ['Don\'t Touch', 'Dance', 'MV']
145
+ },
146
+ {
147
+ id: 'music_past_perfection',
148
+ category: 'music',
149
+ language: 'th',
150
+ query: 'เพลง Past Perfection (过去完成时) ของจวีจิ้งอี ปล่อยออกมาปีไหนและมีความหมายอย่างไร',
151
+ expectedTools: ['search_discography', 'get_media_links'],
152
+ expectedKeywords: ['过去完成时', 'Past Perfection']
153
+ },
154
+ {
155
+ id: 'music_stage_performances',
156
+ category: 'music',
157
+ language: 'th',
158
+ query: 'ขอดูคลิปการแสดงและ Music Video สเตจงาน Gala ของจวีจิ้งอี',
159
+ expectedTools: ['get_music_videos', 'get_media_links'],
160
+ expectedKeywords: ['Official MV', 'Stage Performance']
161
+ },
162
+ {
163
+ id: 'brand_global_ambassador_2026',
164
+ category: 'brand',
165
+ language: 'th',
166
+ query: 'จวีจิ้งอี เป็นแบรนด์แอมบาสเดอร์ระดับโลก (Global Ambassador) ของแบรนด์ใดบ้าง',
167
+ expectedTools: ['get_brand_endorsements'],
168
+ expectedKeywords: ['Global', 'Ambassador', 'Brand']
169
+ },
170
+ {
171
+ id: 'brand_versace_fragrance',
172
+ category: 'brand',
173
+ language: 'en',
174
+ query: 'When did Ju Jingyi become the Global Fragrance Ambassador for Versace?',
175
+ expectedTools: ['get_brand_endorsements'],
176
+ expectedKeywords: ['Versace', 'Fragrance', 'Global Ambassador']
177
+ },
178
+ {
179
+ id: 'brand_makeup_forever',
180
+ category: 'brand',
181
+ language: 'zh',
182
+ query: '鞠婧祎代言的法国彩妆品牌 MAKE UP FOR EVER 是什么级别的代言?',
183
+ expectedTools: ['get_brand_endorsements'],
184
+ expectedKeywords: ['MAKE UP FOR EVER', 'Ambassador', 'Brand']
185
+ },
186
+ {
187
+ id: 'brand_beauty_fashion_en',
188
+ category: 'brand',
189
+ language: 'en',
190
+ query: 'Which cosmetics and luxury fashion brands does Ju Jingyi endorse?',
191
+ expectedTools: ['get_brand_endorsements'],
192
+ expectedKeywords: ['Ambassador', 'Brand']
193
+ },
194
+ {
195
+ id: 'brand_ysl_mistine',
196
+ category: 'brand',
197
+ language: 'th',
198
+ query: 'แบรนด์ความงามและเครื่องสำอางที่จวีจิ้งอีร่วมงาน เช่น YSL หรือ Mistine มีอะไรบ้าง',
199
+ expectedTools: ['get_brand_endorsements'],
200
+ expectedKeywords: ['Mistine', 'Beauty', 'Brand']
201
+ },
202
+ {
203
+ id: 'brand_fashion_week_magazines',
204
+ category: 'brand',
205
+ language: 'th',
206
+ query: 'นิตยสารแฟชั่นชั้นนำที่จวีจิ้งอีเคยขึ้นปก เช่น Harper\'s Bazaar, Elle, L\'Officiel',
207
+ expectedTools: ['get_gallery_photos', 'get_brand_endorsements'],
208
+ expectedKeywords: ['Fashion', 'Magazine', 'Cover']
209
+ },
210
+ {
211
+ id: 'awards_history_th',
212
+ category: 'awards',
213
+ language: 'th',
214
+ query: 'จวีจิ้งอี เคยได้รับรางวัลเกียรติยศอะไรบ้างตลอดเส้นทางอาชีพ',
215
+ expectedTools: ['get_awards'],
216
+ expectedKeywords: ['Award', 'Honor', 'Weibo', 'SNH48']
217
+ },
218
+ {
219
+ id: 'awards_snh48_hall_of_fame',
220
+ category: 'awards',
221
+ language: 'en',
222
+ query: 'How many times did Ju Jingyi win 1st place in SNH48 General Elections before entering the Hall of Fame?',
223
+ expectedTools: ['get_awards', 'get_career_timeline'],
224
+ expectedKeywords: ['SNH48', 'Hall of Fame', 'Senbatsu', '1st']
225
+ },
226
+ {
227
+ id: 'awards_weibo_night',
228
+ category: 'awards',
229
+ language: 'zh',
230
+ query: '鞠婧祎在微博之夜(Weibo Night)获得过哪些年度荣誉奖项?',
231
+ expectedTools: ['get_awards'],
232
+ expectedKeywords: ['微博之夜', 'Weibo Night', 'Award']
233
+ },
234
+ {
235
+ id: 'awards_music_starlight',
236
+ category: 'awards',
237
+ language: 'th',
238
+ query: 'จวีจิ้งอีเคยได้รับรางวัลด้านดนตรี เช่น รางวัล Tencent Starlight Awards หรือ รางวัลเพลงยอดเยี่ยม ERC Chinese Top Ten อะไรบ้าง',
239
+ expectedTools: ['get_awards'],
240
+ expectedKeywords: ['Starlight', 'ERC', 'OST', 'Singer']
241
+ },
242
+ {
243
+ id: 'profile_bio_fandom_th',
244
+ category: 'profile',
245
+ language: 'th',
246
+ query: 'จวีจิ้งอีเกิดวันที่เท่าไหร่ แฟนคลับชื่ออะไร และสโลแกนคืออะไร',
247
+ expectedTools: ['get_profile_and_fandom'],
248
+ expectedKeywords: ['18 มิถุนายน', 'Miju', '蜜橘', '鞠世无双']
249
+ },
250
+ {
251
+ id: 'profile_social_links_en',
252
+ category: 'profile',
253
+ language: 'en',
254
+ query: 'What are Ju Jingyi official social media accounts on Weibo and Instagram?',
255
+ expectedTools: ['get_profile_and_fandom', 'get_social_feed'],
256
+ expectedKeywords: ['weibo', 'instagram', 'social']
257
+ },
258
+ {
259
+ id: 'profile_hometown_specialties',
260
+ category: 'profile',
261
+ language: 'zh',
262
+ query: '鞠婧祎的出生地、特长和精通乐器有哪些?',
263
+ expectedTools: ['get_profile_and_fandom'],
264
+ expectedKeywords: ['四川遂宁', '小提琴', '古筝', 'Suining']
265
+ },
266
+ {
267
+ id: 'timeline_snh48_to_solo',
268
+ category: 'timeline',
269
+ language: 'th',
270
+ query: 'เส้นทางประวัติการทำงานของจวีจิ้งอี ตั้งแต่ SNH48 จนถึงศิลปินเดี่ยว',
271
+ expectedTools: ['get_career_timeline'],
272
+ expectedKeywords: ['2013', 'SNH48', 'Studio', 'Solo']
273
+ },
274
+ {
275
+ id: 'timeline_studio_independence',
276
+ category: 'timeline',
277
+ language: 'en',
278
+ query: 'When did Ju Jingyi establish her personal independent studio?',
279
+ expectedTools: ['get_career_timeline', 'get_profile_and_fandom'],
280
+ expectedKeywords: ['2017', 'Studio', 'Independence', 'Solo']
281
+ },
282
+ {
283
+ id: 'quotes_famous_sayings',
284
+ category: 'profile',
285
+ language: 'th',
286
+ query: 'ขอคำพูดหรือ Quote ให้กำลังใจเด่นๆ ของจวีจิ้งอี',
287
+ expectedTools: ['get_quotes'],
288
+ expectedKeywords: ['Quote', 'คำพูด']
289
+ },
290
+ {
291
+ id: 'verify_contract_dispute',
292
+ category: 'research',
293
+ language: 'th',
294
+ query: 'ตรวจสอบข้อเท็จจริงเกี่ยวกับข้อพิพาทสัญญาระหว่าง จวีจิ้งอี กับ Star48',
295
+ expectedTools: ['verify_claim', 'web_search', 'run_subagent'],
296
+ expectedKeywords: ['Star48', 'Siba', 'Contract', 'Dispute']
297
+ },
298
+ {
299
+ id: 'verify_4000_year_idol_origin',
300
+ category: 'research',
301
+ language: 'en',
302
+ query: 'Verify the truth behind the 4000-year idol nickname origin in Japanese media',
303
+ expectedTools: ['verify_claim', 'get_profile_and_fandom'],
304
+ expectedKeywords: ['4000-year', 'Japanese', 'Translation', 'Mistranslation']
305
+ },
306
+ {
307
+ id: 'research_legal_portrait_rights',
308
+ category: 'research',
309
+ language: 'zh',
310
+ query: '查证鞠婧祎肖像权维权和胜诉判决的相关公开信息',
311
+ expectedTools: ['verify_claim', 'web_search', 'run_deep_research'],
312
+ expectedKeywords: ['肖像权', '维权', '判决', 'Court']
313
+ },
314
+ {
315
+ id: 'research_deep_investigation_en',
316
+ category: 'research',
317
+ language: 'en',
318
+ query: 'Investigate the latest updates regarding Ju Jingyi upcoming drama premiere licenses in 2026',
319
+ expectedTools: ['run_deep_research', 'web_search', 'web_extract'],
320
+ expectedKeywords: ['License', 'Drama', 'Broadcast']
321
+ },
322
+ {
323
+ id: 'multilingual_mixed_thai_chinese',
324
+ category: 'works',
325
+ language: 'th',
326
+ query: 'ผลงานของ 鞠婧祎 (Ju Jingyi) เรื่อง ฮวาเจียนลิ่ง (花间令) ฉายทางแพลตฟอร์มไหนคะ',
327
+ expectedTools: ['search_works', 'get_work_detail'],
328
+ expectedKeywords: ['Youku', 'In Blossom', '花间令']
329
+ },
330
+ {
331
+ id: 'multilingual_pinyin_search',
332
+ category: 'works',
333
+ language: 'en',
334
+ query: 'Find the drama Hua Jian Ling and Yun Xi Zhuan starring Ju Jingyi',
335
+ expectedTools: ['search_works', 'global_search'],
336
+ expectedKeywords: ['In Blossom', 'Legend of Yun Xi']
337
+ },
338
+ {
339
+ id: 'reasoning_compare_dramas',
340
+ category: 'works',
341
+ language: 'th',
342
+ query: 'เปรียบเทียบบทบาทของจวีจิ้งอี ระหว่างเรื่อง ตำนานยุ่นซี (Legend of Yunxi) กับ ฮวาเจียนลิ่ง (In Blossom)',
343
+ expectedTools: ['search_works', 'get_work_detail', 'get_character_roles'],
344
+ expectedKeywords: ['Han Yunxi', 'Yang Caiwei', 'In Blossom', 'Yunxi']
345
+ },
346
+ {
347
+ id: 'reasoning_all_self_sung_osts',
348
+ category: 'music',
349
+ language: 'th',
350
+ query: 'รวมเพลง OST ทั้งหมดที่จวีจิ้งอีร้องให้กับซีรีส์ที่ตัวเองแสดงนำ',
351
+ expectedTools: ['search_discography', 'search_works'],
352
+ expectedKeywords: ['OST', 'Luohua Chengni', 'Fu Rong', 'Qiannian']
353
+ },
354
+ {
355
+ id: 'system_stats_check',
356
+ category: 'system',
357
+ language: 'th',
358
+ query: 'ดูสถิติระบบและจำนวนผลงานทั้งหมดของ Kiku',
359
+ expectedTools: ['get_system_stats'],
360
+ expectedKeywords: ['totalWorks', 'totalUniqueSongs']
361
+ },
362
+ {
363
+ id: 'system_skill_learn',
364
+ category: 'system',
365
+ language: 'th',
366
+ query: 'สอน Kiku ว่าให้ตอบภาษาไทยอย่างสุภาพและลงท้ายด้วยค่ะเสมอ',
367
+ expectedTools: ['manage_skill'],
368
+ expectedKeywords: ['skill', 'learn']
369
+ },
370
+ {
371
+ id: 'system_doctor_check',
372
+ category: 'system',
373
+ language: 'en',
374
+ query: 'Show system doctor status, LM Studio connectivity, and runtime paths',
375
+ expectedTools: ['get_system_stats'],
376
+ expectedKeywords: ['stats', 'system']
377
+ }
378
+ ]);
@@ -0,0 +1,54 @@
1
+ import { describe, it, beforeEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { RoutingInterceptor } from '../routingInterceptor.js';
4
+ import { compactToolsForPrompt } from '../contextCompactor.js';
5
+
6
+ describe('Sprint 3: Schema Routing & Misrouting Elimination', () => {
7
+ let interceptor;
8
+
9
+ beforeEach(() => {
10
+ interceptor = new RoutingInterceptor();
11
+ });
12
+
13
+ it('w13 regression: should prevent global_search on Han Lingsha costume query', () => {
14
+ const call = {
15
+ name: 'global_search',
16
+ params: { query: '仙剑四 韩菱纱 服装颜色' }
17
+ };
18
+ const result = interceptor.interceptToolCall(call, 0);
19
+
20
+ assert.equal(result.rerouted, true);
21
+ assert.equal(result.replacement.name, 'search_works');
22
+ });
23
+
24
+ it('m02 regression: should prevent global_search for View From The Wall Chinese title', () => {
25
+ const call = {
26
+ name: 'global_search',
27
+ params: { query: 'View From The Wall Chinese title' }
28
+ };
29
+ const result = interceptor.interceptToolCall(call, 0);
30
+
31
+ assert.equal(result.rerouted, true);
32
+ assert.equal(result.replacement.name, 'search_discography');
33
+ });
34
+
35
+ it('r15 regression: should detect and abort duplicate queries in multi-turn loop', () => {
36
+ const call = { name: 'search_works', params: { query: '七星彩' } };
37
+
38
+ const firstAttempt = interceptor.interceptToolCall(call, 0);
39
+ assert.equal(firstAttempt.valid, true);
40
+
41
+
42
+ const secondAttempt = interceptor.interceptToolCall(call, 1);
43
+ assert.equal(secondAttempt.aborted, true);
44
+ });
45
+
46
+ it('Dynamic Scoping: should isolate gallery tools for photoshoot queries', () => {
47
+ const query = 'According to the catalog, which jewelry collection from FRED was featured in PORT Magazine?';
48
+ const scoped = compactToolsForPrompt(query, { schemaStrategy: 'domain_grouped' });
49
+
50
+ const toolNames = scoped.map(t => t.schema?.name || t.function?.name || t.name);
51
+ assert.ok(toolNames.includes('get_gallery_photos'));
52
+ assert.equal(toolNames.includes('get_brand_endorsements'), false);
53
+ });
54
+ });