pi-gauntlet 5.18.7 → 5.20.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.
@@ -1,466 +0,0 @@
1
- import { test } from "node:test";
2
- import assert from "node:assert/strict";
3
- import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync, chmodSync } from "node:fs";
4
- import { join, dirname } from "node:path";
5
- import { tmpdir } from "node:os";
6
- import { spawnSync } from "node:child_process";
7
- import { fileURLToPath } from "node:url";
8
-
9
- const CLI = join(dirname(fileURLToPath(import.meta.url)), "gauntlet-telemetry-salvage.mjs");
10
- const SPEC = "doc/specs/x.md";
11
- const REC = ".pi/gauntlet/telemetry/doc/specs/x.yaml";
12
- const RECORD_V1 = "schema: 1\nspec: doc/specs/x.md\nstatus: in_progress\n";
13
- const RECORD_V2 = "schema: 1\nspec: doc/specs/x.md\nstatus: shipped\n";
14
- // Records parseRecord accepts (run_id present). UNFINISHED: in_progress, no ship phase.
15
- const UNFINISHED = "schema: 1\nspec: doc/specs/x.md\nrun_id: r-1\nstatus: in_progress\ncreated_at: 2026-09-17T16:11:45Z\nderived:\n duration_s: 60\n phases:\n brainstorm: { started_at: 2026-09-17T16:11:45Z, duration_s: 60 }\n";
16
- const SHIP_PHASE = UNFINISHED.replace(" brainstorm:", " ship: { started_at: 2026-09-17T16:12:45Z }\n brainstorm:");
17
- const SCALAR_PHASES = UNFINISHED.replace(" phases:\n brainstorm: { started_at: 2026-09-17T16:11:45Z, duration_s: 60 }", " phases: nope");
18
- const FINISHED = UNFINISHED.replace("status: in_progress", "status: shipped\nshipped_at: 2026-09-17T18:56:13Z");
19
-
20
- const git = (cwd, args, env = {}) =>
21
- spawnSync("git", ["-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false", ...args], {
22
- cwd, encoding: "utf8", env: { ...process.env, ...env },
23
- });
24
- const out = (cwd, args) => git(cwd, args).stdout.trim();
25
- const write = (root, rel, text) => {
26
- mkdirSync(join(root, dirname(rel)), { recursive: true });
27
- writeFileSync(join(root, rel), text);
28
- };
29
- const commit = (root, msg, ...paths) => {
30
- git(root, ["add", "-f", "--", ...paths]);
31
- git(root, ["commit", "-q", "-m", msg, "--", ...paths]);
32
- return out(root, ["rev-parse", "HEAD"]);
33
- };
34
- const rmCommit = (root, msg, ...paths) => {
35
- git(root, ["rm", "-q", "--", ...paths]);
36
- git(root, ["commit", "-q", "-m", msg, "--", ...paths]);
37
- return out(root, ["rev-parse", "HEAD"]);
38
- };
39
- // main: README only. feat: spec (+ record when withRecord).
40
- const repo = ({ withRecord = true, record = RECORD_V1 } = {}) => {
41
- const root = mkdtempSync(join(tmpdir(), "gts-"));
42
- git(root, ["init", "-q", "-b", "main"]);
43
- write(root, "README.md", "# fixture\n");
44
- commit(root, "init", "README.md");
45
- git(root, ["checkout", "-q", "-b", "feat"]);
46
- write(root, SPEC, "# Spec X\n\n**Goal:** g.\n");
47
- commit(root, "Add spec", SPEC);
48
- if (withRecord) {
49
- write(root, REC, record);
50
- commit(root, "telemetry: doc/specs/x.md", REC);
51
- }
52
- return root;
53
- };
54
- // An empty preset dir by default: the developer's real ~/.pi/agent/settings.json must never leak into fixtures.
55
- const EMPTY_AGENT = mkdtempSync(join(tmpdir(), "gts-empty-agent-"));
56
- process.on("exit", () => rmSync(EMPTY_AGENT, { recursive: true, force: true }));
57
- // The CLI commits with whatever identity git resolves, like the recorder; CI runners have none.
58
- const IDENTITY = { GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@t" };
59
- const invoke = (args, env = {}, options = {}) => {
60
- const r = spawnSync(process.execPath, [CLI, ...args], {
61
- encoding: "utf8", env: { ...process.env, ...IDENTITY, PI_CODING_AGENT_DIR: EMPTY_AGENT, ...env }, timeout: options.timeout,
62
- });
63
- return { status: r.status, stdout: r.stdout.trim(), stderr: r.stderr.trim(), lines: r.stdout.split("\n").filter(Boolean) };
64
- };
65
- const run = (root, args = [], env = {}) => invoke(["--worktree", root, "--base", "main", ...args], env);
66
- const head = (root) => out(root, ["rev-parse", "HEAD"]);
67
- const commitCount = (root) => Number(out(root, ["rev-list", "--count", "HEAD"]));
68
- const headSubject = (root) => out(root, ["log", "-1", "--format=%s"]);
69
- const headFiles = (root) => out(root, ["show", "--name-only", "--format=", "HEAD"]).split("\n").filter(Boolean);
70
- const porcelain = (root) => out(root, ["status", "--porcelain"]);
71
- const cleanup = (t, ...roots) => t.after(() => roots.forEach((r) => rmSync(r, { recursive: true, force: true })));
72
-
73
- test("present: record in HEAD tree -> present, no commit", (t) => {
74
- const root = repo(); cleanup(t, root);
75
- const before = head(root);
76
- const r = run(root);
77
- assert.equal(r.status, 0, r.stderr);
78
- assert.deepEqual(r.lines, [`present ${REC}`]);
79
- assert.equal(head(root), before);
80
- });
81
-
82
- test("deleted once -> restored from <sha>, byte-identical, one telemetry: commit touching only the record", (t) => {
83
- const root = repo(); cleanup(t, root);
84
- const del = rmCommit(root, "Strip ephemeral plan and telemetry scaffolding", REC);
85
- const n = commitCount(root);
86
- const r = run(root);
87
- assert.equal(r.status, 0, r.stderr);
88
- assert.deepEqual(r.lines, [`restored ${REC} from ${del}`]);
89
- assert.equal(readFileSync(join(root, REC), "utf8"), RECORD_V1);
90
- assert.equal(commitCount(root), n + 1);
91
- assert.equal(headSubject(root), `telemetry: restore record stripped in ${del.slice(0, 10)}`);
92
- assert.deepEqual(headFiles(root), [REC]);
93
- assert.equal(porcelain(root), "");
94
- });
95
-
96
- test("deleted twice -> restores the newest (second) content", (t) => {
97
- const root = repo(); cleanup(t, root);
98
- rmCommit(root, "strip 1", REC);
99
- write(root, REC, RECORD_V2);
100
- commit(root, "telemetry: doc/specs/x.md", REC);
101
- const del2 = rmCommit(root, "strip 2", REC);
102
- const r = run(root);
103
- assert.deepEqual(r.lines, [`restored ${REC} from ${del2}`]);
104
- assert.equal(readFileSync(join(root, REC), "utf8"), RECORD_V2);
105
- });
106
-
107
- test("deleted, then rewritten on disk untracked -> on-disk copy committed, not DEL^", (t) => {
108
- const root = repo(); cleanup(t, root);
109
- const del = rmCommit(root, "strip", REC);
110
- write(root, REC, RECORD_V2);
111
- const r = run(root);
112
- assert.deepEqual(r.lines, [`restored ${REC} from ${del}`]);
113
- assert.equal(out(root, ["show", `HEAD:${REC}`]), RECORD_V2.trim());
114
- assert.equal(porcelain(root), "");
115
- });
116
-
117
- test("deleted and staged again uncommitted -> not present; staged copy committed", (t) => {
118
- const root = repo(); cleanup(t, root);
119
- const del = rmCommit(root, "strip", REC);
120
- write(root, REC, RECORD_V2);
121
- git(root, ["add", "-f", "--", REC]);
122
- const r = run(root);
123
- assert.deepEqual(r.lines, [`restored ${REC} from ${del}`]);
124
- assert.equal(out(root, ["show", `HEAD:${REC}`]), RECORD_V2.trim());
125
- assert.equal(porcelain(root), "");
126
- });
127
-
128
- test("pre-staged record missing from worktree -> failure preserves index and HEAD", (t) => {
129
- const root = repo(); cleanup(t, root);
130
- rmCommit(root, "strip", REC);
131
- write(root, REC, RECORD_V2);
132
- git(root, ["add", "-f", "--", REC]);
133
- rmSync(join(root, REC));
134
- const before = head(root);
135
- const r = run(root);
136
- assert.equal(r.stdout, `restore failed ${REC}: staged copy differs from worktree`);
137
- assert.equal(out(root, ["show", `:${REC}`]), RECORD_V2.trim());
138
- assert.equal(head(root), before);
139
- assert.equal(existsSync(join(root, REC)), false);
140
- });
141
-
142
- test("pre-staged record differing from worktree -> failure preserves both copies and HEAD", (t) => {
143
- const root = repo(); cleanup(t, root);
144
- rmCommit(root, "strip", REC);
145
- write(root, REC, RECORD_V2);
146
- git(root, ["add", "-f", "--", REC]);
147
- write(root, REC, RECORD_V1);
148
- const before = head(root);
149
- const r = run(root);
150
- assert.equal(r.stdout, `restore failed ${REC}: staged copy differs from worktree`);
151
- assert.equal(out(root, ["show", `:${REC}`]), RECORD_V2.trim());
152
- assert.equal(readFileSync(join(root, REC), "utf8"), RECORD_V1);
153
- assert.equal(head(root), before);
154
- });
155
-
156
- test("deleted in its introducing commit (amended away) -> nothing to restore, no file, clean index", (t) => {
157
- const root = repo(); cleanup(t, root);
158
- // The record's only commit is rewritten without it: git records no D entry whose parent lacks the blob,
159
- // so the spec's "introducing commit" limitation surfaces as `never written` and the script leaves nothing behind.
160
- git(root, ["rm", "-q", "--", REC]);
161
- git(root, ["commit", "-q", "--amend", "--allow-empty", "-m", "telemetry: doc/specs/x.md"]);
162
- const before = head(root);
163
- const r = run(root);
164
- assert.equal(r.status, 0, r.stderr);
165
- assert.deepEqual(r.lines, [`never written ${REC}`]);
166
- assert.equal(head(root), before);
167
- assert.equal(existsSync(join(root, REC)), false);
168
- assert.equal(porcelain(root), "");
169
- });
170
-
171
- test("no telemetry: commit on branch -> no telemetry run, no commit", (t) => {
172
- const root = repo({ withRecord: false }); cleanup(t, root);
173
- const n = commitCount(root);
174
- const r = run(root);
175
- assert.deepEqual(r.lines, [`no telemetry run ${REC}`]);
176
- assert.equal(commitCount(root), n);
177
- });
178
-
179
- test("telemetry: commit for another path, no deletion of this one -> never written", (t) => {
180
- const root = repo({ withRecord: false }); cleanup(t, root);
181
- write(root, ".pi/gauntlet/telemetry/doc/specs/other.yaml", "x\n");
182
- commit(root, "telemetry: doc/specs/other.md", ".pi/gauntlet/telemetry/doc/specs/other.yaml");
183
- const r = run(root);
184
- assert.deepEqual(r.lines, [`never written ${REC}`]);
185
- });
186
-
187
- test("--check on a stripped record -> stripped ... in <sha>, no commit, no file", (t) => {
188
- const root = repo(); cleanup(t, root);
189
- const del = rmCommit(root, "strip", REC);
190
- const before = head(root);
191
- const r = run(root, ["--check"]);
192
- assert.deepEqual(r.lines, [`stripped ${REC} in ${del}`]);
193
- assert.equal(head(root), before);
194
- assert.equal(existsSync(join(root, REC)), false);
195
- assert.equal(porcelain(root), "");
196
- });
197
-
198
- test("repo .pi/settings.json telemetry.enabled false -> telemetry disabled only", (t) => {
199
- const root = repo(); cleanup(t, root);
200
- write(root, ".pi/settings.json", JSON.stringify({ piGauntlet: { telemetry: { enabled: false } } }));
201
- commit(root, "settings", ".pi/settings.json");
202
- rmCommit(root, "strip", REC);
203
- const r = run(root);
204
- assert.equal(r.stdout, "telemetry disabled");
205
- });
206
-
207
- test("preset-only telemetry.dir via PI_CODING_AGENT_DIR resolves; --dir overrides both layers", (t) => {
208
- const root = repo({ withRecord: false });
209
- const agent = mkdtempSync(join(tmpdir(), "gts-agent-"));
210
- cleanup(t, root, agent);
211
- writeFileSync(join(agent, "settings.json"), JSON.stringify({ piGauntlet: { telemetry: { dir: "custom/dir" } } }));
212
- const custom = "custom/dir/doc/specs/x.yaml";
213
- write(root, custom, RECORD_V1);
214
- commit(root, "telemetry: doc/specs/x.md", custom);
215
- const del = rmCommit(root, "strip", custom);
216
- const r = run(root, [], { PI_CODING_AGENT_DIR: agent });
217
- assert.deepEqual(r.lines, [`restored ${custom} from ${del}`]);
218
- const r2 = run(root, ["--dir", "elsewhere"], { PI_CODING_AGENT_DIR: agent });
219
- assert.deepEqual(r2.lines, [`never written elsewhere/doc/specs/x.yaml`]);
220
- });
221
-
222
- test("rejecting pre-commit hook -> restore failed, index and worktree identical to before", (t) => {
223
- const root = repo(); cleanup(t, root);
224
- const del = rmCommit(root, "strip", REC);
225
- write(root, ".git/hooks/pre-commit", "#!/bin/sh\nexit 1\n");
226
- chmodSync(join(root, ".git/hooks/pre-commit"), 0o755);
227
- const before = head(root);
228
- const r = run(root);
229
- assert.equal(r.status, 0);
230
- assert.match(r.stdout, new RegExp(`^restore failed ${REC.replace(/\./g, "\\.")}: `));
231
- assert.equal(head(root), before);
232
- assert.equal(existsSync(join(root, REC)), false);
233
- assert.equal(porcelain(root), "");
234
- });
235
-
236
- test("hanging pre-commit hook -> restore failed ... timed out within the bound, state rolled back", (t) => {
237
- const root = repo(); cleanup(t, root);
238
- rmCommit(root, "strip", REC);
239
- write(root, ".git/hooks/pre-commit", "#!/bin/sh\nsleep 60\n");
240
- chmodSync(join(root, ".git/hooks/pre-commit"), 0o755);
241
- const before = head(root);
242
- const r = invoke(["--worktree", root, "--base", "main"], { GAUNTLET_SALVAGE_COMMIT_TIMEOUT_MS: "2000" }, { timeout: 20_000 });
243
- assert.equal(r.status, 0);
244
- assert.equal(r.stdout, `restore failed ${REC}: timed out`);
245
- assert.equal(head(root), before);
246
- assert.equal(existsSync(join(root, REC)), false);
247
- assert.equal(porcelain(root), "");
248
- });
249
-
250
- test("timed-out record presence query -> restore failed, no mutation", (t) => {
251
- const root = repo();
252
- const bin = mkdtempSync(join(tmpdir(), "gts-bin-"));
253
- cleanup(t, root, bin);
254
- const wrapper = join(bin, "git");
255
- writeFileSync(wrapper, `#!/bin/sh
256
- case "$*" in
257
- *"cat-file -e HEAD:${REC}"*) sleep 60 ;;
258
- esac
259
- PATH="${process.env.PATH.split(":").filter((part) => part !== bin).join(":")}" exec git "$@"
260
- `);
261
- chmodSync(wrapper, 0o755);
262
- const before = head(root);
263
- const r = invoke(["--worktree", root, "--base", "main"], { PATH: `${bin}:${process.env.PATH}` }, { timeout: 20_000 });
264
- assert.equal(r.status, 0);
265
- assert.equal(r.stdout, `restore failed ${REC}: timed out`);
266
- assert.equal(head(root), before);
267
- assert.equal(porcelain(root), "");
268
- });
269
-
270
- test("two specs on the branch, one record stripped -> two lines, one restore", (t) => {
271
- const root = repo(); cleanup(t, root);
272
- const spec2 = "doc/specs/y.md"; const rec2 = ".pi/gauntlet/telemetry/doc/specs/y.yaml";
273
- write(root, spec2, "# Y\n"); commit(root, "Add y", spec2);
274
- write(root, rec2, RECORD_V1); commit(root, "telemetry: doc/specs/y.md", rec2);
275
- const del = rmCommit(root, "strip", REC);
276
- const n = commitCount(root);
277
- const r = run(root);
278
- assert.deepEqual(r.lines.sort(), [`present ${rec2}`, `restored ${REC} from ${del}`].sort());
279
- assert.equal(commitCount(root), n + 1);
280
- });
281
-
282
- test("spec deleted on the branch -> no spec on branch", (t) => {
283
- const root = repo(); cleanup(t, root);
284
- rmCommit(root, "reap superseded spec", SPEC, REC);
285
- const r = run(root);
286
- assert.equal(r.stdout, "no spec on branch");
287
- });
288
-
289
- test("unresolvable base without --base -> no base ref; --base main works", (t) => {
290
- const root = repo(); cleanup(t, root);
291
- git(root, ["branch", "-m", "main", "trunk"]);
292
- const bare = invoke(["--worktree", root]);
293
- assert.equal(bare.status, 0);
294
- assert.equal(bare.stdout, "no base ref");
295
- const r = invoke(["--worktree", root, "--base", "trunk"]);
296
- assert.equal(r.stdout, `present ${REC}`);
297
- });
298
-
299
- test("--worktree at a non-git directory -> stderr not a git worktree, empty stdout, exit 0", (t) => {
300
- const dir = mkdtempSync(join(tmpdir(), "gts-nogit-")); cleanup(t, dir);
301
- const r = invoke(["--worktree", dir]);
302
- assert.equal(r.status, 0);
303
- assert.equal(r.stdout, "");
304
- assert.match(r.stderr, /^not a git worktree: /);
305
- });
306
-
307
- test("relative --worktree -> usage on stderr, exit 1, empty stdout", () => {
308
- const r = invoke(["--worktree", "relative/path"]);
309
- assert.equal(r.status, 1);
310
- assert.equal(r.stdout, "");
311
- assert.match(r.stderr, /^usage: gauntlet-telemetry-salvage/);
312
- });
313
-
314
- test("escaping --dir -> usage on stderr, exit 1", (t) => {
315
- const root = repo(); cleanup(t, root);
316
- const r = invoke(["--worktree", root, "--dir", "../outside"]);
317
- assert.equal(r.status, 1);
318
- assert.equal(r.stdout, "");
319
- assert.match(r.stderr, /^usage: gauntlet-telemetry-salvage/);
320
- });
321
-
322
- test("trailing slash in --dir is normalized", (t) => {
323
- const root = repo({ withRecord: false }); cleanup(t, root);
324
- const custom = "custom/dir/doc/specs/x.yaml";
325
- write(root, custom, RECORD_V1);
326
- commit(root, "telemetry: doc/specs/x.md", custom);
327
- const del = rmCommit(root, "strip", custom);
328
- const r = run(root, ["--dir", "custom/dir/"]);
329
- assert.deepEqual(r.lines, [`restored ${custom} from ${del}`]);
330
- });
331
-
332
- test("value flag followed by another flag -> usage, exit 1, no git call", (t) => {
333
- const root = repo(); cleanup(t, root);
334
- const before = head(root);
335
- const r = invoke(["--worktree", root, "--base", "--check"]);
336
- assert.equal(r.status, 1);
337
- assert.match(r.stderr, /^usage:/);
338
- assert.equal(head(root), before);
339
- assert.equal(invoke(["--worktree"]).status, 1);
340
- });
341
-
342
- test("never pushes: remote refs unchanged after a successful restore", (t) => {
343
- const root = repo();
344
- const remote = mkdtempSync(join(tmpdir(), "gts-remote-"));
345
- cleanup(t, root, remote);
346
- git(remote, ["init", "-q", "--bare"]);
347
- git(root, ["remote", "add", "origin", remote]);
348
- git(root, ["push", "-q", "origin", "main", "feat"]);
349
- const remoteBefore = out(remote, ["for-each-ref"]);
350
- rmCommit(root, "strip", REC);
351
- const r = run(root);
352
- assert.match(r.stdout, /^restored /);
353
- assert.equal(out(remote, ["for-each-ref"]), remoteBefore);
354
- });
355
-
356
- test("present + unfinished -> stamped shipped, one telemetry: commit, clean tree", (t) => {
357
- const root = repo({ record: UNFINISHED }); cleanup(t, root);
358
- const n = commitCount(root);
359
- const r = run(root);
360
- assert.equal(r.status, 0, r.stderr);
361
- assert.deepEqual(r.lines, [`present ${REC} (marked shipped)`]);
362
- const text = readFileSync(join(root, REC), "utf8");
363
- assert.match(text, /^status: shipped$/m);
364
- assert.match(text, /^shipped_at: \d{4}-\d{2}-\d{2}T/m);
365
- assert.doesNotMatch(text, /in_progress/);
366
- assert.equal(commitCount(root), n + 1);
367
- assert.equal(headSubject(root), `telemetry: mark ${REC} shipped at landing`);
368
- assert.deepEqual(headFiles(root), [REC]);
369
- assert.equal(porcelain(root), "");
370
- });
371
-
372
- test("present + scalar phases -> stamped shipped, exit 0", (t) => {
373
- const root = repo({ record: SCALAR_PHASES }); cleanup(t, root);
374
- const r = run(root);
375
- assert.equal(r.status, 0, r.stderr);
376
- assert.deepEqual(r.lines, [`present ${REC} (marked shipped)`]);
377
- assert.match(readFileSync(join(root, REC), "utf8"), /^status: shipped$/m);
378
- });
379
-
380
- test("present + in_progress with a ship phase -> untouched", (t) => {
381
- const root = repo({ record: SHIP_PHASE }); cleanup(t, root);
382
- const before = head(root);
383
- const r = run(root);
384
- assert.deepEqual(r.lines, [`present ${REC}`]);
385
- assert.equal(head(root), before);
386
- assert.equal(readFileSync(join(root, REC), "utf8"), SHIP_PHASE);
387
- });
388
-
389
- test("present + unfinished but worktree copy dirty -> untouched, present", (t) => {
390
- const root = repo({ record: UNFINISHED }); cleanup(t, root);
391
- write(root, REC, UNFINISHED + "# local edit\n");
392
- const before = head(root);
393
- const r = run(root);
394
- assert.deepEqual(r.lines, [`present ${REC}`]);
395
- assert.equal(head(root), before);
396
- assert.equal(readFileSync(join(root, REC), "utf8"), UNFINISHED + "# local edit\n");
397
- });
398
-
399
- test("present + already shipped -> byte-identical, no commit", (t) => {
400
- const root = repo({ record: FINISHED }); cleanup(t, root);
401
- const before = head(root);
402
- const r = run(root);
403
- assert.deepEqual(r.lines, [`present ${REC}`]);
404
- assert.equal(head(root), before);
405
- assert.equal(readFileSync(join(root, REC), "utf8"), FINISHED);
406
- });
407
-
408
- test("restore with scalar phases -> stamped shipped, exit 0", (t) => {
409
- const root = repo({ record: SCALAR_PHASES }); cleanup(t, root);
410
- const del = rmCommit(root, "strip", REC);
411
- const r = run(root);
412
- assert.equal(r.status, 0, r.stderr);
413
- assert.deepEqual(r.lines, [`restored ${REC} from ${del} (marked shipped)`]);
414
- assert.match(out(root, ["show", `HEAD:${REC}`]), /^status: shipped$/m);
415
- });
416
-
417
- test("restore of an unfinished record -> one commit whose bytes carry the stamp", (t) => {
418
- const root = repo({ record: UNFINISHED }); cleanup(t, root);
419
- const del = rmCommit(root, "strip", REC);
420
- write(root, REC, UNFINISHED);
421
- const n = commitCount(root);
422
- const r = run(root);
423
- assert.deepEqual(r.lines, [`restored ${REC} from ${del} (marked shipped)`]);
424
- assert.equal(commitCount(root), n + 1);
425
- const committed = out(root, ["show", `HEAD:${REC}`]);
426
- assert.match(committed, /^status: shipped$/m);
427
- assert.match(committed, /^shipped_at: /m);
428
- assert.equal(porcelain(root), "");
429
- });
430
-
431
- test("--check on present + unfinished -> unfinished <rec>, nothing changes", (t) => {
432
- const root = repo({ record: UNFINISHED }); cleanup(t, root);
433
- const before = head(root);
434
- const r = run(root, ["--check"]);
435
- assert.deepEqual(r.lines, [`unfinished ${REC}`]);
436
- assert.equal(head(root), before);
437
- assert.equal(readFileSync(join(root, REC), "utf8"), UNFINISHED);
438
- assert.equal(porcelain(root), "");
439
- });
440
-
441
- test("present + unfinished, rejecting pre-commit hook -> restore failed, HEAD bytes and index restored", (t) => {
442
- const root = repo({ record: UNFINISHED }); cleanup(t, root);
443
- write(root, ".git/hooks/pre-commit", "#!/bin/sh\nexit 1\n");
444
- chmodSync(join(root, ".git/hooks/pre-commit"), 0o755);
445
- const before = head(root);
446
- const r = run(root);
447
- assert.match(r.stdout, new RegExp(`^restore failed ${REC.replace(/\./g, "\\.")}: `));
448
- assert.equal(head(root), before);
449
- assert.equal(readFileSync(join(root, REC), "utf8"), UNFINISHED);
450
- assert.equal(porcelain(root), "");
451
- });
452
-
453
- test("finishing Option 1 fixture: strip-on-branch + salvage + merge --squash leaves the record, not the plan, staged", (t) => {
454
- const root = repo(); cleanup(t, root);
455
- const plan = "doc/plans/x.md";
456
- write(root, plan, "# plan\n"); commit(root, "Add plan", plan);
457
- rmCommit(root, "Strip ephemeral plan and telemetry scaffolding", plan, REC);
458
- const r = run(root);
459
- assert.match(r.stdout, /^restored /);
460
- git(root, ["checkout", "-q", "main"]);
461
- git(root, ["merge", "--squash", "-q", "feat"]);
462
- const staged = out(root, ["diff", "--cached", "--name-only"]).split("\n");
463
- assert.ok(staged.includes(REC), "record staged");
464
- assert.ok(staged.includes(SPEC), "spec staged");
465
- assert.ok(!staged.includes(plan), "plan not staged");
466
- });