@tachikomagundam/abathur 0.1.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/.github/workflows/ci.yml +29 -0
- package/.github/workflows/publish.yml +74 -0
- package/LICENSE +21 -0
- package/README.md +461 -0
- package/config/abathur.jsonc +17 -0
- package/config/genomes/historian.example.jsonc +124 -0
- package/dist/bench/adapter.js +201 -0
- package/dist/bench/fixture-probe.js +92 -0
- package/dist/bench/fixture-support.js +173 -0
- package/dist/bench/fixture.js +236 -0
- package/dist/bench/toy.js +152 -0
- package/dist/cli.js +110 -0
- package/dist/commands/bundle.js +79 -0
- package/dist/commands/genome.js +94 -0
- package/dist/commands/graft.js +71 -0
- package/dist/commands/kernel.js +47 -0
- package/dist/commands/promote.js +25 -0
- package/dist/commands/run.js +145 -0
- package/dist/commands/self-eval.js +240 -0
- package/dist/commands/status.js +186 -0
- package/dist/commands/tombstone.js +72 -0
- package/dist/config.js +161 -0
- package/dist/core/bundle-common.js +119 -0
- package/dist/core/bundle-export.js +212 -0
- package/dist/core/bundle-inspect.js +143 -0
- package/dist/core/bundle-manifest.js +105 -0
- package/dist/core/bundle-mask.js +75 -0
- package/dist/core/bundle-tar.js +240 -0
- package/dist/core/bundle.js +9 -0
- package/dist/core/evolve/brief.js +45 -0
- package/dist/core/evolve/candidate.js +140 -0
- package/dist/core/evolve/child-track.js +197 -0
- package/dist/core/evolve/friction.js +150 -0
- package/dist/core/evolve/reflect.js +191 -0
- package/dist/core/evolve/run-bench.js +170 -0
- package/dist/core/evolve/run-friction.js +63 -0
- package/dist/core/evolve/run-loop.js +282 -0
- package/dist/core/evolve/run-plan.js +39 -0
- package/dist/core/evolve/run-rows.js +145 -0
- package/dist/core/evolve/self-overlay.js +213 -0
- package/dist/core/evolve/self-snapshot.js +170 -0
- package/dist/core/evolve/stub-mutators.mjs +105 -0
- package/dist/core/evolve/udiff.js +189 -0
- package/dist/core/genome-paths.js +76 -0
- package/dist/core/genome.js +176 -0
- package/dist/core/glob.js +106 -0
- package/dist/core/graft-gates.js +184 -0
- package/dist/core/graft-rebench.js +187 -0
- package/dist/core/graft-support.js +181 -0
- package/dist/core/graft.js +218 -0
- package/dist/core/ids.js +154 -0
- package/dist/core/incumbent.js +46 -0
- package/dist/core/kernel.js +112 -0
- package/dist/core/ledger.js +198 -0
- package/dist/core/locks.js +172 -0
- package/dist/core/promote.js +119 -0
- package/dist/core/snapshot.js +61 -0
- package/dist/core/spec.js +178 -0
- package/dist/core/stats-math.js +102 -0
- package/dist/core/stats-pareto.js +57 -0
- package/dist/core/stats.js +184 -0
- package/dist/core/worktree.js +190 -0
- package/dist/exit.js +32 -0
- package/dist/genomes/toy-smoke/genome.jsonc +30 -0
- package/dist/genomes/toy-smoke/grader.mjs +61 -0
- package/dist/genomes/toy-smoke/init.mjs +63 -0
- package/dist/genomes/toy-smoke/units/add.mjs +17 -0
- package/dist/genomes/toy-smoke/units/explode.mjs +4 -0
- package/dist/genomes/toy-smoke/units/hang.mjs +16 -0
- package/dist/genomes/toy-smoke/units/mul.mjs +16 -0
- package/dist/genomes/toy-smoke/units/mutate.mjs +18 -0
- package/dist/genomes/toy-smoke/units/sub.mjs +16 -0
- package/dist/jsonc.js +77 -0
- package/dist/out.js +5 -0
- package/dist/test/bench-adapter.test.js +33 -0
- package/dist/test/bench-fixture.test.js +407 -0
- package/dist/test/bench-toy.test.js +251 -0
- package/dist/test/bundle.test.js +659 -0
- package/dist/test/config.test.js +185 -0
- package/dist/test/d7-gate.test.js +56 -0
- package/dist/test/fixture-loop.test.js +267 -0
- package/dist/test/fixtures/friction-writer.js +16 -0
- package/dist/test/fixtures-historian.js +82 -0
- package/dist/test/fixtures-self.js +143 -0
- package/dist/test/fixtures-wt.js +64 -0
- package/dist/test/friction.test.js +398 -0
- package/dist/test/genome.test.js +453 -0
- package/dist/test/git.test.js +69 -0
- package/dist/test/graft.test.js +567 -0
- package/dist/test/historian-genome.test.js +134 -0
- package/dist/test/historian-grader-io.test.js +148 -0
- package/dist/test/historian-grader.test.js +209 -0
- package/dist/test/ids.test.js +116 -0
- package/dist/test/include-val.test.js +120 -0
- package/dist/test/ledger-lock.test.js +99 -0
- package/dist/test/ledger.test.js +102 -0
- package/dist/test/promote.test.js +394 -0
- package/dist/test/reflect.test.js +410 -0
- package/dist/test/run-loop.test.js +433 -0
- package/dist/test/self-snapshot.test.js +328 -0
- package/dist/test/snapshot.test.js +86 -0
- package/dist/test/stats.test.js +423 -0
- package/dist/test/stub-mutators.test.js +17 -0
- package/dist/test/testutil.js +30 -0
- package/dist/test/worktree.test.js +198 -0
- package/dist/util/freeze.js +30 -0
- package/dist/util/git.js +85 -0
- package/docs/federation.md +184 -0
- package/docs/immutable-kernel.md +87 -0
- package/graders/historian/grader-core.d.mts +53 -0
- package/graders/historian/grader-core.mjs +276 -0
- package/graders/historian/grader-support.d.mts +57 -0
- package/graders/historian/grader-support.mjs +137 -0
- package/graders/historian/grader.mjs +113 -0
- package/graders/historian/mutate.sh +114 -0
- package/graders/historian/reset-sandbox.sh +60 -0
- package/graders/historian/run-scenario.sh +49 -0
- package/graders/historian/seed-wrapped.sh +32 -0
- package/package.json +42 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
// Todo 13 AC pins (plan lines 173-180): `graft` — cross-instance crystallization
|
|
2
|
+
// merge with a LOCAL re-bench.
|
|
3
|
+
// AC(a) honest bundle from a second "machine" (same absolute genome path,
|
|
4
|
+
// different config home + different git history) ⇒ digest gates pass ⇒
|
|
5
|
+
// fresh worktree + LOCAL bench per LOCAL spec ⇒ nominated/culled per
|
|
6
|
+
// LOCAL score; ledger carries imported:true + source fingerprint EVEN
|
|
7
|
+
// when the bundle's claimed verdict/reps differ (peer claims are
|
|
8
|
+
// peerClaim only, never nomination math);
|
|
9
|
+
// AC(b) tampered benchDigest (tree member mutated, files[] re-pinned, manifest
|
|
10
|
+
// digest stale) ⇒ quarantined: ONE graft_import row, ZERO worktrees,
|
|
11
|
+
// ZERO bench, status lists it naming the gate with expected vs actual;
|
|
12
|
+
// AC(c) genome not registered ⇒ pending-bench: queue file under
|
|
13
|
+
// <configDir>/graft-queue/, status reports the real depth + entry lines,
|
|
14
|
+
// ZERO bench runs; registering the genome and re-running graft resolves
|
|
15
|
+
// the queue entry;
|
|
16
|
+
// plus: wrong-genome fingerprint quarantine, requires[] probe quarantine,
|
|
17
|
+
// duplicate-graft refusal (stale_state), malformed containers exit 2
|
|
18
|
+
// clean, prompt_injection sanitization, dirty_worktree immunity,
|
|
19
|
+
// local-settings-rule (bundle reps=99 benches at local reps), promote
|
|
20
|
+
// chain consumes the graft nomination at the human gate, and structural
|
|
21
|
+
// no-promote-import / no --force pins.
|
|
22
|
+
import assert from "node:assert/strict";
|
|
23
|
+
import { spawnSync } from "node:child_process";
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, chmodSync } from "node:fs";
|
|
25
|
+
import { mkdtemp } from "node:fs/promises";
|
|
26
|
+
import * as os from "node:os";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import test, {} from "node:test";
|
|
30
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
31
|
+
import { Ledger } from "../core/ledger.js";
|
|
32
|
+
import { fingerprint } from "../core/ids.js";
|
|
33
|
+
import { registerGenome, requireGenomesByLabel } from "../core/genome.js";
|
|
34
|
+
import { decodeGenerationRecord } from "../core/evolve/run-bench.js";
|
|
35
|
+
import { runEvolution } from "../core/evolve/run-loop.js";
|
|
36
|
+
import { prepareToyGenome } from "../bench/toy.js";
|
|
37
|
+
import { loadGenomeSpecFile } from "../core/spec.js";
|
|
38
|
+
import { exportBundle } from "../core/bundle-export.js";
|
|
39
|
+
import { readTar, writeTar } from "../core/bundle-tar.js";
|
|
40
|
+
import { sha256Hex } from "../core/bundle-common.js";
|
|
41
|
+
import { INCUMBENT_BRANCH } from "../core/incumbent.js";
|
|
42
|
+
import { LEDGER_KIND_GRAFT_IMPORT, graftQueueDir, graftQueuePath, decodeGraftImport, listGraftQueue, } from "../core/graft-support.js";
|
|
43
|
+
const CLI = fileURLToPath(new URL("../../dist/cli.js", import.meta.url));
|
|
44
|
+
const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
|
45
|
+
const ADD_ANCHOR = "return a - b; // seeded bug: must be `return a + b;`";
|
|
46
|
+
const STUB_SOURCE = `#!/usr/bin/env node
|
|
47
|
+
import { readFileSync } from "node:fs";
|
|
48
|
+
const args = process.argv.slice(2);
|
|
49
|
+
const opt = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; };
|
|
50
|
+
const mode = opt("--mode");
|
|
51
|
+
const dir = opt("--dir");
|
|
52
|
+
const read = (f) => readFileSync(dir + "/" + f, "utf8");
|
|
53
|
+
function lineDiff(file, anchor, replacement) {
|
|
54
|
+
const lines = read(file).split("\\n");
|
|
55
|
+
const i = lines.findIndex((l) => l.includes(anchor));
|
|
56
|
+
if (i < 0) { process.stderr.write("stub: no anchor in " + file + "\\n"); process.exit(1); }
|
|
57
|
+
const changed = lines[i].replace(anchor, replacement);
|
|
58
|
+
return "--- a/" + file + "\\n+++ b/" + file + "\\n@@ -" + String(i + 1) + ",1 +" + String(i + 1) + ",1 @@\\n-" + lines[i] + "\\n+" + changed + "\\n";
|
|
59
|
+
}
|
|
60
|
+
const FIX = ${JSON.stringify(ADD_ANCHOR)};
|
|
61
|
+
const out = (candidates) => process.stdout.write(JSON.stringify({ candidates }) + "\\n");
|
|
62
|
+
if (mode === "three") {
|
|
63
|
+
out([
|
|
64
|
+
{ id: "annotate-add", rationale: "annotate add()", diffs: [lineDiff("units/add.mjs", "export function add(", "export function add /* patched */(")] },
|
|
65
|
+
{ id: "fix-add", rationale: "fix add(): subtraction to addition", diffs: [lineDiff("units/add.mjs", FIX, "return a + b;")] },
|
|
66
|
+
]);
|
|
67
|
+
} else {
|
|
68
|
+
process.stderr.write("stub: unknown mode\\n");
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
let instanceSeq = 0;
|
|
73
|
+
async function suite(t) {
|
|
74
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "abathur-graft-"));
|
|
75
|
+
t.after(() => {
|
|
76
|
+
// frozen snapshot dirs need u+w before rm -rf (todo-3 lesson)
|
|
77
|
+
spawnSync("chmod", ["-R", "u+w", root]);
|
|
78
|
+
spawnSync("rm", ["-rf", root]);
|
|
79
|
+
});
|
|
80
|
+
const stub = path.join(root, "stub.mjs");
|
|
81
|
+
writeFileSync(stub, STUB_SOURCE, "utf8");
|
|
82
|
+
chmodSync(stub, 0o755);
|
|
83
|
+
return { root, stub, genomePath: path.join(root, "shared", "genome") };
|
|
84
|
+
}
|
|
85
|
+
async function makeInstance(s, name, opts = {}) {
|
|
86
|
+
instanceSeq += 1;
|
|
87
|
+
const configDir = path.join(s.root, `cfg-${name}`);
|
|
88
|
+
mkdirSync(configDir, { recursive: true });
|
|
89
|
+
writeFileSync(path.join(configDir, "config.jsonc"), '{ "opencodeBin": null }\n', "utf8");
|
|
90
|
+
const repo = await prepareToyGenome(s.genomePath);
|
|
91
|
+
if (opts.requires !== undefined || opts.minEffect !== undefined) {
|
|
92
|
+
const spec = loadGenomeSpecFile(path.join(repo, "genome.jsonc"));
|
|
93
|
+
const edited = {
|
|
94
|
+
...spec,
|
|
95
|
+
bench: opts.minEffect === undefined ? spec.bench : { ...spec.bench, stats: { ...spec.bench.stats, minEffect: opts.minEffect } },
|
|
96
|
+
...(opts.requires === undefined ? {} : { requires: opts.requires }),
|
|
97
|
+
};
|
|
98
|
+
writeFileSync(path.join(repo, "genome.jsonc"), `${JSON.stringify(edited, null, 2)}\n`, "utf8");
|
|
99
|
+
gitC(repo, "-c", "user.name=abathur", "-c", "user.email=abathur@harness.local", "commit", "-am", "graft fixture: spec edit");
|
|
100
|
+
}
|
|
101
|
+
registerGenome(configDir, path.join(repo, "genome.jsonc"));
|
|
102
|
+
const entry = requireGenomesByLabel(configDir, "toy-smoke").entries[0];
|
|
103
|
+
if (entry === undefined)
|
|
104
|
+
throw new Error(`fixture ${name}: toy-smoke registration vanished`);
|
|
105
|
+
const xdgCache = path.join(s.root, `xdg-${name}`);
|
|
106
|
+
return {
|
|
107
|
+
root: s.root,
|
|
108
|
+
name,
|
|
109
|
+
configDir,
|
|
110
|
+
repo,
|
|
111
|
+
entry,
|
|
112
|
+
xdgCache,
|
|
113
|
+
cliEnv: {
|
|
114
|
+
...process.env,
|
|
115
|
+
ABATHUR_CONFIG: path.join(configDir, "config.jsonc"),
|
|
116
|
+
HOME: path.join(s.root, `home-${name}`),
|
|
117
|
+
XDG_CACHE_HOME: xdgCache,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function cli(inst, ...args) {
|
|
122
|
+
return spawnSync(process.execPath, [CLI, ...args], { env: inst.cliEnv, encoding: "utf8", cwd: inst.root });
|
|
123
|
+
}
|
|
124
|
+
function gitC(repo, ...args) {
|
|
125
|
+
const run = spawnSync("git", ["-C", repo, ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
126
|
+
if (run.status !== 0)
|
|
127
|
+
throw new Error(`git ${args.join(" ")}: ${run.stderr}`);
|
|
128
|
+
return run.stdout.trim();
|
|
129
|
+
}
|
|
130
|
+
function genRows(repo) {
|
|
131
|
+
return Ledger.open(repo)
|
|
132
|
+
.readAll()
|
|
133
|
+
.filter((r) => r.kind === "generation_complete" && r.genId !== undefined)
|
|
134
|
+
.map((r) => ({ genId: r.genId, data: decodeGenerationRecord(r) }));
|
|
135
|
+
}
|
|
136
|
+
function graftRows(repo) {
|
|
137
|
+
return Ledger.open(repo)
|
|
138
|
+
.readAll()
|
|
139
|
+
.filter((r) => r.kind === LEDGER_KIND_GRAFT_IMPORT)
|
|
140
|
+
.map((r) => ({ record: r, row: decodeGraftImport(r.data) }));
|
|
141
|
+
}
|
|
142
|
+
/** Real evolution on the source instance; returns its candidate rows. */
|
|
143
|
+
async function evolveSource(inst) {
|
|
144
|
+
if (inst.entry === null)
|
|
145
|
+
throw new Error("fixture: source instance lacks a registry entry");
|
|
146
|
+
const outcome = await runEvolution({
|
|
147
|
+
entry: inst.entry,
|
|
148
|
+
configDir: inst.configDir,
|
|
149
|
+
mutatorCommand: `node ${inst.root}/stub.mjs --mode three --dir {worktree} --brief {brief}`,
|
|
150
|
+
env: { XDG_CACHE_HOME: inst.xdgCache, HOME: path.join(inst.root, `home-${inst.name}`) },
|
|
151
|
+
sandboxRoot: path.join(inst.configDir, "bench-sandboxes"),
|
|
152
|
+
});
|
|
153
|
+
assert.equal(outcome.exitCode, 0, `source evolution must succeed: ${outcome.lines.join("\n")}`);
|
|
154
|
+
return genRows(inst.repo);
|
|
155
|
+
}
|
|
156
|
+
async function exportFrom(inst, genId) {
|
|
157
|
+
if (inst.entry === null)
|
|
158
|
+
throw new Error("fixture: source instance lacks a registry entry");
|
|
159
|
+
const outDir = path.join(inst.root, `bundles-${inst.name}`);
|
|
160
|
+
const outcome = await exportBundle({
|
|
161
|
+
entry: inst.entry,
|
|
162
|
+
configDir: inst.configDir,
|
|
163
|
+
select: { genIds: [genId] },
|
|
164
|
+
outDir,
|
|
165
|
+
home: os.homedir(),
|
|
166
|
+
});
|
|
167
|
+
assert.ok(existsSync(outcome.bundlePath), "bundle written");
|
|
168
|
+
return outcome.bundlePath;
|
|
169
|
+
}
|
|
170
|
+
// --------------------------------------------------------- bundle surgery
|
|
171
|
+
const enc = (text) => new TextEncoder().encode(text);
|
|
172
|
+
function bundleMembers(bundlePath) {
|
|
173
|
+
return readTar(new Uint8Array(gunzipSync(readFileSync(bundlePath))));
|
|
174
|
+
}
|
|
175
|
+
function repack(bundlePath, members) {
|
|
176
|
+
writeFileSync(bundlePath, new Uint8Array(gzipSync(writeTar(members))));
|
|
177
|
+
}
|
|
178
|
+
function manifestDoc(members) {
|
|
179
|
+
const raw = members.find((m) => m.path === "manifest.json");
|
|
180
|
+
if (raw === undefined)
|
|
181
|
+
throw new Error("bundle has no manifest.json");
|
|
182
|
+
return JSON.parse(new TextDecoder().decode(raw.content));
|
|
183
|
+
}
|
|
184
|
+
/** Edit manifest.json only (it is not sha-pinned in files[]) — claims stay schema-valid. */
|
|
185
|
+
function tamperManifest(bundlePath, edit) {
|
|
186
|
+
const members = bundleMembers(bundlePath);
|
|
187
|
+
const doc = manifestDoc(members);
|
|
188
|
+
edit(doc);
|
|
189
|
+
repack(bundlePath, members.map((m) => (m.path === "manifest.json" ? { path: m.path, content: enc(`${JSON.stringify(doc, null, 2)}\n`) } : m)));
|
|
190
|
+
}
|
|
191
|
+
function primaryTreeGen(members) {
|
|
192
|
+
const ids = new Set();
|
|
193
|
+
for (const m of members) {
|
|
194
|
+
if (!m.path.startsWith("trees/"))
|
|
195
|
+
continue;
|
|
196
|
+
ids.add(m.path.slice("trees/".length).split("/")[0]);
|
|
197
|
+
}
|
|
198
|
+
return [...ids].sort().at(-1);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* AC(b) tamper: mutate a digest-bearing tree member (units/mul.mjs), re-pin its
|
|
202
|
+
* files[] entry (byte-consistent repack) and leave manifest.benchDigest STALE.
|
|
203
|
+
*/
|
|
204
|
+
function tamperTreeStaleDigest(bundlePath) {
|
|
205
|
+
const members = bundleMembers(bundlePath);
|
|
206
|
+
const primary = primaryTreeGen(members);
|
|
207
|
+
const target = `trees/${primary}/units/mul.mjs`;
|
|
208
|
+
const member = members.find((m) => m.path === target);
|
|
209
|
+
if (member === undefined)
|
|
210
|
+
throw new Error(`bundle has no ${target}`);
|
|
211
|
+
const content = enc(`${new TextDecoder().decode(member.content)}\n// tampered after export\n`);
|
|
212
|
+
const next = members.map((m) => (m.path === target ? { path: m.path, content } : m));
|
|
213
|
+
const doc = manifestDoc(members);
|
|
214
|
+
const files = doc.files;
|
|
215
|
+
const pin = files.find((f) => f.path === target);
|
|
216
|
+
if (pin === undefined)
|
|
217
|
+
throw new Error(`manifest does not pin ${target}`);
|
|
218
|
+
pin.sha256 = sha256Hex(content);
|
|
219
|
+
pin.len = content.length;
|
|
220
|
+
repack(bundlePath, next.map((m) => (m.path === "manifest.json" ? { path: m.path, content: enc(`${JSON.stringify(doc, null, 2)}\n`) } : m)));
|
|
221
|
+
}
|
|
222
|
+
/** Hand-built evil tar: writeTar refuses traversal, so craft the raw header. */
|
|
223
|
+
function tarWithRawHeader(badName) {
|
|
224
|
+
const header = new Uint8Array(512);
|
|
225
|
+
header.set(enc(badName), 0);
|
|
226
|
+
header.set(enc("0000644\0"), 100);
|
|
227
|
+
header.set(enc("0000000\0"), 108);
|
|
228
|
+
header.set(enc("0000000\0"), 116);
|
|
229
|
+
header.set(enc("00000000001\0"), 124);
|
|
230
|
+
header.set(enc("00000000000\0"), 136);
|
|
231
|
+
header.set(new Uint8Array(8).fill(0x20), 148);
|
|
232
|
+
header[156] = 0x30;
|
|
233
|
+
header.set(enc("ustar\0"), 257);
|
|
234
|
+
header.set(enc("00"), 263);
|
|
235
|
+
let sum = 0;
|
|
236
|
+
for (const b of header)
|
|
237
|
+
sum += b;
|
|
238
|
+
header.set(enc(`${(sum & 0o777777).toString(8).padStart(6, "0")}\0 `), 148);
|
|
239
|
+
const out = new Uint8Array(512 * 4);
|
|
240
|
+
out.set(header, 0);
|
|
241
|
+
out.set(new Uint8Array(512).fill(0x78), 512);
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
function bundleSha(bundlePath) {
|
|
245
|
+
return sha256Hex(new Uint8Array(readFileSync(bundlePath)));
|
|
246
|
+
}
|
|
247
|
+
function incumbentSha(inst) {
|
|
248
|
+
const run = spawnSync("git", ["-C", inst.repo, "rev-parse", "--verify", "-q", `refs/heads/${INCUMBENT_BRANCH}`], { encoding: "utf8" });
|
|
249
|
+
return run.status === 0 ? run.stdout.trim() : null;
|
|
250
|
+
}
|
|
251
|
+
/** Build the AC(a) pair: evolved source instance + honest bundle + fresh twin. */
|
|
252
|
+
async function crossInstanceBundle(s, pick) {
|
|
253
|
+
const src = await makeInstance(s, "src");
|
|
254
|
+
assert.ok(src.entry !== null, "source instance registered");
|
|
255
|
+
const rows = await evolveSource(src);
|
|
256
|
+
const wanted = rows.find((r) => r.data.source === "candidate" && r.data.verdict === pick);
|
|
257
|
+
assert.ok(wanted !== undefined && wanted.data.commitSha !== undefined, `source produced a ${pick} candidate`);
|
|
258
|
+
const bundlePath = await exportFrom(src, wanted.genId);
|
|
259
|
+
const sourceFingerprint = fingerprint(src.entry.spec);
|
|
260
|
+
// The twin instance: same absolute genome path, wiped git, different config home.
|
|
261
|
+
rmSync(s.genomePath, { recursive: true, force: true });
|
|
262
|
+
return { bundlePath, sourceFingerprint };
|
|
263
|
+
}
|
|
264
|
+
// ---------------------------------------------------------------- AC (a)
|
|
265
|
+
test("AC(a): honest cross-instance graft re-benches LOCALLY and nominates; peer claims are peerClaim-only", async (t) => {
|
|
266
|
+
const s = await suite(t);
|
|
267
|
+
const { bundlePath, sourceFingerprint } = await crossInstanceBundle(s, "nominated");
|
|
268
|
+
const dst = await makeInstance(s, "dst");
|
|
269
|
+
// peer claims DIFFERENTLY than the local truth will say: inconclusive @ reps 99
|
|
270
|
+
tamperManifest(bundlePath, (doc) => {
|
|
271
|
+
doc.stats.verdict = "inconclusive";
|
|
272
|
+
doc.benchProvenance.nRepeats = 99;
|
|
273
|
+
});
|
|
274
|
+
const run = cli(dst, "graft", bundlePath, "--genome", "toy-smoke");
|
|
275
|
+
assert.equal(run.status, 0, `graft must nominate: ${run.stdout}${run.stderr}`);
|
|
276
|
+
assert.match(run.stdout, /nominated/i);
|
|
277
|
+
assert.match(run.stdout, /promote/i, "nomination must point at the human gate");
|
|
278
|
+
const imports = graftRows(dst.repo);
|
|
279
|
+
assert.equal(imports.length, 1, "exactly one graft_import row");
|
|
280
|
+
const row = imports[0]?.row;
|
|
281
|
+
assert.ok(row !== null && row !== undefined);
|
|
282
|
+
assert.equal(row.decision, "nominated");
|
|
283
|
+
assert.equal(row.imported, true);
|
|
284
|
+
assert.equal(row.sourceGenomeFingerprint, sourceFingerprint);
|
|
285
|
+
assert.match(row.sourceBenchDigest ?? "", /^[0-9a-f]{64}$/);
|
|
286
|
+
assert.equal(row.bundleSha256, bundleSha(bundlePath));
|
|
287
|
+
assert.equal(row.genomeLabel, "toy-smoke");
|
|
288
|
+
// the lying claim survives only as peerClaim metadata:
|
|
289
|
+
assert.equal(row.peerClaim?.verdict, "inconclusive");
|
|
290
|
+
assert.equal(row.peerClaim?.nRepeats, 99);
|
|
291
|
+
// LOCAL re-bench record: reps = local clampReps(undefined, initial=2), NOT 99
|
|
292
|
+
const gens = genRows(dst.repo);
|
|
293
|
+
const candidate = gens.find((g) => g.data.source === "candidate");
|
|
294
|
+
assert.ok(candidate !== undefined, "graft candidate generation row exists");
|
|
295
|
+
assert.equal(candidate.data.verdict, "nominated");
|
|
296
|
+
assert.equal(candidate.data.reps, 2, "local nReps.initial rules, never the bundle's 99");
|
|
297
|
+
const add = candidate.data.units.find((u) => u.unitId === "add");
|
|
298
|
+
assert.ok(add !== undefined && add.scores.length === 2, "2 local replicates sampled");
|
|
299
|
+
assert.equal(candidate.data.headCommit, gitC(dst.repo, "rev-parse", "HEAD"));
|
|
300
|
+
assert.equal(candidate.genId, imports[0]?.record.genId, "graft_import and generation rows share the graft genId");
|
|
301
|
+
// sealed graft tree == bundle bytes: units/add.mjs fixed, grader untouched
|
|
302
|
+
const fixedAdd = gitC(dst.repo, "show", `${String(candidate.data.commitSha)}:units/add.mjs`);
|
|
303
|
+
assert.match(fixedAdd, /return a \+ b;/);
|
|
304
|
+
assert.doesNotMatch(fixedAdd, /seeded bug/);
|
|
305
|
+
// human gate consumes the nomination (graft itself promoted NOTHING):
|
|
306
|
+
assert.equal(incumbentSha(dst), null, "graft never auto-promotes");
|
|
307
|
+
const promote = cli(dst, "promote", "toy-smoke", candidate.genId);
|
|
308
|
+
assert.equal(promote.status, 0, `promote the graft nomination: ${promote.stderr}`);
|
|
309
|
+
assert.equal(incumbentSha(dst), candidate.data.commitSha);
|
|
310
|
+
const st = cli(dst, "status", "toy-smoke");
|
|
311
|
+
assert.equal(st.status, 0);
|
|
312
|
+
assert.match(st.stdout, /graft decisions: 1/);
|
|
313
|
+
assert.match(st.stdout, /nominated/);
|
|
314
|
+
assert.match(st.stdout, /pending-bench graft queue: 0/);
|
|
315
|
+
});
|
|
316
|
+
test("AC(a-culled): neutral patch loses on LOCAL minEffect ⇒ culled even when the peer claims nominated", async (t) => {
|
|
317
|
+
const s = await suite(t);
|
|
318
|
+
const { bundlePath } = await crossInstanceBundle(s, "culled"); // annotate-add comment patch
|
|
319
|
+
tamperManifest(bundlePath, (doc) => {
|
|
320
|
+
doc.stats.verdict = "nominated"; // lie the other way
|
|
321
|
+
});
|
|
322
|
+
const dst = await makeInstance(s, "dst");
|
|
323
|
+
const run = cli(dst, "graft", bundlePath, "--genome", "toy-smoke");
|
|
324
|
+
assert.equal(run.status, 1, "neutral graft loses locally");
|
|
325
|
+
assert.match(run.stdout + run.stderr, /culled/i);
|
|
326
|
+
const imports = graftRows(dst.repo);
|
|
327
|
+
assert.equal(imports.length, 1);
|
|
328
|
+
assert.equal(imports[0]?.row?.decision, "culled");
|
|
329
|
+
assert.equal(imports[0]?.row?.peerClaim?.verdict, "nominated", "peer claim recorded, not obeyed");
|
|
330
|
+
assert.ok(genRows(dst.repo).some((g) => g.data.source === "candidate" && g.data.verdict === "culled"));
|
|
331
|
+
assert.equal(incumbentSha(dst), null, "a culled graft touches no ref");
|
|
332
|
+
});
|
|
333
|
+
// ---------------------------------------------------------------- AC (b)
|
|
334
|
+
test("AC(b): tampered benchDigest ⇒ quarantined with expected-vs-actual, ZERO worktrees, ZERO bench, status lists it", async (t) => {
|
|
335
|
+
const s = await suite(t);
|
|
336
|
+
const { bundlePath, sourceFingerprint } = await crossInstanceBundle(s, "nominated");
|
|
337
|
+
tamperTreeStaleDigest(bundlePath); // member bytes changed + re-pinned, manifest digest stale
|
|
338
|
+
const dst = await makeInstance(s, "dst");
|
|
339
|
+
const run = cli(dst, "graft", bundlePath, "--genome", "toy-smoke");
|
|
340
|
+
assert.equal(run.status, 1, "integrity mismatch blocks");
|
|
341
|
+
const out = run.stdout + run.stderr;
|
|
342
|
+
assert.match(out, /quarantined/i, "names the quarantine decision");
|
|
343
|
+
assert.match(out, /benchDigest/i, "names WHICH gate failed");
|
|
344
|
+
assert.match(out, /[0-9a-f]{64}.*[0-9a-f]{64}/s, "shows expected vs actual digests");
|
|
345
|
+
const imports = graftRows(dst.repo);
|
|
346
|
+
assert.equal(imports.length, 1, "exactly one graft_import row");
|
|
347
|
+
assert.equal(imports[0]?.row?.decision, "quarantined");
|
|
348
|
+
assert.equal(imports[0]?.row?.imported, true);
|
|
349
|
+
assert.equal(imports[0]?.row?.sourceGenomeFingerprint, sourceFingerprint);
|
|
350
|
+
assert.equal(imports[0]?.row?.bundleSha256, bundleSha(bundlePath));
|
|
351
|
+
assert.ok(!existsSync(path.join(dst.xdgCache, "abathur", "worktrees")), "ZERO worktrees created");
|
|
352
|
+
assert.ok(!existsSync(path.join(dst.configDir, "bench-sandboxes")), "ZERO benches launched");
|
|
353
|
+
const gens = genRows(dst.repo);
|
|
354
|
+
assert.ok(!gens.some((g) => g.data.source === "candidate"), "no graft candidate generation row");
|
|
355
|
+
const st = cli(dst, "status", "toy-smoke");
|
|
356
|
+
assert.equal(st.status, 0);
|
|
357
|
+
assert.match(st.stdout, /graft decisions: 1/);
|
|
358
|
+
assert.match(st.stdout, /quarantined/);
|
|
359
|
+
});
|
|
360
|
+
test("wrong-genome gate: local spec fingerprint differs from the bundle's genome ⇒ quarantined naming fingerprints", async (t) => {
|
|
361
|
+
const s = await suite(t);
|
|
362
|
+
const { bundlePath } = await crossInstanceBundle(s, "nominated");
|
|
363
|
+
// different local bench config ⇒ different fingerprint, same label + path
|
|
364
|
+
const dst = await makeInstance(s, "dst", { minEffect: 0.6 });
|
|
365
|
+
const run = cli(dst, "graft", bundlePath, "--genome", "toy-smoke");
|
|
366
|
+
assert.equal(run.status, 1);
|
|
367
|
+
const out = run.stdout + run.stderr;
|
|
368
|
+
assert.match(out, /quarantined/i);
|
|
369
|
+
assert.match(out, /fingerprint/i, "names the genome-fingerprint gate");
|
|
370
|
+
const imports = graftRows(dst.repo);
|
|
371
|
+
assert.equal(imports.length, 1);
|
|
372
|
+
assert.equal(imports[0]?.row?.decision, "quarantined");
|
|
373
|
+
assert.ok(!existsSync(path.join(dst.xdgCache, "abathur", "worktrees")), "gate 1 quarantines before any worktree");
|
|
374
|
+
});
|
|
375
|
+
// ---------------------------------------------------------------- AC (c)
|
|
376
|
+
test("AC(c): genome not registered ⇒ pending-bench queue + status depth, ZERO bench; register + re-run resolves", async (t) => {
|
|
377
|
+
const s = await suite(t);
|
|
378
|
+
const { bundlePath, sourceFingerprint } = await crossInstanceBundle(s, "nominated");
|
|
379
|
+
// config home with NO genome registered at all:
|
|
380
|
+
const bare = path.join(s.root, "cfg-bare");
|
|
381
|
+
mkdirSync(bare, { recursive: true });
|
|
382
|
+
writeFileSync(path.join(bare, "config.jsonc"), '{ "opencodeBin": null }\n', "utf8");
|
|
383
|
+
const ghost = {
|
|
384
|
+
root: s.root, name: "bare", configDir: bare, repo: s.genomePath,
|
|
385
|
+
entry: null, xdgCache: path.join(s.root, "xdg-bare"),
|
|
386
|
+
cliEnv: { ...process.env, ABATHUR_CONFIG: path.join(bare, "config.jsonc"), HOME: path.join(s.root, "home-bare"), XDG_CACHE_HOME: path.join(s.root, "xdg-bare") },
|
|
387
|
+
};
|
|
388
|
+
const run = cli(ghost, "graft", bundlePath, "--genome", "toy-smoke");
|
|
389
|
+
assert.equal(run.status, 1, "pending-bench is a blocked decision, not a crash");
|
|
390
|
+
assert.match(run.stdout + run.stderr, /pending-bench/i);
|
|
391
|
+
const queueFile = graftQueuePath(bare, bundleSha(bundlePath));
|
|
392
|
+
assert.ok(existsSync(queueFile), "explicit queue entry persisted");
|
|
393
|
+
const entry = JSON.parse(readFileSync(queueFile, "utf8"));
|
|
394
|
+
assert.equal(entry.genomeLabel, "toy-smoke");
|
|
395
|
+
assert.equal(entry.bundlePath, bundlePath);
|
|
396
|
+
assert.equal(entry.sourceGenomeFingerprint, sourceFingerprint);
|
|
397
|
+
const st = cli(ghost, "status", "toy-smoke");
|
|
398
|
+
assert.equal(st.status, 0, "status renders the queue for an unregistered label");
|
|
399
|
+
assert.match(st.stdout, /pending-bench graft queue: 1/);
|
|
400
|
+
assert.match(st.stdout, new RegExp(bundleSha(bundlePath).slice(0, 12)));
|
|
401
|
+
assert.ok(!existsSync(path.join(bare, "bench-sandboxes")), "ZERO bench runs launched");
|
|
402
|
+
assert.ok(!existsSync(path.join(s.root, "xdg-bare", "abathur", "worktrees")), "ZERO worktrees");
|
|
403
|
+
assert.ok(!existsSync(path.join(s.genomePath, ".state")), "no genome repo ⇒ no ledger touched");
|
|
404
|
+
// operator registers the genome (in the SAME config home that holds the queue),
|
|
405
|
+
// re-runs graft: gates pass, queue entry consumed by the success-path decision.
|
|
406
|
+
const twin = await prepareToyGenome(s.genomePath);
|
|
407
|
+
registerGenome(bare, path.join(twin, "genome.jsonc"));
|
|
408
|
+
const again = cli(ghost, "graft", bundlePath, "--genome", "toy-smoke");
|
|
409
|
+
assert.equal(again.status, 0, `re-run grafts: ${again.stdout}${again.stderr}`);
|
|
410
|
+
assert.ok(!existsSync(queueFile), "queue entry consumed by the success-path decision");
|
|
411
|
+
// the resolved graft benched in the REAL per-genome cache dir (worktree.ts:4:
|
|
412
|
+
// $XDG_CACHE_HOME/abathur/worktrees/<genomeFp>/<genId>) — asserting a made-up
|
|
413
|
+
// child of the parent would be vacuously true.
|
|
414
|
+
const bareFp = requireGenomesByLabel(bare, "toy-smoke").entries[0]?.fingerprint;
|
|
415
|
+
assert.ok(bareFp !== undefined, "bare instance carries the registered fingerprint");
|
|
416
|
+
assert.ok(existsSync(path.join(s.root, "xdg-bare", "abathur", "worktrees", bareFp)), "resolved graft created its worktree under the per-genome cache dir");
|
|
417
|
+
const st2 = cli(ghost, "status", "toy-smoke");
|
|
418
|
+
assert.match(st2.stdout, /pending-bench graft queue: 0/);
|
|
419
|
+
assert.match(st2.stdout, /graft decisions: 1/);
|
|
420
|
+
});
|
|
421
|
+
test("requires[] probe failure ⇒ pending-bench row + queue + exit 2, ZERO bench; probe satisfied re-run grafts", async (t) => {
|
|
422
|
+
const s = await suite(t);
|
|
423
|
+
const marker = path.join(s.root, "probe-marker.mjs");
|
|
424
|
+
const requires = [{ cmd: "node", args: [marker], probeExit: 0 }];
|
|
425
|
+
writeFileSync(marker, "process.exit(0);\n", "utf8");
|
|
426
|
+
const src = await makeInstance(s, "src", { requires });
|
|
427
|
+
const rows = await evolveSource(src);
|
|
428
|
+
const fix = rows.find((r) => r.data.source === "candidate" && r.data.verdict === "nominated");
|
|
429
|
+
assert.ok(fix !== undefined);
|
|
430
|
+
const bundlePath = await exportFrom(src, fix.genId);
|
|
431
|
+
rmSync(s.genomePath, { recursive: true, force: true });
|
|
432
|
+
rmSync(marker); // the twin machine lacks the prerequisite
|
|
433
|
+
const dst = await makeInstance(s, "dst", { requires });
|
|
434
|
+
const run = cli(dst, "graft", bundlePath, "--genome", "toy-smoke");
|
|
435
|
+
assert.equal(run.status, 2, "probe failure cannot-answer");
|
|
436
|
+
assert.match(run.stdout + run.stderr, /pending-bench/i);
|
|
437
|
+
assert.match(run.stdout + run.stderr, /probe|prerequisite/i);
|
|
438
|
+
const imports = graftRows(dst.repo);
|
|
439
|
+
assert.equal(imports.length, 1);
|
|
440
|
+
assert.equal(imports[0]?.row?.decision, "pending-bench");
|
|
441
|
+
assert.ok(existsSync(graftQueuePath(dst.configDir, bundleSha(bundlePath))), "queue entry parked for the operator");
|
|
442
|
+
assert.ok(!existsSync(path.join(dst.xdgCache, "abathur", "worktrees")), "probe runs BEFORE any worktree");
|
|
443
|
+
assert.ok(!existsSync(path.join(dst.configDir, "bench-sandboxes")), "probe runs BEFORE any bench");
|
|
444
|
+
writeFileSync(marker, "process.exit(0);\n", "utf8");
|
|
445
|
+
const again = cli(dst, "graft", bundlePath, "--genome", "toy-smoke");
|
|
446
|
+
assert.equal(again.status, 0, `satisfied probe grafts through: ${again.stdout}${again.stderr}`);
|
|
447
|
+
assert.ok(!existsSync(graftQueuePath(dst.configDir, bundleSha(bundlePath))), "queue consumed");
|
|
448
|
+
const decisions = graftRows(dst.repo).map((x) => x.row?.decision);
|
|
449
|
+
assert.deepEqual(decisions, ["pending-bench", "nominated"], "pending rows do not trip the duplicate rule; terminal does");
|
|
450
|
+
});
|
|
451
|
+
// ------------------------------------------------------------ stale_state
|
|
452
|
+
test("stale_state: the same bundle grafts once — a second attempt refuses 'already grafted'", async (t) => {
|
|
453
|
+
const s = await suite(t);
|
|
454
|
+
const { bundlePath } = await crossInstanceBundle(s, "nominated");
|
|
455
|
+
const dst = await makeInstance(s, "dst");
|
|
456
|
+
assert.equal(cli(dst, "graft", bundlePath, "--genome", "toy-smoke").status, 0);
|
|
457
|
+
const twice = cli(dst, "graft", bundlePath, "--genome", "toy-smoke");
|
|
458
|
+
assert.equal(twice.status, 1);
|
|
459
|
+
assert.match(twice.stdout + twice.stderr, /already grafted/i);
|
|
460
|
+
assert.equal(graftRows(dst.repo).length, 1, "still exactly one decision row for the bundle");
|
|
461
|
+
assert.equal(genRows(dst.repo).filter((g) => g.data.source === "candidate").length, 1, "no second bench");
|
|
462
|
+
});
|
|
463
|
+
// --------------------------------------------------------- malformed_input
|
|
464
|
+
test("malformed_input: garbage container, stripped/invalid manifest, traversal tar ⇒ exit 2 clean, no state", async (t) => {
|
|
465
|
+
const s = await suite(t);
|
|
466
|
+
const { bundlePath } = await crossInstanceBundle(s, "nominated");
|
|
467
|
+
const dst = await makeInstance(s, "dst");
|
|
468
|
+
const junk = path.join(s.root, "junk.tgz");
|
|
469
|
+
writeFileSync(junk, "this is not a gzip stream at all\n", "utf8");
|
|
470
|
+
const r1 = cli(dst, "graft", junk, "--genome", "toy-smoke");
|
|
471
|
+
assert.equal(r1.status, 2);
|
|
472
|
+
assert.ok(!r1.stderr.includes(" at "), `no stack trace: ${r1.stderr}`);
|
|
473
|
+
const stripped = path.join(s.root, "stripped.bundle.tgz");
|
|
474
|
+
writeFileSync(stripped, new Uint8Array(gzipSync(writeTar(bundleMembers(bundlePath).filter((m) => m.path !== "manifest.json")))));
|
|
475
|
+
const r2 = cli(dst, "graft", stripped, "--genome", "toy-smoke");
|
|
476
|
+
assert.equal(r2.status, 2, "manifest missing cannot-answer");
|
|
477
|
+
const evil = path.join(s.root, "evil", "e1.bundle.tgz");
|
|
478
|
+
mkdirSync(path.dirname(evil), { recursive: true });
|
|
479
|
+
writeFileSync(evil, new Uint8Array(gzipSync(tarWithRawHeader(`../abathur-graft-evil-${process.pid}`))));
|
|
480
|
+
const r3 = cli(dst, "graft", evil, "--genome", "toy-smoke");
|
|
481
|
+
assert.equal(r3.status, 2, "traversal member refused end-to-end");
|
|
482
|
+
assert.ok(!existsSync(path.join(s.root, `abathur-graft-evil-${process.pid}`)), "evil member never materialised");
|
|
483
|
+
const badFlag = cli(dst, "graft", bundlePath, "--genome", "toy-smoke", "--force");
|
|
484
|
+
assert.equal(badFlag.status, 2, "no bypass flag exists");
|
|
485
|
+
const noLabel = cli(dst, "graft", bundlePath);
|
|
486
|
+
assert.equal(noLabel.status, 2);
|
|
487
|
+
const missing = cli(dst, "graft", path.join(s.root, "nope.tgz"), "--genome", "toy-smoke");
|
|
488
|
+
assert.equal(missing.status, 2);
|
|
489
|
+
// r1..missing each had their exit code asserted above; nothing left to loop over.
|
|
490
|
+
assert.equal(graftRows(dst.repo).length, 0, "integrity-garbage leaves no graft_import row (cannot-answer class)");
|
|
491
|
+
assert.ok(!existsSync(graftQueueDir(dst.configDir)), "garbage never queues pending-bench");
|
|
492
|
+
});
|
|
493
|
+
// --------------------------------------------------------- prompt_injection
|
|
494
|
+
test("prompt_injection: hostile bundle strings never forge status/echo lines", async (t) => {
|
|
495
|
+
const s = await suite(t);
|
|
496
|
+
const { bundlePath } = await crossInstanceBundle(s, "culled");
|
|
497
|
+
tamperManifest(bundlePath, (doc) => {
|
|
498
|
+
doc.rationale = "evil\n\u001b[31mred\u001b[0m fake: graft queue: 999";
|
|
499
|
+
const stats = doc.stats;
|
|
500
|
+
if (stats.matrix[0] !== undefined)
|
|
501
|
+
stats.matrix[0].failures = ["\u001b[31mOWNED\u001b[0m\nquarantine depth: 999"];
|
|
502
|
+
});
|
|
503
|
+
const dst = await makeInstance(s, "dst");
|
|
504
|
+
const run = cli(dst, "graft", bundlePath, "--genome", "toy-smoke");
|
|
505
|
+
assert.equal(run.status, 1, "culled locally regardless of injected strings");
|
|
506
|
+
assert.ok(!run.stdout.includes("\u001b"), "raw ANSI never reaches stdout");
|
|
507
|
+
assert.ok(!run.stderr.includes("\u001b"), "raw ANSI never reaches stderr");
|
|
508
|
+
assert.doesNotMatch(run.stdout, /OWNED/, "bundle stats matrix is never echoed");
|
|
509
|
+
for (const line of run.stdout.split("\n").filter((l) => l.length > 0)) {
|
|
510
|
+
assert.ok(!(line === "quarantine depth: 999" || line === "fake: graft queue: 999"), `injected line echoed: ${line}`);
|
|
511
|
+
}
|
|
512
|
+
const raw = readFileSync(path.join(dst.repo, ".state", "abathur", "ledger.jsonl"), "utf8");
|
|
513
|
+
assert.ok(!raw.includes("\u001b"), "ledger text carries no raw ESC byte (canonicalJson escapes)");
|
|
514
|
+
const st = cli(dst, "status", "toy-smoke");
|
|
515
|
+
assert.equal(st.status, 0);
|
|
516
|
+
assert.ok(!st.stdout.includes("\u001b"), "status stays escape-free rendering graft rows");
|
|
517
|
+
});
|
|
518
|
+
// ------------------------------------------------------------ dirty_worktree
|
|
519
|
+
test("dirty_worktree: local dirt never rides into the graft bench — the sealed bundle tree wins", async (t) => {
|
|
520
|
+
const s = await suite(t);
|
|
521
|
+
const { bundlePath } = await crossInstanceBundle(s, "nominated");
|
|
522
|
+
const dst = await makeInstance(s, "dst");
|
|
523
|
+
const headMul = gitC(dst.repo, "show", "HEAD:units/mul.mjs");
|
|
524
|
+
writeFileSync(path.join(dst.repo, "units", "mul.mjs"), `${headMul}\n// operator dirt, uncommitted\n`, "utf8");
|
|
525
|
+
writeFileSync(path.join(dst.repo, "untracked-dirt.txt"), "junk\n", "utf8");
|
|
526
|
+
const run = cli(dst, "graft", bundlePath, "--genome", "toy-smoke");
|
|
527
|
+
assert.equal(run.status, 0, `dirty worktree does not block graft: ${run.stdout}${run.stderr}`);
|
|
528
|
+
const candidate = genRows(dst.repo).find((g) => g.data.source === "candidate");
|
|
529
|
+
assert.ok(candidate !== undefined && candidate.data.commitSha !== undefined);
|
|
530
|
+
const sealedMul = gitC(dst.repo, "show", `${candidate.data.commitSha}:units/mul.mjs`);
|
|
531
|
+
assert.equal(sealedMul, headMul, "graft tree = bundle bytes, not the dirty worktree");
|
|
532
|
+
assert.doesNotMatch(sealedMul, /operator dirt/);
|
|
533
|
+
const status = gitC(dst.repo, "status", "--porcelain");
|
|
534
|
+
assert.match(status, /units\/mul\.mjs/, "operator dirt left exactly as found");
|
|
535
|
+
});
|
|
536
|
+
test("queue listing is fail-closed: absent dir ⇒ empty, unreadable dir ⇒ rethrow (F2 A3)", async (t) => {
|
|
537
|
+
const cfg = await mkdtemp(path.join(os.tmpdir(), "abathur-queue-failclosed-"));
|
|
538
|
+
t.after(() => rmSync(cfg, { recursive: true, force: true }));
|
|
539
|
+
assert.deepEqual(listGraftQueue(cfg), [], "no queue yet = empty, not an error (ENOENT stays tolerated)");
|
|
540
|
+
// A queue path occupied by a regular file is corruption, not absence: readdirSync
|
|
541
|
+
// fails ENOTDIR and that must bubble — swallowing it rendered a real queue as
|
|
542
|
+
// empty, the fail-open class F2 flagged. chmod-free seam, so it runs as any user.
|
|
543
|
+
writeFileSync(path.join(cfg, "graft-queue"), "not a dir\n", "utf8");
|
|
544
|
+
assert.throws(() => listGraftQueue(cfg), /ENOTDIR/);
|
|
545
|
+
});
|
|
546
|
+
// ---------------------------------------------------------------- structural
|
|
547
|
+
test("structural: graft never imports the promote gate; no --force anywhere in graft code", () => {
|
|
548
|
+
const graftFiles = ["src/core/graft.ts", "src/core/graft-support.ts", "src/core/graft-gates.ts", "src/core/graft-rebench.ts", "src/commands/graft.ts"];
|
|
549
|
+
for (const rel of graftFiles) {
|
|
550
|
+
const text = readFileSync(path.join(REPO_ROOT, rel), "utf8");
|
|
551
|
+
// comments may DOCUMENT the absence of a bypass; the pin is on real code
|
|
552
|
+
const code = text.split("\n").filter((line) => !/^\s*(\/\/|\*)/.test(line)).join("\n");
|
|
553
|
+
assert.doesNotMatch(code, /from\s+"[^"]*promote\.js"/, `${rel} must not import promote`);
|
|
554
|
+
assert.doesNotMatch(code, /--force/, `${rel} must not offer a bypass flag`);
|
|
555
|
+
}
|
|
556
|
+
const queue = readdirSync(path.join(REPO_ROOT, "src", "core"));
|
|
557
|
+
assert.ok(queue.includes("graft.ts") && queue.includes("graft-support.ts"), "graft modules exist");
|
|
558
|
+
});
|
|
559
|
+
test("status without queue or graft rows still prints the honest zero (AC(d) compat)", async (t) => {
|
|
560
|
+
const s = await suite(t);
|
|
561
|
+
const dst = await makeInstance(s, "dst");
|
|
562
|
+
const st = cli(dst, "status", "toy-smoke");
|
|
563
|
+
assert.equal(st.status, 0);
|
|
564
|
+
assert.match(st.stdout, /pending-bench graft queue: 0/);
|
|
565
|
+
assert.match(st.stdout, /quarantine depth: 0/);
|
|
566
|
+
assert.match(st.stdout, /graft decisions: 0/);
|
|
567
|
+
});
|