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,141 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { ModelManager } from '../modelManager.js';
5
+
6
+ function clientWith(models, connected = true) {
7
+ return {
8
+ model: 'requested/model',
9
+ async checkConnection() {
10
+ return connected ? { connected: true, models } : { connected: false, models: [], error: 'offline' };
11
+ },
12
+ };
13
+ }
14
+
15
+ test('model manager keeps an explicitly requested loaded model', async () => {
16
+ const client = clientWith(['requested/model', 'google/gemma-4-e4b']);
17
+ const result = await new ModelManager(client).resolve();
18
+ assert.equal(result.state, 'ready');
19
+ assert.equal(result.usedFallback, false);
20
+ assert.equal(client.model, 'requested/model');
21
+ });
22
+
23
+ test('model manager selects a chat fallback when the requested model is missing', async () => {
24
+ const client = clientWith(['text-embedding-model', 'google/gemma-4-e4b']);
25
+ const result = await new ModelManager(client).resolve();
26
+ assert.equal(result.state, 'fallback_selected');
27
+ assert.equal(result.selectedModel, 'google/gemma-4-e4b');
28
+ assert.equal(client.model, 'google/gemma-4-e4b');
29
+ });
30
+
31
+ test('model manager fails closed for an explicitly requested unavailable model', async () => {
32
+ const client = clientWith(['google/gemma-4-e4b']);
33
+ const result = await new ModelManager(client, { requestedModel: 'Qwen/Qwen3-8B', strictModel: true }).resolve();
34
+ assert.equal(result.state, 'requested_model_missing');
35
+ assert.equal(result.selectedModel, null);
36
+ assert.equal(client.model, 'requested/model');
37
+ });
38
+
39
+ test('model manager prefers the provider recommendation over an arbitrary catalog model', async () => {
40
+ const client = clientWith(['random/chat-model', 'gpt-5.6-luna']);
41
+ client.provider = 'openai';
42
+ client.getProviderInfo = () => ({ recommendedModels: [{ id: 'gpt-5.6-luna' }] });
43
+ const result = await new ModelManager(client).resolve();
44
+ assert.equal(result.state, 'fallback_selected');
45
+ assert.equal(result.selectedModel, 'gpt-5.6-luna');
46
+ assert.equal(client.model, 'gpt-5.6-luna');
47
+ });
48
+
49
+ test('model manager reports online server with no loaded model', async () => {
50
+ const result = await new ModelManager(clientWith([])).resolve();
51
+ assert.equal(result.state, 'server_online_no_models');
52
+ assert.equal(result.selectedModel, undefined);
53
+ });
54
+
55
+ test('model manager rejects unavailable and non-chat manual selections', () => {
56
+ const manager = new ModelManager(clientWith([]));
57
+ assert.throws(() => manager.select('missing', ['chat']), /not available/);
58
+ assert.throws(() => manager.select('embedding-model', ['embedding-model']), /does not appear to support chat/);
59
+ });
60
+
61
+ test('model manager normalizes legacy model aliases before availability checks', () => {
62
+ const client = clientWith([]);
63
+ client.provider = 'deepseek';
64
+ const manager = new ModelManager(client);
65
+ const selected = manager.select('deepseek-chat', ['deepseek-v4-flash']);
66
+ assert.equal(selected, 'deepseek-v4-flash');
67
+ assert.equal(client.model, 'deepseek-v4-flash');
68
+ });
69
+
70
+ test('model manager matches Q8_0 quantization with GGUF file paths and publishers in LM Studio', async () => {
71
+ const client = {
72
+ provider: 'lmstudio',
73
+ model: 'google/gemma-4-e4b-Q8_0',
74
+ async checkConnection() {
75
+ return {
76
+ connected: true,
77
+ models: [
78
+ 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q4_K_M.gguf',
79
+ 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q8_0.gguf'
80
+ ],
81
+ activeLoadedModel: 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q8_0.gguf'
82
+ };
83
+ }
84
+ };
85
+
86
+ const manager = new ModelManager(client, { requestedModel: 'google/gemma-4-e4b-Q8_0' });
87
+ const result = await manager.resolve();
88
+ assert.equal(result.state, 'ready');
89
+ assert.equal(result.selectedModel, 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q8_0.gguf');
90
+ assert.equal(client.model, 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q8_0.gguf');
91
+
92
+ const inspection = await manager.inspect();
93
+ assert.equal(inspection.state, 'ready');
94
+ });
95
+
96
+ test('model manager allows selecting shorthand quantizations like Q8_0, q8, Q6_K in LM Studio', () => {
97
+ const client = {
98
+ provider: 'lmstudio',
99
+ model: 'google/gemma-4-e4b',
100
+ };
101
+ const available = [
102
+ 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q4_K_M.gguf',
103
+ 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q8_0.gguf'
104
+ ];
105
+ const manager = new ModelManager(client);
106
+
107
+ const selectedQ8 = manager.select('Q8_0', available);
108
+ assert.equal(selectedQ8, 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q8_0.gguf');
109
+ assert.equal(client.model, 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q8_0.gguf');
110
+
111
+ const selectedQ8Short = manager.select('q8', available);
112
+ assert.equal(selectedQ8Short, 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q8_0.gguf');
113
+
114
+ const selectedQ4 = manager.select('q4_k_m', available);
115
+ assert.equal(selectedQ4, 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q4_K_M.gguf');
116
+ });
117
+
118
+ test('model manager reports server_online_model_not_loaded when LM Studio has models on disk but none in VRAM', async () => {
119
+ const client = {
120
+ provider: 'lmstudio',
121
+ model: 'google/gemma-4-e4b',
122
+ async checkConnection() {
123
+ return {
124
+ connected: true,
125
+ models: ['google/gemma-4-e4b'],
126
+ activeLoadedModel: null,
127
+ loadedModels: []
128
+ };
129
+ }
130
+ };
131
+
132
+ const manager = new ModelManager(client);
133
+ const result = await manager.resolve();
134
+ assert.equal(result.connected, true);
135
+ assert.equal(result.state, 'server_online_model_not_loaded');
136
+ assert.equal(result.isModelLoaded, false);
137
+ assert.equal(result.activeLoadedModel, null);
138
+ assert.equal(result.selectedModel, 'google/gemma-4-e4b');
139
+ });
140
+
141
+
@@ -0,0 +1,69 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { detectDominantLanguage } from '../agentCore.js';
4
+ import { DynamicToolRouter } from '../toolRouter.js';
5
+ import { executeTool } from '../tools.js';
6
+
7
+ export const MULTILINGUAL_QUERIES = Object.freeze([
8
+ {
9
+ lang: 'th',
10
+ text: 'จวีจิ้งอี เกิดวันที่เท่าไหร่ และมีผลงานเพลงอะไรบ้างค่ะ',
11
+ expectedLanguage: 'th'
12
+ },
13
+ {
14
+ lang: 'zh',
15
+ text: '请问鞠婧祎在2026年有哪些新剧待播?',
16
+ expectedLanguage: 'zh'
17
+ },
18
+ {
19
+ lang: 'en',
20
+ text: 'What are the main awards won by Ju Jingyi in her solo career?',
21
+ expectedLanguage: 'en'
22
+ },
23
+ {
24
+ lang: 'th',
25
+ text: 'Ju Jingyi (鞠婧祎) ผลงานละครเรื่อง ฮวาเจียนลิ่ง (In Blossom) ฉายปีไหนคะ',
26
+ expectedLanguage: 'th'
27
+ },
28
+ {
29
+ lang: 'zh',
30
+ text: '鞠婧祎 Ju Jingyi 个人单曲《每一天》的发行年份和MV',
31
+ expectedLanguage: 'zh'
32
+ },
33
+ {
34
+ lang: 'en',
35
+ text: 'Ju Jingyi Chinese name 鞠婧祎 and fandom name Miju 蜜橘 details',
36
+ expectedLanguage: 'en'
37
+ },
38
+ {
39
+ lang: 'ja',
40
+ text: '鞠婧祎(ジュ・ジンイー)の最新ドラマと代表曲を教えてください',
41
+ expectedLanguage: 'ja'
42
+ },
43
+ {
44
+ lang: 'ko',
45
+ text: '쥐징이의 최신 드라마와 대표곡을 알려주세요',
46
+ expectedLanguage: 'ko'
47
+ }
48
+ ]);
49
+
50
+ test('Multilingual Suite: Accurately detects primary language in mixed bilingual queries', () => {
51
+ for (const item of MULTILINGUAL_QUERIES) {
52
+ const detected = detectDominantLanguage(item.text);
53
+ assert.equal(detected, item.expectedLanguage);
54
+ }
55
+ });
56
+
57
+ test('Multilingual Suite: Universal and domain search tools find records across Chinese, Thai, and English titles', async () => {
58
+ const cnRes = await executeTool('search_works', { query: '花间令' });
59
+ assert.equal(cnRes.status, 'success');
60
+ assert.ok(cnRes.count > 0);
61
+
62
+ const thRes = await executeTool('search_works', { query: 'ฮวาเจียนลิ่ง' });
63
+ assert.equal(thRes.status, 'success');
64
+ assert.ok(thRes.count > 0);
65
+
66
+ const enRes = await executeTool('search_works', { query: 'In Blossom' });
67
+ assert.equal(enRes.status, 'success');
68
+ assert.ok(enRes.count > 0);
69
+ });
@@ -0,0 +1,113 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { EventEmitter } from 'node:events';
4
+ import { Readable } from 'node:stream';
5
+ import { collectMusicControlActions, createMusicControlAction } from '../../web-chat/core/musicControlAction.js';
6
+ import { handleChatRoute } from '../../web-chat/routes/chatRoute.js';
7
+ import { getWebChatHtml } from '../../webChatHtml.js';
8
+
9
+ function request(body) {
10
+ const req = Readable.from([JSON.stringify(body)]);
11
+ req.socket = { remoteAddress: '127.0.0.1' };
12
+ req.headers = {};
13
+ return req;
14
+ }
15
+
16
+ function response() {
17
+ const res = new EventEmitter();
18
+ res.chunks = '';
19
+ res.writeHead = (code) => { res.statusCode = code; res.headersSent = true; };
20
+ res.write = (data) => { res.chunks += data; };
21
+ res.end = (data = '') => { res.chunks += data; res.writableEnded = true; res.emit('finish'); };
22
+ return res;
23
+ }
24
+
25
+ function musicAgent(toolName, result) {
26
+ return {
27
+ session: { id: 'music-test', messages: [] },
28
+ memory: { getStatus: () => ({}) },
29
+ run: async (_query, options) => {
30
+ options.onToolStart(toolName, {});
31
+ options.onToolResult(toolName, result, 3);
32
+ options.onTokenDelta?.('done');
33
+ return { answer: 'done' };
34
+ },
35
+ };
36
+ }
37
+
38
+ test('music tool results normalize to a bounded dedicated UI action', () => {
39
+ const event = createMusicControlAction('play_music', {
40
+ status: 'success',
41
+ action: 'pause',
42
+ playing: 'Heartbeat Alert',
43
+ song: { title: '恋爱告急', youtubeId: '5MLPdCmnlmE', extra: 'not-forwarded' },
44
+ });
45
+
46
+ assert.deepEqual(event, {
47
+ type: 'ui_action',
48
+ target: 'music_player',
49
+ tool: 'play_music',
50
+ action: 'play',
51
+ playing: 'Heartbeat Alert',
52
+ song: {
53
+ id: undefined,
54
+ title: '恋爱告急',
55
+ titleTh: undefined,
56
+ titleCn: undefined,
57
+ titleEn: undefined,
58
+ youtubeId: '5MLPdCmnlmE',
59
+ duration: undefined,
60
+ category: undefined,
61
+ dramaName: undefined,
62
+ },
63
+ });
64
+ assert.equal(createMusicControlAction('search_discography', { status: 'success' }), null);
65
+ assert.deepEqual(createMusicControlAction('pause_music', { status: 'error' }), {
66
+ type: 'ui_action',
67
+ target: 'music_player',
68
+ tool: 'pause_music',
69
+ action: 'pause',
70
+ });
71
+ assert.equal(collectMusicControlActions([
72
+ { name: 'set_volume', result: { status: 'success', volume: 140 } },
73
+ { name: 'seek_music', result: { status: 'success', seconds: -5 } },
74
+ ]).length, 2);
75
+ });
76
+
77
+ test('streaming chat emits music UI actions independently from tool inspector events', async () => {
78
+ const res = response();
79
+ await handleChatRoute(request({ query: 'pause music', provider: 'lmstudio', stream: true }), res, null, {
80
+ resolveConnection: async () => ({
81
+ ready: true,
82
+ agent: musicAgent('pause_music', { status: 'success', action: 'pause', message: 'paused' }),
83
+ }),
84
+ });
85
+
86
+ const events = res.chunks
87
+ .split('\n')
88
+ .filter((line) => line.startsWith('data: {'))
89
+ .map((line) => JSON.parse(line.slice(6)));
90
+ assert.ok(events.some((event) => event.type === 'tool_result' && event.name === 'pause_music'));
91
+ assert.ok(events.some((event) => event.type === 'ui_action' && event.target === 'music_player' && event.action === 'pause'));
92
+ });
93
+
94
+ test('non-streaming chat returns music UI actions and both client stream paths consume them', async () => {
95
+ const res = response();
96
+ await handleChatRoute(request({ query: 'volume 60', provider: 'lmstudio', stream: false }), res, null, {
97
+ resolveConnection: async () => ({
98
+ ready: true,
99
+ agent: musicAgent('set_volume', { status: 'success', action: 'set_volume', volume: 60 }),
100
+ }),
101
+ });
102
+
103
+ const body = JSON.parse(res.chunks);
104
+ assert.equal(body.uiActions[0].action, 'set_volume');
105
+ assert.equal(body.uiActions[0].volume, 60);
106
+
107
+ const html = getWebChatHtml();
108
+ assert.equal((html.match(/event\.type === 'ui_action' && event\.target === 'music_player'/g) || []).length, 2);
109
+ assert.equal((html.match(/const uiActions = Array\.isArray\(data\.uiActions\)/g) || []).length, 2);
110
+ assert.match(html, /miniPlayerState\.isPlaying = false;\s*sendMiniPlayerCommand\('pauseVideo'\)/);
111
+ assert.match(html, /miniPlayerState\.volume = vol;\s*miniPlayerState\.isMuted = vol === 0;/);
112
+ assert.match(html, /sendMiniPlayerCommand\(vol === 0 \? 'mute' : 'unMute'\)/);
113
+ });
@@ -0,0 +1,347 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { routeCommand } from '../commands.js';
5
+ import { AgentMemory } from '../memory.js';
6
+ import { AgentJobManager } from '../jobManager.js';
7
+
8
+ const RECENT_DATE = new Date(Date.now() - 3 * 86_400_000).toISOString().slice(0, 10);
9
+
10
+ async function captureOutput(run) {
11
+ const originalLog = console.log;
12
+ const output = [];
13
+ console.log = (...args) => output.push(args.join(' '));
14
+ try {
15
+ const result = await run();
16
+ return { result, output: output.join('\n') };
17
+ } finally {
18
+ console.log = originalLog;
19
+ }
20
+ }
21
+
22
+ function catalogExecutor(name, args = {}) {
23
+ if (name === 'search_works' && (!args.query || /in blossom/i.test(args.query))) return Promise.resolve({ works: [{ id: 'in-blossom', titleTh: 'In Blossom', year: 2024, role: 'Yang Caiwei', platform: 'Youku', episodes: 32 }] });
24
+ if (name === 'search_discography' && (!args.query || /rain love/i.test(args.query))) return Promise.resolve({ songs: [{ id: 'rain-love', title: 'Rain Love', year: 2024, type: 'Single', duration: '3:45' }] });
25
+ return Promise.resolve({ works: [], songs: [], roles: [], brands: [], awards: [], photos: [], videos: [] });
26
+ }
27
+
28
+ test('/find validates flags and renders normalized catalog results', async () => {
29
+ const found = await captureOutput(() => routeCommand('/find "In Blossom" --type work --limit 2', { executeTool: catalogExecutor }));
30
+ assert.equal(found.result, 'HANDLED');
31
+ assert.match(found.output, /Catalog Search/);
32
+ assert.match(found.output, /In Blossom/);
33
+
34
+ const invalid = await captureOutput(() => routeCommand('/find test --type file', { executeTool: catalogExecutor }));
35
+ assert.equal(invalid.result, 'HANDLED');
36
+ assert.match(invalid.output, /COMMAND ERROR/);
37
+ });
38
+
39
+ test('/compare resolves both sides around the separator', async () => {
40
+ const output = await captureOutput(() => routeCommand('/compare "In Blossom" -- "Rain Love"', { executeTool: catalogExecutor }));
41
+ assert.equal(output.result, 'HANDLED');
42
+ assert.match(output.output, /Compare/);
43
+ assert.match(output.output, /In Blossom/);
44
+ assert.match(output.output, /Rain Love/);
45
+ });
46
+
47
+ test('/favorites adds, lists, removes, and exports resolved entries', async () => {
48
+ const memory = new AgentMemory();
49
+ memory.save = async () => true;
50
+ memory.exportFavorites = async format => ({ filePath: `C:\\safe\\favorites.${format === 'markdown' ? 'md' : 'json'}`, count: memory.getFavorites().length });
51
+ const agent = { memory };
52
+
53
+ const added = await captureOutput(() => routeCommand('/favorites add work "In Blossom"', { agent, executeTool: catalogExecutor }));
54
+ assert.equal(added.result, 'HANDLED');
55
+ assert.equal(memory.getFavorites().length, 1);
56
+ assert.match(added.output, /Favorite Added/);
57
+
58
+ const listed = await captureOutput(() => routeCommand('/favorites list --type work', { agent }));
59
+ assert.match(listed.output, /In Blossom/);
60
+
61
+ const id = memory.getFavorites()[0].id;
62
+ await captureOutput(() => routeCommand(`/favorites remove ${id}`, { agent }));
63
+ assert.equal(memory.getFavorites().length, 0);
64
+
65
+ const exported = await captureOutput(() => routeCommand('/favorites export markdown', { agent }));
66
+ assert.match(exported.output, /Favorites Exported/);
67
+ });
68
+
69
+ test('/news filters unsafe URLs and honors official-only hosts', async () => {
70
+ let query = '';
71
+ const executeTool = async () => ({
72
+ results: [
73
+ { title: `Official update ${RECENT_DATE}`, url: 'https://weibo.com/example', snippet: 'Studio update\u001b[31m' },
74
+ { title: 'Unsafe', url: 'javascript:alert(1)', snippet: 'Ignore' },
75
+ { title: 'Blog', url: 'https://example.com/news', snippet: 'Unofficial' },
76
+ ],
77
+ });
78
+ const output = await captureOutput(() => routeCommand('/news drama --days 7 --official-only', { executeTool: async (name, args) => {
79
+ query = args.query;
80
+ return executeTool(name, args);
81
+ } }));
82
+ assert.equal(output.result, 'HANDLED');
83
+ assert.match(query, /after:\d{4}-\d{2}-\d{2} before:\d{4}-\d{2}-\d{2}/);
84
+ assert.match(output.output, /requested 7-day window/);
85
+ assert.match(output.output, /weibo\.com/);
86
+ assert.doesNotMatch(output.output, /javascript:/);
87
+ assert.doesNotMatch(output.output, /example\.com/);
88
+ assert.doesNotMatch(output.output, /\u001b\[31m/);
89
+ });
90
+
91
+ test('/news labels results without dates as recency unverified', async () => {
92
+ const executeTool = async () => ({ results: [{ title: 'Drama update', url: 'https://example.com/update', snippet: 'No publication date' }] });
93
+ const output = await captureOutput(() => routeCommand('/news drama --days 7', { executeTool }));
94
+ assert.match(output.output, /date unavailable • recency unverified/);
95
+ });
96
+
97
+ test('/jobs lists, shows, cancels, and clears through the agent contract', async () => {
98
+ const manager = new AgentJobManager();
99
+ const job = manager.create('research', 'Current drama news');
100
+ const agent = {
101
+ getJobs: () => manager.list(),
102
+ getJob: id => manager.get(id),
103
+ cancelJob: id => manager.cancel(id),
104
+ clearJobs: () => manager.clear(),
105
+ };
106
+ const listed = await captureOutput(() => routeCommand('/jobs', { agent }));
107
+ assert.match(listed.output, new RegExp(job.id));
108
+ const shown = await captureOutput(() => routeCommand(`/jobs show ${job.id}`, { agent }));
109
+ assert.match(shown.output, /Current drama news/);
110
+ await captureOutput(() => routeCommand(`/jobs cancel ${job.id}`, { agent }));
111
+ assert.equal(manager.get(job.id).state, 'cancelled');
112
+ await captureOutput(() => routeCommand('/jobs clear', { agent }));
113
+ assert.equal(manager.list().length, 0);
114
+ });
115
+
116
+ test('/discover returns one catalog recommendation with deterministic selection', async () => {
117
+ const output = await captureOutput(() => routeCommand('/discover song --mood calm', { executeTool: catalogExecutor, random: () => 0 }));
118
+ assert.equal(output.result, 'HANDLED');
119
+ assert.match(output.output, /Discover/);
120
+ assert.match(output.output, /Rain Love/);
121
+ });
122
+
123
+ test('/discover renders an AI rationale when a chat client is available', async () => {
124
+ const agent = {
125
+ memory: new AgentMemory(),
126
+ client: {
127
+ chat: async () => ({ content: 'A dreamy choice to end the day.' }),
128
+ },
129
+ };
130
+ const output = await captureOutput(() => routeCommand('/discover song --mood calm', { agent, executeTool: catalogExecutor, random: () => 0 }));
131
+ assert.equal(output.result, 'HANDLED');
132
+ assert.match(output.output, /A dreamy choice to end the day/);
133
+ });
134
+
135
+ test('/discover falls back gracefully when the chat client fails', async () => {
136
+ const agent = {
137
+ memory: new AgentMemory(),
138
+ client: {
139
+ chat: async () => { throw new Error('model unavailable'); },
140
+ },
141
+ };
142
+ const output = await captureOutput(() => routeCommand('/discover drama', { agent, executeTool: catalogExecutor, random: () => 0 }));
143
+ assert.equal(output.result, 'HANDLED');
144
+ assert.match(output.output, /Discover/);
145
+ assert.match(output.output, /In Blossom/);
146
+ assert.doesNotMatch(output.output, /COMMAND ERROR/);
147
+ });
148
+
149
+ function fakeAgent(narration) {
150
+ const memory = new AgentMemory();
151
+ memory.save = async () => true;
152
+ return {
153
+ memory,
154
+ client: { chat: async () => ({ content: narration }) },
155
+ };
156
+ }
157
+
158
+ test('/compare renders an AI natural-language summary when a client is available', async () => {
159
+ const output = await captureOutput(() => routeCommand('/compare "In Blossom" -- "Rain Love"', { agent: fakeAgent('In Blossom is a drama while Rain Love is a single.'), executeTool: catalogExecutor }));
160
+ assert.equal(output.result, 'HANDLED');
161
+ assert.match(output.output, /In Blossom is a drama while Rain Love is a single/);
162
+ });
163
+
164
+ test('/compare falls back gracefully when the chat client fails', async () => {
165
+ const agent = { client: { chat: async () => { throw new Error('boom'); } } };
166
+ const output = await captureOutput(() => routeCommand('/compare "In Blossom" -- "Rain Love"', { agent, executeTool: catalogExecutor }));
167
+ assert.equal(output.result, 'HANDLED');
168
+ assert.match(output.output, /Differences/);
169
+ assert.doesNotMatch(output.output, /COMMAND ERROR/);
170
+ });
171
+
172
+ test('/find renders an AI explanation when a client is available', async () => {
173
+ const output = await captureOutput(() => routeCommand('/find "In Blossom" --type work', { agent: fakeAgent('In Blossom matches because the query matches its title.'), executeTool: catalogExecutor }));
174
+ assert.equal(output.result, 'HANDLED');
175
+ assert.match(output.output, /matches because/);
176
+ });
177
+
178
+ test('/find falls back gracefully when no client is available', async () => {
179
+ const output = await captureOutput(() => routeCommand('/find "In Blossom" --type work', { executeTool: catalogExecutor }));
180
+ assert.equal(output.result, 'HANDLED');
181
+ assert.match(output.output, /Catalog Search/);
182
+ assert.match(output.output, /In Blossom/);
183
+ assert.doesNotMatch(output.output, /COMMAND ERROR/);
184
+ });
185
+
186
+ test('/favorites suggest renders an AI recommendation from saved taste', async () => {
187
+ const agent = fakeAgent('Rain Love — a gentle companion piece\nIn Blossom encore');
188
+ await captureOutput(() => routeCommand('/favorites add work "In Blossom"', { agent, executeTool: catalogExecutor }));
189
+ const output = await captureOutput(() => routeCommand('/favorites suggest', { agent, executeTool: catalogExecutor }));
190
+ assert.equal(output.result, 'HANDLED');
191
+ assert.match(output.output, /Favorites Suggest/);
192
+ assert.match(output.output, /Rain Love/);
193
+ });
194
+
195
+ test('/favorites suggest explains when there are no favorites yet', async () => {
196
+ const output = await captureOutput(() => routeCommand('/favorites suggest', { agent: fakeAgent('ignored'), executeTool: catalogExecutor }));
197
+ assert.equal(output.result, 'HANDLED');
198
+ assert.match(output.output, /No favorites yet/);
199
+ assert.doesNotMatch(output.output, /COMMAND ERROR/);
200
+ });
201
+
202
+ test('/news renders an AI summary of results when a client is available', async () => {
203
+ const executeTool = async () => ({ results: [{ title: `Studio update ${RECENT_DATE}`, url: 'https://weibo.com/example', snippet: 'New single released' }] });
204
+ const output = await captureOutput(() => routeCommand('/news drama --days 7', { agent: fakeAgent('The studio announced a new single this week.'), executeTool }));
205
+ assert.equal(output.result, 'HANDLED');
206
+ assert.match(output.output, /announced a new single/);
207
+ });
208
+
209
+ test('/news falls back gracefully when no client is available', async () => {
210
+ const executeTool = async () => ({ results: [{ title: `Studio update ${RECENT_DATE}`, url: 'https://weibo.com/example', snippet: 'New single released' }] });
211
+ const output = await captureOutput(() => routeCommand('/news drama --days 7', { executeTool }));
212
+ assert.equal(output.result, 'HANDLED');
213
+ assert.match(output.output, /News Search/);
214
+ assert.doesNotMatch(output.output, /COMMAND ERROR/);
215
+ });
216
+
217
+ test('/history summary delegates to /session summary and renders an AI summary when messages and client exist', async () => {
218
+ const agent = fakeAgent('Discussed In Blossom release details and music.');
219
+ agent.session = {
220
+ messages: [
221
+ { role: 'user', parts: [{ type: 'text', content: 'Tell me about In Blossom' }] },
222
+ { role: 'assistant', parts: [{ type: 'text', content: 'In Blossom was released in 2024.' }] },
223
+ ],
224
+ };
225
+ const output = await captureOutput(() => routeCommand('/history summary', { agent }));
226
+ assert.equal(output.result, 'HANDLED');
227
+ assert.match(output.output, /Session Summary/);
228
+ assert.match(output.output, /Discussed In Blossom/);
229
+ });
230
+
231
+ test('/history summary handles empty history gracefully', async () => {
232
+ const agent = fakeAgent('ignored');
233
+ agent.session = { messages: [] };
234
+ const output = await captureOutput(() => routeCommand('/history summary', { agent }));
235
+ assert.equal(output.result, 'HANDLED');
236
+ assert.match(output.output, /No messages in the active session/);
237
+ });
238
+
239
+ test('/session summary synthesizes an AI session analysis when messages exist', async () => {
240
+ const agent = fakeAgent('Session focused on 2024 filmography research with 2 tool calls.');
241
+ agent.session = {
242
+ id: 'sess_test_123',
243
+ messages: [
244
+ { role: 'user', parts: [{ type: 'text', content: 'Who did Ju Jingyi play in In Blossom?' }] },
245
+ { role: 'assistant', parts: [{ type: 'text', content: 'She played Yang Caiwei.' }] },
246
+ ],
247
+ stats: { totalQueries: 1, totalToolCalls: 2 },
248
+ };
249
+ const output = await captureOutput(() => routeCommand('/session summary', { agent }));
250
+ assert.equal(output.result, 'HANDLED');
251
+ assert.match(output.output, /Session Summary/);
252
+ assert.match(output.output, /Session focused on 2024/);
253
+ });
254
+
255
+ test('/session summary handles empty session messages gracefully', async () => {
256
+ const agent = fakeAgent('ignored');
257
+ agent.session = { id: 'sess_empty', messages: [], stats: {} };
258
+ const output = await captureOutput(() => routeCommand('/session summary', { agent }));
259
+ assert.equal(output.result, 'HANDLED');
260
+ assert.match(output.output, /No messages in the active session/);
261
+ });
262
+
263
+ test('/mem summary synthesizes an AI persona profile when facts exist', async () => {
264
+ const agent = fakeAgent('User is an avid fan tracking Ju Jingyi dramas and OST releases.');
265
+ agent.memory.addFact('Loves In Blossom (2024)', { source: 'user' });
266
+ const output = await captureOutput(() => routeCommand('/mem summary', { agent }));
267
+ assert.equal(output.result, 'HANDLED');
268
+ assert.match(output.output, /Memory Profile Summary/);
269
+ assert.match(output.output, /avid fan tracking/);
270
+ });
271
+
272
+ test('/mem summary handles empty memory gracefully', async () => {
273
+ const agent = fakeAgent('ignored');
274
+ const output = await captureOutput(() => routeCommand('/mem summary', { agent }));
275
+ assert.equal(output.result, 'HANDLED');
276
+ assert.match(output.output, /No long-term memories/);
277
+ });
278
+
279
+ test('/stats renders AI data narration alongside summary counts', async () => {
280
+ const agent = fakeAgent('Ju Jingyi has a rich discography and television portfolio.');
281
+ const output = await captureOutput(() => routeCommand('/stats', { agent, executeTool: async () => ({ stats: { totalWorks: 15, totalUniqueSongs: 40, brandEndorsements: 12, totalAwards: 25, timelineMilestones: 30 } }) }));
282
+ assert.equal(output.result, 'HANDLED');
283
+ assert.match(output.output, /Database Overview/);
284
+ assert.match(output.output, /rich discography/);
285
+ });
286
+
287
+ test('job manager records completion, failure, and cancellation states', async () => {
288
+ const manager = new AgentJobManager();
289
+ await manager.run('research', 'done', async () => ({ ok: true }));
290
+ assert.equal(manager.list()[0].state, 'completed');
291
+
292
+ await assert.rejects(manager.run('research', 'failed', async () => { throw new Error('failed'); }), /failed/);
293
+ assert.equal(manager.list()[0].state, 'failed');
294
+
295
+ let release;
296
+ const pending = manager.run('research', 'cancel me', ({ signal }) => new Promise((resolve, reject) => {
297
+ release = resolve;
298
+ signal.addEventListener('abort', () => reject(signal.reason), { once: true });
299
+ }));
300
+ const active = manager.list()[0];
301
+ assert.equal(manager.cancel(active.id), true);
302
+ await assert.rejects(pending, /Cancelled by user/);
303
+ assert.equal(manager.get(active.id).state, 'cancelled');
304
+ release?.();
305
+ });
306
+
307
+ test('/session load and /resume restore archived sessions into memory', async () => {
308
+ let restoredTarget = null;
309
+ const mockAgent = {
310
+ session: {
311
+ id: 'sess_active',
312
+ messages: [],
313
+ stats: { totalQueries: 0, totalToolCalls: 0, totalDurationMs: 0 },
314
+ thinkingMode: 'hide',
315
+ },
316
+ memory: {
317
+ getArchivedSessions: (limit) => [
318
+ { id: 'sess_arch_1', createdAt: new Date(), messageCount: 4, stats: { totalQueries: 2, totalToolCalls: 3, totalDurationMs: 1500 } },
319
+ { id: 'sess_arch_2', createdAt: new Date(), messageCount: 2, stats: { totalQueries: 1, totalToolCalls: 1, totalDurationMs: 800 } },
320
+ ],
321
+ },
322
+ restoreSession: async (target) => {
323
+ restoredTarget = target;
324
+ return {
325
+ id: 'sess_arch_1',
326
+ createdAt: new Date(),
327
+ messages: [{ role: 'user' }, { role: 'assistant' }, { role: 'user' }, { role: 'assistant' }],
328
+ stats: { totalQueries: 2, totalToolCalls: 3, totalDurationMs: 1500 },
329
+ };
330
+ },
331
+ };
332
+
333
+ const usageOutput = await captureOutput(() => routeCommand('/session load', { agent: mockAgent, sub: 'load', extra: '' }));
334
+ assert.equal(usageOutput.result, 'HANDLED');
335
+ assert.match(usageOutput.output, /Load Session Usage/);
336
+
337
+ const loadOutput = await captureOutput(() => routeCommand('/session load sess_arch_1', { agent: mockAgent, sub: 'load', extra: 'sess_arch_1' }));
338
+ assert.equal(loadOutput.result, 'HANDLED');
339
+ assert.equal(restoredTarget, 'sess_arch_1');
340
+ assert.match(loadOutput.output, /Session Restored Successfully/);
341
+
342
+ const resumeOutput = await captureOutput(() => routeCommand('/resume 1', { agent: mockAgent, sub: '1', extra: '' }));
343
+ assert.equal(resumeOutput.result, 'HANDLED');
344
+ assert.equal(restoredTarget, '1');
345
+ assert.match(resumeOutput.output, /Session Restored Successfully/);
346
+ });
347
+