Skip unrelated required checks without losing their names (#2542)
* fix(ci): skip unrelated required checks cleanly Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable. * fix(ci): gate changes to install patching script * fix(ci): route cross-package test contracts narrowly * fix(ci): keep routed contracts merge-blocking * fix(server): exclude nested test utilities from builds * fix(ci): cover app-owned native and browser domains * refactor(ci): align tests with runtime domains Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check. * fix(ci): keep routing and desktop suites isolated The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs. * fix(ci): isolate desktop e2e runtime * perf(test): isolate Playwright workers * perf(test): parallelize Playwright within specs * fix(test): keep Playwright file scheduling * fix(test): share dynamic Metro fixture * fix(test): preserve own-host restart fixtures * fix(ci): preserve routed contracts after rebase * Stabilize Playwright wheel-scroll assertions
This commit is contained in:
@@ -0,0 +1,551 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { execSync, spawn, type ChildProcess } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import net from "node:net";
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { buildHostWorkspaceRoute, decodeWorkspaceIdFromPathSegment } from "@/utils/host-routes";
|
||||
import { metroTest as test } from "../support/fixtures";
|
||||
import { buildSeededHost } from "../support/helpers/daemon-registry";
|
||||
import { loadDaemonClientConstructor } from "../support/helpers/daemon-client-loader";
|
||||
import {
|
||||
createNodeWebSocketFactory,
|
||||
type NodeWebSocketFactory,
|
||||
} from "../support/helpers/node-ws-factory";
|
||||
import { withDisabledE2ESpeechEnv } from "../support/helpers/speech-env";
|
||||
import {
|
||||
expectNewWorkspaceProjectSelected,
|
||||
openGlobalNewWorkspaceComposer,
|
||||
selectNewWorkspaceProject,
|
||||
submitNewWorkspaceEmpty,
|
||||
} from "../support/helpers/new-workspace";
|
||||
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
||||
import { getVisibleWorkspaceAgentTabIds } from "../support/helpers/workspace-tabs";
|
||||
|
||||
const LEGACY_AGENT_ID = "10000000-0000-4000-8000-000000000001";
|
||||
const SERVER_ID = `srv_restart_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
||||
|
||||
interface RestartDaemonClient {
|
||||
connect(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
fetchWorkspaces(): Promise<{
|
||||
entries: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
workspaceDirectory: string;
|
||||
projectId: string;
|
||||
}>;
|
||||
}>;
|
||||
listProjects(): Promise<{
|
||||
projects: Array<{ projectId: string; projectKey?: string }>;
|
||||
}>;
|
||||
fetchAgents(options?: { scope?: "active" }): Promise<{
|
||||
entries: Array<{
|
||||
agent: {
|
||||
id: string;
|
||||
workspaceId?: string;
|
||||
status: string;
|
||||
};
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface RestartDaemonClientConfig {
|
||||
url: string;
|
||||
clientId: string;
|
||||
clientType: "cli";
|
||||
appVersion: string;
|
||||
webSocketFactory: NodeWebSocketFactory;
|
||||
}
|
||||
|
||||
interface SeededRestartHome {
|
||||
paseoHome: string;
|
||||
cwd: string;
|
||||
projectId: string;
|
||||
projectDisplayName: string;
|
||||
workspaceA: string;
|
||||
workspaceB: string;
|
||||
cleanup(): void;
|
||||
}
|
||||
|
||||
interface StartedDaemon {
|
||||
port: number;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
async function seedRestartHome(): Promise<SeededRestartHome> {
|
||||
const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-playwright-restart-home-"));
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "paseo-playwright-restart-cwd-"));
|
||||
const projectsDir = path.join(paseoHome, "projects");
|
||||
const agentDir = path.join(paseoHome, "agents", projectDirNameFromCwd(cwd));
|
||||
mkdirSync(projectsDir, { recursive: true });
|
||||
mkdirSync(agentDir, { recursive: true });
|
||||
|
||||
const projectDisplayName = path.basename(cwd);
|
||||
const project = {
|
||||
projectId: `prj_restart_${randomUUID().slice(0, 8)}`,
|
||||
rootPath: cwd,
|
||||
kind: "non_git",
|
||||
displayName: projectDisplayName,
|
||||
customName: null,
|
||||
createdAt: "2026-03-01T00:00:00.000Z",
|
||||
updatedAt: "2026-03-01T00:00:00.000Z",
|
||||
archivedAt: null,
|
||||
};
|
||||
const workspaceA = {
|
||||
workspaceId: "wks_restart_a",
|
||||
projectId: project.projectId,
|
||||
cwd,
|
||||
kind: "directory",
|
||||
displayName: "Original restart workspace",
|
||||
title: null,
|
||||
branch: null,
|
||||
createdAt: "2026-03-01T00:00:00.000Z",
|
||||
updatedAt: "2026-03-01T00:00:00.000Z",
|
||||
archivedAt: null,
|
||||
};
|
||||
const workspaceB = {
|
||||
workspaceId: "wks_restart_b",
|
||||
projectId: project.projectId,
|
||||
cwd,
|
||||
kind: "directory",
|
||||
displayName: "Restart sibling workspace",
|
||||
title: null,
|
||||
branch: null,
|
||||
createdAt: "2026-03-02T00:00:00.000Z",
|
||||
updatedAt: "2026-03-02T00:00:00.000Z",
|
||||
archivedAt: null,
|
||||
};
|
||||
|
||||
writeFileSync(path.join(projectsDir, "projects.json"), JSON.stringify([project]));
|
||||
writeFileSync(
|
||||
path.join(projectsDir, "workspaces.json"),
|
||||
JSON.stringify([workspaceA, workspaceB]),
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(agentDir, `${LEGACY_AGENT_ID}.json`),
|
||||
JSON.stringify({
|
||||
id: LEGACY_AGENT_ID,
|
||||
provider: "codex",
|
||||
cwd,
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
lastActivityAt: "2026-03-01T12:00:00.000Z",
|
||||
lastUserMessageAt: null,
|
||||
title: "Legacy cwd-only running agent",
|
||||
labels: {},
|
||||
lastStatus: "running",
|
||||
lastModeId: "default",
|
||||
config: null,
|
||||
runtimeInfo: { provider: "codex", sessionId: null },
|
||||
persistence: null,
|
||||
archivedAt: null,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
paseoHome,
|
||||
cwd,
|
||||
projectId: project.projectId,
|
||||
projectDisplayName,
|
||||
workspaceA: workspaceA.workspaceId,
|
||||
workspaceB: workspaceB.workspaceId,
|
||||
cleanup: () => {
|
||||
rmSync(paseoHome, { recursive: true, force: true });
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function projectDirNameFromCwd(cwd: string): string {
|
||||
const { root } = path.win32.parse(cwd);
|
||||
const withoutRoot = cwd.slice(root.length).replace(/[\\/]+$/, "");
|
||||
const sanitizedRoot = root.replace(/[:\\/]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
const prefix = sanitizedRoot ? `${sanitizedRoot}-` : "";
|
||||
if (!withoutRoot) {
|
||||
return sanitizedRoot || "root";
|
||||
}
|
||||
return prefix + withoutRoot.replace(/[\\/]+/g, "-");
|
||||
}
|
||||
|
||||
async function getAvailablePort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close(() => reject(new Error("Failed to acquire port")));
|
||||
return;
|
||||
}
|
||||
server.close(() => resolve(address.port));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForServer(port: number, child: ChildProcess): Promise<void> {
|
||||
const startedAt = Date.now();
|
||||
let lastConnectionError: unknown = null;
|
||||
while (Date.now() - startedAt < 20_000) {
|
||||
if (child.exitCode !== null) {
|
||||
throw new Error(`Restart test daemon exited before listening (exit ${child.exitCode}).`);
|
||||
}
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const socket = net.connect(port, "127.0.0.1", () => {
|
||||
socket.end();
|
||||
resolve();
|
||||
});
|
||||
socket.setTimeout(1000, () => {
|
||||
socket.destroy();
|
||||
reject(new Error(`Connection timed out to daemon port ${port}`));
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
lastConnectionError = error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Restart test daemon did not listen on ${port}. Last error: ${
|
||||
lastConnectionError instanceof Error
|
||||
? lastConnectionError.message
|
||||
: String(lastConnectionError)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function stopProcess(child: ChildProcess): Promise<void> {
|
||||
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||
child.kill("SIGTERM");
|
||||
const timeout = setTimeout(() => {
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
}, 5000);
|
||||
try {
|
||||
await once(child, "exit");
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function startRestartDaemon(input: {
|
||||
paseoHome: string;
|
||||
origin: string;
|
||||
}): Promise<StartedDaemon> {
|
||||
const port = await getAvailablePort();
|
||||
if (port === 6767 || String(port) === process.env.E2E_DAEMON_PORT) {
|
||||
return startRestartDaemon(input);
|
||||
}
|
||||
|
||||
const serverDir = path.resolve(__dirname, "../../../server");
|
||||
const tsxBin = execSync("which tsx").toString().trim();
|
||||
const child = spawn(tsxBin, ["scripts/supervisor-entrypoint.ts", "--dev"], {
|
||||
cwd: serverDir,
|
||||
env: withDisabledE2ESpeechEnv({
|
||||
...process.env,
|
||||
PASEO_HOME: input.paseoHome,
|
||||
PASEO_SERVER_ID: SERVER_ID,
|
||||
PASEO_LISTEN: `127.0.0.1:${port}`,
|
||||
PASEO_CORS_ORIGINS: input.origin,
|
||||
PASEO_RELAY_ENABLED: "0",
|
||||
PASEO_NODE_ENV: "development",
|
||||
NODE_ENV: "development",
|
||||
}),
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
detached: false,
|
||||
});
|
||||
let stderr = "";
|
||||
child.stderr?.on("data", (chunk: Buffer) => {
|
||||
stderr += chunk.toString("utf8");
|
||||
stderr = stderr.split("\n").slice(-40).join("\n");
|
||||
});
|
||||
|
||||
try {
|
||||
await waitForServer(port, child);
|
||||
} catch (error) {
|
||||
await stopProcess(child);
|
||||
throw new Error(
|
||||
`${error instanceof Error ? error.message : String(error)}\nDaemon stderr:\n${stderr}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
port,
|
||||
close: () => stopProcess(child),
|
||||
};
|
||||
}
|
||||
|
||||
async function connectRestartDaemonClient(port: number): Promise<RestartDaemonClient> {
|
||||
const DaemonClient = await loadDaemonClientConstructor<
|
||||
RestartDaemonClientConfig,
|
||||
RestartDaemonClient
|
||||
>();
|
||||
const client = new DaemonClient({
|
||||
url: `ws://127.0.0.1:${port}/ws`,
|
||||
clientId: `restart-playwright-${randomUUID()}`,
|
||||
clientType: "cli",
|
||||
appVersion: loadAppVersion(),
|
||||
webSocketFactory: createNodeWebSocketFactory(),
|
||||
});
|
||||
await client.connect();
|
||||
return client;
|
||||
}
|
||||
|
||||
function loadAppVersion(): string {
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(path.resolve(__dirname, "../../package.json"), "utf8"),
|
||||
) as { version?: unknown };
|
||||
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
|
||||
throw new Error("Missing app package version");
|
||||
}
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
async function seedBrowserForDaemon(page: Page, input: { serverId: string; port: number }) {
|
||||
await page.route(/:(6767)\b/, (route) => route.abort());
|
||||
await page.routeWebSocket(/:(6767)\b/, async (ws) => {
|
||||
await ws.close({ code: 1008, reason: "Blocked connection to localhost:6767 during e2e." });
|
||||
});
|
||||
await page.route(
|
||||
"**/*",
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "text/html",
|
||||
body: "<!doctype html><html><body>storage seed</body></html>",
|
||||
});
|
||||
},
|
||||
{ times: 1 },
|
||||
);
|
||||
await page.goto("/");
|
||||
|
||||
const host = buildSeededHost({
|
||||
serverId: input.serverId,
|
||||
endpoint: `127.0.0.1:${input.port}`,
|
||||
label: "restart daemon",
|
||||
nowIso: nowIso(),
|
||||
});
|
||||
await page.evaluate(
|
||||
({ daemon, preferences }) => {
|
||||
localStorage.setItem("@paseo:e2e", "1");
|
||||
localStorage.setItem("@paseo:daemon-registry", JSON.stringify([daemon]));
|
||||
localStorage.removeItem("@paseo:settings");
|
||||
localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(preferences));
|
||||
},
|
||||
{
|
||||
daemon: host,
|
||||
preferences: {
|
||||
serverId: input.serverId,
|
||||
provider: "codex",
|
||||
providerPreferences: {
|
||||
codex: { model: "gpt-5.4-mini", thinkingOptionId: "low" },
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function parseWorkspaceIdFromPageUrl(page: Page, serverId: string): string | null {
|
||||
const pathname = new URL(page.url()).pathname;
|
||||
const match = pathname.match(
|
||||
new RegExp(`^/h/${serverId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/workspace/([^/?#]+)`),
|
||||
);
|
||||
if (!match?.[1]) return null;
|
||||
return decodeWorkspaceIdFromPathSegment(match[1]);
|
||||
}
|
||||
|
||||
async function expectWorkspaceRowHasOnlyIndicator(
|
||||
page: Page,
|
||||
input: { serverId: string; workspaceId: string; indicator: string },
|
||||
) {
|
||||
const row = page.getByTestId(`sidebar-workspace-row-${input.serverId}:${input.workspaceId}`);
|
||||
await expect(row).toBeVisible({ timeout: 30_000 });
|
||||
for (const indicator of ["attention", "done", "failed", "loading", "needs_input", "running"]) {
|
||||
const locator = row.locator(`[data-testid="workspace-status-indicator-${indicator}"]`);
|
||||
if (indicator === input.indicator) {
|
||||
await expect(locator).toBeVisible({ timeout: 30_000 });
|
||||
} else {
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function expectWorkspaceRowDoesNotShowIndicator(
|
||||
page: Page,
|
||||
input: { serverId: string; workspaceId: string; indicator: string },
|
||||
) {
|
||||
const row = page.getByTestId(`sidebar-workspace-row-${input.serverId}:${input.workspaceId}`);
|
||||
await expect(row).toBeVisible({ timeout: 30_000 });
|
||||
await expect(
|
||||
row.locator(`[data-testid="workspace-status-indicator-${input.indicator}"]`),
|
||||
).toHaveCount(0, { timeout: 5_000 });
|
||||
}
|
||||
|
||||
async function expectWorkspaceRowInStatusBucket(
|
||||
page: Page,
|
||||
input: { serverId: string; workspaceId: string; bucket: string },
|
||||
) {
|
||||
await page.getByTestId("sidebar-display-preferences-menu").click();
|
||||
await page.getByTestId("sidebar-grouping-status").click();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId(`sidebar-status-group-rows-${input.bucket}`)
|
||||
.getByTestId(`sidebar-workspace-row-${input.serverId}:${input.workspaceId}`),
|
||||
).toBeVisible({ timeout: 30_000 });
|
||||
}
|
||||
|
||||
async function fetchLegacyAgent(client: RestartDaemonClient) {
|
||||
const agents = await client.fetchAgents({ scope: "active" });
|
||||
return agents.entries.find(hasLegacyAgentId)?.agent ?? null;
|
||||
}
|
||||
|
||||
function hasLegacyAgentId(entry: { agent: { id: string } }): boolean {
|
||||
return entry.agent.id === LEGACY_AGENT_ID;
|
||||
}
|
||||
|
||||
async function fetchWorkspaceStatuses(
|
||||
client: RestartDaemonClient,
|
||||
workspaceIds: string[],
|
||||
): Promise<Record<string, string>> {
|
||||
const workspaces = await client.fetchWorkspaces();
|
||||
const wantedWorkspaceIds = new Set(workspaceIds);
|
||||
const statuses: Record<string, string> = {};
|
||||
for (const workspace of workspaces.entries) {
|
||||
if (wantedWorkspaceIds.has(workspace.id)) {
|
||||
statuses[workspace.id] = workspace.status;
|
||||
}
|
||||
}
|
||||
return statuses;
|
||||
}
|
||||
|
||||
test.describe("Workspace model restart regressions", () => {
|
||||
test("browser-created same-cwd workspace preserves restarted agent status and migrated tab ownership", async ({
|
||||
page,
|
||||
baseURL,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
const seeded = await seedRestartHome();
|
||||
const origin = new URL(baseURL ?? "http://localhost").origin;
|
||||
const daemon = await startRestartDaemon({ paseoHome: seeded.paseoHome, origin });
|
||||
const serverId = SERVER_ID;
|
||||
const client = await connectRestartDaemonClient(daemon.port);
|
||||
|
||||
try {
|
||||
await seedBrowserForDaemon(page, { serverId, port: daemon.port });
|
||||
|
||||
await expect
|
||||
.poll(() => fetchLegacyAgent(client))
|
||||
.toMatchObject({
|
||||
id: LEGACY_AGENT_ID,
|
||||
workspaceId: seeded.workspaceA,
|
||||
status: "running",
|
||||
});
|
||||
|
||||
await page.goto(buildHostWorkspaceRoute(serverId, seeded.workspaceA));
|
||||
await waitForSidebarHydration(page);
|
||||
await expectWorkspaceRowHasOnlyIndicator(page, {
|
||||
serverId,
|
||||
workspaceId: seeded.workspaceA,
|
||||
indicator: "running",
|
||||
});
|
||||
await expectWorkspaceRowDoesNotShowIndicator(page, {
|
||||
serverId,
|
||||
workspaceId: seeded.workspaceB,
|
||||
indicator: "running",
|
||||
});
|
||||
await expect
|
||||
.poll(() => getVisibleWorkspaceAgentTabIds(page), { timeout: 30_000 })
|
||||
.toContain(`workspace-tab-agent_${LEGACY_AGENT_ID}`);
|
||||
|
||||
const reconciledProjectKey = (await client.listProjects()).projects.find(
|
||||
(project) => project.projectId === seeded.projectId,
|
||||
);
|
||||
if (!reconciledProjectKey?.projectKey) {
|
||||
throw new Error(`Workspace ${seeded.workspaceA} was not reconciled with a project key`);
|
||||
}
|
||||
|
||||
await openGlobalNewWorkspaceComposer(page);
|
||||
await selectNewWorkspaceProject(page, {
|
||||
projectKey: reconciledProjectKey.projectKey,
|
||||
projectDisplayName: seeded.projectDisplayName,
|
||||
});
|
||||
await expectNewWorkspaceProjectSelected(page, seeded.projectDisplayName);
|
||||
await submitNewWorkspaceEmpty(page);
|
||||
|
||||
await expect
|
||||
.poll(() => {
|
||||
const workspaceId = parseWorkspaceIdFromPageUrl(page, serverId);
|
||||
return workspaceId && workspaceId !== seeded.workspaceA ? workspaceId : null;
|
||||
})
|
||||
.not.toBeNull();
|
||||
const createdWorkspaceId = parseWorkspaceIdFromPageUrl(page, serverId);
|
||||
if (!createdWorkspaceId) {
|
||||
throw new Error(`Expected browser to navigate to created workspace, got ${page.url()}`);
|
||||
}
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
fetchWorkspaceStatuses(client, [
|
||||
seeded.workspaceA,
|
||||
seeded.workspaceB,
|
||||
createdWorkspaceId,
|
||||
]),
|
||||
)
|
||||
.toEqual({
|
||||
[seeded.workspaceA]: "running",
|
||||
[seeded.workspaceB]: "done",
|
||||
[createdWorkspaceId]: "done",
|
||||
});
|
||||
|
||||
await expectWorkspaceRowDoesNotShowIndicator(page, {
|
||||
serverId,
|
||||
workspaceId: seeded.workspaceB,
|
||||
indicator: "running",
|
||||
});
|
||||
await expectWorkspaceRowDoesNotShowIndicator(page, {
|
||||
serverId,
|
||||
workspaceId: createdWorkspaceId,
|
||||
indicator: "running",
|
||||
});
|
||||
await expectWorkspaceRowInStatusBucket(page, {
|
||||
serverId,
|
||||
workspaceId: seeded.workspaceA,
|
||||
bucket: "running",
|
||||
});
|
||||
await expectWorkspaceRowInStatusBucket(page, {
|
||||
serverId,
|
||||
workspaceId: seeded.workspaceB,
|
||||
bucket: "done",
|
||||
});
|
||||
await expectWorkspaceRowInStatusBucket(page, {
|
||||
serverId,
|
||||
workspaceId: createdWorkspaceId,
|
||||
bucket: "done",
|
||||
});
|
||||
await expect
|
||||
.poll(() => getVisibleWorkspaceAgentTabIds(page), { timeout: 30_000 })
|
||||
.toEqual([]);
|
||||
|
||||
await page.goto(buildHostWorkspaceRoute(serverId, seeded.workspaceA));
|
||||
await expect
|
||||
.poll(() => getVisibleWorkspaceAgentTabIds(page), { timeout: 30_000 })
|
||||
.toContain(`workspace-tab-agent_${LEGACY_AGENT_ID}`);
|
||||
} finally {
|
||||
await client.close().catch(() => undefined);
|
||||
await daemon.close();
|
||||
seeded.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user