@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,251 @@
|
|
|
1
|
+
// Todo 5 acceptance pins (adapter-level only — the CLI `run` lives in todo 9):
|
|
2
|
+
// (a) seed→run→score cycle; (b) identical seed ⇒ identical score matrix;
|
|
3
|
+
// (c) state-mutating unit ⇒ reset restores the exact pre-state digest;
|
|
4
|
+
// (d) grader exit-1 ⇒ inconclusive-for-unit, never a crash;
|
|
5
|
+
// (e) hanging unit ⇒ process-group kill + recorded timeout status.
|
|
6
|
+
// Plan-literal: toy path needs no opencode, no network, no absolute machine paths.
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { test } from "node:test";
|
|
14
|
+
import { ExitSignal } from "../exit.js";
|
|
15
|
+
import { treeDigestAt } from "../core/ids.js";
|
|
16
|
+
import { loadGenomeSpecFile } from "../core/spec.js";
|
|
17
|
+
import { applyPatch, scriptedPatches } from "../core/evolve/stub-mutators.mjs";
|
|
18
|
+
import {} from "../bench/adapter.js";
|
|
19
|
+
import { prepareToyGenome, ToyBenchAdapter } from "../bench/toy.js";
|
|
20
|
+
// ------------------------------------------------------------------- fixtures
|
|
21
|
+
function keep(t, dir) {
|
|
22
|
+
t.after(() => rm(dir, { recursive: true, force: true }));
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
async function freshDir(t, prefix) {
|
|
26
|
+
return keep(t, await mkdtemp(path.join(os.tmpdir(), `abathur-${prefix}-`)));
|
|
27
|
+
}
|
|
28
|
+
/** Self-init toy genome: tmp copy + git repo + repoPath rewritten (init.mjs contract). */
|
|
29
|
+
async function toyFixture(t) {
|
|
30
|
+
const genomeDir = path.join(await freshDir(t, "toygerm"), "toy-smoke");
|
|
31
|
+
await prepareToyGenome(genomeDir);
|
|
32
|
+
return { genomeDir, spec: loadGenomeSpecFile(path.join(genomeDir, "genome.jsonc")) };
|
|
33
|
+
}
|
|
34
|
+
function unitById(spec, id) {
|
|
35
|
+
const unit = spec.bench.units.find((u) => u.id === id);
|
|
36
|
+
if (unit === undefined)
|
|
37
|
+
throw new Error(`fixture spec has no unit '${id}'`);
|
|
38
|
+
return unit;
|
|
39
|
+
}
|
|
40
|
+
function scored(outcome) {
|
|
41
|
+
if (outcome.kind === "scored")
|
|
42
|
+
return outcome.result;
|
|
43
|
+
assert.fail(`expected scored, got inconclusive: ${outcome.reason}`);
|
|
44
|
+
}
|
|
45
|
+
async function scoreMatrix(spec, sandboxDir) {
|
|
46
|
+
const adapter = new ToyBenchAdapter(spec);
|
|
47
|
+
await adapter.reset(sandboxDir);
|
|
48
|
+
await adapter.seed(sandboxDir);
|
|
49
|
+
const rows = [];
|
|
50
|
+
for (const unit of spec.bench.units) {
|
|
51
|
+
rows.push({
|
|
52
|
+
unitId: unit.id,
|
|
53
|
+
run: await adapter.run(unit, sandboxDir, spec.bench.timeoutS),
|
|
54
|
+
score: await adapter.score(unit),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return rows;
|
|
58
|
+
}
|
|
59
|
+
// ------------------------------------------------------------------ AC (a)
|
|
60
|
+
test("(a) seed→run→score cycle: ok statuses, provenance, metrics, val+train gradable", async (t) => {
|
|
61
|
+
const { genomeDir, spec } = await toyFixture(t);
|
|
62
|
+
const sandbox = await freshDir(t, "toysbx");
|
|
63
|
+
const adapter = new ToyBenchAdapter(spec);
|
|
64
|
+
await adapter.reset(sandbox);
|
|
65
|
+
await adapter.seed(sandbox);
|
|
66
|
+
const rows = new Map();
|
|
67
|
+
for (const unit of spec.bench.units) {
|
|
68
|
+
rows.set(unit.id, {
|
|
69
|
+
run: await adapter.run(unit, sandbox, spec.bench.timeoutS),
|
|
70
|
+
score: await adapter.score(unit),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (const { run } of rows.values()) {
|
|
74
|
+
assert.equal(run.status, "ok");
|
|
75
|
+
assert.equal(run.benchProvenance.benchType, "toy");
|
|
76
|
+
assert.deepEqual(run.benchProvenance.versions, [{ bin: "node", version: process.version }]);
|
|
77
|
+
assert.equal(run.metrics.turns, 1);
|
|
78
|
+
assert.ok(run.metrics.tokensEst >= 1);
|
|
79
|
+
}
|
|
80
|
+
// a non-zero unit exit is an observation on an ok run, not an infra failure
|
|
81
|
+
assert.equal(rows.get("explode")?.run.exitCode, 1);
|
|
82
|
+
assert.equal(rows.get("add")?.run.exitCode, 0);
|
|
83
|
+
assert.equal(rows.get("add")?.run.note, undefined);
|
|
84
|
+
assert.equal(scored(rows.get("add").score).score, 0); // seeded bug is graded
|
|
85
|
+
assert.equal(scored(rows.get("add").score).pass, false);
|
|
86
|
+
assert.equal(scored(rows.get("mul").score).score, 1);
|
|
87
|
+
assert.equal(scored(rows.get("sub").score).pass, true); // val unit gradable
|
|
88
|
+
const mulBytes = readFileSync(path.join(sandbox, "units", "mul.mjs")).length;
|
|
89
|
+
assert.equal(rows.get("mul")?.run.metrics.tokensEst, Math.ceil(mulBytes / 4));
|
|
90
|
+
assert.ok(existsSync(path.join(genomeDir, ".git")), "init.mjs must self-create the git repo");
|
|
91
|
+
assert.equal(spec.repoPath, genomeDir); // repoPath rewritten to the tmp copy
|
|
92
|
+
});
|
|
93
|
+
// ------------------------------------------------------------------ AC (b)
|
|
94
|
+
test("(b) identical seed ⇒ identical score matrix (determinism)", async (t) => {
|
|
95
|
+
const { spec } = await toyFixture(t);
|
|
96
|
+
const matrixA = await scoreMatrix(spec, await freshDir(t, "toysbx"));
|
|
97
|
+
const matrixB = await scoreMatrix(spec, await freshDir(t, "toysbx"));
|
|
98
|
+
assert.deepEqual(matrixA, matrixB);
|
|
99
|
+
assert.equal(matrixA.length, spec.bench.units.length);
|
|
100
|
+
});
|
|
101
|
+
// ------------------------------------------------------------------ AC (c)
|
|
102
|
+
test("(c) reset+seed undoes unit state mutation — digest before run 2 == run 1", async (t) => {
|
|
103
|
+
const { spec } = await toyFixture(t);
|
|
104
|
+
const sandbox = await freshDir(t, "toysbx");
|
|
105
|
+
const adapter = new ToyBenchAdapter(spec);
|
|
106
|
+
await adapter.reset(sandbox);
|
|
107
|
+
await adapter.seed(sandbox);
|
|
108
|
+
const preDigest = treeDigestAt(sandbox);
|
|
109
|
+
const mutate = { id: "mutate", path: "units/mutate.mjs", split: "train" };
|
|
110
|
+
const run1 = await adapter.run(mutate, sandbox, spec.bench.timeoutS);
|
|
111
|
+
assert.equal(run1.status, "ok");
|
|
112
|
+
assert.ok(existsSync(path.join(sandbox, "poison.txt")), "mutating unit must dirty the sandbox");
|
|
113
|
+
assert.notEqual(treeDigestAt(sandbox), preDigest);
|
|
114
|
+
await adapter.reset(sandbox);
|
|
115
|
+
await adapter.seed(sandbox);
|
|
116
|
+
assert.ok(!existsSync(path.join(sandbox, "poison.txt")));
|
|
117
|
+
assert.equal(treeDigestAt(sandbox), preDigest); // exact pre-state restored
|
|
118
|
+
const run2 = await adapter.run(mutate, sandbox, spec.bench.timeoutS);
|
|
119
|
+
assert.deepEqual(run2, run1); // identical pre-state ⇒ identical observation
|
|
120
|
+
});
|
|
121
|
+
// ------------------------------------------------------------------ AC (d)
|
|
122
|
+
test("(d) grader exit-1 unit ⇒ inconclusive-for-unit, not a crash", async (t) => {
|
|
123
|
+
const { spec } = await toyFixture(t);
|
|
124
|
+
const sandbox = await freshDir(t, "toysbx");
|
|
125
|
+
const adapter = new ToyBenchAdapter(spec);
|
|
126
|
+
await adapter.reset(sandbox);
|
|
127
|
+
await adapter.seed(sandbox);
|
|
128
|
+
const outcome = await adapter.score(unitById(spec, "explode"));
|
|
129
|
+
assert.equal(outcome.kind, "inconclusive");
|
|
130
|
+
if (outcome.kind === "inconclusive") {
|
|
131
|
+
assert.equal(outcome.unitId, "explode");
|
|
132
|
+
assert.match(outcome.reason, /grader exited 1/);
|
|
133
|
+
}
|
|
134
|
+
// a scored sibling in the same sandbox proves the bench never died with the grader
|
|
135
|
+
assert.equal(scored(await adapter.score(unitById(spec, "mul"))).pass, true);
|
|
136
|
+
// stale-state guard: scoring with no sandbox ever seeded/run is a tool error (exit 2)
|
|
137
|
+
const cold = new ToyBenchAdapter(spec);
|
|
138
|
+
await assert.rejects(() => cold.score(unitById(spec, "mul")), (cause) => cause instanceof ExitSignal && cause.code === 2);
|
|
139
|
+
});
|
|
140
|
+
test("(d2) malformed grader output degrades to inconclusive, never a parse crash", async (t) => {
|
|
141
|
+
const { spec } = await toyFixture(t);
|
|
142
|
+
// tamper the SANDBOX copy of the grader (fixture repo itself stays sealed-clean)
|
|
143
|
+
const sandbox = await freshDir(t, "toysbx");
|
|
144
|
+
const adapter = new ToyBenchAdapter(spec);
|
|
145
|
+
await adapter.reset(sandbox);
|
|
146
|
+
await adapter.seed(sandbox);
|
|
147
|
+
await writeFile(path.join(sandbox, "grader.mjs"), 'process.stdout.write("not json at all\\n");\n', "utf8");
|
|
148
|
+
const outcome = await adapter.score(unitById(spec, "mul"));
|
|
149
|
+
assert.equal(outcome.kind, "inconclusive");
|
|
150
|
+
if (outcome.kind === "inconclusive")
|
|
151
|
+
assert.ok(outcome.reason.length > 0);
|
|
152
|
+
});
|
|
153
|
+
// ------------------------------------------------------------------ AC (e)
|
|
154
|
+
test("(e) hanging unit: process-group SIGKILL at timeoutS, recorded not thrown", async (t) => {
|
|
155
|
+
const { spec } = await toyFixture(t);
|
|
156
|
+
const sandbox = await freshDir(t, "toysbx");
|
|
157
|
+
const adapter = new ToyBenchAdapter(spec);
|
|
158
|
+
await adapter.reset(sandbox);
|
|
159
|
+
await adapter.seed(sandbox);
|
|
160
|
+
const hang = { id: "hang", path: "units/hang.mjs", split: "train" };
|
|
161
|
+
const startedMs = Date.now();
|
|
162
|
+
const run = await adapter.run(hang, sandbox, 1);
|
|
163
|
+
const elapsedMs = Date.now() - startedMs;
|
|
164
|
+
assert.equal(run.status, "timeout");
|
|
165
|
+
assert.equal(run.exitCode, null);
|
|
166
|
+
assert.deepEqual(run.metrics, { tokensEst: 0, turns: 0 });
|
|
167
|
+
assert.match(run.note ?? "", /killed after 1s: process group SIGKILL/);
|
|
168
|
+
assert.ok(elapsedMs < 6000, `timeout path took ${String(elapsedMs)}ms`);
|
|
169
|
+
// group kill must have reaped the exec'd grandchild, not just node itself
|
|
170
|
+
const ps = spawnSync("ps", ["-eo", "args"], { encoding: "utf8" });
|
|
171
|
+
assert.equal(ps.status, 0);
|
|
172
|
+
assert.ok(!ps.stdout.includes("sleep 31.7"), `orphaned 'sleep 31.7' survived the kill`);
|
|
173
|
+
});
|
|
174
|
+
// ----------------------------------------------------- infra / hooks / render
|
|
175
|
+
test("missing runner binary records infra_failed with a reason, excluded not zeroed", async (t) => {
|
|
176
|
+
const { spec } = await toyFixture(t);
|
|
177
|
+
const broken = {
|
|
178
|
+
...spec,
|
|
179
|
+
bench: { ...spec.bench, runCommand: "abathur-no-such-bin {unit.path}" },
|
|
180
|
+
};
|
|
181
|
+
const sandbox = await freshDir(t, "toysbx");
|
|
182
|
+
const adapter = new ToyBenchAdapter(broken);
|
|
183
|
+
await adapter.reset(sandbox);
|
|
184
|
+
await adapter.seed(sandbox);
|
|
185
|
+
const run = await adapter.run(unitById(spec, "mul"), sandbox, 5);
|
|
186
|
+
assert.equal(run.status, "infra_failed");
|
|
187
|
+
assert.equal(run.exitCode, null);
|
|
188
|
+
assert.match(run.note ?? "", /spawn failed/);
|
|
189
|
+
});
|
|
190
|
+
test("seedCommand / resetCommand hooks execute inside the sandbox when configured", async (t) => {
|
|
191
|
+
const { spec } = await toyFixture(t);
|
|
192
|
+
const hooked = {
|
|
193
|
+
...spec,
|
|
194
|
+
bench: { ...spec.bench, seedCommand: "touch seeded.flag", resetCommand: "touch reset.flag" },
|
|
195
|
+
};
|
|
196
|
+
const sandbox = path.join(await freshDir(t, "toysbx"), "nested"); // mkdir -p path
|
|
197
|
+
const adapter = new ToyBenchAdapter(hooked);
|
|
198
|
+
await adapter.reset(sandbox);
|
|
199
|
+
await adapter.seed(sandbox);
|
|
200
|
+
assert.ok(existsSync(path.join(sandbox, "reset.flag")), "resetCommand must run after the wipe");
|
|
201
|
+
assert.ok(existsSync(path.join(sandbox, "seeded.flag")));
|
|
202
|
+
});
|
|
203
|
+
// -------------------------------------------------------------- stub-mutators
|
|
204
|
+
test("stub-mutators: applyPatch refuses missing/ambiguous anchors; fix-add lifts the score", async (t) => {
|
|
205
|
+
const { genomeDir, spec } = await toyFixture(t);
|
|
206
|
+
const scratch = await freshDir(t, "mutscratch");
|
|
207
|
+
await writeFile(path.join(scratch, "dup.txt"), "x\nx\n", "utf8");
|
|
208
|
+
assert.equal(applyPatch(scratch, { file: "dup.txt", from: "x", to: "y" }), false); // ambiguous
|
|
209
|
+
assert.equal(applyPatch(scratch, { file: "nope.txt", from: "x", to: "y" }), false); // missing
|
|
210
|
+
assert.equal(readFileSync(path.join(scratch, "dup.txt"), "utf8"), "x\nx\n"); // nothing written
|
|
211
|
+
const fix = scriptedPatches().find((p) => p.id === "fix-add");
|
|
212
|
+
if (fix === undefined)
|
|
213
|
+
throw new Error("fix-add patch missing from table");
|
|
214
|
+
const sandbox = await freshDir(t, "toysbx");
|
|
215
|
+
const adapter = new ToyBenchAdapter(spec);
|
|
216
|
+
await adapter.reset(sandbox);
|
|
217
|
+
await adapter.seed(sandbox);
|
|
218
|
+
assert.equal(scored(await adapter.score(unitById(spec, "add"))).score, 0); // before: seeded bug
|
|
219
|
+
assert.equal(applyPatch(genomeDir, fix), true);
|
|
220
|
+
await adapter.reset(sandbox);
|
|
221
|
+
await adapter.seed(sandbox);
|
|
222
|
+
assert.equal(scored(await adapter.score(unitById(spec, "add"))).score, 1); // after: evolved
|
|
223
|
+
// re-apply of the same patch is a no-op refusal (anchor already rewritten)
|
|
224
|
+
assert.equal(applyPatch(genomeDir, fix), false);
|
|
225
|
+
});
|
|
226
|
+
// ------------------------------------------------------------------ self-init
|
|
227
|
+
test("init.mjs is idempotent: re-init keeps the git repo and rewrites repoPath", async (t) => {
|
|
228
|
+
const { genomeDir } = await toyFixture(t);
|
|
229
|
+
const head = () => {
|
|
230
|
+
const run = spawnSync("git", ["rev-parse", "HEAD"], { cwd: genomeDir, encoding: "utf8" });
|
|
231
|
+
assert.equal(run.status, 0);
|
|
232
|
+
return (run.stdout ?? "").trim();
|
|
233
|
+
};
|
|
234
|
+
const before = head();
|
|
235
|
+
await prepareToyGenome(genomeDir); // .git already exists ⇒ must NOT re-init
|
|
236
|
+
assert.equal(head(), before);
|
|
237
|
+
});
|
|
238
|
+
test("prepareToyGenome creates missing parent dirs (regression: missing cwd masquerades as spawn ENOENT)", async (t) => {
|
|
239
|
+
const dest = path.join(await freshDir(t, "toyparent"), "missing", "nested-g");
|
|
240
|
+
await prepareToyGenome(dest);
|
|
241
|
+
assert.ok(existsSync(path.join(dest, "genome.jsonc")));
|
|
242
|
+
assert.ok(existsSync(path.join(dest, ".git")));
|
|
243
|
+
const head = () => {
|
|
244
|
+
const run = spawnSync("git", ["rev-parse", "HEAD"], { cwd: dest, encoding: "utf8" });
|
|
245
|
+
assert.equal(run.status, 0);
|
|
246
|
+
return (run.stdout ?? "").trim();
|
|
247
|
+
};
|
|
248
|
+
const before = head();
|
|
249
|
+
await prepareToyGenome(dest); // same dest again ⇒ still idempotent, .git preserved
|
|
250
|
+
assert.equal(head(), before);
|
|
251
|
+
});
|