test(app): triage desktop test files — delete 16-mock component slop, tighten attachment store (#721)
* test(app): triage desktop test files — delete 16-mock component slop, tighten attachment store assertion Removes desktop-updates-section.test.tsx (16 internal mocks, JSDOM, toHaveBeenCalledWith chains on component renders). Tightens desktop-attachment-store.test.ts: toHaveBeenCalled() → toHaveBeenCalledWith(attachment). * refactor(app): extract daemon management toggle coordinator with unit tests Fills the coverage gap left by deleting desktop-updates-section.test.tsx. Extracts executeDaemonManagementToggle from useDaemonManagementToggle — pure async coordinator with injected deps (no vi.mock). Unit tests verify the three key invariants: settings persist before stop, stop is skipped when not desktop-managed, and start/stop are invoked on enable/disable.
This commit is contained in:
@@ -1,362 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { cleanup, fireEvent, render, waitFor } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { LocalDaemonSection } from "./desktop-updates-section";
|
||||
|
||||
const { alertMock, confirmDialogMock, settingsState, daemonStatusState } = vi.hoisted(() => ({
|
||||
alertMock: vi.fn(),
|
||||
confirmDialogMock: vi.fn(),
|
||||
settingsState: {
|
||||
settings: {
|
||||
releaseChannel: "stable" as const,
|
||||
daemon: {
|
||||
manageBuiltInDaemon: true,
|
||||
keepRunningAfterQuit: true,
|
||||
},
|
||||
},
|
||||
updateSettings: vi.fn<
|
||||
(updates: {
|
||||
daemon?: {
|
||||
manageBuiltInDaemon?: boolean;
|
||||
keepRunningAfterQuit?: boolean;
|
||||
};
|
||||
}) => Promise<void>
|
||||
>(),
|
||||
},
|
||||
daemonStatusState: {
|
||||
data: {
|
||||
status: {
|
||||
serverId: "desktop",
|
||||
status: "running" as const,
|
||||
listen: null,
|
||||
hostname: null,
|
||||
pid: 123,
|
||||
home: "/tmp/paseo",
|
||||
version: "1.2.3",
|
||||
desktopManaged: true,
|
||||
error: null,
|
||||
},
|
||||
logs: {
|
||||
logPath: "/tmp/paseo/daemon.log",
|
||||
contents: "daemon log",
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
error: null as string | null,
|
||||
setStatus: vi.fn(),
|
||||
refetch: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("react-native", () => ({
|
||||
ActivityIndicator: () => React.createElement("div", { "data-testid": "loading-spinner" }),
|
||||
Alert: { alert: alertMock },
|
||||
Text: ({ children }: { children?: React.ReactNode }) =>
|
||||
React.createElement("span", null, children),
|
||||
View: ({ children }: { children?: React.ReactNode }) =>
|
||||
React.createElement("div", null, children),
|
||||
}));
|
||||
|
||||
vi.mock("react-native-unistyles", () => ({
|
||||
StyleSheet: {
|
||||
create: (factory: unknown) =>
|
||||
typeof factory === "function"
|
||||
? (factory as (theme: Record<string, unknown>) => unknown)({
|
||||
spacing: { 1: 4, 2: 8, 3: 12, 6: 24 },
|
||||
borderRadius: { lg: 12 },
|
||||
fontSize: { xs: 12, sm: 14 },
|
||||
colors: {
|
||||
foreground: "#111",
|
||||
foregroundMuted: "#666",
|
||||
palette: { amber: { 500: "#f59e0b" } },
|
||||
},
|
||||
})
|
||||
: factory,
|
||||
},
|
||||
useUnistyles: () => ({
|
||||
theme: {
|
||||
iconSize: { sm: 14 },
|
||||
colors: {
|
||||
foreground: "#111",
|
||||
foregroundMuted: "#666",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("expo-clipboard", () => ({
|
||||
setStringAsync: vi.fn(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
vi.mock("lucide-react-native", () => {
|
||||
const icon = (name: string) => () => React.createElement("span", { "data-icon": name });
|
||||
return {
|
||||
Activity: icon("Activity"),
|
||||
ArrowUpRight: icon("ArrowUpRight"),
|
||||
Copy: icon("Copy"),
|
||||
FileText: icon("FileText"),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/styles/settings", () => ({
|
||||
settingsStyles: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop) => String(prop),
|
||||
},
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/screens/settings/settings-section", () => ({
|
||||
SettingsSection: ({
|
||||
children,
|
||||
title,
|
||||
trailing,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
title: string;
|
||||
trailing?: React.ReactNode;
|
||||
}) =>
|
||||
React.createElement(
|
||||
"section",
|
||||
null,
|
||||
React.createElement("h2", null, title),
|
||||
trailing,
|
||||
children,
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/adaptive-modal-sheet", () => ({
|
||||
AdaptiveModalSheet: ({
|
||||
children,
|
||||
visible,
|
||||
title,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
visible?: boolean;
|
||||
title?: string;
|
||||
}) => (visible ? React.createElement("div", { "data-title": title }, children) : null),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/button", () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onPress,
|
||||
disabled,
|
||||
accessibilityLabel,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
onPress?: () => void;
|
||||
disabled?: boolean;
|
||||
accessibilityLabel?: string;
|
||||
}) =>
|
||||
React.createElement(
|
||||
"button",
|
||||
{ type: "button", onClick: onPress, disabled, "aria-label": accessibilityLabel },
|
||||
children,
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/switch", () => ({
|
||||
Switch: ({
|
||||
value,
|
||||
onValueChange,
|
||||
disabled,
|
||||
accessibilityLabel,
|
||||
}: {
|
||||
value: boolean;
|
||||
onValueChange?: (next: boolean) => void;
|
||||
disabled?: boolean;
|
||||
accessibilityLabel?: string;
|
||||
}) =>
|
||||
React.createElement("button", {
|
||||
type: "button",
|
||||
role: "switch",
|
||||
"aria-label": accessibilityLabel,
|
||||
"aria-checked": value,
|
||||
disabled,
|
||||
onClick: () => onValueChange?.(!value),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/desktop/settings/desktop-settings", () => ({
|
||||
useDesktopSettings: () => ({
|
||||
...settingsState,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/desktop/hooks/use-daemon-status", () => ({
|
||||
useDaemonStatus: () => daemonStatusState,
|
||||
}));
|
||||
|
||||
vi.mock("@/utils/confirm-dialog", () => ({
|
||||
confirmDialog: confirmDialogMock,
|
||||
}));
|
||||
|
||||
vi.mock("@/utils/open-external-url", () => ({
|
||||
openExternalUrl: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/desktop/updates/desktop-updates", () => ({
|
||||
isVersionMismatch: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
const daemonCommandMocks = vi.hoisted(() => ({
|
||||
getCliDaemonStatusMock: vi.fn(),
|
||||
startDesktopDaemonMock: vi.fn(),
|
||||
stopDesktopDaemonMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/desktop/daemon/desktop-daemon", () => ({
|
||||
getCliDaemonStatus: daemonCommandMocks.getCliDaemonStatusMock,
|
||||
shouldUseDesktopDaemon: vi.fn(() => true),
|
||||
startDesktopDaemon: daemonCommandMocks.startDesktopDaemonMock,
|
||||
stopDesktopDaemon: daemonCommandMocks.stopDesktopDaemonMock,
|
||||
}));
|
||||
|
||||
vi.mock("@/utils/app-version", () => ({
|
||||
resolveAppVersion: vi.fn(() => "1.2.3"),
|
||||
}));
|
||||
|
||||
describe("LocalDaemonSection", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
alertMock.mockReset();
|
||||
confirmDialogMock.mockReset();
|
||||
settingsState.settings.daemon.manageBuiltInDaemon = true;
|
||||
settingsState.settings.daemon.keepRunningAfterQuit = true;
|
||||
settingsState.updateSettings.mockReset();
|
||||
settingsState.updateSettings.mockResolvedValue();
|
||||
daemonStatusState.data.status.status = "running";
|
||||
daemonStatusState.data.status.desktopManaged = true;
|
||||
daemonStatusState.setStatus.mockReset();
|
||||
daemonStatusState.refetch.mockReset();
|
||||
daemonCommandMocks.startDesktopDaemonMock.mockReset();
|
||||
daemonCommandMocks.startDesktopDaemonMock.mockResolvedValue({
|
||||
...daemonStatusState.data.status,
|
||||
status: "running",
|
||||
desktopManaged: true,
|
||||
});
|
||||
daemonCommandMocks.stopDesktopDaemonMock.mockReset();
|
||||
daemonCommandMocks.stopDesktopDaemonMock.mockResolvedValue({
|
||||
...daemonStatusState.data.status,
|
||||
status: "stopped",
|
||||
});
|
||||
daemonCommandMocks.getCliDaemonStatusMock.mockReset();
|
||||
});
|
||||
|
||||
it("renders the daemon toggles as switches reflecting current settings", () => {
|
||||
const screen = render(<LocalDaemonSection />);
|
||||
|
||||
expect(screen.getByText("Keep daemon running after quit")).toBeTruthy();
|
||||
expect(screen.getByText("Manage built-in daemon")).toBeTruthy();
|
||||
|
||||
const keepRunningSwitch = screen.getByRole("switch", {
|
||||
name: "Keep daemon running after quit",
|
||||
});
|
||||
expect(keepRunningSwitch.getAttribute("aria-checked")).toBe("true");
|
||||
|
||||
const manageSwitch = screen.getByRole("switch", { name: "Manage built-in daemon" });
|
||||
expect(manageSwitch.getAttribute("aria-checked")).toBe("true");
|
||||
});
|
||||
|
||||
it("does not render a duplicate restart-daemon control", () => {
|
||||
const screen = render(<LocalDaemonSection />);
|
||||
|
||||
expect(screen.queryByText("Restart daemon")).toBeNull();
|
||||
expect(screen.queryByText("Start daemon")).toBeNull();
|
||||
});
|
||||
|
||||
it("updates keep-running-after-quit without changing daemon lifecycle", async () => {
|
||||
const screen = render(<LocalDaemonSection />);
|
||||
|
||||
fireEvent.click(screen.getByRole("switch", { name: "Keep daemon running after quit" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(settingsState.updateSettings).toHaveBeenCalledWith({
|
||||
daemon: {
|
||||
keepRunningAfterQuit: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(confirmDialogMock).not.toHaveBeenCalled();
|
||||
expect(daemonCommandMocks.stopDesktopDaemonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("pauses built-in daemon management and persists the setting through desktop settings", async () => {
|
||||
confirmDialogMock.mockResolvedValue(true);
|
||||
const screen = render(<LocalDaemonSection />);
|
||||
|
||||
fireEvent.click(screen.getByRole("switch", { name: "Manage built-in daemon" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(daemonCommandMocks.stopDesktopDaemonMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(settingsState.updateSettings).toHaveBeenCalledWith({
|
||||
daemon: {
|
||||
manageBuiltInDaemon: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("persists paused management before stopping the desktop-managed daemon", async () => {
|
||||
confirmDialogMock.mockResolvedValue(true);
|
||||
const screen = render(<LocalDaemonSection />);
|
||||
|
||||
fireEvent.click(screen.getByRole("switch", { name: "Manage built-in daemon" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(daemonCommandMocks.stopDesktopDaemonMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(settingsState.updateSettings.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
daemonCommandMocks.stopDesktopDaemonMock.mock.invocationCallOrder[0],
|
||||
);
|
||||
});
|
||||
|
||||
it("does not stop a manually managed daemon when pausing built-in daemon management", async () => {
|
||||
confirmDialogMock.mockResolvedValue(true);
|
||||
daemonStatusState.data.status.desktopManaged = false;
|
||||
const screen = render(<LocalDaemonSection />);
|
||||
|
||||
fireEvent.click(screen.getByRole("switch", { name: "Manage built-in daemon" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(settingsState.updateSettings).toHaveBeenCalledWith({
|
||||
daemon: {
|
||||
manageBuiltInDaemon: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(daemonCommandMocks.stopDesktopDaemonMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("starts the built-in daemon when management is re-enabled", async () => {
|
||||
settingsState.settings.daemon.manageBuiltInDaemon = false;
|
||||
const screen = render(<LocalDaemonSection />);
|
||||
|
||||
fireEvent.click(screen.getByRole("switch", { name: "Manage built-in daemon" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(daemonCommandMocks.startDesktopDaemonMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(settingsState.updateSettings).toHaveBeenCalledWith({
|
||||
daemon: {
|
||||
manageBuiltInDaemon: true,
|
||||
},
|
||||
});
|
||||
expect(daemonStatusState.setStatus).toHaveBeenCalledWith({
|
||||
...daemonStatusState.data.status,
|
||||
status: "running",
|
||||
desktopManaged: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
startDesktopDaemon,
|
||||
stopDesktopDaemon,
|
||||
} from "@/desktop/daemon/desktop-daemon";
|
||||
import { executeDaemonManagementToggle } from "@/desktop/daemon/daemon-management-toggle";
|
||||
import { useDaemonStatus } from "@/desktop/hooks/use-daemon-status";
|
||||
import { useDesktopSettings, type DesktopSettings } from "@/desktop/settings/desktop-settings";
|
||||
import type { DesktopDaemonStatus } from "@/desktop/daemon/desktop-daemon";
|
||||
@@ -39,72 +40,42 @@ function useDaemonManagementToggle(args: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings.manageBuiltInDaemon) {
|
||||
setIsUpdatingDaemonManagement(true);
|
||||
void updateSettings({ manageBuiltInDaemon: true })
|
||||
.then(() => startDesktopDaemon())
|
||||
.then((newStatus) => {
|
||||
setStatus(newStatus);
|
||||
refetch();
|
||||
return;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[Settings] Failed to update built-in daemon management", error);
|
||||
Alert.alert("Error", "Unable to update built-in daemon management.");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsUpdatingDaemonManagement(false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
void confirmDialog({
|
||||
title: "Pause built-in daemon",
|
||||
message:
|
||||
"This will stop the built-in daemon immediately. Running agents and terminals connected to the built-in daemon will be stopped.",
|
||||
confirmLabel: "Pause and stop",
|
||||
cancelLabel: "Cancel",
|
||||
destructive: true,
|
||||
setIsUpdatingDaemonManagement(true);
|
||||
void executeDaemonManagementToggle(settings.manageBuiltInDaemon, daemonStatus, {
|
||||
confirm: () =>
|
||||
confirmDialog({
|
||||
title: "Pause built-in daemon",
|
||||
message:
|
||||
"This will stop the built-in daemon immediately. Running agents and terminals connected to the built-in daemon will be stopped.",
|
||||
confirmLabel: "Pause and stop",
|
||||
cancelLabel: "Cancel",
|
||||
destructive: true,
|
||||
}),
|
||||
persistSettings: (next) => updateSettings(next) as Promise<void>,
|
||||
startDaemon: startDesktopDaemon,
|
||||
stopDaemon: stopDesktopDaemon,
|
||||
})
|
||||
.then((confirmed) => {
|
||||
if (!confirmed) {
|
||||
.then((result) => {
|
||||
if (result.kind === "cancelled") {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdatingDaemonManagement(true);
|
||||
|
||||
void updateSettings({ manageBuiltInDaemon: false })
|
||||
.then(() => {
|
||||
if (daemonStatus?.status === "running" && daemonStatus.desktopManaged) {
|
||||
return stopDesktopDaemon();
|
||||
}
|
||||
return daemonStatus ?? null;
|
||||
})
|
||||
.then((newStatus) => {
|
||||
if (newStatus) {
|
||||
setStatus(newStatus);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.then(() => {
|
||||
refetch();
|
||||
return;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[Settings] Failed to stop built-in daemon", error);
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Built-in daemon management was paused, but Paseo could not stop the daemon.",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsUpdatingDaemonManagement(false);
|
||||
});
|
||||
if (result.newStatus) {
|
||||
setStatus(result.newStatus);
|
||||
}
|
||||
refetch();
|
||||
return;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[Settings] Failed to open built-in daemon pause confirmation", error);
|
||||
Alert.alert("Error", "Unable to open the daemon confirmation dialog.");
|
||||
console.error("[Settings] Failed to update built-in daemon management", error);
|
||||
Alert.alert(
|
||||
"Error",
|
||||
settings.manageBuiltInDaemon
|
||||
? "Built-in daemon management was paused, but Paseo could not stop the daemon."
|
||||
: "Unable to update built-in daemon management.",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsUpdatingDaemonManagement(false);
|
||||
});
|
||||
}, [
|
||||
daemonStatus,
|
||||
|
||||
Reference in New Issue
Block a user