@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.
- package/LICENSE +21 -21
- package/README.md +830 -830
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +964 -964
- package/bin/dsh-mneme-choro.mjs +262 -262
- package/bin/dsh-mneme-mcp.mjs +510 -510
- package/bin/dsh-mneme-serve.mjs +151 -151
- package/cordis.patch.yml +21 -21
- package/docs/CONFIGURATION.md +407 -404
- package/docs/CONTINUITY.md +139 -139
- package/docs/DAEMON.md +57 -57
- package/docs/DIRECTORIES.md +114 -114
- package/docs/DISTILL.md +397 -371
- package/docs/DSH-GO.md +97 -97
- package/docs/ENTITIES.md +247 -247
- package/docs/FORGET.md +77 -77
- package/docs/KB.md +110 -110
- package/docs/LOCAL_MODEL.md +222 -222
- package/docs/SEMANTIC.md +256 -256
- package/docs/SHARE.md +552 -552
- package/docs/SLEEP.md +202 -202
- package/docs/STORAGE.md +122 -122
- package/docs/WORKFLOWS.md +366 -366
- package/lib/api-standalone.js +1007 -1007
- package/lib/api.js +3600 -3600
- package/lib/attachment-text.js +122 -122
- package/lib/bootstrap-http.js +105 -105
- package/lib/bootstrap.js +215 -215
- package/lib/candidates.js +454 -454
- package/lib/capabilities.js +819 -819
- package/lib/choro-command.js +111 -111
- package/lib/client.js +14420 -14322
- package/lib/commands.js +95 -95
- package/lib/config-keys.js +144 -144
- package/lib/config.js +1361 -1344
- package/lib/content-hash.js +54 -54
- package/lib/continuity.js +258 -258
- package/lib/date-hint.js +32 -0
- package/lib/diary/auto.js +190 -190
- package/lib/diary/compose.js +440 -440
- package/lib/diary/dir.js +59 -59
- package/lib/diary/export.js +97 -97
- package/lib/diary/index.js +26 -26
- package/lib/diary/library.js +189 -189
- package/lib/diary/qq-inbox.js +142 -142
- package/lib/diary/rollover.js +61 -61
- package/lib/diary/session-provider.js +122 -122
- package/lib/document-index.js +94 -94
- package/lib/document.js +322 -322
- package/lib/dream/clustering.js +123 -123
- package/lib/dream/decisions.js +945 -945
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +2027 -2027
- package/lib/dream/triage.js +139 -139
- package/lib/dream.js +1879 -1879
- package/lib/dsh-go/host.js +162 -162
- package/lib/dsh-go/panel.js +2091 -2091
- package/lib/dsh-go/text.js +342 -342
- package/lib/dsh-go/views/common.js +68 -68
- package/lib/dsh-go/views/entity.js +48 -48
- package/lib/dsh-go/views/memory.js +198 -198
- package/lib/dsh-go/views/root.js +30 -30
- package/lib/dsh-go/views/settings.js +398 -398
- package/lib/dsh-go/views/status.js +51 -51
- package/lib/embedding.js +342 -342
- package/lib/entities/extractor.js +285 -285
- package/lib/fence.js +24 -24
- package/lib/graph/anchoring.js +184 -184
- package/lib/guide.js +74 -74
- package/lib/heat.js +144 -144
- package/lib/hot-memory.js +119 -119
- package/lib/image-describe.js +158 -0
- package/lib/index.js +852 -852
- package/lib/inject.js +656 -651
- package/lib/kb/index.js +309 -309
- package/lib/kb/routes.js +219 -219
- package/lib/kb/tools.js +148 -148
- package/lib/lang.js +607 -595
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +162 -162
- package/lib/memory-source.js +9 -9
- package/lib/mirror.js +330 -330
- package/lib/organize.js +456 -456
- package/lib/peak-hours.js +172 -172
- package/lib/pipeline-activities.js +178 -178
- package/lib/pipeline-attribution.js +165 -165
- package/lib/pipeline-command.js +279 -279
- package/lib/pipeline-confirm.js +142 -142
- package/lib/pipeline-exec.js +174 -174
- package/lib/pipeline-nudge-auto.js +129 -129
- package/lib/pipeline-nudge.js +1623 -1623
- package/lib/pipeline-observations.js +181 -181
- package/lib/pipeline-observe.js +133 -133
- package/lib/pipeline-runner.js +361 -361
- package/lib/pipeline-runs.js +442 -442
- package/lib/pipeline.js +716 -716
- package/lib/placement.js +13 -13
- package/lib/pre-inject-gate.js +354 -354
- package/lib/profile-extract.js +164 -164
- package/lib/profile-facts.js +356 -356
- package/lib/provenance-http.js +251 -251
- package/lib/quality-filter.js +162 -162
- package/lib/recall-stats.js +190 -190
- package/lib/reclaim-http.js +150 -150
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -265
- package/lib/runtime/loader.js +252 -252
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +223 -223
- package/lib/safe-slice.js +18 -18
- package/lib/scope-state.js +277 -277
- package/lib/scope.js +187 -187
- package/lib/search/adaptive.js +60 -60
- package/lib/search/bm25.js +96 -96
- package/lib/semantic.js +572 -572
- package/lib/sensitive-scan.js +209 -209
- package/lib/serve.js +228 -228
- package/lib/service.js +3268 -3268
- package/lib/session-gate.js +91 -91
- package/lib/session-usage.js +350 -350
- package/lib/settings.js +827 -817
- package/lib/share-http.js +281 -281
- package/lib/share-import.js +234 -234
- package/lib/share.js +335 -335
- package/lib/store.js +5895 -5895
- package/lib/summarize.js +1388 -1343
- package/lib/tools.js +1370 -1356
- package/lib/user-settings.js +230 -230
- package/lib/vector-index.js +180 -180
- package/lib/version-check.js +122 -122
- package/lib/wiki/claims.js +267 -267
- package/lib/wiki/cli-hint.js +90 -90
- package/lib/wiki/inject.js +164 -164
- package/lib/wiki/partition.js +200 -200
- package/lib/wiki/plan.js +214 -214
- package/lib/wiki/queries.js +640 -640
- package/lib/wiki/render.js +261 -261
- package/lib/wiki/scan.js +371 -371
- package/lib/wiki/t0.js +500 -500
- package/lib/wiki/t1-jobs.js +232 -232
- package/lib/wiki/t1.js +609 -609
- package/lib/write-admission.js +404 -404
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +1 -1
- package/runtime-manifest.json +526 -526
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +249 -249
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -224
- package/scripts/changelog-prep.mjs +91 -91
- package/scripts/check-sync.js +44 -44
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/publish-gate.mjs +197 -197
- package/scripts/release-docs.mjs +210 -210
- package/scripts/release-notes.mjs +164 -164
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +69 -69
- package/scripts/test-count-sync.mjs +48 -48
- package/src/api-standalone.js +1007 -1007
- package/src/api.js +3600 -3600
- package/src/attachment-text.js +122 -122
- package/src/bootstrap-http.js +105 -105
- package/src/bootstrap.js +215 -215
- package/src/candidates.js +454 -454
- package/src/capabilities.js +819 -819
- package/src/choro-command.js +111 -111
- package/src/commands.js +95 -95
- package/src/config-keys.js +144 -144
- package/src/config.js +1361 -1344
- package/src/content-hash.js +54 -54
- package/src/continuity.js +258 -258
- package/src/date-hint.js +32 -0
- package/src/diary/auto.js +190 -190
- package/src/diary/compose.js +440 -440
- package/src/diary/dir.js +59 -59
- package/src/diary/export.js +97 -97
- package/src/diary/index.js +26 -26
- package/src/diary/library.js +189 -189
- package/src/diary/qq-inbox.js +142 -142
- package/src/diary/rollover.js +61 -61
- package/src/diary/session-provider.js +122 -122
- package/src/document-index.js +94 -94
- package/src/document.js +322 -322
- package/src/dream/clustering.js +123 -123
- package/src/dream/decisions.js +945 -945
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +2027 -2027
- package/src/dream/triage.js +139 -139
- package/src/dream.js +1879 -1879
- package/src/dsh-go/host.js +162 -162
- package/src/dsh-go/panel.js +2091 -2091
- package/src/dsh-go/text.js +342 -342
- package/src/dsh-go/views/common.js +68 -68
- package/src/dsh-go/views/entity.js +48 -48
- package/src/dsh-go/views/memory.js +198 -198
- package/src/dsh-go/views/root.js +30 -30
- package/src/dsh-go/views/settings.js +398 -398
- package/src/dsh-go/views/status.js +51 -51
- package/src/embedding.js +342 -342
- package/src/entities/extractor.js +285 -285
- package/src/fence.js +24 -24
- package/src/graph/anchoring.js +184 -184
- package/src/guide.js +74 -74
- package/src/heat.js +144 -144
- package/src/hot-memory.js +119 -119
- package/src/image-describe.js +158 -0
- package/src/index.js +852 -852
- package/src/inject.js +656 -651
- package/src/kb/index.js +309 -309
- package/src/kb/routes.js +219 -219
- package/src/kb/tools.js +148 -148
- package/src/lang.js +607 -595
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +162 -162
- package/src/memory-source.js +9 -9
- package/src/mirror.js +330 -330
- package/src/organize.js +456 -456
- package/src/peak-hours.js +172 -172
- package/src/pipeline-activities.js +178 -178
- package/src/pipeline-attribution.js +165 -165
- package/src/pipeline-command.js +279 -279
- package/src/pipeline-confirm.js +142 -142
- package/src/pipeline-exec.js +174 -174
- package/src/pipeline-nudge-auto.js +129 -129
- package/src/pipeline-nudge.js +1623 -1623
- package/src/pipeline-observations.js +181 -181
- package/src/pipeline-observe.js +133 -133
- package/src/pipeline-runner.js +361 -361
- package/src/pipeline-runs.js +442 -442
- package/src/pipeline.js +716 -716
- package/src/placement.js +13 -13
- package/src/pre-inject-gate.js +354 -354
- package/src/profile-extract.js +164 -164
- package/src/profile-facts.js +356 -356
- package/src/provenance-http.js +251 -251
- package/src/quality-filter.js +162 -162
- package/src/recall-stats.js +190 -190
- package/src/reclaim-http.js +150 -150
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -265
- package/src/runtime/loader.js +252 -252
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +223 -223
- package/src/safe-slice.js +18 -18
- package/src/scope-state.js +277 -277
- package/src/scope.js +187 -187
- package/src/search/adaptive.js +60 -60
- package/src/search/bm25.js +96 -96
- package/src/semantic.js +572 -572
- package/src/sensitive-scan.js +209 -209
- package/src/serve.js +228 -228
- package/src/service.js +3268 -3268
- package/src/session-gate.js +91 -91
- package/src/session-usage.js +350 -350
- package/src/settings.js +827 -817
- package/src/share-http.js +281 -281
- package/src/share-import.js +234 -234
- package/src/share.js +335 -335
- package/src/store.js +5895 -5895
- package/src/summarize.js +1388 -1343
- package/src/tools.js +1370 -1356
- package/src/user-settings.js +230 -230
- package/src/vector-index.js +180 -180
- package/src/version-check.js +122 -122
- package/src/wiki/claims.js +267 -267
- package/src/wiki/cli-hint.js +90 -90
- package/src/wiki/inject.js +164 -164
- package/src/wiki/partition.js +200 -200
- package/src/wiki/plan.js +214 -214
- package/src/wiki/queries.js +640 -640
- package/src/wiki/render.js +261 -261
- package/src/wiki/scan.js +371 -371
- package/src/wiki/t0.js +500 -500
- package/src/wiki/t1-jobs.js +232 -232
- package/src/wiki/t1.js +609 -609
- package/src/write-admission.js +404 -404
- package/test/adaptive-inject-budget.test.js +85 -85
- package/test/api-hardening.test.js +173 -173
- package/test/api-nudge-route.test.js +259 -259
- package/test/api-routes-gaps.test.js +1092 -1092
- package/test/api.test.js +3007 -2998
- package/test/attachment-text.test.js +139 -139
- package/test/audit.test.js +448 -448
- package/test/baseline-persist.test.js +248 -248
- package/test/benchmark.test.js +94 -94
- package/test/bm25-corpus-cache.test.js +145 -145
- package/test/bootstrap.test.js +228 -228
- package/test/candidates.test.js +345 -345
- package/test/capabilities.test.js +577 -577
- package/test/capacity-warning.test.js +262 -262
- package/test/changelog-prep.test.js +125 -125
- package/test/choro-command.test.js +218 -218
- package/test/cli-enums.test.js +37 -37
- package/test/cli-flag-value.test.js +86 -86
- package/test/cli-receive.test.js +390 -390
- package/test/cli-share.test.js +437 -437
- package/test/client-share.test.js +225 -225
- package/test/client.test.js +4595 -4425
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config-keys-routes.test.js +233 -233
- package/test/config.test.js +153 -153
- package/test/conflict-freeze.test.js +344 -344
- package/test/conflict-ledger-enforce.test.js +452 -452
- package/test/conflict-ledger.test.js +242 -242
- package/test/conflict-reviewed.test.js +79 -79
- package/test/content-hash.test.js +204 -204
- package/test/continuity.test.js +452 -452
- package/test/contrast.test.js +432 -432
- package/test/date-hint.test.js +156 -0
- package/test/dedupe-candidate-window.test.js +180 -180
- package/test/dependency-overrides.test.js +78 -78
- package/test/diary-library.test.js +187 -187
- package/test/diary.test.js +576 -576
- package/test/differentiate-feedback.test.js +248 -248
- package/test/distill-tx-embed-gap.test.js +110 -110
- package/test/docs-config-keys.test.js +65 -65
- package/test/document-dir.test.js +233 -233
- package/test/document.test.js +703 -703
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-baseline-clamp.test.js +174 -174
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream-decision-evidence.test.js +314 -314
- package/test/dream-failure-backoff.test.js +173 -173
- package/test/dream-fence.test.js +207 -207
- package/test/dream-maxtokens-fallback.test.js +132 -132
- package/test/dream-merge-guardrail.test.js +219 -219
- package/test/dream-peak-hours.test.js +194 -194
- package/test/dream.test.js +1528 -1528
- package/test/dsh-go-host.test.js +134 -134
- package/test/dsh-go-panel-actions.test.js +854 -854
- package/test/dsh-go-panel.test.js +251 -251
- package/test/embed-ready-poll.test.js +94 -94
- package/test/embed-text-reasons.test.js +112 -112
- package/test/embedding-cache-f32.test.js +276 -276
- package/test/embedding-ready.test.js +226 -226
- package/test/entities.test.js +711 -711
- package/test/entity-recall.test.js +179 -179
- package/test/epistemic.test.js +325 -325
- package/test/failure-query.test.js +147 -147
- package/test/failure-retention.test.js +105 -105
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/forget-corrections.test.js +70 -70
- package/test/graph-anchoring.test.js +184 -184
- package/test/graph-api.test.js +333 -333
- package/test/graph-boosted-relations.test.js +189 -189
- package/test/graph-inject-budget.test.js +103 -103
- package/test/graph-multipath.test.js +269 -269
- package/test/graph-passive-confirm.test.js +91 -91
- package/test/graph-weight-evolution.test.js +104 -104
- package/test/heat.test.js +175 -175
- package/test/helpers/dream-mock.js +86 -86
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/helpers/write-admission-samples.js +312 -312
- package/test/hot-memory.test.js +259 -259
- package/test/image-description.test.js +271 -0
- package/test/image-memory.test.js +170 -170
- package/test/inject-content-cap.test.js +91 -91
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +77 -77
- package/test/inject-parent-gate.test.js +134 -109
- package/test/inject-pin-explicit.test.js +300 -300
- package/test/inject-pin-vector-crowd.test.js +225 -225
- package/test/inject-pools.test.js +195 -195
- package/test/inject-recall.test.js +172 -172
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +553 -553
- package/test/issue-617-step1-query-and-fusion.test.js +361 -361
- package/test/kb-api-wiring.test.js +128 -128
- package/test/kb-collections.test.js +272 -272
- package/test/kb-routes.test.js +379 -379
- package/test/lang.test.js +98 -98
- package/test/lastrunat-persist.test.js +101 -101
- package/test/lib-smoke.test.js +116 -116
- package/test/llm-audit.test.js +815 -815
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +501 -501
- package/test/mcp-stdio.test.js +385 -385
- package/test/mint-source.test.js +311 -311
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror-types.test.js +208 -208
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -162
- package/test/organize-tool.test.js +232 -232
- package/test/organize.test.js +418 -418
- package/test/panel-chart-scale.test.js +90 -90
- package/test/panel-tokens.test.js +245 -245
- package/test/peak-hours-observability.test.js +70 -70
- package/test/peer-blockers.test.js +193 -193
- package/test/persona-addressing.test.js +83 -83
- package/test/persona-api.test.js +172 -172
- package/test/persona-by-session.test.js +568 -568
- package/test/persona-flag.test.js +36 -36
- package/test/persona-inject.test.js +145 -145
- package/test/persona-settings.test.js +198 -198
- package/test/pipeline-activities.test.js +155 -155
- package/test/pipeline-attribution.test.js +175 -175
- package/test/pipeline-command.test.js +243 -243
- package/test/pipeline-confirm.test.js +148 -148
- package/test/pipeline-exec.test.js +172 -172
- package/test/pipeline-nudge-auto.test.js +248 -248
- package/test/pipeline-nudge.test.js +1278 -1278
- package/test/pipeline-observations.test.js +157 -157
- package/test/pipeline-observe.test.js +127 -127
- package/test/pipeline-r1r2.test.js +278 -278
- package/test/pipeline-runner.test.js +468 -468
- package/test/pipeline-runs.test.js +767 -767
- package/test/pipeline.test.js +636 -636
- package/test/placement.test.js +36 -36
- package/test/policy-epoch.test.js +259 -259
- package/test/pre-inject-gate.test.js +444 -444
- package/test/profile-extract.test.js +269 -269
- package/test/profile-facts.test.js +492 -492
- package/test/prompt-stance.test.js +128 -128
- package/test/provenance-http.test.js +367 -367
- package/test/publish-gate.test.js +272 -272
- package/test/quality-backfill.test.js +290 -290
- package/test/quality-filter.test.js +295 -295
- package/test/readme-claims.test.js +113 -113
- package/test/reasoning-effort.test.js +752 -752
- package/test/recall-anti-update.test.js +82 -82
- package/test/recall-evals.test.js +253 -253
- package/test/recall-layer.test.js +326 -326
- package/test/recall-retention.test.js +128 -128
- package/test/recall-stats.test.js +274 -274
- package/test/receipt-chain.test.js +451 -451
- package/test/reclaim-http.test.js +209 -209
- package/test/reflection-update-gate.test.js +129 -129
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +475 -475
- package/test/reindex-scheduler.test.js +369 -369
- package/test/release-docs.test.js +156 -156
- package/test/release-notes.test.js +265 -265
- package/test/release-workflow.test.js +93 -93
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -83
- package/test/route-policy.test.js +113 -113
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -255
- package/test/runtime-manifest.test.js +159 -159
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +319 -319
- package/test/scope-candidates.test.js +491 -491
- package/test/scope-distill-stamp.test.js +134 -134
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-inject-soft.test.js +106 -106
- package/test/scope-optional-service.test.js +110 -110
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-state.test.js +610 -610
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +367 -367
- package/test/search-fusion.test.js +90 -90
- package/test/security-doc.test.js +188 -188
- package/test/semantic.test.js +124 -124
- package/test/sensitive-scan.test.js +251 -251
- package/test/serve-bin.test.js +121 -121
- package/test/serve.test.js +258 -258
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +410 -410
- package/test/session-gate.test.js +264 -264
- package/test/session-usage.test.js +317 -317
- package/test/settings.test.js +273 -273
- package/test/share-foreign-rows.test.js +144 -144
- package/test/share-http.test.js +144 -144
- package/test/share-import.test.js +577 -577
- package/test/share-imported-guard.test.js +311 -311
- package/test/share-imported-identity.test.js +111 -111
- package/test/share-pack.test.js +511 -511
- package/test/share-receive-http.test.js +756 -756
- package/test/share-routes.test.js +337 -337
- package/test/share-store.test.js +133 -133
- package/test/share-validate.test.js +101 -101
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +441 -441
- package/test/sleep-catchup.test.js +315 -315
- package/test/sleep-failure-backoff.test.js +156 -156
- package/test/sleep-heat.test.js +174 -174
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +1455 -1455
- package/test/standalone-api.test.js +1344 -1344
- package/test/store-vector-cache.test.js +98 -98
- package/test/store-write-path-columns.test.js +191 -191
- package/test/store.test.js +744 -744
- package/test/stress.test.js +209 -209
- package/test/summarize-occurred-at.test.js +127 -127
- package/test/summarize-parse-diagnostics.test.js +75 -75
- package/test/summarize-reasoning-effort.test.js +189 -189
- package/test/summarize.test.js +2251 -2251
- package/test/supersede-state-tracking.test.js +394 -394
- package/test/test-count-sync.test.js +71 -71
- package/test/tool-state-ignore.test.js +65 -65
- package/test/tools-dto-schema-matrix.test.js +398 -398
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +585 -585
- package/test/updated-at-semantics.test.js +113 -113
- package/test/user-settings-preview.test.js +127 -127
- package/test/user-settings-segments.test.js +527 -527
- package/test/vector-index.test.js +334 -334
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
- package/test/wiki-api.test.js +734 -734
- package/test/wiki-claims.test.js +194 -194
- package/test/wiki-cli-hint.test.js +118 -118
- package/test/wiki-inject.test.js +265 -265
- package/test/wiki-partition.test.js +112 -112
- package/test/wiki-plan.test.js +80 -80
- package/test/wiki-provenance.test.js +202 -202
- package/test/wiki-queries.test.js +398 -398
- package/test/wiki-scan.test.js +208 -208
- package/test/wiki-store.test.js +190 -190
- package/test/wiki-t0.test.js +456 -456
- package/test/wiki-t1-jobs.test.js +125 -125
- package/test/wiki-t1.test.js +460 -460
- package/test/wiki-tools.test.js +176 -176
- package/test/workflow-guards.test.js +155 -155
- package/test/write-admission.test.js +668 -668
|
@@ -1,337 +1,337 @@
|
|
|
1
|
-
// 分享包的**两张皮**路由锁。盯四件事:
|
|
2
|
-
// a) policy 归对(token —— 它是全库级读取,与 /export 同门,见 2026-10 审计 P0);
|
|
3
|
-
// b) token 配上时**不带 token 必须 401**,不是静默放行;
|
|
4
|
-
// c) readBody 返回 null(超 4 MiB,它自己已答 413)时 handler 必须直接 return,
|
|
5
|
-
// 不能再答一次 —— 双写响应是 #410 那一类故障;
|
|
6
|
-
// d) 500 必须留下日志:shareReply 把抛错兜成 500,不接 warn 回调时那条 500 是静默的
|
|
7
|
-
// (现场只剩「内部错误」,查不出是哪一次打包炸的)。同款契约见 reclaim 的 500 用例。
|
|
8
|
-
// 装配台照 test/api-routes-gaps.test.js 的 reclaimSetup,**必须落文件库**:`:memory:`
|
|
9
|
-
// 与文件库在 store.all() 的读侧口径上没差别,但分享包吃的正是「全库含归档/已遗忘」,
|
|
10
|
-
// 用内存库时归档位的字节口径与体积水位都摸不到真实数字(同 reclaim 报告的理由)。
|
|
11
|
-
//
|
|
12
|
-
// 后半截是 Bearer 数据面(src/api-standalone.js)的真 HTTP 装配,起服务与读 token 的
|
|
13
|
-
// 形状照 test/standalone-api.test.js。放同一份文件里是有意的:#411 说两张皮只许差在
|
|
14
|
-
// 鉴权与路径,那么「同一份答复」就是两侧共同的不变量——分开两个文件时,改一侧的人
|
|
15
|
-
// 看不见另一侧锁着什么。
|
|
16
|
-
import test from "node:test";
|
|
17
|
-
import assert from "node:assert/strict";
|
|
18
|
-
import { EventEmitter } from "node:events";
|
|
19
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
20
|
-
import { tmpdir } from "node:os";
|
|
21
|
-
import { join } from "node:path";
|
|
22
|
-
import { createStore } from "../src/store.js";
|
|
23
|
-
import { createService } from "../src/service.js";
|
|
24
|
-
import { createSettings } from "../src/settings.js";
|
|
25
|
-
import { createApi } from "../src/api.js";
|
|
26
|
-
import { createStandaloneApi } from "../src/api-standalone.js";
|
|
27
|
-
|
|
28
|
-
const SHARE_PATH = "/api/dsh-mneme/share";
|
|
29
|
-
const MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
30
|
-
|
|
31
|
-
class FakeRes extends EventEmitter {
|
|
32
|
-
constructor({ failFirstHead = false } = {}) {
|
|
33
|
-
super();
|
|
34
|
-
this.statusCode = 0;
|
|
35
|
-
this.body = "";
|
|
36
|
-
this.headers = null;
|
|
37
|
-
// writeHead 计数:双写响应的故障形态是「第二次 writeHead 覆盖掉第一次的状态码」,
|
|
38
|
-
// 只看最终 statusCode 抓不住(413 之后又被 200 盖掉时 body 也换了),要数次数。
|
|
39
|
-
this.heads = 0;
|
|
40
|
-
// 一次性坏掉的 sink:真 socket 上「一个响应两写」的现场形态就是第二次 writeHead 抛
|
|
41
|
-
// ERR_HTTP_HEADERS_SENT(Node 的 writeHead 当场把头发出去)。这里让第一次抛、第二次
|
|
42
|
-
// 正常,好把「promise 链上的错误到底有没有人接」单独问出来;抛掉的那一次不计数,
|
|
43
|
-
// 因为它一个字节都没写出去。
|
|
44
|
-
this.failFirstHead = failFirstHead;
|
|
45
|
-
// 数据面的 handler 不返回 promise(那条链是 `void readBody(...)`),所以等答复只能靠
|
|
46
|
-
// 这一声 end:配超时竞速,缺 .catch 时用例红得干脆,而不是把文件挂死在无限超时上。
|
|
47
|
-
this.settled = new Promise((resolve) => { this.settle = resolve; });
|
|
48
|
-
}
|
|
49
|
-
writeHead(code, headers) {
|
|
50
|
-
if (this.failFirstHead) {
|
|
51
|
-
this.failFirstHead = false;
|
|
52
|
-
throw new Error("sink on fire");
|
|
53
|
-
}
|
|
54
|
-
this.statusCode = code;
|
|
55
|
-
this.headers = headers;
|
|
56
|
-
this.heads += 1;
|
|
57
|
-
return this;
|
|
58
|
-
}
|
|
59
|
-
end(text) {
|
|
60
|
-
this.body = text ?? "";
|
|
61
|
-
this.emit("end");
|
|
62
|
-
this.settle?.("ended");
|
|
63
|
-
return this;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function shareSetup(apiToken = "secret-token", { logger = null } = {}) {
|
|
68
|
-
const dir = mkdtempSync(join(tmpdir(), "dsh-mneme-share-api-"));
|
|
69
|
-
const store = createStore(join(dir, "memory.db"));
|
|
70
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
71
|
-
const settings = createSettings(store.db);
|
|
72
|
-
const commands = { add: () => {}, remove: () => {}, list: () => [] };
|
|
73
|
-
const routes = [];
|
|
74
|
-
const ctx = { logger, webServer: { register(route) { routes.push(route); return () => {}; } } };
|
|
75
|
-
createApi(ctx, service, settings, commands, null, undefined, apiToken, null, null);
|
|
76
|
-
return {
|
|
77
|
-
store, service, routes,
|
|
78
|
-
// 闭包拿 store,不用 this:裸函数调用下 this 是 undefined,异常被吞掉就会变成
|
|
79
|
-
// 「库没关就删目录」的 EPERM(清理失败还说不清原因)。
|
|
80
|
-
close() {
|
|
81
|
-
try { store.close(); } catch { /* already closed */ }
|
|
82
|
-
rmSync(dir, { recursive: true, force: true });
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* 字节必须在 listener 挂上之后再 emit:EventEmitter 不排队,在 `route.handler` 里
|
|
89
|
-
* readBody 注册 data/end 之前 emit 出去等于什么都没发,readBody 的 promise 永不
|
|
90
|
-
* resolve,整个用例会挂到超时——失败原因还完全看不出跟分享有关。
|
|
91
|
-
*/
|
|
92
|
-
function shareReq({ method = "POST", url = SHARE_PATH, body = "", authorized = true } = {}) {
|
|
93
|
-
const req = new EventEmitter();
|
|
94
|
-
req.method = method;
|
|
95
|
-
req.url = url;
|
|
96
|
-
req.headers = authorized ? { authorization: "Bearer secret-token" } : {};
|
|
97
|
-
process.nextTick(() => {
|
|
98
|
-
req.emit("data", Buffer.from(body));
|
|
99
|
-
req.emit("end");
|
|
100
|
-
});
|
|
101
|
-
return req;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function callShare(route, opts = {}) {
|
|
105
|
-
const res = new FakeRes({ failFirstHead: opts.failFirstHead === true });
|
|
106
|
-
let rejection = null;
|
|
107
|
-
try {
|
|
108
|
-
await route.handler(shareReq(opts), res);
|
|
109
|
-
} catch (error) {
|
|
110
|
-
// 把 rejection 收成读数而不是让它冒出去:少了 .catch 时这里就是 #410 的形状——
|
|
111
|
-
// 断言「rejection 必须为 null」比让用例以一个看不懂的异常挂掉说得清得多。
|
|
112
|
-
rejection = String(error);
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
status: res.statusCode,
|
|
116
|
-
body: res.body ? JSON.parse(res.body) : null,
|
|
117
|
-
headers: res.headers,
|
|
118
|
-
heads: res.heads,
|
|
119
|
-
rejection
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function shareRoute(setup) {
|
|
124
|
-
const route = setup.routes.find((r) => r.path === SHARE_PATH);
|
|
125
|
-
assert.ok(route, "route must exist");
|
|
126
|
-
return route;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
test("share route is registered with policy token", () => {
|
|
130
|
-
const s = shareSetup();
|
|
131
|
-
try {
|
|
132
|
-
const route = shareRoute(s);
|
|
133
|
-
assert.equal(route.policy, "token");
|
|
134
|
-
assert.equal(route.kind, "exact");
|
|
135
|
-
} finally { s.close(); }
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("a configured token gates the share route", async () => {
|
|
139
|
-
const s = shareSetup();
|
|
140
|
-
try {
|
|
141
|
-
const route = shareRoute(s);
|
|
142
|
-
s.store.save({ type: "decision", title: "选 SQLite", content: "正文", tags: [] });
|
|
143
|
-
const denied = await callShare(route, { authorized: false, body: "" });
|
|
144
|
-
assert.equal(denied.status, 401);
|
|
145
|
-
const allowed = await callShare(route, { authorized: true, body: "" });
|
|
146
|
-
assert.equal(allowed.status, 200);
|
|
147
|
-
const pack = allowed.body;
|
|
148
|
-
assert.equal(pack.kind, "mneme.share/v1");
|
|
149
|
-
assert.equal(pack.report.included, 1);
|
|
150
|
-
assert.ok(!("id" in pack.memories[0]));
|
|
151
|
-
// 本侧 sendJson 有 headers 第四参(数据面没有):Content-Disposition 是面板这张脸
|
|
152
|
-
// 独有的那半,共用层只回 {status, body},文件名由这里拼——锁它,防止哪天把
|
|
153
|
-
// shareFilename 的连接也一起「统一」掉。
|
|
154
|
-
assert.match(allowed.headers["Content-Disposition"], /^attachment; filename="dsh-mneme-share-\d{8}-[0-9a-f]{8}\.json"$/);
|
|
155
|
-
// 空 body 与「一个字节都不发」同口径:分享的主用法就是按整库口径打。
|
|
156
|
-
const alias = await callShare(route, { body: JSON.stringify({ alias: "桉桉的库" }) });
|
|
157
|
-
assert.equal(alias.status, 200);
|
|
158
|
-
assert.equal(alias.body.alias, "桉桉的库");
|
|
159
|
-
assert.match(alias.body.source_version, /\d/, "包要记下发送方版本,对面按它判兼容");
|
|
160
|
-
assert.ok(Number.isFinite(Date.parse(alias.body.generated_at)));
|
|
161
|
-
} finally { s.close(); }
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("GET on the share route is not an open read", async () => {
|
|
165
|
-
const s = shareSetup();
|
|
166
|
-
try {
|
|
167
|
-
const route = shareRoute(s);
|
|
168
|
-
s.store.save({ type: "decision", title: "选 SQLite", content: "正文", tags: [] });
|
|
169
|
-
const out = await callShare(route, { method: "GET", body: "" });
|
|
170
|
-
// 状态码锁死 405,不留 404 这个后门:#411 说两张皮只许差在鉴权与路径,
|
|
171
|
-
// 而数据面那半(本文件末尾)已经只认 405。这里容忍 404 就等于让「面板哪天
|
|
172
|
-
// 掉回末尾 404」也算通过——两张皮悄悄多出一处差别,没人报警。
|
|
173
|
-
assert.equal(out.status, 405, `GET 必须回 405,got ${out.status}`);
|
|
174
|
-
assert.ok(!out.body || !("memories" in out.body), "GET 不许把包交出去");
|
|
175
|
-
} finally { s.close(); }
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("a body this layer cannot read is a 400 with one code, not a pack", async () => {
|
|
179
|
-
const s = shareSetup();
|
|
180
|
-
try {
|
|
181
|
-
const route = shareRoute(s);
|
|
182
|
-
s.store.save({ type: "decision", title: "选 SQLite", content: "正文", tags: [] });
|
|
183
|
-
for (const raw of ["{oops", "[1,2]", "null"]) {
|
|
184
|
-
const out = await callShare(route, { body: raw });
|
|
185
|
-
assert.equal(out.status, 400, `${raw} 必须被拒`);
|
|
186
|
-
assert.equal(out.headers?.["Content-Type"], "application/json; charset=utf-8");
|
|
187
|
-
assert.ok(!("memories" in out.body), "拒答里绝不带包体");
|
|
188
|
-
}
|
|
189
|
-
} finally { s.close(); }
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("an oversize body answers 413 exactly once", async () => {
|
|
193
|
-
const s = shareSetup();
|
|
194
|
-
try {
|
|
195
|
-
const route = shareRoute(s);
|
|
196
|
-
s.store.save({ type: "decision", title: "选 SQLite", content: "正文", tags: [] });
|
|
197
|
-
// readBody 超限时会自己答 413 并把 text 解析成 null。handler 若没有
|
|
198
|
-
// `if (text === null) return`,它会拿 null 继续往下走:parseShareBody(null) 是
|
|
199
|
-
// 合法的「空 body」,于是整库进包、再答一次 200 —— 一个响应两写,客户端按
|
|
200
|
-
// 状态码分支时看到的是最后那一次(#410 修过的同一类故障)。
|
|
201
|
-
const out = await callShare(route, { body: "x".repeat(MAX_BODY_BYTES + 1) });
|
|
202
|
-
assert.equal(out.heads, 1, "超限时只许有一次响应写");
|
|
203
|
-
assert.equal(out.status, 413);
|
|
204
|
-
assert.equal(out.body.error, "payload-too-large");
|
|
205
|
-
} finally { s.close(); }
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test("a throwing pack build answers 500 and leaves a log line", async () => {
|
|
209
|
-
// 不带 logger 也照答 500(shareReply 的 warn 是可选的),但带时必须真的写日志:
|
|
210
|
-
// 否则「500 且无迹可查」这条现场就无从下手。
|
|
211
|
-
const warnings = [];
|
|
212
|
-
const s = shareSetup("secret-token", { logger: { warn: (m) => warnings.push(String(m)) } });
|
|
213
|
-
try {
|
|
214
|
-
const route = shareRoute(s);
|
|
215
|
-
s.service.all = () => { throw new Error("database is locked"); };
|
|
216
|
-
const out = await callShare(route, { body: "" });
|
|
217
|
-
assert.equal(out.status, 500);
|
|
218
|
-
assert.deepEqual(out.body, { error: "internal" }, "对外只有一个码,不带原始错误文本");
|
|
219
|
-
assert.match(warnings.join(" "), /share pack failed/);
|
|
220
|
-
assert.match(warnings.join(" "), /database is locked/, "原始错误文本要留在日志里");
|
|
221
|
-
} finally { s.close(); }
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test("the panel /share chain answers once when the sink fails mid-write", async () => {
|
|
225
|
-
// 车道 A 的这条路由只有外层 try,而外层 try 在 .then 的回调里早已退栈:回调里抛出的错
|
|
226
|
-
// (这里是 sendJson 的 writeHead 抛——真 socket 上「一个响应两写」的第二次就是这个形状)
|
|
227
|
-
// 没人接,就是一个 unhandled rejection 加「一个字节都没回」(#410)。数据面的同名路由
|
|
228
|
-
// (本文件末尾)一直带着那一声 .catch,两条皮在这里本该只差鉴权与路径(#411),
|
|
229
|
-
// 所以补的是面板这一侧。C6 之后 warn 已被共用层守卫包住,日志坏掉那条路走不到这里,
|
|
230
|
-
// 这个 sink 形状是 .catch 唯一还能被独立问到的入口。
|
|
231
|
-
const s = shareSetup();
|
|
232
|
-
try {
|
|
233
|
-
const route = shareRoute(s);
|
|
234
|
-
s.store.save({ type: "decision", title: "选 SQLite", content: "正文 here", tags: [] });
|
|
235
|
-
const out = await callShare(route, { body: "", failFirstHead: true });
|
|
236
|
-
assert.equal(out.rejection, null, "回调里抛出的错必须由链上的 .catch 接住");
|
|
237
|
-
assert.equal(out.status, 500, "sink 坏了也要把答复交出去");
|
|
238
|
-
assert.deepEqual(out.body, { error: "internal" });
|
|
239
|
-
assert.equal(out.heads, 1, "抛掉的那一次没写出去,成功的只许有一次");
|
|
240
|
-
} finally { s.close(); }
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// --- Bearer 数据面(src/api-standalone.js)-----------------------------------
|
|
244
|
-
|
|
245
|
-
function standaloneSetup() {
|
|
246
|
-
const dir = mkdtempSync(join(tmpdir(), "dsh-mneme-share-std-"));
|
|
247
|
-
const store = createStore(join(dir, "memory.db"));
|
|
248
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
249
|
-
const settings = createSettings(store.db);
|
|
250
|
-
// 起法照 test/standalone-api.test.js:这个模块没有 start()/stop(),监听在构造里
|
|
251
|
-
// 发起、由 ready 兑现,端口是 OS 给的(port: 0),token 挂在 api 上。
|
|
252
|
-
const api = createStandaloneApi({ service, store, config: {}, settings, logger: null, port: 0 });
|
|
253
|
-
return api.ready.then(() => {
|
|
254
|
-
const base = `http://127.0.0.1:${api.port}`;
|
|
255
|
-
return {
|
|
256
|
-
api, store, service, base, auth: { authorization: `Bearer ${api.token}` },
|
|
257
|
-
close() {
|
|
258
|
-
// 拆掉 keep-alive 连接,否则 node --test 的循环排不干净(同那份文件)。
|
|
259
|
-
api.server.closeIdleConnections?.();
|
|
260
|
-
api.server.close();
|
|
261
|
-
try { store.close(); } catch { /* already closed */ }
|
|
262
|
-
rmSync(dir, { recursive: true, force: true });
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
test("standalone /share answers the pack for a Bearer caller and 401 otherwise", async () => {
|
|
269
|
-
const h = await standaloneSetup();
|
|
270
|
-
try {
|
|
271
|
-
h.store.save({ type: "decision", title: "选 SQLite", content: "正文 here", tags: [] });
|
|
272
|
-
const anon = await fetch(`${h.base}/share`, { method: "POST", body: "{}" });
|
|
273
|
-
assert.equal(anon.status, 401, "这一面除 GET /health 外一律要 token,没有 policy 名可绕");
|
|
274
|
-
assert.deepEqual(await anon.json(), { error: "unauthorized" });
|
|
275
|
-
|
|
276
|
-
const ok = await fetch(`${h.base}/share`, {
|
|
277
|
-
method: "POST", headers: h.auth, body: JSON.stringify({ alias: "桉" })
|
|
278
|
-
});
|
|
279
|
-
assert.equal(ok.status, 200);
|
|
280
|
-
const pack = await ok.json();
|
|
281
|
-
assert.equal(pack.kind, "mneme.share/v1", "两张皮交出去的是同一个包形状");
|
|
282
|
-
assert.equal(pack.alias, "桉");
|
|
283
|
-
assert.equal(pack.report.included, 1);
|
|
284
|
-
// 本面**没有** Content-Disposition:它的 sendJson 连 headers 参数都没有,CLI 拿
|
|
285
|
-
// JSON 自己落盘。锁「不许冒出来」,防止哪天有人为了对齐面板而改那个签名。
|
|
286
|
-
assert.equal(ok.headers.get("content-disposition"), null);
|
|
287
|
-
|
|
288
|
-
// 读不懂的 body 在这侧同样是 400 一个码,而不是掉回末尾的 404。
|
|
289
|
-
for (const raw of ["{oops", "[1,2]", "null"]) {
|
|
290
|
-
const bad = await fetch(`${h.base}/share`, { method: "POST", headers: h.auth, body: raw });
|
|
291
|
-
assert.equal(bad.status, 400, `${raw} 必须被拒`);
|
|
292
|
-
assert.ok(!("memories" in await bad.json()), "拒答里绝不带包体");
|
|
293
|
-
}
|
|
294
|
-
} finally { await h.close(); }
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test("standalone GET /share answers 405, matching the panel", async () => {
|
|
298
|
-
// 这一条**故意**与 /maintenance/reclaim 不同(那边 GET 掉到末尾 404)。#411 的口径
|
|
299
|
-
// 是「两张皮只许差在鉴权与路径」,状态码差一次就是第三处差别,所以这里跟着面板回
|
|
300
|
-
// 405 —— 一致性优先于照抄这份文件里的邻居。
|
|
301
|
-
const h = await standaloneSetup();
|
|
302
|
-
try {
|
|
303
|
-
h.store.save({ type: "decision", title: "选 SQLite", content: "正文 here", tags: [] });
|
|
304
|
-
const out = await fetch(`${h.base}/share`, { headers: h.auth });
|
|
305
|
-
assert.equal(out.status, 405);
|
|
306
|
-
const body = await out.json();
|
|
307
|
-
assert.ok(!("memories" in body), "GET 不许把包交出去");
|
|
308
|
-
} finally { await h.close(); }
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
test("the standalone /share chain answers once when the sink fails mid-write", async () => {
|
|
312
|
-
// 本面这条链一直带着 .catch,但**从来没有任何用例走到得过它**:共用层从不抛、日志也
|
|
313
|
-
// 被守卫吞掉之后,剩下的入口只有面自己写答复这一步(真 socket 上「一个响应两写」的
|
|
314
|
-
// 第二次 writeHead 就是这里的形状)。与上面面板那条同名用例是一对——两张皮的 .catch
|
|
315
|
-
// 各锁各的,少一边就只有一边会红(#411)。假 sink 第一次抛、第二次正常;等得到答复
|
|
316
|
-
// 就说明链上有主,等不到(输给超时)就是 #410 的「一个字节都没回」。
|
|
317
|
-
const h = await standaloneSetup();
|
|
318
|
-
try {
|
|
319
|
-
h.store.save({ type: "decision", title: "选 SQLite", content: "正文 here", tags: [] });
|
|
320
|
-
const handlers = h.api.server.listeners("request");
|
|
321
|
-
assert.equal(handlers.length, 1, "这台面上只许挂着一个 request handler,否则不知道在驱动谁");
|
|
322
|
-
const req = new EventEmitter();
|
|
323
|
-
req.method = "POST";
|
|
324
|
-
req.url = "/share";
|
|
325
|
-
req.headers = { authorization: `Bearer ${h.api.token}` };
|
|
326
|
-
const res = new FakeRes({ failFirstHead: true });
|
|
327
|
-
handlers[0](req, res);
|
|
328
|
-
process.nextTick(() => {
|
|
329
|
-
req.emit("data", Buffer.from("{}"));
|
|
330
|
-
req.emit("end");
|
|
331
|
-
});
|
|
332
|
-
const settled = await Promise.race([res.settled, new Promise((r) => setTimeout(() => r("timeout"), 3000))]);
|
|
333
|
-
assert.equal(settled, "ended", "缺了 .catch 就永远等不到答复");
|
|
334
|
-
assert.equal(res.statusCode, 500);
|
|
335
|
-
assert.equal(res.heads, 1, "抛掉的那一次没写出去,成功的只许有一次");
|
|
336
|
-
} finally { await h.close(); }
|
|
337
|
-
});
|
|
1
|
+
// 分享包的**两张皮**路由锁。盯四件事:
|
|
2
|
+
// a) policy 归对(token —— 它是全库级读取,与 /export 同门,见 2026-10 审计 P0);
|
|
3
|
+
// b) token 配上时**不带 token 必须 401**,不是静默放行;
|
|
4
|
+
// c) readBody 返回 null(超 4 MiB,它自己已答 413)时 handler 必须直接 return,
|
|
5
|
+
// 不能再答一次 —— 双写响应是 #410 那一类故障;
|
|
6
|
+
// d) 500 必须留下日志:shareReply 把抛错兜成 500,不接 warn 回调时那条 500 是静默的
|
|
7
|
+
// (现场只剩「内部错误」,查不出是哪一次打包炸的)。同款契约见 reclaim 的 500 用例。
|
|
8
|
+
// 装配台照 test/api-routes-gaps.test.js 的 reclaimSetup,**必须落文件库**:`:memory:`
|
|
9
|
+
// 与文件库在 store.all() 的读侧口径上没差别,但分享包吃的正是「全库含归档/已遗忘」,
|
|
10
|
+
// 用内存库时归档位的字节口径与体积水位都摸不到真实数字(同 reclaim 报告的理由)。
|
|
11
|
+
//
|
|
12
|
+
// 后半截是 Bearer 数据面(src/api-standalone.js)的真 HTTP 装配,起服务与读 token 的
|
|
13
|
+
// 形状照 test/standalone-api.test.js。放同一份文件里是有意的:#411 说两张皮只许差在
|
|
14
|
+
// 鉴权与路径,那么「同一份答复」就是两侧共同的不变量——分开两个文件时,改一侧的人
|
|
15
|
+
// 看不见另一侧锁着什么。
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { EventEmitter } from "node:events";
|
|
19
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { createStore } from "../src/store.js";
|
|
23
|
+
import { createService } from "../src/service.js";
|
|
24
|
+
import { createSettings } from "../src/settings.js";
|
|
25
|
+
import { createApi } from "../src/api.js";
|
|
26
|
+
import { createStandaloneApi } from "../src/api-standalone.js";
|
|
27
|
+
|
|
28
|
+
const SHARE_PATH = "/api/dsh-mneme/share";
|
|
29
|
+
const MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
30
|
+
|
|
31
|
+
class FakeRes extends EventEmitter {
|
|
32
|
+
constructor({ failFirstHead = false } = {}) {
|
|
33
|
+
super();
|
|
34
|
+
this.statusCode = 0;
|
|
35
|
+
this.body = "";
|
|
36
|
+
this.headers = null;
|
|
37
|
+
// writeHead 计数:双写响应的故障形态是「第二次 writeHead 覆盖掉第一次的状态码」,
|
|
38
|
+
// 只看最终 statusCode 抓不住(413 之后又被 200 盖掉时 body 也换了),要数次数。
|
|
39
|
+
this.heads = 0;
|
|
40
|
+
// 一次性坏掉的 sink:真 socket 上「一个响应两写」的现场形态就是第二次 writeHead 抛
|
|
41
|
+
// ERR_HTTP_HEADERS_SENT(Node 的 writeHead 当场把头发出去)。这里让第一次抛、第二次
|
|
42
|
+
// 正常,好把「promise 链上的错误到底有没有人接」单独问出来;抛掉的那一次不计数,
|
|
43
|
+
// 因为它一个字节都没写出去。
|
|
44
|
+
this.failFirstHead = failFirstHead;
|
|
45
|
+
// 数据面的 handler 不返回 promise(那条链是 `void readBody(...)`),所以等答复只能靠
|
|
46
|
+
// 这一声 end:配超时竞速,缺 .catch 时用例红得干脆,而不是把文件挂死在无限超时上。
|
|
47
|
+
this.settled = new Promise((resolve) => { this.settle = resolve; });
|
|
48
|
+
}
|
|
49
|
+
writeHead(code, headers) {
|
|
50
|
+
if (this.failFirstHead) {
|
|
51
|
+
this.failFirstHead = false;
|
|
52
|
+
throw new Error("sink on fire");
|
|
53
|
+
}
|
|
54
|
+
this.statusCode = code;
|
|
55
|
+
this.headers = headers;
|
|
56
|
+
this.heads += 1;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
end(text) {
|
|
60
|
+
this.body = text ?? "";
|
|
61
|
+
this.emit("end");
|
|
62
|
+
this.settle?.("ended");
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function shareSetup(apiToken = "secret-token", { logger = null } = {}) {
|
|
68
|
+
const dir = mkdtempSync(join(tmpdir(), "dsh-mneme-share-api-"));
|
|
69
|
+
const store = createStore(join(dir, "memory.db"));
|
|
70
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
71
|
+
const settings = createSettings(store.db);
|
|
72
|
+
const commands = { add: () => {}, remove: () => {}, list: () => [] };
|
|
73
|
+
const routes = [];
|
|
74
|
+
const ctx = { logger, webServer: { register(route) { routes.push(route); return () => {}; } } };
|
|
75
|
+
createApi(ctx, service, settings, commands, null, undefined, apiToken, null, null);
|
|
76
|
+
return {
|
|
77
|
+
store, service, routes,
|
|
78
|
+
// 闭包拿 store,不用 this:裸函数调用下 this 是 undefined,异常被吞掉就会变成
|
|
79
|
+
// 「库没关就删目录」的 EPERM(清理失败还说不清原因)。
|
|
80
|
+
close() {
|
|
81
|
+
try { store.close(); } catch { /* already closed */ }
|
|
82
|
+
rmSync(dir, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 字节必须在 listener 挂上之后再 emit:EventEmitter 不排队,在 `route.handler` 里
|
|
89
|
+
* readBody 注册 data/end 之前 emit 出去等于什么都没发,readBody 的 promise 永不
|
|
90
|
+
* resolve,整个用例会挂到超时——失败原因还完全看不出跟分享有关。
|
|
91
|
+
*/
|
|
92
|
+
function shareReq({ method = "POST", url = SHARE_PATH, body = "", authorized = true } = {}) {
|
|
93
|
+
const req = new EventEmitter();
|
|
94
|
+
req.method = method;
|
|
95
|
+
req.url = url;
|
|
96
|
+
req.headers = authorized ? { authorization: "Bearer secret-token" } : {};
|
|
97
|
+
process.nextTick(() => {
|
|
98
|
+
req.emit("data", Buffer.from(body));
|
|
99
|
+
req.emit("end");
|
|
100
|
+
});
|
|
101
|
+
return req;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function callShare(route, opts = {}) {
|
|
105
|
+
const res = new FakeRes({ failFirstHead: opts.failFirstHead === true });
|
|
106
|
+
let rejection = null;
|
|
107
|
+
try {
|
|
108
|
+
await route.handler(shareReq(opts), res);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// 把 rejection 收成读数而不是让它冒出去:少了 .catch 时这里就是 #410 的形状——
|
|
111
|
+
// 断言「rejection 必须为 null」比让用例以一个看不懂的异常挂掉说得清得多。
|
|
112
|
+
rejection = String(error);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
status: res.statusCode,
|
|
116
|
+
body: res.body ? JSON.parse(res.body) : null,
|
|
117
|
+
headers: res.headers,
|
|
118
|
+
heads: res.heads,
|
|
119
|
+
rejection
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function shareRoute(setup) {
|
|
124
|
+
const route = setup.routes.find((r) => r.path === SHARE_PATH);
|
|
125
|
+
assert.ok(route, "route must exist");
|
|
126
|
+
return route;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
test("share route is registered with policy token", () => {
|
|
130
|
+
const s = shareSetup();
|
|
131
|
+
try {
|
|
132
|
+
const route = shareRoute(s);
|
|
133
|
+
assert.equal(route.policy, "token");
|
|
134
|
+
assert.equal(route.kind, "exact");
|
|
135
|
+
} finally { s.close(); }
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("a configured token gates the share route", async () => {
|
|
139
|
+
const s = shareSetup();
|
|
140
|
+
try {
|
|
141
|
+
const route = shareRoute(s);
|
|
142
|
+
s.store.save({ type: "decision", title: "选 SQLite", content: "正文", tags: [] });
|
|
143
|
+
const denied = await callShare(route, { authorized: false, body: "" });
|
|
144
|
+
assert.equal(denied.status, 401);
|
|
145
|
+
const allowed = await callShare(route, { authorized: true, body: "" });
|
|
146
|
+
assert.equal(allowed.status, 200);
|
|
147
|
+
const pack = allowed.body;
|
|
148
|
+
assert.equal(pack.kind, "mneme.share/v1");
|
|
149
|
+
assert.equal(pack.report.included, 1);
|
|
150
|
+
assert.ok(!("id" in pack.memories[0]));
|
|
151
|
+
// 本侧 sendJson 有 headers 第四参(数据面没有):Content-Disposition 是面板这张脸
|
|
152
|
+
// 独有的那半,共用层只回 {status, body},文件名由这里拼——锁它,防止哪天把
|
|
153
|
+
// shareFilename 的连接也一起「统一」掉。
|
|
154
|
+
assert.match(allowed.headers["Content-Disposition"], /^attachment; filename="dsh-mneme-share-\d{8}-[0-9a-f]{8}\.json"$/);
|
|
155
|
+
// 空 body 与「一个字节都不发」同口径:分享的主用法就是按整库口径打。
|
|
156
|
+
const alias = await callShare(route, { body: JSON.stringify({ alias: "桉桉的库" }) });
|
|
157
|
+
assert.equal(alias.status, 200);
|
|
158
|
+
assert.equal(alias.body.alias, "桉桉的库");
|
|
159
|
+
assert.match(alias.body.source_version, /\d/, "包要记下发送方版本,对面按它判兼容");
|
|
160
|
+
assert.ok(Number.isFinite(Date.parse(alias.body.generated_at)));
|
|
161
|
+
} finally { s.close(); }
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("GET on the share route is not an open read", async () => {
|
|
165
|
+
const s = shareSetup();
|
|
166
|
+
try {
|
|
167
|
+
const route = shareRoute(s);
|
|
168
|
+
s.store.save({ type: "decision", title: "选 SQLite", content: "正文", tags: [] });
|
|
169
|
+
const out = await callShare(route, { method: "GET", body: "" });
|
|
170
|
+
// 状态码锁死 405,不留 404 这个后门:#411 说两张皮只许差在鉴权与路径,
|
|
171
|
+
// 而数据面那半(本文件末尾)已经只认 405。这里容忍 404 就等于让「面板哪天
|
|
172
|
+
// 掉回末尾 404」也算通过——两张皮悄悄多出一处差别,没人报警。
|
|
173
|
+
assert.equal(out.status, 405, `GET 必须回 405,got ${out.status}`);
|
|
174
|
+
assert.ok(!out.body || !("memories" in out.body), "GET 不许把包交出去");
|
|
175
|
+
} finally { s.close(); }
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("a body this layer cannot read is a 400 with one code, not a pack", async () => {
|
|
179
|
+
const s = shareSetup();
|
|
180
|
+
try {
|
|
181
|
+
const route = shareRoute(s);
|
|
182
|
+
s.store.save({ type: "decision", title: "选 SQLite", content: "正文", tags: [] });
|
|
183
|
+
for (const raw of ["{oops", "[1,2]", "null"]) {
|
|
184
|
+
const out = await callShare(route, { body: raw });
|
|
185
|
+
assert.equal(out.status, 400, `${raw} 必须被拒`);
|
|
186
|
+
assert.equal(out.headers?.["Content-Type"], "application/json; charset=utf-8");
|
|
187
|
+
assert.ok(!("memories" in out.body), "拒答里绝不带包体");
|
|
188
|
+
}
|
|
189
|
+
} finally { s.close(); }
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("an oversize body answers 413 exactly once", async () => {
|
|
193
|
+
const s = shareSetup();
|
|
194
|
+
try {
|
|
195
|
+
const route = shareRoute(s);
|
|
196
|
+
s.store.save({ type: "decision", title: "选 SQLite", content: "正文", tags: [] });
|
|
197
|
+
// readBody 超限时会自己答 413 并把 text 解析成 null。handler 若没有
|
|
198
|
+
// `if (text === null) return`,它会拿 null 继续往下走:parseShareBody(null) 是
|
|
199
|
+
// 合法的「空 body」,于是整库进包、再答一次 200 —— 一个响应两写,客户端按
|
|
200
|
+
// 状态码分支时看到的是最后那一次(#410 修过的同一类故障)。
|
|
201
|
+
const out = await callShare(route, { body: "x".repeat(MAX_BODY_BYTES + 1) });
|
|
202
|
+
assert.equal(out.heads, 1, "超限时只许有一次响应写");
|
|
203
|
+
assert.equal(out.status, 413);
|
|
204
|
+
assert.equal(out.body.error, "payload-too-large");
|
|
205
|
+
} finally { s.close(); }
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("a throwing pack build answers 500 and leaves a log line", async () => {
|
|
209
|
+
// 不带 logger 也照答 500(shareReply 的 warn 是可选的),但带时必须真的写日志:
|
|
210
|
+
// 否则「500 且无迹可查」这条现场就无从下手。
|
|
211
|
+
const warnings = [];
|
|
212
|
+
const s = shareSetup("secret-token", { logger: { warn: (m) => warnings.push(String(m)) } });
|
|
213
|
+
try {
|
|
214
|
+
const route = shareRoute(s);
|
|
215
|
+
s.service.all = () => { throw new Error("database is locked"); };
|
|
216
|
+
const out = await callShare(route, { body: "" });
|
|
217
|
+
assert.equal(out.status, 500);
|
|
218
|
+
assert.deepEqual(out.body, { error: "internal" }, "对外只有一个码,不带原始错误文本");
|
|
219
|
+
assert.match(warnings.join(" "), /share pack failed/);
|
|
220
|
+
assert.match(warnings.join(" "), /database is locked/, "原始错误文本要留在日志里");
|
|
221
|
+
} finally { s.close(); }
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("the panel /share chain answers once when the sink fails mid-write", async () => {
|
|
225
|
+
// 车道 A 的这条路由只有外层 try,而外层 try 在 .then 的回调里早已退栈:回调里抛出的错
|
|
226
|
+
// (这里是 sendJson 的 writeHead 抛——真 socket 上「一个响应两写」的第二次就是这个形状)
|
|
227
|
+
// 没人接,就是一个 unhandled rejection 加「一个字节都没回」(#410)。数据面的同名路由
|
|
228
|
+
// (本文件末尾)一直带着那一声 .catch,两条皮在这里本该只差鉴权与路径(#411),
|
|
229
|
+
// 所以补的是面板这一侧。C6 之后 warn 已被共用层守卫包住,日志坏掉那条路走不到这里,
|
|
230
|
+
// 这个 sink 形状是 .catch 唯一还能被独立问到的入口。
|
|
231
|
+
const s = shareSetup();
|
|
232
|
+
try {
|
|
233
|
+
const route = shareRoute(s);
|
|
234
|
+
s.store.save({ type: "decision", title: "选 SQLite", content: "正文 here", tags: [] });
|
|
235
|
+
const out = await callShare(route, { body: "", failFirstHead: true });
|
|
236
|
+
assert.equal(out.rejection, null, "回调里抛出的错必须由链上的 .catch 接住");
|
|
237
|
+
assert.equal(out.status, 500, "sink 坏了也要把答复交出去");
|
|
238
|
+
assert.deepEqual(out.body, { error: "internal" });
|
|
239
|
+
assert.equal(out.heads, 1, "抛掉的那一次没写出去,成功的只许有一次");
|
|
240
|
+
} finally { s.close(); }
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// --- Bearer 数据面(src/api-standalone.js)-----------------------------------
|
|
244
|
+
|
|
245
|
+
function standaloneSetup() {
|
|
246
|
+
const dir = mkdtempSync(join(tmpdir(), "dsh-mneme-share-std-"));
|
|
247
|
+
const store = createStore(join(dir, "memory.db"));
|
|
248
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
249
|
+
const settings = createSettings(store.db);
|
|
250
|
+
// 起法照 test/standalone-api.test.js:这个模块没有 start()/stop(),监听在构造里
|
|
251
|
+
// 发起、由 ready 兑现,端口是 OS 给的(port: 0),token 挂在 api 上。
|
|
252
|
+
const api = createStandaloneApi({ service, store, config: {}, settings, logger: null, port: 0 });
|
|
253
|
+
return api.ready.then(() => {
|
|
254
|
+
const base = `http://127.0.0.1:${api.port}`;
|
|
255
|
+
return {
|
|
256
|
+
api, store, service, base, auth: { authorization: `Bearer ${api.token}` },
|
|
257
|
+
close() {
|
|
258
|
+
// 拆掉 keep-alive 连接,否则 node --test 的循环排不干净(同那份文件)。
|
|
259
|
+
api.server.closeIdleConnections?.();
|
|
260
|
+
api.server.close();
|
|
261
|
+
try { store.close(); } catch { /* already closed */ }
|
|
262
|
+
rmSync(dir, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
test("standalone /share answers the pack for a Bearer caller and 401 otherwise", async () => {
|
|
269
|
+
const h = await standaloneSetup();
|
|
270
|
+
try {
|
|
271
|
+
h.store.save({ type: "decision", title: "选 SQLite", content: "正文 here", tags: [] });
|
|
272
|
+
const anon = await fetch(`${h.base}/share`, { method: "POST", body: "{}" });
|
|
273
|
+
assert.equal(anon.status, 401, "这一面除 GET /health 外一律要 token,没有 policy 名可绕");
|
|
274
|
+
assert.deepEqual(await anon.json(), { error: "unauthorized" });
|
|
275
|
+
|
|
276
|
+
const ok = await fetch(`${h.base}/share`, {
|
|
277
|
+
method: "POST", headers: h.auth, body: JSON.stringify({ alias: "桉" })
|
|
278
|
+
});
|
|
279
|
+
assert.equal(ok.status, 200);
|
|
280
|
+
const pack = await ok.json();
|
|
281
|
+
assert.equal(pack.kind, "mneme.share/v1", "两张皮交出去的是同一个包形状");
|
|
282
|
+
assert.equal(pack.alias, "桉");
|
|
283
|
+
assert.equal(pack.report.included, 1);
|
|
284
|
+
// 本面**没有** Content-Disposition:它的 sendJson 连 headers 参数都没有,CLI 拿
|
|
285
|
+
// JSON 自己落盘。锁「不许冒出来」,防止哪天有人为了对齐面板而改那个签名。
|
|
286
|
+
assert.equal(ok.headers.get("content-disposition"), null);
|
|
287
|
+
|
|
288
|
+
// 读不懂的 body 在这侧同样是 400 一个码,而不是掉回末尾的 404。
|
|
289
|
+
for (const raw of ["{oops", "[1,2]", "null"]) {
|
|
290
|
+
const bad = await fetch(`${h.base}/share`, { method: "POST", headers: h.auth, body: raw });
|
|
291
|
+
assert.equal(bad.status, 400, `${raw} 必须被拒`);
|
|
292
|
+
assert.ok(!("memories" in await bad.json()), "拒答里绝不带包体");
|
|
293
|
+
}
|
|
294
|
+
} finally { await h.close(); }
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("standalone GET /share answers 405, matching the panel", async () => {
|
|
298
|
+
// 这一条**故意**与 /maintenance/reclaim 不同(那边 GET 掉到末尾 404)。#411 的口径
|
|
299
|
+
// 是「两张皮只许差在鉴权与路径」,状态码差一次就是第三处差别,所以这里跟着面板回
|
|
300
|
+
// 405 —— 一致性优先于照抄这份文件里的邻居。
|
|
301
|
+
const h = await standaloneSetup();
|
|
302
|
+
try {
|
|
303
|
+
h.store.save({ type: "decision", title: "选 SQLite", content: "正文 here", tags: [] });
|
|
304
|
+
const out = await fetch(`${h.base}/share`, { headers: h.auth });
|
|
305
|
+
assert.equal(out.status, 405);
|
|
306
|
+
const body = await out.json();
|
|
307
|
+
assert.ok(!("memories" in body), "GET 不许把包交出去");
|
|
308
|
+
} finally { await h.close(); }
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("the standalone /share chain answers once when the sink fails mid-write", async () => {
|
|
312
|
+
// 本面这条链一直带着 .catch,但**从来没有任何用例走到得过它**:共用层从不抛、日志也
|
|
313
|
+
// 被守卫吞掉之后,剩下的入口只有面自己写答复这一步(真 socket 上「一个响应两写」的
|
|
314
|
+
// 第二次 writeHead 就是这里的形状)。与上面面板那条同名用例是一对——两张皮的 .catch
|
|
315
|
+
// 各锁各的,少一边就只有一边会红(#411)。假 sink 第一次抛、第二次正常;等得到答复
|
|
316
|
+
// 就说明链上有主,等不到(输给超时)就是 #410 的「一个字节都没回」。
|
|
317
|
+
const h = await standaloneSetup();
|
|
318
|
+
try {
|
|
319
|
+
h.store.save({ type: "decision", title: "选 SQLite", content: "正文 here", tags: [] });
|
|
320
|
+
const handlers = h.api.server.listeners("request");
|
|
321
|
+
assert.equal(handlers.length, 1, "这台面上只许挂着一个 request handler,否则不知道在驱动谁");
|
|
322
|
+
const req = new EventEmitter();
|
|
323
|
+
req.method = "POST";
|
|
324
|
+
req.url = "/share";
|
|
325
|
+
req.headers = { authorization: `Bearer ${h.api.token}` };
|
|
326
|
+
const res = new FakeRes({ failFirstHead: true });
|
|
327
|
+
handlers[0](req, res);
|
|
328
|
+
process.nextTick(() => {
|
|
329
|
+
req.emit("data", Buffer.from("{}"));
|
|
330
|
+
req.emit("end");
|
|
331
|
+
});
|
|
332
|
+
const settled = await Promise.race([res.settled, new Promise((r) => setTimeout(() => r("timeout"), 3000))]);
|
|
333
|
+
assert.equal(settled, "ended", "缺了 .catch 就永远等不到答复");
|
|
334
|
+
assert.equal(res.statusCode, 500);
|
|
335
|
+
assert.equal(res.heads, 1, "抛掉的那一次没写出去,成功的只许有一次");
|
|
336
|
+
} finally { await h.close(); }
|
|
337
|
+
});
|