fix(pi-workflow): preserve recovery gate ownership

This commit is contained in:
jb
2026-08-25 19:42:45 -04:00
parent e8bae028d7
commit 805b8e00d4
4 changed files with 172 additions and 5 deletions
+141
View File
@@ -1508,6 +1508,147 @@ test("Git or ownership safety failures open Recovery before remediation", async
});
});
test("Recovery retry reruns failed verification after the remediation limit", async () => {
await withTempDir(async (directory) => {
const setup = await fixture(directory);
let commandRuns = 0;
const hostCommands: HostCommandExecutor = {
async run() {
commandRuns++;
return {
result: {
code: commandRuns === 1 ? 1 : 0,
stdout: commandRuns === 1 ? "failed" : "passed",
stderr: "",
},
};
},
};
const effectiveConfig = {
...structuredClone(DEFAULT_WORKFLOW_CONFIG),
maxReviewCycles: 1,
};
setup.executor.enqueue("discover", {
...discoveryWithQuestions(),
questions: [],
});
setup.executor.enqueue("design", design("minor"));
setup.executor.enqueue("implement", {
summary: "Initial implementation",
changedPaths: ["src/index.ts"],
focusedTests: ["initial test"],
});
setup.executor.enqueue("verify", {
succeeded: true,
focusedBehavioralTests: true,
summary: "Corrected implementation verified",
});
setup.executor.enqueue("review", {
specification: { verdict: "approved", findings: [] },
standards: { verdict: "approved", findings: [] },
});
const planned = await setup.engine("session-1", {
hostCommands,
effectiveConfig,
}).execute(startCommand());
const approved = await setup.engine("session-1", {
hostCommands,
effectiveConfig,
}).execute({
action: "decide",
runId: planned.runId,
gateId: planned.openGate?.id ?? "missing",
expectedRevision: planned.revision,
decision: {
kind: "approve-plan",
approved: true,
acceptedUiImpact: "minor",
},
idempotencyKey: "approve-failed-verification-retry",
});
const waiting = await setup.engine("session-1", {
hostCommands,
effectiveConfig,
}).execute({
action: "decide",
runId: approved.runId,
gateId: approved.openGate?.id ?? "missing",
expectedRevision: approved.revision,
decision: {
kind: "select-implementation-context",
strategy: "current-session",
},
idempotencyKey: "context-failed-verification-retry",
});
assert.equal(waiting.openGate?.kind, "recovery");
assert.equal(commandRuns, 1);
const transientRecovery = setup.engine("transient-session", {
hostCommands,
effectiveConfig,
processId: 200,
isProcessAlive: () => false,
});
assert.deepEqual(await transientRecovery.recover(), []);
const untouched = await setup.store.loadRun(waiting.runId);
assert.equal(untouched.revision, waiting.revision);
assert.equal(untouched.lease?.activeParentSessionId, "session-1");
const liveStolen = structuredClone(untouched);
liveStolen.revision++;
liveStolen.parentSessionIds.push("transient-session");
liveStolen.lease = {
processId: 200,
activeParentSessionId: "transient-session",
heartbeatAt: 0,
worktreeIdentity: liveStolen.workspace.root,
};
await setup.store.saveRun(liveStolen, untouched.revision);
await assert.rejects(
setup.engine("session-1", {
hostCommands,
effectiveConfig,
processId: 300,
isProcessAlive: (processId) => processId === 200,
}).execute({
action: "decide",
runId: waiting.runId,
gateId: waiting.openGate?.id ?? "missing",
expectedRevision: liveStolen.revision,
decision: { kind: "recover", resolution: "retry" },
idempotencyKey: "live-owner-retry-failed-verification",
}),
/Historical parent sessions/,
);
const staleStolen = structuredClone(liveStolen);
staleStolen.revision++;
staleStolen.lease!.processId = 201;
await setup.store.saveRun(staleStolen, liveStolen.revision);
const completed = await setup.engine("session-1", {
hostCommands,
effectiveConfig,
processId: 300,
isProcessAlive: () => false,
}).execute({
action: "decide",
runId: waiting.runId,
gateId: waiting.openGate?.id ?? "missing",
expectedRevision: staleStolen.revision,
decision: { kind: "recover", resolution: "retry" },
idempotencyKey: "retry-failed-verification",
});
assert.equal(completed.status, "completed");
assert.equal(commandRuns, 2);
const persisted = await setup.store.loadRun(completed.runId);
assert.equal(persisted.verificationEvidence?.commands[0]?.status, "succeeded");
});
});
test("changes requested trigger serialized remediation, verification, and fresh review", async () => {
await withTempDir(async (directory) => {
const setup = await fixture(directory);