@modusensus/dsh-mneme 0.14.0-beta.1 → 0.14.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (543) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +830 -830
  3. package/assets/icon.svg +3 -3
  4. package/bin/cli.mjs +964 -964
  5. package/bin/dsh-mneme-choro.mjs +262 -262
  6. package/bin/dsh-mneme-mcp.mjs +510 -510
  7. package/bin/dsh-mneme-serve.mjs +151 -151
  8. package/cordis.patch.yml +21 -21
  9. package/docs/CONFIGURATION.md +407 -404
  10. package/docs/CONTINUITY.md +139 -139
  11. package/docs/DAEMON.md +57 -57
  12. package/docs/DIRECTORIES.md +114 -114
  13. package/docs/DISTILL.md +397 -371
  14. package/docs/DSH-GO.md +97 -97
  15. package/docs/ENTITIES.md +247 -247
  16. package/docs/FORGET.md +77 -77
  17. package/docs/KB.md +110 -110
  18. package/docs/LOCAL_MODEL.md +222 -222
  19. package/docs/SEMANTIC.md +256 -256
  20. package/docs/SHARE.md +552 -552
  21. package/docs/SLEEP.md +202 -202
  22. package/docs/STORAGE.md +122 -122
  23. package/docs/WORKFLOWS.md +366 -366
  24. package/lib/api-standalone.js +1007 -1007
  25. package/lib/api.js +3600 -3600
  26. package/lib/attachment-text.js +122 -122
  27. package/lib/bootstrap-http.js +105 -105
  28. package/lib/bootstrap.js +215 -215
  29. package/lib/candidates.js +454 -454
  30. package/lib/capabilities.js +819 -819
  31. package/lib/choro-command.js +111 -111
  32. package/lib/client.js +14420 -14322
  33. package/lib/commands.js +95 -95
  34. package/lib/config-keys.js +144 -144
  35. package/lib/config.js +1361 -1344
  36. package/lib/content-hash.js +54 -54
  37. package/lib/continuity.js +258 -258
  38. package/lib/date-hint.js +32 -0
  39. package/lib/diary/auto.js +190 -190
  40. package/lib/diary/compose.js +440 -440
  41. package/lib/diary/dir.js +59 -59
  42. package/lib/diary/export.js +97 -97
  43. package/lib/diary/index.js +26 -26
  44. package/lib/diary/library.js +189 -189
  45. package/lib/diary/qq-inbox.js +142 -142
  46. package/lib/diary/rollover.js +61 -61
  47. package/lib/diary/session-provider.js +122 -122
  48. package/lib/document-index.js +94 -94
  49. package/lib/document.js +322 -322
  50. package/lib/dream/clustering.js +123 -123
  51. package/lib/dream/decisions.js +945 -945
  52. package/lib/dream/narratives.js +44 -44
  53. package/lib/dream/sleep.js +2027 -2027
  54. package/lib/dream/triage.js +139 -139
  55. package/lib/dream.js +1879 -1879
  56. package/lib/dsh-go/host.js +162 -162
  57. package/lib/dsh-go/panel.js +2091 -2091
  58. package/lib/dsh-go/text.js +342 -342
  59. package/lib/dsh-go/views/common.js +68 -68
  60. package/lib/dsh-go/views/entity.js +48 -48
  61. package/lib/dsh-go/views/memory.js +198 -198
  62. package/lib/dsh-go/views/root.js +30 -30
  63. package/lib/dsh-go/views/settings.js +398 -398
  64. package/lib/dsh-go/views/status.js +51 -51
  65. package/lib/embedding.js +342 -342
  66. package/lib/entities/extractor.js +285 -285
  67. package/lib/fence.js +24 -24
  68. package/lib/graph/anchoring.js +184 -184
  69. package/lib/guide.js +74 -74
  70. package/lib/heat.js +144 -144
  71. package/lib/hot-memory.js +119 -119
  72. package/lib/image-describe.js +158 -0
  73. package/lib/index.js +852 -852
  74. package/lib/inject.js +656 -651
  75. package/lib/kb/index.js +309 -309
  76. package/lib/kb/routes.js +219 -219
  77. package/lib/kb/tools.js +148 -148
  78. package/lib/lang.js +607 -595
  79. package/lib/local-embedder.js +358 -358
  80. package/lib/maintenance.js +162 -162
  81. package/lib/memory-source.js +9 -9
  82. package/lib/mirror.js +330 -330
  83. package/lib/organize.js +456 -456
  84. package/lib/peak-hours.js +172 -172
  85. package/lib/pipeline-activities.js +178 -178
  86. package/lib/pipeline-attribution.js +165 -165
  87. package/lib/pipeline-command.js +279 -279
  88. package/lib/pipeline-confirm.js +142 -142
  89. package/lib/pipeline-exec.js +174 -174
  90. package/lib/pipeline-nudge-auto.js +129 -129
  91. package/lib/pipeline-nudge.js +1623 -1623
  92. package/lib/pipeline-observations.js +181 -181
  93. package/lib/pipeline-observe.js +133 -133
  94. package/lib/pipeline-runner.js +361 -361
  95. package/lib/pipeline-runs.js +442 -442
  96. package/lib/pipeline.js +716 -716
  97. package/lib/placement.js +13 -13
  98. package/lib/pre-inject-gate.js +354 -354
  99. package/lib/profile-extract.js +164 -164
  100. package/lib/profile-facts.js +356 -356
  101. package/lib/provenance-http.js +251 -251
  102. package/lib/quality-filter.js +162 -162
  103. package/lib/recall-stats.js +190 -190
  104. package/lib/reclaim-http.js +150 -150
  105. package/lib/reranker.js +269 -269
  106. package/lib/runtime/adopt.js +176 -176
  107. package/lib/runtime/closure.js +172 -172
  108. package/lib/runtime/download.js +247 -247
  109. package/lib/runtime/layout.js +265 -265
  110. package/lib/runtime/loader.js +252 -252
  111. package/lib/runtime/model-fetch.js +361 -361
  112. package/lib/runtime/provision.js +237 -237
  113. package/lib/runtime/tarball.js +130 -130
  114. package/lib/runtime/verify.js +223 -223
  115. package/lib/safe-slice.js +18 -18
  116. package/lib/scope-state.js +277 -277
  117. package/lib/scope.js +187 -187
  118. package/lib/search/adaptive.js +60 -60
  119. package/lib/search/bm25.js +96 -96
  120. package/lib/semantic.js +572 -572
  121. package/lib/sensitive-scan.js +209 -209
  122. package/lib/serve.js +228 -228
  123. package/lib/service.js +3268 -3268
  124. package/lib/session-gate.js +91 -91
  125. package/lib/session-usage.js +350 -350
  126. package/lib/settings.js +827 -817
  127. package/lib/share-http.js +281 -281
  128. package/lib/share-import.js +234 -234
  129. package/lib/share.js +335 -335
  130. package/lib/store.js +5895 -5895
  131. package/lib/summarize.js +1388 -1343
  132. package/lib/tools.js +1370 -1356
  133. package/lib/user-settings.js +230 -230
  134. package/lib/vector-index.js +180 -180
  135. package/lib/version-check.js +122 -122
  136. package/lib/wiki/claims.js +267 -267
  137. package/lib/wiki/cli-hint.js +90 -90
  138. package/lib/wiki/inject.js +164 -164
  139. package/lib/wiki/partition.js +200 -200
  140. package/lib/wiki/plan.js +214 -214
  141. package/lib/wiki/queries.js +640 -640
  142. package/lib/wiki/render.js +261 -261
  143. package/lib/wiki/scan.js +371 -371
  144. package/lib/wiki/t0.js +500 -500
  145. package/lib/wiki/t1-jobs.js +232 -232
  146. package/lib/wiki/t1.js +609 -609
  147. package/lib/write-admission.js +404 -404
  148. package/locale/en.json +6 -6
  149. package/locale/zh.json +6 -6
  150. package/package.json +1 -1
  151. package/runtime-manifest.json +526 -526
  152. package/scripts/benchmark-embed.js +201 -201
  153. package/scripts/benchmark-recall.js +249 -249
  154. package/scripts/benchmark-rerank.js +166 -166
  155. package/scripts/build-runtime-manifest.mjs +224 -224
  156. package/scripts/changelog-prep.mjs +91 -91
  157. package/scripts/check-sync.js +44 -44
  158. package/scripts/e2e-dsh.js +220 -220
  159. package/scripts/mneme-runtime.mjs +277 -277
  160. package/scripts/publish-gate.mjs +197 -197
  161. package/scripts/release-docs.mjs +210 -210
  162. package/scripts/release-notes.mjs +164 -164
  163. package/scripts/stress-dsh.js +255 -255
  164. package/scripts/sync-lib.js +47 -47
  165. package/scripts/sync-test-badge.mjs +69 -69
  166. package/scripts/test-count-sync.mjs +48 -48
  167. package/src/api-standalone.js +1007 -1007
  168. package/src/api.js +3600 -3600
  169. package/src/attachment-text.js +122 -122
  170. package/src/bootstrap-http.js +105 -105
  171. package/src/bootstrap.js +215 -215
  172. package/src/candidates.js +454 -454
  173. package/src/capabilities.js +819 -819
  174. package/src/choro-command.js +111 -111
  175. package/src/commands.js +95 -95
  176. package/src/config-keys.js +144 -144
  177. package/src/config.js +1361 -1344
  178. package/src/content-hash.js +54 -54
  179. package/src/continuity.js +258 -258
  180. package/src/date-hint.js +32 -0
  181. package/src/diary/auto.js +190 -190
  182. package/src/diary/compose.js +440 -440
  183. package/src/diary/dir.js +59 -59
  184. package/src/diary/export.js +97 -97
  185. package/src/diary/index.js +26 -26
  186. package/src/diary/library.js +189 -189
  187. package/src/diary/qq-inbox.js +142 -142
  188. package/src/diary/rollover.js +61 -61
  189. package/src/diary/session-provider.js +122 -122
  190. package/src/document-index.js +94 -94
  191. package/src/document.js +322 -322
  192. package/src/dream/clustering.js +123 -123
  193. package/src/dream/decisions.js +945 -945
  194. package/src/dream/narratives.js +44 -44
  195. package/src/dream/sleep.js +2027 -2027
  196. package/src/dream/triage.js +139 -139
  197. package/src/dream.js +1879 -1879
  198. package/src/dsh-go/host.js +162 -162
  199. package/src/dsh-go/panel.js +2091 -2091
  200. package/src/dsh-go/text.js +342 -342
  201. package/src/dsh-go/views/common.js +68 -68
  202. package/src/dsh-go/views/entity.js +48 -48
  203. package/src/dsh-go/views/memory.js +198 -198
  204. package/src/dsh-go/views/root.js +30 -30
  205. package/src/dsh-go/views/settings.js +398 -398
  206. package/src/dsh-go/views/status.js +51 -51
  207. package/src/embedding.js +342 -342
  208. package/src/entities/extractor.js +285 -285
  209. package/src/fence.js +24 -24
  210. package/src/graph/anchoring.js +184 -184
  211. package/src/guide.js +74 -74
  212. package/src/heat.js +144 -144
  213. package/src/hot-memory.js +119 -119
  214. package/src/image-describe.js +158 -0
  215. package/src/index.js +852 -852
  216. package/src/inject.js +656 -651
  217. package/src/kb/index.js +309 -309
  218. package/src/kb/routes.js +219 -219
  219. package/src/kb/tools.js +148 -148
  220. package/src/lang.js +607 -595
  221. package/src/local-embedder.js +358 -358
  222. package/src/maintenance.js +162 -162
  223. package/src/memory-source.js +9 -9
  224. package/src/mirror.js +330 -330
  225. package/src/organize.js +456 -456
  226. package/src/peak-hours.js +172 -172
  227. package/src/pipeline-activities.js +178 -178
  228. package/src/pipeline-attribution.js +165 -165
  229. package/src/pipeline-command.js +279 -279
  230. package/src/pipeline-confirm.js +142 -142
  231. package/src/pipeline-exec.js +174 -174
  232. package/src/pipeline-nudge-auto.js +129 -129
  233. package/src/pipeline-nudge.js +1623 -1623
  234. package/src/pipeline-observations.js +181 -181
  235. package/src/pipeline-observe.js +133 -133
  236. package/src/pipeline-runner.js +361 -361
  237. package/src/pipeline-runs.js +442 -442
  238. package/src/pipeline.js +716 -716
  239. package/src/placement.js +13 -13
  240. package/src/pre-inject-gate.js +354 -354
  241. package/src/profile-extract.js +164 -164
  242. package/src/profile-facts.js +356 -356
  243. package/src/provenance-http.js +251 -251
  244. package/src/quality-filter.js +162 -162
  245. package/src/recall-stats.js +190 -190
  246. package/src/reclaim-http.js +150 -150
  247. package/src/reranker.js +269 -269
  248. package/src/runtime/adopt.js +176 -176
  249. package/src/runtime/closure.js +172 -172
  250. package/src/runtime/download.js +247 -247
  251. package/src/runtime/layout.js +265 -265
  252. package/src/runtime/loader.js +252 -252
  253. package/src/runtime/model-fetch.js +361 -361
  254. package/src/runtime/provision.js +237 -237
  255. package/src/runtime/tarball.js +130 -130
  256. package/src/runtime/verify.js +223 -223
  257. package/src/safe-slice.js +18 -18
  258. package/src/scope-state.js +277 -277
  259. package/src/scope.js +187 -187
  260. package/src/search/adaptive.js +60 -60
  261. package/src/search/bm25.js +96 -96
  262. package/src/semantic.js +572 -572
  263. package/src/sensitive-scan.js +209 -209
  264. package/src/serve.js +228 -228
  265. package/src/service.js +3268 -3268
  266. package/src/session-gate.js +91 -91
  267. package/src/session-usage.js +350 -350
  268. package/src/settings.js +827 -817
  269. package/src/share-http.js +281 -281
  270. package/src/share-import.js +234 -234
  271. package/src/share.js +335 -335
  272. package/src/store.js +5895 -5895
  273. package/src/summarize.js +1388 -1343
  274. package/src/tools.js +1370 -1356
  275. package/src/user-settings.js +230 -230
  276. package/src/vector-index.js +180 -180
  277. package/src/version-check.js +122 -122
  278. package/src/wiki/claims.js +267 -267
  279. package/src/wiki/cli-hint.js +90 -90
  280. package/src/wiki/inject.js +164 -164
  281. package/src/wiki/partition.js +200 -200
  282. package/src/wiki/plan.js +214 -214
  283. package/src/wiki/queries.js +640 -640
  284. package/src/wiki/render.js +261 -261
  285. package/src/wiki/scan.js +371 -371
  286. package/src/wiki/t0.js +500 -500
  287. package/src/wiki/t1-jobs.js +232 -232
  288. package/src/wiki/t1.js +609 -609
  289. package/src/write-admission.js +404 -404
  290. package/test/adaptive-inject-budget.test.js +85 -85
  291. package/test/api-hardening.test.js +173 -173
  292. package/test/api-nudge-route.test.js +259 -259
  293. package/test/api-routes-gaps.test.js +1092 -1092
  294. package/test/api.test.js +3007 -2998
  295. package/test/attachment-text.test.js +139 -139
  296. package/test/audit.test.js +448 -448
  297. package/test/baseline-persist.test.js +248 -248
  298. package/test/benchmark.test.js +94 -94
  299. package/test/bm25-corpus-cache.test.js +145 -145
  300. package/test/bootstrap.test.js +228 -228
  301. package/test/candidates.test.js +345 -345
  302. package/test/capabilities.test.js +577 -577
  303. package/test/capacity-warning.test.js +262 -262
  304. package/test/changelog-prep.test.js +125 -125
  305. package/test/choro-command.test.js +218 -218
  306. package/test/cli-enums.test.js +37 -37
  307. package/test/cli-flag-value.test.js +86 -86
  308. package/test/cli-receive.test.js +390 -390
  309. package/test/cli-share.test.js +437 -437
  310. package/test/client-share.test.js +225 -225
  311. package/test/client.test.js +4595 -4425
  312. package/test/clustering.test.js +100 -100
  313. package/test/commands.test.js +106 -106
  314. package/test/config-keys-routes.test.js +233 -233
  315. package/test/config.test.js +153 -153
  316. package/test/conflict-freeze.test.js +344 -344
  317. package/test/conflict-ledger-enforce.test.js +452 -452
  318. package/test/conflict-ledger.test.js +242 -242
  319. package/test/conflict-reviewed.test.js +79 -79
  320. package/test/content-hash.test.js +204 -204
  321. package/test/continuity.test.js +452 -452
  322. package/test/contrast.test.js +432 -432
  323. package/test/date-hint.test.js +156 -0
  324. package/test/dedupe-candidate-window.test.js +180 -180
  325. package/test/dependency-overrides.test.js +78 -78
  326. package/test/diary-library.test.js +187 -187
  327. package/test/diary.test.js +576 -576
  328. package/test/differentiate-feedback.test.js +248 -248
  329. package/test/distill-tx-embed-gap.test.js +110 -110
  330. package/test/docs-config-keys.test.js +65 -65
  331. package/test/document-dir.test.js +233 -233
  332. package/test/document.test.js +703 -703
  333. package/test/dream-archive-guardrail.test.js +116 -116
  334. package/test/dream-baseline-clamp.test.js +174 -174
  335. package/test/dream-candidate-hybrid.test.js +281 -281
  336. package/test/dream-coverage-degrade.test.js +86 -86
  337. package/test/dream-decision-evidence.test.js +314 -314
  338. package/test/dream-failure-backoff.test.js +173 -173
  339. package/test/dream-fence.test.js +207 -207
  340. package/test/dream-maxtokens-fallback.test.js +132 -132
  341. package/test/dream-merge-guardrail.test.js +219 -219
  342. package/test/dream-peak-hours.test.js +194 -194
  343. package/test/dream.test.js +1528 -1528
  344. package/test/dsh-go-host.test.js +134 -134
  345. package/test/dsh-go-panel-actions.test.js +854 -854
  346. package/test/dsh-go-panel.test.js +251 -251
  347. package/test/embed-ready-poll.test.js +94 -94
  348. package/test/embed-text-reasons.test.js +112 -112
  349. package/test/embedding-cache-f32.test.js +276 -276
  350. package/test/embedding-ready.test.js +226 -226
  351. package/test/entities.test.js +711 -711
  352. package/test/entity-recall.test.js +179 -179
  353. package/test/epistemic.test.js +325 -325
  354. package/test/failure-query.test.js +147 -147
  355. package/test/failure-retention.test.js +105 -105
  356. package/test/fnew-0112.test.js +311 -311
  357. package/test/fnew-03.test.js +422 -422
  358. package/test/forget-corrections.test.js +70 -70
  359. package/test/graph-anchoring.test.js +184 -184
  360. package/test/graph-api.test.js +333 -333
  361. package/test/graph-boosted-relations.test.js +189 -189
  362. package/test/graph-inject-budget.test.js +103 -103
  363. package/test/graph-multipath.test.js +269 -269
  364. package/test/graph-passive-confirm.test.js +91 -91
  365. package/test/graph-weight-evolution.test.js +104 -104
  366. package/test/heat.test.js +175 -175
  367. package/test/helpers/dream-mock.js +86 -86
  368. package/test/helpers/peer-worker.mjs +25 -25
  369. package/test/helpers/runtime-source.js +60 -60
  370. package/test/helpers/tar-builder.js +80 -80
  371. package/test/helpers/write-admission-samples.js +312 -312
  372. package/test/hot-memory.test.js +259 -259
  373. package/test/image-description.test.js +271 -0
  374. package/test/image-memory.test.js +170 -170
  375. package/test/inject-content-cap.test.js +91 -91
  376. package/test/inject-first-round-bm25.test.js +89 -89
  377. package/test/inject-heat.test.js +77 -77
  378. package/test/inject-parent-gate.test.js +134 -109
  379. package/test/inject-pin-explicit.test.js +300 -300
  380. package/test/inject-pin-vector-crowd.test.js +225 -225
  381. package/test/inject-pools.test.js +195 -195
  382. package/test/inject-recall.test.js +172 -172
  383. package/test/inject-rotation.test.js +161 -161
  384. package/test/inject.test.js +553 -553
  385. package/test/issue-617-step1-query-and-fusion.test.js +361 -361
  386. package/test/kb-api-wiring.test.js +128 -128
  387. package/test/kb-collections.test.js +272 -272
  388. package/test/kb-routes.test.js +379 -379
  389. package/test/lang.test.js +98 -98
  390. package/test/lastrunat-persist.test.js +101 -101
  391. package/test/lib-smoke.test.js +116 -116
  392. package/test/llm-audit.test.js +815 -815
  393. package/test/llm-message-source.test.js +207 -207
  394. package/test/local-embedder.test.js +292 -292
  395. package/test/maintenance.test.js +501 -501
  396. package/test/mcp-stdio.test.js +385 -385
  397. package/test/mint-source.test.js +311 -311
  398. package/test/mirror-dirty.test.js +424 -424
  399. package/test/mirror-edit-digest.test.js +187 -187
  400. package/test/mirror-generation.test.js +499 -499
  401. package/test/mirror-types.test.js +208 -208
  402. package/test/mirror.test.js +249 -249
  403. package/test/narrative-bars.test.js +162 -162
  404. package/test/organize-tool.test.js +232 -232
  405. package/test/organize.test.js +418 -418
  406. package/test/panel-chart-scale.test.js +90 -90
  407. package/test/panel-tokens.test.js +245 -245
  408. package/test/peak-hours-observability.test.js +70 -70
  409. package/test/peer-blockers.test.js +193 -193
  410. package/test/persona-addressing.test.js +83 -83
  411. package/test/persona-api.test.js +172 -172
  412. package/test/persona-by-session.test.js +568 -568
  413. package/test/persona-flag.test.js +36 -36
  414. package/test/persona-inject.test.js +145 -145
  415. package/test/persona-settings.test.js +198 -198
  416. package/test/pipeline-activities.test.js +155 -155
  417. package/test/pipeline-attribution.test.js +175 -175
  418. package/test/pipeline-command.test.js +243 -243
  419. package/test/pipeline-confirm.test.js +148 -148
  420. package/test/pipeline-exec.test.js +172 -172
  421. package/test/pipeline-nudge-auto.test.js +248 -248
  422. package/test/pipeline-nudge.test.js +1278 -1278
  423. package/test/pipeline-observations.test.js +157 -157
  424. package/test/pipeline-observe.test.js +127 -127
  425. package/test/pipeline-r1r2.test.js +278 -278
  426. package/test/pipeline-runner.test.js +468 -468
  427. package/test/pipeline-runs.test.js +767 -767
  428. package/test/pipeline.test.js +636 -636
  429. package/test/placement.test.js +36 -36
  430. package/test/policy-epoch.test.js +259 -259
  431. package/test/pre-inject-gate.test.js +444 -444
  432. package/test/profile-extract.test.js +269 -269
  433. package/test/profile-facts.test.js +492 -492
  434. package/test/prompt-stance.test.js +128 -128
  435. package/test/provenance-http.test.js +367 -367
  436. package/test/publish-gate.test.js +272 -272
  437. package/test/quality-backfill.test.js +290 -290
  438. package/test/quality-filter.test.js +295 -295
  439. package/test/readme-claims.test.js +113 -113
  440. package/test/reasoning-effort.test.js +752 -752
  441. package/test/recall-anti-update.test.js +82 -82
  442. package/test/recall-evals.test.js +253 -253
  443. package/test/recall-layer.test.js +326 -326
  444. package/test/recall-retention.test.js +128 -128
  445. package/test/recall-stats.test.js +274 -274
  446. package/test/receipt-chain.test.js +451 -451
  447. package/test/reclaim-http.test.js +209 -209
  448. package/test/reflection-update-gate.test.js +129 -129
  449. package/test/reflection.test.js +226 -226
  450. package/test/reindex-backfill.test.js +475 -475
  451. package/test/reindex-scheduler.test.js +369 -369
  452. package/test/release-docs.test.js +156 -156
  453. package/test/release-notes.test.js +265 -265
  454. package/test/release-workflow.test.js +93 -93
  455. package/test/reranker-single-logit.test.js +73 -73
  456. package/test/reranker.test.js +197 -197
  457. package/test/resident-status.test.js +83 -83
  458. package/test/route-policy.test.js +113 -113
  459. package/test/runtime-adopt.test.js +206 -206
  460. package/test/runtime-cli.test.js +89 -89
  461. package/test/runtime-closure.test.js +193 -193
  462. package/test/runtime-download.test.js +302 -302
  463. package/test/runtime-layout.test.js +163 -163
  464. package/test/runtime-loader.test.js +255 -255
  465. package/test/runtime-manifest.test.js +159 -159
  466. package/test/runtime-model-fetch.test.js +348 -348
  467. package/test/runtime-provision.test.js +101 -101
  468. package/test/runtime-tarball.test.js +81 -81
  469. package/test/runtime-verify.test.js +319 -319
  470. package/test/scope-candidates.test.js +491 -491
  471. package/test/scope-distill-stamp.test.js +134 -134
  472. package/test/scope-explicit.test.js +452 -452
  473. package/test/scope-inject-soft.test.js +106 -106
  474. package/test/scope-optional-service.test.js +110 -110
  475. package/test/scope-retrieval.test.js +197 -197
  476. package/test/scope-state.test.js +610 -610
  477. package/test/scope-storage.test.js +255 -255
  478. package/test/scope-strict.test.js +367 -367
  479. package/test/search-fusion.test.js +90 -90
  480. package/test/security-doc.test.js +188 -188
  481. package/test/semantic.test.js +124 -124
  482. package/test/sensitive-scan.test.js +251 -251
  483. package/test/serve-bin.test.js +121 -121
  484. package/test/serve.test.js +258 -258
  485. package/test/service-search.test.js +174 -174
  486. package/test/service.test.js +410 -410
  487. package/test/session-gate.test.js +264 -264
  488. package/test/session-usage.test.js +317 -317
  489. package/test/settings.test.js +273 -273
  490. package/test/share-foreign-rows.test.js +144 -144
  491. package/test/share-http.test.js +144 -144
  492. package/test/share-import.test.js +577 -577
  493. package/test/share-imported-guard.test.js +311 -311
  494. package/test/share-imported-identity.test.js +111 -111
  495. package/test/share-pack.test.js +511 -511
  496. package/test/share-receive-http.test.js +756 -756
  497. package/test/share-routes.test.js +337 -337
  498. package/test/share-store.test.js +133 -133
  499. package/test/share-validate.test.js +101 -101
  500. package/test/skip-invalid-restore.test.js +262 -262
  501. package/test/sleep-action-set.test.js +441 -441
  502. package/test/sleep-catchup.test.js +315 -315
  503. package/test/sleep-failure-backoff.test.js +156 -156
  504. package/test/sleep-heat.test.js +174 -174
  505. package/test/sleep-scheduler-rearm.test.js +89 -89
  506. package/test/sleep.test.js +1455 -1455
  507. package/test/standalone-api.test.js +1344 -1344
  508. package/test/store-vector-cache.test.js +98 -98
  509. package/test/store-write-path-columns.test.js +191 -191
  510. package/test/store.test.js +744 -744
  511. package/test/stress.test.js +209 -209
  512. package/test/summarize-occurred-at.test.js +127 -127
  513. package/test/summarize-parse-diagnostics.test.js +75 -75
  514. package/test/summarize-reasoning-effort.test.js +189 -189
  515. package/test/summarize.test.js +2251 -2251
  516. package/test/supersede-state-tracking.test.js +394 -394
  517. package/test/test-count-sync.test.js +71 -71
  518. package/test/tool-state-ignore.test.js +65 -65
  519. package/test/tools-dto-schema-matrix.test.js +398 -398
  520. package/test/tools-memory-get-schema.test.js +65 -65
  521. package/test/tools.test.js +585 -585
  522. package/test/updated-at-semantics.test.js +113 -113
  523. package/test/user-settings-preview.test.js +127 -127
  524. package/test/user-settings-segments.test.js +527 -527
  525. package/test/vector-index.test.js +334 -334
  526. package/test/version-check.test.js +152 -152
  527. package/test/webServer-optional.test.js +82 -82
  528. package/test/wiki-api.test.js +734 -734
  529. package/test/wiki-claims.test.js +194 -194
  530. package/test/wiki-cli-hint.test.js +118 -118
  531. package/test/wiki-inject.test.js +265 -265
  532. package/test/wiki-partition.test.js +112 -112
  533. package/test/wiki-plan.test.js +80 -80
  534. package/test/wiki-provenance.test.js +202 -202
  535. package/test/wiki-queries.test.js +398 -398
  536. package/test/wiki-scan.test.js +208 -208
  537. package/test/wiki-store.test.js +190 -190
  538. package/test/wiki-t0.test.js +456 -456
  539. package/test/wiki-t1-jobs.test.js +125 -125
  540. package/test/wiki-t1.test.js +460 -460
  541. package/test/wiki-tools.test.js +176 -176
  542. package/test/workflow-guards.test.js +155 -155
  543. package/test/write-admission.test.js +668 -668
@@ -1,437 +1,437 @@
1
- // CLI `share` 的端到端覆盖:真 spawn `bin/cli.mjs`,打到一个顶替 Bearer 数据面的假 HTTP
2
- // 服务上(127.0.0.1、端口 0,不要求本机开着 DSH,也不出环回)。
3
- //
4
- // 为什么要有这个文件:`cmdShare`/`printShare`/`expandHome` 约 78 行在 A7 里只被一次性探针
5
- // 手工跑过(task-A7-report §5 的 concerned 第 1 条),bin/cli.mjs 对外只 export parseArgv
6
- // 那几个解析件,命令本体从 `node --test` 够不着。codecov 看的是补丁覆盖率,这条路径没有
7
- // 锁就等于「下一个改动它的人只能再手跑一遍」。
8
- //
9
- // 为什么假服务不自己编响应体:答复由 **出货的那一层**生成(parseShareBody + shareReply,
10
- // 就是 api-standalone.js 里挂 /share 的同一对函数),rows 是假的、协议是真的。于是
11
- // `kind` / `too-large` / `hint` / `bytes` 这些字段一漂移,这里当场红 —— 手写字面量的假服务
12
- // 只会把「测试自己编的形状」锁成契约。数据面自身的口径另有 share-routes.test.js 与
13
- // share-http.test.js 两处锁,本文件只管 CLI 这张嘴。
14
- //
15
- // spawn 必须异步:服务跑在本进程里,spawnSync 会堵住事件循环,CLI 只能等到自己 10s 超时
16
- // (maintenance.test.js:23 那条教训,同一个坑不再踩一次)。
17
- import test from "node:test";
18
- import assert from "node:assert/strict";
19
- import { spawn } from "node:child_process";
20
- import { createServer } from "node:http";
21
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
22
- import { tmpdir } from "node:os";
23
- import { dirname, join, resolve } from "node:path";
24
- import { fileURLToPath } from "node:url";
25
- import { parseShareBody, shareReply } from "../src/share-http.js";
26
-
27
- const CLI = resolve(dirname(fileURLToPath(import.meta.url)), "..", "bin", "cli.mjs");
28
- const TOKEN = "cli-share-test-token";
29
- const PACKAGE_VERSION = "0.9.4";
30
- // 打包侧拿来遮正文里 home 前缀的入参(share.js 的 createHomeRedactor),跟 os.homedir() 无关:
31
- // 子进程的 HOME 被下面的 scratch 顶掉了,两者刻意不是同一个目录。
32
- const SOURCE_HOME = "/fake/home/alex";
33
- const NOW = () => new Date("2026-10-22T00:00:00.000Z");
34
-
35
- const row = (over) => ({
36
- id: "r", type: "decision", title: "t", content: "c", tags: [], importance: 3,
37
- archived: 0, forgotten: 0, created_at: "2026-10-01T00:00:00.000Z",
38
- updated_at: "2026-10-01T00:00:00.000Z", content_hash: "h", ...over
39
- });
40
-
41
- // 两条进包 + 一条归档(archived)+ 一条 summary(excluded_type):跳过清单里两种原因都要
42
- // 有实物,printShare 那六档命名才不会退化成「只测了 happy path 的计数器」。
43
- const THIN_ROWS = [
44
- row({ id: "1", type: "decision", title: "选 SQLite", content: "存储层用 node:sqlite", tags: ["存储"], importance: 4 }),
45
- row({ id: "2", type: "preference", title: "浅色主题", content: "界面默认浅色" }),
46
- row({ id: "3", type: "project", title: "归档项", content: "旧决定", archived: 1 }),
47
- row({ id: "4", type: "summary", title: "会话摘要", content: "摘出来的正文" })
48
- ];
49
-
50
- // 413 只由**最终序列化字节**决定,水位 MAX_PACK_BYTES = 3 MiB 是硬编码、没有注入缝,所以这
51
- // 一条只能靠体积本身撞上去(1.2M 个中文字 ≈ 3.4 MB UTF-8)。假服务不自己回 413。
52
- const FAT_ROWS = [row({ id: "f", title: "长正文", content: "字".repeat(1_200_000) })];
53
-
54
- /** 顶替数据面的假服务:认 POST /share、验 Bearer,其余按 api-standalone 的口径回错码。 */
55
- async function startPlane() {
56
- const state = { rows: THIN_ROWS, requests: [], lastStatus: null, lastBodyText: "" };
57
- const server = createServer((req, res) => {
58
- const chunks = [];
59
- req.on("data", (c) => chunks.push(c));
60
- req.on("end", () => {
61
- const text = Buffer.concat(chunks).toString("utf8");
62
- const { pathname } = new URL(req.url, "http://127.0.0.1");
63
- state.requests.push({ method: req.method, pathname, auth: req.headers.authorization ?? "", body: text });
64
- let out;
65
- try {
66
- if (req.method !== "POST" || pathname !== "/share") out = { status: 404, body: { error: "not-found" } };
67
- else if (req.headers.authorization !== `Bearer ${TOKEN}`) out = { status: 401, body: { error: "unauthorized" } };
68
- else {
69
- const parsed = parseShareBody(text);
70
- out = parsed.ok
71
- ? shareReply({ service: { all: () => state.rows }, packageVersion: PACKAGE_VERSION, homeDir: SOURCE_HOME, now: NOW, options: parsed.options })
72
- : { status: 400, body: { error: parsed.error } };
73
- }
74
- } catch (err) {
75
- // 漏这一兜,异常就顺着 socket 变成「一个字节都没回」,现场只剩 CLI 的 10s 超时。
76
- out = { status: 500, body: { error: "fixture-throw", detail: String(err) } };
77
- }
78
- state.lastStatus = out.status;
79
- state.lastBodyText = JSON.stringify(out.body);
80
- res.writeHead(out.status, { "content-type": "application/json" });
81
- res.end(state.lastBodyText);
82
- });
83
- });
84
- await new Promise((r) => server.listen(0, "127.0.0.1", r));
85
- const port = server.address().port;
86
- return {
87
- url: `http://127.0.0.1:${port}`,
88
- state,
89
- overLimit() { state.rows = FAT_ROWS; },
90
- // 终端那格要看得懂「标题为什么是空的」,得有一批含凭据的被跳过行;默认那批
91
- // THIN_ROWS 被好几条用例共用作计数基准,不能为了这一格改它。
92
- use(rows) { state.rows = rows; },
93
- close: () => new Promise((r) => { server.closeIdleConnections?.(); server.close(r); })
94
- };
95
- }
96
-
97
- /**
98
- * 每个用例一套临时目录:`home` 顶替子进程的 HOME(tilde 展开与 cli.json 都落在这里),
99
- * `out` 是产物目录 —— 「干跑不许写文件」就断言这个目录空。
100
- */
101
- function scratch() {
102
- const root = mkdtempSync(join(tmpdir(), "mneme-cli-share-"));
103
- const home = join(root, "home");
104
- const out = join(root, "out");
105
- mkdirSync(home, { recursive: true });
106
- mkdirSync(out, { recursive: true });
107
- return {
108
- home, out,
109
- cleanup() {
110
- try { rmSync(root, { recursive: true, force: true }); } catch { /* win 偶发文件锁,残留临时目录无害 */ }
111
- }
112
- };
113
- }
114
-
115
- async function withPlane(fn) {
116
- const plane = await startPlane();
117
- const s = scratch();
118
- try {
119
- return await fn({
120
- plane, home: s.home, out: s.out, url: plane.url, token: TOKEN,
121
- auth: ["--url", plane.url, "--token", TOKEN]
122
- });
123
- } finally {
124
- s.cleanup();
125
- await plane.close();
126
- }
127
- }
128
-
129
- /**
130
- * HOME/USERPROFILE 一起改:os.homedir() 先读 HOME、Windows 再读 USERPROFILE,子进程里的
131
- * `~` 与 `~\` 才都展开进临时目录(断言失败时也不会有产物落进真人 home)。
132
- * 同时把 cwd 放到临时目录:本机那份 ~/.dsh-mneme/cli.json 不参与,产物也只进 scratch。
133
- *
134
- * DSH_MNEME_URL/TOKEN 给的是 **fixture 那一面**,不是清空。清空会顺着
135
- * `firstNonEmpty(flag, env, file) ?? DEFAULT_URL` 落到 `DEFAULT_URL = 127.0.0.1:8790`,
136
- * 那正是她本机活实例的默认端口:今天每个调用点都显式带 --url/--token 所以打不到,但那是
137
- * 「暂时没漏」而不是「漏不到」。指到 fixture 之后,未来少带一对旗标的调用点打的是本测试
138
- * 自己起的那面;那种错会变成断言当场红,而不是拿她的活库跑测试、还因为只读路由回 401
139
- * 而表现成「这条用例偶尔红」。
140
- */
141
- function runCli(ctx, args, extraEnv = {}) {
142
- return new Promise((res, rej) => {
143
- const child = spawn(process.execPath, [CLI, ...args], {
144
- cwd: ctx.out,
145
- env: { ...process.env, HOME: ctx.home, USERPROFILE: ctx.home, DSH_MNEME_URL: ctx.url, DSH_MNEME_TOKEN: ctx.token, ...extraEnv },
146
- stdio: ["ignore", "pipe", "pipe"]
147
- });
148
- let stdout = "";
149
- let stderr = "";
150
- child.stdout.on("data", (d) => { stdout += d; });
151
- child.stderr.on("data", (d) => { stderr += d; });
152
- child.on("error", rej);
153
- child.on("exit", (code) => res({ code, stdout, stderr }));
154
- });
155
- }
156
-
157
- const listFiles = (dir) => readdirSync(dir).sort();
158
-
159
- test("CLI share --dry-run:报数并解释没写文件,输出目录一个字节不多", { timeout: 60000 }, async () => {
160
- await withPlane(async (ctx) => {
161
- const dry = await runCli(ctx, ["share", "--dry-run", ...ctx.auth]);
162
- assert.equal(dry.code, 0, dry.stderr);
163
- assert.match(dry.stdout, /^干跑报告: 2 条$/m);
164
- assert.match(dry.stdout, /跳过: 归档 1, 不随包走 1/);
165
- assert.match(dry.stdout, /- \[archived\] 归档项/);
166
- assert.match(dry.stdout, /- \[excluded_type\] 会话摘要/);
167
- assert.match(dry.stdout, /没写文件:加 --out <路径> 才会落盘/);
168
- assert.ok(!dry.stdout.includes("已写出"), "干跑不许报落盘");
169
- assert.deepEqual(listFiles(ctx.out), [], "干跑不许产出任何文件");
170
-
171
- // 同时给 --dry-run 与 --out:偏好干跑,那个路径也不许被创建(cmdShare 的 `if (dryRun) return`
172
- // 排在写盘之前)。这一条锁的是「安全的那一侧」:脚本以为有文件会拿到 none,但不能反过来
173
- // ——带了 --dry-run 却动了盘。
174
- const both = join(ctx.out, "must-not-exist.json");
175
- const combo = await runCli(ctx, ["share", "--dry-run", "--out", both, ...ctx.auth]);
176
- assert.equal(combo.code, 0, combo.stderr);
177
- assert.ok(!existsSync(both), "--dry-run 在场时 --out 不得落盘");
178
- assert.deepEqual(listFiles(ctx.out), []);
179
- });
180
- });
181
-
182
- test("漏传 --url/--token 的调用点打到的是 fixture 那一面,不是她本机 8790", { timeout: 60000 }, async () => {
183
- await withPlane(async (ctx) => {
184
- // 这条锁的是 runCli 那份 env,不是 CLI 的行为:DSH_MNEME_URL/TOKEN 给的是本测试自己
185
- // 起的端口,所以「未来某个调用点忘了带旗标」落在这里是当场可看见的一次请求,而不是
186
- // 悄悄打到 `DEFAULT_URL = 127.0.0.1:8790`(她本机活实例的默认端口,只读面回 401、
187
- // 老构建回 404,表现成「这条用例偶尔红」)。清空那两个变量才是这里要防的回归。
188
- const before = ctx.plane.state.requests.length;
189
- const run = await runCli(ctx, ["share", "--dry-run"]);
190
- assert.equal(run.code, 0, run.stderr);
191
- assert.equal(ctx.plane.state.requests.length, before + 1, "请求要落到 fixture 这一面");
192
- assert.deepEqual(listFiles(ctx.out), []);
193
- });
194
- });
195
-
196
- test("CLI 的跳过清单:含凭据那行不给标题,终端要写得像「有意不给」", { timeout: 60000 }, async () => {
197
- await withPlane(async (ctx) => {
198
- // 这条锁的是终端那一格:src/share.js 在源头把命中密钥判据的行的标题留空,skip_list
199
- // 于是只剩原因。空标题直接打出来是一行尾随空格,读的人会当成「这条本来没标题」,
200
- // 而它恰恰是最需要被看见的那一类拒绝。
201
- const secret = "P@ssw0rd-for-prod-db";
202
- ctx.plane.use([
203
- row({ id: "1", type: "project", title: "干净的归档行", content: "没有凭据", archived: 1 }),
204
- row({ id: "2", type: "decision", title: `生产库密码 ${secret} 别丢`, content: `password: ${secret}`, archived: 1 })
205
- ]);
206
- const dry = await runCli(ctx, ["share", "--dry-run", ...ctx.auth]);
207
- assert.equal(dry.code, 0, dry.stderr);
208
- assert.match(dry.stdout, /^干跑报告: 0 条$/m);
209
- assert.match(dry.stdout, /- \[archived\] 干净的归档行/);
210
- assert.match(dry.stdout, /- \[archived\] (标题不外发)/);
211
- // 终端与落盘是同一份 report 的两张嘴:这里也不许出现那个值。
212
- assert.ok(!dry.stdout.includes(secret), "CLI 不许把凭据标题打到终端");
213
- assert.ok(!dry.stderr.includes(secret), "错误面同样不许");
214
- });
215
- });
216
-
217
- // 那句「跳过清单已截断」的判据。旧写法拿**清单长度**当证据(`skip_list.length >= 50`),
218
- // 而清单长度是被服务端截过的读数:恰好 50 条被跳时清单是全量的,那句话在此刻撒谎,
219
- // 而 50 这个数在 CLI 里是抄来的第二份口径(水位一改就错)。真证据一直在场:
220
- // skipped 的六个计数走全量。判据因此是「账比清单长」,与面板 ShareReport 同一条
221
- // (lib/client.js 的 totalSkipped > list.length)。
222
- // 两格各咬一个方向,缺一不可:
223
- // ① 恰好 50(清单=账=水位)→ 不许报截断。旧判据在这一格红,本条就是它的行为锁;
224
- // ② 55 条被跳(账 55 > 清单 50)→ 必须报。这一格旧判据也报,防的是「把提示干脆删掉」
225
- // 冒充修复——删掉提示的人只会看见这一条红。
226
- test("CLI 那句「已截断」由六个计数之和判定,恰好等于水位时不报", { timeout: 60000 }, async () => {
227
- await withPlane(async (ctx) => {
228
- // 水位住在服务端(src/share.js 的 SHARE_SKIP_LIST_MAX),这里只从它自己的答复里读,
229
- // 不在测试里再抄一个 50:抄了就又是两份口径,测试会跟着水位一起漂。
230
- const shareText = readFileSync(resolve(dirname(CLI), "..", "src", "share.js"), "utf8");
231
- const cap = Number(shareText.match(/SHARE_SKIP_LIST_MAX = (\d+)/)?.[1]);
232
- assert.ok(Number.isInteger(cap) && cap > 0, "src/share.js 的水位要读得出来");
233
- const archivedRows = (n, tag) => Array.from({ length: n }, (_, i) =>
234
- row({ id: `${tag}${i}`, type: "decision", title: `归档行 ${tag}${i}`, content: `正文 ${tag}${i}`, archived: 1 }));
235
-
236
- // ①:恰好到水位——清单全量,没有东西被截掉。
237
- ctx.plane.use(archivedRows(cap, "a"));
238
- const atCap = await runCli(ctx, ["share", "--dry-run", ...ctx.auth]);
239
- assert.equal(atCap.code, 0, atCap.stderr);
240
- assert.match(atCap.stdout, new RegExp(`跳过: 归档 ${cap}`), "计数要走全量");
241
- assert.ok(!atCap.stdout.includes("跳过清单已截断"),
242
- `清单长度恰好等于水位时不许报截断,got: ${atCap.stdout}`);
243
-
244
- // ②:账比清单长——这一刻清单确实被截了,必须说。
245
- ctx.plane.use(archivedRows(cap + 5, "b"));
246
- const overCap = await runCli(ctx, ["share", "--dry-run", ...ctx.auth]);
247
- assert.equal(overCap.code, 0, overCap.stderr);
248
- assert.match(overCap.stdout, new RegExp(`跳过: 归档 ${cap + 5}`), "计数仍是全量");
249
- assert.match(overCap.stdout, /跳过清单已截断,计数仍为全量/, "账比清单长时这句提示必须在");
250
- });
251
- });
252
-
253
- test("CLI 报得出包内折叠:included 少的那几条要有去处", { timeout: 60000 }, async () => {
254
- await withPlane(async (ctx) => {
255
- // 同标题、同正文、同内容锚而 scope 不同的两条在库里是两条合法记忆,在包里共用一把
256
- // external_id,于是出包侧折成一条。终端只报 `干跑报告: 1 条` 的话,读的人以为自己
257
- // 少发了 —— 这一格要说清那条去哪了(src/share.js 的 report.deduped)。
258
- ctx.plane.use([
259
- row({ id: "1", type: "decision", title: "同一个标题", content: "同一份正文", content_hash: "same-hash", agent_scope: "preset-a" }),
260
- row({ id: "2", type: "decision", title: "同一个标题", content: "同一份正文", content_hash: "same-hash", agent_scope: "preset-b" })
261
- ]);
262
- const dry = await runCli(ctx, ["share", "--dry-run", ...ctx.auth]);
263
- assert.equal(dry.code, 0, dry.stderr);
264
- assert.match(dry.stdout, /^干跑报告: 1 条$/m);
265
- assert.match(dry.stdout, /包内重复已折叠: 1 条/);
266
- assert.match(dry.stdout, /跳过: 无/, "折叠不走跳过那本账");
267
- assert.deepEqual(JSON.parse(ctx.plane.state.lastBodyText).report.skip_list, []);
268
- });
269
- });
270
-
271
- test("CLI share --out:数据面答复逐字落盘,解析回来 kind 与 report 都对", { timeout: 60000 }, async () => {
272
- await withPlane(async (ctx) => {
273
- const dest = join(ctx.out, "pack.json");
274
- const run = await runCli(ctx, ["share", "--out", dest, ...ctx.auth]);
275
- assert.equal(run.code, 0, run.stderr);
276
- assert.match(run.stdout, /^分享包: 2 条$/m);
277
- assert.ok(run.stdout.includes(`已写出: ${dest}`), run.stdout);
278
- assert.equal(ctx.plane.state.requests.at(-1).auth, `Bearer ${TOKEN}`, "Bearer 头得真的带上,否则这条只是本地写文件");
279
-
280
- const text = readFileSync(dest, "utf8");
281
- // 逐字相同:CLI 只做 JSON.parse → JSON.stringify 的往返,不许顺手美化、加换行或改键序
282
- // ——这份文件是要原样 POST 给对面的(车道 C),多一个空格对面重算的锚就对不上。
283
- assert.equal(text, ctx.plane.state.lastBodyText);
284
- const pack = JSON.parse(text);
285
- assert.equal(pack.kind, "mneme.share/v1");
286
- assert.equal(pack.report.included, 2);
287
- assert.equal(pack.alias, "", "没给 --alias 时包里是空串,不是 undefined");
288
- });
289
- });
290
-
291
- test("CLI share 不带 --out 也不带 --dry-run:先解释再退出,一个请求都不发", { timeout: 60000 }, async () => {
292
- await withPlane(async (ctx) => {
293
- const run = await runCli(ctx, ["share", ...ctx.auth]);
294
- assert.notEqual(run.code, 0, "整库答复直接打 stdout 会把报告淹掉,这条必须拒");
295
- assert.match(run.stderr, /要么写文件(--out <路径>),要么只看报告(--dry-run)/);
296
- assert.ok(run.stderr.includes("dsh-mneme share --out ~/Desktop/pack.json"),
297
- `拒绝里要带一条能照抄的例子,got: ${run.stderr}`);
298
- assert.deepEqual(ctx.plane.state.requests, [], "拒绝发生在请求之前:不许先打一轮再拒");
299
- assert.deepEqual(listFiles(ctx.out), [], "拒绝路径不留文件");
300
- });
301
- });
302
-
303
- test("CLI share --out 的 `~` 前缀:展开到子进程 home(PowerShell/cmd 不会替她展开)", { timeout: 60000 }, async () => {
304
- await withPlane(async (ctx) => {
305
- // Git Bash 会展开 `~/…`,PowerShell 与 cmd 给到 CLI 的是字面量 —— USAGE 里的例子正是
306
- // 这一串,所以 expandHome 是那个例子在 Windows 上能不能用的全部区别。
307
- const NAMES = ["dsh-mneme-cli-share-tilde-probe.json", "dsh-mneme-cli-share-tilde-probe-win.json"];
308
- const tilde = await runCli(ctx, ["share", "--out", `~/${NAMES[0]}`, ...ctx.auth]);
309
- assert.equal(tilde.code, 0, `tilde 展开失败就是 writeFileSync 的 ENOENT 堆栈:${tilde.stderr}`);
310
- assert.ok(existsSync(join(ctx.home, NAMES[0])), `${NAMES[0]} 应落在子进程 home 里,got: ${tilde.stdout}`);
311
- assert.ok(tilde.stdout.includes(NAMES[0]) && !tilde.stdout.includes(`~/${NAMES[0]}`),
312
- `打印的应是展开后的路径,got: ${tilde.stdout}`);
313
-
314
- const backslash = await runCli(ctx, ["share", "--out", `~\\${NAMES[1]}`, ...ctx.auth]);
315
- assert.equal(backslash.code, 0, backslash.stderr);
316
- assert.ok(existsSync(join(ctx.home, NAMES[1])), `${NAMES[1]}:反斜杠形态同一条规则(expandHome 认 ~\\)`);
317
-
318
- // 绝对路径不受影响:expandHome 只改 `~`/`~/`/`~\` 开头,别的原样交给 writeFileSync。
319
- const abs = join(ctx.out, "abs.json");
320
- const plain = await runCli(ctx, ["share", "--out", abs, ...ctx.auth]);
321
- assert.equal(plain.code, 0, plain.stderr);
322
- assert.ok(existsSync(abs) && !existsSync(join(ctx.home, "abs.json")));
323
- // 这里**不**留「万一 HOME 覆盖没生效就去真人 home 里把这两个名字擦掉」的兜底:那个
324
- // rmSync 的靶子是 os.homedir(),只有在测试本来就该红(覆盖失效)的情况下才会动她家目录,
325
- // 而它删的是「同名」不是「本测试刚写的那个 inode」。HOME 覆盖有没有生效由上面两条
326
- // existsSync(join(ctx.home, …)) 负责——红了就停手,产物留在临时 home 里等她自己看。
327
- });
328
- });
329
-
330
- test("CLI share 撞 413:拒答体自带的 hint 与两个字节数走 failHttp 转出来", { timeout: 60000 }, async () => {
331
- await withPlane(async (ctx) => {
332
- ctx.plane.overLimit();
333
- const dest = join(ctx.out, "never.json");
334
- const run = await runCli(ctx, ["share", "--out", dest, ...ctx.auth]);
335
- assert.equal(run.code, 1);
336
- assert.equal(ctx.plane.state.lastStatus, 413, "体积要真撞上去,假服务不许自己编状态码");
337
- assert.match(run.stderr, /请求失败(HTTP 413):too-large \[\/share\]/);
338
- // 56f7b06 修的就是这两句:旧 failHttp 只取 data.error,「大了多少」「怎么分批」留在体里
339
- // 没人看得见 —— 最需要下一步动作的那次拒绝,反而是最没信息量的那次。
340
- assert.match(run.stderr, /用 --type 或 --tag 分批/);
341
- assert.match(run.stderr, /量到 \d+(\.\d)? M?B,水位 3\.0 MB/);
342
- assert.ok(!existsSync(dest), "拒绝路径不许留半个包");
343
- });
344
- });
345
-
346
- test("CLI share --json 撞 413:stdout 保持干净,整份拒答体(含 report)走 stderr", { timeout: 60000 }, async () => {
347
- await withPlane(async (ctx) => {
348
- ctx.plane.overLimit();
349
- const run = await runCli(ctx, ["share", "--json", "--out", join(ctx.out, "never.json"), ...ctx.auth]);
350
- assert.equal(run.code, 1);
351
- // --json 的约定:stdout 只有那一份 JSON。拒答走 fail → console.error(rawJson),
352
- // 所以 stdout 是空的,而 stderr 是可以直接 parse 的原始响应体。
353
- assert.equal(run.stdout.trim(), "", `--json 的 stdout 不许掺解释行:${run.stdout}`);
354
- const body = JSON.parse(run.stderr);
355
- assert.equal(body.error, "too-large");
356
- assert.ok(body.bytes > body.limitBytes, "两个字节数都在体里,出口才有「大了多少」可说");
357
- assert.match(body.hint, /分批/);
358
- assert.equal(body.report.included, 1, "完整报告走 --json:分批的依据就是这份数");
359
- assert.ok(!("memories" in body), "413 不许把刚拒绝掉的几 MB 再发一遍");
360
- });
361
- });
362
-
363
- test("CLI share --json 成功答复:stdout 是一份能 parse 的 JSON,尾巴不许掺「已写出」", { timeout: 60000 }, async () => {
364
- await withPlane(async (ctx) => {
365
- const dest = join(ctx.out, "json-pack.json");
366
- const written = await runCli(ctx, ["share", "--out", dest, "--json", ...ctx.auth]);
367
- assert.equal(written.code, 0, written.stderr);
368
- // A7 报告里实测过的那次 corruption:尾部追一行 已写出 就撞
369
- // SyntaxError: Unexpected non-whitespace character after JSON(jq 同理)。
370
- const parsed = JSON.parse(written.stdout);
371
- assert.deepEqual(Object.keys(parsed).sort(), ["file", "report"]);
372
- assert.equal(parsed.file, dest, "落盘路径已在 payload 里,那行提示只对人有意义");
373
- assert.equal(parsed.report.included, 2);
374
- assert.ok(!written.stdout.includes("已写出"));
375
- assert.ok(existsSync(dest), "--json 不改变落盘这件事");
376
-
377
- const dry = await runCli(ctx, ["share", "--dry-run", "--json", ...ctx.auth]);
378
- assert.equal(dry.code, 0, dry.stderr);
379
- const dryParsed = JSON.parse(dry.stdout);
380
- assert.deepEqual(Object.keys(dryParsed).sort(), ["pack", "report"]);
381
- assert.equal(dryParsed.pack.kind, "mneme.share/v1");
382
- assert.equal(dryParsed.report.included, 2);
383
- });
384
- });
385
-
386
- test("CLI share --type/--alias/--tag:按数据面认得的形状上线", { timeout: 60000 }, async () => {
387
- await withPlane(async (ctx) => {
388
- const run = await runCli(ctx, [
389
- "share", "--type", "decision,preference", "--alias", "桉桉的库", "--tag", "存储", "--dry-run", ...ctx.auth
390
- ]);
391
- assert.equal(run.code, 0, run.stderr);
392
- assert.deepEqual(JSON.parse(ctx.plane.state.requests.at(-1).body), {
393
- type: ["decision", "preference"], alias: "桉桉的库", tag: "存储"
394
- }, "--type 是逗号列表 → 数组;空旗标不该冒出键");
395
- // 收窄是真的生效(不是发出去没人读):两条里只有带 存储 标签那条进包。
396
- assert.match(run.stdout, /^干跑报告: 1 条 来自 桉桉的库$/m);
397
- assert.deepEqual(listFiles(ctx.out), []);
398
- });
399
- });
400
-
401
- // CLI 与 src/share.js 之间没有 import 关系(bin/cli.mjs 是零依赖 HTTP 客户端,那条理由
402
- // 写在它自己的注释里),所以「六个跳过原因键」在 CLI 侧是抄过去的字面量。抄的东西不会
403
- // 自己报错:share.js 改一个键名,CLI 就把那一档读成 0(跳过原因在终端上凭空少一类)。
404
- // 这条静态锁是两份文本之间唯一的接缝——它不重跑逻辑,只比对字面量集合。
405
- //
406
- // 「与 50 的截断数同源」那一半是**换掉**而不是删掉:判据从「清单长度到 50」改成「六个
407
- // 计数之和 > 清单长度」之后,CLI 侧不再有那个数,水位只住在 src/share.js 一处(面板侧
408
- // lib/client.js 的 ShareReport 同一条取舍,那里也从不写 50)。没有抄本,就没有可比对的
409
- // 接缝——所以这里改锁两件方向相反的事:
410
- // ① CLI 不许再出现截断阈值:`).length >= 数字` 那个形状本身就是「恰好一水位就误报」的
411
- // 旧缺陷,数值对不对这件事永远锁不住,只能靠不出现;
412
- // ② 那句提示必须由「账比清单长」判定。
413
- // 判据真不真由上面那条行为用例真跑 CLI 证明(恰好到水位不报、超水位才报),这里只锁形状。
414
- test("CLI 抄过去的原因键跟 src/share.js 同源,且不硬写截断水位", () => {
415
- const shareText = readFileSync(resolve(dirname(CLI), "..", "src", "share.js"), "utf8");
416
- const cliText = readFileSync(CLI, "utf8");
417
-
418
- const skippedBlock = shareText.match(/const skipped = \{([^}]*)\}/)?.[1] ?? "";
419
- const shareKeys = [...skippedBlock.matchAll(/([a-z_]+):\s*0/g)].map((m) => m[1]);
420
- const namedBlock = cliText.match(/const named = \[([\s\S]*?)\];/)?.[1] ?? "";
421
- const cliKeys = [...namedBlock.matchAll(/\['([a-z_]+)'/g)].map((m) => m[1]);
422
- // 两条正则都必须真的抓到东西,否则这个用例退化成永真(同本文件对假服务的口径)。
423
- assert.equal(shareKeys.length, 6, `share.js 的 skipped 应有六个键,抓到 ${shareKeys.length} 个:${shareKeys}`);
424
- assert.equal(cliKeys.length, 6, `cli.mjs 的 named 应有六档,抓到 ${cliKeys.length} 个:${cliKeys}`);
425
- assert.deepEqual(cliKeys, shareKeys, "CLI 的档位名必须与 report.skipped 的键逐字同序");
426
-
427
- // ①:那六个键在「求和那一行」也得原样出现一次——判据读的是同一本账,不是第七份口径。
428
- const censusBlock = cliText.match(/const skippedTotal = \[([\s\S]*?)\]/)?.[1] ?? "";
429
- const censusKeys = [...censusBlock.matchAll(/'([a-z_]+)'/g)].map((m) => m[1]);
430
- assert.deepEqual(censusKeys, shareKeys, "截断判据的六个计数键必须与 report.skipped 同序同源");
431
- assert.ok(/\)\.length\s*>=\s*\d+/.test(cliText) === false,
432
- "cli.mjs 不许再硬写截断水位:那个数字只住在 src/share.js 的 SHARE_SKIP_LIST_MAX");
433
-
434
- // ②:提示由「账比清单长」判定,与面板同一条判据。
435
- assert.ok(cliText.includes("skippedTotal > (r.skip_list ?? []).length)"),
436
- "那句「跳过清单已截断」必须由六个计数之和与清单长度之比来判定");
437
- });
1
+ // CLI `share` 的端到端覆盖:真 spawn `bin/cli.mjs`,打到一个顶替 Bearer 数据面的假 HTTP
2
+ // 服务上(127.0.0.1、端口 0,不要求本机开着 DSH,也不出环回)。
3
+ //
4
+ // 为什么要有这个文件:`cmdShare`/`printShare`/`expandHome` 约 78 行在 A7 里只被一次性探针
5
+ // 手工跑过(task-A7-report §5 的 concerned 第 1 条),bin/cli.mjs 对外只 export parseArgv
6
+ // 那几个解析件,命令本体从 `node --test` 够不着。codecov 看的是补丁覆盖率,这条路径没有
7
+ // 锁就等于「下一个改动它的人只能再手跑一遍」。
8
+ //
9
+ // 为什么假服务不自己编响应体:答复由 **出货的那一层**生成(parseShareBody + shareReply,
10
+ // 就是 api-standalone.js 里挂 /share 的同一对函数),rows 是假的、协议是真的。于是
11
+ // `kind` / `too-large` / `hint` / `bytes` 这些字段一漂移,这里当场红 —— 手写字面量的假服务
12
+ // 只会把「测试自己编的形状」锁成契约。数据面自身的口径另有 share-routes.test.js 与
13
+ // share-http.test.js 两处锁,本文件只管 CLI 这张嘴。
14
+ //
15
+ // spawn 必须异步:服务跑在本进程里,spawnSync 会堵住事件循环,CLI 只能等到自己 10s 超时
16
+ // (maintenance.test.js:23 那条教训,同一个坑不再踩一次)。
17
+ import test from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { spawn } from "node:child_process";
20
+ import { createServer } from "node:http";
21
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import { dirname, join, resolve } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { parseShareBody, shareReply } from "../src/share-http.js";
26
+
27
+ const CLI = resolve(dirname(fileURLToPath(import.meta.url)), "..", "bin", "cli.mjs");
28
+ const TOKEN = "cli-share-test-token";
29
+ const PACKAGE_VERSION = "0.9.4";
30
+ // 打包侧拿来遮正文里 home 前缀的入参(share.js 的 createHomeRedactor),跟 os.homedir() 无关:
31
+ // 子进程的 HOME 被下面的 scratch 顶掉了,两者刻意不是同一个目录。
32
+ const SOURCE_HOME = "/fake/home/alex";
33
+ const NOW = () => new Date("2026-10-22T00:00:00.000Z");
34
+
35
+ const row = (over) => ({
36
+ id: "r", type: "decision", title: "t", content: "c", tags: [], importance: 3,
37
+ archived: 0, forgotten: 0, created_at: "2026-10-01T00:00:00.000Z",
38
+ updated_at: "2026-10-01T00:00:00.000Z", content_hash: "h", ...over
39
+ });
40
+
41
+ // 两条进包 + 一条归档(archived)+ 一条 summary(excluded_type):跳过清单里两种原因都要
42
+ // 有实物,printShare 那六档命名才不会退化成「只测了 happy path 的计数器」。
43
+ const THIN_ROWS = [
44
+ row({ id: "1", type: "decision", title: "选 SQLite", content: "存储层用 node:sqlite", tags: ["存储"], importance: 4 }),
45
+ row({ id: "2", type: "preference", title: "浅色主题", content: "界面默认浅色" }),
46
+ row({ id: "3", type: "project", title: "归档项", content: "旧决定", archived: 1 }),
47
+ row({ id: "4", type: "summary", title: "会话摘要", content: "摘出来的正文" })
48
+ ];
49
+
50
+ // 413 只由**最终序列化字节**决定,水位 MAX_PACK_BYTES = 3 MiB 是硬编码、没有注入缝,所以这
51
+ // 一条只能靠体积本身撞上去(1.2M 个中文字 ≈ 3.4 MB UTF-8)。假服务不自己回 413。
52
+ const FAT_ROWS = [row({ id: "f", title: "长正文", content: "字".repeat(1_200_000) })];
53
+
54
+ /** 顶替数据面的假服务:认 POST /share、验 Bearer,其余按 api-standalone 的口径回错码。 */
55
+ async function startPlane() {
56
+ const state = { rows: THIN_ROWS, requests: [], lastStatus: null, lastBodyText: "" };
57
+ const server = createServer((req, res) => {
58
+ const chunks = [];
59
+ req.on("data", (c) => chunks.push(c));
60
+ req.on("end", () => {
61
+ const text = Buffer.concat(chunks).toString("utf8");
62
+ const { pathname } = new URL(req.url, "http://127.0.0.1");
63
+ state.requests.push({ method: req.method, pathname, auth: req.headers.authorization ?? "", body: text });
64
+ let out;
65
+ try {
66
+ if (req.method !== "POST" || pathname !== "/share") out = { status: 404, body: { error: "not-found" } };
67
+ else if (req.headers.authorization !== `Bearer ${TOKEN}`) out = { status: 401, body: { error: "unauthorized" } };
68
+ else {
69
+ const parsed = parseShareBody(text);
70
+ out = parsed.ok
71
+ ? shareReply({ service: { all: () => state.rows }, packageVersion: PACKAGE_VERSION, homeDir: SOURCE_HOME, now: NOW, options: parsed.options })
72
+ : { status: 400, body: { error: parsed.error } };
73
+ }
74
+ } catch (err) {
75
+ // 漏这一兜,异常就顺着 socket 变成「一个字节都没回」,现场只剩 CLI 的 10s 超时。
76
+ out = { status: 500, body: { error: "fixture-throw", detail: String(err) } };
77
+ }
78
+ state.lastStatus = out.status;
79
+ state.lastBodyText = JSON.stringify(out.body);
80
+ res.writeHead(out.status, { "content-type": "application/json" });
81
+ res.end(state.lastBodyText);
82
+ });
83
+ });
84
+ await new Promise((r) => server.listen(0, "127.0.0.1", r));
85
+ const port = server.address().port;
86
+ return {
87
+ url: `http://127.0.0.1:${port}`,
88
+ state,
89
+ overLimit() { state.rows = FAT_ROWS; },
90
+ // 终端那格要看得懂「标题为什么是空的」,得有一批含凭据的被跳过行;默认那批
91
+ // THIN_ROWS 被好几条用例共用作计数基准,不能为了这一格改它。
92
+ use(rows) { state.rows = rows; },
93
+ close: () => new Promise((r) => { server.closeIdleConnections?.(); server.close(r); })
94
+ };
95
+ }
96
+
97
+ /**
98
+ * 每个用例一套临时目录:`home` 顶替子进程的 HOME(tilde 展开与 cli.json 都落在这里),
99
+ * `out` 是产物目录 —— 「干跑不许写文件」就断言这个目录空。
100
+ */
101
+ function scratch() {
102
+ const root = mkdtempSync(join(tmpdir(), "mneme-cli-share-"));
103
+ const home = join(root, "home");
104
+ const out = join(root, "out");
105
+ mkdirSync(home, { recursive: true });
106
+ mkdirSync(out, { recursive: true });
107
+ return {
108
+ home, out,
109
+ cleanup() {
110
+ try { rmSync(root, { recursive: true, force: true }); } catch { /* win 偶发文件锁,残留临时目录无害 */ }
111
+ }
112
+ };
113
+ }
114
+
115
+ async function withPlane(fn) {
116
+ const plane = await startPlane();
117
+ const s = scratch();
118
+ try {
119
+ return await fn({
120
+ plane, home: s.home, out: s.out, url: plane.url, token: TOKEN,
121
+ auth: ["--url", plane.url, "--token", TOKEN]
122
+ });
123
+ } finally {
124
+ s.cleanup();
125
+ await plane.close();
126
+ }
127
+ }
128
+
129
+ /**
130
+ * HOME/USERPROFILE 一起改:os.homedir() 先读 HOME、Windows 再读 USERPROFILE,子进程里的
131
+ * `~` 与 `~\` 才都展开进临时目录(断言失败时也不会有产物落进真人 home)。
132
+ * 同时把 cwd 放到临时目录:本机那份 ~/.dsh-mneme/cli.json 不参与,产物也只进 scratch。
133
+ *
134
+ * DSH_MNEME_URL/TOKEN 给的是 **fixture 那一面**,不是清空。清空会顺着
135
+ * `firstNonEmpty(flag, env, file) ?? DEFAULT_URL` 落到 `DEFAULT_URL = 127.0.0.1:8790`,
136
+ * 那正是她本机活实例的默认端口:今天每个调用点都显式带 --url/--token 所以打不到,但那是
137
+ * 「暂时没漏」而不是「漏不到」。指到 fixture 之后,未来少带一对旗标的调用点打的是本测试
138
+ * 自己起的那面;那种错会变成断言当场红,而不是拿她的活库跑测试、还因为只读路由回 401
139
+ * 而表现成「这条用例偶尔红」。
140
+ */
141
+ function runCli(ctx, args, extraEnv = {}) {
142
+ return new Promise((res, rej) => {
143
+ const child = spawn(process.execPath, [CLI, ...args], {
144
+ cwd: ctx.out,
145
+ env: { ...process.env, HOME: ctx.home, USERPROFILE: ctx.home, DSH_MNEME_URL: ctx.url, DSH_MNEME_TOKEN: ctx.token, ...extraEnv },
146
+ stdio: ["ignore", "pipe", "pipe"]
147
+ });
148
+ let stdout = "";
149
+ let stderr = "";
150
+ child.stdout.on("data", (d) => { stdout += d; });
151
+ child.stderr.on("data", (d) => { stderr += d; });
152
+ child.on("error", rej);
153
+ child.on("exit", (code) => res({ code, stdout, stderr }));
154
+ });
155
+ }
156
+
157
+ const listFiles = (dir) => readdirSync(dir).sort();
158
+
159
+ test("CLI share --dry-run:报数并解释没写文件,输出目录一个字节不多", { timeout: 60000 }, async () => {
160
+ await withPlane(async (ctx) => {
161
+ const dry = await runCli(ctx, ["share", "--dry-run", ...ctx.auth]);
162
+ assert.equal(dry.code, 0, dry.stderr);
163
+ assert.match(dry.stdout, /^干跑报告: 2 条$/m);
164
+ assert.match(dry.stdout, /跳过: 归档 1, 不随包走 1/);
165
+ assert.match(dry.stdout, /- \[archived\] 归档项/);
166
+ assert.match(dry.stdout, /- \[excluded_type\] 会话摘要/);
167
+ assert.match(dry.stdout, /没写文件:加 --out <路径> 才会落盘/);
168
+ assert.ok(!dry.stdout.includes("已写出"), "干跑不许报落盘");
169
+ assert.deepEqual(listFiles(ctx.out), [], "干跑不许产出任何文件");
170
+
171
+ // 同时给 --dry-run 与 --out:偏好干跑,那个路径也不许被创建(cmdShare 的 `if (dryRun) return`
172
+ // 排在写盘之前)。这一条锁的是「安全的那一侧」:脚本以为有文件会拿到 none,但不能反过来
173
+ // ——带了 --dry-run 却动了盘。
174
+ const both = join(ctx.out, "must-not-exist.json");
175
+ const combo = await runCli(ctx, ["share", "--dry-run", "--out", both, ...ctx.auth]);
176
+ assert.equal(combo.code, 0, combo.stderr);
177
+ assert.ok(!existsSync(both), "--dry-run 在场时 --out 不得落盘");
178
+ assert.deepEqual(listFiles(ctx.out), []);
179
+ });
180
+ });
181
+
182
+ test("漏传 --url/--token 的调用点打到的是 fixture 那一面,不是她本机 8790", { timeout: 60000 }, async () => {
183
+ await withPlane(async (ctx) => {
184
+ // 这条锁的是 runCli 那份 env,不是 CLI 的行为:DSH_MNEME_URL/TOKEN 给的是本测试自己
185
+ // 起的端口,所以「未来某个调用点忘了带旗标」落在这里是当场可看见的一次请求,而不是
186
+ // 悄悄打到 `DEFAULT_URL = 127.0.0.1:8790`(她本机活实例的默认端口,只读面回 401、
187
+ // 老构建回 404,表现成「这条用例偶尔红」)。清空那两个变量才是这里要防的回归。
188
+ const before = ctx.plane.state.requests.length;
189
+ const run = await runCli(ctx, ["share", "--dry-run"]);
190
+ assert.equal(run.code, 0, run.stderr);
191
+ assert.equal(ctx.plane.state.requests.length, before + 1, "请求要落到 fixture 这一面");
192
+ assert.deepEqual(listFiles(ctx.out), []);
193
+ });
194
+ });
195
+
196
+ test("CLI 的跳过清单:含凭据那行不给标题,终端要写得像「有意不给」", { timeout: 60000 }, async () => {
197
+ await withPlane(async (ctx) => {
198
+ // 这条锁的是终端那一格:src/share.js 在源头把命中密钥判据的行的标题留空,skip_list
199
+ // 于是只剩原因。空标题直接打出来是一行尾随空格,读的人会当成「这条本来没标题」,
200
+ // 而它恰恰是最需要被看见的那一类拒绝。
201
+ const secret = "P@ssw0rd-for-prod-db";
202
+ ctx.plane.use([
203
+ row({ id: "1", type: "project", title: "干净的归档行", content: "没有凭据", archived: 1 }),
204
+ row({ id: "2", type: "decision", title: `生产库密码 ${secret} 别丢`, content: `password: ${secret}`, archived: 1 })
205
+ ]);
206
+ const dry = await runCli(ctx, ["share", "--dry-run", ...ctx.auth]);
207
+ assert.equal(dry.code, 0, dry.stderr);
208
+ assert.match(dry.stdout, /^干跑报告: 0 条$/m);
209
+ assert.match(dry.stdout, /- \[archived\] 干净的归档行/);
210
+ assert.match(dry.stdout, /- \[archived\] (标题不外发)/);
211
+ // 终端与落盘是同一份 report 的两张嘴:这里也不许出现那个值。
212
+ assert.ok(!dry.stdout.includes(secret), "CLI 不许把凭据标题打到终端");
213
+ assert.ok(!dry.stderr.includes(secret), "错误面同样不许");
214
+ });
215
+ });
216
+
217
+ // 那句「跳过清单已截断」的判据。旧写法拿**清单长度**当证据(`skip_list.length >= 50`),
218
+ // 而清单长度是被服务端截过的读数:恰好 50 条被跳时清单是全量的,那句话在此刻撒谎,
219
+ // 而 50 这个数在 CLI 里是抄来的第二份口径(水位一改就错)。真证据一直在场:
220
+ // skipped 的六个计数走全量。判据因此是「账比清单长」,与面板 ShareReport 同一条
221
+ // (lib/client.js 的 totalSkipped > list.length)。
222
+ // 两格各咬一个方向,缺一不可:
223
+ // ① 恰好 50(清单=账=水位)→ 不许报截断。旧判据在这一格红,本条就是它的行为锁;
224
+ // ② 55 条被跳(账 55 > 清单 50)→ 必须报。这一格旧判据也报,防的是「把提示干脆删掉」
225
+ // 冒充修复——删掉提示的人只会看见这一条红。
226
+ test("CLI 那句「已截断」由六个计数之和判定,恰好等于水位时不报", { timeout: 60000 }, async () => {
227
+ await withPlane(async (ctx) => {
228
+ // 水位住在服务端(src/share.js 的 SHARE_SKIP_LIST_MAX),这里只从它自己的答复里读,
229
+ // 不在测试里再抄一个 50:抄了就又是两份口径,测试会跟着水位一起漂。
230
+ const shareText = readFileSync(resolve(dirname(CLI), "..", "src", "share.js"), "utf8");
231
+ const cap = Number(shareText.match(/SHARE_SKIP_LIST_MAX = (\d+)/)?.[1]);
232
+ assert.ok(Number.isInteger(cap) && cap > 0, "src/share.js 的水位要读得出来");
233
+ const archivedRows = (n, tag) => Array.from({ length: n }, (_, i) =>
234
+ row({ id: `${tag}${i}`, type: "decision", title: `归档行 ${tag}${i}`, content: `正文 ${tag}${i}`, archived: 1 }));
235
+
236
+ // ①:恰好到水位——清单全量,没有东西被截掉。
237
+ ctx.plane.use(archivedRows(cap, "a"));
238
+ const atCap = await runCli(ctx, ["share", "--dry-run", ...ctx.auth]);
239
+ assert.equal(atCap.code, 0, atCap.stderr);
240
+ assert.match(atCap.stdout, new RegExp(`跳过: 归档 ${cap}`), "计数要走全量");
241
+ assert.ok(!atCap.stdout.includes("跳过清单已截断"),
242
+ `清单长度恰好等于水位时不许报截断,got: ${atCap.stdout}`);
243
+
244
+ // ②:账比清单长——这一刻清单确实被截了,必须说。
245
+ ctx.plane.use(archivedRows(cap + 5, "b"));
246
+ const overCap = await runCli(ctx, ["share", "--dry-run", ...ctx.auth]);
247
+ assert.equal(overCap.code, 0, overCap.stderr);
248
+ assert.match(overCap.stdout, new RegExp(`跳过: 归档 ${cap + 5}`), "计数仍是全量");
249
+ assert.match(overCap.stdout, /跳过清单已截断,计数仍为全量/, "账比清单长时这句提示必须在");
250
+ });
251
+ });
252
+
253
+ test("CLI 报得出包内折叠:included 少的那几条要有去处", { timeout: 60000 }, async () => {
254
+ await withPlane(async (ctx) => {
255
+ // 同标题、同正文、同内容锚而 scope 不同的两条在库里是两条合法记忆,在包里共用一把
256
+ // external_id,于是出包侧折成一条。终端只报 `干跑报告: 1 条` 的话,读的人以为自己
257
+ // 少发了 —— 这一格要说清那条去哪了(src/share.js 的 report.deduped)。
258
+ ctx.plane.use([
259
+ row({ id: "1", type: "decision", title: "同一个标题", content: "同一份正文", content_hash: "same-hash", agent_scope: "preset-a" }),
260
+ row({ id: "2", type: "decision", title: "同一个标题", content: "同一份正文", content_hash: "same-hash", agent_scope: "preset-b" })
261
+ ]);
262
+ const dry = await runCli(ctx, ["share", "--dry-run", ...ctx.auth]);
263
+ assert.equal(dry.code, 0, dry.stderr);
264
+ assert.match(dry.stdout, /^干跑报告: 1 条$/m);
265
+ assert.match(dry.stdout, /包内重复已折叠: 1 条/);
266
+ assert.match(dry.stdout, /跳过: 无/, "折叠不走跳过那本账");
267
+ assert.deepEqual(JSON.parse(ctx.plane.state.lastBodyText).report.skip_list, []);
268
+ });
269
+ });
270
+
271
+ test("CLI share --out:数据面答复逐字落盘,解析回来 kind 与 report 都对", { timeout: 60000 }, async () => {
272
+ await withPlane(async (ctx) => {
273
+ const dest = join(ctx.out, "pack.json");
274
+ const run = await runCli(ctx, ["share", "--out", dest, ...ctx.auth]);
275
+ assert.equal(run.code, 0, run.stderr);
276
+ assert.match(run.stdout, /^分享包: 2 条$/m);
277
+ assert.ok(run.stdout.includes(`已写出: ${dest}`), run.stdout);
278
+ assert.equal(ctx.plane.state.requests.at(-1).auth, `Bearer ${TOKEN}`, "Bearer 头得真的带上,否则这条只是本地写文件");
279
+
280
+ const text = readFileSync(dest, "utf8");
281
+ // 逐字相同:CLI 只做 JSON.parse → JSON.stringify 的往返,不许顺手美化、加换行或改键序
282
+ // ——这份文件是要原样 POST 给对面的(车道 C),多一个空格对面重算的锚就对不上。
283
+ assert.equal(text, ctx.plane.state.lastBodyText);
284
+ const pack = JSON.parse(text);
285
+ assert.equal(pack.kind, "mneme.share/v1");
286
+ assert.equal(pack.report.included, 2);
287
+ assert.equal(pack.alias, "", "没给 --alias 时包里是空串,不是 undefined");
288
+ });
289
+ });
290
+
291
+ test("CLI share 不带 --out 也不带 --dry-run:先解释再退出,一个请求都不发", { timeout: 60000 }, async () => {
292
+ await withPlane(async (ctx) => {
293
+ const run = await runCli(ctx, ["share", ...ctx.auth]);
294
+ assert.notEqual(run.code, 0, "整库答复直接打 stdout 会把报告淹掉,这条必须拒");
295
+ assert.match(run.stderr, /要么写文件(--out <路径>),要么只看报告(--dry-run)/);
296
+ assert.ok(run.stderr.includes("dsh-mneme share --out ~/Desktop/pack.json"),
297
+ `拒绝里要带一条能照抄的例子,got: ${run.stderr}`);
298
+ assert.deepEqual(ctx.plane.state.requests, [], "拒绝发生在请求之前:不许先打一轮再拒");
299
+ assert.deepEqual(listFiles(ctx.out), [], "拒绝路径不留文件");
300
+ });
301
+ });
302
+
303
+ test("CLI share --out 的 `~` 前缀:展开到子进程 home(PowerShell/cmd 不会替她展开)", { timeout: 60000 }, async () => {
304
+ await withPlane(async (ctx) => {
305
+ // Git Bash 会展开 `~/…`,PowerShell 与 cmd 给到 CLI 的是字面量 —— USAGE 里的例子正是
306
+ // 这一串,所以 expandHome 是那个例子在 Windows 上能不能用的全部区别。
307
+ const NAMES = ["dsh-mneme-cli-share-tilde-probe.json", "dsh-mneme-cli-share-tilde-probe-win.json"];
308
+ const tilde = await runCli(ctx, ["share", "--out", `~/${NAMES[0]}`, ...ctx.auth]);
309
+ assert.equal(tilde.code, 0, `tilde 展开失败就是 writeFileSync 的 ENOENT 堆栈:${tilde.stderr}`);
310
+ assert.ok(existsSync(join(ctx.home, NAMES[0])), `${NAMES[0]} 应落在子进程 home 里,got: ${tilde.stdout}`);
311
+ assert.ok(tilde.stdout.includes(NAMES[0]) && !tilde.stdout.includes(`~/${NAMES[0]}`),
312
+ `打印的应是展开后的路径,got: ${tilde.stdout}`);
313
+
314
+ const backslash = await runCli(ctx, ["share", "--out", `~\\${NAMES[1]}`, ...ctx.auth]);
315
+ assert.equal(backslash.code, 0, backslash.stderr);
316
+ assert.ok(existsSync(join(ctx.home, NAMES[1])), `${NAMES[1]}:反斜杠形态同一条规则(expandHome 认 ~\\)`);
317
+
318
+ // 绝对路径不受影响:expandHome 只改 `~`/`~/`/`~\` 开头,别的原样交给 writeFileSync。
319
+ const abs = join(ctx.out, "abs.json");
320
+ const plain = await runCli(ctx, ["share", "--out", abs, ...ctx.auth]);
321
+ assert.equal(plain.code, 0, plain.stderr);
322
+ assert.ok(existsSync(abs) && !existsSync(join(ctx.home, "abs.json")));
323
+ // 这里**不**留「万一 HOME 覆盖没生效就去真人 home 里把这两个名字擦掉」的兜底:那个
324
+ // rmSync 的靶子是 os.homedir(),只有在测试本来就该红(覆盖失效)的情况下才会动她家目录,
325
+ // 而它删的是「同名」不是「本测试刚写的那个 inode」。HOME 覆盖有没有生效由上面两条
326
+ // existsSync(join(ctx.home, …)) 负责——红了就停手,产物留在临时 home 里等她自己看。
327
+ });
328
+ });
329
+
330
+ test("CLI share 撞 413:拒答体自带的 hint 与两个字节数走 failHttp 转出来", { timeout: 60000 }, async () => {
331
+ await withPlane(async (ctx) => {
332
+ ctx.plane.overLimit();
333
+ const dest = join(ctx.out, "never.json");
334
+ const run = await runCli(ctx, ["share", "--out", dest, ...ctx.auth]);
335
+ assert.equal(run.code, 1);
336
+ assert.equal(ctx.plane.state.lastStatus, 413, "体积要真撞上去,假服务不许自己编状态码");
337
+ assert.match(run.stderr, /请求失败(HTTP 413):too-large \[\/share\]/);
338
+ // 56f7b06 修的就是这两句:旧 failHttp 只取 data.error,「大了多少」「怎么分批」留在体里
339
+ // 没人看得见 —— 最需要下一步动作的那次拒绝,反而是最没信息量的那次。
340
+ assert.match(run.stderr, /用 --type 或 --tag 分批/);
341
+ assert.match(run.stderr, /量到 \d+(\.\d)? M?B,水位 3\.0 MB/);
342
+ assert.ok(!existsSync(dest), "拒绝路径不许留半个包");
343
+ });
344
+ });
345
+
346
+ test("CLI share --json 撞 413:stdout 保持干净,整份拒答体(含 report)走 stderr", { timeout: 60000 }, async () => {
347
+ await withPlane(async (ctx) => {
348
+ ctx.plane.overLimit();
349
+ const run = await runCli(ctx, ["share", "--json", "--out", join(ctx.out, "never.json"), ...ctx.auth]);
350
+ assert.equal(run.code, 1);
351
+ // --json 的约定:stdout 只有那一份 JSON。拒答走 fail → console.error(rawJson),
352
+ // 所以 stdout 是空的,而 stderr 是可以直接 parse 的原始响应体。
353
+ assert.equal(run.stdout.trim(), "", `--json 的 stdout 不许掺解释行:${run.stdout}`);
354
+ const body = JSON.parse(run.stderr);
355
+ assert.equal(body.error, "too-large");
356
+ assert.ok(body.bytes > body.limitBytes, "两个字节数都在体里,出口才有「大了多少」可说");
357
+ assert.match(body.hint, /分批/);
358
+ assert.equal(body.report.included, 1, "完整报告走 --json:分批的依据就是这份数");
359
+ assert.ok(!("memories" in body), "413 不许把刚拒绝掉的几 MB 再发一遍");
360
+ });
361
+ });
362
+
363
+ test("CLI share --json 成功答复:stdout 是一份能 parse 的 JSON,尾巴不许掺「已写出」", { timeout: 60000 }, async () => {
364
+ await withPlane(async (ctx) => {
365
+ const dest = join(ctx.out, "json-pack.json");
366
+ const written = await runCli(ctx, ["share", "--out", dest, "--json", ...ctx.auth]);
367
+ assert.equal(written.code, 0, written.stderr);
368
+ // A7 报告里实测过的那次 corruption:尾部追一行 已写出 就撞
369
+ // SyntaxError: Unexpected non-whitespace character after JSON(jq 同理)。
370
+ const parsed = JSON.parse(written.stdout);
371
+ assert.deepEqual(Object.keys(parsed).sort(), ["file", "report"]);
372
+ assert.equal(parsed.file, dest, "落盘路径已在 payload 里,那行提示只对人有意义");
373
+ assert.equal(parsed.report.included, 2);
374
+ assert.ok(!written.stdout.includes("已写出"));
375
+ assert.ok(existsSync(dest), "--json 不改变落盘这件事");
376
+
377
+ const dry = await runCli(ctx, ["share", "--dry-run", "--json", ...ctx.auth]);
378
+ assert.equal(dry.code, 0, dry.stderr);
379
+ const dryParsed = JSON.parse(dry.stdout);
380
+ assert.deepEqual(Object.keys(dryParsed).sort(), ["pack", "report"]);
381
+ assert.equal(dryParsed.pack.kind, "mneme.share/v1");
382
+ assert.equal(dryParsed.report.included, 2);
383
+ });
384
+ });
385
+
386
+ test("CLI share --type/--alias/--tag:按数据面认得的形状上线", { timeout: 60000 }, async () => {
387
+ await withPlane(async (ctx) => {
388
+ const run = await runCli(ctx, [
389
+ "share", "--type", "decision,preference", "--alias", "桉桉的库", "--tag", "存储", "--dry-run", ...ctx.auth
390
+ ]);
391
+ assert.equal(run.code, 0, run.stderr);
392
+ assert.deepEqual(JSON.parse(ctx.plane.state.requests.at(-1).body), {
393
+ type: ["decision", "preference"], alias: "桉桉的库", tag: "存储"
394
+ }, "--type 是逗号列表 → 数组;空旗标不该冒出键");
395
+ // 收窄是真的生效(不是发出去没人读):两条里只有带 存储 标签那条进包。
396
+ assert.match(run.stdout, /^干跑报告: 1 条 来自 桉桉的库$/m);
397
+ assert.deepEqual(listFiles(ctx.out), []);
398
+ });
399
+ });
400
+
401
+ // CLI 与 src/share.js 之间没有 import 关系(bin/cli.mjs 是零依赖 HTTP 客户端,那条理由
402
+ // 写在它自己的注释里),所以「六个跳过原因键」在 CLI 侧是抄过去的字面量。抄的东西不会
403
+ // 自己报错:share.js 改一个键名,CLI 就把那一档读成 0(跳过原因在终端上凭空少一类)。
404
+ // 这条静态锁是两份文本之间唯一的接缝——它不重跑逻辑,只比对字面量集合。
405
+ //
406
+ // 「与 50 的截断数同源」那一半是**换掉**而不是删掉:判据从「清单长度到 50」改成「六个
407
+ // 计数之和 > 清单长度」之后,CLI 侧不再有那个数,水位只住在 src/share.js 一处(面板侧
408
+ // lib/client.js 的 ShareReport 同一条取舍,那里也从不写 50)。没有抄本,就没有可比对的
409
+ // 接缝——所以这里改锁两件方向相反的事:
410
+ // ① CLI 不许再出现截断阈值:`).length >= 数字` 那个形状本身就是「恰好一水位就误报」的
411
+ // 旧缺陷,数值对不对这件事永远锁不住,只能靠不出现;
412
+ // ② 那句提示必须由「账比清单长」判定。
413
+ // 判据真不真由上面那条行为用例真跑 CLI 证明(恰好到水位不报、超水位才报),这里只锁形状。
414
+ test("CLI 抄过去的原因键跟 src/share.js 同源,且不硬写截断水位", () => {
415
+ const shareText = readFileSync(resolve(dirname(CLI), "..", "src", "share.js"), "utf8");
416
+ const cliText = readFileSync(CLI, "utf8");
417
+
418
+ const skippedBlock = shareText.match(/const skipped = \{([^}]*)\}/)?.[1] ?? "";
419
+ const shareKeys = [...skippedBlock.matchAll(/([a-z_]+):\s*0/g)].map((m) => m[1]);
420
+ const namedBlock = cliText.match(/const named = \[([\s\S]*?)\];/)?.[1] ?? "";
421
+ const cliKeys = [...namedBlock.matchAll(/\['([a-z_]+)'/g)].map((m) => m[1]);
422
+ // 两条正则都必须真的抓到东西,否则这个用例退化成永真(同本文件对假服务的口径)。
423
+ assert.equal(shareKeys.length, 6, `share.js 的 skipped 应有六个键,抓到 ${shareKeys.length} 个:${shareKeys}`);
424
+ assert.equal(cliKeys.length, 6, `cli.mjs 的 named 应有六档,抓到 ${cliKeys.length} 个:${cliKeys}`);
425
+ assert.deepEqual(cliKeys, shareKeys, "CLI 的档位名必须与 report.skipped 的键逐字同序");
426
+
427
+ // ①:那六个键在「求和那一行」也得原样出现一次——判据读的是同一本账,不是第七份口径。
428
+ const censusBlock = cliText.match(/const skippedTotal = \[([\s\S]*?)\]/)?.[1] ?? "";
429
+ const censusKeys = [...censusBlock.matchAll(/'([a-z_]+)'/g)].map((m) => m[1]);
430
+ assert.deepEqual(censusKeys, shareKeys, "截断判据的六个计数键必须与 report.skipped 同序同源");
431
+ assert.ok(/\)\.length\s*>=\s*\d+/.test(cliText) === false,
432
+ "cli.mjs 不许再硬写截断水位:那个数字只住在 src/share.js 的 SHARE_SKIP_LIST_MAX");
433
+
434
+ // ②:提示由「账比清单长」判定,与面板同一条判据。
435
+ assert.ok(cliText.includes("skippedTotal > (r.skip_list ?? []).length)"),
436
+ "那句「跳过清单已截断」必须由六个计数之和与清单长度之比来判定");
437
+ });