@sema-agent/core 5.65.0 → 6.0.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 +46 -0
- package/dist/agents/subagent.d.ts +2 -2
- package/dist/agents/subagent.js +11 -0
- package/dist/agents/verify.d.ts +1 -1
- package/dist/brain/anthropic.js +1 -1
- package/dist/brain/errors.d.ts +29 -0
- package/dist/brain/errors.js +20 -0
- package/dist/brain/open-responses.js +2 -2
- package/dist/brain/route-adjudicator.d.ts +8 -1
- package/dist/brain/route-adjudicator.js +1 -0
- package/dist/brain/status-sink.js +12 -1
- package/dist/brain/stream-engine.js +17 -6
- package/dist/core/auto-compaction.d.ts +26 -0
- package/dist/core/auto-compaction.js +7 -2
- package/dist/core/auto-mode-arming.d.ts +138 -0
- package/dist/core/auto-mode-arming.js +181 -0
- package/dist/core/auto-mode-defaults.d.ts +13 -0
- package/dist/core/auto-mode-defaults.js +5 -0
- package/dist/core/auto-mode-prompt.d.ts +14 -3
- package/dist/core/auto-mode-prompt.js +10 -7
- package/dist/core/auto-mode-rebuild.d.ts +75 -0
- package/dist/core/auto-mode-rebuild.js +41 -0
- package/dist/core/auto-mode.d.ts +15 -0
- package/dist/core/auto-mode.js +4 -2
- package/dist/core/checkpoint-store.d.ts +18 -0
- package/dist/core/context-edit.d.ts +47 -5
- package/dist/core/context-guard.d.ts +1 -1
- package/dist/core/file-history-retention.d.ts +106 -0
- package/dist/core/file-history-retention.js +36 -0
- package/dist/core/file-history-store.d.ts +768 -0
- package/dist/core/file-history-store.js +880 -0
- package/dist/core/governance-codes.d.ts +2 -1
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.d.ts +39 -7
- package/dist/core/hooks.js +38 -21
- package/dist/core/lsp.d.ts +2 -2
- package/dist/core/mcp.d.ts +29 -7
- package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
- package/dist/core/memory-engine/consolidation-driver.js +71 -4
- package/dist/core/memory-engine/consolidation.d.ts +25 -2
- package/dist/core/memory-engine/consolidation.js +4 -1
- package/dist/core/memory-engine/distiller.d.ts +84 -1
- package/dist/core/memory-engine/distiller.js +68 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +328 -15
- package/dist/core/memory-engine/engine.js +355 -29
- package/dist/core/memory-engine/file-backend.d.ts +30 -0
- package/dist/core/memory-engine/file-backend.js +14 -13
- package/dist/core/memory-engine/frontmatter.d.ts +22 -1
- package/dist/core/memory-engine/frontmatter.js +3 -0
- package/dist/core/memory-engine/header-hints.d.ts +5 -0
- package/dist/core/memory-engine/index.d.ts +5 -4
- package/dist/core/memory-engine/index.js +5 -4
- package/dist/core/memory-engine/layout.d.ts +88 -2
- package/dist/core/memory-engine/layout.js +112 -3
- package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
- package/dist/core/memory-engine/provenance-wording.js +3 -0
- package/dist/core/memory-engine/tools.d.ts +89 -8
- package/dist/core/memory-engine/tools.js +263 -22
- package/dist/core/memory-engine/types.d.ts +64 -1
- package/dist/core/memory-recall.d.ts +6 -0
- package/dist/core/memory.d.ts +27 -1
- package/dist/core/memory.js +16 -2
- package/dist/core/permission-rule-consent.d.ts +20 -0
- package/dist/core/permission-rule-consent.js +12 -3
- package/dist/core/permission-rule-model.d.ts +67 -7
- package/dist/core/permission-rule-model.js +53 -7
- package/dist/core/permission-rule-store.js +15 -10
- package/dist/core/permission-rule-sync.js +15 -11
- package/dist/core/retention-policy.d.ts +9 -0
- package/dist/core/retention-policy.js +5 -2
- package/dist/core/retention.d.ts +13 -2
- package/dist/core/runner/assemble-result.d.ts +19 -1
- package/dist/core/runner/assemble-result.js +17 -2
- package/dist/core/runner/compaction-call-options.d.ts +93 -0
- package/dist/core/runner/compaction-call-options.js +3 -0
- package/dist/core/runner/memory-capture-optout.d.ts +80 -0
- package/dist/core/runner/memory-capture-optout.js +53 -0
- package/dist/core/runner/prepare-config-doors.d.ts +5 -0
- package/dist/core/runner/prepare-config-doors.js +16 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
- package/dist/core/runner/prepare-hands-readface.js +99 -7
- package/dist/core/runner/prepare-memory.d.ts +88 -0
- package/dist/core/runner/prepare-memory.js +305 -24
- package/dist/core/runner/prepare-task.d.ts +141 -1
- package/dist/core/runner/prepare-task.js +443 -79
- package/dist/core/runner/runtask.d.ts +9 -20
- package/dist/core/runner/runtask.js +133 -96
- package/dist/core/runner/session-file-state-replay.d.ts +18 -10
- package/dist/core/runner/session-file-state-replay.js +52 -1
- package/dist/core/runner/tool-disclosure.js +2 -1
- package/dist/core/runner/turn-attachments.d.ts +22 -12
- package/dist/core/session-store.d.ts +1 -1
- package/dist/core/session-store.js +6 -1
- package/dist/core/session.d.ts +34 -1
- package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/file-history-store-contract.js +720 -0
- package/dist/core/task-registry-shared.js +11 -1
- package/dist/core/tool-errors.js +1 -0
- package/dist/core/tool-policy.d.ts +172 -1
- package/dist/core/tool-policy.js +32 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +24 -0
- package/dist/core/types.d.ts +784 -89
- package/dist/core/types.js +4 -3
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/core/untrusted-text.js +8 -0
- package/dist/core/workflow-run-store-contract.js +6 -2
- package/dist/core/workflow-run-store.d.ts +4 -1
- package/dist/engine/compaction/compaction.d.ts +88 -10
- package/dist/engine/compaction/compaction.js +109 -30
- package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
- package/dist/engine/execution-env/node-execution-env.js +28 -0
- package/dist/engine/harness/agent-harness.d.ts +52 -1
- package/dist/engine/harness/agent-harness.js +36 -1
- package/dist/engine/harness/types.d.ts +26 -1
- package/dist/engine/llm/types.d.ts +50 -4
- package/dist/engine/loop/agent-loop.d.ts +5 -1
- package/dist/engine/loop/agent-loop.js +25 -0
- package/dist/engine/loop/types.d.ts +19 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
- package/dist/engine/session/session.js +1 -1
- package/dist/index.d.ts +18 -8
- package/dist/index.js +14 -6
- package/dist/orchestration/run-workflow-tool.d.ts +20 -2
- package/dist/orchestration/run-workflow-tool.js +22 -3
- package/dist/orchestration/workflow-governance.d.ts +59 -1
- package/dist/orchestration/workflow-governance.js +61 -8
- package/dist/orchestration/workflow-meta.d.ts +4 -2
- package/dist/orchestration/workflow-primitives.js +56 -13
- package/dist/orchestration/workflow-types.d.ts +78 -2
- package/dist/orchestration/workflow.d.ts +20 -0
- package/dist/orchestration/workflow.js +163 -14
- package/dist/prompt-assembly/event-registry.js +1 -1
- package/dist/prompts/default.d.ts +7 -7
- package/dist/stores/file/file-history-store.d.ts +368 -0
- package/dist/stores/file/file-history-store.js +1248 -0
- package/dist/stores/file/index.d.ts +22 -13
- package/dist/stores/file/index.js +4 -4
- package/dist/stores/file/permission-rule-store.js +1 -0
- package/dist/stores/file/strategy-store.d.ts +3 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
- package/dist/tools/fs/bash-readonly-classifier.js +106 -4
- package/dist/tools/fs/fs-bash.js +9 -5
- package/dist/tools/fs/fs-shared.d.ts +52 -1
- package/dist/tools/fs/fs-shared.js +14 -0
- package/dist/tools/fs/fs-write.d.ts +5 -5
- package/dist/tools/fs/fs-write.js +71 -14
- package/dist/tools/fs/index.d.ts +6 -1
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/web.js +2 -1
- package/package.json +5 -1
- package/test/export-surface.snapshot.json +155 -23
- package/dist/core/file-snapshot-store.d.ts +0 -165
- package/dist/core/file-snapshot-store.js +0 -259
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
- package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
- package/dist/stores/file/file-snapshot-store.d.ts +0 -58
- package/dist/stores/file/file-snapshot-store.js +0 -353
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { chmod, mkdtemp, mkdir, rm, stat, symlink, writeFile, readFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { NodeExecutionEnv } from "../../internal/harness.js";
|
|
7
|
+
import { beginContract } from "./contract-harness.js";
|
|
8
|
+
function probeOver(inner) {
|
|
9
|
+
const calls = new Map();
|
|
10
|
+
const paths = new Map();
|
|
11
|
+
const proxied = new Proxy(inner, {
|
|
12
|
+
get(target, prop, receiver) {
|
|
13
|
+
const v = Reflect.get(target, prop, receiver);
|
|
14
|
+
if (typeof v !== "function")
|
|
15
|
+
return v;
|
|
16
|
+
return (...args) => {
|
|
17
|
+
calls.set(String(prop), (calls.get(String(prop)) ?? 0) + 1);
|
|
18
|
+
if (typeof args[0] === "string")
|
|
19
|
+
paths.set(args[0], (paths.get(args[0]) ?? 0) + 1);
|
|
20
|
+
return v.apply(inner, args);
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
return { env: proxied, calls, touched: (p) => paths.get(p) ?? 0 };
|
|
25
|
+
}
|
|
26
|
+
const wireEnvelope = (raw) => raw;
|
|
27
|
+
function envWithFailingRead(inner, fail) {
|
|
28
|
+
return new Proxy(inner, {
|
|
29
|
+
get(target, prop, receiver) {
|
|
30
|
+
const v = Reflect.get(target, prop, receiver);
|
|
31
|
+
if (prop !== "readBinaryFile" || typeof v !== "function")
|
|
32
|
+
return v;
|
|
33
|
+
return async (...args) => fail(String(args[0]));
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function withTree(fn) {
|
|
38
|
+
const dir = await mkdtemp(join(tmpdir(), "fh-kit-"));
|
|
39
|
+
try {
|
|
40
|
+
await fn(dir, new NodeExecutionEnv({ cwd: dir }));
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await rm(dir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function fileHistoryStoreContract(make, runAssertion) {
|
|
47
|
+
const { run, settle } = beginContract(runAssertion);
|
|
48
|
+
run("absent everything: canRestore=false, restore=failed/not_found with an empty ledger, reap=0 — and none of it throws", async () => {
|
|
49
|
+
await withTree(async (dir, env) => {
|
|
50
|
+
const store = make();
|
|
51
|
+
assert.equal(await store.canRestore("sc", "e1"), false);
|
|
52
|
+
const r = await store.restore("sc", "e1", env, dir);
|
|
53
|
+
assert.equal(r.outcome, "failed");
|
|
54
|
+
assert.equal(r.error?.code, "not_found");
|
|
55
|
+
assert.deepEqual(r.filesChanged, []);
|
|
56
|
+
assert.deepEqual(r.refused, []);
|
|
57
|
+
assert.equal(await store.reap("sc", []), 0);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
run("first-touch idempotence: a second trackEdit for the same path is a cheap no-op (ZERO env reads)", async () => {
|
|
61
|
+
await withTree(async (dir, env) => {
|
|
62
|
+
const store = make();
|
|
63
|
+
await writeFile(join(dir, "f.txt"), "v1");
|
|
64
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "f.txt"), env, dir), { ok: true, minted: true }, "the FIRST touch reports minted:true — the caller's annul handle (#491)");
|
|
65
|
+
const probe = probeOver(env);
|
|
66
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "f.txt"), probe.env, dir), { ok: true, minted: false }, "a repeat must report minted:false — annulling it would discard the FIRST edit's baseline");
|
|
67
|
+
assert.equal(probe.calls.get("readBinaryFile") ?? 0, 0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
run("boundary commit visibility + idempotence: canRestore flips only on commit; a repeat snapshot over a COMMITTED boundary is ok with zero env reads", async () => {
|
|
71
|
+
await withTree(async (dir, env) => {
|
|
72
|
+
const store = make();
|
|
73
|
+
await writeFile(join(dir, "f.txt"), "v1");
|
|
74
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "f.txt"), env, dir), { ok: true, minted: true });
|
|
75
|
+
assert.equal(await store.canRestore("sc", "b1"), false);
|
|
76
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, dir), { ok: true });
|
|
77
|
+
assert.equal(await store.canRestore("sc", "b1"), true);
|
|
78
|
+
const probe = probeOver(env);
|
|
79
|
+
assert.deepEqual(await store.snapshot("sc", "b1", probe.env, dir), { ok: true });
|
|
80
|
+
assert.equal(probe.calls.get("readBinaryFile") ?? 0, 0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
run("fencing: an attempt whose signal fired NEVER publishes (canRestore stays false) — including a signal that fires DURING the capture while the env completes its read anyway", async () => {
|
|
84
|
+
await withTree(async (dir, env) => {
|
|
85
|
+
const store = make();
|
|
86
|
+
await writeFile(join(dir, "f.txt"), "v1");
|
|
87
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "f.txt"), env, dir), { ok: true, minted: true });
|
|
88
|
+
const pre = new AbortController();
|
|
89
|
+
pre.abort();
|
|
90
|
+
const r = await store.snapshot("sc", "bX", env, dir, pre.signal);
|
|
91
|
+
assert.equal(r.ok, false);
|
|
92
|
+
assert.equal(await store.canRestore("sc", "bX"), false);
|
|
93
|
+
const mid = new AbortController();
|
|
94
|
+
const abortDuringRead = new Proxy(env, {
|
|
95
|
+
get(target, prop, receiver) {
|
|
96
|
+
const v = Reflect.get(target, prop, receiver);
|
|
97
|
+
if (prop !== "readBinaryFile" || typeof v !== "function")
|
|
98
|
+
return v;
|
|
99
|
+
return async (...args) => {
|
|
100
|
+
mid.abort();
|
|
101
|
+
return v.apply(target, [args[0], undefined]);
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const r2 = await store.snapshot("sc", "bY", abortDuringRead, dir, mid.signal);
|
|
106
|
+
assert.equal(r2.ok, false);
|
|
107
|
+
assert.equal(await store.canRestore("sc", "bY"), false);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
run("capture lease (§10 S-6): a MID-SET failure (second file's read) spends the lease — no boundary appears (catches per-file publishers), and the retry performs ZERO env calls of ANY kind", async () => {
|
|
111
|
+
await withTree(async (dir, env) => {
|
|
112
|
+
const store = make();
|
|
113
|
+
await writeFile(join(dir, "a.txt"), "a1");
|
|
114
|
+
await writeFile(join(dir, "b.txt"), "b1");
|
|
115
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "a.txt"), env, dir), { ok: true, minted: true });
|
|
116
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "b.txt"), env, dir), { ok: true, minted: true });
|
|
117
|
+
let reads = 0;
|
|
118
|
+
const brokenSecond = new Proxy(env, {
|
|
119
|
+
get(target, prop, receiver) {
|
|
120
|
+
const v = Reflect.get(target, prop, receiver);
|
|
121
|
+
if (prop !== "readBinaryFile" || typeof v !== "function")
|
|
122
|
+
return v;
|
|
123
|
+
return async (...args) => {
|
|
124
|
+
if (reads++ >= 1)
|
|
125
|
+
return { ok: false, error: { code: "unknown", message: "injected IO fault" } };
|
|
126
|
+
return v.apply(target, args);
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
assert.equal((await store.snapshot("sc", "bL", brokenSecond, dir)).ok, false);
|
|
131
|
+
assert.equal(await store.canRestore("sc", "bL"), false);
|
|
132
|
+
const probe = probeOver(env);
|
|
133
|
+
const retry = await store.snapshot("sc", "bL", probe.env, dir);
|
|
134
|
+
assert.equal(retry.ok, false);
|
|
135
|
+
const totalCalls = [...probe.calls.values()].reduce((a, b) => a + b, 0);
|
|
136
|
+
assert.equal(totalCalls, 0, `retry must not touch the env AT ALL (saw: ${[...probe.calls.keys()].join(", ")})`);
|
|
137
|
+
assert.equal(await store.canRestore("sc", "bL"), false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
run("v1 permanence across reap: the pristine fallback outlives every boundary (b0-before-track ⇒ restore b0 walks back to pristine after later boundaries were reaped)", async () => {
|
|
141
|
+
await withTree(async (dir, env) => {
|
|
142
|
+
const store = make();
|
|
143
|
+
assert.deepEqual(await store.snapshot("sc", "b0", env, dir), { ok: true });
|
|
144
|
+
await writeFile(join(dir, "f.txt"), "pristine");
|
|
145
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "f.txt"), env, dir), { ok: true, minted: true });
|
|
146
|
+
await writeFile(join(dir, "f.txt"), "edited");
|
|
147
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, dir), { ok: true });
|
|
148
|
+
assert.equal(await store.reap("sc", ["b0"]), 1);
|
|
149
|
+
assert.equal(await store.canRestore("sc", "b1"), false);
|
|
150
|
+
const r = await store.restore("sc", "b0", env, dir);
|
|
151
|
+
assert.equal(r.outcome, "complete");
|
|
152
|
+
assert.equal(await readFile(join(dir, "f.txt"), "utf8"), "pristine");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
run("deletion-marker round trip: a deleted tracked file revives at an earlier boundary; an agent-created file dies at a pre-creation boundary", async () => {
|
|
156
|
+
await withTree(async (dir, env) => {
|
|
157
|
+
const store = make();
|
|
158
|
+
await writeFile(join(dir, "keep.txt"), "keep-v1");
|
|
159
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "keep.txt"), env, dir), { ok: true, minted: true });
|
|
160
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "born.txt"), env, dir), { ok: true, minted: true });
|
|
161
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, dir), { ok: true });
|
|
162
|
+
await writeFile(join(dir, "born.txt"), "created");
|
|
163
|
+
await rm(join(dir, "keep.txt"));
|
|
164
|
+
assert.deepEqual(await store.snapshot("sc", "b2", env, dir), { ok: true });
|
|
165
|
+
let r = await store.restore("sc", "b1", env, dir);
|
|
166
|
+
assert.equal(r.outcome, "complete");
|
|
167
|
+
assert.equal(await readFile(join(dir, "keep.txt"), "utf8"), "keep-v1");
|
|
168
|
+
assert.equal(await readFile(join(dir, "born.txt"), "utf8").then(() => true, () => false), false);
|
|
169
|
+
r = await store.restore("sc", "b2", env, dir);
|
|
170
|
+
assert.equal(r.outcome, "complete");
|
|
171
|
+
assert.equal(await readFile(join(dir, "keep.txt"), "utf8").then(() => true, () => false), false);
|
|
172
|
+
assert.equal(await readFile(join(dir, "born.txt"), "utf8"), "created");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
run("adoptScope: same-scope no-op; adoption copies the graph; EMPTY-TARGET ONLY (typed conflict refusal, target intact)", async () => {
|
|
176
|
+
await withTree(async (dir, env) => {
|
|
177
|
+
const store = make();
|
|
178
|
+
await writeFile(join(dir, "f.txt"), "v1");
|
|
179
|
+
assert.deepEqual(await store.trackEdit("old", join(dir, "f.txt"), env, dir), { ok: true, minted: true });
|
|
180
|
+
await writeFile(join(dir, "f.txt"), "v2");
|
|
181
|
+
assert.deepEqual(await store.snapshot("old", "b", env, dir), { ok: true });
|
|
182
|
+
assert.deepEqual(await store.adoptScope("old", "old"), { ok: true });
|
|
183
|
+
assert.deepEqual(await store.adoptScope("old", "fresh"), { ok: true });
|
|
184
|
+
assert.equal(await store.canRestore("fresh", "b"), true);
|
|
185
|
+
const busy = await store.adoptScope("old", "fresh");
|
|
186
|
+
assert.equal(busy.ok, false);
|
|
187
|
+
if (!busy.ok)
|
|
188
|
+
assert.equal(busy.error.code, "conflict");
|
|
189
|
+
assert.equal(await store.canRestore("fresh", "b"), true);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
run("structural out-of-scope (cowork guarantee) + T-1 positive control + G4 zero listDir: restore IO lands ONLY on tracked files, and no verb enumerates directories", async () => {
|
|
193
|
+
await withTree(async (dir, env) => {
|
|
194
|
+
const store = make();
|
|
195
|
+
await writeFile(join(dir, "tracked.txt"), "v1");
|
|
196
|
+
await writeFile(join(dir, "user-parallel.txt"), "user-work");
|
|
197
|
+
const probe = probeOver(env);
|
|
198
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "tracked.txt"), probe.env, dir), { ok: true, minted: true });
|
|
199
|
+
await writeFile(join(dir, "tracked.txt"), "v2");
|
|
200
|
+
assert.deepEqual(await store.snapshot("sc", "b", probe.env, dir), { ok: true });
|
|
201
|
+
await writeFile(join(dir, "tracked.txt"), "diverged");
|
|
202
|
+
await writeFile(join(dir, "user-new.txt"), "created-after-boundary");
|
|
203
|
+
const r = await store.restore("sc", "b", probe.env, dir);
|
|
204
|
+
assert.equal(r.outcome, "complete");
|
|
205
|
+
assert.ok(probe.touched(join(dir, "tracked.txt")) > 0, "T-1 positive control: the tracked target must show IO in the SAME probe");
|
|
206
|
+
assert.equal(probe.touched(join(dir, "user-parallel.txt")), 0, "a user's parallel file must be structurally invisible");
|
|
207
|
+
assert.equal(probe.touched(join(dir, "user-new.txt")), 0, "a post-boundary user file must be structurally invisible");
|
|
208
|
+
assert.equal(probe.calls.get("listDir") ?? 0, 0, "G4: zero directory enumeration across every verb");
|
|
209
|
+
assert.equal(await readFile(join(dir, "tracked.txt"), "utf8"), "v2");
|
|
210
|
+
assert.equal(await readFile(join(dir, "user-parallel.txt"), "utf8"), "user-work");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
run("annulTrack (#491): the caller's own mint is retractable with ZERO env IO and the path goes structurally invisible again; an absent record is idempotent-ok", async () => {
|
|
214
|
+
await withTree(async (dir, env) => {
|
|
215
|
+
const store = make();
|
|
216
|
+
await writeFile(join(dir, "anchor.txt"), "anchor-v1");
|
|
217
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "anchor.txt"), env, dir), { ok: true, minted: true });
|
|
218
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, dir), { ok: true });
|
|
219
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "never-written.txt"), env, dir), { ok: true, minted: true });
|
|
220
|
+
const probe = probeOver(env);
|
|
221
|
+
assert.deepEqual(await store.annulTrack("sc", join(dir, "never-written.txt"), dir), { ok: true });
|
|
222
|
+
assert.equal([...probe.calls.values()].reduce((a, b) => a + b, 0), 0, "annulTrack without a verify env is a pure record discard — it must perform NO env IO");
|
|
223
|
+
assert.deepEqual(await store.annulTrack("sc", join(dir, "never-written.txt"), dir), { ok: true }, "annulling an absent record is idempotent-ok (the retry arm)");
|
|
224
|
+
await writeFile(join(dir, "never-written.txt"), "USER BYTES");
|
|
225
|
+
await writeFile(join(dir, "anchor.txt"), "anchor-edited");
|
|
226
|
+
const r = await store.restore("sc", "b1", env, dir);
|
|
227
|
+
assert.equal(r.outcome, "complete");
|
|
228
|
+
assert.deepEqual(r.refused, []);
|
|
229
|
+
assert.equal(await readFile(join(dir, "never-written.txt"), "utf8"), "USER BYTES", "the annulled path is structurally out of scope again — restore must not delete it");
|
|
230
|
+
assert.equal(r.filesChanged.some((p) => p.includes("never-written")), false, "an annulled path must not even appear in the ledger");
|
|
231
|
+
assert.equal(await readFile(join(dir, "anchor.txt"), "utf8"), "anchor-v1", "positive control: the genuinely tracked file DID converge in the same restore");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
run("annulTrack EVIDENCE arm: with an env supplied, a path whose bytes moved since the mint keeps its record (deleting a modified file's only baseline is worse than a stale record); an unchanged path still retracts", async () => {
|
|
235
|
+
await withTree(async (dir, env) => {
|
|
236
|
+
const store = make();
|
|
237
|
+
await writeFile(join(dir, "moved.txt"), "pristine");
|
|
238
|
+
await writeFile(join(dir, "still.txt"), "pristine");
|
|
239
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "moved.txt"), env, dir), { ok: true, minted: true });
|
|
240
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "still.txt"), env, dir), { ok: true, minted: true });
|
|
241
|
+
await writeFile(join(dir, "moved.txt"), "half-written by the failing write");
|
|
242
|
+
const refused = await store.annulTrack("sc", join(dir, "moved.txt"), dir, { env });
|
|
243
|
+
assert.equal(refused.ok, false, "a changed file keeps its baseline");
|
|
244
|
+
if (!refused.ok)
|
|
245
|
+
assert.equal(refused.error.code, "conflict");
|
|
246
|
+
assert.deepEqual(await store.annulTrack("sc", join(dir, "still.txt"), dir, { env }), { ok: true });
|
|
247
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, dir), { ok: true });
|
|
248
|
+
await writeFile(join(dir, "moved.txt"), "diverged");
|
|
249
|
+
await writeFile(join(dir, "still.txt"), "USER BYTES");
|
|
250
|
+
const r = await store.restore("sc", "b1", env, dir);
|
|
251
|
+
assert.equal(await readFile(join(dir, "moved.txt"), "utf8"), "half-written by the failing write", "the kept record still protects the modified file");
|
|
252
|
+
assert.equal(await readFile(join(dir, "still.txt"), "utf8"), "USER BYTES", "the retracted one is structurally out of scope again");
|
|
253
|
+
assert.equal(r.outcome, "complete");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
run("annulTrack EVIDENCE arm, base-unavailable case: the DV-14 marker is the refusal state itself, so an ambiguous retraction may not erase it (the PROVEN path still may)", async () => {
|
|
257
|
+
await withTree(async (dir, env) => {
|
|
258
|
+
const store = make();
|
|
259
|
+
assert.deepEqual(await store.snapshot("sc", "b0", env, dir), { ok: true });
|
|
260
|
+
await writeFile(join(dir, "opaque.txt"), "true-pristine");
|
|
261
|
+
await writeFile(join(dir, "opaque2.txt"), "true-pristine");
|
|
262
|
+
const hostile = envWithFailingRead(env, async () => ({ ok: false, error: { code: "unknown", message: "pristine unreadable" } }));
|
|
263
|
+
const t = await store.trackEdit("sc", join(dir, "opaque.txt"), hostile, dir);
|
|
264
|
+
if (t.ok === false)
|
|
265
|
+
return;
|
|
266
|
+
const refused = await store.annulTrack("sc", join(dir, "opaque.txt"), dir, { env });
|
|
267
|
+
assert.equal(refused.ok, false, "an ambiguous retraction must not erase a base-unavailable marker");
|
|
268
|
+
if (!refused.ok)
|
|
269
|
+
assert.equal(refused.error.code, "conflict");
|
|
270
|
+
const t2 = await store.trackEdit("sc", join(dir, "opaque2.txt"), hostile, dir);
|
|
271
|
+
if (t2.ok)
|
|
272
|
+
assert.deepEqual(await store.annulTrack("sc", join(dir, "opaque2.txt"), dir), { ok: true });
|
|
273
|
+
await writeFile(join(dir, "opaque.txt"), "agent-edited");
|
|
274
|
+
const r = await store.restore("sc", "b0", env, dir);
|
|
275
|
+
assert.equal(r.refused.some((f) => f.reason.includes("base_unavailable")), true, `the kept marker must still refuse a below-base restore: ${JSON.stringify(r)}`);
|
|
276
|
+
assert.equal(await readFile(join(dir, "opaque.txt"), "utf8"), "agent-edited", "never overwritten with a fake baseline");
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
run("annulTrack refuses what is no longer a mint: a boundary-referenced record is history (typed conflict, record intact)", async () => {
|
|
280
|
+
await withTree(async (dir, env) => {
|
|
281
|
+
const store = make();
|
|
282
|
+
await writeFile(join(dir, "f.txt"), "pristine");
|
|
283
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "f.txt"), env, dir), { ok: true, minted: true });
|
|
284
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, dir), { ok: true });
|
|
285
|
+
const busy = await store.annulTrack("sc", join(dir, "f.txt"), dir);
|
|
286
|
+
assert.equal(busy.ok, false, "a record a committed boundary references is not the caller's to withdraw");
|
|
287
|
+
if (!busy.ok)
|
|
288
|
+
assert.equal(busy.error.code, "conflict");
|
|
289
|
+
await writeFile(join(dir, "f.txt"), "diverged");
|
|
290
|
+
const r = await store.restore("sc", "b1", env, dir);
|
|
291
|
+
assert.equal(r.outcome, "complete");
|
|
292
|
+
assert.equal(await readFile(join(dir, "f.txt"), "utf8"), "pristine", "the refusal left the record fully intact");
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
run("never-throw against a hostile env: a rejecting readBinaryFile yields typed results on every verb, never an exception", async () => {
|
|
296
|
+
await withTree(async (dir, env) => {
|
|
297
|
+
const store = make();
|
|
298
|
+
await writeFile(join(dir, "f.txt"), "v1");
|
|
299
|
+
const hostile = envWithFailingRead(env, async () => {
|
|
300
|
+
throw new Error("hostile env rejection");
|
|
301
|
+
});
|
|
302
|
+
const t = await store.trackEdit("sc", join(dir, "f.txt"), hostile, dir);
|
|
303
|
+
assert.equal(typeof t.ok, "boolean");
|
|
304
|
+
const sHealthy = await store.trackEdit("sc2", join(dir, "f.txt"), env, dir);
|
|
305
|
+
assert.deepEqual(sHealthy, { ok: true, minted: true });
|
|
306
|
+
const snap = await store.snapshot("sc2", "b", hostile, dir);
|
|
307
|
+
assert.equal(snap.ok, false);
|
|
308
|
+
const r = await store.restore("sc2", "nope", hostile, dir);
|
|
309
|
+
assert.equal(r.outcome, "failed");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
run("DV-14 marker honesty: a backend that TOLERATES an uncapturable pristine (trackEdit ok despite a hostile read) must REFUSE a below-base restore for that path — never silently omit it, never overwrite with a fake baseline", async () => {
|
|
313
|
+
await withTree(async (dir, env) => {
|
|
314
|
+
const store = make();
|
|
315
|
+
assert.deepEqual(await store.snapshot("sc", "b0", env, dir), { ok: true });
|
|
316
|
+
await writeFile(join(dir, "f.txt"), "true-pristine");
|
|
317
|
+
const hostile = envWithFailingRead(env, async () => ({ ok: false, error: { code: "unknown", message: "pristine unreadable" } }));
|
|
318
|
+
const t = await store.trackEdit("sc", join(dir, "f.txt"), hostile, dir);
|
|
319
|
+
if (t.ok === false)
|
|
320
|
+
return;
|
|
321
|
+
await writeFile(join(dir, "f.txt"), "agent-edited");
|
|
322
|
+
const r = await store.restore("sc", "b0", env, dir);
|
|
323
|
+
assert.equal(r.outcome, "partial", "a below-base path must surface as a REFUSAL, not fold into complete");
|
|
324
|
+
assert.equal(r.refused.some((f) => f.reason.includes("base_unavailable")), true, "the refusal must name base_unavailable");
|
|
325
|
+
assert.equal(await readFile(join(dir, "f.txt"), "utf8"), "agent-edited");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
run("track-key COORDINATE (A-082 idx5): an under-root capture re-joins against the CURRENT root (a legal whole-root move still restores, and the OLD root is not written), while an outside-root path is recorded verbatim-absolute and restores in place", async () => {
|
|
329
|
+
await withTree(async (dirA, envA) => {
|
|
330
|
+
await withTree(async (dirB) => {
|
|
331
|
+
await withTree(async (outside, _envOut) => {
|
|
332
|
+
void _envOut;
|
|
333
|
+
const store = make();
|
|
334
|
+
const outsideFile = join(outside, "o.txt");
|
|
335
|
+
await mkdir(join(dirA, "sub"), { recursive: true });
|
|
336
|
+
await writeFile(join(dirA, "sub", "f.txt"), "P-under-root");
|
|
337
|
+
await writeFile(outsideFile, "P-outside-root");
|
|
338
|
+
assert.deepEqual(await store.trackEdit("sc", join(dirA, "sub", "f.txt"), envA, dirA), { ok: true, minted: true });
|
|
339
|
+
assert.deepEqual(await store.trackEdit("sc", outsideFile, envA, dirA), { ok: true, minted: true });
|
|
340
|
+
assert.deepEqual(await store.snapshot("sc", "b1", envA, dirA), { ok: true });
|
|
341
|
+
await writeFile(join(dirA, "sub", "f.txt"), "EDITED");
|
|
342
|
+
await writeFile(outsideFile, "EDITED");
|
|
343
|
+
await mkdir(join(dirB, "sub"), { recursive: true });
|
|
344
|
+
await writeFile(join(dirB, "sub", "f.txt"), "EDITED");
|
|
345
|
+
const envB = new NodeExecutionEnv({ cwd: dirB });
|
|
346
|
+
const r = await store.restore("sc", "b1", envB, dirB);
|
|
347
|
+
assert.equal(r.outcome, "complete", `moved-root restore must converge, got ${JSON.stringify(r)}`);
|
|
348
|
+
assert.equal(await readFile(join(dirB, "sub", "f.txt"), "utf8"), "P-under-root", "the under-root key must re-join against the CURRENT root");
|
|
349
|
+
assert.equal(await readFile(join(dirA, "sub", "f.txt"), "utf8"), "EDITED", "the OLD root must not be written (the key was rel, not absolute)");
|
|
350
|
+
assert.equal(await readFile(outsideFile, "utf8"), "P-outside-root", "an outside-root key is verbatim-absolute — it restores where it was captured, root move or not");
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
run("export/import round trip AFTER a reap: the complete retained state travels (v1 fallbacks, parent guards, key self-description) and the importing scope serves BOTH a boundary restore and the pristine fallback", async () => {
|
|
356
|
+
await withTree(async (dir, env) => {
|
|
357
|
+
const src = make();
|
|
358
|
+
if (src.exportHistory === undefined || src.importHistory === undefined)
|
|
359
|
+
return;
|
|
360
|
+
const shed = contentShed();
|
|
361
|
+
await shed.write(join(dir, "keep.txt"), "pristine");
|
|
362
|
+
assert.deepEqual(await src.snapshot("sc", "b0", env, dir), { ok: true });
|
|
363
|
+
assert.deepEqual(await src.trackEdit("sc", join(dir, "keep.txt"), env, dir), { ok: true, minted: true });
|
|
364
|
+
await shed.write(join(dir, "keep.txt"), "turn-1");
|
|
365
|
+
assert.deepEqual(await src.snapshot("sc", "b1", env, dir), { ok: true });
|
|
366
|
+
assert.equal(await src.reap("sc", ["b0", "b1"]), 0);
|
|
367
|
+
const envelope = await src.exportHistory("sc");
|
|
368
|
+
assert.ok(envelope !== null, "a scope with history must export an envelope");
|
|
369
|
+
assert.equal(envelope.formatVersion, "file-history/1");
|
|
370
|
+
assert.ok(envelope.tracked.every((t) => (t.keyKind === "root-relative" || t.keyKind === "absolute") && (t.keyFamily === "posix" || t.keyFamily === "win")), "every tracked row must carry its key's self-description (kind + family) — a bare key cannot be judged after it crosses machines");
|
|
371
|
+
const dst = make();
|
|
372
|
+
const carried = await dst.importHistory("carried", envelope, shed.fetch);
|
|
373
|
+
assert.deepEqual(carried, { ok: true });
|
|
374
|
+
assert.equal(await dst.canRestore("carried", "b1"), true);
|
|
375
|
+
await writeFile(join(dir, "keep.txt"), "diverged");
|
|
376
|
+
const r1 = await dst.restore("carried", "b1", env, dir);
|
|
377
|
+
assert.equal(r1.outcome, "complete", `imported boundary restore: ${JSON.stringify(r1)}`);
|
|
378
|
+
assert.equal(await readFile(join(dir, "keep.txt"), "utf8"), "turn-1");
|
|
379
|
+
const r0 = await dst.restore("carried", "b0", env, dir);
|
|
380
|
+
assert.equal(r0.outcome, "complete");
|
|
381
|
+
assert.equal(await readFile(join(dir, "keep.txt"), "utf8"), "pristine", "the v1 fallback travelled: a boundary that predates the track still restores pristine");
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
run("import door refuses, never coerces: a foreign formatVersion and an unknown key self-description are typed unsupported_format with NOTHING published", async () => {
|
|
385
|
+
await withTree(async (_dir, env) => {
|
|
386
|
+
void _dir;
|
|
387
|
+
const store = make();
|
|
388
|
+
if (store.importHistory === undefined)
|
|
389
|
+
return;
|
|
390
|
+
const foreignEpoch = wireEnvelope({ formatVersion: "file-snapshot/1", tracked: [], boundaries: [] });
|
|
391
|
+
const v = await store.importHistory("sc", foreignEpoch, async () => undefined);
|
|
392
|
+
assert.equal(v.ok, false);
|
|
393
|
+
if (!v.ok)
|
|
394
|
+
assert.equal(v.error.code, "unsupported_format");
|
|
395
|
+
const unknownFamily = wireEnvelope({
|
|
396
|
+
formatVersion: "file-history/1",
|
|
397
|
+
tracked: [{ relOrAbsPath: "a.txt", keyKind: "root-relative", keyFamily: "vms", baseAvailable: true, versions: [{ version: 1, blobHash: null, backupTime: "t" }] }],
|
|
398
|
+
boundaries: [{ entryId: "b1", files: [{ relOrAbsPath: "a.txt", version: 1 }] }],
|
|
399
|
+
});
|
|
400
|
+
const f = await store.importHistory("sc", unknownFamily, async () => undefined);
|
|
401
|
+
assert.equal(f.ok, false);
|
|
402
|
+
if (!f.ok)
|
|
403
|
+
assert.equal(f.error.code, "unsupported_format");
|
|
404
|
+
assert.equal(await store.canRestore("sc", "b1"), false, "a refused import must publish nothing at all");
|
|
405
|
+
const missingBlob = wireEnvelope({
|
|
406
|
+
formatVersion: "file-history/1",
|
|
407
|
+
tracked: [{ relOrAbsPath: "a.txt", keyKind: "root-relative", keyFamily: "posix", baseAvailable: true, versions: [{ version: 1, blobHash: "a".repeat(64), backupTime: "t" }] }],
|
|
408
|
+
boundaries: [{ entryId: "b2", files: [{ relOrAbsPath: "a.txt", version: 1 }] }],
|
|
409
|
+
});
|
|
410
|
+
const m = await store.importHistory("sc", missingBlob, async () => undefined);
|
|
411
|
+
assert.equal(m.ok, false);
|
|
412
|
+
assert.equal(await store.canRestore("sc", "b2"), false);
|
|
413
|
+
const duplicated = wireEnvelope({
|
|
414
|
+
formatVersion: "file-history/1",
|
|
415
|
+
tracked: [
|
|
416
|
+
{ relOrAbsPath: "dup.txt", keyKind: "root-relative", keyFamily: "posix", baseAvailable: true, versions: [{ version: 1, blobHash: null, backupTime: "t" }] },
|
|
417
|
+
{ relOrAbsPath: "dup.txt", keyKind: "root-relative", keyFamily: "posix", baseAvailable: true, versions: [{ version: 1, blobHash: null, backupTime: "t" }, { version: 2, blobHash: null, backupTime: "t" }] },
|
|
418
|
+
],
|
|
419
|
+
boundaries: [{ entryId: "b3", files: [{ relOrAbsPath: "dup.txt", version: 1 }] }],
|
|
420
|
+
});
|
|
421
|
+
const outOfOrder = wireEnvelope({
|
|
422
|
+
formatVersion: "file-history/1",
|
|
423
|
+
tracked: [
|
|
424
|
+
{
|
|
425
|
+
relOrAbsPath: "ooo.txt",
|
|
426
|
+
keyKind: "root-relative",
|
|
427
|
+
keyFamily: "posix",
|
|
428
|
+
baseAvailable: true,
|
|
429
|
+
versions: [
|
|
430
|
+
{ version: 1, blobHash: null, backupTime: "t" },
|
|
431
|
+
{ version: 3, blobHash: null, backupTime: "t" },
|
|
432
|
+
{ version: 2, blobHash: null, backupTime: "t" },
|
|
433
|
+
],
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
boundaries: [{ entryId: "b4", files: [{ relOrAbsPath: "ooo.txt", version: 1 }] }],
|
|
437
|
+
});
|
|
438
|
+
const o = await store.importHistory("sc", outOfOrder, async () => undefined);
|
|
439
|
+
assert.equal(o.ok, false, "an out-of-order version chain must be refused — a later local boundary would reference an ambiguous version");
|
|
440
|
+
if (!o.ok)
|
|
441
|
+
assert.match(o.error.message, /strictly increasing/);
|
|
442
|
+
assert.equal(await store.canRestore("sc", "b4"), false);
|
|
443
|
+
const d = await store.importHistory("sc", duplicated, async () => undefined);
|
|
444
|
+
assert.equal(d.ok, false, "duplicate tracked rows must be refused — the published graph would differ from the validated one");
|
|
445
|
+
if (!d.ok)
|
|
446
|
+
assert.match(d.error.message, /twice/);
|
|
447
|
+
assert.equal(await store.canRestore("sc", "b3"), false);
|
|
448
|
+
void env;
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
run("import AUTHORITY BOUND: an envelope may not carry an absolute key or an escaping relative one — a graph that would grant restore authority over a path outside the importing root publishes NOTHING", async () => {
|
|
452
|
+
await withTree(async (dir, env) => {
|
|
453
|
+
const store = make();
|
|
454
|
+
if (store.importHistory === undefined)
|
|
455
|
+
return;
|
|
456
|
+
const outsideVictim = join(dir, "victim-outside.txt");
|
|
457
|
+
await writeFile(outsideVictim, "not the agent's file");
|
|
458
|
+
const absolutePayload = wireEnvelope({
|
|
459
|
+
formatVersion: "file-history/1",
|
|
460
|
+
tracked: [{ relOrAbsPath: outsideVictim, keyKind: "absolute", keyFamily: "posix", baseAvailable: true, versions: [{ version: 1, blobHash: null, backupTime: "t" }] }],
|
|
461
|
+
boundaries: [{ entryId: "bA", files: [{ relOrAbsPath: outsideVictim, version: 1 }] }],
|
|
462
|
+
});
|
|
463
|
+
const a = await store.importHistory("hostile", absolutePayload, async () => undefined);
|
|
464
|
+
assert.equal(a.ok, false, "an absolute imported key must be refused at the door");
|
|
465
|
+
if (!a.ok) {
|
|
466
|
+
assert.equal(a.error.code, "unsupported_format");
|
|
467
|
+
assert.match(a.error.message, /ABSOLUTE key/, `the refusal must name the authority bound: ${a.error.message}`);
|
|
468
|
+
}
|
|
469
|
+
const escaping = wireEnvelope({
|
|
470
|
+
formatVersion: "file-history/1",
|
|
471
|
+
tracked: [{ relOrAbsPath: "../escape.txt", keyKind: "root-relative", keyFamily: "posix", baseAvailable: true, versions: [{ version: 1, blobHash: null, backupTime: "t" }] }],
|
|
472
|
+
boundaries: [{ entryId: "bB", files: [{ relOrAbsPath: "../escape.txt", version: 1 }] }],
|
|
473
|
+
});
|
|
474
|
+
const b = await store.importHistory("hostile", escaping, async () => undefined);
|
|
475
|
+
assert.equal(b.ok, false, "a '..' relative key must be refused at the door, not per-file at restore");
|
|
476
|
+
assert.equal(await store.canRestore("hostile", "bA"), false);
|
|
477
|
+
assert.equal(await store.canRestore("hostile", "bB"), false);
|
|
478
|
+
const r = await store.restore("hostile", "bA", env, dir);
|
|
479
|
+
assert.equal(r.outcome, "failed");
|
|
480
|
+
assert.equal(await readFile(outsideVictim, "utf8"), "not the agent's file", "the victim path was never in reach");
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
run("annulTrack refuses a mint ANOTHER lane is already relying on: two edits race one path, the first fails its write, and the second's baseline survives", async () => {
|
|
484
|
+
await withTree(async (dir, env) => {
|
|
485
|
+
const store = make();
|
|
486
|
+
await writeFile(join(dir, "contended.txt"), "pristine");
|
|
487
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "contended.txt"), env, dir), { ok: true, minted: true });
|
|
488
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "contended.txt"), env, dir), { ok: true, minted: false });
|
|
489
|
+
await writeFile(join(dir, "contended.txt"), "B's edit landed");
|
|
490
|
+
const annul = await store.annulTrack("sc", join(dir, "contended.txt"), dir);
|
|
491
|
+
assert.equal(annul.ok, false, "a record another lane was served is no longer the minter's to retract");
|
|
492
|
+
if (!annul.ok)
|
|
493
|
+
assert.equal(annul.error.code, "conflict");
|
|
494
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, dir), { ok: true });
|
|
495
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "contended.txt"), env, dir), { ok: true, minted: false });
|
|
496
|
+
const r = await store.restore("sc", "b1", env, dir);
|
|
497
|
+
assert.equal(r.outcome, "complete");
|
|
498
|
+
assert.equal(await readFile(join(dir, "contended.txt"), "utf8"), "B's edit landed");
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
run("import NON-DESTRUCTIVE target law: the identical envelope re-imports idempotently, while a DIFFERENT graph over locally-grown history is refused typed and the target survives intact", async () => {
|
|
502
|
+
await withTree(async (dir, env) => {
|
|
503
|
+
const src = make();
|
|
504
|
+
if (src.exportHistory === undefined || src.importHistory === undefined)
|
|
505
|
+
return;
|
|
506
|
+
const shed = contentShed();
|
|
507
|
+
await shed.write(join(dir, "f.txt"), "source-pristine");
|
|
508
|
+
assert.deepEqual(await src.trackEdit("sc", join(dir, "f.txt"), env, dir), { ok: true, minted: true });
|
|
509
|
+
assert.deepEqual(await src.snapshot("sc", "b1", env, dir), { ok: true });
|
|
510
|
+
const envelope = (await src.exportHistory("sc"));
|
|
511
|
+
const dst = make();
|
|
512
|
+
assert.deepEqual(await dst.importHistory("target", envelope, shed.fetch), { ok: true });
|
|
513
|
+
assert.deepEqual(await dst.importHistory("target", envelope, shed.fetch), { ok: true }, "the same envelope arriving twice is a re-publish, not a conflict");
|
|
514
|
+
await shed.write(join(dir, "local.txt"), "local-work");
|
|
515
|
+
assert.deepEqual(await dst.trackEdit("target", join(dir, "local.txt"), env, dir), { ok: true, minted: true });
|
|
516
|
+
assert.deepEqual(await dst.snapshot("target", "b-local", env, dir), { ok: true });
|
|
517
|
+
const other = await src.exportHistory("sc");
|
|
518
|
+
const conflict = await dst.importHistory("target", other, shed.fetch);
|
|
519
|
+
assert.equal(conflict.ok, false);
|
|
520
|
+
if (!conflict.ok)
|
|
521
|
+
assert.equal(conflict.error.code, "conflict");
|
|
522
|
+
assert.equal(await dst.canRestore("target", "b-local"), true, "the refusal left the target's own history intact");
|
|
523
|
+
assert.equal(await dst.canRestore("target", "b1"), true);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
run("FOREIGN-FAMILY keys are refused at restore, not re-pointed at this machine's tree (the self-describing key's whole purpose)", async () => {
|
|
527
|
+
await withTree(async (dir, env) => {
|
|
528
|
+
const store = make();
|
|
529
|
+
if (store.importHistory === undefined)
|
|
530
|
+
return;
|
|
531
|
+
const foreign = wireEnvelope({
|
|
532
|
+
formatVersion: "file-history/1",
|
|
533
|
+
tracked: [
|
|
534
|
+
{ relOrAbsPath: "D:\\other\\machine\\x.txt", keyKind: "absolute", keyFamily: "win", baseAvailable: true, versions: [{ version: 1, blobHash: null, backupTime: "t" }] },
|
|
535
|
+
{ relOrAbsPath: "sub\\deep.txt", keyKind: "root-relative", keyFamily: "win", baseAvailable: true, versions: [{ version: 1, blobHash: null, backupTime: "t" }] },
|
|
536
|
+
],
|
|
537
|
+
boundaries: [
|
|
538
|
+
{
|
|
539
|
+
entryId: "bw",
|
|
540
|
+
files: [
|
|
541
|
+
{ relOrAbsPath: "D:\\other\\machine\\x.txt", version: 1 },
|
|
542
|
+
{ relOrAbsPath: "sub\\deep.txt", version: 1 },
|
|
543
|
+
],
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
});
|
|
547
|
+
const imported = await store.importHistory("foreign", foreign, async () => undefined);
|
|
548
|
+
if (!imported.ok)
|
|
549
|
+
return;
|
|
550
|
+
const r = await store.restore("foreign", "bw", env, dir);
|
|
551
|
+
assert.equal(r.outcome, "partial", `foreign-family rows must surface as refusals: ${JSON.stringify(r)}`);
|
|
552
|
+
assert.equal(r.refused.length, 2, "each foreign-family key is refused by name");
|
|
553
|
+
assert.ok(r.refused.every((f) => /foreign-family/.test(f.reason)), `the refusal must name the provenance mismatch: ${JSON.stringify(r.refused)}`);
|
|
554
|
+
assert.deepEqual(r.filesChanged, [], "nothing under this root may be touched on behalf of a foreign-family record");
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
run("mode (DV-8): a recorded POSIX mode is restored with the bytes — including on a file the restore has to RE-CREATE", async () => {
|
|
558
|
+
await withTree(async (dir, env) => {
|
|
559
|
+
const store = make();
|
|
560
|
+
const script = join(dir, "run.sh");
|
|
561
|
+
await writeFile(script, "#!/bin/sh\necho v1\n");
|
|
562
|
+
await chmod(script, 0o755);
|
|
563
|
+
assert.deepEqual(await store.trackEdit("sc", script, env, dir), { ok: true, minted: true });
|
|
564
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, dir), { ok: true });
|
|
565
|
+
const recorded = (await stat(script)).mode & 0o7777;
|
|
566
|
+
if (recorded !== 0o755)
|
|
567
|
+
return;
|
|
568
|
+
await writeFile(script, "#!/bin/sh\necho edited\n");
|
|
569
|
+
await chmod(script, 0o644);
|
|
570
|
+
const back = await store.restore("sc", "b1", env, dir);
|
|
571
|
+
assert.equal(back.outcome, "complete", JSON.stringify(back));
|
|
572
|
+
assert.equal((await stat(script)).mode & 0o7777, 0o755, "the boundary's mode travels with its bytes");
|
|
573
|
+
await rm(script);
|
|
574
|
+
const revived = await store.restore("sc", "b1", env, dir);
|
|
575
|
+
assert.equal(revived.outcome, "complete", JSON.stringify(revived));
|
|
576
|
+
assert.equal((await stat(script)).mode & 0o7777, 0o755, "a re-created file gets the recorded mode, not the umask default");
|
|
577
|
+
assert.equal(revived.modeNotPreserved ?? 0, 0, "an env WITH the mode face reports nothing unpreserved");
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
run("mode (DV-8) honest degradation: an env without the optional setFileMode face restores the BYTES and DISCLOSES the unpreserved mode (never silently, never fatally)", async () => {
|
|
581
|
+
await withTree(async (dir, env) => {
|
|
582
|
+
const store = make();
|
|
583
|
+
const script = join(dir, "run.sh");
|
|
584
|
+
await writeFile(script, "v1");
|
|
585
|
+
await chmod(script, 0o755);
|
|
586
|
+
assert.deepEqual(await store.trackEdit("sc", script, env, dir), { ok: true, minted: true });
|
|
587
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, dir), { ok: true });
|
|
588
|
+
if (((await stat(script)).mode & 0o7777) !== 0o755)
|
|
589
|
+
return;
|
|
590
|
+
await rm(script);
|
|
591
|
+
const modeless = new Proxy(env, {
|
|
592
|
+
get(target, prop, receiver) {
|
|
593
|
+
if (prop === "setFileMode")
|
|
594
|
+
return undefined;
|
|
595
|
+
return Reflect.get(target, prop, receiver);
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
const r = await store.restore("sc", "b1", modeless, dir);
|
|
599
|
+
assert.equal(r.outcome, "complete", "a mode that cannot be applied is a disclosure, not a failure");
|
|
600
|
+
assert.deepEqual(r.filesChanged, [key(dir, script)], "the BYTES still converge");
|
|
601
|
+
assert.equal(r.modeNotPreserved, 1, "…and the count says so — the honest-absence law, in the ledger");
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
run("restore preflight (§7 E5/E4, slice 3): an in-root ancestor swapped for a link OUT of the root is REFUSED per file, a legal whole-root move still restores, and the rest of the set converges either way", async () => {
|
|
605
|
+
await withTree(async (dir, env) => {
|
|
606
|
+
const store = make();
|
|
607
|
+
const outside = await mkdtemp(join(tmpdir(), "fh-kit-outside-"));
|
|
608
|
+
try {
|
|
609
|
+
await mkdir(join(dir, "d"), { recursive: true });
|
|
610
|
+
await writeFile(join(dir, "d", "f.txt"), "pristine");
|
|
611
|
+
await writeFile(join(dir, "sibling.txt"), "sibling-pristine");
|
|
612
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "d", "f.txt"), env, dir), { ok: true, minted: true });
|
|
613
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "sibling.txt"), env, dir), { ok: true, minted: true });
|
|
614
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, dir), { ok: true });
|
|
615
|
+
await writeFile(join(dir, "d", "f.txt"), "agent-edited");
|
|
616
|
+
await writeFile(join(dir, "sibling.txt"), "agent-edited");
|
|
617
|
+
await writeFile(join(outside, "f.txt"), "OUTSIDE-UNTOUCHED");
|
|
618
|
+
await rm(join(dir, "d"), { recursive: true, force: true });
|
|
619
|
+
await symlink(outside, join(dir, "d"), "dir");
|
|
620
|
+
const r = await store.restore("sc", "b1", env, dir);
|
|
621
|
+
assert.equal(r.outcome, "partial", JSON.stringify(r));
|
|
622
|
+
assert.deepEqual(r.refused.map((x) => x.path), [key(dir, join(dir, "d", "f.txt"))], "the swapped path — and ONLY it — is refused");
|
|
623
|
+
assert.equal(await readFile(join(outside, "f.txt"), "utf8"), "OUTSIDE-UNTOUCHED", "E5: zero writes outside the root");
|
|
624
|
+
assert.deepEqual(r.filesChanged, [key(dir, join(dir, "sibling.txt"))], "a security refusal never fails the whole restore");
|
|
625
|
+
}
|
|
626
|
+
finally {
|
|
627
|
+
await rm(outside, { recursive: true, force: true });
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
await withTree(async (oldRoot, env) => {
|
|
631
|
+
const store = make();
|
|
632
|
+
await mkdir(join(oldRoot, "d"), { recursive: true });
|
|
633
|
+
await writeFile(join(oldRoot, "d", "f.txt"), "pristine");
|
|
634
|
+
assert.deepEqual(await store.trackEdit("sc", join(oldRoot, "d", "f.txt"), env, oldRoot), { ok: true, minted: true });
|
|
635
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, oldRoot), { ok: true });
|
|
636
|
+
await withTree(async (newRoot, newEnv) => {
|
|
637
|
+
await mkdir(join(newRoot, "d"), { recursive: true });
|
|
638
|
+
await writeFile(join(newRoot, "d", "f.txt"), "agent-edited");
|
|
639
|
+
const r = await store.restore("sc", "b1", newEnv, newRoot);
|
|
640
|
+
assert.equal(r.outcome, "complete", `E4 must be green in the same breath as E5: ${JSON.stringify(r)}`);
|
|
641
|
+
assert.equal(await readFile(join(newRoot, "d", "f.txt"), "utf8"), "pristine");
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
run("restore preflight (kind arm, G11): a target replaced by a LEAF SYMLINK or a DIRECTORY is refused by name and the link's own target is untouched", async () => {
|
|
646
|
+
await withTree(async (dir, env) => {
|
|
647
|
+
const store = make();
|
|
648
|
+
const outside = await mkdtemp(join(tmpdir(), "fh-kit-kind-"));
|
|
649
|
+
try {
|
|
650
|
+
await writeFile(join(dir, "leaf.txt"), "pristine-leaf");
|
|
651
|
+
await writeFile(join(dir, "dir.txt"), "pristine-dir");
|
|
652
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "leaf.txt"), env, dir), { ok: true, minted: true });
|
|
653
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "dir.txt"), env, dir), { ok: true, minted: true });
|
|
654
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, dir), { ok: true });
|
|
655
|
+
await writeFile(join(outside, "victim.txt"), "OUTSIDE-UNTOUCHED");
|
|
656
|
+
await rm(join(dir, "leaf.txt"));
|
|
657
|
+
await symlink(join(outside, "victim.txt"), join(dir, "leaf.txt"), "file");
|
|
658
|
+
await rm(join(dir, "dir.txt"));
|
|
659
|
+
await mkdir(join(dir, "dir.txt"));
|
|
660
|
+
const r = await store.restore("sc", "b1", env, dir);
|
|
661
|
+
assert.equal(r.outcome, "partial", JSON.stringify(r));
|
|
662
|
+
assert.equal(r.refused.length, 2, "both kind mismatches are refused, each by name");
|
|
663
|
+
assert.equal(await readFile(join(outside, "victim.txt"), "utf8"), "OUTSIDE-UNTOUCHED", "a leaf link is never written THROUGH");
|
|
664
|
+
assert.deepEqual(r.filesChanged, []);
|
|
665
|
+
}
|
|
666
|
+
finally {
|
|
667
|
+
await rm(outside, { recursive: true, force: true });
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
run("diffStats (A-2 two-state law): a real preview lists exactly the files a restore WOULD change with their line arithmetic, an already-converged tracked set is an EMPTY answer, and no boundary at all is NULL — never an empty object", async () => {
|
|
672
|
+
await withTree(async (dir, env) => {
|
|
673
|
+
const store = make();
|
|
674
|
+
if (store.diffStats === undefined)
|
|
675
|
+
return;
|
|
676
|
+
await writeFile(join(dir, "edited.txt"), "one\ntwo\nthree\n");
|
|
677
|
+
await writeFile(join(dir, "untouched.txt"), "same\n");
|
|
678
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "edited.txt"), env, dir), { ok: true, minted: true });
|
|
679
|
+
assert.deepEqual(await store.trackEdit("sc", join(dir, "untouched.txt"), env, dir), { ok: true, minted: true });
|
|
680
|
+
assert.deepEqual(await store.snapshot("sc", "b1", env, dir), { ok: true });
|
|
681
|
+
assert.equal(await store.diffStats("sc", "no-such-boundary", env, dir), null);
|
|
682
|
+
const quiet = await store.diffStats("sc", "b1", env, dir);
|
|
683
|
+
assert.notEqual(quiet, null, "a boundary that exists must produce an answer, not an absence");
|
|
684
|
+
assert.deepEqual(quiet, { filesChanged: [], insertions: 0, deletions: 0 });
|
|
685
|
+
await writeFile(join(dir, "edited.txt"), "one\nTWO\nthree\nfour\n");
|
|
686
|
+
const preview = await store.diffStats("sc", "b1", env, dir);
|
|
687
|
+
assert.notEqual(preview, null);
|
|
688
|
+
assert.deepEqual(preview.filesChanged, [key(dir, join(dir, "edited.txt"))], "an unchanged tracked file is not a change");
|
|
689
|
+
assert.deepEqual({ insertions: preview.insertions, deletions: preview.deletions }, { insertions: 1, deletions: 2 });
|
|
690
|
+
const restored = await store.restore("sc", "b1", env, dir);
|
|
691
|
+
assert.deepEqual(restored.filesChanged, preview.filesChanged);
|
|
692
|
+
await chmod(join(dir, "edited.txt"), 0o600);
|
|
693
|
+
if (((await stat(join(dir, "edited.txt"))).mode & 0o7777) !== 0o600)
|
|
694
|
+
return;
|
|
695
|
+
const modePreview = await store.diffStats("sc", "b1", env, dir);
|
|
696
|
+
assert.notEqual(modePreview, null);
|
|
697
|
+
assert.deepEqual(modePreview.filesChanged, [key(dir, join(dir, "edited.txt"))], "a mode-only restoration is a change the preview must list");
|
|
698
|
+
assert.deepEqual({ insertions: modePreview.insertions, deletions: modePreview.deletions }, { insertions: 0, deletions: 0 }, "…and it moves no lines");
|
|
699
|
+
const modeRestored = await store.restore("sc", "b1", env, dir);
|
|
700
|
+
assert.deepEqual(modeRestored.filesChanged, modePreview.filesChanged, "preview and operation agree on the mode-only arm too");
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
await settle();
|
|
704
|
+
}
|
|
705
|
+
function key(root, absPath) {
|
|
706
|
+
return absPath.slice(root.length + 1).replace(/\\/g, "/");
|
|
707
|
+
}
|
|
708
|
+
function contentShed() {
|
|
709
|
+
const bytesOf = new Map();
|
|
710
|
+
return {
|
|
711
|
+
async write(path, text) {
|
|
712
|
+
const bytes = Buffer.from(text, "utf8");
|
|
713
|
+
bytesOf.set(createHash("sha256").update(bytes).digest("hex"), bytes);
|
|
714
|
+
await writeFile(path, bytes);
|
|
715
|
+
},
|
|
716
|
+
async fetch(hash) {
|
|
717
|
+
return bytesOf.get(hash);
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
}
|