noteconnection 1.8.0 → 1.9.0
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 +257 -1803
- package/dist/src/agent_workspace.contract.parity.test.js +105 -421
- package/dist/src/agent_workspace.frontend.test.js +410 -11
- package/dist/src/android.knowledgebase.picker.contract.test.js +102 -0
- package/dist/src/appimage.patchelf.test.js +113 -0
- package/dist/src/appimage.portability.test.js +156 -0
- package/dist/src/backend/FileLoader.js +18 -4
- package/dist/src/backend/FileLoader.test.js +125 -0
- package/dist/src/backend/GraphBuilder.identity.test.js +86 -0
- package/dist/src/backend/GraphBuilder.js +66 -18
- package/dist/src/backend/GraphBuilder.matching.test.js +127 -0
- package/dist/src/backend/GraphMetrics.js +0 -3
- package/dist/src/backend/ResourceIdentity.js +155 -0
- package/dist/src/backend/ResourceIdentity.test.js +113 -0
- package/dist/src/backend/algorithms/CycleDetection.js +0 -1
- package/dist/src/backend/algorithms/HybridEngine.js +0 -3
- package/dist/src/backend/algorithms/LayoutEngine.js +0 -1
- package/dist/src/backend/algorithms/StatisticalAnalyzer.js +0 -4
- package/dist/src/backend/algorithms/TopologicalSort.js +0 -2
- package/dist/src/backend/build_dag.js +0 -1
- package/dist/src/capacitor.bridge.serialization.contract.test.js +3 -0
- package/dist/src/capacitor.runtime.contract.test.js +11 -2
- package/dist/src/core/Graph.js +138 -42
- package/dist/src/core/Graph.test.js +74 -0
- package/dist/src/core/NoteConnection.js +2 -2
- package/dist/src/core/PathBridge.js +265 -2
- package/dist/src/core/ResourceReference.js +17 -0
- package/dist/src/desktop.godot.pack.test.js +142 -0
- package/dist/src/desktop.runtime.graph.test.js +66 -0
- package/dist/src/export/WorkspaceExportBundle.js +44 -0
- package/dist/src/export/WorkspaceExportBundle.test.js +61 -0
- package/dist/src/foundation.release.evidence.contract.test.js +127 -9
- package/dist/src/foundation.sqlite.runtime.contract.test.js +6 -0
- package/dist/src/frontend/README.md +257 -1803
- package/dist/src/frontend/agent_workspace.js +665 -7
- package/dist/src/frontend/index.html +31 -2
- package/dist/src/frontend/knowledge_projection_contract.js +230 -0
- package/dist/src/frontend/knowledge_projection_store.js +214 -0
- package/dist/src/frontend/libs/path_core.js +158 -42
- package/dist/src/frontend/locales/en.json +32 -0
- package/dist/src/frontend/locales/zh.json +32 -0
- package/dist/src/frontend/mobile_budget_runtime.js +33 -0
- package/dist/src/frontend/mobile_exact_analyzer.js +393 -0
- package/dist/src/frontend/mobile_identity_contract.js +221 -0
- package/dist/src/frontend/mobile_semantic_comparator.js +264 -0
- package/dist/src/frontend/path.html +6 -2
- package/dist/src/frontend/simulationWorker.js +1 -1
- package/dist/src/frontend/source_manager.js +110 -4
- package/dist/src/frontend/storage_provider.js +661 -71
- package/dist/src/frontend/styles.css +107 -13
- package/dist/src/frontend/workspace_panes.js +111 -7
- package/dist/src/frontend.runtime.assets.contract.test.js +81 -0
- package/dist/src/graph.answer.operator.contract.test.js +72 -0
- package/dist/src/index.js +6 -2
- package/dist/src/index.knowledge-root.test.js +118 -0
- package/dist/src/indexing/IndexLifecycle.js +22 -0
- package/dist/src/indexing/IndexLifecycle.test.js +24 -0
- package/dist/src/knowledge.api.contract.test.js +4 -2
- package/dist/src/knowledge_projection.contract.test.js +134 -0
- package/dist/src/knowledge_projection_store.test.js +200 -0
- package/dist/src/learning/AnswerQualityEvaluation.js +153 -0
- package/dist/src/learning/AnswerQualityEvaluation.test.js +118 -0
- package/dist/src/learning/KnowledgeLearningPlatform.js +4687 -3892
- package/dist/src/learning/KnowledgeLearningPlatform.persistence.test.js +418 -0
- package/dist/src/learning/KnowledgeLearningPlatform.test.js +243 -6
- package/dist/src/learning/KnowledgeWorkspaceConversationRegression.js +25 -1
- package/dist/src/learning/KnowledgeWorkspaceConversationRegression.test.js +13 -5
- package/dist/src/learning/MeasurementEvidenceConflicts.test.js +78 -0
- package/dist/src/learning/PublicAnswerContract.test.js +85 -0
- package/dist/src/learning/PublicEvidenceSelection.test.js +94 -0
- package/dist/src/learning/PublicMeasurementDisclosure.test.js +54 -0
- package/dist/src/learning/agentConversationExecution.js +211 -0
- package/dist/src/learning/agentConversationExecution.test.js +209 -0
- package/dist/src/learning/agentConversationSerialization.js +266 -0
- package/dist/src/learning/agentConversationSerialization.test.js +165 -0
- package/dist/src/learning/agentResponseBudget.js +179 -0
- package/dist/src/learning/agentResponseBudget.test.js +97 -0
- package/dist/src/learning/agentWorkspaceCapabilityContracts.js +65 -0
- package/dist/src/learning/answerReleaseReview.js +1191 -250
- package/dist/src/learning/answerReleaseReview.test.js +1293 -7
- package/dist/src/learning/answerTaskPlan.js +473 -0
- package/dist/src/learning/conversationComposer.js +627 -386
- package/dist/src/learning/conversationComposer.test.js +1403 -0
- package/dist/src/learning/evidenceContextAssembler.js +312 -120
- package/dist/src/learning/graphAnswerCoverage.js +37 -0
- package/dist/src/learning/graphAnswerCoverage.test.js +93 -0
- package/dist/src/learning/graphAnswerCoverageCalibration.js +59 -0
- package/dist/src/learning/graphAnswerCoverageCalibration.test.js +46 -0
- package/dist/src/learning/graphAnswerFacts.js +94 -0
- package/dist/src/learning/graphAnswerFacts.test.js +39 -0
- package/dist/src/learning/graphAnswerPlan.js +908 -0
- package/dist/src/learning/graphAnswerPlan.test.js +907 -0
- package/dist/src/learning/graphAnswerQualityCalibration.js +255 -0
- package/dist/src/learning/graphAnswerQualityCalibration.test.js +72 -0
- package/dist/src/learning/graphAnswerQualityPolicy.js +30 -0
- package/dist/src/learning/graphClaimMatcher.js +104 -0
- package/dist/src/learning/graphConditionedContext.js +109 -0
- package/dist/src/learning/graphConditionedContext.test.js +99 -0
- package/dist/src/learning/graphContextAssembler.js +3 -13
- package/dist/src/learning/graphContextAssembler.test.js +2 -2
- package/dist/src/learning/graphExpansionPolicy.js +22 -0
- package/dist/src/learning/graphExpansionPolicy.test.js +33 -0
- package/dist/src/learning/identityMetadata.test.js +26 -0
- package/dist/src/learning/index.js +1 -0
- package/dist/src/learning/mobileAnswerProjection.js +104 -0
- package/dist/src/learning/mobileAnswerProjection.test.js +102 -0
- package/dist/src/learning/queryBackend.js +34 -10
- package/dist/src/learning/queryBackend.test.js +21 -0
- package/dist/src/learning/ragContextPack.js +115 -16
- package/dist/src/learning/ragContextPack.test.js +151 -0
- package/dist/src/learning/ragEvidenceQuality.js +96 -0
- package/dist/src/learning/ragEvidenceQuality.test.js +29 -0
- package/dist/src/learning/ragPublicText.js +67 -4
- package/dist/src/learning/ragPublicText.test.js +57 -0
- package/dist/src/learning/ragSufficiencyJudge.js +11 -1
- package/dist/src/learning/ragSufficiencyProviderJudge.js +16 -26
- package/dist/src/learning/ragSufficiencyProviderJudge.test.js +51 -0
- package/dist/src/learning/requestNormalization.js +106 -0
- package/dist/src/learning/requestNormalization.test.js +101 -0
- package/dist/src/learning/store.js +116 -12
- package/dist/src/learning/store.test.js +299 -0
- package/dist/src/learning/vectorAccelerationAdapter.js +22 -6
- package/dist/src/learning/vectorAccelerationAdapter.test.js +30 -0
- package/dist/src/learning/workspaceHydration.js +168 -0
- package/dist/src/learning/workspaceHydration.test.js +30 -0
- package/dist/src/license.policy.contract.test.js +9 -4
- package/dist/src/markdown/MarkdownGateway.js +4 -0
- package/dist/src/markdown/MarkdownGateway.test.js +59 -0
- package/dist/src/middleware/SseResponseWriter.js +122 -0
- package/dist/src/middleware/SseResponseWriter.test.js +54 -0
- package/dist/src/middleware/auth.js +28 -7
- package/dist/src/middleware/auth.test.js +33 -0
- package/dist/src/middleware/index.js +2 -1
- package/dist/src/mobile.android.evidence.contract.test.js +146 -0
- package/dist/src/mobile.android.signing.contract.test.js +66 -0
- package/dist/src/mobile.artifact.contract.test.js +144 -0
- package/dist/src/mobile.budget.contract.test.js +90 -0
- package/dist/src/mobile.canonical-id.readiness.test.js +54 -0
- package/dist/src/mobile.graph.parity.contract.test.js +207 -0
- package/dist/src/mobile.identity.contract.test.js +66 -0
- package/dist/src/mobile.identity.corpus.manifest.test.js +72 -0
- package/dist/src/mobile.identity.corpus.test.js +40 -0
- package/dist/src/mobile.native.recovery.contract.test.js +64 -0
- package/dist/src/mobile.pipeline.test.js +11 -1
- package/dist/src/mobile.profile.contract.test.js +225 -0
- package/dist/src/mobile.semantic.parity.test.js +92 -0
- package/dist/src/mobile_exact_analyzer.test.js +113 -0
- package/dist/src/notemd/ContentGenerator.js +4 -9
- package/dist/src/notemd/FileProcessor.js +4 -9
- package/dist/src/notemd/NotemdService.js +12 -17
- package/dist/src/notemd/Translator.js +4 -9
- package/dist/src/notemd/progressReporter.js +9 -0
- package/dist/src/notemd.server.integration.test.js +119 -4
- package/dist/src/notemd.workflow.pipeline.test.js +1 -1
- package/dist/src/pathbridge.handshake.contract.test.js +29 -0
- package/dist/src/pathbridge.host-adapter.contract.test.js +133 -0
- package/dist/src/pathcore.bundle.contract.test.js +8 -0
- package/dist/src/platform/ExportProfile.js +28 -0
- package/dist/src/platform/MobileBudget.js +11 -0
- package/dist/src/platform/PlatformCapabilities.js +14 -0
- package/dist/src/platform/PlatformCapabilities.test.js +6 -0
- package/dist/src/query_backend.external_http.integration.test.js +2 -2
- package/dist/src/release.godot.mirror.contract.test.js +28 -4
- package/dist/src/resources/ResourceRegistry.js +40 -0
- package/dist/src/resources/ResourceRegistry.test.js +36 -0
- package/dist/src/routes/index.js +0 -4
- package/dist/src/routes/knowledge.js +46 -33
- package/dist/src/routes/knowledgePayload.js +185 -0
- package/dist/src/routes/knowledgePayload.test.js +53 -0
- package/dist/src/routes/markdown.js +8 -0
- package/dist/src/routes/notemd.js +990 -234
- package/dist/src/routes/registry.contract.test.js +5 -2
- package/dist/src/routes/registry.shadow.contract.test.js +22 -0
- package/dist/src/routes/render.js +14 -51
- package/dist/src/runtime.capabilities.test.js +18 -2
- package/dist/src/server.js +566 -1090
- package/dist/src/server.migration.test.js +417 -15
- package/dist/src/sidecar.build.lock.contract.test.js +201 -0
- package/dist/src/sidecar.freshness.contract.test.js +184 -0
- package/dist/src/sidecar.replacement.boundary.contract.test.js +6 -3
- package/dist/src/sidecar.supply.readiness.contract.test.js +11 -0
- package/dist/src/simulation.worker.offline.test.js +104 -0
- package/dist/src/storage.provider.capacitor.content.contract.test.js +4 -1
- package/dist/src/storage.provider.contract.test.js +152 -1
- package/dist/src/tauri.frontend.build.contract.test.js +8 -0
- package/dist/src/tauri.window.evidence.execution.test.js +68 -0
- package/dist/src/utils/RuntimePaths.js +6 -0
- package/dist/src/utils/RuntimePaths.test.js +27 -0
- package/dist/src/windows.installer.build.test.js +109 -0
- package/dist/src/windows.installer.payload.test.js +76 -0
- package/dist/src/windows.installer.runtime.test.js +69 -0
- package/dist/src/workspace/WorkspaceRegistry.js +13 -0
- package/package.json +32 -10
- package/dist/src/routes/data.js +0 -267
- package/dist/src/routes/diagnostics.js +0 -51
|
@@ -220,11 +220,18 @@ function requestBinary(port, method, requestPath, payload, extraHeaders) {
|
|
|
220
220
|
parsed = text;
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
|
-
|
|
223
|
+
const response = {
|
|
224
224
|
status: res.statusCode || 0,
|
|
225
225
|
body: parsed,
|
|
226
226
|
headers: res.headers
|
|
227
|
-
}
|
|
227
|
+
};
|
|
228
|
+
// A 413 response can precede upload completion; teardown must wait until the request finishes writing.
|
|
229
|
+
if (req.writableFinished) {
|
|
230
|
+
resolve(response);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
req.once('finish', () => resolve(response));
|
|
234
|
+
}
|
|
228
235
|
});
|
|
229
236
|
});
|
|
230
237
|
req.on('error', reject);
|
|
@@ -369,7 +376,7 @@ describe('server migration settings routes', () => {
|
|
|
369
376
|
expect(response.status).toBe(200);
|
|
370
377
|
expect(response.body.folders).toEqual(['financial', 'legal']);
|
|
371
378
|
});
|
|
372
|
-
test
|
|
379
|
+
test('returns runtime diagnostics with wasm parity state and no auth token exposure', async () => {
|
|
373
380
|
const response = await requestJson(port, 'GET', '/api/runtime-diagnostics');
|
|
374
381
|
expect(response.status).toBe(200);
|
|
375
382
|
expect(response.body).toEqual(expect.objectContaining({
|
|
@@ -433,7 +440,7 @@ describe('server migration settings routes', () => {
|
|
|
433
440
|
const serverSource = fs.readFileSync(serverSourcePath, 'utf8');
|
|
434
441
|
expect(serverSource).not.toMatch(/fs\.(existsSync|mkdirSync|readdirSync|writeFileSync|readFileSync|statSync|accessSync)\b/);
|
|
435
442
|
});
|
|
436
|
-
test
|
|
443
|
+
test('merges available targets from folders and cached graph artifacts', async () => {
|
|
437
444
|
const response = await requestJson(port, 'GET', '/api/available-targets');
|
|
438
445
|
expect(response.status).toBe(200);
|
|
439
446
|
expect(response.body.targets).toEqual(['financial', 'legal', 'robotics']);
|
|
@@ -443,20 +450,20 @@ describe('server migration settings routes', () => {
|
|
|
443
450
|
expect(response.status).toBe(200);
|
|
444
451
|
expect(response.body.content).toContain('Inside KB root');
|
|
445
452
|
});
|
|
446
|
-
test
|
|
453
|
+
test('serves /api/content for legacy Knowledge_Base-style paths', async () => {
|
|
447
454
|
const legacyPath = 'C:\\snapshot\\NoteConnection_app\\Knowledge_Base\\financial\\overview.md';
|
|
448
455
|
const response = await requestJson(port, 'GET', `/api/content?path=${encodeURIComponent(legacyPath)}`);
|
|
449
456
|
expect(response.status).toBe(200);
|
|
450
457
|
expect(response.body.content).toContain('Financial Overview');
|
|
451
458
|
});
|
|
452
|
-
test
|
|
459
|
+
test('rejects /api/content requests outside configured KB root', async () => {
|
|
453
460
|
const response = await requestJson(port, 'GET', `/api/content?path=${encodeURIComponent(outsideFilePath)}`);
|
|
454
461
|
expect(response.status).toBe(403);
|
|
455
462
|
expect(response.body).toEqual(expect.objectContaining({
|
|
456
463
|
error: expect.stringContaining('outside configured knowledge base')
|
|
457
464
|
}));
|
|
458
465
|
});
|
|
459
|
-
test
|
|
466
|
+
test('check-cache and restore-cache endpoints work for named targets', async () => {
|
|
460
467
|
const cacheResponse = await requestJson(port, 'GET', '/api/check-cache?target=financial');
|
|
461
468
|
expect(cacheResponse.status).toBe(200);
|
|
462
469
|
expect(cacheResponse.body).toEqual(expect.objectContaining({
|
|
@@ -498,7 +505,7 @@ describe('server migration settings routes', () => {
|
|
|
498
505
|
error: expect.stringContaining('Invalid static file path')
|
|
499
506
|
}));
|
|
500
507
|
});
|
|
501
|
-
test
|
|
508
|
+
test('deduplicates same build request while first build is in-flight', async () => {
|
|
502
509
|
const hold = deferred();
|
|
503
510
|
buildGraphMock.mockImplementationOnce(() => hold.promise);
|
|
504
511
|
const payload = {
|
|
@@ -534,7 +541,7 @@ describe('server migration settings routes', () => {
|
|
|
534
541
|
})
|
|
535
542
|
}));
|
|
536
543
|
});
|
|
537
|
-
test
|
|
544
|
+
test('returns 409 when a different build request arrives during active build', async () => {
|
|
538
545
|
const hold = deferred();
|
|
539
546
|
buildGraphMock.mockImplementationOnce(() => hold.promise);
|
|
540
547
|
const firstRequest = requestJson(port, 'POST', '/api/build', {
|
|
@@ -554,7 +561,18 @@ describe('server migration settings routes', () => {
|
|
|
554
561
|
const firstResponse = await firstRequest;
|
|
555
562
|
expect(firstResponse.status).toBe(200);
|
|
556
563
|
});
|
|
557
|
-
test.
|
|
564
|
+
test.each(['incremental', 'none', 'full'])('graph build applies the requested %s recompute contract', async (mode) => {
|
|
565
|
+
const response = await requestJson(port, 'POST', '/api/build', { target: 'financial', relationRecomputeMode: mode });
|
|
566
|
+
expect(response.status).toBe(200);
|
|
567
|
+
expect(response.body.sync.summary.resolvedRelationRecomputeMode).toBe(mode);
|
|
568
|
+
});
|
|
569
|
+
test('graph build rejects unsupported recompute before construction', async () => {
|
|
570
|
+
const calls = buildGraphMock.mock.calls.length;
|
|
571
|
+
const response = await requestJson(port, 'POST', '/api/build', { target: 'financial', relationRecomputeMode: 'auto' });
|
|
572
|
+
expect(response.status).toBe(400);
|
|
573
|
+
expect(buildGraphMock.mock.calls.length).toBe(calls);
|
|
574
|
+
});
|
|
575
|
+
test('returns 413 when /api/build request body exceeds size limit', async () => {
|
|
558
576
|
const oversizedPayload = {
|
|
559
577
|
target: 'financial',
|
|
560
578
|
pad: 'x'.repeat(700 * 1024)
|
|
@@ -565,7 +583,7 @@ describe('server migration settings routes', () => {
|
|
|
565
583
|
error: expect.stringContaining('too large')
|
|
566
584
|
}));
|
|
567
585
|
});
|
|
568
|
-
test
|
|
586
|
+
test('returns 400 when /api/build request body contains invalid json', async () => {
|
|
569
587
|
const response = await requestRaw(port, 'POST', '/api/build', '{"target":', {
|
|
570
588
|
'Content-Type': 'application/json'
|
|
571
589
|
});
|
|
@@ -643,7 +661,9 @@ describe('server migration settings routes', () => {
|
|
|
643
661
|
const firstResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', {
|
|
644
662
|
userId: 'server_conversation_user',
|
|
645
663
|
sessionId: 'server_session_scope',
|
|
664
|
+
activeTarget: 'optics',
|
|
646
665
|
message: '解释吸收系数和光学衰减',
|
|
666
|
+
answerLanguage: 'zh',
|
|
647
667
|
scope: {
|
|
648
668
|
corpusId: 'optics',
|
|
649
669
|
languages: ['zh']
|
|
@@ -661,10 +681,29 @@ describe('server migration settings routes', () => {
|
|
|
661
681
|
}));
|
|
662
682
|
expect(firstResponse.body.result.citations.length).toBeGreaterThan(0);
|
|
663
683
|
expect(firstResponse.body.result.summary.appliedMemoryCount).toBeGreaterThan(0);
|
|
684
|
+
expect(firstResponse.body.result.assistantBlocks).toEqual(expect.arrayContaining([
|
|
685
|
+
expect.objectContaining({
|
|
686
|
+
type: 'structured_answer',
|
|
687
|
+
title: '可信回答',
|
|
688
|
+
nextActionsMarkdown: expect.stringContaining('## 下一步行动'),
|
|
689
|
+
}),
|
|
690
|
+
expect.objectContaining({ type: 'knowledge_run_summary', title: '知识运行' }),
|
|
691
|
+
expect.objectContaining({ type: 'citations', title: '引用' }),
|
|
692
|
+
expect.objectContaining({ type: 'knowledge_actions', title: '知识操作' }),
|
|
693
|
+
]));
|
|
694
|
+
const firstStructuredBlock = firstResponse.body.result.assistantBlocks.find((block) => block.type === 'structured_answer');
|
|
695
|
+
expect(String(firstStructuredBlock?.overviewMarkdown || '')).toContain('## 回答上下文');
|
|
696
|
+
expect(String(firstStructuredBlock?.explanationMarkdown || '')).toContain('## 说明');
|
|
697
|
+
expect(String(firstStructuredBlock?.evidenceMarkdown || '')).toContain('## 证据摘要');
|
|
698
|
+
expect(firstResponse.body.result.assistantBlocks).toEqual(expect.arrayContaining([
|
|
699
|
+
expect.objectContaining({ type: 'system_notice', text: expect.stringContaining('未召回') }),
|
|
700
|
+
]));
|
|
664
701
|
const replayResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', {
|
|
665
702
|
userId: 'server_conversation_user',
|
|
666
703
|
sessionId: 'server_session_scope',
|
|
704
|
+
activeTarget: 'optics',
|
|
667
705
|
message: '解释吸收系数和光学衰减',
|
|
706
|
+
answerLanguage: 'zh',
|
|
668
707
|
scope: {
|
|
669
708
|
corpusId: 'optics',
|
|
670
709
|
languages: ['zh']
|
|
@@ -675,6 +714,254 @@ describe('server migration settings routes', () => {
|
|
|
675
714
|
});
|
|
676
715
|
expect(replayResponse.status).toBe(200);
|
|
677
716
|
expect(replayResponse.headers?.['x-agent-conversation-replay']).toBe('hit');
|
|
717
|
+
expect(replayResponse.body.result.assistantBlocks).toEqual(expect.arrayContaining([
|
|
718
|
+
expect.objectContaining({ type: 'knowledge_run_summary', title: '知识运行' }),
|
|
719
|
+
expect.objectContaining({ type: 'citations', title: '引用' }),
|
|
720
|
+
expect.objectContaining({ type: 'knowledge_actions', title: '知识操作' }),
|
|
721
|
+
expect.objectContaining({ type: 'system_notice', text: expect.stringContaining('未召回') }),
|
|
722
|
+
]));
|
|
723
|
+
const languageConflictResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', {
|
|
724
|
+
userId: 'server_conversation_user',
|
|
725
|
+
sessionId: 'server_session_scope',
|
|
726
|
+
activeTarget: 'optics',
|
|
727
|
+
message: '解释吸收系数和光学衰减',
|
|
728
|
+
answerLanguage: 'en',
|
|
729
|
+
scope: {
|
|
730
|
+
corpusId: 'optics',
|
|
731
|
+
languages: ['zh']
|
|
732
|
+
},
|
|
733
|
+
persistMemory: true
|
|
734
|
+
}, {
|
|
735
|
+
'X-Agent-Conversation-Turn-Id': turnId
|
|
736
|
+
});
|
|
737
|
+
expect(languageConflictResponse.status).toBe(422);
|
|
738
|
+
expect(languageConflictResponse.body).toEqual(expect.objectContaining({
|
|
739
|
+
errorCode: 'turn_id_conflict',
|
|
740
|
+
}));
|
|
741
|
+
});
|
|
742
|
+
test('conversation turn cache separates full and slim response modes', async () => {
|
|
743
|
+
const ingestResponse = await requestJson(port, 'POST', '/api/knowledge/ingest', {
|
|
744
|
+
incremental: true,
|
|
745
|
+
documents: [{
|
|
746
|
+
documentId: 'doc_server_response_mode',
|
|
747
|
+
sourcePath: 'Knowledge_Base/response-mode/mode.md',
|
|
748
|
+
language: 'en',
|
|
749
|
+
workspaceId: 'response-mode',
|
|
750
|
+
corpusId: 'response-mode',
|
|
751
|
+
content: '# Response Mode Probe\nA response mode probe is a bounded technical fixture used to verify answer detail selection.\n\n## Thermal model\nThe temperature field follows $$\\frac{\\partial T}{\\partial t}=\\alpha\\nabla^2 T$$.',
|
|
752
|
+
}],
|
|
753
|
+
});
|
|
754
|
+
expect(ingestResponse.status).toBe(200);
|
|
755
|
+
const baseRequest = {
|
|
756
|
+
userId: 'server_response_mode_user',
|
|
757
|
+
sessionId: 'server_response_mode_session',
|
|
758
|
+
message: 'what is response mode probe?',
|
|
759
|
+
scope: {
|
|
760
|
+
workspaceId: 'response-mode',
|
|
761
|
+
corpusId: 'response-mode',
|
|
762
|
+
sourcePathPrefixes: ['Knowledge_Base/response-mode'],
|
|
763
|
+
},
|
|
764
|
+
persistMemory: false,
|
|
765
|
+
};
|
|
766
|
+
const fullResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', { ...baseRequest, responseMode: 'full' }, { 'X-Agent-Conversation-Turn-Id': 'turn_response_mode_full' });
|
|
767
|
+
expect(fullResponse.status).toBe(200);
|
|
768
|
+
expect(fullResponse.body.result.responseMode).toBe('full');
|
|
769
|
+
expect(fullResponse.body.result.answer).toContain('Thermal model');
|
|
770
|
+
const slimResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', { ...baseRequest, responseMode: 'slim' }, { 'X-Agent-Conversation-Turn-Id': 'turn_response_mode_slim' });
|
|
771
|
+
expect(slimResponse.status).toBe(200);
|
|
772
|
+
expect(slimResponse.body.result.responseMode).toBe('slim');
|
|
773
|
+
expect(slimResponse.body.result.answer).not.toContain('## Thermal model');
|
|
774
|
+
const fullReplay = await requestJson(port, 'POST', '/api/knowledge/conversation', { ...baseRequest, responseMode: 'full' }, { 'X-Agent-Conversation-Turn-Id': 'turn_response_mode_full' });
|
|
775
|
+
expect(fullReplay.status).toBe(200);
|
|
776
|
+
expect(fullReplay.headers?.['x-agent-conversation-replay']).toBe('hit');
|
|
777
|
+
expect(fullReplay.body.result.responseMode).toBe('full');
|
|
778
|
+
const definitionAliasResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', { ...baseRequest, response_mode: 'definition' }, { 'X-Agent-Conversation-Turn-Id': 'turn_response_mode_definition_alias' });
|
|
779
|
+
expect(definitionAliasResponse.status).toBe(200);
|
|
780
|
+
expect(definitionAliasResponse.body.result.responseMode).toBe('slim');
|
|
781
|
+
const comprehensiveAliasResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', { ...baseRequest, detailMode: 'comprehensive' }, { 'X-Agent-Conversation-Turn-Id': 'turn_response_mode_comprehensive_alias' });
|
|
782
|
+
expect(comprehensiveAliasResponse.status).toBe(200);
|
|
783
|
+
expect(comprehensiveAliasResponse.body.result.responseMode).toBe('full');
|
|
784
|
+
expect(comprehensiveAliasResponse.body.result.answer).toContain('Thermal model');
|
|
785
|
+
});
|
|
786
|
+
test('disconnect cancels a running turn once and its cached failure cannot write memory on replay', async () => {
|
|
787
|
+
const { KnowledgeLearningPlatform } = require('./learning/KnowledgeLearningPlatform');
|
|
788
|
+
const started = deferred();
|
|
789
|
+
const cancelled = deferred();
|
|
790
|
+
const run = jest.spyOn(KnowledgeLearningPlatform.prototype, 'runAgentConversation').mockImplementationOnce(async (...args) => {
|
|
791
|
+
const signal = args[1];
|
|
792
|
+
started.resolve();
|
|
793
|
+
return await new Promise((_resolve, reject) => {
|
|
794
|
+
const abort = () => { cancelled.resolve(); reject(signal.reason); };
|
|
795
|
+
if (signal.aborted)
|
|
796
|
+
abort();
|
|
797
|
+
else
|
|
798
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
const payload = { userId: 'disconnected-user', message: 'Financial overview' };
|
|
802
|
+
const client = http.request({ host: '127.0.0.1', port, path: '/api/knowledge/conversation', method: 'POST', headers: {
|
|
803
|
+
'content-type': 'application/json', accept: 'text/event-stream', 'x-agent-conversation-turn-id': 'turn_disconnect_regression',
|
|
804
|
+
} }, response => response.resume());
|
|
805
|
+
client.on('error', () => undefined);
|
|
806
|
+
try {
|
|
807
|
+
client.end(JSON.stringify(payload));
|
|
808
|
+
await started.promise;
|
|
809
|
+
client.destroy();
|
|
810
|
+
await cancelled.promise;
|
|
811
|
+
const replay = await requestJson(port, 'POST', '/api/knowledge/conversation', payload, { 'x-agent-conversation-turn-id': 'turn_disconnect_regression' });
|
|
812
|
+
expect(replay.status).toBe(499);
|
|
813
|
+
expect(replay.body.errorCode).toBe('runtime_cancelled');
|
|
814
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
815
|
+
const memory = await requestJson(port, 'POST', '/api/knowledge/conversation-memory/search', { userId: 'disconnected-user', query: 'Financial overview' });
|
|
816
|
+
expect(memory.body.result.entries).toHaveLength(0);
|
|
817
|
+
}
|
|
818
|
+
finally {
|
|
819
|
+
client.destroy();
|
|
820
|
+
run.mockRestore();
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
test('conversation budget mode is adaptive by default, supports unbounded desktop full, and stays isolated in cache', async () => {
|
|
824
|
+
const baseRequest = {
|
|
825
|
+
userId: 'server_response_budget_user',
|
|
826
|
+
sessionId: 'server_response_budget_session',
|
|
827
|
+
message: 'what is water glass?',
|
|
828
|
+
responseMode: 'full',
|
|
829
|
+
persistMemory: false,
|
|
830
|
+
};
|
|
831
|
+
const adaptiveResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', baseRequest, { 'X-Agent-Conversation-Turn-Id': 'turn_response_budget_adaptive' });
|
|
832
|
+
expect(adaptiveResponse.status).toBe(200);
|
|
833
|
+
expect(adaptiveResponse.body.result.responseBudget).toEqual(expect.objectContaining({
|
|
834
|
+
mode: 'adaptive',
|
|
835
|
+
tier: 'standard',
|
|
836
|
+
productCapDisabled: false,
|
|
837
|
+
}));
|
|
838
|
+
expect(adaptiveResponse.body.result.summary).toEqual(expect.objectContaining({
|
|
839
|
+
responseBudgetMode: 'adaptive',
|
|
840
|
+
responseBudgetTier: 'standard',
|
|
841
|
+
}));
|
|
842
|
+
const unboundedResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', { ...baseRequest, responseBudgetMode: 'unbounded' }, { 'X-Agent-Conversation-Turn-Id': 'turn_response_budget_unbounded' });
|
|
843
|
+
expect(unboundedResponse.status).toBe(200);
|
|
844
|
+
expect(unboundedResponse.body.result.responseBudget).toEqual(expect.objectContaining({
|
|
845
|
+
mode: 'unbounded',
|
|
846
|
+
tier: 'unbounded',
|
|
847
|
+
productCapDisabled: true,
|
|
848
|
+
}));
|
|
849
|
+
expect(unboundedResponse.body.result.responseBudget.rag.productCapDisabled).toBe(true);
|
|
850
|
+
expect(unboundedResponse.body.result.summary.responseBudgetMode).toBe('unbounded');
|
|
851
|
+
const cacheConflict = await requestJson(port, 'POST', '/api/knowledge/conversation', { ...baseRequest, responseBudgetMode: 'unbounded' }, { 'X-Agent-Conversation-Turn-Id': 'turn_response_budget_adaptive' });
|
|
852
|
+
expect(cacheConflict.status).toBe(422);
|
|
853
|
+
expect(cacheConflict.body).toEqual(expect.objectContaining({ errorCode: 'turn_id_conflict' }));
|
|
854
|
+
const mobileResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', { ...baseRequest, responseBudgetMode: 'unbounded', responseProfile: 'mobile_compact' }, { 'X-Agent-Conversation-Turn-Id': 'turn_response_budget_mobile' });
|
|
855
|
+
expect(mobileResponse.status).toBe(200);
|
|
856
|
+
expect(mobileResponse.body.result.responseMode).toBe('slim');
|
|
857
|
+
expect(mobileResponse.body.result.responseProfile).toBe('mobile_compact');
|
|
858
|
+
expect(mobileResponse.body.result.responseBudget).toBeUndefined();
|
|
859
|
+
expect(mobileResponse.body.result.summary.responseBudgetMode).toBe('adaptive');
|
|
860
|
+
});
|
|
861
|
+
test('disconnecting one joined consumer does not cancel the remaining consumer or duplicate execution', async () => {
|
|
862
|
+
const { KnowledgeLearningPlatform } = require('./learning/KnowledgeLearningPlatform');
|
|
863
|
+
const original = KnowledgeLearningPlatform.prototype.runAgentConversation;
|
|
864
|
+
const started = deferred();
|
|
865
|
+
const held = deferred();
|
|
866
|
+
let signal;
|
|
867
|
+
const run = jest.spyOn(KnowledgeLearningPlatform.prototype, 'runAgentConversation').mockImplementationOnce(async function (...args) {
|
|
868
|
+
signal = args[1];
|
|
869
|
+
started.resolve();
|
|
870
|
+
await held.promise;
|
|
871
|
+
return original.apply(this, args);
|
|
872
|
+
});
|
|
873
|
+
const before = await requestJson(port, 'GET', '/api/knowledge/conversation/turn-cache/diagnostics');
|
|
874
|
+
const previousJoins = before.body.result.counters.inFlightJoinCount;
|
|
875
|
+
const payload = { userId: 'joined-consumer', message: 'Financial overview', persistMemory: false };
|
|
876
|
+
const headers = { 'x-agent-conversation-turn-id': 'turn_joined_consumer' };
|
|
877
|
+
const client = http.request({ host: '127.0.0.1', port, path: '/api/knowledge/conversation', method: 'POST', headers: {
|
|
878
|
+
...headers, 'content-type': 'application/json', accept: 'text/event-stream',
|
|
879
|
+
} }, response => response.resume());
|
|
880
|
+
client.on('error', () => undefined);
|
|
881
|
+
let second;
|
|
882
|
+
try {
|
|
883
|
+
client.end(JSON.stringify(payload));
|
|
884
|
+
await started.promise;
|
|
885
|
+
second = requestJson(port, 'POST', '/api/knowledge/conversation', payload, headers);
|
|
886
|
+
const deadline = Date.now() + 2000;
|
|
887
|
+
let joins = previousJoins;
|
|
888
|
+
while (joins === previousJoins && Date.now() < deadline) {
|
|
889
|
+
joins = (await requestJson(port, 'GET', '/api/knowledge/conversation/turn-cache/diagnostics')).body.result.counters.inFlightJoinCount;
|
|
890
|
+
if (joins === previousJoins)
|
|
891
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
892
|
+
}
|
|
893
|
+
expect(joins).toBeGreaterThan(previousJoins);
|
|
894
|
+
client.destroy();
|
|
895
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
896
|
+
expect(signal?.aborted).toBe(false);
|
|
897
|
+
held.resolve();
|
|
898
|
+
expect((await second).status).toBe(200);
|
|
899
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
900
|
+
}
|
|
901
|
+
finally {
|
|
902
|
+
held.resolve();
|
|
903
|
+
client.destroy();
|
|
904
|
+
await second;
|
|
905
|
+
run.mockRestore();
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
test('conversation JSON and SSE responses preserve an explicit Chinese answer language for an English no-match query', async () => {
|
|
909
|
+
const requestPayload = {
|
|
910
|
+
userId: 'server_language_no_match_user',
|
|
911
|
+
sessionId: 'server_language_no_match_session',
|
|
912
|
+
activeTarget: 'empty-language-contract',
|
|
913
|
+
message: 'what is water glass?',
|
|
914
|
+
answerLanguage: 'zh',
|
|
915
|
+
scope: {
|
|
916
|
+
corpusId: 'empty-language-contract',
|
|
917
|
+
sourcePathPrefixes: ['Knowledge_Base/empty-language-contract']
|
|
918
|
+
},
|
|
919
|
+
persistMemory: false
|
|
920
|
+
};
|
|
921
|
+
const jsonResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', requestPayload, {
|
|
922
|
+
'X-Agent-Conversation-Turn-Id': 'turn_language_no_match_json'
|
|
923
|
+
});
|
|
924
|
+
expect(jsonResponse.status).toBe(200);
|
|
925
|
+
expect(jsonResponse.body.result.answer).toContain('当前范围');
|
|
926
|
+
expect(jsonResponse.body.result.answer).toContain('我暂时不能');
|
|
927
|
+
expect(jsonResponse.body.result.assistantBlocks).toEqual(expect.arrayContaining([
|
|
928
|
+
expect.objectContaining({
|
|
929
|
+
type: 'structured_answer',
|
|
930
|
+
title: '可信回答'
|
|
931
|
+
}),
|
|
932
|
+
expect.objectContaining({ type: 'knowledge_run_summary', title: '知识运行' }),
|
|
933
|
+
expect.objectContaining({ type: 'system_notice', text: expect.stringContaining('未召回') }),
|
|
934
|
+
]));
|
|
935
|
+
const jsonStructuredBlock = jsonResponse.body.result.assistantBlocks.find((block) => block.type === 'structured_answer');
|
|
936
|
+
expect(String(jsonStructuredBlock?.overviewMarkdown || '')).toContain('## 回答上下文');
|
|
937
|
+
expect(String(jsonStructuredBlock?.explanationMarkdown || '')).toContain('## 说明');
|
|
938
|
+
expect(String(jsonStructuredBlock?.evidenceMarkdown || '')).toContain('## 证据摘要');
|
|
939
|
+
const sseResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', requestPayload, {
|
|
940
|
+
Accept: 'text/event-stream',
|
|
941
|
+
'X-Agent-Conversation-Turn-Id': 'turn_language_no_match_sse'
|
|
942
|
+
});
|
|
943
|
+
expect(sseResponse.status).toBe(200);
|
|
944
|
+
expect(String(sseResponse.headers?.['content-type'] || '')).toContain('text/event-stream');
|
|
945
|
+
const completedPayload = String(sseResponse.body || '')
|
|
946
|
+
.split(/\n\n/)
|
|
947
|
+
.map((eventChunk) => eventChunk.split(/\n/).find((line) => line.startsWith('data: ')))
|
|
948
|
+
.filter((line) => Boolean(line))
|
|
949
|
+
.map((line) => JSON.parse(line.slice('data: '.length)))
|
|
950
|
+
.find((event) => event && event.type === 'turn_completed');
|
|
951
|
+
expect(completedPayload?.result?.answer).toContain('当前范围');
|
|
952
|
+
expect(completedPayload?.result?.answer).toContain('我暂时不能');
|
|
953
|
+
expect(completedPayload?.result?.assistantBlocks).toEqual(expect.arrayContaining([
|
|
954
|
+
expect.objectContaining({
|
|
955
|
+
type: 'structured_answer',
|
|
956
|
+
title: '可信回答'
|
|
957
|
+
}),
|
|
958
|
+
expect.objectContaining({ type: 'knowledge_run_summary', title: '知识运行' }),
|
|
959
|
+
expect.objectContaining({ type: 'system_notice', text: expect.stringContaining('未召回') }),
|
|
960
|
+
]));
|
|
961
|
+
const sseStructuredBlock = completedPayload?.result?.assistantBlocks?.find((block) => block.type === 'structured_answer');
|
|
962
|
+
expect(String(sseStructuredBlock?.overviewMarkdown || '')).toContain('## 回答上下文');
|
|
963
|
+
expect(String(sseStructuredBlock?.explanationMarkdown || '')).toContain('## 说明');
|
|
964
|
+
expect(String(sseStructuredBlock?.evidenceMarkdown || '')).toContain('## 证据摘要');
|
|
678
965
|
});
|
|
679
966
|
test('conversation route auto-hydrates the active knowledge target when the learning workspace store is empty', async () => {
|
|
680
967
|
temp.mkdir(path.join('project', 'Knowledge_Base', 'waterglass'));
|
|
@@ -707,6 +994,121 @@ describe('server migration settings routes', () => {
|
|
|
707
994
|
expect(answer).not.toContain('Grounded by');
|
|
708
995
|
expect(answer).not.toContain('Key evidence');
|
|
709
996
|
});
|
|
997
|
+
test('conversation route returns a bounded mobile projection without internal diagnostics', async () => {
|
|
998
|
+
const ingestResponse = await requestJson(port, 'POST', '/api/knowledge/ingest', {
|
|
999
|
+
incremental: true,
|
|
1000
|
+
documents: [
|
|
1001
|
+
{
|
|
1002
|
+
documentId: 'doc_mobile_amorphous_ice',
|
|
1003
|
+
sourcePath: 'Knowledge_Base/mobile-waterglass/amorphous-ice.md',
|
|
1004
|
+
language: 'en',
|
|
1005
|
+
content: '# Amorphous Ice\nAmorphous ice is a solid form of water without long-range crystalline order.\n\n## Structure\nIts structure is described by the radial distribution function $g(r)$.',
|
|
1006
|
+
},
|
|
1007
|
+
],
|
|
1008
|
+
});
|
|
1009
|
+
expect(ingestResponse.status).toBe(200);
|
|
1010
|
+
const response = await requestJson(port, 'POST', '/api/knowledge/conversation', {
|
|
1011
|
+
userId: 'mobile_projection_user',
|
|
1012
|
+
sessionId: 'mobile_projection_session',
|
|
1013
|
+
message: 'what is amorphous ice?',
|
|
1014
|
+
responseMode: 'full',
|
|
1015
|
+
responseProfile: 'mobile_compact',
|
|
1016
|
+
persistMemory: false,
|
|
1017
|
+
}, {
|
|
1018
|
+
'X-Agent-Conversation-Turn-Id': 'turn_mobile_projection',
|
|
1019
|
+
});
|
|
1020
|
+
expect(response.status).toBe(200);
|
|
1021
|
+
const result = response.body.result;
|
|
1022
|
+
expect(result).toEqual(expect.objectContaining({
|
|
1023
|
+
responseMode: 'slim',
|
|
1024
|
+
responseProfile: 'mobile_compact',
|
|
1025
|
+
mobileProjection: expect.objectContaining({
|
|
1026
|
+
schemaVersion: 1,
|
|
1027
|
+
primarySubject: expect.stringContaining('Amorphous Ice'),
|
|
1028
|
+
directAnswer: expect.stringContaining('Amorphous ice'),
|
|
1029
|
+
}),
|
|
1030
|
+
assistantBlocks: [],
|
|
1031
|
+
knowledgePoints: [],
|
|
1032
|
+
citations: [],
|
|
1033
|
+
recalledMemories: [],
|
|
1034
|
+
memoryActions: [],
|
|
1035
|
+
}));
|
|
1036
|
+
expect(result.trace).toBeUndefined();
|
|
1037
|
+
expect(JSON.stringify(result)).not.toContain('ragContextPack');
|
|
1038
|
+
expect(JSON.stringify(result)).not.toContain('knowledgeRun');
|
|
1039
|
+
const jsonReplayResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', {
|
|
1040
|
+
userId: 'mobile_projection_user',
|
|
1041
|
+
sessionId: 'mobile_projection_session',
|
|
1042
|
+
message: 'what is amorphous ice?',
|
|
1043
|
+
responseMode: 'full',
|
|
1044
|
+
responseProfile: 'mobile_compact',
|
|
1045
|
+
persistMemory: false,
|
|
1046
|
+
}, {
|
|
1047
|
+
'X-Agent-Conversation-Turn-Id': 'turn_mobile_projection',
|
|
1048
|
+
});
|
|
1049
|
+
expect(jsonReplayResponse.status).toBe(200);
|
|
1050
|
+
expect(jsonReplayResponse.headers?.['x-agent-conversation-replay']).toBe('hit');
|
|
1051
|
+
expect(jsonReplayResponse.body.result).toEqual(expect.objectContaining({
|
|
1052
|
+
responseMode: 'slim',
|
|
1053
|
+
responseProfile: 'mobile_compact',
|
|
1054
|
+
assistantBlocks: [],
|
|
1055
|
+
knowledgePoints: [],
|
|
1056
|
+
citations: [],
|
|
1057
|
+
}));
|
|
1058
|
+
expect(jsonReplayResponse.body.result.trace).toBeUndefined();
|
|
1059
|
+
const parseCompletedMobileEvent = (body) => String(body || '')
|
|
1060
|
+
.split(/\n\n/)
|
|
1061
|
+
.map((eventChunk) => eventChunk.split(/\n/).find((line) => line.startsWith('data: ')))
|
|
1062
|
+
.filter((line) => Boolean(line))
|
|
1063
|
+
.map((line) => JSON.parse(line.slice('data: '.length)))
|
|
1064
|
+
.find((event) => event && event.type === 'turn_completed');
|
|
1065
|
+
const sseInitialResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', {
|
|
1066
|
+
userId: 'mobile_projection_user',
|
|
1067
|
+
sessionId: 'mobile_projection_session',
|
|
1068
|
+
message: 'what is amorphous ice?',
|
|
1069
|
+
responseMode: 'full',
|
|
1070
|
+
responseProfile: 'mobile_compact',
|
|
1071
|
+
persistMemory: false,
|
|
1072
|
+
}, {
|
|
1073
|
+
Accept: 'text/event-stream',
|
|
1074
|
+
'X-Agent-Conversation-Turn-Id': 'turn_mobile_projection_sse',
|
|
1075
|
+
});
|
|
1076
|
+
expect(sseInitialResponse.status).toBe(200);
|
|
1077
|
+
expect(String(sseInitialResponse.headers?.['content-type'] || '')).toContain('text/event-stream');
|
|
1078
|
+
expect(sseInitialResponse.headers?.['x-agent-conversation-replay']).toBe('miss');
|
|
1079
|
+
const sseInitialCompleted = parseCompletedMobileEvent(sseInitialResponse.body);
|
|
1080
|
+
expect(sseInitialCompleted?.result).toEqual(expect.objectContaining({
|
|
1081
|
+
responseMode: 'slim',
|
|
1082
|
+
responseProfile: 'mobile_compact',
|
|
1083
|
+
assistantBlocks: [],
|
|
1084
|
+
knowledgePoints: [],
|
|
1085
|
+
citations: [],
|
|
1086
|
+
}));
|
|
1087
|
+
expect(sseInitialCompleted?.result?.trace).toBeUndefined();
|
|
1088
|
+
expect(JSON.stringify(sseInitialCompleted?.result || {})).not.toContain('ragContextPack');
|
|
1089
|
+
const sseReplayResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', {
|
|
1090
|
+
userId: 'mobile_projection_user',
|
|
1091
|
+
sessionId: 'mobile_projection_session',
|
|
1092
|
+
message: 'what is amorphous ice?',
|
|
1093
|
+
responseMode: 'full',
|
|
1094
|
+
responseProfile: 'mobile_compact',
|
|
1095
|
+
persistMemory: false,
|
|
1096
|
+
}, {
|
|
1097
|
+
Accept: 'text/event-stream',
|
|
1098
|
+
'X-Agent-Conversation-Turn-Id': 'turn_mobile_projection_sse',
|
|
1099
|
+
});
|
|
1100
|
+
expect(sseReplayResponse.status).toBe(200);
|
|
1101
|
+
expect(sseReplayResponse.headers?.['x-agent-conversation-replay']).toBe('hit');
|
|
1102
|
+
const sseReplayCompleted = parseCompletedMobileEvent(sseReplayResponse.body);
|
|
1103
|
+
expect(sseReplayCompleted?.result).toEqual(expect.objectContaining({
|
|
1104
|
+
responseMode: 'slim',
|
|
1105
|
+
responseProfile: 'mobile_compact',
|
|
1106
|
+
assistantBlocks: [],
|
|
1107
|
+
knowledgePoints: [],
|
|
1108
|
+
citations: [],
|
|
1109
|
+
}));
|
|
1110
|
+
expect(sseReplayCompleted?.result?.trace).toBeUndefined();
|
|
1111
|
+
});
|
|
710
1112
|
test('returns 400 from /api/render/math when expression is empty', async () => {
|
|
711
1113
|
const response = await requestJson(port, 'POST', '/api/render/math', {
|
|
712
1114
|
expression: ' '
|
|
@@ -736,7 +1138,7 @@ describe('server migration settings routes', () => {
|
|
|
736
1138
|
expect(response.status).toBe(200);
|
|
737
1139
|
expect(response.body.svg).toContain('<svg');
|
|
738
1140
|
});
|
|
739
|
-
test
|
|
1141
|
+
test('returns 415 when /api/kb-path request content type is not json', async () => {
|
|
740
1142
|
const response = await requestRaw(port, 'POST', '/api/kb-path', 'kbPath=/tmp', {
|
|
741
1143
|
'Content-Type': 'text/plain'
|
|
742
1144
|
});
|
|
@@ -745,7 +1147,7 @@ describe('server migration settings routes', () => {
|
|
|
745
1147
|
error: expect.stringContaining('Unsupported Content-Type')
|
|
746
1148
|
}));
|
|
747
1149
|
});
|
|
748
|
-
test
|
|
1150
|
+
test('accepts binary PNG upload for clipboard copy without base64 JSON payload', async () => {
|
|
749
1151
|
const tinyPng = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7+VwAAAABJRU5ErkJggg==', 'base64');
|
|
750
1152
|
const response = await requestBinary(port, 'POST', '/api/clipboard/image-binary', tinyPng, {
|
|
751
1153
|
'Content-Type': 'image/png'
|
|
@@ -756,7 +1158,7 @@ describe('server migration settings routes', () => {
|
|
|
756
1158
|
transport: 'binary'
|
|
757
1159
|
}));
|
|
758
1160
|
});
|
|
759
|
-
test
|
|
1161
|
+
test('returns 415 for unsupported binary clipboard content type', async () => {
|
|
760
1162
|
const response = await requestRaw(port, 'POST', '/api/clipboard/image-binary', 'not-a-png', {
|
|
761
1163
|
'Content-Type': 'text/plain'
|
|
762
1164
|
});
|
|
@@ -765,7 +1167,7 @@ describe('server migration settings routes', () => {
|
|
|
765
1167
|
error: expect.stringContaining('Unsupported Content-Type')
|
|
766
1168
|
}));
|
|
767
1169
|
});
|
|
768
|
-
test
|
|
1170
|
+
test('returns 413 when binary clipboard payload exceeds limit', async () => {
|
|
769
1171
|
const oversized = Buffer.alloc(TEST_CLIPBOARD_LIMIT_BYTES + 1, 0x00);
|
|
770
1172
|
const response = await requestBinary(port, 'POST', '/api/clipboard/image-binary', oversized, {
|
|
771
1173
|
'Content-Type': 'application/octet-stream'
|