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,300 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { getWebChatHtml } from '../../webChatHtml.js';
|
|
4
|
+
import { executeDirectCatalogKnowledge, makeInspectableToolPayload } from '../../webChatServer.js';
|
|
5
|
+
|
|
6
|
+
test('web chat tool payloads redact secrets and bound large values', () => {
|
|
7
|
+
const payload = makeInspectableToolPayload({
|
|
8
|
+
query: 'In Blossom',
|
|
9
|
+
apiKey: 'must-not-leak',
|
|
10
|
+
nested: { token: 'must-not-leak-either' },
|
|
11
|
+
values: Array.from({ length: 5 }, (_, index) => index)
|
|
12
|
+
}, { maxArrayItems: 2 });
|
|
13
|
+
|
|
14
|
+
assert.equal(payload.query, 'In Blossom');
|
|
15
|
+
assert.equal(payload.apiKey, '[redacted]');
|
|
16
|
+
assert.equal(payload.nested.token, '[redacted]');
|
|
17
|
+
assert.deepEqual(payload.values, [0, 1, '… [ตัดอีก 3 รายการ]']);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('direct catalog fallback retains inspectable tool input and result', async () => {
|
|
21
|
+
const response = await executeDirectCatalogKnowledge('ผลงานซีรีส์ In Blossom');
|
|
22
|
+
const tool = response.tools.find(item => item.name === 'search_works');
|
|
23
|
+
|
|
24
|
+
assert.ok(tool);
|
|
25
|
+
assert.equal(typeof tool.input, 'object');
|
|
26
|
+
assert.equal(typeof tool.result, 'object');
|
|
27
|
+
assert.ok(Number.isFinite(tool.durationMs));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('web chat HTML isolates answer text and renders expandable tool details', () => {
|
|
31
|
+
const html = getWebChatHtml();
|
|
32
|
+
|
|
33
|
+
assert.match(html, /class="answer-content"/);
|
|
34
|
+
assert.match(html, /class="tool-inspector"/);
|
|
35
|
+
|
|
36
|
+
assert.match(html, /renderToolParamChips\(t\)/);
|
|
37
|
+
assert.match(html, /summarizeToolResult\(t\)/);
|
|
38
|
+
assert.match(html, /tool-call-line/);
|
|
39
|
+
assert.match(html, /tool-result-strip tool-tone-/);
|
|
40
|
+
assert.match(html, /tool-param-chip/);
|
|
41
|
+
assert.match(html, /function renderJsonTree\(/);
|
|
42
|
+
assert.match(html, /function copyToolJson\(/);
|
|
43
|
+
assert.match(html, /ข้อมูลดิบ \(JSON\)/);
|
|
44
|
+
assert.match(html, /\.tool-raw-json\s*\{[^}]*max-width:\s*100%/);
|
|
45
|
+
assert.match(html, /\.tool-raw-json\s*\{[^}]*box-sizing:\s*border-box/);
|
|
46
|
+
assert.match(html, /\.tool-raw-json\s*\{[^}]*overflow:\s*hidden/);
|
|
47
|
+
assert.match(html, /\.tool-json-tree\s*\{[^}]*word-break:\s*break-word/);
|
|
48
|
+
assert.match(html, /\.tool-view-switch-row\s*\{[^}]*flex-wrap:\s*wrap/);
|
|
49
|
+
|
|
50
|
+
assert.doesNotMatch(html, /tool-raw-json[^\n]*106\.38/);
|
|
51
|
+
assert.doesNotMatch(html, /\.tool-inspector:has\(\.tool-raw-json\[open\]\)\s*\{\s*overflow:\s*visible/);
|
|
52
|
+
|
|
53
|
+
assert.doesNotMatch(html, /class="tool-payload"/);
|
|
54
|
+
assert.doesNotMatch(html, /toggleInspectorJsonView/);
|
|
55
|
+
assert.match(html, /extractQuoteFromAnswer\(answerRoot\)/);
|
|
56
|
+
assert.match(html, /restoreInspectorState\(bubble/);
|
|
57
|
+
assert.match(html, /--tool-accent:\s*#0369a1/);
|
|
58
|
+
assert.match(html, /sessionId:\s*currentSessionId/);
|
|
59
|
+
assert.match(html, /chatHistory\.slice\(0, -1\)\.slice\(-24\)/);
|
|
60
|
+
assert.doesNotMatch(html, /Deep Reasoning Trace/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('generated web chat inline scripts remain syntactically valid', () => {
|
|
64
|
+
const html = getWebChatHtml();
|
|
65
|
+
const scripts = Array.from(html.matchAll(/<script(?:\s[^>]*)?>([\s\S]*?)<\/script>/gi), match => match[1]);
|
|
66
|
+
|
|
67
|
+
assert.ok(scripts.length > 0);
|
|
68
|
+
scripts.forEach((script) => {
|
|
69
|
+
assert.doesNotThrow(() => new Function(script));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('drama cards use matching OST video ids and hide unavailable tracks', () => {
|
|
74
|
+
const html = getWebChatHtml();
|
|
75
|
+
|
|
76
|
+
assert.match(html, /youtubeId: 'oEgxqij58LQ',[\s\S]*?ostTitle: '叹云兮 \(Tan Yun Xi\) OST'/);
|
|
77
|
+
assert.match(html, /youtubeId: '-vf6jU96WI4',[\s\S]*?ostTitle: '千年等一回 \(A Millennium Wait\)'/);
|
|
78
|
+
assert.match(html, /youtubeId: '9ke3efb9U7A',[\s\S]*?ostTitle: '芙蓉 \(Fu Rong\) OST'/);
|
|
79
|
+
assert.match(html, /youtubeId: 'lzDP2MfP3eA',[\s\S]*?ostTitle: '如纱 \(Ru Sha\) OST'/);
|
|
80
|
+
assert.match(html, /drama\.youtubeId && drama\.ostTitle/);
|
|
81
|
+
assert.doesNotMatch(html, /youtubeId: 'qjA7K9pM_U8'/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('web chat html registers sw.js and sw.js is accessible', () => {
|
|
85
|
+
const html = getWebChatHtml();
|
|
86
|
+
assert.match(html, /navigator\.serviceWorker\.register\('\/sw\.js'\)/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('floating mini player contains disc dock, progress bar, video toggle, and jukebox controls', () => {
|
|
90
|
+
const html = getWebChatHtml();
|
|
91
|
+
assert.match(html, /id="mini-player-dock"/);
|
|
92
|
+
assert.match(html, /id="mini-player-card"/);
|
|
93
|
+
assert.match(html, /id="btn-mini-disc"/);
|
|
94
|
+
assert.match(html, /id="mini-disc-progress-circle"/);
|
|
95
|
+
assert.match(html, /id="mini-player-progressbar"/);
|
|
96
|
+
assert.match(html, /onclick="toggleMiniPlayerShuffle\(\)"/);
|
|
97
|
+
assert.match(html, /onclick="toggleMiniPlayerRepeat\(\)"/);
|
|
98
|
+
assert.match(html, /onclick="toggleMiniPlayerVideo\(\)"/);
|
|
99
|
+
assert.match(html, /id="mini-player-video-frame-container"/);
|
|
100
|
+
assert.match(html, /onclick="nextMiniPlayerTrack\(\)"/);
|
|
101
|
+
assert.match(html, /onclick="prevMiniPlayerTrack\(\)"/);
|
|
102
|
+
assert.match(html, /JU_JINGYI_PLAYLIST/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('renderFollowUpSuggestions creates intelligent contextual follow-up topic chips', () => {
|
|
106
|
+
const html = getWebChatHtml();
|
|
107
|
+
assert.match(html, /function renderFollowUpSuggestions\(botGroup, answerText, userQuery, serverSuggestions\)/);
|
|
108
|
+
assert.match(html, /follow-up-suggestions/);
|
|
109
|
+
assert.match(html, /💎 บทบาท Versace/);
|
|
110
|
+
assert.match(html, /👗 แบรนด์อื่นๆ ทั้งหมด/);
|
|
111
|
+
assert.match(html, /🌸 ตัวตนกับรูปลักษณ์/);
|
|
112
|
+
assert.match(html, /🌿 เสน่ห์หานหยุนซี/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('song queries render embedded iframes and tool visual mini iframes', () => {
|
|
116
|
+
const html = getWebChatHtml();
|
|
117
|
+
assert.match(html, /function extractSongEmbedsFromContext/);
|
|
118
|
+
assert.match(html, /function renderSongEmbedCard/);
|
|
119
|
+
assert.match(html, /chat-rich-songs-container/);
|
|
120
|
+
assert.match(html, /chat-song-iframe/);
|
|
121
|
+
assert.match(html, /https:\/\/www\.youtube-nocookie\.com\/embed\//);
|
|
122
|
+
assert.match(html, /tool-card-iframe-wrap/);
|
|
123
|
+
assert.match(html, /tool-card-mini-iframe/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('agent interactive question cards detect questions and render action pills', () => {
|
|
127
|
+
const html = getWebChatHtml();
|
|
128
|
+
assert.match(html, /function detectAgentInteractiveQuestion/);
|
|
129
|
+
assert.match(html, /function renderAgentQuestionCard/);
|
|
130
|
+
assert.match(html, /agent-interactive-question-card/);
|
|
131
|
+
assert.match(html, /agent-q-btn btn-primary-pill/);
|
|
132
|
+
assert.match(html, /submitCustomAgentContinuation/);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('luxury Voice HUD renders live orb, visualizer canvas, and direct action controls', () => { const html = getWebChatHtml();
|
|
136
|
+
assert.match(html, /id="voice-hud"/);
|
|
137
|
+
assert.match(html, /class="voice-live-orb"/);
|
|
138
|
+
assert.match(html, /id="voice-canvas"/);
|
|
139
|
+
assert.match(html, /id="voice-hud-text"/);
|
|
140
|
+
assert.match(html, /id="voice-lang-badge"/);
|
|
141
|
+
assert.match(html, /onclick="sendVoiceQueryDirectly\(\)"/);
|
|
142
|
+
assert.match(html, /onclick="cancelSpeechRecognition\(\)"/);
|
|
143
|
+
assert.match(html, /function startVoiceAudioVisualizer/);
|
|
144
|
+
assert.match(html, /function stopVoiceAudioVisualizer/);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('song playback timestamp syncs and mutually pauses between chat embed and mini player', () => {
|
|
148
|
+
const html = getWebChatHtml();
|
|
149
|
+
assert.match(html, /function openMiniPlayerFromCard\(btnEl, videoIdOrUrl, title\)/);
|
|
150
|
+
assert.match(html, /function pauseAllChatSongIframes\(\)/);
|
|
151
|
+
assert.match(html, /function pauseMiniPlayer\(\)/);
|
|
152
|
+
assert.match(html, /function handleChatIframeLoad\(iframe\)/);
|
|
153
|
+
assert.match(html, /handleChatIframeLoad\(this\)/);
|
|
154
|
+
assert.match(html, /openMiniPlayerFromCard\(this/);
|
|
155
|
+
assert.match(html, /chatIframeTimes/);
|
|
156
|
+
assert.match(html, /&start=/);
|
|
157
|
+
assert.match(html, /sendMiniPlayerCommand\('seekTo'/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('message bubble actions work without disruptive toasts and modal elements exist', () => {
|
|
161
|
+
const html = getWebChatHtml();
|
|
162
|
+
assert.match(html, /id="quote-card-modal"/);
|
|
163
|
+
assert.match(html, /id="quote-canvas"/);
|
|
164
|
+
assert.match(html, /id="quote-text-editor"/);
|
|
165
|
+
assert.match(html, /id="share-card-modal"/);
|
|
166
|
+
assert.match(html, /function copyBubbleText\(btn\)/);
|
|
167
|
+
assert.match(html, /function fallbackCopy\(text\)/);
|
|
168
|
+
assert.match(html, /function regenerateMessage\(target\)/);
|
|
169
|
+
assert.match(html, /function makeQuoteCardFromBubble\(btn\)/);
|
|
170
|
+
assert.match(html, /function toggleBookmark\(btn\)/);
|
|
171
|
+
assert.match(html, /function forkConversation\(target\)/);
|
|
172
|
+
assert.match(html, /function openShareCardFromAction\(btn\)/);
|
|
173
|
+
assert.match(html, /btn\.closest\('\.msg-group'\)/);
|
|
174
|
+
assert.doesNotMatch(html, /function speakMessage/);
|
|
175
|
+
assert.doesNotMatch(html, /aria-label="อ่านออกเสียง"/);
|
|
176
|
+
assert.doesNotMatch(html, /showToast\('คัดลอกข้อความแล้ว'\)/);
|
|
177
|
+
assert.doesNotMatch(html, /showToast\('บันทึก Bookmark เรียบร้อย ⭐'\)/);
|
|
178
|
+
assert.doesNotMatch(html, /id="toast-container"/);
|
|
179
|
+
assert.doesNotMatch(html, /对话已导出/);
|
|
180
|
+
assert.doesNotMatch(html, /\.toast-container/);
|
|
181
|
+
assert.doesNotMatch(html, /\.toast-pill/);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('follow-up suggestions persist and re-render upon page refresh and loadActiveSession', () => {
|
|
185
|
+
const html = getWebChatHtml();
|
|
186
|
+
assert.match(html, /function loadActiveSession\(\)/);
|
|
187
|
+
assert.match(html, /renderFollowUpSuggestions\(lastBotGroup, lastBotMessage\.text, userQ, serverSugg\)/);
|
|
188
|
+
assert.match(html, /serverSuggestions:\s*serverSuggestions\s*\|\|\s*\[\]/);
|
|
189
|
+
assert.match(html, /userQuery:\s*query/);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('web-chat displays Sponsor Spotlight ads with sidebar card, modal, and /ads in-chat card', () => {
|
|
193
|
+
const html = getWebChatHtml();
|
|
194
|
+
assert.match(html, /id="sidebar-ad-card"/);
|
|
195
|
+
assert.match(html, /id="sponsors-modal"/);
|
|
196
|
+
assert.match(html, /id="sponsors-modal-body"/);
|
|
197
|
+
assert.match(html, /function renderSidebarAd\(\)/);
|
|
198
|
+
assert.match(html, /function cycleSidebarAd\(/);
|
|
199
|
+
assert.match(html, /function openSponsorsModal\(/);
|
|
200
|
+
assert.match(html, /function showInChatSponsorCard\(/);
|
|
201
|
+
assert.match(html, /modal\.classList\.add\('open'\)/);
|
|
202
|
+
assert.match(html, /body\.sidebar-collapsed\s+\.sidebar-sponsors\s*\{[^}]*display:\s*none\s*!important/);
|
|
203
|
+
assert.match(html, /class="sidebar-ad-cta"/);
|
|
204
|
+
assert.match(html, /class="sidebar-ad-direct-link"/);
|
|
205
|
+
assert.match(html, /width:\s*300px\s*!important/);
|
|
206
|
+
assert.match(html, /width:\s*310px;\s*max-width:\s*86vw/);
|
|
207
|
+
assert.match(html, /\.sponsors-modal-dialog/);
|
|
208
|
+
assert.match(html, /\.sponsor-url-code/);
|
|
209
|
+
assert.match(html, /DashSecurity/);
|
|
210
|
+
assert.match(html, /ThemeHackers/);
|
|
211
|
+
assert.match(html, /https:\/\/dashsecurity\.kpltgroup\.com\//);
|
|
212
|
+
assert.match(html, /https:\/\/jujingyi-kikuuu\.vercel\.app\//);
|
|
213
|
+
assert.match(html, /https:\/\/github\.com\/Themehackers\//);
|
|
214
|
+
assert.match(html, /lowerQuery === '\/ads'/);
|
|
215
|
+
assert.match(html, /cmd_ads/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('btn-quick-actions renders complete plus sign (+) icon with horizontal and vertical lines', () => {
|
|
219
|
+
const html = getWebChatHtml();
|
|
220
|
+
assert.match(html, /id="btn-quick-actions"/);
|
|
221
|
+
assert.match(html, /<line x1="12" y1="5" x2="12" y2="19"\/><line x1="5" y1="12" x2="19" y2="12"\/>/);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('user bubble renders dedicated renderUserContent with high contrast white text and Kiku rose theme', () => {
|
|
225
|
+
const html = getWebChatHtml();
|
|
226
|
+
assert.match(html, /function renderUserContent\(/);
|
|
227
|
+
assert.match(html, /user-content-body/);
|
|
228
|
+
assert.match(html, /--user-msg:\s*linear-gradient\(135deg,\s*#e11d48/);
|
|
229
|
+
assert.match(html, /\.bubble-user\s*,\s*\.bubble-user\s*\*\s*,\s*\.bubble-user\s*p/);
|
|
230
|
+
assert.match(html, /color:\s*#ffffff\s*!important/);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('clarifying questions and interactive question cards are stably pinned without stream flickering', () => {
|
|
234
|
+
const html = getWebChatHtml();
|
|
235
|
+
assert.match(html, /\.clarify-slot/);
|
|
236
|
+
assert.match(html, /clarifySlot\.className = 'clarify-slot'/);
|
|
237
|
+
assert.match(html, /msgGroup\.insertBefore\(clarifySlot,\s*botBubble\)/);
|
|
238
|
+
assert.match(html, /dismissAgentInteractiveQuestion/);
|
|
239
|
+
assert.match(html, /dismissedAgentQuestions/);
|
|
240
|
+
assert.match(html, /!details\.isStreaming/);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('subagent progress renders a dedicated live card with evidence report', () => {
|
|
244
|
+
const html = getWebChatHtml();
|
|
245
|
+
assert.match(html, /function accumulateSubagentProgress\(/);
|
|
246
|
+
assert.match(html, /function finalizeSubagentFromToolResult\(/);
|
|
247
|
+
assert.match(html, /function renderSubagentCards\(/);
|
|
248
|
+
assert.match(html, /event\.type === 'subagent_progress'/);
|
|
249
|
+
assert.match(html, /subagentsAccum/);
|
|
250
|
+
assert.match(html, /subagents: subagentsAccum/);
|
|
251
|
+
assert.match(html, /class="subagents-container"/);
|
|
252
|
+
assert.match(html, /subagent-card/);
|
|
253
|
+
assert.match(html, /subagent-report/);
|
|
254
|
+
assert.match(html, /\.subagent-card\s*\{/);
|
|
255
|
+
assert.match(html, /subagent-pulse/);
|
|
256
|
+
assert.match(html, /run_deep_research: '🔍/);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('error handling renders dedicated retry error card and interactive retry badge', () => {
|
|
260
|
+
const html = getWebChatHtml();
|
|
261
|
+
assert.match(html, /function renderErrorCardWithRetry\(/);
|
|
262
|
+
assert.match(html, /class="chat-error-card"/);
|
|
263
|
+
assert.match(html, /class="btn-retry-chat"/);
|
|
264
|
+
assert.match(html, /onclick="regenerateMessage\(this\)"/);
|
|
265
|
+
assert.match(html, /data-retry-query=/);
|
|
266
|
+
assert.match(html, /badge\.dataset\.status = status;/);
|
|
267
|
+
assert.match(html, /if \(status === STATUS\.ERROR\)/);
|
|
268
|
+
assert.match(html, /globalScope\.regenerateMessage\(group\)/);
|
|
269
|
+
assert.match(html, /\.agent-status-badge\[data-status='error'\]\s*\{/);
|
|
270
|
+
assert.match(html, /\.btn-retry-chat\s*\{/);
|
|
271
|
+
assert.match(html, /btn-retry-chat:hover/);
|
|
272
|
+
assert.match(html, /bubble button:not\(\.btn-retry-chat\)/);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('incomplete answer renders styled resume bar with continue button and CSS styles', () => {
|
|
276
|
+
const html = getWebChatHtml();
|
|
277
|
+
assert.match(html, /resumeWrap\.className = 'resume-incomplete-bar'/);
|
|
278
|
+
assert.match(html, /class="resume-incomplete-info"/);
|
|
279
|
+
assert.match(html, /class="resume-incomplete-badge"/);
|
|
280
|
+
assert.match(html, /class="resume-incomplete-title"/);
|
|
281
|
+
assert.match(html, /class="btn-resume-continue btn-mini-ctrl"/);
|
|
282
|
+
assert.match(html, /resumeIncompleteAnswer\(group, m\)/);
|
|
283
|
+
assert.match(html, /\.resume-incomplete-bar\s*\{/);
|
|
284
|
+
assert.match(html, /\.btn-resume-continue/);
|
|
285
|
+
assert.match(html, /\.btn-mini-ctrl/);
|
|
286
|
+
assert.match(html, /resumeBarArrive/);
|
|
287
|
+
});
|
|
288
|
+
test('input box enhancements: slash commands menu, model chip, relocated attachment tray, and quick action icons', () => {
|
|
289
|
+
const html = getWebChatHtml();
|
|
290
|
+
assert.match(html, /id="slash-commands-menu"/);
|
|
291
|
+
assert.match(html, /class="slash-commands-menu"/);
|
|
292
|
+
assert.match(html, /id="input-model-chip"/);
|
|
293
|
+
assert.match(html, /id="input-model-chip-label"/);
|
|
294
|
+
assert.match(html, /id="input-box-container"[\s\S]*?id="attachment-tray"/);
|
|
295
|
+
assert.match(html, /btn-send-muted/);
|
|
296
|
+
assert.match(html, /SLASH_COMMANDS/);
|
|
297
|
+
assert.match(html, /syncInputModelChip/);
|
|
298
|
+
assert.match(html, /updateSendButtonState/);
|
|
299
|
+
assert.match(html, /drag-over/);
|
|
300
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
6
|
+
|
|
7
|
+
import { KikuAgent } from '../agentCore.js';
|
|
8
|
+
import { McpClientManager } from '../mcpClient.js';
|
|
9
|
+
import { AgentSession } from '../session.js';
|
|
10
|
+
import { AgentMemory } from '../memory.js';
|
|
11
|
+
|
|
12
|
+
function createMemoryStub() {
|
|
13
|
+
return {
|
|
14
|
+
_loaded: true,
|
|
15
|
+
_storageError: null,
|
|
16
|
+
facts: [],
|
|
17
|
+
preferences: { userSkills: [], skillsEnabled: true, topicsOfInterest: [] },
|
|
18
|
+
buildMemoryContext: () => '',
|
|
19
|
+
buildUserSkillContext: () => '',
|
|
20
|
+
trackTopicInterest: () => {},
|
|
21
|
+
getArchivedSessions: () => [],
|
|
22
|
+
getStatus: () => ({ factsCount: 0, sessionsCount: 0 }),
|
|
23
|
+
addFact: () => null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('AgentSession keeps web chat storage files isolated', async () => {
|
|
28
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'kiku-web-sessions-'));
|
|
29
|
+
const firstPath = path.join(tempDir, 'first.json');
|
|
30
|
+
const secondPath = path.join(tempDir, 'second.json');
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const first = new AgentSession({ sessionId: 'web_first', storageFile: firstPath });
|
|
34
|
+
first.createUserMessage('ข้อความของห้องแรก');
|
|
35
|
+
await first.saveToDisk();
|
|
36
|
+
|
|
37
|
+
const second = new AgentSession({ sessionId: 'web_second', storageFile: secondPath });
|
|
38
|
+
second.createUserMessage('ข้อความของห้องที่สอง');
|
|
39
|
+
await second.saveToDisk();
|
|
40
|
+
|
|
41
|
+
const loadedFirst = new AgentSession({ storageFile: firstPath });
|
|
42
|
+
const loadedSecond = new AgentSession({ storageFile: secondPath });
|
|
43
|
+
assert.equal(await loadedFirst.loadFromDisk(), true);
|
|
44
|
+
assert.equal(await loadedSecond.loadFromDisk(), true);
|
|
45
|
+
assert.equal(loadedFirst.messages[0].parts[0].content, 'ข้อความของห้องแรก');
|
|
46
|
+
assert.equal(loadedSecond.messages[0].parts[0].content, 'ข้อความของห้องที่สอง');
|
|
47
|
+
} finally {
|
|
48
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('AgentMemory reports fact counts by category', () => {
|
|
53
|
+
const memory = new AgentMemory();
|
|
54
|
+
memory.facts = [
|
|
55
|
+
{ category: 'music' },
|
|
56
|
+
{ category: 'music' },
|
|
57
|
+
{ category: 'drama' },
|
|
58
|
+
{},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
assert.deepEqual(memory.getFactsByCategory(), { music: 2, drama: 1, general: 1 });
|
|
62
|
+
assert.deepEqual(memory.getStatus().factsByCategory, { music: 2, drama: 1, general: 1 });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('KikuAgent hydrates browser history and persists direct answers', async () => {
|
|
66
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'kiku-web-memory-'));
|
|
67
|
+
const storageFile = path.join(tempDir, 'session.json');
|
|
68
|
+
const observedMessages = [];
|
|
69
|
+
const client = {
|
|
70
|
+
provider: 'lmstudio',
|
|
71
|
+
providerName: 'LM Studio',
|
|
72
|
+
model: 'test-model',
|
|
73
|
+
async chat(options) {
|
|
74
|
+
observedMessages.push(options.messages.map(message => ({ ...message })));
|
|
75
|
+
options.onToken?.('ฉันจำได้ว่าชื่อที่บอกคือ Miju');
|
|
76
|
+
return { role: 'assistant', content: 'ฉันจำได้ว่าชื่อที่บอกคือ Miju' };
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const agent = new KikuAgent({
|
|
80
|
+
client,
|
|
81
|
+
memory: createMemoryStub(),
|
|
82
|
+
mcpManager: new McpClientManager({
|
|
83
|
+
autoLoad: false,
|
|
84
|
+
globalConfigPath: path.join(tempDir, 'mcp.json'),
|
|
85
|
+
}),
|
|
86
|
+
sessionId: 'web_memory_test',
|
|
87
|
+
sessionStorageFile: storageFile,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await agent.ready();
|
|
92
|
+
const hydration = await agent.hydrateConversationHistory([
|
|
93
|
+
{ role: 'user', text: 'เรียกฉันว่าอะไรดี' },
|
|
94
|
+
{ role: 'bot', text: 'ฉันจะเรียกคุณว่า Miju' },
|
|
95
|
+
], { sessionId: 'web_memory_test' });
|
|
96
|
+
assert.equal(hydration.hydrated, true);
|
|
97
|
+
|
|
98
|
+
const answer = await agent.run('ฉันบอกให้เรียกว่าอะไร');
|
|
99
|
+
assert.equal(answer, 'ฉันจำได้ว่าชื่อที่บอกคือ Miju');
|
|
100
|
+
assert.ok(observedMessages[0].some(message =>
|
|
101
|
+
message.role === 'assistant' && message.content === 'ฉันจะเรียกคุณว่า Miju'
|
|
102
|
+
));
|
|
103
|
+
|
|
104
|
+
const saved = JSON.parse(await readFile(storageFile, 'utf8'));
|
|
105
|
+
assert.equal(saved.id, 'web_memory_test');
|
|
106
|
+
assert.equal(saved.messages.length, 4);
|
|
107
|
+
assert.equal(saved.messages.at(-1).parts[0].content, 'ฉันจำได้ว่าชื่อที่บอกคือ Miju');
|
|
108
|
+
} finally {
|
|
109
|
+
await agent.mcpManager.disconnectAll();
|
|
110
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import {
|
|
6
|
+
resolveAllowedOrigin,
|
|
7
|
+
applySecurityHeaders,
|
|
8
|
+
extractApiKey,
|
|
9
|
+
checkChatRateLimit,
|
|
10
|
+
resolveClientIp
|
|
11
|
+
} from '../../web-chat/core/security.js';
|
|
12
|
+
import { handleWebChatRequest } from '../../web-chat/router.js';
|
|
13
|
+
import { getSharedAgent } from '../../routes/agent.js';
|
|
14
|
+
|
|
15
|
+
test('Security: resolveAllowedOrigin correctly validates allowed origins', () => {
|
|
16
|
+
assert.equal(resolveAllowedOrigin('http://localhost:3000'), 'http://localhost:3000');
|
|
17
|
+
assert.equal(resolveAllowedOrigin('http://127.0.0.1:5173'), 'http://127.0.0.1:5173');
|
|
18
|
+
assert.equal(resolveAllowedOrigin('https://jujingyi-fans.vercel.app'), null);
|
|
19
|
+
assert.equal(resolveAllowedOrigin(null), null);
|
|
20
|
+
assert.equal(resolveAllowedOrigin(''), null);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('Security: resolveAllowedOrigin respects custom WEBCHAT_CORS_ORIGINS', () => {
|
|
24
|
+
const origEnv = process.env.WEBCHAT_CORS_ORIGINS;
|
|
25
|
+
try {
|
|
26
|
+
process.env.WEBCHAT_CORS_ORIGINS = 'https://portal.kiku.com, https://admin.kiku.com';
|
|
27
|
+
assert.equal(resolveAllowedOrigin('https://portal.kiku.com'), 'https://portal.kiku.com');
|
|
28
|
+
assert.equal(resolveAllowedOrigin('https://admin.kiku.com'), 'https://admin.kiku.com');
|
|
29
|
+
assert.equal(resolveAllowedOrigin('https://evil-site.com'), null);
|
|
30
|
+
} finally {
|
|
31
|
+
if (origEnv !== undefined) process.env.WEBCHAT_CORS_ORIGINS = origEnv;
|
|
32
|
+
else delete process.env.WEBCHAT_CORS_ORIGINS;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('Security: applySecurityHeaders injects standard defensive headers', () => {
|
|
37
|
+
const headers = {};
|
|
38
|
+
const mockReq = { headers: { origin: 'http://localhost:3000' } };
|
|
39
|
+
const mockRes = {
|
|
40
|
+
setHeader: (k, v) => { headers[k.toLowerCase()] = v; }
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
applySecurityHeaders(mockReq, mockRes);
|
|
44
|
+
assert.equal(headers['x-content-type-options'], 'nosniff');
|
|
45
|
+
assert.equal(headers['x-frame-options'], 'SAMEORIGIN');
|
|
46
|
+
assert.equal(headers['referrer-policy'], 'strict-origin-when-cross-origin');
|
|
47
|
+
assert.equal(headers['access-control-allow-origin'], 'http://localhost:3000');
|
|
48
|
+
assert.match(headers['access-control-allow-methods'], /GET, POST, OPTIONS/);
|
|
49
|
+
assert.match(headers['access-control-allow-headers'], /Authorization/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('Security: extractApiKey extracts tokens securely with priority', () => {
|
|
53
|
+
|
|
54
|
+
const req1 = { headers: { 'x-api-key': 'key_from_x_header' } };
|
|
55
|
+
assert.equal(extractApiKey(req1), 'key_from_x_header');
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
const req2 = { headers: { authorization: 'Bearer key_from_bearer' } };
|
|
59
|
+
assert.equal(extractApiKey(req2), 'key_from_bearer');
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
const req3 = { headers: { 'x-api-key': 'key_header' } };
|
|
63
|
+
const url3 = new URL('http://localhost:3000/api/chat?apiKey=key_query');
|
|
64
|
+
assert.equal(extractApiKey(req3, url3), 'key_header');
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
const req4 = { headers: {} };
|
|
68
|
+
const url4 = new URL('http://localhost:3000/api/chat?apiKey=key_query');
|
|
69
|
+
assert.equal(extractApiKey(req4, url4), null);
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
assert.equal(extractApiKey({ headers: {} }, new URL('http://localhost:3000/api/chat')), null);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('Security: client IP ignores spoofed forwarding headers unless a trusted proxy is configured', () => {
|
|
76
|
+
const original = process.env.KIKU_TRUST_PROXY;
|
|
77
|
+
const originalVercel = process.env.VERCEL;
|
|
78
|
+
try {
|
|
79
|
+
delete process.env.KIKU_TRUST_PROXY;
|
|
80
|
+
delete process.env.VERCEL;
|
|
81
|
+
const req = { headers: { 'x-forwarded-for': '10.0.0.5' }, socket: { remoteAddress: '198.51.100.20' } };
|
|
82
|
+
assert.equal(resolveClientIp(req), '198.51.100.20');
|
|
83
|
+
} finally {
|
|
84
|
+
if (original === undefined) delete process.env.KIKU_TRUST_PROXY;
|
|
85
|
+
else process.env.KIKU_TRUST_PROXY = original;
|
|
86
|
+
if (originalVercel === undefined) delete process.env.VERCEL;
|
|
87
|
+
else process.env.VERCEL = originalVercel;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('Security: remotely reachable agents are isolated and cannot access privileged tools', () => {
|
|
92
|
+
const first = getSharedAgent();
|
|
93
|
+
const second = getSharedAgent();
|
|
94
|
+
assert.notEqual(first, second);
|
|
95
|
+
assert.notEqual(first.memory, second.memory);
|
|
96
|
+
const names = new Set(first.getAllToolsSchema().map(tool => tool.function?.name || tool.name));
|
|
97
|
+
assert.equal(names.has('read_local_file'), false);
|
|
98
|
+
assert.equal(names.has('manage_skill'), false);
|
|
99
|
+
assert.equal(names.has('fetch_json'), false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('Security: web-chat escapes markdown before rendering and never persists API keys in browser storage', () => {
|
|
103
|
+
const source = readFileSync(new URL('../../web-chat/ui/scripts/client.js', import.meta.url), 'utf8');
|
|
104
|
+
assert.match(source, /text = escapeHtml\(reconstructed\)/);
|
|
105
|
+
assert.match(source, /data-query=.*this\.dataset\.query/);
|
|
106
|
+
assert.doesNotMatch(source, /localStorage\.setItem\([^\n]*(?:api.?key|vip.?key|token|secret)/i);
|
|
107
|
+
assert.doesNotMatch(source, /body\.apiKey/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('Security: checkChatRateLimit enforces 60 requests per minute', () => {
|
|
111
|
+
const testIp = '198.51.100.42';
|
|
112
|
+
const first = checkChatRateLimit(testIp);
|
|
113
|
+
assert.equal(first.allowed, true);
|
|
114
|
+
assert.equal(typeof first.remaining, 'number');
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < 60; i++) {
|
|
118
|
+
checkChatRateLimit(testIp);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const blocked = checkChatRateLimit(testIp);
|
|
122
|
+
assert.equal(blocked.allowed, false);
|
|
123
|
+
assert.ok(blocked.retryAfter > 0);
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
const otherIpResult = checkChatRateLimit('203.0.113.99');
|
|
127
|
+
assert.equal(otherIpResult.allowed, true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('Integration: web-chat router handles OPTIONS preflight and rate limiting over HTTP', async () => {
|
|
131
|
+
const server = http.createServer(async (req, res) => {
|
|
132
|
+
await handleWebChatRequest(req, res, { port: 0 });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
136
|
+
const port = server.address().port;
|
|
137
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
|
|
141
|
+
const optRes = await fetch(`${baseUrl}/api/chat`, {
|
|
142
|
+
method: 'OPTIONS',
|
|
143
|
+
headers: { Origin: 'http://localhost:3000' }
|
|
144
|
+
});
|
|
145
|
+
assert.equal(optRes.status, 204);
|
|
146
|
+
assert.equal(optRes.headers.get('access-control-allow-origin'), 'http://localhost:3000');
|
|
147
|
+
assert.equal(optRes.headers.get('x-content-type-options'), 'nosniff');
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
const badReq = await fetch(`${baseUrl}/api/chat`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: { 'Content-Type': 'application/json' },
|
|
153
|
+
body: JSON.stringify({})
|
|
154
|
+
});
|
|
155
|
+
assert.equal(badReq.status, 400);
|
|
156
|
+
const badJson = await badReq.json();
|
|
157
|
+
assert.equal(badJson.error, 'Query is required');
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
const statusRes = await fetch(`${baseUrl}/api/status`, {
|
|
161
|
+
headers: { Authorization: 'Bearer test_agent_key_123' }
|
|
162
|
+
});
|
|
163
|
+
assert.equal(statusRes.status, 200);
|
|
164
|
+
const statusJson = await statusRes.json();
|
|
165
|
+
assert.equal(statusJson.status, 'ok');
|
|
166
|
+
} finally {
|
|
167
|
+
await new Promise((resolve) => server.close(resolve));
|
|
168
|
+
}
|
|
169
|
+
});
|