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
5
.changeset/fn-5347-manual-retry-counter-reset.md
Normal file
5
.changeset/fn-5347-manual-retry-counter-reset.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Manual task retry now resets the full persisted retry-budget counter set (and `nextRecoveryAt`) across CLI, pi extension, and dashboard retry surfaces, so retry badges/details no longer stay inflated after a user-triggered fresh attempt.
|
||||
@@ -232,6 +232,7 @@ Fusion task columns:
|
||||
4. **in-review** — implementation complete; awaiting finalization
|
||||
- If merge/finalization hits a terminal error, tasks can remain in `in-review` with `status: "failed"` for explicit follow-up. This state is intentionally preserved by recovery (not auto-bounced to `todo`).
|
||||
- Retry behavior splits by execution-vs-merge signals: `in-review` tasks with incomplete steps (`pending`/`in-progress`) are treated as execution failures and retried back to `todo` with `preserveProgress: true`; zero-step `in-review` tasks use `mergeRetries` as the tie-breaker (`mergeRetries === 0` or undefined → execution failure path back to `todo`, `mergeRetries > 0` → merge/finalization retry in `in-review` with merge retry state reset); tasks whose steps are all terminal (`done`/`skipped`/`failed`) also stay on the merge/finalization retry path.
|
||||
- Manual retry now clears the full persisted retry-budget counter set (`stuckKillCount`, `recoveryRetryCount`, `taskDoneRetryCount`, `worktreeSessionRetryCount`, `workflowStepRetries`, `verificationFailureCount`, `postReviewFixCount`, `mergeConflictBounceCount`, `branchConflictRecoveryCount`, `reviewerContextRetryCount`, `reviewerFallbackRetryCount`, `completionHandoffLimboRecoveryCount`, `mergeAuditBounceCount`) plus `nextRecoveryAt`; merge retry counters clear only on merge-failure/generic retry paths. `retrySummary` is recomputed from persisted counters at read time, so manual retry resets the dashboard retry badge/details back to zero immediately.
|
||||
- Persisted executor session state is resumed only when it still matches the task's current worktree context. If a retry fails with `Refusing to start coding agent in missing worktree: ...` and the persisted session points at stale worktree metadata, recovery clears stale session pointers and retries fresh so review retries do not reopen deleted worktree paths.
|
||||
- Merge-confirmed tasks still respect `getTaskMergeBlocker()` before the final `in-review` → `done` move. If merge is confirmed but a blocker remains (for example, incomplete steps), Fusion parks the task in `in-review` with `status: "failed"` and an explicit blocker error instead of retry-looping auto-finalization.
|
||||
- Self-healing can still auto-finalize retry-exhausted failed review tasks when it can prove their branch content already landed on the merge target, so already-merged work does not deadlock in `in-review`.
|
||||
|
||||
@@ -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
|
||||
|
||||
38
packages/core/src/__tests__/manual-retry-reset.test.ts
Normal file
38
packages/core/src/__tests__/manual-retry-reset.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildManualRetryResetPatch, MANUAL_RETRY_RESET_COUNTER_KEYS } from "../manual-retry-reset.js";
|
||||
|
||||
const RETRY_SUMMARY_COUNTER_REGEX = /toCount\(task\.(\w+)\)/g;
|
||||
|
||||
describe("buildManualRetryResetPatch", () => {
|
||||
it("resets all manual retry counters to zero", () => {
|
||||
const patch = buildManualRetryResetPatch();
|
||||
|
||||
for (const key of MANUAL_RETRY_RESET_COUNTER_KEYS) {
|
||||
expect(patch[key]).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("includes all retry-summary counters in the reset key list", () => {
|
||||
const retrySummarySource = readFileSync(new URL("../retry-summary.ts", import.meta.url), "utf-8");
|
||||
const retrySummaryKeys = new Set<string>();
|
||||
let match: RegExpExecArray | null = RETRY_SUMMARY_COUNTER_REGEX.exec(retrySummarySource);
|
||||
while (match) {
|
||||
retrySummaryKeys.add(match[1]);
|
||||
match = RETRY_SUMMARY_COUNTER_REGEX.exec(retrySummarySource);
|
||||
}
|
||||
|
||||
for (const key of retrySummaryKeys) {
|
||||
expect(MANUAL_RETRY_RESET_COUNTER_KEYS).toContain(key);
|
||||
}
|
||||
});
|
||||
|
||||
it("sets mergeRetries only when requested", () => {
|
||||
expect(buildManualRetryResetPatch()).not.toHaveProperty("mergeRetries");
|
||||
expect(buildManualRetryResetPatch({ resetMergeRetries: true })).toMatchObject({ mergeRetries: 0 });
|
||||
});
|
||||
|
||||
it("clears nextRecoveryAt", () => {
|
||||
expect(buildManualRetryResetPatch()).toMatchObject({ nextRecoveryAt: null });
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildManualRetryResetPatch, getPrimaryPrInfo } from "../task-helpers.js";
|
||||
import { getPrimaryPrInfo } from "../task-helpers.js";
|
||||
|
||||
describe("getPrimaryPrInfo", () => {
|
||||
it("returns prInfo when only legacy field is set", () => {
|
||||
const prInfo = { number: 1 } as any;
|
||||
@@ -23,12 +24,3 @@ describe("getPrimaryPrInfo", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildManualRetryResetPatch", () => {
|
||||
it("resets only manual retry counters", () => {
|
||||
expect(buildManualRetryResetPatch()).toEqual({
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
stuckKillCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,7 +201,8 @@ export {
|
||||
hasTitleIdDrift,
|
||||
normalizeTitleForTaskId,
|
||||
} from "./task-title-id-drift.js";
|
||||
export { getPrimaryPrInfo, buildManualRetryResetPatch } from "./task-helpers.js";
|
||||
export { getPrimaryPrInfo } from "./task-helpers.js";
|
||||
export { MANUAL_RETRY_RESET_COUNTER_KEYS, buildManualRetryResetPatch } from "./manual-retry-reset.js";
|
||||
export type {
|
||||
TaskIdIntegrityAnomaly,
|
||||
TaskIdIntegrityAnomalyKind,
|
||||
|
||||
33
packages/core/src/manual-retry-reset.ts
Normal file
33
packages/core/src/manual-retry-reset.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Task } from "./types.js";
|
||||
|
||||
export const MANUAL_RETRY_RESET_COUNTER_KEYS = [
|
||||
"stuckKillCount",
|
||||
"recoveryRetryCount",
|
||||
"taskDoneRetryCount",
|
||||
"worktreeSessionRetryCount",
|
||||
"workflowStepRetries",
|
||||
"verificationFailureCount",
|
||||
"postReviewFixCount",
|
||||
"mergeConflictBounceCount",
|
||||
"branchConflictRecoveryCount",
|
||||
"reviewerContextRetryCount",
|
||||
"reviewerFallbackRetryCount",
|
||||
"completionHandoffLimboRecoveryCount",
|
||||
"mergeAuditBounceCount",
|
||||
] as const satisfies ReadonlyArray<keyof Task>;
|
||||
|
||||
export function buildManualRetryResetPatch(options?: { resetMergeRetries?: boolean }): Partial<Task> {
|
||||
const patch: Partial<Task> = {
|
||||
nextRecoveryAt: null as unknown as Task["nextRecoveryAt"],
|
||||
};
|
||||
|
||||
for (const key of MANUAL_RETRY_RESET_COUNTER_KEYS) {
|
||||
patch[key] = 0;
|
||||
}
|
||||
|
||||
if (options?.resetMergeRetries) {
|
||||
patch.mergeRetries = 0;
|
||||
}
|
||||
|
||||
return patch;
|
||||
}
|
||||
@@ -3,11 +3,3 @@ import type { PrInfo, Task } from "./types.js";
|
||||
export function getPrimaryPrInfo(task: Pick<Task, "prInfo" | "prInfos">): PrInfo | undefined {
|
||||
return task.prInfos?.[0] ?? task.prInfo;
|
||||
}
|
||||
|
||||
export function buildManualRetryResetPatch(): Pick<Task, "taskDoneRetryCount" | "workflowStepRetries" | "stuckKillCount"> {
|
||||
return {
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
stuckKillCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ vi.mock("@fusion/engine", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
import { AgentStore, Database, RoutineStore, TaskStore as CoreTaskStore, isGhAvailable, isGhAuthenticated } from "@fusion/core";
|
||||
import { AgentStore, Database, RoutineStore, TaskStore as CoreTaskStore, buildManualRetryResetPatch, isGhAvailable, isGhAuthenticated } from "@fusion/core";
|
||||
import { createFnAgent } from "@fusion/engine";
|
||||
|
||||
const mockIsGhAvailable = vi.mocked(isGhAvailable);
|
||||
@@ -341,11 +341,7 @@ describe("POST /tasks/:id/retry", () => {
|
||||
branch: null,
|
||||
baseBranch: null,
|
||||
baseCommitSha: null,
|
||||
stuckKillCount: 0,
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
recoveryRetryCount: null,
|
||||
nextRecoveryAt: null,
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
|
||||
});
|
||||
@@ -381,11 +377,7 @@ describe("POST /tasks/:id/retry", () => {
|
||||
branch: null,
|
||||
baseBranch: null,
|
||||
baseCommitSha: null,
|
||||
stuckKillCount: 0,
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
recoveryRetryCount: null,
|
||||
nextRecoveryAt: null,
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
|
||||
});
|
||||
@@ -409,11 +401,7 @@ describe("POST /tasks/:id/retry", () => {
|
||||
branch: null,
|
||||
baseBranch: null,
|
||||
baseCommitSha: null,
|
||||
stuckKillCount: 0,
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
recoveryRetryCount: null,
|
||||
nextRecoveryAt: null,
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
|
||||
expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Retry requested from dashboard (stuck kill budget reset)");
|
||||
@@ -440,9 +428,7 @@ describe("POST /tasks/:id/retry", () => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
||||
status: null,
|
||||
error: null,
|
||||
stuckKillCount: 0,
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
...buildManualRetryResetPatch(),
|
||||
});
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true });
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
@@ -473,9 +459,7 @@ describe("POST /tasks/:id/retry", () => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
||||
status: null,
|
||||
error: null,
|
||||
stuckKillCount: 0,
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
...buildManualRetryResetPatch(),
|
||||
});
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true });
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
@@ -510,8 +494,8 @@ describe("POST /tasks/:id/retry", () => {
|
||||
expect(updateCall).not.toHaveProperty("branch");
|
||||
expect(updateCall).not.toHaveProperty("baseBranch");
|
||||
expect(updateCall).not.toHaveProperty("baseCommitSha");
|
||||
expect(updateCall).not.toHaveProperty("recoveryRetryCount");
|
||||
expect(updateCall).not.toHaveProperty("nextRecoveryAt");
|
||||
expect(updateCall.recoveryRetryCount).toBe(0);
|
||||
expect(updateCall.nextRecoveryAt).toBeNull();
|
||||
});
|
||||
|
||||
it("retries execution-failed in-review task by moving to todo with progress preserved", async () => {
|
||||
@@ -538,9 +522,7 @@ describe("POST /tasks/:id/retry", () => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
||||
status: null,
|
||||
error: null,
|
||||
stuckKillCount: 0,
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
...buildManualRetryResetPatch(),
|
||||
});
|
||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true });
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
@@ -573,10 +555,7 @@ describe("POST /tasks/:id/retry", () => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
||||
status: null,
|
||||
error: null,
|
||||
stuckKillCount: 0,
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
mergeRetries: 0,
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
@@ -606,10 +585,7 @@ describe("POST /tasks/:id/retry", () => {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
|
||||
status: null,
|
||||
error: null,
|
||||
stuckKillCount: 0,
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
mergeRetries: 0,
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
@@ -688,11 +664,7 @@ describe("POST /tasks/:id/retry", () => {
|
||||
branch: null,
|
||||
baseBranch: null,
|
||||
baseCommitSha: null,
|
||||
stuckKillCount: 0,
|
||||
taskDoneRetryCount: 0,
|
||||
workflowStepRetries: 0,
|
||||
recoveryRetryCount: null,
|
||||
nextRecoveryAt: null,
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
|
||||
|
||||
@@ -1059,8 +1059,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
await scopedStore.updateTask(req.params.id, {
|
||||
status: null,
|
||||
error: null,
|
||||
...buildManualRetryResetPatch(),
|
||||
mergeRetries: 0,
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
await scopedStore.logEntry(req.params.id, "Retry requested from dashboard (in-review merge retry, mergeRetries reset)");
|
||||
const updated = await scopedStore.getTask(req.params.id);
|
||||
@@ -1075,9 +1074,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
branch: null,
|
||||
baseBranch: null,
|
||||
baseCommitSha: null,
|
||||
recoveryRetryCount: null,
|
||||
nextRecoveryAt: null,
|
||||
...buildManualRetryResetPatch(),
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
|
||||
if (retrySpecification) {
|
||||
|
||||
Reference in New Issue
Block a user