@modusensus/dsh-mneme 0.13.0 → 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 -828
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +964 -964
- package/bin/dsh-mneme-choro.mjs +262 -254
- 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 -402
- 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 -3394
- 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 -13436
- package/lib/commands.js +95 -95
- package/lib/config-keys.js +144 -144
- package/lib/config.js +1361 -1317
- 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 -907
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +2027 -1887
- package/lib/dream/triage.js +139 -0
- package/lib/dream.js +1879 -1764
- 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 -107
- package/lib/image-describe.js +158 -0
- package/lib/index.js +852 -822
- package/lib/inject.js +656 -639
- 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 -584
- 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 -3196
- package/lib/session-gate.js +91 -91
- package/lib/session-usage.js +350 -350
- package/lib/settings.js +827 -802
- 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 -5662
- 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 -0
- 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 -474
- package/lib/wiki/render.js +261 -187
- package/lib/wiki/scan.js +371 -371
- package/lib/wiki/t0.js +500 -463
- package/lib/wiki/t1-jobs.js +232 -232
- package/lib/wiki/t1.js +609 -502
- package/lib/write-admission.js +404 -404
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +15 -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 -204
- package/scripts/release-notes.mjs +164 -161
- 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 -3394
- 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 -1317
- 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 -907
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +2027 -1887
- package/src/dream/triage.js +139 -0
- package/src/dream.js +1879 -1764
- 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 -107
- package/src/image-describe.js +158 -0
- package/src/index.js +852 -822
- package/src/inject.js +656 -639
- 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 -584
- 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 -3196
- package/src/session-gate.js +91 -91
- package/src/session-usage.js +350 -350
- package/src/settings.js +827 -802
- 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 -5662
- 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 -0
- 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 -474
- package/src/wiki/render.js +261 -187
- package/src/wiki/scan.js +371 -371
- package/src/wiki/t0.js +500 -463
- package/src/wiki/t1-jobs.js +232 -232
- package/src/wiki/t1.js +609 -502
- 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 -2988
- 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 -4184
- 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 -0
- package/test/conflict-ledger.test.js +242 -0
- 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 -320
- 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 -0
- 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 -209
- 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 -211
- 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 -106
- 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 -494
- 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 -0
- package/test/panel-tokens.test.js +245 -0
- 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 -136
- package/test/release-notes.test.js +265 -240
- 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 -178
- 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 -1418
- 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 -627
- package/test/wiki-claims.test.js +194 -0
- 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 -0
- package/test/wiki-queries.test.js +398 -277
- package/test/wiki-scan.test.js +208 -208
- package/test/wiki-store.test.js +190 -190
- package/test/wiki-t0.test.js +456 -380
- package/test/wiki-t1-jobs.test.js +125 -125
- package/test/wiki-t1.test.js +460 -235
- 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,317 +1,317 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 会话日志使用证据(src/session-usage.js)的回归测试。
|
|
3
|
-
*
|
|
4
|
-
* 本文件锁的重点是**两类「静默少一大截」的坑**(都真踩过):
|
|
5
|
-
* ① **zstd 多帧**:`zlib.zstdDecompressSync(buf)` 只解**第一帧**且不报错——实测一个
|
|
6
|
-
* 1.5 MB 的会话文件只解出 291 字节,于是扫描得出「零命中」的假结论。所以这里用
|
|
7
|
-
* **两帧拼接的夹具**钉死「两帧都要解出来」。
|
|
8
|
-
* ② **参数在 `arguments` 且是 JSON 字符串**:取 `parameters` 会得到 null,看着像
|
|
9
|
-
* 「调用了但没记参数」。夹具按真实形态写。
|
|
10
|
-
*
|
|
11
|
-
* 另外钉住状态推导的语义边界:`candidate` 是「现存日志里没记录」,不是「从未使用」。
|
|
12
|
-
*/
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from "node:fs";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
import { tmpdir } from "node:os";
|
|
18
|
-
import zlib from "node:zlib";
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
forEachFrame,
|
|
22
|
-
toolCallsInLine,
|
|
23
|
-
identifyCapability,
|
|
24
|
-
deriveUsageState,
|
|
25
|
-
collectUsage,
|
|
26
|
-
summarizeUsage,
|
|
27
|
-
resolveSessionDir,
|
|
28
|
-
findSessionFiles,
|
|
29
|
-
STALE_AFTER_DAYS
|
|
30
|
-
} from "../src/session-usage.js";
|
|
31
|
-
|
|
32
|
-
const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
33
|
-
|
|
34
|
-
/** 把若干段文本各压成一帧再拼起来,模拟真实的多帧文件。 */
|
|
35
|
-
function multiFrameBuffer(chunks) {
|
|
36
|
-
return Buffer.concat(chunks.map((c) => zlib.zstdCompressSync(Buffer.from(c, "utf8"))));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** 造一行真实的 tool-call 记录(参数是 JSON 字符串,不是对象)。 */
|
|
40
|
-
function toolCallLine({ name, args, turn = 1, time = 1700000000000 }) {
|
|
41
|
-
const item = { type: "tool-call", id: `call_${Math.random().toString(36).slice(2)}`, name };
|
|
42
|
-
if (args !== undefined) item.arguments = JSON.stringify(args);
|
|
43
|
-
return JSON.stringify({
|
|
44
|
-
type: "assistant/message",
|
|
45
|
-
time,
|
|
46
|
-
data: { turn, step: 1, message: { role: "assistant", content: [item] } }
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** 造一个会话目录,内容按给定数组写进一个文件。 */
|
|
51
|
-
function makeSessionDir(lines) {
|
|
52
|
-
const base = mkdtempSync(join(tmpdir(), "mneme-usage-"));
|
|
53
|
-
const sessionId = "session-test-0001";
|
|
54
|
-
const dir = join(base, "sessions", `--work-${sessionId.slice(0, 8)}--`, sessionId);
|
|
55
|
-
mkdirSync(dir, { recursive: true });
|
|
56
|
-
writeFileSync(join(dir, "session.v4.jsonl.zstd"), multiFrameBuffer(lines), "utf8");
|
|
57
|
-
return { base, dir: join(base, "sessions"), cleanup: () => rmSync(base, { recursive: true, force: true }) };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
test("多帧 zstd:**每一帧**都要解出来(只解第一帧是踩过的坑)", () => {
|
|
61
|
-
const buf = multiFrameBuffer(["第一帧的内容", "第二帧的内容", "第三帧的内容"]);
|
|
62
|
-
const seen = [];
|
|
63
|
-
const stat = forEachFrame(buf, (text) => seen.push(text));
|
|
64
|
-
assert.deepEqual(seen, ["第一帧的内容", "第二帧的内容", "第三帧的内容"], "三帧都要被回调到");
|
|
65
|
-
assert.equal(stat.frames, 3);
|
|
66
|
-
assert.equal(stat.failed, 0);
|
|
67
|
-
// 反证:证明这个夹具真能暴露「只解第一帧」——数帧头,不依赖 Node 的 zstd 语义。
|
|
68
|
-
// 原来这行是 `zstdDecompressSync(buf)` 只解出第一段;Node 24.22.0 起整块解会吐出全部帧
|
|
69
|
-
// (nodejs/node#65865 让解码跨 chunk 边界),反证随之过期,所以改成数 magic。
|
|
70
|
-
// 注意这行锁的是「切帧判据仍是 magic」,**不是碰撞免疫**:payload 里撞上 magic 时
|
|
71
|
-
// 计数器与解码器会一起错(半片也解得出来),实测 frames 同样变成 4,这条测不出来。
|
|
72
|
-
let frameHeaders = 0;
|
|
73
|
-
for (let at = 0; (at = buf.indexOf(ZSTD_MAGIC, at)) !== -1; at += ZSTD_MAGIC.length) frameHeaders += 1;
|
|
74
|
-
assert.equal(frameHeaders, 3, "夹具必须是三帧,否则上面测不到「只解第一帧」");
|
|
75
|
-
// 帧头数 = 成功数 + 失败数,是实现的契约:`forEachFrame` 用 magic 切帧,每段要么解开要么记失败。
|
|
76
|
-
// 写成 `stat.frames` 只在 failed 恒为 0 时成立,那是巧合不是规矩。
|
|
77
|
-
assert.equal(frameHeaders, stat.frames + stat.failed, "帧头数必须交代得清:每一帧要么解开要么记失败");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("多帧 zstd:每帧都解;截断产生的**半行**由 jsonl 层丢掉(宁可少一条,不要错一条)", () => {
|
|
81
|
-
// 真实的「坏帧」不是帧头损坏,而是**文件正在写**:末帧被截断。
|
|
82
|
-
// 实测结论(写这条时验过):zstd 是流式格式,截断帧会**解出部分文本而不抛错**,
|
|
83
|
-
// 且 Node 的绑定不暴露「还有多少输入未消费」,所以帧层的完整性判不了。
|
|
84
|
-
// 兜底放在**行层**:半行 JSON 解析失败会被丢弃——代价是少一条记录,收益是绝不误用。
|
|
85
|
-
const good = zlib.zstdCompressSync(Buffer.from("完整帧的内容", "utf8"));
|
|
86
|
-
|
|
87
|
-
// 帧层:两帧都要回调到(这是最初踩的坑——只解第一帧)
|
|
88
|
-
const seen = [];
|
|
89
|
-
const stat = forEachFrame(Buffer.concat([good, good]), (t) => seen.push(t));
|
|
90
|
-
assert.equal(stat.frames, 2);
|
|
91
|
-
assert.equal(seen.length, 2);
|
|
92
|
-
|
|
93
|
-
// 行层兜底:一个被截断的 tool-call 行不能被当完整记录用
|
|
94
|
-
const fullLine = toolCallLine({ name: "skill", args: { name: "agent-reach" } });
|
|
95
|
-
assert.equal(toolCallsInLine(fullLine).length, 1, "完整行要解析出来");
|
|
96
|
-
const halfLine = fullLine.slice(0, Math.floor(fullLine.length / 2));
|
|
97
|
-
assert.deepEqual(toolCallsInLine(halfLine), [], "半行必须被丢掉,不能猜出一个不完整的调用");
|
|
98
|
-
|
|
99
|
-
// 真实截断文件:整帧 + 半帧 → 完整的那些记录照常统计,半行被丢
|
|
100
|
-
const base = mkdtempSync(join(tmpdir(), "mneme-usage-trunc-"));
|
|
101
|
-
try {
|
|
102
|
-
const sessionId = "session-trunc-0001";
|
|
103
|
-
const dir = join(base, "sessions", "work", sessionId);
|
|
104
|
-
mkdirSync(dir, { recursive: true });
|
|
105
|
-
const truncated = zlib.zstdCompressSync(Buffer.from(fullLine, "utf8")).subarray(0, 12);
|
|
106
|
-
writeFileSync(join(dir, "session.v4.jsonl.zstd"), Buffer.concat([good, truncated]));
|
|
107
|
-
const c = collectUsage({ sessionDir: join(base, "sessions") });
|
|
108
|
-
assert.equal(c.scannedFiles, 1);
|
|
109
|
-
// 好帧里没有 tool-call,截断帧里的调用不完整 → 一条都不该被记
|
|
110
|
-
assert.equal(c.skillCalls, 0, "截断的半行不能被算成一次调用");
|
|
111
|
-
} finally {
|
|
112
|
-
rmSync(base, { recursive: true, force: true });
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("tool-call 解析:参数在 arguments 且是 JSON 字符串", () => {
|
|
117
|
-
// 真实形态:{"type":"tool-call","name":"skill","arguments":"{\"name\":\"agent-reach\"}"}
|
|
118
|
-
const line = toolCallLine({ name: "skill", args: { name: "agent-reach" }, turn: 7, time: 1792000000000 });
|
|
119
|
-
const calls = toolCallsInLine(line);
|
|
120
|
-
assert.equal(calls.length, 1);
|
|
121
|
-
assert.equal(calls[0].name, "skill");
|
|
122
|
-
assert.equal(calls[0].args.name, "agent-reach", "arguments 要二次解析成对象");
|
|
123
|
-
assert.equal(calls[0].turn, 7);
|
|
124
|
-
assert.equal(calls[0].time, 1792000000000);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("tool-call 解析:坏 JSON、非 tool-call 行、参数缺失都要能分清", () => {
|
|
128
|
-
assert.deepEqual(toolCallsInLine("{ 这不是 JSON"), []);
|
|
129
|
-
assert.deepEqual(toolCallsInLine(JSON.stringify({ type: "user/message" })), []);
|
|
130
|
-
// 参数缺失:**调用本身要保留**(它确实发生过),只是 args 为 null
|
|
131
|
-
const noArgs = toolCallLine({ name: "skill" });
|
|
132
|
-
const calls = toolCallsInLine(noArgs);
|
|
133
|
-
assert.equal(calls.length, 1);
|
|
134
|
-
assert.equal(calls[0].args, null);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("能力识别:skill 取 arguments.name;MCP 取 mcp__<server>__<tool> 的 server 段", () => {
|
|
138
|
-
assert.deepEqual(identifyCapability({ name: "skill", args: { name: "agent-reach" } }), {
|
|
139
|
-
kind: "skill",
|
|
140
|
-
name: "agent-reach"
|
|
141
|
-
});
|
|
142
|
-
const mcp = identifyCapability({ name: "mcp__github__get_file_contents", args: {} });
|
|
143
|
-
assert.equal(mcp.kind, "mcp");
|
|
144
|
-
assert.equal(mcp.name, "github", "MCP 按 server 聚合(清单里的条目是 server 粒度)");
|
|
145
|
-
assert.equal(mcp.tool, "get_file_contents");
|
|
146
|
-
// 内置工具不是能力清单里的东西 → null(丢掉,不计 unmatched)
|
|
147
|
-
assert.equal(identifyCapability({ name: "read", args: {} }), null);
|
|
148
|
-
assert.equal(identifyCapability({ name: "pwsh", args: {} }), null);
|
|
149
|
-
// skill 没记 name → 返回 name:null,让上层计入 unmatched(如实报,不硬凑)
|
|
150
|
-
assert.deepEqual(identifyCapability({ name: "skill", args: {} }), { kind: "skill", name: null });
|
|
151
|
-
// mcp__ 后面 server 段为空同理
|
|
152
|
-
assert.equal(identifyCapability({ name: "mcp____x", args: {} }).name, null);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("状态推导:没记录=candidate、用过=trusted、久未用=stale", () => {
|
|
156
|
-
const now = Date.parse("2026-10-16T00:00:00Z");
|
|
157
|
-
assert.equal(deriveUsageState(null, { now }), "candidate");
|
|
158
|
-
assert.equal(deriveUsageState({ uses: 0, lastUsedAt: null }, { now }), "candidate", "0 次也算没记录");
|
|
159
|
-
// 30 天前用过 → trusted
|
|
160
|
-
assert.equal(deriveUsageState({ uses: 3, lastUsedAt: now - 30 * 86400000 }, { now }), "trusted");
|
|
161
|
-
// 超过阈值 → stale
|
|
162
|
-
assert.equal(deriveUsageState({ uses: 3, lastUsedAt: now - (STALE_AFTER_DAYS + 1) * 86400000 }, { now }), "stale");
|
|
163
|
-
// 恰好在阈值上不算 stale(边界)
|
|
164
|
-
assert.equal(deriveUsageState({ uses: 3, lastUsedAt: now - STALE_AFTER_DAYS * 86400000 }, { now }), "trusted");
|
|
165
|
-
// 没有时间戳但有次数 → 不能判 stale,保守给 trusted
|
|
166
|
-
assert.equal(deriveUsageState({ uses: 1, lastUsedAt: null }, { now }), "trusted");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("整条链路:从会话文件聚合出技能与 MCP 计数", () => {
|
|
170
|
-
const ctx = makeSessionDir([
|
|
171
|
-
toolCallLine({ name: "skill", args: { name: "agent-reach" }, turn: 1, time: 1792000000000 }),
|
|
172
|
-
toolCallLine({ name: "mcp__github__get_file_contents", args: {}, turn: 1 }),
|
|
173
|
-
toolCallLine({ name: "skill", args: { name: "agent-reach" }, turn: 2, time: 1792050000000 }),
|
|
174
|
-
toolCallLine({ name: "skill", args: { name: "euthyna" }, turn: 3 }),
|
|
175
|
-
toolCallLine({ name: "read", args: { path: "x" }, turn: 3 }), // 内置,忽略
|
|
176
|
-
toolCallLine({ name: "skill", args: {}, turn: 4 }) // 缺 name → unmatched
|
|
177
|
-
]);
|
|
178
|
-
try {
|
|
179
|
-
const c = collectUsage({ sessionDir: ctx.dir });
|
|
180
|
-
assert.equal(c.present, true);
|
|
181
|
-
assert.equal(c.scannedFiles, 1);
|
|
182
|
-
assert.equal(c.skillCalls, 3, "两次 agent-reach + 一次 euthyna");
|
|
183
|
-
assert.equal(c.mcpCalls, 1);
|
|
184
|
-
assert.equal(c.unmatched, 1, "缺 name 的那次要对不上,不能静默丢");
|
|
185
|
-
assert.equal(c.skills.get("agent-reach").uses, 2);
|
|
186
|
-
// 最近一次使用要取最大的那个时间戳
|
|
187
|
-
assert.equal(c.skills.get("agent-reach").lastUsedAt, 1792050000000);
|
|
188
|
-
assert.equal(c.mcp.get("github").uses, 1);
|
|
189
|
-
} finally {
|
|
190
|
-
ctx.cleanup();
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("降级:会话目录不存在时如实报 present=false,不抛错不编造", () => {
|
|
195
|
-
const base = mkdtempSync(join(tmpdir(), "mneme-usage-none-"));
|
|
196
|
-
try {
|
|
197
|
-
const c = collectUsage({ sessionDir: join(base, "no-such-dir") });
|
|
198
|
-
assert.equal(c.present, false);
|
|
199
|
-
assert.equal(c.scannedFiles, 0);
|
|
200
|
-
assert.equal(c.skillCalls, 0);
|
|
201
|
-
assert.deepEqual([...c.skills.keys()], []);
|
|
202
|
-
const s = summarizeUsage(c);
|
|
203
|
-
assert.deepEqual(s.skills, []);
|
|
204
|
-
assert.deepEqual(s.mcp, []);
|
|
205
|
-
} finally {
|
|
206
|
-
rmSync(base, { recursive: true, force: true });
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test("会话目录默认值由 dshHome 推导,不写死本机路径", () => {
|
|
211
|
-
const dir = resolveSessionDir({ dshHome: "/custom/dsh" });
|
|
212
|
-
assert.ok(dir.includes("sessions"), "默认落在 sessions 子目录");
|
|
213
|
-
assert.ok(!/[A-Za-z]:\\Users/i.test(dir), "不得出现本机盘符路径");
|
|
214
|
-
// 环境变量优先
|
|
215
|
-
assert.ok(resolveSessionDir({ dshHome: join(tmpdir(), "x") }).includes("x"));
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test("文件发现:**所有版本**的会话文件都要收(只认 v4 会漏掉一半历史)", () => {
|
|
219
|
-
// 实测回归:本机同一会话树下并存三种格式——v4(219)、v3(218)、无版本号(72)。
|
|
220
|
-
// 早期版本只认 `session.v4`,静默漏掉 50 次技能调用(euthyna 44 次),
|
|
221
|
-
// 导致三个明明用过的技能被报成 candidate。所以只要求「以 session 开头」。
|
|
222
|
-
const base = mkdtempSync(join(tmpdir(), "mneme-usage-find-"));
|
|
223
|
-
try {
|
|
224
|
-
const sessions = join(base, "sessions");
|
|
225
|
-
mkdirSync(join(sessions, "a", "id1"), { recursive: true });
|
|
226
|
-
mkdirSync(join(sessions, "b", "id2"), { recursive: true });
|
|
227
|
-
writeFileSync(join(sessions, "a", "id1", "session.v4.jsonl.zstd"), multiFrameBuffer(["x"]));
|
|
228
|
-
writeFileSync(join(sessions, "b", "id2", "session.v3.jsonl.zstd"), multiFrameBuffer(["y"]));
|
|
229
|
-
writeFileSync(join(sessions, "b", "id2", "session.jsonl.zstd"), multiFrameBuffer(["z"]));
|
|
230
|
-
writeFileSync(join(sessions, "a", "id1", "other.jsonl"), "不该被收");
|
|
231
|
-
const found = findSessionFiles(sessions);
|
|
232
|
-
assert.equal(found.files.length, 3, "v4 / v3 / 无版本号三种都要找到,other.jsonl 不要");
|
|
233
|
-
assert.equal(found.unreadable.length, 0);
|
|
234
|
-
assert.equal(found.present, true);
|
|
235
|
-
} finally {
|
|
236
|
-
rmSync(base, { recursive: true, force: true });
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test("文件上限可注入,且截断要留痕(CodeRabbit 抓到的静默截断)", () => {
|
|
241
|
-
// 实测回归:早期版本把上限写死在遍历里(MAX_SESSION_FILES=2000),于是
|
|
242
|
-
// · 调用方传 maxFiles: 5000 也只拿到 2000 个——参数无效,无从察觉;
|
|
243
|
-
// · 截断发生在内层,外层算不出 truncated(`found.files.length > maxFiles` 永假)。
|
|
244
|
-
// 结果就是「只扫了一部分」却看起来像扫全了——本仓库反复防的那类静默错。
|
|
245
|
-
const base = mkdtempSync(join(tmpdir(), "mneme-usage-cap-"));
|
|
246
|
-
try {
|
|
247
|
-
const sessions = join(base, "sessions");
|
|
248
|
-
// 造 12 个会话文件,限 5 个:应拿到 5 个 + truncated=true
|
|
249
|
-
for (let i = 0; i < 12; i++) {
|
|
250
|
-
const d = join(sessions, `s${String(i).padStart(2, "0")}`);
|
|
251
|
-
mkdirSync(d, { recursive: true });
|
|
252
|
-
writeFileSync(join(d, "session.v4.jsonl.zstd"), multiFrameBuffer([`line${i}`]));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ① limit 真的生效(不是被内层写死的上限压住)
|
|
256
|
-
const capped = findSessionFiles(sessions, { limit: 5 });
|
|
257
|
-
assert.equal(capped.files.length, 5, "limit=5 就该只拿 5 个");
|
|
258
|
-
assert.equal(capped.truncated, true, "被截断必须留痕");
|
|
259
|
-
|
|
260
|
-
// ② 不设限时全拿到,且不报截断
|
|
261
|
-
const all = findSessionFiles(sessions);
|
|
262
|
-
assert.equal(all.files.length, 12);
|
|
263
|
-
assert.equal(all.truncated, false, "没截断就不该报截断");
|
|
264
|
-
|
|
265
|
-
// ③ collectUsage 透传:maxFiles 生效,且 truncated 透出来
|
|
266
|
-
const c = collectUsage({ sessionDir: sessions, maxFiles: 5 });
|
|
267
|
-
assert.equal(c.scannedFiles, 5);
|
|
268
|
-
assert.equal(c.truncated, true, "collectUsage 要把截断标记透出来");
|
|
269
|
-
assert.equal(c.totalFiles, 5, "totalFiles 是被限制后的数量(不是磁盘总数)");
|
|
270
|
-
|
|
271
|
-
// ④ 给足够大的 maxFiles 时能扫全(旧实现这里会失败:内层卡 2000)
|
|
272
|
-
const c2 = collectUsage({ sessionDir: sessions, maxFiles: 9999 });
|
|
273
|
-
assert.equal(c2.scannedFiles, 12);
|
|
274
|
-
assert.equal(c2.truncated, false);
|
|
275
|
-
} finally {
|
|
276
|
-
rmSync(base, { recursive: true, force: true });
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test("目录不可读要与「空目录」**可分辨**(不许同形)", () => {
|
|
281
|
-
// 实测回归:早期版本用 existsSync 定 present、readdirSync 失败直接 return,
|
|
282
|
-
// 于是 ACL 拒绝的目录产出与「可读空目录」逐字段相同的读数
|
|
283
|
-
// (present=true, scannedFiles=0, unreadable=[]),清单侧 175 条全 candidate。
|
|
284
|
-
// 这正是本仓库反复防的那类错:读不到被显示成「读到但没有」。
|
|
285
|
-
const base = mkdtempSync(join(tmpdir(), "mneme-usage-unreadable-"));
|
|
286
|
-
try {
|
|
287
|
-
// ① 不存在的目录:present=false
|
|
288
|
-
const missing = findSessionFiles(join(base, "nope"));
|
|
289
|
-
assert.equal(missing.present, false);
|
|
290
|
-
assert.equal(missing.files.length, 0);
|
|
291
|
-
|
|
292
|
-
// ② 存在的空目录:present=true、无 unreadable
|
|
293
|
-
const empty = join(base, "empty");
|
|
294
|
-
mkdirSync(empty, { recursive: true });
|
|
295
|
-
const emptyRes = findSessionFiles(empty);
|
|
296
|
-
assert.equal(emptyRes.present, true);
|
|
297
|
-
assert.equal(emptyRes.files.length, 0);
|
|
298
|
-
assert.equal(emptyRes.unreadable.length, 0, "空目录不该有 unreadable");
|
|
299
|
-
|
|
300
|
-
// ③ 悬空符号链接:present=true,但**必须有 unreadable**,与空目录不同形
|
|
301
|
-
try {
|
|
302
|
-
symlinkSync(join(base, "no-such-target"), join(empty, "session.dangling.jsonl.zstd"), "junction");
|
|
303
|
-
} catch (error) {
|
|
304
|
-
if (error.code === "EPERM" || error.code === "EACCES") return; // 无权限则跳过本断言
|
|
305
|
-
throw error;
|
|
306
|
-
}
|
|
307
|
-
const dangling = findSessionFiles(empty);
|
|
308
|
-
assert.ok(dangling.unreadable.length > 0, "读不到的项必须留痕,否则与空目录同形");
|
|
309
|
-
// 而且上层能区分:collectUsage 的 readable 会变 false
|
|
310
|
-
const c = collectUsage({ sessionDir: empty });
|
|
311
|
-
assert.equal(c.present, true);
|
|
312
|
-
assert.equal(c.readable, false, "有读不动的项时 readable 必须为 false");
|
|
313
|
-
assert.ok(c.unreadable.length > 0, "unreadable 要透到 collectUsage");
|
|
314
|
-
} finally {
|
|
315
|
-
rmSync(base, { recursive: true, force: true });
|
|
316
|
-
}
|
|
317
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* 会话日志使用证据(src/session-usage.js)的回归测试。
|
|
3
|
+
*
|
|
4
|
+
* 本文件锁的重点是**两类「静默少一大截」的坑**(都真踩过):
|
|
5
|
+
* ① **zstd 多帧**:`zlib.zstdDecompressSync(buf)` 只解**第一帧**且不报错——实测一个
|
|
6
|
+
* 1.5 MB 的会话文件只解出 291 字节,于是扫描得出「零命中」的假结论。所以这里用
|
|
7
|
+
* **两帧拼接的夹具**钉死「两帧都要解出来」。
|
|
8
|
+
* ② **参数在 `arguments` 且是 JSON 字符串**:取 `parameters` 会得到 null,看着像
|
|
9
|
+
* 「调用了但没记参数」。夹具按真实形态写。
|
|
10
|
+
*
|
|
11
|
+
* 另外钉住状态推导的语义边界:`candidate` 是「现存日志里没记录」,不是「从未使用」。
|
|
12
|
+
*/
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import zlib from "node:zlib";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
forEachFrame,
|
|
22
|
+
toolCallsInLine,
|
|
23
|
+
identifyCapability,
|
|
24
|
+
deriveUsageState,
|
|
25
|
+
collectUsage,
|
|
26
|
+
summarizeUsage,
|
|
27
|
+
resolveSessionDir,
|
|
28
|
+
findSessionFiles,
|
|
29
|
+
STALE_AFTER_DAYS
|
|
30
|
+
} from "../src/session-usage.js";
|
|
31
|
+
|
|
32
|
+
const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
33
|
+
|
|
34
|
+
/** 把若干段文本各压成一帧再拼起来,模拟真实的多帧文件。 */
|
|
35
|
+
function multiFrameBuffer(chunks) {
|
|
36
|
+
return Buffer.concat(chunks.map((c) => zlib.zstdCompressSync(Buffer.from(c, "utf8"))));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 造一行真实的 tool-call 记录(参数是 JSON 字符串,不是对象)。 */
|
|
40
|
+
function toolCallLine({ name, args, turn = 1, time = 1700000000000 }) {
|
|
41
|
+
const item = { type: "tool-call", id: `call_${Math.random().toString(36).slice(2)}`, name };
|
|
42
|
+
if (args !== undefined) item.arguments = JSON.stringify(args);
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
type: "assistant/message",
|
|
45
|
+
time,
|
|
46
|
+
data: { turn, step: 1, message: { role: "assistant", content: [item] } }
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 造一个会话目录,内容按给定数组写进一个文件。 */
|
|
51
|
+
function makeSessionDir(lines) {
|
|
52
|
+
const base = mkdtempSync(join(tmpdir(), "mneme-usage-"));
|
|
53
|
+
const sessionId = "session-test-0001";
|
|
54
|
+
const dir = join(base, "sessions", `--work-${sessionId.slice(0, 8)}--`, sessionId);
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
writeFileSync(join(dir, "session.v4.jsonl.zstd"), multiFrameBuffer(lines), "utf8");
|
|
57
|
+
return { base, dir: join(base, "sessions"), cleanup: () => rmSync(base, { recursive: true, force: true }) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("多帧 zstd:**每一帧**都要解出来(只解第一帧是踩过的坑)", () => {
|
|
61
|
+
const buf = multiFrameBuffer(["第一帧的内容", "第二帧的内容", "第三帧的内容"]);
|
|
62
|
+
const seen = [];
|
|
63
|
+
const stat = forEachFrame(buf, (text) => seen.push(text));
|
|
64
|
+
assert.deepEqual(seen, ["第一帧的内容", "第二帧的内容", "第三帧的内容"], "三帧都要被回调到");
|
|
65
|
+
assert.equal(stat.frames, 3);
|
|
66
|
+
assert.equal(stat.failed, 0);
|
|
67
|
+
// 反证:证明这个夹具真能暴露「只解第一帧」——数帧头,不依赖 Node 的 zstd 语义。
|
|
68
|
+
// 原来这行是 `zstdDecompressSync(buf)` 只解出第一段;Node 24.22.0 起整块解会吐出全部帧
|
|
69
|
+
// (nodejs/node#65865 让解码跨 chunk 边界),反证随之过期,所以改成数 magic。
|
|
70
|
+
// 注意这行锁的是「切帧判据仍是 magic」,**不是碰撞免疫**:payload 里撞上 magic 时
|
|
71
|
+
// 计数器与解码器会一起错(半片也解得出来),实测 frames 同样变成 4,这条测不出来。
|
|
72
|
+
let frameHeaders = 0;
|
|
73
|
+
for (let at = 0; (at = buf.indexOf(ZSTD_MAGIC, at)) !== -1; at += ZSTD_MAGIC.length) frameHeaders += 1;
|
|
74
|
+
assert.equal(frameHeaders, 3, "夹具必须是三帧,否则上面测不到「只解第一帧」");
|
|
75
|
+
// 帧头数 = 成功数 + 失败数,是实现的契约:`forEachFrame` 用 magic 切帧,每段要么解开要么记失败。
|
|
76
|
+
// 写成 `stat.frames` 只在 failed 恒为 0 时成立,那是巧合不是规矩。
|
|
77
|
+
assert.equal(frameHeaders, stat.frames + stat.failed, "帧头数必须交代得清:每一帧要么解开要么记失败");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("多帧 zstd:每帧都解;截断产生的**半行**由 jsonl 层丢掉(宁可少一条,不要错一条)", () => {
|
|
81
|
+
// 真实的「坏帧」不是帧头损坏,而是**文件正在写**:末帧被截断。
|
|
82
|
+
// 实测结论(写这条时验过):zstd 是流式格式,截断帧会**解出部分文本而不抛错**,
|
|
83
|
+
// 且 Node 的绑定不暴露「还有多少输入未消费」,所以帧层的完整性判不了。
|
|
84
|
+
// 兜底放在**行层**:半行 JSON 解析失败会被丢弃——代价是少一条记录,收益是绝不误用。
|
|
85
|
+
const good = zlib.zstdCompressSync(Buffer.from("完整帧的内容", "utf8"));
|
|
86
|
+
|
|
87
|
+
// 帧层:两帧都要回调到(这是最初踩的坑——只解第一帧)
|
|
88
|
+
const seen = [];
|
|
89
|
+
const stat = forEachFrame(Buffer.concat([good, good]), (t) => seen.push(t));
|
|
90
|
+
assert.equal(stat.frames, 2);
|
|
91
|
+
assert.equal(seen.length, 2);
|
|
92
|
+
|
|
93
|
+
// 行层兜底:一个被截断的 tool-call 行不能被当完整记录用
|
|
94
|
+
const fullLine = toolCallLine({ name: "skill", args: { name: "agent-reach" } });
|
|
95
|
+
assert.equal(toolCallsInLine(fullLine).length, 1, "完整行要解析出来");
|
|
96
|
+
const halfLine = fullLine.slice(0, Math.floor(fullLine.length / 2));
|
|
97
|
+
assert.deepEqual(toolCallsInLine(halfLine), [], "半行必须被丢掉,不能猜出一个不完整的调用");
|
|
98
|
+
|
|
99
|
+
// 真实截断文件:整帧 + 半帧 → 完整的那些记录照常统计,半行被丢
|
|
100
|
+
const base = mkdtempSync(join(tmpdir(), "mneme-usage-trunc-"));
|
|
101
|
+
try {
|
|
102
|
+
const sessionId = "session-trunc-0001";
|
|
103
|
+
const dir = join(base, "sessions", "work", sessionId);
|
|
104
|
+
mkdirSync(dir, { recursive: true });
|
|
105
|
+
const truncated = zlib.zstdCompressSync(Buffer.from(fullLine, "utf8")).subarray(0, 12);
|
|
106
|
+
writeFileSync(join(dir, "session.v4.jsonl.zstd"), Buffer.concat([good, truncated]));
|
|
107
|
+
const c = collectUsage({ sessionDir: join(base, "sessions") });
|
|
108
|
+
assert.equal(c.scannedFiles, 1);
|
|
109
|
+
// 好帧里没有 tool-call,截断帧里的调用不完整 → 一条都不该被记
|
|
110
|
+
assert.equal(c.skillCalls, 0, "截断的半行不能被算成一次调用");
|
|
111
|
+
} finally {
|
|
112
|
+
rmSync(base, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("tool-call 解析:参数在 arguments 且是 JSON 字符串", () => {
|
|
117
|
+
// 真实形态:{"type":"tool-call","name":"skill","arguments":"{\"name\":\"agent-reach\"}"}
|
|
118
|
+
const line = toolCallLine({ name: "skill", args: { name: "agent-reach" }, turn: 7, time: 1792000000000 });
|
|
119
|
+
const calls = toolCallsInLine(line);
|
|
120
|
+
assert.equal(calls.length, 1);
|
|
121
|
+
assert.equal(calls[0].name, "skill");
|
|
122
|
+
assert.equal(calls[0].args.name, "agent-reach", "arguments 要二次解析成对象");
|
|
123
|
+
assert.equal(calls[0].turn, 7);
|
|
124
|
+
assert.equal(calls[0].time, 1792000000000);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("tool-call 解析:坏 JSON、非 tool-call 行、参数缺失都要能分清", () => {
|
|
128
|
+
assert.deepEqual(toolCallsInLine("{ 这不是 JSON"), []);
|
|
129
|
+
assert.deepEqual(toolCallsInLine(JSON.stringify({ type: "user/message" })), []);
|
|
130
|
+
// 参数缺失:**调用本身要保留**(它确实发生过),只是 args 为 null
|
|
131
|
+
const noArgs = toolCallLine({ name: "skill" });
|
|
132
|
+
const calls = toolCallsInLine(noArgs);
|
|
133
|
+
assert.equal(calls.length, 1);
|
|
134
|
+
assert.equal(calls[0].args, null);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("能力识别:skill 取 arguments.name;MCP 取 mcp__<server>__<tool> 的 server 段", () => {
|
|
138
|
+
assert.deepEqual(identifyCapability({ name: "skill", args: { name: "agent-reach" } }), {
|
|
139
|
+
kind: "skill",
|
|
140
|
+
name: "agent-reach"
|
|
141
|
+
});
|
|
142
|
+
const mcp = identifyCapability({ name: "mcp__github__get_file_contents", args: {} });
|
|
143
|
+
assert.equal(mcp.kind, "mcp");
|
|
144
|
+
assert.equal(mcp.name, "github", "MCP 按 server 聚合(清单里的条目是 server 粒度)");
|
|
145
|
+
assert.equal(mcp.tool, "get_file_contents");
|
|
146
|
+
// 内置工具不是能力清单里的东西 → null(丢掉,不计 unmatched)
|
|
147
|
+
assert.equal(identifyCapability({ name: "read", args: {} }), null);
|
|
148
|
+
assert.equal(identifyCapability({ name: "pwsh", args: {} }), null);
|
|
149
|
+
// skill 没记 name → 返回 name:null,让上层计入 unmatched(如实报,不硬凑)
|
|
150
|
+
assert.deepEqual(identifyCapability({ name: "skill", args: {} }), { kind: "skill", name: null });
|
|
151
|
+
// mcp__ 后面 server 段为空同理
|
|
152
|
+
assert.equal(identifyCapability({ name: "mcp____x", args: {} }).name, null);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("状态推导:没记录=candidate、用过=trusted、久未用=stale", () => {
|
|
156
|
+
const now = Date.parse("2026-10-16T00:00:00Z");
|
|
157
|
+
assert.equal(deriveUsageState(null, { now }), "candidate");
|
|
158
|
+
assert.equal(deriveUsageState({ uses: 0, lastUsedAt: null }, { now }), "candidate", "0 次也算没记录");
|
|
159
|
+
// 30 天前用过 → trusted
|
|
160
|
+
assert.equal(deriveUsageState({ uses: 3, lastUsedAt: now - 30 * 86400000 }, { now }), "trusted");
|
|
161
|
+
// 超过阈值 → stale
|
|
162
|
+
assert.equal(deriveUsageState({ uses: 3, lastUsedAt: now - (STALE_AFTER_DAYS + 1) * 86400000 }, { now }), "stale");
|
|
163
|
+
// 恰好在阈值上不算 stale(边界)
|
|
164
|
+
assert.equal(deriveUsageState({ uses: 3, lastUsedAt: now - STALE_AFTER_DAYS * 86400000 }, { now }), "trusted");
|
|
165
|
+
// 没有时间戳但有次数 → 不能判 stale,保守给 trusted
|
|
166
|
+
assert.equal(deriveUsageState({ uses: 1, lastUsedAt: null }, { now }), "trusted");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("整条链路:从会话文件聚合出技能与 MCP 计数", () => {
|
|
170
|
+
const ctx = makeSessionDir([
|
|
171
|
+
toolCallLine({ name: "skill", args: { name: "agent-reach" }, turn: 1, time: 1792000000000 }),
|
|
172
|
+
toolCallLine({ name: "mcp__github__get_file_contents", args: {}, turn: 1 }),
|
|
173
|
+
toolCallLine({ name: "skill", args: { name: "agent-reach" }, turn: 2, time: 1792050000000 }),
|
|
174
|
+
toolCallLine({ name: "skill", args: { name: "euthyna" }, turn: 3 }),
|
|
175
|
+
toolCallLine({ name: "read", args: { path: "x" }, turn: 3 }), // 内置,忽略
|
|
176
|
+
toolCallLine({ name: "skill", args: {}, turn: 4 }) // 缺 name → unmatched
|
|
177
|
+
]);
|
|
178
|
+
try {
|
|
179
|
+
const c = collectUsage({ sessionDir: ctx.dir });
|
|
180
|
+
assert.equal(c.present, true);
|
|
181
|
+
assert.equal(c.scannedFiles, 1);
|
|
182
|
+
assert.equal(c.skillCalls, 3, "两次 agent-reach + 一次 euthyna");
|
|
183
|
+
assert.equal(c.mcpCalls, 1);
|
|
184
|
+
assert.equal(c.unmatched, 1, "缺 name 的那次要对不上,不能静默丢");
|
|
185
|
+
assert.equal(c.skills.get("agent-reach").uses, 2);
|
|
186
|
+
// 最近一次使用要取最大的那个时间戳
|
|
187
|
+
assert.equal(c.skills.get("agent-reach").lastUsedAt, 1792050000000);
|
|
188
|
+
assert.equal(c.mcp.get("github").uses, 1);
|
|
189
|
+
} finally {
|
|
190
|
+
ctx.cleanup();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("降级:会话目录不存在时如实报 present=false,不抛错不编造", () => {
|
|
195
|
+
const base = mkdtempSync(join(tmpdir(), "mneme-usage-none-"));
|
|
196
|
+
try {
|
|
197
|
+
const c = collectUsage({ sessionDir: join(base, "no-such-dir") });
|
|
198
|
+
assert.equal(c.present, false);
|
|
199
|
+
assert.equal(c.scannedFiles, 0);
|
|
200
|
+
assert.equal(c.skillCalls, 0);
|
|
201
|
+
assert.deepEqual([...c.skills.keys()], []);
|
|
202
|
+
const s = summarizeUsage(c);
|
|
203
|
+
assert.deepEqual(s.skills, []);
|
|
204
|
+
assert.deepEqual(s.mcp, []);
|
|
205
|
+
} finally {
|
|
206
|
+
rmSync(base, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("会话目录默认值由 dshHome 推导,不写死本机路径", () => {
|
|
211
|
+
const dir = resolveSessionDir({ dshHome: "/custom/dsh" });
|
|
212
|
+
assert.ok(dir.includes("sessions"), "默认落在 sessions 子目录");
|
|
213
|
+
assert.ok(!/[A-Za-z]:\\Users/i.test(dir), "不得出现本机盘符路径");
|
|
214
|
+
// 环境变量优先
|
|
215
|
+
assert.ok(resolveSessionDir({ dshHome: join(tmpdir(), "x") }).includes("x"));
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("文件发现:**所有版本**的会话文件都要收(只认 v4 会漏掉一半历史)", () => {
|
|
219
|
+
// 实测回归:本机同一会话树下并存三种格式——v4(219)、v3(218)、无版本号(72)。
|
|
220
|
+
// 早期版本只认 `session.v4`,静默漏掉 50 次技能调用(euthyna 44 次),
|
|
221
|
+
// 导致三个明明用过的技能被报成 candidate。所以只要求「以 session 开头」。
|
|
222
|
+
const base = mkdtempSync(join(tmpdir(), "mneme-usage-find-"));
|
|
223
|
+
try {
|
|
224
|
+
const sessions = join(base, "sessions");
|
|
225
|
+
mkdirSync(join(sessions, "a", "id1"), { recursive: true });
|
|
226
|
+
mkdirSync(join(sessions, "b", "id2"), { recursive: true });
|
|
227
|
+
writeFileSync(join(sessions, "a", "id1", "session.v4.jsonl.zstd"), multiFrameBuffer(["x"]));
|
|
228
|
+
writeFileSync(join(sessions, "b", "id2", "session.v3.jsonl.zstd"), multiFrameBuffer(["y"]));
|
|
229
|
+
writeFileSync(join(sessions, "b", "id2", "session.jsonl.zstd"), multiFrameBuffer(["z"]));
|
|
230
|
+
writeFileSync(join(sessions, "a", "id1", "other.jsonl"), "不该被收");
|
|
231
|
+
const found = findSessionFiles(sessions);
|
|
232
|
+
assert.equal(found.files.length, 3, "v4 / v3 / 无版本号三种都要找到,other.jsonl 不要");
|
|
233
|
+
assert.equal(found.unreadable.length, 0);
|
|
234
|
+
assert.equal(found.present, true);
|
|
235
|
+
} finally {
|
|
236
|
+
rmSync(base, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("文件上限可注入,且截断要留痕(CodeRabbit 抓到的静默截断)", () => {
|
|
241
|
+
// 实测回归:早期版本把上限写死在遍历里(MAX_SESSION_FILES=2000),于是
|
|
242
|
+
// · 调用方传 maxFiles: 5000 也只拿到 2000 个——参数无效,无从察觉;
|
|
243
|
+
// · 截断发生在内层,外层算不出 truncated(`found.files.length > maxFiles` 永假)。
|
|
244
|
+
// 结果就是「只扫了一部分」却看起来像扫全了——本仓库反复防的那类静默错。
|
|
245
|
+
const base = mkdtempSync(join(tmpdir(), "mneme-usage-cap-"));
|
|
246
|
+
try {
|
|
247
|
+
const sessions = join(base, "sessions");
|
|
248
|
+
// 造 12 个会话文件,限 5 个:应拿到 5 个 + truncated=true
|
|
249
|
+
for (let i = 0; i < 12; i++) {
|
|
250
|
+
const d = join(sessions, `s${String(i).padStart(2, "0")}`);
|
|
251
|
+
mkdirSync(d, { recursive: true });
|
|
252
|
+
writeFileSync(join(d, "session.v4.jsonl.zstd"), multiFrameBuffer([`line${i}`]));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ① limit 真的生效(不是被内层写死的上限压住)
|
|
256
|
+
const capped = findSessionFiles(sessions, { limit: 5 });
|
|
257
|
+
assert.equal(capped.files.length, 5, "limit=5 就该只拿 5 个");
|
|
258
|
+
assert.equal(capped.truncated, true, "被截断必须留痕");
|
|
259
|
+
|
|
260
|
+
// ② 不设限时全拿到,且不报截断
|
|
261
|
+
const all = findSessionFiles(sessions);
|
|
262
|
+
assert.equal(all.files.length, 12);
|
|
263
|
+
assert.equal(all.truncated, false, "没截断就不该报截断");
|
|
264
|
+
|
|
265
|
+
// ③ collectUsage 透传:maxFiles 生效,且 truncated 透出来
|
|
266
|
+
const c = collectUsage({ sessionDir: sessions, maxFiles: 5 });
|
|
267
|
+
assert.equal(c.scannedFiles, 5);
|
|
268
|
+
assert.equal(c.truncated, true, "collectUsage 要把截断标记透出来");
|
|
269
|
+
assert.equal(c.totalFiles, 5, "totalFiles 是被限制后的数量(不是磁盘总数)");
|
|
270
|
+
|
|
271
|
+
// ④ 给足够大的 maxFiles 时能扫全(旧实现这里会失败:内层卡 2000)
|
|
272
|
+
const c2 = collectUsage({ sessionDir: sessions, maxFiles: 9999 });
|
|
273
|
+
assert.equal(c2.scannedFiles, 12);
|
|
274
|
+
assert.equal(c2.truncated, false);
|
|
275
|
+
} finally {
|
|
276
|
+
rmSync(base, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("目录不可读要与「空目录」**可分辨**(不许同形)", () => {
|
|
281
|
+
// 实测回归:早期版本用 existsSync 定 present、readdirSync 失败直接 return,
|
|
282
|
+
// 于是 ACL 拒绝的目录产出与「可读空目录」逐字段相同的读数
|
|
283
|
+
// (present=true, scannedFiles=0, unreadable=[]),清单侧 175 条全 candidate。
|
|
284
|
+
// 这正是本仓库反复防的那类错:读不到被显示成「读到但没有」。
|
|
285
|
+
const base = mkdtempSync(join(tmpdir(), "mneme-usage-unreadable-"));
|
|
286
|
+
try {
|
|
287
|
+
// ① 不存在的目录:present=false
|
|
288
|
+
const missing = findSessionFiles(join(base, "nope"));
|
|
289
|
+
assert.equal(missing.present, false);
|
|
290
|
+
assert.equal(missing.files.length, 0);
|
|
291
|
+
|
|
292
|
+
// ② 存在的空目录:present=true、无 unreadable
|
|
293
|
+
const empty = join(base, "empty");
|
|
294
|
+
mkdirSync(empty, { recursive: true });
|
|
295
|
+
const emptyRes = findSessionFiles(empty);
|
|
296
|
+
assert.equal(emptyRes.present, true);
|
|
297
|
+
assert.equal(emptyRes.files.length, 0);
|
|
298
|
+
assert.equal(emptyRes.unreadable.length, 0, "空目录不该有 unreadable");
|
|
299
|
+
|
|
300
|
+
// ③ 悬空符号链接:present=true,但**必须有 unreadable**,与空目录不同形
|
|
301
|
+
try {
|
|
302
|
+
symlinkSync(join(base, "no-such-target"), join(empty, "session.dangling.jsonl.zstd"), "junction");
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error.code === "EPERM" || error.code === "EACCES") return; // 无权限则跳过本断言
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
const dangling = findSessionFiles(empty);
|
|
308
|
+
assert.ok(dangling.unreadable.length > 0, "读不到的项必须留痕,否则与空目录同形");
|
|
309
|
+
// 而且上层能区分:collectUsage 的 readable 会变 false
|
|
310
|
+
const c = collectUsage({ sessionDir: empty });
|
|
311
|
+
assert.equal(c.present, true);
|
|
312
|
+
assert.equal(c.readable, false, "有读不动的项时 readable 必须为 false");
|
|
313
|
+
assert.ok(c.unreadable.length > 0, "unreadable 要透到 collectUsage");
|
|
314
|
+
} finally {
|
|
315
|
+
rmSync(base, { recursive: true, force: true });
|
|
316
|
+
}
|
|
317
|
+
});
|