* feat: make relay opt-in for device pairing * fix(relay): keep pairing state authoritative * fix(relay): refresh pairing state across transports * fix(onboard): honor explicit relay opt-out * fix(relay): surface transport startup failures * fix(pairing): gate offers for older daemons * test(cli): rely on real pairing transport coverage * fix(cli): report live relay without a pid file * fix(relay): synchronize config during daemon startup * fix(relay): preserve mutability for ignored overrides * fix(cli): use reachable daemon for pairing * fix(app): redirect invalid pair device routes * fix(pairing): support remote host device pairing Keep Pair Device host-relative through the existing settings and daemon RPC flow. Route CLI output through an injected adapter so workflow tests do not mutate process globals. * fix(pairing): validate pairing destinations Expose the host-relative Pair Device flow on every supported app platform. Reject a reachable daemon whose identity does not match the requested Paseo home before reading or mutating its pairing state. * fix(pairing): render QR codes across platforms Generate SVG text without DOM or canvas and render it through the existing cross-platform SVG stack. * fix(cli): pair from persisted relay state Allow explicit relay pairing while the daemon is stopped when relay is already enabled in persisted configuration.
207 lines
6.9 KiB
TypeScript
207 lines
6.9 KiB
TypeScript
import { expect, test } from "./fixtures";
|
|
import { gotoAppShell, openSettings } from "./helpers/app";
|
|
import { getE2EDaemonPort } from "./helpers/daemon-port";
|
|
import { TEST_HOST_LABEL } from "./helpers/daemon-registry";
|
|
import { getServerId } from "./helpers/server-id";
|
|
import {
|
|
expectSettingsHeader,
|
|
openSettingsHost,
|
|
openHostSection,
|
|
expectHostLabelDisplayed,
|
|
clickEditHostLabel,
|
|
expectHostLabelEditMode,
|
|
expectHostConnectionsCard,
|
|
expectHostInjectMcpCard,
|
|
expectHostActionCards,
|
|
expectHostProvidersCard,
|
|
expectHostNoDaemonLifecycleRow,
|
|
expectRetiredSidebarSectionsAbsent,
|
|
expectHostPageVisible,
|
|
expectLocalHostEntryFirst,
|
|
seedSavedSettingsHosts,
|
|
} from "./helpers/settings";
|
|
|
|
test.describe("Settings host page", () => {
|
|
test("connections section shows the seeded connection endpoint", async ({ page }) => {
|
|
const serverId = getServerId();
|
|
const port = getE2EDaemonPort();
|
|
|
|
await gotoAppShell(page);
|
|
await openSettings(page);
|
|
await openSettingsHost(page, serverId);
|
|
|
|
await expectSettingsHeader(page, "Connections");
|
|
await expectHostConnectionsCard(page, port);
|
|
});
|
|
|
|
test("agents section shows the inject MCP toggle", async ({ page }) => {
|
|
const serverId = getServerId();
|
|
|
|
await gotoAppShell(page);
|
|
await openSettings(page);
|
|
await openSettingsHost(page, serverId);
|
|
|
|
await openHostSection(page, serverId, "agents");
|
|
await expectSettingsHeader(page, "Agents");
|
|
await expectHostInjectMcpCard(page);
|
|
});
|
|
|
|
test("providers section shows the providers card", async ({ page }) => {
|
|
const serverId = getServerId();
|
|
|
|
await gotoAppShell(page);
|
|
await openSettings(page);
|
|
await openSettingsHost(page, serverId);
|
|
|
|
await expectHostProvidersCard(page, serverId);
|
|
await expectSettingsHeader(page, "Providers");
|
|
});
|
|
|
|
test("host section shows the host label and restart/remove action cards", async ({ page }) => {
|
|
const serverId = getServerId();
|
|
|
|
await gotoAppShell(page);
|
|
await openSettings(page);
|
|
await openSettingsHost(page, serverId);
|
|
|
|
await openHostSection(page, serverId, "host");
|
|
await expectSettingsHeader(page, "Overview");
|
|
await expectHostLabelDisplayed(page);
|
|
await expectHostActionCards(page, serverId);
|
|
});
|
|
|
|
test("a failed remote daemon update remains visible in the host UI", async ({
|
|
page,
|
|
outdatedDaemon,
|
|
}) => {
|
|
await seedSavedSettingsHosts(page, [outdatedDaemon]);
|
|
await page.reload();
|
|
await openSettings(page);
|
|
await openSettingsHost(page, outdatedDaemon.serverId);
|
|
await openHostSection(page, outdatedDaemon.serverId, "host");
|
|
|
|
page.once("dialog", (dialog) => dialog.accept());
|
|
const updateButton = page.getByTestId("host-page-update-button");
|
|
await updateButton.click();
|
|
|
|
await expect(
|
|
updateButton.filter({ hasText: /Preparing update|Downloading packages|Installing/ }),
|
|
).toBeDisabled();
|
|
|
|
const updateFailure = page.getByTestId("host-page-update-error");
|
|
await expect(updateFailure).toBeVisible();
|
|
await expect(updateFailure).toContainText("Update failed");
|
|
await expect(updateFailure).toContainText("Failed to update the daemon:");
|
|
await expect(updateButton).toBeEnabled();
|
|
});
|
|
|
|
test("a Desktop-managed daemon explains why its update action is disabled", async ({
|
|
page,
|
|
desktopManagedOutdatedDaemon,
|
|
}) => {
|
|
await seedSavedSettingsHosts(page, [desktopManagedOutdatedDaemon]);
|
|
await page.reload();
|
|
await openSettings(page);
|
|
await openSettingsHost(page, desktopManagedOutdatedDaemon.serverId);
|
|
await openHostSection(page, desktopManagedOutdatedDaemon.serverId, "host");
|
|
|
|
const updateCard = page.getByTestId("host-page-update-card");
|
|
await expect(updateCard).toBeVisible();
|
|
await expect(updateCard).toContainText(
|
|
"This daemon is managed by Paseo Desktop. Update Paseo Desktop on the host.",
|
|
);
|
|
await expect(page.getByTestId("host-page-update-button")).toBeDisabled();
|
|
});
|
|
|
|
test("clicking the label pencil reveals the inline editor", async ({ page }) => {
|
|
const serverId = getServerId();
|
|
|
|
await gotoAppShell(page);
|
|
await openSettings(page);
|
|
await openSettingsHost(page, serverId);
|
|
await openHostSection(page, serverId, "host");
|
|
|
|
await expectHostLabelDisplayed(page);
|
|
await clickEditHostLabel(page);
|
|
await expectHostLabelEditMode(page, TEST_HOST_LABEL);
|
|
});
|
|
|
|
test("host section does not render daemon lifecycle controls for a remote daemon", async ({
|
|
page,
|
|
}) => {
|
|
const serverId = getServerId();
|
|
|
|
await gotoAppShell(page);
|
|
await openSettings(page);
|
|
await openSettingsHost(page, serverId);
|
|
await openHostSection(page, serverId, "host");
|
|
|
|
// TODO: add a local-daemon fixture for positive daemon lifecycle coverage.
|
|
await expectHostNoDaemonLifecycleRow(page);
|
|
});
|
|
|
|
test("settings sidebar exposes the flat App and Host section rows", async ({ page }) => {
|
|
await gotoAppShell(page);
|
|
await openSettings(page);
|
|
|
|
await expectRetiredSidebarSectionsAbsent(page);
|
|
});
|
|
|
|
test("navigating to /settings/hosts/[serverId] redirects to the connections section", async ({
|
|
page,
|
|
}) => {
|
|
const serverId = getServerId();
|
|
|
|
await gotoAppShell(page);
|
|
await page.goto(`/settings/hosts/${encodeURIComponent(serverId)}`);
|
|
|
|
await expectHostPageVisible(page, serverId);
|
|
await expectSettingsHeader(page, "Connections");
|
|
await openHostSection(page, serverId, "host");
|
|
await expectHostLabelDisplayed(page);
|
|
await expectHostActionCards(page, serverId);
|
|
});
|
|
|
|
test("sidebar pins the local daemon host first", async ({ page }) => {
|
|
const serverId = getServerId();
|
|
|
|
// Simulate the Electron desktop bridge so `useIsLocalDaemon` resolves the
|
|
// seeded host to the local daemon. `manageBuiltInDaemon: false` (returned
|
|
// from get_desktop_settings) bypasses the desktop bootstrap flow so only
|
|
// the sidebar's status query runs against the seeded test daemon.
|
|
await page.addInitScript((localServerId) => {
|
|
(window as unknown as { paseoDesktop: unknown }).paseoDesktop = {
|
|
platform: "darwin",
|
|
invoke: async (command: string) => {
|
|
if (command === "desktop_daemon_status") {
|
|
return {
|
|
serverId: localServerId,
|
|
status: "running",
|
|
listen: null,
|
|
hostname: null,
|
|
pid: null,
|
|
home: "",
|
|
version: null,
|
|
desktopManaged: true,
|
|
error: null,
|
|
};
|
|
}
|
|
if (command === "get_desktop_settings") {
|
|
return {
|
|
releaseChannel: "stable",
|
|
daemon: { manageBuiltInDaemon: false, keepRunningAfterQuit: true },
|
|
};
|
|
}
|
|
return null;
|
|
},
|
|
getPendingOpenProject: async () => null,
|
|
events: { on: async () => () => undefined },
|
|
};
|
|
}, serverId);
|
|
|
|
await gotoAppShell(page);
|
|
await openSettings(page);
|
|
await expectLocalHostEntryFirst(page, serverId);
|
|
});
|
|
});
|