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.
- package/README.md +69 -12
- package/package.json +5 -12
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/AGENTS.md +228 -0
- package/server/agent/eval/analyze.py +684 -0
- package/server/agent/eval/gates.json +15 -0
- package/server/agent/eval/modules/analysis/compare.js +198 -0
- package/server/agent/eval/modules/analysis/gates.js +72 -0
- package/server/agent/eval/modules/analysis/judge.js +70 -0
- package/server/agent/eval/modules/core/cli.js +453 -0
- package/server/agent/eval/modules/core/config.js +60 -0
- package/server/agent/eval/modules/core/store.js +236 -0
- package/server/agent/eval/modules/evaluation/cases.js +689 -0
- package/server/agent/eval/modules/evaluation/runner.js +288 -0
- package/server/agent/eval/modules/evaluation/scoring.js +363 -0
- package/server/agent/eval/modules/evaluation/worker.js +122 -0
- package/server/agent/eval/modules/reporting/benchmark.js +265 -0
- package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
- package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
- package/server/agent/eval/modules/server/evalApi.js +1099 -0
- package/server/agent/eval/modules/ui/evalUi.js +4799 -0
- package/server/agent/eval/modules/utils/smokes.js +119 -0
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentBubbleSystem.test.js +65 -0
- package/server/agent/tests/agentFollowUps.test.js +253 -0
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +108 -0
- package/server/agent/tests/architectureFixes.test.js +81 -0
- package/server/agent/tests/architectureRefactoring.test.js +194 -0
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/autoResearch.test.js +64 -0
- package/server/agent/tests/bilibiliEmbed.test.js +296 -0
- package/server/agent/tests/catalogSync.test.js +130 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/citations.test.js +180 -0
- package/server/agent/tests/clarificationAnalytics.test.js +219 -0
- package/server/agent/tests/clarifyingQuestions.test.js +627 -0
- package/server/agent/tests/cliAutomation.test.js +158 -0
- package/server/agent/tests/cloudProviders.test.js +322 -0
- package/server/agent/tests/commandInput.test.js +24 -0
- package/server/agent/tests/commands.test.js +506 -0
- package/server/agent/tests/contextIntelligence.test.js +209 -0
- package/server/agent/tests/conversationSafety.test.js +53 -0
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +167 -0
- package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
- package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
- package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +131 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/entertainmentMetrics.test.js +126 -0
- package/server/agent/tests/envBootstrap.test.js +87 -0
- package/server/agent/tests/envCommand.test.js +284 -0
- package/server/agent/tests/errorHandling.test.js +80 -0
- package/server/agent/tests/errors.test.js +13 -0
- package/server/agent/tests/evalAnalyze.test.js +54 -0
- package/server/agent/tests/evalCommand.test.js +54 -0
- package/server/agent/tests/evalCorpus.test.js +126 -0
- package/server/agent/tests/evalHarness.test.js +62 -0
- package/server/agent/tests/evalJudgeContext.test.js +38 -0
- package/server/agent/tests/evalMetrics.test.js +320 -0
- package/server/agent/tests/evalStore.test.js +190 -0
- package/server/agent/tests/evalStudio.test.js +179 -0
- package/server/agent/tests/feedbackLearning.test.js +186 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/firstRun.test.js +44 -0
- package/server/agent/tests/gemma4eOptimization.test.js +95 -0
- package/server/agent/tests/hardwareTiers.test.js +206 -0
- package/server/agent/tests/hybridWebSearch.test.js +194 -0
- package/server/agent/tests/judgeService.test.js +251 -0
- package/server/agent/tests/lanDiscovery.test.js +324 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/mcpClient.test.js +201 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +141 -0
- package/server/agent/tests/multilingualScenarios.test.js +69 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/newCommands.test.js +347 -0
- package/server/agent/tests/newCommandsTui.test.js +141 -0
- package/server/agent/tests/newTools.test.js +113 -0
- package/server/agent/tests/newsIntelligence.test.js +178 -0
- package/server/agent/tests/nextGenArchitecture.test.js +101 -0
- package/server/agent/tests/offlineInput.test.js +217 -0
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/packagingGuard.test.js +57 -0
- package/server/agent/tests/presentation.test.js +61 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +44 -0
- package/server/agent/tests/queryBenchmarks.test.js +83 -0
- package/server/agent/tests/queryDetails.test.js +126 -0
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/realWorldValidation.test.js +391 -0
- package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
- package/server/agent/tests/runtimeWatchdog.test.js +32 -0
- package/server/agent/tests/sanitizer.test.js +37 -0
- package/server/agent/tests/scenariosData.js +378 -0
- package/server/agent/tests/schemaRouting.test.js +54 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +221 -0
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +146 -0
- package/server/agent/tests/securitySanitizer.test.js +38 -0
- package/server/agent/tests/sessionAndCopy.test.js +267 -0
- package/server/agent/tests/sidebarControls.test.js +135 -0
- package/server/agent/tests/statefulMemory.test.js +277 -0
- package/server/agent/tests/subagent.test.js +216 -0
- package/server/agent/tests/subagentProgress.test.js +109 -0
- package/server/agent/tests/subagentTextFallback.test.js +115 -0
- package/server/agent/tests/suggestions.test.js +199 -0
- package/server/agent/tests/suggestionsTui.test.js +251 -0
- package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
- package/server/agent/tests/terminalImage.test.js +80 -0
- package/server/agent/tests/terminalMarkdown.test.js +166 -0
- package/server/agent/tests/terminalText.test.js +133 -0
- package/server/agent/tests/textProtocolLoop.test.js +197 -0
- package/server/agent/tests/textToolParsing.test.js +187 -0
- package/server/agent/tests/theme.test.js +26 -0
- package/server/agent/tests/toolRegistryParity.test.js +53 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +160 -0
- package/server/agent/tests/tools.test.js +259 -0
- package/server/agent/tests/unifiedEndpoints.test.js +159 -0
- package/server/agent/tests/v110Features.test.js +262 -0
- package/server/agent/tests/versionBump.test.js +69 -0
- package/server/agent/tests/vramEstimator.test.js +595 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +551 -0
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webChatMemory.test.js +112 -0
- package/server/agent/tests/webChatSecurity.test.js +169 -0
- package/server/agent/tests/webExtract.test.js +284 -0
- package/server/agent/tests/webSources.test.js +281 -0
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/eval/judgeRubrics.js +110 -0
- package/server/eval/judgeService.js +290 -0
- package/server/eval/runBenchmark.js +182 -0
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/examples/librechat.example.yaml +16 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +162 -0
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +63 -0
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { parseCliArgs, resolveModelFallbackWarning } from '../cli.js';
|
|
7
|
+
import { isColorDisabled, setColorDisabled, ansiForeground, ansiBackground } from '../theme.js';
|
|
8
|
+
import { C } from '../ui.js';
|
|
9
|
+
import { PACKAGE_VERSION } from '../version.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const cliPath = path.resolve(__dirname, '../cli.js');
|
|
14
|
+
|
|
15
|
+
test('parseCliArgs parses standard flags correctly', () => {
|
|
16
|
+
const opts1 = parseCliArgs(['--print', '-q', 'hello world']);
|
|
17
|
+
assert.equal(opts1.print, true);
|
|
18
|
+
assert.equal(opts1.query, 'hello world');
|
|
19
|
+
assert.equal(opts1.output, 'text');
|
|
20
|
+
|
|
21
|
+
const opts2 = parseCliArgs(['-p', '--output', 'json', 'test query']);
|
|
22
|
+
assert.equal(opts2.print, true);
|
|
23
|
+
assert.equal(opts2.output, 'json');
|
|
24
|
+
assert.equal(opts2.query, 'test query');
|
|
25
|
+
|
|
26
|
+
const opts3 = parseCliArgs(['--plain', '--no-color']);
|
|
27
|
+
assert.equal(opts3.plain, true);
|
|
28
|
+
assert.equal(opts3.noColor, true);
|
|
29
|
+
|
|
30
|
+
const opts4 = parseCliArgs(['--provider', 'openai', '--model', 'gpt-5.6-luna', '--thinking', 'hide']);
|
|
31
|
+
assert.equal(opts4.provider, 'openai');
|
|
32
|
+
assert.equal(opts4.model, 'gpt-5.6-luna');
|
|
33
|
+
assert.equal(opts4.thinking, 'hide');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('parseCliArgs enforces valid output format', () => {
|
|
37
|
+
|
|
38
|
+
const textOpts = parseCliArgs(['--output', 'text']);
|
|
39
|
+
assert.equal(textOpts.output, 'text');
|
|
40
|
+
|
|
41
|
+
const jsonOpts = parseCliArgs(['--output', 'json']);
|
|
42
|
+
assert.equal(jsonOpts.output, 'json');
|
|
43
|
+
assert.equal(jsonOpts.print, true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('parseCliArgs handles --n-gpu-layers, --gpu-offload, and --threads flags', () => {
|
|
47
|
+
const opts1 = parseCliArgs(['--n-gpu-layers', '29', '--threads', '8']);
|
|
48
|
+
assert.equal(opts1.gpuOffload, '29');
|
|
49
|
+
assert.equal(opts1.nGpuLayers, '29');
|
|
50
|
+
assert.equal(opts1.cpuThreads, 8);
|
|
51
|
+
|
|
52
|
+
const opts2 = parseCliArgs(['--gpu-offload', 'max', '--cpu-threads', '4']);
|
|
53
|
+
assert.equal(opts2.gpuOffload, 'max');
|
|
54
|
+
assert.equal(opts2.cpuThreads, 4);
|
|
55
|
+
|
|
56
|
+
const opts3 = parseCliArgs(['-ngl', '0']);
|
|
57
|
+
assert.equal(opts3.gpuOffload, '0');
|
|
58
|
+
assert.equal(opts3.nGpuLayers, '0');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('isColorDisabled and setColorDisabled honor NO_COLOR standards', () => {
|
|
62
|
+
setColorDisabled(true);
|
|
63
|
+
assert.equal(isColorDisabled(), true);
|
|
64
|
+
assert.equal(ansiForeground('255;110;180'), '');
|
|
65
|
+
assert.equal(ansiBackground('12;12;14'), '');
|
|
66
|
+
assert.equal(C.reset, '');
|
|
67
|
+
assert.equal(C.bold, '');
|
|
68
|
+
assert.equal(C.sakura, '');
|
|
69
|
+
assert.equal(C.white, '');
|
|
70
|
+
|
|
71
|
+
setColorDisabled(false);
|
|
72
|
+
assert.equal(isColorDisabled(), false);
|
|
73
|
+
assert.notEqual(C.reset, '');
|
|
74
|
+
assert.notEqual(C.bold, '');
|
|
75
|
+
assert.ok(C.sakura.includes('\x1b[38;2;'));
|
|
76
|
+
|
|
77
|
+
setColorDisabled(null);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('CLI process exits with code 0 on --version', () => {
|
|
81
|
+
const res = spawnSync(process.execPath, [cliPath, '--version'], { encoding: 'utf8' });
|
|
82
|
+
assert.equal(res.status, 0);
|
|
83
|
+
assert.equal(res.stdout.trim(), PACKAGE_VERSION);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('CLI process exits with code 0 and skips welcome hero on --help --plain', () => {
|
|
87
|
+
const res = spawnSync(process.execPath, [cliPath, '--help', '--plain'], { encoding: 'utf8' });
|
|
88
|
+
assert.equal(res.status, 0);
|
|
89
|
+
assert.ok(res.stdout.includes('CLI Flags & Options'));
|
|
90
|
+
assert.ok(res.stdout.includes('--print'));
|
|
91
|
+
assert.ok(res.stdout.includes('--output'));
|
|
92
|
+
|
|
93
|
+
assert.equal(/\x1b\[[0-9;]*m/.test(res.stdout), false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('CLI process exits with code 3 on unknown option', () => {
|
|
97
|
+
const res = spawnSync(process.execPath, [cliPath, '--unknown-option-xyz'], { encoding: 'utf8' });
|
|
98
|
+
assert.equal(res.status, 3);
|
|
99
|
+
assert.ok(res.stderr.includes('Unknown option: --unknown-option-xyz'));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('CLI process exits with code 3 on invalid --output format', () => {
|
|
103
|
+
const res = spawnSync(process.execPath, [cliPath, '--output', 'yaml'], { encoding: 'utf8' });
|
|
104
|
+
assert.equal(res.status, 3);
|
|
105
|
+
assert.ok(res.stderr.includes('Invalid --output format: yaml'));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('CLI process exits with code 2 and outputs valid JSON envelope when server unreachable', () => {
|
|
109
|
+
const res = spawnSync(process.execPath, [cliPath, '--url', 'http://127.0.0.1:9999/v1', '-q', 'test query', '--output', 'json'], { encoding: 'utf8' });
|
|
110
|
+
assert.equal(res.status, 2);
|
|
111
|
+
const payload = JSON.parse(res.stdout);
|
|
112
|
+
assert.equal(payload.version, PACKAGE_VERSION);
|
|
113
|
+
assert.equal(payload.status, 'error');
|
|
114
|
+
assert.equal(payload.query, 'test query');
|
|
115
|
+
assert.ok(payload.error);
|
|
116
|
+
assert.ok(Array.isArray(payload.tools));
|
|
117
|
+
assert.ok(payload.metrics && typeof payload.metrics.durationMs === 'number');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('CLI process exits with code 2 and outputs clean stderr message on --print when server unreachable', () => {
|
|
121
|
+
const res = spawnSync(process.execPath, [cliPath, '--url', 'http://127.0.0.1:9999/v1', '-q', 'test query', '--print'], { encoding: 'utf8' });
|
|
122
|
+
assert.equal(res.status, 2);
|
|
123
|
+
assert.ok(res.stderr.includes('Error: Cannot reach'));
|
|
124
|
+
assert.equal(/\x1b\[[0-9;]*m/.test(res.stderr), false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('CLI process ingests piped stdin as query', () => {
|
|
128
|
+
const res = spawnSync(process.execPath, [cliPath, '--url', 'http://127.0.0.1:9999/v1', '--output', 'json'], {
|
|
129
|
+
input: 'query from piped stdin',
|
|
130
|
+
encoding: 'utf8'
|
|
131
|
+
});
|
|
132
|
+
assert.equal(res.status, 2);
|
|
133
|
+
const payload = JSON.parse(res.stdout);
|
|
134
|
+
assert.equal(payload.query, 'query from piped stdin');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('resolveModelFallbackWarning never reports a null requested model', () => {
|
|
138
|
+
assert.equal(
|
|
139
|
+
resolveModelFallbackWarning({ usedFallback: true, requestedModel: 'google/gemma-4-e4b', selectedModel: 'other/model' }, { model: null }),
|
|
140
|
+
null,
|
|
141
|
+
'no --model flag means no explicit request, so no fallback warning'
|
|
142
|
+
);
|
|
143
|
+
assert.equal(
|
|
144
|
+
resolveModelFallbackWarning({ usedFallback: false, selectedModel: 'google/gemma-4-e4b' }, { model: 'google/gemma-4-e4b' }),
|
|
145
|
+
null,
|
|
146
|
+
'exact match must not warn'
|
|
147
|
+
);
|
|
148
|
+
assert.deepEqual(
|
|
149
|
+
resolveModelFallbackWarning({ usedFallback: true, requestedModel: 'custom/model', selectedModel: 'google/gemma-4-e4b' }, { model: 'custom/model' }),
|
|
150
|
+
{ requested: 'custom/model', selected: 'google/gemma-4-e4b' }
|
|
151
|
+
);
|
|
152
|
+
assert.deepEqual(
|
|
153
|
+
resolveModelFallbackWarning({ usedFallback: true, selectedModel: 'google/gemma-4-e4b' }, { model: 'custom/model' }),
|
|
154
|
+
{ requested: 'custom/model', selected: 'google/gemma-4-e4b' },
|
|
155
|
+
'falls back to the raw --model flag when the resolver omits the normalized name'
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { CLOUD_PROVIDERS, resolveDefaultProvider, getProvider, getProviderList, getRecommendedModels, normalizeProviderModel } from '../config.js';
|
|
5
|
+
import { LLMClient, LMStudioClient } from '../llmClient.js';
|
|
6
|
+
import { ModelManager } from '../modelManager.js';
|
|
7
|
+
import { routeCommand } from '../commands.js';
|
|
8
|
+
|
|
9
|
+
test('cloud providers registry exposes all canonical cloud providers and defaults', () => {
|
|
10
|
+
const expected = ['lmstudio', 'vllm', 'ollama', 'openai', 'openrouter', 'groq', 'deepseek', 'gemini', 'anthropic', 'custom'];
|
|
11
|
+
for (const id of expected) {
|
|
12
|
+
assert.ok(CLOUD_PROVIDERS[id], `Provider "${id}" must be registered in CLOUD_PROVIDERS`);
|
|
13
|
+
assert.ok(CLOUD_PROVIDERS[id].baseUrl !== undefined);
|
|
14
|
+
assert.ok(CLOUD_PROVIDERS[id].defaultModel);
|
|
15
|
+
assert.ok(CLOUD_PROVIDERS[id].docUrl, `Provider "${id}" must have a docUrl`);
|
|
16
|
+
assert.ok(CLOUD_PROVIDERS[id].apiRefUrl, `Provider "${id}" must have an apiRefUrl`);
|
|
17
|
+
assert.ok(Array.isArray(CLOUD_PROVIDERS[id].recommendedModels), `Provider "${id}" must have recommendedModels array`);
|
|
18
|
+
assert.ok(CLOUD_PROVIDERS[id].recommendedModels.length > 0, `Provider "${id}" must have at least one recommended model`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('provider helper functions return correct definitions and models', () => {
|
|
23
|
+
const list = getProviderList();
|
|
24
|
+
assert.equal(list.length, 10);
|
|
25
|
+
|
|
26
|
+
const groq = getProvider('groq');
|
|
27
|
+
assert.equal(groq.id, 'groq');
|
|
28
|
+
assert.equal(groq.name, 'Groq Cloud');
|
|
29
|
+
assert.ok(groq.defaultModel === 'openai/gpt-oss-120b' || groq.defaultModel === 'openai/gpt-oss-20b');
|
|
30
|
+
assert.deepEqual(
|
|
31
|
+
groq.recommendedModels.map(model => model.id),
|
|
32
|
+
['openai/gpt-oss-120b', 'openai/gpt-oss-20b']
|
|
33
|
+
);
|
|
34
|
+
assert.equal(normalizeProviderModel('groq', 'gpt-oss-120b'), 'openai/gpt-oss-120b');
|
|
35
|
+
|
|
36
|
+
const models = getRecommendedModels('gemini');
|
|
37
|
+
assert.equal(getProvider('gemini').defaultModel, 'gemini-3.7-flash');
|
|
38
|
+
assert.deepEqual(models.map(m => m.id), ['gemini-3.7-flash', 'gemini-3.5-flash-lite', 'gemini-3.6-flash']);
|
|
39
|
+
assert.equal(normalizeProviderModel('gemini', 'gemini-3.1-pro'), 'gemini-3.1-pro-preview');
|
|
40
|
+
|
|
41
|
+
const openaiModels = getRecommendedModels('openai');
|
|
42
|
+
assert.equal(getProvider('openai').defaultModel, 'gpt-5.6-luna');
|
|
43
|
+
assert.deepEqual(openaiModels.map(m => m.id), ['gpt-5.6-luna', 'gpt-5.6-terra', 'gpt-5.6-sol']);
|
|
44
|
+
assert.equal(normalizeProviderModel('openai', 'o3-mini'), 'gpt-5.6-luna');
|
|
45
|
+
|
|
46
|
+
assert.equal(getProvider('openrouter').defaultModel, 'openrouter/auto');
|
|
47
|
+
assert.equal(normalizeProviderModel('openrouter', 'auto'), 'openrouter/auto');
|
|
48
|
+
|
|
49
|
+
assert.equal(getProvider('deepseek').defaultModel, 'deepseek-v4-flash');
|
|
50
|
+
assert.equal(getProvider('deepseek').baseUrl, 'https://api.deepseek.com');
|
|
51
|
+
assert.equal(normalizeProviderModel('deepseek', 'deepseek-chat'), 'deepseek-v4-flash');
|
|
52
|
+
assert.equal(normalizeProviderModel('deepseek', 'deepseek-reasoner'), 'deepseek-v4-flash');
|
|
53
|
+
|
|
54
|
+
const lmstudioModels = getRecommendedModels('lmstudio');
|
|
55
|
+
assert.ok(lmstudioModels.some(m => m.id.startsWith('google/gemma-4-e4b')));
|
|
56
|
+
assert.ok(lmstudioModels.some(m => m.id.includes('Q4_K_M') || m.id === 'google/gemma-4-e4b'));
|
|
57
|
+
assert.ok(lmstudioModels.some(m => m.id.includes('Q6_K')));
|
|
58
|
+
assert.ok(lmstudioModels.some(m => m.id.includes('Q8_0')));
|
|
59
|
+
assert.equal(normalizeProviderModel('lmstudio', 'q8_0'), 'google/gemma-4-e4b-Q8_0');
|
|
60
|
+
assert.equal(normalizeProviderModel('lmstudio', 'q8'), 'google/gemma-4-e4b-Q8_0');
|
|
61
|
+
assert.equal(normalizeProviderModel('lmstudio', 'q6_k'), 'google/gemma-4-e4b-Q6_K');
|
|
62
|
+
assert.equal(normalizeProviderModel('lmstudio', 'q4_k_m'), 'google/gemma-4-e4b-Q4_K_M');
|
|
63
|
+
assert.equal(normalizeProviderModel('lmstudio', 'gemma'), 'google/gemma-4-e4b');
|
|
64
|
+
|
|
65
|
+
const vllm = getProvider('vllm');
|
|
66
|
+
assert.equal(vllm.id, 'vllm');
|
|
67
|
+
assert.equal(vllm.name, 'vLLM (Local / WSL)');
|
|
68
|
+
assert.equal(vllm.isLocal, true);
|
|
69
|
+
assert.ok(vllm.platformNotice.includes('You cannot install vLLM natively on Windows'));
|
|
70
|
+
assert.equal(normalizeProviderModel('vllm', 'gemma'), 'google/gemma-4-e4b');
|
|
71
|
+
assert.equal(normalizeProviderModel('vllm', 'qwen'), 'Qwen/Qwen2.5-7B-Instruct');
|
|
72
|
+
|
|
73
|
+
const ollama = getProvider('ollama');
|
|
74
|
+
assert.equal(ollama.id, 'ollama');
|
|
75
|
+
assert.equal(ollama.name, 'Ollama (Local)');
|
|
76
|
+
assert.equal(ollama.isLocal, true);
|
|
77
|
+
assert.equal(ollama.defaultModel, 'qwen2.5:7b');
|
|
78
|
+
assert.equal(normalizeProviderModel('ollama', 'qwen'), 'qwen2.5:7b');
|
|
79
|
+
assert.equal(normalizeProviderModel('ollama', 'llama'), 'llama3.1:8b');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('LLMClient initializes with specified provider and supports switching', () => {
|
|
83
|
+
const client = new LLMClient({
|
|
84
|
+
provider: 'openai',
|
|
85
|
+
apiKey: 'sk-test-key-123',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const info = client.getProviderInfo();
|
|
89
|
+
assert.equal(info.provider, 'openai');
|
|
90
|
+
assert.equal(info.baseUrl, 'https://api.openai.com/v1');
|
|
91
|
+
assert.equal(info.hasApiKey, true);
|
|
92
|
+
assert.equal(info.isLocal, false);
|
|
93
|
+
assert.ok(info.docUrl);
|
|
94
|
+
assert.ok(info.pricingSummary);
|
|
95
|
+
|
|
96
|
+
const headers = client._buildHeaders();
|
|
97
|
+
assert.equal(headers['Authorization'], 'Bearer sk-test-key-123');
|
|
98
|
+
|
|
99
|
+
client.switchProvider('gemini', { apiKey: 'AIzaSy-test-key' });
|
|
100
|
+
assert.equal(client.provider, 'gemini');
|
|
101
|
+
assert.equal(client.baseUrl, 'https://generativelanguage.googleapis.com/v1beta/openai');
|
|
102
|
+
const geminiHeaders = client._buildHeaders();
|
|
103
|
+
assert.equal(geminiHeaders['Authorization'], 'Bearer AIzaSy-test-key');
|
|
104
|
+
assert.equal(geminiHeaders['x-goog-api-key'], 'AIzaSy-test-key');
|
|
105
|
+
|
|
106
|
+
client.switchProvider('groq', { apiKey: 'gsk-test-456' });
|
|
107
|
+
assert.equal(client.provider, 'groq');
|
|
108
|
+
assert.equal(client.baseUrl, 'https://api.groq.com/openai/v1');
|
|
109
|
+
assert.equal(client.apiKey, 'gsk-test-456');
|
|
110
|
+
|
|
111
|
+
client.switchProvider('openrouter', { apiKey: 'sk-or-test' });
|
|
112
|
+
const orHeaders = client._buildHeaders();
|
|
113
|
+
assert.equal(orHeaders['Authorization'], 'Bearer sk-or-test');
|
|
114
|
+
assert.ok(orHeaders['HTTP-Referer']);
|
|
115
|
+
assert.equal(orHeaders['X-OpenRouter-Title'], 'Kiku Agent');
|
|
116
|
+
assert.equal(orHeaders['X-Title'], undefined);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('LLMClient setApiKey and setModel update state dynamically', () => {
|
|
120
|
+
const client = new LLMClient({ provider: 'groq', apiKey: '' });
|
|
121
|
+
assert.equal(client.apiKey, '');
|
|
122
|
+
|
|
123
|
+
client.setApiKey('gsk-new-key-789');
|
|
124
|
+
assert.equal(client.apiKey, 'gsk-new-key-789');
|
|
125
|
+
|
|
126
|
+
client.setModel('gpt-oss-120b');
|
|
127
|
+
assert.equal(client.model, 'openai/gpt-oss-120b');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('LMStudioClient alias maintains backward compatibility with existing code', () => {
|
|
131
|
+
const legacyClient = new LMStudioClient({
|
|
132
|
+
baseUrl: 'http://127.0.0.1:1234/v1',
|
|
133
|
+
model: 'google/gemma-4-e4b',
|
|
134
|
+
});
|
|
135
|
+
assert.equal(legacyClient.baseUrl, 'http://127.0.0.1:1234/v1');
|
|
136
|
+
assert.equal(legacyClient.model, 'google/gemma-4-e4b');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('/provider command displays status, list, models, info, test, and switches provider', async () => {
|
|
140
|
+
const client = new LLMClient({ provider: 'lmstudio' });
|
|
141
|
+
const agent = { client };
|
|
142
|
+
|
|
143
|
+
const statusRes = await routeCommand('/provider status', { client, agent, sub: 'status' });
|
|
144
|
+
assert.equal(statusRes, 'HANDLED');
|
|
145
|
+
|
|
146
|
+
const listRes = await routeCommand('/provider list', { client, agent, sub: 'list' });
|
|
147
|
+
assert.equal(listRes, 'HANDLED');
|
|
148
|
+
|
|
149
|
+
const modelsRes = await routeCommand('/provider models groq', { client, agent, sub: 'models', extra: 'groq' });
|
|
150
|
+
assert.equal(modelsRes, 'HANDLED');
|
|
151
|
+
|
|
152
|
+
const infoRes = await routeCommand('/provider info openai', { client, agent, sub: 'info', extra: 'openai' });
|
|
153
|
+
assert.equal(infoRes, 'HANDLED');
|
|
154
|
+
|
|
155
|
+
const setKeyRes = await routeCommand('/provider setkey groq gsk-runtime-key', {
|
|
156
|
+
client, agent, sub: 'setkey', extra: 'groq gsk-runtime-key', parts: ['/provider', 'setkey', 'groq', 'gsk-runtime-key']
|
|
157
|
+
});
|
|
158
|
+
assert.equal(setKeyRes, 'HANDLED');
|
|
159
|
+
|
|
160
|
+
const useRes = await routeCommand('/provider use groq gpt-oss-120b', {
|
|
161
|
+
client, agent, sub: 'use', extra: 'groq gpt-oss-120b', parts: ['/provider', 'use', 'groq', 'gpt-oss-120b']
|
|
162
|
+
});
|
|
163
|
+
assert.equal(useRes, 'HANDLED');
|
|
164
|
+
assert.equal(client.provider, 'groq');
|
|
165
|
+
assert.equal(client.model, 'openai/gpt-oss-120b');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('LLMClient rejects unknown providers instead of silently using LM Studio', () => {
|
|
169
|
+
assert.throws(
|
|
170
|
+
() => new LLMClient({ provider: 'typo-provider' }),
|
|
171
|
+
/Unknown provider "typo-provider"/
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('Anthropic client uses native Messages API and normalizes tool use', async () => {
|
|
176
|
+
const originalFetch = globalThis.fetch;
|
|
177
|
+
let capturedUrl;
|
|
178
|
+
let capturedBody;
|
|
179
|
+
globalThis.fetch = async (url, options) => {
|
|
180
|
+
capturedUrl = url;
|
|
181
|
+
capturedBody = JSON.parse(options.body);
|
|
182
|
+
return new Response(JSON.stringify({
|
|
183
|
+
content: [
|
|
184
|
+
{ type: 'tool_use', id: 'toolu_1', name: 'search_works', input: { query: 'In Blossom' } }
|
|
185
|
+
],
|
|
186
|
+
usage: { input_tokens: 10, output_tokens: 5 }
|
|
187
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
188
|
+
};
|
|
189
|
+
try {
|
|
190
|
+
const client = new LLMClient({ provider: 'anthropic', apiKey: 'sk-ant-test' });
|
|
191
|
+
const response = await client.chat({
|
|
192
|
+
messages: [{ role: 'system', content: 'Use tools.' }, { role: 'user', content: 'Find In Blossom.' }],
|
|
193
|
+
tools: [{ type: 'function', function: { name: 'search_works', description: 'Search works', parameters: { type: 'object', properties: { query: { type: 'string' } } } } }],
|
|
194
|
+
stream: false,
|
|
195
|
+
});
|
|
196
|
+
assert.equal(capturedUrl, 'https://api.anthropic.com/v1/messages');
|
|
197
|
+
assert.equal(capturedBody.system, 'Use tools.');
|
|
198
|
+
assert.equal(capturedBody.tools[0].input_schema.type, 'object');
|
|
199
|
+
assert.equal(response.tool_calls[0].function.name, 'search_works');
|
|
200
|
+
assert.deepEqual(JSON.parse(response.tool_calls[0].function.arguments), { query: 'In Blossom' });
|
|
201
|
+
} finally {
|
|
202
|
+
globalThis.fetch = originalFetch;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('GPT-5.6 Chat Completions payload uses supported token and tool fields', async () => {
|
|
207
|
+
const originalFetch = globalThis.fetch;
|
|
208
|
+
let payload;
|
|
209
|
+
globalThis.fetch = async (_url, options) => {
|
|
210
|
+
payload = JSON.parse(options.body);
|
|
211
|
+
return new Response(JSON.stringify({
|
|
212
|
+
choices: [{ message: { role: 'assistant', content: 'ok' } }],
|
|
213
|
+
}), {
|
|
214
|
+
status: 200,
|
|
215
|
+
headers: { 'Content-Type': 'application/json' },
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const client = new LLMClient({ provider: 'openai', apiKey: 'sk-test' });
|
|
221
|
+
await client.chat({
|
|
222
|
+
messages: [{ role: 'user', content: 'hello' }],
|
|
223
|
+
tools: [{ type: 'function', function: { name: 'test_tool', parameters: { type: 'object' } } }],
|
|
224
|
+
stream: false,
|
|
225
|
+
});
|
|
226
|
+
assert.equal(payload.model, 'gpt-5.6-luna');
|
|
227
|
+
assert.equal(payload.max_completion_tokens, 8192);
|
|
228
|
+
assert.equal(payload.max_tokens, undefined);
|
|
229
|
+
assert.equal(payload.temperature, undefined);
|
|
230
|
+
assert.equal(payload.tool_choice, 'auto');
|
|
231
|
+
assert.equal(payload.tools.length, 1);
|
|
232
|
+
} finally {
|
|
233
|
+
globalThis.fetch = originalFetch;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('/model recommend displays recommended models for active provider', async () => {
|
|
238
|
+
const client = new LLMClient({ provider: 'gemini' });
|
|
239
|
+
const agent = { client };
|
|
240
|
+
|
|
241
|
+
const recRes = await routeCommand('/model recommend', { client, agent, sub: 'recommend' });
|
|
242
|
+
assert.equal(recRes, 'HANDLED');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('LLMClient checkConnection auto-detects loaded Q8_0 model in LM Studio and syncs model ID', async () => {
|
|
246
|
+
const originalFetch = globalThis.fetch;
|
|
247
|
+
globalThis.fetch = async (url) => {
|
|
248
|
+
if (url.endsWith('/models')) {
|
|
249
|
+
return new Response(JSON.stringify({
|
|
250
|
+
data: [
|
|
251
|
+
{ id: 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q4_K_M.gguf', state: 'not_loaded' },
|
|
252
|
+
{ id: 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q8_0.gguf', state: 'loaded' },
|
|
253
|
+
]
|
|
254
|
+
}), {
|
|
255
|
+
status: 200,
|
|
256
|
+
headers: { 'Content-Type': 'application/json' },
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return new Response('{}', { status: 200 });
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const client = new LLMClient({ provider: 'lmstudio', model: 'google/gemma-4-e4b' });
|
|
264
|
+
const health = await client.checkConnection();
|
|
265
|
+
assert.equal(health.connected, true);
|
|
266
|
+
assert.equal(health.activeLoadedModel, 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q8_0.gguf');
|
|
267
|
+
assert.equal(health.activeModel, 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q8_0.gguf');
|
|
268
|
+
assert.equal(health.isModelAvailable, true);
|
|
269
|
+
assert.equal(client.model, 'bartowski/gemma-4-e4b-GGUF/gemma-4-e4b-Q8_0.gguf');
|
|
270
|
+
} finally {
|
|
271
|
+
globalThis.fetch = originalFetch;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('LLMClient checkConnection probes keyless local custom OpenAI-compatible endpoints', async () => {
|
|
276
|
+
const originalFetch = globalThis.fetch;
|
|
277
|
+
let probed = false;
|
|
278
|
+
globalThis.fetch = async (url) => {
|
|
279
|
+
if (url.endsWith('/models')) {
|
|
280
|
+
probed = true;
|
|
281
|
+
return new Response(JSON.stringify({ data: [{ id: 'google/gemma-4-e4b', state: 'loaded' }] }), {
|
|
282
|
+
status: 200,
|
|
283
|
+
headers: { 'Content-Type': 'application/json' },
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return new Response('{}', { status: 200 });
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const client = new LLMClient({ provider: 'custom', baseUrl: 'http://127.0.0.1:1234/v1', apiKey: '' });
|
|
291
|
+
const health = await client.checkConnection();
|
|
292
|
+
assert.equal(probed, true);
|
|
293
|
+
assert.equal(health.connected, true);
|
|
294
|
+
assert.deepEqual(health.models, ['google/gemma-4-e4b']);
|
|
295
|
+
} finally {
|
|
296
|
+
globalThis.fetch = originalFetch;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('LLMClient checkConnection still requires an API key for remote custom gateways', async () => {
|
|
301
|
+
const originalFetch = globalThis.fetch;
|
|
302
|
+
let probed = false;
|
|
303
|
+
globalThis.fetch = async () => {
|
|
304
|
+
probed = true;
|
|
305
|
+
return new Response('{}', { status: 200 });
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const client = new LLMClient({ provider: 'custom', baseUrl: 'https://gateway.example.com/v1', apiKey: '' });
|
|
310
|
+
const health = await client.checkConnection();
|
|
311
|
+
assert.equal(probed, false);
|
|
312
|
+
assert.equal(health.connected, false);
|
|
313
|
+
assert.match(health.error, /No API key configured/);
|
|
314
|
+
assert.match(health.error, /LLM_API_KEY/);
|
|
315
|
+
} finally {
|
|
316
|
+
globalThis.fetch = originalFetch;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('custom provider envKey points at the API key variable', () => {
|
|
321
|
+
assert.equal(CLOUD_PROVIDERS.custom.envKey, 'LLM_API_KEY');
|
|
322
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { allowlistedValue, boundedInteger, parseCommandInput, tokenizeCommandInput } from '../commandInput.js';
|
|
5
|
+
|
|
6
|
+
test('command parser supports multilingual quoted values, flags, and comparison segments', () => {
|
|
7
|
+
const parsed = parseCommandInput('/compare "บุปผารักอลวน" -- "花戎" --type work --limit=4');
|
|
8
|
+
assert.equal(parsed.command, '/compare');
|
|
9
|
+
assert.deepEqual(parsed.segments, ['บุปผารักอลวน', '花戎']);
|
|
10
|
+
assert.equal(parsed.flags.type, 'work');
|
|
11
|
+
assert.equal(parsed.flags.limit, '4');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('command parser rejects oversized or unterminated input', () => {
|
|
15
|
+
assert.throws(() => tokenizeCommandInput(`/find "${'a'.repeat(600)}`), /500 characters/);
|
|
16
|
+
assert.throws(() => tokenizeCommandInput('/find "In Blossom'), /unclosed quote/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('command value validators reject values outside their contracts', () => {
|
|
20
|
+
assert.equal(boundedInteger('7', 1, 1, 20), 7);
|
|
21
|
+
assert.throws(() => boundedInteger('0', 1, 1, 20), /1 to 20/);
|
|
22
|
+
assert.equal(allowlistedValue('SONG', ['work', 'song'], 'work'), 'song');
|
|
23
|
+
assert.throws(() => allowlistedValue('file', ['work', 'song'], 'work'), /work, song/);
|
|
24
|
+
});
|