noteconnection 1.7.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 (328) hide show
  1. package/LICENSE +674 -21
  2. package/README.md +260 -1626
  3. package/dist/src/agent_workspace.contract.parity.test.js +159 -0
  4. package/dist/src/agent_workspace.frontend.test.js +10388 -0
  5. package/dist/src/agent_workspace.locale.contract.test.js +95 -0
  6. package/dist/src/agent_workspace.runtime.behavior.test.js +5072 -0
  7. package/dist/src/android.knowledgebase.picker.contract.test.js +102 -0
  8. package/dist/src/appimage.patchelf.test.js +113 -0
  9. package/dist/src/appimage.portability.test.js +156 -0
  10. package/dist/src/backend/FileLoader.js +18 -4
  11. package/dist/src/backend/FileLoader.test.js +125 -0
  12. package/dist/src/backend/GraphBuilder.identity.test.js +86 -0
  13. package/dist/src/backend/GraphBuilder.js +66 -18
  14. package/dist/src/backend/GraphBuilder.matching.test.js +127 -0
  15. package/dist/src/backend/GraphMetrics.js +0 -3
  16. package/dist/src/backend/ResourceIdentity.js +155 -0
  17. package/dist/src/backend/ResourceIdentity.test.js +113 -0
  18. package/dist/src/backend/algorithms/CycleDetection.js +0 -1
  19. package/dist/src/backend/algorithms/HybridEngine.js +0 -3
  20. package/dist/src/backend/algorithms/LayoutEngine.js +0 -1
  21. package/dist/src/backend/algorithms/StatisticalAnalyzer.js +0 -4
  22. package/dist/src/backend/algorithms/TopologicalSort.js +0 -2
  23. package/dist/src/backend/build_dag.js +0 -1
  24. package/dist/src/capacitor.bridge.serialization.contract.test.js +3 -0
  25. package/dist/src/capacitor.runtime.contract.test.js +11 -2
  26. package/dist/src/copy.assets.contract.test.js +130 -0
  27. package/dist/src/core/Graph.js +138 -42
  28. package/dist/src/core/Graph.test.js +74 -0
  29. package/dist/src/core/NoteConnection.js +2 -2
  30. package/dist/src/core/PathBridge.js +293 -5
  31. package/dist/src/core/ResourceReference.js +17 -0
  32. package/dist/src/desktop.godot.pack.test.js +142 -0
  33. package/dist/src/desktop.runtime.graph.test.js +66 -0
  34. package/dist/src/export/WorkspaceExportBundle.js +1174 -0
  35. package/dist/src/export/WorkspaceExportBundle.test.js +1541 -0
  36. package/dist/src/export/index.js +18 -0
  37. package/dist/src/export/types.js +2 -0
  38. package/dist/src/fixrisk.issue.verifier.contract.test.js +3 -1
  39. package/dist/src/foundation.ann.runtime.contract.test.js +81 -0
  40. package/dist/src/foundation.release.evidence.contract.test.js +524 -0
  41. package/dist/src/foundation.sqlite.runtime.contract.test.js +88 -0
  42. package/dist/src/frontend/README.md +260 -1626
  43. package/dist/src/frontend/agent_workspace.js +6011 -0
  44. package/dist/src/frontend/agent_workspace_runtime.js +4434 -0
  45. package/dist/src/frontend/analysis.js +31 -4
  46. package/dist/src/frontend/app.js +2178 -62
  47. package/dist/src/frontend/focus_mode_interactions.js +33 -0
  48. package/dist/src/frontend/godot_future_path_renderer.js +540 -0
  49. package/dist/src/frontend/godot_tree_interactions.js +178 -0
  50. package/dist/src/frontend/graph_state.mjs +105 -0
  51. package/dist/src/frontend/hosted_future_path_runtime.js +157 -0
  52. package/dist/src/frontend/i18n.mjs +186 -0
  53. package/dist/src/frontend/index.html +672 -268
  54. package/dist/src/frontend/knowledge_projection_contract.js +230 -0
  55. package/dist/src/frontend/knowledge_projection_store.js +214 -0
  56. package/dist/src/frontend/layout_gpu.js +12 -4
  57. package/dist/src/frontend/libs/path_core.js +158 -42
  58. package/dist/src/frontend/locales/en.json +753 -5
  59. package/dist/src/frontend/locales/zh.json +753 -5
  60. package/dist/src/frontend/main.mjs +60 -0
  61. package/dist/src/frontend/markdown_runtime.js +827 -0
  62. package/dist/src/frontend/mobile_budget_runtime.js +33 -0
  63. package/dist/src/frontend/mobile_exact_analyzer.js +393 -0
  64. package/dist/src/frontend/mobile_identity_contract.js +221 -0
  65. package/dist/src/frontend/mobile_semantic_comparator.js +264 -0
  66. package/dist/src/frontend/notemd.css +49 -0
  67. package/dist/src/frontend/notemd.html +7 -1
  68. package/dist/src/frontend/notemd.js +64 -0
  69. package/dist/src/frontend/path.html +113 -2
  70. package/dist/src/frontend/path_app.js +2108 -130
  71. package/dist/src/frontend/path_layout.mjs +143 -0
  72. package/dist/src/frontend/path_mermaid_utils.mjs +108 -0
  73. package/dist/src/frontend/path_modules_bridge.js +486 -0
  74. package/dist/src/frontend/path_state.mjs +118 -0
  75. package/dist/src/frontend/path_styles.css +146 -0
  76. package/dist/src/frontend/path_worker_bridge.mjs +85 -0
  77. package/dist/src/frontend/reader.js +393 -33
  78. package/dist/src/frontend/runtime_bridge.js +67 -54
  79. package/dist/src/frontend/runtime_bridge.mjs +279 -0
  80. package/dist/src/frontend/settings.js +130 -12
  81. package/dist/src/frontend/simulationWorker.js +1 -1
  82. package/dist/src/frontend/source_manager.js +297 -22
  83. package/dist/src/frontend/storage_provider.js +661 -71
  84. package/dist/src/frontend/styles.css +2814 -72
  85. package/dist/src/frontend/workbench_state.mjs +101 -0
  86. package/dist/src/frontend/workspace_panes.js +10272 -0
  87. package/dist/src/frontend.locale.contract.test.js +62 -0
  88. package/dist/src/frontend.runtime.assets.contract.test.js +81 -0
  89. package/dist/src/godot.sidecar.bootstrap.contract.test.js +244 -0
  90. package/dist/src/graph.answer.operator.contract.test.js +72 -0
  91. package/dist/src/index.js +6 -2
  92. package/dist/src/index.knowledge-root.test.js +118 -0
  93. package/dist/src/indexing/IndexLifecycle.js +217 -0
  94. package/dist/src/indexing/IndexLifecycle.test.js +73 -0
  95. package/dist/src/indexing/SegmentBuilder.js +64 -0
  96. package/dist/src/indexing/UnitBuilder.js +48 -0
  97. package/dist/src/indexing/types.js +2 -0
  98. package/dist/src/knowledge.api.contract.test.js +172 -0
  99. package/dist/src/knowledge_projection.contract.test.js +134 -0
  100. package/dist/src/knowledge_projection_store.test.js +200 -0
  101. package/dist/src/learning/AnswerQualityEvaluation.js +153 -0
  102. package/dist/src/learning/AnswerQualityEvaluation.test.js +118 -0
  103. package/dist/src/learning/KnowledgeLearningPlatform.js +11181 -0
  104. package/dist/src/learning/KnowledgeLearningPlatform.persistence.test.js +745 -0
  105. package/dist/src/learning/KnowledgeLearningPlatform.program-f.test.js +99 -0
  106. package/dist/src/learning/KnowledgeLearningPlatform.test.js +3208 -0
  107. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.js +2998 -0
  108. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.test.js +3936 -0
  109. package/dist/src/learning/MeasurementEvidenceConflicts.test.js +78 -0
  110. package/dist/src/learning/PublicAnswerContract.test.js +85 -0
  111. package/dist/src/learning/PublicEvidenceSelection.test.js +94 -0
  112. package/dist/src/learning/PublicMeasurementDisclosure.test.js +54 -0
  113. package/dist/src/learning/agentConversationExecution.js +211 -0
  114. package/dist/src/learning/agentConversationExecution.test.js +209 -0
  115. package/dist/src/learning/agentConversationSerialization.js +266 -0
  116. package/dist/src/learning/agentConversationSerialization.test.js +165 -0
  117. package/dist/src/learning/agentResponseBudget.js +179 -0
  118. package/dist/src/learning/agentResponseBudget.test.js +97 -0
  119. package/dist/src/learning/agentWorkspaceCapabilityContracts.js +65 -0
  120. package/dist/src/learning/answerReleaseReview.js +5260 -0
  121. package/dist/src/learning/answerReleaseReview.test.js +4174 -0
  122. package/dist/src/learning/answerTaskPlan.js +473 -0
  123. package/dist/src/learning/api.js +2 -0
  124. package/dist/src/learning/conversationComposer.js +1721 -0
  125. package/dist/src/learning/conversationComposer.test.js +3220 -0
  126. package/dist/src/learning/domains/ConversationManager.js +53 -0
  127. package/dist/src/learning/domains/KnowledgeIngestor.js +238 -0
  128. package/dist/src/learning/domains/KnowledgeQuerier.js +187 -0
  129. package/dist/src/learning/domains/MasteryEngine.js +387 -0
  130. package/dist/src/learning/domains/MemoryPolicyManager.js +408 -0
  131. package/dist/src/learning/domains/QualityEvaluator.js +307 -0
  132. package/dist/src/learning/domains/TutorRouter.js +313 -0
  133. package/dist/src/learning/domains/index.js +33 -0
  134. package/dist/src/learning/domains/types.js +7 -0
  135. package/dist/src/learning/errors.js +29 -0
  136. package/dist/src/learning/evidenceContextAssembler.js +1368 -0
  137. package/dist/src/learning/evidenceContextAssembler.test.js +6332 -0
  138. package/dist/src/learning/graphAnswerCoverage.js +37 -0
  139. package/dist/src/learning/graphAnswerCoverage.test.js +93 -0
  140. package/dist/src/learning/graphAnswerCoverageCalibration.js +59 -0
  141. package/dist/src/learning/graphAnswerCoverageCalibration.test.js +46 -0
  142. package/dist/src/learning/graphAnswerFacts.js +94 -0
  143. package/dist/src/learning/graphAnswerFacts.test.js +39 -0
  144. package/dist/src/learning/graphAnswerPlan.js +908 -0
  145. package/dist/src/learning/graphAnswerPlan.test.js +907 -0
  146. package/dist/src/learning/graphAnswerQualityCalibration.js +255 -0
  147. package/dist/src/learning/graphAnswerQualityCalibration.test.js +72 -0
  148. package/dist/src/learning/graphAnswerQualityPolicy.js +30 -0
  149. package/dist/src/learning/graphClaimMatcher.js +104 -0
  150. package/dist/src/learning/graphConditionedContext.js +109 -0
  151. package/dist/src/learning/graphConditionedContext.test.js +99 -0
  152. package/dist/src/learning/graphContextAssembler.js +860 -0
  153. package/dist/src/learning/graphContextAssembler.test.js +1033 -0
  154. package/dist/src/learning/graphExpansionPolicy.js +22 -0
  155. package/dist/src/learning/graphExpansionPolicy.test.js +33 -0
  156. package/dist/src/learning/identityMetadata.test.js +26 -0
  157. package/dist/src/learning/index.js +29 -0
  158. package/dist/src/learning/mobileAnswerProjection.js +104 -0
  159. package/dist/src/learning/mobileAnswerProjection.test.js +102 -0
  160. package/dist/src/learning/queryBackend.js +1922 -0
  161. package/dist/src/learning/queryBackend.test.js +976 -0
  162. package/dist/src/learning/ragContextPack.js +356 -0
  163. package/dist/src/learning/ragContextPack.test.js +311 -0
  164. package/dist/src/learning/ragEvidenceQuality.js +96 -0
  165. package/dist/src/learning/ragEvidenceQuality.test.js +29 -0
  166. package/dist/src/learning/ragPublicText.js +101 -0
  167. package/dist/src/learning/ragPublicText.test.js +57 -0
  168. package/dist/src/learning/ragSufficiencyJudge.js +171 -0
  169. package/dist/src/learning/ragSufficiencyJudge.test.js +177 -0
  170. package/dist/src/learning/ragSufficiencyProviderJudge.js +217 -0
  171. package/dist/src/learning/ragSufficiencyProviderJudge.test.js +207 -0
  172. package/dist/src/learning/requestNormalization.js +304 -0
  173. package/dist/src/learning/requestNormalization.test.js +101 -0
  174. package/dist/src/learning/runtimeCapability.js +4677 -0
  175. package/dist/src/learning/runtimeCapability.test.js +3635 -0
  176. package/dist/src/learning/store.js +1344 -0
  177. package/dist/src/learning/store.test.js +1425 -0
  178. package/dist/src/learning/tutorAdapter.js +2 -0
  179. package/dist/src/learning/types.js +2 -0
  180. package/dist/src/learning/vectorAccelerationAdapter.js +958 -0
  181. package/dist/src/learning/vectorAccelerationAdapter.test.js +412 -0
  182. package/dist/src/learning/workspaceHydration.js +168 -0
  183. package/dist/src/learning/workspaceHydration.test.js +30 -0
  184. package/dist/src/lfs.asset.policy.contract.test.js +153 -0
  185. package/dist/src/license.policy.contract.test.js +71 -0
  186. package/dist/src/markdown/MarkdownGateway.js +4 -0
  187. package/dist/src/markdown/MarkdownGateway.test.js +59 -0
  188. package/dist/src/memory/MemoryGovernance.js +74 -0
  189. package/dist/src/memory/MemoryGovernance.test.js +46 -0
  190. package/dist/src/memory/types.js +2 -0
  191. package/dist/src/mermaid.frontend.guard.contract.test.js +77 -0
  192. package/dist/src/middleware/SseResponseWriter.js +122 -0
  193. package/dist/src/middleware/SseResponseWriter.test.js +54 -0
  194. package/dist/src/middleware/auth.js +38 -0
  195. package/dist/src/middleware/auth.test.js +33 -0
  196. package/dist/src/middleware/body-parser.js +45 -0
  197. package/dist/src/middleware/cors.js +44 -0
  198. package/dist/src/middleware/index.js +22 -0
  199. package/dist/src/middleware/request-trace.js +96 -0
  200. package/dist/src/mobile.android.evidence.contract.test.js +146 -0
  201. package/dist/src/mobile.android.signing.contract.test.js +66 -0
  202. package/dist/src/mobile.artifact.contract.test.js +144 -0
  203. package/dist/src/mobile.budget.contract.test.js +90 -0
  204. package/dist/src/mobile.canonical-id.readiness.test.js +54 -0
  205. package/dist/src/mobile.graph.parity.contract.test.js +207 -0
  206. package/dist/src/mobile.identity.contract.test.js +66 -0
  207. package/dist/src/mobile.identity.corpus.manifest.test.js +72 -0
  208. package/dist/src/mobile.identity.corpus.test.js +40 -0
  209. package/dist/src/mobile.native.recovery.contract.test.js +64 -0
  210. package/dist/src/mobile.pipeline.test.js +11 -1
  211. package/dist/src/mobile.profile.contract.test.js +225 -0
  212. package/dist/src/mobile.semantic.parity.test.js +92 -0
  213. package/dist/src/mobile_exact_analyzer.test.js +113 -0
  214. package/dist/src/notemd/AppConfigToml.js +6 -4
  215. package/dist/src/notemd/ContentGenerator.js +4 -9
  216. package/dist/src/notemd/FileProcessor.js +4 -9
  217. package/dist/src/notemd/MermaidProcessor.js +400 -50
  218. package/dist/src/notemd/NotemdService.js +506 -26
  219. package/dist/src/notemd/PromptManager.js +15 -0
  220. package/dist/src/notemd/Translator.js +4 -9
  221. package/dist/src/notemd/cli/commands.js +357 -0
  222. package/dist/src/notemd/cli/dispatcher.js +225 -0
  223. package/dist/src/notemd/cli/index.js +169 -0
  224. package/dist/src/notemd/cli/parser.js +68 -0
  225. package/dist/src/notemd/cli/types.js +2 -0
  226. package/dist/src/notemd/constants.js +43 -0
  227. package/dist/src/notemd/diagram/diagramGenerationService.js +78 -0
  228. package/dist/src/notemd/diagram/diagramSpec.js +79 -0
  229. package/dist/src/notemd/diagram/diagramSpecResponseParser.js +131 -0
  230. package/dist/src/notemd/diagram/intent.js +95 -0
  231. package/dist/src/notemd/diagram/planner.js +71 -0
  232. package/dist/src/notemd/diagram/prompts/diagramSpecPrompt.js +42 -0
  233. package/dist/src/notemd/diagram/types.js +18 -0
  234. package/dist/src/notemd/index.js +26 -0
  235. package/dist/src/notemd/operations/capabilityManifest.js +23 -0
  236. package/dist/src/notemd/operations/cliContracts.js +17 -0
  237. package/dist/src/notemd/operations/configProfileCommands.js +85 -0
  238. package/dist/src/notemd/operations/registry.contract.test.js +95 -0
  239. package/dist/src/notemd/operations/registry.js +991 -0
  240. package/dist/src/notemd/operations/types.js +2 -0
  241. package/dist/src/notemd/progressReporter.js +9 -0
  242. package/dist/src/notemd/providerDiagnostics.js +220 -0
  243. package/dist/src/notemd/providerProfiles.js +42 -0
  244. package/dist/src/notemd/providerTemplates.js +231 -0
  245. package/dist/src/notemd/search/DuckDuckGoProvider.js +39 -0
  246. package/dist/src/notemd/search/SearchManager.js +13 -0
  247. package/dist/src/notemd/search/SearchProvider.js +2 -0
  248. package/dist/src/notemd/search/TavilyProvider.js +44 -0
  249. package/dist/src/notemd.agent.manifest.test.js +85 -0
  250. package/dist/src/notemd.api.contract.test.js +14 -1
  251. package/dist/src/notemd.app_config_toml.test.js +2 -1
  252. package/dist/src/notemd.batch.workflow.test.js +117 -0
  253. package/dist/src/notemd.cli.e2e.test.js +136 -0
  254. package/dist/src/notemd.core.test.js +51 -0
  255. package/dist/src/notemd.diagram.pipeline.test.js +233 -0
  256. package/dist/src/notemd.providerTemplates.test.js +34 -0
  257. package/dist/src/notemd.server.integration.test.js +262 -39
  258. package/dist/src/notemd.workflow.pipeline.test.js +162 -0
  259. package/dist/src/pathbridge.handshake.contract.test.js +45 -2
  260. package/dist/src/pathbridge.host-adapter.contract.test.js +133 -0
  261. package/dist/src/pathcore.bundle.contract.test.js +8 -0
  262. package/dist/src/pathmode.background.contract.test.js +69 -0
  263. package/dist/src/pathmode.settings.api.contract.test.js +9 -0
  264. package/dist/src/pkg.sidecar.contract.test.js +9 -3
  265. package/dist/src/platform/ExportProfile.js +86 -0
  266. package/dist/src/platform/MobileBudget.js +11 -0
  267. package/dist/src/platform/PlatformCapabilities.js +59 -0
  268. package/dist/src/platform/PlatformCapabilities.test.js +36 -0
  269. package/dist/src/platform/RenderMaterializer.js +33 -0
  270. package/dist/src/platform/RenderMaterializer.test.js +32 -0
  271. package/dist/src/query_backend.external_http.integration.test.js +410 -0
  272. package/dist/src/reader_renderer.js +404 -3
  273. package/dist/src/reader_renderer.test.js +87 -0
  274. package/dist/src/release.godot.mirror.contract.test.js +97 -0
  275. package/dist/src/resources/ResourceRegistry.js +263 -0
  276. package/dist/src/resources/ResourceRegistry.test.js +97 -0
  277. package/dist/src/resources/types.js +2 -0
  278. package/dist/src/routes/agentWorkspaceDiagnostics.js +173 -0
  279. package/dist/src/routes/index.js +19 -0
  280. package/dist/src/routes/knowledge.js +981 -0
  281. package/dist/src/routes/knowledgePayload.js +185 -0
  282. package/dist/src/routes/knowledgePayload.test.js +53 -0
  283. package/dist/src/routes/markdown.js +295 -0
  284. package/dist/src/routes/notemd.js +1321 -0
  285. package/dist/src/routes/registry.contract.test.js +133 -0
  286. package/dist/src/routes/registry.shadow.contract.test.js +22 -0
  287. package/dist/src/routes/render.js +248 -0
  288. package/dist/src/routes/runtimeRunbookRouteOps.js +149 -0
  289. package/dist/src/routes/runtimeRunbookRouteOps.test.js +194 -0
  290. package/dist/src/routes/settings.js +6 -0
  291. package/dist/src/routes/staticFiles.js +94 -0
  292. package/dist/src/routes/types.js +2 -0
  293. package/dist/src/runtime.capabilities.test.js +18 -2
  294. package/dist/src/runtime.transport.adapter.contract.test.js +81 -0
  295. package/dist/src/server.js +10342 -1325
  296. package/dist/src/server.migration.test.js +584 -10
  297. package/dist/src/server.port.fallback.contract.test.js +63 -0
  298. package/dist/src/session/SessionStateStore.js +81 -0
  299. package/dist/src/session/SessionStateStore.test.js +58 -0
  300. package/dist/src/session/types.js +2 -0
  301. package/dist/src/settings.runtime.contract.test.js +50 -0
  302. package/dist/src/shared/types.contract.test.js +107 -0
  303. package/dist/src/shared/types.js +22 -0
  304. package/dist/src/sidecar.build.lock.contract.test.js +201 -0
  305. package/dist/src/sidecar.freshness.contract.test.js +184 -0
  306. package/dist/src/sidecar.replacement.boundary.contract.test.js +131 -0
  307. package/dist/src/sidecar.supply.readiness.contract.test.js +155 -0
  308. package/dist/src/simulation.worker.offline.test.js +104 -0
  309. package/dist/src/source_manager.loadflow.test.js +46 -0
  310. package/dist/src/startup.layout.snapshot.contract.test.js +57 -0
  311. package/dist/src/storage.provider.capacitor.content.contract.test.js +4 -1
  312. package/dist/src/storage.provider.contract.test.js +152 -1
  313. package/dist/src/tauri.frontend.build.contract.test.js +68 -0
  314. package/dist/src/tauri.sidecar.cleanup.contract.test.js +21 -0
  315. package/dist/src/tauri.window.evidence.execution.test.js +68 -0
  316. package/dist/src/utils/RuntimePaths.js +10 -13
  317. package/dist/src/utils/RuntimePaths.test.js +27 -0
  318. package/dist/src/utils/platform.js +153 -0
  319. package/dist/src/windows.installer.build.test.js +109 -0
  320. package/dist/src/windows.installer.payload.test.js +76 -0
  321. package/dist/src/windows.installer.runtime.test.js +69 -0
  322. package/dist/src/workflows/WorkflowArtifactStore.js +96 -0
  323. package/dist/src/workflows/WorkflowArtifactStore.test.js +80 -0
  324. package/dist/src/workflows/types.js +2 -0
  325. package/dist/src/workspace/WorkspaceRegistry.js +135 -0
  326. package/dist/src/workspace/WorkspaceRegistry.test.js +29 -0
  327. package/dist/src/workspace/types.js +2 -0
  328. package/package.json +80 -15
@@ -102,7 +102,7 @@ function deferred() {
102
102
  });
103
103
  return { promise, resolve };
104
104
  }
105
- function requestJson(port, method, requestPath, body) {
105
+ function requestJson(port, method, requestPath, body, extraHeaders) {
106
106
  return new Promise((resolve, reject) => {
107
107
  const payload = typeof body === 'undefined' ? undefined : JSON.stringify(body);
108
108
  const req = http.request({
@@ -113,9 +113,10 @@ function requestJson(port, method, requestPath, body) {
113
113
  headers: payload
114
114
  ? {
115
115
  'Content-Type': 'application/json',
116
- 'Content-Length': Buffer.byteLength(payload)
116
+ 'Content-Length': Buffer.byteLength(payload),
117
+ ...(extraHeaders || {})
117
118
  }
118
- : undefined
119
+ : (extraHeaders || undefined)
119
120
  }, (res) => {
120
121
  let text = '';
121
122
  res.setEncoding('utf8');
@@ -134,7 +135,8 @@ function requestJson(port, method, requestPath, body) {
134
135
  }
135
136
  resolve({
136
137
  status: res.statusCode || 0,
137
- body: parsed
138
+ body: parsed,
139
+ headers: res.headers
138
140
  });
139
141
  });
140
142
  });
@@ -178,7 +180,8 @@ function requestRaw(port, method, requestPath, payload, extraHeaders) {
178
180
  }
179
181
  resolve({
180
182
  status: res.statusCode || 0,
181
- body: parsed
183
+ body: parsed,
184
+ headers: res.headers
182
185
  });
183
186
  });
184
187
  });
@@ -217,10 +220,18 @@ function requestBinary(port, method, requestPath, payload, extraHeaders) {
217
220
  parsed = text;
218
221
  }
219
222
  }
220
- resolve({
223
+ const response = {
221
224
  status: res.statusCode || 0,
222
- body: parsed
223
- });
225
+ body: parsed,
226
+ headers: res.headers
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
+ }
224
235
  });
225
236
  });
226
237
  req.on('error', reject);
@@ -243,7 +254,10 @@ describe('server migration settings routes', () => {
243
254
  let buildGraphMock;
244
255
  let renderMathPngMock;
245
256
  let renderMermaidPngMock;
257
+ let collectMermaidRenderStageSnapshotsMock;
258
+ let normalizeMermaidDefinitionMock;
246
259
  let copyPngToClipboardMock;
260
+ let requestFrontendMermaidRenderMock;
247
261
  let originalArgv;
248
262
  beforeAll(async () => {
249
263
  temp = new TempDir('noteconnection-server');
@@ -284,7 +298,32 @@ describe('server migration settings routes', () => {
284
298
  width: 640,
285
299
  height: 360
286
300
  });
301
+ collectMermaidRenderStageSnapshotsMock = jest.fn().mockResolvedValue([
302
+ {
303
+ stage: 'raw',
304
+ svg: '<svg xmlns="http://www.w3.org/2000/svg"></svg>',
305
+ pngBase64: 'raw-stage-png-base64',
306
+ width: 640,
307
+ height: 360
308
+ }
309
+ ]);
310
+ normalizeMermaidDefinitionMock = jest.fn((source) => source);
287
311
  copyPngToClipboardMock = jest.fn().mockResolvedValue(undefined);
312
+ requestFrontendMermaidRenderMock = jest.fn().mockResolvedValue({
313
+ pngBase64: 'frontend-bridge-png-base64',
314
+ svg: '<svg xmlns="http://www.w3.org/2000/svg"><text>frontend</text></svg>',
315
+ width: 800,
316
+ height: 480,
317
+ renderer: 'frontend-bridge',
318
+ stages: [
319
+ {
320
+ stage: 'final',
321
+ svg: '<svg xmlns="http://www.w3.org/2000/svg"><text>frontend</text></svg>',
322
+ width: 800,
323
+ height: 480,
324
+ }
325
+ ]
326
+ });
288
327
  jest.resetModules();
289
328
  originalArgv = [...process.argv];
290
329
  process.argv = process.argv.slice(0, 2);
@@ -292,11 +331,15 @@ describe('server migration settings routes', () => {
292
331
  buildGraph: buildGraphMock
293
332
  }));
294
333
  jest.doMock('./core/PathBridge', () => ({
295
- PathBridge: jest.fn().mockImplementation(() => ({}))
334
+ PathBridge: jest.fn().mockImplementation(() => ({
335
+ requestFrontendMermaidRender: requestFrontendMermaidRenderMock
336
+ }))
296
337
  }));
297
338
  jest.doMock('./reader_renderer', () => ({
298
339
  renderMathPng: renderMathPngMock,
299
- renderMermaidPng: renderMermaidPngMock
340
+ renderMermaidPng: renderMermaidPngMock,
341
+ collectMermaidRenderStageSnapshots: collectMermaidRenderStageSnapshotsMock,
342
+ normalizeMermaidDefinition: normalizeMermaidDefinitionMock
300
343
  }));
301
344
  jest.doMock('./native_clipboard', () => ({
302
345
  copyPngToClipboard: copyPngToClipboardMock
@@ -441,6 +484,13 @@ describe('server migration settings routes', () => {
441
484
  expect(typeof response.body).toBe('string');
442
485
  expect(response.body).toContain('const graphData');
443
486
  });
487
+ test('returns current KB path when cache-busting query string is present', async () => {
488
+ const response = await requestJson(port, 'GET', '/api/kb-path?v=12345');
489
+ expect(response.status).toBe(200);
490
+ expect(response.body).toEqual(expect.objectContaining({
491
+ kbPath: kbRoot,
492
+ }));
493
+ });
444
494
  test('rejects static traversal attempts with raw parent-segment path', async () => {
445
495
  const response = await requestJson(port, 'GET', '/../../outside/sensitive.md');
446
496
  expect(response.status).toBe(403);
@@ -511,6 +561,17 @@ describe('server migration settings routes', () => {
511
561
  const firstResponse = await firstRequest;
512
562
  expect(firstResponse.status).toBe(200);
513
563
  });
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
+ });
514
575
  test('returns 413 when /api/build request body exceeds size limit', async () => {
515
576
  const oversizedPayload = {
516
577
  target: 'financial',
@@ -546,6 +607,519 @@ describe('server migration settings routes', () => {
546
607
  expect(response.body.svg).toBeUndefined();
547
608
  expect(renderMermaidPngMock).toHaveBeenCalled();
548
609
  });
610
+ test('prefers frontend bridge for /api/render/mermaid when renderer is auto and a frontend bridge is available', async () => {
611
+ requestFrontendMermaidRenderMock.mockClear();
612
+ renderMermaidPngMock.mockClear();
613
+ const response = await requestJson(port, 'POST', '/api/render/mermaid', {
614
+ source: 'graph TD; A-->B',
615
+ renderer: 'auto'
616
+ });
617
+ expect(response.status).toBe(200);
618
+ expect(response.body).toEqual(expect.objectContaining({
619
+ pngBase64: 'frontend-bridge-png-base64',
620
+ width: 800,
621
+ height: 480,
622
+ renderer: 'frontend-bridge'
623
+ }));
624
+ expect(requestFrontendMermaidRenderMock).toHaveBeenCalledWith(expect.objectContaining({
625
+ source: 'graph TD; A-->B',
626
+ theme: 'dark',
627
+ includeStages: false,
628
+ includeSvg: false
629
+ }));
630
+ expect(renderMermaidPngMock).not.toHaveBeenCalled();
631
+ });
632
+ test('suppresses svg for Godot export profile even when includeSvg is requested', async () => {
633
+ const response = await requestJson(port, 'POST', '/api/render/mermaid', {
634
+ source: 'graph TD; A-->B',
635
+ renderer: 'local',
636
+ includeSvg: true,
637
+ exportProfileId: 'godot-path-mode'
638
+ });
639
+ expect(response.status).toBe(200);
640
+ expect(response.body.svg).toBeUndefined();
641
+ expect(response.body.materialization).toEqual(expect.objectContaining({
642
+ exportProfileId: 'godot-path-mode',
643
+ vectorSuppressed: true,
644
+ responseArtifact: 'png'
645
+ }));
646
+ });
647
+ test('conversation route uses turn-cache inline flow with scoped grounded response and replay headers', async () => {
648
+ const ingestResponse = await requestJson(port, 'POST', '/api/knowledge/ingest', {
649
+ incremental: true,
650
+ documents: [
651
+ {
652
+ documentId: 'doc_server_conversation',
653
+ sourcePath: 'Knowledge_Base/optics/absorption.md',
654
+ language: 'zh',
655
+ content: '# 吸收\n吸收系数与光学衰减决定材料中的能量损失。'
656
+ }
657
+ ]
658
+ });
659
+ expect(ingestResponse.status).toBe(200);
660
+ const turnId = 'turn_test_conversation_scope';
661
+ const firstResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', {
662
+ userId: 'server_conversation_user',
663
+ sessionId: 'server_session_scope',
664
+ activeTarget: 'optics',
665
+ message: '解释吸收系数和光学衰减',
666
+ answerLanguage: 'zh',
667
+ scope: {
668
+ corpusId: 'optics',
669
+ languages: ['zh']
670
+ },
671
+ persistMemory: true
672
+ }, {
673
+ 'X-Agent-Conversation-Turn-Id': turnId
674
+ });
675
+ expect(firstResponse.status).toBe(200);
676
+ expect(firstResponse.headers?.['x-agent-conversation-turn-id']).toBe(turnId);
677
+ expect(firstResponse.headers?.['x-agent-conversation-replay']).toBe('miss');
678
+ expect(firstResponse.body.result.trace.usedScope).toEqual(expect.objectContaining({
679
+ corpusId: 'optics',
680
+ source: 'scoped'
681
+ }));
682
+ expect(firstResponse.body.result.citations.length).toBeGreaterThan(0);
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
+ ]));
701
+ const replayResponse = await requestJson(port, 'POST', '/api/knowledge/conversation', {
702
+ userId: 'server_conversation_user',
703
+ sessionId: 'server_session_scope',
704
+ activeTarget: 'optics',
705
+ message: '解释吸收系数和光学衰减',
706
+ answerLanguage: 'zh',
707
+ scope: {
708
+ corpusId: 'optics',
709
+ languages: ['zh']
710
+ },
711
+ persistMemory: true
712
+ }, {
713
+ 'X-Agent-Conversation-Turn-Id': turnId
714
+ });
715
+ expect(replayResponse.status).toBe(200);
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('## 证据摘要');
965
+ });
966
+ test('conversation route auto-hydrates the active knowledge target when the learning workspace store is empty', async () => {
967
+ temp.mkdir(path.join('project', 'Knowledge_Base', 'waterglass'));
968
+ temp.file(path.join('project', 'Knowledge_Base', 'waterglass', 'water glass.md'), '# Water Glass\nwater glass 是一个装有水的玻璃容器系统。');
969
+ const response = await requestJson(port, 'POST', '/api/knowledge/conversation', {
970
+ userId: 'server_auto_hydrate_user',
971
+ sessionId: 'server_auto_hydrate_session',
972
+ activeTarget: 'waterglass',
973
+ message: '什么是waterglass?',
974
+ persistMemory: false
975
+ }, {
976
+ 'X-Agent-Conversation-Turn-Id': 'turn_auto_hydrate_scope'
977
+ });
978
+ expect(response.status).toBe(200);
979
+ expect(response.body.result.trace.usedScope).toEqual(expect.objectContaining({
980
+ workspaceId: 'waterglass',
981
+ corpusId: 'waterglass',
982
+ source: 'scoped',
983
+ }));
984
+ expect(response.body.result.trace.workspaceReadiness).toEqual(expect.objectContaining({
985
+ status: 'ready',
986
+ workspaceId: 'waterglass',
987
+ }));
988
+ expect(response.body.result.trace.planner).toEqual(expect.objectContaining({
989
+ titleLikeQueries: expect.arrayContaining(['water glass', 'waterglass']),
990
+ }));
991
+ expect(response.body.result.citations.length).toBeGreaterThan(0);
992
+ const answer = String(response.body.result.answer || '');
993
+ expect(answer.toLowerCase()).toContain('water glass');
994
+ expect(answer).not.toContain('Grounded by');
995
+ expect(answer).not.toContain('Key evidence');
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
+ });
1112
+ test('returns 400 from /api/render/math when expression is empty', async () => {
1113
+ const response = await requestJson(port, 'POST', '/api/render/math', {
1114
+ expression: ' '
1115
+ });
1116
+ expect(response.status).toBe(400);
1117
+ expect(response.body).toEqual(expect.objectContaining({
1118
+ success: false,
1119
+ error: 'Missing expression'
1120
+ }));
1121
+ expect(renderMathPngMock).not.toHaveBeenCalled();
1122
+ });
549
1123
  test('returns svg from /api/render/mermaid when includeSvg is explicitly enabled', async () => {
550
1124
  const response = await requestJson(port, 'POST', '/api/render/mermaid', {
551
1125
  source: 'graph TD; A-->B',