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(); });