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,133 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getGraphemeSegments,
|
|
6
|
+
getStringDisplayWidth,
|
|
7
|
+
getNewOutputScrollOffset,
|
|
8
|
+
fitStyledText,
|
|
9
|
+
getWelcomeHeroLines,
|
|
10
|
+
getTuiViewportLayout,
|
|
11
|
+
getMultilinePromptLayout,
|
|
12
|
+
nextGraphemeBoundary,
|
|
13
|
+
previousGraphemeBoundary,
|
|
14
|
+
sanitizeTerminalText,
|
|
15
|
+
sliceLineForDisplay,
|
|
16
|
+
} from '../ui.js';
|
|
17
|
+
|
|
18
|
+
test('TUI viewport never overlaps the status bar', () => {
|
|
19
|
+
for (const rows of [10, 24, 30, 60]) {
|
|
20
|
+
const layout = getTuiViewportLayout(rows);
|
|
21
|
+
const viewportLastRow = layout.viewportStartRow + layout.viewportHeight - 1;
|
|
22
|
+
|
|
23
|
+
assert.equal(viewportLastRow, layout.statusBarRow - 1);
|
|
24
|
+
assert.ok(viewportLastRow < layout.statusBarRow);
|
|
25
|
+
assert.ok(layout.statusBarRow < layout.promptRow);
|
|
26
|
+
assert.ok(layout.promptRow < layout.separatorRow);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('long output opens at its first page and preserves manual scroll position', () => {
|
|
31
|
+
const viewportHeight = getTuiViewportLayout(24).viewportHeight;
|
|
32
|
+
assert.equal(getNewOutputScrollOffset(viewportHeight - 1, 24, 0), 0);
|
|
33
|
+
assert.equal(getNewOutputScrollOffset(viewportHeight + 7, 24, 0), 7);
|
|
34
|
+
assert.equal(getNewOutputScrollOffset(5, 24, 9), 14);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('welcome hero keeps the logo without the removed marketing copy', () => {
|
|
38
|
+
const hero = getWelcomeHeroLines().join('\n');
|
|
39
|
+
|
|
40
|
+
assert.match(hero, /██╗/);
|
|
41
|
+
assert.doesNotMatch(hero, /Kiku AI Terminal Agent/);
|
|
42
|
+
assert.doesNotMatch(hero, /Intelligent Fan Portal/);
|
|
43
|
+
assert.doesNotMatch(hero, /SSOT • DAG • Dynamic Tools/);
|
|
44
|
+
assert.doesNotMatch(hero, /LM Studio \/ Cloud Providers/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('grapheme boundaries keep emoji and Thai combining marks intact', () => {
|
|
48
|
+
const value = 'A👩🏽💻ก้B';
|
|
49
|
+
const segments = getGraphemeSegments(value);
|
|
50
|
+
|
|
51
|
+
assert.deepEqual(segments.map(item => item.segment), ['A', '👩🏽💻', 'ก้', 'B']);
|
|
52
|
+
assert.equal(nextGraphemeBoundary(value, 1), 8);
|
|
53
|
+
assert.equal(previousGraphemeBoundary(value, 8), 1);
|
|
54
|
+
assert.equal(nextGraphemeBoundary(value, 8), 10);
|
|
55
|
+
assert.equal(previousGraphemeBoundary(value, 10), 8);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('untrusted terminal text cannot inject ANSI, OSC, or control sequences', () => {
|
|
59
|
+
const malicious = '\x1b[31mred\x1b[0m\x1b]0;owned\x07\nnext\titem';
|
|
60
|
+
const sanitized = sanitizeTerminalText(malicious);
|
|
61
|
+
assert.equal(sanitized, 'red next item');
|
|
62
|
+
assert.equal(sanitized.includes('\x1b'), false);
|
|
63
|
+
assert.equal(sanitized.includes('\x07'), false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('gray surface rows fit narrow terminals without breaking styled graphemes', () => {
|
|
67
|
+
const fitted = fitStyledText('\x1b[1mคำสั่ง 👩🏽💻 autocomplete\x1b[0m', 10);
|
|
68
|
+
|
|
69
|
+
assert.ok(getStringDisplayWidth(fitted) <= 10);
|
|
70
|
+
assert.ok(!fitted.includes('👩') || fitted.includes('👩🏽💻'));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('display width treats joined emoji and Thai marks as terminal graphemes', () => {
|
|
74
|
+
assert.equal(getStringDisplayWidth('👩🏽💻'), 2);
|
|
75
|
+
assert.equal(getStringDisplayWidth('ก้'), 1);
|
|
76
|
+
assert.equal(getStringDisplayWidth('你好'), 4);
|
|
77
|
+
assert.equal(getStringDisplayWidth('《鞠婧祎》'), 10);
|
|
78
|
+
assert.equal(getStringDisplayWidth('(年度最佳)'), 12);
|
|
79
|
+
assert.equal(getStringDisplayWidth('【花间令】'), 10);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('long-line viewport stays within width without splitting graphemes', () => {
|
|
83
|
+
const value = 'aaaaa👩🏽💻ก้bbbbbbbbbbbb';
|
|
84
|
+
const cursor = value.indexOf('ก');
|
|
85
|
+
const viewport = sliceLineForDisplay(value, cursor, 10);
|
|
86
|
+
|
|
87
|
+
assert.ok(getStringDisplayWidth(viewport.displayLine) <= 10);
|
|
88
|
+
assert.ok(!viewport.displayLine.includes('👩') || viewport.displayLine.includes('👩🏽💻'));
|
|
89
|
+
assert.ok(!viewport.displayLine.startsWith('้'));
|
|
90
|
+
assert.ok(viewport.cursorVisualOffset >= 0);
|
|
91
|
+
assert.ok(viewport.cursorVisualOffset <= 10);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('getMultilinePromptLayout wraps long text and positions cursor accurately', () => {
|
|
95
|
+
const single = getMultilinePromptLayout('hello world', 5, 20, 5);
|
|
96
|
+
assert.equal(single.visibleLines.length, 1);
|
|
97
|
+
assert.equal(single.visibleLines[0], 'hello world');
|
|
98
|
+
assert.equal(single.cursorRow, 0);
|
|
99
|
+
assert.equal(single.cursorCol, 5);
|
|
100
|
+
|
|
101
|
+
const multi = getMultilinePromptLayout('line 1\nline 2\nline 3', 10, 20, 5);
|
|
102
|
+
assert.equal(multi.visibleLines.length, 3);
|
|
103
|
+
assert.equal(multi.visibleLines[0], 'line 1');
|
|
104
|
+
assert.equal(multi.visibleLines[1], 'line 2');
|
|
105
|
+
assert.equal(multi.visibleLines[2], 'line 3');
|
|
106
|
+
assert.equal(multi.cursorRow, 1);
|
|
107
|
+
assert.equal(multi.cursorCol, 3);
|
|
108
|
+
|
|
109
|
+
const longText = 'Ju Jingyi 👩🏽💻 ก้ ' + 'a'.repeat(50);
|
|
110
|
+
const wrapped = getMultilinePromptLayout(longText, longText.length, 25, 4);
|
|
111
|
+
assert.ok(wrapped.visibleLines.length > 1);
|
|
112
|
+
assert.ok(wrapped.visibleLines.length <= 4);
|
|
113
|
+
for (const line of wrapped.visibleLines) {
|
|
114
|
+
assert.ok(getStringDisplayWidth(line) <= 25);
|
|
115
|
+
}
|
|
116
|
+
assert.ok(wrapped.cursorRow >= 0 && wrapped.cursorRow < wrapped.visibleLines.length);
|
|
117
|
+
assert.ok(wrapped.cursorCol >= 0 && wrapped.cursorCol <= 25);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('getTuiViewportLayout dynamically expands prompt rows without overlapping', () => {
|
|
121
|
+
for (const promptRows of [1, 2, 3, 5]) {
|
|
122
|
+
const layout = getTuiViewportLayout(24, promptRows);
|
|
123
|
+
const viewportLastRow = layout.viewportStartRow + layout.viewportHeight - 1;
|
|
124
|
+
|
|
125
|
+
assert.equal(viewportLastRow, layout.statusBarRow - 1);
|
|
126
|
+
assert.ok(viewportLastRow < layout.statusBarRow);
|
|
127
|
+
assert.ok(layout.statusBarRow < layout.promptStartRow);
|
|
128
|
+
assert.equal(layout.promptHeight, promptRows);
|
|
129
|
+
assert.equal(layout.promptRow, layout.promptStartRow);
|
|
130
|
+
assert.ok(layout.promptEndRow < 24);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
6
|
+
|
|
7
|
+
import { KikuAgent } from '../agentCore.js';
|
|
8
|
+
import { McpClientManager } from '../mcpClient.js';
|
|
9
|
+
|
|
10
|
+
function createMemoryStub() {
|
|
11
|
+
return {
|
|
12
|
+
_loaded: true,
|
|
13
|
+
facts: [],
|
|
14
|
+
async load() { },
|
|
15
|
+
trackTopicInterest() { },
|
|
16
|
+
buildMemoryContext() { return ''; },
|
|
17
|
+
buildUserSkillContext() { return ''; },
|
|
18
|
+
addFact() { },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('text-emitted tool calls synthesize protocol-valid tool_calls in history', async () => {
|
|
23
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'kiku-textproto-'));
|
|
24
|
+
const storageFile = path.join(tempDir, 'session.json');
|
|
25
|
+
const observedMessages = [];
|
|
26
|
+
let call = 0;
|
|
27
|
+
const client = {
|
|
28
|
+
provider: 'lmstudio',
|
|
29
|
+
providerName: 'LM Studio',
|
|
30
|
+
model: 'test-model',
|
|
31
|
+
async chat(options) {
|
|
32
|
+
call++;
|
|
33
|
+
observedMessages.push(options.messages.map(message => ({
|
|
34
|
+
role: message.role,
|
|
35
|
+
content: typeof message.content === 'string' ? message.content : null,
|
|
36
|
+
tool_calls: message.tool_calls || undefined,
|
|
37
|
+
tool_call_id: message.tool_call_id || undefined,
|
|
38
|
+
})));
|
|
39
|
+
if (call === 1) {
|
|
40
|
+
return {
|
|
41
|
+
role: 'assistant',
|
|
42
|
+
content: 'Checking the catalog.\n```json\n{"action": "get_quotes", "parameters": {"tag": "mindset"}}\n```',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { role: 'assistant', content: 'คำคมที่ยืนยันแล้วจากคลังข้อมูลคือ...' };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const agent = new KikuAgent({
|
|
50
|
+
client,
|
|
51
|
+
memory: createMemoryStub(),
|
|
52
|
+
mcpManager: new McpClientManager({
|
|
53
|
+
autoLoad: false,
|
|
54
|
+
globalConfigPath: path.join(tempDir, 'mcp.json'),
|
|
55
|
+
}),
|
|
56
|
+
sessionId: 'textproto_test',
|
|
57
|
+
sessionStorageFile: storageFile,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await agent.ready();
|
|
62
|
+
const answer = await agent.run('ขอคำคมของจวีจิ้งอีหน่อย');
|
|
63
|
+
|
|
64
|
+
assert.match(answer, /คำคมที่ยืนยันแล้ว/);
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
const secondCall = observedMessages[1];
|
|
68
|
+
const assistantWithCalls = secondCall.find(m => m.role === 'assistant' && m.tool_calls?.length > 0);
|
|
69
|
+
assert.ok(assistantWithCalls, 'assistant message with synthesized tool_calls missing');
|
|
70
|
+
assert.equal(assistantWithCalls.tool_calls[0].function.name, 'get_quotes');
|
|
71
|
+
|
|
72
|
+
const toolMessage = secondCall.find(m => m.role === 'tool');
|
|
73
|
+
assert.ok(toolMessage, 'tool result message missing');
|
|
74
|
+
assert.equal(toolMessage.tool_call_id, assistantWithCalls.tool_calls[0].id);
|
|
75
|
+
assert.match(toolMessage.content, /authoritative/);
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
assert.ok(!assistantWithCalls.content.includes('```'));
|
|
79
|
+
} finally {
|
|
80
|
+
await agent.mcpManager.disconnectAll().catch(() => { });
|
|
81
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('systemDirectives callbacks are injected as system messages without touching the user query', async () => {
|
|
86
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'kiku-directives-'));
|
|
87
|
+
const observedMessages = [];
|
|
88
|
+
const client = {
|
|
89
|
+
provider: 'lmstudio',
|
|
90
|
+
providerName: 'LM Studio',
|
|
91
|
+
model: 'test-model',
|
|
92
|
+
async chat(options) {
|
|
93
|
+
observedMessages.push(options.messages.map(message => ({ role: message.role, content: message.content })));
|
|
94
|
+
return { role: 'assistant', content: 'ตอบแล้วค่ะ' };
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const agent = new KikuAgent({
|
|
99
|
+
client,
|
|
100
|
+
memory: createMemoryStub(),
|
|
101
|
+
mcpManager: new McpClientManager({
|
|
102
|
+
autoLoad: false,
|
|
103
|
+
globalConfigPath: path.join(tempDir, 'mcp.json'),
|
|
104
|
+
}),
|
|
105
|
+
sessionId: 'directives_test',
|
|
106
|
+
sessionStorageFile: path.join(tempDir, 'session.json'),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await agent.ready();
|
|
111
|
+
await agent.run('สวัสดีค่ะ', {
|
|
112
|
+
systemDirectives: ['[SYSTEM DIRECTIVE (Test): stay concise]'],
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const lastCall = observedMessages.at(-1);
|
|
116
|
+
const directiveMessage = lastCall.find(m => m.role === 'system' && m.content.includes('[SYSTEM DIRECTIVE (Test): stay concise]'));
|
|
117
|
+
assert.ok(directiveMessage, 'directive must be injected as a system message');
|
|
118
|
+
|
|
119
|
+
const userMessage = lastCall.find(m => m.role === 'user');
|
|
120
|
+
assert.ok(userMessage);
|
|
121
|
+
assert.ok(!userMessage.content.includes('[SYSTEM DIRECTIVE (Test)'), 'user query must stay clean');
|
|
122
|
+
} finally {
|
|
123
|
+
await agent.mcpManager.disconnectAll().catch(() => { });
|
|
124
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('catalog tool returning not_found automatically escalates to external internet search and calls onToolRemove', async () => {
|
|
129
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'kiku-escalate-'));
|
|
130
|
+
const storageFile = path.join(tempDir, 'session.json');
|
|
131
|
+
const observedMessages = [];
|
|
132
|
+
const removedTools = [];
|
|
133
|
+
const startedTools = [];
|
|
134
|
+
let call = 0;
|
|
135
|
+
|
|
136
|
+
const client = {
|
|
137
|
+
provider: 'lmstudio',
|
|
138
|
+
providerName: 'LM Studio',
|
|
139
|
+
model: 'test-model',
|
|
140
|
+
async chat(options) {
|
|
141
|
+
call++;
|
|
142
|
+
observedMessages.push(options.messages.map(message => ({
|
|
143
|
+
role: message.role,
|
|
144
|
+
content: typeof message.content === 'string' ? message.content : null,
|
|
145
|
+
tool_calls: message.tool_calls || undefined,
|
|
146
|
+
tool_call_id: message.tool_call_id || undefined,
|
|
147
|
+
name: message.name || undefined
|
|
148
|
+
})));
|
|
149
|
+
if (call === 1) {
|
|
150
|
+
return {
|
|
151
|
+
role: 'assistant',
|
|
152
|
+
content: 'ขอตรวจสอบภาพในแค็ตตาล็อกก่อนค่ะ\n```json\n{"action": "get_gallery_photos", "parameters": {"query": "万花世界"}}\n```',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return { role: 'assistant', content: 'พบภาพจากแหล่งข่าวภายนอกแล้วค่ะ' };
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const agent = new KikuAgent({
|
|
160
|
+
client,
|
|
161
|
+
memory: createMemoryStub(),
|
|
162
|
+
mcpManager: new McpClientManager({
|
|
163
|
+
autoLoad: false,
|
|
164
|
+
globalConfigPath: path.join(tempDir, 'mcp.json'),
|
|
165
|
+
}),
|
|
166
|
+
sessionId: 'escalate_test',
|
|
167
|
+
sessionStorageFile: storageFile,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await agent.ready();
|
|
172
|
+
const answer = await agent.run('อยากเห็นภาพซีรีส์ 万花世界 (Wanhua Sin)', {
|
|
173
|
+
onToolStart: (name) => startedTools.push(name),
|
|
174
|
+
onToolRemove: (name) => removedTools.push(name),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
assert.match(answer, /พบภาพจากแหล่งข่าวภายนอกแล้วค่ะ/);
|
|
178
|
+
assert.ok(startedTools.includes('get_gallery_photos'), 'get_gallery_photos must have started');
|
|
179
|
+
assert.ok(removedTools.includes('get_gallery_photos'), 'get_gallery_photos must have been removed from UI');
|
|
180
|
+
assert.ok(
|
|
181
|
+
startedTools.includes('search_web_images') || startedTools.includes('web_search'),
|
|
182
|
+
'external internet search tool must have been triggered'
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const secondCall = observedMessages[1];
|
|
186
|
+
const toolMsg = secondCall.find(m => m.role === 'tool');
|
|
187
|
+
assert.ok(toolMsg, 'tool result message must be sent to model');
|
|
188
|
+
assert.ok(
|
|
189
|
+
toolMsg.name === 'search_web_images' || toolMsg.name === 'web_search',
|
|
190
|
+
'tool message name must be the external search tool'
|
|
191
|
+
);
|
|
192
|
+
} finally {
|
|
193
|
+
await agent.mcpManager.disconnectAll().catch(() => { });
|
|
194
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { extractTopLevelJsonObjects } from '../agentCore.js';
|
|
5
|
+
import { KikuAgent } from '../agentCore.js';
|
|
6
|
+
import { LLMClient } from '../llmClient.js';
|
|
7
|
+
import { executeTool } from '../tools.js';
|
|
8
|
+
|
|
9
|
+
function bareAgent() {
|
|
10
|
+
const agent = Object.create(KikuAgent.prototype);
|
|
11
|
+
agent.mcpManager = null;
|
|
12
|
+
agent.allowedToolNames = null;
|
|
13
|
+
return agent;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('extractTopLevelJsonObjects handles nested objects and braces inside strings', () => {
|
|
17
|
+
const { objects, unbalanced } = extractTopLevelJsonObjects(
|
|
18
|
+
'prefix {"action": "web_search", "parameters": {"query": "In Blossom {花间令} 2024"}} suffix'
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
assert.equal(unbalanced, null);
|
|
22
|
+
assert.equal(objects.length, 1);
|
|
23
|
+
const parsed = JSON.parse(objects[0].raw);
|
|
24
|
+
assert.equal(parsed.action, 'web_search');
|
|
25
|
+
assert.equal(parsed.parameters.query, 'In Blossom {花间令} 2024');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('extractTopLevelJsonObjects reports unterminated objects instead of truncating them', () => {
|
|
29
|
+
const { objects, unbalanced } = extractTopLevelJsonObjects('{"action": "web_search", "parameters": {"query": "truncated"');
|
|
30
|
+
|
|
31
|
+
assert.equal(objects.length, 0);
|
|
32
|
+
assert.ok(unbalanced.startsWith('{"action"'));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('extractTopLevelJsonObjects finds multiple sibling objects', () => {
|
|
36
|
+
const { objects } = extractTopLevelJsonObjects('{"a": {"b": 1}} gap {"c": 2}');
|
|
37
|
+
|
|
38
|
+
assert.equal(objects.length, 2);
|
|
39
|
+
assert.deepEqual(JSON.parse(objects[1].raw), { c: 2 });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('text tool call parsing accepts nested JSON in fenced blocks (previously cut in half)', () => {
|
|
43
|
+
const agent = bareAgent();
|
|
44
|
+
const content = [
|
|
45
|
+
'Let me check the catalog.',
|
|
46
|
+
'```json',
|
|
47
|
+
'{"action": "search_discography", "parameters": {"query": "FIND THE WAY", "type": "single"}}',
|
|
48
|
+
'```',
|
|
49
|
+
].join('\n');
|
|
50
|
+
|
|
51
|
+
const { calls, malformed } = agent._extractTextToolCalls(content);
|
|
52
|
+
assert.equal(malformed, 0);
|
|
53
|
+
assert.equal(calls.length, 1);
|
|
54
|
+
assert.equal(calls[0].function.name, 'search_discography');
|
|
55
|
+
assert.deepEqual(JSON.parse(calls[0].function.arguments), { query: 'FIND THE WAY', type: 'single' });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('text tool call parsing still understands ReAct Action blocks with nested input', () => {
|
|
59
|
+
const agent = bareAgent();
|
|
60
|
+
const content = 'Action: get_work_detail\nAction Input: {"workIdOrTitle": "in-blossom", "options": {"withOst": true}}';
|
|
61
|
+
|
|
62
|
+
const { calls, malformed } = agent._extractTextToolCalls(content);
|
|
63
|
+
assert.equal(malformed, 0);
|
|
64
|
+
assert.equal(calls.length, 1);
|
|
65
|
+
assert.equal(calls[0].function.name, 'get_work_detail');
|
|
66
|
+
const args = JSON.parse(calls[0].function.arguments);
|
|
67
|
+
assert.equal(args.workIdOrTitle, 'in-blossom');
|
|
68
|
+
assert.equal(args.options.withOst, true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('non-JSON Action Input is salvaged as a query argument, not marked malformed', () => {
|
|
72
|
+
const agent = bareAgent();
|
|
73
|
+
const { calls, malformed } = agent._extractTextToolCalls('Action: web_search\nAction Input: Ju Jingyi 2026 drama');
|
|
74
|
+
|
|
75
|
+
assert.equal(malformed, 0);
|
|
76
|
+
assert.equal(calls.length, 1);
|
|
77
|
+
assert.deepEqual(JSON.parse(calls[0].function.arguments), { query: 'Ju Jingyi 2026 drama' });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('malformed tool-call blocks are counted so the loop can feed back instead of answering', () => {
|
|
81
|
+
const agent = bareAgent();
|
|
82
|
+
|
|
83
|
+
const truncated = agent._extractTextToolCalls(
|
|
84
|
+
'```json\n{"action": "web_search", "parameters": {"query": "unterminated\n```'
|
|
85
|
+
);
|
|
86
|
+
assert.equal(truncated.calls.length, 0);
|
|
87
|
+
assert.ok(truncated.malformed >= 1);
|
|
88
|
+
|
|
89
|
+
const trailingComma = agent._extractTextToolCalls(
|
|
90
|
+
'```json\n{"action": "web_search", "parameters": {"query": "x",}}\n```'
|
|
91
|
+
);
|
|
92
|
+
assert.equal(trailingComma.calls.length, 0);
|
|
93
|
+
assert.equal(trailingComma.malformed, 1);
|
|
94
|
+
|
|
95
|
+
const truncatedAction = agent._extractTextToolCalls(
|
|
96
|
+
'Action: web_search\nAction Input: {"query": "broken"'
|
|
97
|
+
);
|
|
98
|
+
assert.equal(truncatedAction.calls.length, 0);
|
|
99
|
+
assert.equal(truncatedAction.malformed, 1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('parseTextToolCalls keeps its historical return shape (calls only)', () => {
|
|
103
|
+
const agent = bareAgent();
|
|
104
|
+
const calls = agent.parseTextToolCalls(
|
|
105
|
+
'```json\n{"action": "get_awards", "parameters": {"year": 2024}}\n```'
|
|
106
|
+
);
|
|
107
|
+
assert.equal(calls.length, 1);
|
|
108
|
+
assert.equal(calls[0].function.name, 'get_awards');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('unknown tool names in text blocks are ignored without breaking parsing', () => {
|
|
112
|
+
const agent = bareAgent();
|
|
113
|
+
const { calls, malformed } = agent._extractTextToolCalls(
|
|
114
|
+
'```json\n{"action": "not_a_real_tool", "parameters": {}}\n```\n```json\n{"action": "get_quotes", "parameters": {}}\n```'
|
|
115
|
+
);
|
|
116
|
+
assert.equal(malformed, 0);
|
|
117
|
+
assert.equal(calls.length, 1);
|
|
118
|
+
assert.equal(calls[0].function.name, 'get_quotes');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('reasoner-class models report that native tools will be omitted', () => {
|
|
122
|
+
const reasoner = Object.create(LLMClient.prototype);
|
|
123
|
+
reasoner.model = 'deepseek-reasoner';
|
|
124
|
+
const normal = Object.create(LLMClient.prototype);
|
|
125
|
+
normal.model = 'gpt-4o';
|
|
126
|
+
|
|
127
|
+
assert.equal(reasoner.toolsWillBeOmitted(), true);
|
|
128
|
+
assert.equal(normal.toolsWillBeOmitted(), false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('static catalog tools are TTL-cached while the social feed stays fresh', async () => {
|
|
132
|
+
const awardsFirst = await executeTool('get_awards', {});
|
|
133
|
+
const awardsSecond = await executeTool('get_awards', {});
|
|
134
|
+
assert.equal(awardsSecond, awardsFirst);
|
|
135
|
+
|
|
136
|
+
const feedFirst = await executeTool('get_social_feed', { limit: 1 });
|
|
137
|
+
const feedSecond = await executeTool('get_social_feed', { limit: 1 });
|
|
138
|
+
assert.notEqual(feedSecond, feedFirst);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('text tool call parsing handles [TOOLCALL] syntax with <|"|> tokenizer delimiters and websearch alias', () => {
|
|
142
|
+
const agent = bareAgent();
|
|
143
|
+
const rawSnippet = [
|
|
144
|
+
'💖 โอ๊ย! เข้าใจเลยค่ะว่าอยากเห็นบรรยากาศของซีรีส์ก่อนดูจริง ๆ ใช่ไหมคะ? ✨',
|
|
145
|
+
'แต่ Concept Art ที่ระบุเจาะจงในฐานข้อมูลแกลเลอรีของเรายังไม่มีการบันทึกเข้ามาอย่างเป็นทางการค่ะ 🥺',
|
|
146
|
+
'ดิฉันจึงได้ทำการค้นหาภาพบรรยากาศล่าสุดจากแหล่งข่าวภายนอกมาให้คุณดู',
|
|
147
|
+
'',
|
|
148
|
+
'[TOOLCALL]',
|
|
149
|
+
'websearch{keywords:<|"|>万花世界 概念图 万花世界 剧照<|"|>,targetlanguage:<|"|>zh<|"|>}<toolcall|>',
|
|
150
|
+
'',
|
|
151
|
+
'พบปัยหาเเล้ว'
|
|
152
|
+
].join('\n');
|
|
153
|
+
|
|
154
|
+
const { calls, malformed } = agent._extractTextToolCalls(rawSnippet);
|
|
155
|
+
assert.equal(malformed, 0);
|
|
156
|
+
assert.equal(calls.length, 1);
|
|
157
|
+
assert.equal(calls[0].function.name, 'web_search');
|
|
158
|
+
const args = JSON.parse(calls[0].function.arguments);
|
|
159
|
+
assert.equal(args.keywords, '万花世界 概念图 万花世界 剧照');
|
|
160
|
+
assert.equal(args.target_language, 'zh');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('stripToolCallBlocks and sanitizeResponse thoroughly strip [TOOLCALL] and synthetic tokens', async () => {
|
|
164
|
+
const { stripToolCallBlocks } = await import('../textToolProtocol.js');
|
|
165
|
+
const { sanitizeResponse } = await import('../sanitizer.js');
|
|
166
|
+
|
|
167
|
+
const rawSnippet = [
|
|
168
|
+
'💖 โอ๊ย! เข้าใจเลยค่ะ',
|
|
169
|
+
'[TOOLCALL]',
|
|
170
|
+
'websearch{keywords:<|"|>万花世界 概念图 万花世界 剧照<|"|>,targetlanguage:<|"|>zh<|"|>}<toolcall|>',
|
|
171
|
+
'ข้อความต่อมา'
|
|
172
|
+
].join('\n');
|
|
173
|
+
|
|
174
|
+
const stripped = stripToolCallBlocks(rawSnippet);
|
|
175
|
+
assert.ok(!stripped.includes('[TOOLCALL]'));
|
|
176
|
+
assert.ok(!stripped.includes('<toolcall'));
|
|
177
|
+
assert.ok(!stripped.includes('<|"|>'));
|
|
178
|
+
assert.ok(!stripped.includes('websearch'));
|
|
179
|
+
assert.ok(stripped.includes('💖 โอ๊ย! เข้าใจเลยค่ะ'));
|
|
180
|
+
assert.ok(stripped.includes('ข้อความต่อมา'));
|
|
181
|
+
|
|
182
|
+
const sanitized = sanitizeResponse(rawSnippet);
|
|
183
|
+
assert.ok(!sanitized.includes('[TOOLCALL]'));
|
|
184
|
+
assert.ok(!sanitized.includes('<toolcall'));
|
|
185
|
+
assert.ok(!sanitized.includes('<|"|>'));
|
|
186
|
+
});
|
|
187
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { C } from '../ui.js';
|
|
5
|
+
import { THEME_NAMES, getThemeName, getThemePalette, setTheme } from '../theme.js';
|
|
6
|
+
|
|
7
|
+
test('theme catalog exposes several distinct choices', () => {
|
|
8
|
+
assert.deepEqual(THEME_NAMES, ['sakura', 'midnight', 'ocean', 'forest', 'sunset', 'monochrome']);
|
|
9
|
+
assert.equal(new Set(THEME_NAMES.map(name => getThemePalette(name).panel)).size, THEME_NAMES.length);
|
|
10
|
+
assert.equal(new Set(THEME_NAMES.map(name => getThemePalette(name).text)).size, THEME_NAMES.length);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('changing theme updates shared ANSI colors without restarting', () => {
|
|
14
|
+
const original = getThemeName();
|
|
15
|
+
const originalPrimary = C.sakura;
|
|
16
|
+
|
|
17
|
+
setTheme(original === 'midnight' ? 'forest' : 'midnight', { persist: false });
|
|
18
|
+
assert.notEqual(C.sakura, originalPrimary);
|
|
19
|
+
assert.match(C.bgPanel, /^\x1b\[48;2;/);
|
|
20
|
+
|
|
21
|
+
setTheme(original, { persist: false });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('unknown theme names are rejected', () => {
|
|
25
|
+
assert.equal(setTheme('not-a-theme', { persist: false }), null);
|
|
26
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { toolsSchema, toolHandlers } from '../tools.js';
|
|
5
|
+
import { TOOL_CLUSTERS } from '../toolRouter.js';
|
|
6
|
+
import { DEFAULT_RESEARCH_TOOLS } from '../subagent.js';
|
|
7
|
+
import { CATALOG_TOOLS, WEB_TOOLS, VERIFY_TOOLS } from '../research.js';
|
|
8
|
+
import { PUBLIC_REMOTE_TOOL_NAMES } from '../publicTools.js';
|
|
9
|
+
import { toolDefinitions } from '../../mcp/tools.js';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const KNOWN_MCP_ALIASES = new Set(['get_profile', 'get_timeline', 'search_gallery']);
|
|
13
|
+
|
|
14
|
+
test('every schema tool has a handler and vice versa', () => {
|
|
15
|
+
const schemaNames = new Set(toolsSchema.map(t => t.function.name));
|
|
16
|
+
const handlerNames = new Set(Object.keys(toolHandlers));
|
|
17
|
+
|
|
18
|
+
const orphans = [...schemaNames].filter(n => !handlerNames.has(n));
|
|
19
|
+
const ghosts = [...handlerNames].filter(n => !schemaNames.has(n));
|
|
20
|
+
assert.deepEqual(orphans, [], 'schema entries without handlers');
|
|
21
|
+
assert.deepEqual(ghosts, [], 'handlers without schema entries');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('every hardcoded tool-name list references real tools', () => {
|
|
25
|
+
const schemaNames = new Set(toolsSchema.map(t => t.function.name));
|
|
26
|
+
const problems = [];
|
|
27
|
+
|
|
28
|
+
const checkList = (label, names) => {
|
|
29
|
+
for (const name of names) {
|
|
30
|
+
if (!schemaNames.has(name)) problems.push(`${label} -> unknown tool "${name}"`);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
for (const [cluster, names] of Object.entries(TOOL_CLUSTERS)) {
|
|
35
|
+
checkList(`TOOL_CLUSTERS.${cluster}`, names);
|
|
36
|
+
}
|
|
37
|
+
checkList('DEFAULT_RESEARCH_TOOLS', DEFAULT_RESEARCH_TOOLS);
|
|
38
|
+
checkList('research CATALOG_TOOLS', CATALOG_TOOLS);
|
|
39
|
+
checkList('research WEB_TOOLS', WEB_TOOLS);
|
|
40
|
+
checkList('research VERIFY_TOOLS', VERIFY_TOOLS);
|
|
41
|
+
checkList('PUBLIC_REMOTE_TOOL_NAMES', PUBLIC_REMOTE_TOOL_NAMES);
|
|
42
|
+
|
|
43
|
+
assert.deepEqual(problems, [], 'stale tool names in hardcoded lists');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('mcp tool definitions reference real tools (plus the documented aliases)', () => {
|
|
47
|
+
const schemaNames = new Set(toolsSchema.map(t => t.function.name));
|
|
48
|
+
const unknown = toolDefinitions
|
|
49
|
+
.map(t => t.name)
|
|
50
|
+
.filter(name => !schemaNames.has(name) && !KNOWN_MCP_ALIASES.has(name));
|
|
51
|
+
|
|
52
|
+
assert.deepEqual(unknown, [], 'mcp definitions referencing non-existent tools');
|
|
53
|
+
});
|