pi-gauntlet 8.3.0 → 8.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +4 -4
- package/bin/gauntlet-performance.mjs +477 -64
- package/bin/gauntlet-performance.test.mjs +27 -27
- package/bin/gauntlet-spec-index.mjs +290 -47
- package/bin/gauntlet-spec-index.test.mjs +42 -18
- package/bin/gauntlet-telemetry-seal.mjs +220 -77
- package/bin/gauntlet-telemetry-seal.test.mjs +222 -457
- package/extensions/lib/gauntlet-settings.test.ts +12 -14
- package/extensions/lib/gauntlet-settings.ts +7 -16
- package/extensions/lib/telemetry-branch.test.ts +102 -0
- package/extensions/lib/telemetry-branch.ts +145 -0
- package/extensions/lib/telemetry-diff.test.ts +2 -2
- package/extensions/lib/telemetry-diff.ts +1 -1
- package/extensions/lib/telemetry-leg.ts +163 -0
- package/extensions/lib/telemetry-record.ts +2 -0
- package/extensions/lib/vcs.ts +6 -23
- package/extensions/phase-tracker.test.ts +6 -3
- package/extensions/phase-tracker.ts +2 -0
- package/extensions/plan-tracker.test.ts +4 -2
- package/extensions/plan-tracker.ts +1 -0
- package/extensions/telemetry-leg-failures.test.ts +138 -0
- package/extensions/telemetry-leg-fields.test.ts +130 -0
- package/extensions/telemetry-leg-no-write.test.ts +90 -0
- package/extensions/telemetry-leg-rounds.test.ts +157 -0
- package/extensions/telemetry-leg-trackers.test.ts +114 -0
- package/extensions/telemetry-leg.test.ts +151 -0
- package/extensions/telemetry-settlement-contract.test.ts +67 -0
- package/extensions/telemetry.test.ts +44 -27
- package/extensions/telemetry.ts +68 -48
- package/extensions/test-support/git-env.mjs +13 -0
- package/package.json +1 -1
- package/skills/brainstorming/SKILL.md +1 -1
- package/skills/check-delivery/SKILL.md +2 -0
- package/skills/finishing-a-development-branch/SKILL.md +13 -13
- package/skills/finishing-a-development-branch/reference/verification-set.md +2 -2
- package/skills/gatekeep-pr/reference/batch.md +3 -1
- package/skills/gatekeep-pr/reference/cleanup.md +1 -1
- package/skills/gatekeep-pr/reference/post-selection-loop.md +7 -1
- package/skills/gauntlet-resume/reference/seed.md +1 -6
- package/skills/shape-ticket/SKILL.md +1 -1
- package/skills/shape-ticket/reference/repo-audit.md +2 -2
|
@@ -1,546 +1,311 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync,
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync, realpathSync, symlinkSync, chmodSync } from "node:fs";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { parse as parseYaml } from "yaml";
|
|
9
|
+
import { isolatedGitEnv } from "../extensions/test-support/git-env.mjs";
|
|
9
10
|
|
|
10
11
|
const CLI = join(dirname(fileURLToPath(import.meta.url)), "gauntlet-telemetry-seal.mjs");
|
|
11
12
|
const SPEC = "doc/specs/x.md";
|
|
12
|
-
const REC = ".
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const RECORD =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"",
|
|
36
|
-
].join("\n");
|
|
13
|
+
const REC = ".git/gauntlet/telemetry/doc/specs/x.yaml";
|
|
14
|
+
const REF = "refs/heads/gauntlet/telemetry";
|
|
15
|
+
const BLOB = "team/product/brainstorming/doc/specs/x.yaml";
|
|
16
|
+
const RECORD = `schema: 1
|
|
17
|
+
spec: doc/specs/x.md
|
|
18
|
+
run_id: r-1
|
|
19
|
+
status: in_progress
|
|
20
|
+
created_at: 2026-09-17T16:00:00Z
|
|
21
|
+
sessions: [s1]
|
|
22
|
+
derived:
|
|
23
|
+
duration_s: 60
|
|
24
|
+
phases: {}
|
|
25
|
+
personas: {}
|
|
26
|
+
conformance_loops: 0
|
|
27
|
+
gates: { spec_rounds: 0, plan_rounds: 0, fix_round_grants: 0, task_reopens: 0 }
|
|
28
|
+
amendments: 0
|
|
29
|
+
spec_edits_after_ship: 0
|
|
30
|
+
spec_writes: {}
|
|
31
|
+
events_dropped: 0
|
|
32
|
+
accumulators: {}
|
|
33
|
+
events:
|
|
34
|
+
- { ts: 2026-09-17T16:00:00Z, session: s1, phase: brainstorm, kind: phase, action: start, name: brainstorm }
|
|
35
|
+
`;
|
|
37
36
|
const SEALED = RECORD.replace("status: in_progress", "status: shipped\nshipped_at: 2026-09-17T18:00:00Z");
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const
|
|
37
|
+
const EMPTY_AGENT = mkdtempSync(join(tmpdir(), "gts-env-"));
|
|
38
|
+
const GIT_ENV = isolatedGitEnv(EMPTY_AGENT);
|
|
39
|
+
process.on("exit", () => rmSync(EMPTY_AGENT, { recursive: true, force: true }));
|
|
40
|
+
const git = (root, args) => {
|
|
41
|
+
const r = spawnSync("git", ["-c", "commit.gpgsign=false", ...args], { cwd: root, encoding: "utf8", env: { ...process.env, ...GIT_ENV } });
|
|
42
|
+
assert.equal(r.status, 0, r.stderr);
|
|
43
|
+
return r.stdout.trim();
|
|
44
|
+
};
|
|
42
45
|
const write = (root, rel, text) => {
|
|
43
46
|
mkdirSync(join(root, dirname(rel)), { recursive: true });
|
|
44
47
|
writeFileSync(join(root, rel), text);
|
|
45
48
|
};
|
|
46
|
-
const commit = (root,
|
|
47
|
-
git(root, ["add", "
|
|
48
|
-
git(root, ["commit", "-
|
|
49
|
-
return out(root, ["rev-parse", "HEAD"]);
|
|
50
|
-
};
|
|
51
|
-
// main: README only. feat: spec + src/a.ts; the record is untracked unless a test commits it.
|
|
52
|
-
const repo = (options = {}) => {
|
|
53
|
-
const record = Object.hasOwn(options, "record") ? options.record : RECORD;
|
|
54
|
-
const root = mkdtempSync(join(tmpdir(), "gts-"));
|
|
55
|
-
git(root, ["init", "-q", "-b", "main"]);
|
|
56
|
-
write(root, "README.md", "# fixture\n");
|
|
57
|
-
commit(root, "init", "README.md");
|
|
58
|
-
git(root, ["checkout", "-q", "-b", "feat"]);
|
|
59
|
-
write(root, SPEC, "# Spec X\n\n**Goal:** g.\n");
|
|
60
|
-
write(root, "src/a.ts", "export const a = 1;\n");
|
|
61
|
-
commit(root, "Add spec and code", SPEC, "src/a.ts");
|
|
62
|
-
if (record !== undefined) write(root, REC, record);
|
|
63
|
-
return root;
|
|
49
|
+
const commit = (root, ...paths) => {
|
|
50
|
+
git(root, ["add", "--", ...paths]);
|
|
51
|
+
git(root, ["commit", "-qm", "fixture"]);
|
|
64
52
|
};
|
|
65
|
-
const
|
|
53
|
+
const repo = (t, { record = RECORD, spec = SPEC } = {}) => {
|
|
66
54
|
const root = mkdtempSync(join(tmpdir(), "gts-"));
|
|
55
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
67
56
|
git(root, ["init", "-q", "-b", "main"]);
|
|
57
|
+
git(root, ["remote", "add", "origin", "https://example.test/team/product.git"]);
|
|
58
|
+
git(root, ["config", `url.${root}.insteadOf`, "https://example.test/team/product.git"]);
|
|
68
59
|
write(root, "README.md", "# fixture\n");
|
|
69
|
-
commit(root, "
|
|
70
|
-
git(root, ["checkout", "-
|
|
71
|
-
write(root,
|
|
60
|
+
commit(root, "README.md");
|
|
61
|
+
git(root, ["checkout", "-qb", "feat"]);
|
|
62
|
+
write(root, spec, "# Spec\n\n**Goal:** g.\n");
|
|
72
63
|
write(root, "src/a.ts", "export const a = 1;\n");
|
|
73
|
-
write(root, "docs/plans/x.md", "# Plan
|
|
74
|
-
commit(root,
|
|
75
|
-
write(root,
|
|
64
|
+
write(root, "docs/plans/x.md", "# Plan\n");
|
|
65
|
+
commit(root, spec, "src/a.ts", "docs/plans/x.md");
|
|
66
|
+
if (record !== null) write(root, `.git/gauntlet/telemetry/${spec.replace(/\.md$/, ".yaml")}`, record.replace(`spec: ${SPEC}`, `spec: ${spec}`));
|
|
76
67
|
return root;
|
|
77
68
|
};
|
|
78
|
-
const EMPTY_AGENT = mkdtempSync(join(tmpdir(), "gts-empty-agent-"));
|
|
79
|
-
process.on("exit", () => rmSync(EMPTY_AGENT, { recursive: true, force: true }));
|
|
80
|
-
const IDENTITY = { GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@t" };
|
|
81
69
|
const invoke = (args, env = {}) => {
|
|
82
|
-
const r = spawnSync(process.execPath, [CLI, ...args], { encoding: "utf8", env: { ...process.env, ...
|
|
83
|
-
return { status: r.status,
|
|
70
|
+
const r = spawnSync(process.execPath, [CLI, ...args], { encoding: "utf8", env: { ...process.env, ...GIT_ENV, PI_CODING_AGENT_DIR: EMPTY_AGENT, ...env } });
|
|
71
|
+
return { status: r.status, stderr: r.stderr.trim(), lines: r.stdout.split("\n").filter(Boolean) };
|
|
84
72
|
};
|
|
85
73
|
const run = (root, args = [], env = {}) => invoke(["--worktree", root, "--option", "squash", "--base", "main", ...args], env);
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const porcelain = (root) => out(root, ["status", "--porcelain", "--untracked-files=all"]);
|
|
91
|
-
const record = (root) => parseYaml(readFileSync(join(root, REC), "utf8"));
|
|
92
|
-
const cleanup = (t, ...roots) => t.after(() => roots.forEach((r) => rmSync(r, { recursive: true, force: true })));
|
|
74
|
+
const blob = (root, spec = SPEC) => git(root, ["show", `${REF}:team/product/brainstorming/${spec.replace(/\.md$/, ".yaml")}`]);
|
|
75
|
+
const record = (root) => parseYaml(blob(root));
|
|
76
|
+
const head = (root) => git(root, ["rev-parse", "HEAD"]);
|
|
77
|
+
const recPath = (root, spec = SPEC) => join(realpathSync(root), `.git/gauntlet/telemetry/${spec.replace(/\.md$/, ".yaml")}`);
|
|
93
78
|
|
|
94
|
-
test("seals
|
|
95
|
-
const root = repo(
|
|
96
|
-
const
|
|
79
|
+
test("seals the common-dir record: stamp, ship event, diff and re-derived duration in an orphan commit only", (t) => {
|
|
80
|
+
const root = repo(t);
|
|
81
|
+
const before = head(root);
|
|
82
|
+
const index = readFileSync(join(root, ".git/index"));
|
|
97
83
|
const r = run(root, ["--spec", SPEC]);
|
|
98
84
|
assert.equal(r.status, 0, r.stderr);
|
|
99
|
-
assert.deepEqual(r.lines, [`sealed ${
|
|
85
|
+
assert.deepEqual(r.lines, [`sealed ${recPath(root)}`]);
|
|
100
86
|
const rec = record(root);
|
|
101
87
|
assert.equal(rec.status, "shipped");
|
|
102
88
|
assert.match(rec.shipped_at, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
|
|
103
89
|
assert.equal(rec.derived.gates.ship_option, "squash");
|
|
104
|
-
assert.equal(typeof rec.derived.gates.ship_option, "string");
|
|
105
90
|
assert.equal(rec.events.at(-1).kind, "ship");
|
|
106
91
|
assert.equal(rec.events.at(-1).option, "squash");
|
|
107
92
|
assert.deepEqual(rec.derived.modified_files, ["src/a.ts"]);
|
|
108
|
-
assert.equal(rec.derived.diff.commits, 1
|
|
93
|
+
assert.equal(rec.derived.diff.commits, 1);
|
|
109
94
|
assert.equal(rec.derived.diff.buckets.code.files, 1);
|
|
110
|
-
|
|
111
|
-
assert.
|
|
112
|
-
assert.
|
|
113
|
-
assert.equal(
|
|
114
|
-
assert.
|
|
115
|
-
assert.equal(
|
|
95
|
+
assert.ok(rec.derived.duration_s > 60);
|
|
96
|
+
assert.equal(head(root), before);
|
|
97
|
+
assert.deepEqual(readFileSync(join(root, ".git/index")), index);
|
|
98
|
+
assert.equal(git(root, ["status", "--porcelain"]), "");
|
|
99
|
+
assert.equal(git(root, ["rev-list", "--parents", "-1", REF]).split(" ").length, 1);
|
|
100
|
+
assert.equal(git(root, ["ls-tree", "-r", "--name-only", REF]), BLOB);
|
|
101
|
+
assert.equal(existsSync(join(root, REC)), false, "in-flight file removed only after commit");
|
|
116
102
|
});
|
|
117
103
|
|
|
118
104
|
test("shipped CLI records companion legs and validates complete retry sets", (t) => {
|
|
119
|
-
const root = repo(
|
|
105
|
+
const root = repo(t);
|
|
120
106
|
const args = ["--spec", SPEC, "--leg", "repo-b=abc1234", "--leg", "repo-c=https://example.test/pull/2?q=yes"];
|
|
121
|
-
|
|
122
|
-
assert.
|
|
123
|
-
|
|
124
|
-
{ repo: "repo-b", sha: "abc1234" },
|
|
125
|
-
{ repo: "repo-c", pr: "https://example.test/pull/2?q=yes" },
|
|
126
|
-
]);
|
|
127
|
-
const sealedHead = head(root);
|
|
107
|
+
assert.equal(run(root, args).status, 0);
|
|
108
|
+
assert.deepEqual(record(root).legs, [{ repo: "repo-b", sha: "abc1234" }, { repo: "repo-c", pr: "https://example.test/pull/2?q=yes" }]);
|
|
109
|
+
const tip = git(root, ["rev-parse", REF]);
|
|
128
110
|
assert.equal(run(root, ["--spec", SPEC, "--leg", args[5], "--leg", args[3]]).status, 0);
|
|
129
|
-
assert.equal(head(root), sealedHead);
|
|
130
111
|
for (const legs of [[], ["--leg", "repo-b=abc1234"], ["--leg", "repo-b=changed", "--leg", args[5]]]) {
|
|
131
112
|
const retry = run(root, ["--spec", SPEC, ...legs]);
|
|
132
113
|
assert.equal(retry.status, 1);
|
|
133
114
|
assert.match(retry.stderr, /legs differ/);
|
|
134
|
-
assert.equal(head(root), sealedHead);
|
|
135
115
|
}
|
|
136
|
-
assert.
|
|
116
|
+
assert.equal(git(root, ["rev-parse", REF]), tip);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("branch-backed retries use a fetched descendant without discarding pending local records", (t) => {
|
|
120
|
+
const root = repo(t);
|
|
121
|
+
const remote = mkdtempSync(join(tmpdir(), "gts-remote-"));
|
|
122
|
+
t.after(() => rmSync(remote, { recursive: true, force: true }));
|
|
123
|
+
git(root, ["init", "--bare", remote]);
|
|
124
|
+
git(root, ["config", `url.${root}.insteadOf`, "unused"]);
|
|
125
|
+
git(root, ["config", `url.${remote}.insteadOf`, "https://example.test/team/product.git"]);
|
|
126
|
+
assert.equal(run(root, ["--spec", SPEC]).status, 0);
|
|
127
|
+
const ancestor = git(root, ["rev-parse", REF]);
|
|
128
|
+
const other = "doc/specs/other.md";
|
|
129
|
+
write(root, `.git/gauntlet/telemetry/${other.replace(/\.md$/, ".yaml")}`, RECORD.replace(SPEC, other));
|
|
130
|
+
const args = ["--spec", other, "--leg", "repo-b=abc1234"];
|
|
131
|
+
assert.equal(run(root, args).status, 0);
|
|
132
|
+
const published = git(remote, ["rev-parse", REF]);
|
|
133
|
+
git(root, ["update-ref", REF, ancestor]);
|
|
134
|
+
const retry = run(root, args);
|
|
135
|
+
assert.equal(retry.status, 0, retry.stderr);
|
|
136
|
+
assert.deepEqual(retry.lines, [`already sealed ${recPath(root, other)}`]);
|
|
137
|
+
const mismatch = run(root, ["--spec", other]);
|
|
138
|
+
assert.equal(mismatch.status, 1);
|
|
139
|
+
assert.match(mismatch.stderr, /legs differ/);
|
|
140
|
+
assert.equal(git(remote, ["rev-parse", REF]), published, "retry adds no commit");
|
|
141
|
+
|
|
142
|
+
git(root, ["config", `url.${remote}.insteadOf`, "unused"]);
|
|
143
|
+
git(root, ["config", `url.${join(remote, "missing")}.insteadOf`, "https://example.test/team/product.git"]);
|
|
144
|
+
const pending = "doc/specs/pending.md";
|
|
145
|
+
write(root, `.git/gauntlet/telemetry/${pending.replace(/\.md$/, ".yaml")}`, RECORD.replace(SPEC, pending));
|
|
146
|
+
assert.match(run(root, ["--spec", pending]).lines.join("\n"), /telemetry: not pushed/);
|
|
147
|
+
const offlineRetry = run(root, ["--spec", pending]);
|
|
148
|
+
assert.equal(offlineRetry.status, 0, offlineRetry.stderr);
|
|
149
|
+
assert.match(offlineRetry.lines.join("\n"), /telemetry: not pushed/);
|
|
150
|
+
assert.doesNotMatch(offlineRetry.lines.join("\n"), /no telemetry run/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("source cleanup failure after publication warns without blocking the ship", (t) => {
|
|
154
|
+
const root = repo(t);
|
|
155
|
+
const source = recPath(root);
|
|
156
|
+
write(root, ".git/hooks/reference-transaction", `#!/bin/sh
|
|
157
|
+
if [ "$1" = committed ]; then
|
|
158
|
+
while read old new ref; do
|
|
159
|
+
if [ "$ref" = "${REF}" ] && [ -f "${source}" ]; then
|
|
160
|
+
rm "${source}"
|
|
161
|
+
mkdir "${source}"
|
|
162
|
+
fi
|
|
163
|
+
done
|
|
164
|
+
fi
|
|
165
|
+
`);
|
|
166
|
+
chmodSync(join(root, ".git/hooks/reference-transaction"), 0o755);
|
|
167
|
+
const result = run(root, ["--spec", SPEC]);
|
|
168
|
+
assert.equal(result.status, 0, result.stderr);
|
|
169
|
+
assert.match(result.stderr, /telemetry: not pushed.*(?:EISDIR|EPERM)/);
|
|
170
|
+
assert.equal(record(root).status, "shipped", "the published record survives failed cleanup");
|
|
171
|
+
assert.equal(git(root, ["rev-parse", REF]), git(root, ["rev-parse", "refs/remotes/origin/gauntlet/telemetry"]));
|
|
137
172
|
});
|
|
138
173
|
|
|
139
174
|
test("shipped CLI rejects malformed leg values before sealing", () => {
|
|
140
175
|
for (const value of ["missing", "=abc", "repo=", "repo= "]) {
|
|
141
|
-
const
|
|
142
|
-
assert.equal(
|
|
143
|
-
assert.match(
|
|
144
|
-
assert.ok(
|
|
176
|
+
const r = invoke(["--leg", value]);
|
|
177
|
+
assert.equal(r.status, 1);
|
|
178
|
+
assert.match(r.stderr, /invalid --leg/);
|
|
179
|
+
assert.ok(r.stderr.includes(value.trim()));
|
|
145
180
|
}
|
|
146
181
|
});
|
|
147
182
|
|
|
148
|
-
test("docs/specs
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const r = run(root);
|
|
158
|
-
assert.equal(r.status, 0, r.stderr);
|
|
159
|
-
assert.deepEqual(r.lines, [`sealed ${REC2}`]);
|
|
160
|
-
const rec = parseYaml(readFileSync(join(root, REC2), "utf8"));
|
|
161
|
-
assert.ok(!rec.derived.modified_files.includes("docs/plans/x.md"), "docs/plans excluded from modified_files");
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("renamed spec still selects the record at its old path", (t) => {
|
|
165
|
-
const root = mkdtempSync(join(tmpdir(), "gts-")); cleanup(t, root);
|
|
166
|
-
git(root, ["init", "-q", "-b", "main"]);
|
|
167
|
-
write(root, SPEC, "# Spec X\n\n**Goal:** g.\n");
|
|
168
|
-
commit(root, "Add original spec", SPEC);
|
|
169
|
-
git(root, ["checkout", "-q", "-b", "feat"]);
|
|
170
|
-
assert.equal(git(root, ["mv", SPEC, "doc/specs/renamed.md"]).status, 0);
|
|
171
|
-
commit(root, "Rename spec", SPEC, "doc/specs/renamed.md");
|
|
172
|
-
write(root, REC, RECORD);
|
|
173
|
-
const r = run(root);
|
|
174
|
-
assert.equal(r.status, 0, r.stderr);
|
|
175
|
-
assert.deepEqual(r.lines, [`sealed ${REC}`]);
|
|
183
|
+
test("docs/specs records seal with explicit and changed-spec selection, excluding plans", async (t) => {
|
|
184
|
+
for (const explicit of [true, false]) await t.test(explicit ? "explicit" : "discovered", (t) => {
|
|
185
|
+
const spec = "docs/specs/y.md";
|
|
186
|
+
const root = repo(t, { spec });
|
|
187
|
+
const r = run(root, explicit ? ["--spec", spec] : []);
|
|
188
|
+
assert.equal(r.status, 0, r.stderr);
|
|
189
|
+
assert.deepEqual(r.lines, [`sealed ${recPath(root, spec)}`]);
|
|
190
|
+
assert.deepEqual(parseYaml(blob(root, spec)).derived.modified_files, ["src/a.ts"]);
|
|
191
|
+
});
|
|
176
192
|
});
|
|
177
193
|
|
|
178
|
-
test("
|
|
179
|
-
const root = repo(
|
|
180
|
-
|
|
181
|
-
write(root,
|
|
194
|
+
test("unrelated and malformed records do not prevent changed-spec selection", (t) => {
|
|
195
|
+
const root = repo(t);
|
|
196
|
+
write(root, ".git/gauntlet/telemetry/bad.yaml", "schema: 1\nspec: bad.md\n");
|
|
197
|
+
write(root, ".git/gauntlet/telemetry/doc/specs/other.yaml", RECORD.replace(SPEC, "doc/specs/other.md"));
|
|
182
198
|
const r = run(root);
|
|
183
199
|
assert.equal(r.status, 0, r.stderr);
|
|
184
|
-
assert.
|
|
185
|
-
assert.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
test("shipped-but-uncommitted docs/specs record is committed on retry without --spec", (t) => {
|
|
189
|
-
const root = docsRepo(); cleanup(t, root);
|
|
190
|
-
write(root, REC2, RECORD.replace("spec: doc/specs/x.md", `spec: ${SPEC2}`).replace("status: in_progress", "status: shipped\nshipped_at: 2026-09-17T18:00:00Z"));
|
|
191
|
-
const r = run(root);
|
|
192
|
-
assert.equal(r.status, 0, r.stderr);
|
|
193
|
-
assert.deepEqual(r.lines, [`sealed ${REC2}`]);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("a record whose spec did not change on the branch is ignored", (t) => {
|
|
197
|
-
const root = repo(); cleanup(t, root);
|
|
198
|
-
write(root, ".pi/gauntlet/telemetry/doc/specs/other.yaml", RECORD.replace("spec: doc/specs/x.md", "spec: doc/specs/other.md"));
|
|
199
|
-
const r = run(root);
|
|
200
|
-
assert.deepEqual(r.lines, [`sealed ${REC}`]);
|
|
200
|
+
assert.deepEqual(r.lines, [`sealed ${recPath(root)}`]);
|
|
201
|
+
assert.equal(existsSync(join(root, ".git/gauntlet/telemetry/bad.yaml")), true);
|
|
202
|
+
assert.equal(git(root, ["ls-tree", "-r", "--name-only", REF]), BLOB);
|
|
201
203
|
});
|
|
202
204
|
|
|
203
205
|
test("seal preserves derived.council through the shipped stamp", (t) => {
|
|
204
206
|
const council = { chair: { model: "p/chair:medium", dispatches: 1, clusters: 0, members_reported: 0 }, members: {} };
|
|
205
|
-
const root = repo({ record: RECORD.replace(" spec_writes: {}", ` council: ${JSON.stringify(council)}\n spec_writes: {}`) });
|
|
206
|
-
|
|
207
|
-
assert.equal(r.status, 0, r.stderr);
|
|
208
|
-
assert.equal(record(root).status, "shipped");
|
|
207
|
+
const root = repo(t, { record: RECORD.replace(" spec_writes: {}", ` council: ${JSON.stringify(council)}\n spec_writes: {}`) });
|
|
208
|
+
assert.equal(run(root, ["--spec", SPEC]).status, 0);
|
|
209
209
|
assert.deepEqual(record(root).derived.council, council);
|
|
210
|
-
assert.deepEqual(parseYaml(out(root, ["show", `HEAD:${REC}`])).derived.council, council);
|
|
211
210
|
});
|
|
212
211
|
|
|
213
|
-
test("--spec
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
test("--spec resolves absolute, symlinked and dot-relative checkout paths", async (t) => {
|
|
213
|
+
for (const form of ["absolute", "symlink", "relative"]) await t.test(form, (t) => {
|
|
214
|
+
const root = repo(t);
|
|
215
|
+
const links = mkdtempSync(join(tmpdir(), "gts-link-"));
|
|
216
|
+
t.after(() => rmSync(links, { recursive: true, force: true }));
|
|
217
|
+
symlinkSync(root, join(links, "checkout"));
|
|
218
|
+
const spec = form === "absolute" ? join(realpathSync(root), SPEC) : form === "symlink" ? join(links, "checkout", SPEC) : `./${SPEC}`;
|
|
219
|
+
const r = run(root, ["--spec", spec]);
|
|
220
|
+
assert.equal(r.status, 0, r.stderr);
|
|
221
|
+
assert.deepEqual(r.lines, [`sealed ${recPath(root)}`]);
|
|
222
|
+
});
|
|
218
223
|
});
|
|
219
224
|
|
|
220
|
-
test("--
|
|
221
|
-
const root = repo();
|
|
222
|
-
const linkDir = mkdtempSync(join(tmpdir(), "gts-link-"));
|
|
223
|
-
cleanup(t, root, linkDir);
|
|
224
|
-
const link = join(linkDir, "checkout");
|
|
225
|
-
symlinkSync(root, link);
|
|
226
|
-
const r = run(root, ["--spec", join(link, SPEC)]);
|
|
227
|
-
assert.equal(r.status, 0, r.stderr);
|
|
228
|
-
assert.deepEqual(r.lines, [`sealed ${REC}`]);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("--spec accepts a dot-relative path", (t) => {
|
|
232
|
-
const root = repo(); cleanup(t, root);
|
|
233
|
-
const r = run(root, ["--spec", `./${SPEC}`]);
|
|
234
|
-
assert.equal(r.status, 0, r.stderr);
|
|
235
|
-
assert.deepEqual(r.lines, [`sealed ${REC}`]);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test("--option pr is recorded; without --spec the candidates are the existing records of changed specs", (t) => {
|
|
239
|
-
const root = repo(); cleanup(t, root);
|
|
225
|
+
test("--option pr is recorded when discovering existing changed-spec records", (t) => {
|
|
226
|
+
const root = repo(t);
|
|
240
227
|
const r = invoke(["--worktree", root, "--option", "pr", "--base", "main"]);
|
|
241
228
|
assert.equal(r.status, 0, r.stderr);
|
|
242
|
-
assert.deepEqual(r.lines, [`sealed ${REC}`]);
|
|
243
229
|
assert.equal(record(root).derived.gates.ship_option, "pr");
|
|
244
230
|
});
|
|
245
231
|
|
|
246
|
-
test("
|
|
247
|
-
const root = repo({ record: SEALED });
|
|
248
|
-
commit(root, "telemetry: doc/specs/x.md", REC);
|
|
232
|
+
test("shipped in-flight bytes commit as is, then branch-only retry is a no-op", (t) => {
|
|
233
|
+
const root = repo(t, { record: SEALED });
|
|
249
234
|
const before = head(root);
|
|
250
|
-
const r = run(root
|
|
235
|
+
const r = run(root);
|
|
251
236
|
assert.equal(r.status, 0, r.stderr);
|
|
252
|
-
assert.deepEqual(r.lines, [`
|
|
237
|
+
assert.deepEqual(r.lines, [`sealed ${recPath(root)}`]);
|
|
238
|
+
assert.equal(blob(root), SEALED.trim());
|
|
239
|
+
const tip = git(root, ["rev-parse", REF]);
|
|
240
|
+
assert.deepEqual(run(root, ["--spec", SPEC]).lines, [`already sealed ${recPath(root)}`]);
|
|
241
|
+
assert.equal(git(root, ["rev-parse", REF]), tip);
|
|
253
242
|
assert.equal(head(root), before);
|
|
254
|
-
assert.equal(record(root).shipped_at, "2026-09-17T18:00:00Z");
|
|
255
243
|
});
|
|
256
244
|
|
|
257
|
-
test("
|
|
258
|
-
const root = repo({ record: RECORD.replace("status: in_progress", "status: abandoned") });
|
|
259
|
-
|
|
260
|
-
const r = run(root, ["--spec", SPEC]);
|
|
261
|
-
assert.equal(r.status, 0, r.stderr);
|
|
262
|
-
assert.deepEqual(r.lines, [`already sealed ${REC}`]);
|
|
263
|
-
assert.equal(head(root), before);
|
|
245
|
+
test("abandoned in-flight records publish without a shipped stamp", (t) => {
|
|
246
|
+
const root = repo(t, { record: RECORD.replace("status: in_progress", "status: abandoned") });
|
|
247
|
+
assert.equal(run(root, ["--spec", SPEC]).status, 0);
|
|
264
248
|
assert.equal(record(root).status, "abandoned");
|
|
249
|
+
assert.equal(record(root).shipped_at, undefined);
|
|
265
250
|
});
|
|
266
251
|
|
|
267
|
-
test("
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test("--spec with no record -> no record at <path>, exit 2, no commit", (t) => {
|
|
279
|
-
const root = repo({ record: undefined }); cleanup(t, root);
|
|
280
|
-
const n = commitCount(root);
|
|
281
|
-
const r = run(root, ["--spec", SPEC]);
|
|
282
|
-
assert.equal(r.status, 2);
|
|
283
|
-
assert.equal(r.stderr, `no record at ${REC}`);
|
|
284
|
-
assert.equal(commitCount(root), n);
|
|
252
|
+
test("missing runs and malformed records are non-blocking and remain untouched", async (t) => {
|
|
253
|
+
for (const content of [null, "schema: 1\nspec: doc/specs/x.md\n"]) await t.test(content === null ? "missing" : "malformed", (t) => {
|
|
254
|
+
const root = repo(t, { record: content });
|
|
255
|
+
for (const args of [["--spec", SPEC], []]) {
|
|
256
|
+
const r = run(root, args);
|
|
257
|
+
assert.equal(r.status, 0, r.stderr);
|
|
258
|
+
assert.match(r.lines.join("\n"), content === null ? /no telemetry run/ : /telemetry: skipped \(unparseable record/);
|
|
259
|
+
}
|
|
260
|
+
if (content !== null) assert.equal(readFileSync(recPath(root), "utf8"), content);
|
|
261
|
+
});
|
|
285
262
|
});
|
|
286
263
|
|
|
287
|
-
test("
|
|
288
|
-
const root = repo(
|
|
289
|
-
const r = run(root, ["--spec", SPEC]);
|
|
290
|
-
assert.equal(r.status, 2);
|
|
291
|
-
assert.match(r.stderr, new RegExp(`^unparseable record ${REC.replace(/\./g, "\\.")}`));
|
|
292
|
-
assert.equal(readFileSync(join(root, REC), "utf8"), "schema: 1\nspec: doc/specs/x.md\n");
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("no telemetry run: a changed spec without a record is not a candidate (supersession edit)", (t) => {
|
|
296
|
-
const root = repo({ record: undefined }); cleanup(t, root);
|
|
297
|
-
const r = run(root);
|
|
298
|
-
assert.equal(r.status, 0, r.stderr);
|
|
299
|
-
assert.deepEqual(r.lines, ["no telemetry run"]);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
test("telemetry disabled -> exit 0, nothing touched", (t) => {
|
|
303
|
-
const root = repo(); cleanup(t, root);
|
|
264
|
+
test("telemetry disabled leaves the common-dir record untouched", (t) => {
|
|
265
|
+
const root = repo(t);
|
|
304
266
|
write(root, ".pi/settings.json", JSON.stringify({ piGauntlet: { telemetry: { enabled: false } } }));
|
|
305
|
-
|
|
306
|
-
assert.equal(
|
|
307
|
-
assert.deepEqual(r.lines, ["telemetry disabled"]);
|
|
308
|
-
assert.equal(record(root).status, "in_progress");
|
|
267
|
+
assert.deepEqual(run(root, ["--spec", SPEC]).lines, ["telemetry disabled"]);
|
|
268
|
+
assert.equal(readFileSync(recPath(root), "utf8"), RECORD);
|
|
309
269
|
});
|
|
310
270
|
|
|
311
|
-
test("
|
|
312
|
-
const root = repo(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
assert.equal(
|
|
328
|
-
assert.
|
|
329
|
-
assert.equal(
|
|
330
|
-
assert.equal(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
chmodSync(hook, 0o755);
|
|
340
|
-
const n = commitCount(root);
|
|
341
|
-
const r = run(root, ["--spec", SPEC]);
|
|
342
|
-
assert.equal(r.status, 1);
|
|
343
|
-
assert.match(r.stderr, /rejected/);
|
|
344
|
-
assert.equal(readFileSync(join(root, REC), "utf8"), RECORD);
|
|
345
|
-
assert.equal(out(root, ["diff", "--cached", "--name-only"]), "");
|
|
346
|
-
assert.equal(commitCount(root), n);
|
|
271
|
+
test("failed branch commit keeps the shipped in-flight record for a successful retry", (t) => {
|
|
272
|
+
const root = repo(t);
|
|
273
|
+
const before = head(root);
|
|
274
|
+
const index = readFileSync(join(root, ".git/index"));
|
|
275
|
+
const hook = ".git/hooks/reference-transaction";
|
|
276
|
+
write(root, hook, '#!/bin/sh\nif [ "$1" = prepared ]; then echo rejected >&2; exit 1; fi\n');
|
|
277
|
+
chmodSync(join(root, hook), 0o755);
|
|
278
|
+
const failed = run(root, ["--spec", SPEC]);
|
|
279
|
+
assert.equal(failed.status, 0, failed.stderr);
|
|
280
|
+
assert.match(failed.lines.join("\n"), /telemetry: not pushed/);
|
|
281
|
+
const kept = readFileSync(recPath(root), "utf8");
|
|
282
|
+
assert.equal(parseYaml(kept).status, "shipped");
|
|
283
|
+
assert.equal(head(root), before);
|
|
284
|
+
assert.deepEqual(readFileSync(join(root, ".git/index")), index);
|
|
285
|
+
write(root, hook, "#!/bin/sh\nexit 0\n");
|
|
286
|
+
const retried = run(root, ["--spec", SPEC]);
|
|
287
|
+
assert.equal(retried.status, 0, retried.stderr);
|
|
288
|
+
assert.deepEqual(retried.lines, [`sealed ${recPath(root)}`]);
|
|
289
|
+
assert.equal(blob(root), kept.trim());
|
|
290
|
+
assert.equal(existsSync(recPath(root)), false);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("checkout hooks do not run for plumbing-only telemetry commits", (t) => {
|
|
294
|
+
const root = repo(t);
|
|
295
|
+
write(root, ".git/hooks/pre-commit", "#!/bin/sh\necho rejected >&2\nexit 1\n");
|
|
296
|
+
chmodSync(join(root, ".git/hooks/pre-commit"), 0o755);
|
|
297
|
+
assert.equal(run(root, ["--spec", SPEC]).status, 0);
|
|
298
|
+
assert.equal(record(root).status, "shipped");
|
|
347
299
|
});
|
|
348
300
|
|
|
349
301
|
test("usage and environment errors exit 1", (t) => {
|
|
350
|
-
const root = repo(
|
|
351
|
-
|
|
352
|
-
assert.equal(noArgs.status, 1);
|
|
353
|
-
assert.match(noArgs.stderr, /^usage: gauntlet-telemetry-seal --worktree <abs path> --option <pr\|squash> --base <ref> \[--spec <spec path inside the checkout>\] \[--dir <telemetry dir>\]/);
|
|
302
|
+
const root = repo(t);
|
|
303
|
+
assert.match(invoke([]).stderr, /^usage: gauntlet-telemetry-seal/);
|
|
354
304
|
assert.equal(invoke(["--worktree", root, "--option", "rebase", "--base", "main"]).status, 1);
|
|
355
|
-
assert.equal(invoke(["--worktree", root, "--option", "squash"]).status, 1
|
|
356
|
-
|
|
357
|
-
assert.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
assert.equal(
|
|
361
|
-
assert.match(invalid.stderr, /^usage:/);
|
|
362
|
-
const noRec = run(root, ["--spec", "README.md"]);
|
|
363
|
-
assert.equal(noRec.status, 2);
|
|
364
|
-
assert.equal(noRec.stderr, "no record at .pi/gauntlet/telemetry/README.yaml");
|
|
365
|
-
write(root, ".pi/settings.json", JSON.stringify({ piGauntlet: { telemetry: { enabled: false } } }));
|
|
366
|
-
const disabledInvalid = run(root, ["--spec", "/tmp/elsewhere/doc/specs/x.md"]);
|
|
367
|
-
assert.equal(disabledInvalid.status, 1);
|
|
368
|
-
assert.match(disabledInvalid.stderr, /^usage:/);
|
|
369
|
-
const notGit = mkdtempSync(join(tmpdir(), "gts-nogit-")); cleanup(t, notGit);
|
|
370
|
-
const ng = invoke(["--worktree", notGit, "--option", "squash", "--base", "main"]);
|
|
371
|
-
assert.equal(ng.status, 1);
|
|
372
|
-
assert.equal(ng.stderr, "not a git or jj checkout");
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
test("--dir overrides the telemetry dir", (t) => {
|
|
376
|
-
const root = repo({ record: undefined }); cleanup(t, root);
|
|
377
|
-
write(root, "tele/doc/specs/x.yaml", RECORD);
|
|
378
|
-
const r = run(root, ["--spec", SPEC, "--dir", "tele"]);
|
|
379
|
-
assert.equal(r.status, 0, r.stderr);
|
|
380
|
-
assert.deepEqual(r.lines, ["sealed tele/doc/specs/x.yaml"]);
|
|
381
|
-
assert.deepEqual(headFiles(root), ["tele/doc/specs/x.yaml"]);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// --- Plain jj workspace (no .git). git and jj are PATH shims: git always fails
|
|
385
|
-
// rev-parse and only logs; jj answers from a per-test JSON script. CI has no jj.
|
|
386
|
-
const JJ_FORK = "f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9";
|
|
387
|
-
// src/a.ts: +10 -2; the spec and the record ride along to prove modified_files filters them out.
|
|
388
|
-
const JJ_PATCH = [
|
|
389
|
-
"diff --git a/src/a.ts b/src/a.ts",
|
|
390
|
-
"index 1111111..2222222 100644",
|
|
391
|
-
"--- a/src/a.ts",
|
|
392
|
-
"+++ b/src/a.ts",
|
|
393
|
-
"@@ -1,4 +1,12 @@",
|
|
394
|
-
...Array.from({ length: 10 }, (_, i) => `+added ${i}`),
|
|
395
|
-
"-removed 1",
|
|
396
|
-
"-removed 2",
|
|
397
|
-
" context",
|
|
398
|
-
"diff --git a/doc/specs/x.md b/doc/specs/x.md",
|
|
399
|
-
"new file mode 100644",
|
|
400
|
-
"index 0000000..3333333",
|
|
401
|
-
"--- /dev/null",
|
|
402
|
-
"+++ b/doc/specs/x.md",
|
|
403
|
-
"@@ -0,0 +1,5 @@",
|
|
404
|
-
...Array.from({ length: 5 }, (_, i) => `+spec ${i}`),
|
|
405
|
-
"diff --git a/.pi/gauntlet/telemetry/doc/specs/x.yaml b/.pi/gauntlet/telemetry/doc/specs/x.yaml",
|
|
406
|
-
"new file mode 100644",
|
|
407
|
-
"index 0000000..4444444",
|
|
408
|
-
"--- /dev/null",
|
|
409
|
-
"+++ b/.pi/gauntlet/telemetry/doc/specs/x.yaml",
|
|
410
|
-
"@@ -0,0 +1,3 @@",
|
|
411
|
-
"+schema: 1",
|
|
412
|
-
"+spec: doc/specs/x.md",
|
|
413
|
-
"+run_id: r-1",
|
|
414
|
-
"",
|
|
415
|
-
].join("\n");
|
|
416
|
-
const GIT_SHIM = (log) => `#!${process.execPath}
|
|
417
|
-
require("node:fs").appendFileSync(${JSON.stringify(log)}, "git " + process.argv.slice(2).join(" ") + "\\n");
|
|
418
|
-
process.stderr.write("fatal: not a git repository (or any of the parent directories): .git\\n");
|
|
419
|
-
process.exit(128);
|
|
420
|
-
`;
|
|
421
|
-
const JJ_SHIM = (log, scriptPath) => `#!${process.execPath}
|
|
422
|
-
const fs = require("node:fs");
|
|
423
|
-
const a = process.argv.slice(2);
|
|
424
|
-
fs.appendFileSync(${JSON.stringify(log)}, "jj " + a.join(" ") + "\\n");
|
|
425
|
-
const script = JSON.parse(fs.readFileSync(${JSON.stringify(scriptPath)}, "utf8"));
|
|
426
|
-
const s = a.join(" ");
|
|
427
|
-
const send = (r) => { const res = r ?? { code: 1, stderr: "unscripted jj call" }; process.stdout.write(res.stdout ?? ""); process.stderr.write(res.stderr ?? ""); process.exit(res.code ?? 0); };
|
|
428
|
-
if (s === "root") send(script.root);
|
|
429
|
-
if (s.includes("--limit")) send(script.base);
|
|
430
|
-
if (s.includes("--count")) send(script.count);
|
|
431
|
-
if (s.includes("fork_point(")) send(script.fork);
|
|
432
|
-
if (s.includes("--name-only")) send(script.names);
|
|
433
|
-
if (s.startsWith("file track")) send(script.track);
|
|
434
|
-
send(script.patch);
|
|
435
|
-
`;
|
|
436
|
-
const jjWorkspace = (t, script, { recordText = RECORD } = {}) => {
|
|
437
|
-
const root = mkdtempSync(join(tmpdir(), "gts-jj-"));
|
|
438
|
-
write(root, SPEC, "# Spec X\n\n**Goal:** g.\n");
|
|
439
|
-
if (recordText !== undefined) write(root, REC, recordText);
|
|
440
|
-
const shims = mkdtempSync(join(tmpdir(), "gts-shim-"));
|
|
441
|
-
const log = join(shims, "calls.log");
|
|
442
|
-
writeFileSync(log, "");
|
|
443
|
-
const scriptPath = join(shims, "script.json");
|
|
444
|
-
writeFileSync(scriptPath, JSON.stringify({ root: { code: 0, stdout: root + "\n" }, ...script }));
|
|
445
|
-
for (const [name, body] of [["git", GIT_SHIM(log)], ["jj", JJ_SHIM(log, scriptPath)]]) {
|
|
446
|
-
const p = join(shims, name);
|
|
447
|
-
writeFileSync(p, body);
|
|
448
|
-
chmodSync(p, 0o755);
|
|
449
|
-
}
|
|
450
|
-
cleanup(t, root, shims);
|
|
451
|
-
return { root, log, env: { PATH: `${shims}:${process.env.PATH}` } };
|
|
452
|
-
};
|
|
453
|
-
const happyJj = {
|
|
454
|
-
base: { code: 0, stdout: JJ_FORK + "\n" },
|
|
455
|
-
fork: { code: 0, stdout: JJ_FORK + "\n" },
|
|
456
|
-
count: { code: 0, stdout: "2\n" },
|
|
457
|
-
patch: { code: 0, stdout: JJ_PATCH },
|
|
458
|
-
track: { code: 0, stdout: "" },
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
test("jj workspace: record stamped shipped from the jj diff, one git probe, no git writes, file track instead of a commit", (t) => {
|
|
462
|
-
const { root, log, env } = jjWorkspace(t, happyJj);
|
|
463
|
-
const r = run(root, ["--spec", SPEC], env);
|
|
464
|
-
assert.equal(r.status, 0, r.stderr);
|
|
465
|
-
assert.deepEqual(r.lines, [`sealed ${REC}`]);
|
|
466
|
-
const rec = record(root);
|
|
467
|
-
assert.equal(rec.status, "shipped");
|
|
468
|
-
assert.equal(rec.events.at(-1).kind, "ship");
|
|
469
|
-
assert.equal(rec.events.at(-1).option, "squash");
|
|
470
|
-
assert.deepEqual(rec.derived.modified_files, ["src/a.ts"], "spec and telemetry-dir paths filtered");
|
|
471
|
-
assert.equal(rec.derived.diff.base, JJ_FORK);
|
|
472
|
-
assert.equal(rec.derived.diff.commits, 2);
|
|
473
|
-
assert.deepEqual(rec.derived.diff.buckets.code, { files: 1, insertions: 10, deletions: 2 });
|
|
474
|
-
const calls = readFileSync(log, "utf8").split("\n").filter(Boolean);
|
|
475
|
-
assert.deepEqual(calls.filter((c) => c.startsWith("git ")).map((c) => c.split(" ")[1]), ["rev-parse"], "git is only the failed checkout probe");
|
|
476
|
-
assert.ok(calls.some((c) => c === "jj root"));
|
|
477
|
-
assert.ok(calls.includes(`jj file track --include-ignored -- ${REC}`), "the record is tracked even under a gitignored dir");
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
test("jj workspace: a failing file track restores the pre-seal bytes and exits 1", (t) => {
|
|
481
|
-
const { root, env } = jjWorkspace(t, { ...happyJj, track: { code: 1, stderr: "Error: Path is not in the workspace\n" } });
|
|
482
|
-
const r = run(root, ["--spec", SPEC], env);
|
|
483
|
-
assert.equal(r.status, 1);
|
|
484
|
-
assert.equal(r.stderr, `seal failed ${REC}: Error: Path is not in the workspace`);
|
|
485
|
-
assert.equal(record(root).status, "in_progress");
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
test("jj workspace with unresolved mainline: warning recorded, diff fields absent, record still sealed", (t) => {
|
|
489
|
-
const { root, env } = jjWorkspace(t, { ...happyJj, fork: { code: 0, stdout: "" } });
|
|
490
|
-
const r = run(root, ["--spec", SPEC], env);
|
|
491
|
-
assert.equal(r.status, 0, r.stderr);
|
|
492
|
-
assert.deepEqual(r.lines, [`sealed ${REC}`]);
|
|
493
|
-
const rec = record(root);
|
|
494
|
-
assert.equal(rec.status, "shipped");
|
|
495
|
-
assert.ok(!("diff" in rec.derived) && !("modified_files" in rec.derived));
|
|
496
|
-
assert.equal(rec.events.find((e) => e.kind === "warning")?.message, "diff omitted: jj mainline unresolved (trunk() is root(); no main/master bookmark)");
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
test("jj diff failure and unparseable patch each seal with a warning and no diff fields", (t) => {
|
|
500
|
-
const failing = jjWorkspace(t, { ...happyJj, patch: { code: 1, stderr: "fatal: diff exploded\n" } });
|
|
501
|
-
const r1 = run(failing.root, ["--spec", SPEC], failing.env);
|
|
502
|
-
assert.equal(r1.status, 0, r1.stderr);
|
|
503
|
-
let rec = record(failing.root);
|
|
504
|
-
assert.equal(rec.status, "shipped");
|
|
505
|
-
assert.ok(!("diff" in rec.derived) && !("modified_files" in rec.derived));
|
|
506
|
-
assert.equal(rec.events.find((e) => e.kind === "warning")?.message, "diff omitted: jj diff failed: fatal: diff exploded");
|
|
507
|
-
|
|
508
|
-
const garbage = jjWorkspace(t, { ...happyJj, patch: { code: 0, stdout: "not a git patch\n" } });
|
|
509
|
-
const r2 = run(garbage.root, ["--spec", SPEC], garbage.env);
|
|
510
|
-
assert.equal(r2.status, 0, r2.stderr);
|
|
511
|
-
rec = record(garbage.root);
|
|
512
|
-
assert.equal(rec.events.find((e) => e.kind === "warning")?.message, "diff omitted: jj diff unparseable");
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
test("jj workspace: an unresolvable or empty --base revset fails no base ref, exit 1", (t) => {
|
|
516
|
-
const unknown = jjWorkspace(t, { ...happyJj, base: { code: 1, stderr: "Error: No such bookmark: nope\n" } });
|
|
517
|
-
const r1 = invoke(["--worktree", unknown.root, "--option", "squash", "--base", "nope", "--spec", SPEC], unknown.env);
|
|
518
|
-
assert.equal(r1.status, 1);
|
|
519
|
-
assert.equal(r1.stderr, "no base ref nope");
|
|
520
|
-
const empty = jjWorkspace(t, { ...happyJj, base: { code: 0, stdout: "" } });
|
|
521
|
-
const r2 = invoke(["--worktree", empty.root, "--option", "squash", "--base", "none()"], empty.env);
|
|
522
|
-
assert.equal(r2.status, 1);
|
|
523
|
-
assert.equal(r2.stderr, "no base ref none()");
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
test("jj workspace without --spec discovers records from the jj fork-point diff", (t) => {
|
|
527
|
-
const found = jjWorkspace(t, { ...happyJj, names: { code: 0, stdout: `${SPEC}\nsrc/a.ts\n` } });
|
|
528
|
-
const r1 = run(found.root, [], found.env);
|
|
529
|
-
assert.equal(r1.status, 0, r1.stderr);
|
|
530
|
-
assert.deepEqual(r1.lines, [`sealed ${REC}`]);
|
|
531
|
-
assert.ok(readFileSync(found.log, "utf8").includes(`jj --color=never diff --name-only --from ${JJ_FORK} --to @`));
|
|
532
|
-
const none = jjWorkspace(t, { ...happyJj, names: { code: 0, stdout: "src/a.ts\n" } });
|
|
533
|
-
const r2 = run(none.root, [], none.env);
|
|
534
|
-
assert.equal(r2.status, 0, r2.stderr);
|
|
535
|
-
assert.deepEqual(r2.lines, ["no telemetry run"]);
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
test("jj workspace: a shipped record is already sealed without any git checks", (t) => {
|
|
539
|
-
const { root, log, env } = jjWorkspace(t, happyJj, { recordText: SEALED });
|
|
540
|
-
const r = run(root, ["--spec", SPEC], env);
|
|
541
|
-
assert.equal(r.status, 0, r.stderr);
|
|
542
|
-
assert.deepEqual(r.lines, [`already sealed ${REC}`]);
|
|
543
|
-
assert.equal(record(root).shipped_at, "2026-09-17T18:00:00Z");
|
|
544
|
-
const calls = readFileSync(log, "utf8").split("\n").filter(Boolean);
|
|
545
|
-
assert.deepEqual(calls.filter((c) => c.startsWith("git ")).map((c) => c.split(" ")[1]), ["rev-parse"]);
|
|
305
|
+
assert.equal(invoke(["--worktree", root, "--option", "squash"]).status, 1);
|
|
306
|
+
assert.equal(run(root, ["--base", "nope"]).stderr, "no base ref nope");
|
|
307
|
+
assert.match(run(root, ["--spec", "/tmp/elsewhere/doc/specs/x.md"]).stderr, /^usage:/);
|
|
308
|
+
const noGit = mkdtempSync(join(tmpdir(), "gts-nogit-"));
|
|
309
|
+
t.after(() => rmSync(noGit, { recursive: true, force: true }));
|
|
310
|
+
assert.equal(run(noGit).stderr, "not a git or jj checkout");
|
|
546
311
|
});
|