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.
Files changed (195) hide show
  1. package/README.md +257 -1803
  2. package/dist/src/agent_workspace.contract.parity.test.js +105 -421
  3. package/dist/src/agent_workspace.frontend.test.js +410 -11
  4. package/dist/src/android.knowledgebase.picker.contract.test.js +102 -0
  5. package/dist/src/appimage.patchelf.test.js +113 -0
  6. package/dist/src/appimage.portability.test.js +156 -0
  7. package/dist/src/backend/FileLoader.js +18 -4
  8. package/dist/src/backend/FileLoader.test.js +125 -0
  9. package/dist/src/backend/GraphBuilder.identity.test.js +86 -0
  10. package/dist/src/backend/GraphBuilder.js +66 -18
  11. package/dist/src/backend/GraphBuilder.matching.test.js +127 -0
  12. package/dist/src/backend/GraphMetrics.js +0 -3
  13. package/dist/src/backend/ResourceIdentity.js +155 -0
  14. package/dist/src/backend/ResourceIdentity.test.js +113 -0
  15. package/dist/src/backend/algorithms/CycleDetection.js +0 -1
  16. package/dist/src/backend/algorithms/HybridEngine.js +0 -3
  17. package/dist/src/backend/algorithms/LayoutEngine.js +0 -1
  18. package/dist/src/backend/algorithms/StatisticalAnalyzer.js +0 -4
  19. package/dist/src/backend/algorithms/TopologicalSort.js +0 -2
  20. package/dist/src/backend/build_dag.js +0 -1
  21. package/dist/src/capacitor.bridge.serialization.contract.test.js +3 -0
  22. package/dist/src/capacitor.runtime.contract.test.js +11 -2
  23. package/dist/src/core/Graph.js +138 -42
  24. package/dist/src/core/Graph.test.js +74 -0
  25. package/dist/src/core/NoteConnection.js +2 -2
  26. package/dist/src/core/PathBridge.js +265 -2
  27. package/dist/src/core/ResourceReference.js +17 -0
  28. package/dist/src/desktop.godot.pack.test.js +142 -0
  29. package/dist/src/desktop.runtime.graph.test.js +66 -0
  30. package/dist/src/export/WorkspaceExportBundle.js +44 -0
  31. package/dist/src/export/WorkspaceExportBundle.test.js +61 -0
  32. package/dist/src/foundation.release.evidence.contract.test.js +127 -9
  33. package/dist/src/foundation.sqlite.runtime.contract.test.js +6 -0
  34. package/dist/src/frontend/README.md +257 -1803
  35. package/dist/src/frontend/agent_workspace.js +665 -7
  36. package/dist/src/frontend/index.html +31 -2
  37. package/dist/src/frontend/knowledge_projection_contract.js +230 -0
  38. package/dist/src/frontend/knowledge_projection_store.js +214 -0
  39. package/dist/src/frontend/libs/path_core.js +158 -42
  40. package/dist/src/frontend/locales/en.json +32 -0
  41. package/dist/src/frontend/locales/zh.json +32 -0
  42. package/dist/src/frontend/mobile_budget_runtime.js +33 -0
  43. package/dist/src/frontend/mobile_exact_analyzer.js +393 -0
  44. package/dist/src/frontend/mobile_identity_contract.js +221 -0
  45. package/dist/src/frontend/mobile_semantic_comparator.js +264 -0
  46. package/dist/src/frontend/path.html +6 -2
  47. package/dist/src/frontend/simulationWorker.js +1 -1
  48. package/dist/src/frontend/source_manager.js +110 -4
  49. package/dist/src/frontend/storage_provider.js +661 -71
  50. package/dist/src/frontend/styles.css +107 -13
  51. package/dist/src/frontend/workspace_panes.js +111 -7
  52. package/dist/src/frontend.runtime.assets.contract.test.js +81 -0
  53. package/dist/src/graph.answer.operator.contract.test.js +72 -0
  54. package/dist/src/index.js +6 -2
  55. package/dist/src/index.knowledge-root.test.js +118 -0
  56. package/dist/src/indexing/IndexLifecycle.js +22 -0
  57. package/dist/src/indexing/IndexLifecycle.test.js +24 -0
  58. package/dist/src/knowledge.api.contract.test.js +4 -2
  59. package/dist/src/knowledge_projection.contract.test.js +134 -0
  60. package/dist/src/knowledge_projection_store.test.js +200 -0
  61. package/dist/src/learning/AnswerQualityEvaluation.js +153 -0
  62. package/dist/src/learning/AnswerQualityEvaluation.test.js +118 -0
  63. package/dist/src/learning/KnowledgeLearningPlatform.js +4687 -3892
  64. package/dist/src/learning/KnowledgeLearningPlatform.persistence.test.js +418 -0
  65. package/dist/src/learning/KnowledgeLearningPlatform.test.js +243 -6
  66. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.js +25 -1
  67. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.test.js +13 -5
  68. package/dist/src/learning/MeasurementEvidenceConflicts.test.js +78 -0
  69. package/dist/src/learning/PublicAnswerContract.test.js +85 -0
  70. package/dist/src/learning/PublicEvidenceSelection.test.js +94 -0
  71. package/dist/src/learning/PublicMeasurementDisclosure.test.js +54 -0
  72. package/dist/src/learning/agentConversationExecution.js +211 -0
  73. package/dist/src/learning/agentConversationExecution.test.js +209 -0
  74. package/dist/src/learning/agentConversationSerialization.js +266 -0
  75. package/dist/src/learning/agentConversationSerialization.test.js +165 -0
  76. package/dist/src/learning/agentResponseBudget.js +179 -0
  77. package/dist/src/learning/agentResponseBudget.test.js +97 -0
  78. package/dist/src/learning/agentWorkspaceCapabilityContracts.js +65 -0
  79. package/dist/src/learning/answerReleaseReview.js +1191 -250
  80. package/dist/src/learning/answerReleaseReview.test.js +1293 -7
  81. package/dist/src/learning/answerTaskPlan.js +473 -0
  82. package/dist/src/learning/conversationComposer.js +627 -386
  83. package/dist/src/learning/conversationComposer.test.js +1403 -0
  84. package/dist/src/learning/evidenceContextAssembler.js +312 -120
  85. package/dist/src/learning/graphAnswerCoverage.js +37 -0
  86. package/dist/src/learning/graphAnswerCoverage.test.js +93 -0
  87. package/dist/src/learning/graphAnswerCoverageCalibration.js +59 -0
  88. package/dist/src/learning/graphAnswerCoverageCalibration.test.js +46 -0
  89. package/dist/src/learning/graphAnswerFacts.js +94 -0
  90. package/dist/src/learning/graphAnswerFacts.test.js +39 -0
  91. package/dist/src/learning/graphAnswerPlan.js +908 -0
  92. package/dist/src/learning/graphAnswerPlan.test.js +907 -0
  93. package/dist/src/learning/graphAnswerQualityCalibration.js +255 -0
  94. package/dist/src/learning/graphAnswerQualityCalibration.test.js +72 -0
  95. package/dist/src/learning/graphAnswerQualityPolicy.js +30 -0
  96. package/dist/src/learning/graphClaimMatcher.js +104 -0
  97. package/dist/src/learning/graphConditionedContext.js +109 -0
  98. package/dist/src/learning/graphConditionedContext.test.js +99 -0
  99. package/dist/src/learning/graphContextAssembler.js +3 -13
  100. package/dist/src/learning/graphContextAssembler.test.js +2 -2
  101. package/dist/src/learning/graphExpansionPolicy.js +22 -0
  102. package/dist/src/learning/graphExpansionPolicy.test.js +33 -0
  103. package/dist/src/learning/identityMetadata.test.js +26 -0
  104. package/dist/src/learning/index.js +1 -0
  105. package/dist/src/learning/mobileAnswerProjection.js +104 -0
  106. package/dist/src/learning/mobileAnswerProjection.test.js +102 -0
  107. package/dist/src/learning/queryBackend.js +34 -10
  108. package/dist/src/learning/queryBackend.test.js +21 -0
  109. package/dist/src/learning/ragContextPack.js +115 -16
  110. package/dist/src/learning/ragContextPack.test.js +151 -0
  111. package/dist/src/learning/ragEvidenceQuality.js +96 -0
  112. package/dist/src/learning/ragEvidenceQuality.test.js +29 -0
  113. package/dist/src/learning/ragPublicText.js +67 -4
  114. package/dist/src/learning/ragPublicText.test.js +57 -0
  115. package/dist/src/learning/ragSufficiencyJudge.js +11 -1
  116. package/dist/src/learning/ragSufficiencyProviderJudge.js +16 -26
  117. package/dist/src/learning/ragSufficiencyProviderJudge.test.js +51 -0
  118. package/dist/src/learning/requestNormalization.js +106 -0
  119. package/dist/src/learning/requestNormalization.test.js +101 -0
  120. package/dist/src/learning/store.js +116 -12
  121. package/dist/src/learning/store.test.js +299 -0
  122. package/dist/src/learning/vectorAccelerationAdapter.js +22 -6
  123. package/dist/src/learning/vectorAccelerationAdapter.test.js +30 -0
  124. package/dist/src/learning/workspaceHydration.js +168 -0
  125. package/dist/src/learning/workspaceHydration.test.js +30 -0
  126. package/dist/src/license.policy.contract.test.js +9 -4
  127. package/dist/src/markdown/MarkdownGateway.js +4 -0
  128. package/dist/src/markdown/MarkdownGateway.test.js +59 -0
  129. package/dist/src/middleware/SseResponseWriter.js +122 -0
  130. package/dist/src/middleware/SseResponseWriter.test.js +54 -0
  131. package/dist/src/middleware/auth.js +28 -7
  132. package/dist/src/middleware/auth.test.js +33 -0
  133. package/dist/src/middleware/index.js +2 -1
  134. package/dist/src/mobile.android.evidence.contract.test.js +146 -0
  135. package/dist/src/mobile.android.signing.contract.test.js +66 -0
  136. package/dist/src/mobile.artifact.contract.test.js +144 -0
  137. package/dist/src/mobile.budget.contract.test.js +90 -0
  138. package/dist/src/mobile.canonical-id.readiness.test.js +54 -0
  139. package/dist/src/mobile.graph.parity.contract.test.js +207 -0
  140. package/dist/src/mobile.identity.contract.test.js +66 -0
  141. package/dist/src/mobile.identity.corpus.manifest.test.js +72 -0
  142. package/dist/src/mobile.identity.corpus.test.js +40 -0
  143. package/dist/src/mobile.native.recovery.contract.test.js +64 -0
  144. package/dist/src/mobile.pipeline.test.js +11 -1
  145. package/dist/src/mobile.profile.contract.test.js +225 -0
  146. package/dist/src/mobile.semantic.parity.test.js +92 -0
  147. package/dist/src/mobile_exact_analyzer.test.js +113 -0
  148. package/dist/src/notemd/ContentGenerator.js +4 -9
  149. package/dist/src/notemd/FileProcessor.js +4 -9
  150. package/dist/src/notemd/NotemdService.js +12 -17
  151. package/dist/src/notemd/Translator.js +4 -9
  152. package/dist/src/notemd/progressReporter.js +9 -0
  153. package/dist/src/notemd.server.integration.test.js +119 -4
  154. package/dist/src/notemd.workflow.pipeline.test.js +1 -1
  155. package/dist/src/pathbridge.handshake.contract.test.js +29 -0
  156. package/dist/src/pathbridge.host-adapter.contract.test.js +133 -0
  157. package/dist/src/pathcore.bundle.contract.test.js +8 -0
  158. package/dist/src/platform/ExportProfile.js +28 -0
  159. package/dist/src/platform/MobileBudget.js +11 -0
  160. package/dist/src/platform/PlatformCapabilities.js +14 -0
  161. package/dist/src/platform/PlatformCapabilities.test.js +6 -0
  162. package/dist/src/query_backend.external_http.integration.test.js +2 -2
  163. package/dist/src/release.godot.mirror.contract.test.js +28 -4
  164. package/dist/src/resources/ResourceRegistry.js +40 -0
  165. package/dist/src/resources/ResourceRegistry.test.js +36 -0
  166. package/dist/src/routes/index.js +0 -4
  167. package/dist/src/routes/knowledge.js +46 -33
  168. package/dist/src/routes/knowledgePayload.js +185 -0
  169. package/dist/src/routes/knowledgePayload.test.js +53 -0
  170. package/dist/src/routes/markdown.js +8 -0
  171. package/dist/src/routes/notemd.js +990 -234
  172. package/dist/src/routes/registry.contract.test.js +5 -2
  173. package/dist/src/routes/registry.shadow.contract.test.js +22 -0
  174. package/dist/src/routes/render.js +14 -51
  175. package/dist/src/runtime.capabilities.test.js +18 -2
  176. package/dist/src/server.js +566 -1090
  177. package/dist/src/server.migration.test.js +417 -15
  178. package/dist/src/sidecar.build.lock.contract.test.js +201 -0
  179. package/dist/src/sidecar.freshness.contract.test.js +184 -0
  180. package/dist/src/sidecar.replacement.boundary.contract.test.js +6 -3
  181. package/dist/src/sidecar.supply.readiness.contract.test.js +11 -0
  182. package/dist/src/simulation.worker.offline.test.js +104 -0
  183. package/dist/src/storage.provider.capacitor.content.contract.test.js +4 -1
  184. package/dist/src/storage.provider.contract.test.js +152 -1
  185. package/dist/src/tauri.frontend.build.contract.test.js +8 -0
  186. package/dist/src/tauri.window.evidence.execution.test.js +68 -0
  187. package/dist/src/utils/RuntimePaths.js +6 -0
  188. package/dist/src/utils/RuntimePaths.test.js +27 -0
  189. package/dist/src/windows.installer.build.test.js +109 -0
  190. package/dist/src/windows.installer.payload.test.js +76 -0
  191. package/dist/src/windows.installer.runtime.test.js +69 -0
  192. package/dist/src/workspace/WorkspaceRegistry.js +13 -0
  193. package/package.json +32 -10
  194. package/dist/src/routes/data.js +0 -267
  195. 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
- resolve({
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.skip('returns runtime diagnostics with wasm parity state and no auth token exposure', async () => {
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.skip('merges available targets from folders and cached graph artifacts', async () => {
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.skip('serves /api/content for legacy Knowledge_Base-style paths', async () => {
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.skip('rejects /api/content requests outside configured KB root', async () => {
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.skip('check-cache and restore-cache endpoints work for named targets', async () => {
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.skip('deduplicates same build request while first build is in-flight', async () => {
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.skip('returns 409 when a different build request arrives during active build', async () => {
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.skip('returns 413 when /api/build request body exceeds size limit', async () => {
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.skip('returns 400 when /api/build request body contains invalid json', async () => {
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.skip('returns 415 when /api/kb-path request content type is not json', async () => {
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.skip('accepts binary PNG upload for clipboard copy without base64 JSON payload', async () => {
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.skip('returns 415 for unsupported binary clipboard content type', async () => {
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.skip('returns 413 when binary clipboard payload exceeds limit', async () => {
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'