feat(FN-5436): skip step retries when review is pending
Added executor logic to skip retries when a review is pending for a task, introducing a `pendingReviewBlockHelper` in the task-done path and updating the retry-gate to consult it; two new reliability-interaction test suites cover the feature behavior and composition with existing retry/backstop laye Fusion-Task-Id: FN-5436
This commit is contained in:
committed by
gsxdsm
parent
7d25d98b2f
commit
a2a643844d
@@ -0,0 +1,134 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import "../executor-test-helpers.js";
|
||||
import { TaskExecutor } from "../../executor.js";
|
||||
import { createFnAgent } from "../../pi.js";
|
||||
import { createMockStore, resetExecutorMocks } from "../executor-test-helpers.js";
|
||||
|
||||
const mockedCreateFnAgent = vi.mocked(createFnAgent);
|
||||
|
||||
function makeTask(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "FN-5436-RI",
|
||||
title: "Pending review skip",
|
||||
description: "",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
taskDoneRetryCount: 0,
|
||||
steps: [{ name: "Step 1", status: "in-progress" as const }],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
prompt: "# test\n## Steps\n### Step 1: Step 1\n- [ ] do work",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
} as any;
|
||||
}
|
||||
|
||||
describe("reliability interactions: FN-5436 executor pending-review skip", () => {
|
||||
beforeEach(() => {
|
||||
resetExecutorMocks();
|
||||
mockedCreateFnAgent.mockResolvedValue({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
subscribe: vi.fn(),
|
||||
on: vi.fn(),
|
||||
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
|
||||
state: {},
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
|
||||
it("FN-5436 composition: implicit-done wins when no in-progress step exists despite stale review logs", async () => {
|
||||
const store = createMockStore();
|
||||
const task = makeTask({
|
||||
id: "FN-5436-RI-A",
|
||||
steps: [{ name: "Step 1", status: "done" }],
|
||||
log: [{ action: "code review Step 1: REVISE", timestamp: new Date().toISOString() }],
|
||||
});
|
||||
store.getTask.mockResolvedValue(task);
|
||||
|
||||
const executor = new TaskExecutor(store as any, "/repo");
|
||||
await executor.execute(task);
|
||||
|
||||
expect(store.updateTask).not.toHaveBeenCalledWith("FN-5436-RI-A", {
|
||||
status: "failed",
|
||||
error: "executor-exit-while-review-pending",
|
||||
});
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-5436-RI-A", "in-review");
|
||||
});
|
||||
|
||||
it("FN-5436 composition: reclaim-abort path takes precedence over pending-review skip", async () => {
|
||||
const store = createMockStore();
|
||||
const task = makeTask({ id: "FN-5436-RI-B", paused: true });
|
||||
store.getTask.mockResolvedValue(task);
|
||||
|
||||
const executor = new TaskExecutor(store as any, "/repo");
|
||||
await executor.execute(task);
|
||||
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-5436-RI-B", "todo", { preserveProgress: true });
|
||||
expect(store.updateTask).not.toHaveBeenCalledWith("FN-5436-RI-B", {
|
||||
status: "failed",
|
||||
error: "executor-exit-while-review-pending",
|
||||
});
|
||||
});
|
||||
|
||||
it("FN-5436 composition: pending-review park does not consume taskDone requeue budget", async () => {
|
||||
const store = createMockStore();
|
||||
const task = makeTask({
|
||||
id: "FN-5436-RI-C",
|
||||
taskDoneRetryCount: 2,
|
||||
log: [{ action: "code review requested for Step 1 (Step 1)", timestamp: new Date().toISOString() }],
|
||||
});
|
||||
store.getTask.mockResolvedValue(task);
|
||||
|
||||
const executor = new TaskExecutor(store as any, "/repo");
|
||||
await executor.execute(task);
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-5436-RI-C", {
|
||||
status: "failed",
|
||||
error: "executor-exit-while-review-pending",
|
||||
});
|
||||
expect(store.updateTask).not.toHaveBeenCalledWith("FN-5436-RI-C", expect.objectContaining({ taskDoneRetryCount: 3 }));
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-5436-RI-C", "in-review");
|
||||
});
|
||||
|
||||
it("FN-5436 composition: recoverApprovedStepsOnResume leaves pending-review skip disabled after approval resolves step", async () => {
|
||||
const store = createMockStore();
|
||||
const task = makeTask({
|
||||
id: "FN-5436-RI-D",
|
||||
steps: [{ name: "Step 1", status: "done" }],
|
||||
log: [{ action: "code review Step 1: APPROVE", timestamp: new Date().toISOString() }],
|
||||
});
|
||||
store.getTask.mockResolvedValue(task);
|
||||
|
||||
const executor = new TaskExecutor(store as any, "/repo");
|
||||
await executor.execute(task);
|
||||
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-5436-RI-D", { workflowStepRetries: undefined, taskDoneRetryCount: null });
|
||||
expect(store.updateTask).not.toHaveBeenCalledWith("FN-5436-RI-D", {
|
||||
status: "failed",
|
||||
error: "executor-exit-while-review-pending",
|
||||
});
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-5436-RI-D", "in-review");
|
||||
});
|
||||
|
||||
it("FN-5436 negative: plan-review UNAVAILABLE advisory remains non-blocking", async () => {
|
||||
const store = createMockStore();
|
||||
const task = makeTask({
|
||||
id: "FN-5436-RI-E",
|
||||
log: [{ action: "plan review Step 1: UNAVAILABLE — proceeding advisory after fallback retry exhausted", timestamp: new Date().toISOString() }],
|
||||
});
|
||||
store.getTask.mockResolvedValue(task);
|
||||
|
||||
const executor = new TaskExecutor(store as any, "/repo");
|
||||
await executor.execute(task);
|
||||
|
||||
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(4);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-5436-RI-E", {
|
||||
status: "failed",
|
||||
error: "Agent finished without calling fn_task_done (after 3 retries)",
|
||||
taskDoneRetryCount: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user