fix: harden workflow execution recovery

This commit is contained in:
liujing
2026-07-17 15:56:58 +08:00
parent 90551414b1
commit fb39ab289a
14 changed files with 355 additions and 30 deletions
+94
View File
@@ -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);
});
});
// ---------------------------------------------------------------------------