pi-plans 0.3.2 → 0.3.3

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.
@@ -0,0 +1,324 @@
1
+ /** Durable review-round reuse tests (I-004). */
2
+
3
+ import * as assert from "node:assert/strict";
4
+ import { spawnSync } from "node:child_process";
5
+ import * as fs from "node:fs";
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+ import { after, before, describe, it } from "node:test";
9
+ import {
10
+ applyLaneResult,
11
+ applyReviewConsolidated,
12
+ createCheckpoint,
13
+ loadCheckpoint,
14
+ mutateCheckpoint,
15
+ readReviewOutput,
16
+ recordLaneOutcome,
17
+ reusableLaneOutputs,
18
+ startReviewRound,
19
+ } from "../src/workflow-state.ts";
20
+ import { initState, startRun } from "../src/state.ts";
21
+
22
+ let tmpRoot: string;
23
+
24
+ function setupRun(name: string): { workdir: string; runId: string; artifactDir: string } {
25
+ const workdir = path.join(tmpRoot, name);
26
+ fs.mkdirSync(workdir, { recursive: true });
27
+ spawnSync("git", ["init"], { cwd: workdir });
28
+ initState(workdir);
29
+ const { run } = startRun(workdir, { topic: name, skill: "plan-normal", requestText: "t" });
30
+ createCheckpoint(workdir, { runId: run.run_id, originWorkdir: workdir, workdir });
31
+ fs.mkdirSync(run.artifact_dir, { recursive: true });
32
+ return { workdir, runId: run.run_id, artifactDir: run.artifact_dir };
33
+ }
34
+
35
+ before(() => {
36
+ tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-rr-"));
37
+ });
38
+
39
+ after(() => {
40
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
41
+ });
42
+
43
+ const BASE_DIR_PLACEHOLDER = path.resolve(import.meta.dirname, "..");
44
+
45
+ describe("round orchestration helpers", () => {
46
+ it("starts a round, persists lanes, and resumes idempotently", () => {
47
+ const { workdir, runId, artifactDir } = setupRun("round-basic");
48
+ const planPath = path.join(artifactDir, "PLAN_v1.md");
49
+ fs.writeFileSync(planPath, "# plan", "utf8");
50
+
51
+ const started = startReviewRound(workdir, runId, {
52
+ roundId: "plan-r1",
53
+ role: "reviewer",
54
+ target: "plan",
55
+ reviewers: 2,
56
+ planPath,
57
+ lanes: [{ laneId: "l1" }, { laneId: "l2" }],
58
+ });
59
+ assert.equal(started.reviewRounds.length, 1);
60
+ assert.equal(started.reviewRounds[0]?.lanes.length, 2);
61
+ assert.equal(started.plan?.sha256.length, 64);
62
+
63
+ // Idempotent resume with the same shape.
64
+ const again = startReviewRound(workdir, runId, {
65
+ roundId: "plan-r1",
66
+ role: "reviewer",
67
+ target: "plan",
68
+ reviewers: 2,
69
+ planPath,
70
+ lanes: [{ laneId: "l1" }, { laneId: "l2" }],
71
+ });
72
+ assert.equal(again.revision, started.revision, "resume start does not bump revision");
73
+
74
+ // Persist two lanes.
75
+ recordLaneOutcome(workdir, runId, "plan-r1", "l1", { ok: true, output: "findings A" });
76
+ recordLaneOutcome(workdir, runId, "plan-r1", "l2", { ok: true, output: "findings B" });
77
+ const loaded = loadCheckpoint(workdir, runId);
78
+ assert.equal(loaded.status, "ok");
79
+ if (loaded.status === "ok") {
80
+ const reusable = reusableLaneOutputs(loaded.checkpoint, "plan-r1");
81
+ assert.equal(reusable.length, 2);
82
+ assert.equal(readReviewOutput(workdir, runId, reusable[0]!.resultFile), "findings A");
83
+ // Idempotent re-record with identical bytes is a no-op.
84
+ const before = loaded.checkpoint.revision;
85
+ recordLaneOutcome(workdir, runId, "plan-r1", "l1", { ok: true, output: "findings A" });
86
+ const after = loadCheckpoint(workdir, runId);
87
+ assert.equal(after.status === "ok" ? after.checkpoint.revision : -1, before + 1, "re-record bumps once");
88
+ }
89
+ });
90
+
91
+ it("refuses reuse across plan versions and shape changes", () => {
92
+ const { workdir, runId, artifactDir } = setupRun("round-guard");
93
+ const planPath = path.join(artifactDir, "PLAN_v1.md");
94
+ fs.writeFileSync(planPath, "# plan v1", "utf8");
95
+ startReviewRound(workdir, runId, {
96
+ roundId: "plan-r9",
97
+ role: "reviewer",
98
+ target: "plan",
99
+ reviewers: 1,
100
+ planPath,
101
+ lanes: [{ laneId: "l1" }],
102
+ });
103
+ // Different lanes, same id → refuse.
104
+ assert.throws(
105
+ () =>
106
+ startReviewRound(workdir, runId, {
107
+ roundId: "plan-r9",
108
+ role: "reviewer",
109
+ target: "plan",
110
+ reviewers: 1,
111
+ planPath,
112
+ lanes: [{ laneId: "other" }],
113
+ }),
114
+ /different lanes/,
115
+ );
116
+ // Plan bytes changed under the same round id → refuse.
117
+ fs.writeFileSync(planPath, "# plan v2 changed", "utf8");
118
+ assert.throws(
119
+ () =>
120
+ startReviewRound(workdir, runId, {
121
+ roundId: "plan-r9",
122
+ role: "reviewer",
123
+ target: "plan",
124
+ reviewers: 1,
125
+ planPath,
126
+ lanes: [{ laneId: "l1" }],
127
+ }),
128
+ /different plan version/,
129
+ );
130
+ });
131
+
132
+ it("failed lanes can re-run; consolidated rounds gate on terminal lanes", () => {
133
+ const { workdir, runId, artifactDir } = setupRun("round-fail");
134
+ const planPath = path.join(artifactDir, "PLAN_v1.md");
135
+ fs.writeFileSync(planPath, "# plan", "utf8");
136
+ startReviewRound(workdir, runId, {
137
+ roundId: "plan-r2",
138
+ role: "reviewer",
139
+ target: "plan",
140
+ reviewers: 2,
141
+ planPath,
142
+ lanes: [{ laneId: "l1" }, { laneId: "l2" }],
143
+ });
144
+ recordLaneOutcome(workdir, runId, "plan-r2", "l1", { ok: true, output: "ok output" });
145
+ recordLaneOutcome(workdir, runId, "plan-r2", "l2", { ok: false, error: "boom" });
146
+ // Not all terminal-success; consolidation still allowed (1 ok + 1 failed).
147
+ mutateCheckpoint(workdir, runId, (cp) => applyReviewConsolidated(cp, "plan-r2"));
148
+ const loaded = loadCheckpoint(workdir, runId);
149
+ if (loaded.status === "ok") {
150
+ assert.equal(loaded.checkpoint.reviewRounds[0]?.consolidated, true);
151
+ // Failed lane can be re-run later: applyLaneResult allows failed → complete.
152
+ const rerun = mutateCheckpoint(workdir, runId, (cp) =>
153
+ applyLaneResult(cp, "plan-r2", "l2", { ok: true, resultFile: writeReviewOutputPath(workdir, runId, "plan-r2", "l2") }),
154
+ );
155
+ assert.equal(rerun.reviewRounds[0]?.lanes.find((lane) => lane.laneId === "l2")?.status, "complete");
156
+ }
157
+ });
158
+
159
+ function writeReviewOutputPath(workdir: string, runId: string, roundId: string, laneId: string): string {
160
+ const rel = recordLaneOutcome(workdir, runId, roundId, laneId, { ok: true, output: "rerun output" });
161
+ return rel.resultFile!;
162
+ }
163
+ });
164
+
165
+ describe("implementation-review round 1 fixes", () => {
166
+ it("F-001: refine tool starts a durable round with lanes on a checkpointed run (current-session mode)", async () => {
167
+ const workdir = path.join(tmpRoot, "f001-tool");
168
+ fs.mkdirSync(workdir, { recursive: true });
169
+ spawnSync("git", ["init"], { cwd: workdir });
170
+ const { initState, startRun, setRole } = await import("../src/state.ts");
171
+ initState(workdir);
172
+ setRole(workdir, { role: "reviewer", mode: "current-session", confirmed: true });
173
+ const { run } = startRun(workdir, { topic: "f001", skill: "plan-normal", requestText: "t" });
174
+ createCheckpoint(workdir, { runId: run.run_id, originWorkdir: workdir, workdir });
175
+ fs.mkdirSync(run.artifact_dir, { recursive: true });
176
+ const planPath = path.join(run.artifact_dir, "PLAN_v1.md");
177
+ fs.writeFileSync(planPath, "# plan", "utf8");
178
+
179
+ const { registerRefineTool } = await import("../tools/refine.ts");
180
+ let tool: { execute: (id: string, params: unknown, signal: undefined, update: undefined, ctx: unknown) => Promise<{ content: Array<{ type: string; text: string }> }> } | undefined;
181
+ const pi = {
182
+ registerTool: (definition: never) => {
183
+ tool = definition as unknown as typeof tool;
184
+ },
185
+ } as never;
186
+ registerRefineTool(pi, BASE_DIR_PLACEHOLDER);
187
+ assert.ok(tool, "refine tool registered");
188
+ const ctx = {
189
+ cwd: workdir,
190
+ sessionManager: {},
191
+ model: null,
192
+ mode: "print",
193
+ hasUI: false,
194
+ ui: { notify: () => {}, setStatus: () => {}, theme: { fg: (_c: string, t: string) => t } },
195
+ };
196
+ const result = await tool!.execute("t1", { role: "reviewer", planPath, reviewers: 2 }, undefined, undefined, ctx);
197
+ assert.match(result.content[0]?.text ?? "", /current-session/);
198
+ // The durable round exists WITH lanes — the pre-fix crash site.
199
+ const loaded = loadCheckpoint(workdir, run.run_id);
200
+ assert.equal(loaded.status, "ok");
201
+ if (loaded.status === "ok") {
202
+ const round = loaded.checkpoint.reviewRounds.at(-1);
203
+ assert.ok(round, "round recorded");
204
+ assert.equal(round!.lanes.length, 2, "reviewer lanes recorded");
205
+ assert.ok(round!.lanes.every((lane) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(lane.laneId)), "lane ids sanitized");
206
+ }
207
+ });
208
+
209
+ it("F-002: in-place plan edits after approval refuse the execution load", async () => {
210
+ const workdir = path.join(tmpRoot, "f002-digest");
211
+ fs.mkdirSync(workdir, { recursive: true });
212
+ spawnSync("git", ["init"], { cwd: workdir });
213
+ spawnSync("git", ["config", "user.email", "t@e.com"], { cwd: workdir });
214
+ spawnSync("git", ["config", "user.name", "T"], { cwd: workdir });
215
+ const { initState, startRun } = await import("../src/state.ts");
216
+ initState(workdir);
217
+ const { run } = startRun(workdir, { topic: "f002", skill: "plan-normal", requestText: "t" });
218
+ createCheckpoint(workdir, { runId: run.run_id, originWorkdir: workdir, workdir });
219
+ fs.mkdirSync(run.artifact_dir, { recursive: true });
220
+ const planPath = path.join(run.artifact_dir, "PLAN_v1.md");
221
+ fs.writeFileSync(planPath, "# plan\n\n## Verifier Checklist\n\n- [ ] `VC-001` covers `I-001`; pass condition: x.\n", "utf8");
222
+ spawnSync("git", ["add", "-A"], { cwd: workdir });
223
+ spawnSync("git", ["commit", "-m", "seed"], { cwd: workdir });
224
+ const { applyExecutionApproved } = await import("../src/workflow-state.ts");
225
+ const { planIdentityOf, resolveHeadAt, resolveWorktreeRoot } = await import("../src/workflow-state.ts");
226
+ mutateCheckpoint(workdir, run.run_id, (cp) => {
227
+ const plan = planIdentityOf(planPath, 1);
228
+ return applyExecutionApproved({ ...cp, plan, nextAction: "accept-execute" }, {
229
+ plan,
230
+ worktree: resolveWorktreeRoot(workdir) ?? workdir,
231
+ headAtApproval: resolveHeadAt(workdir),
232
+ approvedAt: "2026-09-07T00:00:00Z",
233
+ });
234
+ });
235
+ // Edit the plan in place — same path, different bytes.
236
+ fs.writeFileSync(planPath, "# plan (edited)\n\n## Verifier Checklist\n\n- [ ] `VC-001` covers `I-001`; pass condition: x.\n", "utf8");
237
+ const { loadExecutionFromCheckpoint } = await import("../src/exec.ts");
238
+ const pi = { appendEntry: () => {}, sendMessage: () => {} } as never;
239
+ const ctx = { cwd: workdir, sessionManager: {}, ui: { setStatus: () => {}, theme: { fg: (_c: string, t: string) => t } } };
240
+ const load = loadExecutionFromCheckpoint(pi, ctx, run.run_id);
241
+ assert.equal(load.status, "plan-mismatch");
242
+ assert.match((load as { error?: string }).error ?? "", /changed since the approval/);
243
+ });
244
+
245
+ it("F-006: an unverifiable approval HEAD forces re-verification", async () => {
246
+ const workdir = path.join(tmpRoot, "f006-head");
247
+ fs.mkdirSync(workdir, { recursive: true });
248
+ spawnSync("git", ["init"], { cwd: workdir });
249
+ const { initState, startRun } = await import("../src/state.ts");
250
+ initState(workdir);
251
+ const { run } = startRun(workdir, { topic: "f006", skill: "plan-normal", requestText: "t" });
252
+ createCheckpoint(workdir, { runId: run.run_id, originWorkdir: workdir, workdir });
253
+ fs.mkdirSync(run.artifact_dir, { recursive: true });
254
+ const planPath = path.join(run.artifact_dir, "PLAN_v1.md");
255
+ fs.writeFileSync(planPath, "# plan\n\n## Verifier Checklist\n\n- [ ] `VC-001` covers `I-001`; pass condition: x.\n", "utf8");
256
+ const { applyExecutionApproved, applyExecutionProgress, planIdentityOf, resolveWorktreeRoot } = await import("../src/workflow-state.ts");
257
+ mutateCheckpoint(workdir, run.run_id, (cp) => {
258
+ const plan = planIdentityOf(planPath, 1);
259
+ return applyExecutionApproved({ ...cp, plan, nextAction: "accept-execute" }, {
260
+ plan,
261
+ worktree: resolveWorktreeRoot(workdir) ?? workdir,
262
+ headAtApproval: null, // approved without a resolvable HEAD
263
+ approvedAt: "2026-09-07T00:00:00Z",
264
+ });
265
+ });
266
+ mutateCheckpoint(workdir, run.run_id, (cp) => applyExecutionProgress(cp, { doneVcIds: ["VC-001"] }));
267
+ const { loadExecutionFromCheckpoint } = await import("../src/exec.ts");
268
+ const pi = { appendEntry: () => {}, sendMessage: () => {} } as never;
269
+ const ctx = { cwd: workdir, sessionManager: {}, ui: { setStatus: () => {}, theme: { fg: (_c: string, t: string) => t } } };
270
+ const load = loadExecutionFromCheckpoint(pi, ctx, run.run_id);
271
+ assert.equal(load.status, "loaded");
272
+ assert.equal((load as { reverifyAll?: boolean }).reverifyAll, true, "unverifiable HEAD re-verifies");
273
+ });
274
+
275
+ it("F-005: any mutation from a taken-over lease fails automatically", async () => {
276
+ const workdir = path.join(tmpRoot, "f005-auto");
277
+ fs.mkdirSync(workdir, { recursive: true });
278
+ spawnSync("git", ["init"], { cwd: workdir });
279
+ const { initState, startRun } = await import("../src/state.ts");
280
+ initState(workdir);
281
+ const { run } = startRun(workdir, { topic: "f005", skill: "plan-normal", requestText: "t" });
282
+ createCheckpoint(workdir, { runId: run.run_id, originWorkdir: workdir, workdir });
283
+ const { acquireOwnership, releaseOwnership } = await import("../src/run-ownership.ts");
284
+ const owner = acquireOwnership(workdir, run.run_id);
285
+ // Another process takes over (fabricated record with a reused/dead pid).
286
+ fs.writeFileSync(
287
+ path.join(workdir, ".git", "pi_plans", "runs", run.run_id, "owner.json"),
288
+ JSON.stringify({ ...owner, pid: process.pid, pidStart: "bogus-start", processToken: "someone-else", generation: owner.generation + 1 }),
289
+ "utf8",
290
+ );
291
+ assert.throws(
292
+ () => mutateCheckpoint(workdir, run.run_id, (cp) => cp), // NO explicit owner option
293
+ (error: unknown) => error instanceof Error && /no longer owns/.test(error.message),
294
+ );
295
+ releaseOwnership(workdir, run.run_id, owner.processToken); // clears the stale in-memory lease
296
+ assert.doesNotThrow(() => mutateCheckpoint(workdir, run.run_id, (cp) => cp));
297
+ });
298
+
299
+ it("F-007: migration keeps review-phase ordering; terminal phases cannot approve", async () => {
300
+ const { applyMigration, applyExecutionApproved } = await import("../src/workflow-state.ts");
301
+ const workdir = path.join(tmpRoot, "f007-phase");
302
+ fs.mkdirSync(workdir, { recursive: true });
303
+ spawnSync("git", ["init"], { cwd: workdir });
304
+ const { initState, startRun } = await import("../src/state.ts");
305
+ initState(workdir);
306
+ const { run } = startRun(workdir, { topic: "f007", skill: "plan-normal", requestText: "t" });
307
+ createCheckpoint(workdir, { runId: run.run_id, originWorkdir: workdir, workdir });
308
+ const migrated = mutateCheckpoint(workdir, run.run_id, (cp) =>
309
+ applyMigration({ ...cp, phase: "implementation-review" }, { workdir, worktreeRoot: workdir, commonDir: path.join(workdir, ".git") }),
310
+ );
311
+ assert.equal(migrated.nextAction, "run-review", "review phase keeps its ordering");
312
+ assert.throws(
313
+ () =>
314
+ applyExecutionApproved({ ...migrated, nextAction: "accept-execute" }, {
315
+ plan: { path: "/tmp/p", version: 1, sha256: "a".repeat(64) },
316
+ worktree: "/tmp",
317
+ headAtApproval: null,
318
+ approvedAt: "2026-09-07T00:00:00Z",
319
+ }),
320
+ /cannot approve execution from phase/,
321
+ );
322
+ });
323
+ });
324
+