@modusensus/dsh-mneme 0.14.0-beta.1 → 0.14.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +830 -830
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +964 -964
- package/bin/dsh-mneme-choro.mjs +262 -262
- package/bin/dsh-mneme-mcp.mjs +510 -510
- package/bin/dsh-mneme-serve.mjs +151 -151
- package/cordis.patch.yml +21 -21
- package/docs/CONFIGURATION.md +407 -404
- package/docs/CONTINUITY.md +139 -139
- package/docs/DAEMON.md +57 -57
- package/docs/DIRECTORIES.md +114 -114
- package/docs/DISTILL.md +397 -371
- package/docs/DSH-GO.md +97 -97
- package/docs/ENTITIES.md +247 -247
- package/docs/FORGET.md +77 -77
- package/docs/KB.md +110 -110
- package/docs/LOCAL_MODEL.md +222 -222
- package/docs/SEMANTIC.md +256 -256
- package/docs/SHARE.md +552 -552
- package/docs/SLEEP.md +202 -202
- package/docs/STORAGE.md +122 -122
- package/docs/WORKFLOWS.md +366 -366
- package/lib/api-standalone.js +1007 -1007
- package/lib/api.js +3600 -3600
- package/lib/attachment-text.js +122 -122
- package/lib/bootstrap-http.js +105 -105
- package/lib/bootstrap.js +215 -215
- package/lib/candidates.js +454 -454
- package/lib/capabilities.js +819 -819
- package/lib/choro-command.js +111 -111
- package/lib/client.js +14420 -14322
- package/lib/commands.js +95 -95
- package/lib/config-keys.js +144 -144
- package/lib/config.js +1361 -1344
- package/lib/content-hash.js +54 -54
- package/lib/continuity.js +258 -258
- package/lib/date-hint.js +32 -0
- package/lib/diary/auto.js +190 -190
- package/lib/diary/compose.js +440 -440
- package/lib/diary/dir.js +59 -59
- package/lib/diary/export.js +97 -97
- package/lib/diary/index.js +26 -26
- package/lib/diary/library.js +189 -189
- package/lib/diary/qq-inbox.js +142 -142
- package/lib/diary/rollover.js +61 -61
- package/lib/diary/session-provider.js +122 -122
- package/lib/document-index.js +94 -94
- package/lib/document.js +322 -322
- package/lib/dream/clustering.js +123 -123
- package/lib/dream/decisions.js +945 -945
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +2027 -2027
- package/lib/dream/triage.js +139 -139
- package/lib/dream.js +1879 -1879
- package/lib/dsh-go/host.js +162 -162
- package/lib/dsh-go/panel.js +2091 -2091
- package/lib/dsh-go/text.js +342 -342
- package/lib/dsh-go/views/common.js +68 -68
- package/lib/dsh-go/views/entity.js +48 -48
- package/lib/dsh-go/views/memory.js +198 -198
- package/lib/dsh-go/views/root.js +30 -30
- package/lib/dsh-go/views/settings.js +398 -398
- package/lib/dsh-go/views/status.js +51 -51
- package/lib/embedding.js +342 -342
- package/lib/entities/extractor.js +285 -285
- package/lib/fence.js +24 -24
- package/lib/graph/anchoring.js +184 -184
- package/lib/guide.js +74 -74
- package/lib/heat.js +144 -144
- package/lib/hot-memory.js +119 -119
- package/lib/image-describe.js +158 -0
- package/lib/index.js +852 -852
- package/lib/inject.js +656 -651
- package/lib/kb/index.js +309 -309
- package/lib/kb/routes.js +219 -219
- package/lib/kb/tools.js +148 -148
- package/lib/lang.js +607 -595
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +162 -162
- package/lib/memory-source.js +9 -9
- package/lib/mirror.js +330 -330
- package/lib/organize.js +456 -456
- package/lib/peak-hours.js +172 -172
- package/lib/pipeline-activities.js +178 -178
- package/lib/pipeline-attribution.js +165 -165
- package/lib/pipeline-command.js +279 -279
- package/lib/pipeline-confirm.js +142 -142
- package/lib/pipeline-exec.js +174 -174
- package/lib/pipeline-nudge-auto.js +129 -129
- package/lib/pipeline-nudge.js +1623 -1623
- package/lib/pipeline-observations.js +181 -181
- package/lib/pipeline-observe.js +133 -133
- package/lib/pipeline-runner.js +361 -361
- package/lib/pipeline-runs.js +442 -442
- package/lib/pipeline.js +716 -716
- package/lib/placement.js +13 -13
- package/lib/pre-inject-gate.js +354 -354
- package/lib/profile-extract.js +164 -164
- package/lib/profile-facts.js +356 -356
- package/lib/provenance-http.js +251 -251
- package/lib/quality-filter.js +162 -162
- package/lib/recall-stats.js +190 -190
- package/lib/reclaim-http.js +150 -150
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -265
- package/lib/runtime/loader.js +252 -252
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +223 -223
- package/lib/safe-slice.js +18 -18
- package/lib/scope-state.js +277 -277
- package/lib/scope.js +187 -187
- package/lib/search/adaptive.js +60 -60
- package/lib/search/bm25.js +96 -96
- package/lib/semantic.js +572 -572
- package/lib/sensitive-scan.js +209 -209
- package/lib/serve.js +228 -228
- package/lib/service.js +3268 -3268
- package/lib/session-gate.js +91 -91
- package/lib/session-usage.js +350 -350
- package/lib/settings.js +827 -817
- package/lib/share-http.js +281 -281
- package/lib/share-import.js +234 -234
- package/lib/share.js +335 -335
- package/lib/store.js +5895 -5895
- package/lib/summarize.js +1388 -1343
- package/lib/tools.js +1370 -1356
- package/lib/user-settings.js +230 -230
- package/lib/vector-index.js +180 -180
- package/lib/version-check.js +122 -122
- package/lib/wiki/claims.js +267 -267
- package/lib/wiki/cli-hint.js +90 -90
- package/lib/wiki/inject.js +164 -164
- package/lib/wiki/partition.js +200 -200
- package/lib/wiki/plan.js +214 -214
- package/lib/wiki/queries.js +640 -640
- package/lib/wiki/render.js +261 -261
- package/lib/wiki/scan.js +371 -371
- package/lib/wiki/t0.js +500 -500
- package/lib/wiki/t1-jobs.js +232 -232
- package/lib/wiki/t1.js +609 -609
- package/lib/write-admission.js +404 -404
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +1 -1
- package/runtime-manifest.json +526 -526
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +249 -249
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -224
- package/scripts/changelog-prep.mjs +91 -91
- package/scripts/check-sync.js +44 -44
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/publish-gate.mjs +197 -197
- package/scripts/release-docs.mjs +210 -210
- package/scripts/release-notes.mjs +164 -164
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +69 -69
- package/scripts/test-count-sync.mjs +48 -48
- package/src/api-standalone.js +1007 -1007
- package/src/api.js +3600 -3600
- package/src/attachment-text.js +122 -122
- package/src/bootstrap-http.js +105 -105
- package/src/bootstrap.js +215 -215
- package/src/candidates.js +454 -454
- package/src/capabilities.js +819 -819
- package/src/choro-command.js +111 -111
- package/src/commands.js +95 -95
- package/src/config-keys.js +144 -144
- package/src/config.js +1361 -1344
- package/src/content-hash.js +54 -54
- package/src/continuity.js +258 -258
- package/src/date-hint.js +32 -0
- package/src/diary/auto.js +190 -190
- package/src/diary/compose.js +440 -440
- package/src/diary/dir.js +59 -59
- package/src/diary/export.js +97 -97
- package/src/diary/index.js +26 -26
- package/src/diary/library.js +189 -189
- package/src/diary/qq-inbox.js +142 -142
- package/src/diary/rollover.js +61 -61
- package/src/diary/session-provider.js +122 -122
- package/src/document-index.js +94 -94
- package/src/document.js +322 -322
- package/src/dream/clustering.js +123 -123
- package/src/dream/decisions.js +945 -945
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +2027 -2027
- package/src/dream/triage.js +139 -139
- package/src/dream.js +1879 -1879
- package/src/dsh-go/host.js +162 -162
- package/src/dsh-go/panel.js +2091 -2091
- package/src/dsh-go/text.js +342 -342
- package/src/dsh-go/views/common.js +68 -68
- package/src/dsh-go/views/entity.js +48 -48
- package/src/dsh-go/views/memory.js +198 -198
- package/src/dsh-go/views/root.js +30 -30
- package/src/dsh-go/views/settings.js +398 -398
- package/src/dsh-go/views/status.js +51 -51
- package/src/embedding.js +342 -342
- package/src/entities/extractor.js +285 -285
- package/src/fence.js +24 -24
- package/src/graph/anchoring.js +184 -184
- package/src/guide.js +74 -74
- package/src/heat.js +144 -144
- package/src/hot-memory.js +119 -119
- package/src/image-describe.js +158 -0
- package/src/index.js +852 -852
- package/src/inject.js +656 -651
- package/src/kb/index.js +309 -309
- package/src/kb/routes.js +219 -219
- package/src/kb/tools.js +148 -148
- package/src/lang.js +607 -595
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +162 -162
- package/src/memory-source.js +9 -9
- package/src/mirror.js +330 -330
- package/src/organize.js +456 -456
- package/src/peak-hours.js +172 -172
- package/src/pipeline-activities.js +178 -178
- package/src/pipeline-attribution.js +165 -165
- package/src/pipeline-command.js +279 -279
- package/src/pipeline-confirm.js +142 -142
- package/src/pipeline-exec.js +174 -174
- package/src/pipeline-nudge-auto.js +129 -129
- package/src/pipeline-nudge.js +1623 -1623
- package/src/pipeline-observations.js +181 -181
- package/src/pipeline-observe.js +133 -133
- package/src/pipeline-runner.js +361 -361
- package/src/pipeline-runs.js +442 -442
- package/src/pipeline.js +716 -716
- package/src/placement.js +13 -13
- package/src/pre-inject-gate.js +354 -354
- package/src/profile-extract.js +164 -164
- package/src/profile-facts.js +356 -356
- package/src/provenance-http.js +251 -251
- package/src/quality-filter.js +162 -162
- package/src/recall-stats.js +190 -190
- package/src/reclaim-http.js +150 -150
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -265
- package/src/runtime/loader.js +252 -252
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +223 -223
- package/src/safe-slice.js +18 -18
- package/src/scope-state.js +277 -277
- package/src/scope.js +187 -187
- package/src/search/adaptive.js +60 -60
- package/src/search/bm25.js +96 -96
- package/src/semantic.js +572 -572
- package/src/sensitive-scan.js +209 -209
- package/src/serve.js +228 -228
- package/src/service.js +3268 -3268
- package/src/session-gate.js +91 -91
- package/src/session-usage.js +350 -350
- package/src/settings.js +827 -817
- package/src/share-http.js +281 -281
- package/src/share-import.js +234 -234
- package/src/share.js +335 -335
- package/src/store.js +5895 -5895
- package/src/summarize.js +1388 -1343
- package/src/tools.js +1370 -1356
- package/src/user-settings.js +230 -230
- package/src/vector-index.js +180 -180
- package/src/version-check.js +122 -122
- package/src/wiki/claims.js +267 -267
- package/src/wiki/cli-hint.js +90 -90
- package/src/wiki/inject.js +164 -164
- package/src/wiki/partition.js +200 -200
- package/src/wiki/plan.js +214 -214
- package/src/wiki/queries.js +640 -640
- package/src/wiki/render.js +261 -261
- package/src/wiki/scan.js +371 -371
- package/src/wiki/t0.js +500 -500
- package/src/wiki/t1-jobs.js +232 -232
- package/src/wiki/t1.js +609 -609
- package/src/write-admission.js +404 -404
- package/test/adaptive-inject-budget.test.js +85 -85
- package/test/api-hardening.test.js +173 -173
- package/test/api-nudge-route.test.js +259 -259
- package/test/api-routes-gaps.test.js +1092 -1092
- package/test/api.test.js +3007 -2998
- package/test/attachment-text.test.js +139 -139
- package/test/audit.test.js +448 -448
- package/test/baseline-persist.test.js +248 -248
- package/test/benchmark.test.js +94 -94
- package/test/bm25-corpus-cache.test.js +145 -145
- package/test/bootstrap.test.js +228 -228
- package/test/candidates.test.js +345 -345
- package/test/capabilities.test.js +577 -577
- package/test/capacity-warning.test.js +262 -262
- package/test/changelog-prep.test.js +125 -125
- package/test/choro-command.test.js +218 -218
- package/test/cli-enums.test.js +37 -37
- package/test/cli-flag-value.test.js +86 -86
- package/test/cli-receive.test.js +390 -390
- package/test/cli-share.test.js +437 -437
- package/test/client-share.test.js +225 -225
- package/test/client.test.js +4595 -4425
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config-keys-routes.test.js +233 -233
- package/test/config.test.js +153 -153
- package/test/conflict-freeze.test.js +344 -344
- package/test/conflict-ledger-enforce.test.js +452 -452
- package/test/conflict-ledger.test.js +242 -242
- package/test/conflict-reviewed.test.js +79 -79
- package/test/content-hash.test.js +204 -204
- package/test/continuity.test.js +452 -452
- package/test/contrast.test.js +432 -432
- package/test/date-hint.test.js +156 -0
- package/test/dedupe-candidate-window.test.js +180 -180
- package/test/dependency-overrides.test.js +78 -78
- package/test/diary-library.test.js +187 -187
- package/test/diary.test.js +576 -576
- package/test/differentiate-feedback.test.js +248 -248
- package/test/distill-tx-embed-gap.test.js +110 -110
- package/test/docs-config-keys.test.js +65 -65
- package/test/document-dir.test.js +233 -233
- package/test/document.test.js +703 -703
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-baseline-clamp.test.js +174 -174
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream-decision-evidence.test.js +314 -314
- package/test/dream-failure-backoff.test.js +173 -173
- package/test/dream-fence.test.js +207 -207
- package/test/dream-maxtokens-fallback.test.js +132 -132
- package/test/dream-merge-guardrail.test.js +219 -219
- package/test/dream-peak-hours.test.js +194 -194
- package/test/dream.test.js +1528 -1528
- package/test/dsh-go-host.test.js +134 -134
- package/test/dsh-go-panel-actions.test.js +854 -854
- package/test/dsh-go-panel.test.js +251 -251
- package/test/embed-ready-poll.test.js +94 -94
- package/test/embed-text-reasons.test.js +112 -112
- package/test/embedding-cache-f32.test.js +276 -276
- package/test/embedding-ready.test.js +226 -226
- package/test/entities.test.js +711 -711
- package/test/entity-recall.test.js +179 -179
- package/test/epistemic.test.js +325 -325
- package/test/failure-query.test.js +147 -147
- package/test/failure-retention.test.js +105 -105
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/forget-corrections.test.js +70 -70
- package/test/graph-anchoring.test.js +184 -184
- package/test/graph-api.test.js +333 -333
- package/test/graph-boosted-relations.test.js +189 -189
- package/test/graph-inject-budget.test.js +103 -103
- package/test/graph-multipath.test.js +269 -269
- package/test/graph-passive-confirm.test.js +91 -91
- package/test/graph-weight-evolution.test.js +104 -104
- package/test/heat.test.js +175 -175
- package/test/helpers/dream-mock.js +86 -86
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/helpers/write-admission-samples.js +312 -312
- package/test/hot-memory.test.js +259 -259
- package/test/image-description.test.js +271 -0
- package/test/image-memory.test.js +170 -170
- package/test/inject-content-cap.test.js +91 -91
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +77 -77
- package/test/inject-parent-gate.test.js +134 -109
- package/test/inject-pin-explicit.test.js +300 -300
- package/test/inject-pin-vector-crowd.test.js +225 -225
- package/test/inject-pools.test.js +195 -195
- package/test/inject-recall.test.js +172 -172
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +553 -553
- package/test/issue-617-step1-query-and-fusion.test.js +361 -361
- package/test/kb-api-wiring.test.js +128 -128
- package/test/kb-collections.test.js +272 -272
- package/test/kb-routes.test.js +379 -379
- package/test/lang.test.js +98 -98
- package/test/lastrunat-persist.test.js +101 -101
- package/test/lib-smoke.test.js +116 -116
- package/test/llm-audit.test.js +815 -815
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +501 -501
- package/test/mcp-stdio.test.js +385 -385
- package/test/mint-source.test.js +311 -311
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror-types.test.js +208 -208
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -162
- package/test/organize-tool.test.js +232 -232
- package/test/organize.test.js +418 -418
- package/test/panel-chart-scale.test.js +90 -90
- package/test/panel-tokens.test.js +245 -245
- package/test/peak-hours-observability.test.js +70 -70
- package/test/peer-blockers.test.js +193 -193
- package/test/persona-addressing.test.js +83 -83
- package/test/persona-api.test.js +172 -172
- package/test/persona-by-session.test.js +568 -568
- package/test/persona-flag.test.js +36 -36
- package/test/persona-inject.test.js +145 -145
- package/test/persona-settings.test.js +198 -198
- package/test/pipeline-activities.test.js +155 -155
- package/test/pipeline-attribution.test.js +175 -175
- package/test/pipeline-command.test.js +243 -243
- package/test/pipeline-confirm.test.js +148 -148
- package/test/pipeline-exec.test.js +172 -172
- package/test/pipeline-nudge-auto.test.js +248 -248
- package/test/pipeline-nudge.test.js +1278 -1278
- package/test/pipeline-observations.test.js +157 -157
- package/test/pipeline-observe.test.js +127 -127
- package/test/pipeline-r1r2.test.js +278 -278
- package/test/pipeline-runner.test.js +468 -468
- package/test/pipeline-runs.test.js +767 -767
- package/test/pipeline.test.js +636 -636
- package/test/placement.test.js +36 -36
- package/test/policy-epoch.test.js +259 -259
- package/test/pre-inject-gate.test.js +444 -444
- package/test/profile-extract.test.js +269 -269
- package/test/profile-facts.test.js +492 -492
- package/test/prompt-stance.test.js +128 -128
- package/test/provenance-http.test.js +367 -367
- package/test/publish-gate.test.js +272 -272
- package/test/quality-backfill.test.js +290 -290
- package/test/quality-filter.test.js +295 -295
- package/test/readme-claims.test.js +113 -113
- package/test/reasoning-effort.test.js +752 -752
- package/test/recall-anti-update.test.js +82 -82
- package/test/recall-evals.test.js +253 -253
- package/test/recall-layer.test.js +326 -326
- package/test/recall-retention.test.js +128 -128
- package/test/recall-stats.test.js +274 -274
- package/test/receipt-chain.test.js +451 -451
- package/test/reclaim-http.test.js +209 -209
- package/test/reflection-update-gate.test.js +129 -129
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +475 -475
- package/test/reindex-scheduler.test.js +369 -369
- package/test/release-docs.test.js +156 -156
- package/test/release-notes.test.js +265 -265
- package/test/release-workflow.test.js +93 -93
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -83
- package/test/route-policy.test.js +113 -113
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -255
- package/test/runtime-manifest.test.js +159 -159
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +319 -319
- package/test/scope-candidates.test.js +491 -491
- package/test/scope-distill-stamp.test.js +134 -134
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-inject-soft.test.js +106 -106
- package/test/scope-optional-service.test.js +110 -110
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-state.test.js +610 -610
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +367 -367
- package/test/search-fusion.test.js +90 -90
- package/test/security-doc.test.js +188 -188
- package/test/semantic.test.js +124 -124
- package/test/sensitive-scan.test.js +251 -251
- package/test/serve-bin.test.js +121 -121
- package/test/serve.test.js +258 -258
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +410 -410
- package/test/session-gate.test.js +264 -264
- package/test/session-usage.test.js +317 -317
- package/test/settings.test.js +273 -273
- package/test/share-foreign-rows.test.js +144 -144
- package/test/share-http.test.js +144 -144
- package/test/share-import.test.js +577 -577
- package/test/share-imported-guard.test.js +311 -311
- package/test/share-imported-identity.test.js +111 -111
- package/test/share-pack.test.js +511 -511
- package/test/share-receive-http.test.js +756 -756
- package/test/share-routes.test.js +337 -337
- package/test/share-store.test.js +133 -133
- package/test/share-validate.test.js +101 -101
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +441 -441
- package/test/sleep-catchup.test.js +315 -315
- package/test/sleep-failure-backoff.test.js +156 -156
- package/test/sleep-heat.test.js +174 -174
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +1455 -1455
- package/test/standalone-api.test.js +1344 -1344
- package/test/store-vector-cache.test.js +98 -98
- package/test/store-write-path-columns.test.js +191 -191
- package/test/store.test.js +744 -744
- package/test/stress.test.js +209 -209
- package/test/summarize-occurred-at.test.js +127 -127
- package/test/summarize-parse-diagnostics.test.js +75 -75
- package/test/summarize-reasoning-effort.test.js +189 -189
- package/test/summarize.test.js +2251 -2251
- package/test/supersede-state-tracking.test.js +394 -394
- package/test/test-count-sync.test.js +71 -71
- package/test/tool-state-ignore.test.js +65 -65
- package/test/tools-dto-schema-matrix.test.js +398 -398
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +585 -585
- package/test/updated-at-semantics.test.js +113 -113
- package/test/user-settings-preview.test.js +127 -127
- package/test/user-settings-segments.test.js +527 -527
- package/test/vector-index.test.js +334 -334
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
- package/test/wiki-api.test.js +734 -734
- package/test/wiki-claims.test.js +194 -194
- package/test/wiki-cli-hint.test.js +118 -118
- package/test/wiki-inject.test.js +265 -265
- package/test/wiki-partition.test.js +112 -112
- package/test/wiki-plan.test.js +80 -80
- package/test/wiki-provenance.test.js +202 -202
- package/test/wiki-queries.test.js +398 -398
- package/test/wiki-scan.test.js +208 -208
- package/test/wiki-store.test.js +190 -190
- package/test/wiki-t0.test.js +456 -456
- package/test/wiki-t1-jobs.test.js +125 -125
- package/test/wiki-t1.test.js +460 -460
- package/test/wiki-tools.test.js +176 -176
- package/test/workflow-guards.test.js +155 -155
- package/test/write-admission.test.js +668 -668
|
@@ -1,568 +1,568 @@
|
|
|
1
|
-
// 绑会话的人格卡(#606)。为什么需要这一维:人格卡原本只有 agentPreset 一个寻址键,
|
|
2
|
-
// 而 DS Bot 这类宿主把它写成常量——实测 bot 工作区 50 个会话里 43 个是 "standard",
|
|
3
|
-
// 四条主 bot(Amarre / Ephor / Loom / Persis)全部相同,但它们的 session.id 各不相同。
|
|
4
|
-
// 所以「一个 bot 一张卡」只能用会话 id 当键。
|
|
5
|
-
//
|
|
6
|
-
// 本文件锁四件事:
|
|
7
|
-
// 1. 纯加法:没有 bySession 的老数据读回仍是空对象,注入文本逐字节不变。
|
|
8
|
-
// 2. 优先级:bySession > byAgent > default(会话比预设具体)。
|
|
9
|
-
// 3. 严格寻址的两维合判:只建了 bySession 卡时,按 byAgent 寻址的会话也不许被兜底。
|
|
10
|
-
// 4. scenario / mesExample:未设置零输出,设置后排在 replyStyle 之后。
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import { EventEmitter } from "node:events";
|
|
14
|
-
import { createStore } from "../src/store.js";
|
|
15
|
-
import { createService } from "../src/service.js";
|
|
16
|
-
import { createInjector } from "../src/inject.js";
|
|
17
|
-
import { createApi } from "../src/api.js";
|
|
18
|
-
import { createSettings } from "../src/settings.js";
|
|
19
|
-
import { resolvePersonaCard, renderUserSettingsBlock } from "../src/user-settings.js";
|
|
20
|
-
|
|
21
|
-
class FakeRes extends EventEmitter {
|
|
22
|
-
constructor() { super(); this.statusCode = 200; this.body = ""; }
|
|
23
|
-
writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
|
|
24
|
-
end(text) { this.body = text ?? ""; this.emit("end"); return this; }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function req(path, method = "GET") {
|
|
28
|
-
return { url: path, method, headers: {}, on() {}, emit() {} };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// 注入器 + API 共用同一份 settings/config(同源纪律),两边都能按 {agentPreset, sessionId} 寻址。
|
|
32
|
-
function setup(config = {}) {
|
|
33
|
-
const store = createStore(":memory:");
|
|
34
|
-
const service = createService({ store, mirror: null, config });
|
|
35
|
-
const settings = createSettings(store.db);
|
|
36
|
-
const contexts = [];
|
|
37
|
-
const ctx = { systemPrompt: { context(def) { contexts.push(def); return () => {}; } } };
|
|
38
|
-
createInjector(ctx, service, settings, config);
|
|
39
|
-
const settingsCtx = contexts.find((c) => c.name === "user-settings");
|
|
40
|
-
// 造 ctx:只给要测的那几个键,缺省不给(模拟宿主解析不到)。
|
|
41
|
-
const makeCtx = ({ agentPreset = null, sessionId = null, parentSession = null } = {}) => {
|
|
42
|
-
if (!agentPreset && !sessionId && !parentSession) return {};
|
|
43
|
-
const session = {};
|
|
44
|
-
if (sessionId) session.id = sessionId;
|
|
45
|
-
if (agentPreset || parentSession) {
|
|
46
|
-
session.header = {};
|
|
47
|
-
if (agentPreset) session.header.agentPreset = agentPreset;
|
|
48
|
-
if (parentSession) session.header.parentSession = parentSession;
|
|
49
|
-
}
|
|
50
|
-
return { agent: { session } };
|
|
51
|
-
};
|
|
52
|
-
const injectorText = (opts) => settingsCtx.text(makeCtx(opts));
|
|
53
|
-
const routes = [];
|
|
54
|
-
createApi(
|
|
55
|
-
{ webServer: { register: (route) => { routes.push(route); return () => {}; } } },
|
|
56
|
-
service,
|
|
57
|
-
settings,
|
|
58
|
-
{ add: () => {}, remove: () => {}, list: () => [] },
|
|
59
|
-
undefined,
|
|
60
|
-
undefined,
|
|
61
|
-
"",
|
|
62
|
-
config
|
|
63
|
-
);
|
|
64
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/persona");
|
|
65
|
-
const preview = async ({ agent = null, session = null, parent = null } = {}) => {
|
|
66
|
-
const qs = [];
|
|
67
|
-
if (agent) qs.push(`agent=${encodeURIComponent(agent)}`);
|
|
68
|
-
if (session) qs.push(`session=${encodeURIComponent(session)}`);
|
|
69
|
-
if (parent) qs.push(`parent=${encodeURIComponent(parent)}`);
|
|
70
|
-
const res = new FakeRes();
|
|
71
|
-
await route.handler(req(`/api/dsh-mneme/persona${qs.length ? `?${qs.join("&")}` : ""}`), res);
|
|
72
|
-
return JSON.parse(res.body);
|
|
73
|
-
};
|
|
74
|
-
return { store, settings, settingsCtx, injectorText, preview };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function seed(settings) {
|
|
78
|
-
settings.setProfile("用户是规划背景");
|
|
79
|
-
settings.setPersona({ name: "Amarre", personality: "轻松幽默", enabled: true }); // default
|
|
80
|
-
settings.setPersonaFor("standard", { name: "标准卡", personality: "通用", enabled: true }); // byAgent
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---- 1. 纯加法:老数据零变化 -------------------------------------------------
|
|
84
|
-
|
|
85
|
-
test("老数据(只有 {default, byAgent})读回 bySession 为空对象,注入文本逐字节不变", () => {
|
|
86
|
-
const { store, settings } = setup({ personaEnabled: true });
|
|
87
|
-
// 直写 v1.2 形(无 bySession 键)模拟存量库
|
|
88
|
-
store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
|
|
89
|
-
.run(JSON.stringify({
|
|
90
|
-
default: { name: "Amarre", enabled: true },
|
|
91
|
-
byAgent: { standard: { name: "标准卡", enabled: true } }
|
|
92
|
-
}));
|
|
93
|
-
const got = settings.getPersona();
|
|
94
|
-
assert.deepEqual(got.bySession, {}, "缺 bySession 键必须读成空对象,不是 undefined");
|
|
95
|
-
assert.equal(got.default.name, "Amarre");
|
|
96
|
-
assert.equal(got.byAgent.standard.name, "标准卡");
|
|
97
|
-
// 寻址结果与改动前一致:按 preset 命中 byAgent,不因多了这一维而变
|
|
98
|
-
assert.equal(resolvePersonaCard(settings, "standard").source, "byAgent");
|
|
99
|
-
assert.equal(resolvePersonaCard(settings, "unknown").source, "default");
|
|
100
|
-
store.close();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("v1 单对象老数据:读作 default,bySession 为 {}(零迁移)", () => {
|
|
104
|
-
const { store, settings } = setup();
|
|
105
|
-
store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
|
|
106
|
-
.run(JSON.stringify({ name: "Amarre", personality: "轻松幽默", enabled: true }));
|
|
107
|
-
const got = settings.getPersona();
|
|
108
|
-
assert.equal(got.default.name, "Amarre");
|
|
109
|
-
assert.deepEqual(got.byAgent, {});
|
|
110
|
-
assert.deepEqual(got.bySession, {});
|
|
111
|
-
store.close();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("没设 scenario / mesExample 时,注入文本与历史版本逐字节一致", () => {
|
|
115
|
-
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
116
|
-
settings.setProfile("用户是规划背景");
|
|
117
|
-
settings.setRules(["不提及用户名"]);
|
|
118
|
-
settings.setPersona({ name: "Amarre", personality: "轻松幽默", replyStyle: "先结论后展开", enabled: true });
|
|
119
|
-
const text = injectorText({});
|
|
120
|
-
assert.equal(text, [
|
|
121
|
-
"[用户设置] 来自 dsh-mneme 的用户画像与规则:",
|
|
122
|
-
"- 你的人格名字:Amarre",
|
|
123
|
-
"- 人格性格设定:轻松幽默",
|
|
124
|
-
"- 回复要求:先结论后展开",
|
|
125
|
-
"- 用户画像:用户是规划背景",
|
|
126
|
-
"- 规则:不提及用户名"
|
|
127
|
-
].join("\n"), "两个新字段都没设时不许留下空标签或空行");
|
|
128
|
-
store.close();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// ---- 2. 存储往返 -------------------------------------------------------------
|
|
132
|
-
|
|
133
|
-
test("bySession 存取往返:写入 → 读回 → 列出 → 删除", () => {
|
|
134
|
-
const { store, settings } = setup();
|
|
135
|
-
const card = { name: "Ephor", personality: "审计口径", enabled: true };
|
|
136
|
-
assert.equal(settings.setPersonaForSession("session-389faaa7", card), true);
|
|
137
|
-
assert.deepEqual(resolvePersonaCard(settings, null, { sessionId: "session-389faaa7" }).card, card);
|
|
138
|
-
assert.deepEqual(settings.listPersonaSessions(), ["session-389faaa7"]);
|
|
139
|
-
// 与 byAgent 互不干扰
|
|
140
|
-
settings.setPersonaFor("standard", { name: "标准卡", enabled: true });
|
|
141
|
-
assert.deepEqual(settings.listPersonas(), ["standard"]);
|
|
142
|
-
assert.deepEqual(settings.listPersonaSessions(), ["session-389faaa7"]);
|
|
143
|
-
// 删
|
|
144
|
-
assert.equal(settings.removePersonaForSession("session-389faaa7"), true);
|
|
145
|
-
assert.deepEqual(settings.listPersonaSessions(), []);
|
|
146
|
-
assert.equal(settings.removePersonaForSession("session-389faaa7"), false, "删不存在的卡返回 false");
|
|
147
|
-
store.close();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("setPersonaForSession 拒绝空 id 与 default(兜底位不可被顶替)", () => {
|
|
151
|
-
const { store, settings } = setup();
|
|
152
|
-
assert.equal(settings.setPersonaForSession("", { name: "x" }), false);
|
|
153
|
-
assert.equal(settings.setPersonaForSession(" ", { name: "x" }), false);
|
|
154
|
-
assert.equal(settings.setPersonaForSession(null, { name: "x" }), false);
|
|
155
|
-
assert.equal(settings.setPersonaForSession("default", { name: "x" }), false);
|
|
156
|
-
assert.deepEqual(settings.listPersonaSessions(), []);
|
|
157
|
-
store.close();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("写空卡等于删除该会话卡(与 setPersonaFor 同形)", () => {
|
|
161
|
-
const { store, settings } = setup();
|
|
162
|
-
settings.setPersonaForSession("s1", { name: "甲", enabled: true });
|
|
163
|
-
assert.deepEqual(settings.listPersonaSessions(), ["s1"]);
|
|
164
|
-
settings.setPersonaForSession("s1", {});
|
|
165
|
-
assert.deepEqual(settings.listPersonaSessions(), [], "空卡不留空壳键");
|
|
166
|
-
store.close();
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("setPersona 单对象形只改 default,bySession 保留", () => {
|
|
170
|
-
const { store, settings } = setup();
|
|
171
|
-
settings.setPersonaForSession("s1", { name: "会话卡", enabled: true });
|
|
172
|
-
settings.setPersona({ name: "Amarre-新", enabled: true }); // 旧面板写法
|
|
173
|
-
const got = settings.getPersona();
|
|
174
|
-
assert.equal(got.default.name, "Amarre-新");
|
|
175
|
-
assert.equal(got.bySession.s1.name, "会话卡", "bySession 必须活过一次 legacy 形写入");
|
|
176
|
-
store.close();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test("多卡形全量替换:带 bySession 键即整份替换", () => {
|
|
180
|
-
const { store, settings } = setup();
|
|
181
|
-
settings.setPersonaForSession("old", { name: "旧卡", enabled: true });
|
|
182
|
-
settings.setPersona({ default: { name: "新默认", enabled: true }, byAgent: {}, bySession: { new: { name: "新卡", enabled: true } } });
|
|
183
|
-
assert.deepEqual(settings.listPersonaSessions(), ["new"]);
|
|
184
|
-
store.close();
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ---- 3. 寻址优先级 -----------------------------------------------------------
|
|
188
|
-
|
|
189
|
-
test("优先级:bySession 盖过 byAgent(会话比预设具体)", () => {
|
|
190
|
-
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
191
|
-
seed(settings); // default=Amarre, byAgent.standard=标准卡
|
|
192
|
-
settings.setPersonaForSession("session-abc", { name: "会话专属", personality: "只在这次会话", enabled: true });
|
|
193
|
-
const text = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
|
|
194
|
-
assert.ok(text.includes("你的人格名字:会话专属"), "会话卡必须赢");
|
|
195
|
-
assert.ok(!text.includes("标准卡"), "byAgent 卡不该同时出现");
|
|
196
|
-
assert.ok(!text.includes("Amarre"), "default 更不该出现");
|
|
197
|
-
store.close();
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("优先级:会话卡被关掉时落回 byAgent(不是直接跳 default)", () => {
|
|
201
|
-
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
202
|
-
seed(settings);
|
|
203
|
-
settings.setPersonaForSession("session-abc", { name: "会话专属", enabled: false });
|
|
204
|
-
const text = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
|
|
205
|
-
assert.ok(text.includes("你的人格名字:标准卡"), "关掉的会话卡让位给身份卡");
|
|
206
|
-
assert.ok(!text.includes("会话专属"));
|
|
207
|
-
store.close();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test("优先级:没有会话卡时按 byAgent 走(老行为不变)", () => {
|
|
211
|
-
const { store, settings } = setup({ personaEnabled: true });
|
|
212
|
-
seed(settings);
|
|
213
|
-
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-unknown" });
|
|
214
|
-
assert.equal(hit.source, "byAgent");
|
|
215
|
-
assert.equal(hit.card.name, "标准卡");
|
|
216
|
-
store.close();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test("两个键都没有:落 default", () => {
|
|
220
|
-
const { store, settings } = setup({ personaEnabled: true });
|
|
221
|
-
seed(settings);
|
|
222
|
-
const hit = resolvePersonaCard(settings, null, { sessionId: null });
|
|
223
|
-
assert.equal(hit.source, "default");
|
|
224
|
-
assert.equal(hit.card.name, "Amarre");
|
|
225
|
-
store.close();
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test("只给 sessionId、不给 agentId 也能命中会话卡(宿主可能只有会话 id)", () => {
|
|
229
|
-
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
230
|
-
seed(settings);
|
|
231
|
-
settings.setPersonaForSession("session-only", { name: "仅会话卡", enabled: true });
|
|
232
|
-
const text = injectorText({ sessionId: "session-only" });
|
|
233
|
-
assert.ok(text.includes("你的人格名字:仅会话卡"));
|
|
234
|
-
store.close();
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// ---- 4. 严格寻址:两维合判 ---------------------------------------------------
|
|
238
|
-
|
|
239
|
-
test("strict:只建了 bySession 卡时,按 byAgent 寻址的会话也不许被兜底", () => {
|
|
240
|
-
// #587 的例外是「一张卡都没建」才回落 default。用户建了会话卡就已经做过分区,
|
|
241
|
-
// 此时一个没建卡的 preset 会话再拿 default 去兜,等于把分区抹掉一半。
|
|
242
|
-
const { store, settings } = setup();
|
|
243
|
-
settings.setPersona({ name: "Amarre", enabled: true });
|
|
244
|
-
assert.deepEqual(settings.getPersona().byAgent, {}, "前提:byAgent 为空");
|
|
245
|
-
assert.deepEqual(settings.getPersona().bySession, {}, "前提:bySession 也为空");
|
|
246
|
-
// 两维都空 → 仍回落 default(#587 例外保持)
|
|
247
|
-
assert.equal(resolvePersonaCard(settings, "standard", { strict: true }).source, "default");
|
|
248
|
-
// 只建会话卡之后,例外立即失效
|
|
249
|
-
settings.setPersonaForSession("session-x", { name: "会话卡", enabled: true });
|
|
250
|
-
assert.equal(resolvePersonaCard(settings, "standard", { strict: true }).source, "none");
|
|
251
|
-
// 而那张会话卡自己仍然命中
|
|
252
|
-
assert.equal(resolvePersonaCard(settings, null, { strict: true, sessionId: "session-x" }).source, "bySession");
|
|
253
|
-
store.close();
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test("strict:只建了 byAgent 卡时,按会话寻址的会话也不许被兜底", () => {
|
|
257
|
-
const { store, settings } = setup();
|
|
258
|
-
settings.setPersona({ name: "Amarre", enabled: true });
|
|
259
|
-
settings.setPersonaFor("standard", { name: "标准卡", enabled: true });
|
|
260
|
-
// 有会话 id 但没建会话卡 → none(用户已经做过 byAgent 分区)
|
|
261
|
-
assert.equal(resolvePersonaCard(settings, null, { strict: true, sessionId: "session-y" }).source, "none");
|
|
262
|
-
store.close();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test("strict:两个键都解析不到时仍用 default(无名会话是 default 的受众)", () => {
|
|
266
|
-
const { store, settings } = setup();
|
|
267
|
-
settings.setPersona({ name: "Amarre", enabled: true });
|
|
268
|
-
settings.setPersonaForSession("session-x", { name: "会话卡", enabled: true });
|
|
269
|
-
assert.equal(resolvePersonaCard(settings, null, { strict: true }).source, "default");
|
|
270
|
-
store.close();
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// ---- 5. scenario / mesExample ------------------------------------------------
|
|
274
|
-
|
|
275
|
-
test("scenario / mesExample 设置在 replyStyle 之后渲染", () => {
|
|
276
|
-
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
277
|
-
settings.setPersona({
|
|
278
|
-
name: "Amarre",
|
|
279
|
-
personality: "轻松幽默",
|
|
280
|
-
replyStyle: "先结论后展开",
|
|
281
|
-
scenario: "深夜陪你赶论文",
|
|
282
|
-
mesExample: "桉桉:好累。Amarre:先睡,剩下的我盯着。",
|
|
283
|
-
enabled: true
|
|
284
|
-
});
|
|
285
|
-
const text = injectorText({});
|
|
286
|
-
const idxReply = text.indexOf("回复要求:先结论后展开");
|
|
287
|
-
const idxScene = text.indexOf("场景设定:深夜陪你赶论文");
|
|
288
|
-
const idxExample = text.indexOf("对话示例:桉桉:好累。");
|
|
289
|
-
assert.ok(idxReply > -1 && idxScene > -1 && idxExample > -1, "三段都在");
|
|
290
|
-
assert.ok(idxScene > idxReply, "场景在语气之后:身份 → 语气 → 场景 → 示例");
|
|
291
|
-
assert.ok(idxExample > idxScene, "示例在场景之后");
|
|
292
|
-
store.close();
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("scenario 只设一个字段时只出一行,不牵连另一个", () => {
|
|
296
|
-
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
297
|
-
settings.setPersona({ name: "Amarre", scenario: "只在夜里出现", enabled: true });
|
|
298
|
-
const text = injectorText({});
|
|
299
|
-
assert.ok(text.includes("场景设定:只在夜里出现"));
|
|
300
|
-
assert.ok(!text.includes("对话示例"), "没设 mesExample 就不出那一行");
|
|
301
|
-
store.close();
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
test("卡里存着空白 scenario / mesExample 时不出空标签(未设置零输出)", () => {
|
|
305
|
-
// 未设置的字段被 parsePersona 白名单挡在外面,但「存了空白」是另一条路:
|
|
306
|
-
// 卡里确实有这个键、值是空白串。这一行必须整行不出现,不能渲染成「场景设定:」。
|
|
307
|
-
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
308
|
-
settings.setPersona({ name: "Amarre", scenario: " ", mesExample: "\t ", enabled: true });
|
|
309
|
-
const card = settings.getPersona().default;
|
|
310
|
-
assert.equal(card.scenario, " ", "空白串仍被存下(白名单不负责裁空白)");
|
|
311
|
-
const text = injectorText({});
|
|
312
|
-
assert.ok(!text.includes("场景设定"), "空白 scenario 不许渲染出标签");
|
|
313
|
-
assert.ok(!text.includes("对话示例"), "空白 mesExample 不许渲染出标签");
|
|
314
|
-
assert.ok(text.includes("你的人格名字:Amarre"), "其余段落照常");
|
|
315
|
-
store.close();
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test("scenario / mesExample 走同一个转义(花括号不许漏出去)", () => {
|
|
319
|
-
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
320
|
-
settings.setPersona({ name: "Amarre", scenario: "{{evil}}", mesExample: "{{x}}", enabled: true });
|
|
321
|
-
const text = injectorText({});
|
|
322
|
-
assert.ok(!text.includes("{{"), "no raw {{ in user-settings block");
|
|
323
|
-
assert.ok(!text.includes("}}"), "no raw }} in user-settings block");
|
|
324
|
-
store.close();
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
test("scenario / mesExample 只在卡 enabled 时渲染(关掉的卡一个字都不出)", () => {
|
|
328
|
-
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
329
|
-
settings.setPersona({ name: "Amarre", scenario: "深夜", enabled: false });
|
|
330
|
-
settings.setProfile("有画像");
|
|
331
|
-
const text = injectorText({});
|
|
332
|
-
assert.ok(!text.includes("场景设定"), "卡关着时新字段同样不渲染");
|
|
333
|
-
assert.ok(text.includes("用户画像:"));
|
|
334
|
-
store.close();
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
test("scenario / mesExample 过字段白名单(未知字段仍被丢弃)", () => {
|
|
338
|
-
const { store, settings } = setup();
|
|
339
|
-
settings.setPersona({ name: "Amarre", scenario: "场景", mesExample: "示例", sneaky: "x" });
|
|
340
|
-
const card = settings.getPersona().default;
|
|
341
|
-
assert.equal(card.scenario, "场景");
|
|
342
|
-
assert.equal(card.mesExample, "示例");
|
|
343
|
-
assert.equal("sneaky" in card, false, "白名单没放宽");
|
|
344
|
-
store.close();
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
// ---- 6. 端到端:注入链路 + 预览路由 ------------------------------------------
|
|
348
|
-
|
|
349
|
-
test("注入链路端到端:同一个 agentPreset、不同 sessionId 拿到不同人格", () => {
|
|
350
|
-
// 这就是 #606 要解决的场景本身:DS Bot 里四个 bot 的 agentPreset 全是 standard。
|
|
351
|
-
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
352
|
-
settings.setPersona({ name: "Amarre", enabled: true });
|
|
353
|
-
settings.setPersonaForSession("session-d530e132", { name: "Amarre", personality: "主控", enabled: true });
|
|
354
|
-
settings.setPersonaForSession("session-389faaa7", { name: "Ephor", personality: "审计", enabled: true });
|
|
355
|
-
const a = injectorText({ agentPreset: "standard", sessionId: "session-d530e132" });
|
|
356
|
-
const b = injectorText({ agentPreset: "standard", sessionId: "session-389faaa7" });
|
|
357
|
-
assert.ok(a.includes("你的人格名字:Amarre"));
|
|
358
|
-
assert.ok(b.includes("你的人格名字:Ephor"), "同一个 standard 预设下,会话卡必须分得开");
|
|
359
|
-
assert.ok(!b.includes("你的人格名字:Amarre"));
|
|
360
|
-
store.close();
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
test("预览路由支持 &session=,且与注入器逐字节同源", async () => {
|
|
364
|
-
const { store, settings, injectorText, preview } = setup({ personaEnabled: true });
|
|
365
|
-
seed(settings);
|
|
366
|
-
settings.setPersonaForSession("session-abc", { name: "会话专属", personality: "只在这里", enabled: true });
|
|
367
|
-
const fromInjector = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
|
|
368
|
-
const body = await preview({ agent: "standard", session: "session-abc" });
|
|
369
|
-
assert.equal(body.preview.text, fromInjector, "预览必须等于注入器这一帧的输出");
|
|
370
|
-
assert.equal(body.preview.source, "bySession");
|
|
371
|
-
assert.equal(body.preview.session, "session-abc", "面板要靠它说清用的是哪张会话卡");
|
|
372
|
-
assert.ok(body.preview.text.includes("你的人格名字:会话专属"));
|
|
373
|
-
store.close();
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
test("预览只给 session 不给 agent 也走会话卡", async () => {
|
|
377
|
-
const { store, settings, preview } = setup({ personaEnabled: true });
|
|
378
|
-
seed(settings);
|
|
379
|
-
settings.setPersonaForSession("session-only", { name: "仅会话卡", enabled: true });
|
|
380
|
-
const body = await preview({ session: "session-only" });
|
|
381
|
-
assert.equal(body.preview.source, "bySession");
|
|
382
|
-
assert.equal(body.preview.agent, null);
|
|
383
|
-
assert.equal(body.preview.session, "session-only");
|
|
384
|
-
store.close();
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
test("预览的 session 参数在 strict 下同样生效(不许比注入更乐观)", async () => {
|
|
388
|
-
const { store, settings, preview } = setup({ personaEnabled: true, personaStrictAddressing: true });
|
|
389
|
-
seed(settings);
|
|
390
|
-
const miss = await preview({ session: "session-unknown" });
|
|
391
|
-
assert.equal(miss.preview.source, "none", "建过卡、这次会话没建 → 不兜底");
|
|
392
|
-
assert.ok(!miss.preview.text.includes("Amarre"));
|
|
393
|
-
settings.setPersonaForSession("session-hit", { name: "命中卡", enabled: true });
|
|
394
|
-
const hit = await preview({ session: "session-hit" });
|
|
395
|
-
assert.equal(hit.preview.source, "bySession");
|
|
396
|
-
store.close();
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
// ---- 7. 健壮性 ---------------------------------------------------------------
|
|
400
|
-
|
|
401
|
-
test("session.id 取不到(宿主 accessor 抛错)时不影响 agentPreset 寻址", () => {
|
|
402
|
-
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
403
|
-
seed(settings);
|
|
404
|
-
// session.id 是抛错的 getter,header 正常——两个键各自护住,不许互相拖累
|
|
405
|
-
const hostile = {
|
|
406
|
-
agent: {
|
|
407
|
-
session: {
|
|
408
|
-
get id() { throw new Error("no id accessor"); },
|
|
409
|
-
header: { agentPreset: "standard" }
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
};
|
|
413
|
-
const text = settingsCtx.text(hostile);
|
|
414
|
-
assert.ok(text.includes("你的人格名字:标准卡"), "id 抛错不该让 preset 也丢掉");
|
|
415
|
-
store.close();
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
test("bySession 里的损坏卡(非对象)被丢弃,不抛错", () => {
|
|
419
|
-
const { store, settings } = setup();
|
|
420
|
-
store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
|
|
421
|
-
.run(JSON.stringify({ default: {}, byAgent: {}, bySession: { bad: "not a card", good: { name: "好卡", enabled: true } } }));
|
|
422
|
-
const got = settings.getPersona();
|
|
423
|
-
assert.equal("bad" in got.bySession, false, "坏卡读成空对象后不收(与 byAgent 同口径)");
|
|
424
|
-
assert.equal(got.bySession.good.name, "好卡");
|
|
425
|
-
store.close();
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test("renderUserSettingsBlock 返回值带 session 字段(面板定位用)", () => {
|
|
429
|
-
const { store, settings } = setup({ personaEnabled: true });
|
|
430
|
-
settings.setPersona({ name: "Amarre", enabled: true });
|
|
431
|
-
const out = renderUserSettingsBlock({ settings, config: { personaEnabled: true }, agentId: "standard", sessionId: "s1" });
|
|
432
|
-
assert.equal(out.session, "s1");
|
|
433
|
-
assert.equal(out.agent, "standard");
|
|
434
|
-
assert.equal(out.source, "default");
|
|
435
|
-
store.close();
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// ---- 8. 子 agent 继承父会话人格卡 --------------------------------------------
|
|
439
|
-
//
|
|
440
|
-
// 子 agent 是新会话(header 带 parentSession / origin:"subagent"),用户不可能给每个
|
|
441
|
-
// 派生会话建卡。strict 一开,「有身份、没这张身份的卡」= 不注入人格,子 agent 会从
|
|
442
|
-
// 「自称父会话的人格」变成「不知道自己是谁」。所以自己没卡时继承父会话的卡。
|
|
443
|
-
|
|
444
|
-
test("子 agent 没自己的卡时继承父会话卡(source 是 parentSession)", () => {
|
|
445
|
-
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
446
|
-
settings.setPersona({ name: "兜底卡", enabled: true });
|
|
447
|
-
settings.setPersonaForSession("session-parent", { name: "主控人格", personality: "轻松诙谐", enabled: true });
|
|
448
|
-
const text = injectorText({ agentPreset: "standard", sessionId: "session-child", parentSession: "session-parent" });
|
|
449
|
-
assert.ok(text.includes("你的人格名字:主控人格"), "子 agent 必须拿到父会话那张卡");
|
|
450
|
-
assert.ok(!text.includes("兜底卡"), "不许回落到 default");
|
|
451
|
-
const out = renderUserSettingsBlock({
|
|
452
|
-
settings,
|
|
453
|
-
config: { personaEnabled: true },
|
|
454
|
-
agentId: "standard",
|
|
455
|
-
sessionId: "session-child",
|
|
456
|
-
parentSessionId: "session-parent"
|
|
457
|
-
});
|
|
458
|
-
assert.equal(out.source, "parentSession");
|
|
459
|
-
store.close();
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
test("子 agent 自己有卡时自己的卡优先(继承不覆盖专门指定的卡)", () => {
|
|
463
|
-
const { store, settings } = setup({ personaEnabled: true });
|
|
464
|
-
settings.setPersonaForSession("session-parent", { name: "父卡", enabled: true });
|
|
465
|
-
settings.setPersonaForSession("session-child", { name: "子卡", enabled: true });
|
|
466
|
-
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
|
|
467
|
-
assert.equal(hit.source, "bySession");
|
|
468
|
-
assert.equal(hit.card.name, "子卡");
|
|
469
|
-
store.close();
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
test("父卡优先于 byAgent(同一段对话比同一预设更能说明用谁的人格)", () => {
|
|
473
|
-
const { store, settings } = setup({ personaEnabled: true });
|
|
474
|
-
settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
|
|
475
|
-
settings.setPersonaForSession("session-parent", { name: "父卡", enabled: true });
|
|
476
|
-
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
|
|
477
|
-
assert.equal(hit.source, "parentSession");
|
|
478
|
-
assert.equal(hit.card.name, "父卡");
|
|
479
|
-
store.close();
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
test("父卡被显式关掉时不继承,落回 byAgent(关掉就是关掉)", () => {
|
|
483
|
-
const { store, settings } = setup({ personaEnabled: true });
|
|
484
|
-
settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
|
|
485
|
-
settings.setPersonaForSession("session-parent", { name: "父卡", enabled: false });
|
|
486
|
-
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
|
|
487
|
-
assert.equal(hit.source, "byAgent");
|
|
488
|
-
assert.equal(hit.card.name, "预设卡");
|
|
489
|
-
store.close();
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
test("strict:子 agent 只解析到父会话 id 时也不许被 default 兜底", () => {
|
|
493
|
-
const { store, settings } = setup({ personaEnabled: true, personaStrictAddressing: true });
|
|
494
|
-
settings.setPersonaForSession("session-other", { name: "别人的卡", enabled: true });
|
|
495
|
-
// 子会话与父会话都没有卡;anyCard=true 且确实有身份 → none
|
|
496
|
-
const miss = resolvePersonaCard(settings, "standard", {
|
|
497
|
-
strict: true,
|
|
498
|
-
sessionId: "session-child",
|
|
499
|
-
parentSessionId: "session-nobody"
|
|
500
|
-
});
|
|
501
|
-
assert.equal(miss.source, "none", "有身份但没卡,strict 下不兜底");
|
|
502
|
-
store.close();
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
test("strict 关时子 agent 没父卡照旧落 default(零行为变化)", () => {
|
|
506
|
-
const { store, settings } = setup({ personaEnabled: true });
|
|
507
|
-
settings.setPersona({ name: "Amarre", enabled: true });
|
|
508
|
-
settings.setPersonaForSession("session-other", { name: "别人的卡", enabled: true });
|
|
509
|
-
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-nope" });
|
|
510
|
-
assert.equal(hit.source, "default");
|
|
511
|
-
assert.equal(hit.card.name, "Amarre");
|
|
512
|
-
store.close();
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
test("parentSession 与自身 id 相同(脏数据)时不重复查同一个键", () => {
|
|
516
|
-
const { store, settings } = setup({ personaEnabled: true });
|
|
517
|
-
settings.setPersonaForSession("session-same", { name: "同一张", enabled: true });
|
|
518
|
-
const hit = resolvePersonaCard(settings, "standard", {
|
|
519
|
-
sessionId: "session-same",
|
|
520
|
-
parentSessionId: "session-same"
|
|
521
|
-
});
|
|
522
|
-
assert.equal(hit.source, "bySession", "自身维度先命中,不会误标成继承");
|
|
523
|
-
store.close();
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
test("父会话 id 是空白/非字符串时不影响其余寻址", () => {
|
|
527
|
-
const { store, settings } = setup({ personaEnabled: true });
|
|
528
|
-
settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
|
|
529
|
-
for (const bad of ["", " ", null, undefined, 42, {}]) {
|
|
530
|
-
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: bad });
|
|
531
|
-
assert.equal(hit.source, "byAgent", `parentSessionId=${JSON.stringify(bad)} 应被忽略`);
|
|
532
|
-
}
|
|
533
|
-
store.close();
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
test("session.header 取不到时不影响 agentPreset 寻址(新加的父会话读取也各自兜错)", () => {
|
|
537
|
-
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
538
|
-
seed(settings);
|
|
539
|
-
const hostile = {
|
|
540
|
-
agent: {
|
|
541
|
-
session: {
|
|
542
|
-
id: "session-x",
|
|
543
|
-
get header() { throw new Error("no header accessor"); }
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
};
|
|
547
|
-
const text = settingsCtx.text(hostile);
|
|
548
|
-
// header 整体取不到 → agentId/parentSessionId 皆 null,只剩 sessionId,落 default
|
|
549
|
-
assert.ok(text.includes("你的人格名字:Amarre"), "会话卡字段可用时不该整块崩掉");
|
|
550
|
-
store.close();
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
test("注入链路端到端:子 agent 拿到父会话人格,且与预览同源", async () => {
|
|
554
|
-
const { store, settings, injectorText, preview } = setup({ personaEnabled: true });
|
|
555
|
-
settings.setPersona({ name: "兜底", enabled: true });
|
|
556
|
-
settings.setPersonaForSession("session-parent", { name: "父会话人格", personality: "父的性格", enabled: true });
|
|
557
|
-
const fromInjector = injectorText({
|
|
558
|
-
agentPreset: "standard",
|
|
559
|
-
sessionId: "session-child",
|
|
560
|
-
parentSession: "session-parent"
|
|
561
|
-
});
|
|
562
|
-
assert.ok(fromInjector.includes("你的人格名字:父会话人格"));
|
|
563
|
-
const body = await preview({ agent: "standard", session: "session-child", parent: "session-parent" });
|
|
564
|
-
assert.equal(body.preview.text, fromInjector, "预览必须与注入器逐字节同源");
|
|
565
|
-
assert.equal(body.preview.source, "parentSession");
|
|
566
|
-
assert.equal(body.preview.parent, "session-parent");
|
|
567
|
-
store.close();
|
|
568
|
-
});
|
|
1
|
+
// 绑会话的人格卡(#606)。为什么需要这一维:人格卡原本只有 agentPreset 一个寻址键,
|
|
2
|
+
// 而 DS Bot 这类宿主把它写成常量——实测 bot 工作区 50 个会话里 43 个是 "standard",
|
|
3
|
+
// 四条主 bot(Amarre / Ephor / Loom / Persis)全部相同,但它们的 session.id 各不相同。
|
|
4
|
+
// 所以「一个 bot 一张卡」只能用会话 id 当键。
|
|
5
|
+
//
|
|
6
|
+
// 本文件锁四件事:
|
|
7
|
+
// 1. 纯加法:没有 bySession 的老数据读回仍是空对象,注入文本逐字节不变。
|
|
8
|
+
// 2. 优先级:bySession > byAgent > default(会话比预设具体)。
|
|
9
|
+
// 3. 严格寻址的两维合判:只建了 bySession 卡时,按 byAgent 寻址的会话也不许被兜底。
|
|
10
|
+
// 4. scenario / mesExample:未设置零输出,设置后排在 replyStyle 之后。
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { EventEmitter } from "node:events";
|
|
14
|
+
import { createStore } from "../src/store.js";
|
|
15
|
+
import { createService } from "../src/service.js";
|
|
16
|
+
import { createInjector } from "../src/inject.js";
|
|
17
|
+
import { createApi } from "../src/api.js";
|
|
18
|
+
import { createSettings } from "../src/settings.js";
|
|
19
|
+
import { resolvePersonaCard, renderUserSettingsBlock } from "../src/user-settings.js";
|
|
20
|
+
|
|
21
|
+
class FakeRes extends EventEmitter {
|
|
22
|
+
constructor() { super(); this.statusCode = 200; this.body = ""; }
|
|
23
|
+
writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
|
|
24
|
+
end(text) { this.body = text ?? ""; this.emit("end"); return this; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function req(path, method = "GET") {
|
|
28
|
+
return { url: path, method, headers: {}, on() {}, emit() {} };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 注入器 + API 共用同一份 settings/config(同源纪律),两边都能按 {agentPreset, sessionId} 寻址。
|
|
32
|
+
function setup(config = {}) {
|
|
33
|
+
const store = createStore(":memory:");
|
|
34
|
+
const service = createService({ store, mirror: null, config });
|
|
35
|
+
const settings = createSettings(store.db);
|
|
36
|
+
const contexts = [];
|
|
37
|
+
const ctx = { systemPrompt: { context(def) { contexts.push(def); return () => {}; } } };
|
|
38
|
+
createInjector(ctx, service, settings, config);
|
|
39
|
+
const settingsCtx = contexts.find((c) => c.name === "user-settings");
|
|
40
|
+
// 造 ctx:只给要测的那几个键,缺省不给(模拟宿主解析不到)。
|
|
41
|
+
const makeCtx = ({ agentPreset = null, sessionId = null, parentSession = null } = {}) => {
|
|
42
|
+
if (!agentPreset && !sessionId && !parentSession) return {};
|
|
43
|
+
const session = {};
|
|
44
|
+
if (sessionId) session.id = sessionId;
|
|
45
|
+
if (agentPreset || parentSession) {
|
|
46
|
+
session.header = {};
|
|
47
|
+
if (agentPreset) session.header.agentPreset = agentPreset;
|
|
48
|
+
if (parentSession) session.header.parentSession = parentSession;
|
|
49
|
+
}
|
|
50
|
+
return { agent: { session } };
|
|
51
|
+
};
|
|
52
|
+
const injectorText = (opts) => settingsCtx.text(makeCtx(opts));
|
|
53
|
+
const routes = [];
|
|
54
|
+
createApi(
|
|
55
|
+
{ webServer: { register: (route) => { routes.push(route); return () => {}; } } },
|
|
56
|
+
service,
|
|
57
|
+
settings,
|
|
58
|
+
{ add: () => {}, remove: () => {}, list: () => [] },
|
|
59
|
+
undefined,
|
|
60
|
+
undefined,
|
|
61
|
+
"",
|
|
62
|
+
config
|
|
63
|
+
);
|
|
64
|
+
const route = routes.find((r) => r.path === "/api/dsh-mneme/persona");
|
|
65
|
+
const preview = async ({ agent = null, session = null, parent = null } = {}) => {
|
|
66
|
+
const qs = [];
|
|
67
|
+
if (agent) qs.push(`agent=${encodeURIComponent(agent)}`);
|
|
68
|
+
if (session) qs.push(`session=${encodeURIComponent(session)}`);
|
|
69
|
+
if (parent) qs.push(`parent=${encodeURIComponent(parent)}`);
|
|
70
|
+
const res = new FakeRes();
|
|
71
|
+
await route.handler(req(`/api/dsh-mneme/persona${qs.length ? `?${qs.join("&")}` : ""}`), res);
|
|
72
|
+
return JSON.parse(res.body);
|
|
73
|
+
};
|
|
74
|
+
return { store, settings, settingsCtx, injectorText, preview };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function seed(settings) {
|
|
78
|
+
settings.setProfile("用户是规划背景");
|
|
79
|
+
settings.setPersona({ name: "Amarre", personality: "轻松幽默", enabled: true }); // default
|
|
80
|
+
settings.setPersonaFor("standard", { name: "标准卡", personality: "通用", enabled: true }); // byAgent
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- 1. 纯加法:老数据零变化 -------------------------------------------------
|
|
84
|
+
|
|
85
|
+
test("老数据(只有 {default, byAgent})读回 bySession 为空对象,注入文本逐字节不变", () => {
|
|
86
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
87
|
+
// 直写 v1.2 形(无 bySession 键)模拟存量库
|
|
88
|
+
store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
|
|
89
|
+
.run(JSON.stringify({
|
|
90
|
+
default: { name: "Amarre", enabled: true },
|
|
91
|
+
byAgent: { standard: { name: "标准卡", enabled: true } }
|
|
92
|
+
}));
|
|
93
|
+
const got = settings.getPersona();
|
|
94
|
+
assert.deepEqual(got.bySession, {}, "缺 bySession 键必须读成空对象,不是 undefined");
|
|
95
|
+
assert.equal(got.default.name, "Amarre");
|
|
96
|
+
assert.equal(got.byAgent.standard.name, "标准卡");
|
|
97
|
+
// 寻址结果与改动前一致:按 preset 命中 byAgent,不因多了这一维而变
|
|
98
|
+
assert.equal(resolvePersonaCard(settings, "standard").source, "byAgent");
|
|
99
|
+
assert.equal(resolvePersonaCard(settings, "unknown").source, "default");
|
|
100
|
+
store.close();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("v1 单对象老数据:读作 default,bySession 为 {}(零迁移)", () => {
|
|
104
|
+
const { store, settings } = setup();
|
|
105
|
+
store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
|
|
106
|
+
.run(JSON.stringify({ name: "Amarre", personality: "轻松幽默", enabled: true }));
|
|
107
|
+
const got = settings.getPersona();
|
|
108
|
+
assert.equal(got.default.name, "Amarre");
|
|
109
|
+
assert.deepEqual(got.byAgent, {});
|
|
110
|
+
assert.deepEqual(got.bySession, {});
|
|
111
|
+
store.close();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("没设 scenario / mesExample 时,注入文本与历史版本逐字节一致", () => {
|
|
115
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
116
|
+
settings.setProfile("用户是规划背景");
|
|
117
|
+
settings.setRules(["不提及用户名"]);
|
|
118
|
+
settings.setPersona({ name: "Amarre", personality: "轻松幽默", replyStyle: "先结论后展开", enabled: true });
|
|
119
|
+
const text = injectorText({});
|
|
120
|
+
assert.equal(text, [
|
|
121
|
+
"[用户设置] 来自 dsh-mneme 的用户画像与规则:",
|
|
122
|
+
"- 你的人格名字:Amarre",
|
|
123
|
+
"- 人格性格设定:轻松幽默",
|
|
124
|
+
"- 回复要求:先结论后展开",
|
|
125
|
+
"- 用户画像:用户是规划背景",
|
|
126
|
+
"- 规则:不提及用户名"
|
|
127
|
+
].join("\n"), "两个新字段都没设时不许留下空标签或空行");
|
|
128
|
+
store.close();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ---- 2. 存储往返 -------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
test("bySession 存取往返:写入 → 读回 → 列出 → 删除", () => {
|
|
134
|
+
const { store, settings } = setup();
|
|
135
|
+
const card = { name: "Ephor", personality: "审计口径", enabled: true };
|
|
136
|
+
assert.equal(settings.setPersonaForSession("session-389faaa7", card), true);
|
|
137
|
+
assert.deepEqual(resolvePersonaCard(settings, null, { sessionId: "session-389faaa7" }).card, card);
|
|
138
|
+
assert.deepEqual(settings.listPersonaSessions(), ["session-389faaa7"]);
|
|
139
|
+
// 与 byAgent 互不干扰
|
|
140
|
+
settings.setPersonaFor("standard", { name: "标准卡", enabled: true });
|
|
141
|
+
assert.deepEqual(settings.listPersonas(), ["standard"]);
|
|
142
|
+
assert.deepEqual(settings.listPersonaSessions(), ["session-389faaa7"]);
|
|
143
|
+
// 删
|
|
144
|
+
assert.equal(settings.removePersonaForSession("session-389faaa7"), true);
|
|
145
|
+
assert.deepEqual(settings.listPersonaSessions(), []);
|
|
146
|
+
assert.equal(settings.removePersonaForSession("session-389faaa7"), false, "删不存在的卡返回 false");
|
|
147
|
+
store.close();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("setPersonaForSession 拒绝空 id 与 default(兜底位不可被顶替)", () => {
|
|
151
|
+
const { store, settings } = setup();
|
|
152
|
+
assert.equal(settings.setPersonaForSession("", { name: "x" }), false);
|
|
153
|
+
assert.equal(settings.setPersonaForSession(" ", { name: "x" }), false);
|
|
154
|
+
assert.equal(settings.setPersonaForSession(null, { name: "x" }), false);
|
|
155
|
+
assert.equal(settings.setPersonaForSession("default", { name: "x" }), false);
|
|
156
|
+
assert.deepEqual(settings.listPersonaSessions(), []);
|
|
157
|
+
store.close();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("写空卡等于删除该会话卡(与 setPersonaFor 同形)", () => {
|
|
161
|
+
const { store, settings } = setup();
|
|
162
|
+
settings.setPersonaForSession("s1", { name: "甲", enabled: true });
|
|
163
|
+
assert.deepEqual(settings.listPersonaSessions(), ["s1"]);
|
|
164
|
+
settings.setPersonaForSession("s1", {});
|
|
165
|
+
assert.deepEqual(settings.listPersonaSessions(), [], "空卡不留空壳键");
|
|
166
|
+
store.close();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("setPersona 单对象形只改 default,bySession 保留", () => {
|
|
170
|
+
const { store, settings } = setup();
|
|
171
|
+
settings.setPersonaForSession("s1", { name: "会话卡", enabled: true });
|
|
172
|
+
settings.setPersona({ name: "Amarre-新", enabled: true }); // 旧面板写法
|
|
173
|
+
const got = settings.getPersona();
|
|
174
|
+
assert.equal(got.default.name, "Amarre-新");
|
|
175
|
+
assert.equal(got.bySession.s1.name, "会话卡", "bySession 必须活过一次 legacy 形写入");
|
|
176
|
+
store.close();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("多卡形全量替换:带 bySession 键即整份替换", () => {
|
|
180
|
+
const { store, settings } = setup();
|
|
181
|
+
settings.setPersonaForSession("old", { name: "旧卡", enabled: true });
|
|
182
|
+
settings.setPersona({ default: { name: "新默认", enabled: true }, byAgent: {}, bySession: { new: { name: "新卡", enabled: true } } });
|
|
183
|
+
assert.deepEqual(settings.listPersonaSessions(), ["new"]);
|
|
184
|
+
store.close();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ---- 3. 寻址优先级 -----------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
test("优先级:bySession 盖过 byAgent(会话比预设具体)", () => {
|
|
190
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
191
|
+
seed(settings); // default=Amarre, byAgent.standard=标准卡
|
|
192
|
+
settings.setPersonaForSession("session-abc", { name: "会话专属", personality: "只在这次会话", enabled: true });
|
|
193
|
+
const text = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
|
|
194
|
+
assert.ok(text.includes("你的人格名字:会话专属"), "会话卡必须赢");
|
|
195
|
+
assert.ok(!text.includes("标准卡"), "byAgent 卡不该同时出现");
|
|
196
|
+
assert.ok(!text.includes("Amarre"), "default 更不该出现");
|
|
197
|
+
store.close();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("优先级:会话卡被关掉时落回 byAgent(不是直接跳 default)", () => {
|
|
201
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
202
|
+
seed(settings);
|
|
203
|
+
settings.setPersonaForSession("session-abc", { name: "会话专属", enabled: false });
|
|
204
|
+
const text = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
|
|
205
|
+
assert.ok(text.includes("你的人格名字:标准卡"), "关掉的会话卡让位给身份卡");
|
|
206
|
+
assert.ok(!text.includes("会话专属"));
|
|
207
|
+
store.close();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("优先级:没有会话卡时按 byAgent 走(老行为不变)", () => {
|
|
211
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
212
|
+
seed(settings);
|
|
213
|
+
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-unknown" });
|
|
214
|
+
assert.equal(hit.source, "byAgent");
|
|
215
|
+
assert.equal(hit.card.name, "标准卡");
|
|
216
|
+
store.close();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("两个键都没有:落 default", () => {
|
|
220
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
221
|
+
seed(settings);
|
|
222
|
+
const hit = resolvePersonaCard(settings, null, { sessionId: null });
|
|
223
|
+
assert.equal(hit.source, "default");
|
|
224
|
+
assert.equal(hit.card.name, "Amarre");
|
|
225
|
+
store.close();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("只给 sessionId、不给 agentId 也能命中会话卡(宿主可能只有会话 id)", () => {
|
|
229
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
230
|
+
seed(settings);
|
|
231
|
+
settings.setPersonaForSession("session-only", { name: "仅会话卡", enabled: true });
|
|
232
|
+
const text = injectorText({ sessionId: "session-only" });
|
|
233
|
+
assert.ok(text.includes("你的人格名字:仅会话卡"));
|
|
234
|
+
store.close();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ---- 4. 严格寻址:两维合判 ---------------------------------------------------
|
|
238
|
+
|
|
239
|
+
test("strict:只建了 bySession 卡时,按 byAgent 寻址的会话也不许被兜底", () => {
|
|
240
|
+
// #587 的例外是「一张卡都没建」才回落 default。用户建了会话卡就已经做过分区,
|
|
241
|
+
// 此时一个没建卡的 preset 会话再拿 default 去兜,等于把分区抹掉一半。
|
|
242
|
+
const { store, settings } = setup();
|
|
243
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
244
|
+
assert.deepEqual(settings.getPersona().byAgent, {}, "前提:byAgent 为空");
|
|
245
|
+
assert.deepEqual(settings.getPersona().bySession, {}, "前提:bySession 也为空");
|
|
246
|
+
// 两维都空 → 仍回落 default(#587 例外保持)
|
|
247
|
+
assert.equal(resolvePersonaCard(settings, "standard", { strict: true }).source, "default");
|
|
248
|
+
// 只建会话卡之后,例外立即失效
|
|
249
|
+
settings.setPersonaForSession("session-x", { name: "会话卡", enabled: true });
|
|
250
|
+
assert.equal(resolvePersonaCard(settings, "standard", { strict: true }).source, "none");
|
|
251
|
+
// 而那张会话卡自己仍然命中
|
|
252
|
+
assert.equal(resolvePersonaCard(settings, null, { strict: true, sessionId: "session-x" }).source, "bySession");
|
|
253
|
+
store.close();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("strict:只建了 byAgent 卡时,按会话寻址的会话也不许被兜底", () => {
|
|
257
|
+
const { store, settings } = setup();
|
|
258
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
259
|
+
settings.setPersonaFor("standard", { name: "标准卡", enabled: true });
|
|
260
|
+
// 有会话 id 但没建会话卡 → none(用户已经做过 byAgent 分区)
|
|
261
|
+
assert.equal(resolvePersonaCard(settings, null, { strict: true, sessionId: "session-y" }).source, "none");
|
|
262
|
+
store.close();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("strict:两个键都解析不到时仍用 default(无名会话是 default 的受众)", () => {
|
|
266
|
+
const { store, settings } = setup();
|
|
267
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
268
|
+
settings.setPersonaForSession("session-x", { name: "会话卡", enabled: true });
|
|
269
|
+
assert.equal(resolvePersonaCard(settings, null, { strict: true }).source, "default");
|
|
270
|
+
store.close();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ---- 5. scenario / mesExample ------------------------------------------------
|
|
274
|
+
|
|
275
|
+
test("scenario / mesExample 设置在 replyStyle 之后渲染", () => {
|
|
276
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
277
|
+
settings.setPersona({
|
|
278
|
+
name: "Amarre",
|
|
279
|
+
personality: "轻松幽默",
|
|
280
|
+
replyStyle: "先结论后展开",
|
|
281
|
+
scenario: "深夜陪你赶论文",
|
|
282
|
+
mesExample: "桉桉:好累。Amarre:先睡,剩下的我盯着。",
|
|
283
|
+
enabled: true
|
|
284
|
+
});
|
|
285
|
+
const text = injectorText({});
|
|
286
|
+
const idxReply = text.indexOf("回复要求:先结论后展开");
|
|
287
|
+
const idxScene = text.indexOf("场景设定:深夜陪你赶论文");
|
|
288
|
+
const idxExample = text.indexOf("对话示例:桉桉:好累。");
|
|
289
|
+
assert.ok(idxReply > -1 && idxScene > -1 && idxExample > -1, "三段都在");
|
|
290
|
+
assert.ok(idxScene > idxReply, "场景在语气之后:身份 → 语气 → 场景 → 示例");
|
|
291
|
+
assert.ok(idxExample > idxScene, "示例在场景之后");
|
|
292
|
+
store.close();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("scenario 只设一个字段时只出一行,不牵连另一个", () => {
|
|
296
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
297
|
+
settings.setPersona({ name: "Amarre", scenario: "只在夜里出现", enabled: true });
|
|
298
|
+
const text = injectorText({});
|
|
299
|
+
assert.ok(text.includes("场景设定:只在夜里出现"));
|
|
300
|
+
assert.ok(!text.includes("对话示例"), "没设 mesExample 就不出那一行");
|
|
301
|
+
store.close();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("卡里存着空白 scenario / mesExample 时不出空标签(未设置零输出)", () => {
|
|
305
|
+
// 未设置的字段被 parsePersona 白名单挡在外面,但「存了空白」是另一条路:
|
|
306
|
+
// 卡里确实有这个键、值是空白串。这一行必须整行不出现,不能渲染成「场景设定:」。
|
|
307
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
308
|
+
settings.setPersona({ name: "Amarre", scenario: " ", mesExample: "\t ", enabled: true });
|
|
309
|
+
const card = settings.getPersona().default;
|
|
310
|
+
assert.equal(card.scenario, " ", "空白串仍被存下(白名单不负责裁空白)");
|
|
311
|
+
const text = injectorText({});
|
|
312
|
+
assert.ok(!text.includes("场景设定"), "空白 scenario 不许渲染出标签");
|
|
313
|
+
assert.ok(!text.includes("对话示例"), "空白 mesExample 不许渲染出标签");
|
|
314
|
+
assert.ok(text.includes("你的人格名字:Amarre"), "其余段落照常");
|
|
315
|
+
store.close();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("scenario / mesExample 走同一个转义(花括号不许漏出去)", () => {
|
|
319
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
320
|
+
settings.setPersona({ name: "Amarre", scenario: "{{evil}}", mesExample: "{{x}}", enabled: true });
|
|
321
|
+
const text = injectorText({});
|
|
322
|
+
assert.ok(!text.includes("{{"), "no raw {{ in user-settings block");
|
|
323
|
+
assert.ok(!text.includes("}}"), "no raw }} in user-settings block");
|
|
324
|
+
store.close();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("scenario / mesExample 只在卡 enabled 时渲染(关掉的卡一个字都不出)", () => {
|
|
328
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
329
|
+
settings.setPersona({ name: "Amarre", scenario: "深夜", enabled: false });
|
|
330
|
+
settings.setProfile("有画像");
|
|
331
|
+
const text = injectorText({});
|
|
332
|
+
assert.ok(!text.includes("场景设定"), "卡关着时新字段同样不渲染");
|
|
333
|
+
assert.ok(text.includes("用户画像:"));
|
|
334
|
+
store.close();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("scenario / mesExample 过字段白名单(未知字段仍被丢弃)", () => {
|
|
338
|
+
const { store, settings } = setup();
|
|
339
|
+
settings.setPersona({ name: "Amarre", scenario: "场景", mesExample: "示例", sneaky: "x" });
|
|
340
|
+
const card = settings.getPersona().default;
|
|
341
|
+
assert.equal(card.scenario, "场景");
|
|
342
|
+
assert.equal(card.mesExample, "示例");
|
|
343
|
+
assert.equal("sneaky" in card, false, "白名单没放宽");
|
|
344
|
+
store.close();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ---- 6. 端到端:注入链路 + 预览路由 ------------------------------------------
|
|
348
|
+
|
|
349
|
+
test("注入链路端到端:同一个 agentPreset、不同 sessionId 拿到不同人格", () => {
|
|
350
|
+
// 这就是 #606 要解决的场景本身:DS Bot 里四个 bot 的 agentPreset 全是 standard。
|
|
351
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
352
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
353
|
+
settings.setPersonaForSession("session-d530e132", { name: "Amarre", personality: "主控", enabled: true });
|
|
354
|
+
settings.setPersonaForSession("session-389faaa7", { name: "Ephor", personality: "审计", enabled: true });
|
|
355
|
+
const a = injectorText({ agentPreset: "standard", sessionId: "session-d530e132" });
|
|
356
|
+
const b = injectorText({ agentPreset: "standard", sessionId: "session-389faaa7" });
|
|
357
|
+
assert.ok(a.includes("你的人格名字:Amarre"));
|
|
358
|
+
assert.ok(b.includes("你的人格名字:Ephor"), "同一个 standard 预设下,会话卡必须分得开");
|
|
359
|
+
assert.ok(!b.includes("你的人格名字:Amarre"));
|
|
360
|
+
store.close();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("预览路由支持 &session=,且与注入器逐字节同源", async () => {
|
|
364
|
+
const { store, settings, injectorText, preview } = setup({ personaEnabled: true });
|
|
365
|
+
seed(settings);
|
|
366
|
+
settings.setPersonaForSession("session-abc", { name: "会话专属", personality: "只在这里", enabled: true });
|
|
367
|
+
const fromInjector = injectorText({ agentPreset: "standard", sessionId: "session-abc" });
|
|
368
|
+
const body = await preview({ agent: "standard", session: "session-abc" });
|
|
369
|
+
assert.equal(body.preview.text, fromInjector, "预览必须等于注入器这一帧的输出");
|
|
370
|
+
assert.equal(body.preview.source, "bySession");
|
|
371
|
+
assert.equal(body.preview.session, "session-abc", "面板要靠它说清用的是哪张会话卡");
|
|
372
|
+
assert.ok(body.preview.text.includes("你的人格名字:会话专属"));
|
|
373
|
+
store.close();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("预览只给 session 不给 agent 也走会话卡", async () => {
|
|
377
|
+
const { store, settings, preview } = setup({ personaEnabled: true });
|
|
378
|
+
seed(settings);
|
|
379
|
+
settings.setPersonaForSession("session-only", { name: "仅会话卡", enabled: true });
|
|
380
|
+
const body = await preview({ session: "session-only" });
|
|
381
|
+
assert.equal(body.preview.source, "bySession");
|
|
382
|
+
assert.equal(body.preview.agent, null);
|
|
383
|
+
assert.equal(body.preview.session, "session-only");
|
|
384
|
+
store.close();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("预览的 session 参数在 strict 下同样生效(不许比注入更乐观)", async () => {
|
|
388
|
+
const { store, settings, preview } = setup({ personaEnabled: true, personaStrictAddressing: true });
|
|
389
|
+
seed(settings);
|
|
390
|
+
const miss = await preview({ session: "session-unknown" });
|
|
391
|
+
assert.equal(miss.preview.source, "none", "建过卡、这次会话没建 → 不兜底");
|
|
392
|
+
assert.ok(!miss.preview.text.includes("Amarre"));
|
|
393
|
+
settings.setPersonaForSession("session-hit", { name: "命中卡", enabled: true });
|
|
394
|
+
const hit = await preview({ session: "session-hit" });
|
|
395
|
+
assert.equal(hit.preview.source, "bySession");
|
|
396
|
+
store.close();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// ---- 7. 健壮性 ---------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
test("session.id 取不到(宿主 accessor 抛错)时不影响 agentPreset 寻址", () => {
|
|
402
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
403
|
+
seed(settings);
|
|
404
|
+
// session.id 是抛错的 getter,header 正常——两个键各自护住,不许互相拖累
|
|
405
|
+
const hostile = {
|
|
406
|
+
agent: {
|
|
407
|
+
session: {
|
|
408
|
+
get id() { throw new Error("no id accessor"); },
|
|
409
|
+
header: { agentPreset: "standard" }
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const text = settingsCtx.text(hostile);
|
|
414
|
+
assert.ok(text.includes("你的人格名字:标准卡"), "id 抛错不该让 preset 也丢掉");
|
|
415
|
+
store.close();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("bySession 里的损坏卡(非对象)被丢弃,不抛错", () => {
|
|
419
|
+
const { store, settings } = setup();
|
|
420
|
+
store.db.prepare("INSERT INTO user_settings (key, value) VALUES ('persona', ?)")
|
|
421
|
+
.run(JSON.stringify({ default: {}, byAgent: {}, bySession: { bad: "not a card", good: { name: "好卡", enabled: true } } }));
|
|
422
|
+
const got = settings.getPersona();
|
|
423
|
+
assert.equal("bad" in got.bySession, false, "坏卡读成空对象后不收(与 byAgent 同口径)");
|
|
424
|
+
assert.equal(got.bySession.good.name, "好卡");
|
|
425
|
+
store.close();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("renderUserSettingsBlock 返回值带 session 字段(面板定位用)", () => {
|
|
429
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
430
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
431
|
+
const out = renderUserSettingsBlock({ settings, config: { personaEnabled: true }, agentId: "standard", sessionId: "s1" });
|
|
432
|
+
assert.equal(out.session, "s1");
|
|
433
|
+
assert.equal(out.agent, "standard");
|
|
434
|
+
assert.equal(out.source, "default");
|
|
435
|
+
store.close();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ---- 8. 子 agent 继承父会话人格卡 --------------------------------------------
|
|
439
|
+
//
|
|
440
|
+
// 子 agent 是新会话(header 带 parentSession / origin:"subagent"),用户不可能给每个
|
|
441
|
+
// 派生会话建卡。strict 一开,「有身份、没这张身份的卡」= 不注入人格,子 agent 会从
|
|
442
|
+
// 「自称父会话的人格」变成「不知道自己是谁」。所以自己没卡时继承父会话的卡。
|
|
443
|
+
|
|
444
|
+
test("子 agent 没自己的卡时继承父会话卡(source 是 parentSession)", () => {
|
|
445
|
+
const { store, settings, injectorText } = setup({ personaEnabled: true });
|
|
446
|
+
settings.setPersona({ name: "兜底卡", enabled: true });
|
|
447
|
+
settings.setPersonaForSession("session-parent", { name: "主控人格", personality: "轻松诙谐", enabled: true });
|
|
448
|
+
const text = injectorText({ agentPreset: "standard", sessionId: "session-child", parentSession: "session-parent" });
|
|
449
|
+
assert.ok(text.includes("你的人格名字:主控人格"), "子 agent 必须拿到父会话那张卡");
|
|
450
|
+
assert.ok(!text.includes("兜底卡"), "不许回落到 default");
|
|
451
|
+
const out = renderUserSettingsBlock({
|
|
452
|
+
settings,
|
|
453
|
+
config: { personaEnabled: true },
|
|
454
|
+
agentId: "standard",
|
|
455
|
+
sessionId: "session-child",
|
|
456
|
+
parentSessionId: "session-parent"
|
|
457
|
+
});
|
|
458
|
+
assert.equal(out.source, "parentSession");
|
|
459
|
+
store.close();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("子 agent 自己有卡时自己的卡优先(继承不覆盖专门指定的卡)", () => {
|
|
463
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
464
|
+
settings.setPersonaForSession("session-parent", { name: "父卡", enabled: true });
|
|
465
|
+
settings.setPersonaForSession("session-child", { name: "子卡", enabled: true });
|
|
466
|
+
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
|
|
467
|
+
assert.equal(hit.source, "bySession");
|
|
468
|
+
assert.equal(hit.card.name, "子卡");
|
|
469
|
+
store.close();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("父卡优先于 byAgent(同一段对话比同一预设更能说明用谁的人格)", () => {
|
|
473
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
474
|
+
settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
|
|
475
|
+
settings.setPersonaForSession("session-parent", { name: "父卡", enabled: true });
|
|
476
|
+
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
|
|
477
|
+
assert.equal(hit.source, "parentSession");
|
|
478
|
+
assert.equal(hit.card.name, "父卡");
|
|
479
|
+
store.close();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("父卡被显式关掉时不继承,落回 byAgent(关掉就是关掉)", () => {
|
|
483
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
484
|
+
settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
|
|
485
|
+
settings.setPersonaForSession("session-parent", { name: "父卡", enabled: false });
|
|
486
|
+
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-parent" });
|
|
487
|
+
assert.equal(hit.source, "byAgent");
|
|
488
|
+
assert.equal(hit.card.name, "预设卡");
|
|
489
|
+
store.close();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("strict:子 agent 只解析到父会话 id 时也不许被 default 兜底", () => {
|
|
493
|
+
const { store, settings } = setup({ personaEnabled: true, personaStrictAddressing: true });
|
|
494
|
+
settings.setPersonaForSession("session-other", { name: "别人的卡", enabled: true });
|
|
495
|
+
// 子会话与父会话都没有卡;anyCard=true 且确实有身份 → none
|
|
496
|
+
const miss = resolvePersonaCard(settings, "standard", {
|
|
497
|
+
strict: true,
|
|
498
|
+
sessionId: "session-child",
|
|
499
|
+
parentSessionId: "session-nobody"
|
|
500
|
+
});
|
|
501
|
+
assert.equal(miss.source, "none", "有身份但没卡,strict 下不兜底");
|
|
502
|
+
store.close();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("strict 关时子 agent 没父卡照旧落 default(零行为变化)", () => {
|
|
506
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
507
|
+
settings.setPersona({ name: "Amarre", enabled: true });
|
|
508
|
+
settings.setPersonaForSession("session-other", { name: "别人的卡", enabled: true });
|
|
509
|
+
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: "session-nope" });
|
|
510
|
+
assert.equal(hit.source, "default");
|
|
511
|
+
assert.equal(hit.card.name, "Amarre");
|
|
512
|
+
store.close();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("parentSession 与自身 id 相同(脏数据)时不重复查同一个键", () => {
|
|
516
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
517
|
+
settings.setPersonaForSession("session-same", { name: "同一张", enabled: true });
|
|
518
|
+
const hit = resolvePersonaCard(settings, "standard", {
|
|
519
|
+
sessionId: "session-same",
|
|
520
|
+
parentSessionId: "session-same"
|
|
521
|
+
});
|
|
522
|
+
assert.equal(hit.source, "bySession", "自身维度先命中,不会误标成继承");
|
|
523
|
+
store.close();
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("父会话 id 是空白/非字符串时不影响其余寻址", () => {
|
|
527
|
+
const { store, settings } = setup({ personaEnabled: true });
|
|
528
|
+
settings.setPersonaFor("standard", { name: "预设卡", enabled: true });
|
|
529
|
+
for (const bad of ["", " ", null, undefined, 42, {}]) {
|
|
530
|
+
const hit = resolvePersonaCard(settings, "standard", { sessionId: "session-child", parentSessionId: bad });
|
|
531
|
+
assert.equal(hit.source, "byAgent", `parentSessionId=${JSON.stringify(bad)} 应被忽略`);
|
|
532
|
+
}
|
|
533
|
+
store.close();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("session.header 取不到时不影响 agentPreset 寻址(新加的父会话读取也各自兜错)", () => {
|
|
537
|
+
const { store, settings, settingsCtx } = setup({ personaEnabled: true });
|
|
538
|
+
seed(settings);
|
|
539
|
+
const hostile = {
|
|
540
|
+
agent: {
|
|
541
|
+
session: {
|
|
542
|
+
id: "session-x",
|
|
543
|
+
get header() { throw new Error("no header accessor"); }
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
const text = settingsCtx.text(hostile);
|
|
548
|
+
// header 整体取不到 → agentId/parentSessionId 皆 null,只剩 sessionId,落 default
|
|
549
|
+
assert.ok(text.includes("你的人格名字:Amarre"), "会话卡字段可用时不该整块崩掉");
|
|
550
|
+
store.close();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("注入链路端到端:子 agent 拿到父会话人格,且与预览同源", async () => {
|
|
554
|
+
const { store, settings, injectorText, preview } = setup({ personaEnabled: true });
|
|
555
|
+
settings.setPersona({ name: "兜底", enabled: true });
|
|
556
|
+
settings.setPersonaForSession("session-parent", { name: "父会话人格", personality: "父的性格", enabled: true });
|
|
557
|
+
const fromInjector = injectorText({
|
|
558
|
+
agentPreset: "standard",
|
|
559
|
+
sessionId: "session-child",
|
|
560
|
+
parentSession: "session-parent"
|
|
561
|
+
});
|
|
562
|
+
assert.ok(fromInjector.includes("你的人格名字:父会话人格"));
|
|
563
|
+
const body = await preview({ agent: "standard", session: "session-child", parent: "session-parent" });
|
|
564
|
+
assert.equal(body.preview.text, fromInjector, "预览必须与注入器逐字节同源");
|
|
565
|
+
assert.equal(body.preview.source, "parentSession");
|
|
566
|
+
assert.equal(body.preview.parent, "session-parent");
|
|
567
|
+
store.close();
|
|
568
|
+
});
|