kiku-agent 1.3.0 → 1.3.2
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 +7 -7
- package/package.json +4 -7
- package/server/agent/agentCore.js +2 -2
- package/server/agent/catalogEntityResolver.js +8 -0
- package/server/agent/cli.js +4 -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 +298 -0
- package/server/agent/eval/modules/evaluation/scoring.js +351 -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 +4796 -0
- package/server/agent/eval/modules/utils/smokes.js +119 -0
- package/server/agent/ink/App.jsx +348 -0
- package/server/agent/ink/Panel.jsx +54 -0
- package/server/agent/ink/PromptInput.jsx +321 -0
- package/server/agent/ink/TaskPlanPanel.jsx +88 -0
- package/server/agent/ink/ThinkingPanel.jsx +68 -0
- package/server/agent/ink/ToolResult.jsx +133 -0
- package/server/agent/ink/main.jsx +195 -0
- package/server/agent/ink/useEventBus.js +218 -0
- package/server/agent/memory/contextAllocator.js +214 -0
- package/server/agent/memory/hierarchicalStore.js +523 -0
- package/server/agent/memory/reflectionWorker.js +110 -0
- package/server/agent/prompts.js +1 -1
- package/server/agent/telemetry/hardwareTiers.js +332 -0
- package/server/agent/telemetry/runtimeWatchdog.js +66 -0
- package/server/agent/telemetry/techPowerUpClient.js +682 -0
- package/server/agent/telemetry/techpowerup-mcp.json +22 -0
- package/server/agent/tests/agentBubbleSystem.test.js +65 -0
- package/server/agent/tests/agentFollowUps.test.js +127 -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/autoResearch.test.js +64 -0
- package/server/agent/tests/bilibiliEmbed.test.js +251 -0
- package/server/agent/tests/catalogSync.test.js +130 -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 +594 -0
- package/server/agent/tests/cliAutomation.test.js +121 -0
- package/server/agent/tests/cloudProviders.test.js +234 -0
- package/server/agent/tests/commandInput.test.js +24 -0
- package/server/agent/tests/commands.test.js +417 -0
- package/server/agent/tests/contextIntelligence.test.js +162 -0
- package/server/agent/tests/conversationSafety.test.js +53 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +135 -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 +49 -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 +215 -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 +312 -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/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 +192 -0
- package/server/agent/tests/judgeService.test.js +251 -0
- package/server/agent/tests/lanDiscovery.test.js +319 -0
- package/server/agent/tests/mcpClient.test.js +201 -0
- package/server/agent/tests/modelManager.test.js +109 -0
- package/server/agent/tests/multilingualScenarios.test.js +69 -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 +99 -0
- package/server/agent/tests/offlineInput.test.js +217 -0
- package/server/agent/tests/packagingGuard.test.js +57 -0
- package/server/agent/tests/presentation.test.js +61 -0
- package/server/agent/tests/providerPersistence.test.js +39 -0
- package/server/agent/tests/queryBenchmarks.test.js +83 -0
- package/server/agent/tests/queryDetails.test.js +126 -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/securityAuditRemediation.test.js +214 -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 +232 -0
- package/server/agent/tests/sidebarControls.test.js +101 -0
- package/server/agent/tests/statefulMemory.test.js +270 -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 +126 -0
- package/server/agent/tests/textToolParsing.test.js +139 -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 +119 -0
- package/server/agent/tests/tools.test.js +259 -0
- package/server/agent/tests/unifiedEndpoints.test.js +152 -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 +520 -0
- package/server/agent/tests/webChatInspection.test.js +300 -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 +261 -0
- package/server/agent/tests/webSources.test.js +247 -0
- package/server/agent/websearch/adapters/duckduckgo.js +97 -0
- package/server/agent/websearch/adapters/jina.js +77 -0
- package/server/agent/websearch/adapters/searxng.js +57 -0
- package/server/agent/websearch/core/canonical.js +89 -0
- package/server/agent/websearch/core/fusion.js +81 -0
- package/server/agent/websearch/core/types.js +29 -0
- package/server/agent/websearch/extract.js +605 -0
- package/server/agent/websearch/index.js +43 -0
- package/server/agent/websearch/orchestrator.js +266 -0
- package/server/agent/websearch/resilience/breaker.js +97 -0
- package/server/agent/websearch/resilience/cache.js +59 -0
- package/server/agent/websearch/resilience/limiter.js +93 -0
- package/server/agent/websearch/security/filter.js +36 -0
- package/server/agent/websearch/security/ssrf.js +289 -0
- package/server/agent/websearch/telemetry.js +71 -0
- package/server/apiRouter.js +7 -11
- package/server/eval/judgeRubrics.js +110 -0
- package/server/eval/judgeService.js +290 -0
- package/server/eval/runBenchmark.js +182 -0
- package/server/mcp/examples/librechat.example.yaml +16 -0
- package/server/mcp/tests/connection.test.js +156 -0
- package/server/routes/agent.js +55 -0
- package/server/supabase_schema.sql +63 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { ResearchSubagent } from '../subagent.js';
|
|
5
|
+
import { executeTool } from '../tools.js';
|
|
6
|
+
import { sanitizeSubagentProgress } from '../../web-chat/routes/chatRoute.js';
|
|
7
|
+
|
|
8
|
+
test('research subagent emits iteration and tool progress events', async () => {
|
|
9
|
+
const responses = [
|
|
10
|
+
{
|
|
11
|
+
role: 'assistant',
|
|
12
|
+
content: '',
|
|
13
|
+
tool_calls: [{ id: 'call_1', function: { name: 'search_works', arguments: '{"query":"In Blossom"}' } }],
|
|
14
|
+
},
|
|
15
|
+
{ role: 'assistant', content: 'In Blossom is listed in the verified Kiku catalog.' },
|
|
16
|
+
];
|
|
17
|
+
const progress = [];
|
|
18
|
+
const subagent = new ResearchSubagent({
|
|
19
|
+
client: { async chat() { return responses.shift(); } },
|
|
20
|
+
allowedTools: ['search_works'],
|
|
21
|
+
executeTool: async () => ({ status: 'ok', works: [] }),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const result = await subagent.run('Verify In Blossom', { onProgress: (event) => progress.push(event) });
|
|
25
|
+
|
|
26
|
+
assert.ok(progress.some((event) => event.type === 'iteration' && event.iteration === 1));
|
|
27
|
+
assert.ok(progress.some((event) => event.type === 'tool' && event.name === 'search_works'));
|
|
28
|
+
assert.match(result.answer, /In Blossom/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('run_subagent tool handler forwards tagged progress without an LLM', async () => {
|
|
32
|
+
const seen = [];
|
|
33
|
+
const fakeAgent = {
|
|
34
|
+
async runSubagent(task, options = {}) {
|
|
35
|
+
options.onProgress?.({ type: 'iteration', iteration: 1, maxIterations: 3 });
|
|
36
|
+
options.onProgress?.({ type: 'tool', name: 'search_works' });
|
|
37
|
+
return { answer: 'Evidence: In Blossom (2024).', toolCalls: [{ name: 'search_works' }], iterations: 1 };
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const result = await executeTool('run_subagent', { task: 'Verify In Blossom' }, {
|
|
42
|
+
agent: fakeAgent,
|
|
43
|
+
onSubagentProgress: (event) => seen.push(event),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
assert.equal(result.status, 'success');
|
|
47
|
+
assert.match(result.answer, /In Blossom/);
|
|
48
|
+
assert.ok(seen.length >= 3, `expected start + progress events, got ${seen.length}`);
|
|
49
|
+
assert.ok(seen.every((event) => event.source === 'subagent'));
|
|
50
|
+
assert.ok(seen.some((event) => event.type === 'iteration'));
|
|
51
|
+
assert.ok(seen.some((event) => event.type === 'tool' && event.name === 'search_works'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('run_deep_research and verify_claim forward tagged research progress', async () => {
|
|
55
|
+
const seen = [];
|
|
56
|
+
const fakeAgent = {
|
|
57
|
+
async runResearch(question, options = {}) {
|
|
58
|
+
options.onProgress?.({ type: 'worker-start', id: 'w1', label: 'Catalog sweep' });
|
|
59
|
+
options.onProgress?.({ type: 'worker-complete', id: 'w1', label: 'Catalog sweep' });
|
|
60
|
+
options.onProgress?.({ type: 'complete', id: 'job1', sources: 2 });
|
|
61
|
+
return { answer: 'Synthesized answer.', sources: ['https://example.com/a'], reports: [] };
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const research = await executeTool('run_deep_research', { question: 'Latest Ju Jingyi drama?' }, {
|
|
66
|
+
agent: fakeAgent,
|
|
67
|
+
onSubagentProgress: (event) => seen.push(event),
|
|
68
|
+
});
|
|
69
|
+
assert.equal(research.status, 'success');
|
|
70
|
+
assert.ok(seen.every((event) => event.source === 'research'));
|
|
71
|
+
assert.ok(seen.some((event) => event.type === 'worker-start'));
|
|
72
|
+
assert.ok(seen.some((event) => event.type === 'complete'));
|
|
73
|
+
|
|
74
|
+
seen.length = 0;
|
|
75
|
+
const verify = await executeTool('verify_claim', { claim: 'Ju Jingyi won an award in 2024' }, {
|
|
76
|
+
agent: fakeAgent,
|
|
77
|
+
onSubagentProgress: (event) => seen.push(event),
|
|
78
|
+
});
|
|
79
|
+
assert.equal(verify.status, 'success');
|
|
80
|
+
assert.ok(seen.every((event) => event.source === 'verify'));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('sanitizeSubagentProgress allowlists coarse fields and drops internals', () => {
|
|
84
|
+
const dirty = {
|
|
85
|
+
source: 'research',
|
|
86
|
+
type: 'worker-start',
|
|
87
|
+
question: 'Latest drama?'.repeat(50),
|
|
88
|
+
id: 'w1',
|
|
89
|
+
label: 'Catalog sweep',
|
|
90
|
+
args: { secret: 'must-not-leak' },
|
|
91
|
+
content: 'raw evidence must not cross',
|
|
92
|
+
apiKey: 'must-not-leak',
|
|
93
|
+
};
|
|
94
|
+
const clean = sanitizeSubagentProgress(dirty);
|
|
95
|
+
|
|
96
|
+
assert.equal(clean.type, 'subagent_progress');
|
|
97
|
+
assert.equal(clean.source, 'research');
|
|
98
|
+
assert.equal(clean.kind, 'worker-start');
|
|
99
|
+
assert.equal(clean.workerId, 'w1');
|
|
100
|
+
assert.equal(clean.label, 'Catalog sweep');
|
|
101
|
+
assert.ok(clean.subject.length <= 200);
|
|
102
|
+
assert.ok(!('args' in clean));
|
|
103
|
+
assert.ok(!('content' in clean));
|
|
104
|
+
assert.ok(!('apiKey' in clean));
|
|
105
|
+
|
|
106
|
+
const unknown = sanitizeSubagentProgress({ source: 'hacker', type: 'exfiltrate', task: 'x' });
|
|
107
|
+
assert.equal(unknown.source, 'subagent');
|
|
108
|
+
assert.equal(unknown.kind, 'tool');
|
|
109
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { ResearchSubagent } from '../subagent.js';
|
|
5
|
+
|
|
6
|
+
test('subagent parses Action blocks and executes tools when native calling is unavailable', async () => {
|
|
7
|
+
const executed = [];
|
|
8
|
+
const observed = [];
|
|
9
|
+
let chatCalls = 0;
|
|
10
|
+
const client = {
|
|
11
|
+
provider: 'deepseek',
|
|
12
|
+
providerName: 'DeepSeek',
|
|
13
|
+
model: 'deepseek-reasoner',
|
|
14
|
+
toolsWillBeOmitted() { return true; },
|
|
15
|
+
toolsDisabledReason: 'reasoning_model',
|
|
16
|
+
async chat(options) {
|
|
17
|
+
chatCalls++;
|
|
18
|
+
observed.push(options.messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content : null })));
|
|
19
|
+
if (chatCalls === 1) {
|
|
20
|
+
return { role: 'assistant', content: 'Let me check the catalog.\nAction: get_quotes\nAction Input: {"tag": "mindset"}' };
|
|
21
|
+
}
|
|
22
|
+
return { role: 'assistant', content: 'Evidence report: one mindset quote found.' };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const subagent = new ResearchSubagent({
|
|
27
|
+
client,
|
|
28
|
+
executeTool: async (name, args) => {
|
|
29
|
+
executed.push({ name, args });
|
|
30
|
+
return { status: 'success', count: 1, quotes: [{ textTh: 'x' }] };
|
|
31
|
+
},
|
|
32
|
+
maxIterations: 3,
|
|
33
|
+
allowedTools: ['get_quotes', 'web_search'],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = await subagent.run('collect a mindset quote');
|
|
37
|
+
|
|
38
|
+
assert.equal(executed.length, 1);
|
|
39
|
+
assert.equal(executed[0].name, 'get_quotes');
|
|
40
|
+
assert.deepEqual(executed[0].args, { tag: 'mindset' });
|
|
41
|
+
assert.match(result.answer, /Evidence report/);
|
|
42
|
+
assert.equal(result.toolCalls.length, 1);
|
|
43
|
+
|
|
44
|
+
assert.match(observed[0][0].content, /TEXT TOOL PROTOCOL/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('malformed tool-call blocks are fed back instead of ending the run', async () => {
|
|
48
|
+
const executed = [];
|
|
49
|
+
let chatCalls = 0;
|
|
50
|
+
const client = {
|
|
51
|
+
provider: 'lmstudio',
|
|
52
|
+
providerName: 'LM Studio',
|
|
53
|
+
model: 'test-model',
|
|
54
|
+
async chat() {
|
|
55
|
+
chatCalls++;
|
|
56
|
+
if (chatCalls === 1) {
|
|
57
|
+
return { role: 'assistant', content: '```json\n{"action": "get_quotes", "parameters": {"tag": }}\n```' };
|
|
58
|
+
}
|
|
59
|
+
if (chatCalls === 2) {
|
|
60
|
+
return { role: 'assistant', content: 'Action: get_quotes\nAction Input: {"tag": "mindset"}' };
|
|
61
|
+
}
|
|
62
|
+
return { role: 'assistant', content: 'Done — evidence collected.' };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const subagent = new ResearchSubagent({
|
|
67
|
+
client,
|
|
68
|
+
executeTool: async (name) => {
|
|
69
|
+
executed.push(name);
|
|
70
|
+
return { status: 'success', count: 1 };
|
|
71
|
+
},
|
|
72
|
+
maxIterations: 4,
|
|
73
|
+
allowedTools: ['get_quotes'],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const result = await subagent.run('collect a quote');
|
|
77
|
+
|
|
78
|
+
assert.equal(executed.length, 1);
|
|
79
|
+
assert.equal(result.toolCalls.length, 1);
|
|
80
|
+
assert.ok(result.iterations >= 2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('native tool_calls path is unchanged', async () => {
|
|
84
|
+
const executed = [];
|
|
85
|
+
let chatCalls = 0;
|
|
86
|
+
const client = {
|
|
87
|
+
provider: 'lmstudio',
|
|
88
|
+
providerName: 'LM Studio',
|
|
89
|
+
model: 'test-model',
|
|
90
|
+
async chat() {
|
|
91
|
+
chatCalls++;
|
|
92
|
+
if (chatCalls === 1) {
|
|
93
|
+
return {
|
|
94
|
+
role: 'assistant',
|
|
95
|
+
content: '',
|
|
96
|
+
tool_calls: [{ id: 'call_1', type: 'function', function: { name: 'get_awards', arguments: '{}' } }],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { role: 'assistant', content: 'final report' };
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const subagent = new ResearchSubagent({
|
|
104
|
+
client,
|
|
105
|
+
executeTool: async (name) => {
|
|
106
|
+
executed.push(name);
|
|
107
|
+
return { status: 'success', count: 2 };
|
|
108
|
+
},
|
|
109
|
+
maxIterations: 3,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const result = await subagent.run('native path');
|
|
113
|
+
assert.equal(executed[0], 'get_awards');
|
|
114
|
+
assert.match(result.answer, /final report/);
|
|
115
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { SuggestionEngine, SUGGESTION_CATEGORIES } from '../suggestions.js';
|
|
4
|
+
import { routeCommand } from '../commands.js';
|
|
5
|
+
import { eventBus, Events } from '../eventBus.js';
|
|
6
|
+
import { wrapText, formatRichCardLines, stripAnsi, C } from '../ui.js';
|
|
7
|
+
|
|
8
|
+
test('SuggestionEngine generates catalog fallback suggestions cleanly', () => {
|
|
9
|
+
const engine = new SuggestionEngine();
|
|
10
|
+
const suggestions = engine.generateCatalogSuggestions({ limit: 4 });
|
|
11
|
+
|
|
12
|
+
assert.equal(suggestions.length, 4);
|
|
13
|
+
assert.ok(suggestions[0].id === 1);
|
|
14
|
+
assert.ok(suggestions[0].question.length > 5);
|
|
15
|
+
assert.ok(suggestions[0].category);
|
|
16
|
+
assert.ok(SUGESTION_CATEGORIES_SET().has(suggestions[0].category));
|
|
17
|
+
assert.equal(engine.getLastSuggestions().length, 4);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('SuggestionEngine filters catalog suggestions by topic', () => {
|
|
21
|
+
const engine = new SuggestionEngine();
|
|
22
|
+
const dramaSuggestions = engine.generateCatalogSuggestions({ topic: 'drama', limit: 3 });
|
|
23
|
+
|
|
24
|
+
assert.ok(dramaSuggestions.length > 0);
|
|
25
|
+
assert.ok(dramaSuggestions.some(s => s.category === 'DRAMA' || s.question.toLowerCase().includes('drama')));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('SuggestionEngine parses AI JSON suggestions when LLM is available', async () => {
|
|
29
|
+
const mockClient = {
|
|
30
|
+
async chat() {
|
|
31
|
+
return {
|
|
32
|
+
content: JSON.stringify([
|
|
33
|
+
{
|
|
34
|
+
category: 'DRAMA',
|
|
35
|
+
question: "What is Ju Jingyi's role in the 2026 drama In Blossom?",
|
|
36
|
+
description: "Deep dive into Yang Caiwei character."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
category: 'MUSIC',
|
|
40
|
+
question: "What are Ju Jingyi's newest 2025-2026 OST releases?",
|
|
41
|
+
description: "Song list and video links."
|
|
42
|
+
}
|
|
43
|
+
])
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const engine = new SuggestionEngine({ client: mockClient });
|
|
49
|
+
const suggestions = await engine.generateSuggestions({ limit: 2 });
|
|
50
|
+
|
|
51
|
+
assert.equal(suggestions.length, 2);
|
|
52
|
+
assert.equal(suggestions[0].category, 'DRAMA');
|
|
53
|
+
assert.equal(suggestions[0].source, 'ai_model');
|
|
54
|
+
assert.equal(suggestions[1].category, 'MUSIC');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('SuggestionEngine falls back to catalog when LLM outputs invalid JSON', async () => {
|
|
58
|
+
const mockClient = {
|
|
59
|
+
async chat() {
|
|
60
|
+
return { content: 'Sorry, I cannot output JSON right now.' };
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const engine = new SuggestionEngine({ client: mockClient });
|
|
65
|
+
const suggestions = await engine.generateSuggestions({ limit: 3 });
|
|
66
|
+
|
|
67
|
+
assert.equal(suggestions.length, 3);
|
|
68
|
+
assert.equal(suggestions[0].source, 'catalog');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('resolveTargetQuestions parses numeric indexes, all, and custom queries', () => {
|
|
72
|
+
const engine = new SuggestionEngine();
|
|
73
|
+
engine.generateCatalogSuggestions({ limit: 5 });
|
|
74
|
+
|
|
75
|
+
const all = engine.resolveTargetQuestions('all');
|
|
76
|
+
assert.equal(all.length, 5);
|
|
77
|
+
|
|
78
|
+
const single = engine.resolveTargetQuestions('2');
|
|
79
|
+
assert.equal(single.length, 1);
|
|
80
|
+
assert.equal(single[0].id, 2);
|
|
81
|
+
|
|
82
|
+
const multiple = engine.resolveTargetQuestions('1, 3');
|
|
83
|
+
assert.equal(multiple.length, 2);
|
|
84
|
+
assert.equal(multiple[0].id, 1);
|
|
85
|
+
assert.equal(multiple[1].id, 3);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('runBatchSubagents executes multiple workers concurrently and collects results', async () => {
|
|
89
|
+
const mockClient = {
|
|
90
|
+
async chat({ messages }) {
|
|
91
|
+
const last = messages[messages.length - 1];
|
|
92
|
+
if (last.role === 'user' && typeof last.content === 'string') {
|
|
93
|
+
return {
|
|
94
|
+
content: `Evidence findings for task: ${last.content.slice(0, 30)}... (Verified via local catalog).`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return { content: 'Worker report complete.' };
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const engine = new SuggestionEngine({ client: mockClient });
|
|
102
|
+
engine.generateCatalogSuggestions({ limit: 3 });
|
|
103
|
+
|
|
104
|
+
const progressEvents = [];
|
|
105
|
+
const planEvents = [];
|
|
106
|
+
|
|
107
|
+
const unsubStart = eventBus.on(Events.AGENT_PLAN_START, data => planEvents.push({ type: 'start', ...data }));
|
|
108
|
+
const unsubStep = eventBus.on(Events.AGENT_PLAN_STEP_UPDATE, data => planEvents.push({ type: 'step', ...data }));
|
|
109
|
+
const unsubComplete = eventBus.on(Events.AGENT_PLAN_COMPLETE, data => planEvents.push({ type: 'complete', ...data }));
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const batchResult = await engine.runBatchSubagents('1, 2', {
|
|
113
|
+
onProgress: evt => progressEvents.push(evt),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
assert.equal(batchResult.tasksCount, 2);
|
|
117
|
+
assert.equal(batchResult.successfulCount, 2);
|
|
118
|
+
assert.equal(batchResult.results.length, 2);
|
|
119
|
+
assert.ok(batchResult.results[0].answer.includes('Evidence findings'));
|
|
120
|
+
assert.ok(progressEvents.some(e => e.type === 'batch-start'));
|
|
121
|
+
assert.ok(progressEvents.some(e => e.type === 'worker-start'));
|
|
122
|
+
assert.ok(progressEvents.some(e => e.type === 'worker-complete'));
|
|
123
|
+
assert.ok(progressEvents.some(e => e.type === 'batch-complete'));
|
|
124
|
+
assert.ok(planEvents.some(e => e.type === 'start'));
|
|
125
|
+
assert.ok(planEvents.some(e => e.type === 'complete'));
|
|
126
|
+
} finally {
|
|
127
|
+
unsubStart();
|
|
128
|
+
unsubStep();
|
|
129
|
+
unsubComplete();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('/suggestion command routes list, run, and auto subcommands', async () => {
|
|
134
|
+
let batchCalled = false;
|
|
135
|
+
let generateCalled = false;
|
|
136
|
+
|
|
137
|
+
const mockAgent = {
|
|
138
|
+
async generateSuggestions() {
|
|
139
|
+
generateCalled = true;
|
|
140
|
+
return [
|
|
141
|
+
{ id: 1, category: 'DRAMA', question: 'Test drama question?', description: 'Desc 1' },
|
|
142
|
+
{ id: 2, category: 'MUSIC', question: 'Test music question?', description: 'Desc 2' }
|
|
143
|
+
];
|
|
144
|
+
},
|
|
145
|
+
async runSuggestionBatch(targets) {
|
|
146
|
+
batchCalled = true;
|
|
147
|
+
return {
|
|
148
|
+
batchId: 'test_batch',
|
|
149
|
+
tasksCount: 1,
|
|
150
|
+
successfulCount: 1,
|
|
151
|
+
elapsed: '0.42',
|
|
152
|
+
results: [
|
|
153
|
+
{
|
|
154
|
+
index: 1,
|
|
155
|
+
category: 'DRAMA',
|
|
156
|
+
question: 'Test drama question?',
|
|
157
|
+
answer: 'Yang Caiwei is the female lead.',
|
|
158
|
+
toolCalls: [{ name: 'search_works', args: { query: 'In Blossom' } }],
|
|
159
|
+
iterations: 1
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const listRes = await routeCommand('/suggestion', { agent: mockAgent, sub: '', extra: '' });
|
|
167
|
+
assert.equal(listRes, 'HANDLED');
|
|
168
|
+
assert.equal(generateCalled, true);
|
|
169
|
+
|
|
170
|
+
const runRes = await routeCommand('/suggestion run 1', { agent: mockAgent, sub: 'run', extra: '1' });
|
|
171
|
+
assert.equal(runRes, 'HANDLED');
|
|
172
|
+
assert.equal(batchCalled, true);
|
|
173
|
+
|
|
174
|
+
const aliasRes = await routeCommand('/suggest auto', { agent: mockAgent, sub: 'auto', extra: '' });
|
|
175
|
+
assert.equal(aliasRes, 'HANDLED');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('wrapText preserves indentation, hanging indents for list markers, and active ANSI styles', () => {
|
|
179
|
+
const wrappedArrow = wrapText(' \x1b[2m↳ Overview of Versace Global Brand Ambassador role, Maybelline, and top endorsements.\x1b[0m', 45);
|
|
180
|
+
assert.ok(wrappedArrow.length >= 2, 'Arrow line should wrap into at least 2 lines');
|
|
181
|
+
assert.ok(wrappedArrow[0].startsWith(' \x1b[2m↳'), 'First line must retain initial indent of 6 spaces');
|
|
182
|
+
assert.ok(wrappedArrow[1].startsWith(' \x1b[2m'), 'Continuation line must align under text with hanging indent of 8 spaces');
|
|
183
|
+
assert.ok(wrappedArrow[1].includes('\x1b[2m'), 'Continuation line must retain active dim ANSI style');
|
|
184
|
+
|
|
185
|
+
const wrappedBullet = wrapText(' • Run parallel batch with subagents for comprehensive research', 30);
|
|
186
|
+
assert.ok(wrappedBullet.length >= 2, 'Bullet line should wrap');
|
|
187
|
+
assert.ok(wrappedBullet[0].startsWith(' •'), 'First line should have bullet marker');
|
|
188
|
+
assert.ok(wrappedBullet[1].startsWith(' '), 'Continuation line should indent under bullet');
|
|
189
|
+
|
|
190
|
+
const wrappedNumber = wrapText(' [1] [FASHION] What are Ju Jingyi\'s major luxury brand ambassadorships and global fashion partnerships?', 40);
|
|
191
|
+
assert.ok(wrappedNumber.length >= 2, 'Numbered item should wrap');
|
|
192
|
+
assert.ok(wrappedNumber[0].startsWith(' [1]'), 'First line should have number marker');
|
|
193
|
+
assert.ok(wrappedNumber[1].startsWith(' '), 'Continuation line should indent with hanging indent');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
function SUGESTION_CATEGORIES_SET() {
|
|
197
|
+
return new Set(SUGGESTION_CATEGORIES);
|
|
198
|
+
}
|
|
199
|
+
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getCategoryBadge,
|
|
7
|
+
formatWorkerProgressMatrix,
|
|
8
|
+
formatExecutiveFindings,
|
|
9
|
+
stripAnsi,
|
|
10
|
+
} from '../ui.js';
|
|
11
|
+
import {
|
|
12
|
+
renderSuggestionStudioView,
|
|
13
|
+
promptInteractiveSuggestion,
|
|
14
|
+
SUGGESTION_STUDIO_TABS,
|
|
15
|
+
} from '../suggestionTui.js';
|
|
16
|
+
import { routeCommand } from '../commands.js';
|
|
17
|
+
|
|
18
|
+
const SAMPLE_SUGGESTIONS = [
|
|
19
|
+
{
|
|
20
|
+
id: 1,
|
|
21
|
+
category: 'DRAMA',
|
|
22
|
+
question: "What are Ju Jingyi's upcoming 2026/2025 drama projects?",
|
|
23
|
+
description: "Detailed breakdown of upcoming dramas and broadcasting platforms.",
|
|
24
|
+
tools: ['search_works', 'web_search'],
|
|
25
|
+
depth: 'Deep Analysis',
|
|
26
|
+
estimatedSeconds: 3,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 2,
|
|
30
|
+
category: 'MUSIC',
|
|
31
|
+
question: "What are the most popular OSTs and solo singles released by Ju Jingyi?",
|
|
32
|
+
description: "Discography highlights from In Blossom and Legend of Yunxi.",
|
|
33
|
+
tools: ['search_discography', 'search_videos'],
|
|
34
|
+
depth: 'Media Scan',
|
|
35
|
+
estimatedSeconds: 2,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 3,
|
|
39
|
+
category: 'FASHION',
|
|
40
|
+
question: "What are Ju Jingyi's major luxury brand ambassadorships in 2025-2026?",
|
|
41
|
+
description: "Overview of Versace Global Brand Ambassador role and endorsements.",
|
|
42
|
+
tools: ['get_profile_and_fandom'],
|
|
43
|
+
depth: 'Portfolio Scan',
|
|
44
|
+
estimatedSeconds: 2,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
test('getCategoryBadge returns distinctive icons and colors for all 6 categories', () => {
|
|
49
|
+
const drama = getCategoryBadge('DRAMA');
|
|
50
|
+
assert.equal(drama.icon, '🌸');
|
|
51
|
+
assert.equal(drama.label, 'DRAMA');
|
|
52
|
+
assert.ok(drama.color);
|
|
53
|
+
|
|
54
|
+
const music = getCategoryBadge('MUSIC');
|
|
55
|
+
assert.equal(music.icon, '🎵');
|
|
56
|
+
assert.equal(music.label, 'MUSIC');
|
|
57
|
+
|
|
58
|
+
const fashion = getCategoryBadge('FASHION');
|
|
59
|
+
assert.equal(fashion.icon, '✨');
|
|
60
|
+
assert.equal(fashion.label, 'FASHION');
|
|
61
|
+
|
|
62
|
+
const awards = getCategoryBadge('MILESTONE');
|
|
63
|
+
assert.equal(awards.icon, '🌸');
|
|
64
|
+
assert.equal(awards.label, 'AWARDS');
|
|
65
|
+
|
|
66
|
+
const analysis = getCategoryBadge('ANALYSIS');
|
|
67
|
+
assert.equal(analysis.icon, '📖');
|
|
68
|
+
assert.equal(analysis.label, 'ANALYSIS');
|
|
69
|
+
|
|
70
|
+
const trivia = getCategoryBadge('TRIVIA');
|
|
71
|
+
assert.equal(trivia.icon, '💬');
|
|
72
|
+
assert.equal(trivia.label, 'TRIVIA');
|
|
73
|
+
|
|
74
|
+
const unknown = getCategoryBadge('OTHER');
|
|
75
|
+
assert.equal(unknown.icon, '✨');
|
|
76
|
+
assert.equal(unknown.label, 'OTHER');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('renderSuggestionStudioView renders complete interactive studio frame', () => {
|
|
80
|
+
const rows = renderSuggestionStudioView({
|
|
81
|
+
suggestions: SAMPLE_SUGGESTIONS,
|
|
82
|
+
selectedIndex: 0,
|
|
83
|
+
selectedIds: new Set([1]),
|
|
84
|
+
activeCategory: 'ALL',
|
|
85
|
+
termWidth: 80,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.ok(rows.length > 5);
|
|
89
|
+
const plainText = rows.map(r => stripAnsi(r)).join('\n');
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
assert.ok(plainText.includes('🌸 KIKU SUGGESTION STUDIO'));
|
|
93
|
+
assert.ok(plainText.includes('DRAMA'));
|
|
94
|
+
assert.ok(plainText.includes('MUSIC'));
|
|
95
|
+
assert.ok(plainText.includes('FASHION'));
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
assert.ok(plainText.includes('❯ [◉] 1.'));
|
|
99
|
+
assert.ok(plainText.includes('[ ] 2.'));
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
assert.ok(plainText.includes('Focus:'));
|
|
103
|
+
assert.ok(plainText.includes('Tools:'));
|
|
104
|
+
assert.ok(plainText.includes('Est:'));
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
assert.ok(plainText.includes('[↑/↓] Nav'));
|
|
108
|
+
assert.ok(plainText.includes('[Enter] Launch Batch (1)'));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('renderSuggestionStudioView correctly filters suggestions by category tab', () => {
|
|
112
|
+
const dramaRows = renderSuggestionStudioView({
|
|
113
|
+
suggestions: SAMPLE_SUGGESTIONS,
|
|
114
|
+
selectedIndex: 0,
|
|
115
|
+
selectedIds: new Set(),
|
|
116
|
+
activeCategory: 'DRAMA',
|
|
117
|
+
termWidth: 100,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const plainText = dramaRows.map(r => stripAnsi(r)).join('\n');
|
|
121
|
+
assert.ok(plainText.includes('upcoming 2026/2025 drama'));
|
|
122
|
+
assert.ok(!plainText.includes('popular OSTs and solo singles'));
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
const emptyRows = renderSuggestionStudioView({
|
|
126
|
+
suggestions: SAMPLE_SUGGESTIONS,
|
|
127
|
+
selectedIndex: 0,
|
|
128
|
+
selectedIds: new Set(),
|
|
129
|
+
activeCategory: 'TRIVIA',
|
|
130
|
+
termWidth: 80,
|
|
131
|
+
});
|
|
132
|
+
const emptyText = emptyRows.map(r => stripAnsi(r)).join('\n');
|
|
133
|
+
assert.ok(emptyText.includes('No suggestions found under TRIVIA'));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('promptInteractiveSuggestion exits immediately on non-TTY without hanging', async () => {
|
|
137
|
+
const mockStdin = new EventEmitter();
|
|
138
|
+
mockStdin.isTTY = false;
|
|
139
|
+
const mockStdout = { isTTY: false, columns: 80, write: () => {} };
|
|
140
|
+
|
|
141
|
+
const res = await promptInteractiveSuggestion(SAMPLE_SUGGESTIONS, {
|
|
142
|
+
stdin: mockStdin,
|
|
143
|
+
stdout: mockStdout,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
assert.equal(res.action, 'display');
|
|
147
|
+
assert.equal(res.targets, null);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('formatWorkerProgressMatrix renders unified concurrent progress table', () => {
|
|
151
|
+
const workers = [
|
|
152
|
+
{ index: 1, category: 'DRAMA', state: 'running', currentTool: 'search_works', question: 'Upcoming dramas?' },
|
|
153
|
+
{ index: 2, category: 'MUSIC', state: 'complete', currentTool: '', question: 'Popular OSTs?' },
|
|
154
|
+
{ index: 3, category: 'FASHION', state: 'error', currentTool: '', question: 'Versace role?' },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const rows = formatWorkerProgressMatrix({
|
|
158
|
+
workers,
|
|
159
|
+
batchId: 'batch_test_1',
|
|
160
|
+
elapsed: '1.8',
|
|
161
|
+
termWidth: 80,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
assert.ok(rows.length >= 5);
|
|
165
|
+
const plainText = rows.map(r => stripAnsi(r)).join('\n');
|
|
166
|
+
|
|
167
|
+
assert.ok(plainText.includes('CONCURRENT RESEARCH SUBAGENTS'));
|
|
168
|
+
assert.ok(plainText.includes('3 Workers • 1.8s'));
|
|
169
|
+
assert.ok(plainText.includes('W1 [🌸 DRAMA]'));
|
|
170
|
+
assert.ok(plainText.includes('Working'));
|
|
171
|
+
assert.ok(plainText.includes('W2 [🎵 MUSIC]'));
|
|
172
|
+
assert.ok(plainText.includes('Done'));
|
|
173
|
+
assert.ok(plainText.includes('W3 [✨ FASHION]'));
|
|
174
|
+
assert.ok(plainText.includes('Error'));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('formatExecutiveFindings synthesizes findings, takeaways, and grounded citations', () => {
|
|
178
|
+
const batchResult = {
|
|
179
|
+
tasksCount: 2,
|
|
180
|
+
successfulCount: 2,
|
|
181
|
+
elapsed: '2.4',
|
|
182
|
+
results: [
|
|
183
|
+
{
|
|
184
|
+
index: 1,
|
|
185
|
+
category: 'DRAMA',
|
|
186
|
+
question: 'Upcoming dramas?',
|
|
187
|
+
answer: 'Ju Jingyi has multiple upcoming drama productions announced.',
|
|
188
|
+
toolCalls: [{ name: 'search_works' }],
|
|
189
|
+
iterations: 2,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
index: 2,
|
|
193
|
+
category: 'FASHION',
|
|
194
|
+
question: 'Versace role?',
|
|
195
|
+
answer: 'Ju Jingyi was named Versace Global Brand Ambassador.',
|
|
196
|
+
toolCalls: [{ name: 'get_profile_and_fandom' }],
|
|
197
|
+
iterations: 1,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const rows = formatExecutiveFindings({ batchResult, termWidth: 80 });
|
|
203
|
+
assert.ok(rows.length > 5);
|
|
204
|
+
const plainText = rows.map(r => stripAnsi(r)).join('\n');
|
|
205
|
+
|
|
206
|
+
assert.ok(plainText.includes('KIKU EXECUTIVE RESEARCH FINDINGS'));
|
|
207
|
+
assert.ok(plainText.includes('2/2 Succeeded • 2.4s'));
|
|
208
|
+
assert.ok(plainText.includes('[Worker 1] DRAMA: Upcoming dramas?'));
|
|
209
|
+
assert.ok(plainText.includes('Grounded Evidence: 1 tool calls executed'));
|
|
210
|
+
assert.ok(plainText.includes('[Worker 2] FASHION: Versace role?'));
|
|
211
|
+
assert.ok(plainText.includes('/session export'));
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('Commands: /suggestion executes cleanly in non-interactive routeCommand', async () => {
|
|
215
|
+
let generateCalled = false;
|
|
216
|
+
let batchCalled = false;
|
|
217
|
+
|
|
218
|
+
const mockAgent = {
|
|
219
|
+
async generateSuggestions() {
|
|
220
|
+
generateCalled = true;
|
|
221
|
+
return SAMPLE_SUGGESTIONS;
|
|
222
|
+
},
|
|
223
|
+
async runSuggestionBatch(targets) {
|
|
224
|
+
batchCalled = true;
|
|
225
|
+
return {
|
|
226
|
+
batchId: 'batch_test',
|
|
227
|
+
tasksCount: 1,
|
|
228
|
+
successfulCount: 1,
|
|
229
|
+
elapsed: '0.5',
|
|
230
|
+
results: [
|
|
231
|
+
{
|
|
232
|
+
index: 1,
|
|
233
|
+
category: 'DRAMA',
|
|
234
|
+
question: 'Upcoming dramas?',
|
|
235
|
+
answer: 'Yang Caiwei in In Blossom received critical acclaim.',
|
|
236
|
+
toolCalls: [{ name: 'search_works', args: {} }],
|
|
237
|
+
iterations: 1,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const listRes = await routeCommand('/suggestion', { agent: mockAgent, sub: '', extra: '' });
|
|
245
|
+
assert.equal(listRes, 'HANDLED');
|
|
246
|
+
assert.equal(generateCalled, true);
|
|
247
|
+
|
|
248
|
+
const runRes = await routeCommand('/suggestion run 1', { agent: mockAgent, sub: 'run', extra: '1' });
|
|
249
|
+
assert.equal(runRes, 'HANDLED');
|
|
250
|
+
assert.equal(batchCalled, true);
|
|
251
|
+
});
|