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:
committed by
gsxdsm
parent
5a584640d0
commit
3221e57b09
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user