@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
package/test/wiki-api.test.js
CHANGED
|
@@ -1,628 +1,735 @@
|
|
|
1
|
-
// chorography 四条读路由的回归锁(M1)。锁四层闸的语义分叉(#497 口径):
|
|
2
|
-
// choro queries 缺装配 → 503 unavailable(环境缺件);chorographyEnabled 关 →
|
|
3
|
-
// 409 disabled(用户没开);repo 解析失败 → 404 / 歧义 → 400 带可选清单;
|
|
4
|
-
// 快乐路径四条全通。数据用真实 T0 fixture 建(同 wiki-queries 的对账纪律)。
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import assert from "node:assert/strict";
|
|
7
|
-
import { EventEmitter } from "node:events";
|
|
8
|
-
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import { execFileSync } from "node:child_process";
|
|
12
|
-
import { createStore } from "../src/store.js";
|
|
13
|
-
import { createService } from "../src/service.js";
|
|
14
|
-
import { createApi } from "../src/api.js";
|
|
15
|
-
import { createSettings } from "../src/settings.js";
|
|
16
|
-
import { createChoroQueries } from "../src/wiki/queries.js";
|
|
17
|
-
import { runChoroT0 } from "../src/wiki/t0.js";
|
|
18
|
-
|
|
19
|
-
class FakeRes extends EventEmitter {
|
|
20
|
-
constructor() { super(); this.statusCode = 200; this.body = ""; }
|
|
21
|
-
writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
|
|
22
|
-
end(text) { this.body = text ?? ""; this.emit("end"); return this; }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function req(path, method = "GET") {
|
|
26
|
-
const r = new EventEmitter();
|
|
27
|
-
r.url = path;
|
|
28
|
-
r.method = method;
|
|
29
|
-
r.headers = {};
|
|
30
|
-
return r;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function setup({ config = null, choro = null } = {}) {
|
|
34
|
-
const store = createStore(":memory:");
|
|
35
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
36
|
-
const settings = createSettings(store.db);
|
|
37
|
-
const routes = [];
|
|
38
|
-
const ctx = { webServer: { register(route) { routes.push(route); return () => {}; } } };
|
|
39
|
-
createApi(ctx, service, settings, { add: () => {}, remove: () => false, list: () => [] }, null, undefined, "", config, null, null, choro);
|
|
40
|
-
return { routes };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function handler(routes, path) {
|
|
44
|
-
return routes.find((r) => r.path === path);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function invoke(route, path) {
|
|
48
|
-
const res = new FakeRes();
|
|
49
|
-
await route.handler(req(path), res);
|
|
50
|
-
return { status: res.statusCode, body: JSON.parse(res.body || "{}") };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// POST 版:带 JSON body(readBody 走 data/end 事件)
|
|
54
|
-
async function invokePost(route, path, body) {
|
|
55
|
-
const r = req(path, "POST");
|
|
56
|
-
process.nextTick(() => {
|
|
57
|
-
r.emit("data", Buffer.from(JSON.stringify(body ?? {})));
|
|
58
|
-
r.emit("end");
|
|
59
|
-
});
|
|
60
|
-
const res = new FakeRes();
|
|
61
|
-
await route.handler(r, res);
|
|
62
|
-
return { status: res.statusCode, body: JSON.parse(res.body || "{}") };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function makeRepo(store) {
|
|
66
|
-
const root = mkdtempSync(join(tmpdir(), "choro-api-"));
|
|
67
|
-
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "apifix", scripts: { test: "node --test" } }, null, 2) + "\n", "utf8");
|
|
68
|
-
mkdirSync(join(root, "src"), { recursive: true });
|
|
69
|
-
writeFileSync(join(root, "src", "a.js"), 'import { b } from "./b.js";\nexport const a = () => b();\n', "utf8");
|
|
70
|
-
writeFileSync(join(root, "src", "b.js"), "export const b = () => 1;\n", "utf8");
|
|
71
|
-
execFileSync("git", ["init", "-b", "main"], { cwd: root, stdio: "pipe" });
|
|
72
|
-
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "add", "-A"], { cwd: root, stdio: "pipe" });
|
|
73
|
-
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", "init", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
74
|
-
runChoroT0({ repoRoot: root, store, apply: true, now: () => "T" });
|
|
75
|
-
return root;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function rmTempBestEffort(dir) {
|
|
79
|
-
try {
|
|
80
|
-
rmSync(dir, { recursive: true, force: true });
|
|
81
|
-
} catch {
|
|
82
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
|
|
83
|
-
try { rmSync(dir, { recursive: true, force: true }); } catch { /* 留给系统清理 */ }
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
test("四条 choro 路由都在册且 policy 全 open", () => {
|
|
88
|
-
const { routes } = setup({});
|
|
89
|
-
for (const p of ["/api/dsh-mneme/choro/repos", "/api/dsh-mneme/choro/modules", "/api/dsh-mneme/choro/page", "/api/dsh-mneme/choro/search"]) {
|
|
90
|
-
const route = handler(routes, p);
|
|
91
|
-
assert.ok(route, `${p} 在册`);
|
|
92
|
-
assert.equal(route.policy, "open");
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("queries 缺装配 → 503;开关关 → 409 disabled(语义分叉)", async () => {
|
|
97
|
-
const missing = setup({});
|
|
98
|
-
const reposRoute = handler(missing.routes, "/api/dsh-mneme/choro/repos");
|
|
99
|
-
const a = await invoke(reposRoute, "/api/dsh-mneme/choro/repos");
|
|
100
|
-
assert.equal(a.status, 503);
|
|
101
|
-
assert.equal(a.body.error, "unavailable");
|
|
102
|
-
|
|
103
|
-
const off = setup({ config: { chorographyEnabled: false }, choro: createChoroQueries({ store: createStore(":memory:") }) });
|
|
104
|
-
const b = await invoke(handler(off.routes, "/api/dsh-mneme/choro/repos"), "/api/dsh-mneme/choro/repos");
|
|
105
|
-
assert.equal(b.status, 409);
|
|
106
|
-
assert.equal(b.body.error, "disabled");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("快乐路径:repos / modules / page / search 全通", async () => {
|
|
110
|
-
const store = createStore(":memory:");
|
|
111
|
-
const root = makeRepo(store);
|
|
112
|
-
try {
|
|
113
|
-
const { routes } = setup({ config: { chorographyEnabled: true }, choro: createChoroQueries({ store }) });
|
|
114
|
-
const repos = await invoke(handler(routes, "/api/dsh-mneme/choro/repos"), "/api/dsh-mneme/choro/repos");
|
|
115
|
-
assert.equal(repos.status, 200);
|
|
116
|
-
assert.equal(repos.body.repos.length, 1);
|
|
117
|
-
const repoId = repos.body.repos[0].id;
|
|
118
|
-
|
|
119
|
-
const modules = await invoke(handler(routes, "/api/dsh-mneme/choro/modules"), `/api/dsh-mneme/choro/modules?repo=${repoId}`);
|
|
120
|
-
assert.equal(modules.status, 200);
|
|
121
|
-
assert.ok(modules.body.modules.length >= 2);
|
|
122
|
-
assert.ok(Array.isArray(modules.body.edges));
|
|
123
|
-
// 面板的地志页导入要把「根路径 + 页相对路径」拼成磁盘绝对路径(契约 §3.6),
|
|
124
|
-
// 回执里缺 relPath 时它会静默回落到「请先选模块」——这条锁的就是那个真机 bug。
|
|
125
|
-
const withPage = modules.body.modules.filter((m) => m.page_rel_path);
|
|
126
|
-
assert.ok(withPage.length >= 1, "fixture 里至少一个模块有骨架页");
|
|
127
|
-
for (const m of withPage) assert.equal(m.relPath, m.page_rel_path);
|
|
128
|
-
|
|
129
|
-
const page = await invoke(handler(routes, "/api/dsh-mneme/choro/page"), `/api/dsh-mneme/choro/page?repo=${repoId}&slug=src`);
|
|
130
|
-
assert.equal(page.status, 200);
|
|
131
|
-
assert.ok(page.body.page.content.includes("## 文件清单"));
|
|
132
|
-
|
|
133
|
-
const search = await invoke(handler(routes, "/api/dsh-mneme/choro/search"), `/api/dsh-mneme/choro/search?repo=${repoId}&q=b.js`);
|
|
134
|
-
assert.equal(search.status, 200);
|
|
135
|
-
assert.ok(search.body.hits.length >= 1);
|
|
136
|
-
} finally {
|
|
137
|
-
rmTempBestEffort(root);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("repo 解析失败各归各位:未知 → 404;歧义 → 400 带清单;slug 缺 → 400", async () => {
|
|
142
|
-
const store = createStore(":memory:");
|
|
143
|
-
const root = makeRepo(store);
|
|
144
|
-
try {
|
|
145
|
-
const { routes } = setup({ config: { chorographyEnabled: true }, choro: createChoroQueries({ store }) });
|
|
146
|
-
const notFound = await invoke(handler(routes, "/api/dsh-mneme/choro/modules"), "/api/dsh-mneme/choro/modules?repo=nope");
|
|
147
|
-
assert.equal(notFound.status, 404);
|
|
148
|
-
assert.equal(notFound.body.error, "repo-not-found");
|
|
149
|
-
|
|
150
|
-
const root2 = makeRepo(store);
|
|
151
|
-
try {
|
|
152
|
-
const ambiguous = await invoke(handler(routes, "/api/dsh-mneme/choro/modules"), "/api/dsh-mneme/choro/modules");
|
|
153
|
-
assert.equal(ambiguous.status, 400);
|
|
154
|
-
assert.equal(ambiguous.body.error, "repo-ambiguous");
|
|
155
|
-
assert.equal(ambiguous.body.repos.length, 2);
|
|
156
|
-
} finally {
|
|
157
|
-
rmTempBestEffort(root2);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const repoId = 1;
|
|
161
|
-
const noSlug = await invoke(handler(routes, "/api/dsh-mneme/choro/page"), `/api/dsh-mneme/choro/page?repo=${repoId}`);
|
|
162
|
-
assert.equal(noSlug.status, 400);
|
|
163
|
-
assert.equal(noSlug.body.error, "slug-required");
|
|
164
|
-
|
|
165
|
-
const badSlug = await invoke(handler(routes, "/api/dsh-mneme/choro/page"), `/api/dsh-mneme/choro/page?repo=${repoId}&slug=../etc/passwd`);
|
|
166
|
-
assert.equal(badSlug.status, 404, "路径穿越形态的 slug 走未知页 404(slug 只从 DB 白名单解析)");
|
|
167
|
-
} finally {
|
|
168
|
-
rmTempBestEffort(root);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("地志页路由区分缺失文件和不可读文件", async () => {
|
|
173
|
-
const store = createStore(":memory:");
|
|
174
|
-
const root = makeRepo(store);
|
|
175
|
-
try {
|
|
176
|
-
const { routes } = setup({ config: { chorographyEnabled: true }, choro: createChoroQueries({ store }) });
|
|
177
|
-
const pageRoute = handler(routes, "/api/dsh-mneme/choro/page");
|
|
178
|
-
const url = "/api/dsh-mneme/choro/page?slug=src";
|
|
179
|
-
const file = join(root, ".chorography", "modules", "src.md");
|
|
180
|
-
unlinkSync(file);
|
|
181
|
-
const missing = await invoke(pageRoute, url);
|
|
182
|
-
assert.equal(missing.status, 409);
|
|
183
|
-
assert.equal(missing.body.error, "page-missing");
|
|
184
|
-
assert.match(missing.body.hint, /dsh-mneme-choro/);
|
|
185
|
-
mkdirSync(file);
|
|
186
|
-
const unreadable = await invoke(pageRoute, url);
|
|
187
|
-
assert.equal(unreadable.status, 409);
|
|
188
|
-
assert.equal(unreadable.body.error, "page-unreadable");
|
|
189
|
-
assert.match(unreadable.body.hint, /permissions/);
|
|
190
|
-
} finally {
|
|
191
|
-
store.close();
|
|
192
|
-
rmTempBestEffort(root);
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// ---------------------------------------------------------------------------
|
|
197
|
-
// 收尾档:POST /choro/run(面板重建入口,P34)+ /choro/search 的 semantic 路(P37–P39)
|
|
198
|
-
// ---------------------------------------------------------------------------
|
|
199
|
-
test("/choro/run:policy token-write,GET 405,POST 快乐路径重跑 T0", async () => {
|
|
200
|
-
const store = createStore(":memory:");
|
|
201
|
-
const root = makeRepo(store);
|
|
202
|
-
try {
|
|
203
|
-
const { routes } = setup({
|
|
204
|
-
config: { chorographyEnabled: true },
|
|
205
|
-
choro: {
|
|
206
|
-
...createChoroQueries({ store }),
|
|
207
|
-
// 与 index.js 的装配同形:runT0Apply 闭包住 store(P35:写路径与读侧分开装配)
|
|
208
|
-
runT0Apply: ({ repoRoot }) => runChoroT0({ repoRoot, store, apply: true }),
|
|
209
|
-
},
|
|
210
|
-
});
|
|
211
|
-
const route = handler(routes, "/api/dsh-mneme/choro/run");
|
|
212
|
-
assert.equal(route.policy, "token-write", "写仓库磁盘的口归 token-write");
|
|
213
|
-
|
|
214
|
-
const get = await invoke(route, "/api/dsh-mneme/choro/run");
|
|
215
|
-
assert.equal(get.status, 405, "只收 POST");
|
|
216
|
-
|
|
217
|
-
// 破坏页面文件后重建:POST 走真 T0,页回来
|
|
218
|
-
rmSync(join(root, ".chorography", "modules", "src.md"));
|
|
219
|
-
const ok = await invokePost(route, "/api/dsh-mneme/choro/run", { repo: 1 });
|
|
220
|
-
assert.equal(ok.status, 200);
|
|
221
|
-
assert.equal(ok.body.ok, true);
|
|
222
|
-
assert.ok(ok.body.receipt.moduleCount >= 2, "receipt 带划分计数");
|
|
223
|
-
assert.ok(existsSync(join(root, ".chorography", "modules", "src.md")), "重建把页写了回来");
|
|
224
|
-
} finally {
|
|
225
|
-
rmTempBestEffort(root);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
test("/choro/run:开关关 409;repo 未知 404(与读路由同一道闸)", async () => {
|
|
230
|
-
const store = createStore(":memory:");
|
|
231
|
-
const root = makeRepo(store);
|
|
232
|
-
try {
|
|
233
|
-
const off = setup({
|
|
234
|
-
config: { chorographyEnabled: false },
|
|
235
|
-
choro: { ...createChoroQueries({ store }), runT0Apply: () => { throw new Error("不该被调到"); } },
|
|
236
|
-
});
|
|
237
|
-
const a = await invokePost(handler(off.routes, "/api/dsh-mneme/choro/run"), "/api/dsh-mneme/choro/run", { repo: 1 });
|
|
238
|
-
assert.equal(a.status, 409);
|
|
239
|
-
|
|
240
|
-
const on = setup({
|
|
241
|
-
config: { chorographyEnabled: true },
|
|
242
|
-
choro: { ...createChoroQueries({ store }), runT0Apply: () => { throw new Error("不该被调到") } },
|
|
243
|
-
});
|
|
244
|
-
const b = await invokePost(handler(on.routes, "/api/dsh-mneme/choro/run"), "/api/dsh-mneme/choro/run", { repo: 99 });
|
|
245
|
-
assert.equal(b.status, 404);
|
|
246
|
-
} finally {
|
|
247
|
-
rmTempBestEffort(root);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test("/choro/search:embedder 在场 → 惰性物化 + semantic 数组;缺席 → semantic null", async () => {
|
|
252
|
-
const store = createStore(":memory:");
|
|
253
|
-
const root = makeRepo(store);
|
|
254
|
-
try {
|
|
255
|
-
// stub embedder:向量按内容方向分类(src 相关 → [1,0]),保证可排序断言
|
|
256
|
-
const embedder = {
|
|
257
|
-
model: "stub-1",
|
|
258
|
-
embedSingle: async (text) => (/src|b\.js/.test(text) ? [1, 0] : [0, 1]),
|
|
259
|
-
embed: async (text) => (/src|b\.js/.test(text) ? [1, 0] : [0, 1]),
|
|
260
|
-
};
|
|
261
|
-
const { routes } = setup({
|
|
262
|
-
config: { chorographyEnabled: true },
|
|
263
|
-
choro: { ...createChoroQueries({ store }), embedder },
|
|
264
|
-
});
|
|
265
|
-
const s = await invoke(handler(routes, "/api/dsh-mneme/choro/search"), "/api/dsh-mneme/choro/search?repo=1&q=b.js");
|
|
266
|
-
assert.equal(s.status, 200);
|
|
267
|
-
assert.ok(Array.isArray(s.body.hits) && s.body.hits.length >= 1, "keyword hits 照旧");
|
|
268
|
-
assert.ok(Array.isArray(s.body.semantic) && s.body.semantic.length >= 1, "semantic 数组在场(惰性物化发生在这次请求里)");
|
|
269
|
-
assert.equal(s.body.semantic[0].slug, "src", "与查询同向的页排前");
|
|
270
|
-
assert.equal(typeof s.body.semantic[0].score, "number");
|
|
271
|
-
|
|
272
|
-
const noEmbedder = setup({ config: { chorographyEnabled: true }, choro: createChoroQueries({ store }) });
|
|
273
|
-
const t = await invoke(handler(noEmbedder.routes, "/api/dsh-mneme/choro/search"), "/api/dsh-mneme/choro/search?repo=1&q=b.js");
|
|
274
|
-
assert.equal(t.body.semantic, null, "embedder 缺席 → null(调用方退纯关键词)");
|
|
275
|
-
} finally {
|
|
276
|
-
rmTempBestEffort(root);
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
// ---------------------------------------------------------------------------
|
|
281
|
-
// 契约 §3.2(kb-qoder parity):面板闭环要的新字段与新入口
|
|
282
|
-
// ---------------------------------------------------------------------------
|
|
283
|
-
|
|
284
|
-
test("/choro/repos:契约字段齐备,git 漂移 → stale/git,plan 漂移 → stale/plan", async () => {
|
|
285
|
-
const store = createStore(":memory:");
|
|
286
|
-
const root = makeRepo(store);
|
|
287
|
-
try {
|
|
288
|
-
const { routes } = setup({ config: { chorographyEnabled: true }, choro: createChoroQueries({ store }) });
|
|
289
|
-
const url = "/api/dsh-mneme/choro/repos";
|
|
290
|
-
|
|
291
|
-
// 刚重建完:不该有任何过期信号——否则面板会对着一个刚生成的地志喊「代码已变」,
|
|
292
|
-
// 用户第一次用就学会不信任那个标记。
|
|
293
|
-
const fresh = await invoke(handler(routes, url), url);
|
|
294
|
-
assert.equal(fresh.status, 200);
|
|
295
|
-
const a = fresh.body.repos[0];
|
|
296
|
-
for (const key of ["id", "name", "rootPath", "modules", "pages", "claims", "legacy", "stale", "staleReason", "lastRunAt"]) {
|
|
297
|
-
assert.ok(key in a, `契约字段 ${key} 必须在场(面板按它渲染)`);
|
|
298
|
-
}
|
|
299
|
-
assert.equal(a.stale, false);
|
|
300
|
-
assert.equal(a.staleReason, null);
|
|
301
|
-
assert.equal(a.legacy, false);
|
|
302
|
-
assert.equal(typeof a.lastRunAt, "string");
|
|
303
|
-
assert.ok(a.modules >= 2 && a.pages >= 2, "modules/pages 是契约字段名(不是行里的 module_count)");
|
|
304
|
-
|
|
305
|
-
// 代码变了(新 commit)→ 说过期,理由必须是 git:面板那句话是「代码已变」。
|
|
306
|
-
writeFileSync(join(root, "src", "c.js"), "export const c = () => 2;\n", "utf8");
|
|
307
|
-
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "add", "-A"], { cwd: root, stdio: "pipe" });
|
|
308
|
-
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", "drift", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
309
|
-
const drifted = await invoke(handler(routes, url), url);
|
|
310
|
-
assert.equal(drifted.body.repos[0].stale, true);
|
|
311
|
-
assert.equal(drifted.body.repos[0].staleReason, "git");
|
|
312
|
-
|
|
313
|
-
// 先重建一次把 git 基线推平(否则 git 那一路永远优先,plan 判据根本轮不到),
|
|
314
|
-
// 再只改 plan.yaml(HEAD 不动)→ 理由必须是 plan:这条判据不依赖 git,不能被
|
|
315
|
-
// git 那一路掩盖掉,否则「改了模板配置但没提交」的仓库永远显示干净。
|
|
316
|
-
runChoroT0({ repoRoot: root, store, apply: true });
|
|
317
|
-
const cleaned = await invoke(handler(routes, url), url);
|
|
318
|
-
assert.equal(cleaned.body.repos[0].stale, false, "重建后 git 基线推平,过期信号应清掉");
|
|
319
|
-
writeFileSync(join(root, ".chorography", "plan.yaml"), "pages: []\n", "utf8");
|
|
320
|
-
const planDrift = await invoke(handler(routes, url), url);
|
|
321
|
-
assert.equal(planDrift.body.repos[0].stale, true);
|
|
322
|
-
assert.equal(planDrift.body.repos[0].staleReason, "plan");
|
|
323
|
-
} finally {
|
|
324
|
-
rmTempBestEffort(root);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
test("/choro/run:空态按绝对路径登记新仓库,重复路径复用同一 id", async () => {
|
|
329
|
-
const store = createStore(":memory:");
|
|
330
|
-
// 只把仓库目录准备好但不登记:这就是面板「一个仓库都没登记」的真实起点。
|
|
331
|
-
const root = mkdtempSync(join(tmpdir(), "choro-empty-"));
|
|
332
|
-
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "emptyfix" }, null, 2) + "\n", "utf8");
|
|
333
|
-
mkdirSync(join(root, "src"), { recursive: true });
|
|
334
|
-
writeFileSync(join(root, "src", "a.js"), "export const a = 1;\n", "utf8");
|
|
335
|
-
execFileSync("git", ["init", "-b", "main"], { cwd: root, stdio: "pipe" });
|
|
336
|
-
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "add", "-A"], { cwd: root, stdio: "pipe" });
|
|
337
|
-
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", "init", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
338
|
-
try {
|
|
339
|
-
const { routes } = setup({
|
|
340
|
-
config: { chorographyEnabled: true },
|
|
341
|
-
choro: { ...createChoroQueries({ store }), runT0Apply: ({ repoRoot }) => runChoroT0({ repoRoot, store, apply: true }) }
|
|
342
|
-
});
|
|
343
|
-
const route = handler(routes, "/api/dsh-mneme/choro/run");
|
|
344
|
-
const reposUrl = "/api/dsh-mneme/choro/repos";
|
|
345
|
-
const before = await invoke(handler(routes, reposUrl), reposUrl);
|
|
346
|
-
assert.equal(before.body.repos.length, 0, "起点确实是一个仓库都没登记");
|
|
347
|
-
|
|
348
|
-
const first = await invokePost(route, "/api/dsh-mneme/choro/run", { path: root });
|
|
349
|
-
assert.equal(first.status, 200);
|
|
350
|
-
assert.equal(first.body.ok, true);
|
|
351
|
-
assert.ok(first.body.receipt.moduleCount >= 1, "走的是真 T0(有划分回执)");
|
|
352
|
-
const after = await invoke(handler(routes, reposUrl), reposUrl);
|
|
353
|
-
assert.equal(after.body.repos.length, 1, "空态 + 路径 → 自动登记");
|
|
354
|
-
|
|
355
|
-
// 第二次同一路径:不能变成两条记录,也不能报错——面板按钮会被连点。
|
|
356
|
-
const again = await invokePost(route, "/api/dsh-mneme/choro/run", { rootPath: root, refresh: true });
|
|
357
|
-
assert.equal(again.status, 200);
|
|
358
|
-
assert.equal(again.body.refresh, true, "refresh 原样回显,面板据此区分「我要求过重跑」");
|
|
359
|
-
assert.equal((await invoke(handler(routes, reposUrl), reposUrl)).body.repos.length, 1, "同一路径复用既有 id");
|
|
360
|
-
} finally {
|
|
361
|
-
rmTempBestEffort(root);
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
test("/choro/run:路径给错时结构化报错,不落 500 一锅烩", async () => {
|
|
366
|
-
const store = createStore(":memory:");
|
|
367
|
-
const notGit = mkdtempSync(join(tmpdir(), "choro-notgit-"));
|
|
368
|
-
try {
|
|
369
|
-
const { routes } = setup({
|
|
370
|
-
config: { chorographyEnabled: true },
|
|
371
|
-
choro: { ...createChoroQueries({ store }), runT0Apply: ({ repoRoot }) => runChoroT0({ repoRoot, store, apply: true }) }
|
|
372
|
-
});
|
|
373
|
-
const route = handler(routes, "/api/dsh-mneme/choro/run");
|
|
374
|
-
|
|
375
|
-
const rel = await invokePost(route, "/api/dsh-mneme/choro/run", { path: "some/rel/dir" });
|
|
376
|
-
assert.equal(rel.status, 400);
|
|
377
|
-
assert.equal(rel.body.error, "path-not-absolute", "相对路径的语义取决于宿主 cwd,必须挡");
|
|
378
|
-
|
|
379
|
-
const missing = await invokePost(route, "/api/dsh-mneme/choro/run", { path: join(notGit, "nope") });
|
|
380
|
-
assert.equal(missing.status, 400);
|
|
381
|
-
assert.equal(missing.body.error, "path-not-found");
|
|
382
|
-
|
|
383
|
-
const plain = await invokePost(route, "/api/dsh-mneme/choro/run", { path: notGit });
|
|
384
|
-
assert.equal(plain.status, 409);
|
|
385
|
-
assert.equal(plain.body.error, "not-a-git-repo", "目录在但不是 git 仓库 → 409,不是 500");
|
|
386
|
-
} finally {
|
|
387
|
-
rmTempBestEffort(notGit);
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
// T1 会真花钱(模型调用),所以这里注入假的 startT1Job / t1JobStatus:
|
|
392
|
-
// 路由层要锁的只是闸门与 200/409/404 的分叉形状,不是进程本身。
|
|
393
|
-
function withT1({ startT1Job, t1JobStatus } = {}) {
|
|
394
|
-
const store = createStore(":memory:");
|
|
395
|
-
const root = makeRepo(store);
|
|
396
|
-
const choro = { ...createChoroQueries({ store }) };
|
|
397
|
-
if (startT1Job) choro.startT1Job = startT1Job;
|
|
398
|
-
if (t1JobStatus) choro.t1JobStatus = t1JobStatus;
|
|
399
|
-
return { store, root, choro };
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
test("/choro/t1:缺装配 503 / 面板闸关 409 / 同仓并发 409 job-running / 缺省 404", async () => {
|
|
403
|
-
const a = withT1({});
|
|
404
|
-
try {
|
|
405
|
-
const noJobs = setup({ config: { chorographyEnabled: true, chorographyT1FromPanel: true }, choro: a.choro });
|
|
406
|
-
const missing = await invokePost(handler(noJobs.routes, "/api/dsh-mneme/choro/t1"), "/api/dsh-mneme/choro/t1", { repo: 1 });
|
|
407
|
-
assert.equal(missing.status, 503, "没装配任务账 → 503 环境缺件");
|
|
408
|
-
assert.equal(missing.body.error, "unavailable");
|
|
409
|
-
} finally {
|
|
410
|
-
rmTempBestEffort(a.root);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const b = withT1({ startT1Job: () => ({ started: true, jobId: "j-1" }) });
|
|
414
|
-
try {
|
|
415
|
-
const off = setup({
|
|
416
|
-
config: { chorographyEnabled: true, chorographyT1FromPanel: false },
|
|
417
|
-
choro: b.choro
|
|
418
|
-
});
|
|
419
|
-
const disabled = await invokePost(handler(off.routes, "/api/dsh-mneme/choro/t1"), "/api/dsh-mneme/choro/t1", { repo: 1 });
|
|
420
|
-
assert.equal(disabled.status, 409, "T1 花模型额度,面板闸关着就不许起");
|
|
421
|
-
assert.equal(disabled.body.error, "disabled");
|
|
422
|
-
|
|
423
|
-
const on = setup({ config: { chorographyEnabled: true, chorographyT1FromPanel: true }, choro: b.choro });
|
|
424
|
-
const t1Route = handler(on.routes, "/api/dsh-mneme/choro/t1");
|
|
425
|
-
assert.equal(t1Route.policy, "token-write", "起一个会写页面/claims 的后台任务属数据面写");
|
|
426
|
-
|
|
427
|
-
const ok = await invokePost(t1Route, "/api/dsh-mneme/choro/t1", { repo: 1, module: "src", retryFailed: true });
|
|
428
|
-
assert.equal(ok.status, 200);
|
|
429
|
-
assert.deepEqual(ok.body, { jobId: "j-1", started: true });
|
|
430
|
-
|
|
431
|
-
const unknown = await invokePost(t1Route, "/api/dsh-mneme/choro/t1", { repo: 99 });
|
|
432
|
-
assert.equal(unknown.status, 404);
|
|
433
|
-
|
|
434
|
-
const busy = withT1({ startT1Job: () => ({ started: false, code: "job-running", jobId: "j-2" }) });
|
|
435
|
-
try {
|
|
436
|
-
const busySetup = setup({ config: { chorographyEnabled: true, chorographyT1FromPanel: true }, choro: busy.choro });
|
|
437
|
-
const running = await invokePost(handler(busySetup.routes, "/api/dsh-mneme/choro/t1"), "/api/dsh-mneme/choro/t1", { repo: 1 });
|
|
438
|
-
assert.equal(running.status, 409);
|
|
439
|
-
assert.equal(running.body.error, "job-running");
|
|
440
|
-
assert.equal(running.body.jobId, "j-2", "把在跑的那个 jobId 带回给面板,好让它接着轮询而不是重起");
|
|
441
|
-
} finally {
|
|
442
|
-
rmTempBestEffort(busy.root);
|
|
443
|
-
}
|
|
444
|
-
} finally {
|
|
445
|
-
rmTempBestEffort(b.root);
|
|
446
|
-
}
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
test("/choro/t1/status:jobId 缺 400、未知 404、已知 200 带契约字段", async () => {
|
|
450
|
-
const job = {
|
|
451
|
-
jobId: "j-9",
|
|
452
|
-
repo: { id: 1, name: "x", rootPath: "/x" },
|
|
453
|
-
state: "running",
|
|
454
|
-
startedAt: "T0",
|
|
455
|
-
modules: { total: 2, done: 1, failed: 0 },
|
|
456
|
-
logTail: ["line"]
|
|
457
|
-
};
|
|
458
|
-
const f = withT1({ startT1Job: () => ({ started: true, jobId: "j-9" }), t1JobStatus: (id) => (id === "j-9" ? job : null) });
|
|
459
|
-
try {
|
|
460
|
-
const { routes } = setup({ config: { chorographyEnabled: true }, choro: f.choro });
|
|
461
|
-
const route = handler(routes, "/api/dsh-mneme/choro/t1/status");
|
|
462
|
-
|
|
463
|
-
const noId = await invoke(route, "/api/dsh-mneme/choro/t1/status");
|
|
464
|
-
assert.equal(noId.status, 400);
|
|
465
|
-
assert.equal(noId.body.error, "jobId-required");
|
|
466
|
-
|
|
467
|
-
const gone = await invoke(route, "/api/dsh-mneme/choro/t1/status?jobId=nope");
|
|
468
|
-
assert.equal(gone.status, 404, "宿主重启后旧 jobId 必然查不到,面板据此回到「没有在跑的任务」");
|
|
469
|
-
|
|
470
|
-
const found = await invoke(route, "/api/dsh-mneme/choro/t1/status?jobId=j-9");
|
|
471
|
-
assert.equal(found.status, 200);
|
|
472
|
-
assert.equal(found.body.state, "running");
|
|
473
|
-
assert.equal(found.body.modules.total, 2);
|
|
474
|
-
assert.deepEqual(found.body.logTail, ["line"]);
|
|
475
|
-
} finally {
|
|
476
|
-
rmTempBestEffort(f.root);
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
// ---------------------------------------------------------------------------
|
|
481
|
-
// chorographyAutoRefresh:读侧自动重跑 T0(面板文案已经把它当契约写死了:
|
|
482
|
-
// 「读地志页时如果发现仓库代码变了,自动重跑一次 T0 骨架」)。
|
|
483
|
-
// 这一批同时锁「开关关着绝不偷偷改用户工作区」与「开关开着真的会刷」——只锁一端
|
|
484
|
-
// 的测试在实现成「永远不刷」或「永远刷」时都会绿,等于没锁。
|
|
485
|
-
// ---------------------------------------------------------------------------
|
|
486
|
-
|
|
487
|
-
// 计数外壳:真实 queries + 包一层的 runT0Apply(记调用次数、可注入失败)。
|
|
488
|
-
// 不造一个假 T0——「刷了但基线没推进」这种 bug 只有真 T0 才照得出来。
|
|
489
|
-
function countingChoro(store, { throwOnRun = false } = {}) {
|
|
490
|
-
const calls = [];
|
|
491
|
-
const base = createChoroQueries({ store });
|
|
492
|
-
return {
|
|
493
|
-
calls,
|
|
494
|
-
choro: {
|
|
495
|
-
...base,
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
assert.equal(
|
|
526
|
-
assert.equal(res.body.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
assert.equal(res.
|
|
544
|
-
|
|
545
|
-
assert.equal(res.body.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
assert.equal(listed.body.
|
|
570
|
-
assert.
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
const
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
assert.equal(res.
|
|
592
|
-
assert.
|
|
593
|
-
assert.equal(res.body.
|
|
594
|
-
assert.
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
assert.equal(
|
|
622
|
-
assert.equal(
|
|
623
|
-
assert.equal(
|
|
624
|
-
assert.
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
1
|
+
// chorography 四条读路由的回归锁(M1)。锁四层闸的语义分叉(#497 口径):
|
|
2
|
+
// choro queries 缺装配 → 503 unavailable(环境缺件);chorographyEnabled 关 →
|
|
3
|
+
// 409 disabled(用户没开);repo 解析失败 → 404 / 歧义 → 400 带可选清单;
|
|
4
|
+
// 快乐路径四条全通。数据用真实 T0 fixture 建(同 wiki-queries 的对账纪律)。
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { execFileSync } from "node:child_process";
|
|
12
|
+
import { createStore } from "../src/store.js";
|
|
13
|
+
import { createService } from "../src/service.js";
|
|
14
|
+
import { createApi } from "../src/api.js";
|
|
15
|
+
import { createSettings } from "../src/settings.js";
|
|
16
|
+
import { createChoroQueries } from "../src/wiki/queries.js";
|
|
17
|
+
import { runChoroT0 } from "../src/wiki/t0.js";
|
|
18
|
+
|
|
19
|
+
class FakeRes extends EventEmitter {
|
|
20
|
+
constructor() { super(); this.statusCode = 200; this.body = ""; }
|
|
21
|
+
writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
|
|
22
|
+
end(text) { this.body = text ?? ""; this.emit("end"); return this; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function req(path, method = "GET") {
|
|
26
|
+
const r = new EventEmitter();
|
|
27
|
+
r.url = path;
|
|
28
|
+
r.method = method;
|
|
29
|
+
r.headers = {};
|
|
30
|
+
return r;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function setup({ config = null, choro = null } = {}) {
|
|
34
|
+
const store = createStore(":memory:");
|
|
35
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
36
|
+
const settings = createSettings(store.db);
|
|
37
|
+
const routes = [];
|
|
38
|
+
const ctx = { webServer: { register(route) { routes.push(route); return () => {}; } } };
|
|
39
|
+
createApi(ctx, service, settings, { add: () => {}, remove: () => false, list: () => [] }, null, undefined, "", config, null, null, choro);
|
|
40
|
+
return { routes };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function handler(routes, path) {
|
|
44
|
+
return routes.find((r) => r.path === path);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function invoke(route, path) {
|
|
48
|
+
const res = new FakeRes();
|
|
49
|
+
await route.handler(req(path), res);
|
|
50
|
+
return { status: res.statusCode, body: JSON.parse(res.body || "{}") };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// POST 版:带 JSON body(readBody 走 data/end 事件)
|
|
54
|
+
async function invokePost(route, path, body) {
|
|
55
|
+
const r = req(path, "POST");
|
|
56
|
+
process.nextTick(() => {
|
|
57
|
+
r.emit("data", Buffer.from(JSON.stringify(body ?? {})));
|
|
58
|
+
r.emit("end");
|
|
59
|
+
});
|
|
60
|
+
const res = new FakeRes();
|
|
61
|
+
await route.handler(r, res);
|
|
62
|
+
return { status: res.statusCode, body: JSON.parse(res.body || "{}") };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function makeRepo(store) {
|
|
66
|
+
const root = mkdtempSync(join(tmpdir(), "choro-api-"));
|
|
67
|
+
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "apifix", scripts: { test: "node --test" } }, null, 2) + "\n", "utf8");
|
|
68
|
+
mkdirSync(join(root, "src"), { recursive: true });
|
|
69
|
+
writeFileSync(join(root, "src", "a.js"), 'import { b } from "./b.js";\nexport const a = () => b();\n', "utf8");
|
|
70
|
+
writeFileSync(join(root, "src", "b.js"), "export const b = () => 1;\n", "utf8");
|
|
71
|
+
execFileSync("git", ["init", "-b", "main"], { cwd: root, stdio: "pipe" });
|
|
72
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "add", "-A"], { cwd: root, stdio: "pipe" });
|
|
73
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", "init", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
74
|
+
runChoroT0({ repoRoot: root, store, apply: true, now: () => "T" });
|
|
75
|
+
return root;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function rmTempBestEffort(dir) {
|
|
79
|
+
try {
|
|
80
|
+
rmSync(dir, { recursive: true, force: true });
|
|
81
|
+
} catch {
|
|
82
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
|
|
83
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch { /* 留给系统清理 */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
test("四条 choro 路由都在册且 policy 全 open", () => {
|
|
88
|
+
const { routes } = setup({});
|
|
89
|
+
for (const p of ["/api/dsh-mneme/choro/repos", "/api/dsh-mneme/choro/modules", "/api/dsh-mneme/choro/page", "/api/dsh-mneme/choro/search"]) {
|
|
90
|
+
const route = handler(routes, p);
|
|
91
|
+
assert.ok(route, `${p} 在册`);
|
|
92
|
+
assert.equal(route.policy, "open");
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("queries 缺装配 → 503;开关关 → 409 disabled(语义分叉)", async () => {
|
|
97
|
+
const missing = setup({});
|
|
98
|
+
const reposRoute = handler(missing.routes, "/api/dsh-mneme/choro/repos");
|
|
99
|
+
const a = await invoke(reposRoute, "/api/dsh-mneme/choro/repos");
|
|
100
|
+
assert.equal(a.status, 503);
|
|
101
|
+
assert.equal(a.body.error, "unavailable");
|
|
102
|
+
|
|
103
|
+
const off = setup({ config: { chorographyEnabled: false }, choro: createChoroQueries({ store: createStore(":memory:") }) });
|
|
104
|
+
const b = await invoke(handler(off.routes, "/api/dsh-mneme/choro/repos"), "/api/dsh-mneme/choro/repos");
|
|
105
|
+
assert.equal(b.status, 409);
|
|
106
|
+
assert.equal(b.body.error, "disabled");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("快乐路径:repos / modules / page / search 全通", async () => {
|
|
110
|
+
const store = createStore(":memory:");
|
|
111
|
+
const root = makeRepo(store);
|
|
112
|
+
try {
|
|
113
|
+
const { routes } = setup({ config: { chorographyEnabled: true }, choro: createChoroQueries({ store }) });
|
|
114
|
+
const repos = await invoke(handler(routes, "/api/dsh-mneme/choro/repos"), "/api/dsh-mneme/choro/repos");
|
|
115
|
+
assert.equal(repos.status, 200);
|
|
116
|
+
assert.equal(repos.body.repos.length, 1);
|
|
117
|
+
const repoId = repos.body.repos[0].id;
|
|
118
|
+
|
|
119
|
+
const modules = await invoke(handler(routes, "/api/dsh-mneme/choro/modules"), `/api/dsh-mneme/choro/modules?repo=${repoId}`);
|
|
120
|
+
assert.equal(modules.status, 200);
|
|
121
|
+
assert.ok(modules.body.modules.length >= 2);
|
|
122
|
+
assert.ok(Array.isArray(modules.body.edges));
|
|
123
|
+
// 面板的地志页导入要把「根路径 + 页相对路径」拼成磁盘绝对路径(契约 §3.6),
|
|
124
|
+
// 回执里缺 relPath 时它会静默回落到「请先选模块」——这条锁的就是那个真机 bug。
|
|
125
|
+
const withPage = modules.body.modules.filter((m) => m.page_rel_path);
|
|
126
|
+
assert.ok(withPage.length >= 1, "fixture 里至少一个模块有骨架页");
|
|
127
|
+
for (const m of withPage) assert.equal(m.relPath, m.page_rel_path);
|
|
128
|
+
|
|
129
|
+
const page = await invoke(handler(routes, "/api/dsh-mneme/choro/page"), `/api/dsh-mneme/choro/page?repo=${repoId}&slug=src`);
|
|
130
|
+
assert.equal(page.status, 200);
|
|
131
|
+
assert.ok(page.body.page.content.includes("## 文件清单"));
|
|
132
|
+
|
|
133
|
+
const search = await invoke(handler(routes, "/api/dsh-mneme/choro/search"), `/api/dsh-mneme/choro/search?repo=${repoId}&q=b.js`);
|
|
134
|
+
assert.equal(search.status, 200);
|
|
135
|
+
assert.ok(search.body.hits.length >= 1);
|
|
136
|
+
} finally {
|
|
137
|
+
rmTempBestEffort(root);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("repo 解析失败各归各位:未知 → 404;歧义 → 400 带清单;slug 缺 → 400", async () => {
|
|
142
|
+
const store = createStore(":memory:");
|
|
143
|
+
const root = makeRepo(store);
|
|
144
|
+
try {
|
|
145
|
+
const { routes } = setup({ config: { chorographyEnabled: true }, choro: createChoroQueries({ store }) });
|
|
146
|
+
const notFound = await invoke(handler(routes, "/api/dsh-mneme/choro/modules"), "/api/dsh-mneme/choro/modules?repo=nope");
|
|
147
|
+
assert.equal(notFound.status, 404);
|
|
148
|
+
assert.equal(notFound.body.error, "repo-not-found");
|
|
149
|
+
|
|
150
|
+
const root2 = makeRepo(store);
|
|
151
|
+
try {
|
|
152
|
+
const ambiguous = await invoke(handler(routes, "/api/dsh-mneme/choro/modules"), "/api/dsh-mneme/choro/modules");
|
|
153
|
+
assert.equal(ambiguous.status, 400);
|
|
154
|
+
assert.equal(ambiguous.body.error, "repo-ambiguous");
|
|
155
|
+
assert.equal(ambiguous.body.repos.length, 2);
|
|
156
|
+
} finally {
|
|
157
|
+
rmTempBestEffort(root2);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const repoId = 1;
|
|
161
|
+
const noSlug = await invoke(handler(routes, "/api/dsh-mneme/choro/page"), `/api/dsh-mneme/choro/page?repo=${repoId}`);
|
|
162
|
+
assert.equal(noSlug.status, 400);
|
|
163
|
+
assert.equal(noSlug.body.error, "slug-required");
|
|
164
|
+
|
|
165
|
+
const badSlug = await invoke(handler(routes, "/api/dsh-mneme/choro/page"), `/api/dsh-mneme/choro/page?repo=${repoId}&slug=../etc/passwd`);
|
|
166
|
+
assert.equal(badSlug.status, 404, "路径穿越形态的 slug 走未知页 404(slug 只从 DB 白名单解析)");
|
|
167
|
+
} finally {
|
|
168
|
+
rmTempBestEffort(root);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("地志页路由区分缺失文件和不可读文件", async () => {
|
|
173
|
+
const store = createStore(":memory:");
|
|
174
|
+
const root = makeRepo(store);
|
|
175
|
+
try {
|
|
176
|
+
const { routes } = setup({ config: { chorographyEnabled: true }, choro: createChoroQueries({ store }) });
|
|
177
|
+
const pageRoute = handler(routes, "/api/dsh-mneme/choro/page");
|
|
178
|
+
const url = "/api/dsh-mneme/choro/page?slug=src";
|
|
179
|
+
const file = join(root, ".chorography", "modules", "src.md");
|
|
180
|
+
unlinkSync(file);
|
|
181
|
+
const missing = await invoke(pageRoute, url);
|
|
182
|
+
assert.equal(missing.status, 409);
|
|
183
|
+
assert.equal(missing.body.error, "page-missing");
|
|
184
|
+
assert.match(missing.body.hint, /dsh-mneme-choro/);
|
|
185
|
+
mkdirSync(file);
|
|
186
|
+
const unreadable = await invoke(pageRoute, url);
|
|
187
|
+
assert.equal(unreadable.status, 409);
|
|
188
|
+
assert.equal(unreadable.body.error, "page-unreadable");
|
|
189
|
+
assert.match(unreadable.body.hint, /permissions/);
|
|
190
|
+
} finally {
|
|
191
|
+
store.close();
|
|
192
|
+
rmTempBestEffort(root);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// 收尾档:POST /choro/run(面板重建入口,P34)+ /choro/search 的 semantic 路(P37–P39)
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
test("/choro/run:policy token-write,GET 405,POST 快乐路径重跑 T0", async () => {
|
|
200
|
+
const store = createStore(":memory:");
|
|
201
|
+
const root = makeRepo(store);
|
|
202
|
+
try {
|
|
203
|
+
const { routes } = setup({
|
|
204
|
+
config: { chorographyEnabled: true },
|
|
205
|
+
choro: {
|
|
206
|
+
...createChoroQueries({ store }),
|
|
207
|
+
// 与 index.js 的装配同形:runT0Apply 闭包住 store(P35:写路径与读侧分开装配)
|
|
208
|
+
runT0Apply: ({ repoRoot }) => runChoroT0({ repoRoot, store, apply: true }),
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
const route = handler(routes, "/api/dsh-mneme/choro/run");
|
|
212
|
+
assert.equal(route.policy, "token-write", "写仓库磁盘的口归 token-write");
|
|
213
|
+
|
|
214
|
+
const get = await invoke(route, "/api/dsh-mneme/choro/run");
|
|
215
|
+
assert.equal(get.status, 405, "只收 POST");
|
|
216
|
+
|
|
217
|
+
// 破坏页面文件后重建:POST 走真 T0,页回来
|
|
218
|
+
rmSync(join(root, ".chorography", "modules", "src.md"));
|
|
219
|
+
const ok = await invokePost(route, "/api/dsh-mneme/choro/run", { repo: 1 });
|
|
220
|
+
assert.equal(ok.status, 200);
|
|
221
|
+
assert.equal(ok.body.ok, true);
|
|
222
|
+
assert.ok(ok.body.receipt.moduleCount >= 2, "receipt 带划分计数");
|
|
223
|
+
assert.ok(existsSync(join(root, ".chorography", "modules", "src.md")), "重建把页写了回来");
|
|
224
|
+
} finally {
|
|
225
|
+
rmTempBestEffort(root);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("/choro/run:开关关 409;repo 未知 404(与读路由同一道闸)", async () => {
|
|
230
|
+
const store = createStore(":memory:");
|
|
231
|
+
const root = makeRepo(store);
|
|
232
|
+
try {
|
|
233
|
+
const off = setup({
|
|
234
|
+
config: { chorographyEnabled: false },
|
|
235
|
+
choro: { ...createChoroQueries({ store }), runT0Apply: () => { throw new Error("不该被调到"); } },
|
|
236
|
+
});
|
|
237
|
+
const a = await invokePost(handler(off.routes, "/api/dsh-mneme/choro/run"), "/api/dsh-mneme/choro/run", { repo: 1 });
|
|
238
|
+
assert.equal(a.status, 409);
|
|
239
|
+
|
|
240
|
+
const on = setup({
|
|
241
|
+
config: { chorographyEnabled: true },
|
|
242
|
+
choro: { ...createChoroQueries({ store }), runT0Apply: () => { throw new Error("不该被调到") } },
|
|
243
|
+
});
|
|
244
|
+
const b = await invokePost(handler(on.routes, "/api/dsh-mneme/choro/run"), "/api/dsh-mneme/choro/run", { repo: 99 });
|
|
245
|
+
assert.equal(b.status, 404);
|
|
246
|
+
} finally {
|
|
247
|
+
rmTempBestEffort(root);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("/choro/search:embedder 在场 → 惰性物化 + semantic 数组;缺席 → semantic null", async () => {
|
|
252
|
+
const store = createStore(":memory:");
|
|
253
|
+
const root = makeRepo(store);
|
|
254
|
+
try {
|
|
255
|
+
// stub embedder:向量按内容方向分类(src 相关 → [1,0]),保证可排序断言
|
|
256
|
+
const embedder = {
|
|
257
|
+
model: "stub-1",
|
|
258
|
+
embedSingle: async (text) => (/src|b\.js/.test(text) ? [1, 0] : [0, 1]),
|
|
259
|
+
embed: async (text) => (/src|b\.js/.test(text) ? [1, 0] : [0, 1]),
|
|
260
|
+
};
|
|
261
|
+
const { routes } = setup({
|
|
262
|
+
config: { chorographyEnabled: true },
|
|
263
|
+
choro: { ...createChoroQueries({ store }), embedder },
|
|
264
|
+
});
|
|
265
|
+
const s = await invoke(handler(routes, "/api/dsh-mneme/choro/search"), "/api/dsh-mneme/choro/search?repo=1&q=b.js");
|
|
266
|
+
assert.equal(s.status, 200);
|
|
267
|
+
assert.ok(Array.isArray(s.body.hits) && s.body.hits.length >= 1, "keyword hits 照旧");
|
|
268
|
+
assert.ok(Array.isArray(s.body.semantic) && s.body.semantic.length >= 1, "semantic 数组在场(惰性物化发生在这次请求里)");
|
|
269
|
+
assert.equal(s.body.semantic[0].slug, "src", "与查询同向的页排前");
|
|
270
|
+
assert.equal(typeof s.body.semantic[0].score, "number");
|
|
271
|
+
|
|
272
|
+
const noEmbedder = setup({ config: { chorographyEnabled: true }, choro: createChoroQueries({ store }) });
|
|
273
|
+
const t = await invoke(handler(noEmbedder.routes, "/api/dsh-mneme/choro/search"), "/api/dsh-mneme/choro/search?repo=1&q=b.js");
|
|
274
|
+
assert.equal(t.body.semantic, null, "embedder 缺席 → null(调用方退纯关键词)");
|
|
275
|
+
} finally {
|
|
276
|
+
rmTempBestEffort(root);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// 契约 §3.2(kb-qoder parity):面板闭环要的新字段与新入口
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
test("/choro/repos:契约字段齐备,git 漂移 → stale/git,plan 漂移 → stale/plan", async () => {
|
|
285
|
+
const store = createStore(":memory:");
|
|
286
|
+
const root = makeRepo(store);
|
|
287
|
+
try {
|
|
288
|
+
const { routes } = setup({ config: { chorographyEnabled: true }, choro: createChoroQueries({ store }) });
|
|
289
|
+
const url = "/api/dsh-mneme/choro/repos";
|
|
290
|
+
|
|
291
|
+
// 刚重建完:不该有任何过期信号——否则面板会对着一个刚生成的地志喊「代码已变」,
|
|
292
|
+
// 用户第一次用就学会不信任那个标记。
|
|
293
|
+
const fresh = await invoke(handler(routes, url), url);
|
|
294
|
+
assert.equal(fresh.status, 200);
|
|
295
|
+
const a = fresh.body.repos[0];
|
|
296
|
+
for (const key of ["id", "name", "rootPath", "modules", "pages", "claims", "legacy", "stale", "staleReason", "lastRunAt"]) {
|
|
297
|
+
assert.ok(key in a, `契约字段 ${key} 必须在场(面板按它渲染)`);
|
|
298
|
+
}
|
|
299
|
+
assert.equal(a.stale, false);
|
|
300
|
+
assert.equal(a.staleReason, null);
|
|
301
|
+
assert.equal(a.legacy, false);
|
|
302
|
+
assert.equal(typeof a.lastRunAt, "string");
|
|
303
|
+
assert.ok(a.modules >= 2 && a.pages >= 2, "modules/pages 是契约字段名(不是行里的 module_count)");
|
|
304
|
+
|
|
305
|
+
// 代码变了(新 commit)→ 说过期,理由必须是 git:面板那句话是「代码已变」。
|
|
306
|
+
writeFileSync(join(root, "src", "c.js"), "export const c = () => 2;\n", "utf8");
|
|
307
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "add", "-A"], { cwd: root, stdio: "pipe" });
|
|
308
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", "drift", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
309
|
+
const drifted = await invoke(handler(routes, url), url);
|
|
310
|
+
assert.equal(drifted.body.repos[0].stale, true);
|
|
311
|
+
assert.equal(drifted.body.repos[0].staleReason, "git");
|
|
312
|
+
|
|
313
|
+
// 先重建一次把 git 基线推平(否则 git 那一路永远优先,plan 判据根本轮不到),
|
|
314
|
+
// 再只改 plan.yaml(HEAD 不动)→ 理由必须是 plan:这条判据不依赖 git,不能被
|
|
315
|
+
// git 那一路掩盖掉,否则「改了模板配置但没提交」的仓库永远显示干净。
|
|
316
|
+
runChoroT0({ repoRoot: root, store, apply: true });
|
|
317
|
+
const cleaned = await invoke(handler(routes, url), url);
|
|
318
|
+
assert.equal(cleaned.body.repos[0].stale, false, "重建后 git 基线推平,过期信号应清掉");
|
|
319
|
+
writeFileSync(join(root, ".chorography", "plan.yaml"), "pages: []\n", "utf8");
|
|
320
|
+
const planDrift = await invoke(handler(routes, url), url);
|
|
321
|
+
assert.equal(planDrift.body.repos[0].stale, true);
|
|
322
|
+
assert.equal(planDrift.body.repos[0].staleReason, "plan");
|
|
323
|
+
} finally {
|
|
324
|
+
rmTempBestEffort(root);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("/choro/run:空态按绝对路径登记新仓库,重复路径复用同一 id", async () => {
|
|
329
|
+
const store = createStore(":memory:");
|
|
330
|
+
// 只把仓库目录准备好但不登记:这就是面板「一个仓库都没登记」的真实起点。
|
|
331
|
+
const root = mkdtempSync(join(tmpdir(), "choro-empty-"));
|
|
332
|
+
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "emptyfix" }, null, 2) + "\n", "utf8");
|
|
333
|
+
mkdirSync(join(root, "src"), { recursive: true });
|
|
334
|
+
writeFileSync(join(root, "src", "a.js"), "export const a = 1;\n", "utf8");
|
|
335
|
+
execFileSync("git", ["init", "-b", "main"], { cwd: root, stdio: "pipe" });
|
|
336
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "add", "-A"], { cwd: root, stdio: "pipe" });
|
|
337
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", "init", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
338
|
+
try {
|
|
339
|
+
const { routes } = setup({
|
|
340
|
+
config: { chorographyEnabled: true },
|
|
341
|
+
choro: { ...createChoroQueries({ store }), runT0Apply: ({ repoRoot }) => runChoroT0({ repoRoot, store, apply: true }) }
|
|
342
|
+
});
|
|
343
|
+
const route = handler(routes, "/api/dsh-mneme/choro/run");
|
|
344
|
+
const reposUrl = "/api/dsh-mneme/choro/repos";
|
|
345
|
+
const before = await invoke(handler(routes, reposUrl), reposUrl);
|
|
346
|
+
assert.equal(before.body.repos.length, 0, "起点确实是一个仓库都没登记");
|
|
347
|
+
|
|
348
|
+
const first = await invokePost(route, "/api/dsh-mneme/choro/run", { path: root });
|
|
349
|
+
assert.equal(first.status, 200);
|
|
350
|
+
assert.equal(first.body.ok, true);
|
|
351
|
+
assert.ok(first.body.receipt.moduleCount >= 1, "走的是真 T0(有划分回执)");
|
|
352
|
+
const after = await invoke(handler(routes, reposUrl), reposUrl);
|
|
353
|
+
assert.equal(after.body.repos.length, 1, "空态 + 路径 → 自动登记");
|
|
354
|
+
|
|
355
|
+
// 第二次同一路径:不能变成两条记录,也不能报错——面板按钮会被连点。
|
|
356
|
+
const again = await invokePost(route, "/api/dsh-mneme/choro/run", { rootPath: root, refresh: true });
|
|
357
|
+
assert.equal(again.status, 200);
|
|
358
|
+
assert.equal(again.body.refresh, true, "refresh 原样回显,面板据此区分「我要求过重跑」");
|
|
359
|
+
assert.equal((await invoke(handler(routes, reposUrl), reposUrl)).body.repos.length, 1, "同一路径复用既有 id");
|
|
360
|
+
} finally {
|
|
361
|
+
rmTempBestEffort(root);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("/choro/run:路径给错时结构化报错,不落 500 一锅烩", async () => {
|
|
366
|
+
const store = createStore(":memory:");
|
|
367
|
+
const notGit = mkdtempSync(join(tmpdir(), "choro-notgit-"));
|
|
368
|
+
try {
|
|
369
|
+
const { routes } = setup({
|
|
370
|
+
config: { chorographyEnabled: true },
|
|
371
|
+
choro: { ...createChoroQueries({ store }), runT0Apply: ({ repoRoot }) => runChoroT0({ repoRoot, store, apply: true }) }
|
|
372
|
+
});
|
|
373
|
+
const route = handler(routes, "/api/dsh-mneme/choro/run");
|
|
374
|
+
|
|
375
|
+
const rel = await invokePost(route, "/api/dsh-mneme/choro/run", { path: "some/rel/dir" });
|
|
376
|
+
assert.equal(rel.status, 400);
|
|
377
|
+
assert.equal(rel.body.error, "path-not-absolute", "相对路径的语义取决于宿主 cwd,必须挡");
|
|
378
|
+
|
|
379
|
+
const missing = await invokePost(route, "/api/dsh-mneme/choro/run", { path: join(notGit, "nope") });
|
|
380
|
+
assert.equal(missing.status, 400);
|
|
381
|
+
assert.equal(missing.body.error, "path-not-found");
|
|
382
|
+
|
|
383
|
+
const plain = await invokePost(route, "/api/dsh-mneme/choro/run", { path: notGit });
|
|
384
|
+
assert.equal(plain.status, 409);
|
|
385
|
+
assert.equal(plain.body.error, "not-a-git-repo", "目录在但不是 git 仓库 → 409,不是 500");
|
|
386
|
+
} finally {
|
|
387
|
+
rmTempBestEffort(notGit);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// T1 会真花钱(模型调用),所以这里注入假的 startT1Job / t1JobStatus:
|
|
392
|
+
// 路由层要锁的只是闸门与 200/409/404 的分叉形状,不是进程本身。
|
|
393
|
+
function withT1({ startT1Job, t1JobStatus } = {}) {
|
|
394
|
+
const store = createStore(":memory:");
|
|
395
|
+
const root = makeRepo(store);
|
|
396
|
+
const choro = { ...createChoroQueries({ store }) };
|
|
397
|
+
if (startT1Job) choro.startT1Job = startT1Job;
|
|
398
|
+
if (t1JobStatus) choro.t1JobStatus = t1JobStatus;
|
|
399
|
+
return { store, root, choro };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
test("/choro/t1:缺装配 503 / 面板闸关 409 / 同仓并发 409 job-running / 缺省 404", async () => {
|
|
403
|
+
const a = withT1({});
|
|
404
|
+
try {
|
|
405
|
+
const noJobs = setup({ config: { chorographyEnabled: true, chorographyT1FromPanel: true }, choro: a.choro });
|
|
406
|
+
const missing = await invokePost(handler(noJobs.routes, "/api/dsh-mneme/choro/t1"), "/api/dsh-mneme/choro/t1", { repo: 1 });
|
|
407
|
+
assert.equal(missing.status, 503, "没装配任务账 → 503 环境缺件");
|
|
408
|
+
assert.equal(missing.body.error, "unavailable");
|
|
409
|
+
} finally {
|
|
410
|
+
rmTempBestEffort(a.root);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const b = withT1({ startT1Job: () => ({ started: true, jobId: "j-1" }) });
|
|
414
|
+
try {
|
|
415
|
+
const off = setup({
|
|
416
|
+
config: { chorographyEnabled: true, chorographyT1FromPanel: false },
|
|
417
|
+
choro: b.choro
|
|
418
|
+
});
|
|
419
|
+
const disabled = await invokePost(handler(off.routes, "/api/dsh-mneme/choro/t1"), "/api/dsh-mneme/choro/t1", { repo: 1 });
|
|
420
|
+
assert.equal(disabled.status, 409, "T1 花模型额度,面板闸关着就不许起");
|
|
421
|
+
assert.equal(disabled.body.error, "disabled");
|
|
422
|
+
|
|
423
|
+
const on = setup({ config: { chorographyEnabled: true, chorographyT1FromPanel: true }, choro: b.choro });
|
|
424
|
+
const t1Route = handler(on.routes, "/api/dsh-mneme/choro/t1");
|
|
425
|
+
assert.equal(t1Route.policy, "token-write", "起一个会写页面/claims 的后台任务属数据面写");
|
|
426
|
+
|
|
427
|
+
const ok = await invokePost(t1Route, "/api/dsh-mneme/choro/t1", { repo: 1, module: "src", retryFailed: true });
|
|
428
|
+
assert.equal(ok.status, 200);
|
|
429
|
+
assert.deepEqual(ok.body, { jobId: "j-1", started: true });
|
|
430
|
+
|
|
431
|
+
const unknown = await invokePost(t1Route, "/api/dsh-mneme/choro/t1", { repo: 99 });
|
|
432
|
+
assert.equal(unknown.status, 404);
|
|
433
|
+
|
|
434
|
+
const busy = withT1({ startT1Job: () => ({ started: false, code: "job-running", jobId: "j-2" }) });
|
|
435
|
+
try {
|
|
436
|
+
const busySetup = setup({ config: { chorographyEnabled: true, chorographyT1FromPanel: true }, choro: busy.choro });
|
|
437
|
+
const running = await invokePost(handler(busySetup.routes, "/api/dsh-mneme/choro/t1"), "/api/dsh-mneme/choro/t1", { repo: 1 });
|
|
438
|
+
assert.equal(running.status, 409);
|
|
439
|
+
assert.equal(running.body.error, "job-running");
|
|
440
|
+
assert.equal(running.body.jobId, "j-2", "把在跑的那个 jobId 带回给面板,好让它接着轮询而不是重起");
|
|
441
|
+
} finally {
|
|
442
|
+
rmTempBestEffort(busy.root);
|
|
443
|
+
}
|
|
444
|
+
} finally {
|
|
445
|
+
rmTempBestEffort(b.root);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("/choro/t1/status:jobId 缺 400、未知 404、已知 200 带契约字段", async () => {
|
|
450
|
+
const job = {
|
|
451
|
+
jobId: "j-9",
|
|
452
|
+
repo: { id: 1, name: "x", rootPath: "/x" },
|
|
453
|
+
state: "running",
|
|
454
|
+
startedAt: "T0",
|
|
455
|
+
modules: { total: 2, done: 1, failed: 0 },
|
|
456
|
+
logTail: ["line"]
|
|
457
|
+
};
|
|
458
|
+
const f = withT1({ startT1Job: () => ({ started: true, jobId: "j-9" }), t1JobStatus: (id) => (id === "j-9" ? job : null) });
|
|
459
|
+
try {
|
|
460
|
+
const { routes } = setup({ config: { chorographyEnabled: true }, choro: f.choro });
|
|
461
|
+
const route = handler(routes, "/api/dsh-mneme/choro/t1/status");
|
|
462
|
+
|
|
463
|
+
const noId = await invoke(route, "/api/dsh-mneme/choro/t1/status");
|
|
464
|
+
assert.equal(noId.status, 400);
|
|
465
|
+
assert.equal(noId.body.error, "jobId-required");
|
|
466
|
+
|
|
467
|
+
const gone = await invoke(route, "/api/dsh-mneme/choro/t1/status?jobId=nope");
|
|
468
|
+
assert.equal(gone.status, 404, "宿主重启后旧 jobId 必然查不到,面板据此回到「没有在跑的任务」");
|
|
469
|
+
|
|
470
|
+
const found = await invoke(route, "/api/dsh-mneme/choro/t1/status?jobId=j-9");
|
|
471
|
+
assert.equal(found.status, 200);
|
|
472
|
+
assert.equal(found.body.state, "running");
|
|
473
|
+
assert.equal(found.body.modules.total, 2);
|
|
474
|
+
assert.deepEqual(found.body.logTail, ["line"]);
|
|
475
|
+
} finally {
|
|
476
|
+
rmTempBestEffort(f.root);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// chorographyAutoRefresh:读侧自动重跑 T0(面板文案已经把它当契约写死了:
|
|
482
|
+
// 「读地志页时如果发现仓库代码变了,自动重跑一次 T0 骨架」)。
|
|
483
|
+
// 这一批同时锁「开关关着绝不偷偷改用户工作区」与「开关开着真的会刷」——只锁一端
|
|
484
|
+
// 的测试在实现成「永远不刷」或「永远刷」时都会绿,等于没锁。
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
// 计数外壳:真实 queries + 包一层的 runT0Apply(记调用次数、可注入失败)。
|
|
488
|
+
// 不造一个假 T0——「刷了但基线没推进」这种 bug 只有真 T0 才照得出来。
|
|
489
|
+
function countingChoro(store, { throwOnRun = false } = {}) {
|
|
490
|
+
const calls = [];
|
|
491
|
+
const base = createChoroQueries({ store });
|
|
492
|
+
return {
|
|
493
|
+
calls,
|
|
494
|
+
choro: {
|
|
495
|
+
...base,
|
|
496
|
+
// guardT1Loss 必须原样透给真 T0:这条守卫的意义就在于「自动刷新那条路会拒绝」,
|
|
497
|
+
// 外壳把它吞掉就等于测了一个不存在的实现。
|
|
498
|
+
runT0Apply: ({ repoRoot, guardT1Loss = false }) => {
|
|
499
|
+
calls.push(repoRoot);
|
|
500
|
+
if (throwOnRun) throw new Error("磁盘只读(模拟)");
|
|
501
|
+
return runChoroT0({ repoRoot, store, apply: true, guardT1Loss });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 造一个「代码已变」的仓库:登记时的 git_baseline 与当前 HEAD 不同。
|
|
508
|
+
function driftRepo(store) {
|
|
509
|
+
const root = makeRepo(store);
|
|
510
|
+
writeFileSync(join(root, "src", "c.js"), "export const c = () => 2;\n", "utf8");
|
|
511
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "add", "-A"], { cwd: root, stdio: "pipe" });
|
|
512
|
+
execFileSync("git", ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "-m", "drift", "--no-gpg-sign"], { cwd: root, stdio: "pipe" });
|
|
513
|
+
return root;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
test("/choro/repos 自动重跑:开关关着时即使过期也一次都不刷(默认行为逐字节不变)", async () => {
|
|
517
|
+
const store = createStore(":memory:");
|
|
518
|
+
const root = driftRepo(store);
|
|
519
|
+
try {
|
|
520
|
+
const { calls, choro } = countingChoro(store);
|
|
521
|
+
const { routes } = setup({ config: { chorographyEnabled: true }, choro });
|
|
522
|
+
const url = "/api/dsh-mneme/choro/repos?repo=1";
|
|
523
|
+
const REPOS_PATH = "/api/dsh-mneme/choro/repos";
|
|
524
|
+
const res = await invoke(handler(routes, REPOS_PATH), url);
|
|
525
|
+
assert.equal(res.status, 200);
|
|
526
|
+
assert.equal(res.body.repos[0].staleReason, "git", "前提:这个仓库确实是过期的");
|
|
527
|
+
assert.equal(calls.length, 0, "开关缺省 false 时读侧一个字节都不许动用户的工作区");
|
|
528
|
+
assert.equal(res.body.autoRefreshed, false);
|
|
529
|
+
} finally {
|
|
530
|
+
rmTempBestEffort(root);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("/choro/repos 自动重跑:开关开 + ?repo= 点名且 git 过期 → 刷一次,回执按新基线说话", async () => {
|
|
535
|
+
const store = createStore(":memory:");
|
|
536
|
+
const root = driftRepo(store);
|
|
537
|
+
try {
|
|
538
|
+
const { calls, choro } = countingChoro(store);
|
|
539
|
+
const { routes } = setup({ config: { chorographyEnabled: true, chorographyAutoRefresh: true }, choro });
|
|
540
|
+
const url = "/api/dsh-mneme/choro/repos?repo=1";
|
|
541
|
+
const REPOS_PATH = "/api/dsh-mneme/choro/repos";
|
|
542
|
+
const res = await invoke(handler(routes, REPOS_PATH), url);
|
|
543
|
+
assert.equal(res.status, 200);
|
|
544
|
+
assert.equal(calls.length, 1, "开关开 + 指名 + git 过期 = 恰好刷这一个仓库");
|
|
545
|
+
assert.equal(res.body.autoRefreshed, true);
|
|
546
|
+
// 刷完必须改成不 stale:否则面板刚刷完就显示「代码已变」,等于谎报一次失败。
|
|
547
|
+
assert.equal(res.body.repos[0].stale, false);
|
|
548
|
+
assert.equal(res.body.repos[0].staleReason, null);
|
|
549
|
+
} finally {
|
|
550
|
+
rmTempBestEffort(root);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("/choro/repos 自动重跑:不 stale 或没点名 ?repo= 时都不刷(一次面板加载不能扫全库)", async () => {
|
|
555
|
+
const store = createStore(":memory:");
|
|
556
|
+
const cleanRoot = makeRepo(store);
|
|
557
|
+
try {
|
|
558
|
+
const { calls, choro } = countingChoro(store);
|
|
559
|
+
const { routes } = setup({ config: { chorographyEnabled: true, chorographyAutoRefresh: true }, choro });
|
|
560
|
+
const plain = "/api/dsh-mneme/choro/repos";
|
|
561
|
+
const clean = await invoke(handler(routes, plain), plain);
|
|
562
|
+
assert.equal(clean.status, 200);
|
|
563
|
+
assert.equal(calls.length, 0, "staleReason 为 null → 没有刷的理由");
|
|
564
|
+
|
|
565
|
+
// 造第二个过期仓库,但请求不带 ?repo=:列表路径一个都不许刷。
|
|
566
|
+
const drifted = driftRepo(store);
|
|
567
|
+
try {
|
|
568
|
+
const listed = await invoke(handler(routes, plain), plain);
|
|
569
|
+
assert.equal(listed.body.repos.length, 2);
|
|
570
|
+
assert.equal(calls.length, 0, "没点名就不刷:否则打开面板要付 N 次全仓扫描");
|
|
571
|
+
assert.equal(listed.body.autoRefreshed, false);
|
|
572
|
+
assert.ok(listed.body.repos.some((r) => r.staleReason === "git"), "过期信号照常回,只是不顺手刷");
|
|
573
|
+
} finally {
|
|
574
|
+
rmTempBestEffort(drifted);
|
|
575
|
+
}
|
|
576
|
+
} finally {
|
|
577
|
+
rmTempBestEffort(cleanRoot);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("/choro/repos 自动重跑失败:读照常 200、数据在、错误进 autoRefreshError", async () => {
|
|
582
|
+
const store = createStore(":memory:");
|
|
583
|
+
const root = driftRepo(store);
|
|
584
|
+
try {
|
|
585
|
+
const { calls, choro } = countingChoro(store, { throwOnRun: true });
|
|
586
|
+
const { routes } = setup({ config: { chorographyEnabled: true, chorographyAutoRefresh: true }, choro });
|
|
587
|
+
const url = "/api/dsh-mneme/choro/repos?repo=1";
|
|
588
|
+
const REPOS_PATH = "/api/dsh-mneme/choro/repos";
|
|
589
|
+
const res = await invoke(handler(routes, REPOS_PATH), url);
|
|
590
|
+
// 读是主诉求、刷新是附赠:打成 500 等于「顺手的事没办成,正事也不给办」。
|
|
591
|
+
assert.equal(res.status, 200, "刷新失败不许把读请求打成 500");
|
|
592
|
+
assert.equal(calls.length, 1);
|
|
593
|
+
assert.equal(res.body.autoRefreshed, false);
|
|
594
|
+
assert.match(res.body.autoRefreshError, /磁盘只读/);
|
|
595
|
+
assert.equal(res.body.repos.length, 1, "数据必须还在");
|
|
596
|
+
assert.equal(res.body.repos[0].staleReason, "git", "没刷成功就照实说过期");
|
|
597
|
+
} finally {
|
|
598
|
+
rmTempBestEffort(root);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// 自动刷新碰上已富化的仓库:不许动手。这条是「静默降级」的正对面——读页本来
|
|
603
|
+
// 只是个浏览动作,却会替用户把语义段和 claims 一起清掉,而屏幕上没有任何提示。
|
|
604
|
+
// 现在它变成一次主动拒绝,且拒绝与失败分开报(autoRefreshSkipped vs autoRefreshError)。
|
|
605
|
+
test("/choro/repos 自动重跑:仓库里有富化页时主动跳过,不动字节、不说成失败", async () => {
|
|
606
|
+
const store = createStore(":memory:");
|
|
607
|
+
const root = driftRepo(store);
|
|
608
|
+
try {
|
|
609
|
+
const repo = store.choroRepoByRoot(root);
|
|
610
|
+
const page = store.choroPageBySlug(repo.id, "src");
|
|
611
|
+
store.markChoroPageEnriched(page.id, "2026-11-01T00:00:00.000Z", null);
|
|
612
|
+
const pagePath = join(root, page.rel_path);
|
|
613
|
+
writeFileSync(pagePath, `${readFileSync(pagePath, "utf8")}\n<!-- CHORO:T1:START -->\n## 架构\n\na.js 是入口。\n<!-- CHORO:T1:END -->\n`, "utf8");
|
|
614
|
+
const before = readFileSync(pagePath, "utf8");
|
|
615
|
+
|
|
616
|
+
const { calls, choro } = countingChoro(store);
|
|
617
|
+
const { routes } = setup({ config: { chorographyEnabled: true, chorographyAutoRefresh: true }, choro });
|
|
618
|
+
const REPOS_PATH = "/api/dsh-mneme/choro/repos";
|
|
619
|
+
const res = await invoke(handler(routes, REPOS_PATH), `${REPOS_PATH}?repo=1`);
|
|
620
|
+
|
|
621
|
+
assert.equal(res.status, 200);
|
|
622
|
+
assert.equal(calls.length, 0, "有富化页时连 T0 都不该被叫起来");
|
|
623
|
+
assert.equal(res.body.autoRefreshed, false);
|
|
624
|
+
assert.equal(res.body.autoRefreshSkipped, "t1-loss", "跳过要和失败分开报");
|
|
625
|
+
assert.equal(res.body.autoRefreshError, undefined, "主动拒绝不是错误");
|
|
626
|
+
assert.match(res.body.autoRefreshSkipNote, /1 页/, "说清为什么没做");
|
|
627
|
+
assert.equal(readFileSync(pagePath, "utf8"), before, "页一字节未动");
|
|
628
|
+
assert.equal(res.body.repos[0].staleReason, "git", "没刷就照实说过期");
|
|
629
|
+
assert.equal(res.body.repos[0].enrichedPages, 1, "面板要靠这个数解释原因");
|
|
630
|
+
} finally {
|
|
631
|
+
store.close();
|
|
632
|
+
rmTempBestEffort(root);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// 库里的账和磁盘对不上时(status 还是 skeleton、页里却有 T1 段),T0 的读盘判断
|
|
637
|
+
// 会抛 would-drop-t1。到这一层仍按「主动跳过」回,绝不报成刷新失败。
|
|
638
|
+
test("/choro/page 自动重跑:T0 拒绝(would-drop-t1)时回 skipped 而非 error", async () => {
|
|
639
|
+
const store = createStore(":memory:");
|
|
640
|
+
const root = driftRepo(store);
|
|
641
|
+
try {
|
|
642
|
+
const repo = store.choroRepoByRoot(root);
|
|
643
|
+
const page = store.choroPageBySlug(repo.id, "src");
|
|
644
|
+
const pagePath = join(root, page.rel_path);
|
|
645
|
+
// 故意不改库里的 status:模拟「有人手改了 MD」这类账实不符。
|
|
646
|
+
writeFileSync(pagePath, `${readFileSync(pagePath, "utf8")}\n<!-- CHORO:T1:START -->\n## 架构\n\na.js 是入口。\n<!-- CHORO:T1:END -->\n`, "utf8");
|
|
647
|
+
const before = readFileSync(pagePath, "utf8");
|
|
648
|
+
|
|
649
|
+
const { choro } = countingChoro(store);
|
|
650
|
+
const { routes } = setup({ config: { chorographyEnabled: true, chorographyAutoRefresh: true }, choro });
|
|
651
|
+
const PAGE_PATH = "/api/dsh-mneme/choro/page";
|
|
652
|
+
const res = await invoke(handler(routes, PAGE_PATH), `${PAGE_PATH}?repo=1&slug=src`);
|
|
653
|
+
|
|
654
|
+
assert.equal(res.status, 200);
|
|
655
|
+
assert.equal(res.body.autoRefreshed, false);
|
|
656
|
+
assert.equal(res.body.autoRefreshSkipped, "t1-loss");
|
|
657
|
+
assert.equal(res.body.autoRefreshError, undefined);
|
|
658
|
+
assert.match(res.body.autoRefreshSkipNote, /富化/, "T0 那句人话原样透出去");
|
|
659
|
+
assert.equal(readFileSync(pagePath, "utf8"), before, "拒绝发生在写盘之前");
|
|
660
|
+
assert.ok(res.body.page.content.includes("CHORO:T1:START"), "页正文照常回,别把读也带坏");
|
|
661
|
+
} finally {
|
|
662
|
+
store.close();
|
|
663
|
+
rmTempBestEffort(root);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// /choro/page 是面板主路径,也是「读地志页时发现代码变了」那句文案的真正时刻。
|
|
668
|
+
// 顶栏「代码已变」徽章读 state.repo.stale —— 回执里没有 repo 这份数据,徽章永远不亮。
|
|
669
|
+
test("/choro/page 回执带 repo.stale/staleReason,并在这条读路径上自动重跑", async () => {
|
|
670
|
+
const store = createStore(":memory:");
|
|
671
|
+
const root = driftRepo(store);
|
|
672
|
+
try {
|
|
673
|
+
const { calls, choro } = countingChoro(store);
|
|
674
|
+
const url = "/api/dsh-mneme/choro/page?repo=1&slug=src";
|
|
675
|
+
const PAGE_PATH = "/api/dsh-mneme/choro/page";
|
|
676
|
+
|
|
677
|
+
const offSetup = setup({ config: { chorographyEnabled: true }, choro });
|
|
678
|
+
const off = await invoke(handler(offSetup.routes, PAGE_PATH), url);
|
|
679
|
+
assert.equal(off.status, 200);
|
|
680
|
+
assert.equal(off.body.repo.stale, true, "徽章的数据源:开关关着也要照实报过期");
|
|
681
|
+
assert.equal(off.body.repo.staleReason, "git");
|
|
682
|
+
assert.equal(off.body.autoRefreshed, false);
|
|
683
|
+
assert.equal(calls.length, 0);
|
|
684
|
+
|
|
685
|
+
const onSetup = setup({ config: { chorographyEnabled: true, chorographyAutoRefresh: true }, choro });
|
|
686
|
+
const on = await invoke(handler(onSetup.routes, PAGE_PATH), url);
|
|
687
|
+
assert.equal(on.status, 200);
|
|
688
|
+
assert.equal(calls.length, 1, "页读路径点名的就是这一个仓库");
|
|
689
|
+
assert.equal(on.body.autoRefreshed, true);
|
|
690
|
+
assert.equal(on.body.repo.stale, false, "刷完页面与 repo 徽章必须同一时刻一致");
|
|
691
|
+
assert.ok(on.body.page.content.includes("## 文件清单"), "页正文照常回");
|
|
692
|
+
} finally {
|
|
693
|
+
rmTempBestEffort(root);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
// --- /choro/claims(claim 粒度失效,Grounded Claims)-----------------------
|
|
697
|
+
// 锁的是一条态度:算不出来 ≠ 没事。基线认不出时必须 anchorStale,不许回一张空清单
|
|
698
|
+
// 让面板显示「全部结论都还准」——那是 #607 同类的假成功。
|
|
699
|
+
|
|
700
|
+
test("/choro/claims 在册且 policy open,四层闸与其余读路由一致", async () => {
|
|
701
|
+
const off = setup({});
|
|
702
|
+
const route = handler(off.routes, "/api/dsh-mneme/choro/claims");
|
|
703
|
+
assert.ok(route, "/choro/claims 在册");
|
|
704
|
+
assert.equal(route.policy, "open");
|
|
705
|
+
const a = await invoke(route, "/api/dsh-mneme/choro/claims");
|
|
706
|
+
assert.equal(a.status, 503, "queries 缺装配 → 503");
|
|
707
|
+
|
|
708
|
+
const disabled = setup({ config: { chorographyEnabled: false }, choro: createChoroQueries({ store: createStore(":memory:") }) });
|
|
709
|
+
const b = await invoke(handler(disabled.routes, "/api/dsh-mneme/choro/claims"), "/api/dsh-mneme/choro/claims");
|
|
710
|
+
assert.equal(b.status, 409, "开关关 → 409");
|
|
711
|
+
assert.equal(b.body.error, "disabled");
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test("/choro/claims:快乐路径带页清单与 totals;未知 repo 404", async () => {
|
|
715
|
+
const store = createStore(":memory:");
|
|
716
|
+
const root = makeRepo(store);
|
|
717
|
+
try {
|
|
718
|
+
const { routes } = setup({ config: { chorographyEnabled: true }, choro: createChoroQueries({ store }) });
|
|
719
|
+
const route = handler(routes, "/api/dsh-mneme/choro/claims");
|
|
720
|
+
const repoId = store.db.prepare("SELECT id FROM choro_repos LIMIT 1").get().id;
|
|
721
|
+
const r = await invoke(route, `/api/dsh-mneme/choro/claims?repo=${repoId}`);
|
|
722
|
+
assert.equal(r.status, 200);
|
|
723
|
+
assert.ok(r.body.repo && r.body.repo.id === repoId);
|
|
724
|
+
assert.equal(typeof r.body.head, "string");
|
|
725
|
+
assert.ok(Array.isArray(r.body.pages), "页清单要在——面板靠它说「这页没问题」");
|
|
726
|
+
assert.ok(r.body.totals && Number.isInteger(r.body.totals.claims));
|
|
727
|
+
assert.equal(r.body.clean, true, "刚跑完 T0,基线就是 HEAD");
|
|
728
|
+
|
|
729
|
+
const missing = await invoke(route, "/api/dsh-mneme/choro/claims?repo=999999");
|
|
730
|
+
assert.equal(missing.status, 404);
|
|
731
|
+
} finally {
|
|
732
|
+
store.close();
|
|
733
|
+
rmTempBestEffort(root);
|
|
734
|
+
}
|
|
628
735
|
});
|