FN-7341: replan tasks after execution mode changes
Execution-mode changes now confirm and rebuild task plans when active task state would be invalidated. - Require confirmation before changing Standard/Fast mode on todo and in-progress tasks. - Route confirmed execution-mode edits through the task spec rebuild path and close the modal with replanning feedback. - Cover inline and edit-mode execution-mode changes with dashboard tests and localized copy. Files changed: .changeset/fn-7341-execution-mode-replan.md | 7 + .../dashboard/app/components/TaskDetailModal.tsx | 70 +++++++- ...lModal.inline-editing-and-integrations.test.tsx | 181 ++++++++++++++++++--- .../__tests__/TaskDetailModal.test-helpers.ts | 1 + packages/i18n/locales/en/app.json | 3 + packages/i18n/locales/es/app.json | 5 +- packages/i18n/locales/fr/app.json | 5 +- packages/i18n/locales/ko/app.json | 5 +- packages/i18n/locales/zh-CN/app.json | 5 +- packages/i18n/locales/zh-TW/app.json | 5 +- packages/i18n/src/resources.d.ts | 3 + 11 files changed, 257 insertions(+), 33 deletions(-) Fusion-Task-Id: FN-7341 Fusion-Task-Lineage: 2b1e7ac9-81b4-438a-84d1-965da5ed84fb Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7341-execution-mode-replan.md
Normal file
7
.changeset/fn-7341-execution-mode-replan.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Replan active tasks after confirming execution-mode changes.
|
||||
category: fix
|
||||
dev: Dashboard inline execution-mode changes on todo/in-progress tasks now confirm and call the spec rebuild path.
|
||||
@@ -398,6 +398,14 @@ function normalizeExecutionModeValue(executionMode: Task["executionMode"]): "sta
|
||||
return executionMode === "fast" ? "fast" : "standard";
|
||||
}
|
||||
|
||||
function requiresExecutionModeReplan(column: Task["column"]): boolean {
|
||||
/*
|
||||
FNXC:ExecutionModeReplan 2026-06-30-00:00:
|
||||
Todo and in-progress tasks can already hold a generated plan or active execution context. Changing Standard/Fast mode invalidates that plan, so the dashboard must confirm the change and send the task back through the existing replanning path instead of silently patching executionMode in place.
|
||||
*/
|
||||
return column === "todo" || column === "in-progress";
|
||||
}
|
||||
|
||||
interface ProvenanceDisplay {
|
||||
label: string;
|
||||
parentTaskId?: string;
|
||||
@@ -896,6 +904,10 @@ export function TaskDetailContent({
|
||||
const [isSavingInlineExecutionMode, setIsSavingInlineExecutionMode] = useState(false);
|
||||
const [inlineNoCommitsExpected, setInlineNoCommitsExpected] = useState<boolean>(task.noCommitsExpected === true);
|
||||
const [isSavingInlineNoCommitsExpected, setIsSavingInlineNoCommitsExpected] = useState(false);
|
||||
const { confirm, confirmWithChoice, confirmWithCheckbox } = useConfirm();
|
||||
const requestClose = useCallback(() => {
|
||||
onRequestClose?.();
|
||||
}, [onRequestClose]);
|
||||
const mountedRef = useRef(false);
|
||||
const activeTaskIdRef = useRef(task.id);
|
||||
|
||||
@@ -1508,6 +1520,7 @@ export function TaskDetailContent({
|
||||
const [editAutoSaveStatus, setEditAutoSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
|
||||
const editAutoSaveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const editAutoSaveRevisionRef = useRef(0);
|
||||
const editSaveTriggeredReplanRef = useRef(false);
|
||||
|
||||
const buildEditUpdates = useCallback((includeDescription: boolean) => {
|
||||
const updates: Record<string, unknown> = {};
|
||||
@@ -1601,20 +1614,47 @@ export function TaskDetailContent({
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const replanAfterExecutionModeChange = Object.prototype.hasOwnProperty.call(updates, "executionMode") && requiresExecutionModeReplan(task.column);
|
||||
if (replanAfterExecutionModeChange && !includeDescription) {
|
||||
delete updates.executionMode;
|
||||
}
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (replanAfterExecutionModeChange && includeDescription) {
|
||||
const nextMode = normalizeExecutionModeValue(updates.executionMode as Task["executionMode"]);
|
||||
const shouldChangeMode = await confirm({
|
||||
title: t("taskDetail.executionMode.replanTitle", "Change execution mode and replan?"),
|
||||
message: t("taskDetail.executionMode.replanMessage", "Changing execution mode for this task will move it back to Planning so Fusion can rebuild the plan for {{mode}} mode.", { mode: nextMode }),
|
||||
});
|
||||
if (!shouldChangeMode) {
|
||||
setEditExecutionMode(normalizeExecutionModeValue(task.executionMode));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const revision = ++editAutoSaveRevisionRef.current;
|
||||
setIsSaving(true);
|
||||
setEditAutoSaveStatus("saving");
|
||||
try {
|
||||
const updatedTask = await updateTask(task.id, updates as never, projectId);
|
||||
if (revision !== editAutoSaveRevisionRef.current) return;
|
||||
if (replanAfterExecutionModeChange && includeDescription) {
|
||||
const normalizedUpdatedMode = normalizeExecutionModeValue(updatedTask.executionMode);
|
||||
await rebuildTaskSpec(task.id, projectId);
|
||||
editSaveTriggeredReplanRef.current = true;
|
||||
setEditAutoSaveStatus("saved");
|
||||
requestClose();
|
||||
addToast(t("taskDetail.executionMode.replanning", "Execution mode updated to {{mode}} — {{id}} returned to Planning for replanning", { mode: normalizedUpdatedMode, id: task.id }), "info");
|
||||
return true;
|
||||
}
|
||||
onTaskUpdated?.(updatedTask);
|
||||
setEditAutoSaveStatus("saved");
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (revision === editAutoSaveRevisionRef.current) {
|
||||
if (replanAfterExecutionModeChange) {
|
||||
setEditExecutionMode(normalizeExecutionModeValue(task.executionMode));
|
||||
}
|
||||
setEditAutoSaveStatus("error");
|
||||
addToast(t("taskDetail.updateFailed", "Failed to update {{id}}: {{error}}", { id: task.id, error: getErrorMessage(err) }), "error");
|
||||
}
|
||||
@@ -1624,15 +1664,16 @@ export function TaskDetailContent({
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
}, [addToast, buildEditUpdates, onTaskUpdated, projectId, task.id]);
|
||||
}, [addToast, buildEditUpdates, confirm, onTaskUpdated, projectId, requestClose, task.column, task.executionMode, task.id]);
|
||||
|
||||
const handleAutoSaveDescription = useCallback(async (_description: string) => {
|
||||
await persistEditChanges(true);
|
||||
}, [persistEditChanges]);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
editSaveTriggeredReplanRef.current = false;
|
||||
const didSave = await persistEditChanges(true);
|
||||
if (!didSave) {
|
||||
if (!didSave || editSaveTriggeredReplanRef.current) {
|
||||
return;
|
||||
}
|
||||
addToast(t("taskDetail.updateSuccess", "Updated {{id}}", { id: task.id }), "success");
|
||||
@@ -1710,6 +1751,18 @@ export function TaskDetailContent({
|
||||
const currentMode = normalizeExecutionModeValue(task.executionMode);
|
||||
const nextMode = currentMode === "fast" ? "standard" : "fast";
|
||||
const previousMode = inlineExecutionMode;
|
||||
const shouldReplan = requiresExecutionModeReplan(task.column);
|
||||
|
||||
if (shouldReplan) {
|
||||
const shouldChangeMode = await confirm({
|
||||
title: t("taskDetail.executionMode.replanTitle", "Change execution mode and replan?"),
|
||||
message: t("taskDetail.executionMode.replanMessage", "Changing execution mode for this task will move it back to Planning so Fusion can rebuild the plan for {{mode}} mode.", { mode: nextMode }),
|
||||
});
|
||||
if (!shouldChangeMode) {
|
||||
setInlineExecutionMode(previousMode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setInlineExecutionMode(nextMode);
|
||||
setIsSavingInlineExecutionMode(true);
|
||||
@@ -1717,6 +1770,12 @@ export function TaskDetailContent({
|
||||
try {
|
||||
const updatedTask = await updateTask(task.id, { executionMode: nextMode === "fast" ? "fast" : null }, projectId);
|
||||
const normalizedUpdatedMode = normalizeExecutionModeValue(updatedTask.executionMode);
|
||||
if (shouldReplan) {
|
||||
await rebuildTaskSpec(task.id, projectId);
|
||||
requestClose();
|
||||
addToast(t("taskDetail.executionMode.replanning", "Execution mode updated to {{mode}} — {{id}} returned to Planning for replanning", { mode: normalizedUpdatedMode, id: task.id }), "info");
|
||||
return;
|
||||
}
|
||||
setInlineExecutionMode(normalizedUpdatedMode);
|
||||
onTaskUpdated?.(updatedTask);
|
||||
addToast(t("taskDetail.executionMode.updated", "Execution mode updated to {{mode}}", { mode: normalizedUpdatedMode }), "success");
|
||||
@@ -1728,7 +1787,7 @@ export function TaskDetailContent({
|
||||
setIsSavingInlineExecutionMode(false);
|
||||
}
|
||||
}
|
||||
}, [task.id, task.executionMode, projectId, inlineExecutionMode, onTaskUpdated, addToast]);
|
||||
}, [task.id, task.column, task.executionMode, projectId, inlineExecutionMode, onTaskUpdated, addToast, confirm, requestClose]);
|
||||
|
||||
const handleInlineNoCommitsExpectedToggle = useCallback(async () => {
|
||||
const nextValue = !inlineNoCommitsExpected;
|
||||
@@ -1775,7 +1834,6 @@ export function TaskDetailContent({
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { nodes } = useNodes();
|
||||
const { confirm, confirmWithChoice, confirmWithCheckbox } = useConfirm();
|
||||
|
||||
const handleUnlinkGithubIssue = useCallback(async () => {
|
||||
if (!canEdit || !githubTrackedIssue || isSavingGithubTracking) return;
|
||||
@@ -1811,10 +1869,6 @@ export function TaskDetailContent({
|
||||
activeTab === "chat" && activitySegment === "raw-logs",
|
||||
projectId,
|
||||
);
|
||||
const requestClose = useCallback(() => {
|
||||
onRequestClose?.();
|
||||
}, [onRequestClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (embedded) return;
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
|
||||
@@ -892,6 +892,40 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("confirms and replans when edit-mode executionMode changes on a todo task", async () => {
|
||||
const { updateTask, rebuildTaskSpec } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
const mockRebuild = vi.mocked(rebuildTaskSpec);
|
||||
mockUpdate.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "todo", title: "Test", description: "Desc", executionMode: null }) as Task);
|
||||
mockRebuild.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "triage", status: "needs-replan", executionMode: null }) as Task);
|
||||
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
initialTab="definition"
|
||||
task={makeTask({ id: "FN-001", column: "todo", title: "Test", description: "Desc", executionMode: "fast" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(container.querySelector(".modal-edit-btn")!);
|
||||
fireEvent.change(screen.getByTestId("task-form-execution-mode-select"), { target: { value: "standard" } });
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfirm).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: "Change execution mode and replan?",
|
||||
}));
|
||||
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { executionMode: null }, undefined);
|
||||
expect(mockRebuild).toHaveBeenCalledWith("FN-001", undefined);
|
||||
});
|
||||
expect(mockUpdate.mock.invocationCallOrder[0]).toBeLessThan(mockRebuild.mock.invocationCallOrder[0]);
|
||||
});
|
||||
|
||||
it("omits executionMode from update payload when unchanged", async () => {
|
||||
const { updateTask } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
@@ -940,13 +974,13 @@ describe("TaskDetailModal", () => {
|
||||
const { updateTask } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
mockUpdate
|
||||
.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "todo", priority: "urgent", executionMode: "standard" }) as Task)
|
||||
.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "todo", priority: "urgent", executionMode: "fast" }) as Task);
|
||||
.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "triage", priority: "urgent", executionMode: "standard" }) as Task)
|
||||
.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "triage", priority: "urgent", executionMode: "fast" }) as Task);
|
||||
|
||||
render(
|
||||
<TaskDetailModal
|
||||
initialTab="definition"
|
||||
task={makeTask({ id: "FN-001", column: "todo", priority: "high", executionMode: "standard" })}
|
||||
task={makeTask({ id: "FN-001", column: "triage", priority: "high", executionMode: "standard" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -1080,13 +1114,14 @@ describe("TaskDetailModal", () => {
|
||||
expect(addToast).toHaveBeenCalledWith("Failed to update FN-001: Request failed", "error");
|
||||
});
|
||||
|
||||
it("toggles inline execution mode from standard to fast", async () => {
|
||||
const { updateTask } = await import("../../api");
|
||||
it("prompts before replanning a todo task when changing inline execution mode from standard to fast", async () => {
|
||||
const { updateTask, rebuildTaskSpec } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
const mockRebuild = vi.mocked(rebuildTaskSpec);
|
||||
const addToast = vi.fn();
|
||||
const onTaskUpdated = vi.fn();
|
||||
const updatedTask = makeTask({ id: "FN-001", column: "todo", executionMode: "fast" });
|
||||
mockUpdate.mockResolvedValueOnce(updatedTask as Task);
|
||||
mockUpdate.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "todo", executionMode: "fast" }) as Task);
|
||||
mockRebuild.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "triage", status: "needs-replan", executionMode: "fast" }) as Task);
|
||||
|
||||
render(
|
||||
<TaskDetailModal
|
||||
@@ -1102,23 +1137,58 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const toggle = screen.getByRole("button", { name: "Execution mode: standard" });
|
||||
fireEvent.click(toggle);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Execution mode: standard" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfirm).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: "Change execution mode and replan?",
|
||||
message: "Changing execution mode for this task will move it back to Planning so Fusion can rebuild the plan for fast mode.",
|
||||
}));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { executionMode: "fast" }, undefined);
|
||||
expect(mockRebuild).toHaveBeenCalledWith("FN-001", undefined);
|
||||
});
|
||||
expect(onTaskUpdated).toHaveBeenCalledWith(updatedTask);
|
||||
expect(addToast).toHaveBeenCalledWith("Execution mode updated to fast", "success");
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Execution mode: fast" })).toHaveAttribute("aria-pressed", "true");
|
||||
});
|
||||
expect(mockUpdate.mock.invocationCallOrder[0]).toBeLessThan(mockRebuild.mock.invocationCallOrder[0]);
|
||||
expect(onTaskUpdated).not.toHaveBeenCalled();
|
||||
expect(addToast).toHaveBeenCalledWith("Execution mode updated to fast — FN-001 returned to Planning for replanning", "info");
|
||||
});
|
||||
|
||||
it("toggles inline execution mode from fast to standard", async () => {
|
||||
const { updateTask } = await import("../../api");
|
||||
it("cancels a todo inline execution mode change before update or replan", async () => {
|
||||
const { updateTask, rebuildTaskSpec } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
const mockRebuild = vi.mocked(rebuildTaskSpec);
|
||||
mockConfirm.mockResolvedValueOnce(false);
|
||||
|
||||
render(
|
||||
<TaskDetailModal
|
||||
initialTab="definition"
|
||||
task={makeTask({ id: "FN-001", column: "todo", executionMode: "standard" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Execution mode: standard" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfirm).toHaveBeenCalled();
|
||||
});
|
||||
expect(mockUpdate).not.toHaveBeenCalled();
|
||||
expect(mockRebuild).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole("button", { name: "Execution mode: standard" })).toHaveAttribute("aria-pressed", "false");
|
||||
});
|
||||
|
||||
it("prompts and replans a todo task when changing inline execution mode from fast to standard", async () => {
|
||||
const { updateTask, rebuildTaskSpec } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
const mockRebuild = vi.mocked(rebuildTaskSpec);
|
||||
mockUpdate.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "todo", executionMode: null }) as Task);
|
||||
mockRebuild.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "triage", status: "needs-replan", executionMode: null }) as Task);
|
||||
|
||||
render(
|
||||
<TaskDetailModal
|
||||
@@ -1136,18 +1206,88 @@ describe("TaskDetailModal", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "Execution mode: fast" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockConfirm).toHaveBeenCalledWith(expect.objectContaining({
|
||||
message: "Changing execution mode for this task will move it back to Planning so Fusion can rebuild the plan for standard mode.",
|
||||
}));
|
||||
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { executionMode: null }, undefined);
|
||||
expect(mockRebuild).toHaveBeenCalledWith("FN-001", undefined);
|
||||
});
|
||||
expect(mockUpdate.mock.invocationCallOrder[0]).toBeLessThan(mockRebuild.mock.invocationCallOrder[0]);
|
||||
});
|
||||
|
||||
it("prompts and replans an in-progress task when changing inline execution mode", async () => {
|
||||
const { updateTask, rebuildTaskSpec } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
const mockRebuild = vi.mocked(rebuildTaskSpec);
|
||||
mockUpdate.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "in-progress", executionMode: "fast" }) as Task);
|
||||
mockRebuild.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "triage", status: "needs-replan", executionMode: "fast" }) as Task);
|
||||
|
||||
render(
|
||||
<TaskDetailModal
|
||||
initialTab="definition"
|
||||
task={makeTask({ id: "FN-001", column: "in-progress", executionMode: "standard" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Execution mode: standard" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Execution mode: standard" })).toHaveAttribute("aria-pressed", "false");
|
||||
expect(mockConfirm).toHaveBeenCalled();
|
||||
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { executionMode: "fast" }, undefined);
|
||||
expect(mockRebuild).toHaveBeenCalledWith("FN-001", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("reverts inline execution mode when save fails", async () => {
|
||||
const { updateTask } = await import("../../api");
|
||||
it("updates triage inline execution mode without prompting or replanning", async () => {
|
||||
const { updateTask, rebuildTaskSpec } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
const mockRebuild = vi.mocked(rebuildTaskSpec);
|
||||
const addToast = vi.fn();
|
||||
mockUpdate.mockRejectedValueOnce(new Error("Request failed"));
|
||||
const onTaskUpdated = vi.fn();
|
||||
const updatedTask = makeTask({ id: "FN-001", column: "triage", executionMode: "fast" });
|
||||
mockUpdate.mockResolvedValueOnce(updatedTask as Task);
|
||||
|
||||
render(
|
||||
<TaskDetailModal
|
||||
initialTab="definition"
|
||||
task={makeTask({ id: "FN-001", column: "triage", executionMode: "standard" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
onTaskUpdated={onTaskUpdated}
|
||||
addToast={addToast}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Execution mode: standard" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { executionMode: "fast" }, undefined);
|
||||
});
|
||||
expect(mockConfirm).not.toHaveBeenCalled();
|
||||
expect(mockRebuild).not.toHaveBeenCalled();
|
||||
expect(onTaskUpdated).toHaveBeenCalledWith(updatedTask);
|
||||
expect(addToast).toHaveBeenCalledWith("Execution mode updated to fast", "success");
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Execution mode: fast" })).toHaveAttribute("aria-pressed", "true");
|
||||
});
|
||||
});
|
||||
|
||||
it("reverts inline execution mode when active-task replan fails after update", async () => {
|
||||
const { updateTask, rebuildTaskSpec } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
const mockRebuild = vi.mocked(rebuildTaskSpec);
|
||||
const addToast = vi.fn();
|
||||
mockUpdate.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "todo", executionMode: "fast" }) as Task);
|
||||
mockRebuild.mockRejectedValueOnce(new Error("Replan failed"));
|
||||
|
||||
render(
|
||||
<TaskDetailModal
|
||||
@@ -1166,11 +1306,12 @@ describe("TaskDetailModal", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { executionMode: "fast" }, undefined);
|
||||
expect(mockRebuild).toHaveBeenCalledWith("FN-001", undefined);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Execution mode: standard" })).toHaveAttribute("aria-pressed", "false");
|
||||
});
|
||||
expect(addToast).toHaveBeenCalledWith("Failed to update FN-001: Request failed", "error");
|
||||
expect(addToast).toHaveBeenCalledWith("Failed to update FN-001: Replan failed", "error");
|
||||
});
|
||||
|
||||
it("disables inline execution mode toggle while save is in-flight", async () => {
|
||||
@@ -1183,7 +1324,7 @@ describe("TaskDetailModal", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
initialTab="definition"
|
||||
task={makeTask({ id: "FN-001", column: "todo", executionMode: "standard" })}
|
||||
task={makeTask({ id: "FN-001", column: "triage", executionMode: "standard" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
|
||||
@@ -34,6 +34,7 @@ vi.mock("../../api", async (importOriginal) => {
|
||||
fetchTaskDetail: vi.fn().mockResolvedValue(makeTask()),
|
||||
fetchAgentLogs: vi.fn().mockResolvedValue([]),
|
||||
requestSpecRevision: vi.fn().mockResolvedValue({}),
|
||||
rebuildTaskSpec: vi.fn().mockResolvedValue(makeTask({ column: "triage", status: "needs-replan" })),
|
||||
approvePlan: vi.fn().mockResolvedValue({}),
|
||||
rejectPlan: vi.fn().mockResolvedValue({}),
|
||||
duplicateTask: vi.fn().mockResolvedValue({}),
|
||||
|
||||
@@ -7306,6 +7306,9 @@
|
||||
"executionMode": {
|
||||
"ariaLabel": "Execution mode: {{mode}}",
|
||||
"fast": "Fast",
|
||||
"replanMessage": "Changing execution mode for this task will move it back to Planning so Fusion can rebuild the plan for {{mode}} mode.",
|
||||
"replanTitle": "Change execution mode and replan?",
|
||||
"replanning": "Execution mode updated to {{mode}} — {{id}} returned to Planning for replanning",
|
||||
"standard": "Standard",
|
||||
"updated": "Execution mode updated to {{mode}}"
|
||||
},
|
||||
|
||||
@@ -7297,7 +7297,10 @@
|
||||
"ariaLabel": "Modo de ejecución: {{mode}}",
|
||||
"fast": "Rápido",
|
||||
"standard": "Estándar",
|
||||
"updated": "Modo de ejecución actualizado a {{mode}}"
|
||||
"updated": "Modo de ejecución actualizado a {{mode}}",
|
||||
"replanMessage": "Cambiar el modo de ejecución de esta tarea la devolverá a Planificación para que Fusion reconstruya el plan para el modo {{mode}}.",
|
||||
"replanTitle": "¿Cambiar el modo de ejecución y replanificar?",
|
||||
"replanning": "Modo de ejecución actualizado a {{mode}} — {{id}} volvió a Planificación para replanificación"
|
||||
},
|
||||
"executionModeFast": "Rápido",
|
||||
"executionModeStandard": "Estándar",
|
||||
|
||||
@@ -7297,7 +7297,10 @@
|
||||
"ariaLabel": "Mode d'exécution : {{mode}}",
|
||||
"fast": "Rapide",
|
||||
"standard": "Standard",
|
||||
"updated": "Mode d'exécution mis à jour : {{mode}}"
|
||||
"updated": "Mode d'exécution mis à jour : {{mode}}",
|
||||
"replanMessage": "Changer le mode d'exécution de cette tâche la renverra en planification afin que Fusion reconstruise le plan pour le mode {{mode}}.",
|
||||
"replanTitle": "Changer le mode d'exécution et replanifier ?",
|
||||
"replanning": "Mode d'exécution mis à jour vers {{mode}} — {{id}} est revenue en planification pour replanification"
|
||||
},
|
||||
"executionModeFast": "Rapide",
|
||||
"executionModeStandard": "Standard",
|
||||
|
||||
@@ -7297,7 +7297,10 @@
|
||||
"ariaLabel": "실행 모드: {{mode}}",
|
||||
"fast": "빠름",
|
||||
"standard": "표준",
|
||||
"updated": "실행 모드가 {{mode}}로 업데이트되었습니다"
|
||||
"updated": "실행 모드가 {{mode}}로 업데이트되었습니다",
|
||||
"replanMessage": "이 작업의 실행 모드를 변경하면 Fusion이 {{mode}} 모드용 계획을 다시 만들 수 있도록 작업이 계획으로 돌아갑니다.",
|
||||
"replanTitle": "실행 모드를 변경하고 다시 계획할까요?",
|
||||
"replanning": "실행 모드가 {{mode}}로 업데이트되었습니다 — {{id}}가 재계획을 위해 계획으로 돌아갔습니다"
|
||||
},
|
||||
"executionModeFast": "빠름",
|
||||
"executionModeStandard": "표준",
|
||||
|
||||
@@ -7297,7 +7297,10 @@
|
||||
"ariaLabel": "执行模式:{{mode}}",
|
||||
"fast": "快速",
|
||||
"standard": "标准",
|
||||
"updated": "执行模式已更新为 {{mode}}"
|
||||
"updated": "执行模式已更新为 {{mode}}",
|
||||
"replanMessage": "更改此任务的执行模式会将其移回规划,以便 Fusion 可以为 {{mode}} 模式重新生成计划。",
|
||||
"replanTitle": "更改执行模式并重新规划?",
|
||||
"replanning": "执行模式已更新为 {{mode}} — {{id}} 已返回规划以重新规划"
|
||||
},
|
||||
"executionModeFast": "快速",
|
||||
"executionModeStandard": "标准",
|
||||
|
||||
@@ -7297,7 +7297,10 @@
|
||||
"ariaLabel": "執行模式:{{mode}}",
|
||||
"fast": "快速",
|
||||
"standard": "標準",
|
||||
"updated": "執行模式已更新為 {{mode}}"
|
||||
"updated": "執行模式已更新為 {{mode}}",
|
||||
"replanMessage": "變更此任務的執行模式會將其移回規劃,讓 Fusion 可以為 {{mode}} 模式重新產生計畫。",
|
||||
"replanTitle": "變更執行模式並重新規劃?",
|
||||
"replanning": "執行模式已更新為 {{mode}} — {{id}} 已返回規劃以重新規劃"
|
||||
},
|
||||
"executionModeFast": "快速",
|
||||
"executionModeStandard": "標準",
|
||||
|
||||
3
packages/i18n/src/resources.d.ts
vendored
3
packages/i18n/src/resources.d.ts
vendored
@@ -7345,6 +7345,9 @@ export default interface Resources {
|
||||
"executionMode": {
|
||||
"ariaLabel": "Execution mode: {{mode}}",
|
||||
"fast": "Fast",
|
||||
"replanMessage": "Changing execution mode for this task will move it back to Planning so Fusion can rebuild the plan for {{mode}} mode.",
|
||||
"replanTitle": "Change execution mode and replan?",
|
||||
"replanning": "Execution mode updated to {{mode}} — {{id}} returned to Planning for replanning",
|
||||
"standard": "Standard",
|
||||
"updated": "Execution mode updated to {{mode}}"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user