feat(FN-5485): clear userPaused state on manual retry reset with regression
Adds retry-reset logic that clears the user-paused flag on tasks, ensuring they can resume automatically after a retry is triggered, with regression tests covering the behavior across the CLI extension and core manual-reset module. Fusion-Task-Id: FN-5485
This commit is contained in:
committed by
gsxdsm
parent
2d661df870
commit
4b484fd818
@@ -2522,6 +2522,31 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
|
|||||||
expectRetryCountersReset(updated);
|
expectRetryCountersReset(updated);
|
||||||
expect(updated?.mergeRetries).toBe(0);
|
expect(updated?.mergeRetries).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears userPaused when retrying a manually paused failed task", async () => {
|
||||||
|
const store = new TaskStore(tmpDir);
|
||||||
|
await store.init();
|
||||||
|
|
||||||
|
const task = await store.createTask({
|
||||||
|
title: "manually paused failed task",
|
||||||
|
description: "test",
|
||||||
|
column: "todo",
|
||||||
|
});
|
||||||
|
await store.updateTask(task.id, {
|
||||||
|
status: "failed",
|
||||||
|
error: "verification failed",
|
||||||
|
userPaused: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryTool = api.tools.get("fn_task_retry")!;
|
||||||
|
const result = await retryTool.execute("retry-user-paused", { id: task.id }, undefined, undefined, makeCtx(tmpDir));
|
||||||
|
|
||||||
|
expect(result.isError).toBeFalsy();
|
||||||
|
const updated = await store.getTask(task.id);
|
||||||
|
expect(updated?.column).toBe("todo");
|
||||||
|
expect(updated?.status).toBeFalsy();
|
||||||
|
expect(updated?.userPaused).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("fn_list_agents", () => {
|
describe("fn_list_agents", () => {
|
||||||
|
|||||||
@@ -35,4 +35,14 @@ describe("buildManualRetryResetPatch", () => {
|
|||||||
it("clears nextRecoveryAt", () => {
|
it("clears nextRecoveryAt", () => {
|
||||||
expect(buildManualRetryResetPatch()).toMatchObject({ nextRecoveryAt: null });
|
expect(buildManualRetryResetPatch()).toMatchObject({ nextRecoveryAt: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears userPaused for manual retry in all modes", () => {
|
||||||
|
const defaultPatch = buildManualRetryResetPatch();
|
||||||
|
expect(Object.prototype.hasOwnProperty.call(defaultPatch, "userPaused")).toBe(true);
|
||||||
|
expect(defaultPatch.userPaused).toBeUndefined();
|
||||||
|
|
||||||
|
const mergePatch = buildManualRetryResetPatch({ resetMergeRetries: true });
|
||||||
|
expect(Object.prototype.hasOwnProperty.call(mergePatch, "userPaused")).toBe(true);
|
||||||
|
expect(mergePatch.userPaused).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ export const MANUAL_RETRY_RESET_COUNTER_KEYS = [
|
|||||||
"mergeAuditBounceCount",
|
"mergeAuditBounceCount",
|
||||||
] as const satisfies ReadonlyArray<keyof Task>;
|
] as const satisfies ReadonlyArray<keyof Task>;
|
||||||
|
|
||||||
|
/** Resets retry/recovery counters and clears `userPaused` for explicit manual retries. */
|
||||||
export function buildManualRetryResetPatch(options?: { resetMergeRetries?: boolean }): Partial<Task> {
|
export function buildManualRetryResetPatch(options?: { resetMergeRetries?: boolean }): Partial<Task> {
|
||||||
const patch: Partial<Task> = {
|
const patch: Partial<Task> = {
|
||||||
nextRecoveryAt: null as unknown as Task["nextRecoveryAt"],
|
nextRecoveryAt: null as unknown as Task["nextRecoveryAt"],
|
||||||
|
userPaused: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const key of MANUAL_RETRY_RESET_COUNTER_KEYS) {
|
for (const key of MANUAL_RETRY_RESET_COUNTER_KEYS) {
|
||||||
|
|||||||
@@ -2,52 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
<script>
|
|
||||||
// Append ?vpdebug to the URL to show a fixed overlay with the live
|
|
||||||
// viewport / element dimensions. Diagnostic only — remove once the
|
|
||||||
// Android tablet cut-off issue is understood.
|
|
||||||
(function () {
|
|
||||||
if (!/[?&]vpdebug\b/.test(location.search)) return;
|
|
||||||
var box = document.createElement('div');
|
|
||||||
box.style.cssText =
|
|
||||||
'position:fixed;top:0;right:0;z-index:2147483647;background:#000;color:#0f0;font:11px/1.3 monospace;padding:6px 8px;border:1px solid #0f0;white-space:pre;pointer-events:none;max-width:60vw;';
|
|
||||||
var update = function () {
|
|
||||||
var f = function (el) {
|
|
||||||
if (!el) return 'none';
|
|
||||||
var r = el.getBoundingClientRect();
|
|
||||||
return Math.round(r.width) + 'x' + Math.round(r.height) + ' @' + Math.round(r.left) + ',' + Math.round(r.top);
|
|
||||||
};
|
|
||||||
var vv = window.visualViewport;
|
|
||||||
box.textContent =
|
|
||||||
'win ' + window.innerWidth + 'x' + window.innerHeight + '\n' +
|
|
||||||
'vv ' + (vv ? Math.round(vv.width) + 'x' + Math.round(vv.height) + ' s' + vv.scale.toFixed(2) + ' o' + Math.round(vv.offsetLeft) + ',' + Math.round(vv.offsetTop) : 'n/a') + '\n' +
|
|
||||||
'dpr ' + window.devicePixelRatio + '\n' +
|
|
||||||
'html ' + f(document.documentElement) + '\n' +
|
|
||||||
'body ' + f(document.body) + '\n' +
|
|
||||||
'root ' + f(document.getElementById('root')) + '\n' +
|
|
||||||
'board ' + f(document.getElementById('board')) + '\n' +
|
|
||||||
'sx ' + window.scrollX + ' sy ' + window.scrollY + '\n' +
|
|
||||||
'ua ' + (navigator.userAgent || '').slice(0, 80);
|
|
||||||
};
|
|
||||||
var attach = function () {
|
|
||||||
if (!document.body) {
|
|
||||||
requestAnimationFrame(attach);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.body.appendChild(box);
|
|
||||||
update();
|
|
||||||
setInterval(update, 500);
|
|
||||||
window.addEventListener('resize', update);
|
|
||||||
window.addEventListener('orientationchange', update);
|
|
||||||
if (window.visualViewport) {
|
|
||||||
window.visualViewport.addEventListener('resize', update);
|
|
||||||
window.visualViewport.addEventListener('scroll', update);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
attach();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
<title>Fusion</title>
|
<title>Fusion</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
|||||||
@@ -382,6 +382,24 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
|
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears userPaused when retrying a failed todo task", async () => {
|
||||||
|
const failedTaskInTodo = { ...FAKE_TASK_DETAIL, column: "todo", status: "failed", userPaused: true };
|
||||||
|
const movedTask = { ...failedTaskInTodo, status: undefined, userPaused: undefined };
|
||||||
|
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(failedTaskInTodo);
|
||||||
|
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(failedTaskInTodo);
|
||||||
|
(store.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||||
|
|
||||||
|
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const updateCall = (store.updateTask as ReturnType<typeof vi.fn>).mock.calls[0][1];
|
||||||
|
expect(Object.prototype.hasOwnProperty.call(updateCall, "userPaused")).toBe(true);
|
||||||
|
expect(updateCall.userPaused).toBeUndefined();
|
||||||
|
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo");
|
||||||
|
});
|
||||||
|
|
||||||
it("retries a stuck-killed task and moves it to todo", async () => {
|
it("retries a stuck-killed task and moves it to todo", async () => {
|
||||||
const stuckTask = { ...FAKE_TASK_DETAIL, status: "stuck-killed", column: "in-progress" };
|
const stuckTask = { ...FAKE_TASK_DETAIL, status: "stuck-killed", column: "in-progress" };
|
||||||
const movedTask = { ...FAKE_TASK_DETAIL, column: "todo", status: undefined };
|
const movedTask = { ...FAKE_TASK_DETAIL, column: "todo", status: undefined };
|
||||||
@@ -564,6 +582,33 @@ describe("POST /tasks/:id/retry", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears userPaused for merge-retry in-review tasks", async () => {
|
||||||
|
const mergeFailedTask = {
|
||||||
|
...FAKE_TASK_DETAIL,
|
||||||
|
column: "in-review" as const,
|
||||||
|
status: "failed",
|
||||||
|
userPaused: true,
|
||||||
|
steps: [
|
||||||
|
{ name: "Step 0", status: "done" },
|
||||||
|
{ name: "Step 1", status: "done" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
(store.getTask as ReturnType<typeof vi.fn>)
|
||||||
|
.mockResolvedValueOnce(mergeFailedTask)
|
||||||
|
.mockResolvedValueOnce({ ...mergeFailedTask, userPaused: undefined });
|
||||||
|
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue({ ...mergeFailedTask, userPaused: undefined });
|
||||||
|
|
||||||
|
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const updateCall = (store.updateTask as ReturnType<typeof vi.fn>).mock.calls[0][1];
|
||||||
|
expect(Object.prototype.hasOwnProperty.call(updateCall, "userPaused")).toBe(true);
|
||||||
|
expect(updateCall.userPaused).toBeUndefined();
|
||||||
|
expect(store.moveTask).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("retries zero-step merge-failed in-review task with prior merge attempts by staying in-review", async () => {
|
it("retries zero-step merge-failed in-review task with prior merge attempts by staying in-review", async () => {
|
||||||
const mergeFailedTask = {
|
const mergeFailedTask = {
|
||||||
...FAKE_TASK_DETAIL,
|
...FAKE_TASK_DETAIL,
|
||||||
|
|||||||
Reference in New Issue
Block a user