diff --git a/.github/ci-paths.yml b/.github/ci-paths.yml index bf301f8c8..c156ec0a1 100644 --- a/.github/ci-paths.yml +++ b/.github/ci-paths.yml @@ -35,20 +35,15 @@ hub: server: - "packages/server/**" - - "packages/app/e2e/fixtures/recording.*" + - "packages/app/e2e/support/fixtures/recording.*" desktop: - "packages/desktop/**" - -desktop_bridge: - - "packages/desktop/src/*.{ts,tsx}" - - "packages/desktop/src/features/browser-*.{ts,tsx}" - - "packages/desktop/src/features/browser-*/**" - - "packages/desktop/scripts/browser-*.mjs" - - "packages/desktop/*.{json,yml}" - - "packages/app/src/**/browser-*.{ts,tsx}" - - "packages/app/src/**/browser-*/**" - - "packages/app/*.{cjs,js,json,ts}" + - "packages/app/src/desktop/**" + - "packages/server/src/server/browser-tools/**" + - "packages/app/e2e/support/**" + - "packages/app/*config.{cjs,js,ts}" + - "packages/app/package.json" app: - "packages/app/**" @@ -59,21 +54,16 @@ sdk: - "packages/highlight/**" - "packages/protocol/**" -playwright: - - "packages/app/src/**" - - "packages/app/e2e/**" +browser: + - "packages/app/src/!(desktop)/**" + - "packages/app/e2e/browser/**" + - "packages/app/e2e/support/**" - "packages/app/assets/**" - "packages/app/public/**" - "packages/app/index.ts" - "packages/app/*config.{cjs,js,ts}" - "packages/app/package.json" -playwright_desktop: - - "packages/app/src/**/*.electron.{ts,tsx}" - - "packages/app/e2e/**" - - "packages/app/*config.{cjs,js,ts}" - - "packages/app/package.json" - relay: - "packages/relay/**" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 504d34bd1..d516ccf8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,11 +30,9 @@ jobs: hub: ${{ steps.filter.outputs.hub }} server: ${{ steps.filter.outputs.server }} desktop: ${{ steps.filter.outputs.desktop }} - desktop_bridge: ${{ steps.filter.outputs.desktop_bridge }} app: ${{ steps.filter.outputs.app }} sdk: ${{ steps.filter.outputs.sdk }} - playwright: ${{ steps.filter.outputs.playwright }} - playwright_desktop: ${{ steps.filter.outputs.playwright_desktop }} + browser: ${{ steps.filter.outputs.browser }} relay: ${{ steps.filter.outputs.relay }} cli: ${{ steps.filter.outputs.cli }} steps: @@ -188,7 +186,7 @@ jobs: desktop-tests-ubuntu: name: desktop-tests (ubuntu-latest) needs: changes - if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop != 'false' || needs.changes.outputs.desktop_bridge != 'false') }} + if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop != 'false') }} runs-on: ubuntu-latest timeout-minutes: 30 permissions: @@ -208,34 +206,37 @@ jobs: run: npm run build:server - name: Run desktop tests - if: ${{ needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop != 'false' }} run: npm run test --workspace=@getpaseo/desktop - - name: Build app dependencies for desktop E2E - if: ${{ runner.os == 'Linux' && (needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop_bridge != 'false') }} - run: npm run build:app-deps - - name: Install virtual display - if: ${{ runner.os == 'Linux' && (needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop_bridge != 'false') }} + if: ${{ runner.os == 'Linux' }} run: sudo apt-get update && sudo apt-get install -y xvfb xauth - - name: Run real Electron browser tab bridge E2E - if: ${{ runner.os == 'Linux' && (needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop_bridge != 'false') }} - run: npm run test:e2e:browser-tab-bridge --workspace=@getpaseo/desktop - env: - PASEO_TAB_BRIDGE_E2E_ARTIFACT_DIR: ${{ runner.temp }}/browser-tab-bridge-e2e + - name: Install Playwright browser + if: ${{ runner.os == 'Linux' }} + run: npx playwright install chromium - - name: Upload browser tab bridge diagnostics + - name: Run desktop renderer E2E + if: ${{ runner.os == 'Linux' }} + run: npm run test:e2e:renderer --workspace=@getpaseo/desktop + + - name: Run desktop browser E2E + if: ${{ runner.os == 'Linux' }} + run: npm run test:e2e:browser-tabs --workspace=@getpaseo/desktop + env: + PASEO_DESKTOP_BROWSER_E2E_ARTIFACT_DIR: ${{ runner.temp }}/desktop-browser-e2e + + - name: Upload desktop browser diagnostics uses: actions/upload-artifact@v4 - if: ${{ failure() && runner.os == 'Linux' && (needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop_bridge != 'false') }} + if: ${{ failure() && runner.os == 'Linux' }} with: - name: browser-tab-bridge-e2e - path: ${{ runner.temp }}/browser-tab-bridge-e2e + name: desktop-browser-e2e + path: ${{ runner.temp }}/desktop-browser-e2e if-no-files-found: ignore retention-days: 7 - name: Build and smoke unpacked desktop app - if: ${{ runner.os == 'Linux' && (needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop != 'false') }} + if: ${{ runner.os == 'Linux' }} run: npm run build:desktop -- --publish never --linux --x64 --dir env: EP_GH_IGNORE_TIME: true @@ -244,7 +245,7 @@ jobs: - name: Upload packaged smoke diagnostics uses: actions/upload-artifact@v4 - if: ${{ failure() && runner.os == 'Linux' && (needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop != 'false') }} + if: ${{ failure() && runner.os == 'Linux' }} with: name: desktop-packaged-smoke-linux-x64 path: ${{ runner.temp }}/desktop-smoke @@ -325,7 +326,7 @@ jobs: playwright-1: name: playwright (shard 1/4) needs: changes - if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.playwright != 'false') }} + if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }} runs-on: ubuntu-latest env: ELECTRON_SKIP_BINARY_DOWNLOAD: "1" @@ -373,7 +374,7 @@ jobs: playwright-2: name: playwright (shard 2/4) needs: changes - if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.playwright != 'false') }} + if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }} runs-on: ubuntu-latest env: ELECTRON_SKIP_BINARY_DOWNLOAD: "1" @@ -384,7 +385,7 @@ jobs: playwright-3: name: playwright (shard 3/4) needs: changes - if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.playwright != 'false') }} + if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }} runs-on: ubuntu-latest env: ELECTRON_SKIP_BINARY_DOWNLOAD: "1" @@ -395,7 +396,7 @@ jobs: playwright-4: name: playwright (shard 4/4) needs: changes - if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.playwright != 'false') }} + if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }} runs-on: ubuntu-latest env: ELECTRON_SKIP_BINARY_DOWNLOAD: "1" @@ -403,47 +404,6 @@ jobs: PLAYWRIGHT_ARTIFACT: "4" steps: *playwright_test_steps - playwright-desktop: - name: playwright (desktop overlay) - needs: changes - if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.playwright_desktop != 'false') }} - runs-on: ubuntu-latest - env: - ELECTRON_SKIP_BINARY_DOWNLOAD: "1" - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "npm" - - - name: Install dependencies with retry - run: node scripts/npm-retry.mjs ci - - - name: Install Playwright browsers - timeout-minutes: 10 - run: npx playwright install chromium - - - name: Build app dependencies - run: npm run build:app-deps - - - name: Build server stack - run: npm run build:server - - - name: Run desktop-overlay Playwright tests - run: npm run test:e2e:desktop --workspace=@getpaseo/app - - - name: Upload test artifacts - uses: actions/upload-artifact@v4 - if: failure() - with: - name: playwright-results-desktop - path: | - packages/app/test-results/ - packages/app/playwright-report/ - retention-days: 7 - relay-tests: name: relay-tests needs: changes diff --git a/.oxlintrc.json b/.oxlintrc.json index bdcab1dc5..ae06288f4 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -189,9 +189,9 @@ "packages/app/src/components/agent-list.tsx", "packages/app/src/components/agent-status-dot.tsx", "packages/app/src/components/attachment-lightbox.tsx", - "packages/app/src/components/browser-pane.electron.tsx", - "packages/app/src/components/browser-pane.tsx", - "packages/app/src/components/browser-pane.web.tsx", + "packages/app/src/desktop/browser/pane/index.electron.tsx", + "packages/app/src/desktop/browser/pane/index.tsx", + "packages/app/src/desktop/browser/pane/index.web.tsx", "packages/app/src/components/command-center.tsx", "packages/app/src/components/context-window-meter.tsx", "packages/app/src/components/dictation-controls.tsx", @@ -280,7 +280,7 @@ } }, { - "files": ["**/e2e/fixtures.ts"], + "files": ["**/e2e/**/fixtures.ts"], "rules": { "no-empty-pattern": "off" } diff --git a/CLAUDE.md b/CLAUDE.md index a763b616f..d47222b46 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -148,12 +148,12 @@ The app runs on iOS, Android, web (browser), and web (Electron desktop). Code is Import as `@/hooks/use-audio-recorder` — Metro picks the right file automatically. - **Use `.electron.ts` / `.electron.tsx` for Electron-only web modules.** Electron is still the Metro `web` platform, but desktop dev/build sets `PASEO_WEB_PLATFORM=electron`, so Metro first looks for `.electron.*` files and falls back to normal `.web.*` files. Use this when the implementation depends on Electron-only behavior such as `webviewTag`, desktop preload APIs, or the Electron bridge. Keep plain browser web in `.web.*`, and keep native fallbacks in the base file or `.native.*`. ``` - components/ - browser-pane.electron.tsx ← Electron implementation - browser-pane.web.tsx ← plain web fallback - browser-pane.tsx ← native fallback + desktop/browser/pane/ + index.electron.tsx ← Electron implementation + index.web.tsx ← plain web fallback + index.tsx ← native fallback ``` - Import as `@/components/browser-pane` — Electron desktop gets the `.electron.tsx` file, browser web gets `.web.tsx`, and native gets the native/base implementation. + Import as `@/desktop/browser/pane` — Electron desktop gets the `.electron.tsx` file, browser web gets `.web.tsx`, and native gets the native/base implementation. - **NEVER use raw DOM APIs without `isWeb` guard.** DOM APIs crash native. Casting a RN ref to `HTMLElement` is a red flag — ensure the block is web-only. - **NEVER use `onPointerEnter`/`onPointerLeave`.** They don't fire on native iOS. - **Hover only works on web.** React Native's `onHoverIn`/`onHoverOut` on `Pressable` does NOT fire on native iOS/iPad — the underlying W3C pointer events are behind disabled experimental flags. For hover-to-show UI (kebab menus, action buttons), use `isHovered || isNative || isCompact` so the controls are always visible on native and hover-to-show on web. diff --git a/docs/testing.md b/docs/testing.md index 582dcd51e..67b424e46 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -124,14 +124,14 @@ PASEO_DESKTOP_SMOKE_ARTIFACT_DIR=/tmp/paseo-desktop-smoke \ npm run build:desktop -- --publish never --linux --x64 --dir ``` -### Browser tab bridge regression +### Desktop browser regression -The desktop browser tab bridge E2E launches an isolated real daemon, Metro, and Electron app. It forces workspace LRU eviction to reparent the original tab and replace its guest `WebContents`, then makes one MCP call each for tab listing, snapshot, and click against that original browser id. A final MCP wait proves the real target page received the click. +The desktop browser E2E launches an isolated real daemon, Metro, and Electron app. It forces workspace LRU eviction to reparent the original tab and replace its guest `WebContents`, then makes one MCP call each for tab listing, snapshot, and click against that original browser id. A final MCP wait proves the real target page received the click. Run it locally with the same command owned by the Ubuntu `desktop-tests` required check: ```bash -npm run test:e2e:browser-tab-bridge --workspace=@getpaseo/desktop +npm run test:e2e:browser-tabs --workspace=@getpaseo/desktop ``` ## Test organization @@ -154,7 +154,7 @@ Vitest picks up tests by suffix. The suffix tells the runner which category it b | `*.real.e2e.test.ts` | E2E that hits a real provider (Claude/Codex/Copilot/OpenCode/Pi) — needs creds in `packages/server/.env.test` | `npm run test:integration:real` / `test:e2e:real` | | `*.local.e2e.test.ts` | E2E that needs a local-only resource | `npm run test:integration:local` / `test:e2e:local` | -App-level Playwright browser E2E lives in `packages/app/e2e/*.spec.ts` and runs via `npm run test:e2e --workspace=@getpaseo/app` (separate from Vitest E2E). App Playwright specs that hit real providers use `*.real.spec.ts` and run through `npm run test:e2e:real --workspace=@getpaseo/app`; the default app E2E project ignores that suffix so CI does not need provider credentials. +Browser Playwright specs live in `packages/app/e2e/browser/`. Desktop Playwright and real-Electron E2E live in `packages/desktop/e2e/`. Harness code shared by both suites lives in `packages/app/e2e/support/`; neither suite may place specs there. App Playwright specs that hit real providers use `*.real.spec.ts` and run through `npm run test:e2e:real --workspace=@getpaseo/app`; the default browser project ignores that suffix so CI does not need provider credentials. Live provider smoke tests belong in `*.real.e2e.test.ts`, not `*.test.ts`, even when guarded by environment variables. Default unit suites must use deterministic provider adapters/fakes so missing credits, auth outages, and upstream model drift do not block normal CI. @@ -185,7 +185,7 @@ PR checks are routed by the behavior each suite proves, using `.github/ci-paths. Required matrix legs are declared as statically named jobs. Their shared steps use YAML anchors, while job-level `if` conditions let GitHub report an unaffected leg as genuinely skipped without allocating a runner or losing the exact required-check name. -The smallest meaningful contract wins over package ownership. Tiny structural invariants such as daemon launch supervision run unconditionally in the always-running routing job instead of maintaining a transitive file list; this check reads source entrypoints and builds no product. Routed integration contracts use stable domain directories or globs. CLI-side Hub changes select one focused test inside the existing required server jobs, while browser-domain changes select the real Electron bridge step inside the required Ubuntu desktop job. Repository scripts and the shared Vitest configuration run every PR contract because they are cross-cutting toolchain inputs. +The smallest meaningful contract wins over package ownership. Tiny structural invariants such as daemon launch supervision run unconditionally in the always-running routing job instead of maintaining a transitive file list; this check reads source entrypoints and builds no product. Routed integration contracts use stable domain directories. Browser changes select the required Playwright shards; desktop changes select the existing required desktop jobs, with renderer, real-Electron, and packaged-app coverage together in the Ubuntu leg. CLI-side Hub changes select one focused test inside the existing required server jobs. Repository scripts and the shared Vitest configuration run every PR contract because they are cross-cutting toolchain inputs. ## Agent authentication in tests diff --git a/package-lock.json b/package-lock.json index 6f56e046b..8d7342153 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "oxlint": "1.61.0", "oxlint-tsgolint": "^0.22.1", "patch-package": "^8.0.1", + "picomatch": "4.0.5", "playwright": "^1.56.1", "typescript": "^5.9.3", "ws": "^8.20.0" diff --git a/package.json b/package.json index ec45bed38..1f0a9a6ce 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "oxlint": "1.61.0", "oxlint-tsgolint": "^0.22.1", "patch-package": "^8.0.1", + "picomatch": "4.0.5", "playwright": "^1.56.1", "typescript": "^5.9.3", "ws": "^8.20.0" diff --git a/packages/app/e2e/00-sessions-empty.spec.ts b/packages/app/e2e/browser/00-sessions-empty.spec.ts similarity index 85% rename from packages/app/e2e/00-sessions-empty.spec.ts rename to packages/app/e2e/browser/00-sessions-empty.spec.ts index 48cbb9e87..b6998b6af 100644 --- a/packages/app/e2e/00-sessions-empty.spec.ts +++ b/packages/app/e2e/browser/00-sessions-empty.spec.ts @@ -1,9 +1,9 @@ // "00-" prefix is intentional: this file must sort before every other spec. // Sessions history is daemon-global — any agent created by a prior spec hides the empty state. // If the beforeAll probe below fails, a spec sorted before this file is creating agents. -import { test } from "./fixtures"; -import { connectSeedClient } from "./helpers/seed-client"; -import { expectSessionsEmptyState, openSessions } from "./helpers/archive-tab"; +import { test } from "../support/fixtures"; +import { connectSeedClient } from "../support/helpers/seed-client"; +import { expectSessionsEmptyState, openSessions } from "../support/helpers/archive-tab"; test.describe("Sessions screen empty state", () => { test.beforeAll(async () => { diff --git a/packages/app/e2e/acp-provider-catalog.spec.ts b/packages/app/e2e/browser/acp-provider-catalog.spec.ts similarity index 78% rename from packages/app/e2e/acp-provider-catalog.spec.ts rename to packages/app/e2e/browser/acp-provider-catalog.spec.ts index 7671d325c..b3e2529c8 100644 --- a/packages/app/e2e/acp-provider-catalog.spec.ts +++ b/packages/app/e2e/browser/acp-provider-catalog.spec.ts @@ -1,13 +1,13 @@ -import { test } from "./fixtures"; -import { gotoAppShell, openSettings } from "./helpers/app"; -import { getServerId } from "./helpers/server-id"; +import { test } from "../support/fixtures"; +import { gotoAppShell, openSettings } from "../support/helpers/app"; +import { getServerId } from "../support/helpers/server-id"; import { expectProviderInstalledInSettings, installAcpCatalogProvider, openAddProviderArea, openSettingsHost, openSettingsHostSection, -} from "./helpers/settings"; +} from "../support/helpers/settings"; const ACP_PROVIDER = { id: "hermes", diff --git a/packages/app/e2e/add-changed-file-to-chat.spec.ts b/packages/app/e2e/browser/add-changed-file-to-chat.spec.ts similarity index 92% rename from packages/app/e2e/add-changed-file-to-chat.spec.ts rename to packages/app/e2e/browser/add-changed-file-to-chat.spec.ts index 6b5f55001..9307aef33 100644 --- a/packages/app/e2e/add-changed-file-to-chat.spec.ts +++ b/packages/app/e2e/browser/add-changed-file-to-chat.spec.ts @@ -1,7 +1,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; -import { test, expect, type Page } from "./fixtures"; -import { seedMockAgentWorkspace, openAgentRoute } from "./helpers/mock-agent"; +import { test, expect, type Page } from "../support/fixtures"; +import { seedMockAgentWorkspace, openAgentRoute } from "../support/helpers/mock-agent"; function visibleComposer(page: Page) { return page.locator("textarea[data-composer-input]").filter({ visible: true }).first(); diff --git a/packages/app/e2e/add-project-flow.spec.ts b/packages/app/e2e/browser/add-project-flow.spec.ts similarity index 95% rename from packages/app/e2e/add-project-flow.spec.ts rename to packages/app/e2e/browser/add-project-flow.spec.ts index 4705ac090..74c17b804 100644 --- a/packages/app/e2e/add-project-flow.spec.ts +++ b/packages/app/e2e/browser/add-project-flow.spec.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; import { mkdtemp, rm, stat } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { test, expect, type Page } from "./fixtures"; +import { test, expect, type Page } from "../support/fixtures"; import { addProjectFlow, addProjectFlowBack, @@ -14,14 +14,17 @@ import { expectNewWorkspaceForAddedProject, openAddProjectFlow, waitForConnectedHost, -} from "./helpers/add-project-flow"; -import { gotoAppShell } from "./helpers/app"; -import { buildSeededHost } from "./helpers/daemon-registry"; -import { addOfflineHostAndReload } from "./helpers/hosts"; -import { type IsolatedHostDaemon, startIsolatedHostDaemon } from "./helpers/isolated-host-daemon"; -import { expectOpenedProject } from "./helpers/project-picker-ui"; -import { connectSeedClient } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; +} from "../support/helpers/add-project-flow"; +import { gotoAppShell } from "../support/helpers/app"; +import { buildSeededHost } from "../support/helpers/daemon-registry"; +import { addOfflineHostAndReload } from "../support/helpers/hosts"; +import { + type IsolatedHostDaemon, + startIsolatedHostDaemon, +} from "../support/helpers/isolated-host-daemon"; +import { expectOpenedProject } from "../support/helpers/project-picker-ui"; +import { connectSeedClient } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; const EXTRA_HOSTS_KEY = "@paseo:e2e-extra-hosts"; const SECONDARY_HOST_ID = "add-project-flow-secondary"; diff --git a/packages/app/e2e/add-project-github.real.spec.ts b/packages/app/e2e/browser/add-project-github.real.spec.ts similarity index 87% rename from packages/app/e2e/add-project-github.real.spec.ts rename to packages/app/e2e/browser/add-project-github.real.spec.ts index acb7eaab4..4285f05e7 100644 --- a/packages/app/e2e/add-project-github.real.spec.ts +++ b/packages/app/e2e/browser/add-project-github.real.spec.ts @@ -1,7 +1,7 @@ import { mkdir, mkdtemp, rm, stat } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { expect, test } from "./fixtures"; +import { expect, test } from "../support/fixtures"; import { addProjectFlow, addProjectFlowBack, @@ -10,12 +10,16 @@ import { expectAddProjectPage, expectNewWorkspaceForAddedProject, openAddProjectFlow, -} from "./helpers/add-project-flow"; -import { gotoAppShell } from "./helpers/app"; -import { createTempGithubRepo, hasGithubAuth, type GhRepoFixture } from "./helpers/github-fixtures"; -import { expectOpenedProject } from "./helpers/project-picker-ui"; -import { connectSeedClient } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; +} from "../support/helpers/add-project-flow"; +import { gotoAppShell } from "../support/helpers/app"; +import { + createTempGithubRepo, + hasGithubAuth, + type GhRepoFixture, +} from "../support/helpers/github-fixtures"; +import { expectOpenedProject } from "../support/helpers/project-picker-ui"; +import { connectSeedClient } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; test.describe("Add Project GitHub flow", () => { test.describe.configure({ timeout: 300_000 }); diff --git a/packages/app/e2e/agent-message-submission.spec.ts b/packages/app/e2e/browser/agent-message-submission.spec.ts similarity index 96% rename from packages/app/e2e/agent-message-submission.spec.ts rename to packages/app/e2e/browser/agent-message-submission.spec.ts index 092159b9b..d312c470a 100644 --- a/packages/app/e2e/agent-message-submission.spec.ts +++ b/packages/app/e2e/browser/agent-message-submission.spec.ts @@ -1,7 +1,7 @@ import type { Locator, Page } from "@playwright/test"; -import { expect, test as baseTest } from "./fixtures"; -import { awaitToolCall, expectAgentIdle } from "./helpers/agent-stream"; -import { gateNextAgentMessage } from "./helpers/agent-message-gate"; +import { expect, test as baseTest } from "../support/fixtures"; +import { awaitToolCall, expectAgentIdle } from "../support/helpers/agent-stream"; +import { gateNextAgentMessage } from "../support/helpers/agent-message-gate"; import { attachImageFromMenu, expectComposerDraft, @@ -11,16 +11,16 @@ import { fillComposerDraft, sendDraftToQueue, startRunningMockAgent, -} from "./helpers/composer"; -import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; -import { readScrollMetrics } from "./helpers/agent-bottom-anchor"; -import { seedWorkspace } from "./helpers/seed-client"; -import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; -import { getServerId } from "./helpers/server-id"; +} from "../support/helpers/composer"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; +import { readScrollMetrics } from "../support/helpers/agent-bottom-anchor"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; +import { getServerId } from "../support/helpers/server-id"; import { buildHostWorkspaceRoute } from "@/utils/host-routes"; -import { delayBrowserAgentCreatedStatus } from "./helpers/new-workspace"; -import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate"; -import { selectModel } from "./helpers/app"; +import { delayBrowserAgentCreatedStatus } from "../support/helpers/new-workspace"; +import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate"; +import { selectModel } from "../support/helpers/app"; const IMAGE = { name: "message-submission.png", diff --git a/packages/app/e2e/agent-stream-ui.spec.ts b/packages/app/e2e/browser/agent-stream-ui.spec.ts similarity index 94% rename from packages/app/e2e/agent-stream-ui.spec.ts rename to packages/app/e2e/browser/agent-stream-ui.spec.ts index aeeb6cff7..c40759a62 100644 --- a/packages/app/e2e/agent-stream-ui.spec.ts +++ b/packages/app/e2e/browser/agent-stream-ui.spec.ts @@ -1,11 +1,11 @@ -import { test, expect } from "./fixtures"; +import { test, expect } from "../support/fixtures"; import { awaitAssistantMessage, expectAgentIdle, expectInlineWorkingIndicator, expectTurnCopyButton, expectScrollFollowsNewContent, -} from "./helpers/agent-stream"; +} from "../support/helpers/agent-stream"; import { expectScrollStaysFixed, clickToolCallBesideScrollToBottomButton, @@ -13,12 +13,12 @@ import { scrollAgentChatToBottom, scrollChatAwayFromBottom, waitForScrollableChat, -} from "./helpers/agent-bottom-anchor"; -import { delayCreatedAgentInitialTailResponse } from "./helpers/agent-timeline-gate"; -import { selectModel } from "./helpers/app"; -import { clickNewChat } from "./helpers/launcher"; -import { expectComposerVisible, startRunningMockAgent } from "./helpers/composer"; -import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; +} from "../support/helpers/agent-bottom-anchor"; +import { delayCreatedAgentInitialTailResponse } from "../support/helpers/agent-timeline-gate"; +import { selectModel } from "../support/helpers/app"; +import { clickNewChat } from "../support/helpers/launcher"; +import { expectComposerVisible, startRunningMockAgent } from "../support/helpers/composer"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; const SCROLL_AWAY_MIN_SCROLLABLE_DISTANCE = 360; diff --git a/packages/app/e2e/agent-timeline-pagination.spec.ts b/packages/app/e2e/browser/agent-timeline-pagination.spec.ts similarity index 94% rename from packages/app/e2e/agent-timeline-pagination.spec.ts rename to packages/app/e2e/browser/agent-timeline-pagination.spec.ts index 30c0bc379..d33e9e4fc 100644 --- a/packages/app/e2e/agent-timeline-pagination.spec.ts +++ b/packages/app/e2e/browser/agent-timeline-pagination.spec.ts @@ -1,4 +1,4 @@ -import { test } from "./fixtures"; +import { test } from "../support/fixtures"; import { expectLoadedTimelineDoesNotScroll, expectTimelinePromptNotMounted, @@ -8,7 +8,7 @@ import { openAgentTimeline, scrollTimelineUntilOlderHistoryIsReachable, seedLongMockAgentTimeline, -} from "./helpers/timeline-pagination"; +} from "../support/helpers/timeline-pagination"; test.describe("Agent timeline pagination", () => { test("loads older history when the user scrolls to the top of a long agent timeline", async ({ diff --git a/packages/app/e2e/archive-tab.spec.ts b/packages/app/e2e/browser/archive-tab.spec.ts similarity index 94% rename from packages/app/e2e/archive-tab.spec.ts rename to packages/app/e2e/browser/archive-tab.spec.ts index 3daaaef53..e250489a9 100644 --- a/packages/app/e2e/archive-tab.spec.ts +++ b/packages/app/e2e/browser/archive-tab.spec.ts @@ -1,9 +1,9 @@ import { randomUUID } from "node:crypto"; import { expect } from "@playwright/test"; -import { test } from "./fixtures"; -import { getServerId } from "./helpers/server-id"; -import { connectSeedClient } from "./helpers/seed-client"; -import { createTempGitRepo } from "./helpers/workspace"; +import { test } from "../support/fixtures"; +import { getServerId } from "../support/helpers/server-id"; +import { connectSeedClient } from "../support/helpers/seed-client"; +import { createTempGitRepo } from "../support/helpers/workspace"; import { buildHostWorkspaceRoute } from "@/utils/host-routes"; import { archiveAgentFromDaemon, @@ -20,7 +20,7 @@ import { primeAdditionalPage, resetSeededPageState, reloadWorkspace, -} from "./helpers/archive-tab"; +} from "../support/helpers/archive-tab"; test.describe("Archive tab reconciliation", () => { let client: Awaited>; diff --git a/packages/app/e2e/archived-codex-agent.real.spec.ts b/packages/app/e2e/browser/archived-codex-agent.real.spec.ts similarity index 91% rename from packages/app/e2e/archived-codex-agent.real.spec.ts rename to packages/app/e2e/browser/archived-codex-agent.real.spec.ts index 76b2b7ed6..6d079fe01 100644 --- a/packages/app/e2e/archived-codex-agent.real.spec.ts +++ b/packages/app/e2e/browser/archived-codex-agent.real.spec.ts @@ -1,18 +1,18 @@ import { mkdtempSync, realpathSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { test, expect } from "./fixtures"; -import { openSessions } from "./helpers/archive-tab"; +import { test, expect } from "../support/fixtures"; +import { openSessions } from "../support/helpers/archive-tab"; import { assertChatTranscript, cleanupRewindFlow, launchAgent, sendMessage, type AgentHandle, -} from "./helpers/rewind-flow"; -import type { SeedDaemonClient } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; +} from "../support/helpers/rewind-flow"; +import type { SeedDaemonClient } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; interface TimelineClient extends SeedDaemonClient { fetchAgentTimeline( diff --git a/packages/app/e2e/assistant-fork-menu.spec.ts b/packages/app/e2e/browser/assistant-fork-menu.spec.ts similarity index 92% rename from packages/app/e2e/assistant-fork-menu.spec.ts rename to packages/app/e2e/browser/assistant-fork-menu.spec.ts index 965c87b94..5c52774ac 100644 --- a/packages/app/e2e/assistant-fork-menu.spec.ts +++ b/packages/app/e2e/browser/assistant-fork-menu.spec.ts @@ -1,17 +1,17 @@ -import { expect, test as base, type Page } from "./fixtures"; -import { scrollAgentChatToBottom } from "./helpers/agent-bottom-anchor"; -import { awaitAssistantMessage } from "./helpers/agent-stream"; -import { expectComposerVisible, submitMessage } from "./helpers/composer"; -import { getE2EDaemonPort } from "./helpers/daemon-port"; +import { expect, test as base, type Page } from "../support/fixtures"; +import { scrollAgentChatToBottom } from "../support/helpers/agent-bottom-anchor"; +import { awaitAssistantMessage } from "../support/helpers/agent-stream"; +import { expectComposerVisible, submitMessage } from "../support/helpers/composer"; +import { getE2EDaemonPort } from "../support/helpers/daemon-port"; import { openAgentRoute, seedMockAgentWorkspace, type MockAgentOptions, type MockAgentWorkspace, -} from "./helpers/mock-agent"; -import { getServerId } from "./helpers/server-id"; -import { seedSavedSettingsHosts } from "./helpers/settings"; -import { submitNewWorkspaceEmpty } from "./helpers/new-workspace"; +} from "../support/helpers/mock-agent"; +import { getServerId } from "../support/helpers/server-id"; +import { seedSavedSettingsHosts } from "../support/helpers/settings"; +import { submitNewWorkspaceEmpty } from "../support/helpers/new-workspace"; const test = base.extend<{ seedForkWorkspace: (options: MockAgentOptions) => Promise; diff --git a/packages/app/e2e/bottom-sheet-reopen.spec.ts b/packages/app/e2e/browser/bottom-sheet-reopen.spec.ts similarity index 95% rename from packages/app/e2e/bottom-sheet-reopen.spec.ts rename to packages/app/e2e/browser/bottom-sheet-reopen.spec.ts index 10ad84ff2..5e6abb0ad 100644 --- a/packages/app/e2e/bottom-sheet-reopen.spec.ts +++ b/packages/app/e2e/browser/bottom-sheet-reopen.spec.ts @@ -1,6 +1,6 @@ -import { expect, test, type Page } from "./fixtures"; -import { expectComposerVisible } from "./helpers/composer"; -import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; +import { expect, test, type Page } from "../support/fixtures"; +import { expectComposerVisible } from "../support/helpers/composer"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; const MOBILE_VIEWPORT = { width: 390, height: 844 }; diff --git a/packages/app/e2e/branch-switcher.spec.ts b/packages/app/e2e/browser/branch-switcher.spec.ts similarity index 90% rename from packages/app/e2e/branch-switcher.spec.ts rename to packages/app/e2e/browser/branch-switcher.spec.ts index e80f38b27..6e81e9302 100644 --- a/packages/app/e2e/branch-switcher.spec.ts +++ b/packages/app/e2e/browser/branch-switcher.spec.ts @@ -1,15 +1,18 @@ -import { expect, test, type Page } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; +import { expect, test, type Page } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; import { expectNoBranchSwitcherInWorkspaceHeader, expectWorkspaceBranch, openChangesPanel, switchBranchFromChangesPanel, -} from "./helpers/branch-switcher"; -import { seedWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; -import { readWorktreeBranchInfo } from "./helpers/workspace"; -import { switchWorkspaceViaSidebar, waitForSidebarHydration } from "./helpers/workspace-ui"; +} from "../support/helpers/branch-switcher"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; +import { readWorktreeBranchInfo } from "../support/helpers/workspace"; +import { + switchWorkspaceViaSidebar, + waitForSidebarHydration, +} from "../support/helpers/workspace-ui"; async function renameWorkspaceViaSidebar( page: Page, diff --git a/packages/app/e2e/client-slash-commands.spec.ts b/packages/app/e2e/browser/client-slash-commands.spec.ts similarity index 95% rename from packages/app/e2e/client-slash-commands.spec.ts rename to packages/app/e2e/browser/client-slash-commands.spec.ts index 2937a9f34..a3b5a658b 100644 --- a/packages/app/e2e/client-slash-commands.spec.ts +++ b/packages/app/e2e/browser/client-slash-commands.spec.ts @@ -1,12 +1,12 @@ -import { expect, test, type Page } from "./fixtures"; -import { composerLocator, expectComposerVisible, submitMessage } from "./helpers/composer"; -import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; +import { expect, test, type Page } from "../support/fixtures"; +import { composerLocator, expectComposerVisible, submitMessage } from "../support/helpers/composer"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; import { expectSessionRowArchived, expectWorkspaceTabHidden, expectWorkspaceTabVisible, openSessions, -} from "./helpers/archive-tab"; +} from "../support/helpers/archive-tab"; interface SlashCommandScenario { agentId: string; diff --git a/packages/app/e2e/codex-plan-approval.spec.ts b/packages/app/e2e/browser/codex-plan-approval.spec.ts similarity index 80% rename from packages/app/e2e/codex-plan-approval.spec.ts rename to packages/app/e2e/browser/codex-plan-approval.spec.ts index ff2c9aedc..d9ef0c17b 100644 --- a/packages/app/e2e/codex-plan-approval.spec.ts +++ b/packages/app/e2e/browser/codex-plan-approval.spec.ts @@ -1,6 +1,6 @@ -import { expect, test } from "./fixtures"; -import { allowPermission, waitForPermissionPrompt } from "./helpers/permissions"; -import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; +import { expect, test } from "../support/fixtures"; +import { allowPermission, waitForPermissionPrompt } from "../support/helpers/permissions"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; test.describe("Codex plan approval", () => { test("shows a single actionable plan panel and removes it after implementation starts", async ({ diff --git a/packages/app/e2e/command-center-host.spec.ts b/packages/app/e2e/browser/command-center-host.spec.ts similarity index 81% rename from packages/app/e2e/command-center-host.spec.ts rename to packages/app/e2e/browser/command-center-host.spec.ts index 652bca55f..f430969a8 100644 --- a/packages/app/e2e/command-center-host.spec.ts +++ b/packages/app/e2e/browser/command-center-host.spec.ts @@ -1,13 +1,13 @@ 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 { expectAgentTabActive } from "./helpers/launcher"; -import { seedWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; +import { test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { createIdleAgent } from "../support/helpers/archive-tab"; +import { openCommandCenter } from "../support/helpers/command-center"; +import { addOfflineHostAndReload } from "../support/helpers/hosts"; +import { expectAgentTabActive } from "../support/helpers/launcher"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; const PRIMARY_HOST_LABEL = "Primary Host"; const SECONDARY_HOST_ID = "host-command-center-secondary"; diff --git a/packages/app/e2e/command-center-workspaces.spec.ts b/packages/app/e2e/browser/command-center-workspaces.spec.ts similarity index 91% rename from packages/app/e2e/command-center-workspaces.spec.ts rename to packages/app/e2e/browser/command-center-workspaces.spec.ts index d515d3669..44892a499 100644 --- a/packages/app/e2e/command-center-workspaces.spec.ts +++ b/packages/app/e2e/browser/command-center-workspaces.spec.ts @@ -1,13 +1,13 @@ import { execFileSync } from "node:child_process"; 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 { expectAppRoute } from "./helpers/route-assertions"; -import { seedWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; +import { test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { createIdleAgent } from "../support/helpers/archive-tab"; +import { openCommandCenter } from "../support/helpers/command-center"; +import { addOfflineHostAndReload } from "../support/helpers/hosts"; +import { expectAppRoute } from "../support/helpers/route-assertions"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; import { buildHostWorkspaceRoute } from "@/utils/host-routes"; const PRIMARY_HOST_LABEL = "Primary Host"; diff --git a/packages/app/e2e/commit-diff-panel.spec.ts b/packages/app/e2e/browser/commit-diff-panel.spec.ts similarity index 98% rename from packages/app/e2e/commit-diff-panel.spec.ts rename to packages/app/e2e/browser/commit-diff-panel.spec.ts index f290b008b..1f658fac0 100644 --- a/packages/app/e2e/commit-diff-panel.spec.ts +++ b/packages/app/e2e/browser/commit-diff-panel.spec.ts @@ -1,7 +1,7 @@ import { execFileSync } from "node:child_process"; import { writeFile } from "node:fs/promises"; import path from "node:path"; -import { test, expect } from "./fixtures"; +import { test, expect } from "../support/fixtures"; const COMMIT_SUBJECT = "Show commit timestamps"; diff --git a/packages/app/e2e/composer-attachments.spec.ts b/packages/app/e2e/browser/composer-attachments.spec.ts similarity index 91% rename from packages/app/e2e/composer-attachments.spec.ts rename to packages/app/e2e/browser/composer-attachments.spec.ts index 98d0c7435..996e78462 100644 --- a/packages/app/e2e/composer-attachments.spec.ts +++ b/packages/app/e2e/browser/composer-attachments.spec.ts @@ -1,7 +1,7 @@ -import { expect, test } from "./fixtures"; -import { clickNewChat } from "./helpers/launcher"; -import { expectComposerVisible } from "./helpers/composer"; -import { expectAgentIdle } from "./helpers/agent-stream"; +import { expect, test } from "../support/fixtures"; +import { clickNewChat } from "../support/helpers/launcher"; +import { expectComposerVisible } from "../support/helpers/composer"; +import { expectAgentIdle } from "../support/helpers/agent-stream"; import { openAttachmentMenu, openGithubPickerFromMenu, @@ -23,13 +23,19 @@ import { selectGithubOption, expectGithubAttachmentPill, openGithubWorkspace, -} from "./helpers/composer"; -import { delayBrowserAgentCreatedStatus, openNewWorkspaceComposer } from "./helpers/new-workspace"; -import { gotoAppShell } from "./helpers/app"; -import { waitForSidebarHydration, switchWorkspaceViaSidebar } from "./helpers/workspace-ui"; -import { seedWorkspace } from "./helpers/seed-client"; -import { hasGithubAuth, createTempGithubRepo } from "./helpers/github-fixtures"; -import { getServerId } from "./helpers/server-id"; +} from "../support/helpers/composer"; +import { + delayBrowserAgentCreatedStatus, + openNewWorkspaceComposer, +} from "../support/helpers/new-workspace"; +import { gotoAppShell } from "../support/helpers/app"; +import { + waitForSidebarHydration, + switchWorkspaceViaSidebar, +} from "../support/helpers/workspace-ui"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { hasGithubAuth, createTempGithubRepo } from "../support/helpers/github-fixtures"; +import { getServerId } from "../support/helpers/server-id"; const MINIMAL_PNG = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", @@ -144,11 +150,6 @@ test.describe("Composer attachments", () => { // needed before this can be exercised end-to-end. }); - test.fixme("browser-element attachment pill is created from Electron webview selection", async () => { - // The browser-element attachment is only created in browser-pane.electron.tsx via DOM - // element selection in the Electron webview. It is not exercisable in headless Chromium E2E. - }); - test("image lightbox opens on pill click and closes on Escape", async ({ page, withWorkspace, diff --git a/packages/app/e2e/composer-autocomplete.spec.ts b/packages/app/e2e/browser/composer-autocomplete.spec.ts similarity index 97% rename from packages/app/e2e/composer-autocomplete.spec.ts rename to packages/app/e2e/browser/composer-autocomplete.spec.ts index c8b19b31c..37d9ed436 100644 --- a/packages/app/e2e/composer-autocomplete.spec.ts +++ b/packages/app/e2e/browser/composer-autocomplete.spec.ts @@ -1,14 +1,14 @@ -import { expect, test, type Page } from "./fixtures"; -import { composerLocator, expectComposerVisible } from "./helpers/composer"; +import { expect, test, type Page } from "../support/fixtures"; +import { composerLocator, expectComposerVisible } from "../support/helpers/composer"; import { openAgentRoute, seedMockAgentWorkspace, type MockAgentWorkspace, -} from "./helpers/mock-agent"; -import { expectWorkspaceTabVisible, openSessions } from "./helpers/archive-tab"; -import { daemonWsRoutePattern } from "./helpers/daemon-port"; -import { getServerId } from "./helpers/server-id"; -import { switchWorkspaceViaSidebar } from "./helpers/workspace-ui"; +} from "../support/helpers/mock-agent"; +import { expectWorkspaceTabVisible, openSessions } from "../support/helpers/archive-tab"; +import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; +import { getServerId } from "../support/helpers/server-id"; +import { switchWorkspaceViaSidebar } from "../support/helpers/workspace-ui"; const TEST_COMMANDS = [ { diff --git a/packages/app/e2e/diff-row-alignment.spec.ts b/packages/app/e2e/browser/diff-row-alignment.spec.ts similarity index 98% rename from packages/app/e2e/diff-row-alignment.spec.ts rename to packages/app/e2e/browser/diff-row-alignment.spec.ts index aff4789a0..b775371a3 100644 --- a/packages/app/e2e/diff-row-alignment.spec.ts +++ b/packages/app/e2e/browser/diff-row-alignment.spec.ts @@ -1,12 +1,12 @@ import { unlink, writeFile } from "node:fs/promises"; import path from "node:path"; import { type Page } from "@playwright/test"; -import { buildHostWorkspaceRoute, buildSettingsSectionRoute } from "../src/utils/host-routes"; -import { test, expect } from "./fixtures"; -import { getServerId } from "./helpers/server-id"; -import { connectSeedClient } from "./helpers/seed-client"; -import { createTempGitRepo } from "./helpers/workspace"; -import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; +import { buildHostWorkspaceRoute, buildSettingsSectionRoute } from "../../src/utils/host-routes"; +import { test, expect } from "../support/fixtures"; +import { getServerId } from "../support/helpers/server-id"; +import { connectSeedClient } from "../support/helpers/seed-client"; +import { createTempGitRepo } from "../support/helpers/workspace"; +import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; interface DirtyWorkspace { id: string; diff --git a/packages/app/e2e/directory-bootstrap.spec.ts b/packages/app/e2e/browser/directory-bootstrap.spec.ts similarity index 81% rename from packages/app/e2e/directory-bootstrap.spec.ts rename to packages/app/e2e/browser/directory-bootstrap.spec.ts index ad42b991f..6aa3a1a44 100644 --- a/packages/app/e2e/directory-bootstrap.spec.ts +++ b/packages/app/e2e/browser/directory-bootstrap.spec.ts @@ -1,5 +1,5 @@ -import { test } from "./fixtures"; -import { DirectoryBootstrapScenario } from "./helpers/directory-bootstrap-scenario"; +import { test } from "../support/fixtures"; +import { DirectoryBootstrapScenario } from "../support/helpers/directory-bootstrap-scenario"; test.describe("Directory bootstrap correctness", () => { test("connect, pushed deltas, and reconnect keep directories current without duplicate bootstraps", async ({ diff --git a/packages/app/e2e/empty-project-persists.spec.ts b/packages/app/e2e/browser/empty-project-persists.spec.ts similarity index 94% rename from packages/app/e2e/empty-project-persists.spec.ts rename to packages/app/e2e/browser/empty-project-persists.spec.ts index c915d809f..7ff324d7a 100644 --- a/packages/app/e2e/empty-project-persists.spec.ts +++ b/packages/app/e2e/browser/empty-project-persists.spec.ts @@ -1,17 +1,17 @@ import path from "node:path"; import { existsSync } from "node:fs"; -import { test, expect, type Page } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; +import { test, expect, type Page } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; import { addProjectFlowInput, chooseAddProjectMethod, openAddProjectFlow, -} from "./helpers/add-project-flow"; -import { expectOpenedProject } from "./helpers/project-picker-ui"; -import { connectSeedClient, seedWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; -import { createTempGitRepo } from "./helpers/workspace"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; +} from "../support/helpers/add-project-flow"; +import { expectOpenedProject } from "../support/helpers/project-picker-ui"; +import { connectSeedClient, seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; +import { createTempGitRepo } from "../support/helpers/workspace"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; function workspaceRowTestId(workspaceId: string): string { return `sidebar-workspace-row-${getServerId()}:${workspaceId}`; diff --git a/packages/app/e2e/file-editing.spec.ts b/packages/app/e2e/browser/file-editing.spec.ts similarity index 98% rename from packages/app/e2e/file-editing.spec.ts rename to packages/app/e2e/browser/file-editing.spec.ts index 8045b15e9..70d466da5 100644 --- a/packages/app/e2e/file-editing.spec.ts +++ b/packages/app/e2e/browser/file-editing.spec.ts @@ -1,9 +1,13 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; -import { expect, test, type Page } from "./fixtures"; -import { openFileExplorer, openFileFromExplorer, expectFileTabOpen } from "./helpers/file-explorer"; -import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate"; -import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; +import { expect, test, type Page } from "../support/fixtures"; +import { + openFileExplorer, + openFileFromExplorer, + expectFileTabOpen, +} from "../support/helpers/file-explorer"; +import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; const RED_PIXEL = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZQmcAAAAASUVORK5CYII=", diff --git a/packages/app/e2e/file-explorer-collapse.spec.ts b/packages/app/e2e/browser/file-explorer-collapse.spec.ts similarity index 83% rename from packages/app/e2e/file-explorer-collapse.spec.ts rename to packages/app/e2e/browser/file-explorer-collapse.spec.ts index 830b63a6b..705f66726 100644 --- a/packages/app/e2e/file-explorer-collapse.spec.ts +++ b/packages/app/e2e/browser/file-explorer-collapse.spec.ts @@ -1,4 +1,4 @@ -import { test } from "./fixtures"; +import { test } from "../support/fixtures"; import { collapseFolder, expandFolder, @@ -7,9 +7,9 @@ import { expectFileTabOpen, openFileExplorer, openFileFromExplorer, -} from "./helpers/file-explorer"; -import { gotoWorkspace } from "./helpers/launcher"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; +} from "../support/helpers/file-explorer"; +import { gotoWorkspace } from "../support/helpers/launcher"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; let workspace: SeededWorkspace; diff --git a/packages/app/e2e/import-session.opencode.real.spec.ts b/packages/app/e2e/browser/import-session.opencode.real.spec.ts similarity index 95% rename from packages/app/e2e/import-session.opencode.real.spec.ts rename to packages/app/e2e/browser/import-session.opencode.real.spec.ts index 9e1e909f5..25185187d 100644 --- a/packages/app/e2e/import-session.opencode.real.spec.ts +++ b/packages/app/e2e/browser/import-session.opencode.real.spec.ts @@ -2,14 +2,14 @@ import { randomUUID } from "node:crypto"; import path from "node:path"; import { spawn } from "node:child_process"; import { buildHostWorkspaceRoute } from "@/utils/host-routes"; -import { expect, test, type Page } from "./fixtures"; -import { connectSeedClient, type SeededWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; -import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; +import { expect, test, type Page } from "../support/fixtures"; +import { connectSeedClient, type SeededWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; +import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; const OPENCODE_REAL_MODEL = "openrouter/google/gemini-2.5-flash-lite"; const OPENCODE_SEED_TIMEOUT_MS = 45_000; -const PASEO_REPO_PATH = path.resolve(__dirname, "../../.."); +const PASEO_REPO_PATH = path.resolve(__dirname, "../../../.."); interface OpenCodeSeedResult { stdout: string; diff --git a/packages/app/e2e/launcher-tab.spec.ts b/packages/app/e2e/browser/launcher-tab.spec.ts similarity index 96% rename from packages/app/e2e/launcher-tab.spec.ts rename to packages/app/e2e/browser/launcher-tab.spec.ts index 15d916709..d9cff153c 100644 --- a/packages/app/e2e/launcher-tab.spec.ts +++ b/packages/app/e2e/browser/launcher-tab.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "./fixtures"; +import { test, expect } from "../support/fixtures"; import { gotoWorkspace, assertNewChatTileVisible, @@ -13,10 +13,13 @@ import { measureTileTransition, sampleTabsDuringTransition, terminalSurfaceLocator, -} from "./helpers/launcher"; -import { expectComposerVisible, composerLocator } from "./helpers/composer"; -import { expectTerminalSurfaceVisible, setupDeterministicPrompt } from "./helpers/terminal-perf"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; +} from "../support/helpers/launcher"; +import { expectComposerVisible, composerLocator } from "../support/helpers/composer"; +import { + expectTerminalSurfaceVisible, + setupDeterministicPrompt, +} from "../support/helpers/terminal-perf"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; // ─── Shared state ────────────────────────────────────────────────────────── diff --git a/packages/app/e2e/new-workspace-codex-mode-preferences.spec.ts b/packages/app/e2e/browser/new-workspace-codex-mode-preferences.spec.ts similarity index 92% rename from packages/app/e2e/new-workspace-codex-mode-preferences.spec.ts rename to packages/app/e2e/browser/new-workspace-codex-mode-preferences.spec.ts index b106810b8..c6304c6c0 100644 --- a/packages/app/e2e/new-workspace-codex-mode-preferences.spec.ts +++ b/packages/app/e2e/browser/new-workspace-codex-mode-preferences.spec.ts @@ -1,17 +1,17 @@ -import { expect, test, type Page } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; -import { daemonWsRoutePattern } from "./helpers/daemon-port"; -import { openAgentRoute } from "./helpers/mock-agent"; +import { expect, test, type Page } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; +import { openAgentRoute } from "../support/helpers/mock-agent"; import { openGlobalNewWorkspaceComposer, selectNewWorkspaceProject, submitNewWorkspacePrompt, -} from "./helpers/new-workspace"; -import { expectNoTruncation } from "./helpers/no-truncation"; -import { escapeRegex } from "./helpers/regex"; -import { seedWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; +} from "../support/helpers/new-workspace"; +import { expectNoTruncation } from "../support/helpers/no-truncation"; +import { escapeRegex } from "../support/helpers/regex"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; const CREATE_AGENT_PREFERENCES_KEY = "@paseo:create-agent-preferences"; diff --git a/packages/app/e2e/new-workspace-composer-draft.spec.ts b/packages/app/e2e/browser/new-workspace-composer-draft.spec.ts similarity index 83% rename from packages/app/e2e/new-workspace-composer-draft.spec.ts rename to packages/app/e2e/browser/new-workspace-composer-draft.spec.ts index 3606fe7e0..0a63601df 100644 --- a/packages/app/e2e/new-workspace-composer-draft.spec.ts +++ b/packages/app/e2e/browser/new-workspace-composer-draft.spec.ts @@ -1,6 +1,6 @@ -import { test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; -import { getE2EDaemonPort } from "./helpers/daemon-port"; +import { test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { getE2EDaemonPort } from "../support/helpers/daemon-port"; import { expectNewWorkspaceDraft, expectNewWorkspaceProjectSelected, @@ -9,11 +9,11 @@ import { openNewWorkspaceComposer, selectNewWorkspaceHost, selectNewWorkspaceProject, -} from "./helpers/new-workspace"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; -import { seedSavedSettingsHosts } from "./helpers/settings"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; +} from "../support/helpers/new-workspace"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; +import { seedSavedSettingsHosts } from "../support/helpers/settings"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; const DRAFT = `Please investigate the workspace startup failure. diff --git a/packages/app/e2e/new-workspace-entry.spec.ts b/packages/app/e2e/browser/new-workspace-entry.spec.ts similarity index 94% rename from packages/app/e2e/new-workspace-entry.spec.ts rename to packages/app/e2e/browser/new-workspace-entry.spec.ts index b2ad1c884..21716c3a7 100644 --- a/packages/app/e2e/new-workspace-entry.spec.ts +++ b/packages/app/e2e/browser/new-workspace-entry.spec.ts @@ -1,18 +1,21 @@ -import { expect, test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; import { connectNewWorkspaceDaemonClient, expectNewWorkspaceProjectSelected, openGlobalNewWorkspaceComposer, openNewWorkspaceComposer, openNewWorkspaceProjectPickerWithShortcut, -} from "./helpers/new-workspace"; -import { getE2EDaemonPort } from "./helpers/daemon-port"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; -import { seedSavedSettingsHosts } from "./helpers/settings"; -import { getServerId } from "./helpers/server-id"; -import { clickArchiveWorkspaceMenuItem, expectWorkspaceAbsentFromSidebar } from "./helpers/sidebar"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; +} from "../support/helpers/new-workspace"; +import { getE2EDaemonPort } from "../support/helpers/daemon-port"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; +import { seedSavedSettingsHosts } from "../support/helpers/settings"; +import { getServerId } from "../support/helpers/server-id"; +import { + clickArchiveWorkspaceMenuItem, + expectWorkspaceAbsentFromSidebar, +} from "../support/helpers/sidebar"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; // Model B entry points into the New Workspace screen. The surviving entries are // the global button (universal) and each project's per-row New workspace icon diff --git a/packages/app/e2e/new-workspace-isolation-memory.spec.ts b/packages/app/e2e/browser/new-workspace-isolation-memory.spec.ts similarity index 89% rename from packages/app/e2e/new-workspace-isolation-memory.spec.ts rename to packages/app/e2e/browser/new-workspace-isolation-memory.spec.ts index 4f9cc0669..7ec3fd455 100644 --- a/packages/app/e2e/new-workspace-isolation-memory.spec.ts +++ b/packages/app/e2e/browser/new-workspace-isolation-memory.spec.ts @@ -1,5 +1,5 @@ -import { expect, test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; import { archiveLocalWorkspaceFromDaemon, archiveWorkspaceFromDaemon, @@ -10,11 +10,11 @@ import { openProjectViaDaemon, openStartingRefPicker, selectBranchInPicker, -} from "./helpers/new-workspace"; -import { expectNoTruncation } from "./helpers/no-truncation"; -import { createTempGitRepo } from "./helpers/workspace"; -import { getServerId } from "./helpers/server-id"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; +} from "../support/helpers/new-workspace"; +import { expectNoTruncation } from "../support/helpers/no-truncation"; +import { createTempGitRepo } from "../support/helpers/workspace"; +import { getServerId } from "../support/helpers/server-id"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; // Regression for "the local / worktree selection in the new workspace is not // remembered." The isolation choice persists in the create-form preferences diff --git a/packages/app/e2e/new-workspace-mode-cycle-safety.spec.ts b/packages/app/e2e/browser/new-workspace-mode-cycle-safety.spec.ts similarity index 92% rename from packages/app/e2e/new-workspace-mode-cycle-safety.spec.ts rename to packages/app/e2e/browser/new-workspace-mode-cycle-safety.spec.ts index 9a8948669..d5be0ff6a 100644 --- a/packages/app/e2e/new-workspace-mode-cycle-safety.spec.ts +++ b/packages/app/e2e/browser/new-workspace-mode-cycle-safety.spec.ts @@ -1,9 +1,12 @@ -import { expect, test, type Page } from "./fixtures"; -import { daemonWsRoutePattern } from "./helpers/daemon-port"; -import { openAgentRoute } from "./helpers/mock-agent"; -import { openGlobalNewWorkspaceComposer, selectNewWorkspaceProject } from "./helpers/new-workspace"; -import { seedWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; +import { expect, test, type Page } from "../support/fixtures"; +import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; +import { openAgentRoute } from "../support/helpers/mock-agent"; +import { + openGlobalNewWorkspaceComposer, + selectNewWorkspaceProject, +} from "../support/helpers/new-workspace"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; const CREATE_AGENT_PREFERENCES_KEY = "@paseo:create-agent-preferences"; diff --git a/packages/app/e2e/new-workspace-preselect.spec.ts b/packages/app/e2e/browser/new-workspace-preselect.spec.ts similarity index 93% rename from packages/app/e2e/new-workspace-preselect.spec.ts rename to packages/app/e2e/browser/new-workspace-preselect.spec.ts index db627576b..88d6cce77 100644 --- a/packages/app/e2e/new-workspace-preselect.spec.ts +++ b/packages/app/e2e/browser/new-workspace-preselect.spec.ts @@ -1,16 +1,19 @@ -import { expect, test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; -import { getE2EDaemonPort } from "./helpers/daemon-port"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { getE2EDaemonPort } from "../support/helpers/daemon-port"; import { expectNewWorkspaceProjectSelected, openGlobalNewWorkspaceComposer, -} from "./helpers/new-workspace"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; -import { seedSavedSettingsHosts } from "./helpers/settings"; +} from "../support/helpers/new-workspace"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; +import { seedSavedSettingsHosts } from "../support/helpers/settings"; import { LAST_WORKSPACE_SELECTION_STORAGE_KEY } from "@/stores/last-workspace-selection"; import { buildHostWorkspaceRoute, buildNewWorkspaceRoute } from "@/utils/host-routes"; -import { switchWorkspaceViaSidebar, waitForSidebarHydration } from "./helpers/workspace-ui"; +import { + switchWorkspaceViaSidebar, + waitForSidebarHydration, +} from "../support/helpers/workspace-ui"; const OFFLINE_SERVER_IDS = [ "srv_e2e_preselect_offline_1", diff --git a/packages/app/e2e/new-workspace.spec.ts b/packages/app/e2e/browser/new-workspace.spec.ts similarity index 98% rename from packages/app/e2e/new-workspace.spec.ts rename to packages/app/e2e/browser/new-workspace.spec.ts index cd5cd8608..04abc314e 100644 --- a/packages/app/e2e/new-workspace.spec.ts +++ b/packages/app/e2e/browser/new-workspace.spec.ts @@ -1,8 +1,8 @@ import { existsSync } from "node:fs"; import path from "node:path"; import { buildHostWorkspaceRoute } from "@/utils/host-routes"; -import { expect, test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; import { archiveWorkspaceFromDaemon, archiveLocalWorkspaceFromDaemon, @@ -30,26 +30,26 @@ import { selectPickerOptionByKeyboard, selectWorkspaceIsolation, submitNewWorkspacePrompt, -} from "./helpers/new-workspace"; -import { createTempGitRepo, readWorktreeBranchInfo } from "./helpers/workspace"; +} from "../support/helpers/new-workspace"; +import { createTempGitRepo, readWorktreeBranchInfo } from "../support/helpers/workspace"; import { createLocalGithubPrFixture, cloneGithubRepoDefaultBranchOnly, createTempGithubRepo, hasGithubAuth, type LocalGhPrFixture, -} from "./helpers/github-fixtures"; -import { getServerId } from "./helpers/server-id"; -import { getE2EDaemonPort } from "./helpers/daemon-port"; -import { chooseAddProjectMethod, expectAddProjectPage } from "./helpers/add-project-flow"; -import { seedSavedSettingsHosts } from "./helpers/settings"; +} from "../support/helpers/github-fixtures"; +import { getServerId } from "../support/helpers/server-id"; +import { getE2EDaemonPort } from "../support/helpers/daemon-port"; +import { chooseAddProjectMethod, expectAddProjectPage } from "../support/helpers/add-project-flow"; +import { seedSavedSettingsHosts } from "../support/helpers/settings"; import { expectSidebarWorkspaceSelected, expectWorkspaceHeader, switchWorkspaceViaSidebar, waitForSidebarHydration, waitForWorkspaceInSidebar, -} from "./helpers/workspace-ui"; +} from "../support/helpers/workspace-ui"; interface WorkspaceStatusGroupEvent { rowTestId: string; diff --git a/packages/app/e2e/out-of-band-command.codex.real.spec.ts b/packages/app/e2e/browser/out-of-band-command.codex.real.spec.ts similarity index 89% rename from packages/app/e2e/out-of-band-command.codex.real.spec.ts rename to packages/app/e2e/browser/out-of-band-command.codex.real.spec.ts index 2df34398d..1c30060d3 100644 --- a/packages/app/e2e/out-of-band-command.codex.real.spec.ts +++ b/packages/app/e2e/browser/out-of-band-command.codex.real.spec.ts @@ -1,10 +1,10 @@ import { mkdtempSync, realpathSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { expect, test } from "./fixtures"; -import { submitMessage } from "./helpers/composer"; -import { cleanupRewindFlow, launchAgent, type AgentHandle } from "./helpers/rewind-flow"; -import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate"; +import { expect, test } from "../support/fixtures"; +import { submitMessage } from "../support/helpers/composer"; +import { cleanupRewindFlow, launchAgent, type AgentHandle } from "../support/helpers/rewind-flow"; +import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate"; test.describe("Codex out-of-band commands", () => { test.setTimeout(300_000); diff --git a/packages/app/e2e/pr-pane.spec.ts b/packages/app/e2e/browser/pr-pane.spec.ts similarity index 92% rename from packages/app/e2e/pr-pane.spec.ts rename to packages/app/e2e/browser/pr-pane.spec.ts index faea7bfce..a3b623ea1 100644 --- a/packages/app/e2e/pr-pane.spec.ts +++ b/packages/app/e2e/browser/pr-pane.spec.ts @@ -1,18 +1,22 @@ -import { test } from "./fixtures"; +import { test } from "../support/fixtures"; import { openPrPane, expectPrPaneTitle, expectPrPaneState, expectPrPaneCheckSummary, expectPrPaneActivityCount, -} from "./helpers/pr-pane"; -import { gotoWorkspace } from "./helpers/launcher"; -import { hasGithubAuth, createTempGithubRepo, type GhRepoFixture } from "./helpers/github-fixtures"; +} from "../support/helpers/pr-pane"; +import { gotoWorkspace } from "../support/helpers/launcher"; +import { + hasGithubAuth, + createTempGithubRepo, + type GhRepoFixture, +} from "../support/helpers/github-fixtures"; import { connectWorkspaceSetupClient, openProjectViaDaemon, type WorkspaceSetupDaemonClient, -} from "./helpers/workspace-setup"; +} from "../support/helpers/workspace-setup"; const GITHUB_AUTH = hasGithubAuth(); diff --git a/packages/app/e2e/projects-settings.spec.ts b/packages/app/e2e/browser/projects-settings.spec.ts similarity index 96% rename from packages/app/e2e/projects-settings.spec.ts rename to packages/app/e2e/browser/projects-settings.spec.ts index 6330b8cef..8c42d6e91 100644 --- a/packages/app/e2e/projects-settings.spec.ts +++ b/packages/app/e2e/browser/projects-settings.spec.ts @@ -1,7 +1,7 @@ import { chmod, readFile } from "node:fs/promises"; import path from "node:path"; -import { expect, test as base, type Page } from "./fixtures"; -import { connectSeedClient, seedWorkspace } from "./helpers/seed-client"; +import { expect, test as base, type Page } from "../support/fixtures"; +import { connectSeedClient, seedWorkspace } from "../support/helpers/seed-client"; import { blockPaseoConfigWrites, bumpPaseoConfigOnDisk, @@ -29,14 +29,14 @@ import { removeProjectScript, restorePaseoConfig, unblockPaseoConfigWrites, -} from "./helpers/project-settings"; -import { gotoAppShell } from "./helpers/app"; +} from "../support/helpers/project-settings"; +import { gotoAppShell } from "../support/helpers/app"; import { addProjectFlowInput, chooseAddProjectMethod, openAddProjectFlow, -} from "./helpers/add-project-flow"; -import { createTempGitRepo } from "./helpers/workspace"; +} from "../support/helpers/add-project-flow"; +import { createTempGitRepo } from "../support/helpers/workspace"; const updatedSetup = ["npm install", "npm run build"]; diff --git a/packages/app/e2e/provider-removal.spec.ts b/packages/app/e2e/browser/provider-removal.spec.ts similarity index 90% rename from packages/app/e2e/provider-removal.spec.ts rename to packages/app/e2e/browser/provider-removal.spec.ts index dcddc3ad2..7e17a1fbe 100644 --- a/packages/app/e2e/provider-removal.spec.ts +++ b/packages/app/e2e/browser/provider-removal.spec.ts @@ -1,15 +1,15 @@ import type { Dialog } from "@playwright/test"; -import { expect, test, type Page } from "./fixtures"; -import { gotoAppShell, openSettings } from "./helpers/app"; -import { connectDaemonClient } from "./helpers/daemon-client-loader"; -import { getServerId } from "./helpers/server-id"; +import { expect, test, type Page } from "../support/fixtures"; +import { gotoAppShell, openSettings } from "../support/helpers/app"; +import { connectDaemonClient } from "../support/helpers/daemon-client-loader"; +import { getServerId } from "../support/helpers/server-id"; import { expectProviderInstalledInSettings, installAcpCatalogProvider, openAddProviderArea, openSettingsHost, openSettingsHostSection, -} from "./helpers/settings"; +} from "../support/helpers/settings"; const CUSTOM_PROVIDER = { id: "junie", diff --git a/packages/app/e2e/provider-settings-refresh.spec.ts b/packages/app/e2e/browser/provider-settings-refresh.spec.ts similarity index 97% rename from packages/app/e2e/provider-settings-refresh.spec.ts rename to packages/app/e2e/browser/provider-settings-refresh.spec.ts index 3ade95634..622d12cec 100644 --- a/packages/app/e2e/provider-settings-refresh.spec.ts +++ b/packages/app/e2e/browser/provider-settings-refresh.spec.ts @@ -1,7 +1,7 @@ import type { Locator } from "@playwright/test"; -import { expect, test, type Page } from "./fixtures"; -import { expectComposerVisible } from "./helpers/composer"; -import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; +import { expect, test, type Page } from "../support/fixtures"; +import { expectComposerVisible } from "../support/helpers/composer"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; const MOBILE_VIEWPORT = { width: 390, height: 844 }; diff --git a/packages/app/e2e/provider-subagents.real.spec.ts b/packages/app/e2e/browser/provider-subagents.real.spec.ts similarity index 95% rename from packages/app/e2e/provider-subagents.real.spec.ts rename to packages/app/e2e/browser/provider-subagents.real.spec.ts index a4bf02fad..6e9bc64f6 100644 --- a/packages/app/e2e/provider-subagents.real.spec.ts +++ b/packages/app/e2e/browser/provider-subagents.real.spec.ts @@ -1,15 +1,15 @@ import { mkdtempSync, realpathSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { test, expect } from "./fixtures"; +import { test, expect } from "../support/fixtures"; import { cleanupRewindFlow, launchAgent, sendMessage, type AgentHandle, type RewindFlowProvider, -} from "./helpers/rewind-flow"; -import { openSubagentsTrack } from "./helpers/subagents"; +} from "../support/helpers/rewind-flow"; +import { openSubagentsTrack } from "../support/helpers/subagents"; interface ProviderSubagentCase { provider: RewindFlowProvider; diff --git a/packages/app/e2e/provider-usage-settings.spec.ts b/packages/app/e2e/browser/provider-usage-settings.spec.ts similarity index 94% rename from packages/app/e2e/provider-usage-settings.spec.ts rename to packages/app/e2e/browser/provider-usage-settings.spec.ts index f9d21f6a8..0763233e1 100644 --- a/packages/app/e2e/provider-usage-settings.spec.ts +++ b/packages/app/e2e/browser/provider-usage-settings.spec.ts @@ -1,8 +1,8 @@ -import { expect, test } from "./fixtures"; -import { gotoAppShell, openSettings } from "./helpers/app"; -import { installProviderUsageFixture } from "./helpers/provider-usage"; -import { getServerId } from "./helpers/server-id"; -import { openSettingsHostSection } from "./helpers/settings"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell, openSettings } from "../support/helpers/app"; +import { installProviderUsageFixture } from "../support/helpers/provider-usage"; +import { getServerId } from "../support/helpers/server-id"; +import { openSettingsHostSection } from "../support/helpers/settings"; test.describe("provider usage settings", () => { test("renders every provider returned by the daemon usage RPC", async ({ page }) => { diff --git a/packages/app/e2e/provider-usage-tooltip.spec.ts b/packages/app/e2e/browser/provider-usage-tooltip.spec.ts similarity index 91% rename from packages/app/e2e/provider-usage-tooltip.spec.ts rename to packages/app/e2e/browser/provider-usage-tooltip.spec.ts index 55666a7a1..680b7b4b8 100644 --- a/packages/app/e2e/provider-usage-tooltip.spec.ts +++ b/packages/app/e2e/browser/provider-usage-tooltip.spec.ts @@ -1,7 +1,7 @@ -import { expect, test, type Page } from "./fixtures"; -import { expectComposerVisible } from "./helpers/composer"; -import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; -import { installProviderUsageFixture } from "./helpers/provider-usage"; +import { expect, test, type Page } from "../support/fixtures"; +import { expectComposerVisible } from "../support/helpers/composer"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; +import { installProviderUsageFixture } from "../support/helpers/provider-usage"; const MOBILE_VIEWPORT = { width: 390, height: 844 }; diff --git a/packages/app/e2e/question-prompt-pagination.spec.ts b/packages/app/e2e/browser/question-prompt-pagination.spec.ts similarity index 95% rename from packages/app/e2e/question-prompt-pagination.spec.ts rename to packages/app/e2e/browser/question-prompt-pagination.spec.ts index a2d5747fd..b014b2d1c 100644 --- a/packages/app/e2e/question-prompt-pagination.spec.ts +++ b/packages/app/e2e/browser/question-prompt-pagination.spec.ts @@ -1,5 +1,5 @@ -import { test } from "./fixtures"; -import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; +import { test } from "../support/fixtures"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; import { chooseQuestionOption, continueToNextQuestion, @@ -14,7 +14,7 @@ import { openQuestion, submitQuestionAnswers, waitForQuestionPrompt, -} from "./helpers/questions"; +} from "../support/helpers/questions"; const TOTAL_QUESTIONS = 3; const SURFACE_QUESTION = "Which surface should this apply to?"; diff --git a/packages/app/e2e/rewind-flow.claude.real.spec.ts b/packages/app/e2e/browser/rewind-flow.claude.real.spec.ts similarity index 100% rename from packages/app/e2e/rewind-flow.claude.real.spec.ts rename to packages/app/e2e/browser/rewind-flow.claude.real.spec.ts diff --git a/packages/app/e2e/rewind-flow.codex.real.spec.ts b/packages/app/e2e/browser/rewind-flow.codex.real.spec.ts similarity index 100% rename from packages/app/e2e/rewind-flow.codex.real.spec.ts rename to packages/app/e2e/browser/rewind-flow.codex.real.spec.ts diff --git a/packages/app/e2e/rewind-flow.opencode.real.spec.ts b/packages/app/e2e/browser/rewind-flow.opencode.real.spec.ts similarity index 100% rename from packages/app/e2e/rewind-flow.opencode.real.spec.ts rename to packages/app/e2e/browser/rewind-flow.opencode.real.spec.ts diff --git a/packages/app/e2e/rewind-flow.pi.real.spec.ts b/packages/app/e2e/browser/rewind-flow.pi.real.spec.ts similarity index 100% rename from packages/app/e2e/rewind-flow.pi.real.spec.ts rename to packages/app/e2e/browser/rewind-flow.pi.real.spec.ts diff --git a/packages/app/e2e/rewind-flow.shared.ts b/packages/app/e2e/browser/rewind-flow.shared.ts similarity index 96% rename from packages/app/e2e/rewind-flow.shared.ts rename to packages/app/e2e/browser/rewind-flow.shared.ts index c34e9d782..6e89b85f2 100644 --- a/packages/app/e2e/rewind-flow.shared.ts +++ b/packages/app/e2e/browser/rewind-flow.shared.ts @@ -1,7 +1,7 @@ import { mkdtempSync, realpathSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { test } from "./fixtures"; +import { test } from "../support/fixtures"; import { assertChatTranscript, assertComposerIdle, @@ -15,7 +15,7 @@ import { type AgentHandle, type RewindFlowMode, type RewindFlowProvider, -} from "./helpers/rewind-flow"; +} from "../support/helpers/rewind-flow"; const FILE_PROMPT = "Use the Write tool to create ./qa.txt with the exact content: PASEO_QA_TOKEN"; diff --git a/packages/app/e2e/rewind-menu.ui-contract.spec.ts b/packages/app/e2e/browser/rewind-menu.ui-contract.spec.ts similarity index 97% rename from packages/app/e2e/rewind-menu.ui-contract.spec.ts rename to packages/app/e2e/browser/rewind-menu.ui-contract.spec.ts index 7c12f3f5b..d98ea4127 100644 --- a/packages/app/e2e/rewind-menu.ui-contract.spec.ts +++ b/packages/app/e2e/browser/rewind-menu.ui-contract.spec.ts @@ -1,14 +1,14 @@ import type { Locator } from "@playwright/test"; -import { expect, test, type Page } from "./fixtures"; -import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; -import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate"; +import { expect, test, type Page } from "../support/fixtures"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; +import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate"; import { composerLocator, expectComposerDraft, expectComposerVisible, fillComposerDraft, submitMessage, -} from "./helpers/composer"; +} from "../support/helpers/composer"; // UI plumbing contract against the dev mock provider. Real-provider behavior is tested in `daemon-e2e/*-rewind.real.e2e.test.ts`. diff --git a/packages/app/e2e/same-directory-workspaces.spec.ts b/packages/app/e2e/browser/same-directory-workspaces.spec.ts similarity index 92% rename from packages/app/e2e/same-directory-workspaces.spec.ts rename to packages/app/e2e/browser/same-directory-workspaces.spec.ts index 967d274ef..8546ffb4e 100644 --- a/packages/app/e2e/same-directory-workspaces.spec.ts +++ b/packages/app/e2e/browser/same-directory-workspaces.spec.ts @@ -1,10 +1,10 @@ import { writeFile } from "node:fs/promises"; import path from "node:path"; -import { test, expect, type Page } from "./fixtures"; -import { gotoWorkspace, clickNewTerminal } from "./helpers/launcher"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; -import { expectExplorerEntryVisible } from "./helpers/file-explorer"; -import { expectNoTerminalTabs, clickFirstTerminalTab } from "./helpers/workspace-tabs"; +import { test, expect, type Page } from "../support/fixtures"; +import { gotoWorkspace, clickNewTerminal } from "../support/helpers/launcher"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; +import { expectExplorerEntryVisible } from "../support/helpers/file-explorer"; +import { expectNoTerminalTabs, clickFirstTerminalTab } from "../support/helpers/workspace-tabs"; // Model B: two workspaces can back the SAME directory. What follows from that // split is the contract these specs pin: diff --git a/packages/app/e2e/schedules-edit-model-hydration.spec.ts b/packages/app/e2e/browser/schedules-edit-model-hydration.spec.ts similarity index 94% rename from packages/app/e2e/schedules-edit-model-hydration.spec.ts rename to packages/app/e2e/browser/schedules-edit-model-hydration.spec.ts index 0a706b0e2..02491ee23 100644 --- a/packages/app/e2e/schedules-edit-model-hydration.spec.ts +++ b/packages/app/e2e/browser/schedules-edit-model-hydration.spec.ts @@ -1,16 +1,16 @@ -import { expect, test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; import { addFakeScheduleHostAndReload, buildFakeScheduleHostWorkspace, FAKE_HOST_MODEL_ID, FAKE_HOST_MODEL_LABEL, installFakeScheduleHost, -} from "./helpers/schedule-fake-host"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; -import { expectSettled, expectStableHeight } from "./helpers/settled"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; -import { buildSchedulesRoute } from "../src/utils/host-routes"; +} from "../support/helpers/schedule-fake-host"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; +import { expectSettled, expectStableHeight } from "../support/helpers/settled"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; +import { buildSchedulesRoute } from "../../src/utils/host-routes"; interface ScheduleSeedClient { scheduleCreate(input: { diff --git a/packages/app/e2e/schedules-project-target.spec.ts b/packages/app/e2e/browser/schedules-project-target.spec.ts similarity index 96% rename from packages/app/e2e/schedules-project-target.spec.ts rename to packages/app/e2e/browser/schedules-project-target.spec.ts index 836dbbb24..b93c4e0e1 100644 --- a/packages/app/e2e/schedules-project-target.spec.ts +++ b/packages/app/e2e/browser/schedules-project-target.spec.ts @@ -1,17 +1,17 @@ -import { expect, test, type Page } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; +import { expect, test, type Page } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; import { addFakeScheduleHostAndReload, buildFakeScheduleHostWorkspace, installFakeScheduleHost, -} from "./helpers/schedule-fake-host"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; -import { escapeRegex } from "./helpers/regex"; -import { expectNoTruncation } from "./helpers/no-truncation"; -import { expectSettled, expectStableHeight } from "./helpers/settled"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; -import { buildSchedulesRoute } from "../src/utils/host-routes"; +} from "../support/helpers/schedule-fake-host"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; +import { escapeRegex } from "../support/helpers/regex"; +import { expectNoTruncation } from "../support/helpers/no-truncation"; +import { expectSettled, expectStableHeight } from "../support/helpers/settled"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; +import { buildSchedulesRoute } from "../../src/utils/host-routes"; const MOBILE_SHEET_VIEWPORT = { width: 390, height: 844 }; diff --git a/packages/app/e2e/settings-host-page.spec.ts b/packages/app/e2e/browser/settings-host-page.spec.ts similarity index 63% rename from packages/app/e2e/settings-host-page.spec.ts rename to packages/app/e2e/browser/settings-host-page.spec.ts index 7a16a3566..4bf08c723 100644 --- a/packages/app/e2e/settings-host-page.spec.ts +++ b/packages/app/e2e/browser/settings-host-page.spec.ts @@ -1,8 +1,8 @@ -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 { expect, test } from "../support/fixtures"; +import { gotoAppShell, openSettings } from "../support/helpers/app"; +import { getE2EDaemonPort } from "../support/helpers/daemon-port"; +import { TEST_HOST_LABEL } from "../support/helpers/daemon-registry"; +import { getServerId } from "../support/helpers/server-id"; import { expectSettingsHeader, openSettingsHost, @@ -17,9 +17,8 @@ import { expectHostNoLocalOnlyRows, expectRetiredSidebarSectionsAbsent, expectHostPageVisible, - expectLocalHostEntryFirst, seedSavedSettingsHosts, -} from "./helpers/settings"; +} from "../support/helpers/settings"; test.describe("Settings host page", () => { test("connections section shows the seeded connection endpoint", async ({ page }) => { @@ -95,24 +94,6 @@ test.describe("Settings host page", () => { 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(); @@ -161,46 +142,4 @@ test.describe("Settings host page", () => { 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); - }); }); diff --git a/packages/app/e2e/settings-i18n.spec.ts b/packages/app/e2e/browser/settings-i18n.spec.ts similarity index 81% rename from packages/app/e2e/settings-i18n.spec.ts rename to packages/app/e2e/browser/settings-i18n.spec.ts index 556c82f56..808e0741d 100644 --- a/packages/app/e2e/settings-i18n.spec.ts +++ b/packages/app/e2e/browser/settings-i18n.spec.ts @@ -1,6 +1,6 @@ -import { test, expect } from "./fixtures"; -import { gotoAppShell, openSettings } from "./helpers/app"; -import { openSettingsSection } from "./helpers/settings"; +import { test, expect } from "../support/fixtures"; +import { gotoAppShell, openSettings } from "../support/helpers/app"; +import { openSettingsSection } from "../support/helpers/settings"; test("Settings language selector switches General labels", async ({ page }) => { test.setTimeout(120_000); diff --git a/packages/app/e2e/settings-navigation.spec.ts b/packages/app/e2e/browser/settings-navigation.spec.ts similarity index 95% rename from packages/app/e2e/settings-navigation.spec.ts rename to packages/app/e2e/browser/settings-navigation.spec.ts index 70a8de6f4..5a3624f06 100644 --- a/packages/app/e2e/settings-navigation.spec.ts +++ b/packages/app/e2e/browser/settings-navigation.spec.ts @@ -1,12 +1,12 @@ -import { test, expect } from "./fixtures"; +import { test, expect } from "../support/fixtures"; import { buildHostWorkspaceRoute, buildOpenProjectRoute, buildSettingsRoute, buildSettingsSectionRoute, } from "@/utils/host-routes"; -import { gotoAppShell, openSettings } from "./helpers/app"; -import { getE2EDaemonPort } from "./helpers/daemon-port"; +import { gotoAppShell, openSettings } from "../support/helpers/app"; +import { getE2EDaemonPort } from "../support/helpers/daemon-port"; import { closeCompactSettings, openSettingsSection, @@ -39,9 +39,9 @@ import { expectSettingsHostPickerLabel, openSettingsHostSection, removeCurrentHostFromSettings, -} from "./helpers/settings"; -import { getServerId } from "./helpers/server-id"; -import { expectAppRoute } from "./helpers/route-assertions"; +} from "../support/helpers/settings"; +import { getServerId } from "../support/helpers/server-id"; +import { expectAppRoute } from "../support/helpers/route-assertions"; async function openWorkspace( page: import("@playwright/test").Page, diff --git a/packages/app/e2e/settings-toggle-tab-regression.spec.ts b/packages/app/e2e/browser/settings-toggle-tab-regression.spec.ts similarity index 92% rename from packages/app/e2e/settings-toggle-tab-regression.spec.ts rename to packages/app/e2e/browser/settings-toggle-tab-regression.spec.ts index 319289860..ff3d1f7cc 100644 --- a/packages/app/e2e/settings-toggle-tab-regression.spec.ts +++ b/packages/app/e2e/browser/settings-toggle-tab-regression.spec.ts @@ -1,9 +1,9 @@ import { buildHostAgentDetailRoute, buildHostWorkspaceRoute } from "@/utils/host-routes"; -import { expect, test } from "./fixtures"; -import { createIdleAgent, openWorkspaceWithAgents } from "./helpers/archive-tab"; -import { waitForTabBar, expectAgentTabActive } from "./helpers/launcher"; -import { seedWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; +import { expect, test } from "../support/fixtures"; +import { createIdleAgent, openWorkspaceWithAgents } from "../support/helpers/archive-tab"; +import { waitForTabBar, expectAgentTabActive } from "../support/helpers/launcher"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; async function pressSettingsToggleShortcut(page: import("@playwright/test").Page) { const modifier = process.platform === "darwin" ? "Meta" : "Control"; diff --git a/packages/app/e2e/sidebar-help.spec.ts b/packages/app/e2e/browser/sidebar-help.spec.ts similarity index 96% rename from packages/app/e2e/sidebar-help.spec.ts rename to packages/app/e2e/browser/sidebar-help.spec.ts index 129251516..5e3ccf74c 100644 --- a/packages/app/e2e/sidebar-help.spec.ts +++ b/packages/app/e2e/browser/sidebar-help.spec.ts @@ -1,6 +1,6 @@ -import { expect, test, type Page } from "./fixtures"; -import { gotoAppShell, openSettings } from "./helpers/app"; -import { openSettingsSection } from "./helpers/settings"; +import { expect, test, type Page } from "../support/fixtures"; +import { gotoAppShell, openSettings } from "../support/helpers/app"; +import { openSettingsSection } from "../support/helpers/settings"; const DISCORD_DESTINATION = /^https:\/\/(?:discord\.gg\/jz8T2uahpH|discord\.com\/invite\/jz8T2uahpH)(?:[/?#]|$)/; diff --git a/packages/app/e2e/sidebar-host-filter-multi.spec.ts b/packages/app/e2e/browser/sidebar-host-filter-multi.spec.ts similarity index 88% rename from packages/app/e2e/sidebar-host-filter-multi.spec.ts rename to packages/app/e2e/browser/sidebar-host-filter-multi.spec.ts index ee065cb0d..30d4a4422 100644 --- a/packages/app/e2e/sidebar-host-filter-multi.spec.ts +++ b/packages/app/e2e/browser/sidebar-host-filter-multi.spec.ts @@ -1,15 +1,15 @@ import { expect } from "@playwright/test"; -import { test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; +import { test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; import { addOfflineHostAndReload, expectHostFilterRow, openSidebarDisplayPreferences, selectAllHostsFilter, toggleHostFilter, -} from "./helpers/hosts"; -import { seedWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; +} from "../support/helpers/hosts"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; const SECONDARY_HOST_ID = "host-filter-secondary"; diff --git a/packages/app/e2e/sidebar-model-b.spec.ts b/packages/app/e2e/browser/sidebar-model-b.spec.ts similarity index 92% rename from packages/app/e2e/sidebar-model-b.spec.ts rename to packages/app/e2e/browser/sidebar-model-b.spec.ts index 30ebb767b..e55262fd1 100644 --- a/packages/app/e2e/sidebar-model-b.spec.ts +++ b/packages/app/e2e/browser/sidebar-model-b.spec.ts @@ -1,11 +1,11 @@ -import { test, expect, type Page } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; -import { gotoWorkspace, clickNewTerminal } from "./helpers/launcher"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; -import { seedMockAgentWorkspace } from "./helpers/mock-agent"; -import { getServerId } from "./helpers/server-id"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; -import { getVisibleWorkspaceAgentTabIds } from "./helpers/workspace-tabs"; +import { test, expect, type Page } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { gotoWorkspace, clickNewTerminal } from "../support/helpers/launcher"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; +import { seedMockAgentWorkspace } from "../support/helpers/mock-agent"; +import { getServerId } from "../support/helpers/server-id"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; +import { getVisibleWorkspaceAgentTabIds } from "../support/helpers/workspace-tabs"; // Model B sidebar shape: every project — git or non-git, single- or // multi-workspace — renders as the same expandable parent, the deepest sidebar diff --git a/packages/app/e2e/sidebar-reorder.spec.ts b/packages/app/e2e/browser/sidebar-reorder.spec.ts similarity index 88% rename from packages/app/e2e/sidebar-reorder.spec.ts rename to packages/app/e2e/browser/sidebar-reorder.spec.ts index 0979694f0..6c219a564 100644 --- a/packages/app/e2e/sidebar-reorder.spec.ts +++ b/packages/app/e2e/browser/sidebar-reorder.spec.ts @@ -1,9 +1,9 @@ import type { Locator } from "@playwright/test"; -import { expect, test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; -import { getServerId } from "./helpers/server-id"; -import { seedWorkspace } from "./helpers/seed-client"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { getServerId } from "../support/helpers/server-id"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; async function rowTestIds(rows: Locator) { return rows.evaluateAll((elements) => diff --git a/packages/app/e2e/sidebar-resize-handle.spec.ts b/packages/app/e2e/browser/sidebar-resize-handle.spec.ts similarity index 94% rename from packages/app/e2e/sidebar-resize-handle.spec.ts rename to packages/app/e2e/browser/sidebar-resize-handle.spec.ts index eda7f2c81..7df4c64f6 100644 --- a/packages/app/e2e/sidebar-resize-handle.spec.ts +++ b/packages/app/e2e/browser/sidebar-resize-handle.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page } from "./fixtures"; +import { expect, test, type Page } from "../support/fixtures"; test.use({ viewport: { width: 1600, height: 900 } }); diff --git a/packages/app/e2e/sidebar-workspace-pin-shortcut.spec.ts b/packages/app/e2e/browser/sidebar-workspace-pin-shortcut.spec.ts similarity index 96% rename from packages/app/e2e/sidebar-workspace-pin-shortcut.spec.ts rename to packages/app/e2e/browser/sidebar-workspace-pin-shortcut.spec.ts index b9a0bb89e..5f5e710a3 100644 --- a/packages/app/e2e/sidebar-workspace-pin-shortcut.spec.ts +++ b/packages/app/e2e/browser/sidebar-workspace-pin-shortcut.spec.ts @@ -1,8 +1,8 @@ -import { test, expect, type Page } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; -import { daemonWsRoutePattern } from "./helpers/daemon-port"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; +import { test, expect, type Page } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; // The pin shortcut used to be registered by the sidebar row itself, so it silently did nothing // whenever the row was unmounted — a collapsed project section being the common case. It now diff --git a/packages/app/e2e/sidebar-workspace-rename.spec.ts b/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts similarity index 92% rename from packages/app/e2e/sidebar-workspace-rename.spec.ts rename to packages/app/e2e/browser/sidebar-workspace-rename.spec.ts index 8e3dadce0..21ad51a6b 100644 --- a/packages/app/e2e/sidebar-workspace-rename.spec.ts +++ b/packages/app/e2e/browser/sidebar-workspace-rename.spec.ts @@ -1,7 +1,7 @@ -import { test, expect, type Page } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; -import { seedWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; +import { test, expect, type Page } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; function workspaceRowTestId(workspaceId: string): string { return `sidebar-workspace-row-${getServerId()}:${workspaceId}`; diff --git a/packages/app/e2e/sidebar-workspace.spec.ts b/packages/app/e2e/browser/sidebar-workspace.spec.ts similarity index 96% rename from packages/app/e2e/sidebar-workspace.spec.ts rename to packages/app/e2e/browser/sidebar-workspace.spec.ts index 223a0cce3..e6aa92416 100644 --- a/packages/app/e2e/sidebar-workspace.spec.ts +++ b/packages/app/e2e/browser/sidebar-workspace.spec.ts @@ -1,17 +1,17 @@ import path from "node:path"; -import { test, expect } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; +import { test, expect } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; import { closeMobileAgentSidebar, expectMobileAgentSidebarHidden, expectMobileAgentSidebarVisible, openMobileAgentSidebar, pinWorkspaceFromSidebar, -} from "./helpers/sidebar"; -import { seedWorkspace } from "./helpers/seed-client"; -import { expectWorkspaceHeader } from "./helpers/workspace-ui"; -import { getServerId } from "./helpers/server-id"; -import { escapeRegex } from "./helpers/regex"; +} from "../support/helpers/sidebar"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { expectWorkspaceHeader } from "../support/helpers/workspace-ui"; +import { getServerId } from "../support/helpers/server-id"; +import { escapeRegex } from "../support/helpers/regex"; const GITHUB_REMOTE_URL = "https://github.com/test-owner/test-repo.git"; diff --git a/packages/app/e2e/browser/startup-loading.spec.ts b/packages/app/e2e/browser/startup-loading.spec.ts new file mode 100644 index 000000000..e33a733d4 --- /dev/null +++ b/packages/app/e2e/browser/startup-loading.spec.ts @@ -0,0 +1,19 @@ +import { test } from "../support/fixtures"; +import { startupScenario } from "../support/helpers/startup-dsl"; + +test.describe("Startup loading presentation", () => { + test("mobile reconnect preserves the saved host shell", async ({ page }) => { + const startup = await startupScenario(page) + .withMobileViewport() + .withSavedHost({ + serverId: "srv_unreachable_mobile", + label: "Dev", + endpoint: "127.0.0.1:45678", + }) + .openRoot(); + + await startup.expectsSavedHostShell({ serverId: "srv_unreachable_mobile", label: "Dev" }); + await startup.expectsNoSavedHostErrorStatus(); + await startup.expectsNoLocalServerStartupCopy(); + }); +}); diff --git a/packages/app/e2e/startup-wire-metrics.spec.ts b/packages/app/e2e/browser/startup-wire-metrics.spec.ts similarity index 98% rename from packages/app/e2e/startup-wire-metrics.spec.ts rename to packages/app/e2e/browser/startup-wire-metrics.spec.ts index 184d0a458..d624d5e31 100644 --- a/packages/app/e2e/startup-wire-metrics.spec.ts +++ b/packages/app/e2e/browser/startup-wire-metrics.spec.ts @@ -1,8 +1,8 @@ import { Buffer } from "node:buffer"; import type { CDPSession, Page, TestInfo } from "@playwright/test"; -import { expect, test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; type WireDirection = "sent" | "received"; type WirePhase = "startup" | "workspace_clicks"; diff --git a/packages/app/e2e/subagent-detach.spec.ts b/packages/app/e2e/browser/subagent-detach.spec.ts similarity index 73% rename from packages/app/e2e/subagent-detach.spec.ts rename to packages/app/e2e/browser/subagent-detach.spec.ts index b2da2217c..a8a4e0cf4 100644 --- a/packages/app/e2e/subagent-detach.spec.ts +++ b/packages/app/e2e/browser/subagent-detach.spec.ts @@ -1,15 +1,15 @@ -import { test } from "./fixtures"; -import { expectWorkspaceTabVisible } from "./helpers/archive-tab"; -import { expectAgentTabActive } from "./helpers/launcher"; -import { openAgentRoute } from "./helpers/mock-agent"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; +import { test } from "../support/fixtures"; +import { expectWorkspaceTabVisible } from "../support/helpers/archive-tab"; +import { expectAgentTabActive } from "../support/helpers/launcher"; +import { openAgentRoute } from "../support/helpers/mock-agent"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; import { detachSubagentFromTrack, expectSubagentRowGone, expectSubagentRowVisible, openSubagentsTrack, seedParentWithSubagent, -} from "./helpers/subagents"; +} from "../support/helpers/subagents"; test.describe("Subagent detach", () => { let workspace: SeededWorkspace; diff --git a/packages/app/e2e/terminal-activity-indicators.spec.ts b/packages/app/e2e/browser/terminal-activity-indicators.spec.ts similarity index 97% rename from packages/app/e2e/terminal-activity-indicators.spec.ts rename to packages/app/e2e/browser/terminal-activity-indicators.spec.ts index 953611de1..f6f23ac93 100644 --- a/packages/app/e2e/terminal-activity-indicators.spec.ts +++ b/packages/app/e2e/browser/terminal-activity-indicators.spec.ts @@ -1,7 +1,7 @@ import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { test, expect, type Page } from "./fixtures"; -import { TerminalE2EHarness, type TerminalInstance } from "./helpers/terminal-dsl"; +import { test, expect, type Page } from "../support/fixtures"; +import { TerminalE2EHarness, type TerminalInstance } from "../support/helpers/terminal-dsl"; type HookActivityState = "running" | "idle" | "needs-input"; type TabStatusBucket = "running" | "needs_input" | "attention" | "none"; diff --git a/packages/app/e2e/terminal-alternate-screen.spec.ts b/packages/app/e2e/browser/terminal-alternate-screen.spec.ts similarity index 96% rename from packages/app/e2e/terminal-alternate-screen.spec.ts rename to packages/app/e2e/browser/terminal-alternate-screen.spec.ts index 53a657810..db6f0e7e8 100644 --- a/packages/app/e2e/terminal-alternate-screen.spec.ts +++ b/packages/app/e2e/browser/terminal-alternate-screen.spec.ts @@ -1,15 +1,15 @@ import type { Page } from "@playwright/test"; -import { test, expect } from "./fixtures"; -import { TerminalE2EHarness, withTerminalInApp } from "./helpers/terminal-dsl"; -import { getTabTestIds } from "./helpers/launcher"; +import { test, expect } from "../support/fixtures"; +import { TerminalE2EHarness, withTerminalInApp } from "../support/helpers/terminal-dsl"; +import { getTabTestIds } from "../support/helpers/launcher"; import { installTerminalRenderProbe, readTerminalRenderProbe, resetTerminalRenderProbe, startTerminalFrameSampling, summarizeTerminalRenderProbe, -} from "./helpers/terminal-probes"; -import { getTerminalBufferText, waitForTerminalContent } from "./helpers/terminal-perf"; +} from "../support/helpers/terminal-probes"; +import { getTerminalBufferText, waitForTerminalContent } from "../support/helpers/terminal-perf"; interface TerminalLayoutMetrics { visibleSurfaceCount: number; diff --git a/packages/app/e2e/terminal-keystroke-stress.spec.ts b/packages/app/e2e/browser/terminal-keystroke-stress.spec.ts similarity index 98% rename from packages/app/e2e/terminal-keystroke-stress.spec.ts rename to packages/app/e2e/browser/terminal-keystroke-stress.spec.ts index f12adcbfc..fb5bbbd00 100644 --- a/packages/app/e2e/terminal-keystroke-stress.spec.ts +++ b/packages/app/e2e/browser/terminal-keystroke-stress.spec.ts @@ -1,14 +1,14 @@ import { performance } from "node:perf_hooks"; import type { Page, TestInfo } from "@playwright/test"; -import { test, expect } from "./fixtures"; -import { TerminalE2EHarness, type TerminalInstance } from "./helpers/terminal-dsl"; +import { test, expect } from "../support/fixtures"; +import { TerminalE2EHarness, type TerminalInstance } from "../support/helpers/terminal-dsl"; import { installTerminalKeystrokeStressProbe, readTerminalKeystrokeStressReport, resetTerminalKeystrokeStressProbe, type LatencyStats, -} from "./helpers/terminal-probes"; -import { waitForTerminalContent } from "./helpers/terminal-perf"; +} from "../support/helpers/terminal-probes"; +import { waitForTerminalContent } from "../support/helpers/terminal-perf"; const INPUT_TEXT = buildStressText(600); const BIG_DIFF_BYTES = 256_000; diff --git a/packages/app/e2e/terminal-performance.spec.ts b/packages/app/e2e/browser/terminal-performance.spec.ts similarity index 96% rename from packages/app/e2e/terminal-performance.spec.ts rename to packages/app/e2e/browser/terminal-performance.spec.ts index 3ed3f4fae..cf02b4647 100644 --- a/packages/app/e2e/terminal-performance.spec.ts +++ b/packages/app/e2e/browser/terminal-performance.spec.ts @@ -1,12 +1,12 @@ -import { test, expect } from "./fixtures"; -import { TerminalE2EHarness } from "./helpers/terminal-dsl"; +import { test, expect } from "../support/fixtures"; +import { TerminalE2EHarness } from "../support/helpers/terminal-dsl"; import { waitForTerminalContent, measureKeystrokeLatency, computePercentile, round2, type LatencySample, -} from "./helpers/terminal-perf"; +} from "../support/helpers/terminal-perf"; const LINE_COUNT = 50_000; const THROUGHPUT_BUDGET_MS = 30_000; diff --git a/packages/app/e2e/terminal-protocol-query.spec.ts b/packages/app/e2e/browser/terminal-protocol-query.spec.ts similarity index 92% rename from packages/app/e2e/terminal-protocol-query.spec.ts rename to packages/app/e2e/browser/terminal-protocol-query.spec.ts index 6122a5c34..023bf12b9 100644 --- a/packages/app/e2e/terminal-protocol-query.spec.ts +++ b/packages/app/e2e/browser/terminal-protocol-query.spec.ts @@ -1,8 +1,8 @@ import { writeFile } from "node:fs/promises"; import path from "node:path"; -import { test, expect } from "./fixtures"; -import { TerminalE2EHarness } from "./helpers/terminal-dsl"; -import { getTerminalBufferText, waitForTerminalContent } from "./helpers/terminal-perf"; +import { test, expect } from "../support/fixtures"; +import { TerminalE2EHarness } from "../support/helpers/terminal-dsl"; +import { getTerminalBufferText, waitForTerminalContent } from "../support/helpers/terminal-perf"; const OSC11_CAPTURE_SCRIPT = ` let captured = Buffer.alloc(0); diff --git a/packages/app/e2e/terminal-split-resize.spec.ts b/packages/app/e2e/browser/terminal-split-resize.spec.ts similarity index 96% rename from packages/app/e2e/terminal-split-resize.spec.ts rename to packages/app/e2e/browser/terminal-split-resize.spec.ts index 819a3ed5f..7b443907a 100644 --- a/packages/app/e2e/terminal-split-resize.spec.ts +++ b/packages/app/e2e/browser/terminal-split-resize.spec.ts @@ -1,7 +1,7 @@ import type { Page } from "@playwright/test"; -import { test, expect } from "./fixtures"; -import { TerminalE2EHarness, withTerminalInApp } from "./helpers/terminal-dsl"; -import { getTerminalBufferText, waitForTerminalContent } from "./helpers/terminal-perf"; +import { test, expect } from "../support/fixtures"; +import { TerminalE2EHarness, withTerminalInApp } from "../support/helpers/terminal-dsl"; +import { getTerminalBufferText, waitForTerminalContent } from "../support/helpers/terminal-perf"; interface TerminalSize { rows: number | null; diff --git a/packages/app/e2e/terminal-stuck-size.spec.ts b/packages/app/e2e/browser/terminal-stuck-size.spec.ts similarity index 95% rename from packages/app/e2e/terminal-stuck-size.spec.ts rename to packages/app/e2e/browser/terminal-stuck-size.spec.ts index 9e80f16b5..4c41cb598 100644 --- a/packages/app/e2e/terminal-stuck-size.spec.ts +++ b/packages/app/e2e/browser/terminal-stuck-size.spec.ts @@ -1,8 +1,8 @@ -import { test, expect, type Page } from "./fixtures"; -import { TerminalE2EHarness } from "./helpers/terminal-dsl"; -import { getTerminalBufferText } from "./helpers/terminal-perf"; -import { buildHostWorkspaceRoute } from "../src/utils/host-routes"; -import { getServerId } from "./helpers/server-id"; +import { test, expect, type Page } from "../support/fixtures"; +import { TerminalE2EHarness } from "../support/helpers/terminal-dsl"; +import { getTerminalBufferText } from "../support/helpers/terminal-perf"; +import { buildHostWorkspaceRoute } from "../../src/utils/host-routes"; +import { getServerId } from "../support/helpers/server-id"; /** * Regression: a terminal created while the window is unfocused must still claim its PTY size diff --git a/packages/app/e2e/tool-call-shimmer.spec.ts b/packages/app/e2e/browser/tool-call-shimmer.spec.ts similarity index 97% rename from packages/app/e2e/tool-call-shimmer.spec.ts rename to packages/app/e2e/browser/tool-call-shimmer.spec.ts index c0d1ad514..a988626b9 100644 --- a/packages/app/e2e/tool-call-shimmer.spec.ts +++ b/packages/app/e2e/browser/tool-call-shimmer.spec.ts @@ -1,7 +1,7 @@ import type { Locator, Page } from "@playwright/test"; -import { test, expect } from "./fixtures"; -import { daemonWsRoutePattern } from "./helpers/daemon-port"; -import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; +import { test, expect } from "../support/fixtures"; +import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; type WebSocketMessage = string | Buffer; diff --git a/packages/app/e2e/user-message-contract.ui-contract.spec.ts b/packages/app/e2e/browser/user-message-contract.ui-contract.spec.ts similarity index 91% rename from packages/app/e2e/user-message-contract.ui-contract.spec.ts rename to packages/app/e2e/browser/user-message-contract.ui-contract.spec.ts index 4a536368c..55a4df4ba 100644 --- a/packages/app/e2e/user-message-contract.ui-contract.spec.ts +++ b/packages/app/e2e/browser/user-message-contract.ui-contract.spec.ts @@ -1,12 +1,12 @@ -import { expect, test, type Page } from "./fixtures"; -import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; +import { expect, test, type Page } from "../support/fixtures"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; import { composerLocator, expectComposerEditable, expectComposerVisible, fillComposerDraft, submitMessage, -} from "./helpers/composer"; +} from "../support/helpers/composer"; async function expectUserMessageCount(page: Page, expected: number): Promise { await expect(page.getByTestId("user-message")).toHaveCount(expected, { timeout: 15_000 }); diff --git a/packages/app/e2e/viewed-agent-timelines.spec.ts b/packages/app/e2e/browser/viewed-agent-timelines.spec.ts similarity index 93% rename from packages/app/e2e/viewed-agent-timelines.spec.ts rename to packages/app/e2e/browser/viewed-agent-timelines.spec.ts index a16723aec..4118f3e36 100644 --- a/packages/app/e2e/viewed-agent-timelines.spec.ts +++ b/packages/app/e2e/browser/viewed-agent-timelines.spec.ts @@ -1,15 +1,15 @@ import { expect, type Page } from "@playwright/test"; import { buildHostAgentDetailRoute } from "@/utils/host-routes"; -import { test } from "./fixtures"; -import { seedWorkspace, type SeedDaemonClient } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; -import { observeTimelineSubscriptions } from "./helpers/timeline-delivery"; -import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; -import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate"; +import { test } from "../support/fixtures"; +import { seedWorkspace, type SeedDaemonClient } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; +import { observeTimelineSubscriptions } from "../support/helpers/timeline-delivery"; +import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; +import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate"; import { expectReconnectingToastGone, expectReconnectingToastVisible, -} from "./helpers/workspace-ui"; +} from "../support/helpers/workspace-ui"; interface ViewedTimelineScenario { client: SeedDaemonClient; diff --git a/packages/app/e2e/workspace-agent-tab-rename.spec.ts b/packages/app/e2e/browser/workspace-agent-tab-rename.spec.ts similarity index 84% rename from packages/app/e2e/workspace-agent-tab-rename.spec.ts rename to packages/app/e2e/browser/workspace-agent-tab-rename.spec.ts index 68a69de63..2fd2c66e8 100644 --- a/packages/app/e2e/workspace-agent-tab-rename.spec.ts +++ b/packages/app/e2e/browser/workspace-agent-tab-rename.spec.ts @@ -1,11 +1,11 @@ import { randomUUID } from "node:crypto"; -import { test, expect, type Page } from "./fixtures"; -import { seedWorkspace, type SeedDaemonClient } from "./helpers/seed-client"; -import { createIdleAgent, expectWorkspaceTabVisible } from "./helpers/archive-tab"; -import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; +import { test, expect, type Page } from "../support/fixtures"; +import { seedWorkspace, type SeedDaemonClient } from "../support/helpers/seed-client"; +import { createIdleAgent, expectWorkspaceTabVisible } from "../support/helpers/archive-tab"; +import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; import { buildHostAgentDetailRoute } from "@/utils/host-routes"; -import { renameModalInput, renameModalSubmit } from "./helpers/rename"; -import { getServerId } from "./helpers/server-id"; +import { renameModalInput, renameModalSubmit } from "../support/helpers/rename"; +import { getServerId } from "../support/helpers/server-id"; async function openAgentInWorkspace(page: Page, agent: { id: string; workspaceId: string }) { await page.goto(buildHostAgentDetailRoute(getServerId(), agent.id, agent.workspaceId)); diff --git a/packages/app/e2e/workspace-agent-title-handoff.spec.ts b/packages/app/e2e/browser/workspace-agent-title-handoff.spec.ts similarity index 91% rename from packages/app/e2e/workspace-agent-title-handoff.spec.ts rename to packages/app/e2e/browser/workspace-agent-title-handoff.spec.ts index fa9e922ba..b7375104c 100644 --- a/packages/app/e2e/workspace-agent-title-handoff.spec.ts +++ b/packages/app/e2e/browser/workspace-agent-title-handoff.spec.ts @@ -1,10 +1,10 @@ -import { test, expect } from "./fixtures"; -import { expectComposerVisible, submitMessage } from "./helpers/composer"; -import { delayCreatedAgentInitialTailResponse } from "./helpers/agent-timeline-gate"; -import { delayBrowserAgentCreatedStatus } from "./helpers/new-workspace"; -import { seedWorkspace, type SeedDaemonClient } from "./helpers/seed-client"; -import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; -import { getServerId } from "./helpers/server-id"; +import { test, expect } from "../support/fixtures"; +import { expectComposerVisible, submitMessage } from "../support/helpers/composer"; +import { delayCreatedAgentInitialTailResponse } from "../support/helpers/agent-timeline-gate"; +import { delayBrowserAgentCreatedStatus } from "../support/helpers/new-workspace"; +import { seedWorkspace, type SeedDaemonClient } from "../support/helpers/seed-client"; +import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; +import { getServerId } from "../support/helpers/server-id"; import { buildHostWorkspaceRoute } from "@/utils/host-routes"; async function waitForCreatedAgentId( diff --git a/packages/app/e2e/workspace-archive-shortcut.spec.ts b/packages/app/e2e/browser/workspace-archive-shortcut.spec.ts similarity index 68% rename from packages/app/e2e/workspace-archive-shortcut.spec.ts rename to packages/app/e2e/browser/workspace-archive-shortcut.spec.ts index 8e6881bed..8ad2b1f1b 100644 --- a/packages/app/e2e/workspace-archive-shortcut.spec.ts +++ b/packages/app/e2e/browser/workspace-archive-shortcut.spec.ts @@ -1,9 +1,12 @@ import { existsSync } from "node:fs"; -import { expect, test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; -import { seedWorkspace } from "./helpers/seed-client"; -import { expectWorkspaceAbsentFromSidebar, selectWorkspaceInSidebar } from "./helpers/sidebar"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { + expectWorkspaceAbsentFromSidebar, + selectWorkspaceInSidebar, +} from "../support/helpers/sidebar"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; test.describe("Workspace archive shortcut", () => { test("archives the selected workspace without removing its local checkout", async ({ page }) => { diff --git a/packages/app/e2e/workspace-cwd.spec.ts b/packages/app/e2e/browser/workspace-cwd.spec.ts similarity index 93% rename from packages/app/e2e/workspace-cwd.spec.ts rename to packages/app/e2e/browser/workspace-cwd.spec.ts index 01544b735..e702cc0f7 100644 --- a/packages/app/e2e/workspace-cwd.spec.ts +++ b/packages/app/e2e/browser/workspace-cwd.spec.ts @@ -1,13 +1,13 @@ -import { expect, test } from "./fixtures"; -import { clickNewChat, clickNewTerminal, gotoWorkspace } from "./helpers/launcher"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; +import { expect, test } from "../support/fixtures"; +import { clickNewChat, clickNewTerminal, gotoWorkspace } from "../support/helpers/launcher"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; import { expectTerminalSurfaceVisible, focusTerminalSurface, typeInTerminal, setupDeterministicPrompt, waitForTerminalContent, -} from "./helpers/terminal-perf"; +} from "../support/helpers/terminal-perf"; interface CreatedAgentCwdAssertion { workspaceId: string; diff --git a/packages/app/e2e/workspace-focus-mode.spec.ts b/packages/app/e2e/browser/workspace-focus-mode.spec.ts similarity index 97% rename from packages/app/e2e/workspace-focus-mode.spec.ts rename to packages/app/e2e/browser/workspace-focus-mode.spec.ts index 5aa030ea2..8f5680089 100644 --- a/packages/app/e2e/workspace-focus-mode.spec.ts +++ b/packages/app/e2e/browser/workspace-focus-mode.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "./fixtures"; +import { expect, test } from "../support/fixtures"; const modifier = process.platform === "darwin" ? "Meta" : "Control"; diff --git a/packages/app/e2e/workspace-lifecycle.spec.ts b/packages/app/e2e/browser/workspace-lifecycle.spec.ts similarity index 95% rename from packages/app/e2e/workspace-lifecycle.spec.ts rename to packages/app/e2e/browser/workspace-lifecycle.spec.ts index 8e04244ea..ca1accab8 100644 --- a/packages/app/e2e/workspace-lifecycle.spec.ts +++ b/packages/app/e2e/browser/workspace-lifecycle.spec.ts @@ -1,9 +1,9 @@ -import { test } from "./fixtures"; +import { test } from "../support/fixtures"; import { createAgentChatFromLauncher, createStandaloneTerminalFromLauncher, expectTerminalCwd, -} from "./helpers/workspace-lifecycle"; +} from "../support/helpers/workspace-lifecycle"; test.describe("Workspace lifecycle", () => { // The first test after a spec-file switch can intermittently fail because diff --git a/packages/app/e2e/workspace-model-regressions.spec.ts b/packages/app/e2e/browser/workspace-model-regressions.spec.ts similarity index 96% rename from packages/app/e2e/workspace-model-regressions.spec.ts rename to packages/app/e2e/browser/workspace-model-regressions.spec.ts index 5a8554d5a..2866e83ab 100644 --- a/packages/app/e2e/workspace-model-regressions.spec.ts +++ b/packages/app/e2e/browser/workspace-model-regressions.spec.ts @@ -1,7 +1,11 @@ -import { test, expect } from "./fixtures"; -import { expectWorkspaceTabVisible } from "./helpers/archive-tab"; -import { expectComposerEditable, expectComposerVisible, submitMessage } from "./helpers/composer"; -import { clickNewChat, gotoWorkspace } from "./helpers/launcher"; +import { test, expect } from "../support/fixtures"; +import { expectWorkspaceTabVisible } from "../support/helpers/archive-tab"; +import { + expectComposerEditable, + expectComposerVisible, + submitMessage, +} from "../support/helpers/composer"; +import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher"; import { assertNewWorkspaceSidebarAndHeader, connectNewWorkspaceDaemonClient, @@ -10,16 +14,16 @@ import { selectWorkspaceIsolation, submitNewWorkspaceEmpty, submitNewWorkspacePrompt, -} from "./helpers/new-workspace"; -import { getServerId } from "./helpers/server-id"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; +} from "../support/helpers/new-workspace"; +import { getServerId } from "../support/helpers/server-id"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; import { expectSubagentRowVisible, openSubagentsTrack, seedParentWithCrossWorkspaceSubagent, -} from "./helpers/subagents"; -import { expectWorkspaceHeader, waitForSidebarHydration } from "./helpers/workspace-ui"; -import { getVisibleWorkspaceAgentTabIds } from "./helpers/workspace-tabs"; +} from "../support/helpers/subagents"; +import { expectWorkspaceHeader, waitForSidebarHydration } from "../support/helpers/workspace-ui"; +import { getVisibleWorkspaceAgentTabIds } from "../support/helpers/workspace-tabs"; type NewWorkspaceDaemonClient = Awaited>; type WorkspaceIndicator = "attention" | "done" | "failed" | "loading" | "needs_input" | "running"; diff --git a/packages/app/e2e/workspace-model-restart.spec.ts b/packages/app/e2e/browser/workspace-model-restart.spec.ts similarity index 96% rename from packages/app/e2e/workspace-model-restart.spec.ts rename to packages/app/e2e/browser/workspace-model-restart.spec.ts index 3f459bf46..c8fbb99cd 100644 --- a/packages/app/e2e/workspace-model-restart.spec.ts +++ b/packages/app/e2e/browser/workspace-model-restart.spec.ts @@ -7,18 +7,21 @@ import path from "node:path"; import net from "node:net"; import { test, expect, type Page } from "@playwright/test"; import { buildHostWorkspaceRoute, decodeWorkspaceIdFromPathSegment } from "@/utils/host-routes"; -import { buildSeededHost } from "./helpers/daemon-registry"; -import { loadDaemonClientConstructor } from "./helpers/daemon-client-loader"; -import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./helpers/node-ws-factory"; -import { withDisabledE2ESpeechEnv } from "./helpers/speech-env"; +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 "./helpers/new-workspace"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; -import { getVisibleWorkspaceAgentTabIds } from "./helpers/workspace-tabs"; +} 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)}`; @@ -240,7 +243,7 @@ async function startRestartDaemon(input: { return startRestartDaemon(input); } - const serverDir = path.resolve(__dirname, "../../server"); + const serverDir = path.resolve(__dirname, "../../../server"); const tsxBin = execSync("which tsx").toString().trim(); const child = spawn(tsxBin, ["scripts/supervisor-entrypoint.ts", "--dev"], { cwd: serverDir, @@ -297,7 +300,7 @@ async function connectRestartDaemonClient(port: number): Promise { - await page.addInitScript(() => { - const trace: StartupPresentation[] = []; - window.__paseoStartupPresentationTrace = trace; - - document.addEventListener("DOMContentLoaded", () => { - const recordPresentation = () => { - let presentation: StartupPresentation | null = null; - if (document.querySelector('[data-testid="startup-splash"]')) { - presentation = "splash"; - } else if ( - document.querySelector( - '[data-testid="workspace-header-title"], [data-testid="sidebar-settings"]', - ) - ) { - presentation = "app"; - } - if (presentation && trace.at(-1) !== presentation) { - trace.push(presentation); - } - }; - new MutationObserver(recordPresentation).observe(document.documentElement, { - childList: true, - subtree: true, - }); - recordPresentation(); - }); - }); -} - -async function getStartupPresentation(page: Page): Promise { - return page.evaluate(() => window.__paseoStartupPresentationTrace?.slice() ?? []); -} - async function expectNoLoadingWorkspacePane( page: Page, input: { label: string; durationMs?: number }, @@ -127,39 +84,6 @@ async function closeFirstVisibleDraftTab(page: Page): Promise { await closeButton.first().click(); } -async function openWorkspaceThroughApp( - page: Page, - input: { - serverId: string; - workspace: Awaited>; - }, -): Promise { - await gotoAppShell(page); - await waitForSidebarHydration(page); - await switchWorkspaceViaSidebar({ - page, - serverId: input.serverId, - workspaceId: input.workspace.workspaceId, - }); - await waitForWorkspaceTabsVisible(page); - await expectWorkspaceLocation(page, input); -} - -async function expectWorkspaceLocation( - page: Page, - input: { - serverId: string; - workspace: Awaited>; - }, -): Promise { - const workspaceRoute = buildHostWorkspaceRoute(input.serverId, input.workspace.workspaceId); - await expectAppRoute(page, workspaceRoute, { timeout: 30_000 }); - await expectWorkspaceHeader(page, { - title: input.workspace.workspaceName, - subtitle: input.workspace.projectDisplayName, - }); -} - test.describe("Workspace navigation regression", () => { test.describe.configure({ timeout: 240_000 }); @@ -322,47 +246,6 @@ test.describe("Workspace navigation regression", () => { } }); - test("refresh keeps one continuous splash before restoring the workspace route", async ({ - page, - }) => { - const serverId = getServerId(); - const daemonGate = await installDaemonWebSocketGate(page); - const workspace = await seedWorkspace({ repoPrefix: "workspace-refresh-route-" }); - - try { - const agent = await createIdleAgent(workspace.client, { - cwd: workspace.repoPath, - workspaceId: workspace.workspaceId, - title: `workspace-refresh-route-${Date.now()}`, - }); - await injectDesktopBridge(page, { - serverId, - manageBuiltInDaemon: true, - hangDaemonStart: true, - desktopSettingsDelayMs: 250, - daemonListen: `127.0.0.1:${getE2EDaemonPort()}`, - }); - await openWorkspaceThroughApp(page, { serverId, workspace }); - await waitForWorkspaceTabsVisible(page); - await expectWorkspaceTabVisible(page, agent.id); - await expectWorkspaceLocation(page, { serverId, workspace }); - - await observeStartupPresentation(page); - await daemonGate.drop(); - await page.reload(); - await waitForDesktopDaemonStartRequest(page); - daemonGate.restore(); - await waitForSidebarHydration(page); - - await expectWorkspaceLocation(page, { serverId, workspace }); - await waitForWorkspaceTabsVisible(page); - expect(await getStartupPresentation(page)).toEqual(["splash", "app"]); - } finally { - daemonGate.restore(); - await workspace.cleanup(); - } - }); - test("sidebar navigation and reload keep workspace selection and tabs aligned", async ({ page, }) => { diff --git a/packages/app/e2e/workspace-pane-remount.spec.ts b/packages/app/e2e/browser/workspace-pane-remount.spec.ts similarity index 79% rename from packages/app/e2e/workspace-pane-remount.spec.ts rename to packages/app/e2e/browser/workspace-pane-remount.spec.ts index de56c8c4e..e440c7df2 100644 --- a/packages/app/e2e/workspace-pane-remount.spec.ts +++ b/packages/app/e2e/browser/workspace-pane-remount.spec.ts @@ -1,10 +1,10 @@ import { buildHostAgentDetailRoute } from "@/utils/host-routes"; -import { expect, test } from "./fixtures"; -import { createIdleAgent } from "./helpers/archive-tab"; -import { expectComposerVisible } from "./helpers/composer"; -import { seedWorkspace } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; -import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; +import { expect, test } from "../support/fixtures"; +import { createIdleAgent } from "../support/helpers/archive-tab"; +import { expectComposerVisible } from "../support/helpers/composer"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; +import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; test.describe("Workspace pane mounting", () => { test("opening the first split pane keeps the existing agent composer mounted", async ({ diff --git a/packages/app/e2e/workspace-pins.spec.ts b/packages/app/e2e/browser/workspace-pins.spec.ts similarity index 75% rename from packages/app/e2e/workspace-pins.spec.ts rename to packages/app/e2e/browser/workspace-pins.spec.ts index aafac4e0b..8c8e6ec51 100644 --- a/packages/app/e2e/workspace-pins.spec.ts +++ b/packages/app/e2e/browser/workspace-pins.spec.ts @@ -1,9 +1,9 @@ -import { test, expect } from "./fixtures"; -import { gotoWorkspace } from "./helpers/launcher"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; -import { togglePinFromMenu, tabRowPin } from "./helpers/pins"; -import { expectTerminalTabOpen } from "./helpers/workspace-tabs"; -import type { PinnedTabTarget } from "../src/workspace-pins/target"; +import { test, expect } from "../support/fixtures"; +import { gotoWorkspace } from "../support/helpers/launcher"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; +import { togglePinFromMenu, tabRowPin } from "../support/helpers/pins"; +import { expectTerminalTabOpen } from "../support/helpers/workspace-tabs"; +import type { PinnedTabTarget } from "../../src/workspace-pins/target"; const DRAFT_TARGET: PinnedTabTarget = { kind: "draft" }; const TERMINAL_TARGET: PinnedTabTarget = { kind: "terminal" }; diff --git a/packages/app/e2e/workspace-scripts-menu-resize.spec.ts b/packages/app/e2e/browser/workspace-scripts-menu-resize.spec.ts similarity index 87% rename from packages/app/e2e/workspace-scripts-menu-resize.spec.ts rename to packages/app/e2e/browser/workspace-scripts-menu-resize.spec.ts index dec22a14c..17c4d3dd4 100644 --- a/packages/app/e2e/workspace-scripts-menu-resize.spec.ts +++ b/packages/app/e2e/browser/workspace-scripts-menu-resize.spec.ts @@ -1,5 +1,5 @@ -import { test, expect } from "./fixtures"; -import { createTempGitRepo } from "./helpers/workspace"; +import { test, expect } from "../support/fixtures"; +import { createTempGitRepo } from "../support/helpers/workspace"; import { connectWorkspaceSetupClient, createWorkspaceThroughDaemon, @@ -7,10 +7,10 @@ import { seedProjectForWorkspaceSetup, startWorkspaceScriptFromMenu, waitForWorkspaceSetupProgress, -} from "./helpers/workspace-setup"; -import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs"; -import { getServerId } from "./helpers/server-id"; -import { buildHostWorkspaceRoute } from "../src/utils/host-routes"; +} from "../support/helpers/workspace-setup"; +import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs"; +import { getServerId } from "../support/helpers/server-id"; +import { buildHostWorkspaceRoute } from "../../src/utils/host-routes"; test("scripts menu resizes when a service row grows after launch", async ({ page }) => { const client = await connectWorkspaceSetupClient(); diff --git a/packages/app/e2e/workspace-setup-runtime.spec.ts b/packages/app/e2e/browser/workspace-setup-runtime.spec.ts similarity index 90% rename from packages/app/e2e/workspace-setup-runtime.spec.ts rename to packages/app/e2e/browser/workspace-setup-runtime.spec.ts index bd312c139..cc7d9f36a 100644 --- a/packages/app/e2e/workspace-setup-runtime.spec.ts +++ b/packages/app/e2e/browser/workspace-setup-runtime.spec.ts @@ -1,8 +1,8 @@ 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 { expect, test } from "../support/fixtures"; +import { createTempGitRepo } from "../support/helpers/workspace"; +import { clickNewTerminal } from "../support/helpers/launcher"; +import { expectTerminalSurfaceVisible } from "../support/helpers/terminal-perf"; import { connectWorkspaceSetupClient, createWorkspaceThroughDaemon, @@ -10,7 +10,7 @@ import { navigateToWorkspaceViaSidebar, openHomeWithProject, seedProjectForWorkspaceSetup, -} from "./helpers/workspace-setup"; +} from "../support/helpers/workspace-setup"; test.describe("Workspace setup runtime authority", () => { test.describe.configure({ retries: 1 }); diff --git a/packages/app/e2e/workspace-setup-streaming.spec.ts b/packages/app/e2e/browser/workspace-setup-streaming.spec.ts similarity index 95% rename from packages/app/e2e/workspace-setup-streaming.spec.ts rename to packages/app/e2e/browser/workspace-setup-streaming.spec.ts index b9a1dd24a..92351ae19 100644 --- a/packages/app/e2e/workspace-setup-streaming.spec.ts +++ b/packages/app/e2e/browser/workspace-setup-streaming.spec.ts @@ -1,15 +1,18 @@ -import { test, expect } from "./fixtures"; -import { createTempGitRepo } from "./helpers/workspace"; +import { test, expect } from "../support/fixtures"; +import { createTempGitRepo } from "../support/helpers/workspace"; import { waitForWorkspaceTabsVisible, expectNoTerminalTabs, clickFirstTerminalTab, expectFirstTerminalTabContains, -} from "./helpers/workspace-tabs"; -import { clickNewChat } from "./helpers/launcher"; -import { expectComposerVisible } from "./helpers/composer"; -import { openFileExplorer, expectExplorerEntryVisible } from "./helpers/file-explorer"; -import { expectTerminalSurfaceVisible, waitForTerminalAttached } from "./helpers/terminal-perf"; +} from "../support/helpers/workspace-tabs"; +import { clickNewChat } from "../support/helpers/launcher"; +import { expectComposerVisible } from "../support/helpers/composer"; +import { openFileExplorer, expectExplorerEntryVisible } from "../support/helpers/file-explorer"; +import { + expectTerminalSurfaceVisible, + waitForTerminalAttached, +} from "../support/helpers/terminal-perf"; import { connectWorkspaceSetupClient, createWorkspaceThroughDaemon, @@ -21,7 +24,7 @@ import { closeWorkspaceScriptsMenu, seedProjectForWorkspaceSetup, waitForWorkspaceSetupProgress, -} from "./helpers/workspace-setup"; +} from "../support/helpers/workspace-setup"; interface WorkspaceScriptStarter { startWorkspaceScript( diff --git a/packages/app/e2e/workspace-terminal-tab-rename.spec.ts b/packages/app/e2e/browser/workspace-terminal-tab-rename.spec.ts similarity index 93% rename from packages/app/e2e/workspace-terminal-tab-rename.spec.ts rename to packages/app/e2e/browser/workspace-terminal-tab-rename.spec.ts index 6ee477f7c..33eba8676 100644 --- a/packages/app/e2e/workspace-terminal-tab-rename.spec.ts +++ b/packages/app/e2e/browser/workspace-terminal-tab-rename.spec.ts @@ -1,7 +1,7 @@ -import { test, expect, type Page } from "./fixtures"; -import { clickNewTerminal, gotoWorkspace } from "./helpers/launcher"; -import { renameModalInput, renameModalSubmit } from "./helpers/rename"; -import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client"; +import { test, expect, type Page } from "../support/fixtures"; +import { clickNewTerminal, gotoWorkspace } from "../support/helpers/launcher"; +import { renameModalInput, renameModalSubmit } from "../support/helpers/rename"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; async function fetchTerminalTitle( workspace: SeededWorkspace, diff --git a/packages/app/e2e/worktree-archive-risk-warning.spec.ts b/packages/app/e2e/browser/worktree-archive-risk-warning.spec.ts similarity index 91% rename from packages/app/e2e/worktree-archive-risk-warning.spec.ts rename to packages/app/e2e/browser/worktree-archive-risk-warning.spec.ts index 3fe198430..7162a52b2 100644 --- a/packages/app/e2e/worktree-archive-risk-warning.spec.ts +++ b/packages/app/e2e/browser/worktree-archive-risk-warning.spec.ts @@ -3,18 +3,24 @@ import { existsSync } from "node:fs"; import { writeFile } from "node:fs/promises"; import path from "node:path"; import type { Dialog, Page } from "@playwright/test"; -import { expect, test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; import { archiveWorkspaceFromDaemon, connectNewWorkspaceDaemonClient, createWorktreeViaDaemon, openProjectViaDaemon, -} from "./helpers/new-workspace"; -import { getServerId } from "./helpers/server-id"; -import { clickArchiveWorkspaceMenuItem, expectWorkspaceAbsentFromSidebar } from "./helpers/sidebar"; -import { createTempGitRepo } from "./helpers/workspace"; -import { waitForSidebarHydration, waitForWorkspaceInSidebar } from "./helpers/workspace-ui"; +} from "../support/helpers/new-workspace"; +import { getServerId } from "../support/helpers/server-id"; +import { + clickArchiveWorkspaceMenuItem, + expectWorkspaceAbsentFromSidebar, +} from "../support/helpers/sidebar"; +import { createTempGitRepo } from "../support/helpers/workspace"; +import { + waitForSidebarHydration, + waitForWorkspaceInSidebar, +} from "../support/helpers/workspace-ui"; async function seedRiskyWorktree( client: Awaited>, diff --git a/packages/app/e2e/worktree-archive.spec.ts b/packages/app/e2e/browser/worktree-archive.spec.ts similarity index 87% rename from packages/app/e2e/worktree-archive.spec.ts rename to packages/app/e2e/browser/worktree-archive.spec.ts index ed874f18d..e14b357e8 100644 --- a/packages/app/e2e/worktree-archive.spec.ts +++ b/packages/app/e2e/browser/worktree-archive.spec.ts @@ -1,16 +1,22 @@ import { existsSync } from "node:fs"; -import { expect, test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; import { archiveWorkspaceFromDaemon, connectNewWorkspaceDaemonClient, createWorktreeViaDaemon, openProjectViaDaemon, -} from "./helpers/new-workspace"; -import { getServerId } from "./helpers/server-id"; -import { archiveWorkspaceFromSidebar, expectWorkspaceAbsentFromSidebar } from "./helpers/sidebar"; -import { createTempGitRepo } from "./helpers/workspace"; -import { waitForSidebarHydration, waitForWorkspaceInSidebar } from "./helpers/workspace-ui"; +} from "../support/helpers/new-workspace"; +import { getServerId } from "../support/helpers/server-id"; +import { + archiveWorkspaceFromSidebar, + expectWorkspaceAbsentFromSidebar, +} from "../support/helpers/sidebar"; +import { createTempGitRepo } from "../support/helpers/workspace"; +import { + waitForSidebarHydration, + waitForWorkspaceInSidebar, +} from "../support/helpers/workspace-ui"; test.describe("Workspace archive with worktree backing", () => { let client: Awaited>; diff --git a/packages/app/e2e/worktree-restore-after-restart.spec.ts b/packages/app/e2e/browser/worktree-restore-after-restart.spec.ts similarity index 89% rename from packages/app/e2e/worktree-restore-after-restart.spec.ts rename to packages/app/e2e/browser/worktree-restore-after-restart.spec.ts index a11df5daf..25b5de7d7 100644 --- a/packages/app/e2e/worktree-restore-after-restart.spec.ts +++ b/packages/app/e2e/browser/worktree-restore-after-restart.spec.ts @@ -1,19 +1,26 @@ import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; import { expect, test, type Page } from "@playwright/test"; -import { gotoAppShell } from "./helpers/app"; -import { createIdleAgent, expectSessionRowArchived, openSessions } from "./helpers/archive-tab"; -import { buildCreateAgentPreferences, buildSeededHost } from "./helpers/daemon-registry"; -import { startIsolatedHostDaemon, type IsolatedHostDaemon } from "./helpers/isolated-host-daemon"; +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 "./helpers/new-workspace"; -import { connectSeedClient } from "./helpers/seed-client"; -import { createTempGitRepo } from "./helpers/workspace"; -import { waitForSidebarHydration } from "./helpers/workspace-ui"; +} 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)}`; diff --git a/packages/app/e2e/worktree-restore.spec.ts b/packages/app/e2e/browser/worktree-restore.spec.ts similarity index 96% rename from packages/app/e2e/worktree-restore.spec.ts rename to packages/app/e2e/browser/worktree-restore.spec.ts index a3abdf9bf..d8bac26fe 100644 --- a/packages/app/e2e/worktree-restore.spec.ts +++ b/packages/app/e2e/browser/worktree-restore.spec.ts @@ -4,30 +4,33 @@ import { existsSync } from "node:fs"; import { rename } from "node:fs/promises"; import type { Page } from "@playwright/test"; import { buildHostWorkspaceRoute } from "@/utils/host-routes"; -import { expect, test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; +import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; import { expectWorkspaceBranch, openChangesPanel, switchBranchFromChangesPanel, -} from "./helpers/branch-switcher"; +} from "../support/helpers/branch-switcher"; import { createIdleAgent, expectSessionRowNotArchived, fetchAgentArchivedAt, openSessions, -} from "./helpers/archive-tab"; +} from "../support/helpers/archive-tab"; import { archiveWorkspaceFromDaemon, archiveLocalWorkspaceFromDaemon, connectNewWorkspaceDaemonClient, createWorktreeViaDaemon, openProjectViaDaemon, -} from "./helpers/new-workspace"; -import { connectSeedClient } from "./helpers/seed-client"; -import { getServerId } from "./helpers/server-id"; -import { createTempGitRepo } from "./helpers/workspace"; -import { waitForSidebarHydration, waitForWorkspaceInSidebar } from "./helpers/workspace-ui"; +} from "../support/helpers/new-workspace"; +import { connectSeedClient } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; +import { createTempGitRepo } from "../support/helpers/workspace"; +import { + waitForSidebarHydration, + waitForWorkspaceInSidebar, +} from "../support/helpers/workspace-ui"; test.describe("Worktree restore", () => { let client: Awaited>; diff --git a/packages/app/e2e/startup-loading.spec.ts b/packages/app/e2e/startup-loading.spec.ts deleted file mode 100644 index 896b7fb7e..000000000 --- a/packages/app/e2e/startup-loading.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { test } from "./fixtures"; -import { getE2EDaemonPort } from "./helpers/daemon-port"; -import { getServerId } from "./helpers/server-id"; -import { startupScenario } from "./helpers/startup-dsl"; - -test.describe("Startup loading presentation", () => { - test("mobile reconnect preserves the saved host shell", async ({ page }) => { - const startup = await startupScenario(page) - .withMobileViewport() - .withSavedHost({ - serverId: "srv_unreachable_mobile", - label: "Dev", - endpoint: "127.0.0.1:45678", - }) - .openRoot(); - - await startup.expectsSavedHostShell({ serverId: "srv_unreachable_mobile", label: "Dev" }); - await startup.expectsNoSavedHostErrorStatus(); - await startup.expectsNoLocalServerStartupCopy(); - }); - - test("desktop daemon bootstrap keeps the local server startup copy desktop-only", async ({ - page, - }) => { - const startup = await startupScenario(page) - .withPendingDesktopDaemon() - .withBlockedPort(getE2EDaemonPort()) - .openRoot(); - - await startup.expectsDesktopDaemonStartup(); - await startup.expectsSidebarHidden(); - await startup.expectsNoUndefinedRoute(); - }); - - test("host-route refresh does not render route chrome around the bootstrap splash", async ({ - page, - }) => { - const serverId = getServerId(); - const startup = await startupScenario(page) - .withPendingDesktopDaemon() - .withBlockedPort(getE2EDaemonPort()) - .openHostWorkspace({ - serverId, - workspaceId: "workspace-1", - }); - - await startup.expectsDesktopDaemonStartup(); - await startup.expectsSidebarHidden(); - }); -}); diff --git a/packages/app/e2e/fixtures.ts b/packages/app/e2e/support/fixtures.ts similarity index 100% rename from packages/app/e2e/fixtures.ts rename to packages/app/e2e/support/fixtures.ts diff --git a/packages/app/e2e/fixtures/recording.baseline.txt b/packages/app/e2e/support/fixtures/recording.baseline.txt similarity index 100% rename from packages/app/e2e/fixtures/recording.baseline.txt rename to packages/app/e2e/support/fixtures/recording.baseline.txt diff --git a/packages/app/e2e/fixtures/recording.wav b/packages/app/e2e/support/fixtures/recording.wav similarity index 100% rename from packages/app/e2e/fixtures/recording.wav rename to packages/app/e2e/support/fixtures/recording.wav diff --git a/packages/app/e2e/fixtures/recording.webm b/packages/app/e2e/support/fixtures/recording.webm similarity index 100% rename from packages/app/e2e/fixtures/recording.webm rename to packages/app/e2e/support/fixtures/recording.webm diff --git a/packages/app/e2e/global-setup.ts b/packages/app/e2e/support/global-setup.ts similarity index 98% rename from packages/app/e2e/global-setup.ts rename to packages/app/e2e/support/global-setup.ts index 03df64010..c91d9c012 100644 --- a/packages/app/e2e/global-setup.ts +++ b/packages/app/e2e/support/global-setup.ts @@ -12,7 +12,7 @@ import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./helpers import { forkPaseoHomeMetadata, resolvePaseoHomePath } from "./helpers/paseo-home-fork"; import { withDisabledE2ESpeechEnv } from "./helpers/speech-env"; -const wranglerCliPath = path.resolve(__dirname, "../node_modules/wrangler/bin/wrangler.js"); +const wranglerCliPath = path.resolve(__dirname, "../../node_modules/wrangler/bin/wrangler.js"); export interface WaitForServerOptions { host?: string; @@ -658,7 +658,7 @@ interface RelayPorts { } async function startRelay(excludedPorts: Set): Promise { - const relayDir = path.resolve(__dirname, "..", "..", "relay"); + const relayDir = path.resolve(__dirname, "..", "..", "..", "relay"); const maxRelayStartupAttempts = 5; let lastRelayStartupError: unknown = null; @@ -718,7 +718,7 @@ function startMetro(input: { daemonPort: number; buffer: ReturnType; }): ChildProcess { - const appDir = path.resolve(__dirname, ".."); + const appDir = path.resolve(__dirname, "../.."); const child = spawn("npx", ["expo", "start", "--web", "--port", String(input.metroPort)], { cwd: appDir, env: { @@ -771,7 +771,7 @@ interface DaemonSpawnArgs { } function startDaemon(args: DaemonSpawnArgs): ChildProcess { - const serverDir = path.resolve(__dirname, "../../..", "packages/server"); + const serverDir = path.resolve(__dirname, "../../../..", "packages/server"); const tsxBin = execSync("which tsx").toString().trim(); const env = withDisabledE2ESpeechEnv({ ...process.env, @@ -852,7 +852,7 @@ async function performCleanup(shouldRemovePaseoHome: boolean): Promise { } export default async function globalSetup() { - const repoRoot = path.resolve(__dirname, "../../.."); + const repoRoot = path.resolve(__dirname, "../../../.."); ensureRelayBuildArtifact(repoRoot); await loadEnvTestFile(repoRoot); diff --git a/packages/app/e2e/helpers/add-project-flow.ts b/packages/app/e2e/support/helpers/add-project-flow.ts similarity index 100% rename from packages/app/e2e/helpers/add-project-flow.ts rename to packages/app/e2e/support/helpers/add-project-flow.ts diff --git a/packages/app/e2e/helpers/agent-bottom-anchor.ts b/packages/app/e2e/support/helpers/agent-bottom-anchor.ts similarity index 100% rename from packages/app/e2e/helpers/agent-bottom-anchor.ts rename to packages/app/e2e/support/helpers/agent-bottom-anchor.ts diff --git a/packages/app/e2e/helpers/agent-message-gate.ts b/packages/app/e2e/support/helpers/agent-message-gate.ts similarity index 100% rename from packages/app/e2e/helpers/agent-message-gate.ts rename to packages/app/e2e/support/helpers/agent-message-gate.ts diff --git a/packages/app/e2e/helpers/agent-stream.ts b/packages/app/e2e/support/helpers/agent-stream.ts similarity index 100% rename from packages/app/e2e/helpers/agent-stream.ts rename to packages/app/e2e/support/helpers/agent-stream.ts diff --git a/packages/app/e2e/helpers/agent-timeline-gate.ts b/packages/app/e2e/support/helpers/agent-timeline-gate.ts similarity index 100% rename from packages/app/e2e/helpers/agent-timeline-gate.ts rename to packages/app/e2e/support/helpers/agent-timeline-gate.ts diff --git a/packages/app/e2e/helpers/app.ts b/packages/app/e2e/support/helpers/app.ts similarity index 100% rename from packages/app/e2e/helpers/app.ts rename to packages/app/e2e/support/helpers/app.ts diff --git a/packages/app/e2e/helpers/archive-tab.ts b/packages/app/e2e/support/helpers/archive-tab.ts similarity index 100% rename from packages/app/e2e/helpers/archive-tab.ts rename to packages/app/e2e/support/helpers/archive-tab.ts diff --git a/packages/app/e2e/helpers/branch-switcher.ts b/packages/app/e2e/support/helpers/branch-switcher.ts similarity index 100% rename from packages/app/e2e/helpers/branch-switcher.ts rename to packages/app/e2e/support/helpers/branch-switcher.ts diff --git a/packages/app/e2e/helpers/command-center.ts b/packages/app/e2e/support/helpers/command-center.ts similarity index 100% rename from packages/app/e2e/helpers/command-center.ts rename to packages/app/e2e/support/helpers/command-center.ts diff --git a/packages/app/e2e/helpers/composer.ts b/packages/app/e2e/support/helpers/composer.ts similarity index 100% rename from packages/app/e2e/helpers/composer.ts rename to packages/app/e2e/support/helpers/composer.ts diff --git a/packages/app/e2e/helpers/daemon-client-loader.ts b/packages/app/e2e/support/helpers/daemon-client-loader.ts similarity index 94% rename from packages/app/e2e/helpers/daemon-client-loader.ts rename to packages/app/e2e/support/helpers/daemon-client-loader.ts index b9a6e7b22..b36bbae32 100644 --- a/packages/app/e2e/helpers/daemon-client-loader.ts +++ b/packages/app/e2e/support/helpers/daemon-client-loader.ts @@ -8,7 +8,7 @@ import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./node-ws export async function loadDaemonClientConstructor(): Promise< new (config: ClientConfig) => ClientInstance > { - const repoRoot = path.resolve(__dirname, "../../../../"); + const repoRoot = path.resolve(__dirname, "../../../../../"); const moduleUrl = pathToFileURL( path.join(repoRoot, "packages/client/dist/daemon-client.js"), ).href; @@ -57,7 +57,7 @@ export async function connectDaemonClient => { const child = spawn(tsxBin, ["scripts/supervisor-entrypoint.ts", "--dev"], { diff --git a/packages/app/e2e/helpers/launcher.ts b/packages/app/e2e/support/helpers/launcher.ts similarity index 99% rename from packages/app/e2e/helpers/launcher.ts rename to packages/app/e2e/support/helpers/launcher.ts index 87a9bcab1..0c7debd19 100644 --- a/packages/app/e2e/helpers/launcher.ts +++ b/packages/app/e2e/support/helpers/launcher.ts @@ -1,5 +1,5 @@ import { expect, type Page } from "@playwright/test"; -import { buildHostWorkspaceRoute } from "../../src/utils/host-routes"; +import { buildHostWorkspaceRoute } from "../../../src/utils/host-routes"; import { createTempGitRepo } from "./workspace"; import { getServerId } from "./server-id"; diff --git a/packages/app/e2e/helpers/mock-agent.ts b/packages/app/e2e/support/helpers/mock-agent.ts similarity index 96% rename from packages/app/e2e/helpers/mock-agent.ts rename to packages/app/e2e/support/helpers/mock-agent.ts index 687891136..182188f1c 100644 --- a/packages/app/e2e/helpers/mock-agent.ts +++ b/packages/app/e2e/support/helpers/mock-agent.ts @@ -1,7 +1,7 @@ import type { Page } from "@playwright/test"; import { seedWorkspace, type SeedDaemonClient } from "./seed-client"; import { getServerId } from "./server-id"; -import { buildHostAgentDetailRoute } from "../../src/utils/host-routes"; +import { buildHostAgentDetailRoute } from "../../../src/utils/host-routes"; export interface MockAgentWorkspace { agentId: string; diff --git a/packages/app/e2e/helpers/new-workspace.ts b/packages/app/e2e/support/helpers/new-workspace.ts similarity index 100% rename from packages/app/e2e/helpers/new-workspace.ts rename to packages/app/e2e/support/helpers/new-workspace.ts diff --git a/packages/app/e2e/helpers/no-truncation.ts b/packages/app/e2e/support/helpers/no-truncation.ts similarity index 100% rename from packages/app/e2e/helpers/no-truncation.ts rename to packages/app/e2e/support/helpers/no-truncation.ts diff --git a/packages/app/e2e/helpers/node-ws-factory.ts b/packages/app/e2e/support/helpers/node-ws-factory.ts similarity index 100% rename from packages/app/e2e/helpers/node-ws-factory.ts rename to packages/app/e2e/support/helpers/node-ws-factory.ts diff --git a/packages/app/e2e/helpers/paseo-home-fork.ts b/packages/app/e2e/support/helpers/paseo-home-fork.ts similarity index 100% rename from packages/app/e2e/helpers/paseo-home-fork.ts rename to packages/app/e2e/support/helpers/paseo-home-fork.ts diff --git a/packages/app/e2e/helpers/permissions.ts b/packages/app/e2e/support/helpers/permissions.ts similarity index 100% rename from packages/app/e2e/helpers/permissions.ts rename to packages/app/e2e/support/helpers/permissions.ts diff --git a/packages/app/e2e/helpers/pins.ts b/packages/app/e2e/support/helpers/pins.ts similarity index 92% rename from packages/app/e2e/helpers/pins.ts rename to packages/app/e2e/support/helpers/pins.ts index f7a08d3e1..e68c2c24c 100644 --- a/packages/app/e2e/helpers/pins.ts +++ b/packages/app/e2e/support/helpers/pins.ts @@ -1,6 +1,6 @@ import { expect, type Page } from "@playwright/test"; -import type { PinnedTabTarget } from "../../src/workspace-pins/target"; -import { pinnedTargetKey } from "../../src/workspace-pins/target"; +import type { PinnedTabTarget } from "../../../src/workspace-pins/target"; +import { pinnedTargetKey } from "../../../src/workspace-pins/target"; // The new-tab dropdown menu items carry no stable web ARIA role for the inline // pin toggle (it lives inside a reveal-on-hover slot), so the pins flow is diff --git a/packages/app/e2e/helpers/pr-pane.ts b/packages/app/e2e/support/helpers/pr-pane.ts similarity index 100% rename from packages/app/e2e/helpers/pr-pane.ts rename to packages/app/e2e/support/helpers/pr-pane.ts diff --git a/packages/app/e2e/helpers/project-picker-fixture.ts b/packages/app/e2e/support/helpers/project-picker-fixture.ts similarity index 100% rename from packages/app/e2e/helpers/project-picker-fixture.ts rename to packages/app/e2e/support/helpers/project-picker-fixture.ts diff --git a/packages/app/e2e/helpers/project-picker-ui.ts b/packages/app/e2e/support/helpers/project-picker-ui.ts similarity index 100% rename from packages/app/e2e/helpers/project-picker-ui.ts rename to packages/app/e2e/support/helpers/project-picker-ui.ts diff --git a/packages/app/e2e/helpers/project-settings.ts b/packages/app/e2e/support/helpers/project-settings.ts similarity index 100% rename from packages/app/e2e/helpers/project-settings.ts rename to packages/app/e2e/support/helpers/project-settings.ts diff --git a/packages/app/e2e/helpers/provider-usage.ts b/packages/app/e2e/support/helpers/provider-usage.ts similarity index 100% rename from packages/app/e2e/helpers/provider-usage.ts rename to packages/app/e2e/support/helpers/provider-usage.ts diff --git a/packages/app/e2e/helpers/questions.ts b/packages/app/e2e/support/helpers/questions.ts similarity index 100% rename from packages/app/e2e/helpers/questions.ts rename to packages/app/e2e/support/helpers/questions.ts diff --git a/packages/app/e2e/helpers/regex.ts b/packages/app/e2e/support/helpers/regex.ts similarity index 100% rename from packages/app/e2e/helpers/regex.ts rename to packages/app/e2e/support/helpers/regex.ts diff --git a/packages/app/e2e/helpers/rename.ts b/packages/app/e2e/support/helpers/rename.ts similarity index 100% rename from packages/app/e2e/helpers/rename.ts rename to packages/app/e2e/support/helpers/rename.ts diff --git a/packages/app/e2e/helpers/rewind-flow.ts b/packages/app/e2e/support/helpers/rewind-flow.ts similarity index 100% rename from packages/app/e2e/helpers/rewind-flow.ts rename to packages/app/e2e/support/helpers/rewind-flow.ts diff --git a/packages/app/e2e/helpers/route-assertions.ts b/packages/app/e2e/support/helpers/route-assertions.ts similarity index 100% rename from packages/app/e2e/helpers/route-assertions.ts rename to packages/app/e2e/support/helpers/route-assertions.ts diff --git a/packages/app/e2e/helpers/schedule-fake-host.ts b/packages/app/e2e/support/helpers/schedule-fake-host.ts similarity index 100% rename from packages/app/e2e/helpers/schedule-fake-host.ts rename to packages/app/e2e/support/helpers/schedule-fake-host.ts diff --git a/packages/app/e2e/helpers/seed-client.ts b/packages/app/e2e/support/helpers/seed-client.ts similarity index 99% rename from packages/app/e2e/helpers/seed-client.ts rename to packages/app/e2e/support/helpers/seed-client.ts index d3fd79738..56814b410 100644 --- a/packages/app/e2e/helpers/seed-client.ts +++ b/packages/app/e2e/support/helpers/seed-client.ts @@ -230,7 +230,7 @@ export async function seedWorkspace(options: { } function loadAppVersion(): string { - const packageJsonPath = path.resolve(__dirname, "../../package.json"); + const packageJsonPath = path.resolve(__dirname, "../../../package.json"); const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { version?: unknown }; if (typeof packageJson.version !== "string" || packageJson.version.length === 0) { throw new Error(`Missing app version in ${packageJsonPath}`); diff --git a/packages/app/e2e/helpers/server-id.ts b/packages/app/e2e/support/helpers/server-id.ts similarity index 100% rename from packages/app/e2e/helpers/server-id.ts rename to packages/app/e2e/support/helpers/server-id.ts diff --git a/packages/app/e2e/helpers/settings.ts b/packages/app/e2e/support/helpers/settings.ts similarity index 100% rename from packages/app/e2e/helpers/settings.ts rename to packages/app/e2e/support/helpers/settings.ts diff --git a/packages/app/e2e/helpers/settled.ts b/packages/app/e2e/support/helpers/settled.ts similarity index 100% rename from packages/app/e2e/helpers/settled.ts rename to packages/app/e2e/support/helpers/settled.ts diff --git a/packages/app/e2e/helpers/sidebar.ts b/packages/app/e2e/support/helpers/sidebar.ts similarity index 100% rename from packages/app/e2e/helpers/sidebar.ts rename to packages/app/e2e/support/helpers/sidebar.ts diff --git a/packages/app/e2e/helpers/speech-env.ts b/packages/app/e2e/support/helpers/speech-env.ts similarity index 100% rename from packages/app/e2e/helpers/speech-env.ts rename to packages/app/e2e/support/helpers/speech-env.ts diff --git a/packages/app/e2e/helpers/startup-dsl.ts b/packages/app/e2e/support/helpers/startup-dsl.ts similarity index 100% rename from packages/app/e2e/helpers/startup-dsl.ts rename to packages/app/e2e/support/helpers/startup-dsl.ts diff --git a/packages/app/e2e/helpers/subagents.ts b/packages/app/e2e/support/helpers/subagents.ts similarity index 100% rename from packages/app/e2e/helpers/subagents.ts rename to packages/app/e2e/support/helpers/subagents.ts diff --git a/packages/app/e2e/helpers/terminal-dsl.ts b/packages/app/e2e/support/helpers/terminal-dsl.ts similarity index 100% rename from packages/app/e2e/helpers/terminal-dsl.ts rename to packages/app/e2e/support/helpers/terminal-dsl.ts diff --git a/packages/app/e2e/helpers/terminal-perf.ts b/packages/app/e2e/support/helpers/terminal-perf.ts similarity index 98% rename from packages/app/e2e/helpers/terminal-perf.ts rename to packages/app/e2e/support/helpers/terminal-perf.ts index 3ab56dd5c..ac41b7343 100644 --- a/packages/app/e2e/helpers/terminal-perf.ts +++ b/packages/app/e2e/support/helpers/terminal-perf.ts @@ -1,5 +1,5 @@ import { expect, type Page } from "@playwright/test"; -import { buildHostWorkspaceRoute } from "../../src/utils/host-routes"; +import { buildHostWorkspaceRoute } from "../../../src/utils/host-routes"; import { getServerId } from "./server-id"; export function buildTerminalWorkspaceUrl(workspaceId: string, terminalId: string): string { diff --git a/packages/app/e2e/helpers/terminal-probes.ts b/packages/app/e2e/support/helpers/terminal-probes.ts similarity index 100% rename from packages/app/e2e/helpers/terminal-probes.ts rename to packages/app/e2e/support/helpers/terminal-probes.ts diff --git a/packages/app/e2e/helpers/timeline-delivery.ts b/packages/app/e2e/support/helpers/timeline-delivery.ts similarity index 100% rename from packages/app/e2e/helpers/timeline-delivery.ts rename to packages/app/e2e/support/helpers/timeline-delivery.ts diff --git a/packages/app/e2e/helpers/timeline-pagination.ts b/packages/app/e2e/support/helpers/timeline-pagination.ts similarity index 100% rename from packages/app/e2e/helpers/timeline-pagination.ts rename to packages/app/e2e/support/helpers/timeline-pagination.ts diff --git a/packages/app/e2e/helpers/with-workspace.ts b/packages/app/e2e/support/helpers/with-workspace.ts similarity index 100% rename from packages/app/e2e/helpers/with-workspace.ts rename to packages/app/e2e/support/helpers/with-workspace.ts diff --git a/packages/app/e2e/helpers/workspace-lifecycle.ts b/packages/app/e2e/support/helpers/workspace-lifecycle.ts similarity index 100% rename from packages/app/e2e/helpers/workspace-lifecycle.ts rename to packages/app/e2e/support/helpers/workspace-lifecycle.ts diff --git a/packages/app/e2e/helpers/workspace-setup.ts b/packages/app/e2e/support/helpers/workspace-setup.ts similarity index 99% rename from packages/app/e2e/helpers/workspace-setup.ts rename to packages/app/e2e/support/helpers/workspace-setup.ts index cb5e38600..bd93794e9 100644 --- a/packages/app/e2e/helpers/workspace-setup.ts +++ b/packages/app/e2e/support/helpers/workspace-setup.ts @@ -1,7 +1,7 @@ import { realpathSync } from "node:fs"; import { expect, type Page } from "@playwright/test"; import type { DaemonClient as InternalDaemonClient } from "@getpaseo/client/internal/daemon-client"; -import { parseHostWorkspaceRouteFromPathname } from "../../src/utils/host-routes"; +import { parseHostWorkspaceRouteFromPathname } from "../../../src/utils/host-routes"; import { gotoAppShell } from "./app"; import { connectDaemonClient } from "./daemon-client-loader"; import { getServerId } from "./server-id"; diff --git a/packages/app/e2e/helpers/workspace-tabs.ts b/packages/app/e2e/support/helpers/workspace-tabs.ts similarity index 100% rename from packages/app/e2e/helpers/workspace-tabs.ts rename to packages/app/e2e/support/helpers/workspace-tabs.ts diff --git a/packages/app/e2e/helpers/workspace-ui.ts b/packages/app/e2e/support/helpers/workspace-ui.ts similarity index 100% rename from packages/app/e2e/helpers/workspace-ui.ts rename to packages/app/e2e/support/helpers/workspace-ui.ts diff --git a/packages/app/e2e/helpers/workspace.ts b/packages/app/e2e/support/helpers/workspace.ts similarity index 100% rename from packages/app/e2e/helpers/workspace.ts rename to packages/app/e2e/support/helpers/workspace.ts diff --git a/packages/app/package.json b/packages/app/package.json index 3936b7db5..260e43310 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -21,8 +21,7 @@ "typecheck": "tsgo --noEmit", "test": "vitest run", "test:browser": "vitest run --project browser", - "test:e2e": "playwright test --project='Desktop Chrome'", - "test:e2e:desktop": "cross-env E2E_DESKTOP_RUNTIME=1 playwright test --project='Desktop Chrome' e2e/project-picker-desktop.spec.ts", + "test:e2e": "playwright test --project=browser", "test:e2e:real": "cross-env E2E_FORK_PASEO_HOME_FROM=../../.dev/paseo-home playwright test --project=real-provider", "test:e2e:ui": "playwright test --ui", "build": "npm run build:web", diff --git a/packages/app/playwright.config.ts b/packages/app/playwright.config.ts index cc0d117e7..62b1a198b 100644 --- a/packages/app/playwright.config.ts +++ b/packages/app/playwright.config.ts @@ -6,8 +6,8 @@ const baseURL = process.env.E2E_BASE_URL ?? `http://localhost:${process.env.E2E_METRO_PORT ?? "8081"}`; export default defineConfig({ - testDir: "./e2e", - globalSetup: "./e2e/global-setup.ts", + testDir: "./e2e/browser", + globalSetup: "./e2e/support/global-setup.ts", timeout: 60_000, expect: { timeout: 10_000, @@ -26,7 +26,7 @@ export default defineConfig({ }, projects: [ { - name: "Desktop Chrome", + name: "browser", testIgnore: ["**/*.real.spec.ts"], use: { ...devices["Desktop Chrome"] }, }, diff --git a/packages/app/src/browser-automation/handler.test.ts b/packages/app/src/desktop/browser/automation/handler.test.ts similarity index 99% rename from packages/app/src/browser-automation/handler.test.ts rename to packages/app/src/desktop/browser/automation/handler.test.ts index 4af65a17f..56c3c28df 100644 --- a/packages/app/src/browser-automation/handler.test.ts +++ b/packages/app/src/desktop/browser/automation/handler.test.ts @@ -3,7 +3,7 @@ import type { SessionInboundMessage, SessionOutboundMessage } from "@getpaseo/pr import { createJSONStorage, type StateStorage } from "zustand/middleware"; import { mountBrowserAutomationHandler } from "./handler"; import type { DesktopHostBridge } from "@/desktop/host"; -import { useBrowserStore } from "@/stores/browser-store"; +import { useBrowserStore } from "@/desktop/browser/store"; import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store"; import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model"; diff --git a/packages/app/src/browser-automation/handler.ts b/packages/app/src/desktop/browser/automation/handler.ts similarity index 99% rename from packages/app/src/browser-automation/handler.ts rename to packages/app/src/desktop/browser/automation/handler.ts index b7cf3d7cd..a75e666fa 100644 --- a/packages/app/src/browser-automation/handler.ts +++ b/packages/app/src/desktop/browser/automation/handler.ts @@ -4,8 +4,8 @@ import { ensureResidentBrowserWebview as ensureResidentBrowserWebviewDefault, removeResidentBrowserWebview, resizeResidentBrowserWebview, -} from "@/components/browser-webview-resident"; -import { createWorkspaceBrowser, getBrowserRecord, useBrowserStore } from "@/stores/browser-store"; +} from "@/desktop/browser/resident-webviews"; +import { createWorkspaceBrowser, getBrowserRecord, useBrowserStore } from "@/desktop/browser/store"; import { collectAllTabs, useWorkspaceLayoutStore } from "@/stores/workspace-layout-store"; import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model"; diff --git a/packages/app/src/browser/new-tab-requests/index.test.ts b/packages/app/src/desktop/browser/new-tab-requests/index.test.ts similarity index 100% rename from packages/app/src/browser/new-tab-requests/index.test.ts rename to packages/app/src/desktop/browser/new-tab-requests/index.test.ts diff --git a/packages/app/src/browser/new-tab-requests/index.ts b/packages/app/src/desktop/browser/new-tab-requests/index.ts similarity index 100% rename from packages/app/src/browser/new-tab-requests/index.ts rename to packages/app/src/desktop/browser/new-tab-requests/index.ts diff --git a/packages/app/src/components/browser-pane.electron.tsx b/packages/app/src/desktop/browser/pane/index.electron.tsx similarity index 99% rename from packages/app/src/components/browser-pane.electron.tsx rename to packages/app/src/desktop/browser/pane/index.electron.tsx index f3b9141aa..5130d9ce0 100644 --- a/packages/app/src/components/browser-pane.electron.tsx +++ b/packages/app/src/desktop/browser/pane/index.electron.tsx @@ -49,12 +49,12 @@ import { isElectronRuntime, type DesktopBrowserShortcutEvent, } from "@/desktop/host"; -import { useBrowserStore, normalizeWorkspaceBrowserUrl } from "@/stores/browser-store"; +import { useBrowserStore, normalizeWorkspaceBrowserUrl } from "@/desktop/browser/store"; import { prepareBrowserWebview, releaseResidentBrowserWebview, takeResidentBrowserWebview, -} from "./browser-webview-resident"; +} from "../resident-webviews"; type ElectronWebview = HTMLElement & { canGoBack?: () => boolean; diff --git a/packages/app/src/components/browser-pane.tsx b/packages/app/src/desktop/browser/pane/index.tsx similarity index 100% rename from packages/app/src/components/browser-pane.tsx rename to packages/app/src/desktop/browser/pane/index.tsx diff --git a/packages/app/src/components/browser-pane.web.tsx b/packages/app/src/desktop/browser/pane/index.web.tsx similarity index 100% rename from packages/app/src/components/browser-pane.web.tsx rename to packages/app/src/desktop/browser/pane/index.web.tsx diff --git a/packages/app/src/panels/browser-panel.tsx b/packages/app/src/desktop/browser/panel.tsx similarity index 95% rename from packages/app/src/panels/browser-panel.tsx rename to packages/app/src/desktop/browser/panel.tsx index 0f6b4a437..16539a3fd 100644 --- a/packages/app/src/panels/browser-panel.tsx +++ b/packages/app/src/desktop/browser/panel.tsx @@ -2,10 +2,10 @@ import { useMemo } from "react"; import { Image } from "react-native"; import { Globe } from "lucide-react-native"; import invariant from "tiny-invariant"; -import { BrowserPane } from "@/components/browser-pane"; +import { BrowserPane } from "@/desktop/browser/pane"; import { usePaneContext, usePaneFocus } from "@/panels/pane-context"; import type { PanelDescriptor, PanelIconProps, PanelRegistration } from "@/panels/panel-registry"; -import { useBrowserStore } from "@/stores/browser-store"; +import { useBrowserStore } from "@/desktop/browser/store"; import { useWorkspaceDirectory } from "@/stores/session-store-hooks"; function getBrowserLabel(input: { title: string; url: string }): string { diff --git a/packages/app/src/components/browser-webview-resident.browser.test.ts b/packages/app/src/desktop/browser/resident-webviews.browser.test.ts similarity index 99% rename from packages/app/src/components/browser-webview-resident.browser.test.ts rename to packages/app/src/desktop/browser/resident-webviews.browser.test.ts index c4f4de663..b3208205b 100644 --- a/packages/app/src/components/browser-webview-resident.browser.test.ts +++ b/packages/app/src/desktop/browser/resident-webviews.browser.test.ts @@ -8,11 +8,11 @@ import { releaseResidentBrowserWebview, removeResidentBrowserWebview, takeResidentBrowserWebview, -} from "./browser-webview-resident"; +} from "./resident-webviews"; import { setCommandCenterFocusRestoreElement, takeCommandCenterFocusRestoreElement, -} from "../utils/command-center-focus-restore"; +} from "../../utils/command-center-focus-restore"; const RESIDENT_HOST_ID = "paseo-browser-resident-webviews"; const attachedBrowsers: Array<{ diff --git a/packages/app/src/components/browser-webview-resident.ts b/packages/app/src/desktop/browser/resident-webviews.ts similarity index 100% rename from packages/app/src/components/browser-webview-resident.ts rename to packages/app/src/desktop/browser/resident-webviews.ts diff --git a/packages/app/src/desktop/components/browser-data-section.tsx b/packages/app/src/desktop/browser/settings/browser-data-section.tsx similarity index 97% rename from packages/app/src/desktop/components/browser-data-section.tsx rename to packages/app/src/desktop/browser/settings/browser-data-section.tsx index 3d61f922e..0efc3ebf8 100644 --- a/packages/app/src/desktop/components/browser-data-section.tsx +++ b/packages/app/src/desktop/browser/settings/browser-data-section.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"; import { useToast } from "@/contexts/toast-context"; import { getDesktopHost } from "@/desktop/host"; import { SettingsSection } from "@/screens/settings/settings-section"; -import { useBrowserStore } from "@/stores/browser-store"; +import { useBrowserStore } from "@/desktop/browser/store"; import { settingsStyles } from "@/styles/settings"; import { confirmDialog } from "@/utils/confirm-dialog"; diff --git a/packages/app/src/keyboard/browser-shortcuts.test.ts b/packages/app/src/desktop/browser/shortcuts.test.ts similarity index 99% rename from packages/app/src/keyboard/browser-shortcuts.test.ts rename to packages/app/src/desktop/browser/shortcuts.test.ts index 9dd323946..804ed7336 100644 --- a/packages/app/src/keyboard/browser-shortcuts.test.ts +++ b/packages/app/src/desktop/browser/shortcuts.test.ts @@ -3,8 +3,8 @@ import { buildBrowserKeyboardPolicy, parseBrowserShortcutInput, shouldPublishBrowserShortcutPolicy, -} from "./browser-shortcuts"; -import { buildEffectiveBindings, resolveKeyboardShortcut } from "./keyboard-shortcuts"; +} from "./shortcuts"; +import { buildEffectiveBindings, resolveKeyboardShortcut } from "../../keyboard/keyboard-shortcuts"; describe("buildBrowserKeyboardPolicy", () => { it("publishes only chord starts while no browser chord is pending", () => { diff --git a/packages/app/src/keyboard/browser-shortcuts.ts b/packages/app/src/desktop/browser/shortcuts.ts similarity index 98% rename from packages/app/src/keyboard/browser-shortcuts.ts rename to packages/app/src/desktop/browser/shortcuts.ts index 003790802..2bd7def5e 100644 --- a/packages/app/src/keyboard/browser-shortcuts.ts +++ b/packages/app/src/desktop/browser/shortcuts.ts @@ -3,8 +3,8 @@ import { matchesKeyboardShortcutContext, type KeyboardShortcutInput, type ParsedShortcutBinding, -} from "./keyboard-shortcuts"; -import type { KeyCombo } from "./shortcut-string"; +} from "../../keyboard/keyboard-shortcuts"; +import type { KeyCombo } from "../../keyboard/shortcut-string"; export interface BrowserShortcutPrefix { alt: boolean; diff --git a/packages/app/src/stores/browser-store/index.ts b/packages/app/src/desktop/browser/store/index.ts similarity index 100% rename from packages/app/src/stores/browser-store/index.ts rename to packages/app/src/desktop/browser/store/index.ts diff --git a/packages/app/src/stores/browser-store/state.test.ts b/packages/app/src/desktop/browser/store/state.test.ts similarity index 100% rename from packages/app/src/stores/browser-store/state.test.ts rename to packages/app/src/desktop/browser/store/state.test.ts diff --git a/packages/app/src/stores/browser-store/state.ts b/packages/app/src/desktop/browser/store/state.ts similarity index 100% rename from packages/app/src/stores/browser-store/state.ts rename to packages/app/src/desktop/browser/store/state.ts diff --git a/packages/app/src/desktop/host.ts b/packages/app/src/desktop/host.ts index 152f32abc..2c8c17680 100644 --- a/packages/app/src/desktop/host.ts +++ b/packages/app/src/desktop/host.ts @@ -1,6 +1,6 @@ import { Platform } from "react-native"; import { getElectronHost } from "@/desktop/electron/host"; -import type { BrowserKeyboardPolicy } from "@/keyboard/browser-shortcuts"; +import type { BrowserKeyboardPolicy } from "@/desktop/browser/shortcuts"; import type { SessionInboundMessage, SessionOutboundMessage } from "@getpaseo/protocol/messages"; type BrowserAutomationExecuteRequest = Extract< diff --git a/packages/app/src/e2e-metro-readiness.test.ts b/packages/app/src/e2e-metro-readiness.test.ts index 0ba5df9e3..abd5bc841 100644 --- a/packages/app/src/e2e-metro-readiness.test.ts +++ b/packages/app/src/e2e-metro-readiness.test.ts @@ -1,7 +1,7 @@ import { createServer, type Server } from "node:http"; import { afterEach, expect, test } from "vitest"; -import { waitForMetro } from "../e2e/global-setup"; +import { waitForMetro } from "../e2e/support/global-setup"; class MetroPort { private response = { status: 500, body: "fallback" }; diff --git a/packages/app/src/hooks/use-keyboard-shortcuts.ts b/packages/app/src/hooks/use-keyboard-shortcuts.ts index ce6155a5a..bfb0c29da 100644 --- a/packages/app/src/hooks/use-keyboard-shortcuts.ts +++ b/packages/app/src/hooks/use-keyboard-shortcuts.ts @@ -3,7 +3,7 @@ import { usePathname, useRouter } from "expo-router"; import { getIsElectronRuntime } from "@/constants/layout"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { setCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore"; -import { getResidentBrowserWebview } from "@/components/browser-webview-resident"; +import { getResidentBrowserWebview } from "@/desktop/browser/resident-webviews"; import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store"; import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher"; import { @@ -18,7 +18,7 @@ import { buildBrowserKeyboardPolicy, parseBrowserShortcutInput, shouldPublishBrowserShortcutPolicy, -} from "@/keyboard/browser-shortcuts"; +} from "@/desktop/browser/shortcuts"; import type { KeyboardFocusScope, KeyboardShortcutPayload } from "@/keyboard/actions"; import { routeKeyboardShortcut, diff --git a/packages/app/src/panels/register-panels.ts b/packages/app/src/panels/register-panels.ts index 9b8d37069..0e9ed7ad8 100644 --- a/packages/app/src/panels/register-panels.ts +++ b/packages/app/src/panels/register-panels.ts @@ -1,5 +1,5 @@ import { agentPanelRegistration } from "@/panels/agent-panel"; -import { browserPanelRegistration } from "@/panels/browser-panel"; +import { browserPanelRegistration } from "@/desktop/browser/panel"; import { commitDiffPanelRegistration, workingDiffPanelRegistration } from "@/panels/diff-panel"; import { draftPanelRegistration } from "@/panels/draft-panel"; import { filePanelRegistration } from "@/panels/file-panel"; diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index d77682a2f..2df0e275f 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -47,7 +47,7 @@ import { invalidateServerDataQueriesAfterReconnect, mountServerDataPushRouter, } from "@/data/push-router"; -import { mountBrowserAutomationDaemonClientHandler } from "@/browser-automation/handler"; +import { mountBrowserAutomationDaemonClientHandler } from "@/desktop/browser/automation/handler"; import { schedulesQueryBaseKey } from "@/schedules/aggregated-schedules"; import { dispatchComposerAgentMessage, sendQueuedComposerMessageNow } from "@/composer/actions"; import { createMessageSubmissionWriter } from "@/composer/submission/writer"; diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index aebdeb086..a96554e4e 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -76,7 +76,7 @@ import { CommunityLinks } from "@/components/community-links"; import { SegmentedControl } from "@/components/ui/segmented-control"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section"; -import { BrowserDataSection } from "@/desktop/components/browser-data-section"; +import { BrowserDataSection } from "@/desktop/browser/settings/browser-data-section"; import { IntegrationsSection } from "@/desktop/components/integrations-section"; import { isElectronRuntime } from "@/desktop/host"; import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater"; diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 0a89fe302..74babd488 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -110,8 +110,8 @@ import type { CheckoutStatusPayload } from "@/git/use-status-query"; import { confirmDialog } from "@/utils/confirm-dialog"; import { useArchiveAgent } from "@/hooks/use-archive-agent"; import { useStableEvent } from "@/hooks/use-stable-event"; -import { removeResidentBrowserWebview } from "@/components/browser-webview-resident"; -import { createWorkspaceBrowser, useBrowserStore } from "@/stores/browser-store"; +import { removeResidentBrowserWebview } from "@/desktop/browser/resident-webviews"; +import { createWorkspaceBrowser, useBrowserStore } from "@/desktop/browser/store"; import { getDesktopHost } from "@/desktop/host"; import { buildProviderCommand } from "@/utils/provider-command-templates"; import { generateDraftId } from "@/stores/draft-keys"; @@ -135,7 +135,7 @@ import { type WorkspaceTabMenuEntry, type WorkspaceTabMenuLabels, } from "@/screens/workspace/workspace-tab-menu"; -import { useDesktopBrowserNewTabRequests } from "@/browser/new-tab-requests"; +import { useDesktopBrowserNewTabRequests } from "@/desktop/browser/new-tab-requests"; import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types"; import { resolveWorkspaceHeaderRenderState, diff --git a/packages/cli/src/commands/hooks.test.ts b/packages/cli/src/commands/hooks.test.ts index 93f2219be..3f8c31d85 100644 --- a/packages/cli/src/commands/hooks.test.ts +++ b/packages/cli/src/commands/hooks.test.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import { AGENT_HOOK_PROVIDERS } from "@getpaseo/server/agent-hooks"; import { describe, expect, it } from "vitest"; import { runHooksCommand } from "./hooks.js"; @@ -75,6 +76,14 @@ function expectPostedState(fetch: RecordingFetch, state: string) { } describe("runHooksCommand", () => { + it("keeps provider names out of the generic command", () => { + const source = readFileSync(new URL("./hooks.ts", import.meta.url), "utf8").toLowerCase(); + + for (const providerId of Object.keys(AGENT_HOOK_PROVIDERS)) { + expect(source).not.toContain(providerId); + } + }); + it.each([ [claudeProvider.events[0].event, "running"], [claudeProvider.events[1].event, "idle"], diff --git a/packages/cli/tests/34-daemon-status-auth.test.ts b/packages/cli/tests/34-daemon-status-auth.test.ts index 336c79601..bb683ed02 100644 --- a/packages/cli/tests/34-daemon-status-auth.test.ts +++ b/packages/cli/tests/34-daemon-status-auth.test.ts @@ -1,35 +1,16 @@ #!/usr/bin/env npx tsx import assert from "node:assert"; -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import { createTestPaseoDaemon } from "../../server/src/server/test-utils/paseo-daemon.ts"; import { runLocalPaseo } from "./helpers/local-cli.ts"; +import { startTestDaemon } from "./helpers/test-daemon.ts"; console.log("=== Daemon Status Auth ===\n"); -const CORRECT_PASSWORD_HASH = "$2b$12$GMhF7pN4QnMlHOQXOqjd1OitKWPSmAO3FwB0PHzKtcZR/sAMryz76"; - -const daemon = await createTestPaseoDaemon({ - auth: { password: CORRECT_PASSWORD_HASH }, +const daemon = await startTestDaemon({ + env: { PASEO_PASSWORD: "shared-secret" }, }); try { - await writeFile( - join(daemon.paseoHome, "paseo.pid"), - `${JSON.stringify( - { - pid: process.pid, - startedAt: new Date().toISOString(), - hostname: "status-auth-test", - uid: process.getuid?.(), - listen: `0.0.0.0:${daemon.port}`, - }, - null, - 2, - )}\n`, - ); - { console.log("Test 1: status reports password requirement without marking daemon unreachable"); const result = await runLocalPaseo(["daemon", "status", "--json"], { @@ -86,7 +67,7 @@ try { console.log("✓ password-authenticated status remains reachable\n"); } } finally { - await daemon.close(); + await daemon.stop(); } console.log("=== Daemon Status Auth Tests Passed ==="); diff --git a/packages/cli/tests/helpers/test-daemon.ts b/packages/cli/tests/helpers/test-daemon.ts index 5052ec935..8607b1cfb 100644 --- a/packages/cli/tests/helpers/test-daemon.ts +++ b/packages/cli/tests/helpers/test-daemon.ts @@ -169,7 +169,7 @@ export async function createTempDirs(): Promise<{ paseoHome: string; workDir: st * Wait for daemon to be ready by running `paseo agent ls` * This connects via WebSocket and ensures the daemon is responsive */ -async function probeDaemonReady(port: number): Promise { +async function probeDaemonReady(port: number, env?: NodeJS.ProcessEnv): Promise { try { const { exitCode } = await runPaseoCli( { @@ -182,6 +182,7 @@ async function probeDaemonReady(port: number): Promise { stop: async () => {}, }, ["agent", "ls"], + { env }, ); return exitCode === 0; } catch { @@ -189,11 +190,15 @@ async function probeDaemonReady(port: number): Promise { } } -async function waitForDaemonReady(port: number, timeout = 30000): Promise { +async function waitForDaemonReady( + port: number, + timeout = 30000, + env?: NodeJS.ProcessEnv, +): Promise { const deadline = Date.now() + timeout; async function poll(): Promise { - if (await probeDaemonReady(port)) return; + if (await probeDaemonReady(port, env)) return; if (Date.now() >= deadline) { throw new Error(`Daemon failed to become ready on port ${port} within ${timeout}ms`); } @@ -314,7 +319,7 @@ export async function startTestDaemon(options?: { // Wait for daemon to be ready try { - await waitForDaemonReady(port, timeout); + await waitForDaemonReady(port, timeout, options?.env); ctx.isReady = true; } catch (err) { // Daemon failed to start - clean up and rethrow diff --git a/packages/desktop/capture-harness/main.js b/packages/desktop/capture-harness/main.js index e8eeff2bf..b51735275 100644 --- a/packages/desktop/capture-harness/main.js +++ b/packages/desktop/capture-harness/main.js @@ -2434,7 +2434,7 @@ async function runAutomationGroup() { // 1px resident host, and Electron does not propagate CSS-box resizes to a // parked guest's capture surface (see docs/browser-capture-harness.md). // The production resize path is app-owned webview sizing, covered by - // packages/app/src/browser-automation/handler.test.ts. + // packages/app/src/desktop/browser/automation/handler.test.ts. await fsp.writeFile( path.join(OUT_DIR, "automation-results.json"), diff --git a/packages/desktop/scripts/browser-tab-bridge.e2e.mjs b/packages/desktop/e2e/browser-tabs.e2e.mjs similarity index 94% rename from packages/desktop/scripts/browser-tab-bridge.e2e.mjs rename to packages/desktop/e2e/browser-tabs.e2e.mjs index 139a186e6..3b2a833a9 100644 --- a/packages/desktop/scripts/browser-tab-bridge.e2e.mjs +++ b/packages/desktop/e2e/browser-tabs.e2e.mjs @@ -18,10 +18,10 @@ const desktopDir = path.resolve(scriptDir, ".."); const rootDir = path.resolve(desktopDir, "../.."); const devRunner = path.join(desktopDir, "scripts", "dev-runner.mjs"); const workspaceIds = [ - "tab-bridge-original", - "tab-bridge-evict-one", - "tab-bridge-evict-two", - "tab-bridge-evict-three", + "desktop-browser-original", + "desktop-browser-evict-one", + "desktop-browser-evict-two", + "desktop-browser-evict-three", ]; const timeoutMs = 90_000; @@ -91,7 +91,7 @@ function seedPaseoHome(paseoHome, listen, workspaceRoot) { projectId: `project-${workspaceId}`, rootPath: cwd, kind: "non_git", - displayName: `Tab bridge project ${index + 1}`, + displayName: `Desktop browser project ${index + 1}`, customName: null, createdAt: timestamp, updatedAt: timestamp, @@ -103,8 +103,8 @@ function seedPaseoHome(paseoHome, listen, workspaceRoot) { projectId: projects[index].projectId, cwd: projects[index].rootPath, kind: "directory", - displayName: `Tab bridge workspace ${index + 1}`, - title: `Tab bridge workspace ${index + 1}`, + displayName: `Desktop browser workspace ${index + 1}`, + title: `Desktop browser workspace ${index + 1}`, branch: null, baseBranch: null, createdAt: timestamp, @@ -196,7 +196,7 @@ async function startTargetPage() { response.writeHead(200, { "content-type": "text/html; charset=utf-8" }); response.end(` - Tab bridge target + Desktop browser target @@ -240,7 +240,7 @@ async function createCallerAgent(daemonPort) { args: { relationship: { kind: "detached" }, workspace: { kind: "existing", workspaceId: workspaceIds[0] }, - title: "Browser tab bridge E2E caller", + title: "Browser desktop browser E2E caller", provider: "mock/ten-second-stream", settings: { modeId: "load-test" }, initialPrompt: "Remain available while the browser bridge regression runs.", @@ -356,9 +356,9 @@ async function runRegression({ page, client, serverId, targetUrl }) { async function main() { const artifactDir = - process.env.PASEO_TAB_BRIDGE_E2E_ARTIFACT_DIR ?? - fs.mkdtempSync(path.join(os.tmpdir(), "paseo-tab-bridge-e2e-artifacts-")); - const runtimeDir = fs.mkdtempSync(path.join(os.tmpdir(), "paseo-tab-bridge-e2e-")); + process.env.PASEO_DESKTOP_BROWSER_E2E_ARTIFACT_DIR ?? + fs.mkdtempSync(path.join(os.tmpdir(), "paseo-desktop-browser-e2e-artifacts-")); + const runtimeDir = fs.mkdtempSync(path.join(os.tmpdir(), "paseo-desktop-browser-e2e-")); fs.mkdirSync(artifactDir, { recursive: true }); const paseoHome = path.join(runtimeDir, "paseo-home"); const userData = path.join(runtimeDir, "electron-user-data"); @@ -449,10 +449,10 @@ async function main() { }); writeJson(path.join(artifactDir, "result.json"), report); console.log( - `Browser tab bridge E2E passed: WebContents ${report.originalWebContentsId} -> ${report.replacementWebContentsId}; list, snapshot, click passed.`, + `Browser desktop browser E2E passed: WebContents ${report.originalWebContentsId} -> ${report.replacementWebContentsId}; list, snapshot, click passed.`, ); } catch (error) { - console.error(`Browser tab bridge E2E failed. Artifacts: ${artifactDir}`); + console.error(`Browser desktop browser E2E failed. Artifacts: ${artifactDir}`); console.error(error); throw error; } finally { diff --git a/packages/desktop/e2e/local-daemon-host.spec.ts b/packages/desktop/e2e/local-daemon-host.spec.ts new file mode 100644 index 000000000..a15818338 --- /dev/null +++ b/packages/desktop/e2e/local-daemon-host.spec.ts @@ -0,0 +1,42 @@ +import { test } from "../../app/e2e/support/fixtures"; +import { gotoAppShell, openSettings } from "../../app/e2e/support/helpers/app"; +import { getServerId } from "../../app/e2e/support/helpers/server-id"; +import { expectLocalHostEntryFirst } from "../../app/e2e/support/helpers/settings"; + +test("sidebar pins the local desktop daemon host first", async ({ page }) => { + const serverId = getServerId(); + + 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); +}); diff --git a/packages/app/e2e/workspace-open-in-editor.spec.ts b/packages/desktop/e2e/open-in-editor.spec.ts similarity index 91% rename from packages/app/e2e/workspace-open-in-editor.spec.ts rename to packages/desktop/e2e/open-in-editor.spec.ts index 62b35d270..655155d28 100644 --- a/packages/app/e2e/workspace-open-in-editor.spec.ts +++ b/packages/desktop/e2e/open-in-editor.spec.ts @@ -1,8 +1,8 @@ import { readFile, rm } from "node:fs/promises"; -import { expect, test, type Page } from "./fixtures"; -import { gotoAppShell, openSettings } from "./helpers/app"; -import { injectDesktopBridge } from "./helpers/desktop-updates"; -import { clickSettingsBackToWorkspace } from "./helpers/settings"; +import { expect, test, type Page } from "../../app/e2e/support/fixtures"; +import { gotoAppShell, openSettings } from "../../app/e2e/support/helpers/app"; +import { installDesktopRuntime } from "./support/runtime"; +import { clickSettingsBackToWorkspace } from "../../app/e2e/support/helpers/settings"; interface EditorOpenRecord { editorId: string; @@ -75,7 +75,7 @@ test.describe("Workspace open in editor", () => { const serverId = requireE2EEnv("E2E_SERVER_ID"); const recordPath = requireE2EEnv("E2E_EDITOR_RECORD_PATH"); await rm(recordPath, { force: true }); - await injectDesktopBridge(page, { + await installDesktopRuntime(page, { serverId, editorTargets: [ { diff --git a/packages/desktop/scripts/smoke-packaged-desktop-app.js b/packages/desktop/e2e/packaged-app-smoke.js similarity index 100% rename from packages/desktop/scripts/smoke-packaged-desktop-app.js rename to packages/desktop/e2e/packaged-app-smoke.js diff --git a/packages/app/e2e/project-picker-desktop.spec.ts b/packages/desktop/e2e/project-picker.spec.ts similarity index 74% rename from packages/app/e2e/project-picker-desktop.spec.ts rename to packages/desktop/e2e/project-picker.spec.ts index a4a71f056..31612d6ba 100644 --- a/packages/app/e2e/project-picker-desktop.spec.ts +++ b/packages/desktop/e2e/project-picker.spec.ts @@ -1,18 +1,16 @@ -import { test, expect } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; -import { injectDesktopBridge, waitForDirectoryDialog } from "./helpers/desktop-updates"; -import { expectOpenedProject } from "./helpers/project-picker-ui"; -import { expectNewWorkspaceForAddedProject } from "./helpers/add-project-flow"; -import { getServerId } from "./helpers/server-id"; -import { connectSeedClient } from "./helpers/seed-client"; - -test.skip(process.env.E2E_DESKTOP_RUNTIME !== "1", "requires Metro's Electron platform overlay"); +import { test, expect } from "../../app/e2e/support/fixtures"; +import { gotoAppShell } from "../../app/e2e/support/helpers/app"; +import { installDesktopRuntime, waitForDirectoryDialog } from "./support/runtime"; +import { expectOpenedProject } from "../../app/e2e/support/helpers/project-picker-ui"; +import { expectNewWorkspaceForAddedProject } from "../../app/e2e/support/helpers/add-project-flow"; +import { getServerId } from "../../app/e2e/support/helpers/server-id"; +import { connectSeedClient } from "../../app/e2e/support/helpers/seed-client"; test("Browse opens the folder selected by the desktop dialog", async ({ page, projectPickerFixture, }) => { - await injectDesktopBridge(page, { + await installDesktopRuntime(page, { serverId: getServerId(), manageBuiltInDaemon: false, dialogOpenResult: projectPickerFixture.projectPath, @@ -44,7 +42,7 @@ test("canceling Browse returns to the Add Project methods", async ({ page, projectPickerFixture, }) => { - await injectDesktopBridge(page, { + await installDesktopRuntime(page, { serverId: getServerId(), manageBuiltInDaemon: false, dialogOpenResult: null, diff --git a/packages/app/e2e/settings-sidebar-scroll.spec.ts b/packages/desktop/e2e/settings-sidebar-scroll.spec.ts similarity index 92% rename from packages/app/e2e/settings-sidebar-scroll.spec.ts rename to packages/desktop/e2e/settings-sidebar-scroll.spec.ts index 06c6b6b25..942fcdb42 100644 --- a/packages/app/e2e/settings-sidebar-scroll.spec.ts +++ b/packages/desktop/e2e/settings-sidebar-scroll.spec.ts @@ -1,5 +1,5 @@ -import { test, expect } from "./fixtures"; -import { gotoAppShell, openSettings } from "./helpers/app"; +import { test, expect } from "../../app/e2e/support/fixtures"; +import { gotoAppShell, openSettings } from "../../app/e2e/support/helpers/app"; test.describe("Settings sidebar scrolling", () => { test.use({ viewport: { width: 900, height: 260 } }); diff --git a/packages/desktop/e2e/startup.spec.ts b/packages/desktop/e2e/startup.spec.ts new file mode 100644 index 000000000..502f7e01a --- /dev/null +++ b/packages/desktop/e2e/startup.spec.ts @@ -0,0 +1,32 @@ +import { test } from "../../app/e2e/support/fixtures"; +import { getE2EDaemonPort } from "../../app/e2e/support/helpers/daemon-port"; +import { getServerId } from "../../app/e2e/support/helpers/server-id"; +import { startupScenario } from "../../app/e2e/support/helpers/startup-dsl"; + +test.describe("Desktop startup", () => { + test("daemon bootstrap keeps the local server startup copy desktop-only", async ({ page }) => { + const startup = await startupScenario(page) + .withPendingDesktopDaemon() + .withBlockedPort(getE2EDaemonPort()) + .openRoot(); + + await startup.expectsDesktopDaemonStartup(); + await startup.expectsSidebarHidden(); + await startup.expectsNoUndefinedRoute(); + }); + + test("host-route refresh does not render route chrome around the bootstrap splash", async ({ + page, + }) => { + const startup = await startupScenario(page) + .withPendingDesktopDaemon() + .withBlockedPort(getE2EDaemonPort()) + .openHostWorkspace({ + serverId: getServerId(), + workspaceId: "workspace-1", + }); + + await startup.expectsDesktopDaemonStartup(); + await startup.expectsSidebarHidden(); + }); +}); diff --git a/packages/app/e2e/helpers/desktop-updates.ts b/packages/desktop/e2e/support/runtime.ts similarity index 96% rename from packages/app/e2e/helpers/desktop-updates.ts rename to packages/desktop/e2e/support/runtime.ts index c52d042c0..a803bdc51 100644 --- a/packages/app/e2e/helpers/desktop-updates.ts +++ b/packages/desktop/e2e/support/runtime.ts @@ -1,10 +1,14 @@ import { readFileSync } from "node:fs"; import { appendFile } from "node:fs/promises"; import { expect, type Page } from "@playwright/test"; -import { openSettings } from "./app"; -import { getE2EDaemonPort } from "./daemon-port"; -import { escapeRegex } from "./regex"; -import { openSettingsHost, openSettingsHostSection, openSettingsSection } from "./settings"; +import { openSettings } from "../../../app/e2e/support/helpers/app"; +import { getE2EDaemonPort } from "../../../app/e2e/support/helpers/daemon-port"; +import { escapeRegex } from "../../../app/e2e/support/helpers/regex"; +import { + openSettingsHost, + openSettingsHostSection, + openSettingsSection, +} from "../../../app/e2e/support/helpers/settings"; interface DaemonApiStatus { version: string; @@ -49,7 +53,7 @@ export async function loadRealDaemonState(): Promise { return { version: data.version, pid, logPath: `${paseoHome}/daemon.log` }; } -export interface DesktopBridgeConfig { +export interface DesktopRuntimeConfig { serverId: string; updateAvailable?: boolean; latestVersion?: string; @@ -116,7 +120,10 @@ declare global { * dialog.ask captures call arguments on window.__capturedDialogCall so tests * can assert dialog copy without depending on window.confirm concatenation. */ -export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfig): Promise { +export async function installDesktopRuntime( + page: Page, + config: DesktopRuntimeConfig, +): Promise { if (config.editorRecordPath) { await page.exposeFunction( "__recordDesktopEditorOpen", diff --git a/packages/app/e2e/desktop-updates.spec.ts b/packages/desktop/e2e/updates.spec.ts similarity index 76% rename from packages/app/e2e/desktop-updates.spec.ts rename to packages/desktop/e2e/updates.spec.ts index ee9277f86..6cef40e50 100644 --- a/packages/app/e2e/desktop-updates.spec.ts +++ b/packages/desktop/e2e/updates.spec.ts @@ -1,9 +1,14 @@ -import { test } from "./fixtures"; -import { gotoAppShell } from "./helpers/app"; -import { getServerId } from "./helpers/server-id"; +import { expect, test } from "../../app/e2e/support/fixtures"; +import { gotoAppShell, openSettings } from "../../app/e2e/support/helpers/app"; +import { getServerId } from "../../app/e2e/support/helpers/server-id"; +import { + openHostSection, + openSettingsHost, + seedSavedSettingsHosts, +} from "../../app/e2e/support/helpers/settings"; import { loadRealDaemonState, - injectDesktopBridge, + installDesktopRuntime, openDesktopAboutSettings, openDesktopSettings, expectUpdateBanner, @@ -19,13 +24,31 @@ import { expectDaemonStatusPid, expectDaemonStatusLogPath, expectDaemonStatusVersion, -} from "./helpers/desktop-updates"; +} from "./support/runtime"; // No Playwright Electron runner exists; we simulate the desktop bridge via // addInitScript so Electron-gated UI activates without a real Electron process. test.describe("Desktop updates", () => { + 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("update banner appears in the sidebar when an app update is available", async ({ page }) => { - await injectDesktopBridge(page, { + await installDesktopRuntime(page, { serverId: getServerId(), updateAvailable: true, latestVersion: "1.2.3", @@ -36,7 +59,7 @@ test.describe("Desktop updates", () => { }); test("clicking install shows the installing state on the callout", async ({ page }) => { - await injectDesktopBridge(page, { + await installDesktopRuntime(page, { serverId: getServerId(), updateAvailable: true, latestVersion: "1.2.3", @@ -50,7 +73,7 @@ test.describe("Desktop updates", () => { }); test("manual check reports a found update while it downloads", async ({ page }) => { - await injectDesktopBridge(page, { + await installDesktopRuntime(page, { serverId: getServerId(), updateAvailable: true, latestVersion: "1.2.3", @@ -70,7 +93,7 @@ test.describe("Desktop daemon management", () => { page, }) => { const serverId = getServerId(); - await injectDesktopBridge(page, { + await installDesktopRuntime(page, { serverId, manageBuiltInDaemon: true, confirmShouldAccept: false, @@ -86,7 +109,7 @@ test.describe("Desktop daemon management", () => { test("cancelling the confirm dialog leaves the daemon management toggle on", async ({ page }) => { const serverId = getServerId(); - await injectDesktopBridge(page, { + await installDesktopRuntime(page, { serverId, manageBuiltInDaemon: true, confirmShouldAccept: false, @@ -101,7 +124,7 @@ test.describe("Desktop daemon management", () => { test("confirming the dialog disables built-in daemon management", async ({ page }) => { const serverId = getServerId(); - await injectDesktopBridge(page, { + await installDesktopRuntime(page, { serverId, manageBuiltInDaemon: true, confirmShouldAccept: true, @@ -119,7 +142,7 @@ test.describe("Desktop daemon management", () => { }) => { const serverId = getServerId(); const realState = await loadRealDaemonState(); - await injectDesktopBridge(page, { + await installDesktopRuntime(page, { serverId, manageBuiltInDaemon: false, daemonPid: realState.pid, @@ -137,7 +160,7 @@ test.describe("Desktop daemon management", () => { test("stopping and restarting the daemon updates the PID", async ({ page }) => { const serverId = getServerId(); const realState = await loadRealDaemonState(); - await injectDesktopBridge(page, { + await installDesktopRuntime(page, { serverId, manageBuiltInDaemon: true, daemonPid: realState.pid, diff --git a/packages/desktop/e2e/workspace-restart.spec.ts b/packages/desktop/e2e/workspace-restart.spec.ts new file mode 100644 index 000000000..826b79293 --- /dev/null +++ b/packages/desktop/e2e/workspace-restart.spec.ts @@ -0,0 +1,120 @@ +import { buildHostWorkspaceRoute } from "../../app/src/utils/host-routes"; +import { expect, test, type Page } from "../../app/e2e/support/fixtures"; +import { gotoAppShell } from "../../app/e2e/support/helpers/app"; +import { + createIdleAgent, + expectWorkspaceTabVisible, +} from "../../app/e2e/support/helpers/archive-tab"; +import { getE2EDaemonPort } from "../../app/e2e/support/helpers/daemon-port"; +import { installDaemonWebSocketGate } from "../../app/e2e/support/helpers/daemon-websocket-gate"; +import { expectAppRoute } from "../../app/e2e/support/helpers/route-assertions"; +import { seedWorkspace } from "../../app/e2e/support/helpers/seed-client"; +import { getServerId } from "../../app/e2e/support/helpers/server-id"; +import { waitForWorkspaceTabsVisible } from "../../app/e2e/support/helpers/workspace-tabs"; +import { + expectWorkspaceHeader, + switchWorkspaceViaSidebar, + waitForSidebarHydration, +} from "../../app/e2e/support/helpers/workspace-ui"; +import { installDesktopRuntime, waitForDesktopDaemonStartRequest } from "./support/runtime"; + +type StartupPresentation = "splash" | "app"; + +declare global { + interface Window { + __paseoStartupPresentationTrace?: StartupPresentation[]; + } +} + +async function observeStartupPresentation(page: Page): Promise { + await page.addInitScript(() => { + const trace: StartupPresentation[] = []; + window.__paseoStartupPresentationTrace = trace; + + document.addEventListener("DOMContentLoaded", () => { + const recordPresentation = () => { + let presentation: StartupPresentation | null = null; + if (document.querySelector('[data-testid="startup-splash"]')) { + presentation = "splash"; + } else if ( + document.querySelector( + '[data-testid="workspace-header-title"], [data-testid="sidebar-settings"]', + ) + ) { + presentation = "app"; + } + if (presentation && trace.at(-1) !== presentation) { + trace.push(presentation); + } + }; + new MutationObserver(recordPresentation).observe(document.documentElement, { + childList: true, + subtree: true, + }); + recordPresentation(); + }); + }); +} + +async function getStartupPresentation(page: Page): Promise { + return page.evaluate(() => window.__paseoStartupPresentationTrace?.slice() ?? []); +} + +async function expectWorkspaceLocation( + page: Page, + input: { + serverId: string; + workspace: Awaited>; + }, +): Promise { + await expectAppRoute(page, buildHostWorkspaceRoute(input.serverId, input.workspace.workspaceId), { + timeout: 30_000, + }); + await expectWorkspaceHeader(page, { + title: input.workspace.workspaceName, + subtitle: input.workspace.projectDisplayName, + }); +} + +test("refresh keeps one continuous splash before restoring the desktop workspace", async ({ + page, +}) => { + const serverId = getServerId(); + const daemonGate = await installDaemonWebSocketGate(page); + const workspace = await seedWorkspace({ repoPrefix: "workspace-refresh-route-" }); + + try { + const agent = await createIdleAgent(workspace.client, { + cwd: workspace.repoPath, + workspaceId: workspace.workspaceId, + title: `workspace-refresh-route-${Date.now()}`, + }); + await installDesktopRuntime(page, { + serverId, + manageBuiltInDaemon: true, + hangDaemonStart: true, + desktopSettingsDelayMs: 250, + daemonListen: `127.0.0.1:${getE2EDaemonPort()}`, + }); + await gotoAppShell(page); + await waitForSidebarHydration(page); + await switchWorkspaceViaSidebar({ page, serverId, workspaceId: workspace.workspaceId }); + await waitForWorkspaceTabsVisible(page); + await expectWorkspaceTabVisible(page, agent.id); + await expectWorkspaceLocation(page, { serverId, workspace }); + + await observeStartupPresentation(page); + await daemonGate.drop(); + await page.reload(); + await waitForDesktopDaemonStartRequest(page); + daemonGate.restore(); + await waitForSidebarHydration(page); + + await expectWorkspaceLocation(page, { serverId, workspace }); + await waitForWorkspaceTabsVisible(page); + expect(await getStartupPresentation(page)).toEqual(["splash", "app"]); + } finally { + daemonGate.restore(); + await workspace.cleanup(); + } +}); diff --git a/packages/desktop/package.json b/packages/desktop/package.json index c6e97aa10..42a5ce0c1 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -20,7 +20,8 @@ "capture-harness": "./capture-harness/run.sh", "dev": "./scripts/dev.sh", "dev:win": "powershell ./scripts/dev.ps1", - "test:e2e:browser-tab-bridge": "npm run build:main && node ./scripts/browser-tab-bridge.e2e.mjs", + "test:e2e:renderer": "cross-env E2E_DESKTOP_RUNTIME=1 playwright test --config=playwright.config.ts --project=desktop", + "test:e2e:browser-tabs": "npm run build:main && node ./e2e/browser-tabs.e2e.mjs", "verify:electron-cdp": "node ./scripts/verify-electron-cdp.mjs", "test": "vitest run", "typecheck": "tsgo --noEmit -p tsconfig.json" diff --git a/packages/desktop/playwright.config.ts b/packages/desktop/playwright.config.ts new file mode 100644 index 000000000..3e74d1a5d --- /dev/null +++ b/packages/desktop/playwright.config.ts @@ -0,0 +1,30 @@ +import { defineConfig, devices } from "@playwright/test"; + +const baseURL = + process.env.E2E_BASE_URL ?? `http://localhost:${process.env.E2E_METRO_PORT ?? "8081"}`; + +export default defineConfig({ + testDir: "./e2e", + testMatch: ["**/*.spec.ts"], + globalSetup: "../app/e2e/support/global-setup.ts", + timeout: 60_000, + expect: { + timeout: 10_000, + }, + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 1 : 0, + reporter: [["list"]], + use: { + baseURL, + trace: "retain-on-failure", + screenshot: "only-on-failure", + video: process.env.E2E_RECORD_VIDEO === "1" ? "on" : "retain-on-failure", + }, + projects: [ + { + name: "desktop", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}); diff --git a/packages/desktop/scripts/after-pack.js b/packages/desktop/scripts/after-pack.js index ecbc69fd9..e5222d68f 100644 --- a/packages/desktop/scripts/after-pack.js +++ b/packages/desktop/scripts/after-pack.js @@ -1,7 +1,7 @@ const fs = require("fs"); const path = require("path"); -const { smokePackagedDesktopApp } = require("./smoke-packaged-desktop-app.js"); +const { smokePackagedDesktopApp } = require("../e2e/packaged-app-smoke.js"); const EXECUTABLE_NAME = "Paseo"; diff --git a/packages/desktop/scripts/after-sign.js b/packages/desktop/scripts/after-sign.js index 7d3aa4bbd..25a0a141e 100644 --- a/packages/desktop/scripts/after-sign.js +++ b/packages/desktop/scripts/after-sign.js @@ -1,6 +1,6 @@ const path = require("node:path"); -const { smokePackagedDesktopApp } = require("./smoke-packaged-desktop-app.js"); +const { smokePackagedDesktopApp } = require("../e2e/packaged-app-smoke.js"); const EXECUTABLE_NAME = "Paseo"; diff --git a/packages/protocol/src/messages.wire-compat.test.ts b/packages/protocol/src/messages.wire-compat.test.ts new file mode 100644 index 000000000..983699b54 --- /dev/null +++ b/packages/protocol/src/messages.wire-compat.test.ts @@ -0,0 +1,210 @@ +import { describe, expect, test } from "vitest"; +import { z } from "zod"; +import { + AgentSnapshotPayloadSchema, + AgentTimelineItemPayloadSchema, + ServerInfoStatusPayloadSchema, + WSHelloMessageSchema, +} from "./messages.js"; + +const LegacySubAgentToolCallSchema = z.object({ + type: z.literal("tool_call"), + callId: z.string(), + name: z.string(), + status: z.enum(["running", "completed", "failed", "canceled"]), + error: z.unknown().nullable(), + detail: z.object({ + type: z.literal("sub_agent"), + subAgentType: z.string().optional(), + description: z.string().optional(), + log: z.string(), + // Copied from v0.1.65-beta.3: actions was required even though the UI ignored it. + actions: z.array( + z.object({ + index: z.number().int().positive(), + toolName: z.string(), + summary: z.string().optional(), + }), + ), + }), +}); + +const LegacyAgentCapabilityFlagsSchema = z.object({ + supportsStreaming: z.boolean(), + supportsSessionPersistence: z.boolean(), + supportsDynamicModes: z.boolean(), + supportsMcpServers: z.boolean(), + supportsReasoningStream: z.boolean(), + supportsToolInvocations: z.boolean(), +}); + +const LegacyAgentSnapshotPayloadSchema = AgentSnapshotPayloadSchema.extend({ + capabilities: LegacyAgentCapabilityFlagsSchema, +}); + +describe("wire schema compatibility", () => { + test("hello parses with and without the project update capability", () => { + const legacy = WSHelloMessageSchema.parse({ + type: "hello", + clientId: "legacy-client", + clientType: "mobile", + protocolVersion: 1, + }); + const capable = WSHelloMessageSchema.parse({ + type: "hello", + clientId: "capable-client", + clientType: "mobile", + protocolVersion: 1, + capabilities: { project_updates: true }, + }); + + expect([legacy, capable]).toEqual([ + { + type: "hello", + clientId: "legacy-client", + clientType: "mobile", + protocolVersion: 1, + }, + { + type: "hello", + clientId: "capable-client", + clientType: "mobile", + protocolVersion: 1, + capabilities: { project_updates: true }, + }, + ]); + }); + + test("server info accepts legacy feature payloads without stable project identity", () => { + const parsed = ServerInfoStatusPayloadSchema.parse({ + status: "server_info", + serverId: "legacy-server", + features: { workspaceGithubClone: true }, + }); + + expect(parsed).toEqual({ + status: "server_info", + serverId: "legacy-server", + hostname: null, + version: null, + features: {}, + }); + }); + + test("assistant timeline message ids are optional on the wire", () => { + expect( + AgentTimelineItemPayloadSchema.parse({ + type: "assistant_message", + text: "old daemon shape", + }), + ).toEqual({ + type: "assistant_message", + text: "old daemon shape", + }); + expect( + AgentTimelineItemPayloadSchema.parse({ + type: "assistant_message", + text: "new daemon shape", + messageId: "msg-1", + }), + ).toEqual({ + type: "assistant_message", + text: "new daemon shape", + messageId: "msg-1", + }); + }); + + test("sub_agent tool-call payload still parses against the v0.1.65-beta.3 schema", () => { + const parsed = LegacySubAgentToolCallSchema.parse({ + type: "tool_call", + callId: "call-sub-agent-1", + name: "Task", + status: "completed", + error: null, + detail: { + type: "sub_agent", + subAgentType: "Explore", + description: "Inspect repository structure", + childSessionId: "child-session-1", + log: "[Read] README.md", + actions: [], + }, + }); + + expect(parsed.detail.actions).toEqual([]); + }); + + test("old clients parse agent snapshots with rewind capabilities", () => { + const parsed = LegacyAgentSnapshotPayloadSchema.parse({ + id: "agent-1", + provider: "claude", + cwd: "/tmp/project", + model: null, + thinkingOptionId: null, + effectiveThinkingOptionId: null, + createdAt: "2026-05-23T00:00:00.000Z", + updatedAt: "2026-05-23T00:00:00.000Z", + lastUserMessageAt: null, + status: "idle", + capabilities: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: true, + supportsReasoningStream: true, + supportsToolInvocations: true, + supportsRewindConversation: true, + supportsRewindFiles: true, + supportsRewindBoth: true, + }, + currentModeId: null, + availableModes: [], + pendingPermissions: [], + persistence: null, + title: null, + labels: {}, + }); + + expect(parsed.capabilities).toEqual({ + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: true, + supportsReasoningStream: true, + supportsToolInvocations: true, + }); + }); + + test("new clients parse agent snapshots without rewind capabilities", () => { + const parsed = AgentSnapshotPayloadSchema.parse({ + id: "agent-1", + provider: "claude", + cwd: "/tmp/project", + model: null, + thinkingOptionId: null, + effectiveThinkingOptionId: null, + createdAt: "2026-05-23T00:00:00.000Z", + updatedAt: "2026-05-23T00:00:00.000Z", + lastUserMessageAt: null, + status: "idle", + capabilities: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: true, + supportsReasoningStream: true, + supportsToolInvocations: true, + }, + currentModeId: null, + availableModes: [], + pendingPermissions: [], + persistence: null, + title: null, + labels: {}, + }); + + expect(parsed.capabilities.supportsRewindConversation).toBe(false); + expect(parsed.capabilities.supportsRewindFiles).toBe(false); + expect(parsed.capabilities.supportsRewindBoth).toBe(false); + }); +}); diff --git a/packages/server/src/server/daemon-client.e2e.test.ts b/packages/server/src/server/daemon-client.e2e.test.ts index bdc1b269d..f05c49f4d 100644 --- a/packages/server/src/server/daemon-client.e2e.test.ts +++ b/packages/server/src/server/daemon-client.e2e.test.ts @@ -29,7 +29,7 @@ const openaiApiKey = process.env.OPENAI_API_KEY ?? null; const localModelsDir = process.env.PASEO_LOCAL_MODELS_DIR ?? path.join(homedir(), ".paseo", "models", "local-speech"); const testFileDir = path.dirname(fileURLToPath(import.meta.url)); -const appE2eFixturesDir = path.resolve(testFileDir, "../../../app/e2e/fixtures"); +const appE2eFixturesDir = path.resolve(testFileDir, "../../../app/e2e/support/fixtures"); function fixturePath(fileName: string): string { return path.join(appE2eFixturesDir, fileName); diff --git a/packages/server/src/server/wire-compat.test.ts b/packages/server/src/server/wire-compat.test.ts index 0b2c07974..0a6ad1856 100644 --- a/packages/server/src/server/wire-compat.test.ts +++ b/packages/server/src/server/wire-compat.test.ts @@ -4,14 +4,11 @@ import { describe, expect, test } from "vitest"; import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities"; import { - AgentSnapshotPayloadSchema, AgentTimelineItemPayloadSchema, FetchAgentTimelineResponseMessageSchema, - ServerInfoStatusPayloadSchema, SessionInboundMessageSchema, SessionOutboundMessageSchema, type SessionOutboundMessage, - WSHelloMessageSchema, } from "@getpaseo/protocol/messages"; import { Session, type SessionOptions } from "./session.js"; import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js"; @@ -42,41 +39,6 @@ const LegacyFetchAgentTimelineResponseMessageSchema = z.object({ }), }); -const LegacySubAgentToolCallSchema = z.object({ - type: z.literal("tool_call"), - callId: z.string(), - name: z.string(), - status: z.enum(["running", "completed", "failed", "canceled"]), - error: z.unknown().nullable(), - detail: z.object({ - type: z.literal("sub_agent"), - subAgentType: z.string().optional(), - description: z.string().optional(), - log: z.string(), - // Copied from v0.1.65-beta.3: actions was required even though the UI ignored it. - actions: z.array( - z.object({ - index: z.number().int().positive(), - toolName: z.string(), - summary: z.string().optional(), - }), - ), - }), -}); - -const LegacyAgentCapabilityFlagsSchema = z.object({ - supportsStreaming: z.boolean(), - supportsSessionPersistence: z.boolean(), - supportsDynamicModes: z.boolean(), - supportsMcpServers: z.boolean(), - supportsReasoningStream: z.boolean(), - supportsToolInvocations: z.boolean(), -}); - -const LegacyAgentSnapshotPayloadSchema = AgentSnapshotPayloadSchema.extend({ - capabilities: LegacyAgentCapabilityFlagsSchema, -}); - interface SessionInternals { handleFetchAgentTimelineRequest: ( message: Extract< @@ -376,77 +338,6 @@ describe("wire compatibility", () => { ]); }); - test("hello parses with and without the project update capability", () => { - const legacy = WSHelloMessageSchema.parse({ - type: "hello", - clientId: "legacy-client", - clientType: "mobile", - protocolVersion: 1, - }); - const capable = WSHelloMessageSchema.parse({ - type: "hello", - clientId: "capable-client", - clientType: "mobile", - protocolVersion: 1, - capabilities: { [CLIENT_CAPS.projectUpdates]: true }, - }); - - expect([legacy, capable]).toEqual([ - { - type: "hello", - clientId: "legacy-client", - clientType: "mobile", - protocolVersion: 1, - }, - { - type: "hello", - clientId: "capable-client", - clientType: "mobile", - protocolVersion: 1, - capabilities: { project_updates: true }, - }, - ]); - }); - - test("server info accepts legacy feature payloads without stable project identity", () => { - const parsed = ServerInfoStatusPayloadSchema.parse({ - status: "server_info", - serverId: "legacy-server", - features: { workspaceGithubClone: true }, - }); - - expect(parsed).toEqual({ - status: "server_info", - serverId: "legacy-server", - hostname: null, - version: null, - features: {}, - }); - }); - - test("assistant timeline message ids are optional on the wire", () => { - expect( - AgentTimelineItemPayloadSchema.parse({ - type: "assistant_message", - text: "old daemon shape", - }), - ).toEqual({ - type: "assistant_message", - text: "old daemon shape", - }); - expect( - AgentTimelineItemPayloadSchema.parse({ - type: "assistant_message", - text: "new daemon shape", - messageId: "msg-1", - }), - ).toEqual({ - type: "assistant_message", - text: "new daemon shape", - messageId: "msg-1", - }); - }); - test("downgrades reasoning_merge for clients that do not declare the capability", async () => { const response = await emitTimelineResponse(); @@ -466,100 +357,6 @@ describe("wire compatibility", () => { expect(currentParsed.payload.entries[0]?.collapsed).toContain("reasoning_merge"); }); - test("sub_agent tool-call payload still parses against the v0.1.65-beta.3 schema", () => { - const parsed = LegacySubAgentToolCallSchema.parse({ - type: "tool_call", - callId: "call-sub-agent-1", - name: "Task", - status: "completed", - error: null, - detail: { - type: "sub_agent", - subAgentType: "Explore", - description: "Inspect repository structure", - childSessionId: "child-session-1", - log: "[Read] README.md", - actions: [], - }, - }); - - expect(parsed.detail.actions).toEqual([]); - }); - - test("old clients parse agent snapshots with rewind capabilities", () => { - const parsed = LegacyAgentSnapshotPayloadSchema.parse({ - id: "agent-1", - provider: "claude", - cwd: "/tmp/project", - model: null, - thinkingOptionId: null, - effectiveThinkingOptionId: null, - createdAt: "2026-05-23T00:00:00.000Z", - updatedAt: "2026-05-23T00:00:00.000Z", - lastUserMessageAt: null, - status: "idle", - capabilities: { - supportsStreaming: true, - supportsSessionPersistence: true, - supportsDynamicModes: true, - supportsMcpServers: true, - supportsReasoningStream: true, - supportsToolInvocations: true, - supportsRewindConversation: true, - supportsRewindFiles: true, - supportsRewindBoth: true, - }, - currentModeId: null, - availableModes: [], - pendingPermissions: [], - persistence: null, - title: null, - labels: {}, - }); - - expect(parsed.capabilities).toEqual({ - supportsStreaming: true, - supportsSessionPersistence: true, - supportsDynamicModes: true, - supportsMcpServers: true, - supportsReasoningStream: true, - supportsToolInvocations: true, - }); - }); - - test("new clients parse agent snapshots without rewind capabilities", () => { - const parsed = AgentSnapshotPayloadSchema.parse({ - id: "agent-1", - provider: "claude", - cwd: "/tmp/project", - model: null, - thinkingOptionId: null, - effectiveThinkingOptionId: null, - createdAt: "2026-05-23T00:00:00.000Z", - updatedAt: "2026-05-23T00:00:00.000Z", - lastUserMessageAt: null, - status: "idle", - capabilities: { - supportsStreaming: true, - supportsSessionPersistence: true, - supportsDynamicModes: true, - supportsMcpServers: true, - supportsReasoningStream: true, - supportsToolInvocations: true, - }, - currentModeId: null, - availableModes: [], - pendingPermissions: [], - persistence: null, - title: null, - labels: {}, - }); - - expect(parsed.capabilities.supportsRewindConversation).toBe(false); - expect(parsed.capabilities.supportsRewindFiles).toBe(false); - expect(parsed.capabilities.supportsRewindBoth).toBe(false); - }); - test("legacy worktree request shape normalizes to the same internal input as the new shape", async () => { const workflow = new InMemoryWorktreeWorkflow(); diff --git a/packages/server/src/terminal/agent-hooks/claude/claude.test.ts b/packages/server/src/terminal/agent-hooks/claude/claude.test.ts index 488b236cc..b92ad24b2 100644 --- a/packages/server/src/terminal/agent-hooks/claude/claude.test.ts +++ b/packages/server/src/terminal/agent-hooks/claude/claude.test.ts @@ -1,8 +1,7 @@ import { spawnSync } from "node:child_process"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { delimiter, dirname, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { delimiter, join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { isPlatform } from "../../../test-utils/platform.js"; import { buildTerminalEnvironment } from "../../terminal.js"; @@ -15,8 +14,6 @@ import { } from "../provider-registry.js"; const temporaryDirs: string[] = []; -const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../../.."); - afterEach(() => { while (temporaryDirs.length > 0) { const dir = temporaryDirs.pop(); @@ -167,17 +164,14 @@ describe("Claude terminal agent hooks", () => { }, ); - it("keeps provider names out of generic CLI and bootstrap integration points", () => { - const genericFiles = [ - join(repositoryRoot, "packages", "cli", "src", "commands", "hooks.ts"), - join(repositoryRoot, "packages", "server", "src", "server", "bootstrap.ts"), - ]; + it("keeps provider names out of the generic server bootstrap", () => { + const source = readFileSync( + new URL("../../../server/bootstrap.ts", import.meta.url), + "utf8", + ).toLowerCase(); - for (const filePath of genericFiles) { - const contents = readFileSync(filePath, "utf8").toLowerCase(); - for (const providerId of Object.keys(AGENT_HOOK_PROVIDERS)) { - expect(contents).not.toContain(providerId); - } + for (const providerId of Object.keys(AGENT_HOOK_PROVIDERS)) { + expect(source).not.toContain(providerId); } }); diff --git a/scripts/ci-workflow.test.mjs b/scripts/ci-workflow.test.mjs index 6d55dd8d3..99ca30f15 100644 --- a/scripts/ci-workflow.test.mjs +++ b/scripts/ci-workflow.test.mjs @@ -1,7 +1,8 @@ import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import { matchesGlob } from "node:path"; +import { readFileSync, readdirSync } from "node:fs"; +import { relative as relativePath } from "node:path"; import test from "node:test"; +import picomatch from "picomatch"; const repoRoot = new URL("../", import.meta.url); const ciWorkflowPath = new URL(".github/workflows/ci.yml", repoRoot); @@ -16,18 +17,14 @@ const gatedCiJobs = new Map([ ["typecheck", { name: "typecheck", contract: "quality" }], ["server-tests-ubuntu", { name: "server-tests (ubuntu-latest)", contracts: ["server", "hub"] }], ["server-tests-windows", { name: "server-tests (windows-latest)", contracts: ["server", "hub"] }], - [ - "desktop-tests-ubuntu", - { name: "desktop-tests (ubuntu-latest)", contracts: ["desktop", "desktop_bridge"] }, - ], + ["desktop-tests-ubuntu", { name: "desktop-tests (ubuntu-latest)", contract: "desktop" }], ["desktop-tests-windows", { name: "desktop-tests (windows-latest)", contract: "desktop" }], ["app-tests", { name: "app-tests", contract: "app" }], ["sdk-tests", { name: "sdk-tests", contract: "sdk" }], - ["playwright-1", { name: "playwright (shard 1/4)", contract: "playwright" }], - ["playwright-2", { name: "playwright (shard 2/4)", contract: "playwright" }], - ["playwright-3", { name: "playwright (shard 3/4)", contract: "playwright" }], - ["playwright-4", { name: "playwright (shard 4/4)", contract: "playwright" }], - ["playwright-desktop", { name: "playwright (desktop overlay)", contract: "playwright_desktop" }], + ["playwright-1", { name: "playwright (shard 1/4)", contract: "browser" }], + ["playwright-2", { name: "playwright (shard 2/4)", contract: "browser" }], + ["playwright-3", { name: "playwright (shard 3/4)", contract: "browser" }], + ["playwright-4", { name: "playwright (shard 4/4)", contract: "browser" }], ["relay-tests", { name: "relay-tests", contract: "relay" }], ["cli-tests-1", { name: "cli-tests (shard 1/3)", contract: "cli" }], ["cli-tests-2", { name: "cli-tests (shard 2/3)", contract: "cli" }], @@ -68,7 +65,21 @@ function loadFilters(path) { } function matchesFilter(filters, filterName, changedPath) { - return filters[filterName].some((pattern) => matchesGlob(changedPath, pattern)); + return filters[filterName].some((pattern) => picomatch(pattern, { dot: true })(changedPath)); +} + +function filesUnder(relativeDirectory, predicate) { + const directory = new URL(`${relativeDirectory}/`, repoRoot); + return readdirSync(directory, { recursive: true, withFileTypes: true }) + .filter((entry) => entry.isFile()) + .map((entry) => + [relativeDirectory, relativePath(directory.pathname, entry.parentPath), entry.name] + .filter(Boolean) + .join("/") + .replaceAll("\\", "/"), + ) + .filter(predicate) + .sort(); } function affectedContracts(filters, changedPath) { @@ -126,9 +137,11 @@ test("focused contracts stay inside existing required checks", () => { assert.match(server, /npm run test --workspace=@getpaseo\/server/); assert.ok(!jobs.has("hub-cli-contract")); - assert.match(desktop, /test:e2e:browser-tab-bridge/); + assert.match(desktop, /test:e2e:renderer/); + assert.match(desktop, /test:e2e:browser-tabs/); assert.match(desktop, /npm run test --workspace=@getpaseo\/desktop/); assert.ok(!jobs.has("desktop-browser-bridge")); + assert.ok(!jobs.has("playwright-desktop")); }); test("server builds exclude test utilities at every domain depth", () => { @@ -140,13 +153,13 @@ test("server builds exclude test utilities at every domain depth", () => { test("PR routing follows test contracts instead of package consumers", () => { const filters = loadFilters(filtersPath); const cases = new Map([ - ["packages/app/src/components/message.tsx", ["app", "format", "playwright", "quality"]], + ["packages/app/src/components/message.tsx", ["app", "browser", "format", "quality"]], ["packages/server/src/server/bootstrap.ts", ["format", "quality", "server"]], ["packages/cli/src/commands/agent/ls.ts", ["cli", "format", "quality"]], - ["packages/desktop/src/main.ts", ["desktop", "desktop_bridge", "format", "quality"]], + ["packages/desktop/src/main.ts", ["desktop", "format", "quality"]], [ - "packages/app/src/components/browser-pane.electron.tsx", - ["app", "desktop_bridge", "format", "playwright", "playwright_desktop", "quality"], + "packages/app/src/desktop/browser/pane/index.electron.tsx", + ["app", "desktop", "format", "quality"], ], ["packages/cli/src/commands/hub/index.ts", ["cli", "format", "hub", "quality"]], [ @@ -176,26 +189,25 @@ test("tooling and domain contracts use stable ownership boundaries", () => { [".agents/skills/release-beta/SKILL.md", ["format"]], ["docker/docker-compose.example.yml", ["format"]], [ - "packages/app/e2e/helpers/project-picker-ui.ts", - ["app", "format", "playwright", "playwright_desktop", "quality"], + "packages/app/e2e/support/helpers/project-picker-ui.ts", + ["app", "browser", "desktop", "format", "quality"], ], [ - "packages/app/e2e/global-setup.ts", - ["app", "format", "playwright", "playwright_desktop", "quality"], + "packages/app/e2e/support/global-setup.ts", + ["app", "browser", "desktop", "format", "quality"], ], ["packages/desktop/src/daemon/runtime-paths.ts", ["desktop", "format", "quality"]], + ["packages/desktop/src/features/browser-profile.ts", ["desktop", "format", "quality"]], [ - "packages/desktop/src/features/browser-profile.ts", - ["desktop", "desktop_bridge", "format", "quality"], + "packages/server/src/server/browser-tools/broker.ts", + ["desktop", "format", "quality", "server"], ], [ - "packages/app/src/components/browser-webview-resident.ts", - ["app", "desktop_bridge", "format", "playwright", "quality"], - ], - [ - "packages/app/src/browser-automation/handler.ts", - ["app", "desktop_bridge", "format", "playwright", "quality"], + "packages/app/src/desktop/browser/resident-webviews.ts", + ["app", "desktop", "format", "quality"], ], + ["packages/app/e2e/browser/startup-loading.spec.ts", ["app", "browser", "format", "quality"]], + ["packages/desktop/e2e/startup.spec.ts", ["desktop", "format", "quality"]], ]); for (const [changedPath, expected] of cases) { @@ -203,6 +215,71 @@ test("tooling and domain contracts use stable ownership boundaries", () => { } }); +test("cross-package invariants live in the suite that owns them", () => { + const cliTests = filesUnder("packages/cli", (path) => path.endsWith(".test.ts")); + assert.ok(cliTests.length > 0); + for (const path of cliTests) { + assert.doesNotMatch( + readFileSync(new URL(path, repoRoot), "utf8"), + /server\/src\/server\/test-utils/, + path, + ); + } + + const protocolWireCompatibility = new URL( + "packages/protocol/src/messages.wire-compat.test.ts", + repoRoot, + ); + assert.match(readFileSync(protocolWireCompatibility, "utf8"), /wire schema compatibility/); +}); + +test("browser and desktop tests have exclusive, directory-owned suites", () => { + const filters = loadFilters(filtersPath); + const browserSpecs = filesUnder("packages/app/e2e", (path) => path.endsWith(".spec.ts")); + const desktopSpecs = filesUnder("packages/desktop/e2e", (path) => path.endsWith(".spec.ts")); + const electronModules = filesUnder("packages/app/src", (path) => /\.electron\.tsx?$/.test(path)); + + assert.ok(browserSpecs.length > 0); + assert.ok(desktopSpecs.length > 0); + assert.ok(browserSpecs.every((path) => path.startsWith("packages/app/e2e/browser/"))); + assert.ok(desktopSpecs.every((path) => path.startsWith("packages/desktop/e2e/"))); + assert.ok(electronModules.every((path) => path.startsWith("packages/app/src/desktop/"))); + + for (const path of browserSpecs) { + assert.equal(matchesFilter(filters, "browser", path), true, path); + assert.equal(matchesFilter(filters, "desktop", path), false, path); + assert.doesNotMatch( + readFileSync(new URL(path, repoRoot), "utf8"), + /paseoDesktop|injectDesktopBridge/, + ); + } + for (const path of desktopSpecs) { + assert.equal(matchesFilter(filters, "desktop", path), true, path); + assert.equal(matchesFilter(filters, "browser", path), false, path); + } + + const routingSource = readFileSync(filtersPath, "utf8"); + assert.doesNotMatch(routingSource, /desktop_bridge|playwright_desktop|browser-\*|browser-\*\//); + assert.deepEqual(filters.desktop, [ + "packages/desktop/**", + "packages/app/src/desktop/**", + "packages/server/src/server/browser-tools/**", + "packages/app/e2e/support/**", + "packages/app/*config.{cjs,js,ts}", + "packages/app/package.json", + ]); + assert.deepEqual(filters.browser, [ + "packages/app/src/!(desktop)/**", + "packages/app/e2e/browser/**", + "packages/app/e2e/support/**", + "packages/app/assets/**", + "packages/app/public/**", + "packages/app/index.ts", + "packages/app/*config.{cjs,js,ts}", + "packages/app/package.json", + ]); +}); + test("root dependency and CI infrastructure changes run every contract", () => { const filters = loadFilters(filtersPath); const allContracts = Object.keys(filters) diff --git a/scripts/daemon-launch-contract.test.mjs b/scripts/daemon-launch-contract.test.mjs index 3dd459fb1..54a000703 100644 --- a/scripts/daemon-launch-contract.test.mjs +++ b/scripts/daemon-launch-contract.test.mjs @@ -37,7 +37,7 @@ test("every executable daemon entrypoint enters the supervisor", async () => { nixPackage, ] = await Promise.all([ readFile(join(repoRoot, "packages/server/package.json"), "utf8"), - readFile(join(repoRoot, "packages/app/e2e/global-setup.ts"), "utf8"), + readFile(join(repoRoot, "packages/app/e2e/support/global-setup.ts"), "utf8"), readFile( join(repoRoot, "packages/server/src/server/daemon-e2e/connection-offer.e2e.test.ts"), "utf8",