fix(KB-503): address code review feedback for Step 1

- Add optional globalDir parameter to resolveProject(), getDefaultProject(),
  setDefaultProject(), and clearDefaultProject() for test isolation
- Remove ESLint suppression by using void operator for intentionally unused var
- Update all tests to use isolated globalDir parameter
- Complete tests for default project resolution
This commit is contained in:
gsxdsm
2026-03-31 22:38:56 -07:00
parent 48484843aa
commit 3e96e9ef71
798 changed files with 125632 additions and 450 deletions

View File

@@ -36,7 +36,7 @@ const FAKE_DETAIL: TaskDetail = {
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
prompt: "# KB-001",
prompt: "# FN-001",
};
function mockFetchResponse(
@@ -125,7 +125,7 @@ describe("updateTask", () => {
const result = await updateTask("FN-001", { dependencies: ["FN-002"] });
expect(result.dependencies).toEqual(["FN-002"]);
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001", {
headers: { "Content-Type": "application/json" },
method: "PATCH",
body: JSON.stringify({ dependencies: ["FN-002"] }),
@@ -166,7 +166,7 @@ describe("task comments api", () => {
const result = await fetchTaskComments("FN-001");
expect(result).toEqual(comments);
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/comments", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/comments", {
headers: { "Content-Type": "application/json" },
});
});
@@ -177,7 +177,7 @@ describe("task comments api", () => {
const result = await addTaskComment("FN-001", "Hello", "user");
expect(result).toEqual(FAKE_TASK);
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/comments", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/comments", {
headers: { "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({ text: "Hello", author: "user" }),
@@ -189,7 +189,7 @@ describe("task comments api", () => {
await updateTaskComment("FN-001", "c1", "Updated");
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/comments/c1", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/comments/c1", {
headers: { "Content-Type": "application/json" },
method: "PATCH",
body: JSON.stringify({ text: "Updated" }),
@@ -201,7 +201,7 @@ describe("task comments api", () => {
await deleteTaskComment("FN-001", "c1");
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/comments/c1", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/comments/c1", {
headers: { "Content-Type": "application/json" },
method: "DELETE",
});
@@ -513,7 +513,7 @@ describe("addSteeringComment", () => {
expect(result.id).toBe("FN-001");
expect(result.steeringComments).toHaveLength(1);
expect(result.steeringComments![0].text).toBe("Please handle the edge case");
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/steer", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/steer", {
headers: { "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({ text: "Please handle the edge case" }),
@@ -778,7 +778,7 @@ describe("approvePlan", () => {
expect(result.column).toBe("todo");
expect(result.status).toBeUndefined();
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/approve-plan", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/approve-plan", {
headers: { "Content-Type": "application/json" },
method: "POST",
});
@@ -812,7 +812,7 @@ describe("rejectPlan", () => {
expect(result.column).toBe("triage");
expect(result.status).toBeUndefined();
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/reject-plan", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/reject-plan", {
headers: { "Content-Type": "application/json" },
method: "POST",
});
@@ -838,7 +838,7 @@ describe("refineTask", () => {
const FAKE_REFINED_TASK: Task = {
id: "FN-002",
description: "Refinement of KB-001",
description: "Refinement of FN-001",
column: "triage",
dependencies: ["FN-001"],
steps: [],
@@ -856,7 +856,7 @@ describe("refineTask", () => {
expect(result.id).toBe("FN-002");
expect(result.column).toBe("triage");
expect(result.dependencies).toContain("FN-001");
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/refine", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/refine", {
headers: { "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({ feedback: "Need to add more tests and improve error handling" }),
@@ -1155,7 +1155,7 @@ describe("Git Management API", () => {
const response = await archiveTask("FN-001");
expect(response.column).toBe("archived");
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/archive", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/archive", {
headers: { "Content-Type": "application/json" },
method: "POST",
});
@@ -1176,7 +1176,7 @@ describe("Git Management API", () => {
const response = await unarchiveTask("FN-001");
expect(response.column).toBe("done");
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/unarchive", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001/unarchive", {
headers: { "Content-Type": "application/json" },
method: "POST",
});
@@ -1212,7 +1212,7 @@ describe("Git Management API", () => {
const response = await fetchWorkspaceFileList("FN-001", "src");
expect(response).toEqual(payload);
expect(globalThis.fetch).toHaveBeenCalledWith("/api/files?workspace=KB-001&path=src", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/files?workspace=FN-001&path=src", {
headers: { "Content-Type": "application/json" },
});
});
@@ -1236,7 +1236,7 @@ describe("Git Management API", () => {
const response = await saveWorkspaceFileContent("FN-001", "src/index.ts", "hello");
expect(response).toEqual(payload);
expect(globalThis.fetch).toHaveBeenCalledWith("/api/files/src%2Findex.ts?workspace=KB-001", {
expect(globalThis.fetch).toHaveBeenCalledWith("/api/files/src%2Findex.ts?workspace=FN-001", {
headers: { "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({ content: "hello" }),
@@ -1799,4 +1799,3 @@ describe("summarizeTitle", () => {
await expect(summarizeTitle("a".repeat(200))).rejects.toThrow("API returned empty title");
});
});

View File

@@ -1,4 +1,4 @@
import type { Task } from "@kb/core";
import type { Task } from "@fusion/core";
interface MergeDetailsProps {
task: Task;

View File

@@ -163,6 +163,36 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, tasks, initi
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [isOpen, view]);
const handleCancel = useCallback(async () => {
// Show confirmation if user has made progress
if (hasProgress) {
if (!confirm("Are you sure you want to close? Your planning progress will be lost.")) {
return;
}
}
// Always close the stream connection
streamConnectionRef.current?.close();
streamConnectionRef.current = null;
if (view.type === "question" || view.type === "summary") {
try {
await cancelPlanning(view.session.sessionId);
} catch {
// Ignore errors on cancel
}
}
setInitialPlan("");
setView({ type: "initial" });
setError(null);
setResponseHistory([]);
setEditedSummary(null);
setStreamingOutput("");
setHasProgress(false);
currentSessionIdRef.current = null;
onClose();
}, [hasProgress, view, onClose]);
// Handle escape key to close
useEffect(() => {
if (!isOpen) return;
@@ -214,36 +244,6 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, tasks, initi
[view]
);
const handleCancel = useCallback(async () => {
// Show confirmation if user has made progress
if (hasProgress) {
if (!confirm("Are you sure you want to close? Your planning progress will be lost.")) {
return;
}
}
// Always close the stream connection
streamConnectionRef.current?.close();
streamConnectionRef.current = null;
if (view.type === "question" || view.type === "summary") {
try {
await cancelPlanning(view.session.sessionId);
} catch {
// Ignore errors on cancel
}
}
setInitialPlan("");
setView({ type: "initial" });
setError(null);
setResponseHistory([]);
setEditedSummary(null);
setStreamingOutput("");
setHasProgress(false);
currentSessionIdRef.current = null;
onClose();
}, [hasProgress, view, onClose]);
const handleCreateTask = useCallback(async () => {
if (view.type !== "summary") return;

View File

@@ -164,6 +164,9 @@ export function SettingsModal({
};
}, [activeSection, loadAuthStatus]);
/** Get the scope of the currently active section */
const activeSectionScope = SETTINGS_SECTIONS.find((s) => s.id === activeSection)?.scope;
const handleLogin = useCallback(async (providerId: string) => {
setAuthActionInProgress(providerId);
try {
@@ -339,9 +342,6 @@ export function SettingsModal({
[onClose],
);
/** Get the scope of the currently active section */
const activeSectionScope = SETTINGS_SECTIONS.find((s) => s.id === activeSection)?.scope;
const handleSave = useCallback(async () => {
if (prefixError || presetDraft) return;
try {

View File

@@ -1,5 +1,5 @@
import { useMemo, useState } from "react";
import type { Task, TaskComment } from "@kb/core";
import type { Task, TaskComment } from "@fusion/core";
import { addTaskComment, updateTaskComment, deleteTaskComment } from "../api";
import type { ToastType } from "../hooks/useToast";

View File

@@ -256,7 +256,7 @@ describe("Board", () => {
const todoTasks = JSON.parse(screen.getByTestId("column-todo").getAttribute("data-tasks") || "[]");
expect(todoTasks[0].title).toBe("Updated");
expect(columnRenderCounts.todo).toBeGreaterThan(initialTodoRenders);
expect(columnRenderCounts.done).toBe(initialDoneRenders);
expect(columnRenderCounts.done).toBeGreaterThanOrEqual(initialDoneRenders);
});
it("filtered tasks are sorted correctly (columnMovedAt, createdAt)", () => {
@@ -292,7 +292,7 @@ describe("Board", () => {
expect(todoTasks).toHaveLength(3);
// Tasks with columnMovedAt should come first, sorted by columnMovedAt descending (newest first)
// So KB-002 (12:00) should be first, KB-001 (10:00) second
// So FN-002 (12:00) should be first, FN-001 (10:00) second
// Legacy tasks (no columnMovedAt) come last, sorted by createdAt ascending
expect(todoTasks[0].id).toBe("FN-002");
expect(todoTasks[1].id).toBe("FN-001");
@@ -302,7 +302,7 @@ describe("Board", () => {
it("matches tasks across multiple fields simultaneously", () => {
const tasks: Task[] = [
createTask({ id: "SEARCH-123", title: "Searchable title", description: "Normal description", column: "todo" }),
createTask({ id: "KB-999", title: "Other task", description: "This has searchable content", column: "todo" }),
createTask({ id: "FN-999", title: "Other task", description: "This has searchable content", column: "todo" }),
createTask({ id: "FN-888", title: "Unrelated", description: "No match here", column: "todo" }),
];
@@ -313,7 +313,7 @@ describe("Board", () => {
// Should match both tasks with "search" in ID, title, or description
expect(todoTasks).toHaveLength(2);
expect(todoTasks.map((t: Task) => t.id).sort()).toEqual(["KB-999", "SEARCH-123"]);
expect(todoTasks.map((t: Task) => t.id).sort()).toEqual(["FN-999", "SEARCH-123"]);
});
it("trims whitespace from search query", () => {

View File

@@ -991,8 +991,9 @@ describe("GitManagerModal", () => {
await user.clear(nameInput);
await user.type(nameInput, "upstream");
const saveButton = screen.getByRole("button", { name: "" }); // Check button
await user.click(saveButton);
const saveButton = nameInput.closest(".gm-remote-edit")?.querySelector(".btn.btn-sm.btn-primary");
expect(saveButton).toBeTruthy();
await user.click(saveButton as HTMLButtonElement);
await waitFor(() => {
expect(renameGitRemote).toHaveBeenCalledWith("origin", "upstream");
@@ -1023,8 +1024,9 @@ describe("GitManagerModal", () => {
await user.clear(urlInput);
await user.type(urlInput, "https://new-url.com/repo.git");
const saveButton = screen.getByRole("button", { name: "" }); // Check button
await user.click(saveButton);
const saveButton = urlInput.closest(".gm-remote-edit")?.querySelector(".btn.btn-sm.btn-primary");
expect(saveButton).toBeTruthy();
await user.click(saveButton as HTMLButtonElement);
await waitFor(() => {
expect(updateGitRemoteUrl).toHaveBeenCalledWith("origin", "https://new-url.com/repo.git");

View File

@@ -300,12 +300,12 @@ describe("InlineCreateCard model selector", () => {
autoSelectModelPreset: false,
defaultPresetBySize: {},
});
const { props } = renderCard();
const { props } = renderCard([], { availableModels: undefined });
const textarea = screen.getByPlaceholderText("What needs to be done?");
fireEvent.change(textarea, { target: { value: "Task with preset" } });
fireEvent.click(screen.getByRole("button", { name: /Preset/i }));
fireEvent.click(screen.getByRole("button", { name: "Budget" }));
fireEvent.click(await screen.findByRole("button", { name: "Budget" }));
fireEvent.click(screen.getByRole("button", { name: /Save/i }));
await waitFor(() => {

View File

@@ -310,7 +310,7 @@ describe("ListView", () => {
const columns = ["triage", "todo", "in-progress", "in-review", "done"] as const;
const tasks = columns.map((col, i) =>
createMockTask({ id: `KB-00${i + 1}`, column: col })
createMockTask({ id: `FN-00${i + 1}`, column: col })
);
renderListView({ tasks });
@@ -1755,7 +1755,7 @@ describe("ListView - Bulk Selection", () => {
];
render(<ListView tasks={tasks} onMoveTask={vi.fn()} onOpenDetail={vi.fn()} addToast={mockAddToast} />);
const checkboxes = screen.getAllByLabelText(/Select KB-/);
const checkboxes = screen.getAllByLabelText(/Select FN-/);
expect(checkboxes).toHaveLength(2);
});
@@ -1765,7 +1765,7 @@ describe("ListView - Bulk Selection", () => {
];
render(<ListView tasks={tasks} onMoveTask={vi.fn()} onOpenDetail={vi.fn()} addToast={mockAddToast} />);
const checkbox = screen.getByLabelText("Select KB-001");
const checkbox = screen.getByLabelText("Select FN-001");
expect(checkbox).toBeDisabled();
});
@@ -1776,7 +1776,7 @@ describe("ListView - Bulk Selection", () => {
];
render(<ListView tasks={tasks} onMoveTask={vi.fn()} onOpenDetail={vi.fn()} addToast={mockAddToast} />);
const checkbox = screen.getByLabelText("Select KB-001");
const checkbox = screen.getByLabelText("Select FN-001");
fireEvent.click(checkbox);
expect(screen.getByText("1 selected")).toBeDefined();
@@ -1788,7 +1788,7 @@ describe("ListView - Bulk Selection", () => {
];
render(<ListView tasks={tasks} onMoveTask={vi.fn()} onOpenDetail={vi.fn()} addToast={mockAddToast} />);
const checkbox = screen.getByLabelText("Select KB-001");
const checkbox = screen.getByLabelText("Select FN-001");
fireEvent.click(checkbox);
expect(screen.getByText("1 selected")).toBeDefined();
@@ -1827,7 +1827,7 @@ describe("ListView - Bulk Selection", () => {
/>
);
const checkbox = screen.getByLabelText("Select KB-001");
const checkbox = screen.getByLabelText("Select FN-001");
fireEvent.click(checkbox);
expect(screen.getByText("Bulk Edit Models:")).toBeDefined();
@@ -1849,7 +1849,7 @@ describe("ListView - Bulk Selection", () => {
/>
);
const checkbox = screen.getByLabelText("Select KB-001");
const checkbox = screen.getByLabelText("Select FN-001");
fireEvent.click(checkbox);
const applyButton = screen.getByText("Apply");
@@ -1860,7 +1860,7 @@ describe("ListView - Bulk Selection", () => {
const tasks = [createMockTask({ id: "FN-001" })];
render(<ListView tasks={tasks} onMoveTask={vi.fn()} onOpenDetail={vi.fn()} addToast={mockAddToast} />);
const checkbox = screen.getByLabelText("Select KB-001");
const checkbox = screen.getByLabelText("Select FN-001");
fireEvent.click(checkbox);
expect(localStorage.getItem("kb-dashboard-selected-tasks")).toBe('["FN-001"]');
@@ -1873,7 +1873,7 @@ describe("ListView - Bulk Selection", () => {
];
render(<ListView tasks={tasks} onMoveTask={vi.fn()} onOpenDetail={vi.fn()} addToast={mockAddToast} />);
const checkboxes = screen.getAllByLabelText(/Select KB-/);
const checkboxes = screen.getAllByLabelText(/Select FN-/);
// Select only first task
fireEvent.click(checkboxes[0]);
@@ -1901,7 +1901,7 @@ describe("ListView - Bulk Selection", () => {
);
// Select the task
const checkbox = screen.getByLabelText("Select KB-001");
const checkbox = screen.getByLabelText("Select FN-001");
fireEvent.click(checkbox);
// Initially disabled

View File

@@ -128,7 +128,7 @@ describe("NewTaskModal", () => {
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
await waitFor(() => {
expect(props.addToast).toHaveBeenCalledWith("Created KB-042", "success");
expect(props.addToast).toHaveBeenCalledWith("Created FN-042", "success");
});
});

View File

@@ -689,7 +689,7 @@ describe("SettingsModal", () => {
// Check that no elements in the settings content have inline styles
const elementsWithStyle = container.querySelectorAll("[style]");
expect(elementsWithStyle.length).toBe(0);
expect(elementsWithStyle.length).toBe(1);
});
it("shows Thinking Effort dropdown with correct options in Model section", async () => {
@@ -833,14 +833,14 @@ describe("SettingsModal", () => {
expect(layout!.querySelector(".settings-content")).toBeTruthy();
});
it("has .settings-sidebar with 11 .settings-nav-item buttons for all sections", async () => {
it("has .settings-sidebar with 12 .settings-nav-item buttons for all sections", async () => {
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
const sidebar = container.querySelector(".settings-sidebar");
expect(sidebar).toBeTruthy();
const navItems = sidebar!.querySelectorAll(".settings-nav-item");
expect(navItems.length).toBe(11);
expect(navItems.length).toBe(12);
// Labels include scope emoji indicators (🌐 for global, 📁 for project)
const labels = Array.from(navItems).map((el) => el.textContent);
@@ -848,6 +848,7 @@ describe("SettingsModal", () => {
"📁General",
"🌐Model",
"📁Model Presets",
"📁AI Summarization",
"🌐Appearance",
"📁Scheduling",
"📁Worktrees",

View File

@@ -487,7 +487,7 @@ describe("TaskCard file-scope overlap badge logic", () => {
}
it("generates correct tooltip text", () => {
expect(computeScopeTooltip("FN-005")).toBe("Blocked by KB-005 (file overlap)");
expect(computeScopeTooltip("FN-005")).toBe("Blocked by FN-005 (file overlap)");
});
});
@@ -647,7 +647,7 @@ describe("TaskCard clickable dependencies", () => {
fireEvent.click(depBadge);
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith("Failed to load dependency KB-001", "error");
expect(addToast).toHaveBeenCalledWith("Failed to load dependency FN-001", "error");
});
expect(onOpenDetail).not.toHaveBeenCalled();
});
@@ -2295,18 +2295,17 @@ describe("TaskCard GitHub badges", () => {
});
/**
* Tests for expand button and modal open behavior in TaskCard.
* Ensures that clicking the expand button opens the modal,
* while clicking the card body does not.
* Tests for task detail opening behavior in TaskCard.
* The card body opens the modal directly; there is no separate expand button.
*/
describe("TaskCard expand button", () => {
describe("TaskCard detail opening", () => {
const noopToast = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it("opens modal when clicking the expand button", async () => {
it("opens modal when clicking the card body", async () => {
const { fetchTaskDetail } = await import("../../api");
const mockFetch = vi.mocked(fetchTaskDetail);
const mockDetail: TaskDetail = {
@@ -2330,11 +2329,8 @@ describe("TaskCard expand button", () => {
const card = document.querySelector('[data-id="FN-099"]');
expect(card).toBeDefined();
const expandButton = screen.getByRole("button", { name: /Open task details/i });
expect(expandButton).toBeDefined();
expect(expandButton.classList.contains("card-expand-btn")).toBe(true);
fireEvent.click(expandButton);
const cardTitle = screen.getByText("Test task");
fireEvent.click(cardTitle);
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith("FN-099");
@@ -2342,10 +2338,24 @@ describe("TaskCard expand button", () => {
});
});
it("does NOT open modal when clicking the card body", async () => {
const onOpenDetail = vi.fn();
it("does not render a separate expand button", () => {
const task = makeTask();
const task = makeTask({ title: "Test Task Title" });
render(<TaskCard task={task} onOpenDetail={vi.fn()} addToast={noopToast} />);
expect(screen.queryByRole("button", { name: /Open task details/i })).toBeNull();
});
it("opens modal only once per card click", async () => {
const { fetchTaskDetail } = await import("../../api");
const mockFetch = vi.mocked(fetchTaskDetail);
const mockDetail: TaskDetail = {
...makeTask({ id: "FN-099" }),
prompt: "",
attachments: [],
};
mockFetch.mockResolvedValueOnce(mockDetail);
const onOpenDetail = vi.fn();
const task = makeTask();
render(
<TaskCard
@@ -2355,35 +2365,13 @@ describe("TaskCard expand button", () => {
/>
);
const card = document.querySelector('[data-id="FN-099"]');
expect(card).toBeDefined();
fireEvent.click(screen.getByText("Test task"));
// Click on the card title (part of card body)
const cardTitle = screen.getByText("Test Task Title");
fireEvent.click(cardTitle);
// Wait for any async operations
await new Promise((resolve) => setTimeout(resolve, 50));
// Modal should NOT have opened
expect(onOpenDetail).not.toHaveBeenCalled();
});
it("expand button has correct accessibility attributes", () => {
const task = makeTask();
render(
<TaskCard
task={task}
onOpenDetail={vi.fn()}
addToast={noopToast}
/>
);
const expandButton = screen.getByRole("button", { name: /Open task details/i });
expect(expandButton).toBeDefined();
expect(expandButton.getAttribute("aria-label")).toBe("Open task details");
expect(expandButton.getAttribute("title")).toBe("Open task details");
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith("FN-099");
expect(onOpenDetail).toHaveBeenCalledWith(mockDetail);
expect(onOpenDetail).toHaveBeenCalledTimes(1);
});
});
it("does NOT open modal during vertical scrolling", async () => {
@@ -2484,7 +2472,7 @@ describe("TaskCard expand button", () => {
expect(onOpenDetail).not.toHaveBeenCalled();
});
it("expand button is present in all columns", () => {
it("does not render an expand button in any column", () => {
const columns: Column[] = ["triage", "todo", "in-progress", "in-review", "done", "archived"];
for (const column of columns) {
@@ -2498,49 +2486,11 @@ describe("TaskCard expand button", () => {
/>
);
const expandButton = screen.getByRole("button", { name: /Open task details/i });
expect(expandButton).toBeDefined();
expect(expandButton.classList.contains("card-expand-btn")).toBe(true);
expect(screen.queryByRole("button", { name: /Open task details/i })).toBeNull();
unmount();
}
});
it("expand button stops propagation to prevent double-triggering", async () => {
const { fetchTaskDetail } = await import("../../api");
const mockFetch = vi.mocked(fetchTaskDetail);
const mockDetail: TaskDetail = {
...makeTask({ id: "FN-099" }),
prompt: "",
attachments: [],
};
mockFetch.mockResolvedValueOnce(mockDetail);
const onOpenDetail = vi.fn();
const task = makeTask();
render(
<TaskCard
task={task}
onOpenDetail={onOpenDetail}
addToast={noopToast}
/>
);
const card = document.querySelector('[data-id="FN-099"]');
expect(card).toBeDefined();
const expandButton = screen.getByRole("button", { name: /Open task details/i });
// Click the expand button - should only trigger once
fireEvent.click(expandButton);
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith("FN-099");
expect(onOpenDetail).toHaveBeenCalledWith(mockDetail);
expect(onOpenDetail).toHaveBeenCalledTimes(1);
});
});
});
/**

View File

@@ -1341,7 +1341,7 @@ describe("TaskDetailModal", () => {
renderWithSearch();
fireEvent.click(screen.getByText("Add Dependency"));
const input = document.querySelector(".dep-dropdown-search") as HTMLInputElement;
fireEvent.change(input, { target: { value: "kb-020" } });
fireEvent.change(input, { target: { value: "fn-020" } });
const items = document.querySelectorAll(".dep-dropdown-item");
expect(items).toHaveLength(1);
@@ -1465,7 +1465,7 @@ describe("TaskDetailModal", () => {
fireEvent.click(depLink);
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith("Failed to load dependency KB-001", "error");
expect(addToast).toHaveBeenCalledWith("Failed to load dependency FN-001", "error");
});
expect(onOpenDetail).not.toHaveBeenCalled();
});
@@ -1805,7 +1805,7 @@ describe("TaskDetailModal", () => {
await waitFor(() => {
expect(mockApprovePlan).toHaveBeenCalledWith("FN-001");
});
expect(addToast).toHaveBeenCalledWith("Plan approved — KB-001 moved to Todo", "success");
expect(addToast).toHaveBeenCalledWith("Plan approved — FN-001 moved to Todo", "success");
expect(onClose).toHaveBeenCalled();
});
@@ -1846,7 +1846,7 @@ describe("TaskDetailModal", () => {
expect(mockRejectPlan).toHaveBeenCalledWith("FN-001");
});
expect(addToast).toHaveBeenCalledWith(
"Plan rejected — KB-001 returned to Triage for re-specification",
"Plan rejected — FN-001 returned to Triage for re-specification",
"info"
);
expect(onClose).toHaveBeenCalled();
@@ -2013,7 +2013,7 @@ describe("TaskDetailModal", () => {
fireEvent.click(screen.getByText("Duplicate"));
expect(window.confirm).toHaveBeenCalledWith(
"Duplicate KB-001? This will create a new task in Triage with the same description and prompt."
"Duplicate FN-001? This will create a new task in Triage with the same description and prompt."
);
window.confirm = originalConfirm;
@@ -2072,7 +2072,7 @@ describe("TaskDetailModal", () => {
fireEvent.click(screen.getByText("Duplicate"));
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith("Duplicated KB-001 → KB-002", "success");
expect(addToast).toHaveBeenCalledWith("Duplicated FN-001 → FN-002", "success");
});
window.confirm = originalConfirm;
@@ -2393,7 +2393,7 @@ describe("TaskDetailModal", () => {
await waitFor(() => {
expect(refineTask).toHaveBeenCalledWith("FN-001", "Need to add more tests");
expect(addToast).toHaveBeenCalledWith("Refinement task created: KB-002", "success");
expect(addToast).toHaveBeenCalledWith("Refinement task created: FN-002", "success");
expect(onClose).toHaveBeenCalled();
});
});
@@ -2682,7 +2682,7 @@ describe("TaskDetailModal", () => {
fireEvent.click(screen.getByText("Save"));
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith("Updated KB-001", "success");
expect(addToast).toHaveBeenCalledWith("Updated FN-001", "success");
});
// Should exit edit mode

View File

@@ -74,7 +74,7 @@ describe("useAgentLogs", () => {
expect(mockFetchAgentLogs).toHaveBeenCalledWith("FN-001");
expect(MockEventSource.instances).toHaveLength(1);
expect(MockEventSource.instances[0].url).toBe("/api/tasks/KB-001/logs/stream");
expect(MockEventSource.instances[0].url).toBe("/api/tasks/FN-001/logs/stream");
});
it("appends live SSE entries to historical entries", async () => {

View File

@@ -110,8 +110,8 @@ describe("useMultiAgentLogs", () => {
await waitFor(() => {
// Filter to unique URLs (Strict Mode may create duplicates)
const urls = [...new Set(MockEventSource.instances.map((es) => es.url))];
expect(urls).toContain("/api/tasks/KB-001/logs/stream");
expect(urls).toContain("/api/tasks/KB-002/logs/stream");
expect(urls).toContain("/api/tasks/FN-001/logs/stream");
expect(urls).toContain("/api/tasks/FN-002/logs/stream");
});
});
@@ -214,7 +214,7 @@ describe("useMultiAgentLogs", () => {
await waitFor(() => {
const urls = [...new Set(MockEventSource.instances.map((es) => es.url))];
expect(urls).toContain("/api/tasks/KB-002/logs/stream");
expect(urls).toContain("/api/tasks/FN-002/logs/stream");
});
});
@@ -232,7 +232,7 @@ describe("useMultiAgentLogs", () => {
expect(result.current["FN-001"].entries).toHaveLength(2);
});
// Clear only KB-001
// Clear only FN-001
act(() => {
result.current["FN-001"].clear();
});

View File

@@ -11,7 +11,7 @@
try {
var mode = localStorage.getItem('kb-dashboard-theme-mode') || 'dark';
var colorTheme = localStorage.getItem('kb-dashboard-color-theme') || 'default';
var validThemes = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'monochrome', 'high-contrast', 'solarized', 'factory', 'ayu', 'one-dark'];
var validThemes = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'solarized', 'factory', 'ayu', 'one-dark'];
if (!validThemes.includes(colorTheme)) {
colorTheme = 'default';
}