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);
|
||||
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", () => {
|
||||
|
||||
@@ -35,4 +35,14 @@ describe("buildManualRetryResetPatch", () => {
|
||||
it("clears nextRecoveryAt", () => {
|
||||
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",
|
||||
] 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> {
|
||||
const patch: Partial<Task> = {
|
||||
nextRecoveryAt: null as unknown as Task["nextRecoveryAt"],
|
||||
userPaused: undefined,
|
||||
};
|
||||
|
||||
for (const key of MANUAL_RETRY_RESET_COUNTER_KEYS) {
|
||||
|
||||
@@ -2,52 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, 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>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>Fusion</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
@@ -382,6 +382,24 @@ describe("POST /tasks/:id/retry", () => {
|
||||
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 () => {
|
||||
const stuckTask = { ...FAKE_TASK_DETAIL, status: "stuck-killed", column: "in-progress" };
|
||||
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 () => {
|
||||
const mergeFailedTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
|
||||
Reference in New Issue
Block a user