fix: harden workflow execution recovery
This commit is contained in:
@@ -102,6 +102,7 @@ describe("Retry-execute recovery", () => {
|
||||
let repoRoot: string;
|
||||
let stateRoot: string;
|
||||
let originalAgentDir: string | undefined;
|
||||
let originalHome: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-workflow-retry-test-"));
|
||||
@@ -116,11 +117,15 @@ describe("Retry-execute recovery", () => {
|
||||
execFileSync("git", ["commit", "-m", "init"], { cwd: repoRoot });
|
||||
originalAgentDir = process.env.AGENT_WORKFLOW_DIR;
|
||||
process.env.AGENT_WORKFLOW_DIR = stateRoot;
|
||||
originalHome = process.env.HOME;
|
||||
process.env.HOME = tmpDir;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalAgentDir === undefined) delete process.env.AGENT_WORKFLOW_DIR;
|
||||
else process.env.AGENT_WORKFLOW_DIR = originalAgentDir;
|
||||
if (originalHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = originalHome;
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -261,6 +266,95 @@ echo '{"session_id":"fake-session-123","conversation_id":"fake-conv-456"}'
|
||||
expect(events.find((event) => event.kind === "executor_retried")?.data.recoveryMode)
|
||||
.toBe("initial_continuation");
|
||||
});
|
||||
|
||||
it("rebinds a validated Claude session before retrying the failed cycle", async () => {
|
||||
const { retryExecuteWorkflow } = await import("./workflow-engine.js");
|
||||
const { gitHead, initWorkflowState, readState, writeState } = await import("./workflow-state.js");
|
||||
const runId = "claude-rebind-123";
|
||||
const sessionId = "22778b47-e4f3-4645-84cd-1c6a74a912d8";
|
||||
const dir = runDir(repoRoot, runId);
|
||||
const claudeProject = path.join(tmpDir, ".claude", "projects", "test-project");
|
||||
fs.mkdirSync(claudeProject, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(claudeProject, `${sessionId}.jsonl`),
|
||||
`${JSON.stringify({ sessionId, cwd: repoRoot, message: { content: "# Implementation Task: Test" } })}\n`,
|
||||
);
|
||||
|
||||
const fakeClaude = path.join(tmpDir, "fake-claude");
|
||||
fs.writeFileSync(fakeClaude, `#!/bin/bash
|
||||
printf '{"type":"system","subtype":"init","session_id":"${sessionId}"}\\n'
|
||||
printf '{"type":"result","result":"done","session_id":"${sessionId}"}\\n'
|
||||
`, { mode: 0o755 });
|
||||
|
||||
const state = initWorkflowState({
|
||||
runId,
|
||||
repoRoot,
|
||||
baselineHead: gitHead(repoRoot),
|
||||
plan: plan(),
|
||||
executor: "claude",
|
||||
maxCycles: 3,
|
||||
});
|
||||
state.status = "blocked";
|
||||
state.stopDescription = "Executor failed to resume: missing session";
|
||||
state.currentCycle = 1;
|
||||
state.cycles = [{
|
||||
cycleIndex: 1,
|
||||
startedAt: new Date().toISOString(),
|
||||
executorLogFile: "cycle-1-exec.log",
|
||||
executorAttemptLogs: ["cycle-1-exec.log"],
|
||||
}];
|
||||
state.sessionHandle = { kind: "claude", sessionId: "97a606e8-cfa3-4996-83a8-ddb7ece4234b" };
|
||||
state.executorConfig = { binary: fakeClaude };
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "cycle-1-exec.log"), "initial attempt failed");
|
||||
writeState(dir, state);
|
||||
|
||||
await retryExecuteWorkflow({ runId, sessionId });
|
||||
|
||||
const recovered = readState(dir)!;
|
||||
expect(recovered.status).toBe("awaiting_review");
|
||||
expect(recovered.sessionHandle).toEqual({ kind: "claude", sessionId });
|
||||
const events = fs.readFileSync(path.join(dir, "events.jsonl"), "utf8")
|
||||
.trim().split("\n").map((line) => JSON.parse(line));
|
||||
expect(events.find((event) => event.kind === "executor_session_rebound")?.data).toEqual({
|
||||
previousSessionId: "97a606e8-cfa3-4996-83a8-ddb7ece4234b",
|
||||
sessionId,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a Claude session from another repository without changing state", async () => {
|
||||
const { retryExecuteWorkflow } = await import("./workflow-engine.js");
|
||||
const { gitHead, initWorkflowState, readState, writeState } = await import("./workflow-state.js");
|
||||
const runId = "claude-rebind-reject";
|
||||
const sessionId = "22778b47-e4f3-4645-84cd-1c6a74a912d8";
|
||||
const dir = runDir(repoRoot, runId);
|
||||
const claudeProject = path.join(tmpDir, ".claude", "projects", "other-project");
|
||||
fs.mkdirSync(claudeProject, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(claudeProject, `${sessionId}.jsonl`),
|
||||
`${JSON.stringify({ sessionId, cwd: path.join(tmpDir, "other"), message: { content: "Test" } })}\n`,
|
||||
);
|
||||
|
||||
const state = initWorkflowState({
|
||||
runId,
|
||||
repoRoot,
|
||||
baselineHead: gitHead(repoRoot),
|
||||
plan: plan(),
|
||||
executor: "claude",
|
||||
maxCycles: 3,
|
||||
});
|
||||
state.status = "blocked";
|
||||
state.stopDescription = "Executor failed to resume: missing session";
|
||||
state.currentCycle = 1;
|
||||
state.cycles = [{ cycleIndex: 1, startedAt: new Date().toISOString() }];
|
||||
state.sessionHandle = { kind: "claude", sessionId: "97a606e8-cfa3-4996-83a8-ddb7ece4234b" };
|
||||
writeState(dir, state);
|
||||
|
||||
await expect(retryExecuteWorkflow({ runId, sessionId }))
|
||||
.rejects.toThrow(/does not belong to repository/);
|
||||
expect(readState(dir)?.sessionHandle).toEqual(state.sessionHandle);
|
||||
expect(fs.existsSync(path.join(dir, "events.jsonl"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user