Skip unrelated required checks without losing their names (#2542)

* fix(ci): skip unrelated required checks cleanly

Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable.

* fix(ci): gate changes to install patching script

* fix(ci): route cross-package test contracts narrowly

* fix(ci): keep routed contracts merge-blocking

* fix(server): exclude nested test utilities from builds

* fix(ci): cover app-owned native and browser domains

* refactor(ci): align tests with runtime domains

Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check.

* fix(ci): keep routing and desktop suites isolated

The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs.

* fix(ci): isolate desktop e2e runtime

* perf(test): isolate Playwright workers

* perf(test): parallelize Playwright within specs

* fix(test): keep Playwright file scheduling

* fix(test): share dynamic Metro fixture

* fix(test): preserve own-host restart fixtures

* fix(ci): preserve routed contracts after rebase

* Stabilize Playwright wheel-scroll assertions
This commit is contained in:
Mohamed Boudra
2026-08-02 00:56:05 +08:00
committed by GitHub
parent 429155b536
commit 5a30affd7f
256 changed files with 2693 additions and 2674 deletions
+71
View File
@@ -0,0 +1,71 @@
# Pull-request checks are routed by the behavior they prove. Package consumers
# do not inherit their dependencies' test suites; main and manual runs execute
# every contract regardless of these paths.
routing:
- ".github/ci-paths.yml"
workspace:
- ".mise.toml"
- ".tool-versions"
- "package.json"
- "package-lock.json"
- "patches/**"
- "scripts/**"
- "tsconfig.json"
- "tsconfig.base.json"
- "vitest.config.ts"
ci:
- ".github/actions/**"
- ".github/workflows/ci.yml"
format:
- ".agents/**/*.{cjs,css,html,js,json,jsonc,jsx,md,mjs,ts,tsx,yaml,yml}"
- ".github/**/*.{cjs,css,html,js,json,jsonc,jsx,md,mjs,ts,tsx,yaml,yml}"
- "**/*.{cjs,css,html,js,json,jsonc,jsx,md,mjs,ts,tsx,yaml,yml}"
- "packages/expo-two-way-audio/**"
quality:
- "**/*.{cjs,js,json,jsx,mjs,ts,tsx}"
- "packages/expo-two-way-audio/**"
hub:
- "packages/cli/src/commands/hub/**"
- "packages/server/src/server/hub/**"
server:
- "packages/server/**"
- "packages/app/e2e/support/fixtures/recording.*"
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"
app:
- "packages/app/**"
- "packages/expo-two-way-audio/**"
sdk:
- "packages/client/**"
- "packages/highlight/**"
- "packages/protocol/**"
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"
relay:
- "packages/relay/**"
cli:
- "packages/cli/**"
+180 -248
View File
@@ -19,110 +19,47 @@ env:
jobs:
changes:
name: changes
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
quality: ${{ steps.filter.outputs.shared != 'false' || steps.filter.outputs.quality != 'false' }}
server: ${{ steps.filter.outputs.shared != 'false' || steps.filter.outputs.server != 'false' }}
desktop: ${{ steps.filter.outputs.shared != 'false' || steps.filter.outputs.desktop != 'false' }}
desktop_package: ${{ steps.filter.outputs.shared != 'false' || steps.filter.outputs.desktop_package != 'false' }}
app: ${{ steps.filter.outputs.shared != 'false' || steps.filter.outputs.app != 'false' }}
sdk: ${{ steps.filter.outputs.shared != 'false' || steps.filter.outputs.sdk != 'false' }}
playwright: ${{ steps.filter.outputs.shared != 'false' || steps.filter.outputs.playwright != 'false' }}
relay: ${{ steps.filter.outputs.shared != 'false' || steps.filter.outputs.relay != 'false' }}
cli: ${{ steps.filter.outputs.shared != 'false' || steps.filter.outputs.cli != 'false' }}
full: ${{ github.event_name != 'pull_request' || steps.filter.outputs.routing != 'false' || steps.filter.outputs.workspace != 'false' || steps.filter.outputs.ci != 'false' }}
format: ${{ steps.filter.outputs.format }}
quality: ${{ steps.filter.outputs.quality }}
hub: ${{ steps.filter.outputs.hub }}
server: ${{ steps.filter.outputs.server }}
desktop: ${{ steps.filter.outputs.desktop }}
app: ${{ steps.filter.outputs.app }}
sdk: ${{ steps.filter.outputs.sdk }}
browser: ${{ steps.filter.outputs.browser }}
relay: ${{ steps.filter.outputs.relay }}
cli: ${{ steps.filter.outputs.cli }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
- name: Detect affected CI jobs
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Detect affected CI contracts
id: filter
uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3.0.3
with:
filters: |
shared:
- '.github/workflows/ci.yml'
- '.github/actions/**'
- '.mise.toml'
- '.tool-versions'
- 'package.json'
- 'package-lock.json'
- 'patches/**'
- 'scripts/**'
- 'tsconfig.json'
- 'tsconfig.base.json'
- 'vitest.config.ts'
quality:
- 'packages/**'
- '*.cjs'
- '*.js'
- '*.json'
- '*.mjs'
- '*.ts'
server:
- 'packages/app/e2e/fixtures/recording.*'
- 'packages/client/**'
- 'packages/cli/src/**'
- 'packages/highlight/**'
- 'packages/protocol/**'
- 'packages/relay/**'
- 'packages/server/**'
desktop:
- 'packages/app/**'
- 'packages/cli/**'
- 'packages/client/**'
- 'packages/desktop/**'
- 'packages/expo-two-way-audio/**'
- 'packages/highlight/**'
- 'packages/protocol/**'
- 'packages/relay/**'
- 'packages/server/**'
desktop_package:
- '.github/workflows/ci.yml'
- 'packages/desktop/**'
app:
- 'packages/app/**'
- 'packages/client/**'
- 'packages/expo-two-way-audio/**'
- 'packages/highlight/**'
- 'packages/protocol/**'
- 'packages/relay/**'
sdk:
- 'packages/client/**'
- 'packages/protocol/**'
- 'packages/relay/**'
playwright:
- 'packages/app/**'
- 'packages/client/**'
- 'packages/expo-two-way-audio/**'
- 'packages/highlight/**'
- 'packages/protocol/**'
- 'packages/relay/**'
- 'packages/server/**'
relay:
- 'packages/relay/**'
cli:
- 'nix/**'
- 'packages/app/e2e/global-setup.ts'
- 'packages/cli/**'
- 'packages/client/**'
- 'packages/desktop/src/daemon/runtime-paths.ts'
- 'packages/highlight/**'
- 'packages/protocol/**'
- 'packages/relay/**'
- 'packages/server/**'
filters: .github/ci-paths.yml
- name: Validate CI workflow
run: node --test scripts/ci-workflow.test.mjs
- name: Validate CI contracts
run: node --test scripts/ci-workflow.test.mjs scripts/daemon-launch-contract.test.mjs
format:
name: format
needs: changes
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.format != 'false') }}
runs-on: ubuntu-latest
env:
# This job never executes Electron. Skipping the hosted binary avoids
# unrelated npm ci failures when Electron's CDN returns 504.
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
steps:
- uses: actions/checkout@v4
@@ -136,15 +73,12 @@ jobs:
run: node scripts/npm-retry.mjs ci
- name: Check formatting
run: npx oxfmt --check .
run: npm run format:check
lint:
name: lint
needs: changes
if: >-
${{ !cancelled() &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.result != 'success' ||
needs.changes.outputs.quality != 'false') }}
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.quality != 'false') }}
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
@@ -169,12 +103,9 @@ jobs:
run: npm run lint
typecheck:
name: typecheck
needs: changes
if: >-
${{ !cancelled() &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.result != 'success' ||
needs.changes.outputs.quality != 'false') }}
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.quality != 'false') }}
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
@@ -188,6 +119,7 @@ jobs:
- name: Install dependencies
run: node scripts/npm-retry.mjs ci
- name: Build server stack
run: npm run build:server
@@ -200,130 +132,112 @@ jobs:
npm pack --dry-run --ignore-scripts --workspace=@getpaseo/client
npm pack --dry-run --ignore-scripts --workspace=@getpaseo/server
server-tests:
server-tests-ubuntu:
name: server-tests (ubuntu-latest)
needs: changes
if: ${{ !cancelled() }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
name: server-tests (${{ matrix.os }})
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.server != 'false' || needs.changes.outputs.hub != 'false') }}
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
RUN_TESTS: >-
${{ github.event_name == 'workflow_dispatch' ||
needs.changes.result != 'success' ||
needs.changes.outputs.server != 'false' }}
steps:
- name: Skip unaffected server tests
if: env.RUN_TESTS != 'true'
run: echo "No server changes detected."
steps: &server_test_steps
- uses: actions/checkout@v4
if: env.RUN_TESTS == 'true'
with:
fetch-depth: 0
- uses: actions/setup-node@v4
if: env.RUN_TESTS == 'true'
with:
node-version: "22"
cache: "npm"
- name: Fetch origin/main (worktree tests)
if: env.RUN_TESTS == 'true'
if: ${{ needs.changes.outputs.full != 'false' || needs.changes.outputs.server != 'false' }}
run: git fetch --no-tags origin main:refs/remotes/origin/main
- name: Install dependencies
if: env.RUN_TESTS == 'true'
run: node scripts/npm-retry.mjs ci
- name: Install agent CLIs for provider tests
if: env.RUN_TESTS == 'true'
if: ${{ needs.changes.outputs.full != 'false' || needs.changes.outputs.server != 'false' }}
run: node scripts/npm-retry.mjs install -g @anthropic-ai/claude-code opencode-ai
- name: Build server dependencies
if: env.RUN_TESTS == 'true'
run: npm run build:server-deps
- name: Run server tests
if: env.RUN_TESTS == 'true'
if: ${{ needs.changes.outputs.full != 'false' || needs.changes.outputs.server != 'false' }}
run: npm run test --workspace=@getpaseo/server
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
desktop-tests:
- name: Run Hub CLI/daemon contract
if: ${{ needs.changes.outputs.full == 'false' && needs.changes.outputs.server == 'false' && needs.changes.outputs.hub != 'false' }}
run: npm run test:hub-cli-contract --workspace=@getpaseo/server
server-tests-windows:
name: server-tests (windows-latest)
needs: changes
if: ${{ !cancelled() }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.server != 'false' || needs.changes.outputs.hub != 'false') }}
runs-on: windows-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
steps: *server_test_steps
desktop-tests-ubuntu:
name: desktop-tests (ubuntu-latest)
needs: changes
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop != 'false') }}
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
env:
RUN_TESTS: >-
${{ github.event_name == 'workflow_dispatch' ||
needs.changes.result != 'success' ||
needs.changes.outputs.desktop != 'false' }}
steps:
- name: Skip unaffected desktop tests
if: env.RUN_TESTS != 'true'
run: echo "No desktop changes detected."
steps: &desktop_test_steps
- uses: actions/checkout@v4
if: env.RUN_TESTS == 'true'
- uses: actions/setup-node@v4
if: env.RUN_TESTS == 'true'
with:
node-version: "22"
cache: "npm"
- name: Install dependencies with retry
if: env.RUN_TESTS == 'true'
run: node scripts/npm-retry.mjs ci
- name: Build server stack
if: env.RUN_TESTS == 'true'
run: npm run build:server
- name: Run desktop tests
if: env.RUN_TESTS == 'true'
run: npm run test --workspace=@getpaseo/desktop
- name: Build app dependencies for desktop E2E
if: env.RUN_TESTS == 'true' && matrix.os == 'ubuntu-latest'
run: npm run build:app-deps
- name: Install virtual display
if: env.RUN_TESTS == 'true' && matrix.os == 'ubuntu-latest'
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: env.RUN_TESTS == 'true' && matrix.os == 'ubuntu-latest'
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: env.RUN_TESTS == 'true' && failure() && matrix.os == 'ubuntu-latest'
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: >-
env.RUN_TESTS == 'true' && matrix.os == 'ubuntu-latest' &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.result != 'success' ||
needs.changes.outputs.desktop_package != 'false')
if: ${{ runner.os == 'Linux' }}
run: npm run build:desktop -- --publish never --linux --x64 --dir
env:
EP_GH_IGNORE_TIME: true
@@ -331,25 +245,28 @@ jobs:
PASEO_DESKTOP_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/desktop-smoke
- name: Upload packaged smoke diagnostics
if: >-
env.RUN_TESTS == 'true' && failure() && matrix.os == 'ubuntu-latest' &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.result != 'success' ||
needs.changes.outputs.desktop_package != 'false')
uses: actions/upload-artifact@v4
if: ${{ failure() && runner.os == 'Linux' }}
with:
name: desktop-packaged-smoke-linux-x64
path: ${{ runner.temp }}/desktop-smoke
if-no-files-found: ignore
retention-days: 7
app-tests:
desktop-tests-windows:
name: desktop-tests (windows-latest)
needs: changes
if: >-
${{ !cancelled() &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.result != 'success' ||
needs.changes.outputs.app != 'false') }}
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.desktop != 'false') }}
runs-on: windows-latest
timeout-minutes: 30
permissions:
contents: read
steps: *desktop_test_steps
app-tests:
name: app-tests
needs: changes
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.app != 'false') }}
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
@@ -363,6 +280,7 @@ jobs:
- name: Install dependencies with retry
run: node scripts/npm-retry.mjs ci
- name: Install Playwright browsers
timeout-minutes: 10
run: npx playwright install chromium
@@ -374,12 +292,9 @@ jobs:
run: npm run test --workspace=@getpaseo/app
sdk-tests:
name: sdk-tests
needs: changes
if: >-
${{ !cancelled() &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.result != 'success' ||
needs.changes.outputs.sdk != 'false') }}
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.sdk != 'false') }}
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
@@ -393,6 +308,7 @@ jobs:
- name: Install dependencies
run: node scripts/npm-retry.mjs ci
- name: Build client dependencies
run: npm run build:client
@@ -402,90 +318,97 @@ jobs:
- name: Run client tests
run: npm run test --workspace=@getpaseo/client
- name: Run highlight tests
run: npm run test --workspace=@getpaseo/highlight
- name: Typecheck client examples
run: npm run typecheck:examples --workspace=@getpaseo/client
playwright:
playwright-1:
name: playwright (shard 1/4)
needs: changes
if: ${{ !cancelled() }}
strategy:
fail-fast: false
matrix:
include:
- { label: "shard 1/4", shard: 1, desktop: false }
- { label: "shard 2/4", shard: 2, desktop: false }
- { label: "shard 3/4", shard: 3, desktop: false }
- { label: "shard 4/4", shard: 4, desktop: false }
- { label: "desktop overlay", shard: "desktop", desktop: true }
name: playwright (${{ matrix.label }})
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
RUN_TESTS: >-
${{ github.event_name == 'workflow_dispatch' ||
needs.changes.result != 'success' ||
needs.changes.outputs.playwright != 'false' }}
steps:
- name: Skip unaffected Playwright tests
if: env.RUN_TESTS != 'true'
run: echo "No Playwright changes detected."
PLAYWRIGHT_SHARD: "1/4"
PLAYWRIGHT_ARTIFACT: "1"
steps: &playwright_test_steps
- uses: actions/checkout@v4
if: env.RUN_TESTS == 'true'
- uses: actions/setup-node@v4
if: env.RUN_TESTS == 'true'
with:
node-version: "22"
cache: "npm"
- name: Install dependencies with retry
if: env.RUN_TESTS == 'true'
run: node scripts/npm-retry.mjs ci
- name: Install Playwright browsers
if: env.RUN_TESTS == 'true'
timeout-minutes: 10
run: npx playwright install chromium
- name: Build app dependencies
if: env.RUN_TESTS == 'true'
run: npm run build:app-deps
- name: Build server stack
if: env.RUN_TESTS == 'true'
run: npm run build:server
- name: Install agent CLIs for provider tests
if: env.RUN_TESTS == 'true' && !matrix.desktop
run: node scripts/npm-retry.mjs install -g @anthropic-ai/claude-code @openai/codex@0.105.0 opencode-ai
- name: Run Playwright E2E tests
if: env.RUN_TESTS == 'true' && !matrix.desktop
run: npm run test:e2e --workspace=@getpaseo/app -- --shard=${{ matrix.shard }}/4
run: npm run test:e2e --workspace=@getpaseo/app -- --shard=${{ env.PLAYWRIGHT_SHARD }}
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Run desktop-overlay Playwright tests
if: env.RUN_TESTS == 'true' && matrix.desktop
run: npm run test:e2e:desktop --workspace=@getpaseo/app
- name: Upload test artifacts
uses: actions/upload-artifact@v4
if: env.RUN_TESTS == 'true' && failure()
if: failure()
with:
name: playwright-results-${{ matrix.shard }}
name: playwright-results-${{ env.PLAYWRIGHT_ARTIFACT }}
path: |
packages/app/test-results/
packages/app/playwright-report/
retention-days: 7
relay-tests:
playwright-2:
name: playwright (shard 2/4)
needs: changes
if: >-
${{ !cancelled() &&
(github.event_name == 'workflow_dispatch' ||
needs.changes.result != 'success' ||
needs.changes.outputs.relay != 'false') }}
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
PLAYWRIGHT_SHARD: "2/4"
PLAYWRIGHT_ARTIFACT: "2"
steps: *playwright_test_steps
playwright-3:
name: playwright (shard 3/4)
needs: changes
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
PLAYWRIGHT_SHARD: "3/4"
PLAYWRIGHT_ARTIFACT: "3"
steps: *playwright_test_steps
playwright-4:
name: playwright (shard 4/4)
needs: changes
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.browser != 'false') }}
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
PLAYWRIGHT_SHARD: "4/4"
PLAYWRIGHT_ARTIFACT: "4"
steps: *playwright_test_steps
relay-tests:
name: relay-tests
needs: changes
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.relay != 'false') }}
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
@@ -506,53 +429,62 @@ jobs:
- name: Run relay tests
run: npm run test --workspace=@getpaseo/relay
cli-tests:
cli-tests-1:
name: cli-tests (shard 1/3)
needs: changes
if: ${{ !cancelled() }}
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.cli != 'false') }}
runs-on: ubuntu-latest
name: cli-tests (shard ${{ matrix.shard }}/3)
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
RUN_TESTS: >-
${{ github.event_name == 'workflow_dispatch' ||
needs.changes.result != 'success' ||
needs.changes.outputs.cli != 'false' }}
steps:
- name: Skip unaffected CLI tests
if: env.RUN_TESTS != 'true'
run: echo "No CLI changes detected."
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0"
PASEO_DICTATION_ENABLED: "0"
PASEO_VOICE_MODE_ENABLED: "0"
PASEO_CLI_TEST_SHARD: "1"
PASEO_CLI_TEST_SHARD_TOTAL: "3"
steps: &cli_test_steps
- uses: actions/checkout@v4
if: env.RUN_TESTS == 'true'
- uses: actions/setup-node@v4
if: env.RUN_TESTS == 'true'
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
if: env.RUN_TESTS == 'true'
run: node scripts/npm-retry.mjs ci
- name: Build server stack
if: env.RUN_TESTS == 'true'
run: npm run build:server
- name: Install agent CLIs for provider tests
if: env.RUN_TESTS == 'true'
run: node scripts/npm-retry.mjs install -g @anthropic-ai/claude-code @openai/codex@0.105.0 opencode-ai
- name: Run CLI tests
if: env.RUN_TESTS == 'true'
run: npm run test --workspace=@getpaseo/cli
env:
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0"
PASEO_DICTATION_ENABLED: "0"
PASEO_VOICE_MODE_ENABLED: "0"
PASEO_CLI_TEST_SHARD: ${{ matrix.shard }}
PASEO_CLI_TEST_SHARD_TOTAL: "3"
cli-tests-2:
name: cli-tests (shard 2/3)
needs: changes
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.cli != 'false') }}
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0"
PASEO_DICTATION_ENABLED: "0"
PASEO_VOICE_MODE_ENABLED: "0"
PASEO_CLI_TEST_SHARD: "2"
PASEO_CLI_TEST_SHARD_TOTAL: "3"
steps: *cli_test_steps
cli-tests-3:
name: cli-tests (shard 3/3)
needs: changes
if: ${{ !cancelled() && (needs.changes.outputs.full != 'false' || needs.changes.outputs.cli != 'false') }}
runs-on: ubuntu-latest
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0"
PASEO_DICTATION_ENABLED: "0"
PASEO_VOICE_MODE_ENABLED: "0"
PASEO_CLI_TEST_SHARD: "3"
PASEO_CLI_TEST_SHARD_TOTAL: "3"
steps: *cli_test_steps
+4 -4
View File
@@ -188,9 +188,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",
@@ -279,7 +279,7 @@
}
},
{
"files": ["**/e2e/fixtures.ts"],
"files": ["**/e2e/**/fixtures.ts"],
"rules": {
"no-empty-pattern": "off"
}
+5 -5
View File
@@ -162,12 +162,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 <webview> implementation
browser-pane.web.tsx ← plain web fallback
browser-pane.tsx ← native fallback
desktop/browser/pane/
index.electron.tsx ← Electron <webview> 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.
+1 -1
View File
@@ -30,7 +30,7 @@ Terminal frames share the daemon main event loop with all agent traffic. The `ev
- **Node-only benchmark (fast iteration, server pipeline):** `npx tsx scripts/benchmark-terminal-latency.ts`. Boots an isolated daemon (fresh `PASEO_HOME`, random port — never 6767), measures echo latency percentiles, burst jitter, and snapshot counts under ramped mock-agent load. Writes JSON to `/tmp/paseo-terminal-bench/`. Healthy numbers (2026-06): echo p50 ~2.3ms, p95 ~3.3ms, a 2MB burst fully streamed with `snap=0`.
- **Browser perf specs (user-perceived path):** gated behind `PASEO_TERMINAL_PERF_E2E=1` —
`packages/app/e2e/terminal-performance.spec.ts` and `packages/app/e2e/terminal-keystroke-stress.spec.ts` (per-stage keydown→xterm-commit breakdown under mock-agent load). Healthy: keydown→commit p50 ~18ms under 600-key burst.
`packages/app/e2e/browser/terminal-performance.spec.ts` and `packages/app/e2e/browser/terminal-keystroke-stress.spec.ts` (per-stage keydown→xterm-commit breakdown under mock-agent load). Healthy: keydown→commit p50 ~18ms under 600-key burst.
- **Production:** grep `daemon.log` for `ws_runtime_metrics` and read `eventLoopDelay` + `bufferedAmount`.
- **Git pressure:** the same log line includes `git.commands` (limiter occupancy, queue age,
queue wait, execution time, failures, timeouts, and top operations),
+17 -8
View File
@@ -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 leg of the existing `desktop-tests` CI check:
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.
@@ -175,9 +175,18 @@ Test suites in this repo are heavy. Running them in bulk freezes the machine, es
- Never re-run a suite another agent already reported green.
- For full-suite confidence, push to CI and check GitHub Actions.
- Never run the full Playwright E2E suite locally — defer whole-suite verification to CI. Targeted Playwright specs are allowed when you changed or need to prove that specific flow.
- App Playwright specs share one isolated daemon per run. Helpers that create projects or workspaces must remove the daemon project record during cleanup, not only delete the temp directory. Agent helpers must pass the intended `workspaceId` through to agent creation; never infer ownership from `cwd`.
- CI can shard app Playwright across multiple jobs; each shard still owns a full isolated daemon/relay/Metro stack from global setup. Helpers that restart the daemon must preserve the global setup environment, including disabled speech/local-model settings, so a restart does not change the tested surface or start background downloads.
- Global setup starts Metro before Wrangler, assigns Wrangler explicit distinct relay and inspector ports, and accepts Metro as ready only when `/status` returns `packager-status:running`. A generic TCP listener is not sufficient readiness evidence.
- App Playwright shares one warmed Metro server per run and gives every Playwright worker its own isolated daemon and `PASEO_HOME`. Spec files run concurrently without exposing one file's projects, agents, terminals, history, or provider configuration to another worker; tests within a file remain together so file-level setup is not repeated.
- Helpers that create projects or workspaces own those records until cleanup. Their clients remove the daemon project on close, and an automatic fixture fails any test that still leaks a project record. Deleting only the temporary directory is not cleanup. Agent helpers pass the intended `workspaceId` through to agent creation; they never infer ownership from `cwd`.
- Tests whose subject is daemon-global state, such as an empty history or daemon restart, start a dedicated host explicitly. Filenames and directories describe product behavior, never execution order or isolation mechanics.
- Global setup accepts Metro as ready only when `/status` returns `packager-status:running`, then fetches the document's scripts so the cold bundle compilation finishes before Playwright's per-test timeout starts. A generic TCP listener is not sufficient readiness evidence. The browser suite uses direct local daemon connections and does not start a relay.
## Pull-request test routing
PR checks are routed by the behavior each suite proves, using `.github/ci-paths.yml`. A package does not inherit every test suite of its runtime consumers: app changes do not run CLI or Electron-wrapper tests, and protocol changes do not run every package that imports the protocol. Cross-package static compatibility belongs to `typecheck`; full integration coverage runs after merge on main and in manual CI runs.
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. 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
@@ -1,34 +0,0 @@
// "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";
test.describe("Sessions screen empty state", () => {
test.beforeAll(async () => {
const client = await connectSeedClient();
try {
const history = await client.fetchAgentHistory({ page: { limit: 1 } });
if (history.entries.length > 0) {
throw new Error(
`Sessions empty-state precondition failed: daemon already has ${history.entries.length} agent(s). ` +
`Either a spec that sorts before 00-sessions-empty.spec.ts created agents, ` +
`or the daemon has stale history from a previous run.`,
);
}
} finally {
await client.close().catch(() => undefined);
}
});
test("shows empty placeholder when there is no session history", async ({
page,
withWorkspace,
}) => {
const workspace = await withWorkspace({ prefix: "sessions-empty-" });
await workspace.navigateTo();
await openSessions(page);
await expectSessionsEmptyState(page);
});
});
@@ -1,29 +0,0 @@
import { test } from "./fixtures";
import { gotoAppShell, openSettings } from "./helpers/app";
import { getServerId } from "./helpers/server-id";
import {
expectProviderInstalledInSettings,
installAcpCatalogProvider,
openAddProviderArea,
openSettingsHost,
openSettingsHostSection,
} from "./helpers/settings";
const ACP_PROVIDER = {
id: "hermes",
name: "Hermes",
};
test.describe("ACP provider catalog", () => {
test("adds a catalog provider from settings", async ({ page }) => {
await gotoAppShell(page);
await openSettings(page);
await openSettingsHost(page, getServerId());
// Providers moved to their own host section; add-provider lives there now.
await openSettingsHostSection(page, getServerId(), "providers");
await openAddProviderArea(page);
await installAcpCatalogProvider(page, ACP_PROVIDER.name);
await expectProviderInstalledInSettings(page, ACP_PROVIDER.name);
});
});
@@ -0,0 +1,43 @@
import { test } 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 "../support/helpers/settings";
const ACP_PROVIDER = {
id: "hermes",
name: "Hermes",
};
interface ProviderCatalogDaemonClient {
connect(): Promise<void>;
close(): Promise<void>;
patchDaemonConfig(config: { removeProviders?: string[] }): Promise<unknown>;
}
test.describe("ACP provider catalog", () => {
test("adds a catalog provider from settings", async ({ page }) => {
const client = await connectDaemonClient<ProviderCatalogDaemonClient>({
clientIdPrefix: "provider-catalog-e2e",
});
try {
await gotoAppShell(page);
await openSettings(page);
await openSettingsHost(page, getServerId());
await openSettingsHostSection(page, getServerId(), "providers");
await openAddProviderArea(page);
await installAcpCatalogProvider(page, ACP_PROVIDER.name);
await expectProviderInstalledInSettings(page, ACP_PROVIDER.name);
} finally {
await client.patchDaemonConfig({ removeProviders: [ACP_PROVIDER.id] }).catch(() => undefined);
await client.close().catch(() => undefined);
}
});
});
@@ -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();
@@ -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 } from "./fixtures";
import { test, expect } from "../support/fixtures";
import {
addProjectFlow,
addProjectFlowBack,
@@ -13,17 +13,20 @@ import {
expectAddProjectPage,
expectNewWorkspaceForAddedProject,
openAddProjectFlow,
} from "./helpers/add-project-flow";
import { gotoAppShell } from "./helpers/app";
} from "../support/helpers/add-project-flow";
import { gotoAppShell } from "../support/helpers/app";
import {
addConnectedHostAndReload,
addOfflineHostAndReload,
waitForConnectedHost,
} 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/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 SECONDARY_HOST_ID = "add-project-flow-secondary";
const SECONDARY_HOST_LABEL = "Secondary Host";
@@ -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 });
@@ -1,20 +1,20 @@
import type { Page } from "@playwright/test";
import { expect, test as baseTest } from "./fixtures";
import { expectAgentIdle } from "./helpers/agent-stream";
import { expect, test as baseTest } from "../support/fixtures";
import { expectAgentIdle } from "../support/helpers/agent-stream";
import {
attachImageFromMenu,
expectAttachmentPill,
expectComposerVisible,
submitMessage,
} from "./helpers/composer";
import { clickNewChat, gotoWorkspace } from "./helpers/launcher";
import { seedWorkspace } from "./helpers/seed-client";
} from "../support/helpers/composer";
import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher";
import { seedWorkspace } from "../support/helpers/seed-client";
import {
openAgentRoute,
seedMockAgentWorkspace,
type MockAgentWorkspace,
} from "./helpers/mock-agent";
import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate";
} from "../support/helpers/mock-agent";
import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
interface FrameRect {
top: number;
@@ -1,13 +1,13 @@
import type { Locator } from "@playwright/test";
import { expect, test, type Page } from "./fixtures";
import { expectAgentIdle, expectAgentReadyToInterrupt } from "./helpers/agent-stream";
import { expect, test, type Page } from "../support/fixtures";
import { expectAgentIdle, expectAgentReadyToInterrupt } from "../support/helpers/agent-stream";
import {
cancelAgent,
expectComposerDraft,
expectComposerVisible,
submitMessage,
} from "./helpers/composer";
import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent";
} from "../support/helpers/composer";
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
async function completeSubmittedTurn(
page: Page,
@@ -1,5 +1,5 @@
import type { Locator, Page } from "@playwright/test";
import { expect, test as baseTest } from "./fixtures";
import { expect, test as baseTest } from "../support/fixtures";
import {
awaitToolCall,
expectAgentIdle,
@@ -7,8 +7,8 @@ import {
expectAgentSurfacesIdle,
expectRunningAgentChrome,
expectVisibleAgentSurfacesIdle,
} from "./helpers/agent-stream";
import { gateNextAgentMessage } from "./helpers/agent-message-gate";
} from "../support/helpers/agent-stream";
import { gateNextAgentMessage } from "../support/helpers/agent-message-gate";
import {
attachImageFromMenu,
expectComposerDraft,
@@ -21,26 +21,26 @@ import {
sendDraftToQueue,
startRunningMockAgent,
submitMessage,
} from "./helpers/composer";
import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent";
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 { 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";
import {
scrollTimelineToNewestLoadedEdge,
scrollTimelineUntilOlderHistoryIsReachable,
} from "./helpers/timeline-pagination";
} from "../support/helpers/timeline-pagination";
import {
appendSettledTimelineTurns,
createSettledMockAgent,
createSmallAssistantPng,
emitSettledAssistantImage,
expectAssistantImageRendered,
} from "./helpers/assistant-images";
} from "../support/helpers/assistant-images";
const IMAGE = {
name: "message-submission.png",
@@ -1,4 +1,4 @@
import { test, expect } from "./fixtures";
import { test, expect } from "../support/fixtures";
import {
awaitAssistantMessage,
expectAgentIdle,
@@ -6,7 +6,7 @@ import {
expectRunningAgentChrome,
expectTurnCopyButton,
expectScrollFollowsNewContent,
} from "./helpers/agent-stream";
} from "../support/helpers/agent-stream";
import {
expectScrollStaysFixed,
clickToolCallBesideScrollToBottomButton,
@@ -14,16 +14,16 @@ 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";
} 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,
seedRunningMockAgentWorkspace,
} from "./helpers/mock-agent";
} from "../support/helpers/mock-agent";
const SCROLL_AWAY_MIN_SCROLLABLE_DISTANCE = 360;
@@ -1,4 +1,4 @@
import { test as base } from "./fixtures";
import { test as base } from "../support/fixtures";
import {
appendSettledTimelineTurns,
createNearTenMegabyteAssistantPng,
@@ -13,8 +13,8 @@ import {
sendFollowUpAndExpectVisibleResponse,
switchAwayAndBackWithoutImageInstability,
userPagesUntilAssistantImageRenders,
} from "./helpers/assistant-images";
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
} from "../support/helpers/assistant-images";
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
const test = base.extend<{ imageWorkspace: SeededWorkspace }>({
imageWorkspace: async ({ page: _page }, provide) => {
@@ -1,4 +1,4 @@
import { expect, test } from "./fixtures";
import { expect, test } from "../support/fixtures";
import {
expectSameOlderHistoryLoadingOperation,
expectStableHistoryStartGutter,
@@ -26,7 +26,7 @@ import {
expectTimelineViewportAnchoredAfterPrepend,
userNavigatesTimelineToHistoryStartWithKeyboard,
userScrollsTimelineToHistoryStart,
} from "./helpers/timeline-pagination";
} from "../support/helpers/timeline-pagination";
test.describe("Agent timeline pagination", () => {
test("keeps a fixed history-start gutter before, during, and after pagination", async ({
@@ -1,5 +1,5 @@
import { test } from "./fixtures";
import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate";
import { test } from "../support/fixtures";
import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
import {
expectTimelinePresentationUnchanged,
expectTimelinePromptNotMounted,
@@ -9,14 +9,14 @@ import {
scrollTimelinePromptIntoView,
scrollTimelineUntilOlderHistoryIsReachable,
seedLongMockAgentTimeline,
} from "./helpers/timeline-pagination";
} from "../support/helpers/timeline-pagination";
import {
commitTimelineTurnsWhileDisconnected,
disconnectViewedTimeline,
expectOneTailCheckWithoutCatchUpPages,
rememberTimelineRequestCounts,
restoreViewedTimelineWithHeldTail,
} from "./helpers/timeline-resume";
} from "../support/helpers/timeline-resume";
test.describe("Agent timeline resume", () => {
test("a long disconnected run lands as one latest-tail update", async ({ page }) => {
@@ -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<ReturnType<typeof connectSeedClient>>;
@@ -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(
@@ -1,5 +1,5 @@
import { expect, test as base } from "./fixtures";
import { awaitAssistantMessage } from "./helpers/agent-stream";
import { expect, test as base } from "../support/fixtures";
import { awaitAssistantMessage } from "../support/helpers/agent-stream";
import {
expectChatHistoryAttachment,
expectInFlightForkAvailable,
@@ -8,18 +8,18 @@ import {
forkMostRecentAssistantTurnToNewTab,
forkMostRecentAssistantTurnToNewWorkspace,
observeForkAttachment,
} from "./helpers/assistant-fork";
import { expectComposerVisible, submitMessage } from "./helpers/composer";
import { getE2EDaemonPort } from "./helpers/daemon-port";
} from "../support/helpers/assistant-fork";
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<MockAgentWorkspace>;
@@ -1,15 +1,25 @@
import { existsSync } from "node:fs";
import { expect, test, type Page } from "./fixtures";
import { gotoAppShell } from "./helpers/app";
import { clickSessionRow, expectSessionRowArchived, openSessions } from "./helpers/archive-tab";
import { cloneGithubRepoDefaultBranchOnly, createTempGithubRepo } from "./helpers/github-fixtures";
import { expect, test, type Page } from "../support/fixtures";
import { gotoAppShell } from "../support/helpers/app";
import {
clickSessionRow,
expectSessionRowArchived,
openSessions,
} from "../support/helpers/archive-tab";
import {
cloneGithubRepoDefaultBranchOnly,
createTempGithubRepo,
} from "../support/helpers/github-fixtures";
import {
archiveWorkspaceFromDaemon,
connectNewWorkspaceDaemonClient,
} from "./helpers/new-workspace";
import { connectSeedClient } from "./helpers/seed-client";
import { getServerId } from "./helpers/server-id";
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 {
waitForSidebarHydration,
waitForWorkspaceInSidebar,
} from "../support/helpers/workspace-ui";
test.describe("Auto-archive after pull request merge", () => {
test.describe.configure({ retries: 0, timeout: 180_000 });
@@ -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 };
@@ -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,
@@ -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;
@@ -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 ({
@@ -1,5 +1,5 @@
import { test } from "./fixtures";
import { closeCommandCenter, openCommandCenter } from "./helpers/command-center";
import { test } from "../support/fixtures";
import { closeCommandCenter, openCommandCenter } from "../support/helpers/command-center";
import {
applyCommandCenterAgentControls,
chooseCommandCenterAgentControl,
@@ -8,11 +8,11 @@ import {
expectWorkspaceAgentConfiguration,
submitDraftAgent,
waitForDraftComposer,
} from "./helpers/command-center-agent-controls";
import { clickNewChat, gotoWorkspace } from "./helpers/launcher";
import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent";
import { seedWorkspace } from "./helpers/seed-client";
import { getServerId } from "./helpers/server-id";
} from "../support/helpers/command-center-agent-controls";
import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher";
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
import { seedWorkspace } from "../support/helpers/seed-client";
import { getServerId } from "../support/helpers/server-id";
const CREATE_AGENT_PREFERENCES_KEY = "@paseo:create-agent-preferences";
@@ -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";
@@ -1,5 +1,5 @@
import { test } from "./fixtures";
import { gotoAppShell } from "./helpers/app";
import { test } from "../support/fixtures";
import { gotoAppShell } from "../support/helpers/app";
import {
expectCommandCenterToRemainAtTheTop,
expectPrimaryCommandCenterActions,
@@ -8,8 +8,8 @@ import {
expectWorkspaceResultsToBeLimitedUntilSearch,
observeCommandCenterScroll,
openCommandCenterWithKeyboard,
} from "./helpers/command-center-scroll";
import { seedWorkspace } from "./helpers/seed-client";
} from "../support/helpers/command-center-scroll";
import { seedWorkspace } from "../support/helpers/seed-client";
test.use({
userAgent:
@@ -1,14 +1,14 @@
import { test } from "./fixtures";
import { gotoAppShell } from "./helpers/app";
import { openCommandCenter } from "./helpers/command-center";
import { test } from "../support/fixtures";
import { gotoAppShell } from "../support/helpers/app";
import { openCommandCenter } from "../support/helpers/command-center";
import {
createTerminalFromCommandCenter,
expectDefaultWorkspaceActions,
expectWorkspaceActionsAvailableThroughSearch,
makeWorkspacePrimaryGitActionCommit,
openWorkspaceFromCommandCenter,
} from "./helpers/command-center-workspace-actions";
import { seedWorkspace } from "./helpers/seed-client";
} from "../support/helpers/command-center-workspace-actions";
import { seedWorkspace } from "../support/helpers/seed-client";
test("workspace actions use the Git policy and dispatch through the active workspace", async ({
page,
@@ -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";
@@ -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";
@@ -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,
@@ -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 = [
{
@@ -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;
@@ -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 ({
@@ -1,18 +1,18 @@
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 { projectEquivalenceViewKey } from "./helpers/project-view-key";
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 { projectEquivalenceViewKey } from "../support/helpers/project-view-key";
import { createTempGitRepo } from "../support/helpers/workspace";
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
function workspaceRowTestId(workspaceId: string): string {
return `sidebar-workspace-row-${getServerId()}:${workspaceId}`;
@@ -1,14 +1,18 @@
import { mkdir, readFile, rename, rm, unlink, writeFile } from "node:fs/promises";
import path from "node:path";
import { expect, test, type Page } from "./fixtures";
import { expectFileTabOpen, openFileExplorer, openFileFromExplorer } from "./helpers/file-explorer";
import type { WithWorkspace } from "./helpers/with-workspace";
import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate";
import { expect, test, type Page } from "../support/fixtures";
import {
expectFileTabOpen,
openFileExplorer,
openFileFromExplorer,
} from "../support/helpers/file-explorer";
import type { WithWorkspace } from "../support/helpers/with-workspace";
import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
import {
expectFileCalloutWasRendered,
expectNoFileCalloutWasRendered,
recordFileCallouts,
} from "./helpers/file-callouts";
} from "../support/helpers/file-callouts";
function visibleEditor(page: Page) {
return page.getByTestId("file-source-editor").filter({ visible: true }).locator(".cm-content");
@@ -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=",
@@ -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;
@@ -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;
@@ -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 ──────────────────────────────────────────────────────────
@@ -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";
@@ -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.
@@ -1,9 +1,12 @@
import { expect, test, type Page } from "./fixtures";
import { gotoAppShell } from "./helpers/app";
import { daemonWsRoutePattern } from "./helpers/daemon-port";
import { openNewWorkspaceComposer, selectWorkspaceIsolation } from "./helpers/new-workspace";
import { seedWorkspace } from "./helpers/seed-client";
import { waitForSidebarHydration } from "./helpers/workspace-ui";
import { expect, test, type Page } from "../support/fixtures";
import { gotoAppShell } from "../support/helpers/app";
import { daemonWsRoutePattern } from "../support/helpers/daemon-port";
import {
openNewWorkspaceComposer,
selectWorkspaceIsolation,
} from "../support/helpers/new-workspace";
import { seedWorkspace } from "../support/helpers/seed-client";
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
type WebSocketMessage = string | Buffer;
@@ -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 {
connectNewWorkspaceDaemonClient,
expectNewWorkspaceControlsEnabled,
@@ -8,14 +8,17 @@ import {
openMissingProjectNewWorkspaceComposer,
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 { projectEquivalenceViewKey } from "./helpers/project-view-key";
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 { projectEquivalenceViewKey } from "../support/helpers/project-view-key";
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
@@ -1,17 +1,20 @@
import { expect, test as base } from "./fixtures";
import { openProjectDirectoryWithHosts } from "./helpers/project-grouping";
import { projectPlacementViewKey } from "./helpers/project-view-key";
import { type IsolatedHostDaemon, startIsolatedHostDaemon } from "./helpers/isolated-host-daemon";
import { expect, test as base } from "../support/fixtures";
import { openProjectDirectoryWithHosts } from "../support/helpers/project-grouping";
import { projectPlacementViewKey } from "../support/helpers/project-view-key";
import {
type IsolatedHostDaemon,
startIsolatedHostDaemon,
} from "../support/helpers/isolated-host-daemon";
import {
expectNewWorkspaceProjectSelected,
openGlobalNewWorkspaceComposer,
selectNewWorkspaceHost,
selectNewWorkspaceProject,
} from "./helpers/new-workspace";
import { connectSeedClient, type SeedDaemonClient } from "./helpers/seed-client";
import { getServerId } from "./helpers/server-id";
import { switchWorkspaceViaSidebar } from "./helpers/workspace-ui";
import { createTempGitRepo } from "./helpers/workspace";
} from "../support/helpers/new-workspace";
import { connectSeedClient, type SeedDaemonClient } from "../support/helpers/seed-client";
import { getServerId } from "../support/helpers/server-id";
import { switchWorkspaceViaSidebar } from "../support/helpers/workspace-ui";
import { createTempGitRepo } from "../support/helpers/workspace";
const PRIMARY_HOST_LABEL = "Primary host";
const SECONDARY_HOST_LABEL = "Secondary host";
@@ -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
@@ -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";
@@ -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",
@@ -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;
@@ -1,9 +1,9 @@
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 { expect, test } from "../support/fixtures";
import { submitMessage } from "../support/helpers/composer";
import { cleanupRewindFlow, launchAgent, type AgentHandle } from "../support/helpers/rewind-flow";
test.describe("Codex out-of-band commands", () => {
test.setTimeout(300_000);
@@ -0,0 +1,20 @@
import { test } from "../support/fixtures";
import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon";
import {
expectRelayConsent,
openPairDeviceModal,
preparePairingHost,
} from "../support/helpers/pair-device";
test("opens relay consent in browser web", async ({ page }) => {
const daemon = await startIsolatedHostDaemon("pair-device-browser-relay-off", {
mutableRelay: { enabled: false },
});
try {
await preparePairingHost(page, daemon);
await openPairDeviceModal(page);
await expectRelayConsent(page);
} finally {
await daemon.close();
}
});
@@ -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();
@@ -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,
@@ -40,14 +40,14 @@ import {
returnToProjectsList,
saveProjectEdits,
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"];
@@ -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",
@@ -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 };
@@ -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;
@@ -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 }) => {
@@ -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 };
@@ -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?";
@@ -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";
@@ -1,15 +1,15 @@
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 { scrollChatAwayFromBottom } from "./helpers/agent-bottom-anchor";
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 { scrollChatAwayFromBottom } from "../support/helpers/agent-bottom-anchor";
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`.
@@ -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:
@@ -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: {
@@ -1,18 +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 {
addFakeScheduleHostAndReload,
buildFakeScheduleHostWorkspace,
installFakeScheduleHost,
} from "./helpers/schedule-fake-host";
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
import { getServerId } from "./helpers/server-id";
import { projectEquivalenceViewKey } from "./helpers/project-view-key";
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 { projectEquivalenceViewKey } from "../support/helpers/project-view-key";
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 };
@@ -0,0 +1,44 @@
import { randomUUID } from "node:crypto";
import { metroTest as test } from "../support/fixtures";
import { expectSessionsEmptyState, openSessions } from "../support/helpers/archive-tab";
import { gotoAppShell } from "../support/helpers/app";
import { buildCreateAgentPreferences, buildSeededHost } from "../support/helpers/daemon-registry";
import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon";
import { seedWorkspace } from "../support/helpers/seed-client";
test("Sessions shows an empty placeholder when the host has no history", async ({ page }) => {
const serverId = `srv_sessions_empty_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
const daemon = await startIsolatedHostDaemon(serverId);
let workspace: Awaited<ReturnType<typeof seedWorkspace>> | null = null;
try {
workspace = await seedWorkspace({
repoPrefix: "sessions-empty-",
port: daemon.port,
});
const host = buildSeededHost({
serverId,
endpoint: `127.0.0.1:${daemon.port}`,
nowIso: new Date().toISOString(),
});
await page.route(/:6767\b/, (route) => route.abort());
await page.routeWebSocket(/:6767\b/, async (webSocket) => {
await webSocket.close({ code: 1008, reason: "Blocked developer daemon during e2e." });
});
await page.addInitScript(
({ seededHost, preferences }) => {
localStorage.setItem("@paseo:e2e", "1");
localStorage.setItem("@paseo:daemon-registry", JSON.stringify([seededHost]));
localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(preferences));
},
{ seededHost: host, preferences: buildCreateAgentPreferences(serverId) },
);
await gotoAppShell(page);
await openSessions(page);
await expectSessionsEmptyState(page);
} finally {
await workspace?.cleanup();
await daemon.close();
}
});
@@ -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 {
expectHostNoDaemonLifecycleRow,
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);
});
});
@@ -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);
@@ -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,
@@ -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";
@@ -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)(?:[/?#]|$)/;
@@ -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";
@@ -1,12 +1,12 @@
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 { projectEquivalenceViewKey } from "./helpers/project-view-key";
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 { projectEquivalenceViewKey } from "../support/helpers/project-view-key";
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
@@ -1,6 +1,6 @@
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { test as base } from "./fixtures";
import { test as base } from "../support/fixtures";
import {
beginWorkspaceFromProject,
createWorkspaceWithoutAgent,
@@ -14,11 +14,14 @@ import {
openProjectDirectoryWithHosts,
renameProject,
selectWorkspaceHost,
} from "./helpers/project-grouping";
import { type IsolatedHostDaemon, startIsolatedHostDaemon } from "./helpers/isolated-host-daemon";
import { connectSeedClient, type SeedDaemonClient } from "./helpers/seed-client";
import { getServerId } from "./helpers/server-id";
import { createTempGitRepo } from "./helpers/workspace";
} from "../support/helpers/project-grouping";
import {
type IsolatedHostDaemon,
startIsolatedHostDaemon,
} from "../support/helpers/isolated-host-daemon";
import { connectSeedClient, type SeedDaemonClient } from "../support/helpers/seed-client";
import { getServerId } from "../support/helpers/server-id";
import { createTempGitRepo } from "../support/helpers/workspace";
const PRIMARY_HOST_LABEL = "Primary Host";
const SECONDARY_HOST_LABEL = "Secondary Host";
@@ -139,8 +142,14 @@ const test = base.extend<{
const secondaryRepo = await createTempGitRepo("grouped-legacy-secondary-", {
originUrl: SHARED_REMOTE_URL,
});
const primaryClient = await connectSeedClient({ port: primaryHost.port });
const secondaryClient = await connectSeedClient({ port: secondaryHost.port });
const primaryClient = await connectSeedClient({
port: primaryHost.port,
projectOwnership: "host",
});
const secondaryClient = await connectSeedClient({
port: secondaryHost.port,
projectOwnership: "host",
});
try {
await createProject(primaryClient, {
@@ -1,10 +1,10 @@
import type { Locator } from "@playwright/test";
import { expect, test } from "./fixtures";
import { gotoAppShell } from "./helpers/app";
import { projectEquivalenceViewKey } from "./helpers/project-view-key";
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 { projectEquivalenceViewKey } from "../support/helpers/project-view-key";
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) =>
@@ -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 } });
@@ -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
@@ -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}`;
@@ -1,18 +1,18 @@
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 { projectEquivalenceViewKey } from "./helpers/project-view-key";
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 { projectEquivalenceViewKey } from "../support/helpers/project-view-key";
import { escapeRegex } from "../support/helpers/regex";
const GITHUB_REMOTE_URL = "https://github.com/test-owner/test-repo.git";
@@ -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();
});
});
@@ -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";
@@ -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;
@@ -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";
@@ -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;
@@ -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;
@@ -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;
@@ -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);
@@ -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;
@@ -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
@@ -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;
@@ -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<void> {
await expect(page.getByTestId("user-message")).toHaveCount(expected, { timeout: 15_000 });
@@ -1,20 +1,20 @@
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 {
expectAgentIdle,
expectInlineWorkingIndicator,
expectTurnCopyButton,
} from "./helpers/agent-stream";
} from "../support/helpers/agent-stream";
import {
expectReconnectingToastGone,
expectReconnectingToastVisible,
} from "./helpers/workspace-ui";
} from "../support/helpers/workspace-ui";
interface ViewedTimelineScenario {
client: SeedDaemonClient;
@@ -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));
@@ -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(

Some files were not shown because too many files have changed in this diff Show More