feat(FN-4855): complete Step 2-3 — harden disable route and modal state

Fusion-Task-Id: FN-4855
Fusion-Task-Lineage: 47f093b7-dffc-4c1a-8dee-2fe01d0c9ed6
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 01:00:21 -07:00
committed by gsxdsm
parent 5a584640d0
commit 3221e57b09
4 changed files with 146 additions and 1 deletions

View File

@@ -930,6 +930,9 @@ export function TaskDetailContent({
enabled: nextEnabled,
},
}, projectId);
setFullDetail((prev) => prev
? ({ ...prev, ...updatedTask, githubTracking: updatedTask.githubTracking } as TaskDetail)
: (updatedTask as TaskDetail));
onTaskUpdated?.(updatedTask);
} catch (err) {
setGithubTrackingEnabledDraft(workingTask.githubTracking?.enabled === true);
@@ -953,6 +956,9 @@ export function TaskDetailContent({
repoOverride: githubRepoOverrideTrimmed.length > 0 ? githubRepoOverrideTrimmed : null,
},
}, projectId);
setFullDetail((prev) => prev
? ({ ...prev, ...updatedTask, githubTracking: updatedTask.githubTracking } as TaskDetail)
: (updatedTask as TaskDetail));
onTaskUpdated?.(updatedTask);
} catch (err) {
addToast(`Failed to update ${task.id}: ${getErrorMessage(err)}`, "error");
@@ -974,6 +980,9 @@ export function TaskDetailContent({
enabled: true,
},
}, projectId);
setFullDetail((prev) => prev
? ({ ...prev, ...updatedTask, githubTracking: updatedTask.githubTracking } as TaskDetail)
: (updatedTask as TaskDetail));
onTaskUpdated?.(updatedTask);
addToast("Requested GitHub tracking issue creation", "info");
} catch (err) {

View File

@@ -2,7 +2,7 @@ import { describe, it, expect, vi } from "vitest";
import { useState } from "react";
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { Task } from "@fusion/core";
import type { Task, TaskDetail } from "@fusion/core";
import {
makeTask,
noop,
@@ -2697,6 +2697,80 @@ describe("TaskDetailModal", () => {
});
});
it("keeps disabled githubTracking state sticky across follow-up sparse task prop updates", async () => {
const { updateTask, fetchTaskDetail } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);
const mockFetchTaskDetail = vi.mocked(fetchTaskDetail);
const baseTask = makeTask({
id: "FN-001",
title: "Tracking task",
column: "in-progress",
githubTracking: {
enabled: true,
repoOverride: "runfusion/fusion",
issue: {
owner: "runfusion",
repo: "fusion",
number: 200,
url: "https://github.com/runfusion/fusion/issues/200",
createdAt: "2026-01-01T00:00:00Z",
},
},
});
mockFetchTaskDetail.mockResolvedValue(baseTask);
mockUpdate.mockResolvedValueOnce({
...baseTask,
githubTracking: {
enabled: false,
repoOverride: "runfusion/fusion",
},
} as Task);
function Harness(): JSX.Element {
const [taskState, setTaskState] = useState(baseTask);
return (
<TaskDetailModal
task={taskState}
onClose={noop}
onOpenDetail={noopOpenDetail}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onTaskUpdated={(nextTask) => {
setTaskState(nextTask as TaskDetail);
setTimeout(() => {
setTaskState((current) => ({
...current,
githubTracking: undefined,
}));
}, 0);
}}
addToast={noop}
/>
);
}
render(<Harness />);
expandGithubTracking();
const toggle = screen.getByRole("checkbox", { name: "Enable GitHub tracking" }) as HTMLInputElement;
expect(toggle.checked).toBe(true);
fireEvent.click(toggle);
await waitFor(() => {
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { githubTracking: { enabled: false } }, undefined);
});
await waitFor(() => {
expect((screen.getByRole("checkbox", { name: "Enable GitHub tracking" }) as HTMLInputElement).checked).toBe(false);
});
expect(screen.queryByRole("button", { name: /create tracking issue/i })).not.toBeInTheDocument();
});
it("sends repo override updates and null when cleared", async () => {
const { updateTask } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);

View File

@@ -245,6 +245,7 @@ const TASK_TOKEN_USAGE_FIXTURE = {
const FAKE_TASK_DETAIL: TaskDetail = {
id: "FN-001",
title: "Test task",
description: "Test task",
column: "in-progress",
dependencies: [],
@@ -2043,6 +2044,61 @@ describe("PATCH /tasks/:id", () => {
}
});
it("PATCH disable unlinks and persists disabled githubTracking without recreating issue", async () => {
const rootDir = mkdtempSync(join(tmpdir(), "kb-routes-disable-github-tracking-"));
const globalDir = mkdtempSync(join(tmpdir(), "kb-routes-disable-github-tracking-global-"));
const realStore = new CoreTaskStore(rootDir, globalDir, { inMemoryDb: true });
await realStore.init();
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockResolvedValue({
owner: "runfusion",
repo: "fusion",
number: 99,
htmlUrl: "https://github.com/runfusion/fusion/issues/99",
createdAt: "2026-01-01T00:00:00.000Z",
});
try {
const created = await realStore.createTask({ description: "route disable flow", column: "todo" });
await realStore.updateGithubTracking(created.id, {
enabled: true,
repoOverride: "runfusion/fusion",
issue: {
owner: "runfusion",
repo: "fusion",
number: 12,
url: "https://github.com/runfusion/fusion/issues/12",
createdAt: "2026-01-01T00:00:00.000Z",
},
});
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(realStore));
const patchRes = await REQUEST(app, "PATCH", `/api/tasks/${created.id}`, JSON.stringify({
githubTracking: { enabled: false },
}), {
"Content-Type": "application/json",
});
expect(patchRes.status).toBe(200);
expect(patchRes.body.githubTracking?.enabled).toBe(false);
expect(patchRes.body.githubTracking?.issue).toBeUndefined();
expect(createIssueSpy).not.toHaveBeenCalled();
const getRes = await REQUEST(app, "GET", `/api/tasks/${created.id}`);
expect(getRes.status).toBe(200);
expect(getRes.body.githubTracking?.enabled).toBe(false);
expect(getRes.body.githubTracking?.issue).toBeUndefined();
} finally {
createIssueSpy.mockRestore();
realStore.close();
rmSync(rootDir, { recursive: true, force: true });
rmSync(globalDir, { recursive: true, force: true });
}
});
it("does not recreate tracking issue during explicit manual unlink patch", async () => {
const createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue").mockResolvedValue({
owner: "runfusion",

View File

@@ -2040,8 +2040,14 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
validatedGithubTracking !== null &&
typeof validatedGithubTracking === "object" &&
validatedGithubTracking.issue === null;
const disableRequested =
hasBodyField("githubTracking") &&
validatedGithubTracking !== null &&
typeof validatedGithubTracking === "object" &&
validatedGithubTracking.enabled === false;
const shouldAttemptTrackingIssueCreate =
!manualUnlinkRequested &&
!disableRequested &&
task.githubTracking?.enabled === true &&
!task.githubTracking?.issue;