* 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
143 lines
5.8 KiB
TypeScript
143 lines
5.8 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { existsSync } from "node:fs";
|
|
import { expect, type Page } from "@playwright/test";
|
|
import { metroTest as test } from "../support/fixtures";
|
|
import { gotoAppShell } from "../support/helpers/app";
|
|
import {
|
|
createIdleAgent,
|
|
expectSessionRowArchived,
|
|
openSessions,
|
|
} from "../support/helpers/archive-tab";
|
|
import { buildCreateAgentPreferences, buildSeededHost } from "../support/helpers/daemon-registry";
|
|
import {
|
|
startIsolatedHostDaemon,
|
|
type IsolatedHostDaemon,
|
|
} from "../support/helpers/isolated-host-daemon";
|
|
import {
|
|
archiveWorkspaceFromDaemon,
|
|
connectNewWorkspaceDaemonClient,
|
|
createWorktreeViaDaemon,
|
|
openProjectViaDaemon,
|
|
} from "../support/helpers/new-workspace";
|
|
import { connectSeedClient } from "../support/helpers/seed-client";
|
|
import { createTempGitRepo } from "../support/helpers/workspace";
|
|
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
|
|
|
test.describe("Worktree restore after daemon restart", () => {
|
|
const serverId = `srv_worktree_restart_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
let daemon: IsolatedHostDaemon;
|
|
let client: Awaited<ReturnType<typeof connectSeedClient>>;
|
|
let worktreeClient: Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>;
|
|
let tempRepo: { path: string; cleanup: () => Promise<void> };
|
|
const createdWorktreeDirectories = new Set<string>();
|
|
const createdProjectIds = new Set<string>();
|
|
|
|
test.describe.configure({ retries: 0, timeout: 180_000 });
|
|
|
|
test.beforeEach(async () => {
|
|
daemon = await startIsolatedHostDaemon(serverId);
|
|
client = await connectSeedClient({ port: daemon.port });
|
|
worktreeClient = await connectNewWorkspaceDaemonClient({
|
|
port: daemon.port,
|
|
ownProjects: false,
|
|
});
|
|
tempRepo = await createTempGitRepo("wt-restart-");
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
for (const directory of createdWorktreeDirectories) {
|
|
await archiveWorkspaceFromDaemon(worktreeClient, directory).catch(() => undefined);
|
|
}
|
|
createdWorktreeDirectories.clear();
|
|
for (const projectId of createdProjectIds) {
|
|
await worktreeClient.removeProject(projectId).catch(() => undefined);
|
|
}
|
|
createdProjectIds.clear();
|
|
await client?.close().catch(() => undefined);
|
|
await worktreeClient?.close().catch(() => undefined);
|
|
await tempRepo?.cleanup().catch(() => undefined);
|
|
await daemon?.close().catch(() => undefined);
|
|
});
|
|
|
|
async function seedBrowser(page: Page) {
|
|
const nowIso = new Date().toISOString();
|
|
await page.addInitScript(
|
|
({ host, preferences }) => {
|
|
localStorage.setItem("@paseo:e2e", "1");
|
|
localStorage.setItem("@paseo:daemon-registry", JSON.stringify([host]));
|
|
localStorage.removeItem("@paseo:settings");
|
|
localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(preferences));
|
|
},
|
|
{
|
|
host: buildSeededHost({
|
|
serverId,
|
|
endpoint: `127.0.0.1:${daemon.port}`,
|
|
label: "restart daemon",
|
|
nowIso,
|
|
}),
|
|
preferences: buildCreateAgentPreferences(serverId),
|
|
},
|
|
);
|
|
}
|
|
|
|
test("after archiving a worktree and restarting the daemon, History shows the worktree branch (not main) before any restore", async ({
|
|
page,
|
|
}) => {
|
|
// A paseo worktree is cut on its own branch named after the slug, and the
|
|
// worktree workspace is displayed under the same name. These are the values
|
|
// the History table cells must show after restore — never "main".
|
|
const worktreeSlug = `restart-restore-${randomUUID().slice(0, 8)}`;
|
|
|
|
const project = await openProjectViaDaemon(worktreeClient, tempRepo.path);
|
|
createdProjectIds.add(project.projectKey);
|
|
const worktree = await createWorktreeViaDaemon(worktreeClient, {
|
|
cwd: tempRepo.path,
|
|
slug: worktreeSlug,
|
|
});
|
|
createdProjectIds.add(worktree.projectKey);
|
|
createdWorktreeDirectories.add(worktree.workspaceDirectory);
|
|
|
|
const agent = await createIdleAgent(client, {
|
|
cwd: worktree.workspaceDirectory,
|
|
workspaceId: worktree.workspaceId,
|
|
title: `restart-restore-${randomUUID().slice(0, 8)}`,
|
|
});
|
|
expect(existsSync(worktree.workspaceDirectory)).toBe(true);
|
|
|
|
// Archive through the default production path (no scope): the worktree dir is deleted.
|
|
await archiveWorkspaceFromDaemon(worktreeClient, worktree.workspaceDirectory);
|
|
await expect
|
|
.poll(() => existsSync(worktree.workspaceDirectory), { timeout: 30_000 })
|
|
.toBe(false);
|
|
|
|
// Restart this spec's daemon on the same home and port so it rebuilds all
|
|
// workspace/agent links from persisted state without replacing the shared
|
|
// Playwright daemon owned by global setup.
|
|
await client.close().catch(() => undefined);
|
|
await worktreeClient.close().catch(() => undefined);
|
|
await daemon.restart();
|
|
client = await connectSeedClient({ port: daemon.port });
|
|
worktreeClient = await connectNewWorkspaceDaemonClient({
|
|
port: daemon.port,
|
|
ownProjects: false,
|
|
});
|
|
|
|
await seedBrowser(page);
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openSessions(page);
|
|
await expectSessionRowArchived(page, agent.title);
|
|
|
|
// KEY ASSERTION: reproduce the screenshot state. Right after the daemon
|
|
// restart, with NO restore and NO row click, the rendered History table cells
|
|
// (fed by each agent row's projectPlacement via fetch_agent_history) must read
|
|
// the worktree branch and the worktree workspace name — never "main".
|
|
const branchCell = page.getByTestId(`agent-row-branch-${serverId}-${agent.id}`);
|
|
const workspaceCell = page.getByTestId(`agent-row-workspace-${serverId}-${agent.id}`);
|
|
|
|
await expect(branchCell).toBeVisible({ timeout: 60_000 });
|
|
await expect(branchCell).toHaveText(worktreeSlug, { timeout: 60_000 });
|
|
await expect(workspaceCell).toHaveText(worktree.workspaceName, { timeout: 60_000 });
|
|
});
|
|
});
|