Files
paseo/packages/app/e2e/workspace-setup-runtime.spec.ts
Mohamed Boudra 2e1b907c63 Improve Playwright E2E test quality (#1210)
* chore(e2e): begin Playwright test-quality pass

* test(e2e): migrate terminal-performance spec onto TerminalE2EHarness

Replace the hand-rolled daemon-client lifecycle (connectTerminalClient +
openProject + createTerminal/navigateToTerminal/killTerminal) with the shared
TerminalE2EHarness, matching terminal-keystroke-stress.spec.ts. The spec now
seeds the workspace and terminals through the harness vocabulary instead of
duplicating setup, cutting the file by ~40 lines with no behavior change.

* test(e2e): extract shared mock-agent workspace helper

Five specs reimplemented the same setup: seed a temp repo, open it as a
project, create a mock-provider agent, then navigate to its workspace route.
Extract seedMockAgentWorkspace + openAgentRoute into helpers/mock-agent.ts and
migrate the rewind-menu and user-message-contract UI-contract specs onto them,
dropping their local getServerId/openAgent/inline-seed duplication.

* test(e2e): migrate terminal tab rename spec onto TerminalE2EHarness

Replace the spec's hand-rolled daemon client setup (connectTerminalClient +
openProject + createTerminal + navigateToTerminal + manual cleanup) with the
shared TerminalE2EHarness and withTerminalInApp helpers, matching the other
terminal specs. Behavior and assertions are unchanged.

* test(e2e): share openProjectViaDaemon across sidebar specs

Both sidebar-workspace and sidebar-workspace-rename rolled their own
identical openProjectViaDaemon helper against the workspace-setup daemon
client. Promote a single shared helper into helpers/workspace-setup.ts
(returning id/name/workspaceDirectory) and have seedProjectForWorkspaceSetup
delegate to it, removing the duplicated open-project logic.

* test(e2e): migrate client-slash-commands spec onto shared mock-agent helper

Replace the spec's hand-rolled openProject/createReadyMockAgent/
openActiveAgentTab/getServerId setup with seedMockAgentWorkspace and
openAgentRoute from helpers/mock-agent, matching the rewind-menu and
user-message-contract specs. Drops ~60 lines of duplicated daemon-client
wiring; the test bodies now read as domain intent.

* test(e2e): converge remaining workspace-setup specs onto shared daemon helpers

Replace the last inline openProject + null-check blocks with the shared
openProjectViaDaemon helper in the file-explorer-collapse and pr-pane specs,
and route workspace-setup-runtime project registration through
seedProjectForWorkspaceSetup. Behavior is identical; the seeding vocabulary now
lives entirely in helpers/workspace-setup.ts.

* test(e2e): migrate composer-autocomplete spec onto shared mock-agent helper

Replace the spec's hand-rolled daemon client setup (connectTerminalClient +
openProject + createAgent + route building + bespoke cleanup) with
seedMockAgentWorkspace + openAgentRoute from helpers/mock-agent, matching the
client-slash-commands migration. Drops the local getServerId, openProject, and
cleanupWithin helpers.

* test(e2e): migrate codex-plan-approval spec onto shared mock-agent helper

Replace the hand-rolled daemon client, project open, agent creation, and
manual route building with seedMockAgentWorkspace + openAgentRoute. The spec
now reads as intent: seed a mock agent, open it, approve the plan, assert the
panel clears.

* test(e2e): converge daemon-client bootstrap onto one shared factory

The five e2e helpers each rolled their own daemon-client connect logic —
duplicating the ws-url resolution, node WebSocket factory, client
construction, and connect call, plus a redeclared config interface.

Extract a single connectDaemonClient factory in daemon-client-loader.ts
that each helper delegates to with its own typed client interface. This
also propagates the port-6767 safety guard (previously only in two
helpers) to all of them, so no test client can target the developer
daemon. Specs are untouched; behavior is preserved.

* test(e2e): promote shared daemon seed client out of terminal-perf helper

The general-purpose E2E daemon client (workspace/agent/terminal seeding and
driving) had grown inside terminal-perf.ts under the name
TerminalPerfDaemonClient, even though most consumers — mock-agent, composer,
rewind-flow, and the launcher/title-handoff specs — have nothing to do with
terminal performance. Move the interface and its connect factory into a
neutrally-named helpers/seed-client.ts (SeedDaemonClient / connectSeedClient)
and point every consumer at it. terminal-perf.ts keeps only its terminal page
helpers. Pure rename/relocate; no behavior change.

* test(e2e): converge E2E_SERVER_ID lookup onto one shared helper

The server-id env accessor was copy-pasted as a local `getServerId`
function in six helpers and five specs (plus a `requireServerId` twin in
the sidebar helper). Extract a single `helpers/server-id.ts` accessor and
route every caller through it, so a new spec imports the vocabulary
instead of re-deriving it. Pure refactor — identical lookup behavior.

* test(e2e): finish converging server-id lookup onto the shared helper

The previous pass extracted helpers/server-id.ts but only routed callers
that wrapped the env read in a local function. Eight specs still re-derived
the lookup inline — some as their own copy-pasted getSeededServerId/
getE2EServerId functions, some as bare `process.env.E2E_SERVER_ID` blocks
inside test bodies. Route them all through getServerId() so a new spec
imports the vocabulary instead of re-deriving it. Pure refactor — identical
lookup behavior; daemon-port reads are left untouched.

* test(e2e): converge workspace seeding onto a shared seedWorkspace helper

Extract the repeated temp-repo + seed-client + openProject bootstrap into
seedWorkspace() in helpers/seed-client.ts, returning a {client, repoPath,
workspaceId, cleanup} handle. seedMockAgentWorkspace and the agent-title-handoff
spec now build on it instead of re-rolling the trio, so the open-project error
handling and teardown live in one place.

* test(e2e): converge launcher-tab spec onto the shared seedWorkspace helper

Replace the hand-rolled createTempGitRepo + connectSeedClient + openProject
trio in beforeAll with seedWorkspace(), and drop the redundant second seed
client in the terminal-title block in favor of the shared workspace.client.

* test(e2e): converge file-explorer-collapse onto the shared seedWorkspace helper

Teach seedWorkspace to forward createTempGitRepo options (files/branches/
remote) so file-seeding specs can drop their hand-rolled
createTempGitRepo + connect + openProjectViaDaemon trio. Migrate
file-explorer-collapse onto it, removing its bespoke WorkspaceSetup client setup.

* test(e2e): converge non-git project setup onto a shared workspace helper

Promote the non-git createTempDirectory out of sidebar-workspace.spec.ts
into helpers/workspace.ts next to createTempGitRepo, and dedupe the
copy-pasted temp-root resolution (workspace.ts, with-workspace.ts, and the
spec) into one shared resolveTempRoot(). The spec drops its low-level
node:fs imports and reads in domain terms.

* test(e2e): converge agent-tab-rename spec onto the shared seedWorkspace helper

Drop the bespoke daemon-client + temp-repo + manual-cleanup trio in
workspace-agent-tab-rename and seed through seedWorkspace, matching the
other converged specs. createIdleAgent now takes a minimal structural
client interface so it accepts either the archive-tab client or the
shared seed client (type-only; the existing archive-tab callers are
unchanged). Verified by running the spec on Desktop Chrome.

* test(e2e): converge pane-remount spec onto the shared seedWorkspace helper

Drop the bespoke archive-tab daemon client + temp-repo + manual-cleanup
trio in workspace-pane-remount and seed through seedWorkspace, matching
the other converged specs. createIdleAgent already accepts the shared
seed client structurally, so the only behavior change is teardown now
goes through workspace.cleanup(). Verified by running the spec on
Desktop Chrome.

* test(e2e): converge sidebar-workspace-rename onto the shared seedWorkspace helper

Expose workspaceName and workspaceDirectory on SeededWorkspace (sourced from
the open-project response) so branch-rename specs can read the resolved branch
name and checkout directory without a bespoke client. Migrate
sidebar-workspace-rename off its hand-rolled connectWorkspaceSetupClient +
createTempGitRepo + openProjectViaDaemon trio, dropping the per-test
client/repo cleanup in favor of seedWorkspace's single cleanup handle.

* test(e2e): converge sidebar-workspace list onto the shared seedWorkspace helper

Migrate all five "Sidebar workspace list" tests off their hand-rolled
connectWorkspaceSetupClient + createTempGitRepo/createTempDirectory +
openProjectViaDaemon trio onto seedWorkspace, collapsing each test's
client/repo cleanup into the single seedWorkspace cleanup handle and dropping
the spec-local setGitHubRemote/execSync machinery.

Two small helper additions make the full file converge:

- seedWorkspace gains a `git: false` option that seeds a plain non-git
  directory (via createTempDirectory) instead of a git repo, so the non-git
  project test gets the same single-handle treatment.
- createTempGitRepo's configureRemote now relabels origin to a display URL
  when both `withRemote` and `originUrl` are given: it sets up the local
  tracking remote, pushes, then `git remote set-url` to the GitHub URL. This
  reproduces the prior withRemote + setGitHubRemote git state exactly (real
  local tracking refs, GitHub origin URL for project grouping) in one fixture
  call, so the GitHub-remote tests are behavior-preserving.

* test(e2e): converge projects-settings onto the shared seedWorkspace helper

Replace the per-fixture connectNewWorkspaceDaemonClient + createTempGitRepo +
openProjectViaDaemon trio with seedWorkspace(), and expose the daemon's
projectId/projectDisplayName on SeededWorkspace so fixtures can read the
project label directly. All 9 specs pass.

* test(e2e): converge composer-attachments onto the shared seedWorkspace helper

Replace the inline connectNewWorkspaceDaemonClient + createTempGitRepo +
openProjectViaDaemon trio in the "composer is locked while new workspace agent
is being created" test with a single seedWorkspace() call, dropping the manual
client.close()/repo.cleanup() teardown in favor of workspace.cleanup(). The
test still passes against a real daemon.

* test(e2e): converge settings-toggle-tab-regression onto the shared seedWorkspace helper

Both tests rolled their own daemon client + temp git repo + manual agent
archive cleanup. Replace that trio with seedWorkspace(), drive idle agents
through workspace.client, and route off workspace.repoPath, leaving cleanup
to workspace.cleanup(). Matches the pattern already used by
workspace-pane-remount and workspace-agent-tab-rename.

* test(e2e): converge workspace-navigation-regression onto the shared seedWorkspace helper

Replace the bespoke connect/openProject/createTempGitRepo/archive trios in
the reconnect, cold-URL, and sidebar-navigation tests with seedWorkspace(),
matching the other migrated specs. Cleanup collapses to workspace.cleanup().

* test(e2e): drop orphaned dead code from agent-bottom-anchor helper

The agent-bottom-anchor spec was removed in a prior cleanup, but its
helper kept a private daemon-client interface and connect fn (duplicating
the shared seed client), seedBottomAnchorAgent, the reply-message builders,
and several scroll helpers that no spec references anymore. Only
readScrollMetrics, expectNearBottom, and waitForContentGrowth are still
used (by agent-stream.ts); keep those and delete the rest.

* test(e2e): converge archive-tab daemon client onto the shared seed client

Fold archiveAgent and fetchAgentHistory into the canonical SeedDaemonClient
and route the archive-tab and sessions-empty specs through connectSeedClient,
deleting the bespoke ArchiveTabDaemonClient wrapper. Both specs only need
general-purpose agent seed/drive operations, so they now share one client
interface instead of re-declaring their own.

* test(e2e): derive workspace-setup daemon client from the real client type

Replace the hand-rolled WorkspaceSetupDaemonClient interface with a
Pick<InternalDaemonClient, ...> over the real daemon client, matching the
pattern already used by the new-workspace helper. The 45-line re-declaration
of RPC method signatures could silently drift from the protocol; deriving it
from the source of truth keeps the test client honest and shrinks the helper.

* test(e2e): converge duplicated escapeRegex onto one shared helper

Seven byte-identical copies of escapeRegex lived across three specs and four
helpers. Extract a single helpers/regex.ts and route every caller through it,
so the suite has one regex-escaping primitive instead of re-declaring the same
pure function per file.

* test(e2e): converge E2E_DAEMON_PORT resolution onto one shared accessor

The isolated test daemon's port was re-read from the environment in ~10
places — three local helper functions in specs, two inline blocks, and
several helper modules — each repeating the "throw if unset" check and,
in the safety-critical paths, the "refuse the developer daemon (6767)"
guard.

Add helpers/daemon-port.ts exporting getE2EDaemonPort(), mirroring
getServerId(), and route every reader through it. The 6767 guard now
applies everywhere: the test port is never legitimately 6767, so
refusing it uniformly keeps every spec off the developer daemon.

While here, route the two inline port-regex escapes through the existing
escapeRegex helper instead of hand-inlining the same pattern.

* test(e2e): capture create-agent cwd via shared WS-frame helper

workspace-cwd's draft-agent test rolled its own request recorder by
monkeypatching WebSocket.prototype.send and stashing frames on a
window global, then reading them back through page.evaluate. Replace
that with captureWsSessionFrames — the same outbound-frame helper four
other specs already use for create_agent_request — so the assertion
reads the cwd directly and the spec drops the browser-side internals
reach-around. No behavior change; the three cwd cases still pass.

* test(e2e): converge daemon WS-route regex onto one shared helper

Five sites rebuilt the Playwright routeWebSocket matcher for the E2E
daemon inline as `new RegExp(`:${escapeRegex(getE2EDaemonPort())}\b`)`
(new-workspace, project-settings, composer-autocomplete, and two in
workspace-navigation-regression), and startup-dsl rolled the same regex
for arbitrary blocked test-host ports. Add daemonWsRoutePattern() and
wsRoutePatternForPort(port) to daemon-port.ts — whose docstring already
promised route patterns live here — and point every site at them.

The emitted regex is byte-identical, so interception behavior is
unchanged; composer-autocomplete still passes. Drops the now-unused
escapeRegex/getE2EDaemonPort imports and the redundant daemonPort locals.
2026-05-29 09:46:15 +08:00

89 lines
3.2 KiB
TypeScript

import { existsSync } from "node:fs";
import { expect, test } from "./fixtures";
import { createTempGitRepo } from "./helpers/workspace";
import { clickNewTerminal } from "./helpers/launcher";
import { expectTerminalSurfaceVisible } from "./helpers/terminal-perf";
import {
connectWorkspaceSetupClient,
createWorkspaceThroughDaemon,
findWorktreeWorkspaceForProject,
navigateToWorkspaceViaSidebar,
openHomeWithProject,
seedProjectForWorkspaceSetup,
} from "./helpers/workspace-setup";
test.describe("Workspace setup runtime authority", () => {
test.describe.configure({ retries: 1 });
test("worktree workspace is created in its own directory", async ({ page }) => {
test.setTimeout(90_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("workspace-setup-chat-");
try {
await seedProjectForWorkspaceSetup(client, repo.path);
const workspace = await createWorkspaceThroughDaemon(client, {
cwd: repo.path,
worktreeSlug: `setup-chat-${Date.now()}`,
});
const workspaceId = workspace.id;
const wsInfo = await findWorktreeWorkspaceForProject(client, repo.path);
expect(wsInfo.workspaceDirectory).not.toBe(repo.path);
expect(existsSync(wsInfo.workspaceDirectory)).toBe(true);
await openHomeWithProject(page, repo.path);
await navigateToWorkspaceViaSidebar(page, workspaceId);
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
} finally {
await client.close();
await repo.cleanup();
}
});
test("first terminal opens in the created workspace directory", async ({ page }) => {
test.setTimeout(90_000);
const client = await connectWorkspaceSetupClient();
const repo = await createTempGitRepo("workspace-setup-terminal-");
try {
await seedProjectForWorkspaceSetup(client, repo.path);
// Create workspace via daemon API since the new workspace screen
// no longer has a standalone terminal button
const worktreeSlug = `setup-terminal-${Date.now()}`;
const result = await client.createPaseoWorktree({
cwd: repo.path,
worktreeSlug,
});
if (!result.workspace || result.error) {
throw new Error(result.error ?? "Failed to create workspace");
}
const workspaceDir = result.workspace.workspaceDirectory;
const workspaceId = result.workspace.id;
// Navigate to the worktree workspace via sidebar click (direct URL
// navigation for freshly created worktree workspaces can race with
// Expo Router hydration, so we use the sidebar which is authoritative).
await openHomeWithProject(page, repo.path);
await navigateToWorkspaceViaSidebar(page, workspaceId);
await clickNewTerminal(page);
await expectTerminalSurfaceVisible(page);
// Verify terminal is listed under the worktree directory, not the original repo
await expect
.poll(async () => (await client.listTerminals(workspaceDir)).terminals.length > 0, {
timeout: 30_000,
})
.toBe(true);
expect((await client.listTerminals(repo.path)).terminals.length).toBe(0);
} finally {
await client.close();
await repo.cleanup();
}
});
});