@tachikomagundam/abathur 0.2.3 → 0.2.4

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.
@@ -8,6 +8,11 @@
8
8
  //
9
9
  // ${ABATHUR_HISTORIAN_REPO} absolute path of the historian bench repo
10
10
  // (contains scenarios/, rubric.md, seed_sandbox.sh)
11
+ // — repoPath ONLY: the S4 engine seam delivers the
12
+ // bench's ACTIVE TREE (incumbent repoPath /
13
+ // candidate worktree) to the commands below as the
14
+ // {repoRoot} template var, so unit/seed paths must
15
+ // NOT bake a materialized repo literal here.
11
16
  // ${ABATHUR_REPO} absolute path of THIS Abathur checkout
12
17
  // (hosts graders/historian/*)
13
18
  // ${ABATHUR_WIKI_BASE} canonical wiki root, e.g. http://localhost:3000
@@ -47,11 +52,11 @@
47
52
  { "id": "scenario-08", "path": "scenarios/08-current-state-g5.md", "split": "train" },
48
53
  { "id": "scenario-09", "path": "scenarios/09-timeline-week-groups.md", "split": "val" }
49
54
  ],
50
- "seedCommand": "bash ${ABATHUR_REPO}/graders/historian/seed-wrapped.sh ${ABATHUR_HISTORIAN_REPO}/seed_sandbox.sh",
55
+ "seedCommand": "bash ${ABATHUR_REPO}/graders/historian/seed-wrapped.sh {repoRoot}/seed_sandbox.sh",
51
56
  "resetCommand": "bash ${ABATHUR_REPO}/graders/historian/reset-sandbox.sh",
52
- "runCommand": "bash ${ABATHUR_REPO}/graders/historian/run-scenario.sh {unit.id} ${ABATHUR_HISTORIAN_REPO}/{unit.path}",
53
- "graderCommand": "node ${ABATHUR_REPO}/graders/historian/grader.mjs {unit.id} ${ABATHUR_HISTORIAN_REPO}/{unit.path} ${ABATHUR_WIKI_BASE}",
54
- "agentModel": "bailian-token-plan/qwen3.8-flash",
57
+ "runCommand": "bash ${ABATHUR_REPO}/graders/historian/run-scenario.sh {unit.id} {repoRoot}/{unit.path}",
58
+ "graderCommand": "node ${ABATHUR_REPO}/graders/historian/grader.mjs {unit.id} {repoRoot}/{unit.path} ${ABATHUR_WIKI_BASE}",
59
+ "agentModel": "<provider>/<model-id>", // "<provider>/<model-id> as registered in your opencode config"
55
60
  // Real agent runs, not unit tests: 1200s caps one stuck scenario without
56
61
  // poisoning the generation (timeout => unscored/inconclusive, excluded by
57
62
  // the stats layer). seed+reset hooks share a FIXED 60s adapter budget
@@ -10,11 +10,29 @@
10
10
  // excluded from score distributions (hr discipline), not zeroed.
11
11
  import { spawn } from "node:child_process";
12
12
  import { cannotAnswer } from "../exit.js";
13
- export function unitVars(unit, sandboxDir) {
14
- return { "unit.path": unit.path, "unit.id": unit.id, sandbox: sandboxDir, workdir: sandboxDir };
13
+ /**
14
+ * `{repoRoot}` = the bench's ACTIVE TREE: the genome repoPath for the incumbent,
15
+ * the sealed candidate worktree for a candidate (run-loop.ts swaps spec.repoPath
16
+ * per target; adapters resolve it through effectiveRepoPath at construction).
17
+ * Omitting repoRoot keeps `{repoRoot}` an UNKNOWN placeholder — fail-closed
18
+ * exit 2, never a silently half-substituted command.
19
+ */
20
+ export function unitVars(unit, sandboxDir, repoRoot) {
21
+ const vars = {
22
+ "unit.path": unit.path,
23
+ "unit.id": unit.id,
24
+ sandbox: sandboxDir,
25
+ workdir: sandboxDir,
26
+ };
27
+ if (repoRoot !== undefined)
28
+ vars["repoRoot"] = repoRoot;
29
+ return vars;
15
30
  }
16
- export function sandboxVars(sandboxDir) {
17
- return { sandbox: sandboxDir, workdir: sandboxDir };
31
+ export function sandboxVars(sandboxDir, repoRoot) {
32
+ const vars = { sandbox: sandboxDir, workdir: sandboxDir };
33
+ if (repoRoot !== undefined)
34
+ vars["repoRoot"] = repoRoot;
35
+ return vars;
18
36
  }
19
37
  const PLACEHOLDER = /\{([A-Za-z0-9_.]+)\}/g;
20
38
  /**
@@ -32,6 +32,7 @@ import { cannotAnswer, ExitSignal } from "../exit.js";
32
32
  import { resolveConfigDir } from "../config.js";
33
33
  import { fingerprint } from "../core/ids.js";
34
34
  import { acquireGenomeLock, Ledger } from "../core/ledger.js";
35
+ import { effectiveRepoPath } from "../core/spec.js";
35
36
  import { childStatus, firstLine, inconclusive, parseGraderLine, renderCommand, runChild, sandboxVars, unitVars, ZERO_METRICS, } from "./adapter.js";
36
37
  import { buildManifest, gateVal, mirrorSandboxHome, parseRunMeta, sandboxHomeDir, transcriptPathFor, writeManifest, } from "./fixture-support.js";
37
38
  import { probeEngines, resolveOpencodeBin } from "./fixture-probe.js";
@@ -52,7 +53,10 @@ export class FixtureScenariosAdapter {
52
53
  if (spec.bench.type !== "opencode-fixture-scenarios") {
53
54
  cannotAnswer(`fixture adapter: bench.type '${spec.bench.type}' is not "opencode-fixture-scenarios"`, "use ToyBenchAdapter for bench.type 'toy'");
54
55
  }
55
- const root = path.resolve(spec.repoPath);
56
+ // Active-tree resolution follows the run-loop notion (run-loop.ts:142): a
57
+ // concrete repoPath resolves exactly as before; a `${VAR}` env literal
58
+ // resolves at THIS fs boundary or exits 2 naming the variable.
59
+ const root = effectiveRepoPath(spec.repoPath);
56
60
  if (!statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
57
61
  cannotAnswer(`fixture: repoPath '${spec.repoPath}' is not a readable directory`);
58
62
  }
@@ -84,7 +88,7 @@ export class FixtureScenariosAdapter {
84
88
  const transcript = transcriptPathFor(sandboxDir, unit.id);
85
89
  mkdirSync(path.dirname(transcript), { recursive: true });
86
90
  const outcome = await runChild({
87
- argv: renderCommand(this.spec.bench.runCommand, unitVars(unit, sandboxDir)),
91
+ argv: renderCommand(this.spec.bench.runCommand, unitVars(unit, sandboxDir, this.repoRoot)),
88
92
  cwd: sandboxDir,
89
93
  timeoutS,
90
94
  env: this.sandboxEnv(sandboxDir, unit),
@@ -109,7 +113,7 @@ export class FixtureScenariosAdapter {
109
113
  cannotAnswer("fixture adapter: score() called before any reset()/seed()/run() — no sandbox yet", "drive the adapter in reset→seed→run→score order");
110
114
  }
111
115
  const outcome = await runChild({
112
- argv: renderCommand(this.spec.bench.graderCommand, unitVars(unit, sandbox)),
116
+ argv: renderCommand(this.spec.bench.graderCommand, unitVars(unit, sandbox, this.repoRoot)),
113
117
  cwd: sandbox,
114
118
  timeoutS: this.spec.bench.timeoutS,
115
119
  env: this.sandboxEnv(sandbox),
@@ -211,7 +215,7 @@ export class FixtureScenariosAdapter {
211
215
  if (template === undefined)
212
216
  return;
213
217
  const outcome = await runChild({
214
- argv: renderCommand(template, sandboxVars(sandboxDir)),
218
+ argv: renderCommand(template, sandboxVars(sandboxDir, this.repoRoot)),
215
219
  cwd: sandboxDir,
216
220
  timeoutS: HOOK_TIMEOUT_S,
217
221
  env: this.sandboxEnv(sandboxDir),
package/dist/bench/toy.js CHANGED
@@ -53,7 +53,7 @@ export class ToyBenchAdapter {
53
53
  async run(unit, sandboxDir, timeoutS) {
54
54
  this.activeSandbox = sandboxDir;
55
55
  const outcome = await runChild({
56
- argv: renderCommand(this.spec.bench.runCommand, unitVars(unit, sandboxDir)),
56
+ argv: renderCommand(this.spec.bench.runCommand, unitVars(unit, sandboxDir, this.repoRoot)),
57
57
  cwd: sandboxDir,
58
58
  timeoutS,
59
59
  ...(this.opts.onChild === undefined ? {} : { onChild: this.opts.onChild }),
@@ -75,7 +75,7 @@ export class ToyBenchAdapter {
75
75
  cannotAnswer("toy adapter: score() called before any reset()/seed()/run() — no sandbox yet", "drive the adapter in reset→seed→run→score order");
76
76
  }
77
77
  const outcome = await runChild({
78
- argv: renderCommand(this.spec.bench.graderCommand, unitVars(unit, sandbox)),
78
+ argv: renderCommand(this.spec.bench.graderCommand, unitVars(unit, sandbox, this.repoRoot)),
79
79
  cwd: sandbox,
80
80
  timeoutS: this.spec.bench.timeoutS,
81
81
  ...(this.opts.onChild === undefined ? {} : { onChild: this.opts.onChild }),
@@ -109,7 +109,7 @@ export class ToyBenchAdapter {
109
109
  if (template === undefined)
110
110
  return;
111
111
  const outcome = await runChild({
112
- argv: renderCommand(template, sandboxVars(sandboxDir)),
112
+ argv: renderCommand(template, sandboxVars(sandboxDir, this.repoRoot)),
113
113
  cwd: sandboxDir,
114
114
  timeoutS: HOOK_TIMEOUT_S,
115
115
  ...(this.opts.onChild === undefined ? {} : { onChild: this.opts.onChild }),
@@ -15,6 +15,7 @@ import { tryGit } from "../util/git.js";
15
15
  import { readRegistry } from "../core/genome.js";
16
16
  import { LEDGER_KIND_GRAFT_IMPORT, decodeGraftImport, listGraftQueue, } from "../core/graft-support.js";
17
17
  import { resolveUniqueEntry } from "./run.js";
18
+ import { effectiveRepoPath } from "../core/spec.js";
18
19
  const KIND_GENERATION = "generation_complete";
19
20
  const KIND_PROMOTE = "promote";
20
21
  const KIND_TOMBSTONE = "tombstone";
@@ -113,7 +114,11 @@ async function runStatus(args) {
113
114
  renderQueue(queue);
114
115
  return EXIT_OK;
115
116
  }
116
- const repo = entry.spec.repoPath;
117
+ // Resolve at the fs seam exactly like run/genome/kernel (run-loop.ts:142):
118
+ // feeding the stored UNRESOLVED `${VAR}` literal to git/ledger paths makes
119
+ // Node report the missing cwd as the misleading `spawn git ENOENT`; an unset
120
+ // variable is now a clean exit 2 that names it.
121
+ const repo = effectiveRepoPath(entry.spec.repoPath);
117
122
  const records = readLedgerRecords(ledgerPath(repo));
118
123
  const gens = generations(records);
119
124
  writeStdout(`genome: ${flat(entry.label)} (${entry.fingerprint}) repo: ${flat(repo, 200)}`);
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { cannotAnswer } from "../../exit.js";
5
5
  import { runId } from "../ids.js";
6
6
  import { buildManifest } from "../kernel.js";
7
+ import { effectiveRepoPath } from "../spec.js";
7
8
  import { budgetExhausted } from "../stats.js";
8
9
  import { ToyBenchAdapter } from "../../bench/toy.js";
9
10
  import { FixtureScenariosAdapter } from "../../bench/fixture.js";
@@ -164,7 +165,7 @@ export async function benchTarget(o) {
164
165
  spent,
165
166
  provenance: provenance ?? { benchType: o.spec.bench.type, versions: [] },
166
167
  complete,
167
- manifest: buildManifest(path.resolve(o.spec.repoPath), o.spec.kernel.immutableGlobs),
168
+ manifest: buildManifest(effectiveRepoPath(o.spec.repoPath), o.spec.kernel.immutableGlobs),
168
169
  failures,
169
170
  };
170
171
  }
@@ -31,3 +31,27 @@ test("renderCommand: quote-aware argv, placeholders, fail-closed junk (exit 2)",
31
31
  expectExit2(() => renderCommand(" ", unitVars(unit, "/s")), "empty");
32
32
  expectExit2(() => renderCommand('node "oops', unitVars(unit, "/s")), "unbalanced quote");
33
33
  });
34
+ // S4 engine seam: {repoRoot} = the bench's ACTIVE TREE, threaded through the
35
+ // shared CommandVars constructors so both adapters resolve it identically.
36
+ // Without a threaded repoRoot the placeholder must stay FAIL-CLOSED (exit 2),
37
+ // exactly like any unknown placeholder — never a half-substituted command.
38
+ test("{repoRoot}: unitVars/sandboxVars carry the active tree into run/grader/hook templates", () => {
39
+ const unit = { id: "u1", path: "scenarios/a.md", split: "train" };
40
+ assert.deepEqual(renderCommand("bash run-scenario.sh {unit.id} {repoRoot}/{unit.path}", unitVars(unit, "/sbx", "/active/tree")), ["bash", "run-scenario.sh", "u1", "/active/tree/scenarios/a.md"]);
41
+ assert.deepEqual(renderCommand("bash seed-wrapped.sh {repoRoot}/seed_sandbox.sh", sandboxVars("/sbx", "/active/tree")), ["bash", "seed-wrapped.sh", "/active/tree/seed_sandbox.sh"]);
42
+ // pre-existing keys untouched by the seam
43
+ assert.deepEqual(unitVars(unit, "/sbx", "/r"), {
44
+ "unit.path": "scenarios/a.md",
45
+ "unit.id": "u1",
46
+ sandbox: "/sbx",
47
+ workdir: "/sbx",
48
+ repoRoot: "/r",
49
+ });
50
+ assert.deepEqual(sandboxVars("/sbx", "/r"), { sandbox: "/sbx", workdir: "/sbx", repoRoot: "/r" });
51
+ });
52
+ test("{repoRoot} unthreaded ⇒ fail-closed exit 2 (unknown placeholder behavior UNCHANGED)", () => {
53
+ const unit = { id: "u1", path: "p", split: "train" };
54
+ expectExit2(() => renderCommand("node x {repoRoot}", unitVars(unit, "/s")), "unknown placeholder");
55
+ expectExit2(() => renderCommand("node x {repoRoot}", sandboxVars("/s")), "unknown placeholder");
56
+ expectExit2(() => renderCommand("node x {repoRoot}", { worktree: "/w", brief: "/b" }), "unknown placeholder");
57
+ });
@@ -0,0 +1,197 @@
1
+ // S4 engine seam — the {repoRoot} command variable. The mutation channel's
2
+ // delivery hinge: run/grader/seed/reset templates must resolve {repoRoot} to
3
+ // the bench's ACTIVE TREE — the genome repoPath for the incumbent, the sealed
4
+ // candidate worktree for a candidate (run-loop.ts swaps `spec.repoPath` before
5
+ // benching each candidate; the adapter must follow that same notion, including
6
+ // effectiveRepoPath's env-literal resolution at the FS boundary).
7
+ // Unknown placeholders stay fail-closed (exit 2). Offline: capture scripts
8
+ // record the rendered argv; the fake opencode only answers --version.
9
+ import assert from "node:assert/strict";
10
+ import { chmodSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
12
+ import * as os from "node:os";
13
+ import path from "node:path";
14
+ import { test } from "node:test";
15
+ import { ExitSignal } from "../exit.js";
16
+ import { parseGenomeSpecDocument } from "../core/spec.js";
17
+ import { FixtureScenariosAdapter } from "../bench/fixture.js";
18
+ // ------------------------------------------------------------------- helpers
19
+ async function freshDir(t, prefix) {
20
+ const dir = await mkdtemp(path.join(os.tmpdir(), `abathur-${prefix}-`));
21
+ t.after(() => rm(dir, { recursive: true, force: true }));
22
+ return dir;
23
+ }
24
+ async function writeScript(dir, name, body) {
25
+ const filePath = path.join(dir, name);
26
+ await writeFile(filePath, body, "utf8");
27
+ chmodSync(filePath, 0o755);
28
+ return filePath;
29
+ }
30
+ /** Records `argv[mode, out, ...rest]`: rest joined per line into `out`; prints the metric/score line. */
31
+ const CAPTURE = `#!/usr/bin/env node
32
+ import { writeFileSync } from "node:fs";
33
+ const [mode, out, ...rest] = process.argv.slice(2);
34
+ writeFileSync(out, rest.join("\\n") + "\\n", "utf8");
35
+ if (mode === "score") {
36
+ process.stdout.write(JSON.stringify({ unit: "one", score: 1, pass: true, metrics: { tokensEst: 3, turns: 1 } }) + "\\n");
37
+ } else {
38
+ process.stdout.write(JSON.stringify({ tokensEst: 3, turns: 1 }) + "\\n");
39
+ }
40
+ `;
41
+ const FAKE_OC = `#!/usr/bin/env bash
42
+ if [ "\${1:-}" = "--version" ]; then echo "1.2.3"; exit 0; fi
43
+ exit 0
44
+ `;
45
+ const UNIT = { id: "one", path: "scenarios/one.md", split: "train" };
46
+ const VAL_UNIT = { id: "v1", path: "scenarios/v1.md", split: "val" };
47
+ async function seamFixture(t, genDir) {
48
+ const root = await freshDir(t, "seam-ad");
49
+ const capPath = await writeScript(root, "cap.mjs", CAPTURE);
50
+ const binDir = await freshDir(t, "seam-bin");
51
+ const binPath = await writeScript(binDir, "oc-fake", FAKE_OC);
52
+ const home = await freshDir(t, "seam-home");
53
+ const configDir = await freshDir(t, "seam-cfg");
54
+ const outDir = path.join(root, "caps");
55
+ mkdirSync(outDir, { recursive: true });
56
+ const out = (name) => path.join(outDir, `${name}.txt`);
57
+ const spec = parseGenomeSpecDocument({
58
+ label: "seam-germ",
59
+ repoPath: genDir,
60
+ bench: {
61
+ type: "opencode-fixture-scenarios",
62
+ units: [UNIT, VAL_UNIT],
63
+ runCommand: `node ${capPath} run ${out("run")} {repoRoot} {unit.path}`,
64
+ seedCommand: `node ${capPath} seed ${out("seed")} {repoRoot}`,
65
+ resetCommand: `node ${capPath} reset ${out("reset")} {repoRoot}`,
66
+ graderCommand: `node ${capPath} score ${out("grader")} {repoRoot}`,
67
+ agentModel: "test/agent",
68
+ timeoutS: 30,
69
+ stats: { halfWidth: 0.15, minEffect: 0.1, nReps: { initial: 1, max: 2 } },
70
+ },
71
+ budget: { maxCandidates: 2, maxModelCalls: 4, maxTokens: 10000, maxWallS: 600 },
72
+ kernel: { immutableGlobs: [] },
73
+ requires: [{ cmd: "node", args: ["--version"], probeExit: 0 }],
74
+ opencodeBinVersion: { minVersion: "1.0.0" },
75
+ }, "<test>");
76
+ const sandbox = path.join(root, "sandbox");
77
+ return {
78
+ spec,
79
+ genDir,
80
+ sandbox,
81
+ home,
82
+ cap: capPath,
83
+ binPath,
84
+ out,
85
+ adapter: (s) => new FixtureScenariosAdapter(s, { env: { HOME: home }, home, configDir, opencodeBin: binPath }),
86
+ };
87
+ }
88
+ /** Minimal genome dir (the fixture adapter only requires repoPath to be a directory). */
89
+ async function genomeTree(t, name = "germ") {
90
+ const genRoot = await freshDir(t, `seam-${name}`);
91
+ const genDir = path.join(genRoot, "germ");
92
+ mkdirSync(path.join(genDir, "scenarios"), { recursive: true });
93
+ writeFileSync(path.join(genDir, "scenarios", "one.md"), "# one\n\n## Brief\ndo it\n", "utf8");
94
+ writeFileSync(path.join(genDir, "scenarios", "v1.md"), "# v1\n\n## Brief\ndo it val\n", "utf8");
95
+ return genDir;
96
+ }
97
+ function readCap(file) {
98
+ return readFileSync(file, "utf8").trim();
99
+ }
100
+ function isExit2Matching(needle) {
101
+ return (cause) => cause instanceof ExitSignal && cause.code === 2 && needle.test(cause.message);
102
+ }
103
+ // --------------------------------------------------------------------- pins
104
+ test("{repoRoot} renders to the incumbent genome repoPath in run + grader + hooks", async (t) => {
105
+ const genDir = await genomeTree(t);
106
+ const f = await seamFixture(t, genDir);
107
+ const adapter = f.adapter(f.spec);
108
+ try {
109
+ await adapter.reset(f.sandbox);
110
+ await adapter.seed(f.sandbox);
111
+ const run = await adapter.run(UNIT, f.sandbox, 30);
112
+ assert.equal(run.status, "ok");
113
+ const score = await adapter.score(UNIT);
114
+ assert.equal(score.kind, "scored");
115
+ assert.equal(readCap(f.out("run")), `${genDir}\nscenarios/one.md`);
116
+ assert.equal(readCap(f.out("grader")), genDir);
117
+ assert.equal(readCap(f.out("seed")), genDir);
118
+ assert.equal(readCap(f.out("reset")), genDir);
119
+ }
120
+ finally {
121
+ adapter.release();
122
+ }
123
+ });
124
+ test("{repoRoot} follows the candidate worktree when spec.repoPath is swapped (run-loop.ts pattern)", async (t) => {
125
+ const genDir = await genomeTree(t);
126
+ const f = await seamFixture(t, genDir);
127
+ // simulate run-loop.ts:279 — benchTarget receives { ...spec, repoPath: worktreePath }
128
+ const worktree = path.join(path.dirname(genDir), "wt");
129
+ cpSync(genDir, worktree, { recursive: true });
130
+ mkdirSync(path.join(worktree, "abathur-notes"), { recursive: true });
131
+ writeFileSync(path.join(worktree, "abathur-notes", "card.md"), "planted run card\n", "utf8");
132
+ const candidate = f.adapter({ ...f.spec, repoPath: worktree });
133
+ try {
134
+ const sandbox = path.join(f.sandbox, "cand");
135
+ await candidate.reset(sandbox);
136
+ await candidate.seed(sandbox);
137
+ const run = await candidate.run(UNIT, sandbox, 30);
138
+ assert.equal(run.status, "ok");
139
+ const score = await candidate.score(UNIT);
140
+ assert.equal(score.kind, "scored");
141
+ assert.equal(readCap(f.out("run")), `${worktree}\nscenarios/one.md`);
142
+ assert.equal(readCap(f.out("grader")), worktree);
143
+ }
144
+ finally {
145
+ candidate.release();
146
+ }
147
+ });
148
+ test("fixture ctor resolves an env-literal repoPath via effectiveRepoPath (run-loop.ts:142 pattern)", async (t) => {
149
+ const genDir = await genomeTree(t, "lit");
150
+ const f = await seamFixture(t, genDir);
151
+ const litSpec = { ...f.spec, repoPath: "${ABATHUR_SEAM_LIT_REPO}" };
152
+ const base = {
153
+ env: { HOME: f.home },
154
+ home: f.home,
155
+ configDir: path.join(f.home, ".config", "abathur"),
156
+ opencodeBin: f.binPath,
157
+ };
158
+ delete process.env["ABATHUR_SEAM_LIT_REPO"];
159
+ try {
160
+ // unset env ⇒ clean exit 2 NAMING the variable (never a garbage-path spawn error)
161
+ assert.throws(() => new FixtureScenariosAdapter(litSpec, base), isExit2Matching(/requires env var ABATHUR_SEAM_LIT_REPO/));
162
+ // set env ⇒ resolves at the FS boundary; {repoRoot} renders to the resolved tree
163
+ process.env["ABATHUR_SEAM_LIT_REPO"] = genDir;
164
+ const adapter = new FixtureScenariosAdapter(litSpec, base);
165
+ try {
166
+ const sandbox = path.join(f.sandbox, "lit");
167
+ await adapter.reset(sandbox);
168
+ await adapter.seed(sandbox);
169
+ const run = await adapter.run(UNIT, sandbox, 30);
170
+ assert.equal(run.status, "ok");
171
+ assert.equal(readCap(f.out("run")), `${genDir}\nscenarios/one.md`);
172
+ }
173
+ finally {
174
+ adapter.release();
175
+ }
176
+ }
177
+ finally {
178
+ delete process.env["ABATHUR_SEAM_LIT_REPO"];
179
+ }
180
+ });
181
+ test("fail-closed UNCHANGED: unknown placeholder through the adapter is still exit 2", async (t) => {
182
+ const genDir = await genomeTree(t, "unk");
183
+ const f = await seamFixture(t, genDir);
184
+ const bad = {
185
+ ...f.spec,
186
+ bench: { ...f.spec.bench, runCommand: `node ${f.cap} run ${f.out("bad")} {nope}` },
187
+ };
188
+ const adapter = f.adapter(bad);
189
+ try {
190
+ await adapter.reset(f.sandbox);
191
+ await adapter.seed(f.sandbox);
192
+ await assert.rejects(() => adapter.run(UNIT, f.sandbox, 30), isExit2Matching(/unknown placeholder/));
193
+ }
194
+ finally {
195
+ adapter.release();
196
+ }
197
+ });