@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,375 +0,0 @@
|
|
|
1
|
-
// WP4a — WAL rotation / folding / pruning (DESIGN §3.5, §3.7, §5.4).
|
|
2
|
-
//
|
|
3
|
-
// These tests drive the WAL directly on the same on-disk layout the service
|
|
4
|
-
// uses, plus one end-to-end crash-recovery case through the proposal engine and
|
|
5
|
-
// queue rebuild. They assert real state transitions: segment names, chain
|
|
6
|
-
// hashes across boundaries, snapshot contents, prune policy and ticket survival.
|
|
7
|
-
|
|
8
|
-
import assert from 'node:assert/strict';
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import test from 'node:test';
|
|
12
|
-
|
|
13
|
-
import { normalizeRouterConfig } from '../../src/router/service/config.mjs';
|
|
14
|
-
import { createProposalEngine } from '../../src/router/service/proposals.mjs';
|
|
15
|
-
import { createQueueManager } from '../../src/router/service/queue.mjs';
|
|
16
|
-
import { createWal, walLineHash } from '../../src/router/service/wal.mjs';
|
|
17
|
-
import { readQueueFile } from '../../src/router/service/store.mjs';
|
|
18
|
-
import { readJson, tempRoot, writeMemory } from './helpers.mjs';
|
|
19
|
-
|
|
20
|
-
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
21
|
-
|
|
22
|
-
function walDir(prefix) {
|
|
23
|
-
return path.join(tempRoot(prefix), 'vault', 'proposals');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function makeWal(prefix, options = {}) {
|
|
27
|
-
const dir = walDir(prefix);
|
|
28
|
-
const audit = [];
|
|
29
|
-
const wal = createWal({
|
|
30
|
-
file: path.join(dir, 'wal.jsonl'),
|
|
31
|
-
audit: (kind, data = {}) => audit.push({ kind, ...data }),
|
|
32
|
-
...options,
|
|
33
|
-
});
|
|
34
|
-
return { dir, wal, audit, file: path.join(dir, 'wal.jsonl'), snapshotFile: path.join(dir, 'wal.snapshot.json') };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function rawLines(file) {
|
|
38
|
-
if (!fs.existsSync(file)) return [];
|
|
39
|
-
return fs.readFileSync(file, 'utf8').split('\n').filter((line) => line.trim() !== '').map((line) => JSON.parse(line));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function setup(prefix, configExtra = {}) {
|
|
43
|
-
const temp = tempRoot(prefix);
|
|
44
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
45
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
46
|
-
writeMemory(memoryRoot);
|
|
47
|
-
const vaultDir = path.join(temp, 'vault');
|
|
48
|
-
const config = normalizeRouterConfig({ proposalEnabled: true, applyEnabled: true, ...configExtra });
|
|
49
|
-
const audit = [];
|
|
50
|
-
const auditFn = (kind, data = {}) => audit.push({ kind, ...data });
|
|
51
|
-
const paths = { vaultDir, proposalsDir: path.join(vaultDir, 'proposals'), liveRoot: memoryRoot };
|
|
52
|
-
const walFile = path.join(paths.proposalsDir, 'wal.jsonl');
|
|
53
|
-
const wal = createWal({ file: walFile, audit: auditFn });
|
|
54
|
-
const engine = createProposalEngine({ config, paths, memoryRoot, audit: auditFn, now: () => new Date() });
|
|
55
|
-
return {
|
|
56
|
-
temp,
|
|
57
|
-
memoryRoot,
|
|
58
|
-
vaultDir,
|
|
59
|
-
config,
|
|
60
|
-
paths,
|
|
61
|
-
audit,
|
|
62
|
-
wal,
|
|
63
|
-
engine,
|
|
64
|
-
walFile,
|
|
65
|
-
snapshotFile: path.join(paths.proposalsDir, 'wal.snapshot.json'),
|
|
66
|
-
cleanup: () => fs.rmSync(temp, { recursive: true, force: true }),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const TARGETS = Object.freeze({
|
|
71
|
-
one: 'journal/2026-11.md',
|
|
72
|
-
first: 'journal/2026-11.md',
|
|
73
|
-
two: 'topics/two.md',
|
|
74
|
-
second: 'topics/second.md',
|
|
75
|
-
three: 'journal/2026-12.md',
|
|
76
|
-
four: 'topics/four.md',
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
function propose(engine, content) {
|
|
80
|
-
const target = TARGETS[content] ?? `topics/${content}.md`;
|
|
81
|
-
return engine.propose({
|
|
82
|
-
op: target.startsWith('journal/') ? 'journal.append' : 'topic.append',
|
|
83
|
-
// One target per proposal: distinct targets avoid §5.8 small-write merge,
|
|
84
|
-
// which would otherwise fold two test proposals into one.
|
|
85
|
-
target,
|
|
86
|
-
title: `t-${content}`,
|
|
87
|
-
content,
|
|
88
|
-
}, { caller: { kind: 'root', sessionId: 'root-session' } });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
test('WAL-1: the line limit rotates before writing and names segments by first seq', () => {
|
|
92
|
-
const env = makeWal('wal-rot-lines-', { maxLines: 3, maxBytes: 1_000_000 });
|
|
93
|
-
try {
|
|
94
|
-
for (let index = 1; index <= 8; index += 1) {
|
|
95
|
-
env.wal.append('queue.reconcile', { reason: `n${index}` });
|
|
96
|
-
}
|
|
97
|
-
const segments = env.wal.listSegments().map((segment) => segment.name);
|
|
98
|
-
assert.deepEqual(segments, ['wal.1.jsonl', 'wal.4.jsonl']);
|
|
99
|
-
for (const segment of env.wal.listSegments()) {
|
|
100
|
-
const first = rawLines(segment.file)[0];
|
|
101
|
-
assert.equal(first.seq, segment.firstSeq, `${segment.name} starts at its seq`);
|
|
102
|
-
}
|
|
103
|
-
// Each archived segment is at or under the line limit (rotation happens
|
|
104
|
-
// before the line that would exceed it).
|
|
105
|
-
for (const segment of env.wal.listSegments()) {
|
|
106
|
-
assert.ok(rawLines(segment.file).length <= 3, `${segment.name} respects maxLines`);
|
|
107
|
-
}
|
|
108
|
-
const state = env.wal.read();
|
|
109
|
-
assert.equal(state.broken, false);
|
|
110
|
-
assert.equal(state.lastSeq, 8);
|
|
111
|
-
assert.deepEqual(state.lines.map((line) => line.seq), [7, 8]);
|
|
112
|
-
// The whole physical chain still verifies when the snapshot is removed.
|
|
113
|
-
fs.rmSync(env.snapshotFile);
|
|
114
|
-
const replay = env.wal.read();
|
|
115
|
-
assert.equal(replay.broken, false);
|
|
116
|
-
assert.deepEqual(replay.lines.map((line) => line.seq), [1, 2, 3, 4, 5, 6, 7, 8]);
|
|
117
|
-
for (const line of replay.lines) assert.equal(line.hash, walLineHash(line));
|
|
118
|
-
} finally {
|
|
119
|
-
fs.rmSync(path.dirname(path.dirname(env.dir)), { recursive: true, force: true });
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('WAL-2: the byte limit rotates and the active file stays bounded', () => {
|
|
124
|
-
const env = makeWal('wal-rot-bytes-', { maxLines: 100_000, maxBytes: 400 });
|
|
125
|
-
try {
|
|
126
|
-
const padding = 'x'.repeat(260);
|
|
127
|
-
for (let index = 1; index <= 6; index += 1) {
|
|
128
|
-
env.wal.append('queue.reconcile', { reason: `${index}-${padding}` });
|
|
129
|
-
}
|
|
130
|
-
const segments = env.wal.listSegments();
|
|
131
|
-
assert.ok(segments.length >= 2, `expected multiple segments, got ${segments.length}`);
|
|
132
|
-
for (const segment of segments) {
|
|
133
|
-
const bytes = fs.statSync(segment.file).size;
|
|
134
|
-
assert.ok(bytes <= 400 + 512, `${segment.name} stayed near maxBytes (${bytes})`);
|
|
135
|
-
}
|
|
136
|
-
assert.equal(env.wal.read().broken, false);
|
|
137
|
-
assert.equal(env.wal.read().lastSeq, 6);
|
|
138
|
-
} finally {
|
|
139
|
-
fs.rmSync(path.dirname(path.dirname(env.dir)), { recursive: true, force: true });
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test('WAL-3: only the newest three archived segments are retained once older terminal lines are prunable', () => {
|
|
144
|
-
const env = makeWal('wal-rot-keep-', { maxLines: 2, maxBytes: 1_000_000, keepSegments: 3 });
|
|
145
|
-
try {
|
|
146
|
-
const old = new Date(Date.now() - 8 * DAY_MS).toISOString();
|
|
147
|
-
for (let index = 1; index <= 10; index += 1) {
|
|
148
|
-
env.wal.append('apply.done', { at: old, reason: `n${index}` });
|
|
149
|
-
}
|
|
150
|
-
const names = env.wal.listSegments().map((segment) => segment.name);
|
|
151
|
-
assert.deepEqual(names, ['wal.3.jsonl', 'wal.5.jsonl', 'wal.7.jsonl']);
|
|
152
|
-
assert.equal(fs.existsSync(path.join(env.dir, 'wal.1.jsonl')), false);
|
|
153
|
-
assert.equal(env.wal.read().lastSeq, 10);
|
|
154
|
-
assert.equal(env.wal.read().broken, false);
|
|
155
|
-
} finally {
|
|
156
|
-
fs.rmSync(path.dirname(path.dirname(env.dir)), { recursive: true, force: true });
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test('WAL-4: snapshot folds non-terminal proposals, assign summaries, seqCounter and head', async () => {
|
|
161
|
-
const env = setup('wal-rot-snapshot-');
|
|
162
|
-
try {
|
|
163
|
-
const first = await propose(env.engine, 'first');
|
|
164
|
-
const second = await propose(env.engine, 'second');
|
|
165
|
-
assert.equal(first.ok, true);
|
|
166
|
-
assert.equal(second.ok, true);
|
|
167
|
-
const approved = env.engine.approve({ id: first.id }, { caller: { kind: 'root', sessionId: 'root-session' } });
|
|
168
|
-
assert.equal(approved.ok, true);
|
|
169
|
-
const rotated = env.engine.wal.rotate({ force: true });
|
|
170
|
-
assert.equal(rotated.rotated, true);
|
|
171
|
-
assert.equal(rotated.snapshotError, null);
|
|
172
|
-
|
|
173
|
-
// Read-back: the file on disk parses to the same snapshot the WAL returned.
|
|
174
|
-
const snapshot = readJson(env.snapshotFile);
|
|
175
|
-
assert.equal(snapshot.v, 1);
|
|
176
|
-
assert.deepEqual(rotated.snapshot, snapshot);
|
|
177
|
-
assert.equal(snapshot.upToSeq, 3); // 2 assigns + 1 approve
|
|
178
|
-
assert.equal(snapshot.lastSeq, 3);
|
|
179
|
-
assert.match(snapshot.lastHash, /^sha256:[0-9a-f]{64}$/u);
|
|
180
|
-
assert.equal(snapshot.proposals.length, 2);
|
|
181
|
-
assert.deepEqual(snapshot.proposals.map((proposal) => proposal.id).sort(), [first.id, second.id].sort());
|
|
182
|
-
assert.ok(snapshot.proposals.every((proposal) => proposal.status === 'pending' || proposal.status === 'approved'));
|
|
183
|
-
assert.deepEqual(
|
|
184
|
-
snapshot.assignments.map((assignment) => [assignment.proposalId, assignment.ticket]).sort(),
|
|
185
|
-
[[first.id, first.ticketName], [second.id, second.ticketName]].sort(),
|
|
186
|
-
);
|
|
187
|
-
assert.equal(snapshot.seqCounter, 3);
|
|
188
|
-
const queue = readQueueFile(path.join(env.paths.proposalsDir, 'queue.json'));
|
|
189
|
-
assert.deepEqual(snapshot.head, { ticket: queue.entries[0].ticket, proposalId: queue.entries[0].proposalId });
|
|
190
|
-
|
|
191
|
-
// Replay after folding: no suffix lines remain, and the boundary hash matches.
|
|
192
|
-
const state = env.engine.wal.read();
|
|
193
|
-
assert.equal(state.broken, false);
|
|
194
|
-
assert.equal(state.lines.length, 0);
|
|
195
|
-
assert.equal(state.lastSeq, 3);
|
|
196
|
-
assert.equal(state.boundaryLineSeen, true);
|
|
197
|
-
// Atomic write leaves no tmp file behind.
|
|
198
|
-
assert.equal(fs.readdirSync(env.paths.proposalsDir).filter((name) => name.includes('.tmp')).length, 0);
|
|
199
|
-
} finally {
|
|
200
|
-
env.cleanup();
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test('WAL-5: an assignment folded into the snapshot survives queue rebuild and wins over proposal drift', async () => {
|
|
205
|
-
const env = setup('wal-rot-survive-');
|
|
206
|
-
try {
|
|
207
|
-
const first = await propose(env.engine, 'first');
|
|
208
|
-
const second = await propose(env.engine, 'second');
|
|
209
|
-
assert.equal(env.engine.wal.rotate({ force: true }).rotated, true);
|
|
210
|
-
|
|
211
|
-
// Crash/restart shape: no active WAL, no queue.json, only snapshot+segment.
|
|
212
|
-
fs.rmSync(env.walFile, { force: true });
|
|
213
|
-
fs.rmSync(path.join(env.paths.proposalsDir, 'queue.json'), { force: true });
|
|
214
|
-
const restartedWal = createWal({ file: env.walFile, audit: () => {} });
|
|
215
|
-
const queue = createQueueManager({
|
|
216
|
-
paths: env.paths,
|
|
217
|
-
config: env.config,
|
|
218
|
-
wal: restartedWal,
|
|
219
|
-
audit: (kind, data = {}) => env.audit.push({ kind, ...data }),
|
|
220
|
-
now: () => new Date(),
|
|
221
|
-
});
|
|
222
|
-
const rebuilt = queue.rebuild();
|
|
223
|
-
assert.equal(rebuilt.recovered.length, 0);
|
|
224
|
-
assert.equal(rebuilt.reconciled.length, 0);
|
|
225
|
-
assert.equal(rebuilt.seqCounter, 3);
|
|
226
|
-
assert.deepEqual(readQueueFile(path.join(env.paths.proposalsDir, 'queue.json')).entries.map((entry) => entry.ticket).sort(), [first.ticketName, second.ticketName].sort());
|
|
227
|
-
|
|
228
|
-
// Propose-side drift: WAL wins, ticket restored, no new recovered assign.
|
|
229
|
-
const proposalFile = path.join(env.paths.proposalsDir, `${first.id}.json`);
|
|
230
|
-
const drifted = readJson(proposalFile);
|
|
231
|
-
drifted.queue.ticket = 'q-0009';
|
|
232
|
-
drifted.queue.seq = 9;
|
|
233
|
-
fs.writeFileSync(proposalFile, `${JSON.stringify(drifted)}\n`);
|
|
234
|
-
const repaired = queue.rebuild();
|
|
235
|
-
assert.equal(repaired.recovered.length, 0);
|
|
236
|
-
assert.equal(repaired.reconciled.length, 1);
|
|
237
|
-
assert.equal(readJson(proposalFile).queue.ticket, first.ticketName);
|
|
238
|
-
assert.ok(env.audit.some((row) => row.kind === 'wal.reconcile'));
|
|
239
|
-
assert.equal(restartedWal.read().lines.filter((line) => line.ev === 'queue.assign.recovered').length, 0);
|
|
240
|
-
assert.equal(second.ticketName, 'q-0002');
|
|
241
|
-
} finally {
|
|
242
|
-
env.cleanup();
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
test('WAL-6: seven-day terminal events are pruned, incident records are not', () => {
|
|
247
|
-
const env = makeWal('wal-rot-prune-', { maxLines: 1000, keepSegments: 0 });
|
|
248
|
-
try {
|
|
249
|
-
const old = new Date(Date.now() - 8 * DAY_MS).toISOString();
|
|
250
|
-
const recent = new Date(Date.now() - 60 * 1000).toISOString();
|
|
251
|
-
const doneId = 'p-20261105T000000-aaaaaa';
|
|
252
|
-
fs.mkdirSync(env.dir, { recursive: true });
|
|
253
|
-
fs.writeFileSync(path.join(env.dir, `${doneId}.json`), JSON.stringify({
|
|
254
|
-
v: 1,
|
|
255
|
-
id: doneId,
|
|
256
|
-
status: 'done',
|
|
257
|
-
queue: { ticket: 'q-0001' },
|
|
258
|
-
}));
|
|
259
|
-
env.wal.append('queue.assign', { ticket: 'q-0001', proposalId: doneId, at: old });
|
|
260
|
-
env.wal.append('apply.done', { proposalId: doneId, at: old }); // terminal via proposal status
|
|
261
|
-
env.wal.append('lease.expired', { at: old });
|
|
262
|
-
env.wal.append('apply.conflict', { at: old });
|
|
263
|
-
env.wal.append('rollback.done', { at: old });
|
|
264
|
-
env.wal.append('queue.cancel', { at: old, proposalId: doneId });
|
|
265
|
-
env.wal.append('lease.release', { at: old });
|
|
266
|
-
env.wal.append('queue.reconcile', { at: old });
|
|
267
|
-
env.wal.append('apply.done', { at: recent, proposalId: 'other' });
|
|
268
|
-
env.wal.rotate({ force: true });
|
|
269
|
-
const segment = env.wal.listSegments()[0];
|
|
270
|
-
const kept = rawLines(segment.file).map((line) => line.ev);
|
|
271
|
-
assert.deepEqual(kept, ['lease.expired', 'apply.conflict', 'rollback.done', 'queue.reconcile', 'apply.done']);
|
|
272
|
-
assert.equal(env.audit.filter((row) => row.kind === 'wal.pruned_event').length, 4);
|
|
273
|
-
assert.ok(env.audit.every((row) => row.kind !== 'wal.prune_failed'));
|
|
274
|
-
// Folded gaps are not a chain break: replay starts from the snapshot.
|
|
275
|
-
assert.equal(env.wal.read().broken, false);
|
|
276
|
-
} finally {
|
|
277
|
-
fs.rmSync(path.dirname(path.dirname(env.dir)), { recursive: true, force: true });
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test('WAL-7: without an audit archive nothing is pruned', () => {
|
|
282
|
-
const dir = path.join(tempRoot('wal-rot-noaudit-'), 'vault', 'proposals');
|
|
283
|
-
try {
|
|
284
|
-
const file = path.join(dir, 'wal.jsonl');
|
|
285
|
-
const wal = createWal({ file, maxLines: 1000, keepSegments: 0 });
|
|
286
|
-
const old = new Date(Date.now() - 8 * DAY_MS).toISOString();
|
|
287
|
-
for (let index = 0; index < 4; index += 1) wal.append('apply.done', { at: old, seqNote: index });
|
|
288
|
-
const rotated = wal.rotate({ force: true });
|
|
289
|
-
assert.equal(rotated.rotated, true);
|
|
290
|
-
const segment = wal.listSegments()[0];
|
|
291
|
-
assert.equal(rawLines(segment.file).length, 4, 'incident evidence kept without an archive');
|
|
292
|
-
assert.equal(fs.existsSync(path.join(dir, 'wal.snapshot.json')), true);
|
|
293
|
-
} finally {
|
|
294
|
-
fs.rmSync(path.dirname(path.dirname(dir)), { recursive: true, force: true });
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test('WAL-8: replay verifies the chain continuously across segments and stops at a break', () => {
|
|
299
|
-
const dir = path.join(tempRoot('wal-rot-chain-'), 'vault', 'proposals');
|
|
300
|
-
try {
|
|
301
|
-
const file = path.join(dir, 'wal.jsonl');
|
|
302
|
-
const wal = createWal({ file, maxLines: 1000 });
|
|
303
|
-
for (let index = 1; index <= 4; index += 1) wal.append('queue.reconcile', { reason: `a${index}` });
|
|
304
|
-
assert.equal(wal.rotate({ force: true }).rotated, true);
|
|
305
|
-
for (let index = 5; index <= 8; index += 1) wal.append('queue.reconcile', { reason: `b${index}` });
|
|
306
|
-
assert.equal(wal.rotate({ force: true }).rotated, true);
|
|
307
|
-
assert.deepEqual(wal.listSegments().map((segment) => segment.name), ['wal.1.jsonl', 'wal.5.jsonl']);
|
|
308
|
-
|
|
309
|
-
// Missing snapshot: replay both segments + active as one chain.
|
|
310
|
-
fs.rmSync(path.join(dir, 'wal.snapshot.json'));
|
|
311
|
-
const replay = wal.read();
|
|
312
|
-
assert.equal(replay.broken, false);
|
|
313
|
-
assert.deepEqual(replay.lines.map((line) => line.seq), [1, 2, 3, 4, 5, 6, 7, 8]);
|
|
314
|
-
for (const line of replay.lines) assert.equal(line.hash, walLineHash(line));
|
|
315
|
-
|
|
316
|
-
// Break the second line of the oldest segment: stop there, never guess.
|
|
317
|
-
const segmentFile = wal.listSegments()[0].file;
|
|
318
|
-
const broken = rawLines(segmentFile);
|
|
319
|
-
broken[1].seq = 22;
|
|
320
|
-
fs.writeFileSync(segmentFile, `${broken.map((line) => JSON.stringify(line)).join('\n')}\n`);
|
|
321
|
-
const state = wal.read();
|
|
322
|
-
assert.equal(state.broken, true);
|
|
323
|
-
assert.equal(state.brokenAt, 1);
|
|
324
|
-
assert.deepEqual(state.lines.map((line) => line.seq), [1]);
|
|
325
|
-
assert.throws(() => wal.append('queue.reconcile', { reason: 'nope' }), (error) => error.code === 'E_WAL_BROKEN');
|
|
326
|
-
} finally {
|
|
327
|
-
fs.rmSync(path.dirname(path.dirname(dir)), { recursive: true, force: true });
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
test('WAL-9: crash between rename and snapshot keeps every assignment and the ticket sequence', async () => {
|
|
332
|
-
const env = setup('wal-rot-crash-');
|
|
333
|
-
try {
|
|
334
|
-
const one = await propose(env.engine, 'one');
|
|
335
|
-
const two = await propose(env.engine, 'two');
|
|
336
|
-
const three = await propose(env.engine, 'three');
|
|
337
|
-
assert.equal(env.engine.wal.rotate({ force: true }).rotated, true);
|
|
338
|
-
// Fault injection: the segment exists, the snapshot was never written.
|
|
339
|
-
fs.rmSync(env.snapshotFile);
|
|
340
|
-
assert.deepEqual(env.engine.wal.listSegments().map((segment) => segment.name), ['wal.1.jsonl']);
|
|
341
|
-
|
|
342
|
-
const restartedWal = createWal({ file: env.walFile, audit: () => {} });
|
|
343
|
-
const restartedEngine = createProposalEngine({
|
|
344
|
-
config: env.config,
|
|
345
|
-
paths: env.paths,
|
|
346
|
-
memoryRoot: env.memoryRoot,
|
|
347
|
-
audit: () => {},
|
|
348
|
-
now: () => new Date(),
|
|
349
|
-
});
|
|
350
|
-
const four = await propose(restartedEngine, 'four');
|
|
351
|
-
assert.equal(four.ok, true);
|
|
352
|
-
assert.equal(four.ticket, 4);
|
|
353
|
-
assert.equal(four.ticketName, 'q-0004');
|
|
354
|
-
const replay = restartedWal.read();
|
|
355
|
-
assert.equal(replay.broken, false);
|
|
356
|
-
assert.equal(replay.lastSeq, 4);
|
|
357
|
-
assert.equal(replay.lines.filter((line) => line.ev === 'queue.assign').length, 4);
|
|
358
|
-
|
|
359
|
-
const queue = createQueueManager({
|
|
360
|
-
paths: env.paths,
|
|
361
|
-
config: env.config,
|
|
362
|
-
wal: restartedWal,
|
|
363
|
-
audit: () => {},
|
|
364
|
-
now: () => new Date(),
|
|
365
|
-
});
|
|
366
|
-
const rebuilt = queue.rebuild();
|
|
367
|
-
assert.equal(rebuilt.recovered.length, 0);
|
|
368
|
-
assert.equal(rebuilt.reconciled.length, 0);
|
|
369
|
-
const tickets = readQueueFile(path.join(env.paths.proposalsDir, 'queue.json')).entries.map((entry) => entry.ticket).sort();
|
|
370
|
-
assert.deepEqual(tickets, [one.ticketName, two.ticketName, three.ticketName, four.ticketName].sort());
|
|
371
|
-
assert.equal(new Set(tickets).size, 4);
|
|
372
|
-
} finally {
|
|
373
|
-
env.cleanup();
|
|
374
|
-
}
|
|
375
|
-
});
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import test from 'node:test';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
normalizeRouterConfig,
|
|
6
|
-
normalizeRouterConfigFromPlugin,
|
|
7
|
-
ROUTER_CONFIG_KEYS,
|
|
8
|
-
ROUTER_DEFAULTS,
|
|
9
|
-
} from '../../src/router/service/config.mjs';
|
|
10
|
-
|
|
11
|
-
const DESIGN_KEYS = [
|
|
12
|
-
'routerEnabled',
|
|
13
|
-
'routerProvider',
|
|
14
|
-
'routerModel',
|
|
15
|
-
'routerTickMs',
|
|
16
|
-
'routerReflectOnTurn',
|
|
17
|
-
'routerReflectCoalesceMs',
|
|
18
|
-
'routerAutonomy',
|
|
19
|
-
'routerAllowAgent',
|
|
20
|
-
'routerMaxTokensPerRun',
|
|
21
|
-
'routerMaxTokensPerDay',
|
|
22
|
-
'routerCallMaxTokens',
|
|
23
|
-
'routerMaxAttempts',
|
|
24
|
-
'routerMaxConsecutiveErrors',
|
|
25
|
-
'routerAnchorPath',
|
|
26
|
-
'routerStopFile',
|
|
27
|
-
'routerAllowMissingAnchor',
|
|
28
|
-
'routerVaultDir',
|
|
29
|
-
'routerAgentTimeoutMs',
|
|
30
|
-
'routerDisposeTimeoutMs',
|
|
31
|
-
'routerFlowSlice',
|
|
32
|
-
'routerSharedBoard',
|
|
33
|
-
'routerHintScope',
|
|
34
|
-
'routerHintTokens',
|
|
35
|
-
'routerReflectHintTokens',
|
|
36
|
-
'routerInjectEveryTurns',
|
|
37
|
-
'routerInjectStateMaxSessions',
|
|
38
|
-
'routerInjectMaxPerSessionPerDay',
|
|
39
|
-
'routerInjectMaxTotalTokens',
|
|
40
|
-
'routerLiveRowsMax',
|
|
41
|
-
'routerBoardMaxSessions',
|
|
42
|
-
'routerProposalEnabled',
|
|
43
|
-
'routerApplyEnabled',
|
|
44
|
-
'routerL4MergeEnabled',
|
|
45
|
-
'routerProposalMaxChars',
|
|
46
|
-
'routerProposalsPerSessionHour',
|
|
47
|
-
'routerMaxProposals',
|
|
48
|
-
'routerAgingSeconds',
|
|
49
|
-
'routerLeaseRenewMax',
|
|
50
|
-
'routerLeaseWaitTimeoutMs',
|
|
51
|
-
'routerBlockedOnUserTtlMs',
|
|
52
|
-
'routerProposalTtlH',
|
|
53
|
-
'routerWakeEnabled',
|
|
54
|
-
'routerWakeCooldownMs',
|
|
55
|
-
'routerWakeDailyQuota',
|
|
56
|
-
'routerWakeLogMaxBytes',
|
|
57
|
-
'routerStarvationAlertMs',
|
|
58
|
-
'routerQueueMaxBytes',
|
|
59
|
-
'routerWalMaxBytes',
|
|
60
|
-
'routerWriterLeaseMs',
|
|
61
|
-
'routerSnapshotKeepDays',
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
test('the 50 DESIGN §8.1 keys are all present exactly once', () => {
|
|
65
|
-
assert.equal(DESIGN_KEYS.length, 50);
|
|
66
|
-
assert.equal(Object.keys(ROUTER_DEFAULTS).length, 50);
|
|
67
|
-
assert.deepEqual(Object.values(ROUTER_CONFIG_KEYS).sort(), [...DESIGN_KEYS].sort());
|
|
68
|
-
const cfg = normalizeRouterConfig({});
|
|
69
|
-
for (const [short, long] of Object.entries(ROUTER_CONFIG_KEYS)) {
|
|
70
|
-
assert.ok(short in cfg, `${short} (${long}) missing from normalized config`);
|
|
71
|
-
}
|
|
72
|
-
for (const long of DESIGN_KEYS) assert.equal(long in cfg, false, `normalized config must not keep ${long}`);
|
|
73
|
-
assert.equal('maxQueue' in cfg, false);
|
|
74
|
-
assert.equal('routerMaxQueue' in cfg, false);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('DESIGN defaults for budget, injection and merge are authoritative', () => {
|
|
78
|
-
const cfg = normalizeRouterConfig({ routerEnabled: true });
|
|
79
|
-
assert.equal(cfg.maxTokensPerRun, 80000);
|
|
80
|
-
assert.equal(cfg.maxTokensPerDay, 1000000);
|
|
81
|
-
assert.equal(cfg.callMaxTokens, 2048);
|
|
82
|
-
assert.equal(cfg.maxAttempts, 2);
|
|
83
|
-
assert.equal(cfg.injectMaxPerSessionPerDay, 24);
|
|
84
|
-
assert.equal(cfg.injectMaxTotalTokens, 12000);
|
|
85
|
-
assert.equal(cfg.l4MergeEnabled, false);
|
|
86
|
-
assert.equal(cfg.hintTokens, 700);
|
|
87
|
-
assert.equal(cfg.reflectHintTokens, 150);
|
|
88
|
-
assert.equal(cfg.flowSlice, 1200);
|
|
89
|
-
assert.equal(cfg.snapshotKeepDays, 30);
|
|
90
|
-
assert.deepEqual(cfg.warnings, []);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('range-clamped keys clamp instead of falling back to default', () => {
|
|
94
|
-
const high = normalizeRouterConfig({ routerHintTokens: 900, routerReflectHintTokens: 900, routerWriterLeaseMs: 999999 });
|
|
95
|
-
assert.equal(high.hintTokens, 700);
|
|
96
|
-
assert.equal(high.reflectHintTokens, 150);
|
|
97
|
-
assert.equal(high.writerLeaseMs, 120000);
|
|
98
|
-
assert.ok(high.warnings.includes('CONFIG_CLAMPED:hintTokens'));
|
|
99
|
-
assert.ok(high.warnings.includes('CONFIG_CLAMPED:reflectHintTokens'));
|
|
100
|
-
assert.ok(high.warnings.includes('CONFIG_CLAMPED:writerLeaseMs'));
|
|
101
|
-
|
|
102
|
-
const low = normalizeRouterConfig({ routerHintTokens: 1, routerReflectHintTokens: 1, routerWriterLeaseMs: 1 });
|
|
103
|
-
assert.equal(low.hintTokens, 200);
|
|
104
|
-
assert.equal(low.reflectHintTokens, 50);
|
|
105
|
-
assert.equal(low.writerLeaseMs, 60000);
|
|
106
|
-
|
|
107
|
-
const notNumber = normalizeRouterConfig({ routerWriterLeaseMs: 'later' });
|
|
108
|
-
assert.equal(notNumber.writerLeaseMs, 120000);
|
|
109
|
-
assert.ok(notNumber.warnings.includes('CONFIG_INVALID:writerLeaseMs'));
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('legacy keys migrate on read, and the normalized object only carries new keys', () => {
|
|
113
|
-
const merged = normalizeRouterConfig({ routerMergeEnabled: true });
|
|
114
|
-
assert.equal(merged.l4MergeEnabled, true);
|
|
115
|
-
assert.ok(merged.warnings.includes('CONFIG_MIGRATED:routerMergeEnabled'));
|
|
116
|
-
|
|
117
|
-
const plural = normalizeRouterConfig({ routerProposalsEnabled: true });
|
|
118
|
-
assert.equal(plural.proposalEnabled, true);
|
|
119
|
-
assert.ok(plural.warnings.includes('CONFIG_MIGRATED:routerProposalsEnabled'));
|
|
120
|
-
|
|
121
|
-
const lease = normalizeRouterConfig({ routerLeaseTtlMs: 90000 });
|
|
122
|
-
assert.equal(lease.writerLeaseMs, 90000);
|
|
123
|
-
assert.ok(lease.warnings.includes('CONFIG_MIGRATED:routerLeaseTtlMs'));
|
|
124
|
-
|
|
125
|
-
// 新键优先,旧键不再写回。
|
|
126
|
-
const both = normalizeRouterConfig({ routerMergeEnabled: true, routerL4MergeEnabled: false });
|
|
127
|
-
assert.equal(both.l4MergeEnabled, false);
|
|
128
|
-
assert.equal(both.warnings.some((warning) => warning.includes('MIGRATED')), false);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test('unknown router keys warn; non-router keys and removed routerMaxQueue are handled', () => {
|
|
132
|
-
const cfg = normalizeRouterConfig({
|
|
133
|
-
routerNope: 1,
|
|
134
|
-
routerRuntime: 'off',
|
|
135
|
-
routerPolicyProfile: 'x',
|
|
136
|
-
unknownKey: 'ignored',
|
|
137
|
-
routerMaxQueue: 999,
|
|
138
|
-
});
|
|
139
|
-
assert.ok(cfg.warnings.includes('CONFIG_UNKNOWN:routerNope'));
|
|
140
|
-
assert.equal(cfg.warnings.some((warning) => warning.includes('unknownKey')), false);
|
|
141
|
-
assert.equal(cfg.warnings.some((warning) => warning.includes('routerRuntime')), false);
|
|
142
|
-
assert.ok(cfg.warnings.includes('CONFIG_DEPRECATED:routerMaxQueue'));
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test('illegal values fall back to defaults with a warning', () => {
|
|
146
|
-
const cfg = normalizeRouterConfig({
|
|
147
|
-
routerFlowSlice: 'x',
|
|
148
|
-
routerInjectEveryTurns: 0,
|
|
149
|
-
routerHintScope: 'some',
|
|
150
|
-
routerProposalMaxChars: 10,
|
|
151
|
-
routerSharedBoard: 'yes',
|
|
152
|
-
routerProvider: 42,
|
|
153
|
-
});
|
|
154
|
-
assert.equal(cfg.flowSlice, 1200);
|
|
155
|
-
assert.equal(cfg.injectEveryTurns, 10);
|
|
156
|
-
assert.equal(cfg.hintScope, 'root');
|
|
157
|
-
assert.equal(cfg.proposalMaxChars, 4000);
|
|
158
|
-
assert.equal(cfg.sharedBoard, false);
|
|
159
|
-
assert.equal(cfg.provider, 'deepseek-official');
|
|
160
|
-
for (const key of ['flowSlice', 'injectEveryTurns', 'hintScope', 'proposalMaxChars', 'sharedBoard', 'provider']) {
|
|
161
|
-
assert.ok(cfg.warnings.includes(`CONFIG_INVALID:${key}`), `missing CONFIG_INVALID:${key}`);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('normalizeRouterConfigFromPlugin is the same pure normalizer', () => {
|
|
166
|
-
const cfg = normalizeRouterConfigFromPlugin({ routerEnabled: true, routerProposalEnabled: true });
|
|
167
|
-
assert.equal(cfg.enabled, true);
|
|
168
|
-
assert.equal(cfg.proposalEnabled, true);
|
|
169
|
-
assert.equal(cfg.warnings.length, 0);
|
|
170
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import test from 'node:test';
|
|
5
|
-
|
|
6
|
-
import { createRouterService } from '../../src/router/service/service.mjs';
|
|
7
|
-
import { MockLlm } from '../../src/router/background/mock-llm.mjs';
|
|
8
|
-
import { apply } from '../../src/index.js';
|
|
9
|
-
import { tempRoot } from './helpers.mjs';
|
|
10
|
-
|
|
11
|
-
test('a disabled service installs no timers or listeners and writes nothing', async () => {
|
|
12
|
-
const temp = tempRoot('longlive-router-disabled-');
|
|
13
|
-
const vaultDir = path.join(temp, 'vault');
|
|
14
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
15
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
16
|
-
const service = createRouterService(
|
|
17
|
-
{ logger: () => ({ warn: () => {}, info: () => {} }), on: () => () => {} },
|
|
18
|
-
{ enabled: false, provider: 'mock', model: 'mock-1', vaultDir, memoryRoot },
|
|
19
|
-
{ llm: new MockLlm(), installTimers: true, installListener: true },
|
|
20
|
-
);
|
|
21
|
-
try {
|
|
22
|
-
assert.equal(service.timers.size, 0);
|
|
23
|
-
const result = await service.start();
|
|
24
|
-
assert.deepEqual(result, { ok: false, code: 'E_DISABLED', state: 'stopped' });
|
|
25
|
-
assert.equal(service.timers.size, 0);
|
|
26
|
-
assert.equal(service.tickTimer, null);
|
|
27
|
-
assert.equal(service.heartbeatTimer, null);
|
|
28
|
-
assert.equal(service.firstTickTimer, null);
|
|
29
|
-
assert.equal(service.listenerDisposers.length, 0);
|
|
30
|
-
assert.equal(fs.existsSync(vaultDir), false);
|
|
31
|
-
const status = service.status();
|
|
32
|
-
assert.equal(status.state, 'stopped');
|
|
33
|
-
assert.equal(status.nextTickAt, null);
|
|
34
|
-
assert.deepEqual(status.warnings, []);
|
|
35
|
-
} finally {
|
|
36
|
-
await service.dispose();
|
|
37
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('proposal flags do not bypass the master switch: still 22 tools and no .longlive', async () => {
|
|
42
|
-
const temp = tempRoot('longlive-router-off-proposals-');
|
|
43
|
-
const home = path.join(temp, 'home');
|
|
44
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
45
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
46
|
-
fs.writeFileSync(path.join(memoryRoot, 'STATE.md'), '## 我在哪\n测试\n', 'utf8');
|
|
47
|
-
const tools = [];
|
|
48
|
-
const effects = [];
|
|
49
|
-
const ctx = {
|
|
50
|
-
tools: { register: (definition) => { tools.push(definition); return () => {}; } },
|
|
51
|
-
on: () => () => {},
|
|
52
|
-
logger: () => ({ warn: () => {}, info: () => {} }),
|
|
53
|
-
effect: (fn) => { const dispose = fn(); effects.push(typeof dispose === 'function' ? dispose : () => {}); return effects.at(-1); },
|
|
54
|
-
get: () => undefined,
|
|
55
|
-
};
|
|
56
|
-
try {
|
|
57
|
-
const returned = await apply(ctx, {
|
|
58
|
-
home,
|
|
59
|
-
memoryRoot,
|
|
60
|
-
env: {},
|
|
61
|
-
heartbeatMs: 0,
|
|
62
|
-
recoverOnStart: false,
|
|
63
|
-
defineTool: (definition) => definition,
|
|
64
|
-
routerRuntime: 'off',
|
|
65
|
-
routerEnabled: false,
|
|
66
|
-
routerProposalEnabled: true,
|
|
67
|
-
routerApplyEnabled: true,
|
|
68
|
-
routerProposalsEnabled: true,
|
|
69
|
-
});
|
|
70
|
-
assert.equal(returned, 22);
|
|
71
|
-
assert.equal(tools.some((tool) => String(tool.name).startsWith('router_')), false);
|
|
72
|
-
assert.equal(fs.existsSync(path.join(memoryRoot, '.longlive')), false);
|
|
73
|
-
} finally {
|
|
74
|
-
for (const dispose of effects.reverse()) dispose();
|
|
75
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
76
|
-
}
|
|
77
|
-
});
|