@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,293 +0,0 @@
|
|
|
1
|
-
// 1.1.2 contract checks for the three v6 FAILs that remained after 1.1.1:
|
|
2
|
-
// K-08 (expiry keeps the proposal body), L-03 (restart rebuilds the queue view
|
|
3
|
-
// from proposals/*.json) and K-04 (a pending row is cancellable; `superseded`
|
|
4
|
-
// is a legitimate terminal state and is not).
|
|
5
|
-
|
|
6
|
-
import assert from 'node:assert/strict';
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import test from 'node:test';
|
|
10
|
-
|
|
11
|
-
import { createRouterTools } from '../../src/router/service/tools.mjs';
|
|
12
|
-
import { makeService, readJson, tempRoot, writeMemory } from './helpers.mjs';
|
|
13
|
-
|
|
14
|
-
const CALLER = { kind: 'root', sessionId: 'root-session' };
|
|
15
|
-
|
|
16
|
-
function propose(service, { title, content, target = 'journal/2026-11.md' }) {
|
|
17
|
-
return service.proposeProposal({
|
|
18
|
-
op: 'journal.append',
|
|
19
|
-
target,
|
|
20
|
-
title,
|
|
21
|
-
content,
|
|
22
|
-
}, { caller: CALLER });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function auditRows(vaultDir) {
|
|
26
|
-
const file = path.join(vaultDir, 'audit.jsonl');
|
|
27
|
-
if (!fs.existsSync(file)) return [];
|
|
28
|
-
return fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function writeStaleQueue(vaultDir, proposalId, ticket) {
|
|
32
|
-
const file = path.join(vaultDir, 'proposals', 'queue.json');
|
|
33
|
-
const queue = readJson(file);
|
|
34
|
-
assert.ok(queue, 'queue.json must exist before it is made stale');
|
|
35
|
-
queue.rev = Number(queue.rev ?? 0) + 1;
|
|
36
|
-
queue.entries = (queue.entries ?? []).map((entry) => (
|
|
37
|
-
entry.proposalId === proposalId ? { ...entry, status: 'pending' } : entry
|
|
38
|
-
));
|
|
39
|
-
if (!queue.entries.some((entry) => entry.proposalId === proposalId)) {
|
|
40
|
-
queue.entries.push({
|
|
41
|
-
proposalId,
|
|
42
|
-
ticket,
|
|
43
|
-
status: 'pending',
|
|
44
|
-
sessionId: 'unknown-session',
|
|
45
|
-
target: 'journal/2026-11.md',
|
|
46
|
-
op: 'journal.append',
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
fs.writeFileSync(file, JSON.stringify(queue, null, 2));
|
|
50
|
-
return queue;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function newEnv(temp, { config = {}, now } = {}) {
|
|
54
|
-
const memoryRoot = path.join(temp, 'memory');
|
|
55
|
-
fs.mkdirSync(memoryRoot, { recursive: true });
|
|
56
|
-
writeMemory(memoryRoot);
|
|
57
|
-
return makeService({
|
|
58
|
-
temp,
|
|
59
|
-
memoryRoot,
|
|
60
|
-
vaultDir: path.join(temp, 'vault'),
|
|
61
|
-
...(now ? { now } : {}),
|
|
62
|
-
config: { proposalEnabled: true, applyEnabled: true, ...config },
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
test('v6b K-08: blocked-on-user expiry keeps the proposal body and only moves status', async () => {
|
|
67
|
-
const temp = tempRoot('v6b-k08-retention-');
|
|
68
|
-
let clock = new Date('2026-11-05T05:00:00.000Z');
|
|
69
|
-
const env = newEnv(temp, {
|
|
70
|
-
now: () => clock,
|
|
71
|
-
config: { blockedOnUserTtlMs: 24 * 3600 * 1000 },
|
|
72
|
-
});
|
|
73
|
-
try {
|
|
74
|
-
const started = await env.service.start();
|
|
75
|
-
assert.equal(started.ok, true, JSON.stringify(started));
|
|
76
|
-
const proposed = await propose(env.service, { title: 'blocked', content: 'blocked body' });
|
|
77
|
-
assert.equal(proposed.ok, true, JSON.stringify(proposed));
|
|
78
|
-
await env.service.requestApply({ mode: 'approve', id: proposed.id, confirm: proposed.id.slice(-6) }, { caller: CALLER });
|
|
79
|
-
|
|
80
|
-
// A foreign live writer wins the lease, so the apply attempt parks the row
|
|
81
|
-
// in blocked-on-user instead of applying it.
|
|
82
|
-
fs.mkdirSync(path.join(env.vaultDir, 'locks'), { recursive: true });
|
|
83
|
-
fs.writeFileSync(path.join(env.vaultDir, 'locks', 'writer.lease.json'), JSON.stringify({
|
|
84
|
-
v: 1,
|
|
85
|
-
holder: 'v6b-other',
|
|
86
|
-
proposalId: 'other',
|
|
87
|
-
pid: process.pid,
|
|
88
|
-
host: 'other-host',
|
|
89
|
-
runId: 'other-run',
|
|
90
|
-
acquiredAt: clock.toISOString(),
|
|
91
|
-
expiresAt: new Date(clock.getTime() + 10 * 60 * 1000).toISOString(),
|
|
92
|
-
renewals: 0,
|
|
93
|
-
}));
|
|
94
|
-
const queued = await env.service.requestApply({ mode: 'apply', id: proposed.id, confirm: proposed.id.slice(-6) }, { caller: CALLER });
|
|
95
|
-
assert.equal(queued.ok, true, JSON.stringify(queued));
|
|
96
|
-
|
|
97
|
-
clock = new Date(clock.getTime() + 5 * 60 * 1000 + 1000);
|
|
98
|
-
await env.service.tick({ tier: 'full', reason: 'v6b-k08' });
|
|
99
|
-
const file = path.join(env.vaultDir, 'proposals', `${proposed.id}.json`);
|
|
100
|
-
assert.equal(readJson(file).status, 'blocked-on-user');
|
|
101
|
-
assert.equal(fs.existsSync(file), true);
|
|
102
|
-
|
|
103
|
-
clock = new Date(clock.getTime() + 24 * 3600 * 1000 + 1000);
|
|
104
|
-
await env.service.tick({ tier: 'full', reason: 'v6b-k08' });
|
|
105
|
-
assert.equal(fs.existsSync(file), true, 'expiry must not delete the proposal body');
|
|
106
|
-
const expired = readJson(file);
|
|
107
|
-
assert.equal(expired.status, 'expired');
|
|
108
|
-
assert.equal(expired.queue.ticket, 'q-0001', 'the released ticket stays readable on the expired body');
|
|
109
|
-
assert.equal(env.service.status().queue.expired, 1);
|
|
110
|
-
|
|
111
|
-
const rows = auditRows(env.vaultDir);
|
|
112
|
-
const expiry = rows.filter((row) => row.kind === 'proposal.expired').at(-1);
|
|
113
|
-
assert.ok(expiry, 'expiry must be audited');
|
|
114
|
-
assert.equal(expiry.bodyKept, true);
|
|
115
|
-
assert.equal(expiry.reason, 'blocked-on-user-ttl');
|
|
116
|
-
assert.equal(rows.some((row) => row.kind === 'proposal.retention_lost'), false);
|
|
117
|
-
|
|
118
|
-
// Ticket release keeps `tailTicket` semantics: the released q-0001 is not
|
|
119
|
-
// reused, the next proposal goes to the tail as q-0002.
|
|
120
|
-
const next = await propose(env.service, { title: 'next', content: 'next body', target: 'journal/2026-12.md' });
|
|
121
|
-
assert.equal(next.ticket, 2);
|
|
122
|
-
assert.equal(next.ticketName, 'q-0002');
|
|
123
|
-
} finally {
|
|
124
|
-
await env.service.dispose();
|
|
125
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('v6b K-08: the 72h proposal TTL expires in place instead of removing the file', async () => {
|
|
130
|
-
const temp = tempRoot('v6b-k08-ttl-');
|
|
131
|
-
let clock = new Date('2026-11-05T05:00:00.000Z');
|
|
132
|
-
const env = newEnv(temp, { now: () => clock, config: { proposalTtlH: 72 } });
|
|
133
|
-
try {
|
|
134
|
-
const started = await env.service.start();
|
|
135
|
-
assert.equal(started.ok, true, JSON.stringify(started));
|
|
136
|
-
const proposed = await propose(env.service, { title: 'stale', content: 'stale body' });
|
|
137
|
-
assert.equal(proposed.ok, true, JSON.stringify(proposed));
|
|
138
|
-
clock = new Date(clock.getTime() + 72 * 3600 * 1000 + 1000);
|
|
139
|
-
await env.service.tick({ tier: 'full', reason: 'v6b-k08-ttl' });
|
|
140
|
-
|
|
141
|
-
const file = path.join(env.vaultDir, 'proposals', `${proposed.id}.json`);
|
|
142
|
-
assert.equal(fs.existsSync(file), true, 'proposal TTL must not delete the proposal body');
|
|
143
|
-
assert.equal(readJson(file).status, 'expired');
|
|
144
|
-
assert.equal(env.service.status().queue.expired, 1);
|
|
145
|
-
const expiry = auditRows(env.vaultDir).filter((row) => row.kind === 'proposal.expired').at(-1);
|
|
146
|
-
assert.equal(expiry.bodyKept, true);
|
|
147
|
-
assert.equal(expiry.reason, 'proposal-ttl');
|
|
148
|
-
} finally {
|
|
149
|
-
await env.service.dispose();
|
|
150
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test('v6b L-03: a restart rebuilds list/queue/status from proposals/*.json', async () => {
|
|
155
|
-
const temp = tempRoot('v6b-l03-restart-');
|
|
156
|
-
const first = newEnv(temp);
|
|
157
|
-
let firstId = null;
|
|
158
|
-
try {
|
|
159
|
-
const started = await first.service.start();
|
|
160
|
-
assert.equal(started.ok, true, JSON.stringify(started));
|
|
161
|
-
const one = await propose(first.service, { title: 'one', content: 'body one' });
|
|
162
|
-
const two = await propose(first.service, { title: 'two', content: 'body two' });
|
|
163
|
-
assert.equal(one.ok, true, JSON.stringify(one));
|
|
164
|
-
assert.equal(two.ok, true, JSON.stringify(two));
|
|
165
|
-
// Same-target small writes coalesce: the second row is legitimately
|
|
166
|
-
// superseded by the first (DESIGN §5.8/K20), it is not a lost proposal.
|
|
167
|
-
assert.equal(readJson(path.join(first.vaultDir, 'proposals', `${two.id}.json`)).status, 'superseded');
|
|
168
|
-
await first.service.requestApply({ mode: 'approve', id: one.id, confirm: one.id.slice(-6) }, { caller: CALLER });
|
|
169
|
-
firstId = one.id;
|
|
170
|
-
await first.service.stop('v6b-l03');
|
|
171
|
-
await first.service.dispose();
|
|
172
|
-
|
|
173
|
-
// Simulate the crash window: the proposal file says approved, the queue
|
|
174
|
-
// cache still says pending. Start must reconcile the cache, not trust it.
|
|
175
|
-
writeStaleQueue(first.vaultDir, firstId, 'q-0001');
|
|
176
|
-
|
|
177
|
-
const second = newEnv(temp);
|
|
178
|
-
try {
|
|
179
|
-
const restarted = await second.service.start();
|
|
180
|
-
assert.equal(restarted.ok, true, JSON.stringify(restarted));
|
|
181
|
-
const caller = CALLER;
|
|
182
|
-
const list = second.service.proposalsView({ mode: 'list', limit: 50 }, { caller });
|
|
183
|
-
assert.equal(list.ok, true, JSON.stringify(list));
|
|
184
|
-
assert.equal(list.proposals.length, 2, JSON.stringify(list.proposals));
|
|
185
|
-
const byTicket = new Map(list.proposals.map((row) => [row.ticket, row]));
|
|
186
|
-
assert.equal(byTicket.get('q-0001').state, 'approved');
|
|
187
|
-
assert.equal(byTicket.get('q-0001').status, 'approved');
|
|
188
|
-
assert.equal(byTicket.get('q-0002').state, 'superseded');
|
|
189
|
-
|
|
190
|
-
const queue = second.service.proposalsView({ mode: 'queue' }, { caller });
|
|
191
|
-
const entry = (queue.entries ?? []).find((item) => item.ticket === 'q-0001');
|
|
192
|
-
assert.equal(entry.status, 'approved', 'the rebuilt queue view must agree with the proposal file');
|
|
193
|
-
|
|
194
|
-
const queueFile = readJson(path.join(second.vaultDir, 'proposals', 'queue.json'));
|
|
195
|
-
assert.equal(queueFile.entries.find((item) => item.ticket === 'q-0001').status, 'approved');
|
|
196
|
-
|
|
197
|
-
const status = second.service.status();
|
|
198
|
-
assert.equal(status.proposals.pending, 0);
|
|
199
|
-
assert.equal(status.proposals.approved, 1);
|
|
200
|
-
assert.equal(status.proposals.superseded, 1);
|
|
201
|
-
assert.equal(status.queue.pending, 0);
|
|
202
|
-
|
|
203
|
-
const restored = auditRows(second.vaultDir).filter((row) => row.kind === 'queue.restored');
|
|
204
|
-
assert.ok(restored.length >= 1, 'a reconciliation must be audited');
|
|
205
|
-
assert.equal(restored.at(-1).written, true);
|
|
206
|
-
} finally {
|
|
207
|
-
await second.service.dispose();
|
|
208
|
-
}
|
|
209
|
-
} finally {
|
|
210
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test('v6b K-04: pending rows are cancellable by id and by ticket; superseded rows are terminal', async () => {
|
|
215
|
-
const temp = tempRoot('v6b-k04-cancel-');
|
|
216
|
-
const env = newEnv(temp);
|
|
217
|
-
try {
|
|
218
|
-
const started = await env.service.start();
|
|
219
|
-
assert.equal(started.ok, true, JSON.stringify(started));
|
|
220
|
-
const one = await propose(env.service, { title: 'keep', content: 'keep body' });
|
|
221
|
-
const two = await propose(env.service, { title: 'merge-2', content: 'merge body 2' });
|
|
222
|
-
const three = await propose(env.service, { title: 'merge-3', content: 'merge body 3' });
|
|
223
|
-
assert.equal(one.ok, true, JSON.stringify(one));
|
|
224
|
-
|
|
225
|
-
const list = env.service.proposalsView({ mode: 'list', limit: 20 }, { caller: CALLER });
|
|
226
|
-
assert.equal(list.ok, true);
|
|
227
|
-
assert.equal(list.proposals.length, 3);
|
|
228
|
-
const rows = new Map(list.proposals.map((row) => [row.ticket, row]));
|
|
229
|
-
assert.equal(rows.get('q-0001').state, 'pending');
|
|
230
|
-
assert.equal(rows.get('q-0001').cancellable, true);
|
|
231
|
-
assert.equal(rows.get('q-0001').terminal, false);
|
|
232
|
-
for (const absorbedId of [two.id, three.id]) {
|
|
233
|
-
const stored = readJson(path.join(env.vaultDir, 'proposals', `${absorbedId}.json`));
|
|
234
|
-
const row = rows.get(stored.queue?.ticket);
|
|
235
|
-
assert.ok(row, `absorbed row ${absorbedId} must stay listed`);
|
|
236
|
-
assert.equal(row.state, 'superseded');
|
|
237
|
-
assert.equal(row.cancellable, false);
|
|
238
|
-
assert.equal(row.terminal, true);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Cancel the pending row through the tool seam, addressed by ticket only.
|
|
242
|
-
const tools = createRouterTools(env.service);
|
|
243
|
-
const applyTool = tools.find((tool) => tool.name === 'router_apply');
|
|
244
|
-
assert.ok(applyTool, 'router_apply must exist');
|
|
245
|
-
const cancelled = await applyTool.invoke({ mode: 'cancel', ticket: 'q-0001', note: 'v6b' }, { caller: CALLER });
|
|
246
|
-
assert.equal(cancelled.ok, true, JSON.stringify(cancelled));
|
|
247
|
-
assert.equal(cancelled.state, 'cancelled');
|
|
248
|
-
assert.equal(readJson(path.join(env.vaultDir, 'proposals', `${one.id}.json`)).status, 'cancelled');
|
|
249
|
-
assert.ok(auditRows(env.vaultDir).some((row) => row.kind === 'proposal.cancel' && row.id === one.id));
|
|
250
|
-
|
|
251
|
-
// A terminal row cannot be cancelled again, and superseded rows never were
|
|
252
|
-
// cancellable: the criterion is "pending rows are cancellable", not "all rows".
|
|
253
|
-
const again = await env.service.requestApply({ mode: 'cancel', id: one.id }, { caller: CALLER });
|
|
254
|
-
assert.equal(again.ok, false);
|
|
255
|
-
assert.equal(again.code, 'E_BAD_STATE');
|
|
256
|
-
const supersededId = readJson(path.join(env.vaultDir, 'proposals', `${two.id}.json`)).id;
|
|
257
|
-
const rejected = await env.service.requestApply({ mode: 'cancel', id: supersededId }, { caller: CALLER });
|
|
258
|
-
assert.equal(rejected.ok, false);
|
|
259
|
-
assert.equal(rejected.code, 'E_BAD_STATE');
|
|
260
|
-
} finally {
|
|
261
|
-
await env.service.dispose();
|
|
262
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test('v6b K-04/L-03: router_proposals rows expose state+status and match status counts', async () => {
|
|
267
|
-
const temp = tempRoot('v6b-rows-');
|
|
268
|
-
const env = newEnv(temp);
|
|
269
|
-
try {
|
|
270
|
-
const started = await env.service.start();
|
|
271
|
-
assert.equal(started.ok, true, JSON.stringify(started));
|
|
272
|
-
const one = await propose(env.service, { title: 'rows', content: 'row body' });
|
|
273
|
-
const two = await propose(env.service, { title: 'rows-2', content: 'row body 2' });
|
|
274
|
-
assert.equal(one.ok, true, JSON.stringify(one));
|
|
275
|
-
|
|
276
|
-
const tools = createRouterTools(env.service);
|
|
277
|
-
const listTool = tools.find((tool) => tool.name === 'router_proposals');
|
|
278
|
-
const listed = await listTool.invoke({ mode: 'list', limit: 20 }, { caller: CALLER });
|
|
279
|
-
assert.equal(listed.ok, true, JSON.stringify(listed));
|
|
280
|
-
for (const row of listed.proposals) {
|
|
281
|
-
assert.equal(typeof row.state, 'string');
|
|
282
|
-
assert.equal(row.status, row.state, 'status must mirror state');
|
|
283
|
-
assert.equal(typeof row.cancellable, 'boolean');
|
|
284
|
-
assert.equal(typeof row.terminal, 'boolean');
|
|
285
|
-
}
|
|
286
|
-
const status = env.service.status();
|
|
287
|
-
assert.equal(listed.proposals.length, status.proposals.pending + status.proposals.approved + status.proposals.superseded);
|
|
288
|
-
assert.ok(listed.proposals.some((row) => row.ticket === 'q-0002' && row.state === 'superseded'));
|
|
289
|
-
} finally {
|
|
290
|
-
await env.service.dispose();
|
|
291
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
292
|
-
}
|
|
293
|
-
});
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
// 1.1.3 contract checks for the v7 gaps that 1.1.2 still exposed:
|
|
2
|
-
// P0 anchor off-by-one (restart with a ledger must not raise E_LEDGER_TAMPERED)
|
|
3
|
-
// F-03 audit.repair (a truncated half-line must leave an audit event)
|
|
4
|
-
// E-06/G-02/G-03 budget.reclaim (previous-run reservations are released and audited)
|
|
5
|
-
// D-05 routerTestHooks (full+reflect both over budget can be constructed; off by default)
|
|
6
|
-
//
|
|
7
|
-
// Everything is local temp storage; no host, no network, no real model.
|
|
8
|
-
|
|
9
|
-
import assert from 'node:assert/strict';
|
|
10
|
-
import fs from 'node:fs';
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
import test from 'node:test';
|
|
13
|
-
|
|
14
|
-
import { readAnchor } from '../../src/router/background/anchor.mjs';
|
|
15
|
-
import { AuditLog, readAudit } from '../../src/router/background/audit.mjs';
|
|
16
|
-
import { BudgetLedger, foldRecords, readLedger } from '../../src/router/background/budget-ledger.mjs';
|
|
17
|
-
import {
|
|
18
|
-
appendTask,
|
|
19
|
-
makeService,
|
|
20
|
-
taskInput,
|
|
21
|
-
tempRoot,
|
|
22
|
-
} from './helpers.mjs';
|
|
23
|
-
|
|
24
|
-
function auditRows(file) {
|
|
25
|
-
if (!fs.existsSync(file)) return [];
|
|
26
|
-
return fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function recordAt(records, seq) {
|
|
30
|
-
return records.find((record) => record.seq === seq) ?? null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function stopAndDispose(env) {
|
|
34
|
-
try { await env.service.stop(); } catch { /* best effort */ }
|
|
35
|
-
try { await env.service.dispose(); } catch { /* best effort */ }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
test('v6c P0: restart with a ledger is healthy; the anchor points at the last audit record', async () => {
|
|
39
|
-
const temp = tempRoot('v6c-p0-');
|
|
40
|
-
const first = makeService({ temp, deps: { runId: 'run-p0-a' } });
|
|
41
|
-
let second = null;
|
|
42
|
-
try {
|
|
43
|
-
const started = await first.service.start();
|
|
44
|
-
assert.equal(started.ok, true, JSON.stringify(started));
|
|
45
|
-
appendTask(first.service, taskInput('p0-task'));
|
|
46
|
-
const tick = await first.service.tick({ tier: 'full' });
|
|
47
|
-
assert.equal(tick.ok, true, JSON.stringify(tick));
|
|
48
|
-
|
|
49
|
-
const ledgerFile = path.join(temp, 'vault', 'runs', 'run-p0-a', 'budget.ledger.jsonl');
|
|
50
|
-
const folded = foldRecords(readLedger(ledgerFile).records);
|
|
51
|
-
assert.ok(folded.used > 0, 'the tick must settle real tokens, so the restart is "with a ledger"');
|
|
52
|
-
|
|
53
|
-
await stopAndDispose(first);
|
|
54
|
-
|
|
55
|
-
const auditFile = path.join(temp, 'vault', 'audit.jsonl');
|
|
56
|
-
const anchorFile = path.join(temp, 'vault', 'runs', 'run-p0-a', 'anchor.jsonl');
|
|
57
|
-
const auditRecords = readAudit(auditFile).records;
|
|
58
|
-
const anchorRecords = readAnchor(anchorFile).records;
|
|
59
|
-
const lastAnchor = anchorRecords[anchorRecords.length - 1];
|
|
60
|
-
assert.ok(lastAnchor, 'stop() must leave an anchor behind');
|
|
61
|
-
// The seal may happen before the final audit row of stop(), so check the
|
|
62
|
-
// prefix it claims: the seq must exist and its hash must match.
|
|
63
|
-
const claimed = recordAt(auditRecords, lastAnchor.audit.seq);
|
|
64
|
-
assert.ok(claimed, `anchor claims audit seq ${lastAnchor.audit.seq}, which does not exist (off-by-one)`);
|
|
65
|
-
assert.equal(claimed.hash, lastAnchor.audit.last_hash);
|
|
66
|
-
|
|
67
|
-
second = makeService({ temp, deps: { runId: 'run-p0-b' } });
|
|
68
|
-
const restarted = await second.service.start();
|
|
69
|
-
assert.equal(restarted.ok, true, JSON.stringify(restarted));
|
|
70
|
-
assert.equal(restarted.state, 'running');
|
|
71
|
-
assert.notEqual(restarted.code, 'E_LEDGER_TAMPERED');
|
|
72
|
-
assert.ok(!(second.service.status().warnings ?? []).includes('E_LEDGER_TAMPERED'));
|
|
73
|
-
} finally {
|
|
74
|
-
if (second) await stopAndDispose(second);
|
|
75
|
-
if (first.service.status().state === 'running') await stopAndDispose(first);
|
|
76
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('v6c F-03: a truncated audit half-line is dropped and recorded as audit.repair', async () => {
|
|
81
|
-
const temp = tempRoot('v6c-f03-');
|
|
82
|
-
const file = path.join(temp, 'audit.jsonl');
|
|
83
|
-
try {
|
|
84
|
-
const log = new AuditLog({ file, runId: 'run-f03' });
|
|
85
|
-
log.open();
|
|
86
|
-
log.append('test.before', { n: 1 });
|
|
87
|
-
log.append('test.before', { n: 2 });
|
|
88
|
-
log.close();
|
|
89
|
-
|
|
90
|
-
const partial = '{"v":1,"seq":99,"kind":"broken-tail"';
|
|
91
|
-
fs.appendFileSync(file, partial, 'utf8');
|
|
92
|
-
|
|
93
|
-
const reopened = new AuditLog({ file, runId: 'run-f03' });
|
|
94
|
-
reopened.open();
|
|
95
|
-
const afterOpen = readAudit(file);
|
|
96
|
-
assert.equal(afterOpen.badLines, 0);
|
|
97
|
-
assert.equal(afterOpen.truncated, 0);
|
|
98
|
-
assert.deepEqual(afterOpen.records.map((record) => record.seq), [1, 2, 3]);
|
|
99
|
-
const repaired = afterOpen.records[2];
|
|
100
|
-
assert.equal(repaired.kind, 'audit.repair');
|
|
101
|
-
assert.equal(repaired.bytes_dropped, Buffer.byteLength(partial));
|
|
102
|
-
assert.equal(repaired.reason, 'partial_tail');
|
|
103
|
-
|
|
104
|
-
// Later writes continue the sequence and stay parseable.
|
|
105
|
-
reopened.append('test.after', { n: 3 });
|
|
106
|
-
const afterWrite = readAudit(file);
|
|
107
|
-
assert.equal(afterWrite.badLines, 0);
|
|
108
|
-
assert.equal(afterWrite.records.length, 4);
|
|
109
|
-
assert.equal(afterWrite.records[3].seq, 4);
|
|
110
|
-
assert.equal(afterWrite.records[3].kind, 'test.after');
|
|
111
|
-
reopened.close();
|
|
112
|
-
|
|
113
|
-
// A clean reopen must not invent a repair event.
|
|
114
|
-
const clean = new AuditLog({ file, runId: 'run-f03' });
|
|
115
|
-
clean.open();
|
|
116
|
-
assert.equal(readAudit(file).records.length, 4);
|
|
117
|
-
clean.close();
|
|
118
|
-
} finally {
|
|
119
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('v6c E-06/G-02/G-03: boot reclaims previous-run reservations and audits budget.reclaim', async () => {
|
|
124
|
-
const temp = tempRoot('v6c-reclaim-');
|
|
125
|
-
const vaultDir = path.join(temp, 'vault');
|
|
126
|
-
const orphanDir = path.join(vaultDir, 'runs', 'run-orphan');
|
|
127
|
-
fs.mkdirSync(orphanDir, { recursive: true });
|
|
128
|
-
const orphanFile = path.join(orphanDir, 'budget.ledger.jsonl');
|
|
129
|
-
const tokens = 777;
|
|
130
|
-
|
|
131
|
-
// A crashed child left one reservation behind: its pid is still alive (ours),
|
|
132
|
-
// so the lease is what marks it dead.
|
|
133
|
-
const orphan = new BudgetLedger({
|
|
134
|
-
file: orphanFile,
|
|
135
|
-
runId: 'run-orphan',
|
|
136
|
-
maxTokens: 50000,
|
|
137
|
-
pid: process.pid,
|
|
138
|
-
reservationLeaseMs: 1,
|
|
139
|
-
});
|
|
140
|
-
const reserved = orphan.reserve({ taskId: 'crashed-task', tokens, leaseMs: 1 });
|
|
141
|
-
assert.equal(reserved.ok, true, JSON.stringify(reserved));
|
|
142
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
143
|
-
|
|
144
|
-
let first = null;
|
|
145
|
-
let second = null;
|
|
146
|
-
try {
|
|
147
|
-
first = makeService({
|
|
148
|
-
temp,
|
|
149
|
-
deps: { runId: 'run-after-crash-1' },
|
|
150
|
-
config: { allowMissingAnchor: true },
|
|
151
|
-
});
|
|
152
|
-
const started = await first.service.start();
|
|
153
|
-
assert.equal(started.ok, true, JSON.stringify(started));
|
|
154
|
-
assert.equal(started.state, 'running');
|
|
155
|
-
|
|
156
|
-
const audit = auditRows(path.join(vaultDir, 'audit.jsonl'));
|
|
157
|
-
const reclaims = audit.filter((row) => row.kind === 'budget.reclaim');
|
|
158
|
-
assert.equal(reclaims.length, 1, JSON.stringify(reclaims));
|
|
159
|
-
const [reclaim] = reclaims;
|
|
160
|
-
assert.equal(reclaim.origin, 'startup');
|
|
161
|
-
assert.equal(reclaim.released, 1);
|
|
162
|
-
assert.equal(reclaim.freed_tokens, tokens);
|
|
163
|
-
assert.equal(reclaim.charged, 0);
|
|
164
|
-
assert.equal(reclaim.runs.length, 1);
|
|
165
|
-
assert.equal(reclaim.runs[0].run_id, 'run-orphan');
|
|
166
|
-
assert.equal(reclaim.runs[0].scope, 'previous_run');
|
|
167
|
-
assert.equal(reclaim.runs[0].released, 1);
|
|
168
|
-
|
|
169
|
-
// The old run is back to zero: no charge, no active reservation.
|
|
170
|
-
const folded = foldRecords(readLedger(orphanFile).records);
|
|
171
|
-
assert.equal(folded.used, 0);
|
|
172
|
-
assert.equal(folded.reserved, 0);
|
|
173
|
-
assert.equal(folded.active.size, 0);
|
|
174
|
-
const release = readLedger(orphanFile).records.find((record) => record.op === 'release');
|
|
175
|
-
assert.ok(release, 'reclaim must append a release record');
|
|
176
|
-
assert.equal(release.released_tokens, tokens);
|
|
177
|
-
|
|
178
|
-
const status = first.service.status();
|
|
179
|
-
assert.equal(status.run.usedTokens, 0);
|
|
180
|
-
assert.equal(status.today.usedTokens, 0);
|
|
181
|
-
assert.ok(!audit.some((row) => row.kind === 'budget.settle' && row.run_id === 'run-orphan'));
|
|
182
|
-
|
|
183
|
-
await stopAndDispose(first);
|
|
184
|
-
first = null;
|
|
185
|
-
|
|
186
|
-
// A second restart has nothing left to reclaim: no duplicate billing event.
|
|
187
|
-
second = makeService({
|
|
188
|
-
temp,
|
|
189
|
-
deps: { runId: 'run-after-crash-2' },
|
|
190
|
-
config: { allowMissingAnchor: true },
|
|
191
|
-
});
|
|
192
|
-
const restarted = await second.service.start();
|
|
193
|
-
assert.equal(restarted.ok, true, JSON.stringify(restarted));
|
|
194
|
-
const reclaimsAgain = auditRows(path.join(vaultDir, 'audit.jsonl'))
|
|
195
|
-
.filter((row) => row.kind === 'budget.reclaim');
|
|
196
|
-
assert.equal(reclaimsAgain.length, 1, 'reclaim must be idempotent across restarts');
|
|
197
|
-
assert.equal(foldRecords(readLedger(orphanFile).records).used, 0);
|
|
198
|
-
} finally {
|
|
199
|
-
if (first) await stopAndDispose(first);
|
|
200
|
-
if (second) await stopAndDispose(second);
|
|
201
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test('v6c D-05: the test hook can force full+reflect over budget without touching a model', async () => {
|
|
206
|
-
const temp = tempRoot('v6c-d05-block-');
|
|
207
|
-
const env = makeService({
|
|
208
|
-
temp,
|
|
209
|
-
deps: { runId: 'run-d05-block', testHooks: { forceContextOverBudget: true } },
|
|
210
|
-
});
|
|
211
|
-
try {
|
|
212
|
-
const started = await env.service.start();
|
|
213
|
-
assert.equal(started.ok, true, JSON.stringify(started));
|
|
214
|
-
appendTask(env.service, taskInput('d05-task'));
|
|
215
|
-
const tick = await env.service.tick({ tier: 'full' });
|
|
216
|
-
assert.equal(tick.ok, true, JSON.stringify(tick));
|
|
217
|
-
|
|
218
|
-
const status = env.service.status();
|
|
219
|
-
assert.equal(status.state, 'blocked');
|
|
220
|
-
assert.equal(status.reason, 'E_CONTEXT_OVER_BUDGET');
|
|
221
|
-
assert.equal(env.llm.calls, 0, 'a blocked context must not call the model');
|
|
222
|
-
assert.equal(status.run.usedTokens, 0);
|
|
223
|
-
assert.equal(foldRecords(readLedger(path.join(temp, 'vault', 'runs', 'run-d05-block', 'budget.ledger.jsonl')).records).reserved, 0);
|
|
224
|
-
assert.ok(!auditRows(path.join(temp, 'vault', 'audit.jsonl'))
|
|
225
|
-
.some((row) => row.kind === 'budget.settle'));
|
|
226
|
-
} finally {
|
|
227
|
-
await stopAndDispose(env);
|
|
228
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test('v6c D-05: without the hook the same service runs normally (hook is off by default)', async () => {
|
|
233
|
-
const temp = tempRoot('v6c-d05-normal-');
|
|
234
|
-
const env = makeService({ temp, deps: { runId: 'run-d05-normal' } });
|
|
235
|
-
try {
|
|
236
|
-
const started = await env.service.start();
|
|
237
|
-
assert.equal(started.ok, true, JSON.stringify(started));
|
|
238
|
-
appendTask(env.service, taskInput('d05-normal'));
|
|
239
|
-
const tick = await env.service.tick({ tier: 'full' });
|
|
240
|
-
assert.equal(tick.ok, true, JSON.stringify(tick));
|
|
241
|
-
assert.equal(tick.processed, 1);
|
|
242
|
-
assert.equal(env.llm.calls, 1);
|
|
243
|
-
assert.equal(env.service.status().state, 'running');
|
|
244
|
-
assert.equal(env.service.status().reason, 'started');
|
|
245
|
-
} finally {
|
|
246
|
-
await stopAndDispose(env);
|
|
247
|
-
fs.rmSync(temp, { recursive: true, force: true });
|
|
248
|
-
}
|
|
249
|
-
});
|