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,199 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { getDocsHtml } from '../../web-chat/ui/docsBuilder.js';
|
|
4
|
+
import { getSettingsHtml } from '../../web-chat/ui/settingsBuilder.js';
|
|
5
|
+
import { handleStaticRoute } from '../../web-chat/routes/staticRoute.js';
|
|
6
|
+
|
|
7
|
+
test('Inference Control Center Web Documentation Portal', async (t) => {
|
|
8
|
+
await t.test('getDocsHtml returns valid HTML with all 7 chapters and hardware telemetry', () => {
|
|
9
|
+
const html = getDocsHtml();
|
|
10
|
+
assert.ok(typeof html === 'string' && html.length > 5000, 'HTML should be generated and non-empty');
|
|
11
|
+
assert.ok(html.includes('<!DOCTYPE html>'), 'Should have DOCTYPE');
|
|
12
|
+
assert.ok(html.includes('Inference Control Center'), 'Should have title');
|
|
13
|
+
assert.ok(html.includes('Architecture & System Manual'), 'Should have manual subtitle in title');
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
assert.ok(html.includes('id="section-arch"'), 'Chapter 1: Architecture section exists');
|
|
17
|
+
assert.ok(html.includes('id="section-engines"'), 'Chapter 2: Engines section exists');
|
|
18
|
+
assert.ok(html.includes('id="section-cpu-ht"'), 'Chapter 3: CPU SMT section exists');
|
|
19
|
+
assert.ok(html.includes('id="section-vram"'), 'Chapter 4: VRAM section exists');
|
|
20
|
+
assert.ok(html.includes('id="section-lan"'), 'Chapter 5: LAN Discovery section exists');
|
|
21
|
+
assert.ok(html.includes('id="section-api"'), 'Chapter 6: API section exists');
|
|
22
|
+
assert.ok(html.includes('id="section-tuning"'), 'Chapter 7: Tuning section exists');
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
assert.ok(html.includes('Hyper-Threading') || html.includes('SMT'), 'Includes CPU SMT & Hyper-Threading documentation');
|
|
26
|
+
assert.ok(html.includes('VRAM'), 'Includes VRAM formula and calculation');
|
|
27
|
+
assert.ok(html.includes('LM Studio') && html.includes('vLLM'), 'Includes engine comparisons');
|
|
28
|
+
assert.ok(html.includes('SSRF'), 'Includes Zero-Key LAN security');
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
assert.ok(html.includes('DOCS_I18N'), 'Includes DOCS_I18N dictionary');
|
|
32
|
+
assert.ok(html.includes('data-theme="dark"'), 'Supports dark theme by default');
|
|
33
|
+
assert.ok(html.includes('toggleTheme'), 'Includes theme toggle function');
|
|
34
|
+
assert.ok(html.includes('toggleLanguage'), 'Includes language toggle function');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await t.test('staticRoute serves /docs and /settings/docs with 200 and caching headers', () => {
|
|
38
|
+
const mockReq = (url) => ({
|
|
39
|
+
url,
|
|
40
|
+
headers: {
|
|
41
|
+
host: 'localhost:3002',
|
|
42
|
+
'accept-encoding': 'gzip, deflate, br'
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const createMockRes = () => {
|
|
47
|
+
let statusCode = 200;
|
|
48
|
+
let headers = {};
|
|
49
|
+
let bodyChunks = [];
|
|
50
|
+
return {
|
|
51
|
+
writeHead(status, hdrs) {
|
|
52
|
+
statusCode = status;
|
|
53
|
+
if (hdrs) Object.assign(headers, hdrs);
|
|
54
|
+
},
|
|
55
|
+
setHeader(k, v) {
|
|
56
|
+
headers[k] = v;
|
|
57
|
+
},
|
|
58
|
+
write(chunk) {
|
|
59
|
+
bodyChunks.push(chunk);
|
|
60
|
+
},
|
|
61
|
+
end(chunk) {
|
|
62
|
+
if (chunk) bodyChunks.push(chunk);
|
|
63
|
+
},
|
|
64
|
+
get result() {
|
|
65
|
+
const buf = Buffer.isBuffer(bodyChunks[0])
|
|
66
|
+
? Buffer.concat(bodyChunks)
|
|
67
|
+
: Buffer.from(bodyChunks.join(''));
|
|
68
|
+
return { statusCode, headers, buf };
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
const res1 = createMockRes();
|
|
75
|
+
const handled1 = handleStaticRoute(mockReq('/docs'), res1, '/docs', 3002);
|
|
76
|
+
assert.strictEqual(handled1, true, '/docs should be handled by staticRoute');
|
|
77
|
+
assert.strictEqual(res1.result.statusCode, 200);
|
|
78
|
+
assert.strictEqual(res1.result.headers['Content-Type'], 'text/html; charset=utf-8');
|
|
79
|
+
assert.ok(res1.result.headers['ETag'], 'Should return an ETag');
|
|
80
|
+
assert.ok(res1.result.headers['Content-Encoding'], 'Should use compression (e.g. br or gzip)');
|
|
81
|
+
assert.ok(res1.result.buf.length > 0, 'Should return compressed body');
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
const res2 = createMockRes();
|
|
85
|
+
const handled2 = handleStaticRoute(mockReq('/settings/docs'), res2, '/settings/docs', 3002);
|
|
86
|
+
assert.strictEqual(handled2, true, '/settings/docs should be handled by staticRoute');
|
|
87
|
+
assert.strictEqual(res2.result.statusCode, 200);
|
|
88
|
+
assert.strictEqual(res2.result.headers['Content-Type'], 'text/html; charset=utf-8');
|
|
89
|
+
assert.strictEqual(res2.result.headers['ETag'], res1.result.headers['ETag'], 'ETags should match for identical bundles');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await t.test('settings page contains navigation button linking to /docs', () => {
|
|
93
|
+
const settingsHtml = getSettingsHtml();
|
|
94
|
+
assert.ok(settingsHtml.includes('href="/docs"'), 'Settings navbar should link to /docs');
|
|
95
|
+
assert.ok(settingsHtml.includes('data-i18n="btnDocsGuide"'), 'Settings navbar has i18n key for docs guide');
|
|
96
|
+
assert.ok(settingsHtml.includes('btnDocsGuide:'), 'SETTINGS_I18N contains btnDocsGuide entry');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await t.test('docs page comprehensively supports smartphones and mobile viewports', () => {
|
|
100
|
+
const html = getDocsHtml();
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
assert.ok(html.includes('@media (max-width: 960px)'), 'Includes tablet/mobile breakpoint');
|
|
104
|
+
assert.ok(html.includes('@media (max-width: 480px)'), 'Includes small phone breakpoint');
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
assert.ok(html.includes('scroll-snap-type: x proximity'), 'Supports touch horizontal snap');
|
|
108
|
+
assert.ok(html.includes('-webkit-overflow-scrolling: touch'), 'Supports momentum scrolling on iOS');
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
assert.ok(html.includes('id="btn-scroll-top"'), 'Contains floating back to top button');
|
|
112
|
+
assert.ok(html.includes('.btn-scroll-top'), 'Contains styles for floating button');
|
|
113
|
+
assert.ok(html.includes('initScrollTop'), 'Initializes scroll-to-top handler');
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
assert.ok(html.includes('initScrollSpy'), 'Initializes scrollspy observer');
|
|
117
|
+
assert.ok(html.includes('safe-area-inset-bottom'), 'Respects mobile safe area insets');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await t.test('docs page has exhaustive technical Table of Contents sub-anchors and model tuning playbook', () => {
|
|
121
|
+
const html = getDocsHtml();
|
|
122
|
+
|
|
123
|
+
const expectedSubAnchors = [
|
|
124
|
+
'arch-topology', 'arch-ipc', 'arch-persistence',
|
|
125
|
+
'engine-arch', 'engine-quant', 'engine-flags',
|
|
126
|
+
'cpu-bandwidth', 'cpu-thrashing', 'cpu-modes',
|
|
127
|
+
'vram-weights', 'vram-kv', 'vram-overhead', 'vram-split',
|
|
128
|
+
'lan-scanner', 'lan-ports', 'lan-security',
|
|
129
|
+
'cmd-cli', 'params-load', 'params-sampling', 'api-reference',
|
|
130
|
+
'tuning-tiers', 'tuning-oom', 'tuning-pcie'
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
for (const anchor of expectedSubAnchors) {
|
|
134
|
+
assert.ok(html.includes(`href="#${anchor}"`), `TOC should link to sub-anchor #${anchor}`);
|
|
135
|
+
assert.ok(html.includes(`id="${anchor}"`), `Content section should contain anchor id="${anchor}"`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
assert.ok(html.includes('contextLength'), 'Includes contextLength parameter card');
|
|
139
|
+
assert.ok(html.includes('gpuOffload'), 'Includes gpuOffload parameter card');
|
|
140
|
+
assert.ok(html.includes('htMode'), 'Includes htMode parameter card');
|
|
141
|
+
assert.ok(html.includes('flashAttn'), 'Includes flashAttn parameter card');
|
|
142
|
+
assert.ok(html.includes('batchSize'), 'Includes batchSize parameter card');
|
|
143
|
+
assert.ok(html.includes('splitMode'), 'Includes splitMode parameter card');
|
|
144
|
+
assert.ok(html.includes('kvQuant'), 'Includes kvQuant parameter card');
|
|
145
|
+
|
|
146
|
+
assert.ok(html.includes('temperature'), 'Includes temperature sampling card');
|
|
147
|
+
assert.ok(html.includes('min_p'), 'Includes min_p sampling card');
|
|
148
|
+
assert.ok(html.includes('top_p'), 'Includes top_p sampling card');
|
|
149
|
+
assert.ok(html.includes('repeatPenalty'), 'Includes repeatPenalty sampling card');
|
|
150
|
+
assert.ok(html.includes('maxTokens'), 'Includes maxTokens sampling card');
|
|
151
|
+
|
|
152
|
+
assert.ok(html.includes('/lan'), 'Includes /lan command documentation');
|
|
153
|
+
assert.ok(html.includes('/model'), 'Includes /model command documentation');
|
|
154
|
+
assert.ok(html.includes('/provider'), 'Includes /provider command documentation');
|
|
155
|
+
assert.ok(html.includes('/eval'), 'Includes /eval command documentation');
|
|
156
|
+
assert.ok(html.includes('/vram'), 'Includes /vram command documentation');
|
|
157
|
+
assert.ok(html.includes('/gpu'), 'Includes /gpu command documentation');
|
|
158
|
+
assert.ok(html.includes('/mcp'), 'Includes /mcp command documentation');
|
|
159
|
+
|
|
160
|
+
assert.ok(html.includes('highlightSublink'), 'Includes highlightSublink function');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await t.test('docs page has 100% comprehensive i18n coverage across TH, ZH, and EN', () => {
|
|
164
|
+
const html = getDocsHtml();
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
const matches = [...html.matchAll(/data-i18n="([^"]+)"/g)].map(m => m[1]);
|
|
168
|
+
const uniqueKeys = [...new Set(matches)];
|
|
169
|
+
|
|
170
|
+
assert.ok(uniqueKeys.length >= 80, `Expected at least 80 data-i18n keys across all chapters, found ${uniqueKeys.length}`);
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
const dictMatch = html.match(/const DOCS_I18N = (\{[\s\S]*?\n\s*\};)/);
|
|
174
|
+
assert.ok(dictMatch, 'DOCS_I18N object should exist in HTML script');
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
const getDict = new Function(`return ${dictMatch[1]}`);
|
|
178
|
+
const dict = getDict();
|
|
179
|
+
|
|
180
|
+
assert.ok(dict.th && dict.zh && dict.en, 'Dictionary should have th, zh, and en languages');
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
const missingTh = uniqueKeys.filter(k => dict.th[k] === undefined);
|
|
184
|
+
const missingZh = uniqueKeys.filter(k => dict.zh[k] === undefined);
|
|
185
|
+
const missingEn = uniqueKeys.filter(k => dict.en[k] === undefined);
|
|
186
|
+
|
|
187
|
+
assert.deepStrictEqual(missingTh, [], `Missing TH translations for keys: ${missingTh.join(', ')}`);
|
|
188
|
+
assert.deepStrictEqual(missingZh, [], `Missing ZH translations for keys: ${missingZh.join(', ')}`);
|
|
189
|
+
assert.deepStrictEqual(missingEn, [], `Missing EN translations for keys: ${missingEn.join(', ')}`);
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
const thKeys = Object.keys(dict.th).sort();
|
|
193
|
+
const zhKeys = Object.keys(dict.zh).sort();
|
|
194
|
+
const enKeys = Object.keys(dict.en).sort();
|
|
195
|
+
assert.deepStrictEqual(thKeys, zhKeys, 'TH and ZH should have identical keys');
|
|
196
|
+
assert.deepStrictEqual(thKeys, enKeys, 'TH and EN should have identical keys');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import test, { after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { Readable } from 'node:stream';
|
|
7
|
+
import { EventEmitter } from 'node:events';
|
|
8
|
+
import vm from 'node:vm';
|
|
9
|
+
|
|
10
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'kiku-inference-'));
|
|
11
|
+
process.env.KIKU_DATA_DIR = temp;
|
|
12
|
+
process.env.NODE_ENV = 'test';
|
|
13
|
+
process.env.WEBCHAT_ADMIN_KEY = 'inference-test-admin';
|
|
14
|
+
process.env.WEBCHAT_ALLOW_TEST_ADMIN = 'false';
|
|
15
|
+
after(() => fs.rmSync(temp, { recursive: true, force: true }));
|
|
16
|
+
const { validateInferenceBaseUrl, sanitizeInferenceError, isTrustedRequest } = await import('../../web-chat/core/urlGuard.js');
|
|
17
|
+
const { handleProviderUpdateRoute } = await import('../../web-chat/routes/providerRoute.js');
|
|
18
|
+
const { handleChatRoute } = await import('../../web-chat/routes/chatRoute.js');
|
|
19
|
+
const { handleAiSuggestRoute } = await import('../../web-chat/routes/suggestRoute.js');
|
|
20
|
+
const { setCurrentAgent } = await import('../../web-chat/core/connection.js');
|
|
21
|
+
const { getSettingsHtml } = await import('../../web-chat/ui/settingsBuilder.js');
|
|
22
|
+
|
|
23
|
+
function request(body, ip = '127.0.0.1', key = '') {
|
|
24
|
+
const req = Readable.from([JSON.stringify(body)]);
|
|
25
|
+
req.socket = { remoteAddress: ip };
|
|
26
|
+
req.headers = key ? { authorization: 'Bearer ' + key } : {};
|
|
27
|
+
return req;
|
|
28
|
+
}
|
|
29
|
+
function response() {
|
|
30
|
+
const res = new EventEmitter();
|
|
31
|
+
res.chunks = '';
|
|
32
|
+
res.writeHead = (code) => { res.statusCode = code; res.headersSent = true; };
|
|
33
|
+
res.write = (data) => { res.chunks += data; };
|
|
34
|
+
res.end = (data = '') => { res.chunks += data; res.writableEnded = true; res.emit('finish'); };
|
|
35
|
+
res.json = () => JSON.parse(res.chunks);
|
|
36
|
+
return res;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('inference URL guard rejects dangerous schemes, metadata aliases, and private remote targets', async () => {
|
|
40
|
+
for (const url of ['file:///etc/passwd', 'ftp://example.com', 'http://user:pass@example.com', 'http://169.254.169.254', 'http://[::ffff:a9fe:a9fe]', 'http://metadata.google.internal', 'http://[fd00:ec2::254]']) {
|
|
41
|
+
assert.equal((await validateInferenceBaseUrl(url, { callerIsTrusted: true })).ok, false, url);
|
|
42
|
+
}
|
|
43
|
+
for (const url of ['http://127.0.0.1:1234/v1', 'http://192.168.1.5:8000', 'http://[::1]:1234', 'http://[::ffff:7f00:1]']) {
|
|
44
|
+
assert.equal((await validateInferenceBaseUrl(url)).ok, false, url);
|
|
45
|
+
assert.equal((await validateInferenceBaseUrl(url, { callerIsTrusted: true })).ok, true, url);
|
|
46
|
+
}
|
|
47
|
+
assert.equal((await validateInferenceBaseUrl('https://8.8.8.8/v1')).ok, true);
|
|
48
|
+
assert.equal((await validateInferenceBaseUrl('https://example.com/' + 'x'.repeat(2048))).ok, false);
|
|
49
|
+
});
|
|
50
|
+
test('DNS checks every answer and blocks metadata even for trusted callers', async () => {
|
|
51
|
+
const lookupHost = async () => [{ address: '8.8.8.8' }, { address: '169.254.1.1' }];
|
|
52
|
+
assert.equal((await validateInferenceBaseUrl('https://inference.example', { callerIsTrusted: true, lookupHost })).ok, false);
|
|
53
|
+
assert.equal((await validateInferenceBaseUrl('http://local.example', { callerIsTrusted: true, lookupHost: async () => [{address:'192.168.1.1'}] })).ok, true);
|
|
54
|
+
});
|
|
55
|
+
test('remote errors are generic and local diagnostics are capped and redacted', () => {
|
|
56
|
+
assert.equal(sanitizeInferenceError('secret upstream response').includes('secret'), false);
|
|
57
|
+
const text = sanitizeInferenceError('Bearer secret sk-testsecret ' + 'x'.repeat(300), true);
|
|
58
|
+
assert.ok(text.length <= 200);
|
|
59
|
+
assert.ok(!text.includes('secret'));
|
|
60
|
+
assert.equal(isTrustedRequest(request({}, '203.0.113.1', 'wrong')), false);
|
|
61
|
+
assert.equal(isTrustedRequest(request({}, '203.0.113.1', 'inference-test-admin')), true);
|
|
62
|
+
});
|
|
63
|
+
test('provider validates model and returns live switch failures', async () => {
|
|
64
|
+
let res = response();
|
|
65
|
+
await handleProviderUpdateRoute(request({provider:'lmstudio', model:'bad<script>'}), res);
|
|
66
|
+
assert.equal(res.statusCode, 400);
|
|
67
|
+
setCurrentAgent({ client: { provider:'lmstudio', switchProvider: async () => { throw new Error('switch failed'); } } });
|
|
68
|
+
res = response();
|
|
69
|
+
await handleProviderUpdateRoute(request({provider:'lmstudio', model:'google/gemma-4-e4b'}), res);
|
|
70
|
+
assert.equal(res.statusCode, 200);
|
|
71
|
+
assert.equal(res.json().switchError, 'switch failed');
|
|
72
|
+
});
|
|
73
|
+
test('remote cloud calls require actual admin credentials, including ai-suggest', async () => {
|
|
74
|
+
for (const route of [handleChatRoute, handleAiSuggestRoute]) {
|
|
75
|
+
const res = response();
|
|
76
|
+
await route(request({provider:'openai', query:'hello'}, '203.0.113.1', 'wrong'), res);
|
|
77
|
+
assert.equal(res.statusCode, 403);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
test('invalid provider and oversized query are client errors', async () => {
|
|
81
|
+
for (const body of [{query:'hello',provider:'no-such-provider'}, {query:'x'.repeat(32001)}]) {
|
|
82
|
+
const res = response();
|
|
83
|
+
await handleChatRoute(request(body), res);
|
|
84
|
+
assert.equal(res.statusCode, 400);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
test('settings generated inline scripts compile and define all inline handlers', () => {
|
|
88
|
+
const html = getSettingsHtml();
|
|
89
|
+
let scriptContent = '';
|
|
90
|
+
for (const match of html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)) {
|
|
91
|
+
new vm.Script(match[1]);
|
|
92
|
+
scriptContent += '\n' + match[1];
|
|
93
|
+
}
|
|
94
|
+
assert.ok(scriptContent.includes('function onGpuOffloadChange'), 'onGpuOffloadChange must be defined');
|
|
95
|
+
assert.ok(scriptContent.includes('isLocal && !modelLoaded'), 'testConnectionProbe must distinguish server online vs model loaded in VRAM');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('settings page comprehensively supports 3 languages (Thai, Chinese, English)', () => {
|
|
99
|
+
const html = getSettingsHtml();
|
|
100
|
+
assert.ok(html.includes('id="btn-lang-toggle"'), 'Language toggle button must be present in navbar');
|
|
101
|
+
assert.ok(html.includes('toggleSettingsLanguage()'), 'Language toggle button must invoke toggleSettingsLanguage()');
|
|
102
|
+
assert.ok(html.includes('data-i18n="heroTitle"'), 'Hero section must have data-i18n');
|
|
103
|
+
assert.ok(html.includes('data-i18n="tabEngine"'), 'Tabs must have data-i18n');
|
|
104
|
+
|
|
105
|
+
let scriptContent = '';
|
|
106
|
+
for (const match of html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)) {
|
|
107
|
+
scriptContent += '\n' + match[1];
|
|
108
|
+
}
|
|
109
|
+
assert.ok(scriptContent.includes('const SETTINGS_I18N = {'), 'SETTINGS_I18N dictionary must be defined');
|
|
110
|
+
assert.ok(scriptContent.includes('function applySettingsLanguage('), 'applySettingsLanguage must be defined');
|
|
111
|
+
assert.ok(scriptContent.includes('function toggleSettingsLanguage()'), 'toggleSettingsLanguage must be defined');
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
const sandbox = { window: {}, document: {}, localStorage: { getItem: () => 'th', setItem: () => {} }, console };
|
|
115
|
+
const ctx = vm.createContext(sandbox);
|
|
116
|
+
const i18nMatch = scriptContent.match(/const SETTINGS_I18N\s*=\s*(\{[\s\S]*?\n\s{4}\};)/);
|
|
117
|
+
assert.ok(i18nMatch, 'SETTINGS_I18N object match found');
|
|
118
|
+
vm.runInContext(i18nMatch[0].replace('const SETTINGS_I18N', 'SETTINGS_I18N'), ctx);
|
|
119
|
+
const dict = sandbox.SETTINGS_I18N;
|
|
120
|
+
assert.ok(dict.th && dict.zh && dict.en, 'All 3 languages (th, zh, en) must be defined in SETTINGS_I18N');
|
|
121
|
+
const thKeys = Object.keys(dict.th);
|
|
122
|
+
assert.ok(thKeys.length >= 70, 'Dictionary should cover at least 70 keys');
|
|
123
|
+
for (const key of thKeys) {
|
|
124
|
+
assert.ok(dict.zh[key], `Chinese translation missing for key: ${key}`);
|
|
125
|
+
assert.ok(dict.en[key], `English translation missing for key: ${key}`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const settingsApi = await import('../config.js');
|
|
130
|
+
const { handleStatusRoute } = await import('../../web-chat/routes/statusRoute.js');
|
|
131
|
+
const { checkRateLimit } = await import('../../web-chat/core/security.js');
|
|
132
|
+
test('inference persistence clamps, round trips, and preserves unrelated settings under queued writes', async () => {
|
|
133
|
+
fs.writeFileSync(path.join(temp, 'settings.json'), JSON.stringify({ legacy: 'kept' }));
|
|
134
|
+
await Promise.all([
|
|
135
|
+
settingsApi.withSettingsWriteQueue(() => settingsApi.savePersistedProviderBaseUrl('vllm', 'http://127.0.0.1:8000/v1')),
|
|
136
|
+
settingsApi.withSettingsWriteQueue(() => settingsApi.savePersistedActiveProvider('lmstudio', 'model-a')),
|
|
137
|
+
settingsApi.withSettingsWriteQueue(() => settingsApi.savePersistedProviderModel('lmstudio', 'model-b')),
|
|
138
|
+
settingsApi.withSettingsWriteQueue(() => settingsApi.savePersistedInferenceSettings({ temperature: 9, topP: 0, contextLimit: 1000000, persona: 'fan', thinking: 'hide', stream: false }))
|
|
139
|
+
]);
|
|
140
|
+
const saved = JSON.parse(fs.readFileSync(path.join(temp, 'settings.json'), 'utf8'));
|
|
141
|
+
assert.equal(saved.legacy, 'kept');
|
|
142
|
+
assert.equal(saved.providerModels.lmstudio, 'model-b');
|
|
143
|
+
assert.equal(saved.providerBaseUrls.vllm, 'http://127.0.0.1:8000/v1');
|
|
144
|
+
assert.deepEqual(saved.inference, { temperature:2, topP:0.01, contextLimit:131072, persona:'fan', thinking:'hide', stream:false });
|
|
145
|
+
settingsApi.invalidateSettingsCache();
|
|
146
|
+
assert.deepEqual(settingsApi.getPersistedInferenceSettings(), saved.inference);
|
|
147
|
+
assert.ok(!fs.readdirSync(temp).some(name => name.endsWith('.tmp')));
|
|
148
|
+
await assert.rejects(settingsApi.withSettingsWriteQueue(() => { throw new Error('write failure'); }));
|
|
149
|
+
await settingsApi.withSettingsWriteQueue(() => settingsApi.savePersistedInferenceSettings({temperature:0}));
|
|
150
|
+
assert.equal(settingsApi.getPersistedInferenceSettings().temperature, 0);
|
|
151
|
+
});
|
|
152
|
+
test('rate limit buckets remain independent', () => {
|
|
153
|
+
for(let n=0;n<6;n++) assert.equal(checkRateLimit('test-ip', 'lan-discovery', 6).allowed, true);
|
|
154
|
+
assert.equal(checkRateLimit('test-ip', 'lan-discovery', 6).allowed, false);
|
|
155
|
+
assert.equal(checkRateLimit('test-ip', 'chat', 60).allowed, true);
|
|
156
|
+
for(let n=0;n<120;n++) assert.equal(checkRateLimit('test-ip', 'status-light', 120).allowed, true);
|
|
157
|
+
assert.equal(checkRateLimit('test-ip', 'status-light', 120).allowed, false);
|
|
158
|
+
});
|
|
159
|
+
test('light status performs no fetches and reports persisted inference', async t => {
|
|
160
|
+
const fetchMock = t.mock.method(globalThis, 'fetch', () => { throw new Error('light status must not fetch'); });
|
|
161
|
+
const res = response();
|
|
162
|
+
await handleStatusRoute(request({}), res, new URL('http://localhost/api/status?light=1'), 3000);
|
|
163
|
+
assert.equal(res.statusCode, 200);
|
|
164
|
+
assert.equal(res.json().light, true);
|
|
165
|
+
assert.equal(fetchMock.mock.callCount(), 0);
|
|
166
|
+
assert.equal(res.json().inference.contextLimit, 131072);
|
|
167
|
+
});
|
|
168
|
+
test('chat caps inference overrides and emits visible fallback before catalog answer', async () => {
|
|
169
|
+
let options;
|
|
170
|
+
const resolveConnection = async opts => { options = opts; return { ready:false, error:'Upstream 429' }; };
|
|
171
|
+
let res = response();
|
|
172
|
+
await handleChatRoute(request({query:'hello',provider:'lmstudio',stream:false}), res, null, {resolveConnection});
|
|
173
|
+
assert.equal(res.statusCode, 502);
|
|
174
|
+
res = response();
|
|
175
|
+
await handleChatRoute(request({query:'hello',provider:'lmstudio',stream:true}), res, null, {resolveConnection});
|
|
176
|
+
assert.equal(res.statusCode, 200);
|
|
177
|
+
const events = res.chunks.split('\n').filter(line => line.startsWith('data: {')).map(line => JSON.parse(line.slice(6)));
|
|
178
|
+
assert.equal(events[0].type, 'error');
|
|
179
|
+
assert.equal(events[0].fallback, true);
|
|
180
|
+
assert.ok(events.some(e => e.type === 'final'));
|
|
181
|
+
assert.ok(res.chunks.includes('[DONE]'));
|
|
182
|
+
assert.equal(options.thinking, 'hide');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const { KIKU_DEFAULT_MODEL } = await import('../modelDefaults.js');
|
|
186
|
+
const connModule = await import('../../web-chat/core/connection.js');
|
|
187
|
+
const { handleLanApplyRoute } = await import('../../web-chat/routes/lanDiscoveryRoute.js');
|
|
188
|
+
|
|
189
|
+
test('model defaults and web agent connection cleanup', () => {
|
|
190
|
+
assert.equal(KIKU_DEFAULT_MODEL, 'google/gemma-4-e4b');
|
|
191
|
+
assert.equal(typeof connModule.setCurrentAgent, 'function');
|
|
192
|
+
assert.equal(connModule.webAgentCache, undefined);
|
|
193
|
+
const html = getSettingsHtml();
|
|
194
|
+
assert.ok(html.includes("window.KIKU_DEFAULT_MODEL = '" + KIKU_DEFAULT_MODEL + "'"));
|
|
195
|
+
assert.ok(html.includes('id="btn-lang-toggle"'));
|
|
196
|
+
assert.ok(html.includes('gemini-3.7-flash'));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('lan apply route persists configuration for discovered host', async () => {
|
|
200
|
+
const req = request({
|
|
201
|
+
host: '192.168.1.120',
|
|
202
|
+
port: 1234,
|
|
203
|
+
serverType: 'lmstudio',
|
|
204
|
+
model: 'custom-model'
|
|
205
|
+
});
|
|
206
|
+
const res = response();
|
|
207
|
+
await handleLanApplyRoute(req, res);
|
|
208
|
+
assert.equal(res.statusCode, 200);
|
|
209
|
+
assert.equal(res.json().status, 'ok');
|
|
210
|
+
assert.equal(res.json().applied.provider, 'lmstudio');
|
|
211
|
+
assert.equal(res.json().applied.model, 'custom-model');
|
|
212
|
+
assert.equal(res.json().applied.baseUrl, 'http://192.168.1.120:1234/v1');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('chat route accepts attached images with empty query and injects attachment context', async () => {
|
|
216
|
+
let capturedQuery = null;
|
|
217
|
+
let capturedDirectives = null;
|
|
218
|
+
const resolveConnection = async () => ({
|
|
219
|
+
ready: true,
|
|
220
|
+
agent: {
|
|
221
|
+
run: async (q, opts) => {
|
|
222
|
+
capturedQuery = q;
|
|
223
|
+
capturedDirectives = opts.systemDirectives;
|
|
224
|
+
return { answer: 'รูปภาพนี้คือผลงานของจวีจิ้งอีค่ะ' };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const attachments = [
|
|
230
|
+
{ name: 'jujingyi_drama.jpg', size: 10240, type: 'image/jpeg', isImage: true, dataUrl: 'data:image/jpeg;base64,/9j/4AAQSkZJRg==' }
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
const resStream = response();
|
|
235
|
+
await handleChatRoute(request({ query: '', attachments, provider: 'lmstudio', stream: true }), resStream, null, { resolveConnection });
|
|
236
|
+
assert.equal(resStream.statusCode, 200);
|
|
237
|
+
assert.ok(capturedQuery.includes('jujingyi_drama.jpg'));
|
|
238
|
+
assert.ok(capturedQuery.includes('[USER ATTACHMENT:'));
|
|
239
|
+
assert.ok(capturedDirectives.some(d => d.includes('User Image Attachments Received')));
|
|
240
|
+
const events = resStream.chunks.split('\n').filter(line => line.startsWith('data: {')).map(line => JSON.parse(line.slice(6)));
|
|
241
|
+
const finalEvent = events.find(e => e.type === 'final');
|
|
242
|
+
assert.ok(finalEvent);
|
|
243
|
+
assert.equal(finalEvent.attachments?.length, 1);
|
|
244
|
+
assert.equal(finalEvent.attachments[0].name, 'jujingyi_drama.jpg');
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
const resJson = response();
|
|
248
|
+
await handleChatRoute(request({ query: 'ช่วยดูรูปนี้ให้หน่อย', attachments, provider: 'lmstudio', stream: false }), resJson, null, { resolveConnection });
|
|
249
|
+
assert.equal(resJson.statusCode, 200);
|
|
250
|
+
const jsonBody = resJson.json();
|
|
251
|
+
assert.equal(jsonBody.status, 'ok');
|
|
252
|
+
assert.equal(jsonBody.attachments?.length, 1);
|
|
253
|
+
assert.equal(jsonBody.attachments[0].name, 'jujingyi_drama.jpg');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('chat route rescues refusal or empty answers for image attachments with helpful image fallback', async () => {
|
|
257
|
+
const resolveConnection = async () => ({
|
|
258
|
+
ready: true,
|
|
259
|
+
agent: {
|
|
260
|
+
run: async () => ({ answer: 'ขออภัยค่ะ ไม่สามารถประมวลผลคำตอบได้' })
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const attachments = [
|
|
265
|
+
{ name: 'in_blossom_poster.jpg', size: 20480, type: 'image/jpeg', isImage: true, dataUrl: 'data:image/jpeg;base64,/9j/4AAQSkZJRg==' }
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
const resJson = response();
|
|
269
|
+
await handleChatRoute(request({ query: '', attachments, provider: 'lmstudio', stream: false }), resJson, null, { resolveConnection });
|
|
270
|
+
assert.equal(resJson.statusCode, 200);
|
|
271
|
+
const jsonBody = resJson.json();
|
|
272
|
+
assert.ok(!jsonBody.answer.includes('ขออภัยค่ะ ไม่สามารถประมวลผลคำตอบได้'));
|
|
273
|
+
assert.ok(jsonBody.answer.includes('ได้รับรูปภาพเรียบร้อยแล้วค่ะ') || jsonBody.answer.includes('In Blossom') || jsonBody.answer.includes('ฮวาเจียนลิ่ง'));
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('chat route resolves catalog entities from image filename into recognition hints', async () => {
|
|
277
|
+
let capturedQuery = null;
|
|
278
|
+
const resolveConnection = async () => ({
|
|
279
|
+
ready: true,
|
|
280
|
+
agent: {
|
|
281
|
+
run: async (q) => {
|
|
282
|
+
capturedQuery = q;
|
|
283
|
+
return { answer: 'รับทราบภาพ In Blossom ค่ะ' };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const attachments = [
|
|
289
|
+
{ name: 'in_blossom.jpg', size: 15000, type: 'image/jpeg', isImage: true, dataUrl: 'data:image/jpeg;base64,/9j/4AAQSkZJRg==' }
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
const resJson = response();
|
|
293
|
+
await handleChatRoute(request({ query: 'รูปนี้เรื่องอะไร', attachments, provider: 'lmstudio', stream: false }), resJson, null, { resolveConnection });
|
|
294
|
+
assert.equal(resJson.statusCode, 200);
|
|
295
|
+
assert.ok(capturedQuery.includes('[IMAGE RECOGNITION HINTS:'));
|
|
296
|
+
assert.ok(capturedQuery.includes('In Blossom') || capturedQuery.includes('花间令'));
|
|
297
|
+
});
|
|
298
|
+
|