mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Compare commits
9 Commits
main
...
fix-ci-con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b37aa4077 | ||
|
|
1c2cb89a42 | ||
|
|
61409dfe9c | ||
|
|
cec40f6616 | ||
|
|
11973905c9 | ||
|
|
93866a8c4f | ||
|
|
7d1d28e4ab | ||
|
|
b58507e4b0 | ||
|
|
3b29439e7b |
71
.github/ci-paths.yml
vendored
Normal file
71
.github/ci-paths.yml
vendored
Normal 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/**"
|
||||
429
.github/workflows/ci.yml
vendored
429
.github/workflows/ci.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -19,110 +18,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 +72,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 +102,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 +118,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: node scripts/npm-retry.mjs ci
|
||||
|
||||
- name: Build server stack
|
||||
run: npm run build:server
|
||||
|
||||
@@ -200,130 +131,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 +244,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 +279,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 +291,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 +307,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: node scripts/npm-retry.mjs ci
|
||||
|
||||
- name: Build client dependencies
|
||||
run: npm run build:client
|
||||
|
||||
@@ -402,90 +317,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 +428,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
|
||||
|
||||
@@ -189,9 +189,9 @@
|
||||
"packages/app/src/components/agent-list.tsx",
|
||||
"packages/app/src/components/agent-status-dot.tsx",
|
||||
"packages/app/src/components/attachment-lightbox.tsx",
|
||||
"packages/app/src/components/browser-pane.electron.tsx",
|
||||
"packages/app/src/components/browser-pane.tsx",
|
||||
"packages/app/src/components/browser-pane.web.tsx",
|
||||
"packages/app/src/desktop/browser/pane/index.electron.tsx",
|
||||
"packages/app/src/desktop/browser/pane/index.tsx",
|
||||
"packages/app/src/desktop/browser/pane/index.web.tsx",
|
||||
"packages/app/src/components/command-center.tsx",
|
||||
"packages/app/src/components/context-window-meter.tsx",
|
||||
"packages/app/src/components/dictation-controls.tsx",
|
||||
@@ -280,7 +280,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/e2e/fixtures.ts"],
|
||||
"files": ["**/e2e/**/fixtures.ts"],
|
||||
"rules": {
|
||||
"no-empty-pattern": "off"
|
||||
}
|
||||
|
||||
10
CLAUDE.md
10
CLAUDE.md
@@ -148,12 +148,12 @@ The app runs on iOS, Android, web (browser), and web (Electron desktop). Code is
|
||||
Import as `@/hooks/use-audio-recorder` — Metro picks the right file automatically.
|
||||
- **Use `.electron.ts` / `.electron.tsx` for Electron-only web modules.** Electron is still the Metro `web` platform, but desktop dev/build sets `PASEO_WEB_PLATFORM=electron`, so Metro first looks for `.electron.*` files and falls back to normal `.web.*` files. Use this when the implementation depends on Electron-only behavior such as `webviewTag`, desktop preload APIs, or the Electron bridge. Keep plain browser web in `.web.*`, and keep native fallbacks in the base file or `.native.*`.
|
||||
```
|
||||
components/
|
||||
browser-pane.electron.tsx ← Electron <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.
|
||||
|
||||
@@ -21,8 +21,16 @@ the agent runs through `ensureAgentLoaded()`, which resumes the durable provider
|
||||
same Paseo agent ID. Provider history is not appended again when the canonical timeline is already
|
||||
primed.
|
||||
|
||||
Idle agents remain resident indefinitely. Runtime closure happens only through an explicit lifecycle
|
||||
action such as archive, replacement, reload, workspace teardown, or daemon shutdown.
|
||||
The daemon collects an eligible idle runtime after 30 minutes and sweeps every minute. Only
|
||||
unarchived, non-internal agents that are exactly `idle`, have no active or pending run, replacement,
|
||||
or permission, and have not been activated during the idle window are eligible. `running`,
|
||||
`initializing`, and `error` agents stay resident. An idle parent also stays resident while current
|
||||
in-memory state shows a running managed child or provider subagent. Otherwise agents are evaluated
|
||||
independently; collection does not cascade or change parentage.
|
||||
|
||||
Active schedules targeting an existing agent protect that agent from collection. Paused, completed,
|
||||
and new-agent schedules do not. A pane may remain open after collection; its next prompt resumes the
|
||||
runtime.
|
||||
|
||||
### Cancellation
|
||||
|
||||
@@ -51,8 +59,9 @@ The provider still owns the underlying runtime. Paseo keeps an agent record so t
|
||||
|
||||
Archive is a **soft delete**: the agent record stays on disk with `archivedAt` set, the runtime is closed, and the agent disappears from active lists. Archive is **global** — it lives on the server and propagates to every connected client.
|
||||
|
||||
Archive sets `archivedAt`, invokes the provider's native archive hook, and cascades to managed
|
||||
children.
|
||||
Archive is distinct from runtime collection. Archive sets `archivedAt`, invokes the provider's native
|
||||
archive hook, and cascades to managed children. Runtime collection does none of those things; it only
|
||||
releases the live runtime and writes `lastStatus: closed` on the still-active record.
|
||||
|
||||
`create_agent_request` can opt an agent into `autoArchive`. In that mode the daemon archives the agent after the first terminal turn event (`turn_completed`, `turn_failed`, or `turn_canceled`). When the agent owns an isolated workspace, auto-archive archives that workspace too; the managed worktree is removed when its final workspace reference is gone.
|
||||
|
||||
|
||||
@@ -38,14 +38,6 @@ There are two supported ways to ship from `main`:
|
||||
1. **Direct stable release**: you are ready to ship the current `main` commit to everyone immediately.
|
||||
2. **Beta flow**: release candidates on the `beta` channel. Betas carry an in-place changelog entry (beta users check it), publish npm only on the explicit `beta` dist-tag, and never move the website download target off the latest stable.
|
||||
|
||||
Paseo has one linear release track even though npm dist-tags are independent
|
||||
pointers. The npm invariant is:
|
||||
|
||||
- A beta release moves only `beta`; `latest` remains on the newest stable.
|
||||
- A stable release moves both `latest` and `beta` to that stable version. This
|
||||
keeps users who install `@getpaseo/cli@beta` on the newest Paseo release after
|
||||
a beta is promoted or superseded by a direct stable release.
|
||||
|
||||
## Release version decision
|
||||
|
||||
Every fresh release starts by classifying the full previous-stable-to-`HEAD`
|
||||
@@ -84,20 +76,6 @@ npm run release:minor
|
||||
|
||||
This bumps the version across all workspaces, runs checks, publishes to npm, and pushes the branch + tag. The tag push triggers `Desktop Release`, `Android APK Release`, `Docker`, and `Release Notes Sync` on GitHub Actions. EAS picks up the same tag via the EAS GitHub app and starts the iOS + Android store builds in parallel (see "Mobile builds (EAS)" below) — there is no `release-mobile.yml` in this repo.
|
||||
|
||||
After the stable release succeeds, move npm's `beta` pointer to the new stable
|
||||
version for every published package. This changes dist-tags only; do not
|
||||
republish the packages:
|
||||
|
||||
```bash
|
||||
PASEO_VERSION=$(node -p "require('./package.json').version")
|
||||
for package in highlight relay protocol client server cli; do
|
||||
npm dist-tag add "@getpaseo/$package@$PASEO_VERSION" beta
|
||||
done
|
||||
```
|
||||
|
||||
Verify both npm tags now resolve to `PASEO_VERSION` before considering the
|
||||
stable release complete.
|
||||
|
||||
The Docker workflow builds images from the checked-out source tree on pull requests and on `main` as non-publishing checks. Stable `vX.Y.Z` tag pushes publish `ghcr.io/getpaseo/paseo:X.Y.Z` and `ghcr.io/getpaseo/paseo:latest`; beta `vX.Y.Z-beta.N` tag pushes publish only `ghcr.io/getpaseo/paseo:X.Y.Z-beta.N` and never move `latest`.
|
||||
|
||||
The production relay is the Elixir service in [getpaseo/paseo-relay](https://github.com/getpaseo/paseo-relay), with its own deployment process. Paseo releases and pushes to this repository do not deploy it. The Cloudflare relay code and workflow in this repository are legacy and are not used in production.
|
||||
@@ -114,7 +92,6 @@ npm run version:all:patch
|
||||
npm run version:all:minor
|
||||
npm run release:publish # Publish to npm
|
||||
npm run release:push # Push HEAD + tag (triggers CI workflows)
|
||||
# Then move npm's beta dist-tag to this stable version using the command above.
|
||||
```
|
||||
|
||||
## Beta flow
|
||||
@@ -529,7 +506,6 @@ Betas are checkpoints along the way; the entry is the single record for the jump
|
||||
- [ ] Update `CHANGELOG.md` with user-facing release notes (features, fixes — not refactors). When promoting from beta, overwrite the existing `## X.Y.Z-beta.N` heading in place (heading → `X.Y.Z`, date → promotion day) — do not add a new entry on top of the beta one
|
||||
- [ ] Verify the changelog heading follows strict `## X.Y.Z - YYYY-MM-DD` format
|
||||
- [ ] `npm run release:patch`, `npm run release:minor`, or `npm run release:promote` completes successfully
|
||||
- [ ] Move npm's `beta` dist-tag to the new stable version for every published package and verify both `latest` and `beta` resolve to it
|
||||
- [ ] GitHub `Desktop Release` workflow for the `v*` tag is green
|
||||
- [ ] GitHub `Android APK Release` workflow for the same tag is green
|
||||
- [ ] EAS `Release Mobile` workflow for the same tag is green
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -179,6 +179,14 @@ Test suites in this repo are heavy. Running them in bulk freezes the machine, es
|
||||
- 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.
|
||||
|
||||
## 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
|
||||
|
||||
Agent providers handle their own auth. Do not add auth checks, environment variable gates, or conditional skips to tests. If auth fails, report it.
|
||||
|
||||
@@ -87,14 +87,42 @@ its completion advances `seqEnd`, followed by a merged assistant message. The ap
|
||||
remaining page through the existing stream reducer. It must not append full projected text to a
|
||||
live prefix.
|
||||
|
||||
Optimistic user prompts occupy stable timeline slots. Catch-up never extracts, delays, or reinserts
|
||||
them. A canonical user row replaces its matching slot in place; an unmatched prompt stays exactly
|
||||
where the user submitted it. Other canonical rows are applied after the already-present timeline
|
||||
instead of relocating visible user messages around newly fetched history.
|
||||
Every path that sends a message to an agent — composer send, dictation accept-and-send, queued
|
||||
send-now, and the automatic queue drain in `HostRuntime` — goes through
|
||||
`dispatchComposerAgentMessage` with a submission writer. There is no second transport for the same
|
||||
product action: calling `client.sendAgentMessage` directly skips the submitted row and the pending
|
||||
footer, and permanently drops attachments because the daemon does not echo them back.
|
||||
|
||||
A submitted prompt is one `UserMessageItem` row. That row is the authoritative local presentation:
|
||||
its stable identity, text, timestamp, images, and attachments do not change when the provider
|
||||
acknowledges it. Submission lifecycle is a separate record keyed by agent, not another row shape or
|
||||
a property inferred from message identity. The transaction registry holds every unresolved send and
|
||||
records RPC acceptance and provider acknowledgement independently. Provider acknowledgement exists
|
||||
solely so a later transport error cannot roll back a prompt already observed canonically.
|
||||
|
||||
The daemon's accepted response already waits for the correlated run start, but its response and the
|
||||
directory update reach client state separately. An accepted transaction remains active until the
|
||||
directory observes that run or canonical ingestion acknowledges the prompt, bridging those ordered
|
||||
authorities without inspecting timeline snapshots. Either signal clears only an RPC-accepted
|
||||
transaction, regardless of which arrived first; it cannot settle a fresh send.
|
||||
Overlapping sends settle independently rather than collapsing to one newest pending message.
|
||||
|
||||
Canonical submitted user rows carry the provider's `messageId` and Paseo's optional
|
||||
`clientMessageId`. Clients reconcile optimistic prompts by `clientMessageId`. Content matching is
|
||||
limited to the dated compatibility path for daemon timelines created before that field existed.
|
||||
`clientMessageId`. The user-message producer reconciles them by `clientMessageId`, adds provider
|
||||
identity to the existing row, and keeps the local presentation in its original timeline slot.
|
||||
Content matching is limited to the dated compatibility path for daemon timelines created before
|
||||
that field existed. Canonical ingestion may match only an explicit unreconciled local candidate;
|
||||
the draft-create handoff is the one boundary that also permits the legacy canonical twin to have
|
||||
arrived first. Generic reducers and consumers do not reimplement message identity matching.
|
||||
|
||||
Ordinary bootstrap, same-epoch reset, and catch-up replacement preserve unmatched locally submitted
|
||||
rows because a provider may never echo them. A known epoch change or rewind replaces history and
|
||||
drops acknowledged local rows omitted by the new canonical epoch; every transaction not yet
|
||||
acknowledged by the provider, and no other local row, crosses that destructive boundary.
|
||||
|
||||
Canonical replacement owns both timeline lanes. A matching local row keeps its presentation ID and
|
||||
payload while taking the canonical row's ordered position. If a live assistant head is the
|
||||
canonical assistant prefix, it stays in the head lane. No row may be returned in both lanes.
|
||||
|
||||
## Relevant code
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// "00-" prefix is intentional: this file must sort before every other spec.
|
||||
// Sessions history is daemon-global — any agent created by a prior spec hides the empty state.
|
||||
// If the beforeAll probe below fails, a spec sorted before this file is creating agents.
|
||||
import { test } from "./fixtures";
|
||||
import { connectSeedClient } from "./helpers/seed-client";
|
||||
import { expectSessionsEmptyState, openSessions } from "./helpers/archive-tab";
|
||||
import { test } from "../support/fixtures";
|
||||
import { connectSeedClient } from "../support/helpers/seed-client";
|
||||
import { expectSessionsEmptyState, openSessions } from "../support/helpers/archive-tab";
|
||||
|
||||
test.describe("Sessions screen empty state", () => {
|
||||
test.beforeAll(async () => {
|
||||
@@ -1,13 +1,13 @@
|
||||
import { test } from "./fixtures";
|
||||
import { gotoAppShell, openSettings } from "./helpers/app";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { test } from "../support/fixtures";
|
||||
import { gotoAppShell, openSettings } from "../support/helpers/app";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import {
|
||||
expectProviderInstalledInSettings,
|
||||
installAcpCatalogProvider,
|
||||
openAddProviderArea,
|
||||
openSettingsHost,
|
||||
openSettingsHostSection,
|
||||
} from "./helpers/settings";
|
||||
} from "../support/helpers/settings";
|
||||
|
||||
const ACP_PROVIDER = {
|
||||
id: "hermes",
|
||||
@@ -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, type Page } from "./fixtures";
|
||||
import { test, expect, type Page } from "../support/fixtures";
|
||||
import {
|
||||
addProjectFlow,
|
||||
addProjectFlowBack,
|
||||
@@ -14,14 +14,17 @@ import {
|
||||
expectNewWorkspaceForAddedProject,
|
||||
openAddProjectFlow,
|
||||
waitForConnectedHost,
|
||||
} from "./helpers/add-project-flow";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
import { buildSeededHost } from "./helpers/daemon-registry";
|
||||
import { addOfflineHostAndReload } from "./helpers/hosts";
|
||||
import { type IsolatedHostDaemon, startIsolatedHostDaemon } from "./helpers/isolated-host-daemon";
|
||||
import { expectOpenedProject } from "./helpers/project-picker-ui";
|
||||
import { connectSeedClient } from "./helpers/seed-client";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
} from "../support/helpers/add-project-flow";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import { buildSeededHost } from "../support/helpers/daemon-registry";
|
||||
import { addOfflineHostAndReload } from "../support/helpers/hosts";
|
||||
import {
|
||||
type IsolatedHostDaemon,
|
||||
startIsolatedHostDaemon,
|
||||
} from "../support/helpers/isolated-host-daemon";
|
||||
import { expectOpenedProject } from "../support/helpers/project-picker-ui";
|
||||
import { connectSeedClient } from "../support/helpers/seed-client";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
|
||||
const EXTRA_HOSTS_KEY = "@paseo:e2e-extra-hosts";
|
||||
const SECONDARY_HOST_ID = "add-project-flow-secondary";
|
||||
@@ -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 });
|
||||
685
packages/app/e2e/browser/agent-message-submission.spec.ts
Normal file
685
packages/app/e2e/browser/agent-message-submission.spec.ts
Normal file
@@ -0,0 +1,685 @@
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
import { expect, test as baseTest } from "../support/fixtures";
|
||||
import { awaitToolCall, expectAgentIdle } from "../support/helpers/agent-stream";
|
||||
import { gateNextAgentMessage } from "../support/helpers/agent-message-gate";
|
||||
import {
|
||||
attachImageFromMenu,
|
||||
expectComposerDraft,
|
||||
expectComposerEditable,
|
||||
expectAttachmentPill,
|
||||
expectComposerVisible,
|
||||
fillComposerDraft,
|
||||
sendDraftToQueue,
|
||||
startRunningMockAgent,
|
||||
} from "../support/helpers/composer";
|
||||
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
||||
import { readScrollMetrics } from "../support/helpers/agent-bottom-anchor";
|
||||
import { seedWorkspace } from "../support/helpers/seed-client";
|
||||
import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||
import { delayBrowserAgentCreatedStatus } from "../support/helpers/new-workspace";
|
||||
import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
|
||||
import { selectModel } from "../support/helpers/app";
|
||||
|
||||
const IMAGE = {
|
||||
name: "message-submission.png",
|
||||
mimeType: "image/png",
|
||||
buffer: Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
||||
"base64",
|
||||
),
|
||||
};
|
||||
|
||||
interface MessageGeometry {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface SubmissionScenario {
|
||||
gate: Awaited<ReturnType<typeof gateNextAgentMessage>>;
|
||||
}
|
||||
|
||||
interface DraftCreateScenario {
|
||||
workspaceId: string;
|
||||
agentCreatedDelay: Awaited<ReturnType<typeof delayBrowserAgentCreatedStatus>>;
|
||||
}
|
||||
|
||||
interface RejectionScenario {
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
interface UnrelatedRunningScenario {
|
||||
gate: Awaited<ReturnType<typeof gateNextAgentMessage>>;
|
||||
agent: Awaited<ReturnType<typeof seedMockAgentWorkspace>>;
|
||||
}
|
||||
|
||||
const test = baseTest.extend<{
|
||||
submissionScenario: SubmissionScenario;
|
||||
draftCreateScenario: DraftCreateScenario;
|
||||
rejectionScenario: RejectionScenario;
|
||||
unrelatedRunningScenario: UnrelatedRunningScenario;
|
||||
}>({
|
||||
submissionScenario: async ({ page }, provide, testInfo) => {
|
||||
const gate = await gateNextAgentMessage(page);
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: `message-submission-${testInfo.workerIndex}-`,
|
||||
title: "Message submission regression",
|
||||
model: "ten-second-stream",
|
||||
});
|
||||
await openAgentRoute(page, { workspaceId: agent.workspaceId, agentId: agent.agentId });
|
||||
await expectComposerVisible(page);
|
||||
await expectAgentIdle(page);
|
||||
await provide({ gate });
|
||||
await agent.cleanup();
|
||||
},
|
||||
draftCreateScenario: async ({ page }, provide, testInfo) => {
|
||||
const agentCreatedDelay = await delayBrowserAgentCreatedStatus(page);
|
||||
const workspace = await seedWorkspace({
|
||||
repoPrefix: `message-create-handoff-${testInfo.workerIndex}-`,
|
||||
});
|
||||
await provide({ workspaceId: workspace.workspaceId, agentCreatedDelay });
|
||||
agentCreatedDelay.release();
|
||||
await workspace.cleanup();
|
||||
},
|
||||
rejectionScenario: async ({ page }, provide, testInfo) => {
|
||||
const errorMessage = "Requested mock prompt rejection";
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: `message-rejection-${testInfo.workerIndex}-`,
|
||||
title: "Message rejection regression",
|
||||
model: "ten-second-stream",
|
||||
featureValues: { mockPromptRejections: 1 },
|
||||
});
|
||||
await openAgentRoute(page, { workspaceId: agent.workspaceId, agentId: agent.agentId });
|
||||
await expectComposerVisible(page);
|
||||
await expectAgentIdle(page);
|
||||
await provide({ errorMessage });
|
||||
await agent.cleanup();
|
||||
},
|
||||
unrelatedRunningScenario: async ({ page }, provide, testInfo) => {
|
||||
const gate = await gateNextAgentMessage(page);
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: `unrelated-running-${testInfo.workerIndex}-`,
|
||||
title: "Unrelated running transition",
|
||||
model: "one-minute-stream",
|
||||
});
|
||||
await openAgentRoute(page, { workspaceId: agent.workspaceId, agentId: agent.agentId });
|
||||
await expectComposerVisible(page);
|
||||
await expectAgentIdle(page);
|
||||
await provide({ gate, agent });
|
||||
await agent.cleanup();
|
||||
},
|
||||
});
|
||||
|
||||
async function submitMessageWithImage(page: Page, prompt: string): Promise<Locator> {
|
||||
await attachImageFromMenu(page, IMAGE);
|
||||
await expectAttachmentPill(page, "composer-image-attachment-pill");
|
||||
const composer = page.getByRole("textbox", { name: "Message agent..." }).first();
|
||||
await composer.fill(prompt);
|
||||
await composer.press("Enter");
|
||||
const nextFrame = await composer.evaluate(
|
||||
(composerElement, submittedPrompt) =>
|
||||
new Promise<{
|
||||
rowPresent: boolean;
|
||||
workingPresent: boolean;
|
||||
composerValue: string | null;
|
||||
attachmentPresent: boolean;
|
||||
}>((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
const rows = Array.from(document.querySelectorAll('[data-testid="user-message"]'));
|
||||
const composerInput = composerElement as HTMLInputElement | HTMLTextAreaElement;
|
||||
resolve({
|
||||
rowPresent: rows.some((row) => row.textContent?.includes(submittedPrompt)),
|
||||
workingPresent: Boolean(
|
||||
document.querySelector('[data-testid="turn-working-indicator"]'),
|
||||
),
|
||||
composerValue: composerInput.value,
|
||||
attachmentPresent: Boolean(
|
||||
document.querySelector('[data-testid="composer-image-attachment-pill"]'),
|
||||
),
|
||||
});
|
||||
});
|
||||
}),
|
||||
prompt,
|
||||
);
|
||||
expect(nextFrame).toEqual({
|
||||
rowPresent: true,
|
||||
workingPresent: true,
|
||||
composerValue: "",
|
||||
attachmentPresent: false,
|
||||
});
|
||||
return page.getByTestId("user-message").filter({ hasText: prompt }).last();
|
||||
}
|
||||
|
||||
async function submitImageOnlyMessage(page: Page): Promise<Locator> {
|
||||
await attachImageFromMenu(page, IMAGE);
|
||||
await expectAttachmentPill(page, "composer-image-attachment-pill");
|
||||
await page.getByRole("textbox", { name: "Message agent..." }).first().press("Enter");
|
||||
const userMessage = page.getByTestId("user-message").last();
|
||||
await expect(userMessage).toBeVisible();
|
||||
await expect(userMessage.getByRole("button", { name: "Open image attachment" })).toBeVisible();
|
||||
return userMessage;
|
||||
}
|
||||
|
||||
async function expectPendingSubmission(page: Page, userMessage: Locator): Promise<void> {
|
||||
await expect(userMessage).toBeVisible();
|
||||
await expect(page.getByTestId("turn-working-indicator")).toBeVisible();
|
||||
await expect(page.getByRole("textbox", { name: "Message agent..." }).first()).toHaveValue("");
|
||||
await expect(page.getByTestId("composer-image-attachment-pill")).toHaveCount(0);
|
||||
await expect(userMessage.getByTestId("user-message-timestamp")).toBeAttached();
|
||||
await expect(userMessage.getByTestId("user-message-trailing-row")).toHaveCSS("opacity", "0");
|
||||
await expect(userMessage).toHaveAttribute("aria-busy", "true");
|
||||
await expect(userMessage.getByRole("button", { name: "Open image attachment" })).toBeVisible();
|
||||
}
|
||||
|
||||
async function readMessageGeometry(page: Page, userMessage: Locator): Promise<MessageGeometry> {
|
||||
const box = await userMessage.boundingBox();
|
||||
if (!box) throw new Error("Submitted user message has no browser geometry");
|
||||
const { offsetY } = await readScrollMetrics(page);
|
||||
return { x: box.x, y: box.y + offsetY, width: box.width, height: box.height };
|
||||
}
|
||||
|
||||
async function beginWorkingFooterContinuityCheck(page: Page): Promise<() => Promise<void>> {
|
||||
await expect(page.getByTestId("turn-working-indicator")).toBeVisible();
|
||||
await page.evaluate(() => {
|
||||
const state = { active: true, sawMissing: false };
|
||||
const windowState = window as unknown as Record<string, unknown>;
|
||||
windowState.__messageSubmissionFooterContinuity = state;
|
||||
const checkFrame = () => {
|
||||
if (!state.active) return;
|
||||
if (!document.querySelector('[data-testid="turn-working-indicator"]')) {
|
||||
state.sawMissing = true;
|
||||
}
|
||||
requestAnimationFrame(checkFrame);
|
||||
};
|
||||
requestAnimationFrame(checkFrame);
|
||||
});
|
||||
|
||||
return async () => {
|
||||
const sawMissing = await page.evaluate(() => {
|
||||
const windowState = window as unknown as Record<string, unknown>;
|
||||
const state = windowState.__messageSubmissionFooterContinuity as
|
||||
| { active: boolean; sawMissing: boolean }
|
||||
| undefined;
|
||||
if (!state) throw new Error("Working-footer continuity check was not started");
|
||||
state.active = false;
|
||||
delete windowState.__messageSubmissionFooterContinuity;
|
||||
return state.sawMissing;
|
||||
});
|
||||
expect(sawMissing).toBe(false);
|
||||
};
|
||||
}
|
||||
|
||||
async function expectAcceptedSubmission(
|
||||
page: Page,
|
||||
userMessage: Locator,
|
||||
submittedGeometry: MessageGeometry,
|
||||
): Promise<void> {
|
||||
await expect(page.getByTestId("turn-working-indicator")).toBeVisible();
|
||||
await expect(userMessage).toHaveAttribute("aria-busy", "false", { timeout: 30_000 });
|
||||
await expect(page.getByTestId("turn-working-indicator")).toBeVisible();
|
||||
expect(await readMessageGeometry(page, userMessage)).toEqual(submittedGeometry);
|
||||
}
|
||||
|
||||
async function submitMessageThatWillBeRejected(page: Page, prompt: string): Promise<void> {
|
||||
await attachImageFromMenu(page, IMAGE);
|
||||
await expectAttachmentPill(page, "composer-image-attachment-pill");
|
||||
const composer = page.getByRole("textbox", { name: "Message agent..." }).first();
|
||||
await composer.fill(prompt);
|
||||
await composer.press("Enter");
|
||||
}
|
||||
|
||||
async function expectRejectedSubmissionRestored(
|
||||
page: Page,
|
||||
input: { prompt: string; errorMessage: string },
|
||||
): Promise<void> {
|
||||
await expect(page.getByText(input.errorMessage)).toBeVisible({ timeout: 30_000 });
|
||||
await expectComposerDraft(page, input.prompt);
|
||||
await expectComposerEditable(page);
|
||||
await expectAttachmentPill(page, "composer-image-attachment-pill");
|
||||
await expect(page.getByRole("button", { name: "Send message" })).toBeEnabled();
|
||||
await expect(page.getByTestId("user-message").filter({ hasText: input.prompt })).toHaveCount(0);
|
||||
await expect(page.getByTestId("turn-working-indicator")).toHaveCount(0);
|
||||
}
|
||||
|
||||
async function retryRestoredSubmission(page: Page, prompt: string): Promise<void> {
|
||||
await page.getByRole("textbox", { name: "Message agent..." }).first().press("Enter");
|
||||
const userMessage = page.getByTestId("user-message").filter({ hasText: prompt });
|
||||
await expect(userMessage).toHaveCount(1);
|
||||
await expect(userMessage).toHaveAttribute("aria-busy", "false", { timeout: 30_000 });
|
||||
await expect(userMessage.getByRole("button", { name: "Open image attachment" })).toBeVisible();
|
||||
await expect(page.getByTestId("composer-image-attachment-pill")).toHaveCount(0);
|
||||
}
|
||||
|
||||
async function queueMessage(page: Page, prompt: string): Promise<void> {
|
||||
await fillComposerDraft(page, prompt);
|
||||
await sendDraftToQueue(page);
|
||||
}
|
||||
|
||||
async function expectQueuedSendFailuresRestored(page: Page, prompts: string[]): Promise<void> {
|
||||
await expect(page.getByRole("button", { name: "Send queued message now" })).toHaveCount(
|
||||
prompts.length,
|
||||
);
|
||||
for (const prompt of prompts) {
|
||||
await expect(page.getByTestId("user-message").filter({ hasText: prompt })).toHaveCount(0);
|
||||
}
|
||||
}
|
||||
|
||||
async function expectFailedSubmissionRestored(page: Page, prompt: string): Promise<void> {
|
||||
await expectComposerDraft(page, prompt);
|
||||
await expectComposerEditable(page);
|
||||
await expect(page.getByTestId("user-message").filter({ hasText: prompt })).toHaveCount(0);
|
||||
}
|
||||
|
||||
async function expectInterruptedTurnOrderAfterReconnect(
|
||||
page: Page,
|
||||
testInfo: { workerIndex: number },
|
||||
): Promise<void> {
|
||||
const gate = await installDaemonWebSocketGate(page);
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: `submission-reconnect-${testInfo.workerIndex}-`,
|
||||
title: "Submission reconnect ordering",
|
||||
model: "ten-second-stream",
|
||||
});
|
||||
const prompt = "Keep this prompt before its response.";
|
||||
try {
|
||||
await openAgentRoute(page, { workspaceId: agent.workspaceId, agentId: agent.agentId });
|
||||
await expectComposerVisible(page);
|
||||
await agent.client.sendAgentMessage(agent.agentId, "Start the turn that will be interrupted.");
|
||||
await expect(page.getByRole("button", { name: /stop|cancel/i }).first()).toBeVisible();
|
||||
await expect(page.getByText("Cycle 1", { exact: true })).toBeVisible();
|
||||
await queueMessage(page, prompt);
|
||||
gate.setAgentStreamSuppressed(true);
|
||||
await page.getByRole("button", { name: "Send queued message now" }).click();
|
||||
const promptRow = page.getByTestId("user-message").filter({ hasText: prompt });
|
||||
await expect(promptRow).toBeVisible();
|
||||
await gate.waitForServerMessage("send_agent_message_response");
|
||||
await gate.drop();
|
||||
await agent.client.waitForFinish(agent.agentId, 30_000);
|
||||
gate.setAgentStreamSuppressed(false);
|
||||
gate.forceNextTimelineEpochReset();
|
||||
gate.restoreFresh();
|
||||
await gate.waitForServerMessage("fetch_agent_timeline_response", 2);
|
||||
const response = page.getByText("(end of synthetic stream)", { exact: true }).last();
|
||||
await expect(promptRow).toBeVisible();
|
||||
await expect(response).toBeVisible();
|
||||
await expectRenderedBefore(promptRow, response);
|
||||
} finally {
|
||||
gate.restore();
|
||||
await agent.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function expectCompletedSubmissionClearsAfterMissedRunningTransition(
|
||||
page: Page,
|
||||
testInfo: { workerIndex: number },
|
||||
): Promise<void> {
|
||||
const gate = await installDaemonWebSocketGate(page);
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: `submission-missed-running-${testInfo.workerIndex}-`,
|
||||
title: "Submission missed running transition",
|
||||
model: "ten-second-stream",
|
||||
});
|
||||
try {
|
||||
await openAgentRoute(page, { workspaceId: agent.workspaceId, agentId: agent.agentId });
|
||||
await expectComposerVisible(page);
|
||||
await expectAgentIdle(page);
|
||||
gate.holdNextClientRequest("send_agent_message_request");
|
||||
const userMessage = await submitImageOnlyMessage(page);
|
||||
await gate.waitForHeldClientRequest();
|
||||
gate.setServerMessageSuppressed("agent_status", true);
|
||||
gate.setServerMessageSuppressed("agent_update", true);
|
||||
gate.releaseHeldClientRequest();
|
||||
await gate.waitForServerMessage("send_agent_message_response");
|
||||
await expect(userMessage).toHaveAttribute("aria-busy", "false");
|
||||
await gate.drop();
|
||||
await agent.client.waitForFinish(agent.agentId, 30_000);
|
||||
gate.setServerMessageSuppressed("agent_status", false);
|
||||
gate.setServerMessageSuppressed("agent_update", false);
|
||||
gate.restoreFresh();
|
||||
await gate.waitForServerMessage("fetch_agent_timeline_response", 2);
|
||||
await expect(page.getByText("(end of synthetic stream)", { exact: true }).last()).toBeVisible();
|
||||
await expect(page.getByTestId("turn-working-indicator")).toHaveCount(0);
|
||||
await expect(userMessage).toHaveAttribute("aria-busy", "false");
|
||||
} finally {
|
||||
gate.restore();
|
||||
await agent.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function expectProviderAcknowledgementBeforeRpcAcceptanceSettlesSubmission(
|
||||
page: Page,
|
||||
testInfo: { workerIndex: number },
|
||||
): Promise<void> {
|
||||
const gate = await installDaemonWebSocketGate(page);
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: `submission-ack-before-rpc-${testInfo.workerIndex}-`,
|
||||
title: "Submission acknowledgement before RPC",
|
||||
model: "ten-second-stream",
|
||||
});
|
||||
const prompt = "Settle this provider-acknowledged submission.";
|
||||
try {
|
||||
await openAgentRoute(page, { workspaceId: agent.workspaceId, agentId: agent.agentId });
|
||||
await expectComposerVisible(page);
|
||||
await expectAgentIdle(page);
|
||||
gate.setServerMessageSuppressed("agent_status", true);
|
||||
gate.setServerMessageSuppressed("agent_update", true);
|
||||
gate.holdNextServerMessage("send_agent_message_response");
|
||||
const userMessage = await submitMessageWithImage(page, prompt);
|
||||
await gate.waitForHeldServerMessage();
|
||||
await gate.waitForAgentStreamItem("user_message");
|
||||
gate.releaseHeldServerMessage();
|
||||
await gate.drop();
|
||||
await expect(page.getByTestId("turn-working-indicator")).toHaveCount(0);
|
||||
await expect(userMessage).toHaveAttribute("aria-busy", "false");
|
||||
} finally {
|
||||
gate.restore();
|
||||
await agent.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function expectLegacyAssistantStartsAfterInterruptedPrompt(
|
||||
page: Page,
|
||||
testInfo: { workerIndex: number },
|
||||
): Promise<void> {
|
||||
const gate = await installDaemonWebSocketGate(page);
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: `submission-legacy-assistant-${testInfo.workerIndex}-`,
|
||||
title: "Legacy assistant interrupt boundary",
|
||||
model: "ten-second-stream",
|
||||
});
|
||||
const prompt = "Start the replacement answer after this prompt.";
|
||||
try {
|
||||
await openAgentRoute(page, { workspaceId: agent.workspaceId, agentId: agent.agentId });
|
||||
await expectComposerVisible(page);
|
||||
await agent.client.sendAgentMessage(agent.agentId, "Start the interrupted answer.");
|
||||
await expect(page.getByText("Cycle 1", { exact: true })).toBeVisible();
|
||||
await queueMessage(page, prompt);
|
||||
gate.setAssistantMessageIdsStripped(true);
|
||||
gate.setAgentStreamEventSuppressed("turn_canceled", true);
|
||||
await page.getByRole("button", { name: "Send queued message now" }).click();
|
||||
const promptRow = page.getByTestId("user-message").filter({ hasText: prompt });
|
||||
const replacementAnswer = page.getByText("(end of synthetic stream)", { exact: true }).last();
|
||||
await expect(promptRow).toBeVisible();
|
||||
await expect(replacementAnswer).toBeVisible({ timeout: 30_000 });
|
||||
await expectRenderedBefore(promptRow, replacementAnswer);
|
||||
} finally {
|
||||
gate.setAssistantMessageIdsStripped(false);
|
||||
gate.setAgentStreamEventSuppressed("turn_canceled", false);
|
||||
await agent.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function expectStaleCanonicalPagePreservesNewerLiveOutput(
|
||||
page: Page,
|
||||
testInfo: { workerIndex: number },
|
||||
): Promise<void> {
|
||||
const gate = await installDaemonWebSocketGate(page);
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: `submission-stale-canonical-${testInfo.workerIndex}-`,
|
||||
title: "Stale canonical page race",
|
||||
model: "one-minute-stream",
|
||||
});
|
||||
try {
|
||||
await openAgentRoute(page, { workspaceId: agent.workspaceId, agentId: agent.agentId });
|
||||
await expectComposerVisible(page);
|
||||
await agent.client.sendAgentMessage(agent.agentId, "End the snapshot at a tool call.");
|
||||
await awaitToolCall(page, "read");
|
||||
await page
|
||||
.getByRole("button", { name: /stop|cancel/i })
|
||||
.first()
|
||||
.click();
|
||||
await expectAgentIdle(page);
|
||||
|
||||
gate.holdNextServerMessage("fetch_agent_timeline_response");
|
||||
gate.requestTimelineTail(agent.agentId);
|
||||
await gate.waitForHeldServerMessage();
|
||||
gate.truncateHeldTimelineAfterLast("tool_call");
|
||||
expect(gate.getHeldTimelineLastItemType()).toBe("tool_call");
|
||||
|
||||
const nextPrompt = "Stream after the stale snapshot.";
|
||||
await agent.client.sendAgentMessage(agent.agentId, nextPrompt);
|
||||
const nextPromptRow = page.getByTestId("user-message").filter({ hasText: nextPrompt });
|
||||
const liveAssistant = nextPromptRow.locator(
|
||||
'xpath=following::*[@data-testid="assistant-message"][1]',
|
||||
);
|
||||
await expect(nextPromptRow).toBeVisible();
|
||||
await expect(liveAssistant).toContainText("Cycle 1");
|
||||
gate.releaseHeldServerMessage();
|
||||
await expect(liveAssistant).toContainText("Cycle 1");
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function expectCanonicalOrderWinsAcrossOverlappingClients(
|
||||
page: Page,
|
||||
testInfo: { workerIndex: number },
|
||||
): Promise<void> {
|
||||
const gate = await installDaemonWebSocketGate(page);
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: `submission-cross-client-order-${testInfo.workerIndex}-`,
|
||||
title: "Cross-client submission order",
|
||||
model: "ten-second-stream",
|
||||
});
|
||||
const localPrompt = "Send this after the other client turn.";
|
||||
const remotePrompt = "Commit this other client turn first.";
|
||||
try {
|
||||
await openAgentRoute(page, { workspaceId: agent.workspaceId, agentId: agent.agentId });
|
||||
await expectComposerVisible(page);
|
||||
await expectAgentIdle(page);
|
||||
gate.holdNextClientRequest("send_agent_message_request");
|
||||
const localRow = await submitMessageWithImage(page, localPrompt);
|
||||
await gate.waitForHeldClientRequest();
|
||||
|
||||
await agent.client.sendAgentMessage(agent.agentId, remotePrompt);
|
||||
await agent.client.waitForFinish(agent.agentId, 30_000);
|
||||
const remoteRow = page.getByTestId("user-message").filter({ hasText: remotePrompt });
|
||||
await expect(remoteRow).toBeVisible();
|
||||
|
||||
const userMessageCount = gate.getAgentStreamItemCount("user_message");
|
||||
gate.releaseHeldClientRequest();
|
||||
await gate.waitForAgentStreamItem("user_message", userMessageCount + 1);
|
||||
await expect(localRow).toHaveAttribute("aria-busy", "false");
|
||||
await expect(localRow.getByRole("button", { name: "Open image attachment" })).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const localElement = await localRow.elementHandle();
|
||||
if (!localElement) return false;
|
||||
return remoteRow.evaluate(
|
||||
(remoteElement, localNode) =>
|
||||
Boolean(
|
||||
remoteElement.compareDocumentPosition(localNode) & Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
),
|
||||
localElement,
|
||||
);
|
||||
})
|
||||
.toBe(true);
|
||||
} finally {
|
||||
gate.restore();
|
||||
await agent.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function expectRenderedBefore(first: Locator, second: Locator): Promise<void> {
|
||||
const secondElement = await second.elementHandle();
|
||||
if (!secondElement) throw new Error("Expected the second timeline item to be rendered");
|
||||
expect(
|
||||
await first.evaluate(
|
||||
(firstElement, secondNode) =>
|
||||
Boolean(
|
||||
firstElement.compareDocumentPosition(secondNode) & Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
),
|
||||
secondElement,
|
||||
),
|
||||
).toBe(true);
|
||||
}
|
||||
|
||||
async function openWorkspaceDraft(page: Page, workspaceId: string): Promise<void> {
|
||||
await page.goto(buildHostWorkspaceRoute(getServerId(), workspaceId));
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
await page.getByTestId("workspace-new-agent-tab-inline").click();
|
||||
await expectComposerVisible(page);
|
||||
}
|
||||
|
||||
async function expectCreatedAgentHandoff(
|
||||
page: Page,
|
||||
prompt: string,
|
||||
userMessage: Locator,
|
||||
): Promise<void> {
|
||||
await expect(page.getByTestId("turn-working-indicator")).toBeVisible();
|
||||
await expect(page.getByTestId(/^workspace-tab-agent_/).first()).toBeVisible({ timeout: 30_000 });
|
||||
await expect(userMessage).toHaveAttribute("aria-busy", "false", { timeout: 30_000 });
|
||||
await expect(page.getByTestId("turn-working-indicator")).toBeVisible();
|
||||
await expect(page.getByTestId("user-message").filter({ hasText: prompt })).toHaveCount(1);
|
||||
await expect(userMessage.getByRole("button", { name: "Open image attachment" })).toBeVisible();
|
||||
}
|
||||
|
||||
interface DraftCreatePendingSubmission {
|
||||
prompt: string;
|
||||
userMessage: Locator;
|
||||
}
|
||||
|
||||
async function beginDraftCreateSubmission(
|
||||
page: Page,
|
||||
scenario: DraftCreateScenario,
|
||||
): Promise<DraftCreatePendingSubmission> {
|
||||
await openWorkspaceDraft(page, scenario.workspaceId);
|
||||
await selectModel(page, "one-minute-stream");
|
||||
const prompt = "Keep this row through create handoff.";
|
||||
const userMessage = await submitMessageWithImage(page, prompt);
|
||||
await scenario.agentCreatedDelay.waitForCreateRequest();
|
||||
await scenario.agentCreatedDelay.waitForDelayedCreatedStatus();
|
||||
await expectPendingSubmission(page, userMessage);
|
||||
return { prompt, userMessage };
|
||||
}
|
||||
|
||||
async function completeDraftCreateSubmission(
|
||||
page: Page,
|
||||
scenario: DraftCreateScenario,
|
||||
pending: DraftCreatePendingSubmission,
|
||||
): Promise<void> {
|
||||
scenario.agentCreatedDelay.release();
|
||||
await expectCreatedAgentHandoff(page, pending.prompt, pending.userMessage);
|
||||
}
|
||||
|
||||
test.describe("Agent message submission", () => {
|
||||
test("keeps the submitted row stable when the host accepts", async ({
|
||||
page,
|
||||
submissionScenario,
|
||||
}) => {
|
||||
const userMessage = await submitMessageWithImage(page, "Hold this submission.");
|
||||
await expectPendingSubmission(page, userMessage);
|
||||
await submissionScenario.gate.waitForRequest();
|
||||
const submittedGeometry = await readMessageGeometry(page, userMessage);
|
||||
const finishFooterContinuityCheck = await beginWorkingFooterContinuityCheck(page);
|
||||
submissionScenario.gate.accept();
|
||||
await expectAcceptedSubmission(page, userMessage, submittedGeometry);
|
||||
await finishFooterContinuityCheck();
|
||||
});
|
||||
|
||||
test("keeps the submitted row stable through draft create handoff", async ({
|
||||
page,
|
||||
draftCreateScenario,
|
||||
}) => {
|
||||
test.setTimeout(120_000);
|
||||
const pending = await beginDraftCreateSubmission(page, draftCreateScenario);
|
||||
await completeDraftCreateSubmission(page, draftCreateScenario, pending);
|
||||
});
|
||||
|
||||
test("restores a rejected submission and accepts its retry", async ({
|
||||
page,
|
||||
rejectionScenario,
|
||||
}) => {
|
||||
const prompt = "Restore this rejected submission.";
|
||||
await submitMessageThatWillBeRejected(page, prompt);
|
||||
await expectRejectedSubmissionRestored(page, { prompt, ...rejectionScenario });
|
||||
await retryRestoredSubmission(page, prompt);
|
||||
});
|
||||
|
||||
test("restores overlapping queued sends when their connection fails", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.setTimeout(120_000);
|
||||
const gate = await gateNextAgentMessage(page);
|
||||
const agent = await startRunningMockAgent(page, {
|
||||
prefix: `overlapping-queued-send-${testInfo.workerIndex}-`,
|
||||
model: "one-minute-stream",
|
||||
prompt: "Keep the agent running while messages queue.",
|
||||
});
|
||||
const prompts = ["Restore the first queued send.", "Restore the second queued send."];
|
||||
try {
|
||||
await queueMessage(page, prompts[0]);
|
||||
await queueMessage(page, prompts[1]);
|
||||
await page.getByRole("button", { name: "Send queued message now" }).first().click();
|
||||
await gate.waitForRequest(1);
|
||||
await page.getByRole("button", { name: "Send queued message now" }).first().click();
|
||||
await gate.waitForRequest(2);
|
||||
await gate.disconnect();
|
||||
await expectQueuedSendFailuresRestored(page, prompts);
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("does not accept a failed submission from an unrelated running turn", async ({
|
||||
page,
|
||||
unrelatedRunningScenario,
|
||||
}) => {
|
||||
const prompt = "Restore this unsent prompt.";
|
||||
await submitMessageThatWillBeRejected(page, prompt);
|
||||
await unrelatedRunningScenario.gate.waitForRequest();
|
||||
await unrelatedRunningScenario.agent.client.sendAgentMessage(
|
||||
unrelatedRunningScenario.agent.agentId,
|
||||
"Start an unrelated turn.",
|
||||
);
|
||||
await expect(
|
||||
page.getByTestId("user-message").filter({ hasText: "Start an unrelated turn." }),
|
||||
).toBeVisible();
|
||||
await unrelatedRunningScenario.gate.disconnect();
|
||||
await expectFailedSubmissionRestored(page, prompt);
|
||||
});
|
||||
|
||||
test("keeps a submitted prompt before its response when canonical history arrives", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.setTimeout(90_000);
|
||||
await expectInterruptedTurnOrderAfterReconnect(page, testInfo);
|
||||
});
|
||||
|
||||
test("clears an attachment-only submission when canonical history arrives after a missed running transition", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.setTimeout(90_000);
|
||||
await expectCompletedSubmissionClearsAfterMissedRunningTransition(page, testInfo);
|
||||
});
|
||||
|
||||
test("clears a provider acknowledgement that arrives before RPC acceptance", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.setTimeout(90_000);
|
||||
await expectProviderAcknowledgementBeforeRpcAcceptanceSettlesSubmission(page, testInfo);
|
||||
});
|
||||
|
||||
test("keeps an old-daemon replacement answer after its interrupted prompt", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.setTimeout(90_000);
|
||||
await expectLegacyAssistantStartsAfterInterruptedPrompt(page, testInfo);
|
||||
});
|
||||
|
||||
test("preserves newer live output when a stale canonical page arrives", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.setTimeout(90_000);
|
||||
await expectStaleCanonicalPagePreservesNewerLiveOutput(page, testInfo);
|
||||
});
|
||||
|
||||
test("uses canonical order when another client turn overtakes a held submission", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await expectCanonicalOrderWinsAcrossOverlappingClients(page, testInfo);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
import { test, expect } from "../support/fixtures";
|
||||
import {
|
||||
awaitAssistantMessage,
|
||||
expectAgentIdle,
|
||||
expectInlineWorkingIndicator,
|
||||
expectTurnCopyButton,
|
||||
expectScrollFollowsNewContent,
|
||||
} from "./helpers/agent-stream";
|
||||
} from "../support/helpers/agent-stream";
|
||||
import {
|
||||
expectScrollStaysFixed,
|
||||
clickToolCallBesideScrollToBottomButton,
|
||||
@@ -13,12 +13,12 @@ import {
|
||||
scrollAgentChatToBottom,
|
||||
scrollChatAwayFromBottom,
|
||||
waitForScrollableChat,
|
||||
} from "./helpers/agent-bottom-anchor";
|
||||
import { delayCreatedAgentInitialTailResponse } from "./helpers/agent-timeline-gate";
|
||||
import { selectModel } from "./helpers/app";
|
||||
import { clickNewChat } from "./helpers/launcher";
|
||||
import { expectComposerVisible, startRunningMockAgent } from "./helpers/composer";
|
||||
import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent";
|
||||
} from "../support/helpers/agent-bottom-anchor";
|
||||
import { delayCreatedAgentInitialTailResponse } from "../support/helpers/agent-timeline-gate";
|
||||
import { selectModel } from "../support/helpers/app";
|
||||
import { clickNewChat } from "../support/helpers/launcher";
|
||||
import { expectComposerVisible, startRunningMockAgent } from "../support/helpers/composer";
|
||||
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
||||
|
||||
const SCROLL_AWAY_MIN_SCROLLABLE_DISTANCE = 360;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test } from "./fixtures";
|
||||
import { test } from "../support/fixtures";
|
||||
import {
|
||||
expectLoadedTimelineDoesNotScroll,
|
||||
expectTimelinePromptNotMounted,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
openAgentTimeline,
|
||||
scrollTimelineUntilOlderHistoryIsReachable,
|
||||
seedLongMockAgentTimeline,
|
||||
} from "./helpers/timeline-pagination";
|
||||
} from "../support/helpers/timeline-pagination";
|
||||
|
||||
test.describe("Agent timeline pagination", () => {
|
||||
test("loads older history when the user scrolls to the top of a long agent timeline", async ({
|
||||
@@ -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,17 +1,17 @@
|
||||
import { expect, test as base, type Page } from "./fixtures";
|
||||
import { scrollAgentChatToBottom } from "./helpers/agent-bottom-anchor";
|
||||
import { awaitAssistantMessage } from "./helpers/agent-stream";
|
||||
import { expectComposerVisible, submitMessage } from "./helpers/composer";
|
||||
import { getE2EDaemonPort } from "./helpers/daemon-port";
|
||||
import { expect, test as base, type Page } from "../support/fixtures";
|
||||
import { scrollAgentChatToBottom } from "../support/helpers/agent-bottom-anchor";
|
||||
import { awaitAssistantMessage } from "../support/helpers/agent-stream";
|
||||
import { expectComposerVisible, submitMessage } from "../support/helpers/composer";
|
||||
import { getE2EDaemonPort } from "../support/helpers/daemon-port";
|
||||
import {
|
||||
openAgentRoute,
|
||||
seedMockAgentWorkspace,
|
||||
type MockAgentOptions,
|
||||
type MockAgentWorkspace,
|
||||
} from "./helpers/mock-agent";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { seedSavedSettingsHosts } from "./helpers/settings";
|
||||
import { submitNewWorkspaceEmpty } from "./helpers/new-workspace";
|
||||
} from "../support/helpers/mock-agent";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { seedSavedSettingsHosts } from "../support/helpers/settings";
|
||||
import { submitNewWorkspaceEmpty } from "../support/helpers/new-workspace";
|
||||
|
||||
const test = base.extend<{
|
||||
seedForkWorkspace: (options: MockAgentOptions) => Promise<MockAgentWorkspace>;
|
||||
@@ -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,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,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,17 +1,17 @@
|
||||
import path from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import { test, expect, type Page } from "./fixtures";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
import { test, expect, type Page } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import {
|
||||
addProjectFlowInput,
|
||||
chooseAddProjectMethod,
|
||||
openAddProjectFlow,
|
||||
} from "./helpers/add-project-flow";
|
||||
import { expectOpenedProject } from "./helpers/project-picker-ui";
|
||||
import { connectSeedClient, seedWorkspace } from "./helpers/seed-client";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
import { waitForSidebarHydration } from "./helpers/workspace-ui";
|
||||
} from "../support/helpers/add-project-flow";
|
||||
import { expectOpenedProject } from "../support/helpers/project-picker-ui";
|
||||
import { connectSeedClient, seedWorkspace } from "../support/helpers/seed-client";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { createTempGitRepo } from "../support/helpers/workspace";
|
||||
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
||||
|
||||
function workspaceRowTestId(workspaceId: string): string {
|
||||
return `sidebar-workspace-row-${getServerId()}:${workspaceId}`;
|
||||
@@ -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,18 +1,21 @@
|
||||
import { expect, test } from "./fixtures";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import {
|
||||
connectNewWorkspaceDaemonClient,
|
||||
expectNewWorkspaceProjectSelected,
|
||||
openGlobalNewWorkspaceComposer,
|
||||
openNewWorkspaceComposer,
|
||||
openNewWorkspaceProjectPickerWithShortcut,
|
||||
} from "./helpers/new-workspace";
|
||||
import { getE2EDaemonPort } from "./helpers/daemon-port";
|
||||
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
|
||||
import { seedSavedSettingsHosts } from "./helpers/settings";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { clickArchiveWorkspaceMenuItem, expectWorkspaceAbsentFromSidebar } from "./helpers/sidebar";
|
||||
import { waitForSidebarHydration } from "./helpers/workspace-ui";
|
||||
} from "../support/helpers/new-workspace";
|
||||
import { getE2EDaemonPort } from "../support/helpers/daemon-port";
|
||||
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
||||
import { seedSavedSettingsHosts } from "../support/helpers/settings";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import {
|
||||
clickArchiveWorkspaceMenuItem,
|
||||
expectWorkspaceAbsentFromSidebar,
|
||||
} from "../support/helpers/sidebar";
|
||||
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
||||
|
||||
// Model B entry points into the New Workspace screen. The surviving entries are
|
||||
// the global button (universal) and each project's per-row New workspace icon
|
||||
@@ -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;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { mkdtempSync, realpathSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { submitMessage } from "../support/helpers/composer";
|
||||
import { cleanupRewindFlow, launchAgent, type AgentHandle } from "../support/helpers/rewind-flow";
|
||||
import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
|
||||
|
||||
test.describe("Codex out-of-band commands", () => {
|
||||
test.setTimeout(300_000);
|
||||
|
||||
test("settles the submitted row when a goal command completes without a turn", async ({
|
||||
page,
|
||||
}) => {
|
||||
const cwd = realpathSync(mkdtempSync(path.join(tmpdir(), "paseo-codex-command-")));
|
||||
let handle: AgentHandle | undefined;
|
||||
|
||||
try {
|
||||
handle = await launchAgent({ page, provider: "codex", cwd, mode: "full-access" });
|
||||
await submitMessage(page, "/goal clear");
|
||||
const command = page.getByTestId("user-message").filter({ hasText: "/goal clear" });
|
||||
|
||||
await expect(command).toBeVisible();
|
||||
await expect(command).toHaveAttribute("aria-busy", "false", { timeout: 30_000 });
|
||||
await expect(page.getByTestId("turn-working-indicator")).toHaveCount(0);
|
||||
} finally {
|
||||
await cleanupRewindFlow({ handle, cwd });
|
||||
}
|
||||
});
|
||||
|
||||
test("settles the submitted row when an older daemon omits submission disposition", async ({
|
||||
page,
|
||||
}) => {
|
||||
const gate = await installDaemonWebSocketGate(page);
|
||||
gate.setMessageSubmissionDispositionStripped(true);
|
||||
const cwd = realpathSync(mkdtempSync(path.join(tmpdir(), "paseo-codex-command-compat-")));
|
||||
let handle: AgentHandle | undefined;
|
||||
|
||||
try {
|
||||
handle = await launchAgent({ page, provider: "codex", cwd, mode: "full-access" });
|
||||
await submitMessage(page, "/goal clear");
|
||||
const command = page.getByTestId("user-message").filter({ hasText: "/goal clear" });
|
||||
|
||||
await expect(command).toBeVisible();
|
||||
await expect(command).toHaveAttribute("aria-busy", "false", { timeout: 30_000 });
|
||||
await expect(page.getByTestId("turn-working-indicator")).toHaveCount(0);
|
||||
} finally {
|
||||
gate.restore();
|
||||
await cleanupRewindFlow({ handle, cwd });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
@@ -29,14 +29,14 @@ import {
|
||||
removeProjectScript,
|
||||
restorePaseoConfig,
|
||||
unblockPaseoConfigWrites,
|
||||
} from "./helpers/project-settings";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
} from "../support/helpers/project-settings";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import {
|
||||
addProjectFlowInput,
|
||||
chooseAddProjectMethod,
|
||||
openAddProjectFlow,
|
||||
} from "./helpers/add-project-flow";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
} from "../support/helpers/add-project-flow";
|
||||
import { createTempGitRepo } from "../support/helpers/workspace";
|
||||
|
||||
const updatedSetup = ["npm install", "npm run build"];
|
||||
|
||||
@@ -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,13 +1,14 @@
|
||||
import type { Locator } from "@playwright/test";
|
||||
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 { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
|
||||
import {
|
||||
composerLocator,
|
||||
expectComposerDraft,
|
||||
expectComposerVisible,
|
||||
fillComposerDraft,
|
||||
submitMessage,
|
||||
} from "./helpers/composer";
|
||||
} from "../support/helpers/composer";
|
||||
|
||||
// UI plumbing contract against the dev mock provider. Real-provider behavior is tested in `daemon-e2e/*-rewind.real.e2e.test.ts`.
|
||||
|
||||
@@ -23,7 +24,152 @@ async function expectUserMessageVisible(page: Page, text: string): Promise<void>
|
||||
await expect(userMessage(page, text)).toBeVisible();
|
||||
}
|
||||
|
||||
async function rewriteCachedMessageAsLegacyRow(page: Page, prompt: string): Promise<void> {
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate((messageText) => {
|
||||
const raw = localStorage.getItem("@paseo:replica-cache");
|
||||
if (!raw) return false;
|
||||
const cache = JSON.parse(raw) as {
|
||||
hosts?: Array<{ timeline?: { items?: Array<Record<string, unknown>> } | null }>;
|
||||
};
|
||||
for (const host of cache.hosts ?? []) {
|
||||
for (const item of host.timeline?.items ?? []) {
|
||||
if (item.kind === "user_message" && item.text === messageText && item.messageId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, prompt),
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
await page.evaluate((messageText) => {
|
||||
const key = "@paseo:replica-cache";
|
||||
const raw = localStorage.getItem(key);
|
||||
if (!raw) throw new Error("Replica cache was not persisted");
|
||||
const cache = JSON.parse(raw) as {
|
||||
hosts?: Array<{ timeline?: { items?: Array<Record<string, unknown>> } | null }>;
|
||||
};
|
||||
const cachedMessage = cache.hosts
|
||||
?.flatMap((host) => host.timeline?.items ?? [])
|
||||
.find((item) => item.kind === "user_message" && item.text === messageText);
|
||||
if (!cachedMessage) throw new Error("Cached user message was not found");
|
||||
delete cachedMessage.messageId;
|
||||
localStorage.setItem(key, JSON.stringify(cache));
|
||||
}, prompt);
|
||||
}
|
||||
|
||||
async function waitForCurrentSubmissionExcludedFromCache(
|
||||
page: Page,
|
||||
prompt: string,
|
||||
): Promise<void> {
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate((messageText) => {
|
||||
const raw = localStorage.getItem("@paseo:replica-cache");
|
||||
if (!raw) return false;
|
||||
const cache = JSON.parse(raw) as {
|
||||
hosts?: Array<{ timeline?: { items?: Array<Record<string, unknown>> } | null }>;
|
||||
};
|
||||
return !cache.hosts
|
||||
?.flatMap((host) => host.timeline?.items ?? [])
|
||||
.some(
|
||||
(item) =>
|
||||
item.kind === "user_message" &&
|
||||
item.text === messageText &&
|
||||
typeof item.clientMessageId === "string" &&
|
||||
item.messageId === undefined,
|
||||
);
|
||||
}, prompt),
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
async function waitForCachedMessageWithoutProviderId(page: Page, prompt: string): Promise<void> {
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate((messageText) => {
|
||||
const raw = localStorage.getItem("@paseo:replica-cache");
|
||||
if (!raw) return false;
|
||||
const cache = JSON.parse(raw) as {
|
||||
hosts?: Array<{ timeline?: { items?: Array<Record<string, unknown>> } | null }>;
|
||||
};
|
||||
return cache.hosts
|
||||
?.flatMap((host) => host.timeline?.items ?? [])
|
||||
.some(
|
||||
(item) =>
|
||||
item.kind === "user_message" &&
|
||||
item.text === messageText &&
|
||||
item.messageId === undefined,
|
||||
);
|
||||
}, prompt),
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
async function expectPendingSubmissionNotRestoredAfterReload(page: Page): Promise<void> {
|
||||
const prompt = "Keep this cached submission pending.";
|
||||
const gate = await installDaemonWebSocketGate(page);
|
||||
const session = await seedMockAgentWorkspace({
|
||||
repoPrefix: "rewind-current-cache-e2e-",
|
||||
title: "Current cache submission e2e",
|
||||
});
|
||||
|
||||
try {
|
||||
await openAgentRoute(page, session);
|
||||
await expectComposerVisible(page);
|
||||
gate.holdNextClientRequest("send_agent_message_request");
|
||||
await submitMessage(page, prompt);
|
||||
await gate.waitForHeldClientRequest();
|
||||
await waitForCurrentSubmissionExcludedFromCache(page, prompt);
|
||||
await gate.drop();
|
||||
await page.reload();
|
||||
|
||||
await expect(userMessage(page, prompt)).toHaveCount(0);
|
||||
} finally {
|
||||
gate.restore();
|
||||
await session.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
test.describe("Rewind sheet", () => {
|
||||
test("does not restore a local-only submission from the display cache", async ({ page }) => {
|
||||
await expectPendingSubmissionNotRestoredAfterReload(page);
|
||||
});
|
||||
|
||||
test("does not invent rewind identity for an ID-less cached message", async ({ page }) => {
|
||||
const prompt = "Restore this rewind identity from the legacy cache.";
|
||||
const gate = await installDaemonWebSocketGate(page);
|
||||
const session = await seedMockAgentWorkspace({
|
||||
repoPrefix: "rewind-cache-upgrade-e2e-",
|
||||
title: "Rewind cache upgrade e2e",
|
||||
initialPrompt: prompt,
|
||||
});
|
||||
let heldTimelineRequest = false;
|
||||
|
||||
try {
|
||||
await openAgentRoute(page, session);
|
||||
await expectUserMessageVisible(page, prompt);
|
||||
await rewriteCachedMessageAsLegacyRow(page, prompt);
|
||||
gate.holdNextClientRequest("fetch_agent_timeline_request");
|
||||
await page.reload();
|
||||
await gate.waitForHeldClientRequest();
|
||||
heldTimelineRequest = true;
|
||||
|
||||
const restoredMessage = userMessage(page, prompt);
|
||||
await expect(restoredMessage).toBeVisible();
|
||||
await restoredMessage.hover();
|
||||
await expect(restoredMessage.getByTestId("rewind-menu-trigger")).toHaveCount(0);
|
||||
await waitForCachedMessageWithoutProviderId(page, prompt);
|
||||
} finally {
|
||||
if (heldTimelineRequest) gate.releaseHeldClientRequest();
|
||||
gate.restore();
|
||||
await session.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("rewinds from a user message sheet option", async ({ page }) => {
|
||||
const firstPrompt = "emit 1 coalesced agent stream updates for first rewind turn.";
|
||||
const secondPrompt = "Prepare deleted rewind turn assistant content.";
|
||||
@@ -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,17 +1,17 @@
|
||||
import { expect, test, type Page } from "./fixtures";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
import { expect, test, type Page } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import {
|
||||
addFakeScheduleHostAndReload,
|
||||
buildFakeScheduleHostWorkspace,
|
||||
installFakeScheduleHost,
|
||||
} from "./helpers/schedule-fake-host";
|
||||
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { escapeRegex } from "./helpers/regex";
|
||||
import { expectNoTruncation } from "./helpers/no-truncation";
|
||||
import { expectSettled, expectStableHeight } from "./helpers/settled";
|
||||
import { waitForSidebarHydration } from "./helpers/workspace-ui";
|
||||
import { buildSchedulesRoute } from "../src/utils/host-routes";
|
||||
} from "../support/helpers/schedule-fake-host";
|
||||
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { escapeRegex } from "../support/helpers/regex";
|
||||
import { expectNoTruncation } from "../support/helpers/no-truncation";
|
||||
import { expectSettled, expectStableHeight } from "../support/helpers/settled";
|
||||
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
||||
import { buildSchedulesRoute } from "../../src/utils/host-routes";
|
||||
|
||||
const MOBILE_SHEET_VIEWPORT = { width: 390, height: 844 };
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { expect, test } from "./fixtures";
|
||||
import { gotoAppShell, openSettings } from "./helpers/app";
|
||||
import { getE2EDaemonPort } from "./helpers/daemon-port";
|
||||
import { TEST_HOST_LABEL } from "./helpers/daemon-registry";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { gotoAppShell, openSettings } from "../support/helpers/app";
|
||||
import { getE2EDaemonPort } from "../support/helpers/daemon-port";
|
||||
import { TEST_HOST_LABEL } from "../support/helpers/daemon-registry";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import {
|
||||
expectSettingsHeader,
|
||||
openSettingsHost,
|
||||
@@ -17,9 +17,8 @@ import {
|
||||
expectHostNoLocalOnlyRows,
|
||||
expectRetiredSidebarSectionsAbsent,
|
||||
expectHostPageVisible,
|
||||
expectLocalHostEntryFirst,
|
||||
seedSavedSettingsHosts,
|
||||
} from "./helpers/settings";
|
||||
} from "../support/helpers/settings";
|
||||
|
||||
test.describe("Settings host page", () => {
|
||||
test("connections section shows the seeded connection endpoint", async ({ page }) => {
|
||||
@@ -95,24 +94,6 @@ test.describe("Settings host page", () => {
|
||||
await expect(updateButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test("a Desktop-managed daemon explains why its update action is disabled", async ({
|
||||
page,
|
||||
desktopManagedOutdatedDaemon,
|
||||
}) => {
|
||||
await seedSavedSettingsHosts(page, [desktopManagedOutdatedDaemon]);
|
||||
await page.reload();
|
||||
await openSettings(page);
|
||||
await openSettingsHost(page, desktopManagedOutdatedDaemon.serverId);
|
||||
await openHostSection(page, desktopManagedOutdatedDaemon.serverId, "host");
|
||||
|
||||
const updateCard = page.getByTestId("host-page-update-card");
|
||||
await expect(updateCard).toBeVisible();
|
||||
await expect(updateCard).toContainText(
|
||||
"This daemon is managed by Paseo Desktop. Update Paseo Desktop on the host.",
|
||||
);
|
||||
await expect(page.getByTestId("host-page-update-button")).toBeDisabled();
|
||||
});
|
||||
|
||||
test("clicking the label pencil reveals the inline editor", async ({ page }) => {
|
||||
const serverId = getServerId();
|
||||
|
||||
@@ -161,46 +142,4 @@ test.describe("Settings host page", () => {
|
||||
await expectHostLabelDisplayed(page);
|
||||
await expectHostActionCards(page, serverId);
|
||||
});
|
||||
|
||||
test("sidebar pins the local daemon host first", async ({ page }) => {
|
||||
const serverId = getServerId();
|
||||
|
||||
// Simulate the Electron desktop bridge so `useIsLocalDaemon` resolves the
|
||||
// seeded host to the local daemon. `manageBuiltInDaemon: false` (returned
|
||||
// from get_desktop_settings) bypasses the desktop bootstrap flow so only
|
||||
// the sidebar's status query runs against the seeded test daemon.
|
||||
await page.addInitScript((localServerId) => {
|
||||
(window as unknown as { paseoDesktop: unknown }).paseoDesktop = {
|
||||
platform: "darwin",
|
||||
invoke: async (command: string) => {
|
||||
if (command === "desktop_daemon_status") {
|
||||
return {
|
||||
serverId: localServerId,
|
||||
status: "running",
|
||||
listen: null,
|
||||
hostname: null,
|
||||
pid: null,
|
||||
home: "",
|
||||
version: null,
|
||||
desktopManaged: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
if (command === "get_desktop_settings") {
|
||||
return {
|
||||
releaseChannel: "stable",
|
||||
daemon: { manageBuiltInDaemon: false, keepRunningAfterQuit: true },
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getPendingOpenProject: async () => null,
|
||||
events: { on: async () => () => undefined },
|
||||
};
|
||||
}, serverId);
|
||||
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
await expectLocalHostEntryFirst(page, serverId);
|
||||
});
|
||||
});
|
||||
@@ -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,11 +1,11 @@
|
||||
import { test, expect, type Page } from "./fixtures";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
import { gotoWorkspace, clickNewTerminal } from "./helpers/launcher";
|
||||
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
|
||||
import { seedMockAgentWorkspace } from "./helpers/mock-agent";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { waitForSidebarHydration } from "./helpers/workspace-ui";
|
||||
import { getVisibleWorkspaceAgentTabIds } from "./helpers/workspace-tabs";
|
||||
import { test, expect, type Page } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import { gotoWorkspace, clickNewTerminal } from "../support/helpers/launcher";
|
||||
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
||||
import { seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
||||
import { getVisibleWorkspaceAgentTabIds } from "../support/helpers/workspace-tabs";
|
||||
|
||||
// Model B sidebar shape: every project — git or non-git, single- or
|
||||
// multi-workspace — renders as the same expandable parent, the deepest sidebar
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Locator } from "@playwright/test";
|
||||
import { expect, test } from "./fixtures";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { seedWorkspace } from "./helpers/seed-client";
|
||||
import { waitForSidebarHydration } from "./helpers/workspace-ui";
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { seedWorkspace } from "../support/helpers/seed-client";
|
||||
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
||||
|
||||
async function rowTestIds(rows: Locator) {
|
||||
return rows.evaluateAll((elements) =>
|
||||
@@ -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,17 +1,17 @@
|
||||
import path from "node:path";
|
||||
import { test, expect } from "./fixtures";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
import { test, expect } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import {
|
||||
closeMobileAgentSidebar,
|
||||
expectMobileAgentSidebarHidden,
|
||||
expectMobileAgentSidebarVisible,
|
||||
openMobileAgentSidebar,
|
||||
pinWorkspaceFromSidebar,
|
||||
} from "./helpers/sidebar";
|
||||
import { seedWorkspace } from "./helpers/seed-client";
|
||||
import { expectWorkspaceHeader } from "./helpers/workspace-ui";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { escapeRegex } from "./helpers/regex";
|
||||
} from "../support/helpers/sidebar";
|
||||
import { seedWorkspace } from "../support/helpers/seed-client";
|
||||
import { expectWorkspaceHeader } from "../support/helpers/workspace-ui";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { escapeRegex } from "../support/helpers/regex";
|
||||
|
||||
const GITHUB_REMOTE_URL = "https://github.com/test-owner/test-repo.git";
|
||||
|
||||
19
packages/app/e2e/browser/startup-loading.spec.ts
Normal file
19
packages/app/e2e/browser/startup-loading.spec.ts
Normal file
@@ -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,15 +1,15 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
|
||||
import { test } from "./fixtures";
|
||||
import { seedWorkspace, type SeedDaemonClient } from "./helpers/seed-client";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { observeTimelineSubscriptions } from "./helpers/timeline-delivery";
|
||||
import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs";
|
||||
import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate";
|
||||
import { test } from "../support/fixtures";
|
||||
import { seedWorkspace, type SeedDaemonClient } from "../support/helpers/seed-client";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { observeTimelineSubscriptions } from "../support/helpers/timeline-delivery";
|
||||
import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs";
|
||||
import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
|
||||
import {
|
||||
expectReconnectingToastGone,
|
||||
expectReconnectingToastVisible,
|
||||
} from "./helpers/workspace-ui";
|
||||
} from "../support/helpers/workspace-ui";
|
||||
|
||||
interface ViewedTimelineScenario {
|
||||
client: SeedDaemonClient;
|
||||
@@ -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(
|
||||
@@ -1,9 +1,12 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { expect, test } from "./fixtures";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
import { seedWorkspace } from "./helpers/seed-client";
|
||||
import { expectWorkspaceAbsentFromSidebar, selectWorkspaceInSidebar } from "./helpers/sidebar";
|
||||
import { waitForSidebarHydration } from "./helpers/workspace-ui";
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import { seedWorkspace } from "../support/helpers/seed-client";
|
||||
import {
|
||||
expectWorkspaceAbsentFromSidebar,
|
||||
selectWorkspaceInSidebar,
|
||||
} from "../support/helpers/sidebar";
|
||||
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
||||
|
||||
test.describe("Workspace archive shortcut", () => {
|
||||
test("archives the selected workspace without removing its local checkout", async ({ page }) => {
|
||||
@@ -1,13 +1,13 @@
|
||||
import { expect, test } from "./fixtures";
|
||||
import { clickNewChat, clickNewTerminal, gotoWorkspace } from "./helpers/launcher";
|
||||
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { clickNewChat, clickNewTerminal, gotoWorkspace } from "../support/helpers/launcher";
|
||||
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
||||
import {
|
||||
expectTerminalSurfaceVisible,
|
||||
focusTerminalSurface,
|
||||
typeInTerminal,
|
||||
setupDeterministicPrompt,
|
||||
waitForTerminalContent,
|
||||
} from "./helpers/terminal-perf";
|
||||
} from "../support/helpers/terminal-perf";
|
||||
|
||||
interface CreatedAgentCwdAssertion {
|
||||
workspaceId: string;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "./fixtures";
|
||||
import { expect, test } from "../support/fixtures";
|
||||
|
||||
const modifier = process.platform === "darwin" ? "Meta" : "Control";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { test } from "./fixtures";
|
||||
import { test } from "../support/fixtures";
|
||||
import {
|
||||
createAgentChatFromLauncher,
|
||||
createStandaloneTerminalFromLauncher,
|
||||
expectTerminalCwd,
|
||||
} from "./helpers/workspace-lifecycle";
|
||||
} from "../support/helpers/workspace-lifecycle";
|
||||
|
||||
test.describe("Workspace lifecycle", () => {
|
||||
// The first test after a spec-file switch can intermittently fail because
|
||||
@@ -1,7 +1,11 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
import { expectWorkspaceTabVisible } from "./helpers/archive-tab";
|
||||
import { expectComposerEditable, expectComposerVisible, submitMessage } from "./helpers/composer";
|
||||
import { clickNewChat, gotoWorkspace } from "./helpers/launcher";
|
||||
import { test, expect } from "../support/fixtures";
|
||||
import { expectWorkspaceTabVisible } from "../support/helpers/archive-tab";
|
||||
import {
|
||||
expectComposerEditable,
|
||||
expectComposerVisible,
|
||||
submitMessage,
|
||||
} from "../support/helpers/composer";
|
||||
import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher";
|
||||
import {
|
||||
assertNewWorkspaceSidebarAndHeader,
|
||||
connectNewWorkspaceDaemonClient,
|
||||
@@ -10,16 +14,16 @@ import {
|
||||
selectWorkspaceIsolation,
|
||||
submitNewWorkspaceEmpty,
|
||||
submitNewWorkspacePrompt,
|
||||
} from "./helpers/new-workspace";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
|
||||
} from "../support/helpers/new-workspace";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
||||
import {
|
||||
expectSubagentRowVisible,
|
||||
openSubagentsTrack,
|
||||
seedParentWithCrossWorkspaceSubagent,
|
||||
} from "./helpers/subagents";
|
||||
import { expectWorkspaceHeader, waitForSidebarHydration } from "./helpers/workspace-ui";
|
||||
import { getVisibleWorkspaceAgentTabIds } from "./helpers/workspace-tabs";
|
||||
} from "../support/helpers/subagents";
|
||||
import { expectWorkspaceHeader, waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
||||
import { getVisibleWorkspaceAgentTabIds } from "../support/helpers/workspace-tabs";
|
||||
|
||||
type NewWorkspaceDaemonClient = Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>;
|
||||
type WorkspaceIndicator = "attention" | "done" | "failed" | "loading" | "needs_input" | "running";
|
||||
@@ -7,18 +7,21 @@ import path from "node:path";
|
||||
import net from "node:net";
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { buildHostWorkspaceRoute, decodeWorkspaceIdFromPathSegment } from "@/utils/host-routes";
|
||||
import { buildSeededHost } from "./helpers/daemon-registry";
|
||||
import { loadDaemonClientConstructor } from "./helpers/daemon-client-loader";
|
||||
import { createNodeWebSocketFactory, type NodeWebSocketFactory } from "./helpers/node-ws-factory";
|
||||
import { withDisabledE2ESpeechEnv } from "./helpers/speech-env";
|
||||
import { buildSeededHost } from "../support/helpers/daemon-registry";
|
||||
import { loadDaemonClientConstructor } from "../support/helpers/daemon-client-loader";
|
||||
import {
|
||||
createNodeWebSocketFactory,
|
||||
type NodeWebSocketFactory,
|
||||
} from "../support/helpers/node-ws-factory";
|
||||
import { withDisabledE2ESpeechEnv } from "../support/helpers/speech-env";
|
||||
import {
|
||||
expectNewWorkspaceProjectSelected,
|
||||
openGlobalNewWorkspaceComposer,
|
||||
selectNewWorkspaceProject,
|
||||
submitNewWorkspaceEmpty,
|
||||
} from "./helpers/new-workspace";
|
||||
import { waitForSidebarHydration } from "./helpers/workspace-ui";
|
||||
import { getVisibleWorkspaceAgentTabIds } from "./helpers/workspace-tabs";
|
||||
} from "../support/helpers/new-workspace";
|
||||
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
||||
import { getVisibleWorkspaceAgentTabIds } from "../support/helpers/workspace-tabs";
|
||||
|
||||
const LEGACY_AGENT_ID = "10000000-0000-4000-8000-000000000001";
|
||||
const SERVER_ID = `srv_restart_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
||||
@@ -240,7 +243,7 @@ async function startRestartDaemon(input: {
|
||||
return startRestartDaemon(input);
|
||||
}
|
||||
|
||||
const serverDir = path.resolve(__dirname, "../../server");
|
||||
const serverDir = path.resolve(__dirname, "../../../server");
|
||||
const tsxBin = execSync("which tsx").toString().trim();
|
||||
const child = spawn(tsxBin, ["scripts/supervisor-entrypoint.ts", "--dev"], {
|
||||
cwd: serverDir,
|
||||
@@ -297,7 +300,7 @@ async function connectRestartDaemonClient(port: number): Promise<RestartDaemonCl
|
||||
|
||||
function loadAppVersion(): string {
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(path.resolve(__dirname, "../package.json"), "utf8"),
|
||||
readFileSync(path.resolve(__dirname, "../../package.json"), "utf8"),
|
||||
) as { version?: unknown };
|
||||
if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
|
||||
throw new Error("Missing app package version");
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test, expect, type Page } from "./fixtures";
|
||||
import { gotoAppShell } from "./helpers/app";
|
||||
import { gotoWorkspace } from "./helpers/launcher";
|
||||
import { test, expect, type Page } from "../support/fixtures";
|
||||
import { gotoAppShell } from "../support/helpers/app";
|
||||
import { gotoWorkspace } from "../support/helpers/launcher";
|
||||
import {
|
||||
assertNewWorkspaceSidebarAndHeader,
|
||||
connectNewWorkspaceDaemonClient,
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
selectNewWorkspaceProject,
|
||||
selectWorkspaceIsolation,
|
||||
submitNewWorkspaceEmpty,
|
||||
} from "./helpers/new-workspace";
|
||||
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
|
||||
import { expectExplorerEntryVisible } from "./helpers/file-explorer";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { waitForSidebarHydration } from "./helpers/workspace-ui";
|
||||
} from "../support/helpers/new-workspace";
|
||||
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
||||
import { expectExplorerEntryVisible } from "../support/helpers/file-explorer";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
||||
|
||||
// Model B reshape: a workspace is the unit, its isolation (local checkout or
|
||||
// worktree) is a CHOICE at creation, and creation NEVER dedupes by
|
||||
@@ -3,23 +3,23 @@ import {
|
||||
buildHostWorkspaceOpenRoute,
|
||||
buildHostWorkspaceRoute,
|
||||
} from "@/utils/host-routes";
|
||||
import { expect, test, type Page } from "./fixtures";
|
||||
import { gotoAppShell, openSettings } from "./helpers/app";
|
||||
import { expect, test, type Page } from "../support/fixtures";
|
||||
import { gotoAppShell, openSettings } from "../support/helpers/app";
|
||||
import {
|
||||
createIdleAgent,
|
||||
expectWorkspaceTabHidden,
|
||||
expectWorkspaceTabVisible,
|
||||
openWorkspaceWithAgents,
|
||||
} from "./helpers/archive-tab";
|
||||
import { expectComposerVisible } from "./helpers/composer";
|
||||
import { daemonWsRoutePattern, getE2EDaemonPort } from "./helpers/daemon-port";
|
||||
import { seedWorkspace } from "./helpers/seed-client";
|
||||
} from "../support/helpers/archive-tab";
|
||||
import { expectComposerVisible } from "../support/helpers/composer";
|
||||
import { daemonWsRoutePattern } from "../support/helpers/daemon-port";
|
||||
import { seedWorkspace } from "../support/helpers/seed-client";
|
||||
import {
|
||||
getVisibleWorkspaceAgentTabIds,
|
||||
expectOnlyWorkspaceAgentTabsVisible,
|
||||
waitForWorkspaceTabsVisible,
|
||||
expectWorkspaceTabsAbsent,
|
||||
} from "./helpers/workspace-tabs";
|
||||
} from "../support/helpers/workspace-tabs";
|
||||
import {
|
||||
expectSidebarWorkspaceSelected,
|
||||
expectWorkspaceHeader,
|
||||
@@ -32,57 +32,14 @@ import {
|
||||
waitForSidebarHydration,
|
||||
workspaceDeckEntryLocator,
|
||||
expectWorkspaceDeckEntryCount,
|
||||
} from "./helpers/workspace-ui";
|
||||
import { clickSettingsBackToWorkspace } from "./helpers/settings";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { injectDesktopBridge, waitForDesktopDaemonStartRequest } from "./helpers/desktop-updates";
|
||||
import { expectAppRoute } from "./helpers/route-assertions";
|
||||
import { installDaemonWebSocketGate } from "./helpers/daemon-websocket-gate";
|
||||
import { addOfflineHostAndReload } from "./helpers/hosts";
|
||||
} from "../support/helpers/workspace-ui";
|
||||
import { clickSettingsBackToWorkspace } from "../support/helpers/settings";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { expectAppRoute } from "../support/helpers/route-assertions";
|
||||
import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
|
||||
import { addOfflineHostAndReload } from "../support/helpers/hosts";
|
||||
|
||||
const LOADING_WORKSPACE_TEXT_PATTERN = /Loading workspace/i;
|
||||
type StartupPresentation = "splash" | "app";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__paseoStartupPresentationTrace?: StartupPresentation[];
|
||||
}
|
||||
}
|
||||
|
||||
async function observeStartupPresentation(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
const trace: StartupPresentation[] = [];
|
||||
window.__paseoStartupPresentationTrace = trace;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const recordPresentation = () => {
|
||||
let presentation: StartupPresentation | null = null;
|
||||
if (document.querySelector('[data-testid="startup-splash"]')) {
|
||||
presentation = "splash";
|
||||
} else if (
|
||||
document.querySelector(
|
||||
'[data-testid="workspace-header-title"], [data-testid="sidebar-settings"]',
|
||||
)
|
||||
) {
|
||||
presentation = "app";
|
||||
}
|
||||
if (presentation && trace.at(-1) !== presentation) {
|
||||
trace.push(presentation);
|
||||
}
|
||||
};
|
||||
new MutationObserver(recordPresentation).observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
recordPresentation();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function getStartupPresentation(page: Page): Promise<StartupPresentation[]> {
|
||||
return page.evaluate(() => window.__paseoStartupPresentationTrace?.slice() ?? []);
|
||||
}
|
||||
|
||||
async function expectNoLoadingWorkspacePane(
|
||||
page: Page,
|
||||
input: { label: string; durationMs?: number },
|
||||
@@ -127,39 +84,6 @@ async function closeFirstVisibleDraftTab(page: Page): Promise<void> {
|
||||
await closeButton.first().click();
|
||||
}
|
||||
|
||||
async function openWorkspaceThroughApp(
|
||||
page: Page,
|
||||
input: {
|
||||
serverId: string;
|
||||
workspace: Awaited<ReturnType<typeof seedWorkspace>>;
|
||||
},
|
||||
): Promise<void> {
|
||||
await gotoAppShell(page);
|
||||
await waitForSidebarHydration(page);
|
||||
await switchWorkspaceViaSidebar({
|
||||
page,
|
||||
serverId: input.serverId,
|
||||
workspaceId: input.workspace.workspaceId,
|
||||
});
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
await expectWorkspaceLocation(page, input);
|
||||
}
|
||||
|
||||
async function expectWorkspaceLocation(
|
||||
page: Page,
|
||||
input: {
|
||||
serverId: string;
|
||||
workspace: Awaited<ReturnType<typeof seedWorkspace>>;
|
||||
},
|
||||
): Promise<void> {
|
||||
const workspaceRoute = buildHostWorkspaceRoute(input.serverId, input.workspace.workspaceId);
|
||||
await expectAppRoute(page, workspaceRoute, { timeout: 30_000 });
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: input.workspace.workspaceName,
|
||||
subtitle: input.workspace.projectDisplayName,
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("Workspace navigation regression", () => {
|
||||
test.describe.configure({ timeout: 240_000 });
|
||||
|
||||
@@ -322,47 +246,6 @@ test.describe("Workspace navigation regression", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("refresh keeps one continuous splash before restoring the workspace route", async ({
|
||||
page,
|
||||
}) => {
|
||||
const serverId = getServerId();
|
||||
const daemonGate = await installDaemonWebSocketGate(page);
|
||||
const workspace = await seedWorkspace({ repoPrefix: "workspace-refresh-route-" });
|
||||
|
||||
try {
|
||||
const agent = await createIdleAgent(workspace.client, {
|
||||
cwd: workspace.repoPath,
|
||||
workspaceId: workspace.workspaceId,
|
||||
title: `workspace-refresh-route-${Date.now()}`,
|
||||
});
|
||||
await injectDesktopBridge(page, {
|
||||
serverId,
|
||||
manageBuiltInDaemon: true,
|
||||
hangDaemonStart: true,
|
||||
desktopSettingsDelayMs: 250,
|
||||
daemonListen: `127.0.0.1:${getE2EDaemonPort()}`,
|
||||
});
|
||||
await openWorkspaceThroughApp(page, { serverId, workspace });
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
await expectWorkspaceTabVisible(page, agent.id);
|
||||
await expectWorkspaceLocation(page, { serverId, workspace });
|
||||
|
||||
await observeStartupPresentation(page);
|
||||
await daemonGate.drop();
|
||||
await page.reload();
|
||||
await waitForDesktopDaemonStartRequest(page);
|
||||
daemonGate.restore();
|
||||
await waitForSidebarHydration(page);
|
||||
|
||||
await expectWorkspaceLocation(page, { serverId, workspace });
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
expect(await getStartupPresentation(page)).toEqual(["splash", "app"]);
|
||||
} finally {
|
||||
daemonGate.restore();
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("sidebar navigation and reload keep workspace selection and tabs aligned", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -1,10 +1,10 @@
|
||||
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
|
||||
import { expect, test } from "./fixtures";
|
||||
import { createIdleAgent } from "./helpers/archive-tab";
|
||||
import { expectComposerVisible } from "./helpers/composer";
|
||||
import { seedWorkspace } from "./helpers/seed-client";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs";
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { createIdleAgent } from "../support/helpers/archive-tab";
|
||||
import { expectComposerVisible } from "../support/helpers/composer";
|
||||
import { seedWorkspace } from "../support/helpers/seed-client";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs";
|
||||
|
||||
test.describe("Workspace pane mounting", () => {
|
||||
test("opening the first split pane keeps the existing agent composer mounted", async ({
|
||||
@@ -1,9 +1,9 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
import { gotoWorkspace } from "./helpers/launcher";
|
||||
import { seedWorkspace, type SeededWorkspace } from "./helpers/seed-client";
|
||||
import { togglePinFromMenu, tabRowPin } from "./helpers/pins";
|
||||
import { expectTerminalTabOpen } from "./helpers/workspace-tabs";
|
||||
import type { PinnedTabTarget } from "../src/workspace-pins/target";
|
||||
import { test, expect } from "../support/fixtures";
|
||||
import { gotoWorkspace } from "../support/helpers/launcher";
|
||||
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
||||
import { togglePinFromMenu, tabRowPin } from "../support/helpers/pins";
|
||||
import { expectTerminalTabOpen } from "../support/helpers/workspace-tabs";
|
||||
import type { PinnedTabTarget } from "../../src/workspace-pins/target";
|
||||
|
||||
const DRAFT_TARGET: PinnedTabTarget = { kind: "draft" };
|
||||
const TERMINAL_TARGET: PinnedTabTarget = { kind: "terminal" };
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
import { test, expect } from "../support/fixtures";
|
||||
import { createTempGitRepo } from "../support/helpers/workspace";
|
||||
import {
|
||||
connectWorkspaceSetupClient,
|
||||
createWorkspaceThroughDaemon,
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
seedProjectForWorkspaceSetup,
|
||||
startWorkspaceScriptFromMenu,
|
||||
waitForWorkspaceSetupProgress,
|
||||
} from "./helpers/workspace-setup";
|
||||
import { waitForWorkspaceTabsVisible } from "./helpers/workspace-tabs";
|
||||
import { getServerId } from "./helpers/server-id";
|
||||
import { buildHostWorkspaceRoute } from "../src/utils/host-routes";
|
||||
} from "../support/helpers/workspace-setup";
|
||||
import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { buildHostWorkspaceRoute } from "../../src/utils/host-routes";
|
||||
|
||||
test("scripts menu resizes when a service row grows after launch", async ({ page }) => {
|
||||
const client = await connectWorkspaceSetupClient();
|
||||
@@ -1,8 +1,8 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { expect, test } from "./fixtures";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
import { clickNewTerminal } from "./helpers/launcher";
|
||||
import { expectTerminalSurfaceVisible } from "./helpers/terminal-perf";
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { createTempGitRepo } from "../support/helpers/workspace";
|
||||
import { clickNewTerminal } from "../support/helpers/launcher";
|
||||
import { expectTerminalSurfaceVisible } from "../support/helpers/terminal-perf";
|
||||
import {
|
||||
connectWorkspaceSetupClient,
|
||||
createWorkspaceThroughDaemon,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
navigateToWorkspaceViaSidebar,
|
||||
openHomeWithProject,
|
||||
seedProjectForWorkspaceSetup,
|
||||
} from "./helpers/workspace-setup";
|
||||
} from "../support/helpers/workspace-setup";
|
||||
|
||||
test.describe("Workspace setup runtime authority", () => {
|
||||
test.describe.configure({ retries: 1 });
|
||||
@@ -1,15 +1,18 @@
|
||||
import { test, expect } from "./fixtures";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
import { test, expect } from "../support/fixtures";
|
||||
import { createTempGitRepo } from "../support/helpers/workspace";
|
||||
import {
|
||||
waitForWorkspaceTabsVisible,
|
||||
expectNoTerminalTabs,
|
||||
clickFirstTerminalTab,
|
||||
expectFirstTerminalTabContains,
|
||||
} from "./helpers/workspace-tabs";
|
||||
import { clickNewChat } from "./helpers/launcher";
|
||||
import { expectComposerVisible } from "./helpers/composer";
|
||||
import { openFileExplorer, expectExplorerEntryVisible } from "./helpers/file-explorer";
|
||||
import { expectTerminalSurfaceVisible, waitForTerminalAttached } from "./helpers/terminal-perf";
|
||||
} from "../support/helpers/workspace-tabs";
|
||||
import { clickNewChat } from "../support/helpers/launcher";
|
||||
import { expectComposerVisible } from "../support/helpers/composer";
|
||||
import { openFileExplorer, expectExplorerEntryVisible } from "../support/helpers/file-explorer";
|
||||
import {
|
||||
expectTerminalSurfaceVisible,
|
||||
waitForTerminalAttached,
|
||||
} from "../support/helpers/terminal-perf";
|
||||
import {
|
||||
connectWorkspaceSetupClient,
|
||||
createWorkspaceThroughDaemon,
|
||||
@@ -21,7 +24,7 @@ import {
|
||||
closeWorkspaceScriptsMenu,
|
||||
seedProjectForWorkspaceSetup,
|
||||
waitForWorkspaceSetupProgress,
|
||||
} from "./helpers/workspace-setup";
|
||||
} from "../support/helpers/workspace-setup";
|
||||
|
||||
interface WorkspaceScriptStarter {
|
||||
startWorkspaceScript(
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user