feat(FN-5347): reset manual retry counters across task retry surfaces
- Add a shared manual retry reset helper in core and export it for consumers - Wire retry counter resets into CLI task retry, extension task retry, and dashboard task workflow routes - Align retry reset behavior with task typing updates and remove superseded task-helper reset paths - Add focused core/CLI/extension/dashboard tests plus docs and a changeset describing retry reset behavior
This commit is contained in:
committed by
gsxdsm
parent
a0aa6c8c5b
commit
216c32bfe5
@@ -27,7 +27,7 @@ vi.mock("../commands/task.js", () => ({
|
||||
}));
|
||||
|
||||
import kbExtension from "../extension.js";
|
||||
import { TaskStore, AgentStore, RESEARCH_RUN_STATUSES } from "@fusion/core";
|
||||
import { TaskStore, AgentStore, MANUAL_RETRY_RESET_COUNTER_KEYS, RESEARCH_RUN_STATUSES } from "@fusion/core";
|
||||
import { isGhAvailable, isGhAuthenticated, runGhJsonAsync } from "@fusion/core/gh-cli";
|
||||
import { runTaskPlan } from "../commands/task.js";
|
||||
|
||||
@@ -2331,6 +2331,20 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
||||
});
|
||||
|
||||
describe("fn_task_retry", () => {
|
||||
const nonZeroRetryCounters = Object.fromEntries(
|
||||
MANUAL_RETRY_RESET_COUNTER_KEYS.map((key, index) => [key, index + 1]),
|
||||
);
|
||||
|
||||
const expectRetryCountersReset = (task: Awaited<ReturnType<TaskStore["getTask"]>>) => {
|
||||
expect(task).toBeTruthy();
|
||||
if (!task) return;
|
||||
for (const key of MANUAL_RETRY_RESET_COUNTER_KEYS) {
|
||||
expect(task[key] ?? 0).toBe(0);
|
||||
}
|
||||
expect(task.nextRecoveryAt ?? null).toBeNull();
|
||||
expect(task.retrySummary?.total ?? 0).toBe(0);
|
||||
};
|
||||
|
||||
it("moves execution-failed in-review task (incomplete steps) to todo preserving progress", async () => {
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
@@ -2352,9 +2366,9 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
||||
await store.updateTask(task.id, {
|
||||
status: "failed",
|
||||
error: "429 rate limited",
|
||||
taskDoneRetryCount: 2,
|
||||
workflowStepRetries: 3,
|
||||
stuckKillCount: 4,
|
||||
mergeRetries: 9,
|
||||
nextRecoveryAt: new Date(Date.now() + 60_000).toISOString(),
|
||||
...nonZeroRetryCounters,
|
||||
});
|
||||
|
||||
const retryTool = api.tools.get("fn_task_retry")!;
|
||||
@@ -2368,9 +2382,8 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
||||
expect(updated?.status).toBeFalsy();
|
||||
expect(updated?.error).toBeFalsy();
|
||||
expect(updated?.steps[1].status).toBe("in-progress");
|
||||
expect(updated?.taskDoneRetryCount).toBe(0);
|
||||
expect(updated?.workflowStepRetries).toBe(0);
|
||||
expect(updated?.stuckKillCount).toBe(0);
|
||||
expectRetryCountersReset(updated);
|
||||
expect(updated?.mergeRetries).toBe(9);
|
||||
});
|
||||
|
||||
it("moves zero-step execution-failed in-review task to todo and clears failure state", async () => {
|
||||
@@ -2423,9 +2436,8 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
||||
status: "failed",
|
||||
error: "merge conflict",
|
||||
mergeRetries: 3,
|
||||
taskDoneRetryCount: 5,
|
||||
workflowStepRetries: 4,
|
||||
stuckKillCount: 7,
|
||||
nextRecoveryAt: new Date(Date.now() + 60_000).toISOString(),
|
||||
...nonZeroRetryCounters,
|
||||
});
|
||||
|
||||
const retryTool = api.tools.get("fn_task_retry")!;
|
||||
@@ -2438,10 +2450,8 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
||||
expect(updated?.column).toBe("in-review");
|
||||
expect(updated?.status).toBeFalsy();
|
||||
expect(updated?.error).toBeFalsy();
|
||||
expectRetryCountersReset(updated);
|
||||
expect(updated?.mergeRetries).toBe(0);
|
||||
expect(updated?.taskDoneRetryCount).toBe(0);
|
||||
expect(updated?.workflowStepRetries).toBe(0);
|
||||
expect(updated?.stuckKillCount).toBe(0);
|
||||
});
|
||||
|
||||
it("keeps zero-step merge-failed in-review task with prior merge attempts in-review and resets merge state", async () => {
|
||||
@@ -2457,7 +2467,14 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
||||
await store.updateTask(task.id, { steps: [] });
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
await store.moveTask(task.id, "in-review");
|
||||
await store.updateTask(task.id, { status: "failed", error: "merge conflict", mergeRetries: 2, steps: [] });
|
||||
await store.updateTask(task.id, {
|
||||
status: "failed",
|
||||
error: "merge conflict",
|
||||
mergeRetries: 2,
|
||||
steps: [],
|
||||
nextRecoveryAt: new Date(Date.now() + 60_000).toISOString(),
|
||||
...nonZeroRetryCounters,
|
||||
});
|
||||
|
||||
const retryTool = api.tools.get("fn_task_retry")!;
|
||||
const result = await retryTool.execute("retry-zero-step-merge", { id: task.id }, undefined, undefined, makeCtx(tmpDir));
|
||||
@@ -2470,6 +2487,39 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
||||
expect(updated?.status).toBeFalsy();
|
||||
expect(updated?.error).toBeFalsy();
|
||||
expect(updated?.steps).toEqual([]);
|
||||
expectRetryCountersReset(updated);
|
||||
expect(updated?.mergeRetries).toBe(0);
|
||||
});
|
||||
|
||||
it("moves non-review failed task to todo and resets all retry counters", async () => {
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
|
||||
const task = await store.createTask({
|
||||
title: "failed in-progress task",
|
||||
description: "test",
|
||||
column: "todo",
|
||||
});
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
await store.updateTask(task.id, {
|
||||
status: "failed",
|
||||
error: "verification failed",
|
||||
mergeRetries: 8,
|
||||
nextRecoveryAt: new Date(Date.now() + 60_000).toISOString(),
|
||||
...nonZeroRetryCounters,
|
||||
});
|
||||
|
||||
const retryTool = api.tools.get("fn_task_retry")!;
|
||||
const result = await retryTool.execute("retry-generic", { id: task.id }, undefined, undefined, makeCtx(tmpDir));
|
||||
|
||||
expect(result.isError).toBeFalsy();
|
||||
expect(result.details.newColumn).toBe("todo");
|
||||
|
||||
const updated = await store.getTask(task.id);
|
||||
expect(updated?.column).toBe("todo");
|
||||
expect(updated?.status).toBeFalsy();
|
||||
expect(updated?.error).toBeFalsy();
|
||||
expectRetryCountersReset(updated);
|
||||
expect(updated?.mergeRetries).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2413,11 +2413,21 @@ describe("runTaskRetry", () => {
|
||||
branch: null,
|
||||
baseBranch: null,
|
||||
baseCommitSha: null,
|
||||
recoveryRetryCount: null,
|
||||
nextRecoveryAt: null,
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
stuckKillCount: 0,
|
||||
recoveryRetryCount: 0,
|
||||
taskDoneRetryCount: 0,
|
||||
worktreeSessionRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
verificationFailureCount: 0,
|
||||
postReviewFixCount: 0,
|
||||
mergeConflictBounceCount: 0,
|
||||
branchConflictRecoveryCount: 0,
|
||||
reviewerContextRetryCount: 0,
|
||||
reviewerFallbackRetryCount: 0,
|
||||
completionHandoffLimboRecoveryCount: 0,
|
||||
mergeAuditBounceCount: 0,
|
||||
mergeRetries: 0,
|
||||
});
|
||||
expect(mockMoveTask).toHaveBeenCalledWith("FN-001", "todo");
|
||||
expect(mockLogEntry).toHaveBeenCalledWith("FN-001", "Retry requested from CLI", "Task reset to todo for retry");
|
||||
@@ -2476,11 +2486,21 @@ describe("runTaskRetry", () => {
|
||||
branch: null,
|
||||
baseBranch: null,
|
||||
baseCommitSha: null,
|
||||
recoveryRetryCount: null,
|
||||
nextRecoveryAt: null,
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
stuckKillCount: 0,
|
||||
recoveryRetryCount: 0,
|
||||
taskDoneRetryCount: 0,
|
||||
worktreeSessionRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
verificationFailureCount: 0,
|
||||
postReviewFixCount: 0,
|
||||
mergeConflictBounceCount: 0,
|
||||
branchConflictRecoveryCount: 0,
|
||||
reviewerContextRetryCount: 0,
|
||||
reviewerFallbackRetryCount: 0,
|
||||
completionHandoffLimboRecoveryCount: 0,
|
||||
mergeAuditBounceCount: 0,
|
||||
mergeRetries: 0,
|
||||
});
|
||||
expect(mockMoveTask).toHaveBeenCalledWith("FN-001", "todo");
|
||||
expect(mockLogEntry).toHaveBeenCalledWith("FN-001", "Retry requested from CLI", "Task reset to todo for retry");
|
||||
|
||||
@@ -1009,9 +1009,7 @@ export async function runTaskRetry(id: string, projectName?: string) {
|
||||
branch: null,
|
||||
baseBranch: null,
|
||||
baseCommitSha: null,
|
||||
recoveryRetryCount: null,
|
||||
nextRecoveryAt: null,
|
||||
...buildManualRetryResetPatch(),
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
|
||||
// Move to todo column
|
||||
|
||||
@@ -1016,8 +1016,7 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
await store.updateTask(params.id, {
|
||||
status: null,
|
||||
error: null,
|
||||
...buildManualRetryResetPatch(),
|
||||
mergeRetries: 0,
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
await store.logEntry(params.id, "Retry requested via Fusion extension (in-review merge retry, mergeRetries reset)");
|
||||
return {
|
||||
@@ -1030,7 +1029,7 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
await store.updateTask(params.id, {
|
||||
status: null,
|
||||
error: null,
|
||||
...buildManualRetryResetPatch(),
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
|
||||
// Move to todo column
|
||||
|
||||
Reference in New Issue
Block a user