@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,197 +0,0 @@
|
|
|
1
|
-
// rc.5 — red-team regressions R4-01 .. R4-02.
|
|
2
|
-
//
|
|
3
|
-
// R4-01 two hosts that BOTH registered the fallback pre-step hook before either
|
|
4
|
-
// injected used to append the receipt twice: the cross-process claim was
|
|
5
|
-
// released before the second host looked, and the second host only read
|
|
6
|
-
// the state at startup. Now the winner re-reads the state while holding
|
|
7
|
-
// the claim, and the atomic mark is checked too; the loser records
|
|
8
|
-
// `skipped-already`.
|
|
9
|
-
// R4-02 the receipt printed only the first 8 characters of a legal session id.
|
|
10
|
-
// A legal `[a-z0-9-]` id is now echoed in full; only an id that fails the
|
|
11
|
-
// whitelist is sanitised and shortened.
|
|
12
|
-
//
|
|
13
|
-
// Each case builds a throwaway memory root under os.tmpdir(); nothing here
|
|
14
|
-
// touches a real session, the host, a profile or the network.
|
|
15
|
-
|
|
16
|
-
import assert from 'node:assert/strict';
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
import os from 'node:os';
|
|
19
|
-
import path from 'node:path';
|
|
20
|
-
import test from 'node:test';
|
|
21
|
-
|
|
22
|
-
import { registerFeatures } from '../src/features.js';
|
|
23
|
-
import {
|
|
24
|
-
CLASSIFICATIONS,
|
|
25
|
-
RECOVERY_REASON,
|
|
26
|
-
displaySessionId,
|
|
27
|
-
readSurgeryState,
|
|
28
|
-
receiptText,
|
|
29
|
-
surgeryStatePath,
|
|
30
|
-
} from '../src/surgery-recovery.js';
|
|
31
|
-
|
|
32
|
-
const RECEIPT = '【手术回执】R4-01 fixture';
|
|
33
|
-
const PAYLOAD = { step: 1, agent: { session: { id: 'session-live', header: { origin: 'main' } } } };
|
|
34
|
-
const NEXT = async () => ({ kind: 'enter', messages: [] });
|
|
35
|
-
|
|
36
|
-
function makeRoot() {
|
|
37
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'longlive-rc5-'));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function writeState(root, receipt = null, sessionId = 'session-live') {
|
|
41
|
-
fs.mkdirSync(path.join(root, '.longlive'), { recursive: true });
|
|
42
|
-
const state = {
|
|
43
|
-
version: 1,
|
|
44
|
-
kind: 'longlive-surgery',
|
|
45
|
-
surgeryId: 'surgery-r4-01-fixture',
|
|
46
|
-
phase: 'restart',
|
|
47
|
-
sessionPath: null,
|
|
48
|
-
sessionId,
|
|
49
|
-
memoryRoot: root,
|
|
50
|
-
dryRun: false,
|
|
51
|
-
receipt,
|
|
52
|
-
history: [],
|
|
53
|
-
};
|
|
54
|
-
fs.writeFileSync(surgeryStatePath(root), `${JSON.stringify(state, null, 2)}\n`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** One host process: register the feature, keep its hook list and warnings. */
|
|
58
|
-
function registerHost(root) {
|
|
59
|
-
const hooks = [];
|
|
60
|
-
const warnings = [];
|
|
61
|
-
const ctx = {
|
|
62
|
-
on(name, listener) {
|
|
63
|
-
const entry = { name, listener };
|
|
64
|
-
hooks.push(entry);
|
|
65
|
-
return () => {
|
|
66
|
-
const index = hooks.indexOf(entry);
|
|
67
|
-
if (index >= 0) hooks.splice(index, 1);
|
|
68
|
-
};
|
|
69
|
-
},
|
|
70
|
-
logger() {
|
|
71
|
-
return {
|
|
72
|
-
warn: (message) => warnings.push(String(message)),
|
|
73
|
-
info: () => {},
|
|
74
|
-
};
|
|
75
|
-
},
|
|
76
|
-
tools: {
|
|
77
|
-
register() {
|
|
78
|
-
return () => {};
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
const returned = registerFeatures(ctx, {}, { memoryRoot: root, surgeryReceipt: RECEIPT });
|
|
83
|
-
assert.equal(hooks.length, 1, 'one receipt hook per host');
|
|
84
|
-
return { hooks, warnings, returned };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function auditText(root) {
|
|
88
|
-
const file = path.join(root, '.longlive', 'audit.log');
|
|
89
|
-
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
// R4-01 — two hosts that registered first still inject only once
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
|
|
96
|
-
test('R4-01: two hosts register before either injects -> one receipt, loser records skipped-already', async () => {
|
|
97
|
-
const root = makeRoot();
|
|
98
|
-
try {
|
|
99
|
-
writeState(root);
|
|
100
|
-
// Both hosts saw "receipt missing" at startup and registered their hook
|
|
101
|
-
// before either one reached step 1: exactly the R3-03 residual.
|
|
102
|
-
const hostA = registerHost(root);
|
|
103
|
-
const hostB = registerHost(root);
|
|
104
|
-
|
|
105
|
-
const first = await hostA.hooks[0].listener(PAYLOAD, NEXT);
|
|
106
|
-
assert.equal(first.messages.length, 1, 'the first host injects the receipt');
|
|
107
|
-
assert.match(first.messages[0].id, /^surgery-receipt-/);
|
|
108
|
-
assert.equal(readSurgeryState(root).state.receipt.fallbackInjected, true);
|
|
109
|
-
|
|
110
|
-
const second = await hostB.hooks[0].listener(PAYLOAD, NEXT);
|
|
111
|
-
assert.equal(second.messages.length, 0, 'the second host must not append a duplicate');
|
|
112
|
-
assert.equal(hostB.hooks.length, 0, 'the losing hook unregisters itself');
|
|
113
|
-
assert.match(auditText(root), /surgery-fallback-inject\tstate\tskipped-already/);
|
|
114
|
-
assert.equal(
|
|
115
|
-
fs.existsSync(path.join(root, '.longlive', 'surgery-fallback-inject.lock')),
|
|
116
|
-
false,
|
|
117
|
-
'the skipped-already path must release the claim too',
|
|
118
|
-
);
|
|
119
|
-
} finally {
|
|
120
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('R4-01: a receipt written while the hook was registered is caught by the second state read', async () => {
|
|
125
|
-
const root = makeRoot();
|
|
126
|
-
try {
|
|
127
|
-
writeState(root);
|
|
128
|
-
const host = registerHost(root);
|
|
129
|
-
// The startup check ran when receipt was null; the real receipt path (or
|
|
130
|
-
// another host) finishes before this host reaches step 1.
|
|
131
|
-
writeState(root, {
|
|
132
|
-
written: true,
|
|
133
|
-
eventId: 'surgery-receipt-early',
|
|
134
|
-
seq: 12,
|
|
135
|
-
at: '2026-11-04T06:40:00.000Z',
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const decision = await host.hooks[0].listener(PAYLOAD, NEXT);
|
|
139
|
-
assert.equal(decision.messages.length, 0, 'an already-written receipt is never injected again');
|
|
140
|
-
assert.equal(host.hooks.length, 0);
|
|
141
|
-
assert.match(auditText(root), /surgery-fallback-inject\tstate\tskipped-already/);
|
|
142
|
-
// The real receipt marker is untouched.
|
|
143
|
-
assert.equal(readSurgeryState(root).state.receipt.written, true);
|
|
144
|
-
} finally {
|
|
145
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
// R4-02 — a legal session id is printed in full
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
function reportFor(sessionId) {
|
|
154
|
-
return {
|
|
155
|
-
classification: CLASSIFICATIONS.APPLIED_VERIFIED,
|
|
156
|
-
reasonCode: RECOVERY_REASON.OK_VERIFIED,
|
|
157
|
-
statePhase: 'restart',
|
|
158
|
-
sessionId,
|
|
159
|
-
checks: {
|
|
160
|
-
phase: 'restart',
|
|
161
|
-
sessionId,
|
|
162
|
-
sessionPath: 'C:\\sessions\\scope\\fixture\\session.v4.jsonl.zstd',
|
|
163
|
-
sessionBytes: 1000,
|
|
164
|
-
beforeBytes: 4000,
|
|
165
|
-
sessionEvents: 10,
|
|
166
|
-
beforeEvents: 40,
|
|
167
|
-
sessionMessages: 8,
|
|
168
|
-
beforeMessages: 20,
|
|
169
|
-
shaMatch: true,
|
|
170
|
-
expectedSha256: 'a'.repeat(64),
|
|
171
|
-
backupPath: 'C:\\backups\\fixture.pre-surgery-1',
|
|
172
|
-
backupExists: true,
|
|
173
|
-
},
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
test('R4-02: a legal 36-character session id appears in the receipt in full', () => {
|
|
178
|
-
const id = '0f8fad5b-d9cb-469f-a165-70867728950e';
|
|
179
|
-
assert.equal(displaySessionId(id), id);
|
|
180
|
-
const text = receiptText(reportFor(id), { appendingReceipt: true, seq: 9 });
|
|
181
|
-
const idLine = text.split(/\r?\n/).find((line) => line.startsWith('会话 id:'));
|
|
182
|
-
assert.equal(idLine, `会话 id:${id}`, text);
|
|
183
|
-
assert.ok(!/leak-root|C:\\/.test(text), text);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test('R4-02: an illegal id is still sanitised and shortened, and never leaks a path', () => {
|
|
187
|
-
const tampered = '..\\..\\C:\\leak-root\\profiles\\Administrator';
|
|
188
|
-
const text = receiptText(reportFor(tampered), { appendingReceipt: true, seq: 9 });
|
|
189
|
-
assert.match(text, /会话 id:\?{6}c\?/, text);
|
|
190
|
-
assert.ok(!text.includes('leak-root'), text);
|
|
191
|
-
assert.ok(!text.includes('..\\'), text);
|
|
192
|
-
assert.ok(!text.includes('C:'), text);
|
|
193
|
-
// The whitelist still governs the helper; only legal values get the full form.
|
|
194
|
-
assert.equal(displaySessionId('ABCD-1234'), 'abcd-123');
|
|
195
|
-
assert.equal(displaySessionId('d2-fixture'), 'd2-fixture');
|
|
196
|
-
assert.equal(displaySessionId(''), null);
|
|
197
|
-
});
|
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
// rc.6 — R4-03 Chinese-environment regressions.
|
|
2
|
-
//
|
|
3
|
-
// The VM drill (drill/run-rc6-drill.mjs) exercises the real PowerShell 5.1
|
|
4
|
-
// executor end to end. These cases keep the invariants in the ordinary suite:
|
|
5
|
-
// every node capture goes through UTF-8 decoders, an exit-0-but-unparseable CLI
|
|
6
|
-
// result is classified `applied-unparsed` instead of `failed`, and a receipt
|
|
7
|
-
// written for a Chinese session round-trips byte-for-byte into the transcript.
|
|
8
|
-
//
|
|
9
|
-
// Nothing here touches a real session, the host, the real memory root, or the
|
|
10
|
-
// network.
|
|
11
|
-
|
|
12
|
-
import assert from 'node:assert/strict';
|
|
13
|
-
import { spawnSync } from 'node:child_process';
|
|
14
|
-
import fs from 'node:fs';
|
|
15
|
-
import os from 'node:os';
|
|
16
|
-
import path from 'node:path';
|
|
17
|
-
import test from 'node:test';
|
|
18
|
-
import { fileURLToPath } from 'node:url';
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
MESSAGE_TYPES,
|
|
22
|
-
encodeSession,
|
|
23
|
-
foldSurface,
|
|
24
|
-
parseSession,
|
|
25
|
-
sha256,
|
|
26
|
-
} from '../src/codec.js';
|
|
27
|
-
import {
|
|
28
|
-
CLASSIFICATIONS,
|
|
29
|
-
RECOVERY_REASON,
|
|
30
|
-
buildRecoveryReport,
|
|
31
|
-
displaySessionId,
|
|
32
|
-
readSurgeryState,
|
|
33
|
-
receiptText,
|
|
34
|
-
surgeryStatePath,
|
|
35
|
-
writeSurgeryReceipt,
|
|
36
|
-
} from '../src/surgery-recovery.js';
|
|
37
|
-
import { sanitizeSessionId } from '../src/surgery-recovery.js';
|
|
38
|
-
|
|
39
|
-
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
40
|
-
const PS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
|
|
41
|
-
const NODE = process.execPath;
|
|
42
|
-
const NOW = new Date('2026-11-04T06:30:00.000Z');
|
|
43
|
-
const ZH_ID = 'zh-session-0f8fad5b-d9cb-469f-a165-70867728950e';
|
|
44
|
-
const ZH_FIRST = '好的,今天做听觉训练';
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Fixtures
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
function makeRoot(tag = 'longlive-rc6-') {
|
|
51
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), tag));
|
|
52
|
-
const sessionDir = path.join(root, 'sessions', 'scope-fixtures', ZH_ID);
|
|
53
|
-
fs.mkdirSync(sessionDir, { recursive: true });
|
|
54
|
-
return { root, sessionDir, sessionPath: path.join(sessionDir, 'session.v4.jsonl.zstd') };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function buildSession({ turns = 20, pad = 60, id = ZH_ID, firstText = ZH_FIRST } = {}) {
|
|
58
|
-
const events = [];
|
|
59
|
-
const base = 1_760_000_000_000;
|
|
60
|
-
const push = (event) => {
|
|
61
|
-
event.seq = events.length;
|
|
62
|
-
event.time = base + events.length;
|
|
63
|
-
events.push(event);
|
|
64
|
-
};
|
|
65
|
-
push({
|
|
66
|
-
type: 'system/message',
|
|
67
|
-
surfaceOp: 'append',
|
|
68
|
-
data: {
|
|
69
|
-
message: {
|
|
70
|
-
role: 'system',
|
|
71
|
-
id: 'system-1',
|
|
72
|
-
source: { kind: 'system-prompt' },
|
|
73
|
-
content: [{ type: 'text', text: `system ${'s'.repeat(pad)}` }],
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
for (let t = 1; t <= turns; t += 1) {
|
|
78
|
-
push({ type: 'turn/start', data: { turn: t } });
|
|
79
|
-
push({ type: 'step/start', data: { turn: t, step: 1 } });
|
|
80
|
-
push({
|
|
81
|
-
type: 'user/message',
|
|
82
|
-
surfaceOp: 'append',
|
|
83
|
-
data: {
|
|
84
|
-
turn: t,
|
|
85
|
-
step: 1,
|
|
86
|
-
role: 'user',
|
|
87
|
-
id: `user-${t}`,
|
|
88
|
-
content: [{ type: 'text', text: t === 1 ? firstText : `第 ${t} 轮问题 ${'问'.repeat(pad)}` }],
|
|
89
|
-
source: { kind: 'user', clientTimeZone: 'Asia/Shanghai' },
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
push({
|
|
93
|
-
type: 'assistant/message',
|
|
94
|
-
surfaceOp: 'append',
|
|
95
|
-
data: {
|
|
96
|
-
turn: t,
|
|
97
|
-
step: 1,
|
|
98
|
-
message: {
|
|
99
|
-
role: 'assistant',
|
|
100
|
-
id: `assistant-${t}`,
|
|
101
|
-
content: [{ type: 'text', text: `第 ${t} 轮回答 ${'答'.repeat(pad)}` }],
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
push({ type: 'step/end', data: { turn: t, step: 1 } });
|
|
106
|
-
push({ type: 'turn/end', data: { turn: t, reason: { kind: 'completed' } } });
|
|
107
|
-
}
|
|
108
|
-
const header = {
|
|
109
|
-
type: 'session',
|
|
110
|
-
version: 4,
|
|
111
|
-
id,
|
|
112
|
-
createdAt: '2026-11-04T00:00:00.000Z',
|
|
113
|
-
cwd: 'C:\\fixtures',
|
|
114
|
-
isSeeded: false,
|
|
115
|
-
delegationDepth: 0,
|
|
116
|
-
};
|
|
117
|
-
return encodeSession(header, events);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function snapshot(buf) {
|
|
121
|
-
const parsed = parseSession(buf);
|
|
122
|
-
return {
|
|
123
|
-
bytes: buf.length,
|
|
124
|
-
sha256: sha256(buf),
|
|
125
|
-
events: parsed.events.length,
|
|
126
|
-
messages: foldSurface(parsed.events).filter((node) => MESSAGE_TYPES.has(node.type)).length,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function writeSession(file, buf) {
|
|
131
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
132
|
-
fs.writeFileSync(file, buf);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function writeState(root, state) {
|
|
136
|
-
fs.mkdirSync(path.join(root, '.longlive'), { recursive: true });
|
|
137
|
-
fs.writeFileSync(surgeryStatePath(root), `${JSON.stringify(state, null, 2)}\n`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function stateBase(overrides = {}) {
|
|
141
|
-
return {
|
|
142
|
-
version: 1,
|
|
143
|
-
kind: 'longlive-surgery',
|
|
144
|
-
surgeryId: 'surgery-20261104-183000-001',
|
|
145
|
-
phase: 'cut',
|
|
146
|
-
startedAt: '2026-11-04T06:28:00.000Z',
|
|
147
|
-
updatedAt: '2026-11-04T06:29:00.000Z',
|
|
148
|
-
pid: 12345,
|
|
149
|
-
sessionPath: null,
|
|
150
|
-
sessionId: ZH_ID,
|
|
151
|
-
memoryRoot: null,
|
|
152
|
-
appPath: 'C:\\Program Files\\DSH\\DSH.exe',
|
|
153
|
-
keepTurns: 6,
|
|
154
|
-
keepTokens: 40000,
|
|
155
|
-
startDelaySeconds: 0,
|
|
156
|
-
pluginVersion: '0.9.0-rc.6',
|
|
157
|
-
hostVersion: '0.9.0',
|
|
158
|
-
dryRun: false,
|
|
159
|
-
before: null,
|
|
160
|
-
backup: null,
|
|
161
|
-
cut: null,
|
|
162
|
-
after: null,
|
|
163
|
-
verify: null,
|
|
164
|
-
restart: null,
|
|
165
|
-
done: null,
|
|
166
|
-
error: null,
|
|
167
|
-
history: [{ phase: 'prepare', at: '2026-11-04T06:28:00.000Z' }],
|
|
168
|
-
...overrides,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
// 1. Every PowerShell -> node capture must be UTF-8 (R4-03).
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
|
|
176
|
-
test('R4-03: 三个 PowerShell 执行器都用 UTF-8 解码 node 输出(静态审计)', () => {
|
|
177
|
-
const files = [
|
|
178
|
-
path.join(PACKAGE_ROOT, 'scripts', 'unattended-surgery.ps1'),
|
|
179
|
-
path.join(PACKAGE_ROOT, 'scripts', 'auto-truncate-run.ps1'),
|
|
180
|
-
path.join(PACKAGE_ROOT, 'scripts', 'lib', 'auto-truncate-run.ps1'),
|
|
181
|
-
];
|
|
182
|
-
for (const file of files) {
|
|
183
|
-
const buf = fs.readFileSync(file);
|
|
184
|
-
assert.notDeepEqual([...buf.subarray(0, 3)], [0xef, 0xbb, 0xbf], `${file} must not start with a BOM`);
|
|
185
|
-
const text = buf.toString('utf8');
|
|
186
|
-
const code = text
|
|
187
|
-
.replace(/<#[\s\S]*?#>/g, '')
|
|
188
|
-
.split(/\r?\n/)
|
|
189
|
-
.map((line) => line.replace(/\s+#.*$/, ''))
|
|
190
|
-
.join('\n');
|
|
191
|
-
assert.match(text, /ProcessStartInfo/, `${path.basename(file)} must use an explicit process start`);
|
|
192
|
-
assert.match(text, /StandardOutputEncoding/, `${path.basename(file)} must set stdout decoding`);
|
|
193
|
-
assert.match(text, /StandardErrorEncoding/, `${path.basename(file)} must set stderr decoding`);
|
|
194
|
-
assert.match(text, /UTF8Encoding/, `${path.basename(file)} must name UTF-8 explicitly`);
|
|
195
|
-
assert.doesNotMatch(code, /&\s*\$\w+\s+@\w+\s+2>&1/, `${path.basename(file)} must not use the GBK capture idiom`);
|
|
196
|
-
assert.doesNotMatch(code, /Substring\(\s*0\s*,\s*8\s*\)/, `${path.basename(file)} must not hard-truncate ids`);
|
|
197
|
-
}
|
|
198
|
-
const surgery = fs.readFileSync(files[0], 'utf8');
|
|
199
|
-
assert.match(surgery, /applied-unparsed/);
|
|
200
|
-
assert.match(surgery, /needs-attention/);
|
|
201
|
-
assert.match(surgery, /E_CUT_UNVERIFIED/);
|
|
202
|
-
assert.match(surgery, /Write-RawCapture/);
|
|
203
|
-
assert.match(surgery, /Test-CutLanded/);
|
|
204
|
-
const auto = fs.readFileSync(files[1], 'utf8');
|
|
205
|
-
assert.match(auto, /Test-CutApplied/);
|
|
206
|
-
assert.match(auto, /applied-unparsed/);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// ---------------------------------------------------------------------------
|
|
210
|
-
// 2. Ids are shown in full (R4-01 stays fixed).
|
|
211
|
-
// ---------------------------------------------------------------------------
|
|
212
|
-
|
|
213
|
-
test('R4-03: 合法会话 id 完整显示,非法 id 才清洗并缩短', () => {
|
|
214
|
-
assert.equal(displaySessionId(ZH_ID), ZH_ID);
|
|
215
|
-
const dirty = '../..\\tok en\\\\secret-session-id';
|
|
216
|
-
const shown = displaySessionId(dirty);
|
|
217
|
-
assert.ok(shown.length <= 8, `dirty id must be shortened, got ${shown.length}`);
|
|
218
|
-
assert.doesNotMatch(shown, /[\\/]/);
|
|
219
|
-
assert.equal(sanitizeSessionId(ZH_ID), ZH_ID.slice(0, 8));
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// ---------------------------------------------------------------------------
|
|
223
|
-
// 3. applied-unparsed is a success class, not a failure.
|
|
224
|
-
// ---------------------------------------------------------------------------
|
|
225
|
-
|
|
226
|
-
test('R4-03: applied-unparsed 归类为成功,回执说明编码异常', () => {
|
|
227
|
-
const { root, sessionPath } = makeRoot();
|
|
228
|
-
const beforeBuf = buildSession({ turns: 20 });
|
|
229
|
-
const afterBuf = buildSession({ turns: 6 });
|
|
230
|
-
writeSession(sessionPath, afterBuf);
|
|
231
|
-
const backupBuf = beforeBuf;
|
|
232
|
-
const backupPath = `${sessionPath}.pre-surgery-20261104-183000`;
|
|
233
|
-
fs.writeFileSync(backupPath, backupBuf);
|
|
234
|
-
writeState(root, stateBase({
|
|
235
|
-
phase: 'done',
|
|
236
|
-
sessionPath,
|
|
237
|
-
memoryRoot: root,
|
|
238
|
-
before: snapshot(beforeBuf),
|
|
239
|
-
after: snapshot(afterBuf),
|
|
240
|
-
backup: { path: backupPath, bytes: backupBuf.length, sha256: sha256(backupBuf) },
|
|
241
|
-
cut: {
|
|
242
|
-
outcome: 'applied-unparsed',
|
|
243
|
-
encoding: { unparsed: true, capturePath: null },
|
|
244
|
-
verify: { ok: true, method: 'independent-readback-unparsed' },
|
|
245
|
-
},
|
|
246
|
-
verify: { ok: true, method: 'independent-readback-unparsed' },
|
|
247
|
-
done: { outcome: 'applied-unparsed' },
|
|
248
|
-
receipt: { written: true, text: 'placeholder' },
|
|
249
|
-
}));
|
|
250
|
-
|
|
251
|
-
const report = buildRecoveryReport({ memoryRoot: root, now: NOW, loadedVersion: '0.9.0-rc.6' });
|
|
252
|
-
assert.equal(report.ok, true);
|
|
253
|
-
assert.equal(report.classification, CLASSIFICATIONS.APPLIED_UNPARSED);
|
|
254
|
-
assert.equal(report.reasonCode, RECOVERY_REASON.OK_UNPARSED);
|
|
255
|
-
assert.equal(report.checks.cutOutcome, 'applied-unparsed');
|
|
256
|
-
|
|
257
|
-
const text = receiptText(report);
|
|
258
|
-
assert.match(text, /成功/);
|
|
259
|
-
assert.match(text, /编码/);
|
|
260
|
-
assert.match(text, new RegExp(`会话 id:${ZH_ID}`));
|
|
261
|
-
assert.doesNotMatch(text, /\uFFFD/);
|
|
262
|
-
assert.doesNotMatch(text, /[A-Za-z]:\\/);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
// 4. needs-attention is a distinct state; the reason is not "failed".
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
|
|
269
|
-
test('R4-03: needs-attention 不冒充 failed,recommendation 不泄露绝对路径', () => {
|
|
270
|
-
const { root, sessionPath } = makeRoot();
|
|
271
|
-
const buf = buildSession({ turns: 20 });
|
|
272
|
-
writeSession(sessionPath, buf);
|
|
273
|
-
writeState(root, stateBase({
|
|
274
|
-
phase: 'needs-attention',
|
|
275
|
-
sessionPath,
|
|
276
|
-
memoryRoot: root,
|
|
277
|
-
before: snapshot(buf),
|
|
278
|
-
cut: {
|
|
279
|
-
outcome: 'unverified',
|
|
280
|
-
verification: { sessionReadable: true, shaChanged: false, eventsDecreased: false },
|
|
281
|
-
},
|
|
282
|
-
needsAttention: {
|
|
283
|
-
at: '2026-11-04T06:29:00.000Z',
|
|
284
|
-
code: 'E_CUT_UNVERIFIED',
|
|
285
|
-
phase: 'verify',
|
|
286
|
-
message: 'truncate-cli exited 0 but verification did not confirm the cut',
|
|
287
|
-
checks: { shaChanged: false, eventsDecreased: false },
|
|
288
|
-
},
|
|
289
|
-
after: null,
|
|
290
|
-
receipt: { written: false, at: '2026-11-04T06:29:00.000Z' },
|
|
291
|
-
}));
|
|
292
|
-
|
|
293
|
-
const report = buildRecoveryReport({ memoryRoot: root, now: NOW, loadedVersion: '0.9.0-rc.6' });
|
|
294
|
-
assert.equal(report.ok, false);
|
|
295
|
-
assert.equal(report.classification, CLASSIFICATIONS.NEEDS_ATTENTION);
|
|
296
|
-
assert.equal(report.reasonCode, RECOVERY_REASON.CUT_UNVERIFIED);
|
|
297
|
-
assert.equal(report.checks.needsAttentionCode, 'E_CUT_UNVERIFIED');
|
|
298
|
-
assert.doesNotMatch(report.recommendation, /[A-Za-z]:\\/);
|
|
299
|
-
const actions = Array.isArray(report.actions) ? report.actions.join(' ') : '';
|
|
300
|
-
assert.doesNotMatch(actions, /[A-Za-z]:\\/);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// ---------------------------------------------------------------------------
|
|
304
|
-
// 5. Receipt text for a Chinese session: UTF-8 consistent + idempotent.
|
|
305
|
-
// ---------------------------------------------------------------------------
|
|
306
|
-
|
|
307
|
-
test('R4-03: 中文会话回执 UTF-8 一致、id 完整、二次写入 0 条新事件', () => {
|
|
308
|
-
const { root, sessionPath } = makeRoot();
|
|
309
|
-
const beforeBuf = buildSession({ turns: 20 });
|
|
310
|
-
const afterBuf = buildSession({ turns: 6 });
|
|
311
|
-
writeSession(sessionPath, afterBuf);
|
|
312
|
-
const backupPath = `${sessionPath}.pre-surgery-20261104-183000`;
|
|
313
|
-
fs.writeFileSync(backupPath, beforeBuf);
|
|
314
|
-
writeState(root, stateBase({
|
|
315
|
-
phase: 'done',
|
|
316
|
-
sessionPath,
|
|
317
|
-
memoryRoot: root,
|
|
318
|
-
before: snapshot(beforeBuf),
|
|
319
|
-
after: snapshot(afterBuf),
|
|
320
|
-
backup: { path: backupPath, bytes: beforeBuf.length, sha256: sha256(beforeBuf) },
|
|
321
|
-
cut: {
|
|
322
|
-
outcome: 'applied-unparsed',
|
|
323
|
-
verify: { ok: true, method: 'independent-readback-unparsed' },
|
|
324
|
-
},
|
|
325
|
-
verify: { ok: true, method: 'independent-readback-unparsed' },
|
|
326
|
-
done: { outcome: 'applied-unparsed' },
|
|
327
|
-
}));
|
|
328
|
-
|
|
329
|
-
const first = writeSurgeryReceipt({ sessionPath, memoryRoot: root, now: NOW });
|
|
330
|
-
assert.equal(first.ok, true, JSON.stringify(first));
|
|
331
|
-
assert.equal(first.receipt.written, true);
|
|
332
|
-
assert.match(first.receipt.text, /【手术回执】/);
|
|
333
|
-
assert.match(first.receipt.text, new RegExp(`会话 id:${ZH_ID}`));
|
|
334
|
-
assert.match(first.receipt.text, /编码/);
|
|
335
|
-
assert.doesNotMatch(first.receipt.text, /\uFFFD/);
|
|
336
|
-
|
|
337
|
-
const parsed = parseSession(fs.readFileSync(sessionPath));
|
|
338
|
-
const receipts = parsed.events.filter((event) => String(event.data?.id ?? '').startsWith('surgery-receipt-'));
|
|
339
|
-
assert.equal(receipts.length, 1);
|
|
340
|
-
const eventText = receipts[0].data.content.map((part) => part.text).join('');
|
|
341
|
-
assert.equal(eventText, first.receipt.text);
|
|
342
|
-
assert.equal(eventText, readSurgeryState(root).state.receipt.text);
|
|
343
|
-
assert.ok(eventText.includes(ZH_FIRST) === false); // receipt text is not the carried first message
|
|
344
|
-
|
|
345
|
-
const second = writeSurgeryReceipt({ sessionPath, memoryRoot: root, now: NOW });
|
|
346
|
-
assert.equal(second.ok, true);
|
|
347
|
-
assert.equal(second.alreadyWritten, true);
|
|
348
|
-
const again = parseSession(fs.readFileSync(sessionPath));
|
|
349
|
-
assert.equal(again.events.length, parsed.events.length);
|
|
350
|
-
assert.equal(again.events.filter((event) => String(event.data?.id ?? '').startsWith('surgery-receipt-')).length, 1);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// ---------------------------------------------------------------------------
|
|
354
|
-
// 6. Real executor dry run under a GBK console: Chinese path survives.
|
|
355
|
-
// ---------------------------------------------------------------------------
|
|
356
|
-
|
|
357
|
-
test('R4-03: GBK 控制台下真实执行器读回中文路径(dry run)', { skip: process.platform !== 'win32' }, () => {
|
|
358
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'longlive-rc6-gbk-'));
|
|
359
|
-
const sessionDir = path.join(root, '会话 备份', 'sessions', 'scope', ZH_ID);
|
|
360
|
-
fs.mkdirSync(sessionDir, { recursive: true });
|
|
361
|
-
const sessionPath = path.join(sessionDir, 'session.v4.jsonl.zstd');
|
|
362
|
-
fs.writeFileSync(sessionPath, buildSession({ turns: 20 }));
|
|
363
|
-
const memoryRoot = path.join(root, '记忆 根');
|
|
364
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
365
|
-
|
|
366
|
-
const launcher = path.join(root, 'launch-gbk.ps1');
|
|
367
|
-
// PS 5.1 reads a script without BOM as ANSI, which would mangle the Chinese
|
|
368
|
-
// fixture path inside the launcher itself. The BOM makes the launcher UTF-8
|
|
369
|
-
// for the parser; the package scripts stay UTF-8 without BOM and ASCII-only.
|
|
370
|
-
fs.writeFileSync(launcher, '\uFEFF' + [
|
|
371
|
-
"$ErrorActionPreference = 'Stop'",
|
|
372
|
-
'try { [Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding(936) } catch { }',
|
|
373
|
-
'try { $OutputEncoding = [System.Text.Encoding]::GetEncoding(936) } catch { }',
|
|
374
|
-
`& '${path.join(PACKAGE_ROOT, 'scripts', 'unattended-surgery.ps1')}' -SessionPath '${sessionPath}' -MemoryRoot '${memoryRoot}' -NodeExe '${NODE}' -DryRun`,
|
|
375
|
-
'exit $LASTEXITCODE',
|
|
376
|
-
'',
|
|
377
|
-
].join('\r\n'), 'utf8');
|
|
378
|
-
|
|
379
|
-
const proc = spawnSync(PS, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', launcher], {
|
|
380
|
-
encoding: 'utf8',
|
|
381
|
-
env: { ...process.env, DSH_HOME: root },
|
|
382
|
-
timeout: 120000,
|
|
383
|
-
});
|
|
384
|
-
assert.equal(proc.status, 0, `executor failed: ${proc.stderr}\n${proc.stdout}`);
|
|
385
|
-
|
|
386
|
-
const statePath = path.join(memoryRoot, '.longlive', 'surgery-state.json');
|
|
387
|
-
const raw = fs.readFileSync(statePath, 'utf8');
|
|
388
|
-
assert.doesNotMatch(raw, /\uFFFD/);
|
|
389
|
-
const state = JSON.parse(raw);
|
|
390
|
-
assert.equal(state.phase, 'dryrun');
|
|
391
|
-
assert.equal(state.sessionPath, sessionPath);
|
|
392
|
-
assert.equal(state.sessionId, ZH_ID);
|
|
393
|
-
assert.equal(state.dryRun, true);
|
|
394
|
-
});
|