Show host in search results and filter sidebar by multiple hosts (#1825)
* feat(app): show host in command center and multi-select host filter Command-center agent results now show which host the workspace lives on, shown only when more than one host is connected (matching the sidebar). The sidebar display-preferences filter becomes multi-select — pick any number of hosts instead of one-or-all — and its host rows now carry a status dot on the left like the other host pickers. Persisted filter state migrates from the old single host to a host list. * test(app): extract host-filter and command-center e2e DSL helpers Move the mechanical menu/search interactions out of the new spec bodies into focused helpers so the tests read as user intent. * fix(app): drop the icon from the host filter "All hosts" item * fix(app): make multi-host e2e seeding order-independent The offline-host seed relied on Playwright running the test's init script after the fixture's registry reset — an ordering Playwright does not guarantee. Write the full registry and set the fixture's disable-once flag, then reload, so the multi-host seed survives regardless of script order. Also tag the pre-v2 hostFilter migration reader with COMPAT so the cleanup sweep can find it.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { expect } from "@playwright/test";
|
||||
import { test } from "./fixtures";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
import { createIdleAgent } from "./helpers/archive-tab";
|
||||
import { openCommandCenter } from "./helpers/command-center";
|
||||
import { addOfflineHostAndReload } from "./helpers/hosts";
|
||||
import { seedWorkspace } from "./helpers/seed-client";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
|
||||
const PRIMARY_HOST_LABEL = "Primary Host";
|
||||
const SECONDARY_HOST_ID = "host-command-center-secondary";
|
||||
|
||||
test.describe("Command center host labels", () => {
|
||||
test.describe.configure({ timeout: 180_000 });
|
||||
|
||||
test("agent results show the host they live on when more than one host exists", async ({
|
||||
page,
|
||||
}) => {
|
||||
const seeded = await seedWorkspace({ repoPrefix: "command-center-host-" });
|
||||
const title = `cc-host-${randomUUID().slice(0, 8)}`;
|
||||
|
||||
try {
|
||||
const agent = await createIdleAgent(seeded.client, {
|
||||
cwd: seeded.repoPath,
|
||||
workspaceId: seeded.workspaceId,
|
||||
title,
|
||||
});
|
||||
|
||||
// A second (offline) host makes the view multi-host, which is when the host label earns its space.
|
||||
await gotoAppShell(page);
|
||||
await addOfflineHostAndReload(page, {
|
||||
serverId: SECONDARY_HOST_ID,
|
||||
label: "Secondary Host",
|
||||
primaryLabel: PRIMARY_HOST_LABEL,
|
||||
});
|
||||
|
||||
const panel = await openCommandCenter(page);
|
||||
|
||||
// The shared daemon may carry agents from other specs, so target this agent by its id.
|
||||
const row = panel.getByTestId(`command-center-agent-${getServerId()}:${agent.id}`);
|
||||
await expect(row).toBeVisible({ timeout: 30_000 });
|
||||
await expect(row).toContainText(title);
|
||||
await expect(row).toContainText(PRIMARY_HOST_LABEL);
|
||||
} finally {
|
||||
await seeded.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { expect, type Locator, type Page } from "@playwright/test";
|
||||
|
||||
// Opens the command center / global search palette from the sidebar and returns its panel.
|
||||
export async function openCommandCenter(page: Page): Promise<Locator> {
|
||||
await page.getByTestId("sidebar-command-center-search").click();
|
||||
const panel = page.getByTestId("command-center-panel");
|
||||
await expect(panel).toBeVisible({ timeout: 30_000 });
|
||||
return panel;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { buildSeededHost } from "./daemon-registry";
|
||||
|
||||
const REGISTRY_KEY = "@paseo:daemon-registry";
|
||||
const SEED_NONCE_KEY = "@paseo:e2e-seed-nonce";
|
||||
const DISABLE_DEFAULT_SEED_ONCE_KEY = "@paseo:e2e-disable-default-seed-once";
|
||||
|
||||
// The multi-host UI (the command-center host label, the sidebar host filter) only renders once
|
||||
// more than one host exists. The e2e harness runs a single real daemon, so we add an extra registry
|
||||
// entry pointing at an unreachable endpoint: it stays offline, which is enough to make the UI treat
|
||||
// the view as multi-host without standing up a second daemon.
|
||||
//
|
||||
// Must run AFTER the first navigation: the auto-seed fixture writes the registry + nonce on load,
|
||||
// and reseeds on every navigation. We write the full registry here and set the fixture's
|
||||
// disable-once flag, then reload — so the fixture skips its reset and the registry survives. This
|
||||
// avoids depending on the (unspecified) ordering of multiple Playwright init scripts. Optionally
|
||||
// relabels the seeded primary host so assertions can target a distinctive name.
|
||||
export async function addOfflineHostAndReload(
|
||||
page: Page,
|
||||
input: { serverId: string; label: string; primaryLabel?: string },
|
||||
): Promise<void> {
|
||||
const offlineHost = buildSeededHost({
|
||||
serverId: input.serverId,
|
||||
label: input.label,
|
||||
endpoint: "127.0.0.1:59999",
|
||||
nowIso: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await page.evaluate(
|
||||
({ host, keys, primaryLabel }) => {
|
||||
const nonce = localStorage.getItem(keys.nonce);
|
||||
if (!nonce) {
|
||||
throw new Error("Expected the e2e seed nonce before overriding the host registry.");
|
||||
}
|
||||
const raw = localStorage.getItem(keys.registry);
|
||||
const registry: Array<{ serverId: string; label?: string }> = raw ? JSON.parse(raw) : [];
|
||||
if (primaryLabel && registry[0]) {
|
||||
registry[0].label = primaryLabel;
|
||||
}
|
||||
if (!registry.some((entry) => entry.serverId === host.serverId)) {
|
||||
registry.push(host);
|
||||
}
|
||||
localStorage.setItem(keys.registry, JSON.stringify(registry));
|
||||
localStorage.setItem(keys.disableSeedOnce, nonce);
|
||||
},
|
||||
{
|
||||
host: offlineHost,
|
||||
keys: {
|
||||
registry: REGISTRY_KEY,
|
||||
nonce: SEED_NONCE_KEY,
|
||||
disableSeedOnce: DISABLE_DEFAULT_SEED_ONCE_KEY,
|
||||
},
|
||||
primaryLabel: input.primaryLabel,
|
||||
},
|
||||
);
|
||||
|
||||
await page.reload();
|
||||
}
|
||||
|
||||
export async function openSidebarDisplayPreferences(page: Page): Promise<void> {
|
||||
await page.getByTestId("sidebar-display-preferences-menu").click();
|
||||
await expect(page.getByTestId("sidebar-display-preferences-content")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
// A host's filter row carries a status dot on the left next to its label.
|
||||
export async function expectHostFilterRow(page: Page, serverId: string): Promise<void> {
|
||||
await expect(page.getByTestId(`sidebar-host-filter-${serverId}`)).toBeVisible();
|
||||
await expect(page.getByTestId(`sidebar-host-filter-status-${serverId}`)).toBeVisible();
|
||||
}
|
||||
|
||||
export async function toggleHostFilter(page: Page, serverId: string): Promise<void> {
|
||||
await page.getByTestId(`sidebar-host-filter-${serverId}`).click();
|
||||
}
|
||||
|
||||
export async function selectAllHostsFilter(page: Page): Promise<void> {
|
||||
await page.getByTestId("sidebar-host-filter-all").click();
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { test } from "./fixtures";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
import {
|
||||
addOfflineHostAndReload,
|
||||
expectHostFilterRow,
|
||||
openSidebarDisplayPreferences,
|
||||
selectAllHostsFilter,
|
||||
toggleHostFilter,
|
||||
} from "./helpers/hosts";
|
||||
import { seedWorkspace } from "./helpers/seed-client";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
|
||||
const SECONDARY_HOST_ID = "host-filter-secondary";
|
||||
|
||||
test.describe("Sidebar host filter (multi-select)", () => {
|
||||
test.describe.configure({ timeout: 120_000 });
|
||||
|
||||
test("pins the sidebar to multiple selected hosts at once", async ({ page }) => {
|
||||
const seeded = await seedWorkspace({ repoPrefix: "host-filter-" });
|
||||
const serverId = getServerId();
|
||||
const workspaceRow = page.getByTestId(
|
||||
`sidebar-workspace-row-${serverId}:${seeded.workspaceId}`,
|
||||
);
|
||||
|
||||
try {
|
||||
// A second (offline) host is enough to surface the host filter without a second daemon.
|
||||
await gotoAppShell(page);
|
||||
await addOfflineHostAndReload(page, { serverId: SECONDARY_HOST_ID, label: "Secondary Host" });
|
||||
await expect(workspaceRow).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await openSidebarDisplayPreferences(page);
|
||||
await expectHostFilterRow(page, serverId);
|
||||
await expectHostFilterRow(page, SECONDARY_HOST_ID);
|
||||
|
||||
// Pin the primary host — its workspace stays visible.
|
||||
await toggleHostFilter(page, serverId);
|
||||
await expect(workspaceRow).toBeVisible();
|
||||
|
||||
// Add the secondary host without clearing the primary. Under single-select this would replace
|
||||
// the primary and hide the workspace; multi-select keeps both pinned, so it stays visible.
|
||||
await toggleHostFilter(page, SECONDARY_HOST_ID);
|
||||
await expect(workspaceRow).toBeVisible();
|
||||
|
||||
// Drop the primary host — only the (empty) secondary host remains pinned, so the workspace hides.
|
||||
await toggleHostFilter(page, serverId);
|
||||
await expect(workspaceRow).toHaveCount(0, { timeout: 10_000 });
|
||||
|
||||
// Back to all hosts — the workspace returns.
|
||||
await selectAllHostsFilter(page);
|
||||
await expect(workspaceRow).toBeVisible({ timeout: 10_000 });
|
||||
} finally {
|
||||
await seeded.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user