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,436 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import {
5
+ checkEngineCompatibility,
6
+ buildVllmServeArgs,
7
+ buildVllmExecutionPlan,
8
+ detectPlatformAndWsl,
9
+ calculateAutoTensorSplit,
10
+ } from '../engineService.js';
11
+ import {
12
+ MODEL_LOAD_DEFAULTS,
13
+ normalizeModelLoadSettings,
14
+ getActiveLocalEngine,
15
+ setActiveLocalEngine,
16
+ getPersistedModelLoadSettings,
17
+ savePersistedModelLoadSettings,
18
+ } from '../config.js';
19
+ import { routeCommand } from '../commands.js';
20
+
21
+ test('engine compatibility: LM Studio supports Windows, macOS, and Linux', () => {
22
+ const win = checkEngineCompatibility('lms', { platform: 'win32', hasWsl: false });
23
+ assert.equal(win.supported, true);
24
+ assert.equal(win.engine, 'lms');
25
+
26
+ const mac = checkEngineCompatibility('lms', { platform: 'darwin', hasWsl: false });
27
+ assert.equal(mac.supported, true);
28
+
29
+ const linux = checkEngineCompatibility('lms', { platform: 'linux', hasWsl: false });
30
+ assert.equal(linux.supported, true);
31
+ });
32
+
33
+ test('engine compatibility: vLLM is enterprise Linux only (supported on Linux and Windows via WSL2, rejected on macOS)', () => {
34
+ const linux = checkEngineCompatibility('vllm', { platform: 'linux', isWsl: false });
35
+ assert.equal(linux.supported, true);
36
+ assert.equal(linux.isEnterprise, true);
37
+
38
+ const winWithWsl = checkEngineCompatibility('vllm', { platform: 'win32', hasWsl: true });
39
+ assert.equal(winWithWsl.supported, true);
40
+ assert.equal(winWithWsl.viaWsl, true);
41
+ assert.equal(winWithWsl.isEnterprise, true);
42
+
43
+ const winNoWsl = checkEngineCompatibility('vllm', { platform: 'win32', hasWsl: false });
44
+ assert.equal(winNoWsl.supported, false);
45
+ assert.equal(winNoWsl.isEnterprise, true);
46
+ assert.match(winNoWsl.reason, /WSL2/i);
47
+
48
+ const mac = checkEngineCompatibility('vllm', { platform: 'darwin', hasWsl: false });
49
+ assert.equal(mac.supported, false);
50
+ assert.equal(mac.isEnterprise, true);
51
+ assert.match(mac.reason, /macOS/i);
52
+ });
53
+
54
+ test('buildVllmServeArgs constructs CLI options accurately with custom parameters', () => {
55
+ const args = buildVllmServeArgs('Qwen/Qwen2.5-7B-Instruct', {
56
+ port: 8000,
57
+ gpuMemoryUtilization: 0.85,
58
+ tensorParallelSize: 2,
59
+ maxModelLen: 8192,
60
+ dtype: 'bfloat16',
61
+ enforceEager: true,
62
+ enablePrefixCaching: true,
63
+ trustRemoteCode: true,
64
+ });
65
+
66
+ assert.ok(args.includes('serve'));
67
+ assert.ok(args.includes('Qwen/Qwen2.5-7B-Instruct'));
68
+ assert.ok(args.includes('--port'));
69
+ assert.ok(args.includes('8000'));
70
+ assert.ok(args.includes('--gpu-memory-utilization'));
71
+ assert.ok(args.includes('0.85'));
72
+ assert.ok(args.includes('--tensor-parallel-size'));
73
+ assert.ok(args.includes('2'));
74
+ assert.ok(args.includes('--max-model-len'));
75
+ assert.ok(args.includes('8192'));
76
+ assert.ok(args.includes('--dtype'));
77
+ assert.ok(args.includes('bfloat16'));
78
+ assert.ok(args.includes('--enforce-eager'));
79
+ assert.ok(args.includes('--enable-prefix-caching'));
80
+ assert.ok(args.includes('--trust-remote-code'));
81
+ });
82
+
83
+ test('buildVllmExecutionPlan formats command for Windows WSL2 or native Linux', () => {
84
+ const winPlan = buildVllmExecutionPlan('google/gemma-4-e4b', {
85
+ platform: 'win32',
86
+ distro: 'Ubuntu-26.04',
87
+ gpuMemoryUtilization: 0.9,
88
+ });
89
+ assert.equal(winPlan.isWsl, true);
90
+ assert.equal(winPlan.command, 'wsl.exe');
91
+ assert.ok(winPlan.args.includes('-d'));
92
+ assert.ok(winPlan.args.includes('Ubuntu-26.04'));
93
+ assert.ok(winPlan.displayCommand.includes('wsl -d Ubuntu-26.04'));
94
+
95
+ const linuxPlan = buildVllmExecutionPlan('google/gemma-4-e4b', {
96
+ platform: 'linux',
97
+ gpuMemoryUtilization: 0.9,
98
+ });
99
+ assert.equal(linuxPlan.isWsl, false);
100
+ assert.equal(linuxPlan.command, 'vllm');
101
+ assert.ok(linuxPlan.displayCommand.startsWith('vllm serve'));
102
+ });
103
+
104
+ test('config: normalizeModelLoadSettings and active engine management', () => {
105
+ const normalized = normalizeModelLoadSettings({
106
+ activeEngine: 'vllm',
107
+ gpuOffload: 'max',
108
+ cpuThreads: 6,
109
+ gpuMemoryUtilization: 0.88,
110
+ tensorParallelSize: 2,
111
+ });
112
+
113
+ assert.equal(normalized.activeEngine, 'vllm');
114
+ assert.equal(normalized.gpuOffload, 'max');
115
+ assert.equal(normalized.cpuThreads, 6);
116
+ assert.equal(normalized.vllm.gpuMemoryUtilization, 0.88);
117
+ assert.equal(normalized.vllm.tensorParallelSize, 2);
118
+
119
+ setActiveLocalEngine('vllm');
120
+ assert.equal(getActiveLocalEngine(), 'vllm');
121
+ setActiveLocalEngine('lms');
122
+ assert.equal(getActiveLocalEngine(), 'lms');
123
+ });
124
+
125
+ test('/model load handles parameter parsing and OS compatibility checks', async () => {
126
+ const logs = [];
127
+ const origLog = console.log;
128
+ console.log = (...args) => logs.push(args.join(' '));
129
+
130
+ const ctx = {
131
+ client: { provider: 'lmstudio', providerName: 'LM Studio' },
132
+ setIsProcessing: () => {},
133
+ };
134
+
135
+ try {
136
+
137
+ const resInvalid = await routeCommand('/model load test-model --engine unknown', ctx);
138
+ assert.equal(resInvalid, 'HANDLED');
139
+ assert.ok(logs.some(l => l.includes('Incompatible') || l.includes('UNKNOWN') || l.includes('unknown')));
140
+
141
+
142
+ savePersistedModelLoadSettings({ activeEngine: 'lms', gpuOffload: 'max', cpuThreads: 8 });
143
+ assert.equal(getActiveLocalEngine(), 'lms');
144
+ const persisted = getPersistedModelLoadSettings();
145
+ assert.equal(persisted.activeEngine, 'lms');
146
+ assert.equal(persisted.gpuOffload, 'max');
147
+ assert.equal(persisted.cpuThreads, 8);
148
+ } finally {
149
+ console.log = origLog;
150
+ }
151
+ });
152
+
153
+ test('/model engine handles inspection, compatibility, and switching', async () => {
154
+ const logs = [];
155
+ const origLog = console.log;
156
+ console.log = (...args) => logs.push(args.join(' '));
157
+
158
+ const ctx = {
159
+ client: {
160
+ provider: 'lmstudio',
161
+ providerName: 'LM Studio',
162
+ model: 'google/gemma-4-e4b',
163
+ switchProvider: (p, opt) => { ctx.client.provider = p; if (opt?.model) ctx.client.model = opt.model; },
164
+ },
165
+ setIsProcessing: () => {},
166
+ };
167
+
168
+ try {
169
+ const resInspect = await routeCommand('/model engine', ctx);
170
+ assert.equal(resInspect, 'HANDLED');
171
+
172
+ const resSwitchLms = await routeCommand('/model engine lms', ctx);
173
+ assert.equal(resSwitchLms, 'HANDLED');
174
+ assert.equal(getActiveLocalEngine(), 'lms');
175
+
176
+ const resInvalid = await routeCommand('/model engine invalidEngine123', ctx);
177
+ assert.equal(resInvalid, 'HANDLED');
178
+ } finally {
179
+ console.log = origLog;
180
+ }
181
+ });
182
+
183
+ test('/model config and /model params inspect, update, and reset parameters', async () => {
184
+ const logs = [];
185
+ const origLog = console.log;
186
+ console.log = (...args) => logs.push(args.join(' '));
187
+
188
+ const ctx = {
189
+ client: { provider: 'lmstudio', providerName: 'LM Studio' },
190
+ setIsProcessing: () => {},
191
+ };
192
+
193
+ try {
194
+ const resShow = await routeCommand('/model config', ctx);
195
+ assert.equal(resShow, 'HANDLED');
196
+
197
+ const resUpdate = await routeCommand('/model config --gpu max --threads 8 --mem-util 0.85 --enforce-eager --prefix-caching', ctx);
198
+ assert.equal(resUpdate, 'HANDLED');
199
+ const updated = getPersistedModelLoadSettings();
200
+ assert.equal(updated.gpuOffload, 'max');
201
+ assert.equal(updated.cpuThreads, 8);
202
+ assert.equal(updated.vllm.gpuMemoryUtilization, 0.85);
203
+ assert.equal(updated.vllm.enforceEager, true);
204
+ assert.equal(updated.vllm.enablePrefixCaching, true);
205
+
206
+ const resReset = await routeCommand('/model params --reset', ctx);
207
+ assert.equal(resReset, 'HANDLED');
208
+ const resetSettings = getPersistedModelLoadSettings();
209
+ assert.equal(resetSettings.gpuOffload, 'auto');
210
+ assert.equal(resetSettings.cpuThreads, 1);
211
+ assert.equal(resetSettings.contextLength, 32768);
212
+ } finally {
213
+ console.log = origLog;
214
+ }
215
+ });
216
+
217
+ test('/model preset lists curated presets and applies preset', async () => {
218
+ const logs = [];
219
+ const origLog = console.log;
220
+ console.log = (...args) => logs.push(args.join(' '));
221
+
222
+ let switchedProvider = null;
223
+ let switchedModel = null;
224
+ const ctx = {
225
+ client: {
226
+ provider: 'lmstudio',
227
+ providerName: 'LM Studio',
228
+ model: 'google/gemma-4-e4b',
229
+ switchProvider: (p, opt) => {
230
+ switchedProvider = p;
231
+ if (opt?.model) switchedModel = opt.model;
232
+ },
233
+ },
234
+ setIsProcessing: () => {},
235
+ };
236
+
237
+ try {
238
+ const resList = await routeCommand('/model preset', ctx);
239
+ assert.equal(resList, 'HANDLED');
240
+
241
+ const resApply = await routeCommand('/model preset gemma4', ctx);
242
+ assert.equal(resApply, 'HANDLED');
243
+ assert.equal(switchedProvider, 'lmstudio');
244
+ assert.equal(switchedModel, 'google/gemma-4-e4b');
245
+
246
+ const resUnknown = await routeCommand('/model preset unknown_xyz_preset', ctx);
247
+ assert.equal(resUnknown, 'HANDLED');
248
+ } finally {
249
+ console.log = origLog;
250
+ }
251
+ });
252
+
253
+ test('/model files discovers installed GGUF models', async () => {
254
+ const logs = [];
255
+ const origLog = console.log;
256
+ console.log = (...args) => logs.push(args.join(' '));
257
+
258
+ const ctx = {
259
+ client: { provider: 'lmstudio', providerName: 'LM Studio' },
260
+ setIsProcessing: () => {},
261
+ };
262
+
263
+ try {
264
+ const resFiles = await routeCommand('/model files', ctx);
265
+ assert.equal(resFiles, 'HANDLED');
266
+
267
+ const resGguf = await routeCommand('/model gguf', ctx);
268
+ assert.equal(resGguf, 'HANDLED');
269
+ } finally {
270
+ console.log = origLog;
271
+ }
272
+ });
273
+
274
+ test('/model inference parameters (temp, topp, ctx, thinking) update settings', async () => {
275
+ const logs = [];
276
+ const origLog = console.log;
277
+ console.log = (...args) => logs.push(args.join(' '));
278
+
279
+ const ctx = {
280
+ client: { provider: 'lmstudio', providerName: 'LM Studio' },
281
+ setIsProcessing: () => {},
282
+ };
283
+
284
+ try {
285
+ const resTemp = await routeCommand('/model temp 0.85', ctx);
286
+ assert.equal(resTemp, 'HANDLED');
287
+
288
+ const resTopp = await routeCommand('/model topp 0.90', ctx);
289
+ assert.equal(resTopp, 'HANDLED');
290
+
291
+ const resCtx = await routeCommand('/model ctx 32k', ctx);
292
+ assert.equal(resCtx, 'HANDLED');
293
+
294
+ const resThinking = await routeCommand('/model thinking collapse', ctx);
295
+ assert.equal(resThinking, 'HANDLED');
296
+
297
+ const { getPersistedInferenceSettings } = await import('../config.js');
298
+ const inf = getPersistedInferenceSettings();
299
+ assert.equal(inf.temperature, 0.85);
300
+ assert.equal(inf.topP, 0.90);
301
+ assert.equal(inf.contextLimit, 32768);
302
+ assert.equal(inf.thinking, 'collapse');
303
+ } finally {
304
+ console.log = origLog;
305
+ }
306
+ });
307
+
308
+ test('/model status renders enriched engine and inference diagnostics', async () => {
309
+ const logs = [];
310
+ const origLog = console.log;
311
+ console.log = (...args) => logs.push(args.join(' '));
312
+
313
+ const ctx = {
314
+ client: {
315
+ provider: 'lmstudio',
316
+ providerName: 'LM Studio',
317
+ model: 'google/gemma-4-e4b',
318
+ checkConnection: async () => ({ connected: true, latencyMs: 12, models: ['google/gemma-4-e4b'], activeLoadedModel: 'google/gemma-4-e4b' }),
319
+ },
320
+ setIsProcessing: () => {},
321
+ baseUrl: 'http://127.0.0.1:1234/v1',
322
+ };
323
+
324
+ try {
325
+ const resStatus = await routeCommand('/model status', ctx);
326
+ assert.equal(resStatus, 'HANDLED');
327
+ } finally {
328
+ console.log = origLog;
329
+ }
330
+ });
331
+
332
+ test('calculateAutoTensorSplit computes safe ratios across multiple GPUs with headroom deduction', () => {
333
+ const dualGpus = {
334
+ gpus: [
335
+ { index: 0, name: 'RTX 4090', freeGb: 23.5, totalGb: 24 },
336
+ { index: 1, name: 'RTX 4070', freeGb: 12.0, totalGb: 12 },
337
+ ]
338
+ };
339
+ const split = calculateAutoTensorSplit(dualGpus, 3.5, 0);
340
+ assert.equal(split.applicable, true);
341
+ assert.equal(split.tensorSplit, '20,12');
342
+ assert.deepEqual(split.ratios, [20, 12]);
343
+ assert.equal(String(split), '20,12');
344
+
345
+ const singleGpu = {
346
+ gpus: [{ index: 0, name: 'RTX 3060', freeGb: 12.0, totalGb: 12 }]
347
+ };
348
+ const singleSplit = calculateAutoTensorSplit(singleGpu);
349
+ assert.equal(singleSplit.applicable, false);
350
+ assert.equal(singleSplit.tensorSplit, '1');
351
+ });
352
+
353
+ test('/model minp and /model repeat update inference samplers', async () => {
354
+ const logs = [];
355
+ const origLog = console.log;
356
+ console.log = (...args) => logs.push(args.join(' '));
357
+
358
+ const ctx = {
359
+ client: { provider: 'lmstudio', providerName: 'LM Studio' },
360
+ setIsProcessing: () => {},
361
+ };
362
+
363
+ try {
364
+ const resMinP = await routeCommand('/model minp 0.08', ctx);
365
+ assert.equal(resMinP, 'HANDLED');
366
+
367
+ const resRepeat = await routeCommand('/model repeat 1.15', ctx);
368
+ assert.equal(resRepeat, 'HANDLED');
369
+
370
+ const { getPersistedInferenceSettings } = await import('../config.js');
371
+ const inf = getPersistedInferenceSettings();
372
+ assert.equal(inf.minP, 0.08);
373
+ assert.equal(inf.repeatPenalty, 1.15);
374
+ } finally {
375
+ console.log = origLog;
376
+ }
377
+ });
378
+
379
+ test('/model config parses multi-GPU, flash-attention, and batching parameters', async () => {
380
+ const logs = [];
381
+ const origLog = console.log;
382
+ console.log = (...args) => logs.push(args.join(' '));
383
+
384
+ const ctx = {
385
+ client: { provider: 'lmstudio', providerName: 'LM Studio' },
386
+ setIsProcessing: () => {},
387
+ };
388
+
389
+ try {
390
+ const res = await routeCommand('/model config --split-mode layer --tensor-split 20,12 --main-gpu 0 --fa -b 2048 -ub 512 --ctk q8_0', ctx);
391
+ assert.equal(res, 'HANDLED');
392
+
393
+ const settings = getPersistedModelLoadSettings();
394
+ assert.equal(settings.splitMode, 'layer');
395
+ assert.equal(settings.tensorSplit, '20,12');
396
+ assert.equal(settings.mainGpu, 0);
397
+ assert.equal(settings.flashAttn, true);
398
+ assert.equal(settings.batchSize, 2048);
399
+ assert.equal(settings.ubatchSize, 512);
400
+ assert.equal(settings.kvCacheDtype, 'q8_0');
401
+ } finally {
402
+ console.log = origLog;
403
+ }
404
+ });
405
+
406
+ test('LLMClient includes min_p and repeat_penalty in chat completion payload', async () => {
407
+ const { LLMClient } = await import('../llmClient.js');
408
+ const client = new LLMClient({ provider: 'lmstudio', baseUrl: 'http://127.0.0.1:1234/v1' });
409
+
410
+ let capturedBody = null;
411
+ const originalFetch = globalThis.fetch;
412
+ globalThis.fetch = async (url, options) => {
413
+ if (url.includes('/chat/completions')) {
414
+ capturedBody = JSON.parse(options.body);
415
+ return new Response(JSON.stringify({
416
+ id: 'chatcmpl-test',
417
+ choices: [{ message: { role: 'assistant', content: 'hello' } }]
418
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } });
419
+ }
420
+ return originalFetch(url, options);
421
+ };
422
+
423
+ try {
424
+ await client.chat([{ role: 'user', content: 'test' }], {
425
+ stream: false,
426
+ minP: 0.08,
427
+ repeatPenalty: 1.10,
428
+ });
429
+
430
+ assert.ok(capturedBody);
431
+ assert.equal(capturedBody.min_p, 0.08);
432
+ assert.equal(capturedBody.repeat_penalty, 1.10);
433
+ } finally {
434
+ globalThis.fetch = originalFetch;
435
+ }
436
+ });
@@ -0,0 +1,94 @@
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { unloadModelFromEngine } from '../engineService.js';
4
+ import { unloadLmStudioModel } from '../lmStudioWakeup.js';
5
+ import { handleEngineUnloadRoute } from '../../web-chat/routes/engineRoute.js';
6
+
7
+ describe('Local Inference Engine Unload Pipeline', () => {
8
+ test('unloadLmStudioModel handles unreachable LM Studio server gracefully', async () => {
9
+ const res = await unloadLmStudioModel('test-model', {
10
+ baseUrl: 'http://127.0.0.1:59999/v1',
11
+ timeoutMs: 500
12
+ });
13
+ assert.ok(typeof res.ok === 'boolean');
14
+ assert.equal(res.engine, 'lms');
15
+ assert.ok(res.message);
16
+ });
17
+
18
+ test('unloadModelFromEngine routes to lms engine gracefully', async () => {
19
+ const res = await unloadModelFromEngine('lms', 'google/gemma-4-e4b', {
20
+ baseUrl: 'http://127.0.0.1:59999/v1',
21
+ timeoutMs: 500
22
+ });
23
+ assert.ok(typeof res.ok === 'boolean');
24
+ assert.equal(res.engine, 'lms');
25
+ });
26
+
27
+ test('unloadModelFromEngine stops vllm engine background process', async () => {
28
+ const res = await unloadModelFromEngine('vllm', 'google/gemma-4-e4b');
29
+ assert.equal(res.ok, true);
30
+ assert.equal(res.engine, 'vllm');
31
+ assert.equal(res.unloaded, true);
32
+ assert.ok(res.message.includes('vLLM'));
33
+ });
34
+
35
+ test('unloadModelFromEngine rejects unknown engine gracefully', async () => {
36
+ const res = await unloadModelFromEngine('unknown_custom_engine', 'some-model');
37
+ assert.equal(res.ok, false);
38
+ assert.ok(res.message.includes('Unsupported engine'));
39
+ });
40
+
41
+ test('handleEngineUnloadRoute handles valid POST request and returns json', async () => {
42
+ const payloadStr = JSON.stringify({
43
+ engine: 'vllm',
44
+ model: 'google/gemma-4-e4b'
45
+ });
46
+
47
+ const mockReq = {
48
+ method: 'POST',
49
+ url: '/api/engine/unload',
50
+ headers: { 'content-type': 'application/json' },
51
+ socket: { remoteAddress: '127.0.0.1' },
52
+ on(event, handler) {
53
+ if (event === 'data') {
54
+ process.nextTick(() => handler(Buffer.from(payloadStr)));
55
+ } else if (event === 'end') {
56
+ process.nextTick(() => handler());
57
+ }
58
+ return this;
59
+ }
60
+ };
61
+
62
+ let statusCode = 0;
63
+ let headers = {};
64
+ let responseBody = '';
65
+
66
+ const mockRes = {
67
+ writeHead(code, head) {
68
+ statusCode = code;
69
+ headers = head || {};
70
+ },
71
+ end(payload) {
72
+ responseBody = payload;
73
+ }
74
+ };
75
+
76
+ await handleEngineUnloadRoute(mockReq, mockRes);
77
+
78
+ assert.equal(statusCode, 200);
79
+ assert.ok(headers['Content-Type']?.includes('application/json'));
80
+ const parsed = JSON.parse(responseBody);
81
+ assert.equal(parsed.status, 'ok');
82
+ assert.equal(parsed.engine, 'vllm');
83
+ assert.ok(parsed.message);
84
+ });
85
+
86
+ test('unloadLmStudioModel handles gguf filename and unloads matched loaded model instance', async () => {
87
+ const res = await unloadLmStudioModel('gemma-4-E4B-it-Q4_K_M.gguf', {
88
+ baseUrl: 'http://127.0.0.1:59999/v1',
89
+ timeoutMs: 300
90
+ });
91
+ assert.ok(typeof res.ok === 'boolean');
92
+ assert.equal(res.engine, 'lms');
93
+ });
94
+ });
@@ -0,0 +1,126 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { executeTool, toolHandlers } from '../tools.js';
5
+ import { getMcpToolNames, handleToolCall } from '../../mcp/tools.js';
6
+ import { readResource, resourceDefinitions } from '../../mcp/resources.js';
7
+
8
+ import * as thaiCatalog from '../../../src/data/industryIndexData.js';
9
+ import * as mcpCatalog from '../../mcp/data/industryIndex.js';
10
+
11
+ test('dual catalog integrity: src/data/industryIndexData.js and server/mcp/data/industryIndex.js share identical export keys', () => {
12
+ const thaiKeys = Object.keys(thaiCatalog).sort();
13
+ const mcpKeys = Object.keys(mcpCatalog).sort();
14
+
15
+ assert.deepEqual(thaiKeys, mcpKeys, 'Export keys must match across both catalog copies');
16
+ assert.ok(thaiCatalog.platformsCatalog.length === 9, 'Must contain all 9 third-party big data platforms');
17
+ assert.ok(mcpCatalog.platformsCatalog.length === 9, 'MCP catalog must contain all 9 platforms');
18
+
19
+ const thaiIds = thaiCatalog.platformsCatalog.map(p => p.id).sort();
20
+ const mcpIds = mcpCatalog.platformsCatalog.map(p => p.id).sort();
21
+ assert.deepEqual(thaiIds, mcpIds, 'Platform IDs must match 1:1 between Thai and MCP catalogs');
22
+
23
+ const expectedPlatforms = [
24
+ 'vlinkage', 'datawin', 'guduo',
25
+ 'yunhe', 'maoyan', 'dengta',
26
+ 'weibo', 'douban', 'xiaohongshu'
27
+ ].sort();
28
+ assert.deepEqual(thaiIds, expectedPlatforms, 'All 9 required platforms must be present');
29
+ });
30
+
31
+ test('get_entertainment_metrics returns all platforms by default with Ju Jingyi benchmarks', async () => {
32
+ const res = await executeTool('get_entertainment_metrics');
33
+ assert.equal(res.status, 'success');
34
+ assert.equal(res.totalPlatforms, 9);
35
+ assert.ok(Array.isArray(res.platforms));
36
+ assert.ok(res.platforms.length === 9);
37
+ assert.ok(Array.isArray(res.dramaBenchmarks));
38
+ assert.ok(res.dramaBenchmarks.length >= 4);
39
+ assert.ok(res.artistOverview);
40
+ assert.equal(res.artistOverview.nameCn, '鞠婧祎');
41
+ assert.ok(res.socialBuzzOverview);
42
+ });
43
+
44
+ test('get_entertainment_metrics filters by specific platform ID', async () => {
45
+ const resYunhe = await executeTool('get_entertainment_metrics', { platform: 'yunhe' });
46
+ assert.equal(resYunhe.status, 'success');
47
+ assert.equal(resYunhe.totalPlatforms, 1);
48
+ assert.equal(resYunhe.platforms[0].id, 'yunhe');
49
+ assert.equal(resYunhe.platforms[0].officialDomain, 'enlightent.cn');
50
+ assert.ok(resYunhe.platforms[0].industryRoleTh.includes('Effective Views') || resYunhe.platforms[0].industryRoleTh.includes('正片有效播放'));
51
+
52
+ const resVlinkage = await executeTool('get_entertainment_metrics', { platform: 'vlinkage' });
53
+ assert.equal(resVlinkage.status, 'success');
54
+ assert.equal(resVlinkage.platforms[0].id, 'vlinkage');
55
+
56
+ const resDatawin = await executeTool('get_entertainment_metrics', { platform: 'datawin' });
57
+ assert.equal(resDatawin.status, 'success');
58
+ assert.equal(resDatawin.platforms[0].id, 'datawin');
59
+ });
60
+
61
+ test('get_entertainment_metrics filters by category pillar', async () => {
62
+ const resStreaming = await executeTool('get_entertainment_metrics', { category: 'streaming_boxoffice' });
63
+ assert.equal(resStreaming.status, 'success');
64
+ assert.equal(resStreaming.totalPlatforms, 3);
65
+ const ids = resStreaming.platforms.map(p => p.id).sort();
66
+ assert.deepEqual(ids, ['dengta', 'maoyan', 'yunhe']);
67
+
68
+ const resActorDrama = await executeTool('get_entertainment_metrics', { category: 'actor_drama_index' });
69
+ assert.equal(resActorDrama.status, 'success');
70
+ assert.equal(resActorDrama.totalPlatforms, 3);
71
+ const actorIds = resActorDrama.platforms.map(p => p.id).sort();
72
+ assert.deepEqual(actorIds, ['datawin', 'guduo', 'vlinkage']);
73
+
74
+ const resSocial = await executeTool('get_entertainment_metrics', { category: 'social_commercial' });
75
+ assert.equal(resSocial.status, 'success');
76
+ assert.equal(resSocial.totalPlatforms, 3);
77
+ const socialIds = resSocial.platforms.map(p => p.id).sort();
78
+ assert.deepEqual(socialIds, ['douban', 'weibo', 'xiaohongshu']);
79
+ });
80
+
81
+ test('get_entertainment_metrics filters by query keywords (e.g. In Blossom or aesthetics)', async () => {
82
+ const res = await executeTool('get_entertainment_metrics', { query: 'In Blossom' });
83
+ assert.equal(res.status, 'success');
84
+ assert.ok(res.dramaBenchmarks.some(b => b.titleEn === 'In Blossom'));
85
+
86
+ const resAesthetic = await executeTool('get_entertainment_metrics', { query: '美学' });
87
+ assert.equal(resAesthetic.status, 'success');
88
+ assert.ok(resAesthetic.platforms.some(p => p.id === 'xiaohongshu'));
89
+ });
90
+
91
+ test('get_industry_index alias works identically', async () => {
92
+ const res = await executeTool('get_industry_index', { platform: 'maoyan' });
93
+ assert.equal(res.status, 'success');
94
+ assert.equal(res.platforms[0].id, 'maoyan');
95
+ assert.equal(res.platforms[0].nameCn, '猫眼专业版');
96
+ });
97
+
98
+ test('MCP server exposes get_entertainment_metrics and get_industry_index as public tools', () => {
99
+ const mcpToolNames = getMcpToolNames();
100
+ assert.ok(mcpToolNames.includes('get_entertainment_metrics'));
101
+ assert.ok(mcpToolNames.includes('get_industry_index'));
102
+ });
103
+
104
+ test('MCP server can execute get_entertainment_metrics over tool calling protocol', async () => {
105
+ const result = await handleToolCall('get_entertainment_metrics', { platform: 'datawin' });
106
+ assert.equal(result.status, 'success');
107
+ assert.equal(result._meta.protocol, 'mcp');
108
+ assert.equal(result.platforms[0].id, 'datawin');
109
+ assert.equal(result.platforms[0].nameEn, 'DataWin');
110
+ });
111
+
112
+ test('MCP server resource kiku://industry-index is defined and readable', async () => {
113
+ const def = resourceDefinitions.find(r => r.uri === 'kiku://industry-index');
114
+ assert.ok(def, 'Resource kiku://industry-index must be registered in resourceDefinitions');
115
+ assert.equal(def.mimeType, 'application/json');
116
+
117
+ const resource = await readResource('kiku://industry-index');
118
+ assert.equal(resource.contents[0].uri, 'kiku://industry-index');
119
+ assert.equal(resource.contents[0].mimeType, 'application/json');
120
+
121
+ const parsed = JSON.parse(resource.contents[0].text);
122
+ assert.ok(Array.isArray(parsed.platformsCatalog));
123
+ assert.equal(parsed.platformsCatalog.length, 9);
124
+ assert.ok(parsed.artistIndustryMetrics);
125
+ assert.equal(parsed.artistIndustryMetrics.artist.nameCn, '鞠婧祎');
126
+ });