import { afterEach, beforeEach, expect, test, vi } from "vitest";
import "@testing-library/jest-dom/vitest";
import { act, fireEvent, render, screen, waitFor, within, cleanup } from "@testing-library/react";
import { useState } from "react";
import type { Task } from "../../ui/types";
const server = vi.hoisted(() => ({ call: vi.fn(), openLink: vi.fn(), displayMode: "unknown", hostContextChanged: undefined as undefined | ((context: { displayMode: string }) => void), teardown: undefined as undefined | (() => object) }));
vi.mock("./ProjectPicker", () => ({ ProjectPicker: ({ data, value, onChange }: { data: Array<{label: string; value: string}>; value: string; onChange: (value: string) => void }) => }));
vi.mock("@mantine/core", async () => {
const React = await import("react");
const wrap = (tag: string) => ({ children, component, ...props }: Record) => React.createElement(component as string || tag, { "aria-label": props["aria-label"], role: props.role, id: props.id }, children as React.ReactNode);
return {
createTheme: (theme: unknown) => theme,
ActionIcon: ({ children, onClick, ...props }: Record) => React.createElement("button", { "aria-label": props["aria-label"], onClick }, children as React.ReactNode),
Modal: ({ opened, children }: { opened: boolean; children: React.ReactNode }) => opened ? {children}
: null,
Alert: wrap("div"), Box: wrap("div"), Button: ({ children, onClick, disabled }: {children: React.ReactNode; onClick?: () => void; disabled?: boolean}) => , Group: wrap("div"),
MantineProvider: ({ children }: { children: React.ReactNode }) => children,
TextInput: ({ value, onChange, ...props }: { value: string; onChange: React.ChangeEventHandler; "aria-label": string }) => ,
Paper: wrap("div"), Stack: wrap("div"), Text: wrap("p"), Title: wrap("h2"),
SegmentedControl: ({ data, onChange, value, ...props }: { data: Array<{ label: string; value: string }>; onChange: (value: string) => void; value: string; "aria-label": string }) => React.createElement("div", { role: "radiogroup", "aria-label": props["aria-label"] }, data.map((item) => React.createElement("button", { key: item.value, role: "radio", "aria-checked": item.value === value, onClick: () => onChange(item.value) }, item.label))),
Switch: ({ label, checked, onChange }: { label: string; checked: boolean; onChange: (event: React.ChangeEvent) => void }) => ,
Select: ({ data, onChange, value, ...props }: { data: Array<{ label: string; value: string }>; onChange: (value: string) => void; value: string; "aria-label": string }) => React.createElement("select", { "aria-label": props["aria-label"], value, onChange: (event: React.ChangeEvent) => onChange(event.target.value) }, data.map((item) => React.createElement("option", { key: item.value, value: item.value }, item.label))),
};
});
vi.mock("@modelcontextprotocol/ext-apps", () => ({
App: class {
openLink(input: { url: string }) { return server.openLink(input); }
connect() { return Promise.resolve(); }
getHostContext() { return { displayMode: server.displayMode }; }
callServerTool(input: { name: string; arguments: Record }) { return server.call(input); }
set onhostcontextchanged(handler: (context: { displayMode: string }) => void) { server.hostContextChanged = handler; }
set onteardown(handler: () => object) { server.teardown = handler; }
},
}));
vi.mock("../../ui/components/TaskCard", () => ({
TaskCard: ({ task, onOpenCodex, onOpenDetail }: { task: Task; onOpenCodex: (task: Task) => void; onOpenDetail: (task: Task) => void }) => ,
}));
vi.mock("../../ui/components/TaskBoard", () => ({
TaskBoard: ({ showCardUsage, tasks, onOpenDetail, onOpenCodex, doneNotice, onVisibleTasksChange }: { showCardUsage?: boolean; onVisibleTasksChange?: (ids: string[]) => void; doneNotice?: React.ReactNode; tasks: Task[]; onOpenDetail: (task: Task) => void; onOpenCodex: (task: Task) => void }) => {doneNotice}{tasks.map((task) => )},
}));
vi.mock("./NextTaskDetail", () => ({
NextTaskDetail: ({ task, opened, usageLoading, onClose, onTransition, extraActions }: {
task: Task | null; opened: boolean; usageLoading?: boolean; onClose: () => void; extraActions?: React.ReactNode;
onTransition: (status: "completed" | "failed", actionId: string) => Promise;
}) => {
const [confirm, setConfirm] = useState(false);
return opened && task ? {task.title}
{task.status}
{task.sessionUsage ? "Usage loaded" : usageLoading ? "Loading…" : "Usage unavailable."}{extraActions}{confirm && } : null;
},
}));
import { TaskChefApp } from "./main";
const task = (id: string, status: Task["status"] = "working", updatedAt = new Date().toISOString(), project = "Example"): Task => ({
id, title: `Task ${id}`, instruction: "Do the work", status, updatedAt, createdAt: updatedAt,
project: { name: project, path: `/${project}`, githubRepos: [] }, summary: "Initial summary",
threadId: null, turnRef: null, turnId: null, lastResult: null, latestTurn: null,
});
let notificationState = { revision: 0, items: [] as import("./NextNotificationCenter").NextNotification[] };
let visibility: {showExec: boolean; showCli: boolean; showArchived: boolean; showCardUsage?: boolean} = { showExec: false, showCli: false, showArchived: false };
let tasks: Task[];
let registeredProjects: Array<{id: string; name: string; path: string; githubRepos: string[]}>;
let details: Map;
let transition: () => Promise;
let detailFailure: string | null;
beforeEach(() => {
server.displayMode = "unknown";
server.hostContextChanged = undefined;
server.teardown = undefined;
window.localStorage.clear();
server.openLink.mockResolvedValue({ isError: false });
visibility = { showExec: false, showCli: false, showArchived: false };
notificationState = { revision: 0, items: [] };
tasks = [task("one")];
registeredProjects = [];
details = new Map(tasks.map((item) => [item.id, item]));
transition = async () => ({ structuredContent: { task: task("one", "completed") } });
detailFailure = null;
server.call.mockImplementation(({ name, arguments: args }) => {
if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: { tasks, projects: registeredProjects, healthy: true } } });
if (name === "taskchef_app_task" && detailFailure) return Promise.resolve({ isError: true, content: [{ type: "text", text: detailFailure }] });
if (name === "taskchef_app_task") return Promise.resolve({ structuredContent: { task: details.get(args.taskId as string) } });
if (name === "taskchef_app_notifications") {
if (args.action === "error") notificationState = { revision: notificationState.revision + 1, items: [{ id: `n${notificationState.revision}`, taskId: args.taskId ?? null, title: args.operation === "settings" ? "Could not open plugin settings" : "Could not open Task one", detail: String(args.error), read: false, timestamp: new Date().toISOString(), kind: "error" }, ...notificationState.items] };
if (args.action === "clear") notificationState = { revision: notificationState.revision + 1, items: [] };
if (args.action === "read_all" || args.action === "read") notificationState = { revision: notificationState.revision + 1, items: notificationState.items.map((item) => args.action === "read_all" || item.id === args.id ? { ...item, read: true } : item) };
return Promise.resolve({ structuredContent: { notifications: notificationState } });
}
if (name === "taskchef_app_transition") return transition();
if (name === "taskchef_app_open_settings") return Promise.resolve({ structuredContent: { message: "Requested settings." } });
if (name === "taskchef_app_open_chat") return Promise.resolve({ isError: true, content: [{ type: "text", text: "Codex could not be opened." }] });
throw new Error(`Unexpected tool: ${name}`);
});
});
afterEach(() => { cleanup(); server.call.mockReset(); vi.restoreAllMocks(); });
function mount() { render(); }
test("lifecycle events log readable single strings without suppressing repeats", () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const { unmount } = render();
const details = JSON.stringify({ visibility: document.visibilityState, displayMode: "unknown" });
expect(warn).toHaveBeenCalledWith(`[TaskChef] TaskChef lifecycle: mounted ${details}`);
warn.mockClear();
fireEvent(document, new Event("visibilitychange"));
expect(warn).toHaveBeenCalledWith(`[TaskChef] TaskChef lifecycle: visibilitychange ${details}`);
fireEvent(window, new Event("pagehide"));
fireEvent(window, new Event("pagehide"));
expect(warn.mock.calls.filter(([message]) => message === `[TaskChef] TaskChef lifecycle: pagehide ${details}`)).toHaveLength(2);
act(() => server.hostContextChanged?.({ displayMode: "fullscreen" }));
expect(warn).toHaveBeenCalledWith(`[TaskChef] TaskChef lifecycle: host context changed ${JSON.stringify({ visibility: document.visibilityState, displayMode: "fullscreen" })}`);
act(() => server.teardown?.());
expect(warn).toHaveBeenCalledWith(`[TaskChef] TaskChef lifecycle: host teardown ${details}`);
unmount();
expect(warn).toHaveBeenCalledWith(`[TaskChef] TaskChef lifecycle: unmounted ${details}`);
expect(warn.mock.calls.every((call) => call.length === 1 && typeof call[0] === "string" && !call[0].includes("[object Object]"))).toBe(true);
warn.mockClear();
fireEvent(window, new Event("pagehide"));
expect(warn).not.toHaveBeenCalled();
});
test("shows only Board, ignores a saved List choice, and filters project and date", async () => {
window.localStorage.setItem("taskchef.app.view", "list");
tasks = [task("one", "working", new Date().toISOString(), "Alpha"), task("two", "completed", "2026-01-01T00:00:00Z", "Alpha"), task("three", "interrupted", new Date().toISOString(), "Beta")];
mount();
await screen.findByRole("button", { name: "Task one" });
expect(screen.getByRole("region", { name: "Task board" })).toBeVisible();
expect(screen.queryByRole("radiogroup", { name: "View" })).not.toBeInTheDocument();
expect(screen.queryByRole("radiogroup", { name: "Status" })).not.toBeInTheDocument();
fireEvent.change(screen.getByRole("combobox", { name: "Project" }), { target: { value: "/Alpha" } });
expect(screen.queryByRole("button", { name: "Task three" })).not.toBeInTheDocument();
fireEvent.change(screen.getByRole("combobox", { name: "Updated" }), { target: { value: "24h" } });
expect(screen.queryByRole("button", { name: "Task two" })).not.toBeInTheDocument();
});
test("search finds chat text, projects, PR titles and repositories, combines filters, and keeps the query after refresh", async () => {
tasks = [task("one", "working", new Date().toISOString(), "Alpha"), task("two", "needs_input", new Date().toISOString(), "Beta"), task("old", "completed", "2026-01-01T00:00:00Z", "Alpha")];
tasks[0].title = "Fix login";
tasks[0].replyExcerpt = "First line\nSecond line\n**Database** migration passed";
tasks[1].replyExcerpt = "Database migration passed";
tasks[2].replyExcerpt = "Database migration passed";
tasks[0].project.githubRepos = ["acme/login-service"];
tasks[1].pullRequests = [{ url: "https://github.com/example/search-engine/pull/12", title: "Improve indexing", state: "open", checks: "unknown" }];
mount();
await screen.findByRole("button", { name: "Fix login" });
const input = screen.getByLabelText("Search Chats");
const callsBeforeTyping = server.call.mock.calls.length;
fireEvent.change(input, { target: { value: " LOGIN database " } });
expect(screen.getByRole("button", { name: "Fix login" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Task two" })).not.toBeInTheDocument();
expect(server.call.mock.calls.length).toBe(callsBeforeTyping);
for (const query of ["Alpha", "login-service"]) {
fireEvent.change(input, { target: { value: query } });
expect(screen.getByRole("button", { name: "Fix login" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Task two" })).not.toBeInTheDocument();
}
for (const query of ["Beta", "INDEXING", "search-engine", "example/search-engine", "beta indexing search-engine"]) {
fireEvent.change(input, { target: { value: query } });
expect(screen.getByRole("button", { name: "Task two" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Fix login" })).not.toBeInTheDocument();
}
expect(server.call.mock.calls.length).toBe(callsBeforeTyping);
fireEvent.change(input, { target: { value: "migration" } });
expect(screen.getByRole("button", { name: "Task two" })).toBeVisible();
fireEvent.change(screen.getByRole("combobox", { name: "Project" }), { target: { value: "/Alpha" } });
expect(screen.queryByRole("button", { name: "Task two" })).not.toBeInTheDocument();
fireEvent.change(screen.getByRole("combobox", { name: "Updated" }), { target: { value: "24h" } });
expect(screen.queryByRole("button", { name: "Task old" })).not.toBeInTheDocument();
await act(async () => { fireEvent.click(screen.getByRole("button", { name: "Refresh" })); });
expect(input).toHaveValue("migration");
fireEvent.change(input, { target: { value: "not-a-match" } });
expect(within(screen.getByRole("region", { name: "Task board" })).queryAllByRole("article")).toHaveLength(0);
fireEvent.change(input, { target: { value: "" } });
expect(screen.getByRole("button", { name: "Fix login" })).toBeVisible();
});
test("Settings opens the installed plugin details link and reports failure", async () => {
mount(); await screen.findByRole("button", { name: "Task one" });
fireEvent.click(screen.getByRole("button", { name: "Settings" }));
await waitFor(() => expect(server.call).toHaveBeenCalledWith({ name: "taskchef_app_open_settings", arguments: {} }));
const original = server.call.getMockImplementation()!;
server.call.mockImplementation((input) => input.name === "taskchef_app_open_settings"
? Promise.resolve({ isError: true, content: [{ type: "text", text: "Desktop opener failed." }] }) : original(input));
fireEvent.click(screen.getByRole("button", { name: "Settings" }));
expect(await screen.findByText("Could not open plugin settings")).toBeVisible();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});
test("filters projects with the same name by directory", async () => {
const first = task("one", null, new Date().toISOString(), "project");
const second = task("two", null, new Date().toISOString(), "project");
first.project.path = "/work/first/project";
second.project.path = "/work/second/project";
tasks = [first, second];
mount();
expect(await screen.findByRole("button", { name: "Task one" })).toBeVisible();
const picker = screen.getByRole("combobox", { name: "Project" });
expect(within(picker).getByRole("option", { name: "project (/work/first/project)" })).toBeVisible();
expect(within(picker).getByRole("option", { name: "project (/work/second/project)" })).toBeVisible();
fireEvent.change(picker, { target: { value: "/work/second/project" } });
expect(screen.queryByRole("button", { name: "Task one" })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Task two" })).toBeVisible();
expect(within(screen.getByRole("region", { name: "Task board" })).getAllByRole("button", { name: /^Task / })).toHaveLength(1);
});
test("refresh updates detail and removes a disappeared selection", async () => {
mount();
fireEvent.click(await screen.findByRole("button", { name: "Task one" }));
await screen.findByRole("region", { name: "Task detail" });
details.set("one", task("one", "needs_input", "2026-10-02T00:00:00Z"));
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
await waitFor(() => expect(screen.getByRole("region", { name: "Task detail" })).toHaveTextContent("needs_input"));
tasks = [];
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
await waitFor(() => expect(screen.queryByRole("region", { name: "Task detail" })).not.toBeInTheDocument());
});
test("refresh sends the last revision and keeps selection detail current on unchanged snapshots", async () => {
let revision = 1;
server.call.mockImplementation(({ name, arguments: args }) => {
if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: args.revision === revision
? { unchanged: true, revision, settings: visibility }
: { settings: visibility, snapshot: { tasks, healthy: true, revision } } });
if (name === "taskchef_app_task") return Promise.resolve({ structuredContent: { task: details.get(args.taskId as string) } });
throw new Error(`Unexpected tool: ${name}`);
});
mount();
fireEvent.click(await screen.findByRole("button", { name: "Task one" }));
details.set("one", task("one", "needs_input"));
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
await waitFor(() => expect(screen.getByRole("region", { name: "Task detail" })).toHaveTextContent("needs_input"));
expect(server.call).toHaveBeenCalledWith({ name: "taskchef_app_snapshot", arguments: { revision: 1, force: true, date: "all", project: "", visibleTaskIds: [] } });
tasks = [task("two")];
revision = 2;
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
await waitFor(() => expect(screen.queryByRole("region", { name: "Task detail" })).not.toBeInTheDocument());
expect(screen.getByRole("button", { name: "Task two" })).toBeVisible();
});
test("an older selection detail cannot replace a newer refresh detail", async () => {
let releaseFirst!: (value: unknown) => void;
const first = new Promise((resolve) => { releaseFirst = resolve; });
let detailCalls = 0;
server.call.mockImplementation(({ name }) => {
if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: { tasks, projects: registeredProjects, healthy: true } } });
if (name === "taskchef_app_task") return ++detailCalls === 1
? first
: Promise.resolve({ structuredContent: { task: task("one", "needs_input") } });
throw new Error(`Unexpected tool: ${name}`);
});
mount();
fireEvent.click(await screen.findByRole("button", { name: "Task one" }));
await waitFor(() => expect(detailCalls).toBe(1));
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
await waitFor(() => expect(screen.getByRole("region", { name: "Task detail" })).toHaveTextContent("needs_input"));
await act(async () => { releaseFirst({ structuredContent: { task: task("one", "working") } }); await first; });
expect(screen.getByRole("region", { name: "Task detail" })).toHaveTextContent("needs_input");
});
test("shows a card Open chat failure without hiding cards or opening detail", async () => {
mount();
fireEvent.click(await screen.findByRole("button", { name: "Open chat for Task one" }));
expect(await screen.findByText(/Codex could not be opened/)).toBeVisible();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Task one" })).toBeVisible();
expect(screen.getByRole("button", { name: "Open chat for Task one" })).toBeVisible();
expect(screen.queryByRole("region", { name: "Task detail" })).not.toBeInTheDocument();
});
test("an older selection response cannot replace the current task", async () => {
tasks = [task("one"), task("two")];
let release!: (value: unknown) => void;
const first = new Promise((resolve) => { release = resolve; });
server.call.mockImplementation(({ name, arguments: args }) => {
if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: { tasks, projects: registeredProjects, healthy: true } } });
if (name === "taskchef_app_task") return args.taskId === "one" ? first : Promise.resolve({ structuredContent: { task: task("two") } });
throw new Error(`Unexpected tool: ${name}`);
});
mount();
fireEvent.click(await screen.findByRole("button", { name: "Task one" }));
fireEvent.click(screen.getByRole("button", { name: "Task two" }));
await waitFor(() => expect(screen.getByRole("region", { name: "Task detail" })).toHaveTextContent("Task two"));
release({ structuredContent: { task: task("one") } });
await waitFor(() => expect(screen.getByRole("region", { name: "Task detail" })).toHaveTextContent("Task two"));
});
test("keeps scan diagnostics out of the board", async () => {
server.call.mockImplementation(({ name }) => {
if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: {
tasks: [], healthy: true, revision: 1,
scan: { source: "database", mode: "database", checkedAt: "2026-10-05T00:00:00Z", intervalSeconds: 5,
indexedFiles: 8620, visibleFiles: 300, unreadFiles: 8320, errors: 0 },
} } });
throw new Error(`Unexpected tool: ${name}`);
});
mount();
await screen.findByRole("region", { name: "Task board" });
expect(screen.queryByText(/Read-only database/)).not.toBeInTheDocument();
expect(screen.queryByText(/shown of.*top-level chats/)).not.toBeInTheDocument();
expect(screen.queryByText(/checks every 5s/)).not.toBeInTheDocument();
expect(screen.queryByText(/title \(may contain user text\)/)).not.toBeInTheDocument();
});
test("shows an initial inventory failure without undefined scan counts", async () => {
server.call.mockImplementation(({ name }) => {
if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: {
tasks: [], healthy: false, revision: 0,
scan: { mode: "error", checkedAt: "2026-10-05T00:00:00Z", error: "EACCES" },
} } });
throw new Error(`Unexpected tool: ${name}`);
});
mount();
expect(await screen.findByRole("alert")).toHaveTextContent("cannot read the Codex databases");
expect(screen.queryByText(/Read-only database/)).not.toBeInTheDocument();
expect(screen.queryByText(/undefined/)).not.toBeInTheDocument();
expect(screen.queryByText(/shown of/)).not.toBeInTheDocument();
});
test("inline mode shows a compact recent-chat view", async () => {
server.displayMode = "inline";
tasks = [task("one"), task("two"), task("three"), task("four")];
mount();
expect(await screen.findByText("4 eligible top-level chats")).toBeVisible();
expect(screen.getByRole("button", { name: "Task one" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Task four" })).not.toBeInTheDocument();
expect(screen.queryByRole("region", { name: "Task board" })).not.toBeInTheDocument();
expect(screen.queryByText(/View:.*document:/)).not.toBeInTheDocument();
});
test("inline mode loads once and begins polling when expanded", async () => {
server.displayMode = "inline";
const setInterval = vi.spyOn(window, "setInterval");
const clearInterval = vi.spyOn(window, "clearInterval");
const snapshotCalls = () => server.call.mock.calls.filter(([input]) => input.name === "taskchef_app_snapshot").length;
mount();
expect(await screen.findByText("1 eligible top-level chats")).toBeVisible();
expect(snapshotCalls()).toBe(1);
expect(setInterval.mock.calls.filter(([, delay]) => delay === 5000)).toHaveLength(0);
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
await waitFor(() => expect(snapshotCalls()).toBe(2));
server.displayMode = "fullscreen";
act(() => server.hostContextChanged?.({ displayMode: "fullscreen" }));
await waitFor(() => expect(snapshotCalls()).toBe(3));
const pollIndex = setInterval.mock.calls.findIndex(([, delay]) => delay === 5000);
expect(pollIndex).toBeGreaterThanOrEqual(0);
await act(async () => { (setInterval.mock.calls[pollIndex][0] as () => void)(); });
expect(snapshotCalls()).toBe(4);
server.displayMode = "inline";
act(() => server.hostContextChanged?.({ displayMode: "inline" }));
expect(clearInterval).toHaveBeenCalledWith(setInterval.mock.results[pollIndex].value);
});
test("shows the Node requirement when the scanner rejects its runtime", async () => {
server.call.mockResolvedValue({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: {
tasks: [], healthy: false, revision: 0,
scan: { mode: "error", checkedAt: "2026-10-05T00:00:00Z", error: "TaskChef requires Node.js 22.18.0 or later for read-only SQLite (current: 22.17.9)." },
} } });
mount();
expect(await screen.findByRole("alert")).toHaveTextContent("TaskChef requires Node.js 22.18.0 or later");
});
test("a database failure after a healthy snapshot removes stale tasks", async () => {
mount();
expect(await screen.findByRole("button", { name: "Task one" })).toBeVisible();
server.call.mockImplementation(({ name }) => {
if (name === "taskchef_app_snapshot") return Promise.resolve({ structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: {
tasks: [], healthy: false, revision: 2,
scan: { source: "database", mode: "error", checkedAt: new Date().toISOString() },
} } });
throw new Error(`Unexpected tool: ${name}`);
});
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
expect(await screen.findByRole("alert")).toHaveTextContent("cannot read the Codex databases");
expect(screen.queryByRole("button", { name: "Task one" })).not.toBeInTheDocument();
});
test("Done control calls the local mark tool with the displayed turn and refreshes", async () => {
const current = { ...task("one", "needs_input"), turnId: "turn-one", observed: { archive: false, lastTurnEvent: "completed", lastTurnEventAt: null, recentFileActivity: false } };
tasks = [current]; details = new Map([[current.id, current]]);
server.call.mockImplementation(async ({ name, arguments: args }) => {
if (name === "taskchef_app_set_done") {
const next = { ...current, status: args.done ? "completed" : "needs_input", manualDone: args.done } as Task;
tasks = [next]; details.set(current.id, next);
return { structuredContent: { task: next } };
}
if (name === "taskchef_app_snapshot") return { structuredContent: { notifications: notificationState, settingsUrl: "codex://plugins/taskchef-next?marketplacePath=%2Fexample", settings: visibility, snapshot: { tasks, projects: registeredProjects, healthy: true } } };
if (name === "taskchef_app_task") return { structuredContent: { task: details.get(current.id) } };
throw new Error(name);
});
mount();
fireEvent.click(await screen.findByRole("button", { name: "Task one" }));
fireEvent.click(await screen.findByRole("button", { name: "Mark Done" }));
await waitFor(() => expect(within(screen.getByRole("region", { name: "Task detail" })).getByText("completed")).toBeVisible());
expect(screen.queryByRole("button", { name: "Reopen" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Mark Done" })).not.toBeInTheDocument();
expect(server.call).toHaveBeenCalledWith({ name: "taskchef_app_set_done", arguments: { taskId: "one", expectedTurnId: "turn-one", done: true } });
});
test("native visibility settings update counts and survive view reloads", async () => {
tasks = [task("desktop"), ...["exec", "cli"].map((source) => ({ ...task(source), observed: { source, archive: false, lastTurnEvent: "completed", lastTurnEventAt: null, recentFileActivity: false } })), { ...task("archive", "archived"), observed: { source: "vscode", archive: true, lastTurnEvent: "completed", lastTurnEventAt: null, recentFileActivity: false } }];
details = new Map(tasks.map((item) => [item.id, item]));
const first = render();
expect(await screen.findByRole("button", { name: "Task desktop" })).toBeVisible();
expect(within(screen.getByRole("region", { name: "Task board" })).getAllByRole("button", { name: /^Task / })).toHaveLength(1);
visibility = { showExec: true, showCli: true, showArchived: true };
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
expect(await screen.findByRole("button", { name: "Task archive" })).toBeVisible();
expect(within(screen.getByRole("region", { name: "Task board" })).getAllByRole("button", { name: /^Task / })).toHaveLength(4);
first.unmount(); mount();
expect(await screen.findByRole("button", { name: "Task exec" })).toBeVisible();
visibility = { showExec: false, showCli: false, showArchived: false };
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
await waitFor(() => expect(screen.queryByRole("button", { name: "Task exec" })).not.toBeInTheDocument());
expect(screen.queryByRole("button", { name: "Task cli" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Task archive" })).not.toBeInTheDocument();
});
test("a selected CLI chat closes when its source becomes hidden during refresh", async () => {
visibility.showCli = true;
const cli = { ...task("cli"), observed: { source: "cli", archive: false, lastTurnEvent: "inProgress", lastTurnEventAt: null, recentFileActivity: false } };
tasks = [cli]; details = new Map([[cli.id, cli]]);
mount();
fireEvent.click(await screen.findByRole("button", { name: "Task cli" }));
expect(await screen.findByRole("region", { name: "Task detail" })).toBeVisible();
// Another visible client, or updated metadata, can make this chat archived.
const archived = { ...cli, status: "archived" as const, observed: { ...cli.observed, archive: true } };
tasks = [archived]; details.set(cli.id, archived);
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
await waitFor(() => expect(screen.queryByRole("region", { name: "Task detail" })).not.toBeInTheDocument());
expect(screen.queryByRole("button", { name: "Task cli" })).not.toBeInTheDocument();
});
test("inline mode follows the exec, CLI and archive visibility settings", async () => {
server.displayMode = "inline";
tasks = [task("desktop"), { ...task("exec"), observed: { source: "exec", archive: false, lastTurnEvent: "completed", lastTurnEventAt: null, recentFileActivity: false } }, { ...task("cli"), observed: { source: "cli", archive: false, lastTurnEvent: "completed", lastTurnEventAt: null, recentFileActivity: false } }, { ...task("archive", "archived"), observed: { archive: true, lastTurnEvent: "completed", lastTurnEventAt: null, recentFileActivity: false } }];
mount();
expect(await screen.findByText("1 eligible top-level chats")).toBeVisible();
expect(screen.queryByRole("button", { name: "Task cli" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Task archive" })).not.toBeInTheDocument();
});
test("notification center restores saved history quietly and shares read/clear actions", async () => {
notificationState = { revision: 1, items: [{ id: "saved", taskId: "one", title: "Saved notification", detail: "", kind: "ready", read: false, timestamp: new Date().toISOString() }] };
mount();
await screen.findByRole("button", { name: "Task one" });
expect(screen.queryByText("Saved notification")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Notifications, unread" })).toBeVisible();
expect(screen.getByRole("button", { name: "Notifications, unread" })).not.toHaveTextContent(/\d/);
fireEvent.click(screen.getByRole("button", { name: /^Notifications/ }));
const center = screen.getByRole("region", { name: "Notification center" });
expect(within(center).getByText("Saved notification")).toBeVisible();
fireEvent.click(within(center).getByRole("button", { name: "Mark all read" }));
await waitFor(() => expect(screen.queryByRole("button", { name: "Notifications, unread" })).not.toBeInTheDocument());
fireEvent.click(within(center).getByRole("button", { name: "Unread" }));
expect(within(center).getByText("No unread notifications")).toBeVisible();
fireEvent.click(within(center).getByRole("button", { name: "All" }));
fireEvent.click(within(center).getByRole("button", { name: /Saved notification/ }));
expect(await screen.findByRole("region", { name: "Task detail" })).toBeVisible();
expect(screen.queryByRole("region", { name: "Notification center" })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Close" }));
fireEvent.click(screen.getByRole("button", { name: /^Notifications/ }));
fireEvent.click(screen.getByRole("button", { name: "Clear all" }));
expect(await screen.findByText("No notifications yet")).toBeVisible();
});
test("new notifications toast once for five seconds without replacing the board", async () => {
mount();
await screen.findByRole("button", { name: "Task one" });
vi.useFakeTimers();
try {
notificationState = { revision: 1, items: [{ id: "new", taskId: "one", title: "Task one is ready for input or review", detail: "", kind: "ready", read: false, timestamp: new Date().toISOString() }] };
await act(async () => { fireEvent.click(screen.getByRole("button", { name: "Refresh" })); });
expect(screen.getByText("Task one is ready for input or review")).toBeVisible();
expect(screen.getByRole("region", { name: "Task board" })).toBeVisible();
await act(async () => { vi.advanceTimersByTime(4000); });
expect(screen.getByText("Task one is ready for input or review")).toBeVisible();
await act(async () => { vi.advanceTimersByTime(1000); });
expect(screen.queryByText("Task one is ready for input or review")).not.toBeInTheDocument();
await act(async () => { fireEvent.click(screen.getByRole("button", { name: "Refresh" })); });
expect(screen.queryByText("Task one is ready for input or review")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /^Notifications/ }));
expect(screen.getByText("Task one is ready for input or review")).toBeVisible();
fireEvent.keyDown(document, { key: "Escape" });
expect(screen.queryByRole("region", { name: "Notification center" })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: /^Notifications/ })).toHaveFocus();
} finally { vi.useRealTimers(); }
});
test("a saved notification can open Details for a chat hidden by Settings", async () => {
const hidden = { ...task("hidden", "archived"), observed: { source: "cli", archive: true } } as Task;
tasks.push(hidden);
details.set(hidden.id, hidden);
notificationState = { revision: 1, items: [{ id: "saved", taskId: "hidden", title: "Task hidden is ready for input or review", detail: "", kind: "ready", read: false, timestamp: new Date().toISOString() }] };
mount();
await screen.findByRole("button", { name: "Task one" });
expect(screen.queryByRole("button", { name: "Task hidden" })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: /^Notifications/ }));
fireEvent.click(screen.getByRole("button", { name: /Task hidden is ready/ }));
expect(await screen.findByRole("region", { name: "Task detail" })).toHaveTextContent("Task hidden");
fireEvent.click(screen.getByRole("button", { name: "Refresh" }));
await waitFor(() => expect(screen.getByRole("region", { name: "Task detail" })).toHaveTextContent("Task hidden"));
expect(screen.queryByRole("button", { name: "Notifications, unread" })).not.toBeInTheDocument();
});
test("saved project selection groups original and worktree chats and includes empty projects", async () => {
tasks = [task("original"), task("worktree"), task("plain")];
tasks[0].project = { id: "saved", name: "My project", path: "/repo", githubRepos: [] };
tasks[1].project = { id: "saved", name: "My project", path: "/worktree", githubRepos: [] };
tasks[2].project = { id: "projectless", name: "No project", path: "/Documents/Codex/date/task", githubRepos: [] };
registeredProjects = [{ id: "saved", name: "My project", path: "/repo", githubRepos: [] }, { id: "empty", name: "Empty project", path: "/empty", githubRepos: [] }];
mount(); await screen.findByRole("button", { name: "Task original" });
fireEvent.change(screen.getByRole("combobox", { name: "Project" }), { target: { value: "saved" } });
expect(screen.getByRole("button", { name: "Task worktree" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Task plain" })).not.toBeInTheDocument();
fireEvent.change(screen.getByRole("combobox", { name: "Project" }), { target: { value: "projectless" } });
expect(screen.getByRole("button", { name: "Task plain" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Task original" })).not.toBeInTheDocument();
fireEvent.change(screen.getByRole("combobox", { name: "Project" }), { target: { value: "empty" } });
expect(within(screen.getByRole("region", { name: "Task board" })).queryAllByRole("article")).toHaveLength(0);
});
test("active scheduled chat hides Mark Done in Details", async () => {
const current = { ...task("one", "needs_input"), scheduled: true, turnId: "turn-one", observed: { archive: false, lastTurnEvent: "completed", lastTurnEventAt: null, recentFileActivity: false } };
tasks = [current]; details = new Map([[current.id, current]]);
mount();
fireEvent.click(await screen.findByRole("button", { name: "Task one" }));
expect(await screen.findByRole("region", { name: "Task detail" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Mark Done" })).not.toBeInTheDocument();
});
test("Done connection link opens GitHub controls without leaving the board", async () => {
mount();
const link = await screen.findByRole("button", {name:"Connect GitHub"});
fireEvent.click(link);
expect(await screen.findByRole("button", {name:"Open plugin settings"})).toBeInTheDocument();
expect(screen.getByRole("region", {name:"Task board"})).toBeVisible();
});
test("All time loads local records first and passes visible card IDs on the next request", async () => {
render();
await screen.findByRole("button", { name: "Report card in view" });
expect(server.call).toHaveBeenCalledWith({name:"taskchef_app_snapshot",arguments:{project:"",date:"all",visibleTaskIds:[]}});
server.call.mockClear();
fireEvent.click(screen.getByRole("button", { name: "Report card in view" }));
await waitFor(() => expect(server.call).toHaveBeenCalledWith({name:"taskchef_app_snapshot",arguments:{project:"",date:"all",visibleTaskIds:["one"]}}));
});
test("returning to a visible panel reuses PR cache; manual Refresh forces a check",async()=>{
vi.spyOn(document,"visibilityState","get").mockReturnValue("visible");
mount(); await screen.findByRole("button",{name:"Task one"});
server.call.mockClear();
fireEvent(document,new Event("visibilitychange"));
await waitFor(()=>expect(server.call).toHaveBeenCalledWith(expect.objectContaining({name:"taskchef_app_snapshot"})));
expect(server.call.mock.calls.filter(([input])=>input.name==="taskchef_app_snapshot").every(([input])=>input.arguments.force !== true)).toBe(true);
server.call.mockClear();
fireEvent.click(screen.getByRole("button",{name:"Refresh"}));
await waitFor(()=>expect(server.call).toHaveBeenCalledWith(expect.objectContaining({name:"taskchef_app_snapshot",arguments:expect.objectContaining({force:true})})));
});
test("appearance selection changes colors and survives reopening the view", async () => {
const first = render();
fireEvent.click(screen.getByRole("button", { name: "Appearance" }));
expect(screen.getByRole("radio", { name: "NIGHT" })).toHaveAttribute("aria-checked", "true");
fireEvent.click(screen.getByRole("radio", { name: "DAY" }));
expect(document.documentElement.dataset.taskchefTheme).toBe("light");
expect(window.localStorage.getItem("taskchef.app.theme")).toBe("light");
fireEvent.click(screen.getByRole("radio", { name: "Catppuccin" }));
expect(window.localStorage.getItem("taskchef.app.visualTheme.light")).toBe("catppuccin");
fireEvent.click(screen.getByRole("radio", { name: "NIGHT" }));
fireEvent.click(screen.getByRole("radio", { name: "Nord" }));
expect(document.documentElement.dataset.taskchefPalette).toBe("nord");
fireEvent.click(screen.getByRole("radio", { name: "DAY" }));
expect(document.documentElement.dataset.taskchefPalette).toBe("catppuccin");
first.unmount();
render();
fireEvent.click(screen.getByRole("button", { name: "Appearance" }));
expect(screen.getByRole("radio", { name: "DAY" })).toHaveAttribute("aria-checked", "true");
expect(screen.getByRole("radio", { name: "Catppuccin" })).toHaveAttribute("aria-checked", "true");
expect(document.documentElement.dataset.taskchefTheme).toBe("light");
});
test("opening and reopening Details use cached PRs; explicit Refresh requests new status", async () => {
render();
await screen.findByRole("button", {name:"Task one"});
fireEvent.click(screen.getByRole("button", {name:"Task one"}));
await screen.findByRole("region", {name:"Task detail"});
await waitFor(()=>expect(server.call).toHaveBeenCalledWith({name:"taskchef_app_task",arguments:{taskId:"one"}}));
fireEvent.click(screen.getByRole("button", {name:"Close"}));
fireEvent.click(screen.getByRole("button", {name:"Task one"}));
await screen.findByRole("region", {name:"Task detail"});
expect(server.call.mock.calls.filter(([request])=>request.name==="taskchef_app_task").every(([request])=>!request.arguments.refreshGithub)).toBe(true);
fireEvent.click(screen.getByRole("button", {name:"Refresh"}));
await waitFor(()=>expect(server.call).toHaveBeenCalledWith({name:"taskchef_app_task",arguments:{taskId:"one",refreshGithub:true}}));
});
test("a slow Details read finishes across successful and failed board refreshes", async () => {
const original=server.call.getMockImplementation()!;
let finish!: (value: unknown) => void, failSnapshot=false;
server.call.mockImplementation((args)=>{
if(args.name === "taskchef_app_task") return new Promise(resolve=>{finish=resolve;});
if(args.name === "taskchef_app_snapshot" && failSnapshot) return Promise.reject(new Error("Snapshot failed"));
return original(args);
});
mount();fireEvent.click(await screen.findByRole("button",{name:"Task one"}));
expect(await screen.findByText("Loading…")).toBeVisible();
await act(async()=>{fireEvent(document,new Event("visibilitychange"));});
expect(server.call.mock.calls.filter(([args])=>args.name === "taskchef_app_task")).toHaveLength(1);
failSnapshot=true;
await act(async()=>{fireEvent(document,new Event("visibilitychange"));});
expect(screen.getByText("Loading…")).toBeVisible();
const value={tokens:{input_tokens:100,cached_input_tokens:0,cache_write_input_tokens:0,output_tokens:10,reasoning_output_tokens:0,total_tokens:110},samples:1,costUsd:0.1,partial:false,costPartial:false};
await act(async()=>finish({structuredContent:{task:{...task("one"),sessionUsage:{latest:value,total:value,subagents:0,pricingDate:"2026-10-10"}}}}));
expect(within(screen.getByRole("region",{name:"Usage"})).getByText("Usage loaded")).toBeVisible();
expect(screen.queryByText("Loading…")).toBeNull();
});
test("native card usage setting defaults on and changes without hiding detail usage", async () => {
render();
const board = await screen.findByRole("region", {name:"Task board"});
expect(board).toHaveAttribute("data-card-usage", "true");
visibility = {...visibility, showCardUsage:false};
fireEvent.click(screen.getByRole("button", {name:"Refresh"}));
await waitFor(() => expect(board).toHaveAttribute("data-card-usage", "false"));
fireEvent.click(screen.getByRole("button", {name:"Task one"}));
expect(await screen.findByRole("region", {name:"Usage"})).toBeInTheDocument();
});