@jjh32456/dsh-longlive 1.6.7 → 1.8.0
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/CHANGELOG.md +70 -1508
- package/LICENSE +1 -1
- package/QUICKSTART.md +34 -13
- package/README.en.md +18 -7
- package/README.md +34 -9
- package/SPEC.md +5 -5
- package/cordis.patch.yml +23 -9
- package/docs/ANSWER-GUARD.md +92 -0
- package/docs/ARCHITECTURE.md +4 -3
- package/docs/AUTO-RECALL-WIRING.md +1 -1
- package/docs/AUTO-RECALL.md +15 -9
- package/docs/CONFIG.md +168 -35
- package/docs/ERRORS.md +17 -5
- package/docs/FAQ.md +7 -7
- package/docs/FEATURES.md +96 -28
- package/docs/MIGRATION-1.6.0.md +2 -2
- package/docs/OPERATIONS.md +6 -6
- package/docs/PANEL-INTEGRATION.md +6 -0
- package/docs/PRIVACY.md +88 -0
- package/docs/QUICKSTART.md +5 -5
- package/docs/README.en.md +4 -2
- package/docs/RESIDENT-WATCHER.md +1 -1
- package/docs/SESSION-WATCH.md +6 -6
- package/docs/TROUBLESHOOTING.md +4 -4
- package/package.json +47 -117
- package/runbook.md +2 -2
- package/scripts/backfill-auto-cards.mjs +98 -0
- package/scripts/install-surgery-task.ps1 +5 -2
- package/scripts/lib/session-watch-fixture.mjs +1 -1
- package/scripts/longlive-doctor.mjs +6 -0
- package/scripts/migrate-180.mjs +163 -0
- package/scripts/recall-index-build.mjs +128 -0
- package/scripts/session-watch-drill.mjs +1 -1
- package/scripts/surgery-run.cmd +1 -1
- package/scripts/unattended-surgery.ps1 +5 -2
- package/scripts/verify-card-anchors.mjs +138 -0
- package/src/actions/audit.js +125 -0
- package/src/actions/evidence.js +122 -0
- package/src/actions/fs.js +162 -0
- package/src/activity/index.js +760 -0
- package/src/activity/model.js +351 -0
- package/src/activity/store.js +644 -0
- package/src/activity/thinking.js +176 -0
- package/src/adapters/constants.js +11 -0
- package/src/adapters/engram/data/aliases.json +51 -0
- package/src/adapters/engram/data/policy.json +8 -0
- package/src/adapters/engram/data/routing-rules.json +546 -0
- package/src/adapters/engram/index.js +235 -0
- package/src/adapters/registry.js +315 -0
- package/src/answer/audit.js +193 -0
- package/src/answer/evidence.js +30 -0
- package/src/answer/guard.js +1370 -0
- package/src/answer/index.js +1067 -0
- package/src/answer/query.js +111 -0
- package/src/answer/second-hop.mjs +101 -0
- package/src/auto-truncate.js +1 -1
- package/src/config.js +171 -47
- package/src/deps.js +6 -4
- package/src/features.js +182 -6
- package/src/heartbeat.js +156 -23
- package/src/identity/handover.js +52 -1
- package/src/identity/index.js +13 -5
- package/src/identity/inject-position.js +2 -2
- package/src/index.js +97 -7
- package/src/lifecycle/gap-clocks.js +328 -0
- package/src/lifecycle/index.js +269 -0
- package/src/live.js +22 -2
- package/src/memory/activity-capture.js +229 -0
- package/src/memory/anchors.js +194 -0
- package/src/memory/audit-records.js +222 -0
- package/src/memory/auto-cards.js +401 -0
- package/src/memory/capture.js +442 -0
- package/src/memory/card-anchors.js +203 -0
- package/src/memory/conflicts.js +106 -0
- package/src/memory/decay.js +311 -0
- package/src/memory/distill.js +471 -0
- package/src/memory/event-index.js +917 -0
- package/src/memory/evidence-chain.js +295 -0
- package/src/memory/evidence.js +145 -0
- package/src/memory/filter-runner.js +1 -1
- package/src/memory/filter.js +5 -1
- package/src/memory/forget-v2.js +410 -0
- package/src/memory/forget.js +71 -0
- package/src/memory/frames-activation.js +247 -0
- package/src/memory/fts-query.js +174 -0
- package/src/memory/governance.js +604 -0
- package/src/memory/index.js +1 -0
- package/src/memory/l2-gate.js +169 -0
- package/src/memory/lock.js +121 -0
- package/src/memory/migrate.js +139 -0
- package/src/memory/recall-v2.js +223 -0
- package/src/memory/recall.js +65 -2
- package/src/memory/records.js +425 -0
- package/src/memory/retention.js +128 -0
- package/src/memory/review.js +124 -0
- package/src/memory/trust.js +88 -0
- package/src/memory/version-chain.js +264 -0
- package/src/memory/wal.js +137 -0
- package/src/memory/working-frame.js +516 -0
- package/src/metrics/classify.mjs +5 -3
- package/src/metrics/index.mjs +21 -1
- package/src/metrics/prestep-latency.mjs +300 -0
- package/src/metrics/store.mjs +25 -0
- package/src/policy-profile.js +18 -3
- package/src/recall/anaphora.mjs +87 -0
- package/src/recall/cards.mjs +7 -0
- package/src/recall/embedding.mjs +8 -8
- package/src/recall/entities.mjs +112 -0
- package/src/recall/index.js +235 -19
- package/src/recall/inject-order.mjs +10 -8
- package/src/recall/privacy.mjs +105 -16
- package/src/recall/recall-core.mjs +116 -10
- package/src/recall/shared-visibility.js +302 -0
- package/src/recall/time-normalizer.mjs +196 -0
- package/src/role/activation.js +191 -0
- package/src/role/attention.js +318 -0
- package/src/role/base.js +36 -0
- package/src/role/channels.js +423 -0
- package/src/role/index.js +425 -0
- package/src/role/interfaces.js +115 -0
- package/src/role/personality.js +370 -0
- package/src/role/readers.js +151 -0
- package/src/role/relationships.js +477 -0
- package/src/role/store.js +202 -0
- package/src/role/writeprotect.js +373 -0
- package/src/router/aliases.json +176 -25
- package/src/router/background/agent-runner.mjs +1 -1
- package/src/router/background/anchor.mjs +1 -1
- package/src/router/background/audit.mjs +2 -2
- package/src/router/background/budget-ledger.mjs +2 -2
- package/src/router/background/host-sim.mjs +1 -1
- package/src/router/background/memory.mjs +4 -4
- package/src/router/background/queue.mjs +1 -1
- package/src/router/background/retry.mjs +1 -1
- package/src/router/background/router.mjs +1 -1
- package/src/router/background/stream.mjs +1 -1
- package/src/router/background/tool-policy.mjs +1 -1
- package/src/router/design-headings.json +5 -5
- package/src/router/face/config.mjs +1 -1
- package/src/router/face/identity.mjs +3 -3
- package/src/router/judge.mjs +8 -7
- package/src/router/policies/generic.json +21 -4
- package/src/router/route.js +109 -22
- package/src/router/router.mjs +10 -6
- package/src/router/routing-rules.json +260 -445
- package/src/router/run-sim.mjs +8 -8
- package/src/router/runtime.js +23 -12
- package/src/router/service/board.mjs +95 -15
- package/src/router/service/config.mjs +5 -5
- package/src/router/service/context.mjs +1 -1
- package/src/router/service/hint.mjs +14 -0
- package/src/router/service/queue.mjs +1 -1
- package/src/router/service/service.mjs +27 -11
- package/src/router/shadow.mjs +4 -3
- package/src/surgery-recovery.js +2 -2
- package/src/tool-output.js +2 -2
- package/src/tools.js +39 -7
- package/src/truncate-visible.js +2 -2
- package/src/ui/actions.js +1312 -0
- package/src/ui/panel-api.js +367 -0
- package/src/ui/panel-auth.js +251 -0
- package/src/ui/panel-integrity.js +243 -0
- package/src/ui/routes-readonly.js +695 -0
- package/src/ui/routes.js +3 -0
- package/docs/DEV.md +0 -279
- package/docs/PRESENCE-BUS-WIRING.md +0 -491
- package/docs/PRESENCE-BUS.md +0 -694
- package/docs/SURGERY.md +0 -186
- package/scripts/mark-core-cards.mjs +0 -289
- package/scripts/pack.mjs +0 -392
- package/scripts/router-proposals.mjs +0 -386
- package/src/router/fixtures/M1.json +0 -16
- package/src/router/fixtures/M2.json +0 -16
- package/src/router/fixtures/M3.json +0 -20
- package/src/router/fixtures/M4.json +0 -20
- package/src/router/fixtures/M5.json +0 -45
- package/src/router/fixtures/M6.json +0 -53
- package/src/router/fixtures/M7.json +0 -46
- package/src/router/fixtures/S1.json +0 -20
- package/src/router/fixtures/S2.json +0 -23
- package/src/router/fixtures/S3.json +0 -20
- package/src/router/fixtures/S4.json +0 -34
- package/src/router/fixtures/S5.json +0 -43
- package/src/router/fixtures/S6.json +0 -47
- package/src/router/fixtures/T1.json +0 -23
- package/src/router/fixtures/T2.json +0 -16
- package/src/router/fixtures/T3.json +0 -19
- package/src/router/fixtures/T4.json +0 -16
- package/src/router/fixtures/X1.json +0 -17
- package/src/router/fixtures/X2.json +0 -20
- package/src/router/gen-design-headings.mjs +0 -107
- package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
- package/test/auto-recall.test.mjs +0 -474
- package/test/auto-truncate.test.mjs +0 -931
- package/test/autobind.test.mjs +0 -385
- package/test/bus.test.mjs +0 -1572
- package/test/cli-guard.test.mjs +0 -198
- package/test/conclusion.test.mjs +0 -408
- package/test/config-schema.test.mjs +0 -204
- package/test/d6-output.test.mjs +0 -194
- package/test/d6.test.mjs +0 -395
- package/test/deps.test.mjs +0 -342
- package/test/doctor-setup.test.mjs +0 -297
- package/test/embedding-recall.test.mjs +0 -347
- package/test/feature-flags.test.mjs +0 -568
- package/test/filter-shadow.test.mjs +0 -170
- package/test/fixtures/baseline-0.8.0-registration.json +0 -556
- package/test/fixtures/d6-header-tools.json +0 -1
- package/test/fixtures/lock-child.mjs +0 -67
- package/test/fixtures/pre-step-container.mjs +0 -100
- package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
- package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
- package/test/fixtures/profile-session-longlive-config.json +0 -20
- package/test/fixtures/proposal-cas-child.mjs +0 -66
- package/test/host-engine-status.test.mjs +0 -307
- package/test/hot-truncate.test.mjs +0 -1349
- package/test/identity-card-marker.test.mjs +0 -307
- package/test/identity-router/run.mjs +0 -721
- package/test/integration-pre-step.test.mjs +0 -553
- package/test/live.test.mjs +0 -1020
- package/test/maintenance-schedule.test.mjs +0 -538
- package/test/memory.test.mjs +0 -2828
- package/test/metrics.test.mjs +0 -478
- package/test/orchestrator.test.mjs +0 -266
- package/test/p1_probe.mjs +0 -97
- package/test/packaging.test.mjs +0 -182
- package/test/portability.test.mjs +0 -343
- package/test/rc4-regressions.test.mjs +0 -562
- package/test/rc5-regressions.test.mjs +0 -197
- package/test/rc6-chinese.test.mjs +0 -394
- package/test/router-1100/apply-off.test.mjs +0 -175
- package/test/router-1100/budget-reconcile.test.mjs +0 -215
- package/test/router-1100/config-context.test.mjs +0 -211
- package/test/router-1100/helpers.mjs +0 -129
- package/test/router-1100/hint-envelope.test.mjs +0 -150
- package/test/router-1100/lock-crossproc.test.mjs +0 -206
- package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
- package/test/router-1100/router-apply.test.mjs +0 -372
- package/test/router-1100/router-fixb.test.mjs +0 -602
- package/test/router-1100/router-proposals.test.mjs +0 -405
- package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
- package/test/router-1100/router-security-cr.test.mjs +0 -333
- package/test/router-1100/service-lifecycle.test.mjs +0 -220
- package/test/router-1100/tools-contract.test.mjs +0 -164
- package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
- package/test/router-1100/v6-contract.test.mjs +0 -158
- package/test/router-1100/v6b-contract.test.mjs +0 -293
- package/test/router-1100/v6c-contract.test.mjs +0 -249
- package/test/router-1100/wal-rotation.test.mjs +0 -375
- package/test/router-1100/wp1-config.test.mjs +0 -170
- package/test/router-1100/wp1-default-off.test.mjs +0 -77
- package/test/router-1100/wp1-model-status.test.mjs +0 -90
- package/test/router-1100/wp1-status.test.mjs +0 -220
- package/test/router-1100/wp1-tools.test.mjs +0 -146
- package/test/router-1100/wp5-cli.test.mjs +0 -240
- package/test/router-1100/wp5-model-auto.test.mjs +0 -418
- package/test/router-1100/wp6-merge.test.mjs +0 -386
- package/test/router-1100/wp6-wake.test.mjs +0 -502
- package/test/router-background/audit.test.mjs +0 -37
- package/test/router-background/budget-ledger.test.mjs +0 -330
- package/test/router-background/cross-process.test.mjs +0 -222
- package/test/router-background/fixtures/cross-child.mjs +0 -85
- package/test/router-background/fixtures/real-shapes.mjs +0 -239
- package/test/router-background/router.test.mjs +0 -1044
- package/test/router-background/stream-retry.test.mjs +0 -358
- package/test/router-background/tool-agent.test.mjs +0 -384
- package/test/router-face/bridge.test.mjs +0 -169
- package/test/router-face/face.test.mjs +0 -316
- package/test/router-failclosed.test.mjs +0 -466
- package/test/router-identity.test.mjs +0 -384
- package/test/router-l1-l2.contract.test.mjs +0 -1075
- package/test/router-memory-tools.test.mjs +0 -262
- package/test/router-policy.test.mjs +0 -225
- package/test/router-runtime.test.mjs +0 -438
- package/test/router-shadow.test.mjs +0 -287
- package/test/router.test.mjs +0 -903
- package/test/run-rc.mjs +0 -299
- package/test/run.mjs +0 -2469
- package/test/safety-defaults.test.mjs +0 -179
- package/test/seeded-load.test.mjs +0 -436
- package/test/session-original.test.mjs +0 -338
- package/test/surgery-recovery.test.mjs +0 -936
- package/test/t7_drive.mjs +0 -151
- package/test/tool-output.test.mjs +0 -393
- package/test/tool-schema.test.mjs +0 -177
- package/test/tools_contract.test.mjs +0 -716
- package/test/truncate-visible.test.mjs +0 -1150
- package/test/ui-flags.test.mjs +0 -477
- package/test/v4-hardening.test.mjs +0 -427
|
@@ -1,553 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 真·集成测试:两个插件的 apply() 跑在复刻的 Cordis pre-step 容器里。
|
|
3
|
-
*
|
|
4
|
-
* 红队第 5 条:旧测试 0 次调用 apply()、顺序靠手拼数组。这里所有顺序断言
|
|
5
|
-
* 都来自 PreStepBus 的 waterfall 合成(见 fixtures/pre-step-container.mjs),
|
|
6
|
-
* 不出现 `[a, b, c]` 式的人工排序。
|
|
7
|
-
*
|
|
8
|
-
* 运行:node --test test/
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import test from 'node:test';
|
|
12
|
-
import assert from 'node:assert/strict';
|
|
13
|
-
import fs from 'node:fs';
|
|
14
|
-
import os from 'node:os';
|
|
15
|
-
import path from 'node:path';
|
|
16
|
-
|
|
17
|
-
import { apply as applyAuto } from '../src/recall/index.js';
|
|
18
|
-
import { apply as applyIdentity } from '../src/identity/index.js';
|
|
19
|
-
import { loadCards } from '../src/recall/cards.mjs';
|
|
20
|
-
import { PRIVACY_HIDDEN_PATH, buildQueryText, chooseRecall, renderItemHeader } from '../src/recall/recall-core.mjs';
|
|
21
|
-
import { isAutoMessage, isIdentityMessage } from '../src/recall/inject-order.mjs';
|
|
22
|
-
import { PreStepBus, makeUserMessageFactory, noopLogger, textMessage } from './fixtures/pre-step-container.mjs';
|
|
23
|
-
|
|
24
|
-
const BOOT_MARK = '【开机卡】';
|
|
25
|
-
const AUTO_MARK = '【自动想起】';
|
|
26
|
-
|
|
27
|
-
/** 宿主 MessageSourceMap 的完整成员(dsh-llm/lib/typert.host.js:475-477)。 */
|
|
28
|
-
const HOST_SOURCE_KINDS = new Set([
|
|
29
|
-
'user', 'model', 'tool', 'system-prompt', 'model-selection', 'runtime-context',
|
|
30
|
-
'working-directory', 'user-approval', 'ptc-mode', 'tool-registry', 'user-rpc',
|
|
31
|
-
'agent-message', 'subagent-settled', 'skill-invocation', 'compact-checkpoint',
|
|
32
|
-
'cordis-host-runner', 'team-message', 'user-question-reply', 'goal', 'plan-mode',
|
|
33
|
-
'schedule', 'session-reference', 'webhook',
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
const AGENT = { session: { id: 'sess-integration', header: { origin: 'main' } } };
|
|
37
|
-
|
|
38
|
-
function writeFile(root, rel, text) {
|
|
39
|
-
const full = path.join(root, rel);
|
|
40
|
-
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
41
|
-
fs.writeFileSync(full, text, 'utf8');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 临时记忆根:topics + handover + STATE.md。测试结束自动删。
|
|
46
|
-
* @param {import('node:test').TestContext} t
|
|
47
|
-
* @param {{topics?:Record<string,string>, handover?:Record<string,string>, state?:string}} [input]
|
|
48
|
-
*/
|
|
49
|
-
function makeMemory(t, { topics = {}, handover = {}, state = null } = {}) {
|
|
50
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'auto-recall-it-'));
|
|
51
|
-
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
52
|
-
writeFile(root, 'STATE.md', state ?? [
|
|
53
|
-
'## 我在哪',
|
|
54
|
-
'自动想起集成测试',
|
|
55
|
-
'## 在等什么',
|
|
56
|
-
'红队验收',
|
|
57
|
-
'## 最近三件',
|
|
58
|
-
'- 修顺序',
|
|
59
|
-
'- 修打包',
|
|
60
|
-
'- 修测试',
|
|
61
|
-
].join('\n'));
|
|
62
|
-
for (const [name, text] of Object.entries(topics)) writeFile(root, `topics/${name}`, text);
|
|
63
|
-
for (const [name, text] of Object.entries(handover)) writeFile(root, `handover/${name}`, text);
|
|
64
|
-
return root;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const TOPIC_CARD = [
|
|
68
|
-
'---',
|
|
69
|
-
'title: 听觉训练',
|
|
70
|
-
'scope_in: ["音程辨认"]',
|
|
71
|
-
'---',
|
|
72
|
-
'# 听觉训练',
|
|
73
|
-
'- 每天练音程辨认,先听根音再听音程。',
|
|
74
|
-
].join('\n');
|
|
75
|
-
|
|
76
|
-
const HANDOVER_CARD = [
|
|
77
|
-
'---',
|
|
78
|
-
'title: 测试交接卡一',
|
|
79
|
-
'scope_in: ["测试交接卡一"]',
|
|
80
|
-
'---',
|
|
81
|
-
'# 测试交接卡一',
|
|
82
|
-
'- 上次停在 auto-recall 的红队修复。',
|
|
83
|
-
].join('\n');
|
|
84
|
-
|
|
85
|
-
function makeStack({ memoryRoot, identity = true, auto = true }) {
|
|
86
|
-
const bus = new PreStepBus();
|
|
87
|
-
const ctx = { on: bus.on.bind(bus), logger: noopLogger() };
|
|
88
|
-
const factory = makeUserMessageFactory();
|
|
89
|
-
if (identity) {
|
|
90
|
-
applyIdentity(ctx, {
|
|
91
|
-
memoryRoot,
|
|
92
|
-
createUserMessage: factory,
|
|
93
|
-
writeOnTurnEnd: false,
|
|
94
|
-
writeOnSessionEnd: false,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
if (auto) {
|
|
98
|
-
applyAuto(ctx, {
|
|
99
|
-
memoryRoot,
|
|
100
|
-
createUserMessage: factory,
|
|
101
|
-
dirs: ['topics', 'handover'],
|
|
102
|
-
log: false,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return { bus, factory };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function textOf(message) {
|
|
109
|
-
return (Array.isArray(message?.content) ? message.content : [])
|
|
110
|
-
.filter((b) => b?.type === 'text')
|
|
111
|
-
.map((b) => b.text)
|
|
112
|
-
.join('\n');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function roleOf(message) {
|
|
116
|
-
const text = textOf(message);
|
|
117
|
-
if (text.startsWith(BOOT_MARK)) return 'boot';
|
|
118
|
-
if (text.startsWith(AUTO_MARK)) return 'auto';
|
|
119
|
-
if (message?.source?.kind === 'runtime-context') return 'runtime';
|
|
120
|
-
return 'user';
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function runtimeMessage(text = 'runtime context snapshot') {
|
|
124
|
-
return textMessage(text, { role: 'user', source: { kind: 'runtime-context' } });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function legacyBootMessage(text = `${BOOT_MARK}旧版 identity 在 next() 之后追加`) {
|
|
128
|
-
return textMessage(text, { id: 'legacy-boot' });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/* ================================================================== *
|
|
132
|
-
* A:两插件都在 → [user, boot, runtime, auto](v3:boot 在 user 之后)
|
|
133
|
-
* ================================================================== */
|
|
134
|
-
|
|
135
|
-
test('apply() 集成 A:identity + auto 合成后 user 在首、boot 紧随其后、auto 在尾', async (t) => {
|
|
136
|
-
const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD }, handover: { '2026-11-04-boot.md': HANDOVER_CARD } });
|
|
137
|
-
const { bus } = makeStack({ memoryRoot });
|
|
138
|
-
const user = textMessage('今天继续练音程辨认,顺便看测试交接卡一', { id: 'user-1' });
|
|
139
|
-
|
|
140
|
-
const { decision, claimed } = await bus.compose({
|
|
141
|
-
messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// 顺序只由注册语义 + 插件代码合成,断言不手工拼期望数组。
|
|
145
|
-
const order = decision.messages.map(roleOf);
|
|
146
|
-
assert.deepEqual(order, ['user', 'boot', 'runtime', 'auto'], `实际:${order.join(',')}`);
|
|
147
|
-
// 宿主共享的 claimed 里也只允许 [user, boot]:boot 插在真实 user 之后,不是最前。
|
|
148
|
-
assert.deepEqual(claimed.map(roleOf), ['user', 'boot'], 'boot 必须紧跟真实 user,不能抢首位');
|
|
149
|
-
assert.equal(decision.messages.filter((m) => roleOf(m) === 'boot').length, 1);
|
|
150
|
-
assert.equal(decision.messages.filter((m) => roleOf(m) === 'auto').length, 1);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
/* ================================================================== *
|
|
154
|
-
* TurnNavigator 等价:宿主取该轮第一个可见 kind:'user' 节点做 prompt
|
|
155
|
-
* (dsh-client-ui-chat/lib/client.js:8371-8383;tooltip :460-467)
|
|
156
|
-
* ================================================================== */
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* 复刻宿主投影:user/message 且 source.kind === 'user' → kind:'user' 可见节点,
|
|
160
|
-
* 其余(session-reference / runtime-context 等)→ context 节点(client.js:9887-9893)。
|
|
161
|
-
* @param {object[]} messages
|
|
162
|
-
* @returns {{kind:string, data:object}[]}
|
|
163
|
-
*/
|
|
164
|
-
function chatNodes(messages) {
|
|
165
|
-
return (Array.isArray(messages) ? messages : []).map((message) => ({
|
|
166
|
-
kind: message?.source?.kind === 'user' ? 'user' : 'context',
|
|
167
|
-
data: message,
|
|
168
|
-
}));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** 复刻 turnNavigationItem 的 prompt 提取:loaded.find(kind === 'user')。 */
|
|
172
|
-
function firstVisibleUserPrompt(messages) {
|
|
173
|
-
const user = chatNodes(messages).filter((node) => node.kind === 'user')[0];
|
|
174
|
-
return user === undefined ? '' : textOf(user.data);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
test('TurnNavigator 等价:第 1 轮 prompt 取首个非注入 user,不是开机卡', async (t) => {
|
|
178
|
-
const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD }, handover: { '2026-11-04-boot.md': HANDOVER_CARD } });
|
|
179
|
-
const { bus } = makeStack({ memoryRoot });
|
|
180
|
-
const user = textMessage('今天继续练音程辨认,顺便看测试交接卡一', { id: 'user-1' });
|
|
181
|
-
|
|
182
|
-
const { decision } = await bus.compose({
|
|
183
|
-
messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const prompt = firstVisibleUserPrompt(decision.messages);
|
|
187
|
-
assert.ok(prompt.startsWith('今天继续练音程辨认'), `tooltip 必须是用户提问,实际:${prompt.slice(0, 40)}`);
|
|
188
|
-
assert.doesNotMatch(prompt, /【开机卡】/, 'tooltip 不能显示开机卡');
|
|
189
|
-
assert.equal(prompt, textOf(user), '首个可见 user 节点必须就是用户原话');
|
|
190
|
-
|
|
191
|
-
// 反证:把 boot 的 source.kind 改回 v2 的 'user',同一个提取函数就会拿到开机卡
|
|
192
|
-
// (v4 的 session-reference 不会再投影成 user 节点)。
|
|
193
|
-
const boot = decision.messages.find((m) => roleOf(m) === 'boot');
|
|
194
|
-
const legacyBoot = { ...boot, source: { ...boot.source, kind: 'user' } };
|
|
195
|
-
const legacyOrder = [legacyBoot, user];
|
|
196
|
-
assert.match(firstVisibleUserPrompt(legacyOrder), /【开机卡】/);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
/* ================================================================== *
|
|
200
|
-
* B:只有 auto → [user, runtime, auto]
|
|
201
|
-
* ================================================================== */
|
|
202
|
-
|
|
203
|
-
test('apply() 集成 B:没有 identity 时 auto 追加在 runtime 之后,不伪造 boot', async (t) => {
|
|
204
|
-
const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD } });
|
|
205
|
-
const { bus } = makeStack({ memoryRoot, identity: false });
|
|
206
|
-
const user = textMessage('今天继续练音程辨认', { id: 'user-1' });
|
|
207
|
-
|
|
208
|
-
const { decision } = await bus.compose({
|
|
209
|
-
messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
|
|
210
|
-
});
|
|
211
|
-
assert.deepEqual(decision.messages.map(roleOf), ['user', 'runtime', 'auto']);
|
|
212
|
-
assert.equal(decision.messages.some((m) => roleOf(m) === 'boot'), false);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
/* ================================================================== *
|
|
216
|
-
* 回归夹具:旧 identity 在 next() 之后 append → before 顺序
|
|
217
|
-
* ================================================================== */
|
|
218
|
-
|
|
219
|
-
test('旧 identity 语义(next 后 append)确实复现 [user, runtime, auto, boot]', async (t) => {
|
|
220
|
-
const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD } });
|
|
221
|
-
const { bus } = makeStack({ memoryRoot, identity: false });
|
|
222
|
-
const user = textMessage('今天继续练音程辨认', { id: 'user-1' });
|
|
223
|
-
|
|
224
|
-
// 只模拟旧实现的注入点(outer prepend + next 后 append),不模拟最终数组。
|
|
225
|
-
bus.on('agent/pre-step', async (payload, next) => {
|
|
226
|
-
const decision = await next();
|
|
227
|
-
if (!decision || decision.kind === 'reject' || Number(payload?.step) !== 1) return decision;
|
|
228
|
-
return { ...decision, messages: [...decision.messages, legacyBootMessage()] };
|
|
229
|
-
}, { prepend: true });
|
|
230
|
-
|
|
231
|
-
const { decision } = await bus.compose({
|
|
232
|
-
messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
|
|
233
|
-
});
|
|
234
|
-
assert.deepEqual(decision.messages.map(roleOf), ['user', 'runtime', 'auto', 'boot']);
|
|
235
|
-
const bootIndex = decision.messages.findIndex((m) => roleOf(m) === 'boot');
|
|
236
|
-
assert.equal(bootIndex, decision.messages.length - 1, '旧语义下 boot 被追加到末尾');
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
/* ================================================================== *
|
|
240
|
-
* mustFix #1:firstUserText 污染 —— v2 红 / v3 绿
|
|
241
|
-
* ================================================================== */
|
|
242
|
-
|
|
243
|
-
function firstText(message) {
|
|
244
|
-
for (const block of Array.isArray(message?.content) ? message.content : []) {
|
|
245
|
-
if (block?.type === 'text' && typeof block.text === 'string') return block.text;
|
|
246
|
-
}
|
|
247
|
-
return '';
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/** v2 的 session/event 实现:只按 type 取第一条 user/message,不区分注入。 */
|
|
251
|
-
function legacyFirstUserText(events) {
|
|
252
|
-
const first = (Array.isArray(events) ? events : []).find((e) => e?.type === 'user/message');
|
|
253
|
-
return first ? firstText(first.data) : '';
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function userMessageEvent(message, seq) {
|
|
257
|
-
return { type: 'user/message', data: message, seq };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const BOOT_TOKEN = '开机卡专属词';
|
|
261
|
-
const USER_TOKEN = '真实问题词';
|
|
262
|
-
|
|
263
|
-
/** 污染回归共用夹具:STATE.md 与一张 topic 都含开机卡专属词,另一张只含真实问题词。 */
|
|
264
|
-
function makePollutionMemory(t) {
|
|
265
|
-
const state = [
|
|
266
|
-
'## 我在哪',
|
|
267
|
-
`- ${BOOT_TOKEN}:这句只该出现在 STATE.md 和开机卡里。`,
|
|
268
|
-
'## 在等什么',
|
|
269
|
-
'- 红队验收',
|
|
270
|
-
'## 最近三件',
|
|
271
|
-
'- 修污染',
|
|
272
|
-
].join('\n');
|
|
273
|
-
const card = (title) => [
|
|
274
|
-
'---',
|
|
275
|
-
`title: ${title}`,
|
|
276
|
-
`scope_in: ["${title}"]`,
|
|
277
|
-
'---',
|
|
278
|
-
`# ${title}`,
|
|
279
|
-
`- ${title}:这是测试卡。`,
|
|
280
|
-
].join('\n');
|
|
281
|
-
return makeMemory(t, {
|
|
282
|
-
state,
|
|
283
|
-
topics: { 'boot-token.md': card(BOOT_TOKEN), 'user-token.md': card(USER_TOKEN) },
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
test('污染回归:v2 把【开机卡】当 firstUserText(红),v3 跳过注入消息(绿)', async (t) => {
|
|
288
|
-
const memoryRoot = makePollutionMemory(t);
|
|
289
|
-
const { bus } = makeStack({ memoryRoot });
|
|
290
|
-
const user = textMessage('今天先随便聊聊,不开工。', { id: 'user-1' });
|
|
291
|
-
|
|
292
|
-
const first = await bus.compose({
|
|
293
|
-
messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
|
|
294
|
-
});
|
|
295
|
-
const boot = first.decision.messages.find((m) => roleOf(m) === 'boot');
|
|
296
|
-
assert.ok(boot, '第一轮应注入开机卡');
|
|
297
|
-
assert.match(textOf(boot), new RegExp(BOOT_TOKEN), '开机卡正文带着专属词(污染源)');
|
|
298
|
-
|
|
299
|
-
// v2 的现实:boot 在最前 → 事件流里第一条 user/message 就是开机卡。
|
|
300
|
-
const events = [
|
|
301
|
-
userMessageEvent(boot, 1),
|
|
302
|
-
userMessageEvent(user, 2),
|
|
303
|
-
userMessageEvent(runtimeMessage(), 3),
|
|
304
|
-
];
|
|
305
|
-
const legacyText = legacyFirstUserText(events);
|
|
306
|
-
assert.equal(legacyText, textOf(boot), '红:v2 把开机卡当首条用户消息');
|
|
307
|
-
const cards = loadCards(memoryRoot, { dirs: ['topics'] });
|
|
308
|
-
const legacyQuery = buildQueryText({
|
|
309
|
-
firstUserText: legacyText,
|
|
310
|
-
recentTexts: [`${USER_TOKEN}是什么`],
|
|
311
|
-
cwd: process.cwd(),
|
|
312
|
-
});
|
|
313
|
-
const legacyHits = chooseRecall({ queryText: legacyQuery, cards, options: { minScore: 0.7 } });
|
|
314
|
-
const legacyPaths = legacyHits.items.map((item) => item.path);
|
|
315
|
-
assert.ok(
|
|
316
|
-
legacyPaths.some((p) => p.includes('boot-token')),
|
|
317
|
-
`红:污染查询会召回开机卡专属词卡,实际 ${legacyPaths.join(',')}`,
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
// v3:把同一段「boot 在前」的事件流喂给真实 handler;注入消息必须被跳过。
|
|
321
|
-
for (const event of events) await bus.emit('session/event', { id: AGENT.session.id }, event);
|
|
322
|
-
await bus.emit('session/event', { id: AGENT.session.id }, { type: 'turn/end', data: { turn: 1 } });
|
|
323
|
-
await bus.emit('session/event', { id: AGENT.session.id }, { type: 'compaction/end' });
|
|
324
|
-
|
|
325
|
-
const turn2 = textMessage(`${USER_TOKEN}是什么`, { id: 'user-2' });
|
|
326
|
-
const second = await bus.compose({
|
|
327
|
-
messages: [turn2], agent: AGENT, turn: 2, step: 1, context: runtimeMessage(),
|
|
328
|
-
});
|
|
329
|
-
const auto = second.decision.messages.find((m) => roleOf(m) === 'auto');
|
|
330
|
-
assert.ok(auto, '第二轮应召回真实问题词卡');
|
|
331
|
-
assert.match(textOf(auto), new RegExp(USER_TOKEN));
|
|
332
|
-
assert.doesNotMatch(textOf(auto), new RegExp(BOOT_TOKEN), '绿:开机卡专属词不能再借 firstUserText 混进第二轮');
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
test('实测:v3 真实两轮事件流(user→boot→runtime)不污染第二轮查询', async (t) => {
|
|
336
|
-
const memoryRoot = makePollutionMemory(t);
|
|
337
|
-
const { bus } = makeStack({ memoryRoot });
|
|
338
|
-
const turn1 = textMessage('今天先随便聊聊,不开工。', { id: 'user-1' });
|
|
339
|
-
|
|
340
|
-
const first = await bus.compose({
|
|
341
|
-
messages: [turn1], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
|
|
342
|
-
});
|
|
343
|
-
assert.deepEqual(first.decision.messages.map(roleOf), ['user', 'boot', 'runtime']);
|
|
344
|
-
|
|
345
|
-
// 严格按 decision.messages 顺序发事件,镜像宿主 step() 的 session 写入。
|
|
346
|
-
let seq = 1;
|
|
347
|
-
for (const message of first.decision.messages) {
|
|
348
|
-
await bus.emit('session/event', { id: AGENT.session.id }, userMessageEvent(message, seq));
|
|
349
|
-
seq += 1;
|
|
350
|
-
}
|
|
351
|
-
await bus.emit('session/event', { id: AGENT.session.id }, { type: 'turn/end', data: { turn: 1 } });
|
|
352
|
-
await bus.emit('session/event', { id: AGENT.session.id }, { type: 'compaction/end' });
|
|
353
|
-
|
|
354
|
-
const turn2 = textMessage(`${USER_TOKEN}是什么`, { id: 'user-2' });
|
|
355
|
-
const second = await bus.compose({
|
|
356
|
-
messages: [turn2], agent: AGENT, turn: 2, step: 1, context: runtimeMessage(),
|
|
357
|
-
});
|
|
358
|
-
const auto = second.decision.messages.find((m) => roleOf(m) === 'auto');
|
|
359
|
-
assert.ok(auto, '第二轮应召回真实问题词卡');
|
|
360
|
-
assert.match(textOf(auto), new RegExp(USER_TOKEN));
|
|
361
|
-
assert.doesNotMatch(textOf(auto), new RegExp(BOOT_TOKEN));
|
|
362
|
-
|
|
363
|
-
// 同一时刻 TurnNavigator 拿到的仍是用户原话。
|
|
364
|
-
assert.equal(firstVisibleUserPrompt(first.decision.messages), textOf(turn1));
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
/* ================================================================== *
|
|
368
|
-
* boot 在 user 之后 + 路径去重:auto 仍看得到,且不重复注入同一张 handover
|
|
369
|
-
* ================================================================== */
|
|
370
|
-
|
|
371
|
-
test('apply() 集成:boot 在 user 之后时 auto 仍看得到,且不重复注入同一张 handover', async (t) => {
|
|
372
|
-
const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD }, handover: { '2026-11-04-boot.md': HANDOVER_CARD } });
|
|
373
|
-
const { bus } = makeStack({ memoryRoot });
|
|
374
|
-
const user = textMessage('测试交接卡一 和 音程辨认 都看一下', { id: 'user-1' });
|
|
375
|
-
|
|
376
|
-
const { decision } = await bus.compose({
|
|
377
|
-
messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
|
|
378
|
-
});
|
|
379
|
-
const auto = decision.messages.find((m) => roleOf(m) === 'auto');
|
|
380
|
-
assert.ok(auto, 'auto 应该有注入');
|
|
381
|
-
const autoText = textOf(auto);
|
|
382
|
-
assert.match(autoText, /听觉训练/);
|
|
383
|
-
assert.doesNotMatch(autoText, /测试交接卡一|handover\//, 'identity 已注入的 handover 卡不能再由 auto 注入');
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
/* ================================================================== *
|
|
387
|
-
* 首轮 handover 不该被无条件吞掉(P2 handover-suppressed)
|
|
388
|
-
* ================================================================== */
|
|
389
|
-
|
|
390
|
-
test('apply() 集成:没装 identity 时首轮 handover 仍可被 auto 召回', async (t) => {
|
|
391
|
-
const memoryRoot = makeMemory(t, { handover: { '2026-11-04-boot.md': HANDOVER_CARD } });
|
|
392
|
-
const { bus } = makeStack({ memoryRoot, identity: false });
|
|
393
|
-
const user = textMessage('测试交接卡一 接着上次的做', { id: 'user-1' });
|
|
394
|
-
|
|
395
|
-
const { decision } = await bus.compose({
|
|
396
|
-
messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
|
|
397
|
-
});
|
|
398
|
-
const auto = decision.messages.find((m) => roleOf(m) === 'auto');
|
|
399
|
-
assert.ok(auto, '首轮 handover 应该能被召回,而不是被 sessionStartExcludedPaths 吞掉');
|
|
400
|
-
assert.match(textOf(auto), /测试交接卡一/);
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
/* ================================================================== *
|
|
404
|
-
* PII 路径:整卡 drop;render 层再兜一层 <hidden>
|
|
405
|
-
* ================================================================== */
|
|
406
|
-
|
|
407
|
-
test('apply() 集成:路径带手机号的卡整卡 drop,注入文本不含号码', async (t) => {
|
|
408
|
-
const piiCard = [
|
|
409
|
-
'---',
|
|
410
|
-
'title: 订单备注',
|
|
411
|
-
'scope_in: ["订单备注"]',
|
|
412
|
-
'---',
|
|
413
|
-
'- 订单备注:按常规口径归档。',
|
|
414
|
-
].join('\n');
|
|
415
|
-
const memoryRoot = makeMemory(t, { topics: { '13900002222-订单.md': piiCard } });
|
|
416
|
-
const { bus } = makeStack({ memoryRoot, identity: false });
|
|
417
|
-
const user = textMessage('订单备注 该怎么归档', { id: 'user-1' });
|
|
418
|
-
|
|
419
|
-
const { decision } = await bus.compose({
|
|
420
|
-
messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
|
|
421
|
-
});
|
|
422
|
-
assert.deepEqual(decision.messages.map(roleOf), ['user', 'runtime']);
|
|
423
|
-
assert.doesNotMatch(decision.messages.map(textOf).join('\n'), /13900002222/);
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
test('chooseRecall 路径隐私:命中文件名手机号时整卡 drop 并计入 droppedByPrivacy', () => {
|
|
427
|
-
const result = chooseRecall({
|
|
428
|
-
queryText: '订单备注 该怎么归档',
|
|
429
|
-
cards: [{
|
|
430
|
-
path: 'topics/13900002222-订单.md',
|
|
431
|
-
title: '订单备注',
|
|
432
|
-
body: '- 订单备注:按常规口径归档。',
|
|
433
|
-
scopeIn: ['订单备注'],
|
|
434
|
-
aliases: [],
|
|
435
|
-
}],
|
|
436
|
-
options: { minScore: 0 },
|
|
437
|
-
});
|
|
438
|
-
assert.equal(result.items.length, 0);
|
|
439
|
-
assert.equal(result.hits.droppedByPrivacy, 1);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
test('renderItemHeader 对路径兜底:命中 PII 只显示 <hidden>', () => {
|
|
443
|
-
const header = renderItemHeader({
|
|
444
|
-
path: 'topics/13900002222-订单.md',
|
|
445
|
-
title: '订单备注',
|
|
446
|
-
score: 1.5,
|
|
447
|
-
});
|
|
448
|
-
assert.ok(header.includes(PRIVACY_HIDDEN_PATH));
|
|
449
|
-
assert.doesNotMatch(header, /13900002222/);
|
|
450
|
-
|
|
451
|
-
const clean = renderItemHeader({ path: 'topics/ear.md', title: '听觉训练', score: 1.5 });
|
|
452
|
-
assert.match(clean, /topics\/ear\.md/);
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
/* ================================================================== *
|
|
456
|
-
* 零命中沉默
|
|
457
|
-
* ================================================================== */
|
|
458
|
-
|
|
459
|
-
test('apply() 集成:零命中时一个字段都不改(decision 原样返回)', async (t) => {
|
|
460
|
-
const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD } });
|
|
461
|
-
const { bus } = makeStack({ memoryRoot, identity: false });
|
|
462
|
-
const user = textMessage('今天晚饭吃什么', { id: 'user-1' });
|
|
463
|
-
|
|
464
|
-
const { decision } = await bus.compose({
|
|
465
|
-
messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
|
|
466
|
-
});
|
|
467
|
-
assert.deepEqual(decision.messages.map(roleOf), ['user', 'runtime']);
|
|
468
|
-
assert.equal(decision.messages.some((m) => isAutoMessage(m)), false);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
/* ================================================================== *
|
|
472
|
-
* source.kind 契约
|
|
473
|
-
* ================================================================== */
|
|
474
|
-
|
|
475
|
-
test('apply() 集成:注入消息 source.kind 是 session-reference,形状 form/version/references 完整', async (t) => {
|
|
476
|
-
const memoryRoot = makeMemory(t, { topics: { 'ear.md': TOPIC_CARD }, handover: { '2026-11-04-boot.md': HANDOVER_CARD } });
|
|
477
|
-
const { bus } = makeStack({ memoryRoot });
|
|
478
|
-
const user = textMessage('今天继续练音程辨认', { id: 'user-1' });
|
|
479
|
-
|
|
480
|
-
const { decision } = await bus.compose({
|
|
481
|
-
messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
|
|
482
|
-
});
|
|
483
|
-
const injected = decision.messages.filter((m) => isAutoMessage(m) || isIdentityMessage(m));
|
|
484
|
-
assert.equal(injected.length, 2, '应各有一条 auto / identity 注入');
|
|
485
|
-
for (const message of injected) {
|
|
486
|
-
assert.equal(message.source?.kind, 'session-reference');
|
|
487
|
-
assert.ok(HOST_SOURCE_KINDS.has(message.source.kind), `kind=${message.source.kind} 不在封闭 union`);
|
|
488
|
-
assert.equal(message.source.form, 'recall');
|
|
489
|
-
assert.equal(message.source.version, 1);
|
|
490
|
-
assert.deepEqual(message.source.references, []);
|
|
491
|
-
assert.equal(message.source.sections, undefined);
|
|
492
|
-
assert.equal(message.source.cardPaths, undefined);
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
/* ================================================================== *
|
|
497
|
-
* reject 时撤回 boot(不在 claimed 里留半个注入)
|
|
498
|
-
* ================================================================== */
|
|
499
|
-
|
|
500
|
-
test('apply() 集成:下游 reject 时 identity 撤回已 prepend 的 boot', async (t) => {
|
|
501
|
-
const memoryRoot = makeMemory(t, { handover: { '2026-11-04-boot.md': HANDOVER_CARD } });
|
|
502
|
-
const { bus } = makeStack({ memoryRoot, identity: true, auto: false });
|
|
503
|
-
bus.on('agent/pre-step', async () => ({ kind: 'reject', reason: 'test-reject' }), { prepend: false });
|
|
504
|
-
const user = textMessage('任意消息', { id: 'user-1' });
|
|
505
|
-
|
|
506
|
-
const { decision, claimed } = await bus.compose({
|
|
507
|
-
messages: [user], agent: AGENT, turn: 1, step: 1, context: runtimeMessage(),
|
|
508
|
-
});
|
|
509
|
-
assert.equal(decision.kind, 'reject');
|
|
510
|
-
assert.deepEqual(claimed.map(roleOf), ['user'], 'reject 后 claimed 里不能残留 boot');
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
/* ================================================================== *
|
|
514
|
-
* 类型契约:直接读宿主 MessageSourceMap 的声明,不靠手抄列表
|
|
515
|
-
* ================================================================== */
|
|
516
|
-
|
|
517
|
-
function declaredString(source, name) {
|
|
518
|
-
const at = source.indexOf(`"name": "${name}"`);
|
|
519
|
-
if (at === -1) return null;
|
|
520
|
-
const key = '"declaration": "';
|
|
521
|
-
const start = source.indexOf(key, at);
|
|
522
|
-
if (start === -1) return null;
|
|
523
|
-
let i = start + key.length;
|
|
524
|
-
let escaped = false;
|
|
525
|
-
let raw = '';
|
|
526
|
-
for (; i < source.length; i += 1) {
|
|
527
|
-
const ch = source[i];
|
|
528
|
-
if (escaped) { raw += `\\${ch}`; escaped = false; continue; }
|
|
529
|
-
if (ch === '\\') { escaped = true; continue; }
|
|
530
|
-
if (ch === '"') break;
|
|
531
|
-
raw += ch;
|
|
532
|
-
}
|
|
533
|
-
try {
|
|
534
|
-
return JSON.parse(`"${raw}"`);
|
|
535
|
-
} catch {
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
test('宿主类型:MessageSourceMap 是封闭 union,包含 user、不含 plugin:*', (t) => {
|
|
541
|
-
const candidates = [
|
|
542
|
-
process.env.DSH_LLM_TYPERT,
|
|
543
|
-
'C:/work/redteam2/asar-extract/dsh/node_modules/@deepseek-ai/dsh-llm/lib/typert.host.js',
|
|
544
|
-
].filter(Boolean);
|
|
545
|
-
const file = candidates.find((p) => fs.existsSync(p));
|
|
546
|
-
if (!file) return t.skip('没有宿主类型文件(设置 DSH_LLM_TYPERT 可补跑)');
|
|
547
|
-
|
|
548
|
-
const declaration = declaredString(fs.readFileSync(file, 'utf8'), 'MessageSourceMap');
|
|
549
|
-
assert.ok(declaration, '没能解析出 MessageSourceMap 声明');
|
|
550
|
-
assert.match(declaration, /user:\s*\{\s*kind:\s*'user';\s*\}/);
|
|
551
|
-
assert.doesNotMatch(declaration, /plugin:/, '宿主 union 里不应有 plugin:*');
|
|
552
|
-
// 本测试与 apply 集成里的 HOST_SOURCE_KINDS 互相独立:一个读声明,一个读运行时消息。
|
|
553
|
-
});
|