kiku-agent 1.3.2 → 1.3.4

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 (152) hide show
  1. package/README.md +69 -12
  2. package/package.json +2 -1
  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/modules/evaluation/runner.js +4 -14
  18. package/server/agent/eval/modules/evaluation/scoring.js +15 -3
  19. package/server/agent/eval/modules/server/evalApi.js +5 -5
  20. package/server/agent/eval/modules/ui/evalUi.js +7 -4
  21. package/server/agent/eventBus.js +1 -0
  22. package/server/agent/ink/main.jsx +196 -191
  23. package/server/agent/jobManager.js +11 -1
  24. package/server/agent/lanDiscovery.js +721 -23
  25. package/server/agent/llmClient.js +317 -71
  26. package/server/agent/lmStudioWakeup.js +517 -65
  27. package/server/agent/locationService.js +797 -0
  28. package/server/agent/mcpQuizData.js +509 -0
  29. package/server/agent/memory/contextAllocator.js +19 -7
  30. package/server/agent/memory.js +3 -1
  31. package/server/agent/modelCompatibility.js +352 -0
  32. package/server/agent/modelDefaults.js +1 -0
  33. package/server/agent/modelManager.js +46 -2
  34. package/server/agent/newsWatch.js +9 -3
  35. package/server/agent/processManager.js +229 -0
  36. package/server/agent/prompts.js +174 -8
  37. package/server/agent/routingInterceptor.js +9 -1
  38. package/server/agent/sanitizer.js +41 -6
  39. package/server/agent/searchQueryPlanner.js +202 -0
  40. package/server/agent/session.js +59 -4
  41. package/server/agent/tests/agentFollowUps.test.js +128 -2
  42. package/server/agent/tests/agentMusicControl.test.js +245 -0
  43. package/server/agent/tests/apiRouter.test.js +1 -1
  44. package/server/agent/tests/autoContext.test.js +176 -0
  45. package/server/agent/tests/bilibiliEmbed.test.js +45 -0
  46. package/server/agent/tests/chatHardening.test.js +121 -0
  47. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  48. package/server/agent/tests/clarifyingQuestions.test.js +33 -0
  49. package/server/agent/tests/cliAutomation.test.js +38 -1
  50. package/server/agent/tests/cloudProviders.test.js +91 -3
  51. package/server/agent/tests/commands.test.js +92 -3
  52. package/server/agent/tests/contextIntelligence.test.js +50 -3
  53. package/server/agent/tests/cpuMetrics.test.js +101 -0
  54. package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
  55. package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
  56. package/server/agent/tests/engineService.test.js +436 -0
  57. package/server/agent/tests/engineUnload.test.js +94 -0
  58. package/server/agent/tests/envCommand.test.js +69 -0
  59. package/server/agent/tests/evalMetrics.test.js +8 -0
  60. package/server/agent/tests/fetchApi.test.js +99 -0
  61. package/server/agent/tests/hybridWebSearch.test.js +2 -0
  62. package/server/agent/tests/lanDiscovery.test.js +5 -0
  63. package/server/agent/tests/lanInterfaces.test.js +510 -0
  64. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  65. package/server/agent/tests/locationTool.test.js +278 -0
  66. package/server/agent/tests/memoryRoute.test.js +173 -0
  67. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  68. package/server/agent/tests/modelCompatibility.test.js +148 -0
  69. package/server/agent/tests/modelManager.test.js +32 -0
  70. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  71. package/server/agent/tests/nextGenArchitecture.test.js +4 -2
  72. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  73. package/server/agent/tests/openDomainSearch.test.js +121 -0
  74. package/server/agent/tests/processManager.test.js +144 -0
  75. package/server/agent/tests/providerPersistence.test.js +10 -5
  76. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  77. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  78. package/server/agent/tests/securityAuditRemediation.test.js +16 -9
  79. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  80. package/server/agent/tests/securityHardening.test.js +4 -4
  81. package/server/agent/tests/sessionAndCopy.test.js +39 -4
  82. package/server/agent/tests/sidebarControls.test.js +34 -0
  83. package/server/agent/tests/statefulMemory.test.js +8 -1
  84. package/server/agent/tests/textProtocolLoop.test.js +71 -0
  85. package/server/agent/tests/textToolParsing.test.js +48 -0
  86. package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
  87. package/server/agent/tests/unifiedEndpoints.test.js +13 -6
  88. package/server/agent/tests/v110Features.test.js +1 -1
  89. package/server/agent/tests/vramEstimator.test.js +75 -0
  90. package/server/agent/tests/webChatDocs.test.js +199 -0
  91. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  92. package/server/agent/tests/webChatInspection.test.js +256 -5
  93. package/server/agent/tests/webChatMcp.test.js +94 -0
  94. package/server/agent/tests/webExtract.test.js +23 -0
  95. package/server/agent/tests/webSources.test.js +46 -12
  96. package/server/agent/textToolProtocol.js +173 -46
  97. package/server/agent/toolRouter.js +318 -44
  98. package/server/agent/tools.js +877 -96
  99. package/server/agent/ui.js +39 -34
  100. package/server/agent/vramService.js +35 -54
  101. package/server/agent/webSearch.js +441 -162
  102. package/server/agent/websearch/orchestrator.js +15 -5
  103. package/server/agent/websearch/resilience/cache.js +9 -2
  104. package/server/agent/websearch/security/ssrf.js +41 -10
  105. package/server/agent/youtubeAuth.js +5 -20
  106. package/server/apiRouter.js +5558 -4422
  107. package/server/mcp/data/discography.js +2 -2
  108. package/server/mcp/examples/antigravity.example.json +17 -0
  109. package/server/mcp/handler.js +8 -4
  110. package/server/mcp/security.js +220 -21
  111. package/server/mcp/server.js +2 -1
  112. package/server/mcp/tests/connection.test.js +14 -8
  113. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  114. package/server/mcp/tools.js +7 -1
  115. package/server/supabase_migration_prefix.sql +102 -0
  116. package/server/supabase_schema.sql +18 -18
  117. package/server/web-chat/core/connection.js +179 -218
  118. package/server/web-chat/core/conversationContext.js +9 -2
  119. package/server/web-chat/core/directCatalogEngine.js +52 -28
  120. package/server/web-chat/core/dramaKnowledge.js +33 -22
  121. package/server/web-chat/core/followUpEngine.js +735 -81
  122. package/server/web-chat/core/futureEventGuard.js +5 -1
  123. package/server/web-chat/core/musicControlAction.js +70 -0
  124. package/server/web-chat/core/security.js +32 -21
  125. package/server/web-chat/core/toolInspector.js +6 -6
  126. package/server/web-chat/core/urlGuard.js +45 -0
  127. package/server/web-chat/router.js +204 -157
  128. package/server/web-chat/routes/chatRoute.js +885 -488
  129. package/server/web-chat/routes/engineRoute.js +270 -0
  130. package/server/web-chat/routes/exportRoute.js +1 -1
  131. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  132. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  133. package/server/web-chat/routes/mcpRoute.js +386 -0
  134. package/server/web-chat/routes/memoryRoute.js +253 -0
  135. package/server/web-chat/routes/providerRoute.js +49 -16
  136. package/server/web-chat/routes/staticRoute.js +306 -17
  137. package/server/web-chat/routes/statusRoute.js +194 -73
  138. package/server/web-chat/routes/suggestRoute.js +11 -3
  139. package/server/web-chat/routes/verifyRoute.js +26 -4
  140. package/server/web-chat/server.js +7 -6
  141. package/server/web-chat/ui/docsBuilder.js +3558 -0
  142. package/server/web-chat/ui/htmlBuilder.js +12 -0
  143. package/server/web-chat/ui/scripts/client.js +3401 -1302
  144. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  145. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  146. package/server/web-chat/ui/styles/lan-card.css +165 -0
  147. package/server/web-chat/ui/styles/main.css +4289 -447
  148. package/server/web-chat/ui/sw.js +64 -0
  149. package/server/web-chat/ui/templates/body.html +407 -281
  150. package/src/data/awardsData.js +146 -146
  151. package/src/data/musicVideoData.js +6 -4
  152. package/src/lib/vram/estimator.js +195 -19
@@ -0,0 +1,278 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createServer } from 'node:http';
4
+
5
+ import {
6
+ probeLmStudioRuntime,
7
+ isLmStudioResponding,
8
+ getLoadedModels,
9
+ wakeUpLmStudio,
10
+ clearLmStudioProbeCache,
11
+ buildLmsLoadArgs,
12
+ resolveParallelSlotBudget
13
+ } from '../lmStudioWakeup.js';
14
+ import { probeLmStudioRuntime as vramProbe } from '../vramService.js';
15
+
16
+ test('probeLmStudioRuntime returns safe offline defaults when unreachable', async () => {
17
+ clearLmStudioProbeCache();
18
+ const offlineUrl = 'http://127.0.0.1:9/v1';
19
+ const result = await probeLmStudioRuntime(offlineUrl, { timeoutMs: 500, bypassCache: true });
20
+
21
+ assert.equal(result.reachable, false);
22
+ assert.equal(result.loadedModel, null);
23
+ assert.deepEqual(result.loadedModels, []);
24
+ assert.equal(result.loadedContextLength, null);
25
+ assert.equal(result.quantization, null);
26
+ assert.deepEqual(result.models, []);
27
+ });
28
+
29
+ test('isLmStudioResponding and getLoadedModels return expected offline values', async () => {
30
+ clearLmStudioProbeCache();
31
+ const offlineUrl = 'http://127.0.0.1:9/v1';
32
+ const isResponding = await isLmStudioResponding(offlineUrl, 300);
33
+ assert.equal(isResponding, false);
34
+
35
+ const loaded = await getLoadedModels(offlineUrl, 300);
36
+ assert.deepEqual(loaded, []);
37
+ });
38
+
39
+ test('vramService re-exports probeLmStudioRuntime with identical signature and behavior', async () => {
40
+ clearLmStudioProbeCache();
41
+ const offlineUrl = 'http://127.0.0.1:9/v1';
42
+ const result = await vramProbe(offlineUrl);
43
+ assert.equal(result.reachable, false);
44
+ assert.equal(result.loadedModel, null);
45
+ assert.equal(result.loadedContextLength, null);
46
+ });
47
+
48
+ test('wakeUpLmStudio strictly skips non-local endpoints', async () => {
49
+ const remoteResult = await wakeUpLmStudio({ baseUrl: 'https://remote-llm-host.example.com/v1' });
50
+ assert.equal(remoteResult.ok, false);
51
+ assert.match(remoteResult.reason, /Non-local|non-standard/i);
52
+ });
53
+
54
+ test('probeLmStudioRuntime correctly parses proprietary /api/v0/models structure', async () => {
55
+ clearLmStudioProbeCache();
56
+ const server = createServer((req, res) => {
57
+ if (req.url === '/api/v0/models') {
58
+ res.writeHead(200, { 'Content-Type': 'application/json' });
59
+ res.end(JSON.stringify({
60
+ data: [
61
+ {
62
+ id: 'google/gemma-4-e4b',
63
+ type: 'llm',
64
+ state: 'loaded',
65
+ loaded_context_length: 32768,
66
+ quantization: 'q4_k_m'
67
+ },
68
+ {
69
+ id: 'qwen2.5-coder-7b',
70
+ type: 'llm',
71
+ state: 'not_loaded'
72
+ }
73
+ ]
74
+ }));
75
+ return;
76
+ }
77
+ res.writeHead(404);
78
+ res.end();
79
+ });
80
+
81
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
82
+ const port = server.address().port;
83
+ const mockUrl = `http://127.0.0.1:${port}/v1`;
84
+
85
+ try {
86
+ const probe = await probeLmStudioRuntime(mockUrl, { bypassCache: true });
87
+ assert.equal(probe.reachable, true);
88
+ assert.equal(probe.loadedModel, 'google/gemma-4-e4b');
89
+ assert.deepEqual(probe.loadedModels, ['google/gemma-4-e4b']);
90
+ assert.equal(probe.loadedContextLength, 32768);
91
+ assert.equal(probe.quantization, 'q4_k_m');
92
+ assert.deepEqual(probe.models, ['google/gemma-4-e4b', 'qwen2.5-coder-7b']);
93
+ } finally {
94
+ await new Promise(resolve => server.close(resolve));
95
+ }
96
+ });
97
+
98
+ test('probeLmStudioRuntime falls back to /v1/models if /api/v0/models returns 404', async () => {
99
+ clearLmStudioProbeCache();
100
+ const server = createServer((req, res) => {
101
+ if (req.url === '/v1/models') {
102
+ res.writeHead(200, { 'Content-Type': 'application/json' });
103
+ res.end(JSON.stringify({
104
+ data: [
105
+ { id: 'fallback-model-a' },
106
+ { id: 'fallback-model-b' }
107
+ ]
108
+ }));
109
+ return;
110
+ }
111
+ res.writeHead(404);
112
+ res.end();
113
+ });
114
+
115
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
116
+ const port = server.address().port;
117
+ const mockUrl = `http://127.0.0.1:${port}/v1`;
118
+
119
+ try {
120
+ const probe = await probeLmStudioRuntime(mockUrl, { bypassCache: true });
121
+ assert.equal(probe.reachable, true);
122
+ assert.equal(probe.loadedModel, 'fallback-model-a');
123
+ assert.deepEqual(probe.models, ['fallback-model-a', 'fallback-model-b']);
124
+ } finally {
125
+ await new Promise(resolve => server.close(resolve));
126
+ }
127
+ });
128
+
129
+ test('buildLmsLoadArgs constructs correct lms CLI args with GPU offload and threads', () => {
130
+ const base = buildLmsLoadArgs('google/gemma-4-e4b');
131
+ assert.deepEqual(base, [
132
+ 'load', 'google/gemma-4-e4b', '-y', '--identifier', 'google/gemma-4-e4b',
133
+ '--context-length', '32768', '--parallel', '1'
134
+ ]);
135
+
136
+ const withMax = buildLmsLoadArgs('test-model', { gpuOffload: 'max' });
137
+ assert.ok(withMax.includes('--gpu'));
138
+ assert.equal(withMax[withMax.indexOf('--gpu') + 1], 'max');
139
+
140
+ const withOff = buildLmsLoadArgs('test-model', { gpuOffload: 'off' });
141
+ assert.equal(withOff[withOff.indexOf('--gpu') + 1], 'off');
142
+
143
+ const withRatio = buildLmsLoadArgs('test-model', { gpuOffload: 0.75 });
144
+ assert.equal(withRatio[withRatio.indexOf('--gpu') + 1], '0.75');
145
+
146
+ const withCtx = buildLmsLoadArgs('test-model', { contextLength: 32768 });
147
+ assert.ok(withCtx.includes('--context-length'));
148
+ assert.equal(withCtx[withCtx.indexOf('--context-length') + 1], '32768');
149
+
150
+ const withThreads = buildLmsLoadArgs('test-model', { cpuThreads: 4 });
151
+ assert.ok(withThreads.includes('--parallel'));
152
+ assert.equal(withThreads[withThreads.indexOf('--parallel') + 1], '4');
153
+
154
+ const withAuto = buildLmsLoadArgs('test-model', { gpuOffload: 'auto' });
155
+ assert.ok(!withAuto.includes('--gpu'), 'auto should not add --gpu flag');
156
+
157
+ const withAll = buildLmsLoadArgs('test-model', { gpuOffload: 'max', cpuThreads: 8, contextLength: 16384 });
158
+ assert.ok(withAll.includes('--gpu'));
159
+ assert.ok(withAll.includes('--parallel'));
160
+ assert.ok(withAll.includes('--context-length'));
161
+ });
162
+
163
+ test('buildLmsLoadArgs maps layer counts and handles out-of-range values safely', () => {
164
+ const withLayers = buildLmsLoadArgs('test-model', { nGpuLayers: 29 });
165
+ assert.ok(withLayers.includes('--gpu'));
166
+ assert.equal(withLayers[withLayers.indexOf('--gpu') + 1], '0.88');
167
+
168
+ const with99Layers = buildLmsLoadArgs('test-model', { nGpuLayers: 99 });
169
+ assert.equal(with99Layers[with99Layers.indexOf('--gpu') + 1], 'max');
170
+
171
+ const with0Layers = buildLmsLoadArgs('test-model', { nGpuLayers: 0 });
172
+ assert.equal(with0Layers[with0Layers.indexOf('--gpu') + 1], 'off');
173
+
174
+ const withThreadsAlias = buildLmsLoadArgs('test-model', { threads: 6 });
175
+ assert.ok(withThreadsAlias.includes('--parallel'));
176
+ assert.equal(withThreadsAlias[withThreadsAlias.indexOf('--parallel') + 1], '6');
177
+
178
+ const badCtx = buildLmsLoadArgs('test-model', { contextLength: 100 });
179
+ assert.ok(!badCtx.includes('--context-length'), 'ctx < 512 should be ignored');
180
+
181
+ const badThreads = buildLmsLoadArgs('test-model', { cpuThreads: 999 });
182
+ assert.ok(!badThreads.includes('--parallel'), 'threads > 256 should be ignored');
183
+
184
+ const nullGpu = buildLmsLoadArgs('test-model', { gpuOffload: null });
185
+ assert.ok(!nullGpu.includes('--gpu'), 'null should not add --gpu');
186
+ });
187
+
188
+ test('buildLmsLoadArgs clamps --parallel to available CPU resources', () => {
189
+ const clamped = buildLmsLoadArgs('test-model', { cpuThreads: 64, availableCpus: 8 });
190
+ assert.ok(clamped.includes('--parallel'));
191
+ assert.equal(clamped[clamped.indexOf('--parallel') + 1], '8');
192
+
193
+ const within = buildLmsLoadArgs('test-model', { cpuThreads: 4, availableCpus: 8 });
194
+ assert.equal(within[within.indexOf('--parallel') + 1], '4');
195
+
196
+ const zero = buildLmsLoadArgs('test-model', { cpuThreads: 0, availableCpus: 8 });
197
+ assert.ok(!zero.includes('--parallel'), 'threads < 1 should be ignored');
198
+
199
+ const tinyHost = buildLmsLoadArgs('test-model', { parallel: 4, availableCpus: 1 });
200
+ assert.equal(tinyHost[tinyHost.indexOf('--parallel') + 1], '1');
201
+
202
+ const aliasClamped = buildLmsLoadArgs('test-model', { threads: 32, availableCpus: 6 });
203
+ assert.equal(aliasClamped[aliasClamped.indexOf('--parallel') + 1], '6');
204
+
205
+ assert.equal(resolveParallelSlotBudget(32, 8).value, 8);
206
+ assert.equal(resolveParallelSlotBudget(32, 8).capped, true);
207
+ assert.equal(resolveParallelSlotBudget(4, 8).value, 4);
208
+ assert.equal(resolveParallelSlotBudget(4, 8).capped, false);
209
+ assert.equal(resolveParallelSlotBudget(999, 8).value, undefined);
210
+ assert.equal(resolveParallelSlotBudget('abc', 8).value, undefined);
211
+ assert.equal(resolveParallelSlotBudget(4, 0).ceiling, 4);
212
+ });
213
+
214
+ test('normalizeModelLoadSettings round-trips persisted values correctly', async () => {
215
+ const { normalizeModelLoadSettings, MODEL_LOAD_DEFAULTS } = await import('../config.js');
216
+
217
+ const defaults = normalizeModelLoadSettings({});
218
+ assert.equal(defaults.gpuOffload, 'auto');
219
+ assert.equal(defaults.cpuThreads, 1);
220
+ assert.equal(defaults.contextLength, 32768);
221
+
222
+ const withMax = normalizeModelLoadSettings({ gpuOffload: 'max', cpuThreads: 4 });
223
+ assert.equal(withMax.gpuOffload, 'max');
224
+ assert.equal(withMax.cpuThreads, 4);
225
+
226
+ const withRatio = normalizeModelLoadSettings({ gpuOffload: '0.5' });
227
+ assert.equal(withRatio.gpuOffload, 0.5);
228
+
229
+ const resetThreads = normalizeModelLoadSettings({ cpuThreads: null }, withMax);
230
+ assert.equal(resetThreads.cpuThreads, null);
231
+ assert.equal(resetThreads.gpuOffload, 'max');
232
+
233
+ const clamped = normalizeModelLoadSettings({ cpuThreads: 999 });
234
+ assert.equal(clamped.cpuThreads, null, 'out-of-range threads should be rejected');
235
+
236
+ const badGpu = normalizeModelLoadSettings({ gpuOffload: 'invalid' });
237
+ assert.equal(badGpu.gpuOffload, 'auto', 'invalid GPU value should fall back to auto');
238
+ });
239
+
240
+ test('normalizeLmStudioModelKey resolves raw GGUF filenames to valid LM Studio keys', async () => {
241
+ const { normalizeLmStudioModelKey } = await import('../lmStudioWakeup.js');
242
+ assert.equal(normalizeLmStudioModelKey('gemma-4-E4B-it-Q4_K_M.gguf'), 'google/gemma-4-e4b');
243
+ assert.equal(normalizeLmStudioModelKey('C:\\models\\gemma-4-E4B-it-Q4_K_M.gguf'), 'google/gemma-4-e4b');
244
+ assert.equal(normalizeLmStudioModelKey('gemma-4-e4b-Q6_K.gguf'), 'google/gemma-4-e4b@q6_k');
245
+ assert.equal(normalizeLmStudioModelKey('gemma-4-e4b-Q8_0.gguf'), 'google/gemma-4-e4b@q8_0');
246
+ assert.equal(normalizeLmStudioModelKey('google/gemma-4-e4b'), 'google/gemma-4-e4b');
247
+ assert.equal(normalizeLmStudioModelKey('google/gemma-4-e4b@q4_k_m'), 'google/gemma-4-e4b@q4_k_m');
248
+ assert.equal(normalizeLmStudioModelKey('custom-chat-model.gguf'), 'custom-chat-model');
249
+ });
250
+
251
+ test('buildLmsLoadArgs strictly excludes unsupported CLI options (flash-attn, split-mode, tensor-split, etc.)', async () => {
252
+ const { getPersistedModelLoadSettings } = await import('../config.js');
253
+ const persisted = getPersistedModelLoadSettings();
254
+ const args = buildLmsLoadArgs('google/gemma-4-e4b', {
255
+ ...persisted,
256
+ flashAttn: true,
257
+ splitMode: 'layer',
258
+ tensorSplit: '20,12',
259
+ mainGpu: 0,
260
+ batchSize: 2048,
261
+ ubatchSize: 512,
262
+ });
263
+
264
+ assert.ok(!args.includes('--flash-attn'), 'lms load does not support --flash-attn');
265
+ assert.ok(!args.includes('--split-mode'), 'lms load does not support --split-mode');
266
+ assert.ok(!args.includes('--tensor-split'), 'lms load does not support --tensor-split');
267
+ assert.ok(!args.includes('--main-gpu'), 'lms load does not support --main-gpu');
268
+ assert.ok(!args.includes('--batch-size'), 'lms load does not support --batch-size');
269
+ assert.ok(!args.includes('--ubatch-size'), 'lms load does not support --ubatch-size');
270
+
271
+ const validPrefixes = ['load', '-y', '--identifier', '--gpu', '--context-length', '--parallel', '--ttl'];
272
+ for (const item of args) {
273
+ if (item.startsWith('-')) {
274
+ assert.ok(validPrefixes.includes(item), `Unexpected CLI option passed to lms load: ${item}`);
275
+ }
276
+ }
277
+ });
278
+
@@ -0,0 +1,278 @@
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { toolsSchema, toolHandlers } from '../tools.js';
4
+ import { dynamicToolRouter } from '../toolRouter.js';
5
+ import { buildSystemPrompt } from '../prompts.js';
6
+ import {
7
+ calculateHaversineDistance,
8
+ estimateWalkingTime,
9
+ estimateDrivingTime,
10
+ formatDistance,
11
+ findNearestLandmark,
12
+ buildOsmMapTag,
13
+ buildOsmUrl,
14
+ cleanLocationQuery,
15
+ LANDMARK_REGISTRY,
16
+ detectIpLocation,
17
+ fetchRealtimeWeather,
18
+ rankLandmarksByScore,
19
+ scoreLocationSearchResult,
20
+ rankLocationSearchResults,
21
+ scoreLocationConfidence
22
+ } from '../locationService.js';
23
+
24
+ describe('get_user_location & Precise Geolocation Pipeline', () => {
25
+ test('calculateHaversineDistance calculates accurate spherical distance', () => {
26
+ const distBangkokToChiangMai = calculateHaversineDistance(13.7563, 100.5018, 18.7883, 98.9853);
27
+ assert.ok(distBangkokToChiangMai > 570000 && distBangkokToChiangMai < 610000);
28
+
29
+ const distClose = calculateHaversineDistance(13.7563, 100.5018, 13.7567, 100.5018);
30
+ assert.ok(distClose < 100);
31
+ });
32
+
33
+ test('estimateWalkingTime and estimateDrivingTime calculate logical travel times', () => {
34
+ const walking1600m = estimateWalkingTime(1600);
35
+ assert.equal(walking1600m, 20);
36
+
37
+ const driving20km = estimateDrivingTime(20000);
38
+ assert.equal(driving20km, 30);
39
+
40
+ assert.equal(formatDistance(450), '450 m');
41
+ assert.equal(formatDistance(3200), '3.2 km');
42
+ });
43
+
44
+ test('findNearestLandmark accurately matches nearest landmark in Thailand and China', () => {
45
+ const nearDemocracy = findNearestLandmark(13.7563, 100.5018);
46
+ assert.ok(nearDemocracy);
47
+ assert.equal(nearDemocracy.landmark.id, 'democracy_monument');
48
+ assert.ok(nearDemocracy.distanceMeters < 100);
49
+
50
+ const nearHengdian = findNearestLandmark(29.1454, 120.3104);
51
+ assert.ok(nearHengdian);
52
+ assert.equal(nearHengdian.landmark.id, 'hengdian_studios');
53
+ assert.ok(nearHengdian.distanceMeters < 100);
54
+ });
55
+
56
+ test('buildOsmMapTag and buildOsmUrl format compliant tags and links', () => {
57
+ const tag = buildOsmMapTag(13.7563, 100.5018, 'Bangkok Center');
58
+ assert.equal(tag, '[MAP_OSM:13.7563,100.5018,Bangkok Center]');
59
+
60
+ const url = buildOsmUrl(13.7563, 100.5018, 16);
61
+ assert.ok(url.includes('openstreetmap.org/?mlat=13.7563&mlon=100.5018'));
62
+ });
63
+
64
+ test('cleanLocationQuery strips colloquial prefixes', () => {
65
+ assert.equal(cleanLocationQuery('ขอแผนที่สยามพารากอน'), 'สยามพารากอน');
66
+ assert.equal(cleanLocationQuery('พิกัดของพระราชวังต้องห้าม'), 'พระราชวังต้องห้าม');
67
+ assert.equal(cleanLocationQuery('where is Hengdian World Studios'), 'Hengdian World Studios');
68
+ });
69
+
70
+ test('toolsSchema includes get_user_location and search_location_map with correct schemas', () => {
71
+ const locTool = toolsSchema.find(t => (t.function?.name || t.name) === 'get_user_location');
72
+ assert.ok(locTool);
73
+ const locFn = locTool.function || locTool;
74
+ assert.equal(locFn.parameters.type, 'object');
75
+ assert.ok(locFn.parameters.properties.reason);
76
+ assert.ok(locFn.parameters.properties.query);
77
+ assert.ok(locFn.parameters.properties.latitude);
78
+ assert.ok(locFn.parameters.properties.longitude);
79
+
80
+ const mapTool = toolsSchema.find(t => (t.function?.name || t.name) === 'search_location_map');
81
+ assert.ok(mapTool);
82
+ const mapFn = mapTool.function || mapTool;
83
+ assert.equal(mapFn.parameters.type, 'object');
84
+ assert.ok(mapFn.parameters.properties.query);
85
+ assert.ok(mapFn.parameters.required.includes('query'));
86
+ });
87
+
88
+ test('toolHandlers.get_user_location returns enhanced coordinates and landmark context', async () => {
89
+ const mockClientLocation = {
90
+ latitude: 13.7563,
91
+ longitude: 100.5018,
92
+ accuracy: 12,
93
+ city: 'Bangkok',
94
+ country: 'Thailand',
95
+ source: 'gps'
96
+ };
97
+
98
+ const res = await toolHandlers.get_user_location(
99
+ { reason: 'Check current weather' },
100
+ { clientLocation: mockClientLocation }
101
+ );
102
+
103
+ assert.equal(res.status, 'success');
104
+ assert.equal(res.location_granted, true);
105
+ assert.equal(res.source, 'client_location');
106
+ assert.equal(res.latitude, 13.7563);
107
+ assert.equal(res.longitude, 100.5018);
108
+ assert.ok(res.nearest_landmark);
109
+ assert.equal(res.nearest_landmark.landmark.id, 'democracy_monument');
110
+ assert.ok(res.map_url);
111
+ assert.ok(res.map_tag.startsWith('[MAP_OSM:13.7563,100.5018'));
112
+ });
113
+
114
+ test('toolHandlers.get_user_location returns system or IP location fallback with nearest landmark', async () => {
115
+ const res = await toolHandlers.get_user_location(
116
+ { reason: 'Check local time and weather' },
117
+ {}
118
+ );
119
+
120
+ assert.equal(res.status, 'success');
121
+ assert.ok(typeof res.timezone === 'string' && res.timezone.length > 0);
122
+ assert.ok(typeof res.estimated_city === 'string');
123
+ assert.ok(typeof res.latitude === 'number');
124
+ assert.ok(typeof res.longitude === 'number');
125
+ assert.equal(res.location_granted, true);
126
+ assert.ok(res.source === 'ip_lookup' || res.source === 'system_timezone_estimate');
127
+ assert.ok(res.nearest_landmark);
128
+ assert.ok(res.map_tag);
129
+ });
130
+
131
+ test('toolHandlers.get_user_location handles client denied status gracefully', async () => {
132
+ const deniedClientLocation = {
133
+ denied: true
134
+ };
135
+
136
+ const res = await toolHandlers.get_user_location(
137
+ { reason: 'Check current weather' },
138
+ { clientLocation: deniedClientLocation }
139
+ );
140
+
141
+ assert.equal(res.status, 'permission_denied_or_unavailable');
142
+ assert.equal(res.location_granted, false);
143
+ assert.ok(res.message);
144
+ });
145
+
146
+ test('toolHandlers.search_location_map requires query and handles empty input gracefully', async () => {
147
+ const res = await toolHandlers.search_location_map({});
148
+ assert.equal(res.status, 'error');
149
+ assert.ok(res.message.includes('query'));
150
+ });
151
+
152
+ test('routeTools prioritizes get_user_location and search_location_map for weather and map queries', () => {
153
+ const weatherQueries = [
154
+ 'สภาพอากาศในตอนนี้',
155
+ 'อากาศวันนี้เป็นอย่างไรบ้าง',
156
+ 'What is the weather today?',
157
+ 'พยากรณ์อากาศกรุงเทพ',
158
+ 'ร้านอาหารใกล้ฉัน'
159
+ ];
160
+
161
+ for (const q of weatherQueries) {
162
+ const routed = dynamicToolRouter.routeToolsForQuery(q);
163
+ const names = routed.map(t => t.function?.name || t.name);
164
+ assert.ok(
165
+ names.includes('get_user_location') || names.includes('get_weather'),
166
+ `Query "${q}" should route to location or weather tools`
167
+ );
168
+ assert.ok(
169
+ names.includes('get_weather'),
170
+ `Query "${q}" should route to get_weather`
171
+ );
172
+ }
173
+
174
+ const mapQueries = [
175
+ 'ขอแผนที่สยามพารากอน',
176
+ 'พิกัดของโรงถ่ายเหิงเตี้ยน',
177
+ 'where is Shanghai Grand Theatre map',
178
+ 'เปิดแมพนำทางไปวัดพระแก้ว'
179
+ ];
180
+
181
+ for (const q of mapQueries) {
182
+ const routed = dynamicToolRouter.routeToolsForQuery(q);
183
+ const names = routed.map(t => t.function?.name || t.name);
184
+ assert.ok(
185
+ names.includes('search_location_map'),
186
+ `Query "${q}" should route to search_location_map`
187
+ );
188
+ }
189
+ });
190
+
191
+ test('buildSystemPrompt includes anti-refusal directive and map instructions', () => {
192
+ const prompt = buildSystemPrompt(['get_user_location', 'search_location_map']);
193
+ assert.ok(prompt.includes('get_user_location'));
194
+ assert.ok(prompt.includes('search_location_map'));
195
+ assert.ok(prompt.includes('MAP_OSM'));
196
+ assert.ok(
197
+ prompt.includes('สภาพอากาศ') ||
198
+ prompt.includes('location') ||
199
+ prompt.includes('พิกัด')
200
+ );
201
+ });
202
+
203
+ test('detectIpLocation resolves coordinates and city without crashing', async () => {
204
+ const loc = await detectIpLocation();
205
+ assert.ok(loc);
206
+ assert.ok(typeof loc.latitude === 'number');
207
+ assert.ok(typeof loc.longitude === 'number');
208
+ assert.ok(typeof loc.city === 'string');
209
+ assert.ok(typeof loc.country === 'string');
210
+ });
211
+
212
+ test('fetchRealtimeWeather retrieves real-time weather metrics from Open-Meteo', async () => {
213
+ const weather = await fetchRealtimeWeather(13.7563, 100.5018);
214
+ if (weather) {
215
+ assert.equal(weather.status, 'success');
216
+ assert.ok(typeof weather.temperature === 'number');
217
+ assert.ok(typeof weather.humidity === 'number');
218
+ assert.ok(typeof weather.summaryTh === 'string');
219
+ assert.ok(typeof weather.conditionTh === 'string');
220
+ }
221
+ });
222
+
223
+ test('toolHandlers.get_weather resolves weather for location or detected coordinates', async () => {
224
+ const res = await toolHandlers.get_weather({ location: 'Bangkok' });
225
+ assert.equal(res.status, 'success');
226
+ assert.ok(res.location.includes('Bangkok') || res.location.includes('กรุงเทพ'));
227
+ assert.ok(res.latitude);
228
+ assert.ok(res.longitude);
229
+ assert.ok(res.map_tag);
230
+
231
+ const autoRes = await toolHandlers.get_weather({});
232
+ assert.equal(autoRes.status, 'success');
233
+ assert.ok(autoRes.location);
234
+ assert.ok(autoRes.latitude);
235
+ assert.ok(autoRes.map_tag);
236
+ });
237
+
238
+ test('rankLandmarksByScore ranks landmarks using multi-factor scores', () => {
239
+ const ranked = rankLandmarksByScore(13.7563, 100.5018, { city: 'Bangkok' });
240
+ assert.ok(Array.isArray(ranked) && ranked.length > 0);
241
+ assert.ok(ranked[0].finalScore >= ranked[1].finalScore);
242
+ assert.ok(typeof ranked[0].distanceScore === 'number');
243
+ assert.ok(typeof ranked[0].prominenceScore === 'number');
244
+ assert.equal(ranked[0].landmark.id, 'democracy_monument');
245
+ });
246
+
247
+ test('rankLocationSearchResults ranks items by name match, type, and importance', () => {
248
+ const mockResults = [
249
+ { name: 'กรุงเทพมหานคร', displayName: 'กรุงเทพมหานคร, ประเทศไทย', type: 'city', category: 'administrative', importance: 0.8 },
250
+ { name: 'ซอยกรุงเทพ', displayName: 'ซอยกรุงเทพ, นนทบุรี', type: 'residential', category: 'highway', importance: 0.2 }
251
+ ];
252
+ const ranked = rankLocationSearchResults(mockResults, 'กรุงเทพมหานคร');
253
+ assert.equal(ranked[0].name, 'กรุงเทพมหานคร');
254
+ assert.ok(ranked[0].relevanceScore > ranked[1].relevanceScore);
255
+ });
256
+
257
+ test('scoreLocationConfidence computes quantitative confidence score and tier', () => {
258
+ const high = scoreLocationConfidence({ source: 'explicit_coordinates', accuracy: 15 });
259
+ assert.equal(high.confidenceTier, 'high');
260
+ assert.ok(high.score >= 90);
261
+ assert.equal(high.isEstimated, false);
262
+
263
+ const est = scoreLocationConfidence({ source: 'ip_lookup' });
264
+ assert.equal(est.confidenceTier, 'medium');
265
+ assert.equal(est.isEstimated, true);
266
+ });
267
+
268
+ test('toolHandlers.search_location_map bundles real-time weather and map pin for named venue', async () => {
269
+ const res = await toolHandlers.search_location_map({ query: 'CentralWorld' });
270
+ assert.equal(res.status, 'success');
271
+ assert.ok(res.results.length > 0);
272
+ assert.ok(res.map_tag);
273
+ if (res.weather) {
274
+ assert.ok(typeof res.weather.temperature === 'number');
275
+ assert.ok(res.message.includes('สภาพอากาศ') || res.message.includes('Weather'));
276
+ }
277
+ });
278
+ });