@jjh32456/dsh-longlive 1.6.8 → 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 +69 -1539
- package/LICENSE +1 -1
- package/QUICKSTART.md +33 -12
- package/README.en.md +17 -7
- package/README.md +29 -10
- package/SPEC.md +5 -5
- package/cordis.patch.yml +17 -12
- package/docs/ANSWER-GUARD.md +92 -0
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-RECALL-WIRING.md +1 -1
- package/docs/AUTO-RECALL.md +15 -9
- package/docs/CONFIG.md +143 -37
- package/docs/ERRORS.md +17 -5
- package/docs/FAQ.md +7 -7
- package/docs/FEATURES.md +66 -29
- 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 +3 -2
- package/docs/RESIDENT-WATCHER.md +1 -1
- package/docs/SESSION-WATCH.md +6 -6
- package/docs/TROUBLESHOOTING.md +4 -4
- package/package.json +42 -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 +129 -18
- package/src/activity/model.js +11 -1
- package/src/activity/store.js +22 -3
- 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 +145 -48
- package/src/deps.js +6 -4
- package/src/features.js +158 -8
- 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/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 +1 -1
- 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/config.mjs +5 -5
- package/src/router/service/context.mjs +1 -1
- package/src/router/service/queue.mjs +1 -1
- 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/activity-realtime.test.mjs +0 -959
- package/test/activity-stream.test.mjs +0 -512
- package/test/auto-recall.test.mjs +0 -474
- package/test/auto-truncate.test.mjs +0 -931
- package/test/autobind.test.mjs +0 -399
- 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 -210
- 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 -585
- package/test/filter-shadow.test.mjs +0 -170
- package/test/fixtures/activity-append-child.mjs +0 -29
- 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 -1023
- 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 -184
- 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 -1076
- 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 -315
- package/test/run.mjs +0 -2469
- package/test/safety-defaults.test.mjs +0 -181
- package/test/seeded-load.test.mjs +0 -436
- package/test/session-original.test.mjs +0 -338
- package/test/state-truth.test.mjs +0 -937
- 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
- package/test/version-consistency.test.mjs +0 -286
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
// WP4b — apply nine-step protocol, lease/WAL ordering, failure injection
|
|
2
|
-
// (K4, K7, K8, K19, K25) and lease bounds (K26).
|
|
3
|
-
|
|
4
|
-
import assert from 'node:assert/strict';
|
|
5
|
-
import crypto from 'node:crypto';
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import test from 'node:test';
|
|
9
|
-
|
|
10
|
-
import { clampLeaseTtl, computeMaxHoldMs, createWriterLease } from '../../src/router/service/lease.mjs';
|
|
11
|
-
import { makeService, readJson, tempRoot, writeMemory } from './helpers.mjs';
|
|
12
|
-
|
|
13
|
-
function sha256Buffer(buffer) {
|
|
14
|
-
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function setup(prefix, config = {}, deps = {}) {
|
|
18
|
-
const temp = tempRoot(prefix);
|
|
19
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
20
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
21
|
-
writeMemory(memoryRoot);
|
|
22
|
-
const vaultDir = path.join(temp, 'vault');
|
|
23
|
-
const env = makeService({
|
|
24
|
-
temp,
|
|
25
|
-
memoryRoot,
|
|
26
|
-
vaultDir,
|
|
27
|
-
config: { proposalEnabled: true, applyEnabled: true, tickMs: 60000, ...config },
|
|
28
|
-
deps: { installTimers: false, installListener: false, ...deps },
|
|
29
|
-
});
|
|
30
|
-
const started = await env.service.start();
|
|
31
|
-
assert.equal(started.ok, true, `service start failed: ${JSON.stringify(started)}`);
|
|
32
|
-
return { ...env, temp, memoryRoot, vaultDir, engine: env.service.proposalEngine };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function proposeJournal(env, content, target = 'journal/2026-11.md') {
|
|
36
|
-
return env.service.proposeProposal({
|
|
37
|
-
op: 'journal.append',
|
|
38
|
-
target,
|
|
39
|
-
title: 'apply test',
|
|
40
|
-
content,
|
|
41
|
-
}, { caller: { kind: 'root', sessionId: 'root-session' } });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function approve(env, id) {
|
|
45
|
-
const result = await env.service.requestApply({
|
|
46
|
-
mode: 'approve',
|
|
47
|
-
id,
|
|
48
|
-
confirm: id.slice(-6),
|
|
49
|
-
}, { caller: { kind: 'root', sessionId: 'root-session' } });
|
|
50
|
-
assert.equal(result.ok, true, `approve failed: ${JSON.stringify(result)}`);
|
|
51
|
-
return result;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function apply(env, id) {
|
|
55
|
-
return env.service.requestApply({
|
|
56
|
-
mode: 'apply',
|
|
57
|
-
id,
|
|
58
|
-
confirm: id.slice(-6),
|
|
59
|
-
}, { caller: { kind: 'root', sessionId: 'root-session' } });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function walEvents(vaultDir) {
|
|
63
|
-
const file = path.join(vaultDir, 'proposals', 'wal.jsonl');
|
|
64
|
-
if (!fs.existsSync(file)) return [];
|
|
65
|
-
return fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function auditRows(vaultDir) {
|
|
69
|
-
const file = path.join(vaultDir, 'audit.jsonl');
|
|
70
|
-
if (!fs.existsSync(file)) return [];
|
|
71
|
-
return fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
test('K1/K4: apply writes canonical only inside the nine-step window and records WAL order', async () => {
|
|
75
|
-
const env = await setup('wp4b-happy-');
|
|
76
|
-
try {
|
|
77
|
-
const proposed = await proposeJournal(env, 'hello apply');
|
|
78
|
-
assert.equal(proposed.ok, true);
|
|
79
|
-
const id = proposed.id;
|
|
80
|
-
const targetFile = path.join(env.memoryRoot, 'journal', '2026-11.md');
|
|
81
|
-
await approve(env, id);
|
|
82
|
-
assert.equal(fs.existsSync(targetFile), false, 'approve never writes canonical');
|
|
83
|
-
|
|
84
|
-
const result = await apply(env, id);
|
|
85
|
-
assert.equal(result.ok, true, JSON.stringify(result));
|
|
86
|
-
assert.equal(result.state, 'done');
|
|
87
|
-
const proposal = readJson(path.join(env.vaultDir, 'proposals', `${id}.json`));
|
|
88
|
-
assert.equal(proposal.status, 'done');
|
|
89
|
-
assert.equal(sha256Buffer(fs.readFileSync(targetFile)), proposal.result.afterSha);
|
|
90
|
-
assert.ok(fs.existsSync(path.join(env.vaultDir, proposal.result.snapshot)), 'snapshot kept');
|
|
91
|
-
assert.ok(fs.existsSync(path.join(env.vaultDir, 'snapshots', 'manifest.jsonl')));
|
|
92
|
-
|
|
93
|
-
const events = walEvents(env.vaultDir).map((line) => line.ev);
|
|
94
|
-
const begin = events.indexOf('apply.begin');
|
|
95
|
-
const done = events.indexOf('apply.done');
|
|
96
|
-
assert.ok(begin >= 0 && done > begin, 'WAL begin precedes done');
|
|
97
|
-
assert.equal(fs.existsSync(path.join(env.vaultDir, 'locks', 'writer.lease.json')), false, 'lease released');
|
|
98
|
-
assert.ok(auditRows(env.vaultDir).some((row) => row.kind === 'apply.done'));
|
|
99
|
-
const queue = readJson(path.join(env.vaultDir, 'proposals', 'queue.json'));
|
|
100
|
-
assert.equal(queue.entries.length, 0);
|
|
101
|
-
} finally {
|
|
102
|
-
env.cleanup();
|
|
103
|
-
fs.rmSync(env.temp, { recursive: true, force: true });
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('K4: a target changed after approve is a zero-write conflict before apply.begin', async () => {
|
|
108
|
-
const env = await setup('wp4b-moved-');
|
|
109
|
-
try {
|
|
110
|
-
const stateFile = path.join(env.memoryRoot, 'STATE.md');
|
|
111
|
-
const before = fs.readFileSync(stateFile);
|
|
112
|
-
const proposed = await env.service.proposeProposal({
|
|
113
|
-
op: 'state.section',
|
|
114
|
-
target: 'STATE.md',
|
|
115
|
-
title: '我在哪',
|
|
116
|
-
content: '新位置:测试台',
|
|
117
|
-
baseSha: sha256Buffer(before),
|
|
118
|
-
}, { caller: { kind: 'root', sessionId: 'root-session' } });
|
|
119
|
-
assert.equal(proposed.ok, true, JSON.stringify(proposed));
|
|
120
|
-
await approve(env, proposed.id);
|
|
121
|
-
const ownerBytes = Buffer.from(before.toString('utf8').replace('我在哪', '我在哪(第三方改)'));
|
|
122
|
-
fs.writeFileSync(stateFile, ownerBytes);
|
|
123
|
-
|
|
124
|
-
const result = await apply(env, proposed.id);
|
|
125
|
-
assert.equal(result.ok, false);
|
|
126
|
-
assert.equal(result.code, 'E_BASE_MOVED');
|
|
127
|
-
assert.deepEqual(fs.readFileSync(stateFile), ownerBytes, 'target bytes untouched');
|
|
128
|
-
const proposal = readJson(path.join(env.vaultDir, 'proposals', `${proposed.id}.json`));
|
|
129
|
-
assert.equal(proposal.status, 'conflict');
|
|
130
|
-
assert.equal(walEvents(env.vaultDir).some((line) => line.ev === 'apply.begin'), false, 'no begin before target validation');
|
|
131
|
-
// D-CR17: a validate-time base move is a conflict, never an OWNER_WRITE_RACE.
|
|
132
|
-
assert.equal(env.service.status().warnings.includes('OWNER_WRITE_RACE'), false);
|
|
133
|
-
assert.equal(auditRows(env.vaultDir).some((row) => row.kind === 'warn' && row.code === 'OWNER_WRITE_RACE'), false);
|
|
134
|
-
} finally {
|
|
135
|
-
env.cleanup();
|
|
136
|
-
fs.rmSync(env.temp, { recursive: true, force: true });
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test('K19: owner write after the tmp is a zero-write owner-write-race abort', async () => {
|
|
141
|
-
const env = await setup('wp4b-race-', {}, { testHooks: { forceInjection: true } });
|
|
142
|
-
try {
|
|
143
|
-
const proposed = await proposeJournal(env, 'race body');
|
|
144
|
-
await approve(env, proposed.id);
|
|
145
|
-
const targetFile = path.join(env.memoryRoot, 'journal', '2026-11.md');
|
|
146
|
-
const result = await apply(env, proposed.id);
|
|
147
|
-
assert.equal(result.ok, false);
|
|
148
|
-
assert.equal(result.code, 'E_BASE_MOVED');
|
|
149
|
-
assert.equal(result.reason, 'owner-write-race');
|
|
150
|
-
assert.equal(fs.existsSync(targetFile), false, 'zero canonical writes');
|
|
151
|
-
assert.equal(fs.readdirSync(path.dirname(targetFile)).some((name) => name.includes('.tmp-')), false, 'tmp removed');
|
|
152
|
-
const events = walEvents(env.vaultDir).map((line) => line.ev);
|
|
153
|
-
assert.ok(events.includes('apply.abort'));
|
|
154
|
-
assert.equal(events.includes('apply.done'), false);
|
|
155
|
-
const proposal = readJson(path.join(env.vaultDir, 'proposals', `${proposed.id}.json`));
|
|
156
|
-
assert.equal(proposal.status, 'conflict');
|
|
157
|
-
assert.equal(proposal.conflict.reason, 'owner-write-race');
|
|
158
|
-
assert.ok(auditRows(env.vaultDir).some((row) => row.kind === 'apply.abort' && row.reason === 'owner-write-race'));
|
|
159
|
-
assert.ok(auditRows(env.vaultDir).some((row) => row.kind === 'warn' && row.code === 'OWNER_WRITE_RACE'));
|
|
160
|
-
// D-CR17: the warning also reaches the service status surface.
|
|
161
|
-
assert.equal(env.service.status().warnings.includes('OWNER_WRITE_RACE'), true);
|
|
162
|
-
} finally {
|
|
163
|
-
env.cleanup();
|
|
164
|
-
fs.rmSync(env.temp, { recursive: true, force: true });
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('K8: readback failure leaves the rename result but demands manual restore (no auto rollback)', async () => {
|
|
169
|
-
const env = await setup('wp4b-readback-', {}, { testHooks: { failReadback: true } });
|
|
170
|
-
try {
|
|
171
|
-
const proposed = await proposeJournal(env, 'readback body');
|
|
172
|
-
await approve(env, proposed.id);
|
|
173
|
-
const result = await apply(env, proposed.id);
|
|
174
|
-
assert.equal(result.ok, false);
|
|
175
|
-
assert.equal(result.code, 'E_RESTORE_REQUIRED');
|
|
176
|
-
assert.equal(result.restoreRequired, true);
|
|
177
|
-
const proposal = readJson(path.join(env.vaultDir, 'proposals', `${proposed.id}.json`));
|
|
178
|
-
assert.equal(proposal.status, 'conflict');
|
|
179
|
-
assert.equal(proposal.result.restoreRequired, true);
|
|
180
|
-
assert.ok(fs.existsSync(path.join(env.vaultDir, proposal.result.snapshot)), 'snapshot kept for manual restore');
|
|
181
|
-
const events = walEvents(env.vaultDir).map((line) => line.ev);
|
|
182
|
-
assert.ok(events.includes('apply.conflict'));
|
|
183
|
-
assert.ok(events.includes('restore.required'));
|
|
184
|
-
assert.equal(events.includes('apply.done'), false);
|
|
185
|
-
} finally {
|
|
186
|
-
env.cleanup();
|
|
187
|
-
fs.rmSync(env.temp, { recursive: true, force: true });
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test('K7: snapshot failure aborts with zero canonical writes', async () => {
|
|
192
|
-
const env = await setup('wp4b-snapshot-', {}, { testHooks: { failSnapshot: true } });
|
|
193
|
-
try {
|
|
194
|
-
const proposed = await proposeJournal(env, 'snapshot body');
|
|
195
|
-
await approve(env, proposed.id);
|
|
196
|
-
const targetFile = path.join(env.memoryRoot, 'journal', '2026-11.md');
|
|
197
|
-
const result = await apply(env, proposed.id);
|
|
198
|
-
assert.equal(result.ok, false);
|
|
199
|
-
assert.equal(result.code, 'E_SNAPSHOT_FAILED');
|
|
200
|
-
assert.equal(fs.existsSync(targetFile), false);
|
|
201
|
-
assert.ok(walEvents(env.vaultDir).some((line) => line.ev === 'apply.abort' && line.reason === 'snapshot-failed'));
|
|
202
|
-
assert.ok(auditRows(env.vaultDir).some((row) => row.kind === 'snapshot_failed'));
|
|
203
|
-
} finally {
|
|
204
|
-
env.cleanup();
|
|
205
|
-
fs.rmSync(env.temp, { recursive: true, force: true });
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test('K8: crash after tmp write leaves applying state and recovery reschedules approved', async () => {
|
|
210
|
-
const env = await setup('wp4b-crash-', {}, { testHooks: { failWriteAfter: true } });
|
|
211
|
-
try {
|
|
212
|
-
const proposed = await proposeJournal(env, 'crash body');
|
|
213
|
-
await approve(env, proposed.id);
|
|
214
|
-
const targetFile = path.join(env.memoryRoot, 'journal', '2026-11.md');
|
|
215
|
-
await assert.rejects(() => apply(env, proposed.id), /after-tmp-write|crash/i);
|
|
216
|
-
assert.equal(fs.existsSync(targetFile), false, 'no rename happened');
|
|
217
|
-
let proposal = readJson(path.join(env.vaultDir, 'proposals', `${proposed.id}.json`));
|
|
218
|
-
assert.equal(proposal.status, 'applying');
|
|
219
|
-
assert.ok(walEvents(env.vaultDir).some((line) => line.ev === 'apply.begin'));
|
|
220
|
-
|
|
221
|
-
const recovered = env.engine.applier.recoverApplying();
|
|
222
|
-
assert.equal(recovered.recovered.length, 1);
|
|
223
|
-
proposal = readJson(path.join(env.vaultDir, 'proposals', `${proposed.id}.json`));
|
|
224
|
-
assert.equal(proposal.status, 'approved');
|
|
225
|
-
assert.equal(proposal.queue.attempts, 1);
|
|
226
|
-
assert.equal(fs.existsSync(path.join(env.vaultDir, 'locks', 'writer.lease.json')), false);
|
|
227
|
-
} finally {
|
|
228
|
-
env.cleanup();
|
|
229
|
-
fs.rmSync(env.temp, { recursive: true, force: true });
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test('K8 step 1: a held writer lease returns state=queued, never a busy error or a write', async () => {
|
|
234
|
-
const env = await setup('wp4b-held-');
|
|
235
|
-
try {
|
|
236
|
-
const proposed = await proposeJournal(env, 'held body');
|
|
237
|
-
await approve(env, proposed.id);
|
|
238
|
-
const lockDir = path.join(env.vaultDir, 'locks');
|
|
239
|
-
const other = createWriterLease({
|
|
240
|
-
file: path.join(lockDir, 'writer.lease.json'),
|
|
241
|
-
lockDir,
|
|
242
|
-
ttlMs: 60000,
|
|
243
|
-
renewalsMax: 2,
|
|
244
|
-
holder: { sessionId: 'other-session', runId: 'other-run', pid: process.pid, host: 'local' },
|
|
245
|
-
});
|
|
246
|
-
assert.equal(other.acquire({ ticket: 'q-0001', proposalId: proposed.id }).ok, true);
|
|
247
|
-
|
|
248
|
-
const result = await apply(env, proposed.id);
|
|
249
|
-
assert.equal(result.ok, true);
|
|
250
|
-
assert.equal(result.state, 'queued');
|
|
251
|
-
assert.equal(result.holder, 'other-session');
|
|
252
|
-
assert.equal(fs.existsSync(path.join(env.memoryRoot, 'journal', '2026-11.md')), false);
|
|
253
|
-
assert.equal(walEvents(env.vaultDir).some((line) => line.ev === 'apply.begin'), false);
|
|
254
|
-
assert.equal(readJson(path.join(env.vaultDir, 'proposals', `${proposed.id}.json`)).status, 'approved');
|
|
255
|
-
} finally {
|
|
256
|
-
env.cleanup();
|
|
257
|
-
fs.rmSync(env.temp, { recursive: true, force: true });
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
test('K8 step 2: a proposal revoked after approval is E_BAD_STATE with no apply.begin', async () => {
|
|
262
|
-
const env = await setup('wp4b-revoked-');
|
|
263
|
-
try {
|
|
264
|
-
const proposed = await proposeJournal(env, 'revoked body');
|
|
265
|
-
await approve(env, proposed.id);
|
|
266
|
-
const file = path.join(env.vaultDir, 'proposals', `${proposed.id}.json`);
|
|
267
|
-
const proposal = readJson(file);
|
|
268
|
-
proposal.status = 'rejected';
|
|
269
|
-
fs.writeFileSync(file, JSON.stringify(proposal));
|
|
270
|
-
const result = await apply(env, proposed.id);
|
|
271
|
-
assert.equal(result.ok, false);
|
|
272
|
-
assert.equal(result.code, 'E_BAD_STATE');
|
|
273
|
-
assert.equal(fs.existsSync(path.join(env.memoryRoot, 'journal', '2026-11.md')), false);
|
|
274
|
-
assert.equal(walEvents(env.vaultDir).some((line) => line.ev === 'apply.begin'), false);
|
|
275
|
-
assert.equal(fs.existsSync(path.join(env.vaultDir, 'locks', 'writer.lease.json')), false);
|
|
276
|
-
} finally {
|
|
277
|
-
env.cleanup();
|
|
278
|
-
fs.rmSync(env.temp, { recursive: true, force: true });
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test('K8 step 6: fsync failure aborts before rename with zero canonical writes', async () => {
|
|
283
|
-
const env = await setup('wp4b-fsync-', {}, { testHooks: { failFsync: true } });
|
|
284
|
-
try {
|
|
285
|
-
const proposed = await proposeJournal(env, 'fsync body');
|
|
286
|
-
await approve(env, proposed.id);
|
|
287
|
-
const targetFile = path.join(env.memoryRoot, 'journal', '2026-11.md');
|
|
288
|
-
const result = await apply(env, proposed.id);
|
|
289
|
-
assert.equal(result.ok, false);
|
|
290
|
-
assert.equal(result.code, 'E_IO');
|
|
291
|
-
assert.equal(fs.existsSync(targetFile), false);
|
|
292
|
-
const events = walEvents(env.vaultDir);
|
|
293
|
-
assert.ok(events.some((line) => line.ev === 'apply.abort' && line.reason === 'write-failed'));
|
|
294
|
-
assert.equal(events.some((line) => line.ev === 'apply.done'), false);
|
|
295
|
-
assert.equal(readJson(path.join(env.vaultDir, 'proposals', `${proposed.id}.json`)).status, 'conflict');
|
|
296
|
-
} finally {
|
|
297
|
-
env.cleanup();
|
|
298
|
-
fs.rmSync(env.temp, { recursive: true, force: true });
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
test('production seam: the applier never reads env/config test hooks, only injected deps', async () => {
|
|
303
|
-
const source = fs.readFileSync(new URL('../../src/router/service/apply.mjs', import.meta.url), 'utf8');
|
|
304
|
-
assert.equal(/routerTestHooks|LONGLIVE_ROUTER_TEST|process\.env/u.test(source), false);
|
|
305
|
-
const env = await setup('wp4b-prod-seam-', { routerTestHooks: { failReadback: true } });
|
|
306
|
-
try {
|
|
307
|
-
const proposed = await proposeJournal(env, 'seam body');
|
|
308
|
-
await approve(env, proposed.id);
|
|
309
|
-
const result = await apply(env, proposed.id);
|
|
310
|
-
assert.equal(result.ok, true, 'a config hook must be ignored outside the test build');
|
|
311
|
-
assert.equal(result.state, 'done');
|
|
312
|
-
} finally {
|
|
313
|
-
env.cleanup();
|
|
314
|
-
fs.rmSync(env.temp, { recursive: true, force: true });
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test('K26: lease bounds are computed from TTL and renewals, never hardcoded', () => {
|
|
319
|
-
assert.equal(computeMaxHoldMs(120000, 2), 240000);
|
|
320
|
-
assert.equal(computeMaxHoldMs(60000, 2), 120000);
|
|
321
|
-
assert.equal(computeMaxHoldMs(90000, 2), 180000);
|
|
322
|
-
assert.equal(clampLeaseTtl(30000), 60000);
|
|
323
|
-
assert.equal(clampLeaseTtl(200000), 120000);
|
|
324
|
-
assert.equal(clampLeaseTtl(90000), 90000);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
test('K25: an approved proposal that never wins the lease blocks then expires, ticket released', async () => {
|
|
328
|
-
const env = await setup('wp4b-blocked-', { blockedOnUserTtlMs: 3600000 });
|
|
329
|
-
try {
|
|
330
|
-
const proposed = await proposeJournal(env, 'blocked body');
|
|
331
|
-
await approve(env, proposed.id);
|
|
332
|
-
const file = path.join(env.vaultDir, 'proposals', `${proposed.id}.json`);
|
|
333
|
-
let proposal = readJson(file);
|
|
334
|
-
proposal.queue.deadlineAt = new Date(Date.now() - 1000).toISOString();
|
|
335
|
-
fs.writeFileSync(file, JSON.stringify(proposal));
|
|
336
|
-
env.service.queueManager.maintain();
|
|
337
|
-
proposal = readJson(file);
|
|
338
|
-
assert.equal(proposal.status, 'blocked-on-user');
|
|
339
|
-
assert.equal(proposal.queue.blockedReason, 'LEASE_TIMEOUT');
|
|
340
|
-
|
|
341
|
-
proposal.queue.deadlineAt = new Date(Date.now() - 1000).toISOString();
|
|
342
|
-
fs.writeFileSync(file, JSON.stringify(proposal));
|
|
343
|
-
env.service.queueManager.maintain();
|
|
344
|
-
proposal = readJson(file);
|
|
345
|
-
assert.equal(proposal.status, 'expired');
|
|
346
|
-
const queue = readJson(path.join(env.vaultDir, 'proposals', 'queue.json'));
|
|
347
|
-
assert.equal(queue.entries.some((entry) => entry.proposalId === proposed.id), false);
|
|
348
|
-
|
|
349
|
-
// D-CR11: approving an expired proposal re-queues it at the tail with a new
|
|
350
|
-
// ticket and a WAL queue.assign.recovered record; canonical stays untouched.
|
|
351
|
-
const targetFile = path.join(env.memoryRoot, 'journal', '2026-11.md');
|
|
352
|
-
const again = await env.service.requestApply({ mode: 'approve', id: proposed.id, confirm: proposed.id.slice(-6) });
|
|
353
|
-
assert.equal(again.ok, true);
|
|
354
|
-
assert.equal(again.state, 'pending');
|
|
355
|
-
assert.equal(again.requeued, true);
|
|
356
|
-
assert.notEqual(again.ticket, proposal.queue.ticket);
|
|
357
|
-
proposal = readJson(file);
|
|
358
|
-
assert.equal(proposal.status, 'pending');
|
|
359
|
-
assert.equal(proposal.queue.ticket, again.ticket);
|
|
360
|
-
assert.equal(proposal.queue.lease, null);
|
|
361
|
-
assert.equal(proposal.queue.deadlineAt, null);
|
|
362
|
-
assert.equal(fs.existsSync(targetFile), false);
|
|
363
|
-
const recovered = walEvents(env.vaultDir).find((line) => line.ev === 'queue.assign.recovered'
|
|
364
|
-
&& line.proposalId === proposed.id && line.reason === 'expired-requeue');
|
|
365
|
-
assert.ok(recovered);
|
|
366
|
-
const requeued = readJson(path.join(env.vaultDir, 'proposals', 'queue.json'));
|
|
367
|
-
assert.equal(requeued.entries.some((entry) => entry.proposalId === proposed.id && entry.ticket === again.ticket), true);
|
|
368
|
-
} finally {
|
|
369
|
-
env.cleanup();
|
|
370
|
-
fs.rmSync(env.temp, { recursive: true, force: true });
|
|
371
|
-
}
|
|
372
|
-
});
|