Files
kandev/apps/web/components/settings/system/log-viewer.test.tsx
T
2026-09-25 18:07:55 +01:00

330 lines
13 KiB
TypeScript

import { StrictMode } from "react";
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { TauriEventInternals } from "@/lib/desktop/tauri-event-transport";
import type { DesktopDownloadFeedback } from "@/lib/desktop/protocol";
import { LogViewer } from "./log-viewer";
const createMock = vi.fn();
const fetchMock = vi.fn();
const capabilitiesMock = vi.fn();
const sessionsMock = vi.fn();
const DOWNLOAD_URL = "/download/bundle-1";
const LISTEN_COMMAND_SUFFIX = "|listen";
const DIAGNOSTIC_SAVE_PENDING = "Choose where to save the diagnostic ZIP.";
const DIAGNOSTIC_SAVE_FAILED = "The diagnostic ZIP could not be saved. Try again.";
const DIAGNOSTIC_BUNDLE_FILE_NAME = "kandev-diagnostic-logs.zip";
const CUSTOMIZE_BUNDLE_TEST_ID = "customize-diagnostic-bundle";
const CREATE_BUNDLE_TEST_ID = "create-custom-diagnostic-bundle";
const BUNDLE_STATUS_TEST_ID = "diagnostic-bundle-status";
const downloadURLMock = vi.fn((..._args: unknown[]) => DOWNLOAD_URL);
const CHECKED_STATE = "checked";
const DATA_STATE_ATTRIBUTE = "data-state";
let originalTauriInternals: PropertyDescriptor | undefined;
vi.mock("@/lib/api/domains/system-api", () => ({
createDiagnosticBundle: (...args: unknown[]) => createMock(...args),
fetchDiagnosticBundle: (...args: unknown[]) => fetchMock(...args),
fetchDiagnosticBundleCapabilities: (...args: unknown[]) => capabilitiesMock(...args),
fetchDiagnosticACPSessions: (...args: unknown[]) => sessionsMock(...args),
buildDiagnosticBundleDownloadUrl: (...args: unknown[]) => downloadURLMock(...args),
}));
beforeEach(() => {
originalTauriInternals = Object.getOwnPropertyDescriptor(window, "__TAURI_INTERNALS__");
createMock.mockReset();
fetchMock.mockReset();
capabilitiesMock.mockReset();
sessionsMock.mockReset();
capabilitiesMock.mockResolvedValue({
sources: ["backend", "frontend", "runtime"],
acp_debug_enabled: false,
acp_max_sessions: 10,
});
sessionsMock.mockResolvedValue([]);
downloadURLMock.mockClear();
vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});
});
afterEach(() => {
cleanup();
vi.useRealTimers();
if (originalTauriInternals) {
Object.defineProperty(window, "__TAURI_INTERNALS__", originalTauriInternals);
} else {
Reflect.deleteProperty(window, "__TAURI_INTERNALS__");
}
vi.restoreAllMocks();
});
function installDesktopEvents({ failListener = false, failListenerAttempts = 0 } = {}) {
let onNativeEvent: ((event: { payload: unknown }) => void) | undefined;
let failedListenerAttempts = 0;
let listenerAttempts = 0;
const internals: TauriEventInternals = {
invoke: vi.fn(async (command: string) => {
if (command.endsWith(LISTEN_COMMAND_SUFFIX)) {
listenerAttempts += 1;
if (failListener || failedListenerAttempts < failListenerAttempts) {
failedListenerAttempts += 1;
throw new Error("listener failed");
}
}
return command.endsWith(LISTEN_COMMAND_SUFFIX) ? 41 : undefined;
}),
transformCallback: vi.fn((callback) => {
onNativeEvent = callback;
return 7;
}),
};
Object.defineProperty(window, "__TAURI_INTERNALS__", { configurable: true, value: internals });
return {
internals,
emit: (payload: unknown) => onNativeEvent?.({ payload }),
listenerAttempts: () => listenerAttempts,
};
}
async function mountDesktopLogViewer(options?: {
failListener?: boolean;
failListenerAttempts?: number;
}) {
createMock.mockResolvedValue({ id: "bundle-1", status: "ready", warnings: [] });
const desktop = installDesktopEvents(options);
render(<LogViewer />);
await waitFor(() =>
expect(desktop.internals.invoke).toHaveBeenCalledWith(
"plugin:event|listen",
expect.objectContaining({ event: "kandev-desktop-v1-download" }),
),
);
return desktop;
}
function clickDesktopDownload() {
fireEvent.click(screen.getByTestId(CUSTOMIZE_BUNDLE_TEST_ID));
fireEvent.click(screen.getByTestId(CREATE_BUNDLE_TEST_ID));
}
async function startDesktopDownload() {
clickDesktopDownload();
await screen.findByText(DIAGNOSTIC_SAVE_PENDING);
}
async function prepareDesktopDownload() {
const desktop = await mountDesktopLogViewer();
await startDesktopDownload();
return desktop;
}
describe("LogViewer download feedback", () => {
it("reports a desktop bundle as saved only after native completion", async () => {
const desktop = await prepareDesktopDownload();
expect(screen.queryByText("The diagnostic ZIP was saved.")).toBeNull();
expect(screen.getByTestId(CUSTOMIZE_BUNDLE_TEST_ID).hasAttribute("disabled")).toBe(true);
desktop.emit({
status: "saved",
url: new URL(DOWNLOAD_URL, window.location.href).href,
fileName: DIAGNOSTIC_BUNDLE_FILE_NAME,
});
expect(await screen.findByText("The diagnostic ZIP was saved.")).toBeTruthy();
});
it("keeps native cancellation neutral and the bundle action retryable", async () => {
const desktop = await prepareDesktopDownload();
desktop.emit({
status: "cancelled",
url: new URL(DOWNLOAD_URL, window.location.href).href,
fileName: DIAGNOSTIC_BUNDLE_FILE_NAME,
});
await waitFor(() => expect(screen.queryByTestId(BUNDLE_STATUS_TEST_ID)).toBeNull());
expect(screen.getByTestId(CUSTOMIZE_BUNDLE_TEST_ID).hasAttribute("disabled")).toBe(false);
});
it("shows a native save error and keeps the bundle action retryable", async () => {
const desktop = await prepareDesktopDownload();
desktop.emit({
status: "failed",
url: new URL(DOWNLOAD_URL, window.location.href).href,
fileName: DIAGNOSTIC_BUNDLE_FILE_NAME,
});
const status = await screen.findByTestId(BUNDLE_STATUS_TEST_ID);
expect(status.textContent).toContain(DIAGNOSTIC_SAVE_FAILED);
expect(screen.getByTestId(CUSTOMIZE_BUNDLE_TEST_ID).hasAttribute("disabled")).toBe(false);
});
it("recovers with a retryable error when no native result arrives", async () => {
await mountDesktopLogViewer();
const browserSetTimeout = window.setTimeout.bind(window);
vi.spyOn(window, "setTimeout").mockImplementation(
(handler, timeout, ...args) =>
browserSetTimeout(
handler,
(timeout ?? 0) >= 600_000 ? 0 : (timeout ?? 0),
...args,
) as unknown as ReturnType<typeof setTimeout>,
);
await startDesktopDownload();
const status = await screen.findByTestId(BUNDLE_STATUS_TEST_ID);
expect(status.textContent).toContain(DIAGNOSTIC_SAVE_FAILED);
expect(screen.getByTestId(CUSTOMIZE_BUNDLE_TEST_ID).hasAttribute("disabled")).toBe(false);
});
it("does not start the transfer when feedback listener setup keeps failing", async () => {
const desktop = await mountDesktopLogViewer({ failListener: true });
clickDesktopDownload();
const status = await screen.findByTestId(BUNDLE_STATUS_TEST_ID);
expect(status.textContent).toContain(DIAGNOSTIC_SAVE_FAILED);
expect(screen.getByTestId(CUSTOMIZE_BUNDLE_TEST_ID).hasAttribute("disabled")).toBe(false);
expect(HTMLAnchorElement.prototype.click).not.toHaveBeenCalled();
expect(desktop.listenerAttempts()).toBe(2);
});
it("retries listener setup before starting a download after a transient listener failure", async () => {
const desktop = await mountDesktopLogViewer({ failListenerAttempts: 1 });
clickDesktopDownload();
await waitFor(() => expect(HTMLAnchorElement.prototype.click).toHaveBeenCalledOnce());
expect(desktop.listenerAttempts()).toBe(2);
desktop.emit({
status: "saved",
url: new URL(DOWNLOAD_URL, window.location.href).href,
fileName: DIAGNOSTIC_BUNDLE_FILE_NAME,
});
expect(await screen.findByText("The diagnostic ZIP was saved.")).toBeTruthy();
});
it("does not time out while the native Save dialog is open", async () => {
const desktop = await prepareDesktopDownload();
vi.useFakeTimers();
desktop.emit({
status: "selecting",
url: new URL(DOWNLOAD_URL, window.location.href).href,
fileName: DIAGNOSTIC_BUNDLE_FILE_NAME,
} as DesktopDownloadFeedback);
act(() => vi.advanceTimersByTime(15 * 60 * 1_000));
expect(screen.getByText(DIAGNOSTIC_SAVE_PENDING)).toBeTruthy();
expect(screen.getByTestId(CUSTOMIZE_BUNDLE_TEST_ID).hasAttribute("disabled")).toBe(true);
});
it("clears its result watchdog after a terminal native event", async () => {
const desktop = await prepareDesktopDownload();
const clearTimeoutSpy = vi.spyOn(window, "clearTimeout");
desktop.emit({
status: "saved",
url: new URL(DOWNLOAD_URL, window.location.href).href,
fileName: DIAGNOSTIC_BUNDLE_FILE_NAME,
});
expect(clearTimeoutSpy).toHaveBeenCalled();
});
});
describe("LogViewer customizer", () => {
it("uses one customizer with the standard backend and frontend defaults", async () => {
createMock.mockResolvedValue({
id: "bundle-1",
status: "collecting",
warnings: [],
});
fetchMock.mockResolvedValue({
id: "bundle-1",
status: "partial",
warnings: ["One browser did not respond."],
});
render(
<StrictMode>
<LogViewer />
</StrictMode>,
);
expect(screen.getByText("Review before sharing")).toBeTruthy();
expect(screen.queryByText("Recent log output")).toBeNull();
expect(screen.queryByTestId("download-diagnostic-bundle")).toBeNull();
expect(screen.queryByTestId("download-diagnostic-bundle-with-acp")).toBeNull();
fireEvent.click(screen.getByTestId(CUSTOMIZE_BUNDLE_TEST_ID));
expect(
screen.getByRole("checkbox", { name: "Backend logs" }).getAttribute(DATA_STATE_ATTRIBUTE),
).toBe(CHECKED_STATE);
expect(
screen.getByRole("checkbox", { name: "Frontend logs" }).getAttribute(DATA_STATE_ATTRIBUTE),
).toBe(CHECKED_STATE);
expect(
screen.getByRole("checkbox", { name: "Runtime index" }).getAttribute(DATA_STATE_ATTRIBUTE),
).not.toBe(CHECKED_STATE);
fireEvent.click(screen.getByTestId(CREATE_BUNDLE_TEST_ID));
await waitFor(() => expect(createMock).toHaveBeenCalledWith(["backend", "frontend"], []));
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith("bundle-1"), { timeout: 1_500 });
expect(await screen.findByText(/partial ZIP is downloading/)).toBeTruthy();
expect(downloadURLMock).toHaveBeenCalledWith("bundle-1");
});
it("offers ACP in the one customizer with task links and bounded bulk selection", async () => {
capabilitiesMock.mockResolvedValue({
sources: ["backend", "frontend", "runtime", "acp"],
acp_debug_enabled: true,
acp_max_sessions: 1,
});
sessionsMock.mockResolvedValue([
{
task_id: "task-1",
task_title: "Repair failing diagnostics",
session_id: "session-1",
agent: "claude-acp",
model: "sonnet",
status: "running",
executor_type: "local_docker",
acp_availability: "reachable",
},
{
task_id: "task-2",
task_title: "Inspect timeout",
session_id: "session-2",
agent: "codex-acp",
acp_availability: "host_retained",
},
]);
render(<LogViewer />);
await waitFor(() => expect(screen.getByTestId(CUSTOMIZE_BUNDLE_TEST_ID)).toBeTruthy());
expect(screen.queryByTestId("download-diagnostic-bundle")).toBeNull();
expect(screen.queryByTestId("download-diagnostic-bundle-with-acp")).toBeNull();
fireEvent.click(screen.getByTestId(CUSTOMIZE_BUNDLE_TEST_ID));
fireEvent.click(screen.getByRole("checkbox", { name: "ACP debug messages" }));
expect((await screen.findAllByText("ACP debug messages")).length).toBeGreaterThanOrEqual(1);
expect(screen.getByText(/can contain prompts/)).toBeTruthy();
const create = screen.getByTestId(CREATE_BUNDLE_TEST_ID);
expect(create.hasAttribute("disabled")).toBe(true);
const taskLink = await screen.findByTestId("acp-session-task-link-session-1");
expect(taskLink.textContent).toContain("Repair failing diagnostics");
expect(taskLink.getAttribute("href")).toBe("/t/task-1");
expect(taskLink.getAttribute("target")).toBe("_blank");
fireEvent.click(screen.getByTestId("select-all-acp-sessions"));
await waitFor(() => expect(create.hasAttribute("disabled")).toBe(false));
expect(screen.getByText("1 session selected")).toBeTruthy();
expect(
screen.getByRole("checkbox", { name: "session-1" }).getAttribute(DATA_STATE_ATTRIBUTE),
).toBe(CHECKED_STATE);
expect(
screen.getByRole("checkbox", { name: "session-2" }).getAttribute(DATA_STATE_ATTRIBUTE),
).toBe("unchecked");
fireEvent.click(screen.getByTestId("clear-acp-session-selection"));
await waitFor(() => expect(create.hasAttribute("disabled")).toBe(true));
});
});