diff --git a/.github/actions/prepare-orcad-prebuilds/action.yml b/.github/actions/prepare-orcad-prebuilds/action.yml index 635fb9d6d54..126e2b60f40 100644 --- a/.github/actions/prepare-orcad-prebuilds/action.yml +++ b/.github/actions/prepare-orcad-prebuilds/action.yml @@ -63,5 +63,5 @@ runs: fi pnpm build:orcad-prebuilds fi - pnpm build:orcad-prebuilds --require-slots "$(node config/scripts/build-orcad-prebuilds.mjs --print-slot)" - pnpm build:orcad-prebuilds --smoke + node config/scripts/build-orcad-prebuilds.mjs --require-slots "$(node config/scripts/build-orcad-prebuilds.mjs --print-slot)" + node config/scripts/build-orcad-prebuilds.mjs --smoke diff --git a/.github/actions/prepare-wayland-clipboard/action.yml b/.github/actions/prepare-wayland-clipboard/action.yml new file mode 100644 index 00000000000..78c622855bd --- /dev/null +++ b/.github/actions/prepare-wayland-clipboard/action.yml @@ -0,0 +1,53 @@ +name: Prepare Wayland clipboard helper +description: Reuse the source-matched Ubuntu 20.04 helper, validating its architecture and glibc requirements every time. + +inputs: + save-cache: + description: Publish a validated helper from the default branch. + default: 'false' + +runs: + using: composite + steps: + - name: Resolve Wayland clipboard build identity + id: identity + shell: bash + run: | + node --input-type=module <<'NODE' + import { appendFileSync } from 'node:fs' + import { waylandClipboardFingerprint } from './config/scripts/build-wayland-clipboard.mjs' + const fingerprint = waylandClipboardFingerprint(process.cwd(), process.arch) + // Immutable invalid hits need a version bump here before publishing their repair. + appendFileSync(process.env.GITHUB_OUTPUT, `key=wayland-clipboard-v1-${process.arch}-${fingerprint}\n`) + NODE + + - name: Restore Wayland clipboard helper + id: cache + uses: actions/cache/restore@v5 + continue-on-error: true + with: + path: native/wayland-clipboard/.build + key: ${{ steps.identity.outputs.key }} + + - name: Build or validate Wayland clipboard helper + shell: bash + env: + CACHE_HIT: ${{ steps.cache.outputs.cache-hit }} + run: | + if node config/scripts/build-wayland-clipboard.mjs; then + exit 0 + fi + if [ "$CACHE_HIT" != true ]; then + exit 1 + fi + echo 'Discarding an unusable Wayland clipboard cache and rebuilding from source.' + rm -rf -- native/wayland-clipboard/.build + node config/scripts/build-wayland-clipboard.mjs + + - name: Save validated Wayland clipboard helper from main + if: inputs.save-cache == 'true' && steps.cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + uses: actions/cache/save@v5 + continue-on-error: true + with: + path: native/wayland-clipboard/.build + key: ${{ steps.identity.outputs.key }} diff --git a/.github/workflows/ci-cache-warmup-platform-jobs.yml b/.github/workflows/ci-cache-warmup-platform-jobs.yml index 47d1238909d..b4a7be987d0 100644 --- a/.github/workflows/ci-cache-warmup-platform-jobs.yml +++ b/.github/workflows/ci-cache-warmup-platform-jobs.yml @@ -53,6 +53,29 @@ jobs: - name: Verify native cache is usable run: node config/scripts/ensure-native-runtime.mjs --check-only + - name: Cache Windows CLI launcher + uses: actions/cache@v5 + with: + path: | + native/windows-cli-launcher/.build + native/windows-cli-launcher/target + native/windows-agent-hook/.build + native/windows-agent-hook/target + ~/.cargo/registry + key: windows-cli-launcher-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/windows-cli-launcher/**', 'native/windows-agent-hook/**', 'config/scripts/build-windows-cli-launcher.mjs', 'config/scripts/build-windows-agent-hook.mjs', 'config/scripts/windows-rust-executable-build.mjs', 'resources/build/icon.ico', 'package.json') }} + + - name: Build the Windows native executables + shell: pwsh + run: | + if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { + rustup toolchain install stable --profile minimal + rustup default stable + } + node config/scripts/build-windows-agent-hook.mjs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + node config/scripts/build-windows-cli-launcher.mjs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + warm-linux-package-fixtures: concurrency: group: ci-cache-warmup-linux-package-fixtures-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/ci-cache-warmup-platforms.yml b/.github/workflows/ci-cache-warmup-platforms.yml index 7643747da79..8f49c2a63aa 100644 --- a/.github/workflows/ci-cache-warmup-platforms.yml +++ b/.github/workflows/ci-cache-warmup-platforms.yml @@ -34,6 +34,12 @@ on: - 'config/scripts/space-sharing-copy.mjs' - 'config/patches/**' - 'native/windows-registry/**' + - 'native/windows-cli-launcher/**' + - 'native/windows-agent-hook/**' + - 'config/scripts/build-windows-cli-launcher.mjs' + - 'config/scripts/build-windows-agent-hook.mjs' + - 'config/scripts/windows-rust-executable-build.mjs' + - 'resources/build/icon.ico' pull_request: paths: - '.github/workflows/ci-cache-warmup.yml' diff --git a/.github/workflows/ci-cache-warmup.yml b/.github/workflows/ci-cache-warmup.yml index 848cf7ef42a..b44598f7ac6 100644 --- a/.github/workflows/ci-cache-warmup.yml +++ b/.github/workflows/ci-cache-warmup.yml @@ -18,6 +18,9 @@ on: - '.github/actions/prepare-headless-compiler/**' - 'config/scripts/headless-detector-compiler-cache.mjs' - '.github/actions/prepare-linux-package-fixture/**' + - '.github/actions/prepare-wayland-clipboard/**' + - 'native/wayland-clipboard/**' + - 'config/scripts/build-wayland-clipboard.mjs' - 'config/docker/headless-serve-shutdown/**' - 'config/docker/cli-launch-contract/**' - 'config/docker/daemon-shutdown-descendants/**' @@ -48,6 +51,7 @@ on: - '.github/workflows/ci-cache-warmup-platforms.yml' - '.github/actions/prepare-headless-compiler/**' - 'config/scripts/headless-detector-compiler-cache.mjs' + - '.github/actions/prepare-wayland-clipboard/**' permissions: contents: read @@ -83,6 +87,10 @@ jobs: - uses: ./.github/actions/prepare-git-compatibility + - uses: ./.github/actions/prepare-wayland-clipboard + with: + save-cache: 'true' + warm-platforms: if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' uses: ./.github/workflows/ci-cache-warmup-platform-jobs.yml diff --git a/.github/workflows/cloud-deploy-relay-production-capacity-job.yml b/.github/workflows/cloud-deploy-relay-production-capacity-job.yml index dbb44e5f372..728e8eabb65 100644 --- a/.github/workflows/cloud-deploy-relay-production-capacity-job.yml +++ b/.github/workflows/cloud-deploy-relay-production-capacity-job.yml @@ -47,8 +47,8 @@ jobs: timeout-minutes: 75 environment: production env: - # These drains are unpaced, but restart-safe still needs a quiet runtime for the production - # pace window, the same one the same-cap drain uses. + # These drains are unpaced, but restart-safe still waits out the production pace window + # from the drain, the same one the same-cap drain uses. RESTART_SAFE_PACE_WINDOW_MS: '300000' GCP_PROJECT_ID: onorca-cloud GCP_REGION: ${{ vars.PRODUCTION_GCP_REGION }} diff --git a/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml b/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml index 0c973641150..8d659ce3349 100644 --- a/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml +++ b/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml @@ -54,8 +54,7 @@ jobs: # /v1/assign at hosts / window and is re-placed through the director's drain-return # lane. The wave validator admits only its closed set, and anything below the 300000 # default only on US general cells: Asia drains are bound by their targets' accept - # rate. The transition wait below is the 15-min migration lease plus this window, plus - # its excess over 300000, since restart-safe waits a window of quiet after the drain. A + # rate. The transition wait below is the 15-min migration lease plus this window. A # cell on an older image drains at once, or refuses a window above 300000. DRAIN_PACE_WINDOW_MS: ${{ inputs.drain-pace-window-ms }} steps: @@ -504,7 +503,7 @@ jobs: run: echo 'Exact same-cap rollback point verified.' # The sample can age the first one-hour token by up to 20 min; the drain's restart-safe - # wait (20 min at the default pace, 50 at the slowest) gets a fresh one. + # wait (20 min at the default pace, 35 at the slowest) gets a fresh one. - id: drain-auth if: ${{ inputs.mode != 'verify' && env.ROLLBACK_RESUME != 'true' }} uses: google-github-actions/auth@v2 @@ -555,7 +554,7 @@ jobs: # What the cell accepted, which is 0 when an older image fell back to an unpaced drain. echo "drain-applied-pace-window-ms=$(jq -er '.paceWindowMs' <<< "${DRAIN_RESULT}")" \ >> "${GITHUB_OUTPUT}" - # Restart-safe means an empty runtime for a whole pace window after the drain; leases + # Restart-safe means an empty runtime that lasts past the drain's pace window; leases # held by unplaceable hosts are reported, not waited on (c28, 2026-10-01). node dev/scripts/verify-relay-capacity-transition.mjs \ --director-origin "${DIRECTOR_ORIGIN}" --cell-origin "${CELL_ORIGIN}" \ @@ -564,7 +563,7 @@ jobs: --heartbeat either --admission migration-only --draining required \ --activity restart-safe --expected-image-digests "${PREDECESSOR_IMAGE_DIGEST}" \ --pace-window-ms "${DRAIN_PACE_WINDOW_MS}" \ - --timeout-ms "$((900000 + DRAIN_PACE_WINDOW_MS + (DRAIN_PACE_WINDOW_MS > 300000 ? DRAIN_PACE_WINDOW_MS - 300000 : 0)))" + --timeout-ms "$((900000 + DRAIN_PACE_WINDOW_MS))" echo "drain-settled-at=$(date -u +%FT%TZ)" >> "${GITHUB_OUTPUT}" - id: capacity-auth diff --git a/.github/workflows/cloud-operate-relay-production-reserve-watch.yml b/.github/workflows/cloud-operate-relay-production-reserve-watch.yml new file mode 100644 index 00000000000..e045d10928e --- /dev/null +++ b/.github/workflows/cloud-operate-relay-production-reserve-watch.yml @@ -0,0 +1,327 @@ +name: Operate Relay Production Reserve Watch + +# Watches one cell already switched to reserve mode and flips it back to db on the first stop +# rule it trips (step 5 design §9). Manual only. dry-run evaluates and logs, writes nothing. +# An operator watches the run. A refused watch or a flip back that did not land raises an ::error:: +# annotation, as the clock-skew monitor does, and in write mode logs the line the reserve-watch +# alert pages on. Refuses to start while a rollout holds the Cloud SQL rollout lease. +on: + workflow_dispatch: + inputs: + mode: + description: dry-run logs what a trip would write; write flips the cell back on a trip + required: true + default: dry-run + type: choice + options: [dry-run, write] + cell-id: + description: Exact cell id already in reserve mode, e.g. production-gce-c26 + required: true + type: string + duration-minutes: + description: How long to watch, 5 to 300 + required: true + default: '120' + type: string + flipped-at: + description: >- + When the reserve write applied, ISO with a zone, e.g. 2026-10-14T12:00:00Z. The 503 + background is the median of the 10 minutes before it + required: false + default: '' + type: string + background-503-per-minute: + description: >- + Director 503s per minute before the flip; overrides the one measured before flipped-at. + One of the two is required + required: false + default: '' + type: string + unverifiable-limit: + description: Consecutive samples with any unreadable signal that count as a trip + required: true + default: '3' + type: string + confirmation: + description: Required for write, exactly "WRITE_RELAY_CELL_FLAGS " + required: false + type: string + +permissions: + contents: read + id-token: write + +# One watch per cell; why not the flag workflow's group: cloud/docs/relay-workflows.md. +concurrency: + group: relay-production-reserve-watch-${{ inputs.cell-id }} + cancel-in-progress: false + +defaults: + run: + # Explicit bash runs with pipefail, so the script's exit code survives the tee. + shell: bash + working-directory: cloud + +env: + MODE: ${{ inputs.mode }} + CELL_ID: ${{ inputs.cell-id }} + CONFIRMATION: ${{ inputs.confirmation }} + GCP_PROJECT_ID: onorca-cloud + +jobs: + watch: + if: ${{ vars.ORCA_CLOUD_OPERATIONS_ENABLED == 'true' }} + runs-on: ubuntu-latest + # Six 55-minute segments cover the longest watch; under the 360-minute job limit. + timeout-minutes: 350 + environment: production + outputs: + state: ${{ steps.outcome.outputs.state }} + verdict: ${{ steps.outcome.outputs.verdict }} + env: + DURATION_MINUTES: ${{ inputs.duration-minutes }} + FLIPPED_AT: ${{ inputs.flipped-at }} + BACKGROUND_503_PER_MINUTE: ${{ inputs.background-503-per-minute }} + UNVERIFIABLE_LIMIT: ${{ inputs.unverifiable-limit }} + # One segment, after the auth step before it: fixed text, the inputs arrive as env vars. + WATCH_SEGMENT: | + set -euo pipefail + [[ "${CELL_ID}" =~ ^production-gce-c[0-9]{1,3}$ ]] + node -e "if (!/^[^.]+[.][^.]+[.][^.]+$/.test(process.env.ORCA_RELAY_ADMIN_ID_TOKEN ?? '')) process.exit(1)" + node dev/scripts/watch-relay-reserve-cell.mjs watch \ + --mode "${MODE}" --cell-id "${CELL_ID}" \ + --cell-origin "https://${CELL_ID#production-gce-}.relay.onorca.dev" \ + --project-id "${GCP_PROJECT_ID}" --director-origin "https://relay.onorca.dev" \ + --director-service orca-cloud-relay --confirmation "${CONFIRMATION}" \ + --duration-minutes "${DURATION_MINUTES}" --segment-minutes 55 \ + --flipped-at "${FLIPPED_AT}" --background-503-per-minute "${BACKGROUND_503_PER_MINUTE}" \ + --unverifiable-limit "${UNVERIFIABLE_LIMIT}" \ + --state-file "${RUNNER_TEMP}/relay-reserve-watch.json" --github-output "${GITHUB_OUTPUT}" \ + | tee -a "${RUNNER_TEMP}/relay-reserve-watch.jsonl" + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-node@v4 + with: + node-version: 24 + + - id: google-auth-1 + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - uses: google-github-actions/setup-gcloud@v2 + with: + skip_install: true + + - id: watch-1 + name: Watch segment 1 of 6 + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth-1.outputs.id_token }} + run: bash -c "${WATCH_SEGMENT}" + + - id: google-auth-2 + if: ${{ steps.watch-1.outputs.state == 'watching' }} + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - id: watch-2 + name: Watch segment 2 of 6 + if: ${{ steps.watch-1.outputs.state == 'watching' }} + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth-2.outputs.id_token }} + run: bash -c "${WATCH_SEGMENT}" + + - id: google-auth-3 + if: ${{ steps.watch-2.outputs.state == 'watching' }} + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - id: watch-3 + name: Watch segment 3 of 6 + if: ${{ steps.watch-2.outputs.state == 'watching' }} + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth-3.outputs.id_token }} + run: bash -c "${WATCH_SEGMENT}" + + - id: google-auth-4 + if: ${{ steps.watch-3.outputs.state == 'watching' }} + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - id: watch-4 + name: Watch segment 4 of 6 + if: ${{ steps.watch-3.outputs.state == 'watching' }} + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth-4.outputs.id_token }} + run: bash -c "${WATCH_SEGMENT}" + + - id: google-auth-5 + if: ${{ steps.watch-4.outputs.state == 'watching' }} + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - id: watch-5 + name: Watch segment 5 of 6 + if: ${{ steps.watch-4.outputs.state == 'watching' }} + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth-5.outputs.id_token }} + run: bash -c "${WATCH_SEGMENT}" + + - id: google-auth-6 + if: ${{ steps.watch-5.outputs.state == 'watching' }} + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - id: watch-6 + name: Watch segment 6 of 6 + if: ${{ steps.watch-5.outputs.state == 'watching' }} + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth-6.outputs.id_token }} + run: bash -c "${WATCH_SEGMENT}" + + - id: outcome + name: Record the watch outcome + if: ${{ always() }} + run: | + node dev/scripts/watch-relay-reserve-cell.mjs outcome \ + --state-file "${RUNNER_TEMP}/relay-reserve-watch.json" --github-output "${GITHUB_OUTPUT}" + + # The documented flow writes reserve before dispatching, so a refusal can leave it unwatched. + - name: Alarm on a refused watch + if: ${{ always() && steps.outcome.outputs.state == 'refused' }} + env: + WATCH_VERDICT: ${{ steps.outcome.outputs.verdict }} + run: | + node dev/scripts/watch-relay-reserve-cell.mjs alarm --kind refused \ + --mode "${MODE}" --cell-id "${CELL_ID}" --project-id "${GCP_PROJECT_ID}" --verdict "${WATCH_VERDICT}" + + - name: Audit summary + if: ${{ always() }} + env: + WATCH_STATE: ${{ steps.outcome.outputs.state }} + WATCH_VERDICT: ${{ steps.outcome.outputs.verdict }} + run: | + { + echo "### Relay reserve watch: ${MODE} ${CELL_ID}" + echo "Triggered by: ${GITHUB_TRIGGERING_ACTOR}, run ${GITHUB_RUN_ID} attempt ${GITHUB_RUN_ATTEMPT}" + echo "Outcome: ${WATCH_STATE}: ${WATCH_VERDICT}" + echo '```json' + tail -n 20 "${RUNNER_TEMP}/relay-reserve-watch.jsonl" 2>/dev/null || echo '(no output)' + echo '```' + } >> "${GITHUB_STEP_SUMMARY}" + + flip-back: + needs: watch + # Missing data is never healthy: a watch that crashed, timed out or was cancelled (by an + # operator too) flips back like a trip. In dry-run this only plans the write. + if: ${{ always() && vars.ORCA_CLOUD_OPERATIONS_ENABLED == 'true' && needs.watch.result != 'skipped' && !contains(fromJSON('["completed","stopped","refused"]'), needs.watch.outputs.state) }} + runs-on: ubuntu-latest + # A flip back waits for the cell to lease every control: 8-50 minutes, scaled to its controls. + timeout-minutes: 60 + environment: production + # Only the run's per-cell group above, which a running run never yields (cancel-in-progress off). + # Every step runs on always() too, so an ordinary cancel cannot skip the switch-file write; a force-cancel + # can, and leaves the cell in reserve for the operator to flip back by hand. A cancel during the + # lease wait can still kill the process before Postgres records db: the file says db, Postgres reserve. + env: + VERDICT: ${{ needs.watch.outputs.verdict || format('the watch ended {0} without a verdict', needs.watch.result) }} + steps: + - uses: actions/checkout@v4 + if: ${{ always() }} + with: + persist-credentials: false + + - uses: actions/setup-node@v4 + if: ${{ always() }} + with: + node-version: 24 + + - id: google-auth + if: ${{ always() }} + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.PRODUCTION_GCP_RELAY_DEPLOY_SERVICE_ACCOUNT }} + token_format: id_token + id_token_audience: https://relay.onorca.dev/v1/admin/drain + id_token_include_email: true + + - uses: google-github-actions/setup-gcloud@v2 + if: ${{ always() }} + with: + skip_install: true + + - name: Flip the cell back to db, or preview it + if: ${{ always() }} + env: + ORCA_RELAY_ADMIN_ID_TOKEN: ${{ steps.google-auth.outputs.id_token }} + run: | + [[ "${CELL_ID}" =~ ^production-gce-c[0-9]{1,3}$ ]] + ORCA_RELAY_CONTROL_ACCESS_TOKEN="$(gcloud auth print-access-token)" + echo "::add-mask::${ORCA_RELAY_CONTROL_ACCESS_TOKEN}" + export ORCA_RELAY_CONTROL_ACCESS_TOKEN + node dev/scripts/watch-relay-reserve-cell.mjs flip-back \ + --mode "${MODE}" --cell-id "${CELL_ID}" \ + --cell-origin "https://${CELL_ID#production-gce-}.relay.onorca.dev" \ + --project-id "${GCP_PROJECT_ID}" --director-origin "https://relay.onorca.dev" \ + --confirmation "${CONFIRMATION}" --verdict "${VERDICT}" \ + | tee "${RUNNER_TEMP}/relay-reserve-flip-back.jsonl" + + # The flip-back line is logged only once the write (or its dry-run plan) finished; without + # it a missing token, a failed login or a silent cell left the cell in reserve, unwatched. + - name: Alarm when the flip back did not land + if: ${{ always() }} + run: | + if grep -qs '"event":"orca_relay_reserve_watch_flip_back"' "${RUNNER_TEMP}/relay-reserve-flip-back.jsonl"; then + exit 0 + fi + { + echo "## FLIP BACK FAILED: ${CELL_ID} may still be in reserve mode" + echo "Flip it to db by hand with Operate Relay Production Cell Flags." + } >> "${GITHUB_STEP_SUMMARY}" + node dev/scripts/watch-relay-reserve-cell.mjs alarm --kind flip-back-failed \ + --mode "${MODE}" --cell-id "${CELL_ID}" --project-id "${GCP_PROJECT_ID}" --verdict "${VERDICT}" + + - name: Audit summary + if: ${{ always() }} + run: | + { + echo "### Relay reserve watch flip back: ${MODE} ${CELL_ID}" + echo "Triggered by: ${GITHUB_TRIGGERING_ACTOR}, run ${GITHUB_RUN_ID} attempt ${GITHUB_RUN_ATTEMPT}" + echo "Verdict: ${VERDICT}" + echo '```json' + cat "${RUNNER_TEMP}/relay-reserve-flip-back.jsonl" 2>/dev/null || echo '(no output)' + echo '```' + } >> "${GITHUB_STEP_SUMMARY}" diff --git a/.github/workflows/cloud-prove-relay-staging-capacity.yml b/.github/workflows/cloud-prove-relay-staging-capacity.yml index 8ee7116b29e..d841c3f397d 100644 --- a/.github/workflows/cloud-prove-relay-staging-capacity.yml +++ b/.github/workflows/cloud-prove-relay-staging-capacity.yml @@ -69,8 +69,8 @@ jobs: DIRECTOR_SERVICE_NAME: orca-cloud-relay-staging DIRECTOR_ORIGIN: https://relay-staging.onorca.dev CELL_ORIGIN: https://c3.relay-staging.onorca.dev - # These drains are unpaced, but restart-safe still needs a quiet runtime for the production - # pace window, the same one the same-cap drain uses. + # These drains are unpaced, but restart-safe still waits out the production pace window + # from the drain, the same one the same-cap drain uses. RESTART_SAFE_PACE_WINDOW_MS: '300000' TARGET_CELL_ID: staging-gce-c3 FALLBACK_CELL_ORIGIN: https://c2.relay-staging.onorca.dev diff --git a/.github/workflows/cloud-verify.yml b/.github/workflows/cloud-verify.yml index 6c4cb9a53a1..fe4877d47de 100644 --- a/.github/workflows/cloud-verify.yml +++ b/.github/workflows/cloud-verify.yml @@ -4,12 +4,14 @@ on: pull_request: paths: - cloud/** + - src/packages/process-host/** - .github/workflows/cloud-*.yml - .github/actions/cloud-sql-rollout-lease/** push: branches: [main] paths: - cloud/** + - src/packages/process-host/** - .github/workflows/cloud-*.yml - .github/actions/cloud-sql-rollout-lease/** @@ -93,8 +95,8 @@ jobs: - run: pnpm build - run: pnpm typecheck - # Runs in parallel with build. `pnpm test` compiles the one workspace - # package it needs through the relay pretest hook, so it does not depend on + # Runs in parallel with build. `pnpm test` compiles the workspace + # packages it needs through pretest hooks, so it does not depend on # `pnpm build` having run. test: runs-on: ubuntu-22.04 @@ -113,6 +115,7 @@ jobs: --health-timeout 5s --health-retries 10 env: + ORCA_BACKGROUND_LAUNCH: '1' ORCA_PUSH_TEST_DATABASE_URL: postgres://relay_test:relay_test@127.0.0.1:5432/orca_relay_test ORCA_RELAY_TEST_POSTGRES_URL: postgres://relay_test:relay_test@127.0.0.1:5432/orca_relay_test steps: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2408eeaa91e..82c8aa889ec 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -52,7 +52,9 @@ jobs: name: build e2e app if: inputs.test_files == '' || github.event_name != 'pull_request' || inputs.needs_build runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 15 + env: + ORCA_BACKGROUND_LAUNCH: '1' steps: - name: Checkout @@ -60,10 +62,44 @@ jobs: with: ref: ${{ inputs.ref || github.ref }} + - name: Install native build tools + env: + ORCA_E2E_APT_PACKAGES: build-essential python3 + run: &install_e2e_tools | + if [ "$ORCA_E2E_APT_PACKAGES" = 'build-essential python3' ] && + command -v make >/dev/null && command -v g++ >/dev/null && command -v python3 >/dev/null; then + exit 0 + fi + read -r -a packages <<< "$ORCA_E2E_APT_PACKAGES" + # Why apt-mirrors.txt: runner images resolve mirror+file: sources through it, so editing the lists alone leaves Azure in use. + for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do + if [ -f "$source" ]; then + sudo sed -i 's|https*://azure\.archive\.ubuntu\.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' "$source" + fi + done + sudo tee /etc/apt/apt.conf.d/99-orca-e2e >/dev/null <<'APTCONF' + Acquire::http::Timeout "15"; + Acquire::https::Timeout "15"; + Acquire::Retries "1"; + APTCONF + timeout 120 sudo apt-get update + timeout 300 sudo apt-get install -y "${packages[@]}" || timeout 300 sudo apt-get install -y --fix-missing "${packages[@]}" + # Why: the build's plain-Node daemon smoke load resolves node-pty. - uses: ./.github/actions/install-node-dependencies + id: deps with: native-runtime: node + persist-native-cache: 'false' + cache-electron-package: 'true' + + # Save Node's ABI before the Electron preparation replaces the same build paths. + - name: Save compiled Node native modules + if: steps.deps.outputs.native-cache-key != '' && steps.deps.outputs.native-cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ${{ steps.deps.outputs.native-cache-path }} + key: ${{ steps.deps.outputs.native-cache-key }} # Why: building here avoids parallel builds inside Playwright globalSetup; # paired-browser specs also need the standalone web bundle. @@ -88,7 +124,19 @@ jobs: - wait: e2e-cli + - name: Check shared native runtime action + id: native-runtime-action + run: | + if [ -f .github/actions/prepare-native-runtime/action.yml ]; then + echo 'available=true' >> "$GITHUB_OUTPUT" + else + echo 'available=false' >> "$GITHUB_OUTPUT" + fi + + # Only the completed output upload overlaps the change from Node to Electron ABI. - name: Upload E2E build output + id: e2e-build-upload + background: true uses: actions/upload-artifact@v7 with: name: e2e-build-out${{ inputs.artifact_suffix }} @@ -97,46 +145,25 @@ jobs: retention-days: 1 if-no-files-found: error - # Build Electron-native dependencies once per workflow. Consumer shards restore - # this immutable cache instead of compiling the same ABI concurrently. - prepare-native-cache: - name: prepare Electron native cache - if: inputs.test_files == '' || github.event_name != 'pull_request' || inputs.needs_build - runs-on: ubuntu-latest - timeout-minutes: 15 - - steps: - - name: Checkout - uses: actions/checkout@v6 + - name: Prepare shared Electron native runtime + id: electron-native-runtime + if: steps.native-runtime-action.outputs.available == 'true' && steps.deps.outputs.node-version != '' + uses: ./.github/actions/prepare-native-runtime with: - ref: ${{ inputs.ref || github.ref }} + native-runtime: electron + node-version: ${{ steps.deps.outputs.node-version }} - - name: Install native build tools - env: - ORCA_E2E_APT_PACKAGES: build-essential python3 - run: &install_e2e_tools | - read -r -a packages <<< "$ORCA_E2E_APT_PACKAGES" - # Why apt-mirrors.txt: runner images resolve mirror+file: sources through it, so editing the lists alone leaves Azure in use. - for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do - if [ -f "$source" ]; then - sudo sed -i 's|https*://azure\.archive\.ubuntu\.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' "$source" - fi - done - sudo tee /etc/apt/apt.conf.d/99-orca-e2e >/dev/null <<'APTCONF' - Acquire::http::Timeout "15"; - Acquire::https::Timeout "15"; - Acquire::Retries "1"; - APTCONF - timeout 120 sudo apt-get update - timeout 300 sudo apt-get install -y "${packages[@]}" || timeout 300 sudo apt-get install -y --fix-missing "${packages[@]}" - - - uses: ./.github/actions/install-node-dependencies + - name: Prepare Electron natives for older refs + if: steps.native-runtime-action.outputs.available != 'true' || steps.deps.outputs.node-version == '' + uses: ./.github/actions/install-node-dependencies with: native-runtime: electron + - wait: e2e-build-upload + e2e: name: e2e ${{ matrix.shard_name }} - needs: [build, prepare-native-cache] + needs: [build] if: inputs.test_files == '' runs-on: ubuntu-latest timeout-minutes: 30 @@ -263,7 +290,7 @@ jobs: changed-e2e: name: changed e2e specs - needs: [build, prepare-native-cache] + needs: [build] if: inputs.test_files != '' && (github.event_name != 'pull_request' || inputs.run_changed_e2e) runs-on: ubuntu-latest timeout-minutes: 45 @@ -351,7 +378,7 @@ jobs: orcad-serve-mode-switch: name: orca serve Electron/orcad mode switch (D7) - needs: [build, prepare-native-cache] + needs: [build] if: >- inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/orcad-serve-mode-switch.spec.ts') || @@ -411,7 +438,7 @@ jobs: # orcad template for the fixture's target (Debian, linux-x64-glibc), which only this job builds. orcad-ssh-docker: name: ssh on managed orcad (Docker ${{ matrix.shard }}) - needs: [build, prepare-native-cache] + needs: [build] if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/ssh-orcad-') runs-on: ubuntu-latest timeout-minutes: 60 diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index f0edb9c52ee..f6265737195 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -36,6 +36,7 @@ on: - 'config/scripts/mobile-release-check-scope*' - 'config/scripts/mobile-test-change-scope*' - 'config/scripts/pr-code-change-scope.mjs' + - 'config/scripts/pr-package-test-paths.mjs' - 'config/scripts/pr-layout-oracle-route.mjs' # Why main too: two pull requests can each pass against their own base and disagree once both # land, and `verify` never runs on main. The same source paths as above, less the CI inputs only diff --git a/.github/workflows/node-server-tests.yml b/.github/workflows/node-server-tests.yml index 54d8b40e16b..457382b1b91 100644 --- a/.github/workflows/node-server-tests.yml +++ b/.github/workflows/node-server-tests.yml @@ -27,7 +27,7 @@ on: description: Also merge every lane's slot into the desktop orcad template artifact type: boolean default: false - # Release packaging calls this to build the orcad template it ships (design D2). + # Release and dev builds qualify every platform for the template they ship. workflow_call: inputs: ref: @@ -38,11 +38,14 @@ on: description: Upload each lane's release slot and merge them into the orcad-template artifact type: boolean default: false + # Requalify fresh builds daily; publish missing immutable Windows cache keys. + schedule: + - cron: '30 11 * * *' permissions: contents: read -# Main pushes must finish detection before they can supersede relevant qualification. +# Keep template callers independent while replacing superseded PR qualification. concurrency: group: node-server-${{ (inputs.build_template || github.event_name == 'push') && format('run-{0}', github.run_id) || github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ !inputs.build_template && github.event_name != 'push' }} @@ -117,9 +120,8 @@ jobs: concurrency: group: node-server-persistence-${{ matrix.os }}-${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' && github.ref || github.run_id }} cancel-in-progress: ${{ github.event_name == 'push' && !inputs.build_template && inputs.ref == '' }} - # Missing/failed detection runs the full matrix; manual runs remain unconditional. - # A draft carries no platform verdict; readiness re-triggers this workflow. Spelled against - # the event name so the push and schedule paths do not rest on a null property comparison. + # Missing/failed detection runs the full matrix; scheduled and manual runs stay unconditional. + # Drafts re-trigger at readiness; inherited caller events must not depend on PR fields. if: >- ${{ !cancelled() && needs.changes.outputs.should_run != 'false' && (github.event_name != 'pull_request' || github.event.pull_request.draft != true) }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9308d68a268..894469dadb9 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -79,6 +79,7 @@ jobs: /config/.bun-version /config/scripts/check-readme-local-links.mjs /config/scripts/pr-code-change-scope.mjs + /config/scripts/pr-package-test-paths.mjs /config/scripts/pr-layout-oracle-route.mjs /config/scripts/pr-e2e-source-routing.mjs /config/scripts/ci-e2e-job-selection.mjs @@ -219,6 +220,7 @@ jobs: with: bun-version-file: config/.bun-version + # Run gate scripts directly to avoid repeated package-manager startup. - name: Check process-host imports if: '!cancelled()' id: process-host-imports @@ -239,7 +241,7 @@ jobs: PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - bun --bun run check:main-sync-spawn-ratchet + bun config/scripts/check-main-sync-spawn-ratchet.mjs # Keep each check in its own log while sharing this runner. - name: Lint @@ -251,7 +253,7 @@ jobs: PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - pnpm exec oxlint --format github + node node_modules/oxlint/bin/oxlint --format github - name: Reject low-evidence patterns if: needs.code_paths.outputs.static_analysis == 'true' @@ -270,11 +272,11 @@ jobs: PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - pnpm run audit:code-quality:native + node node_modules/oxlint/bin/oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings - name: Enforce type-aware code-quality baseline if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run audit:code-quality:type-aware + run: node node_modules/oxlint/bin/oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings # Mobile installation changes import resolution for the native cycle check. - wait: native-code-quality @@ -294,11 +296,11 @@ jobs: PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - pnpm run check:code-quality:changed -- "${{ github.event.pull_request.base.sha }}" + node config/scripts/check-changed-code-quality.mjs "${{ github.event.pull_request.base.sha }}" - name: Enforce React Doctor on changed lines if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:react-doctor:changed -- "${{ github.event.pull_request.base.sha }}" + run: node config/scripts/check-react-doctor-changed.mjs "${{ github.event.pull_request.base.sha }}" - wait: changed-code-quality @@ -316,15 +318,15 @@ jobs: - name: Check Zustand selector fan-out budget if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:zustand-selector-fanout + run: node config/scripts/zustand-selector-fanout-benchmark.mjs --check - name: Check reliability gate manifest if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:reliability-gates + run: node config/scripts/check-reliability-gates.mjs - name: Enforce dead design-system classes if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:dead-classes + run: node node_modules/oxlint/bin/oxlint --config config/oxlint-dead-classes.json src/renderer - name: Check VM runtime rollback compatibility if: needs.code_paths.outputs.static_analysis == 'true' @@ -350,19 +352,19 @@ jobs: - name: Enforce max-lines ratchet if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:max-lines-ratchet + run: node config/scripts/check-max-lines-ratchet.mjs - name: Enforce ts-nocheck ratchet if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:ts-nocheck-ratchet + run: node config/scripts/check-ts-nocheck-ratchet.mjs - name: Enforce runtime Electron-import ratchet if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:runtime-electron-ratchet + run: node config/scripts/check-runtime-electron-ratchet.mjs - name: Enforce focus-read ratchet if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:focus-read-ratchet + run: node config/scripts/check-focus-read-ratchet.mjs - name: Enforce layout-writer ratchets if: needs.code_paths.outputs.static_analysis == 'true' @@ -370,7 +372,7 @@ jobs: - name: Check Node runtime pin if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run check:node-runtime-pin + run: node config/scripts/check-node-runtime-pin.mjs # Why: extraction writes sorted evidence to an isolated temporary path, # so feature PRs need one normalized AST pass rather than a three-OS matrix. @@ -414,19 +416,19 @@ jobs: - name: Verify the generated RPC params catalog if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run verify:rpc-params-catalog + run: node config/scripts/generate-rpc-params-catalog.mjs --check - name: Verify the generated ACP protocol schema if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run verify:acp-protocol + run: node config/scripts/acp/generate-protocol.mjs --check - name: Verify bundled skill guides if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run verify:bundled-skill-guides + run: node config/scripts/generate-bundled-skill-guides.mjs --check - name: Verify skill freshness manifest if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm run verify:skill-bundle-manifest + run: node config/scripts/generate-skill-bundle-manifest.mjs - name: Verify localization catalogs if: '!cancelled()' @@ -437,7 +439,7 @@ jobs: PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} run: | if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - pnpm run verify:localization-catalogs + bun config/scripts/verify-localization-catalogs.mjs - name: Verify localization coverage if: needs.code_paths.outputs.static_analysis == 'true' @@ -465,11 +467,11 @@ jobs: - name: Check feature wall asset budget if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm check:feature-wall-assets + run: node config/scripts/check-feature-wall-assets.mjs - name: Verify macOS entitlements if: needs.code_paths.outputs.static_analysis == 'true' - run: pnpm verify:macos-entitlements + run: node config/scripts/verify-macos-entitlements.mjs # Planning shares setup and stays off the compiler's critical path. - name: Plan unit selection @@ -1099,6 +1101,15 @@ jobs: src/main/browser/browser-route-h3-egress.electron.test.ts src/main/browser/browser-route-dns-prefetch.electron.test.ts src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts + src/main/browser/browser-popup-compatibility.electron.test.ts + src/main/browser/browser-popup-native-compatibility.electron.test.ts + src/main/browser/browser-popup-background-compatibility.electron.test.ts + + # The isolated glibc-floor build writes only the Wayland helper, so JavaScript can overlap. + - name: Build native components + id: linux-native + background: true + uses: ./.github/actions/prepare-wayland-clipboard # Restore independent fixtures during builds; keep native lifecycle probes serial. - uses: ./.github/actions/prepare-linux-package-fixture @@ -1154,10 +1165,7 @@ jobs: - name: Build mobile web bundle run: pnpm run build:mobile-web - - name: Build native components - run: pnpm run build:native - - - wait: [linux-package-tools, web-client] + - wait: [linux-native, linux-package-tools, web-client] - name: Package unpacked app env: diff --git a/.github/workflows/unit-module-cache-benchmark.yml b/.github/workflows/unit-module-cache-benchmark.yml new file mode 100644 index 00000000000..d15f262d235 --- /dev/null +++ b/.github/workflows/unit-module-cache-benchmark.yml @@ -0,0 +1,49 @@ +name: Unit transform cache comparison + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: unit-module-cache-comparison-${{ github.ref }} + cancel-in-progress: true + +jobs: + baseline: + uses: ./.github/workflows/unit-tests.yml + with: + node_versions: '["24.21.0"]' + runner: ubuntu-24.04-arm + shards: '[{"index":1,"count":5},{"index":2,"count":5},{"index":3,"count":5},{"index":4,"count":5},{"index":5,"count":5}]' + artifact_prefix: baseline-unit + phase_diagnostics: true + run_relay_integration: false + + producer: + needs: baseline + uses: ./.github/workflows/unit-tests.yml + with: + node_versions: '["24.21.0"]' + runner: ubuntu-24.04-arm + shards: '[{"index":1,"count":5},{"index":2,"count":5},{"index":3,"count":5},{"index":4,"count":5},{"index":5,"count":5}]' + artifact_prefix: producer-unit + module_cache: cold + module_cache_namespace: ${{ github.run_id }}-${{ github.run_attempt }} + module_cache_seed: true + phase_diagnostics: true + run_relay_integration: false + + warm: + needs: producer + uses: ./.github/workflows/unit-tests.yml + with: + node_versions: '["24.21.0"]' + runner: ubuntu-24.04-arm + shards: '[{"index":1,"count":5},{"index":2,"count":5},{"index":3,"count":5},{"index":4,"count":5},{"index":5,"count":5}]' + artifact_prefix: warm-unit + module_cache: warm + module_cache_namespace: ${{ github.run_id }}-${{ github.run_attempt }} + phase_diagnostics: true + run_relay_integration: false diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 99d57c48888..330070c0b36 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -31,13 +31,43 @@ on: required: true type: string + module_cache: + description: Transform cache policy (off, cold producer, or warm exact restore). + default: 'off' + type: string + + module_cache_namespace: + description: Isolate a same-source cache comparison from existing archives. + default: '' + type: string + + module_cache_seed: + description: Publish comparison caches from a manually dispatched workflow. + default: false + type: boolean + + phase_diagnostics: + description: Attach bounded per-file worker phase diagnostics. + default: false + type: boolean + + artifact_prefix: + description: Distinguish evidence from repeated same-source comparison lanes. + default: unit + type: string + + run_relay_integration: + description: Run relay contracts alongside normal unit checks. + default: true + type: boolean + permissions: contents: read jobs: test: if: inputs.run_shards - name: tests node ${{ matrix.node }} ${{ matrix.shard.index }}/${{ matrix.shard.count }} + name: ${{ inputs.artifact_prefix != 'unit' && format('{0} ', inputs.artifact_prefix) || '' }}tests node ${{ matrix.node }} ${{ matrix.shard.index }}/${{ matrix.shard.count }} runs-on: ${{ inputs.runner }} timeout-minutes: 60 strategy: @@ -65,17 +95,51 @@ jobs: with: bun-version-file: config/.bun-version + - name: Resolve unit module cache + id: module-cache + if: inputs.module_cache == 'cold' || inputs.module_cache == 'warm' + env: + ORCA_UNIT_SHARD_INDEX: ${{ matrix.shard.index }} + ORCA_UNIT_SHARD_COUNT: ${{ matrix.shard.count }} + ORCA_UNIT_CACHE_NAMESPACE: ${{ inputs.module_cache_namespace }} + run: node config/scripts/ci-unit-module-cache.mjs identity + + - name: Restore exact unit module cache + id: module-cache-restore + if: inputs.module_cache == 'warm' && steps.module-cache.outputs.key != '' + continue-on-error: true + uses: actions/cache/restore@v5 + with: + path: ${{ steps.module-cache.outputs.path }} + key: ${{ steps.module-cache.outputs.key }} + + - name: Validate restored unit module cache or use cold transforms + if: steps.module-cache.outputs.key != '' + env: + ORCA_UNIT_CACHE_RESTORE_HIT: ${{ steps.module-cache-restore.outputs.cache-hit }} + run: node config/scripts/ci-unit-module-cache.mjs prepare + - uses: actions/download-artifact@v8 continue-on-error: true with: name: unit-selection-attempt-${{ github.run_attempt }} path: ci-shards/ + - name: Record full unit discovery + if: inputs.phase_diagnostics + continue-on-error: true + run: >- + node --input-type=module -e ' + import { discoverUnitFiles } from "./config/scripts/ci-unit-files.mjs"; + import { writeAssignment } from "./config/scripts/ci-shard-assignment.mjs"; + writeAssignment("ci-shards/unit-discovery.json", { files: discoverUnitFiles() });' + - name: Test shard env: NODE_COMPILE_CACHE: ${{ runner.temp }}/orca-node-compile-cache-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.node }}-${{ matrix.shard.index }} ORCA_BALANCE_UNIT_SHARDS: '1' ORCA_BACKGROUND_LAUNCH: '1' + ORCA_UNIT_PHASE_REPORT: ${{ inputs.phase_diagnostics && 'ci-shards/unit-phases.json' || '' }} run: | ORCA_SHARD_SOURCE_SHA="$(git rev-parse HEAD)" export ORCA_SHARD_SOURCE_SHA @@ -83,18 +147,37 @@ jobs: --shard=${{ matrix.shard.index }}/${{ matrix.shard.count }} \ ${{ inputs.runner == 'ubuntu-24.04-arm' && '--maxWorkers=4' || '' }} + - name: Seal unit module cache + id: module-cache-seal + if: >- + success() && steps.module-cache.outputs.key != '' && + steps.module-cache-restore.outputs.cache-hit != 'true' && + (github.event_name == 'pull_request' || + github.ref == 'refs/heads/main' && github.event_name != 'pull_request' || + inputs.module_cache_seed && github.event_name == 'workflow_dispatch') + run: node config/scripts/ci-unit-module-cache.mjs seal + + - name: Save unit module cache + if: steps.module-cache-seal.outputs.ready == 'true' + continue-on-error: true + uses: actions/cache/save@v5 + with: + path: ${{ steps.module-cache.outputs.path }} + key: ${{ steps.module-cache.outputs.key }} + - name: Upload unit shard assignment if: always() # Diagnostic upload outages must not change the test verdict. continue-on-error: true uses: actions/upload-artifact@v7 with: - name: unit-shard-node-${{ matrix.node }}-${{ matrix.shard.index }}-attempt-${{ github.run_attempt }} + name: ${{ inputs.artifact_prefix }}-shard-node-${{ matrix.node }}-${{ matrix.shard.index }}-attempt-${{ github.run_attempt }} path: ci-shards/ retention-days: 14 if-no-files-found: warn relay_integration: + if: inputs.run_relay_integration name: relay integration node ${{ matrix.node }} strategy: fail-fast: false diff --git a/cloud/apps/push/src/durable-push-fixture-lock-isolation.test.ts b/cloud/apps/push/src/durable-push-fixture-lock-isolation.test.ts new file mode 100644 index 00000000000..be3a10802c4 --- /dev/null +++ b/cloud/apps/push/src/durable-push-fixture-lock-isolation.test.ts @@ -0,0 +1,42 @@ +import { afterEach, expect, it } from 'vitest' +import { DurablePushStore } from './durable-push-store.js' +import { + cleanupDurablePushFixtures, + DEVICE_HEAD_SQL, + durablePushTestDatabaseUrl, + fixture, + notification, + pauseAfter, + within +} from './durable-push-store.test-fixture.js' + +afterEach(cleanupDurablePushFixtures) + +it.skipIf(!durablePushTestDatabaseUrl)( + 'claims the same device in another schema while a fixture holds its device lock', + async () => { + const first = await fixture() + const second = await fixture() + const [firstScope] = await first.db.query( + 'SELECT current_database() AS database, current_schema() AS schema' + ) + const [secondScope] = await second.db.query( + 'SELECT current_database() AS database, current_schema() AS schema' + ) + expect(firstScope?.database).toBe(secondScope?.database) + expect(firstScope?.schema).not.toBe(secondScope?.schema) + await first.store.accept('host', 'phone', notification(1)) + await second.store.accept('host', 'phone', notification(2)) + const paused = pauseAfter(first.db, DEVICE_HEAD_SQL) + const held = new DurablePushStore(paused.wrapped, first.clock).claim() + await paused.atCandidate + try { + expect(await within(first.store.claim(), 2_000)).toBeNull() + expect((await within(second.store.claim(), 2_000))?.notification.notificationSeq).toBe(2) + } finally { + paused.release() + await held + } + expect((await held)?.notification.notificationSeq).toBe(1) + } +) diff --git a/cloud/apps/push/src/durable-push-store.test-fixture.ts b/cloud/apps/push/src/durable-push-store.test-fixture.ts index 417931878d1..d4fedd46074 100644 --- a/cloud/apps/push/src/durable-push-store.test-fixture.ts +++ b/cloud/apps/push/src/durable-push-store.test-fixture.ts @@ -11,7 +11,10 @@ const cleanups: (() => Promise)[] = [] export async function cleanupDurablePushFixtures(): Promise { await Promise.all(cleanups.splice(0).map((cleanup) => cleanup())) } -export const notification = (seq: number, kind: 'alert' | 'dismiss' = 'alert'): PushNotification => ({ +export const notification = ( + seq: number, + kind: 'alert' | 'dismiss' = 'alert' +): PushNotification => ({ notificationId: `notification-${seq}`, notificationEpoch: 'epoch', notificationSeq: seq, @@ -21,11 +24,26 @@ export const notification = (seq: number, kind: 'alert' | 'dismiss' = 'alert'): body: '', kind }) -// ownDatabase: advisory locks are database-wide, so a test that holds a global key must not share one. + +// Schemas isolate rows, but advisory locks need the same fixture boundary. +function scopeFixtureLocks(database: PushDatabase, scope: string): PushDatabase { + return { + dialect: database.dialect, + query: (sql, params) => database.query(sql, params), + transaction: (run) => database.transaction((tx) => run(scopeFixtureLocks(tx, scope))), + lockQuotaScope: (key) => database.lockQuotaScope(`${scope}:${key}`), + tryLockScope: (key) => database.tryLockScope(`${scope}:${key}`), + tryLockSharedScope: (key) => database.tryLockSharedScope(`${scope}:${key}`), + close: () => database.close() + } +} + +// ownDatabase keeps production lock keys for mixed-revision tests. export async function fixture({ ownDatabase = false } = {}) { const databaseUrl = durablePushTestDatabaseUrl - if (databaseUrl && !process.env.CI && new URL(databaseUrl).port !== '55440') + if (databaseUrl && !process.env.CI && new URL(databaseUrl).port !== '55440') { throw new Error('isolated_postgres_port_required') + } let db: PushDatabase if (databaseUrl) { const admin = new pg.Client({ connectionString: databaseUrl }) @@ -38,7 +56,9 @@ export async function fixture({ ownDatabase = false } = {}) { } finally { try { await admin.query( - ownDatabase ? `DROP DATABASE IF EXISTS ${name}` : `DROP SCHEMA IF EXISTS ${name} CASCADE` + ownDatabase + ? `DROP DATABASE IF EXISTS ${name}` + : `DROP SCHEMA IF EXISTS ${name} CASCADE` ) } finally { await admin.end() @@ -53,7 +73,8 @@ export async function fixture({ ownDatabase = false } = {}) { await admin.query(`CREATE SCHEMA ${name}`) url.searchParams.set('options', `-c search_path=${name}`) } - db = scoped = await openPushDatabase({ databaseUrl: url.toString(), dataDir: '', poolMax: 4 }) + scoped = await openPushDatabase({ databaseUrl: url.toString(), dataDir: '', poolMax: 4 }) + db = ownDatabase ? scoped : scopeFixtureLocks(scoped, name) } else { db = await openInMemoryPushDatabase() cleanups.push(() => db.close()) @@ -117,7 +138,9 @@ export async function within(operation: Promise, ms: number): Promise { try { return await Promise.race([ operation, - new Promise((_, reject) => (timer = setTimeout(() => reject(new Error('claim_blocked')), ms))) + new Promise( + (_, reject) => (timer = setTimeout(() => reject(new Error('claim_blocked')), ms)) + ) ]) } finally { clearTimeout(timer) diff --git a/cloud/apps/relay/package.json b/cloud/apps/relay/package.json index 0a85bc9343e..431156103d2 100644 --- a/cloud/apps/relay/package.json +++ b/cloud/apps/relay/package.json @@ -1,7 +1,7 @@ { "name": "@orca-cloud/relay", - "private": true, "version": "0.0.0", + "private": true, "type": "module", "main": "dist/index.js", "scripts": { @@ -11,7 +11,7 @@ "lint": "tsc -p tsconfig.json --noEmit", "pretest": "pnpm --filter @orca-cloud/postgres-schema build && pnpm --filter @orca-cloud/relay-contract build", "start": "node dist/index.js", - "test": "vitest run", + "test": "node --import tsx --conditions=orca-source ../../dev/scripts/run-relay-tests.mjs", "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { diff --git a/cloud/apps/relay/src/app.ts b/cloud/apps/relay/src/app.ts index 130e53fea38..a9d6c370c16 100644 --- a/cloud/apps/relay/src/app.ts +++ b/cloud/apps/relay/src/app.ts @@ -81,6 +81,11 @@ import { import { createRelayTokenVerifier, readBearer } from './relay-token-verifier.js' import type { RelayJWKSet } from './stale-if-error-jwks.js' import { stagingAsiaProofMembership } from './staging-asia-proof-admission.js' +import { + runInStickySlot, + StickySlotStatements, + type StickySlotSample +} from './sticky-slot-statements.js' const RelayCellConnectionHardCapSchema = z.custom( isRelayCellConnectionHardCap @@ -159,6 +164,7 @@ export function createRelayApp( ) => void recordDrainReturnRetryAfter?: (seconds: number) => void recordAdmissionServiceMs?: (lane: 'sticky' | 'drain-return', durationMs: number) => void + recordStickySlot?: (sample: StickySlotSample) => void recordAssignmentUnavailable?: (cause: AssignmentUnavailableCause) => void recordRegionRequest?: (region: RelayRegion | undefined) => void // Step 3 shadow directory; absent unless the director polls seat feeds. @@ -269,16 +275,25 @@ export function createRelayApp( return context.json({ error: 'assignments_temporarily_unavailable' }, 503) } // The slot's hold, not the request's latency: herd recovery is slots / this. - const timeStickySlot = (lease: { release(): void }): { release(): void } => { + const timeStickySlot = ( + lease: { release(): void }, + queueWaitMs: number + ): { release(): void; measure(work: () => Promise): Promise } => { const startedAt = performance.now() + const statements = new StickySlotStatements() let released = false return { release: () => { if (released) return released = true - operations.recordAdmissionServiceMs?.('sticky', performance.now() - startedAt) - lease.release() - } + try { + operations.recordAdmissionServiceMs?.('sticky', performance.now() - startedAt) + operations.recordStickySlot?.({ queueWaitMs, ...statements.close() }) + } finally { + lease.release() + } + }, + measure: (work) => runInStickySlot(statements, work) } } // Classified server-side from the host's own assignment row, never from a @@ -480,8 +495,11 @@ export function createRelayApp( } let admission: { release(): void } | null = null let lane: AssignmentAdmissionLane = 'placement' + // Charges the sticky slot's SQL to its request; any other lane runs work as is. + let measureHeld = (work: () => Promise): Promise => work() if (body.data.reconnect) { let rejection: AssignmentAdmissionRejection | undefined + const queuedAt = performance.now() const stickySlot = await stickyAssignmentAdmission.acquire(claims.relayHostId, (reason) => { rejection = reason }) @@ -497,12 +515,12 @@ export function createRelayApp( }) return rejectStickyAssignment(context) } - const fastLane = timeStickySlot(stickySlot) + const fastLane = timeStickySlot(stickySlot, performance.now() - queuedAt) let verified: ResolvedRelayAssignment | null = null try { - verified = await operations.assignments.resolve(identity, { - classifyHomeRollIsolation: true - }) + verified = await fastLane.measure(() => + operations.assignments.resolve(identity, { classifyHomeRollIsolation: true }) + ) } catch (error) { fastLane.release() operations.recordAssignmentAdmission?.('sticky-rejected') @@ -546,6 +564,7 @@ export function createRelayApp( } else if (verified) { lane = 'sticky' admission = fastLane + measureHeld = fastLane.measure operations.recordAssignmentAdmission?.('sticky') } else { // An unverified hint joins the placement lane with today's semantics. @@ -574,25 +593,27 @@ export function createRelayApp( let regionCorrection: RegionCorrectionResponse | undefined try { if (body.data.regionCorrection?.action === 'report') { - const current = await operations.assignments.resolve(identity) + const current = await measureHeld(() => operations.assignments.resolve(identity)) if (!current) return context.json({ error: 'assignment_not_found' }, 409) assignment = current } else { // The host asked: it may leave a reserve cell the director proves dead. - assignment = await operations.assignments.assign( - identity, - requestedRegion, - requestedRegion ? targetRegion : undefined, - undefined, - { ...step5, hostRequested: true } + assignment = await measureHeld(() => + operations.assignments.assign( + identity, + requestedRegion, + requestedRegion ? targetRegion : undefined, + undefined, + { ...step5, hostRequested: true } + ) ) } if (body.data.regionCorrection) { try { - regionCorrection = await operations.assignments.exchangeRegionCorrection( - identity, - body.data.regionCorrection, - assignment.assignmentEpoch + const correction = body.data.regionCorrection + const epoch = assignment.assignmentEpoch + regionCorrection = await measureHeld(() => + operations.assignments.exchangeRegionCorrection(identity, correction, epoch) ) } catch (error) { if (body.data.regionCorrection.action === 'report') throw error diff --git a/cloud/apps/relay/src/cell-reserve-admission.blackbox.test.ts b/cloud/apps/relay/src/cell-reserve-admission.blackbox.test.ts index a952679ff53..543d40040f5 100644 --- a/cloud/apps/relay/src/cell-reserve-admission.blackbox.test.ts +++ b/cloud/apps/relay/src/cell-reserve-admission.blackbox.test.ts @@ -330,6 +330,19 @@ describe('a reserve-mode cell admits from memory with its database wedged', () = expect(cell.book(identity, 9)).toEqual([{ outcome: 'ok' }]) }) + it('leaves a drained seat to its drain grace instead of the shorter demotion close', async () => { + const cell = await startCell({ admitMode: 'reserve' }) + const identity = await cell.host() + cell.book(identity, 9) + await cell.connect(identity, 9) + const page = cell.relay.sessions.seatFeed(null) + const joinedAt = 'full' in page ? page.full[0]!.joinedAt : -1 + cell.relay.sessions.drain(120_000) + const seat = { v: 1 as const, userId: 'user-1', relayHostId: identity.hostId, epoch: 9, joinedAt } + expect(cell.relay.sessions.demote(seat)).toBe('draining') + expect(cell.relay.sessions.demote(seat)).toBe('draining') + }) + it('gives every booking unit back when the cell starts to drain', async () => { const cell = await startCell({ admitMode: 'reserve' }) const identity = await cell.host() diff --git a/cloud/apps/relay/src/database.ts b/cloud/apps/relay/src/database.ts index 8569fcca53c..089ecb968e5 100644 --- a/cloud/apps/relay/src/database.ts +++ b/cloud/apps/relay/src/database.ts @@ -14,6 +14,7 @@ import { import { applyPostgresSchema } from './postgres-schema-startup.js' import { POSTGRES_STATEMENT_STATS_MIGRATION } from './postgres-statement-stats.js' import { reportPostgresQueryFailure } from './postgres-query-failure.js' +import { noteStickySlotPoolWait, noteStickySlotStatement } from './sticky-slot-statements.js' import { isPostgresReadTimeout, isRelayDatabaseLayerError, @@ -910,19 +911,25 @@ type PostgresQueryTimeout = () => number | undefined async function timedQuery( client: pg.PoolClient, + connectionId: number, text: string, values: unknown[] | undefined, queryTimeout: PostgresQueryTimeout | undefined ): Promise { - const timeout = queryTimeout?.() - if (timeout === undefined) return values === undefined ? await client.query(text) : await client.query(text, values) - // pg@8.22 reads it at client.js:660; @types/pg does not declare it on QueryConfig. - const config: pg.QueryConfig & { query_timeout: number } = { - text, - ...(values === undefined ? {} : { values }), - query_timeout: timeout + const startedAt = performance.now() + try { + const timeout = queryTimeout?.() + if (timeout === undefined) return values === undefined ? await client.query(text) : await client.query(text, values) + // pg@8.22 reads it at client.js:660; @types/pg does not declare it on QueryConfig. + const config: pg.QueryConfig & { query_timeout: number } = { + text, + ...(values === undefined ? {} : { values }), + query_timeout: timeout + } + return await client.query(config) + } finally { + noteStickySlotStatement(performance.now() - startedAt, connectionId) } - return await client.query(config) } class PostgresTransaction implements RelayDatabase { @@ -938,6 +945,7 @@ class PostgresTransaction implements RelayDatabase { constructor( protected readonly client: pg.PoolClient, + private readonly connectionId: number, private readonly queryTimeout?: PostgresQueryTimeout ) {} @@ -966,7 +974,7 @@ class PostgresTransaction implements RelayDatabase { `WITH final_write AS (${inlinePostgresParameters(sql, params, this.client)}) ` + 'SELECT 1 / (SELECT count(*)::int FROM final_write); COMMIT' try { - await timedQuery(this.client, message, undefined, this.queryTimeout) + await timedQuery(this.client, this.connectionId, message, undefined, this.queryTimeout) } catch (error) { // Simple query stops at the first error, so any server error means COMMIT never ran: // retryable codes take the caller's normal rollback-and-retry path. A lost connection @@ -1017,7 +1025,13 @@ class PostgresTransaction implements RelayDatabase { this.assertNotCommitted() this.failIfPoisoned() try { - const result = await timedQuery(this.client, postgresSql(sql), params, this.queryTimeout) + const result = await timedQuery( + this.client, + this.connectionId, + postgresSql(sql), + params, + this.queryTimeout + ) return returnsRows(sql) ? (result.rows as SqlRow[]) : [{ changes: result.rowCount ?? 0 }] } catch (error) { this.noteFailure(error) @@ -1177,6 +1191,9 @@ export class PostgresDatabase implements RelayDatabase { readonly dialect = 'postgres' as const private readonly pressure: PostgresPoolPressure private readonly holds = new CellInventoryHoldSamples() + // pg-pool hands the same client object back for the same connection. + private readonly connectionIds = new WeakMap() + private nextConnectionId = 1 consumeHoldCounts(): CellInventoryHoldCounts { return this.holds.consumeCounts() @@ -1207,9 +1224,15 @@ export class PostgresDatabase implements RelayDatabase { let client: pg.PoolClient | undefined let lostReply: Error | undefined try { - client = await this.pressure.connect(lane) + client = await this.connect(lane) phase = 'execute' - const result = await timedQuery(client, postgresSql(sql), params, this.queryTimeout) + const result = await timedQuery( + client, + this.idOfConnection(client), + postgresSql(sql), + params, + this.queryTimeout + ) return returnsRows(sql) ? (result.rows as SqlRow[]) : [{ changes: result.rowCount ?? 0 }] } catch (error) { markRelayDatabaseError(error) @@ -1257,15 +1280,18 @@ export class PostgresDatabase implements RelayDatabase { options: RelayTransactionOptions = {} ): Promise { for (let attempt = 1; attempt <= POSTGRES_TRANSACTION_ATTEMPTS; attempt++) { - const client = await this.pressure.connect() - const transaction = new PostgresTransaction(client, this.queryTimeout) + const client = await this.connect() + const connectionId = this.idOfConnection(client) + const transaction = new PostgresTransaction(client, connectionId, this.queryTimeout) let lostReply: Error | undefined try { - await timedQuery(client, 'BEGIN', undefined, this.queryTimeout) + await timedQuery(client, connectionId, 'BEGIN', undefined, this.queryTimeout) const result = await operation(transaction) // Even when the operation caught it: COMMIT must not run after a lost reply. if (transaction.poisonedBy) throw transaction.poisonedBy - if (transaction.open) await timedQuery(client, 'COMMIT', undefined, this.queryTimeout) + if (transaction.open) { + await timedQuery(client, connectionId, 'COMMIT', undefined, this.queryTimeout) + } recordMeasuredHold(this.holds, transaction) this.holds.recordUnavailable(transaction.consumeLockUnavailable()) this.holds.recordLockTimeout(transaction.consumeLockTimeouts()) @@ -1322,6 +1348,24 @@ export class PostgresDatabase implements RelayDatabase { await this.pool.end() } + private async connect(lane?: PostgresPoolLane): Promise { + const startedAt = performance.now() + try { + return await this.pressure.connect(lane) + } finally { + noteStickySlotPoolWait(performance.now() - startedAt) + } + } + + private idOfConnection(client: pg.PoolClient): number { + let id = this.connectionIds.get(client) + if (id === undefined) { + id = this.nextConnectionId++ + this.connectionIds.set(client, id) + } + return id + } + consumePoolPressure(): PostgresPoolPressureCounts { return this.pressure.consumeCounts() } diff --git a/cloud/apps/relay/src/drain-release-cell-row-contention-postgres.test.ts b/cloud/apps/relay/src/drain-release-cell-row-contention-postgres.test.ts index a484fc81246..6e2b0974f16 100644 --- a/cloud/apps/relay/src/drain-release-cell-row-contention-postgres.test.ts +++ b/cloud/apps/relay/src/drain-release-cell-row-contention-postgres.test.ts @@ -32,7 +32,8 @@ const describePostgres = databaseUrl ? describe : describe.skip // asia-east2 cell to the us-central1 database, measured in production. const ASIA_ROUND_TRIP_MS = 171 const LOCAL_ROUND_TRIP_MS = 1 -const WINDOW_MS = 10_000 +// Keep the measured contention rates; repeating them for longer only repeats the same locks. +const WINDOW_MS = 5_000 // Reconnecting hosts dialling the directors during the window. Above the // ~6-15/s the production tail sustained, so the director side is never idle. const DIAL_RATE_PER_SECOND = 20 diff --git a/cloud/apps/relay/src/host-session-preauth-latency.test.ts b/cloud/apps/relay/src/host-session-preauth-latency.test.ts new file mode 100644 index 00000000000..ae7b9eec5d3 --- /dev/null +++ b/cloud/apps/relay/src/host-session-preauth-latency.test.ts @@ -0,0 +1,321 @@ +import { createHash, createHmac } from 'node:crypto' +import { EventEmitter } from 'node:events' +import { + buildHostProofMacInput, + HOST_CHALLENGE_PLAINTEXT_DOMAIN, + RELAY_CLOSE_CODE +} from '@orca-cloud/relay-contract' +import nacl from 'tweetnacl' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type WebSocket from 'ws' +import type { RelayAssignmentStore } from './assignment-store.js' +import type { CellReserveBook } from './cell-reserve-book.js' +import type { RelayConfig } from './config.js' +import type { RelayCredentialStore } from './credential-store.js' +import { HostSessionRegistry, type CellReserveAdmission } from './host-session-registry.js' +import type { RelayRuntimeObserver } from './relay-observability.js' +import type { RelayTokenClaims } from './relay-token-verifier.js' +import { ProcessQueuedByteBudget } from './splice-forwarder.js' + +const CELL_ID = 'staging-gce-c1' + +class FakeSocket extends EventEmitter { + readonly CLOSING = 2 + readonly OPEN = 1 + readonly CLOSED = 3 + readyState = this.OPEN + readonly send = vi.fn() + readonly close = vi.fn((code?: number) => { + this.readyState = this.CLOSED + this.emit('close', code, Buffer.from('')) + }) + + asWebSocket(): WebSocket { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: FakeSocket implements the registry's WebSocket event and lifecycle surface. + return this as unknown as WebSocket + } + + // The peer's close frame: ws leaves OPEN at once and emits 'close' later. + hangUp(): void { + this.readyState = this.CLOSING + } +} + +function hostKey(): { keyPair: nacl.BoxKeyPair; identity: RelayTokenClaims } { + const keyPair = nacl.box.keyPair() + const relayHostId = createHash('sha256') + .update(keyPair.publicKey) + .digest('base64url') + .slice(0, 16) + return { + keyPair, + identity: { + sub: 'user-1', + prof: 'profile-1', + relayHostId, + purpose: 'host-control', + exp: 4_102_444_800 + } + } +} + +function setup(input: { + admitMode: 'db' | 'reserve' + databaseSays: boolean + databaseShedding?: boolean +}) { + let clock = 1_000_000 + const recordHostPreAuth = vi.fn>() + const observer = { + recordAuth: vi.fn(), + recordForwardedBytes: vi.fn(), + recordHttp: vi.fn(), + recordReconnect: vi.fn(), + recordSql: vi.fn(), + recordHostPreAuth + } satisfies RelayRuntimeObserver + const take = vi.fn((userId, relayHostId, epoch) => ({ + userId, + relayHostId, + epoch, + directorId: 'director-1', + expiresAt: clock + 60_000 + })) + const reserveAdmission: CellReserveAdmission = { + mode: () => input.admitMode, + ticketEnforce: () => false, + dryRunEnabled: () => false, + databaseShedding: () => input.databaseShedding ?? false, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: beginProof calls only take() on the reserve book. + book: { take } as unknown as CellReserveBook, + verifyLease: vi.fn(async () => 'absent' as const) + } + let answerDatabase: (valid: boolean) => void = () => {} + const databaseAnswered = new Promise((resolve) => { + answerDatabase = resolve + }) + // Answers at once unless the test holds it with holdDatabase(). + let held = false + const verifyCellAssignment = vi.fn(async () => (held ? databaseAnswered : input.databaseSays)) + const registry = new HostSessionRegistry( + config(), + vi.fn(), + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the host hello path never reads credentials. + {} as RelayCredentialStore, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the host hello path calls only verifyCellAssignment(). + { verifyCellAssignment } as unknown as RelayAssignmentStore, + new ProcessQueuedByteBudget(), + observer, + () => clock, + Math.random, + 'incarnation-1', + undefined, + reserveAdmission + ) + return { + registry, + recordHostPreAuth, + verifyCellAssignment, + take, + holdDatabase: () => { + held = true + }, + answerDatabase: (valid: boolean) => answerDatabase(valid), + advance: (ms: number) => { + clock += ms + } + } +} + +function sendHello(socket: FakeSocket, keyPair: nacl.BoxKeyPair, relayHostId: string): void { + socket.emit( + 'message', + Buffer.from( + JSON.stringify({ + type: 'host-hello', + v: 1, + relayHostId, + assignmentEpoch: 4, + hostPublicKeyB64: Buffer.from(keyPair.publicKey).toString('base64'), + appVersion: '1.4.200' + }) + ), + false + ) +} + +function answerChallenge(socket: FakeSocket, keyPair: nacl.BoxKeyPair): void { + const challenge: unknown = JSON.parse(String(socket.send.mock.calls[0]?.[0])) + const field = (name: string): string => { + const value: unknown = + typeof challenge === 'object' && challenge !== null ? Reflect.get(challenge, name) : null + if (typeof value !== 'string') throw new Error(`host-challenge has no ${name}`) + return value + } + const plaintext = nacl.box.open( + Buffer.from(field('ciphertextB64'), 'base64'), + Buffer.from(field('nonceB64'), 'base64'), + Buffer.from(field('relayEphemeralPublicKeyB64'), 'base64'), + keyPair.secretKey + ) + if (!plaintext) throw new Error('host-challenge did not open') + const domainLength = new TextEncoder().encode(`${HOST_CHALLENGE_PLAINTEXT_DOMAIN}\0`).length + const transcriptLength = new DataView( + plaintext.buffer, + plaintext.byteOffset + domainLength, + 4 + ).getUint32(0, false) + const transcriptStart = domainLength + 4 + const transcript = plaintext.slice(transcriptStart, transcriptStart + transcriptLength) + const secret = plaintext.slice(transcriptStart + transcriptLength) + socket.emit( + 'message', + Buffer.from( + JSON.stringify({ + type: 'host-challenge-ack', + challengeId: field('challengeId'), + proofB64: createHmac('sha256', secret) + .update(buildHostProofMacInput(transcript)) + .digest('base64') + }) + ), + false + ) +} + +describe('host pre-auth latency', () => { + afterEach(() => vi.useRealTimers()) + + it('records a booked hello on the reserve path, from accept to a verified proof', async () => { + const h = setup({ admitMode: 'reserve', databaseSays: false }) + const { keyPair, identity } = hostKey() + const socket = new FakeSocket() + h.registry.acceptControl(socket.asWebSocket(), identity) + h.advance(30) + sendHello(socket, keyPair, identity.relayHostId) + await vi.waitFor(() => expect(socket.send).toHaveBeenCalledTimes(1)) + expect(h.recordHostPreAuth).not.toHaveBeenCalled() + h.advance(45) + answerChallenge(socket, keyPair) + + expect(h.take).toHaveBeenCalledOnce() + expect(h.verifyCellAssignment).not.toHaveBeenCalled() + expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('reserve', 'admitted', 75) + }) + + it('records a hello the database refuses on the db path', async () => { + const h = setup({ admitMode: 'db', databaseSays: false }) + const { keyPair, identity } = hostKey() + const socket = new FakeSocket() + h.registry.acceptControl(socket.asWebSocket(), identity) + h.advance(120) + sendHello(socket, keyPair, identity.relayHostId) + await vi.waitFor(() => expect(socket.close).toHaveBeenCalled()) + + expect(socket.close).toHaveBeenCalledWith(RELAY_CLOSE_CODE.WRONG_CELL, 'wrong assignment epoch') + expect(h.take).not.toHaveBeenCalled() + expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('db', 'refused', 120) + }) + + it('records a hello timeout as a refusal before any check, once', () => { + vi.useFakeTimers() + const h = setup({ admitMode: 'db', databaseSays: true }) + const { identity } = hostKey() + const socket = new FakeSocket() + h.registry.acceptControl(socket.asWebSocket(), identity) + h.advance(2_000) + vi.advanceTimersByTime(2_000) + + expect(socket.close).toHaveBeenCalledWith( + RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, + 'host hello timeout' + ) + expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('beforeCheck', 'refused', 2_000) + }) + + it('records a host that hangs up before its hello as abandoned', () => { + const h = setup({ admitMode: 'db', databaseSays: true }) + const { identity } = hostKey() + const socket = new FakeSocket() + h.registry.acceptControl(socket.asWebSocket(), identity) + h.advance(300) + socket.close() + + expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('beforeCheck', 'abandoned', 300) + }) + + it.each([true, false])( + 'records a database answer (%s) that arrives after the host hung up as abandoned', + async (valid) => { + const h = setup({ admitMode: 'db', databaseSays: valid }) + h.holdDatabase() + const { keyPair, identity } = hostKey() + const socket = new FakeSocket() + h.registry.acceptControl(socket.asWebSocket(), identity) + sendHello(socket, keyPair, identity.relayHostId) + await vi.waitFor(() => expect(h.verifyCellAssignment).toHaveBeenCalled()) + socket.hangUp() + h.advance(900) + h.answerDatabase(valid) + await vi.waitFor(() => expect(h.recordHostPreAuth).toHaveBeenCalled()) + + expect(socket.send).not.toHaveBeenCalled() + expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('db', 'abandoned', 900) + } + ) + + it('records a host that hangs up during its proof as abandoned on its path', async () => { + const h = setup({ admitMode: 'reserve', databaseSays: false }) + const { keyPair, identity } = hostKey() + const socket = new FakeSocket() + h.registry.acceptControl(socket.asWebSocket(), identity) + sendHello(socket, keyPair, identity.relayHostId) + await vi.waitFor(() => expect(socket.send).toHaveBeenCalledTimes(1)) + h.advance(700) + socket.close() + + expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('reserve', 'abandoned', 700) + }) + + it('records the pool-pressure shed as a refusal before any check', async () => { + const h = setup({ admitMode: 'reserve', databaseSays: true, databaseShedding: true }) + h.take.mockReturnValue(null) + const { keyPair, identity } = hostKey() + const socket = new FakeSocket() + h.registry.acceptControl(socket.asWebSocket(), identity) + h.advance(40) + sendHello(socket, keyPair, identity.relayHostId) + await vi.waitFor(() => expect(socket.close).toHaveBeenCalled()) + + expect(h.verifyCellAssignment).not.toHaveBeenCalled() + expect(h.recordHostPreAuth).toHaveBeenCalledExactlyOnceWith('beforeCheck', 'refused', 40) + }) +}) + +function config(): RelayConfig { + return { + port: 8080, + publicUrl: 'https://c1.relay.example.test', + cellUrl: 'https://c1.relay.example.test', + authIssuer: 'https://auth.example.test', + authAudience: 'orca-relay', + jwksUrl: 'https://auth.example.test/jwks', + assignmentSigningKey: new TextEncoder().encode('0123456789abcdef0123456789abcdef'), + role: 'cell', + cellId: CELL_ID, + cells: [], + adminAudience: 'https://relay.example.test/v1/admin/drain', + deployServiceAccount: 'deploy@example.test', + runtimeServiceAccount: 'relay-cell@example.test', + adminJwksUrl: 'https://auth.example.test/jwks', + databasePoolMax: 10, + publicAssignmentsEnabled: true, + publicAssignmentConcurrency: 2, + publicAssignmentQueueMax: 128, + publicAssignmentWaitMs: 4_000, + publicResolveConcurrency: 1, + publicResolveWaitMs: 5_000, + publicAssignmentRetryAfterSeconds: 5, + dataDir: './data' + } +} diff --git a/cloud/apps/relay/src/host-session-registry.ts b/cloud/apps/relay/src/host-session-registry.ts index 1e3210a3fda..4bbbf896830 100644 --- a/cloud/apps/relay/src/host-session-registry.ts +++ b/cloud/apps/relay/src/host-session-registry.ts @@ -42,6 +42,8 @@ import { relayHostLogDigest } from './relay-host-log-digest.js' import type { RelayTokenClaims } from './relay-token-verifier.js' import { percentile, + type HostPreAuthOutcome, + type HostPreAuthPath, type RelayClientAcceptStage, type RelayClientAcceptTimedStage, type RelayRuntimeObserver @@ -186,26 +188,44 @@ function readControlFrame( socket: WebSocket, timeoutMs: number, timeoutReason: string, - receive: (raw: RawData, isBinary: boolean) => void + receive: (raw: RawData, isBinary: boolean) => void, + ended?: (how: 'timeout' | 'closed') => void ): void { - if (socket.readyState !== socket.OPEN) return + if (socket.readyState !== socket.OPEN) { + ended?.('closed') + return + } const timer = setTimeout(() => { finish() + ended?.('timeout') socket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, timeoutReason) }, timeoutMs) function finish(): void { clearTimeout(timer) socket.off('message', onMessage) - socket.off('close', finish) + socket.off('close', onClose) + } + function onClose(): void { + finish() + ended?.('closed') } function onMessage(raw: RawData, isBinary: boolean): void { finish() receive(raw, isBinary) } socket.once('message', onMessage) - socket.once('close', finish) + socket.once('close', onClose) } +// Records one host control's pre-auth time, from the cell accepting the WebSocket to the +// hello's admission and proof being decided, exactly once. +type SettlePreAuth = (outcome: HostPreAuthOutcome, path: HostPreAuthPath) => void + +const preAuthEnded = + (settle: SettlePreAuth, path: HostPreAuthPath) => + (how: 'timeout' | 'closed'): void => + settle(how === 'timeout' ? 'refused' : 'abandoned', path) + // Hosts abandon connects after 15s; waiting much longer than that behind a // stalled predecessor only accumulates doomed sockets. const ACTIVATION_QUEUE_WAIT_MS = 30_000 @@ -699,7 +719,7 @@ export class HostSessionRegistry { // A director names this seat the loser of a duplicate: stop taking phones for it now, and // close it once its splices have had time to finish. Repeats are one demotion. - demote(request: DemoteRequest): 'demoted' | 'already-demoted' | 'not-seated' | 'off' { + demote(request: DemoteRequest): 'demoted' | 'already-demoted' | 'draining' | 'not-seated' | 'off' { // Only a reserve-mode cell takes demotions, so flipping admitMode back reverts the route. if (!this.reserveMode()) return 'off' const session = this.get(request) @@ -715,6 +735,8 @@ export class HostSessionRegistry { ) { return 'not-seated' } + // A drain already told this host to go: its own grace stands, never cut to the demotion's. + if (this.drainSentHosts.has(session.relayHostId) || session.regionalDrainAttemptId) return 'draining' const socket = session.socket if (this.demotedSeats.get(key) === socket) return 'already-demoted' this.demotedSeats.set(key, socket) @@ -1212,35 +1234,64 @@ export class HostSessionRegistry { hostCapabilities?: ReadonlySet, assignmentLease?: string ): void { + const settlePreAuth = this.startPreAuth(socket) + const refuse = (code: number, reason: string): void => { + settlePreAuth('refused', 'beforeCheck') + socket.close(code, reason) + } if (this.idleAttempts.has(identity.relayHostId)) { - socket.close(RELAY_CLOSE_CODE.WRONG_CELL, 'idle cutover in progress') + refuse(RELAY_CLOSE_CODE.WRONG_CELL, 'idle cutover in progress') return } // Keyed by socket, not session: a rebind swaps the session's socket, and the // successor's own advertisement is the only one that describes its decoder. if (hostCapabilities?.size) this.hostCapabilities.set(socket, hostCapabilities) if (this.draining) { - socket.close(RELAY_CLOSE_CODE.DRAINING, 'relay draining') + refuse(RELAY_CLOSE_CODE.DRAINING, 'relay draining') return } - readControlFrame(socket, 2_000, 'host hello timeout', (raw, isBinary) => { - if (isBinary) { - socket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'host hello must be text') - return - } - this.guardSessionTask( - () => - this.beginProof( - socket, - identity, - payload(raw, 'host-hello'), - connectionInclusionWatermark, - assignmentLease - ), - socket, - 'host hello proof' - ) - }) + readControlFrame( + socket, + 2_000, + 'host hello timeout', + (raw, isBinary) => { + if (isBinary) { + refuse(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'host hello must be text') + return + } + this.guardSessionTask( + () => + this.beginProof( + socket, + identity, + payload(raw, 'host-hello'), + settlePreAuth, + connectionInclusionWatermark, + assignmentLease + ).catch((error: unknown) => { + // guardSessionTask closes the socket, so any throw refuses the hello as beforeCheck, + // including one after the database answered (e.g. in send or the session lookup). + settlePreAuth('refused', 'beforeCheck') + throw error + }), + socket, + 'host hello proof' + ) + }, + preAuthEnded(settlePreAuth, 'beforeCheck') + ) + } + + private startPreAuth(socket: WebSocket): SettlePreAuth { + const startedAt = this.now() + let settled = false + return (outcome, path) => { + if (settled) return + settled = true + // A decision made after the host hung up never reached it, so it counts as abandoned. + const recorded = socket.readyState === socket.OPEN ? outcome : 'abandoned' + this.observer.recordHostPreAuth?.(path, recorded, nonNegativeMs(this.now() - startedAt)) + } } // A dependency failure (e.g. a database connect timeout) must cost one @@ -1379,29 +1430,34 @@ export class HostSessionRegistry { socket: WebSocket, identity: RelayTokenClaims, candidate: unknown, + settlePreAuth: SettlePreAuth, connectionInclusionWatermark?: number, assignmentLease?: string ): Promise { + // Which check decided the hello; refusals before one answers stay 'beforeCheck'. + let path: HostPreAuthPath = 'beforeCheck' + const refuse = (code: number, reason: string): void => { + settlePreAuth('refused', path) + this.observer.recordAuth(false) + socket.close(code, reason) + } const hello = HostHelloSchema.safeParse(candidate) const hostPublicKey = hello.success ? decodeCanonicalBase64(hello.data.hostPublicKeyB64, 32) : null if (!hello.success) { - this.observer.recordAuth(false) - socket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'invalid host hello') + refuse(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'invalid host hello') return } if (!hostPublicKey) { - this.observer.recordAuth(false) - socket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'invalid host public key') + refuse(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'invalid host public key') return } if ( hello.data.relayHostId !== identity.relayHostId || relayHostId(hostPublicKey) !== identity.relayHostId ) { - this.observer.recordAuth(false) - socket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'host key binding mismatch') + refuse(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'host key binding mismatch') return } const leaseShadow = this.config.role === 'cell' ? this.assignmentLeaseShadow : undefined @@ -1434,12 +1490,12 @@ export class HostSessionRegistry { relayHostId: identity.relayHostId, helloEpoch: hello.data.assignmentEpoch })) === 'valid') + if (admittedFromMemory) path = 'reserve' } if (reserveMode && !admittedFromMemory && this.reserveAdmission!.databaseShedding()) { // Memory cannot admit it (e.g. seated here, but at another epoch), and the wedged pool // would only time out its database check: shed it now, as the upgrade shed does. - this.observer.recordAuth(false) - socket.close(RELAY_CLOSE_CODE.LIMIT_EXCEEDED, 'relay temporarily unavailable') + refuse(RELAY_CLOSE_CODE.LIMIT_EXCEEDED, 'relay temporarily unavailable') return } const assignmentReadStartedAt = performance.now() @@ -1462,12 +1518,16 @@ export class HostSessionRegistry { performance.now() - assignmentReadStartedAt ) } + // The `combined` role compares the epoch without querying but is still labelled db. + if (!admittedFromMemory) path = 'db' if (!assignmentValid) { - this.observer.recordAuth(false) - socket.close(RELAY_CLOSE_CODE.WRONG_CELL, 'wrong assignment epoch') + refuse(RELAY_CLOSE_CODE.WRONG_CELL, 'wrong assignment epoch') + return + } + if (socket.readyState !== socket.OPEN) { + settlePreAuth('abandoned', path) return } - if (socket.readyState !== socket.OPEN) return const key = this.key(identity.sub, identity.relayHostId) const existing = this.sessions.get(key) @@ -1513,40 +1573,46 @@ export class HostSessionRegistry { ciphertextB64: Buffer.from(ciphertext).toString('base64'), expiresAt }) - readControlFrame(socket, 10_000, 'host proof timeout', (raw, isBinary) => { - const ack = isBinary - ? null - : HostChallengeAckSchema.safeParse(payload(raw, 'host-challenge-ack')) - const proof = ack?.success ? decodeCanonicalBase64(ack.data.proofB64, 32) : null - if ( - !ack?.success || - ack.data.challengeId !== challengeId || - !proof || - this.now() > expiresAt || - !timingSafeEqual(proof, expectedProof) - ) { - this.observer.recordAuth(false) - socket.close(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'invalid host proof') - return - } - this.observer.recordAuth(true) - this.guardSessionTask( - () => - this.activate( - socket, - identity, - existing ?? null, - generation, - rebind, - hello.data.assignmentEpoch, - hello.data.appVersion, - connectionInclusionWatermark, - admittedFromMemory ? { reservedBy } : undefined - ), - socket, - 'host activation' - ) - }) + readControlFrame( + socket, + 10_000, + 'host proof timeout', + (raw, isBinary) => { + const ack = isBinary + ? null + : HostChallengeAckSchema.safeParse(payload(raw, 'host-challenge-ack')) + const proof = ack?.success ? decodeCanonicalBase64(ack.data.proofB64, 32) : null + if ( + !ack?.success || + ack.data.challengeId !== challengeId || + !proof || + this.now() > expiresAt || + !timingSafeEqual(proof, expectedProof) + ) { + refuse(RELAY_CLOSE_CODE.BAD_OUTER_CREDENTIAL, 'invalid host proof') + return + } + settlePreAuth('admitted', path) + this.observer.recordAuth(true) + this.guardSessionTask( + () => + this.activate( + socket, + identity, + existing ?? null, + generation, + rebind, + hello.data.assignmentEpoch, + hello.data.appVersion, + connectionInclusionWatermark, + admittedFromMemory ? { reservedBy } : undefined + ), + socket, + 'host activation' + ) + }, + preAuthEnded(settlePreAuth, path) + ) } private activate( diff --git a/cloud/apps/relay/src/postgres-lost-reply-postgres.test.ts b/cloud/apps/relay/src/postgres-lost-reply-postgres.test.ts index d189e10f060..d89c6dcbf5f 100644 --- a/cloud/apps/relay/src/postgres-lost-reply-postgres.test.ts +++ b/cloud/apps/relay/src/postgres-lost-reply-postgres.test.ts @@ -22,6 +22,7 @@ const PROBE_TABLE = 'relay_lost_reply_probe' async function blackholeProxy(target: URL, trigger: string, delayMs?: number) { const sockets = new Set() const afterTrigger: Buffer[] = [] + let forwardingResumed = false const server: Server = createServer((client) => { const upstream = connect(Number(target.port || 5432), target.hostname) sockets.add(client) @@ -33,7 +34,7 @@ async function blackholeProxy(target: URL, trigger: string, delayMs?: number) { upstream.write(chunk) }) upstream.on('data', (chunk: Buffer) => { - if (!swallowing) client.write(chunk) + if (!swallowing || forwardingResumed) client.write(chunk) else if (delayMs !== undefined) setTimeout(() => client.write(chunk), delayMs) }) for (const socket of [client, upstream]) { @@ -52,6 +53,9 @@ async function blackholeProxy(target: URL, trigger: string, delayMs?: number) { url.port = String(address.port) return { url: url.toString(), + resumeForwarding: () => { + forwardingResumed = true + }, sentAfterTrigger: () => Buffer.concat(afterTrigger).toString('latin1'), close: async () => { for (const socket of sockets) socket.destroy() @@ -174,6 +178,7 @@ describePostgres('a lost PostgreSQL reply against a real server', () => { { probe: 'stalled_reply_probe' } ]) expect(performance.now() - startedAt).toBeGreaterThan(6_000) + proxy.resumeForwarding() // Not destroyed: the one pooled connection is still the same backend. expect(await pid()).toBe(before) }, 30_000) diff --git a/cloud/apps/relay/src/public-assignment-drain-return-lane.test.ts b/cloud/apps/relay/src/public-assignment-drain-return-lane.test.ts index cd1344dc5b6..361dcad7575 100644 --- a/cloud/apps/relay/src/public-assignment-drain-return-lane.test.ts +++ b/cloud/apps/relay/src/public-assignment-drain-return-lane.test.ts @@ -233,17 +233,17 @@ describe('drain-return lane', () => { expect(redial.headers.get('retry-after')).toBe('2') }) - it('admits a 2,500-host drain across five directors without a sticky or placement rejection', async () => { + it('admits a paced drain across five directors without a sticky or placement rejection', async () => { vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'Date'] }) vi.spyOn(console, 'warn').mockImplementation(() => {}) // Five directors behind a load balancer that sends each request to any of - // them. 2,500 drained hosts over the 300 s pace window, beside ordinary - // reconnects (c29 baseline ~13/s) and a trickle of new placements. + // them. A shorter cohort preserves the production arrival rates: ~8 drained + // hosts/s, ~13 ordinary reconnects/s, and a trickle of new placements. const DIRECTORS = 5 - const DRAINED = 2_500 - const PACE_WINDOW_MS = 300_000 - const ORDINARY = 3_900 - const PLACEMENTS = 30 + const DRAINED = 250 + const PACE_WINDOW_MS = 30_000 + const ORDINARY = 390 + const PLACEMENTS = 3 // Measured c29 2026-10-01: ~5.8 re-placements/s fleet-wide, bound by the // region's inventory lock, so ~172 ms of lock per re-placement. const INVENTORY_LOCK_MS = 172 @@ -348,7 +348,7 @@ describe('drain-return lane', () => { } let elapsedMs = 0 - while (inFlight > 0 && elapsedMs < 1_200_000) { + while (inFlight > 0 && elapsedMs < 120_000) { await vi.advanceTimersByTimeAsync(20) elapsedMs += 20 // Signing the lease completes off the timer queue. @@ -363,10 +363,11 @@ describe('drain-return lane', () => { expect(outcomes.get('sticky')).toBe(ORDINARY) expect(outcomes.get('placement')).toBe(PLACEMENTS) expect(outcomes.get('drain-return')).toBe(DRAINED) + expect(outcomes.get('drain-return-deferred') ?? 0).toBeGreaterThan(0) // Paced returns: fewer than two deferrals per drained host, none past the cap. expect(outcomes.get('drain-return-deferred') ?? 0).toBeLessThan(2 * DRAINED) expect(Math.max(...retryAfters)).toBeLessThanOrEqual(300) - // The cohort finishes at the inventory lock's rate, not minutes behind it. + // Retry rounding and off-thread signing keep their original fixed allowance. expect(elapsedMs).toBeLessThan(DRAINED * INVENTORY_LOCK_MS + 60_000) }, 300_000) }) diff --git a/cloud/apps/relay/src/public-assignment-reconnect-lane.test.ts b/cloud/apps/relay/src/public-assignment-reconnect-lane.test.ts index dde9cd77d22..8b20a760b0c 100644 --- a/cloud/apps/relay/src/public-assignment-reconnect-lane.test.ts +++ b/cloud/apps/relay/src/public-assignment-reconnect-lane.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it, vi } from 'vitest' import { RelayAssignmentRowBusyError, type RelayAssignment } from './assignment-store.js' import type { RelayConfig } from './config.js' +import type { StickySlotSample } from './sticky-slot-statements.js' +import { + ScriptedPoolClient, + scriptedPostgresDatabase +} from './test-fixtures/scripted-postgres-pool.js' const fakes = vi.hoisted(() => ({ verifyRelayToken: vi.fn(async (token: string) => ({ @@ -199,6 +204,64 @@ describe('public assignment reconnect lane', () => { warn.mockRestore() }) + it('reports what each sticky slot spent its hold on', async () => { + const first = 'ffffffffffffffff' + const queued = 'qqqqqqqqqqqqqqqq' + const newcomer = 'nnnnnnnnnnnnnnnn' + const firstHost = new ScriptedPoolClient() + const queuedHost = new ScriptedPoolClient() + // first: verify, assign; newcomer: assign; queued: verify, assign. + const database = scriptedPostgresDatabase([ + firstHost, + firstHost, + new ScriptedPoolClient(), + queuedHost, + queuedHost + ]) + const firstAssign = deferred() + const resolve = vi.fn(async ({ relayHostId }: { relayHostId: string }) => { + await database.query('SELECT verify') + return assignment('cell-r', relayHostId) + }) + const assign = vi.fn(async ({ relayHostId }: { relayHostId: string }) => { + await database.transaction(async (transaction) => { + await transaction.query('SELECT pin') + await transaction.query('UPDATE lease') + }) + if (relayHostId === first) return await firstAssign.promise + return assignment('cell-r', relayHostId) + }) + const samples: StickySlotSample[] = [] + const app = createRelayApp(config(), { + store: {} as never, + assignments: { assign, resolve } as never, + drain: vi.fn(), + ready: vi.fn(async () => true), + recordStickySlot: (sample) => samples.push(sample) + }) + + const holding = app.request('/v1/assign', assignmentRequest(first, { reconnect: true })) + await vi.waitFor(() => expect(firstHost.statements).toHaveLength(5)) + const waiting = app.request('/v1/assign', assignmentRequest(queued, { reconnect: true })) + // A placement request's statements are its own, even while a sticky slot is held. + expect((await app.request('/v1/assign', assignmentRequest(newcomer))).status).toBe(200) + await new Promise((resolveDelay) => setTimeout(resolveDelay, 60)) + firstAssign.resolve(assignment('cell-r', first)) + expect((await holding).status).toBe(200) + expect((await waiting).status).toBe(200) + + // Verify, then BEGIN, two statements and COMMIT, each on that host's own connection. + const roundTrips = (sample: StickySlotSample | undefined) => + [...(sample?.statementMsByConnection ?? [])].map(([id, ms]) => [id, ms.length]) + expect(samples).toHaveLength(2) + expect(samples[0]).toMatchObject({ statements: 5 }) + expect(roundTrips(samples[0])).toEqual([[1, 5]]) + expect(samples[0]?.queueWaitMs).toBeLessThan(40) + expect(samples[1]).toMatchObject({ statements: 5 }) + expect(roundTrips(samples[1])).toEqual([[3, 5]]) + expect(samples[1]?.queueWaitMs).toBeGreaterThanOrEqual(40) + }) + it('never probes for unhinted requests', async () => { const host = 'uuuuuuuuuuuuuuuu' const assign = vi.fn(async () => assignment('cell-u', host)) diff --git a/cloud/apps/relay/src/relay-observability.test.ts b/cloud/apps/relay/src/relay-observability.test.ts index 2517282b947..8e0e209cdf8 100644 --- a/cloud/apps/relay/src/relay-observability.test.ts +++ b/cloud/apps/relay/src/relay-observability.test.ts @@ -5,8 +5,10 @@ import { observeRelayDatabase } from './observed-relay-database.js' import { CONTROL_RTT_RESERVOIR_LIMIT, observedRelayRequests, + PRE_AUTH_RESERVOIR_LIMIT, percentile, RelayObservability, + STICKY_CONNECTION_LIMIT, type RelayProcessCounts } from './relay-observability.js' import { RELAY_FIX_LEVEL } from './relay-fix-level.js' @@ -283,6 +285,75 @@ describe('relay observability', () => { expect(entries[1]).not.toHaveProperty('drainReturnServiceMsP50') }) + it('summarises sticky slots per window and per pooled connection', () => { + const entries: Array> = [] + const observability = new RelayObservability( + { role: 'director', cellId: 'director', region: 'us-central1' }, + (entry) => entries.push(entry) + ) + for (let ms = 1; ms <= 100; ms++) { + observability.recordStickySlot({ + queueWaitMs: ms * 20, + poolWaitMs: ms, + sqlMs: ms * 2, + statements: 14, + // The slow connection serves every other request. + statementMsByConnection: new Map([ms % 2 === 0 ? [1, [300]] : [2, [5, 6]]]) + }) + } + observability.flush(counts) + observability.flush(counts) + + expect(entries[0]).toMatchObject({ + stickySlotsDelta: 100, + stickyQueueWaitMsP50: 1_000, + stickyQueueWaitMsP99: 1_980, + stickyQueueWaitMsMax: 2_000, + stickyPoolWaitMsP50: 50, + stickyPoolWaitMsP99: 99, + stickySqlMsP50: 100, + stickySqlMsP99: 198, + stickyStatementsP50: 14, + stickyStatementsMax: 14, + stickySqlByConnection: { + '1': { + requests: 50, + statements: 50, + statementMsP50: 300, + statementMsP99: 300, + statementMsMax: 300 + }, + '2': { requests: 50, statements: 100, statementMsP50: 5, statementMsP99: 6 } + }, + stickySqlConnectionSamplesDroppedDelta: 0 + }) + expect(entries[1]).not.toHaveProperty('stickySlotsDelta') + expect(entries[1]).not.toHaveProperty('stickySqlByConnection') + }) + + it('bounds the per-connection sticky summary', () => { + const entries: Array> = [] + const observability = new RelayObservability( + { role: 'director', cellId: 'director', region: 'us-central1' }, + (entry) => entries.push(entry) + ) + for (let id = 1; id <= STICKY_CONNECTION_LIMIT + 3; id++) { + observability.recordStickySlot({ + queueWaitMs: 0, + poolWaitMs: 0, + sqlMs: 1, + statements: 1, + statementMsByConnection: new Map([[id, [1]]]) + }) + } + observability.flush(counts) + + expect(Object.keys(entries[0]?.stickySqlByConnection as object)).toHaveLength( + STICKY_CONNECTION_LIMIT + ) + expect(entries[0]).toMatchObject({ stickySqlConnectionSamplesDroppedDelta: 3 }) + }) + it('sums lock waiters per role and table over the samples taken', () => { const entries: Array> = [] const observability = new RelayObservability( @@ -570,6 +641,109 @@ describe('relay observability', () => { expect(entries[0]!.controlRttMsMax).toBe(900) }) + it('summarises host pre-auth latency per path and outcome', () => { + const entries: Array> = [] + const observability = new RelayObservability( + { role: 'cell', cellId: 'staging-gce-c1', region: 'us-central1' }, + (entry) => entries.push(entry) + ) + for (let ms = 1; ms <= 100; ms++) observability.recordHostPreAuth('db', 'admitted', ms) + observability.recordHostPreAuth('db', 'abandoned', 6_000) + observability.recordHostPreAuth('beforeCheck', 'refused', 2_000) + observability.recordHostPreAuth('reserve', 'admitted', 12.3456) + observability.flush(counts) + observability.flush(counts) + + const none = { admitted: { count: 0 }, refused: { count: 0 }, abandoned: { count: 0 } } + const buckets = (filled: Partial>): Record => ({ + '50': 0, + '100': 0, + '200': 0, + '300': 0, + '450': 0, + '700': 0, + '1000': 0, + '2000': 0, + '5000': 0, + '+Inf': 0, + ...filled + }) + expect(entries[0]!.preAuthLatencyMs).toEqual({ + beforeCheck: { + ...none, + refused: { + count: 1, + p50: 2_000, + p95: 2_000, + p99: 2_000, + max: 2_000, + bucketsLeMs: buckets({ '2000': 1 }) + } + }, + db: { + ...none, + admitted: { + count: 100, + p50: 50, + p95: 95, + p99: 99, + max: 100, + bucketsLeMs: buckets({ '50': 50, '100': 50 }) + }, + abandoned: { + count: 1, + p50: 6_000, + p95: 6_000, + p99: 6_000, + max: 6_000, + bucketsLeMs: buckets({ '+Inf': 1 }) + } + }, + reserve: { + ...none, + admitted: { + count: 1, + p50: 12.346, + p95: 12.346, + p99: 12.346, + max: 12.346, + bucketsLeMs: buckets({ '50': 1 }) + } + } + }) + // The window resets, and an empty window publishes counts only. + expect(entries[1]!.preAuthLatencyMs).toEqual({ beforeCheck: none, db: none, reserve: none }) + }) + + it('counts every hello in the pre-auth buckets, inclusive of each bound', () => { + const entries: Array> = [] + const observability = new RelayObservability( + { role: 'cell', cellId: 'staging-gce-c1', region: 'us-central1' }, + (entry) => entries.push(entry) + ) + const flooded = PRE_AUTH_RESERVOIR_LIMIT * 5 + for (let sample = 0; sample < flooded; sample++) { + observability.recordHostPreAuth('reserve', 'admitted', 450) + } + observability.recordHostPreAuth('reserve', 'admitted', 450.001) + observability.recordHostPreAuth('reserve', 'admitted', 9_000) + observability.flush(counts) + + expect(entries[0]).toMatchObject({ + preAuthLatencyMs: { + reserve: { + admitted: { + // Exact past the reservoir cap, even when it declined the slowest hello. + count: flooded + 2, + max: 9_000, + p50: 450, + bucketsLeMs: { '300': 0, '450': flooded, '700': 1, '5000': 0, '+Inf': 1 } + } + } + } + }) + }) + it('observes successful and failed database calls including transactions', async () => { const recordSql = vi.fn() const underlying: RelayDatabase = { diff --git a/cloud/apps/relay/src/relay-observability.ts b/cloud/apps/relay/src/relay-observability.ts index 17dd1ab3a44..6f43248b445 100644 --- a/cloud/apps/relay/src/relay-observability.ts +++ b/cloud/apps/relay/src/relay-observability.ts @@ -6,6 +6,7 @@ import type { CellInventoryHoldCounts } from './cell-inventory-hold-samples.js' import type { PostgresPoolPressureCounts } from './postgres-pool-pressure.js' import type { RelayReadinessGraceEvent, RelayReadinessObservation } from './relay-readiness.js' import { RELAY_FIX_LEVEL } from './relay-fix-level.js' +import type { StickySlotSample } from './sticky-slot-statements.js' export type RelayRuntimeCounts = { totalConnections: number @@ -90,6 +91,7 @@ export interface RelayRuntimeObserver { recordAssignmentRejectionReason?(lane: AssignmentAdmissionLane, reason: string): void recordDrainReturnRetryAfter?(seconds: number): void recordAdmissionServiceMs?(lane: 'sticky' | 'drain-return', durationMs: number): void + recordStickySlot?(sample: StickySlotSample): void recordAssignmentUnavailable?(cause: AssignmentUnavailableCause): void recordDatabaseLockWaitSample?(sample: DatabaseLockWaitSample): void recordRegionRequest?(region: RelayRegion | undefined): void @@ -103,6 +105,42 @@ export interface RelayRuntimeObserver { recordClientAcceptAbandoned?(stage: RelayClientAcceptStage, elapsedMs: number): void recordClientAcceptCompleted?(sample: RelayClientAcceptSample): void recordControlRtt?(rttMs: number): void + recordHostPreAuth?(path: HostPreAuthPath, outcome: HostPreAuthOutcome, durationMs: number): void +} + +// Which check decided a host hello: the cell's memory in reserve mode (a booking, a seat it +// remembers, or a lease), the database assignment check, or neither (beforeCheck: draining, +// idle cutover, a bad or late hello, the pool shed, or a check that threw). +export type HostPreAuthPath = 'beforeCheck' | 'db' | 'reserve' +// Abandoned: the host hung up before a decision, or before the cell could act on one. +export type HostPreAuthOutcome = 'admitted' | 'refused' | 'abandoned' + +// Upper bounds in ms; the last bucket is unbounded. 450 is the step-5 t_preauth gate. +const PRE_AUTH_BUCKET_BOUNDS_MS = [50, 100, 200, 300, 450, 700, 1_000, 2_000, 5_000] as const + +type PreAuthLatencyGroup = { + samplesMs: number[] + observed: number + maxMs: number + // Non-cumulative; one more slot than there are bounds, for the unbounded bucket. + bucketCounts: number[] +} +type PreAuthLatency = Record> + +const emptyPreAuthLatencyGroup = (): PreAuthLatencyGroup => ({ + samplesMs: [], + observed: 0, + maxMs: 0, + bucketCounts: Array.from({ length: PRE_AUTH_BUCKET_BOUNDS_MS.length + 1 }, () => 0) +}) + +const emptyPreAuthLatency = (): PreAuthLatency => { + const byOutcome = (): Record => ({ + admitted: emptyPreAuthLatencyGroup(), + refused: emptyPreAuthLatencyGroup(), + abandoned: emptyPreAuthLatencyGroup() + }) + return { beforeCheck: byOutcome(), db: byOutcome(), reserve: byOutcome() } } // Which serialized accept step the phone had already hung up behind. @@ -148,6 +186,12 @@ type RelayMetricDeltas = { drainReturnRetryAfterSecondsSum: number drainReturnServiceMs: number[] stickyServiceMs: number[] + stickySlotQueueWaitMs: number[] + stickySlotPoolWaitMs: number[] + stickySlotSqlMs: number[] + stickySlotStatements: number[] + stickySlotByConnection: Map + stickySlotConnectionSamplesDropped: number assign503sByCause: Record dbLockWaitSamples: number dbLockWaitersByKey: Record @@ -170,11 +214,41 @@ type RelayMetricDeltas = { controlActivityRecoveries: number controlActivityRecoveryFailures: number hostHellosShed: number + preAuthLatency: PreAuthLatency } +type StickyConnectionStatements = { + requests: number + statements: number + maxMs: number + samplesMs: number[] +} + +// A window sees each pooled connection the sticky lane used; a pool that keeps +// replacing connections must not grow the line without bound. +export const STICKY_CONNECTION_LIMIT = 64 +export const STICKY_STATEMENT_RESERVOIR_LIMIT = 1024 + // A host chooses how often it answers a ping, so the process-wide window is a // reservoir: the heap cost of a flood is capped and the percentiles stay unbiased. export const CONTROL_RTT_RESERVOIR_LIMIT = 1024 +// Per path and outcome; a reconnect burst can put thousands of hellos in one window. +export const PRE_AUTH_RESERVOIR_LIMIT = 1024 + +// Algorithm R: every sample in the window keeps an equal chance of being kept. +function keepInReservoir( + samples: number[], + observedBefore: number, + value: number, + limit: number +): void { + if (samples.length < limit) { + samples.push(value) + return + } + const slot = Math.floor(Math.random() * (observedBefore + 1)) + if (slot < limit) samples[slot] = value +} type MetricWriter = (entry: Record) => void @@ -200,6 +274,12 @@ const emptyDeltas = (): RelayMetricDeltas => ({ drainReturnRetryAfterSecondsSum: 0, drainReturnServiceMs: [], stickyServiceMs: [], + stickySlotQueueWaitMs: [], + stickySlotPoolWaitMs: [], + stickySlotSqlMs: [], + stickySlotStatements: [], + stickySlotByConnection: new Map(), + stickySlotConnectionSamplesDropped: 0, assign503sByCause: {}, dbLockWaitSamples: 0, dbLockWaitersByKey: {}, @@ -227,7 +307,8 @@ const emptyDeltas = (): RelayMetricDeltas => ({ controlRenewalFlushRowsMax: 0, controlActivityRecoveries: 0, controlActivityRecoveryFailures: 0, - hostHellosShed: 0 + hostHellosShed: 0, + preAuthLatency: emptyPreAuthLatency() }) function ascending(values: number[]): number[] { @@ -252,12 +333,18 @@ function roundMs(value: number): number { // enough of it, so the maximum is folded instead. The fold is also not // interchangeable with the sorted last element: it is seeded with zero, so an // all-negative or NaN window reads differently. -function latencySummary(samples: number[]): { p50: number; p95: number; max: number } { - // One sorted copy serves both ranks. +function latencySummary(samples: number[]): { + p50: number + p95: number + p99: number + max: number +} { + // One sorted copy serves every rank. const sorted = samples.length === 0 ? samples : ascending(samples) return { p50: roundMs(nearestRank(sorted, 0.5)), p95: roundMs(nearestRank(sorted, 0.95)), + p99: roundMs(nearestRank(sorted, 0.99)), max: roundMs(samples.reduce((highest, sample) => Math.max(highest, sample), 0)) } } @@ -331,6 +418,36 @@ export class RelayObservability implements RelayRuntimeObserver { else this.deltas.drainReturnServiceMs.push(durationMs) } + recordStickySlot(sample: StickySlotSample): void { + const deltas = this.deltas + deltas.stickySlotQueueWaitMs.push(sample.queueWaitMs) + deltas.stickySlotPoolWaitMs.push(sample.poolWaitMs) + deltas.stickySlotSqlMs.push(sample.sqlMs) + deltas.stickySlotStatements.push(sample.statements) + for (const [connectionId, samplesMs] of sample.statementMsByConnection) { + let connection = deltas.stickySlotByConnection.get(connectionId) + if (!connection) { + if (deltas.stickySlotByConnection.size >= STICKY_CONNECTION_LIMIT) { + deltas.stickySlotConnectionSamplesDropped++ + continue + } + connection = { requests: 0, statements: 0, maxMs: 0, samplesMs: [] } + deltas.stickySlotByConnection.set(connectionId, connection) + } + connection.requests++ + for (const durationMs of samplesMs) { + const observedBefore = connection.statements++ + connection.maxMs = Math.max(connection.maxMs, durationMs) + keepInReservoir( + connection.samplesMs, + observedBefore, + durationMs, + STICKY_STATEMENT_RESERVOIR_LIMIT + ) + } + } + } + recordAssignmentUnavailable(cause: AssignmentUnavailableCause): void { increment(this.deltas.assign503sByCause, cause) } @@ -433,15 +550,23 @@ export class RelayObservability implements RelayRuntimeObserver { } recordControlRtt(rttMs: number): void { - const samples = this.deltas.controlRttSamplesMs const observedBefore = this.deltas.controlRttObserved++ - if (samples.length < CONTROL_RTT_RESERVOIR_LIMIT) { - samples.push(rttMs) - return - } - // Algorithm R: every round trip in the window keeps an equal chance of being kept. - const slot = Math.floor(Math.random() * (observedBefore + 1)) - if (slot < CONTROL_RTT_RESERVOIR_LIMIT) samples[slot] = rttMs + keepInReservoir( + this.deltas.controlRttSamplesMs, + observedBefore, + rttMs, + CONTROL_RTT_RESERVOIR_LIMIT + ) + } + + recordHostPreAuth(path: HostPreAuthPath, outcome: HostPreAuthOutcome, durationMs: number): void { + const group = this.deltas.preAuthLatency[path][outcome] + const observedBefore = group.observed++ + // Folded outside the reservoir so a dropped sample cannot hide the worst hello. + group.maxMs = Math.max(group.maxMs, durationMs) + const bucket = PRE_AUTH_BUCKET_BOUNDS_MS.findIndex((bound) => durationMs <= bound) + group.bucketCounts[bucket === -1 ? PRE_AUTH_BUCKET_BOUNDS_MS.length : bucket]! += 1 + keepInReservoir(group.samplesMs, observedBefore, durationMs, PRE_AUTH_RESERVOIR_LIMIT) } start(readCounts: () => RelayProcessCounts, intervalMs = 30_000): void { @@ -527,6 +652,7 @@ export class RelayObservability implements RelayRuntimeObserver { stickyServiceMsP50: roundMs(percentile(deltas.stickyServiceMs, 0.5)), stickyServiceMsP99: roundMs(percentile(deltas.stickyServiceMs, 0.99)) }), + ...stickySlotFields(deltas), assign503sByCauseDelta: deltas.assign503sByCause, assignNonDrain503sDelta: Object.entries(deltas.assign503sByCause) .filter(([cause]) => cause !== 'drain-return-deferred') @@ -584,6 +710,7 @@ export class RelayObservability implements RelayRuntimeObserver { controlActivityRecoveriesDelta: deltas.controlActivityRecoveries, controlActivityRecoveryFailuresDelta: deltas.controlActivityRecoveryFailures, hostHellosShedDelta: deltas.hostHellosShed, + preAuthLatencyMs: preAuthLatencyFields(deltas.preAuthLatency), // Meaning changed when renewals began batching: for a batched row this is // the flush's duration, not that row's own statement latency. The // per-flush fields below are the ones to read for statement cost. @@ -619,6 +746,71 @@ function regionCounterFields( ) } +// Count and buckets cover every hello in the window, so buckets sum exactly across windows and +// cells; the percentiles summarise the reservoir and are omitted when empty. +function preAuthLatencyFields( + latency: PreAuthLatency +): Record>> { + const summarize = (group: PreAuthLatencyGroup): Record => { + if (group.observed === 0) return { count: 0 } + const { p50, p95, p99 } = latencySummary(group.samplesMs) + const bucketsLeMs: Record = {} + PRE_AUTH_BUCKET_BOUNDS_MS.forEach((bound, index) => { + bucketsLeMs[String(bound)] = group.bucketCounts[index]! + }) + bucketsLeMs['+Inf'] = group.bucketCounts[PRE_AUTH_BUCKET_BOUNDS_MS.length]! + return { count: group.observed, p50, p95, p99, max: roundMs(group.maxMs), bucketsLeMs } + } + const byOutcome = ( + groups: Record + ): Record> => ({ + admitted: summarize(groups.admitted), + refused: summarize(groups.refused), + abandoned: summarize(groups.abandoned) + }) + return { + beforeCheck: byOutcome(latency.beforeCheck), + db: byOutcome(latency.db), + reserve: byOutcome(latency.reserve) + } +} + +// What the sticky slot spent its hold on: the queue before it, then pool waits and +// round trips inside it; per connection, whether one slow connection keeps serving it. +function stickySlotFields(deltas: RelayMetricDeltas): Record { + if (deltas.stickySlotQueueWaitMs.length === 0) return {} + const queueWait = latencySummary(deltas.stickySlotQueueWaitMs) + const poolWait = latencySummary(deltas.stickySlotPoolWaitMs) + const sql = latencySummary(deltas.stickySlotSqlMs) + const statements = latencySummary(deltas.stickySlotStatements) + const byConnection: Record> = {} + for (const [connectionId, connection] of deltas.stickySlotByConnection) { + const { p50, p99 } = latencySummary(connection.samplesMs) + byConnection[String(connectionId)] = { + requests: connection.requests, + statements: connection.statements, + statementMsP50: p50, + statementMsP99: p99, + statementMsMax: roundMs(connection.maxMs) + } + } + return { + stickySlotsDelta: deltas.stickySlotQueueWaitMs.length, + stickyQueueWaitMsP50: queueWait.p50, + stickyQueueWaitMsP99: queueWait.p99, + stickyQueueWaitMsMax: queueWait.max, + stickyPoolWaitMsP50: poolWait.p50, + stickyPoolWaitMsP99: poolWait.p99, + stickySqlMsP50: sql.p50, + stickySqlMsP99: sql.p99, + stickyStatementsP50: statements.p50, + stickyStatementsMax: statements.max, + stickySqlByConnection: byConnection, + // One per request-connection pair that found the window's table full. + stickySqlConnectionSamplesDroppedDelta: deltas.stickySlotConnectionSamplesDropped + } +} + function lockWaiters(byKey: Record, waiterRole: string): number { return Object.entries(byKey) .filter(([key]) => key.startsWith(`${waiterRole}:relay_cells:`)) diff --git a/cloud/apps/relay/src/relay-server.ts b/cloud/apps/relay/src/relay-server.ts index 078a799ab62..7d98d8adb7b 100644 --- a/cloud/apps/relay/src/relay-server.ts +++ b/cloud/apps/relay/src/relay-server.ts @@ -255,6 +255,7 @@ export function createRelayServer( epoch: join.epoch }) } + directory.onOutranked = (seat) => reservePlacement?.supersedeOutranked(seat) reserveLedger.start() const reconcile = (): void => { const now = (options.now ?? Date.now)() @@ -469,6 +470,7 @@ export function createRelayServer( recordDrainReturnRetryAfter: (seconds) => observability.recordDrainReturnRetryAfter?.(seconds), recordAdmissionServiceMs: (lane, durationMs) => observability.recordAdmissionServiceMs?.(lane, durationMs), + recordStickySlot: (sample) => observability.recordStickySlot?.(sample), recordAssignmentUnavailable: (cause) => observability.recordAssignmentUnavailable?.(cause), recordRegionRequest: (region) => observability.recordRegionRequest?.(region), shadowSeats: shadowSeatPoller?.directory, diff --git a/cloud/apps/relay/src/reserve-assignment.test.ts b/cloud/apps/relay/src/reserve-assignment.test.ts index 56c465fd0d5..38a6aa27e08 100644 --- a/cloud/apps/relay/src/reserve-assignment.test.ts +++ b/cloud/apps/relay/src/reserve-assignment.test.ts @@ -52,6 +52,7 @@ function setup( databaseBusy?: () => boolean mode?: 'off' | 'dry-run' | 'on' startedAt?: number + log?: (line: string) => void } = {} ) { const now = { value: 100_000 } @@ -88,7 +89,7 @@ function setup( ...(options.databaseBusy ? { databaseBusy: options.databaseBusy } : {}), now: () => now.value, random: () => 0.25, - log: () => undefined + log: options.log ?? (() => undefined) }) // Starts each cell's refill clock, as a director's first look does. for (const cell of assignment.placementCells()) placer.hasBudget(cell) @@ -350,6 +351,17 @@ describe('reserve assignment on a director', () => { expect(demoted).toEqual([{ cellId: 'c1', epoch: 5, joinedAt: 1_000 }]) }) + // The booking map had not seen the c1 seat (half-open, joined within a poll lag): the feed + // demotes it once both joins are in the map, instead of the 75 s silence close. + it('tells an old reserve seat the booking missed to go once a newer join shows in the feed', () => { + const { assignment, directory, demoted, now } = setup() + directory.onOutranked = (seat) => assignment.supersedeOutranked(seat) + directory.apply('c2', feed('c2', { full: [seated(6, 3_000)] }), now.value, now.value) + expect(demoted).toEqual([]) + directory.apply('c1', feed('c1', { full: [seated(5, 1_000)] }), now.value, now.value) + expect(demoted).toEqual([{ cellId: 'c1', epoch: 5, joinedAt: 1_000 }]) + }) + it('answers a reconnect only at the newest epoch, newest join first', async () => { const { assignment, reserved } = setup({ feeds: [feed('c1', { full: [seated(6, 1_000)] }), feed('c2', { full: [seated(5, 3_000)] })] @@ -370,4 +382,38 @@ describe('reserve assignment on a director', () => { expect(plan.kind === 'database' && (await plan.placeFresh?.())).toBeNull() expect(reserved).toMatchObject([{ dryRun: true }]) }) + + it('in dry run counts outcomes per picked cell, and a `full` refusal while the map showed room', async () => { + const summaries: Array<{ dryRun: Record }> = [] + const dryRunOn = async (units: number, outcome: string) => { + const { assignment, now } = setup({ + mode: 'dry-run', + cells: cellList(['c1']), + feeds: [feed('c1', { counts: { seats: units, controls: 0, bookings: 0, units, ceiling: 500 } })], + reserve: () => ({ outcome }), + log: (line) => summaries.push(JSON.parse(line)) + }) + for (let index = 0; index < 2; index += 1) { + const plan = await assignment.plan(HOST, { reconnect: false, region: US }) + if (plan.kind === 'database') await plan.placeFresh?.() + } + // Lets the fire-and-forget dry-run reserves settle before the summary flushes. + await Promise.resolve() + now.value += 60_000 + await assignment.plan(HOST, { reconnect: false, region: US }) + } + // 1 free seat of 500 is under the 1% bound; 250 free is far over it. + await dryRunOn(499, 'full') + await dryRunOn(250, 'full') + await dryRunOn(250, 'ok') + expect(summaries.map((summary) => summary.dryRun)).toMatchObject([ + { + outcomes: { full: 2 }, + count: 2, + cells: { c1: { outcomes: { full: 2 }, fullWithRoom: 0, minFullFreeShare: 0.002 } } + }, + { outcomes: { full: 2 }, cells: { c1: { outcomes: { full: 2 }, fullWithRoom: 2, minFullFreeShare: 0.5 } } }, + { outcomes: { ok: 2 }, cells: { c1: { outcomes: { ok: 2 }, fullWithRoom: 0, minFullFreeShare: null } } } + ]) + }) }) diff --git a/cloud/apps/relay/src/reserve-assignment.ts b/cloud/apps/relay/src/reserve-assignment.ts index 47fe8d66883..a6417457110 100644 --- a/cloud/apps/relay/src/reserve-assignment.ts +++ b/cloud/apps/relay/src/reserve-assignment.ts @@ -13,7 +13,7 @@ import { type PlacementCell, type ReservePlacer } from './reserve-placement.js' -import type { SeatFeedCell, ShadowSeat, ShadowSeatDirectory } from './shadow-seat-directory.js' +import type { OutrankedSeat, SeatFeedCell, ShadowSeat, ShadowSeatDirectory } from './shadow-seat-directory.js' // Step 5 on a director: answer a desktop from the in-memory map for cells that report // admitMode=reserve, and fall back to today's database path for everything else. The cell @@ -24,6 +24,8 @@ export const RESERVE_STICKY_RECENT_MS = 10 * 60_000 // A starting director waits this long for every live cell before it answers. export const RESERVE_STARTUP_GATE_MS = 30_000 const SUMMARY_INTERVAL_MS = 60_000 +// A dry-run `full` while the map showed more free room than this share of the ceiling is `fullWithRoom`. +export const RESERVE_DRY_RUN_ROOM_BOUND = 0.01 // A leaver for these reasons was moved off its cell; it is not sent back there. const NO_REJOIN_CLOSE_CODES: ReadonlySet = new Set([ RELAY_CLOSE_CODE.DRAINING, @@ -342,13 +344,23 @@ export class ReserveAssignment { const cell = this.input.placer.pick(placements, region, new Set()) const target = cell && this.input.cells().find((entry) => entry.cellId === cell.cellId) if (!target) return null + // The map's free room at pick time; pick only returns a cell with a ceiling. + const freeShare = cell.ceiling ? this.input.placer.free(cell) / cell.ceiling : 0 const startedAt = performance.now() void this.input.client .reserve(target, { ...identity, epoch: 1, ttlMs: CELL_RESERVE_TTL_MS }, true) .then((answer) => { const elapsedMs = performance.now() - startedAt - this.summary.dryRun.outcomes[answer.outcome] = (this.summary.dryRun.outcomes[answer.outcome] ?? 0) + 1 - this.summary.dryRun.latenciesMs.push(elapsedMs) + const { dryRun } = this.summary + dryRun.outcomes[answer.outcome] = (dryRun.outcomes[answer.outcome] ?? 0) + 1 + dryRun.latenciesMs.push(elapsedMs) + const perCell = dryRun.cells[cell.cellId] ?? { outcomes: {}, fullWithRoom: 0, minFullFreeShare: null } + dryRun.cells[cell.cellId] = perCell + perCell.outcomes[answer.outcome] = (perCell.outcomes[answer.outcome] ?? 0) + 1 + if (answer.outcome === 'full') { + if (freeShare > RESERVE_DRY_RUN_ROOM_BOUND) perCell.fullWithRoom += 1 + perCell.minFullFreeShare = Math.min(perCell.minFullFreeShare ?? 1, freeShare) + } }) .catch(() => undefined) return null @@ -377,10 +389,10 @@ export class ReserveAssignment { return undefined } - // Fire-and-forget; the cell checks the seat's epoch and join itself. - demoteSeat(seat: { userId: string; relayHostId: string; cellId: string; epoch: number; joinedAt: number }): void { + // Fire-and-forget; the cell checks the seat's epoch and join itself. False: no such cell listed. + demoteSeat(seat: { userId: string; relayHostId: string; cellId: string; epoch: number; joinedAt: number }): boolean { const cell = this.input.cells().find((entry) => entry.cellId === seat.cellId) - if (!cell) return + if (!cell) return false void this.input.client.demote(cell, { v: 1, userId: seat.userId, @@ -388,6 +400,7 @@ export class ReserveAssignment { epoch: seat.epoch, joinedAt: seat.joinedAt }) + return true } // Supersede at placement: the old seats this director's map holds on reserve cells are told @@ -396,32 +409,39 @@ export class ReserveAssignment { for (const seat of seats) { if (seat.cellId === cellId || seat.epoch >= epoch) continue if (this.input.directory.admitModeOf(seat.cellId) !== 'reserve') continue - const cell = this.input.cells().find((entry) => entry.cellId === seat.cellId) - if (!cell) continue - this.summary.supersedes += 1 - void this.input.client.demote(cell, { - v: 1, - userId: identity.userId, - relayHostId: identity.relayHostId, - epoch: seat.epoch, - joinedAt: seat.joinedAt - }) - this.input.log?.( - JSON.stringify({ - event: 'orca_relay_reserve_supersede', - relayHostIdDigest: relayHostLogDigest(identity.relayHostId), - fromCellId: seat.cellId, - toCellId: cellId - }) - ) + this.demoteSuperseded({ ...identity, ...seat }, cellId, 'placement') } } + // The feed's half of supersede: a seat the booking map did not hold, found once both joins + // are in this map. Without it that seat holds its cell until the 75 s silence close. + supersedeOutranked(seat: OutrankedSeat): void { + this.demoteSuperseded(seat, seat.byCellId, 'feed') + } + + private demoteSuperseded( + seat: { userId: string; relayHostId: string; cellId: string; epoch: number; joinedAt: number }, + toCellId: string, + via: 'placement' | 'feed' + ): void { + if (!this.demoteSeat(seat)) return + this.summary.supersedes += 1 + this.input.log?.( + JSON.stringify({ + event: 'orca_relay_reserve_supersede', + relayHostIdDigest: relayHostLogDigest(seat.relayHostId), + fromCellId: seat.cellId, + toCellId, + via + }) + ) + } + private flushSummary(): void { const now = this.now() if (now - this.summaryAt < SUMMARY_INTERVAL_MS) return this.summaryAt = now - const { latenciesMs, outcomes } = this.summary.dryRun + const { latenciesMs, outcomes, cells } = this.summary.dryRun latenciesMs.sort((left, right) => left - right) const percentile = (share: number) => latenciesMs.length === 0 @@ -432,7 +452,7 @@ export class ReserveAssignment { event: 'orca_relay_reserve_placement_summary', mode: this.input.mode, ...this.summary, - dryRun: { outcomes, count: latenciesMs.length, p50Ms: percentile(0.5), p99Ms: percentile(0.99) } + dryRun: { outcomes, count: latenciesMs.length, p50Ms: percentile(0.5), p99Ms: percentile(0.99), cells } }) ) this.summary = emptySummary() @@ -448,7 +468,19 @@ type PlacementSummary = { supersedes: number outcomes: Record retries: { mapIncomplete: number; database: number; paced: number } - dryRun: { outcomes: Record; latenciesMs: number[] } + dryRun: { + outcomes: Record + latenciesMs: number[] + // Keyed by the picked cell, so bounded by the fleet. + cells: Record + } +} + +type DryRunCellSummary = { + outcomes: Record + fullWithRoom: number + // Lowest map free share (of the ceiling) among this cell's `full` refusals. + minFullFreeShare: number | null } function emptySummary(): PlacementSummary { @@ -461,6 +493,6 @@ function emptySummary(): PlacementSummary { supersedes: 0, outcomes: {}, retries: { mapIncomplete: 0, database: 0, paced: 0 }, - dryRun: { outcomes: {}, latenciesMs: [] } + dryRun: { outcomes: {}, latenciesMs: [], cells: Object.create(null) } } } diff --git a/cloud/apps/relay/src/reserve-placement-simulation.test.ts b/cloud/apps/relay/src/reserve-placement-simulation.test.ts index 6dae3349684..13bd867b93c 100644 --- a/cloud/apps/relay/src/reserve-placement-simulation.test.ts +++ b/cloud/apps/relay/src/reserve-placement-simulation.test.ts @@ -294,6 +294,8 @@ describe('step 5 deterministic simulation', () => { [1, nearFull({ bookPastCap: true })], [2, drain({ intakeBurst: 200, directorShare: 0.05 })], [3, drainedWithoutSupersede()], + // Half-open seats the booking map had not seen hold their cell until the silence close. + [3, levelSeats(11, { skipFeedSupersede: true })], [4, levelSeats(11, { localTieBreak: true })], [5, mixed({ unguardedLedger: true })], // A director restart mid-queue loses its booked joins' rows until its boot reconcile. @@ -315,13 +317,6 @@ describe('step 5 deterministic simulation', () => { // A director whose map has not seen the leave yet may re-place level with the row; the // WRONG_CELL row read keeps that a small share (the rest are repaired by one more reconcile). expect(report.badFirstReseats / report.rowAheadDemotions).toBeLessThan(0.1) - const withoutRead = await runReservePlacementSimulation({ ...config, faults: { noWrongCellRowRead: true } }) - process.stderr.write( - `row_ahead_repair ${JSON.stringify({ - withRead: { demotions: report.rowAheadDemotions, badFirstReseats: report.badFirstReseats, perHost: report.rowAheadDemotionsPerHost }, - withoutRead: { demotions: withoutRead.rowAheadDemotions, badFirstReseats: withoutRead.badFirstReseats, perHost: withoutRead.rowAheadDemotionsPerHost } - })}\n` - ) }, 240_000) // Every director misses its feed polls for 150 s: each reserve cell's dead-man trips (60-120 s) diff --git a/cloud/apps/relay/src/reserve-placement-simulation.ts b/cloud/apps/relay/src/reserve-placement-simulation.ts index 8eaa673d189..c6b9483ab61 100644 --- a/cloud/apps/relay/src/reserve-placement-simulation.ts +++ b/cloud/apps/relay/src/reserve-placement-simulation.ts @@ -9,6 +9,7 @@ import { ReservePlacer, type PlacementCell } from './reserve-placement.js' +import { seatOutranks } from './shadow-seat-directory.js' // A seeded discrete-event model of step 5 that drives the real CellReserveBook and // ReservePlacer. Everything else (sockets, the database, desktops) is modelled. The checks @@ -111,8 +112,10 @@ export type SimulationFaults = { intakeBurst?: number // Each director assumes this share of every cell's intake, instead of a fifth. directorShare?: number - // Placement does not supersede the old seat: a duplicate then outlives its bound. + // Nothing supersedes the old seat, at placement or from the feed: a duplicate outlives its bound. skipSupersede?: boolean + // Only placement supersedes: a half-open seat the booking map had not seen lasts to its silence close. + skipFeedSupersede?: boolean // A restarted director forgets its ledger queue and never reconciles. noBootReconcile?: boolean // The reconcile leaves a seat whose row is ahead of it alone. @@ -666,10 +669,15 @@ export async function runReservePlacementSimulation( placement: placementCell(cell, snapshot, requestedAt) } director.views.set(cell.cellId, current) + supersedeOutranked(director, seats!.keys()) } else if (current && current.incarnation === snapshot.incarnation) { for (const change of changes) applyChange(director, cell, current, change) current.cursor = snapshot.cursor current.placement = placementCell(cell, snapshot, requestedAt) + supersedeOutranked( + director, + changes.filter((change) => change.kind === 'join').map((change) => change.host) + ) } else { // A restarted director's (or a stale) delta: the next poll asks for a full snapshot. return @@ -758,6 +766,26 @@ export async function runReservePlacementSimulation( } } + // As shadow-seat-directory.ts onOutranked: once both joins are in this map, a reserve seat behind + // a later join at a newer epoch is told to go. A lower epoch that joined later is the row's. + function supersedeOutranked(director: SimDirector, hosts: Iterable): void { + if (faults.skipSupersede || faults.skipFeedSupersede) return + for (const host of new Set(hosts)) { + const seats = [...director.views].flatMap(([cellId, view]) => { + const seat = view.seats.get(host) + return seat ? [{ cellId, view, seat }] : [] + }) + if (seats.length < 2) continue + for (const { cellId, view, seat } of seats) { + if (seat.demoted || !view.placement.reserve) continue + const by = seats.find((other) => seatOutranks(other.seat, seat)) + if (!by) continue + report.supersedes += 1 + demoteFor(director, host, cellId, seat, { cellId: by.cellId, epoch: by.seat.epoch }) + } + } + } + // The seats the booking director's map held when it placed each booking: only those it supersedes. const heldAtBooking = new Map>() @@ -1258,9 +1286,9 @@ export async function runReservePlacementSimulation( report.maxOccupancyShare[cell.cellId] = Math.max(report.maxOccupancyShare[cell.cellId] ?? 0, share) } } - // A booking supersedes the old seats its director's map held within a round trip. Any other - // duplicate (a half-open seat today's path placed past, or one the booking map had not yet - // seen) has no lease to re-check and lasts until that seat's silence close. + // A booking supersedes the old seats its director's map held within a round trip, and the feed + // any seat a later join outranks. Any other duplicate (a half-open seat today's path placed past + // at a lower epoch than it) has no lease to re-check and lasts until that seat's silence close. const supersedeBound = Math.max(...directors.map((director) => config.pollLagMs(director.index))) + 2 * Math.max(...cells.map((cell) => config.rttMs(cell.region))) + @@ -1293,13 +1321,20 @@ export async function runReservePlacementSimulation( for (const { cell, seat } of seats) { if (cell.mode !== 'reserve' || seat.epoch === newest) continue // A leased seat goes by its renewal, as today. - const bound = !seat.leased && supersededBy(cell.cellId, seat.joinedAt) ? supersedeBound : duplicateBound + const tight = + !seat.leased && + (supersededBy(cell.cellId, seat.joinedAt) || seats.some((other) => seatOutranks(other.seat, seat))) + // The loose clock runs from the first outranking; the tight one from when supersede applies. const key = `${host}\u0000${cell.cellId}\u0000${seat.joinedAt}` - live.add(key) - const since = outrankedSince.get(key) ?? clock.now - outrankedSince.set(key, since) - if (clock.now - since > bound) { - violate(3, `${host}@${seat.epoch} on ${cell.cellId} outranked for ${clock.now - since} ms`) + const clocks: Array<[string, number]> = [[key, duplicateBound]] + if (tight) clocks.push([`${key}\u0000tight`, supersedeBound]) + for (const [clockKey, bound] of clocks) { + live.add(clockKey) + const since = outrankedSince.get(clockKey) ?? clock.now + outrankedSince.set(clockKey, since) + if (clock.now - since > bound) { + violate(3, `${host}@${seat.epoch} on ${cell.cellId} outranked for ${clock.now - since} ms`) + } } } } diff --git a/cloud/apps/relay/src/reserve-placement.test.ts b/cloud/apps/relay/src/reserve-placement.test.ts index a79f3b642ea..38f92e5f898 100644 --- a/cloud/apps/relay/src/reserve-placement.test.ts +++ b/cloud/apps/relay/src/reserve-placement.test.ts @@ -29,6 +29,13 @@ function warmPlacer(now: { value: number }, random = () => 0.3) { } describe('reserve placer', () => { + it('reports free seats under the ceiling, never below zero or without a ceiling', () => { + const placer = warmPlacer({ value: 0 }) + expect(placer.free(cell('c1', { seats: 60, bookings: 10 }))).toBe(30) + expect(placer.free(cell('c1', { seats: 120 }))).toBe(0) + expect(placer.free(cell('c1', { ceiling: undefined }))).toBe(0) + }) + it('places on the first cell that answers ok and counts one call', async () => { const now = { value: 0 } const placer = warmPlacer(now) diff --git a/cloud/apps/relay/src/reserve-placement.ts b/cloud/apps/relay/src/reserve-placement.ts index b3436f148c0..3e677c054f5 100644 --- a/cloud/apps/relay/src/reserve-placement.ts +++ b/cloud/apps/relay/src/reserve-placement.ts @@ -68,6 +68,11 @@ export class ReservePlacer { return cell.seats + cell.bookings + this.ownBookingsSince(cell) } + // Estimated free seats under the ceiling; 0 when the cell reports none. + free(cell: PlacementCell): number { + return cell.ceiling === undefined ? 0 : Math.max(0, cell.ceiling - this.load(cell)) + } + hasSeat(cell: PlacementCell): boolean { return cell.ceiling !== undefined && this.load(cell) < cell.ceiling } @@ -158,7 +163,7 @@ export class ReservePlacer { if (cell.region !== region || !cell.general || cell.draining) continue if (cell.reserve) { if (this.isCandidate(cell, region, now)) { - reserveFree += Math.max(0, cell.ceiling! - this.load(cell)) + reserveFree += this.free(cell) } continue } diff --git a/cloud/apps/relay/src/shadow-directory-compare.test.ts b/cloud/apps/relay/src/shadow-directory-compare.test.ts index 896ad9297c4..c2a36d64ede 100644 --- a/cloud/apps/relay/src/shadow-directory-compare.test.ts +++ b/cloud/apps/relay/src/shadow-directory-compare.test.ts @@ -5,6 +5,7 @@ import { classifyShadowSeat, SHADOW_COMPARE_FLUSH_MS, SHADOW_COMPARE_SAMPLES_PER_CLASS, + SHADOW_COMPARE_SUPERSEDED_CLOSE_MS, ShadowDirectoryCompare } from './shadow-directory-compare.js' import { ShadowSeatDirectory, type SeatFeedResponse } from './shadow-seat-directory.js' @@ -27,8 +28,8 @@ function feed(cellId: string, overrides: Partial = {}): SeatFe return { v: 1, cellId, incarnation: `inc-${cellId}`, seq: 1, at: 1, full: [], ...overrides } } -function seat(epoch: number, state = 'active', relayHostId = HOST) { - return { userId: 'user-a', relayHostId, epoch, generation: 1, state, joinedAt: 1 } +function seat(epoch: number, state = 'active', relayHostId = HOST, joinedAt = 1) { + return { userId: 'user-a', relayHostId, epoch, generation: 1, state, joinedAt } } function directoryWith( @@ -84,7 +85,7 @@ describe('classifyShadowSeat', () => { true ], [ - directoryWith({ 'cell-a': [seat(3)], 'cell-b': [seat(2)] }), + directoryWith({ 'cell-a': [seat(3)], 'cell-b': [seat(3)] }), at('cell-a', 3), 'duplicate-seat', false @@ -191,6 +192,178 @@ describe('classifyShadowSeat', () => { }) }) + describe('a duplicate seat left by a superseded half-open control', () => { + // Seated on cell-b at epoch 2 long ago; the director first saw epoch 3 on cell-a at firstSeen. + const firstSeen = 1_000_000 + const directoryAt = (seats: Record[]>) => { + const directory = directoryWith(seats) + directory.observeDatabaseEpoch('user-a', HOST, 3, firstSeen) + return directory + } + const superseded = (joinedAt = firstSeen) => + directoryAt({ + 'cell-a': [seat(3, 'active', HOST, joinedAt)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + + it('is explained while the old control may still await its silence close', () => { + for (const now of [firstSeen + 1_700, firstSeen + SHADOW_COMPARE_SUPERSEDED_CLOSE_MS]) { + expect(classifyShadowSeat(superseded(), IDENTITY, at('cell-a', 3), now)).toEqual({ + class: 'duplicate-seat', + explained: true + }) + } + }) + + it('is unexplained just past the close bound', () => { + const now = firstSeen + SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + 1 + expect(classifyShadowSeat(superseded(), IDENTITY, at('cell-a', 3), now)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + + it('stays unexplained past the bound after a same-epoch rejoin resets joinedAt', () => { + const now = firstSeen + SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + 30_000 + expect(classifyShadowSeat(superseded(now - 1_000), IDENTITY, at('cell-a', 3), now)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + + const unseen = () => + directoryWith({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + + it('is its own unexplained class with no sighting in the instance\'s first 100 s', () => { + const now = firstSeen + 1_700 + const startedAt = now - SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + expect(classifyShadowSeat(unseen(), IDENTITY, at('cell-a', 3), now, startedAt)).toEqual({ + class: 'superseded-unknown-age', + explained: false + }) + }) + + it('is a plain unexplained duplicate with no sighting after the first 100 s', () => { + const now = firstSeen + 1_700 + const startedAt = now - SHADOW_COMPARE_SUPERSEDED_CLOSE_MS - 1 + for (const classified of [ + classifyShadowSeat(unseen(), IDENTITY, at('cell-a', 3), now, startedAt), + classifyShadowSeat(unseen(), IDENTITY, at('cell-a', 3), now) + ]) { + expect(classified).toEqual({ class: 'duplicate-seat', explained: false }) + } + }) + + it('stays a plain duplicate with no sighting when the current seat joined long ago', () => { + const now = firstSeen + SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + 1 + const directory = directoryWith({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + expect(classifyShadowSeat(directory, IDENTITY, at('cell-a', 3), now)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + + it('classifies before recording, so a later epoch bump is explained', () => { + // Seated on cell-a at epoch 2 and on cell-b at epoch 1; the database names cell-a at 2. + const directory = directoryWith({ + 'cell-a': [seat(2, 'active', HOST, firstSeen)], + 'cell-b': [seat(1, 'active', HOST, 1)] + }) + let now = firstSeen + const compare = new ShadowDirectoryCompare(directory, () => now, () => {}) + expect(compare.compare('sticky-verify', IDENTITY, at('cell-a', 2))).toEqual({ + class: 'superseded-unknown-age', + explained: false + }) + // A re-assign to cell-b at epoch 3 supersedes cell-a's epoch-2 seat. + now = firstSeen + 1_700 + directory.apply( + 'cell-b', + feed('cell-b', { seq: 2, full: [seat(3, 'active', HOST, now)] }), + now + ) + expect(compare.compare('sticky-verify', IDENTITY, at('cell-b', 3))).toEqual({ + class: 'duplicate-seat', + explained: true + }) + }) + + it('is explained when the director last saw the host at a lower epoch', () => { + const directory = directoryWith({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + directory.observeDatabaseEpoch('user-a', HOST, 2, 1) + expect(classifyShadowSeat(directory, IDENTITY, at('cell-a', 3), firstSeen + 1_700)).toEqual({ + class: 'duplicate-seat', + explained: true + }) + }) + + it('is unexplained after a lower-epoch sighting when the current seat joined long ago', () => { + const now = firstSeen + SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + 1 + const directory = directoryWith({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + directory.observeDatabaseEpoch('user-a', HOST, 2, 1) + expect(classifyShadowSeat(directory, IDENTITY, at('cell-a', 3), now)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + + it('is unexplained when the database answer regresses below the recorded epoch', () => { + const directory = directoryWith({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + directory.observeDatabaseEpoch('user-a', HOST, 4, firstSeen) + expect(classifyShadowSeat(directory, IDENTITY, at('cell-a', 3), firstSeen + 1_700)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + + it('is unexplained when the database names a newer epoch than the seat on its cell', () => { + const directory = directoryWith({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(2, 'active', HOST, 1)] + }) + directory.observeDatabaseEpoch('user-a', HOST, 4, firstSeen) + expect(classifyShadowSeat(directory, IDENTITY, at('cell-a', 4), firstSeen + 1_700)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + + it('is unexplained when the database names the lower epoch, or matches no seat', () => { + for (const answer of [at('cell-b', 2), at('cell-a', 4), at('cell-b', 3)]) { + expect(classifyShadowSeat(superseded(), IDENTITY, answer, firstSeen + 1_700)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + } + }) + + it('is unexplained on a level epoch, which only the row can break', () => { + const directory = directoryAt({ + 'cell-a': [seat(3, 'active', HOST, firstSeen)], + 'cell-b': [seat(3, 'active', HOST, 1)] + }) + expect(classifyShadowSeat(directory, IDENTITY, at('cell-a', 3), firstSeen + 1_700)).toEqual({ + class: 'duplicate-seat', + explained: false + }) + }) + }) + it('answers map-incomplete until every listed cell has answered', () => { const directory = new ShadowSeatDirectory() directory.setCells(['cell-a', 'cell-b']) @@ -225,7 +398,8 @@ describe('ShadowDirectoryCompare', () => { class: 'map-only', explained: false, count: SHADOW_COMPARE_SAMPLES_PER_CLASS + 5, - windowMs: SHADOW_COMPARE_FLUSH_MS + windowMs: SHADOW_COMPARE_FLUSH_MS, + countByCell: { 'cell-a': SHADOW_COMPARE_SAMPLES_PER_CLASS + 5 } }, { event: 'orca_relay_shadow_compare', @@ -233,10 +407,55 @@ describe('ShadowDirectoryCompare', () => { class: 'agree', explained: true, count: 2, - windowMs: SHADOW_COMPARE_FLUSH_MS + windowMs: SHADOW_COMPARE_FLUSH_MS, + countByCell: { 'cell-a': 2 } } ]) }) + + it('counts a compare once on every cell it involves, and on none for a host neither side has', () => { + const lines: Array> = [] + const compareOn = (seats: Record[]>) => + new ShadowDirectoryCompare(directoryWith(seats), () => 0, (line) => lines.push(JSON.parse(line))) + const unseen = compareOn({ 'cell-a': [], 'cell-b': [] }) + unseen.compare('sticky-verify', IDENTITY, assignment('cell-a')) + unseen.compare('sticky-verify', IDENTITY, assignment('cell-a')) + unseen.compare('sticky-verify', IDENTITY, assignment('cell-b')) + unseen.compare('resolve', IDENTITY, null) + unseen.flush(0) + // The database says cell-a at epoch 3; the map seats the host on cell-b at a newer epoch. + const mismatch = compareOn({ 'cell-a': [], 'cell-b': [seat(4)] }) + mismatch.compare('sticky-verify', IDENTITY, assignment('cell-a')) + mismatch.flush(0) + // A level-epoch duplicate counts on the database's own cell once, and on the other seat cell. + const duplicate = compareOn({ 'cell-a': [seat(3)], 'cell-b': [seat(3)] }) + duplicate.compare('sticky-verify', IDENTITY, assignment('cell-a')) + duplicate.flush(0) + // A drain-only second seat is explained, so it counts only on the database's cell. + const draining = compareOn({ 'cell-a': [seat(3)], 'cell-b': [seat(2, 'drain-only')] }) + draining.compare('sticky-verify', IDENTITY, assignment('cell-a')) + draining.flush(0) + // A superseded older seat: the first-ever sighting has no age, so it is its own unexplained + // class on both cells; seen before, it is explained and counts only on the database's cell. + const superseded = compareOn({ 'cell-a': [seat(3)], 'cell-b': [seat(2)] }) + superseded.compare('sticky-verify', IDENTITY, assignment('cell-a')) + superseded.compare('sticky-verify', IDENTITY, assignment('cell-a')) + superseded.flush(0) + const mapOnly = compareOn({ 'cell-a': [seat(3)], 'cell-b': [seat(3)] }) + mapOnly.compare('resolve', IDENTITY, null) + mapOnly.flush(0) + const counts = lines.filter((line) => line.event === 'orca_relay_shadow_compare') + expect(counts.map((line) => [line.class, line.explained, line.count, line.countByCell])).toEqual([ + ['db-only-unseen', true, 3, { 'cell-a': 2, 'cell-b': 1 }], + ['agree-absent', true, 1, {}], + ['cell-mismatch', false, 1, { 'cell-a': 1, 'cell-b': 1 }], + ['duplicate-seat', false, 1, { 'cell-a': 1, 'cell-b': 1 }], + ['duplicate-seat', true, 1, { 'cell-a': 1 }], + ['superseded-unknown-age', false, 1, { 'cell-a': 1, 'cell-b': 1 }], + ['duplicate-seat', true, 1, { 'cell-a': 1 }], + ['map-only', false, 1, { 'cell-a': 1, 'cell-b': 1 }] + ]) + }) }) describe('unexplained samples', () => { diff --git a/cloud/apps/relay/src/shadow-directory-compare.ts b/cloud/apps/relay/src/shadow-directory-compare.ts index 328b5fa3bb4..0fbf24f90cb 100644 --- a/cloud/apps/relay/src/shadow-directory-compare.ts +++ b/cloud/apps/relay/src/shadow-directory-compare.ts @@ -1,3 +1,4 @@ +import { RELAY_PROTOCOL_LIMITS } from '@orca-cloud/relay-contract' import { relayHostLogDigest } from './relay-host-log-digest.js' import type { ShadowSeatDirectory } from './shadow-seat-directory.js' @@ -20,6 +21,8 @@ export type ShadowCompareClass = | 'cell-mismatch' | 'epoch-mismatch' | 'duplicate-seat' + // A superseded-shaped duplicate with no prior sighting, in the instance's first 100 s. + | 'superseded-unknown-age' | 'map-only' // The map seats the host on a cell the database does not call live, so resolve answered null. | 'map-only-cell-unlive' @@ -34,16 +37,28 @@ export const SHADOW_COMPARE_FLUSH_MS = 60_000 // One poll, its timeout, and a reconnect (p95 ~6 s on 10-02). A move older than // this is not "the map catching up": a lost leave must not read as explained forever. export const SHADOW_COMPARE_LAG_BOUND_MS = 10_000 +// A superseded half-open control lingers until the cell's silence close (the timeout plus up +// to one ping interval), then the map trails that close by a poll. +export const SHADOW_COMPARE_SUPERSEDED_CLOSE_MS = + RELAY_PROTOCOL_LIMITS.controlSilenceTimeoutMs + + RELAY_PROTOCOL_LIMITS.controlPingIntervalMs + + SHADOW_COMPARE_LAG_BOUND_MS // Per class per flush window, so a systemic fault cannot flood the log. export const SHADOW_COMPARE_SAMPLES_PER_CLASS = 10 +// `countByCell` is keyed by cell, so bounded by the fleet; prototype-free, so any cell id is a key. +type ShadowCompareCount = { count: number; countByCell: Record } + export function classifyShadowSeat( directory: ShadowSeatDirectory, identity: { userId: string; relayHostId: string }, answer: ShadowCompareDatabaseAnswer, - now: number + now: number, + // When this instance began comparing; omitted, a missing sighting is never excused. + startedAt = -Infinity ): ShadowCompareVerdict { - // Pure: only ShadowDirectoryCompare records sightings. An epoch never seen is new. + // Pure: only ShadowDirectoryCompare records sightings. An epoch never seen is new; unlike the + // superseded rule, a wrong guess here only explains a mismatch for one lag bound. const movedAt = answer ? (directory.databaseEpochFirstSeen( identity.userId, @@ -69,7 +84,31 @@ export function classifyShadowSeat( } if (seats.length > 1) { // Inside a drain or rehome grace the old seat is drain-only until it closes. - return { class: 'duplicate-seat', explained: seats.some((seat) => seat.state === 'drain-only') } + if (seats.some((seat) => seat.state === 'drain-only')) { + return { class: 'duplicate-seat', explained: true } + } + // A newer DB seat supersedes older-epoch seats until their silence close; + // a level epoch is a real tie. + const current = seats.find( + (seat) => seat.cellId === answer.cellId && seat.epoch === answer.assignmentEpoch + ) + const supersededShape = + current !== undefined && + seats.every((seat) => seat === current || seat.epoch < current.epoch) && + // A long-ago lower-epoch sighting dates the move to now; the join bounds the seat itself. + now - current.joinedAt <= SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + if (!supersededShape) return { class: 'duplicate-seat', explained: false } + const known = directory.databaseEpochOf(identity.userId, identity.relayHostId) + if (!known) { + // Later, no sighting means another instance served the earlier compares, or the map evicted + // the host, so a harmless supersede still reads as an unexplained duplicate-seat. + const starting = now - startedAt <= SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + return { class: starting ? 'superseded-unknown-age' : 'duplicate-seat', explained: false } + } + const supersededAt = movedSince(known, answer, now) + const superseded = + supersededAt !== undefined && now - supersededAt <= SHADOW_COMPARE_SUPERSEDED_CLOSE_MS + return { class: 'duplicate-seat', explained: superseded } } const seat = seats[0] if (!seat) { @@ -99,17 +138,54 @@ export function classifyShadowSeat( return { class: 'agree', explained: true } } +// When the move to the answer's epoch happened, from the sighting recorded before this compare. +// A regressed epoch means unknown, not new. +function movedSince( + known: { epoch: number; firstSeenAt: number }, + answer: { assignmentEpoch: number }, + now: number +): number | undefined { + if (known.epoch > answer.assignmentEpoch) return undefined + // A lower recorded epoch: the move is this sighting. + return known.epoch < answer.assignmentEpoch ? now : known.firstSeenAt +} + +// The database's cell, or with no row every seat's cell; an unexplained cell-mismatch or +// duplicate-seat also counts on every seat cell, so a stale seat feed shows on its own cell. +// A compare with neither (agree-absent, or map-incomplete with no row and no seat) has none. +function comparedCells( + directory: ShadowSeatDirectory, + identity: { userId: string; relayHostId: string }, + answer: ShadowCompareDatabaseAnswer, + verdict: ShadowCompareVerdict +): Set { + const seats = directory.seatsOf(identity.userId, identity.relayHostId) + if (!answer) return new Set(seats.map((seat) => seat.cellId)) + const cells = new Set([answer.cellId]) + if ( + !verdict.explained && + (verdict.class === 'cell-mismatch' || + verdict.class === 'duplicate-seat' || + verdict.class === 'superseded-unknown-age') + ) { + for (const seat of seats) cells.add(seat.cellId) + } + return cells +} + export class ShadowDirectoryCompare { - private counts = new Map() + private counts = new Map() private samples = new Map() private windowStartedAt: number + private readonly startedAt: number constructor( private readonly directory: ShadowSeatDirectory, private readonly now: () => number = Date.now, private readonly log: (line: string) => void = (line) => console.warn(line) ) { - this.windowStartedAt = now() + this.startedAt = now() + this.windowStartedAt = this.startedAt } compare( @@ -118,6 +194,8 @@ export class ShadowDirectoryCompare { answer: ShadowCompareDatabaseAnswer ): ShadowCompareVerdict { const now = this.now() + // Classify first, so it can tell a first-ever sighting from one seen before. + const verdict = classifyShadowSeat(this.directory, identity, answer, now, this.startedAt) // Every answer, so a move's age counts from its first sighting on either route. if (answer) { this.directory.observeDatabaseEpoch( @@ -127,17 +205,22 @@ export class ShadowDirectoryCompare { now ) } - const verdict = classifyShadowSeat(this.directory, identity, answer, now) const key = `${route}\u0000${verdict.class}\u0000${verdict.explained}` - this.counts.set(key, (this.counts.get(key) ?? 0) + 1) + const entry = this.counts.get(key) ?? { count: 0, countByCell: Object.create(null) } + this.counts.set(key, entry) + entry.count += 1 + for (const cellId of comparedCells(this.directory, identity, answer, verdict)) { + entry.countByCell[cellId] = (entry.countByCell[cellId] ?? 0) + 1 + } if (!verdict.explained) this.sample(route, identity, answer, verdict, now) if (now - this.windowStartedAt >= SHADOW_COMPARE_FLUSH_MS) this.flush(now) return verdict } - // One line per route and class per window; the log metric sums `count`. + // One line per route and class per window; the log metric sums `count`. `countByCell` + // counts each compare once on every cell it involves (see comparedCells). flush(now: number = this.now()): void { - for (const [key, count] of this.counts) { + for (const [key, { count, countByCell }] of this.counts) { const [route, verdictClass, explained] = key.split('\u0000') this.log( JSON.stringify({ @@ -146,7 +229,8 @@ export class ShadowDirectoryCompare { class: verdictClass, explained: explained === 'true', count, - windowMs: now - this.windowStartedAt + windowMs: now - this.windowStartedAt, + countByCell }) ) } diff --git a/cloud/apps/relay/src/shadow-seat-directory.test.ts b/cloud/apps/relay/src/shadow-seat-directory.test.ts index 09cc2a352dd..fa0d4635d24 100644 --- a/cloud/apps/relay/src/shadow-seat-directory.test.ts +++ b/cloud/apps/relay/src/shadow-seat-directory.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { RelayAssignmentStore } from './assignment-store.js' import { openInMemoryRelayDatabase } from './database.js' import { + SHADOW_SEAT_OUTRANK_SKEW_MS, SHADOW_SEAT_RECENTLY_LEFT_MAX_HOSTS, ShadowSeatDirectory, startShadowSeatPoller, @@ -577,3 +578,76 @@ describe('ShadowSeatDirectory.placementModeOf', () => { expect(directory.placementModeOf('cell-a')).toBe('reserve') }) }) + +describe('ShadowSeatDirectory.onOutranked', () => { + const reserve = { generation: 3, flags: { admitMode: 'reserve' } } + const db = { generation: 3, flags: { admitMode: 'db' } } + const full = (cellId: string, epoch: number, joinedAt: number, flags = reserve, state = 'active') => + feed({ + cellId, + seq: 1, + flagsApplied: flags, + full: [{ userId: 'user-a', relayHostId: 'host-1', epoch, generation: 1, state, joinedAt }] + }) + const directory = () => { + const map = new ShadowSeatDirectory() + map.setCells(['cell-a', 'cell-b']) + const outranked: unknown[] = [] + map.onOutranked = (seat) => outranked.push(seat) + return { map, outranked } + } + const halfOpen = { + cellId: 'cell-a', + userId: 'user-a', + relayHostId: 'host-1', + epoch: 3, + joinedAt: 10, + byCellId: 'cell-b' + } + + it('reports a reserve seat a later, newer join elsewhere outranks, in either feed order', () => { + const first = directory() + first.map.apply('cell-a', full('cell-a', 3, 10), 100) + first.map.apply('cell-b', full('cell-b', 4, 2_000), 101) + expect(first.outranked).toEqual([halfOpen]) + const reversed = directory() + reversed.map.apply('cell-b', full('cell-b', 4, 2_000), 100) + reversed.map.apply('cell-a', full('cell-a', 3, 10), 101) + expect(reversed.outranked).toEqual([halfOpen]) + const justPastSkew = directory() + justPastSkew.map.apply('cell-a', full('cell-a', 3, 10), 100) + justPastSkew.map.apply('cell-b', full('cell-b', 4, 11 + SHADOW_SEAT_OUTRANK_SKEW_MS), 101) + expect(justPastSkew.outranked).toEqual([halfOpen]) + }) + + it('reports a seat once, even when a full resync re-judges its host', () => { + const { map, outranked } = directory() + map.apply('cell-a', full('cell-a', 3, 10), 100) + map.apply('cell-b', full('cell-b', 4, 2_000), 101) + map.apply('cell-b', { ...full('cell-b', 4, 2_000), incarnation: '22222222-2222-4222-8222-222222222222' }, 102) + map.apply('cell-a', { ...full('cell-a', 3, 10), incarnation: '22222222-2222-4222-8222-222222222222' }, 103) + expect(outranked).toEqual([halfOpen]) + }) + + it('leaves a database-cell seat, a drain-only seat, a later lower epoch, and a join within the skew', () => { + const onDatabaseCell = directory() + onDatabaseCell.map.apply('cell-a', full('cell-a', 3, 10, db), 100) + onDatabaseCell.map.apply('cell-b', full('cell-b', 4, 2_000), 101) + const drainOnly = directory() + drainOnly.map.apply('cell-a', full('cell-a', 3, 10, reserve, 'drain-only'), 100) + drainOnly.map.apply('cell-b', full('cell-b', 4, 2_000), 101) + // Answered the old seat from a stale map after the newer join: the row decides that one. + const staleSticky = directory() + staleSticky.map.apply('cell-a', full('cell-a', 3, 3_000), 100) + staleSticky.map.apply('cell-b', full('cell-b', 4, 2_000), 101) + const withinSkew = directory() + withinSkew.map.apply('cell-a', full('cell-a', 3, 10), 100) + withinSkew.map.apply('cell-b', full('cell-b', 4, 10 + SHADOW_SEAT_OUTRANK_SKEW_MS), 101) + expect([onDatabaseCell, drainOnly, staleSticky, withinSkew].map(({ outranked }) => outranked)).toEqual([ + [], + [], + [], + [] + ]) + }) +}) diff --git a/cloud/apps/relay/src/shadow-seat-directory.ts b/cloud/apps/relay/src/shadow-seat-directory.ts index 144270bd8f3..f57db08dea7 100644 --- a/cloud/apps/relay/src/shadow-seat-directory.ts +++ b/cloud/apps/relay/src/shadow-seat-directory.ts @@ -16,6 +16,16 @@ export const SHADOW_SEAT_POLL_TIMEOUT_MS = 2_000 export const SHADOW_SEAT_CELL_LIST_REFRESH_MS = 30_000 export const SHADOW_SEAT_RECENTLY_LEFT_TTL_MS = 24 * 60 * 60 * 1_000 export const SHADOW_SEAT_RECENTLY_LEFT_MAX_HOSTS = 100_000 +// Cells stamp joinedAt by their own clocks: a later join must clear this to outrank across cells. +export const SHADOW_SEAT_OUTRANK_SKEW_MS = 1_000 +const SHADOW_SEAT_OUTRANKED_REPORTS_MAX = 100_000 + +export function seatOutranks( + newer: { epoch: number; joinedAt: number }, + seat: { epoch: number; joinedAt: number } +): boolean { + return newer.epoch > seat.epoch && newer.joinedAt > seat.joinedAt + SHADOW_SEAT_OUTRANK_SKEW_MS +} const RECENTLY_LEFT_PER_HOST = 4 // Google identity tokens live an hour; one per poll would be 26 metadata reads a second. const IDENTITY_TOKEN_REUSE_MS = 10 * 60_000 @@ -172,6 +182,16 @@ export type BookedJoin = { reservedBy: string } +// A reserve seat behind a later, newer-epoch join of its host elsewhere that placement supersede missed. +export type OutrankedSeat = { + cellId: string + userId: string + relayHostId: string + epoch: number + joinedAt: number + byCellId: string +} + type CellCursor = SeatFeedCellState & { seats: Map } export type HeartbeatSnapshot = { readAt: number; expiresAt: ReadonlyMap } @@ -180,6 +200,11 @@ function hostKey(userId: string, relayHostId: string): string { return `${userId}\u0000${relayHostId}` } +function parseHostKey(key: string): { userId: string; relayHostId: string } { + const separator = key.indexOf('\u0000') + return { userId: key.slice(0, separator), relayHostId: key.slice(separator + 1) } +} + export class ShadowSeatDirectory { private readonly cells = new Map() private readonly hosts = new Map>() @@ -193,6 +218,10 @@ export class ShadowSeatDirectory { // Per host, the newest booking any cell reported (step 5 epoch mint). Age-ordered. private readonly bookings = new Map() onBookedJoin?: (join: BookedJoin) => void + onOutranked?: (seat: OutrankedSeat) => void + private readonly joinedSinceApply = new Set() + // Seats already reported, insertion-ordered: a full resync re-judges a host without a second demote. + private readonly reportedOutranked = new Set() // Cells no longer listed are forgotten with their seats; new ones start pending. // `required` defaults to every listed cell. @@ -225,6 +254,11 @@ export class ShadowSeatDirectory { // `polledAt` is when the request left; the reply holds every change before it. apply(cellId: string, response: SeatFeedResponse, now: number, polledAt = now): void { + this.applyResponse(cellId, response, now, polledAt) + this.reportOutranked() + } + + private applyResponse(cellId: string, response: SeatFeedResponse, now: number, polledAt: number): void { const cursor = this.cells.get(cellId) if (!cursor) return if (response.cellId !== cellId) { @@ -307,10 +341,7 @@ export class ShadowSeatDirectory { seatsOnCell(cellId: string): Array<{ userId: string; relayHostId: string; seat: ShadowSeat }> { const cursor = this.cells.get(cellId) if (!cursor) return [] - return [...cursor.seats].map(([key, seat]) => { - const separator = key.indexOf('\u0000') - return { userId: key.slice(0, separator), relayHostId: key.slice(separator + 1), seat } - }) + return [...cursor.seats].map(([key, seat]) => ({ ...parseHostKey(key), seat })) } // An old cell (404) has no feed; its last known seats stay, marked by status. @@ -373,6 +404,15 @@ export class ShadowSeatDirectory { return known && known.epoch >= epoch ? known.firstSeenAt : undefined } + // The newest epoch this director has seen the database name for the host, if still kept. + databaseEpochOf( + userId: string, + relayHostId: string + ): { epoch: number; firstSeenAt: number } | undefined { + const known = this.databaseEpochs.get(hostKey(userId, relayHostId)) + return known ? { ...known } : undefined + } + // Records a sighting; returns when this director first saw the host at this epoch. observeDatabaseEpoch(userId: string, relayHostId: string, epoch: number, now: number): number { const key = hostKey(userId, relayHostId) @@ -558,6 +598,34 @@ export class ShadowSeatDirectory { this.hosts.set(key, seats) } seats.set(cursor.cellId, seat) + if (this.onOutranked && seats.size > 1) this.joinedSinceApply.add(key) + } + + // After the whole response, so a cell's first snapshot is judged by the mode it carries. Only a + // join triggers it, once per seat: a cell flipped to reserve later, or a lost demote, falls to + // the silence close or the ledger reconcile, as does a lower epoch that joined later. + private reportOutranked(): void { + const report = this.onOutranked + const keys = [...this.joinedSinceApply] + this.joinedSinceApply.clear() + if (!report) return + for (const key of keys) { + const seats = [...(this.hosts.get(key)?.values() ?? [])] + for (const seat of seats) { + if (seat.state !== 'active' || this.admitModeOf(seat.cellId) !== 'reserve') continue + const by = seats.find((other) => seatOutranks(other, seat)) + if (!by) continue + const { cellId, epoch, joinedAt } = seat + const reported = `${key}\u0000${cellId}\u0000${epoch}\u0000${joinedAt}` + if (this.reportedOutranked.has(reported)) continue + this.reportedOutranked.add(reported) + if (this.reportedOutranked.size > SHADOW_SEAT_OUTRANKED_REPORTS_MAX) { + const oldest = this.reportedOutranked.values().next().value + if (oldest !== undefined) this.reportedOutranked.delete(oldest) + } + report({ ...parseHostKey(key), cellId, epoch, joinedAt, byCellId: by.cellId }) + } + } } private unseat(cursor: CellCursor, key: string): void { diff --git a/cloud/apps/relay/src/sticky-slot-statements.test.ts b/cloud/apps/relay/src/sticky-slot-statements.test.ts new file mode 100644 index 00000000000..a4cb215da1f --- /dev/null +++ b/cloud/apps/relay/src/sticky-slot-statements.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from 'vitest' +import { runInStickySlot, StickySlotStatements } from './sticky-slot-statements.js' +import { + ScriptedPoolClient, + scriptedPostgresDatabase +} from './test-fixtures/scripted-postgres-pool.js' + +function gated() { + let open!: () => void + const opened = new Promise((resolve) => (open = resolve)) + return { open, wait: async () => await opened } +} + +describe('sticky slot statements', () => { + it('charges the slot every round trip it made, by pooled connection', async () => { + const first = new ScriptedPoolClient() + const second = new ScriptedPoolClient() + const database = scriptedPostgresDatabase([first, second, first]) + const slot = new StickySlotStatements() + await runInStickySlot(slot, async () => { + await database.query('SELECT verify') + await database.transaction(async (transaction) => { + await transaction.query('SELECT pin') + await transaction.queryLocked('SELECT row', [], { lockTimeoutMs: 200 }) + }) + }) + // Outside the slot: not this request's work. + await database.query('SELECT other') + + const sample = slot.close() + // BEGIN, the statement, both lock_timeout SETs, the locked read and COMMIT. + expect(second.statements).toHaveLength(6) + expect(sample.statements).toBe(7) + expect(sample.sqlMs).toBeGreaterThanOrEqual(0) + expect( + [...sample.statementMsByConnection].map(([id, samples]) => [id, samples.length]) + ).toEqual([ + [1, 1], + [2, 6] + ]) + }) + + it('leaves out statements another request runs while the slot is held', async () => { + const otherGate = gated() + const database = scriptedPostgresDatabase([ + new ScriptedPoolClient(otherGate.wait), + new ScriptedPoolClient() + ]) + const slot = new StickySlotStatements() + const other = database.query('SELECT other') + await runInStickySlot(slot, async () => { + await database.query('SELECT mine') + otherGate.open() + await other + }) + + const sample = slot.close() + expect(sample.statements).toBe(1) + expect([...sample.statementMsByConnection.keys()]).toEqual([2]) + }) + + it('ignores work the request left running after it released the slot', async () => { + const late = gated() + const database = scriptedPostgresDatabase([new ScriptedPoolClient(), new ScriptedPoolClient()]) + const slot = new StickySlotStatements() + let leftRunning: Promise | undefined + await runInStickySlot(slot, async () => { + await database.query('SELECT 1') + leftRunning = late.wait().then(async () => await database.query('SELECT late')) + }) + const sample = slot.close() + late.open() + await leftRunning + expect(sample.statements).toBe(1) + }) + + it('reports the pool wait apart from the statements', async () => { + const database = scriptedPostgresDatabase([new ScriptedPoolClient()], 30) + const slot = new StickySlotStatements() + await runInStickySlot(slot, async () => await database.query('SELECT 1')) + + const sample = slot.close() + expect(sample.poolWaitMs).toBeGreaterThanOrEqual(20) + expect(sample.sqlMs).toBeLessThan(20) + }) + + it('names each pooled connection once, however often it is checked out', async () => { + const first = new ScriptedPoolClient() + const second = new ScriptedPoolClient() + const database = scriptedPostgresDatabase([first, second, first]) + const slot = new StickySlotStatements() + await runInStickySlot(slot, async () => { + for (let index = 0; index < 3; index++) await database.query('SELECT 1') + }) + + expect( + [...slot.close().statementMsByConnection].map(([id, samples]) => [id, samples.length]) + ).toEqual([ + [1, 2], + [2, 1] + ]) + }) +}) diff --git a/cloud/apps/relay/src/sticky-slot-statements.ts b/cloud/apps/relay/src/sticky-slot-statements.ts new file mode 100644 index 00000000000..a7981961c6c --- /dev/null +++ b/cloud/apps/relay/src/sticky-slot-statements.ts @@ -0,0 +1,62 @@ +import { AsyncLocalStorage } from 'node:async_hooks' + +export type StickySlotSample = { + queueWaitMs: number + // Waiting for a pooled connection, apart from the statements run on one. + poolWaitMs: number + sqlMs: number + statements: number + statementMsByConnection: ReadonlyMap +} + +type StickySlotStatementTotals = Omit + +// PostgreSQL round trips (all but ROLLBACK) made while one request holds the sticky +// slot. Closed at release, so work the request left running is not charged to it. +export class StickySlotStatements { + private closed = false + private poolWaitMs = 0 + private sqlMs = 0 + private statements = 0 + private readonly statementMsByConnection = new Map() + + notePoolWait(durationMs: number): void { + if (!this.closed) this.poolWaitMs += durationMs + } + + noteStatement(durationMs: number, connectionId: number): void { + if (this.closed) return + this.statements++ + this.sqlMs += durationMs + const samples = this.statementMsByConnection.get(connectionId) + if (samples) samples.push(durationMs) + else this.statementMsByConnection.set(connectionId, [durationMs]) + } + + close(): StickySlotStatementTotals { + this.closed = true + return { + poolWaitMs: this.poolWaitMs, + sqlMs: this.sqlMs, + statements: this.statements, + statementMsByConnection: this.statementMsByConnection + } + } +} + +const activeStickySlot = new AsyncLocalStorage() + +export function runInStickySlot( + statements: StickySlotStatements, + work: () => Promise +): Promise { + return activeStickySlot.run(statements, work) +} + +export function noteStickySlotPoolWait(durationMs: number): void { + activeStickySlot.getStore()?.notePoolWait(durationMs) +} + +export function noteStickySlotStatement(durationMs: number, connectionId: number): void { + activeStickySlot.getStore()?.noteStatement(durationMs, connectionId) +} diff --git a/cloud/apps/relay/src/test-fixtures/scripted-postgres-pool.ts b/cloud/apps/relay/src/test-fixtures/scripted-postgres-pool.ts new file mode 100644 index 00000000000..ac1397bc2ce --- /dev/null +++ b/cloud/apps/relay/src/test-fixtures/scripted-postgres-pool.ts @@ -0,0 +1,40 @@ +import { EventEmitter } from 'node:events' +import { PostgresDatabase } from '../database.js' + +export class ScriptedPoolClient extends EventEmitter { + readonly statements: string[] = [] + release: () => void = () => undefined + + // Resolves each statement only once `gate` does, so a test can hold one mid-flight. + constructor(private readonly gate: () => Promise = async () => undefined) { + super() + } + + query = async (sql: string | { text: string }) => { + this.statements.push(typeof sql === 'string' ? sql : sql.text) + await this.gate() + return { rows: [], rowCount: 0 } + } +} + +// Hands out the listed clients in order, reusing an object the way pg-pool reuses a connection. +export function scriptedPostgresDatabase( + checkouts: ScriptedPoolClient[], + connectDelayMs = 0 +): PostgresDatabase { + const pool = { + totalCount: checkouts.length, + idleCount: 0, + waitingCount: 0, + connect: async () => { + if (connectDelayMs > 0) await new Promise((resolve) => setTimeout(resolve, connectDelayMs)) + const client = checkouts.shift() + if (!client) throw new Error('scripted_pool_exhausted') + // pg-pool assigns a fresh release on every checkout. + client.release = () => undefined + return client + } + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: PostgresDatabase and its pool pressure read only connect, these counts and the optional options. + return new PostgresDatabase(pool as never) +} diff --git a/cloud/apps/relay/vitest.config.ts b/cloud/apps/relay/vitest.config.ts index f56bbd7be3a..90621de9e9d 100644 --- a/cloud/apps/relay/vitest.config.ts +++ b/cloud/apps/relay/vitest.config.ts @@ -1,26 +1,22 @@ import { readdirSync, readFileSync } from 'node:fs' +import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { defaultExclude, defineConfig } from 'vitest/config' -// Every test file that opens ORCA_RELAY_TEST_POSTGRES_URL shares one CI -// database, and those tests take session-level locks with a 1s lock_timeout. -// Running them alongside anything else collides into lock timeouts, capacity -// exhaustion, and afterAll hangs. Keep them in their own serialized project so -// only they give up file parallelism; the rest of the suite opens SQLite data -// directories and stays fully parallel. +// PostgreSQL files share session locks and a small connection budget. +// The runner overlaps this serialized project with SQLite tests after other packages finish. const sourceDirectory = fileURLToPath(new URL('src', import.meta.url)) const sharedPostgresTests = readdirSync(sourceDirectory) .filter((entry) => entry.endsWith('.test.ts')) .filter((entry) => - readFileSync(`${sourceDirectory}/${entry}`, 'utf8').includes( - 'ORCA_RELAY_TEST_POSTGRES_URL' - ) + readFileSync(join(sourceDirectory, entry), 'utf8').includes('ORCA_RELAY_TEST_POSTGRES_URL') ) .map((entry) => `src/${entry}`) const timeouts = { testTimeout: 15_000, hookTimeout: 15_000 } export default defineConfig({ + cacheDir: process.env.ORCA_RELAY_TEST_CACHE_DIR, test: { projects: [ { diff --git a/cloud/dev/fixtures/terraform-root-partition/families.json b/cloud/dev/fixtures/terraform-root-partition/families.json index 87bd04316c9..89d97154b0a 100644 --- a/cloud/dev/fixtures/terraform-root-partition/families.json +++ b/cloud/dev/fixtures/terraform-root-partition/families.json @@ -146,6 +146,7 @@ "google_logging_metric.relay_event_loop_stall", "google_logging_metric.relay_long_cell_inventory_hold", "google_logging_metric.relay_reserve_placement_off_with_reserve_cells", + "google_logging_metric.relay_reserve_watch_alarm", "google_logging_metric.relay_snapshot", "google_monitoring_alert_policy.relay_assignment_5xx", "google_monitoring_alert_policy.relay_assignment_edge_429", @@ -176,6 +177,7 @@ "google_monitoring_alert_policy.relay_process_fatal", "google_monitoring_alert_policy.relay_region_hint_skew", "google_monitoring_alert_policy.relay_reserve_placement_off_with_reserve_cells", + "google_monitoring_alert_policy.relay_reserve_watch_alarm", "google_monitoring_dashboard.relay_incident", "google_project_iam_custom_role.github_production_relay_capacity_mutation", "google_project_iam_custom_role.github_relay_asia_topology_mutation", @@ -304,6 +306,10 @@ "production": "relay", "staging": "apps" }, + "google_project_iam_member.github_log_writer": { + "production": "relay", + "staging": "apps" + }, "google_project_iam_member.github_logging_viewer": { "production": "relay", "staging": "apps" diff --git a/cloud/dev/scripts/operate-relay-cell-flags.mjs b/cloud/dev/scripts/operate-relay-cell-flags.mjs index 9d02572849f..6eebd7cd2ed 100644 --- a/cloud/dev/scripts/operate-relay-cell-flags.mjs +++ b/cloud/dev/scripts/operate-relay-cell-flags.mjs @@ -244,7 +244,7 @@ function objectUrl(request, suffix = '') { } // One transient 5xx or network failure is retried once, as every relay admin step does. -async function readRuntime(fetchImpl, request, idToken, wait) { +export async function readRuntime(fetchImpl, request, idToken, wait) { const response = await fetchAdminOnceMore( fetchImpl, `${request.cellOrigin}/v1/admin/runtime-status`, @@ -262,7 +262,7 @@ async function readRuntime(fetchImpl, request, idToken, wait) { return await response.json() } -async function readCurrentObject(fetchImpl, request, accessToken) { +export async function readCurrentObject(fetchImpl, request, accessToken) { const headers = { authorization: `Bearer ${accessToken}` } const metadata = await fetchImpl(objectUrl(request), { headers }) if (metadata.status === 404) { @@ -278,6 +278,12 @@ async function readCurrentObject(fetchImpl, request, accessToken) { return { generation: String(generation), object: await media.json().catch(() => null) } } +function generationConflict(current, expected) { + return Object.assign(new Error(`current generation is ${current}, not expected ${expected}`), { + generationConflict: true + }) +} + async function writeObject(fetchImpl, request, object, accessToken, audit) { const boundary = `relay-cell-flags-${Date.now()}` const metadata = { @@ -315,7 +321,8 @@ async function writeObject(fetchImpl, request, object, accessToken, audit) { if (response.status === 412) { await response.body?.cancel().catch(() => undefined) throw Object.assign(new Error('the object changed since expected-generation; re-read and retry'), { - notWritten: true + notWritten: true, + generationConflict: true }) } if (!response.ok) { @@ -409,7 +416,7 @@ async function breakGlassToDatabase(request, dependencies, { fetchImpl, log, sle } const current = await readCurrentObject(fetchImpl, request, accessToken) if (current.generation !== request.expectedGeneration) { - throw new Error(`current generation is ${current.generation}, not expected ${request.expectedGeneration}`) + throw generationConflict(current.generation, request.expectedGeneration) } const object = desiredObject(request, current.object) const generation = await writeObject(fetchImpl, request, object, accessToken, audit) @@ -459,9 +466,7 @@ export async function operateCellFlags(request, dependencies) { // Before the generation check, so a dry run always shows the generation to pass next. log(JSON.stringify(plan)) if (current.generation !== request.expectedGeneration) { - throw new Error( - `current generation is ${current.generation}, not expected ${request.expectedGeneration}` - ) + throw generationConflict(current.generation, request.expectedGeneration) } // The cell applies every readable write; a generation it did not apply was voided there. A // write of an object this image accepts whole (checked above) repairs it. diff --git a/cloud/dev/scripts/relay-production-identity-boundaries.test.mjs b/cloud/dev/scripts/relay-production-identity-boundaries.test.mjs index 5e7671523b7..92c8825fcc9 100644 --- a/cloud/dev/scripts/relay-production-identity-boundaries.test.mjs +++ b/cloud/dev/scripts/relay-production-identity-boundaries.test.mjs @@ -33,6 +33,7 @@ test('no workflow names the retired generic production deploy identity', async ( 'operate-relay-asia-admission.yml', 'operate-relay-production-cell-flags.yml', 'operate-relay-production-rehome-job.yml', + 'operate-relay-production-reserve-watch.yml', 'publish-relay-production.yml' ].map((name) => relayWorkflowFile(name)).sort()) }) diff --git a/cloud/dev/scripts/relay-production-same-cap-wave.test.mjs b/cloud/dev/scripts/relay-production-same-cap-wave.test.mjs index d679ae3c7e4..3e7ae952a47 100644 --- a/cloud/dev/scripts/relay-production-same-cap-wave.test.mjs +++ b/cloud/dev/scripts/relay-production-same-cap-wave.test.mjs @@ -922,18 +922,14 @@ test('the workflows offer exactly the closed set and scale the drain wait with i assert.match(job, /\n DRAIN_PACE_WINDOW_MS: \$\{\{ inputs\.drain-pace-window-ms \}\}\n/) assert.doesNotMatch(job, /DRAIN_PACE_WINDOW_MS: '/) assert.match(job, /cell-class \\\n\s+--cell-id "\$\{TARGET_CELL_ID\}" --drain-pace-window-ms "\$\{DRAIN_PACE_WINDOW_MS\}"/) - // The 15-min migration lease plus the window, plus its excess over the default: restart-safe - // waits a whole window of quiet after the last send, so a slow drain needs about twice it. + // The 15-min migration lease plus the window: restart-safe passes once the window has ended. const timeout = /--timeout-ms "\$\(\((.+)\)\)"\n/.exec(job)[1] const timeoutMs = (pace) => Number(execFileSync( 'bash', ['-c', `DRAIN_PACE_WINDOW_MS=${pace}; echo $((${timeout}))`], { encoding: 'utf8' } )) - // Exactly the timeouts every pace had before the slower ones existed. - for (const pace of [300_000, 60_000, 30_000]) assert.equal(timeoutMs(pace), 900_000 + pace) for (const pace of SAME_CAP_DRAIN_PACE_WINDOWS_MS) { - assert.ok(timeoutMs(pace) >= 2 * pace + 600_000, String(pace)) + assert.equal(timeoutMs(pace), 900_000 + pace, String(pace)) } - assert.equal(timeoutMs(1_200_000), 3_000_000) const slowest = timeoutMs(Math.max(...SAME_CAP_DRAIN_PACE_WINDOWS_MS)) // The rest of the job keeps the 70 min it had beside the old 20-min drain step in 90. const jobMinutes = Number(/\n rollout:\n(?: [^\n]*\n)*? timeout-minutes: (\d+)\n/.exec(job)[1]) diff --git a/cloud/dev/scripts/relay-public-workflow-contract.test.mjs b/cloud/dev/scripts/relay-public-workflow-contract.test.mjs index 3f34e96a107..1d6befc616e 100644 --- a/cloud/dev/scripts/relay-public-workflow-contract.test.mjs +++ b/cloud/dev/scripts/relay-public-workflow-contract.test.mjs @@ -20,7 +20,7 @@ const UNGATED = relayWorkflowFile('verify.yml') const relayWorkflows = () => workflowFiles().filter((file) => file !== UNGATED) test('the copy carries every relay workflow', () => { - assert.equal(relayWorkflows().length, 27) + assert.equal(relayWorkflows().length, 28) }) // Why: workflow_run chains match by display name, not filename. Renaming a file is safe; renaming diff --git a/cloud/dev/scripts/relay-reserve-dead-man-alert.test.mjs b/cloud/dev/scripts/relay-reserve-dead-man-alert.test.mjs index b35d2767eb1..71533ea61e8 100644 --- a/cloud/dev/scripts/relay-reserve-dead-man-alert.test.mjs +++ b/cloud/dev/scripts/relay-reserve-dead-man-alert.test.mjs @@ -12,6 +12,8 @@ const deadMan = read('../../apps/relay/src/cell-reserve-dead-man.ts') const registry = read('../../apps/relay/src/host-session-registry.ts') const server = read('../../apps/relay/src/relay-server.ts') const poller = read('../../apps/relay/src/shadow-seat-directory.ts') +const watch = read('./watch-relay-reserve-cell.mjs') +const githubActions = read('../../infra/terraform/relay-github-actions.tf') const block = (kind, name) => { const body = new RegExp(`resource "${kind}" "${name}" \\{([\\s\\S]*?)\\n\\}`).exec(terraform)?.[1] @@ -84,3 +86,24 @@ test('the withheld dead-cell verdict alert counts every director withheld line, assert.match(read('../../infra/terraform/variables.tf'), /variable "relay_dead_cell_verdict_alert" \{[\s\S]*?default = false/) assert.match(read('../../infra/terraform/environments/staging.tfvars'), /relay_dead_cell_verdict_alert\s+= true/) }) + +test('the reserve watch alarm pages on any line the watch runner writes, per cell', () => { + // The runner writes a global-resource entry under its own log name. + assert.match(watch, /export const ALARM_EVENT = 'orca_relay_reserve_watch_alarm'/) + assert.match(watch, /export const ALARM_LOG_NAME = 'orca-relay-reserve-watch'/) + assert.match(watch, /JSON\.stringify\(\{ event: ALARM_EVENT, cellId, kind, reason \}\)/) + const metric = block('google_logging_metric', 'relay_reserve_watch_alarm') + assert.ok( + metric.includes( + 'resource.type=\\"global\\" AND logName=\\"projects/${var.project_id}/logs/orca-relay-reserve-watch\\" AND jsonPayload.event=\\"orca_relay_reserve_watch_alarm\\"' + ) + ) + assert.ok(metric.includes('cell_id = "EXTRACT(jsonPayload.cellId)"')) + notifyingOnAnyLine('relay_reserve_watch_alarm', 'orca_relay_reserve_watch_alarm', 'global', 'cell_id') + // The deploy identity the workflow runs as can write that line, and only in production. + const grant = /resource "google_project_iam_member" "github_log_writer" \{([\s\S]*?)\n\}/.exec(githubActions)?.[1] + assert.ok(grant, 'github_log_writer grant not found') + assert.match(grant, /count = local\.relay_create_production_ops_identity \? 1 : 0/) + assert.match(grant, /role = "roles\/logging\.logWriter"/) + assert.match(grant, /member = google_service_account\.github_deploy\[0\]\.member/) +}) diff --git a/cloud/dev/scripts/relay-reserve-watch-rules.mjs b/cloud/dev/scripts/relay-reserve-watch-rules.mjs new file mode 100644 index 00000000000..7fd9aae89f2 --- /dev/null +++ b/cloud/dev/scripts/relay-reserve-watch-rules.mjs @@ -0,0 +1,258 @@ +// Stop rules for a cell watched in reserve mode (step 5 design §9, "Auto-abort per flip"). Pure: +// it takes one already-read sample and the watch state, and returns the next state and whether +// to flip the cell back. The reader lives in watch-relay-reserve-cell.mjs. +import { backgroundOf, SHADOW_GATE_THRESHOLDS } from './relay-same-cap-shadow-gate-verdict.mjs' + +const MINUTE_MS = 60_000 + +export const RESERVE_WATCH_RULES = { + // The roll ladder's warn line: max(1.5x, background + 20), so a zero background is not tripped + // by one 503. Three complete minutes in a row, not two. The read spans more minutes than that so + // a late publishing front still leaves three published minutes to judge. + director503: { + multiple: SHADOW_GATE_THRESHOLDS.nonDrain503Budget.warnMultiple, + marginPerMinute: SHADOW_GATE_THRESHOLDS.nonDrain503Budget.warnMarginPerMinute, + consecutiveMinutes: 3, + readMinutes: 10, + // Cloud Run publishes a minute's request count up to 3 minutes late. + publishDelayMs: 3 * MINUTE_MS + }, + // The cell closes a demoted seat after 60 s (DEMOTION_CLOSE_MS); 30 s more covers a director's + // map still holding the closed seat. + duplicateSeat: { graceMs: 60_000 + 30_000, lookbackMs: 15 * MINUTE_MS }, + // The floor keeps an almost empty cell's ordinary churn from reading as a 5% drop. + hostDrop: { share: 0.05, windowMs: 5 * MINUTE_MS, minHosts: 10 }, + ledgerDrift: { share: 0.005, sustainMs: 2 * MINUTE_MS }, + // A placing director logs its ledger summary every 60 s; none in this long is no evidence. + ledgerSummaryMaxAgeMs: 150_000, + // A watch dispatched before the reserve write applies waits this long for the cell to report it. + reserveApplyWaitMs: 10 * MINUTE_MS +} + +// Precedence: the first tripped rule names the verdict, so exact signals come before proxies. The +// sample line's `rules` keys follow this order too. +export const RULE_NAMES = ['deadMan', 'overrun', 'director503', 'duplicateSeat', 'hostDrop', 'ledgerDrift'] + +export function initialWatchState({ cellId, startedAt, durationMinutes, background503PerMinute, flippedAt }) { + return { + v: 1, + cellId, + startedAt, + deadlineAt: startedAt + durationMinutes * MINUTE_MS, + state: 'watching', + verdict: null, + samples: 0, + unverifiableStreak: 0, + // The background is measured in the minutes before the flip, unless the operator supplied it. + flippedAt: flippedAt ?? null, + background: + background503PerMinute === null ? null : { medianPerMinute: background503PerMinute, source: 'input' }, + hosts: [], + reserveSeenAt: null, + ledgerAboveSince: null, + seatBehindRowFirstSeen: {} + } +} + +// The background from the per-minute 503 counts before the flip. `publishedThrough` is the newest +// minute any response code has a point for: a 503 minute at or before it with no point had none, +// and the background waits (null) until it covers the last minute. +export function measuredBackground(perMinute, minutes, publishedThrough) { + if (minutes.length === 0 || publishedThrough === null || publishedThrough < minutes.at(-1)) return null + const background = backgroundOf({ minutes, series: minutes.map((minute) => perMinute[minute] ?? 0), unverified: false }) + return { medianPerMinute: background.medianPerMinute, minutes: background.minutes, source: 'measured' } +} + +const pass = (detail = {}) => ({ status: 'pass', ...detail }) +const trip = (reason, detail = {}) => ({ status: 'trip', reason, ...detail }) +const unverifiable = (reason) => ({ status: 'unverifiable', reason }) +// Not readable yet by design, so it does not count toward the unverifiable streak. +const pending = (reason) => ({ status: 'pending', reason }) + +function judgeDeadMan(cell) { + if (!cell.ok) return unverifiable(cell.error) + if (cell.admitModeEffective !== 'db' && cell.admitModeEffective !== 'reserve') { + return unverifiable('runtime-status reports no admitModeEffective') + } + if (cell.admitModeEffective === 'db') { + return trip('the cell tripped its dead-man (reserve configured, db effective); Postgres still records reserve') + } + return pass() +} + +// The cell ledger's total (seats, bookings, in-flight and phone units) against its hard cap. The +// ledger refuses past the cap, so this is an invariant check: it trips only if that refusal broke. +function judgeOverrun(cell) { + if (!cell.ok) return unverifiable(cell.error) + if (!Number.isInteger(cell.hardCap) || !Number.isInteger(cell.enforcedUnits)) { + return unverifiable('runtime-status reports no hard cap or ledger units') + } + if (cell.enforcedUnits > cell.hardCap) { + return trip(`ledger units ${cell.enforcedUnits} over hard cap ${cell.hardCap}`, { + enforcedUnits: cell.enforcedUnits, + hardCap: cell.hardCap + }) + } + return pass({ enforcedUnits: cell.enforcedUnits, hardCap: cell.hardCap }) +} + +// Fleet-wide director 503s: no existing signal attributes a 503 to the cell a host was bound for, +// so a problem local to one cell can stay under the fleet's line. This proxy can miss. +function judgeDirector503(read, state, at) { + const { multiple, marginPerMinute, consecutiveMinutes, publishDelayMs } = RESERVE_WATCH_RULES.director503 + const { background } = state + if (!background) { + const publishesAt = state.flippedAt === null ? null : Math.floor(state.flippedAt / MINUTE_MS) * MINUTE_MS + publishDelayMs + if (publishesAt !== null && at < publishesAt) return pending('the 503 background before the flip is not published yet') + return unverifiable('no 503 background yet') + } + if (!read.ok) return unverifiable(read.error) + const perMinute = background.medianPerMinute + const above = Math.max(perMinute * multiple, perMinute + marginPerMinute) + // Minute keys sort as time. Only minutes the publishing front has reached are judged: a 503 + // minute there with no point had none, one past it is not published yet and is left out. + if (read.publishedThrough === null) return unverifiable('the director request series is empty') + const recent = read.minutes.filter((minute) => minute <= read.publishedThrough).slice(-consecutiveMinutes) + if (recent.length < consecutiveMinutes) { + return unverifiable(`director requests are published only through ${read.publishedThrough}`) + } + const counts = recent.map((minute) => read.perMinute[minute] ?? 0) + const detail = { backgroundPerMinute: perMinute, above, recent: Object.fromEntries(recent.map((m, i) => [m, counts[i]])) } + if (counts.every((count) => count > above)) { + return trip(`director 503s above ${above}/min for ${consecutiveMinutes} min (${counts.join(', ')})`, detail) + } + return pass(detail) +} + +// A seat the ledger reconcile found behind its row and found again after the demotion should +// have closed it. The line names no host, so (director instance, cell, epoch, rowEpoch) stands in +// for one: a proxy that can only over-count, when two hosts share that key. +function judgeDuplicateSeat(read, firstSeen, at) { + if (!read.ok) return { verdict: unverifiable(read.error), firstSeen } + const { graceMs, lookbackMs } = RESERVE_WATCH_RULES.duplicateSeat + const next = Object.fromEntries( + Object.entries(firstSeen).filter(([, seenAt]) => seenAt >= at - 2 * lookbackMs) + ) + for (const report of [...read.reports].sort((left, right) => left.at - right.at)) { + next[report.key] = Math.min(next[report.key] ?? report.at, report.at) + } + for (const report of read.reports) { + const ageMs = report.at - next[report.key] + if (ageMs > graceMs) { + return { + verdict: trip(`a seat behind its ledger row (${report.key}) is still seated ${Math.round(ageMs / 1000)} s after first report`), + firstSeen: next + } + } + } + return { verdict: pass({ reportsInLookback: read.reports.length }), firstSeen: next } +} + +function judgeHostDrop(hosts) { + const { share, minHosts, windowMs } = RESERVE_WATCH_RULES.hostDrop + const current = hosts.at(-1) + if (!current) return unverifiable('no host count') + if (hosts.some((entry) => entry.draining)) return pass({ controls: current.controls, draining: true }) + const peak = Math.max(...hosts.map((entry) => entry.controls)) + const drop = peak - current.controls + const detail = { controls: current.controls, peakInWindow: peak } + if (peak > 0 && drop >= minHosts && drop / peak > share) { + return trip(`connected hosts fell ${peak} -> ${current.controls} within ${windowMs / MINUTE_MS} min outside a drain`, detail) + } + return pass(detail) +} + +// Rows the placing directors have not written (queued) or gave up on (dropped), per seat. The +// summaries are fleet-wide: exact while this is the only reserve cell, an over-count otherwise. +function judgeLedgerDrift(cell, ledger, aboveSince, at) { + if (!cell.ok) return { verdict: unverifiable(cell.error), aboveSince } + if (!ledger.ok) return { verdict: unverifiable(ledger.error), aboveSince } + const { share, sustainMs } = RESERVE_WATCH_RULES.ledgerDrift + const ratio = ledger.unwrittenRows / Math.max(cell.controls, 1) + const detail = { unwrittenRows: ledger.unwrittenRows, directors: ledger.directors, ratio } + if (ratio <= share) return { verdict: pass(detail), aboveSince: null } + const since = aboveSince ?? at + if (at - since >= sustainMs) { + return { + verdict: trip(`ledger drift ${(ratio * 100).toFixed(2)}% for ${Math.round((at - since) / 1000)} s`, detail), + aboveSince: since + } + } + return { verdict: pass({ ...detail, aboveForMs: at - since }), aboveSince: since } +} + +/** + * One sample. `sample.cell` is the cell's runtime-status, `director503` the complete minutes of + * director 503s with the request series' publishing front (`publishedThrough`), `ledger` the placing directors' newest ledger summaries, `seatBehindRow` this + * cell's reconcile reports in the lookback. Each is `{ ok: false, error }` when it could not be read. + */ +export function evaluateWatchSample(state, sample, { unverifiableLimit }) { + const at = sample.at + const next = { ...state, samples: state.samples + 1 } + const { cell } = sample + if (cell.ok && cell.admitModeConfigured !== 'reserve') { + const seen = state.reserveSeenAt !== null + if (!seen && at < state.startedAt + RESERVE_WATCH_RULES.reserveApplyWaitMs) { + // Not judged, and the streak holds: the reserve write may not have applied yet. + const rules = { reserve: pending(`switch file says ${cell.admitModeConfigured}; waiting for reserve to apply`) } + return { state: next, rules, line: sampleLine(next, sample, rules) } + } + // Someone else flipped it back (or it never became reserve): nothing left to watch. + next.state = seen ? 'stopped' : 'refused' + next.verdict = seen + ? `${state.cellId} is not in reserve mode (switch file says ${cell.admitModeConfigured})` + : `${state.cellId} did not report reserve mode within ${RESERVE_WATCH_RULES.reserveApplyWaitMs / MINUTE_MS} min (switch file says ${cell.admitModeConfigured})` + return { state: next, rules: {}, line: sampleLine(next, sample, {}) } + } + if (cell.ok) { + next.reserveSeenAt = state.reserveSeenAt ?? at + next.hosts = [...state.hosts, { at, controls: cell.controls, draining: cell.draining }].filter( + (entry) => entry.at > at - RESERVE_WATCH_RULES.hostDrop.windowMs + ) + } + const duplicate = judgeDuplicateSeat(sample.seatBehindRow, state.seatBehindRowFirstSeen, at) + next.seatBehindRowFirstSeen = duplicate.firstSeen + const ledger = judgeLedgerDrift(cell, sample.ledger, state.ledgerAboveSince, at) + next.ledgerAboveSince = ledger.aboveSince + const rules = { + deadMan: judgeDeadMan(cell), + overrun: judgeOverrun(cell), + director503: judgeDirector503(sample.director503, state, at), + duplicateSeat: duplicate.verdict, + hostDrop: cell.ok ? judgeHostDrop(next.hosts) : unverifiable(cell.error), + ledgerDrift: ledger.verdict + } + const tripped = RULE_NAMES.find((name) => rules[name].status === 'trip') + if (tripped) { + next.state = 'tripped' + next.verdict = `flip back ${state.cellId}: ${tripped}: ${rules[tripped].reason}` + return { state: next, rules, line: sampleLine(next, sample, rules) } + } + const missing = RULE_NAMES.filter((name) => rules[name].status === 'unverifiable') + const waiting = RULE_NAMES.some((name) => rules[name].status === 'pending') + // A pending rule holds the streak where it was: neither evidence of health nor a gap. + next.unverifiableStreak = missing.length > 0 ? state.unverifiableStreak + 1 : waiting ? state.unverifiableStreak : 0 + if (next.unverifiableStreak >= unverifiableLimit) { + next.state = 'tripped' + next.verdict = + `flip back ${state.cellId}: unverifiable: ${next.unverifiableStreak} samples in a row could not ` + + `read ${missing.join(', ')}` + } else if (at >= state.deadlineAt) { + next.state = 'completed' + next.verdict = `${state.cellId} stayed healthy in reserve mode for the whole watch` + } + return { state: next, rules, line: sampleLine(next, sample, rules) } +} + +function sampleLine(state, sample, rules) { + return JSON.stringify({ + event: 'orca_relay_reserve_watch_sample', + cellId: state.cellId, + at: new Date(sample.at).toISOString(), + sample: state.samples, + state: state.state, + unverifiableStreak: state.unverifiableStreak, + rules, + ...(state.verdict ? { verdict: state.verdict } : {}) + }) +} diff --git a/cloud/dev/scripts/relay-same-cap-shadow-gate.mjs b/cloud/dev/scripts/relay-same-cap-shadow-gate.mjs index 9332017f39a..2914b282095 100644 --- a/cloud/dev/scripts/relay-same-cap-shadow-gate.mjs +++ b/cloud/dev/scripts/relay-same-cap-shadow-gate.mjs @@ -122,7 +122,7 @@ async function retryingRead(reader, read) { return { failed: true, error: String(lastError?.message ?? lastError) } } -async function readLogEntries(reader, { filter, projection, limit = ENTRY_LIMIT }) { +export async function readLogEntries(reader, { filter, projection, limit = ENTRY_LIMIT }) { const args = [ 'logging', 'read', filter, '--project', reader.projectId, @@ -154,21 +154,22 @@ async function readTimestampsOverWindow(reader, { filter, window }) { // Cloud Run's own request counter, aligned per minute server-side: a brownout's tens of thousands // of 503s are counted, where a log read of them stops at its entry limit (10-01 c29). -async function readDirector503PerMinute(reader, { config, span }) { +async function readDirectorRequestCountSeries(reader, { config, span, filters = [], groupByFields }) { const url = new URL(`https://monitoring.googleapis.com/v3/projects/${config.projectId}/timeSeries`) url.searchParams.set('filter', [ 'metric.type="run.googleapis.com/request_count"', 'resource.type="cloud_run_revision"', `resource.label."service_name"="${config.directorService}"`, - 'metric.label."response_code"="503"' + ...filters ].join(' AND ')) url.searchParams.set('interval.startTime', span.startedAt.toISOString()) url.searchParams.set('interval.endTime', span.endedAt.toISOString()) url.searchParams.set('aggregation.alignmentPeriod', '60s') url.searchParams.set('aggregation.perSeriesAligner', 'ALIGN_DELTA') url.searchParams.set('aggregation.crossSeriesReducer', 'REDUCE_SUM') + if (groupByFields) url.searchParams.set('aggregation.groupByFields', groupByFields) url.searchParams.set('pageSize', '1000') - const read = await retryingRead(reader, async (timeoutMs) => { + return await retryingRead(reader, async (timeoutMs) => { const { stdout } = await reader.runGcloud(['auth', 'print-access-token'], { timeoutMs }) const response = await reader.fetch(url, { headers: { authorization: `Bearer ${stdout.trim()}` }, @@ -179,17 +180,44 @@ async function readDirector503PerMinute(reader, { config, span }) { if (body.nextPageToken) throw new Error('Cloud Monitoring pagination is incomplete') return { timeSeries: body.timeSeries ?? [] } }) - const perMinute = {} - for (const series of read.timeSeries ?? []) { - for (const point of series.points ?? []) { - // A delta point covers the minute that ends at its end time; a minute with no point had none. - const minute = minuteKey(Date.parse(point.interval.endTime) - 60_000) - perMinute[minute] = (perMinute[minute] ?? 0) + Number(point.value.int64Value ?? 0) - } +} + +function addPointsPerMinute(perMinute, series) { + for (const point of series.points ?? []) { + // A delta point covers the minute that ends at its end time; a minute with no point had none. + const minute = minuteKey(Date.parse(point.interval.endTime) - 60_000) + perMinute[minute] = (perMinute[minute] ?? 0) + Number(point.value.int64Value ?? 0) } + return perMinute +} + +export async function readDirector503PerMinute(reader, { config, span }) { + const read = await readDirectorRequestCountSeries(reader, { + config, + span, + filters: ['metric.label."response_code"="503"'] + }) + const perMinute = {} + for (const series of read.timeSeries ?? []) addPointsPerMinute(perMinute, series) return { perMinute, failed: read.failed } } +// Every response code in one read, so a minute missing from the 503 series can be told apart from +// one not published yet: the other codes' series carry the same publishing front. +export async function readDirectorRequestsByCodePerMinute(reader, { config, span }) { + const read = await readDirectorRequestCountSeries(reader, { + config, + span, + groupByFields: 'metric.label."response_code"' + }) + const byCode = {} + for (const series of read.timeSeries ?? []) { + const code = series.metric?.labels?.response_code ?? 'unknown' + byCode[code] = addPointsPerMinute(byCode[code] ?? {}, series) + } + return { byCode, failed: read.failed } +} + // Cells log through the COS container agent, so the text lives in jsonPayload.message; a // textPayload filter matches nothing here and returns zero without saying so. const CELL_LOG_SCOPE = 'resource.type="gce_instance" AND logName:"cos_containers"' @@ -419,8 +447,8 @@ export async function evaluateShadowGate(config, { appliedPaceWindowMs: config.drainAppliedPaceWindowMs, targetHosts: config.targetHosts, settledAt: config.drainSettledAt || null, - // Isolate to restart-safe, which includes the quiet the restart wait holds after the cell - // empties: (ceil(pace / 5 s) + 1) samples of 5 s. + // Isolate to restart-safe: the later of the pace window's end and the cell emptying, plus + // one 5 s sample. settledAfterSeconds: config.drainStartedAt && config.drainSettledAt ? Math.round((Date.parse(config.drainSettledAt) - Date.parse(config.drainStartedAt)) / 1000) : null diff --git a/cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs b/cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs index 619be7f444b..f5e183b4748 100644 --- a/cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs +++ b/cloud/dev/scripts/relay-staging-c4-refresh-workflow.test.mjs @@ -42,9 +42,9 @@ const asiaCells = ['production-gce-c27', 'production-gce-c28', 'production-gce-c const c30Digest = '4158d8a2e18e9caec439d257f0c1e45d92ffea8c0262f057b2f08c76a134bcf0' // C31 launches on the digest the director served when it was declared. const c31Digest = 'f30b5cb1ec52b6b6145efecfa1b8be9e3d309403beffd8abcc64197a2087e269' -// Staging C4 is declared on image F (main 3c1141f2, the six-cell dead-cell rerun); the staging +// Staging C4 is declared on image G (main 2de73812, E-pre with the t_preauth metric); the staging // director deploy binds its digest to this line. -const stagingC4Digest = '41cba995b288cab70579882ebef23e74c5ee426b6f8924cea7c986aba23bcbfe' +const stagingC4Digest = '07b5bf67a7706b2a61ef5e9a6d8ce85ee37c2a08609496a94eb5254881b5b87d' function cellBlock(tfvars, cellId) { const start = tfvars.indexOf(`"${cellId}"`) diff --git a/cloud/dev/scripts/run-relay-tests.mjs b/cloud/dev/scripts/run-relay-tests.mjs new file mode 100644 index 00000000000..0cbc689c070 --- /dev/null +++ b/cloud/dev/scripts/run-relay-tests.mjs @@ -0,0 +1,126 @@ +import { runProcess } from '@orca/process-host' +import { + forceTerminateProcessTree, + signalProcessTree +} from '@orca/process-host/process-tree-termination' +import { mkdtempSync, mkdirSync, realpathSync, rmSync } from 'node:fs' +import { createRequire } from 'node:module' +import { availableParallelism, tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const require = createRequire(import.meta.url) +const vitest = join(dirname(require.resolve('vitest/package.json')), 'vitest.mjs') +const relayDirectory = fileURLToPath(new URL('../../apps/relay', import.meta.url)) + +export async function runRelayTests({ + args = [], + cwd = relayDirectory, + env = process.env, + parallelism = availableParallelism(), + cli = vitest, + run = runProcess, + events = process, + report = console.error, + scratchParent = tmpdir() +} = {}) { + const workerBudget = Math.max(parallelism - 1, 1) + // Explicit selectors and alternate database fixtures retain their existing execution path. + const serial = + args.length > 0 || workerBudget <= 1 || env.ORCA_IDLE_REHOME_POSTGRES_URL || env.DATABASE_URL + const roles = serial + ? [{ name: 'all', args }] + : [ + { name: 'relay', args: ['--project=relay', `--maxWorkers=${workerBudget - 1}`] }, + { name: 'relay-postgres', args: ['--project=relay-postgres', '--maxWorkers=1'] } + ] + const scratch = realpathSync(mkdtempSync(join(scratchParent, 'orca-relay-tests-'))) + const abort = new AbortController() + const closed = new Set() + const terminatedTrees = new Set() + let signalExitCode + const onInterrupt = () => { + signalExitCode ??= 130 + abort.abort() + } + const onTerminate = () => { + signalExitCode ??= 143 + abort.abort() + } + events.on('SIGINT', onInterrupt) + events.on('SIGTERM', onTerminate) + + async function runRole(role) { + const owned = join(scratch, role.name) + try { + mkdirSync(owned) + return await run({ + program: process.execPath, + args: [cli, 'run', ...role.args], + cwd, + env: { + ...env, + TMPDIR: owned, + TMP: owned, + TEMP: owned, + ORCA_BACKGROUND_LAUNCH: '1', + ORCA_RELAY_TEST_CACHE_DIR: join(owned, 'cache') + }, + stdio: 'inherit', + timeoutMs: null, + signal: abort.signal, + terminationBarrier: { + async signal(child, signal) { + const terminated = await signalProcessTree(child, signal) + if (process.platform === 'win32' && terminated) { + terminatedTrees.add(role.name) + } + return terminated + }, + async force(child) { + const terminated = await forceTerminateProcessTree(child) + if (terminated) { + terminatedTrees.add(role.name) + } + return terminated + } + }, + onChildTerminated: () => closed.add(role.name) + }) + } catch (error) { + abort.abort() + throw error + } + } + + let exitCode = 0 + try { + const results = await Promise.allSettled(roles.map(runRole)) + for (const result of results) { + if (result.status === 'rejected') { + report(result.reason) + exitCode ||= 1 + } else if (result.value.code !== 0 || result.value.signal || result.value.timedOut) { + exitCode ||= result.value.code || 1 + } + } + } finally { + events.removeListener('SIGINT', onInterrupt) + events.removeListener('SIGTERM', onTerminate) + // A cancelled root's close event alone does not prove its descendants stopped using scratch. + if ( + closed.size === roles.length && + (!abort.signal.aborted || terminatedTrees.size === roles.length) + ) { + rmSync(scratch, { recursive: true, force: true }) + } else { + report(`Relay test process closure is unverifiable; retained ${scratch}`) + exitCode ||= 1 + } + } + return signalExitCode ?? exitCode +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exitCode = await runRelayTests({ args: process.argv.slice(2) }) +} diff --git a/cloud/dev/scripts/run-relay-tests.test.mjs b/cloud/dev/scripts/run-relay-tests.test.mjs new file mode 100644 index 00000000000..1f806f183dc --- /dev/null +++ b/cloud/dev/scripts/run-relay-tests.test.mjs @@ -0,0 +1,274 @@ +import assert from 'node:assert/strict' +import { EventEmitter } from 'node:events' +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + realpathSync, + rmSync, + symlinkSync, + writeFileSync +} from 'node:fs' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { setImmediate } from 'node:timers/promises' +import { pathToFileURL } from 'node:url' +import test from 'node:test' +import { runProcess } from '@orca/process-host' +import { runRelayTests } from './run-relay-tests.mjs' + +const success = { code: 0, signal: null, timedOut: false } +const databaseUrl = 'postgres://relay_test:relay_test@127.0.0.1:5432/orca_relay_test' +const require = createRequire(import.meta.url) +const cloud = resolve(import.meta.dirname, '../..') + +function temporary(t) { + const directory = realpathSync(mkdtempSync(join(tmpdir(), 'orca-relay-runner-contract-'))) + t.after(() => rmSync(directory, { recursive: true, force: true })) + return directory +} + +function start(t, options = {}) { + const scratchParent = temporary(t) + const calls = [] + const reports = [] + const events = new EventEmitter() + const env = { ORCA_RELAY_TEST_POSTGRES_URL: databaseUrl, PRESERVED: 'yes', ...options.env } + const pending = runRelayTests({ + cwd: join(scratchParent, 'relay'), + cli: join(scratchParent, 'vitest.mjs'), + parallelism: 4, + ...options, + env, + events, + scratchParent, + report: (message) => reports.push(message), + run: (spec) => { + const deferred = Promise.withResolvers() + calls.push({ spec, ...deferred }) + return deferred.promise + } + }) + return { pending, calls, reports, events, scratchParent, env } +} + +function finish(call, result = success, closed = true) { + if (closed) { + call.spec.onChildTerminated() + } + call.resolve(result) +} + +function assertClean(harness) { + assert.deepEqual(readdirSync(harness.scratchParent), []) + assert.equal(harness.events.listenerCount('SIGINT'), 0) + assert.equal(harness.events.listenerCount('SIGTERM'), 0) +} + +test('overlaps both projects within the worker budget and isolates their scratch and caches', async (t) => { + const h = start(t) + assert.equal(h.calls.length, 2) + const [regular, postgres] = h.calls.map((call) => call.spec) + assert.deepEqual(regular.args.slice(1), ['run', '--project=relay', '--maxWorkers=2']) + assert.deepEqual(postgres.args.slice(1), ['run', '--project=relay-postgres', '--maxWorkers=1']) + assert.notEqual(regular.env.TMPDIR, postgres.env.TMPDIR) + assert.notEqual(regular.env.ORCA_RELAY_TEST_CACHE_DIR, postgres.env.ORCA_RELAY_TEST_CACHE_DIR) + for (const spec of [regular, postgres]) { + assert.equal(spec.env.TMP, spec.env.TMPDIR) + assert.equal(spec.env.TEMP, spec.env.TMPDIR) + assert.equal(spec.env.ORCA_RELAY_TEST_CACHE_DIR, join(spec.env.TMPDIR, 'cache')) + assert.equal(spec.env.ORCA_RELAY_TEST_POSTGRES_URL, databaseUrl) + assert.equal(spec.env.PRESERVED, 'yes') + assert.equal(spec.env.ORCA_BACKGROUND_LAUNCH, '1') + assert.ok(existsSync(spec.env.TMPDIR)) + assert.equal(spec.timeoutMs, null) + assert.equal(typeof spec.terminationBarrier.force, 'function') + } + assert.equal(regular.signal, postgres.signal) + let settled = false + h.pending.then(() => { + settled = true + }) + finish(h.calls[0], { ...success, code: 7 }) + await setImmediate() + assert.equal(settled, false) + assert.equal(postgres.signal.aborted, false) + assert.ok(existsSync(postgres.env.TMPDIR)) + finish(h.calls[1]) + assert.equal(await h.pending, 7) + assertClean(h) +}) + +for (const [name, options] of [ + ['selectors', { args: ['src/assignment.test.ts', '-t', 'requested behavior'] }], + [ + 'alternate PostgreSQL', + { env: { ORCA_IDLE_REHOME_POSTGRES_URL: 'postgres://localhost:55440/relay' } } + ], + ['application database', { env: { DATABASE_URL: databaseUrl } }], + ['one CPU', { parallelism: 1 }], + ['two CPUs', { parallelism: 2 }] +]) { + test(`retains the original single run for ${name}`, async (t) => { + const h = start(t, options) + assert.equal(h.calls.length, 1) + const { spec } = h.calls[0] + assert.deepEqual(spec.args.slice(1), ['run', ...(options.args ?? [])]) + for (const [key, value] of Object.entries(h.env)) { + assert.equal(spec.env[key], value) + } + finish(h.calls[0]) + assert.equal(await h.pending, 0) + assertClean(h) + }) +} + +for (const [name, result] of [ + ['null exit code', { ...success, code: null }], + ['termination signal', { ...success, signal: 'SIGTERM' }], + ['timeout', { ...success, timedOut: true }] +]) { + test(`fails a ${name} even when other result fields indicate success`, async (t) => { + const h = start(t) + finish(h.calls[0], result) + finish(h.calls[1]) + assert.equal(await h.pending, 1) + assertClean(h) + }) +} + +test('fails and retains scratch when a child never reports closure', async (t) => { + const h = start(t) + finish(h.calls[0], success, false) + finish(h.calls[1]) + assert.equal(await h.pending, 1) + assert.ok(existsSync(h.calls[0].spec.env.TMPDIR)) + assert.match(String(h.reports[0]), /closure is unverifiable; retained/) + assert.equal(h.events.listenerCount('SIGINT'), 0) + assert.equal(h.events.listenerCount('SIGTERM'), 0) +}) + +for (const [signal, exitCode, laterSignal] of [ + ['SIGINT', 130, 'SIGTERM'], + ['SIGTERM', 143, 'SIGINT'] +]) { + test(`${signal} cancels every child and retains scratch despite root close callbacks`, async (t) => { + const h = start(t) + h.events.emit(signal) + h.events.emit(laterSignal) + for (const call of h.calls) { + assert.equal(call.spec.signal.aborted, true) + finish(call) + } + assert.equal(await h.pending, exitCode) + for (const call of h.calls) { + assert.ok(existsSync(call.spec.env.TMPDIR)) + } + assert.match(String(h.reports[0]), /closure is unverifiable; retained/) + assert.equal(h.events.listenerCount('SIGINT'), 0) + assert.equal(h.events.listenerCount('SIGTERM'), 0) + }) +} + +for (const verified of [1, 2]) { + test(`requires verification of both cancelled trees (${verified} verified)`, async (t) => { + const h = start(t) + h.events.emit('SIGINT') + for (const call of h.calls.slice(0, verified)) { + // An unspawned handle proves absence without signalling a real process. + assert.equal(await call.spec.terminationBarrier.force({ kill: () => true }), true) + } + for (const call of h.calls) { + finish(call) + } + assert.equal(await h.pending, 130) + if (verified === 2) { + assertClean(h) + } else { + assert.ok(existsSync(h.calls[0].spec.env.TMPDIR)) + } + }) +} + +test('reports a spawn failure and waits for its cancelled peer before returning', async (t) => { + const h = start(t) + const failure = new Error('fixture spawn failed') + h.calls[0].reject(failure) + let settled = false + h.pending.then(() => { + settled = true + }) + await setImmediate() + assert.equal(h.calls[1].spec.signal.aborted, true) + assert.equal(settled, false) + finish(h.calls[1]) + assert.equal(await h.pending, 1) + assert.equal(h.reports[0], failure) + assert.ok(existsSync(h.calls[1].spec.env.TMPDIR)) +}) + +test('launches with only the Cloud dependency link and no root install or compiled host package', async (t) => { + const root = temporary(t) + const standalone = join(root, 'cloud') + const manifest = JSON.parse(readFileSync(join(cloud, 'package.json'), 'utf8')) + const dependency = manifest.devDependencies['@orca/process-host'] + assert.equal(dependency, 'link:../src/packages/process-host') + const packageDir = resolve(standalone, dependency.slice('link:'.length)) + mkdirSync(packageDir, { recursive: true }) + for (const file of ['package.json', 'src']) { + cpSync(join(cloud, '..', 'src', 'packages', 'process-host', file), join(packageDir, file), { + recursive: true + }) + } + const scripts = join(standalone, 'dev', 'scripts') + mkdirSync(scripts, { recursive: true }) + cpSync(join(cloud, 'dev', 'scripts', 'run-relay-tests.mjs'), join(scripts, 'run-relay-tests.mjs')) + writeFileSync(join(standalone, 'package.json'), JSON.stringify(manifest)) + mkdirSync(join(standalone, 'apps', 'relay'), { recursive: true }) + mkdirSync(join(standalone, 'node_modules', '@orca'), { recursive: true }) + symlinkSync( + packageDir, + join(standalone, 'node_modules', '@orca', 'process-host'), + process.platform === 'win32' ? 'junction' : 'dir' + ) + const fakeVitest = join(standalone, 'node_modules', 'vitest') + mkdirSync(fakeVitest) + writeFileSync(join(fakeVitest, 'package.json'), '{"name":"vitest","version":"0.0.0"}') + writeFileSync( + join(fakeVitest, 'vitest.mjs'), + 'console.log(JSON.stringify({args:process.argv.slice(2), background:process.env.ORCA_BACKGROUND_LAUNCH, database:process.env.ORCA_RELAY_TEST_POSTGRES_URL}))' + ) + assert.equal(existsSync(join(root, 'node_modules')), false) + assert.equal(existsSync(join(packageDir, 'dist')), false) + const result = await runProcess({ + program: process.execPath, + args: [ + '--import', + pathToFileURL(require.resolve('tsx')).href, + '--conditions=orca-source', + join(scripts, 'run-relay-tests.mjs'), + 'src/requested.test.ts' + ], + cwd: standalone, + timeoutMs: 15_000, + terminationBarrier: true, + env: { + ...process.env, + NODE_PATH: '', + NODE_OPTIONS: '', + ORCA_BACKGROUND_LAUNCH: '1', + ORCA_RELAY_TEST_POSTGRES_URL: databaseUrl + } + }) + assert.equal(result.code, 0, result.stderr) + assert.deepEqual(JSON.parse(result.stdout), { + args: ['run', 'src/requested.test.ts'], + background: '1', + database: databaseUrl + }) + assert.equal(existsSync(join(packageDir, 'dist')), false) +}) diff --git a/cloud/dev/scripts/verify-relay-capacity-transition.mjs b/cloud/dev/scripts/verify-relay-capacity-transition.mjs index 0c819adc9b6..9e0ce7a71ac 100644 --- a/cloud/dev/scripts/verify-relay-capacity-transition.mjs +++ b/cloud/dev/scripts/verify-relay-capacity-transition.mjs @@ -420,14 +420,17 @@ export async function verifyCapacityTransition(config, overrides = {}) { if (health.ok !== true || health.connectionCapacityProtocol !== CAPACITY_PROTOCOL) { throw new Error('director is not capacity-protocol compatible') } - const deadline = now() + config.timeoutMs - // A paced drain can refill the runtime until its window ends, so the quiet must outlast it. - // Each poll waits a full interval, so consecutive samples bound the quiet time from below. - const requiredRestartSafeSamples = - Math.max(Math.ceil((config.paceWindowMs ?? 0) / POLL_INTERVAL_MS) + 1, 2) + const startedAt = now() + const deadline = startedAt + config.timeoutMs + // A paced drain can refill the runtime until its window ends, so the second of two consecutive + // quiet samples must be taken at or after that end. This runs after the cell accepted the drain, + // so the cell's window ends no later than this one. + const paceWindowEndsAt = startedAt + (config.paceWindowMs ?? 0) let restartSafeSamples = 0 let lastObservation = { runtimeAvailable: false } for (;;) { + // Before the read, so a sample taken inside the window cannot pass. + const sampledAt = now() const runtime = await cellRuntime(fetchImpl, config, token) lastObservation = { runtimeAvailable: runtime !== null } if ((runtime === null) === (config.runtime === 'unavailable')) { @@ -469,7 +472,7 @@ export async function verifyCapacityTransition(config, overrides = {}) { if ( matches && (config.activity !== 'restart-safe' || - restartSafeSamples + 1 >= requiredRestartSafeSamples) + (restartSafeSamples >= 1 && sampledAt >= paceWindowEndsAt)) ) { return { cellId: status.cellId, @@ -491,15 +494,16 @@ export async function verifyCapacityTransition(config, overrides = {}) { restartSafeSamples = 0 } if (config.activity === 'restart-safe') { + const paceWindowRemainingMs = Math.max(paceWindowEndsAt - sampledAt, 0) lastObservation = { ...lastObservation, restartSafeSamples, - requiredRestartSafeSamples + paceWindowRemainingMs } progress({ event: 'relay_capacity_transition_restart_progress', restartSafeSamples, - requiredRestartSafeSamples, + paceWindowRemainingMs, totalConnections: lastObservation.runtime?.totalConnections ?? null, preAuthConnections: lastObservation.runtime?.preAuthConnections ?? null, runtimeAvailable: lastObservation.runtimeAvailable, diff --git a/cloud/dev/scripts/verify-relay-capacity-transition.test.mjs b/cloud/dev/scripts/verify-relay-capacity-transition.test.mjs index 71d8d88b6a8..8d861b3bd57 100644 --- a/cloud/dev/scripts/verify-relay-capacity-transition.test.mjs +++ b/cloud/dev/scripts/verify-relay-capacity-transition.test.mjs @@ -856,8 +856,7 @@ test('restart timeout reports one of two safe settling samples', async () => { { fetch: base, token: 'masked-token' } ), (error) => { - assert.match(error.message, /"restartSafeSamples":1/) - assert.match(error.message, /"requiredRestartSafeSamples":2/) + assert.match(error.message, /"restartSafeSamples":1,"paceWindowRemainingMs":0/) return true } ) @@ -1177,8 +1176,8 @@ const pacedRestartConfig = { timeoutMs: 1_200_000 } -function fakeClock() { - let now = 0 +function fakeClock(start = 0) { + let now = start return { now: () => now, wait: async (milliseconds) => { @@ -1231,7 +1230,8 @@ test('passes after one pace window of an empty runtime and reports stranded leas }, token: 'masked-token', progress: (line) => lines.push(line), - ...fakeClock() + // A late start catches a window measured from zero instead of from the verifier's start. + ...fakeClock(1_000_000) }) // One sample at each end of a full five-minute pace window. assert.equal(runtimeReads, 61) @@ -1246,12 +1246,15 @@ test('passes after one pace window of an empty runtime and reports stranded leas assert.equal(lines.length, 60) assert.ok(lines.every((line) => line.event === 'relay_capacity_transition_restart_progress' && - line.requiredRestartSafeSamples === 61 && line.stranded.restartBlockingActivityLeases === 4)) assert.deepEqual(lines.slice(0, 2).map((line) => line.restartSafeSamples), [1, 2]) + assert.deepEqual( + lines.slice(0, 2).map((line) => line.paceWindowRemainingMs), + [300_000, 295_000] + ) }) -test('the pace window resets on any non-zero runtime sample', async () => { +test('a non-zero runtime sample restarts the quiet count', async () => { const quiet = harness(strandedCell) const busy = harness({ ...strandedCell, controls: 1 }) let runtimeReads = 0 @@ -1265,17 +1268,37 @@ test('the pace window resets on any non-zero runtime sample', async () => { progress: (line) => lines.push(line), ...fakeClock() }) - assert.equal(runtimeReads, 91) + // A mid-window reset adds no time; the quiet after it still reaches the window's end. + assert.equal(runtimeReads, 61) assert.deepEqual(lines.slice(28, 31).map((line) => line.restartSafeSamples), [29, 0, 1]) await assert.rejects( verifyCapacityTransition( { ...pacedRestartConfig, timeoutMs: 295_000 }, { fetch: quiet, token: 'masked-token', progress: () => {}, ...fakeClock() } ), - /"restartSafeSamples":60,"requiredRestartSafeSamples":61/ + /"restartSafeSamples":60,"paceWindowRemainingMs":5000/ ) }) +// c27 on 2026-10-13: the 20-minute paced drain emptied the runtime as its window closed, and the +// old gate then waited a second full window of quiet samples before the restart. +test('a runtime that empties as the pace window ends passes after two quiet samples', async () => { + let runtimeReads = 0 + const lines = [] + await verifyCapacityTransition(pacedRestartConfig, { + fetch: async (url, options) => { + if (new URL(url).pathname === '/v1/admin/runtime-status') runtimeReads += 1 + return await harness({ ...strandedCell, controls: runtimeReads <= 60 ? 1 : 0 })(url, options) + }, + token: 'masked-token', + progress: (line) => lines.push(line), + ...fakeClock() + }) + // Busy through 295 s; quiet at 300 s and 305 s, the first eligible pair. + assert.equal(runtimeReads, 62) + assert.deepEqual(lines.slice(59).map((line) => line.restartSafeSamples), [0, 1]) +}) + test('restart-safe fails on live runtime work or an open migration regardless of leases', async (t) => { for (const blocker of [ { controls: 1 }, @@ -1299,7 +1322,7 @@ test('restart-safe fails on live runtime work or an open migration regardless of } }) -test('unauthenticated redials do not reset the pace window', async () => { +test('unauthenticated redials do not restart the quiet count', async () => { let runtimeReads = 0 const lines = [] const result = await verifyCapacityTransition(pacedRestartConfig, { @@ -1439,7 +1462,7 @@ test('restart progress prints every counter the gate reads', async () => { assert.deepEqual(lines[0], { event: 'relay_capacity_transition_restart_progress', restartSafeSamples: 0, - requiredRestartSafeSamples: 61, + paceWindowRemainingMs: 300_000, totalConnections: 7, preAuthConnections: 1, runtimeAvailable: true, diff --git a/cloud/dev/scripts/watch-relay-reserve-cell.mjs b/cloud/dev/scripts/watch-relay-reserve-cell.mjs new file mode 100644 index 00000000000..6f5cc38e279 --- /dev/null +++ b/cloud/dev/scripts/watch-relay-reserve-cell.mjs @@ -0,0 +1,581 @@ +#!/usr/bin/env node +// Watches one cell already in reserve mode and, on the first stop rule it trips, flips it back to +// db through the flag tool's own write (CAS, read-back, re-registration wait). Step 5 design §9. +// +// watch: one segment of the watch (the workflow re-authenticates between segments, since an +// ID token lasts an hour). State carries over in --state-file. Writes nothing. +// outcome: the job's state and verdict, from the state file. +// flip-back: the flag tool's admitMode=db write (or its dry run), then exits non-zero with the +// watch's verdict. Why it holds no flag-workflow group: docs/relay-workflows.md. +// alarm: a refused watch or a flip back that did not land: an error annotation, and in write +// mode a log line the reserve-watch alert pages on. +import { execFile } from 'node:child_process' +import { appendFile, readFile, writeFile } from 'node:fs/promises' +import { parseArgs, promisify } from 'node:util' +import { pathToFileURL } from 'node:url' +import { + BREAK_GLASS_CONFIRMATION, + operateCellFlags, + parseCellFlagsRequest, + readCurrentObject, + readRuntime, + WRITE_CONFIRMATION +} from './operate-relay-cell-flags.mjs' +import { readDirectorRequestsByCodePerMinute, readLogEntries } from './relay-same-cap-shadow-gate.mjs' +import { minutesOf } from './relay-same-cap-shadow-gate-verdict.mjs' +import { PRODUCTION_LEASE } from './cloud-sql-rollout-lock-census.mjs' +import { RELAY_REPOSITORY_ROOT } from './relay-repository.mjs' +import { + evaluateWatchSample, + initialWatchState, + measuredBackground, + RESERVE_WATCH_RULES +} from './relay-reserve-watch-rules.mjs' + +const execFileAsync = promisify(execFile) +const MINUTE_MS = 60_000 +export const SAMPLE_INTERVAL_MS = MINUTE_MS +export const MAX_WATCH_MINUTES = 300 +// Below the hour an ID token lasts, with room for the segment's last sample. +export const MAX_SEGMENT_MINUTES = 55 +const METRIC_DELAY_MS = RESERVE_WATCH_RULES.director503.publishDelayMs +const BACKGROUND_MINUTES = 10 +// A flipped-at older than this is a typo or a stale dispatch, not this flip. +const MAX_FLIPPED_AT_AGE_MS = 24 * 60 * MINUTE_MS +export const FLIP_BACK_ATTEMPTS = 5 +const FLIP_BACK_BACKOFF_MS = 2_000 +// One slow runtime-status reply is not a dead cell: retried this often before break glass. +export const FLIP_BACK_RUNTIME_ATTEMPTS = 4 +const SAMPLE_READ_BUDGET_MS = 40_000 +const LOG_ENTRY_LIMIT = 1_000 + +function integerIn(name, raw, min, max) { + const value = Number(raw) + if (!/^[0-9]+$/.test(raw ?? '') || value < min || value > max) { + throw new Error(`--${name} must be an integer from ${min} to ${max}`) + } + return value +} + +// The flip back's request. Its generation is a placeholder: the flip back re-reads the current one. +export function dbFlipRequest(values) { + return parseCellFlagsRequest({ ...values, set: 'admitMode=db', 'expected-generation': '0' }) +} + +// `now` is the watch's start: a later segment of a long watch must not age flipped-at out. +function parseFlippedAt(raw, now) { + if (raw === '') return null + const at = Date.parse(raw) + if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})$/.test(raw) || !Number.isFinite(at)) { + throw new Error('--flipped-at must be an ISO time with a zone, e.g. 2026-10-14T12:00:00Z') + } + if (at > now) throw new Error('--flipped-at is in the future') + if (at < now - MAX_FLIPPED_AT_AGE_MS) throw new Error('--flipped-at is more than 24 hours ago') + return at +} + +export function parseWatchArguments(values, now = Date.now()) { + // The flag tool's own checks, so the watch is of exactly the cell the flip back would write; + // a write-mode watch refuses a missing confirmation now, not hours later. + const request = dbFlipRequest(values) + if (!/^[a-z][a-z0-9-]{0,62}$/.test(values['director-service'] ?? '')) { + throw new Error('--director-service is not a Cloud Run service name') + } + const background = values['background-503-per-minute'] ?? '' + if (background !== '' && !/^(0|[1-9][0-9]{0,6})(\.[0-9]+)?$/.test(background)) { + throw new Error('--background-503-per-minute must be empty (measure it) or a non-negative number') + } + // The background must predate the flip: a watch starts after it, so its own past is not one. + const flippedAt = parseFlippedAt(values['flipped-at'] ?? '', now) + if (background === '' && flippedAt === null) { + throw new Error('--flipped-at (the background is the 10 minutes before it) or --background-503-per-minute is required') + } + if (!values['state-file']) throw new Error('--state-file is required') + return { + request, + directorService: values['director-service'], + durationMinutes: integerIn('duration-minutes', values['duration-minutes'], 5, MAX_WATCH_MINUTES), + segmentMinutes: integerIn('segment-minutes', values['segment-minutes'], 1, MAX_SEGMENT_MINUTES), + unverifiableLimit: integerIn('unverifiable-limit', values['unverifiable-limit'], 1, 30), + background503PerMinute: background === '' ? null : Number(background), + flippedAt, + stateFile: values['state-file'], + githubOutput: values['github-output'] || null + } +} + +const failedRead = (error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) }) + +export async function readCellSample(fetchImpl, request, idToken, wait) { + try { + const runtime = await readRuntime(fetchImpl, request, idToken, wait) + if (runtime.role !== 'cell' || runtime.cellId !== request.cellId) { + return failedRead(`origin answers as ${runtime.role}/${runtime.cellId}`) + } + if (!runtime.flagsApplied) return failedRead('runtime-status reports no flagsApplied') + const controls = runtime.runtime?.controls + if (!Number.isInteger(controls)) return failedRead('runtime-status reports no control count') + return { + ok: true, + admitModeConfigured: runtime.flagsApplied.flags?.admitMode ?? 'db', + admitModeEffective: runtime.admitModeEffective, + draining: runtime.draining === true, + controls, + enforcedUnits: runtime.runtime?.enforcedConnectionUnits, + hardCap: runtime.connectionCapacity?.hardCap + } + } catch (error) { + return failedRead(error) + } +} + +const minuteFloor = (at) => Math.floor(at / MINUTE_MS) * MINUTE_MS +const failed503Read = (error) => ({ ok: false, error, perMinute: {}, minutes: [], publishedThrough: null }) + +// The `minutes` whole minutes that end at the minute boundary `endedAt`: the 503 counts, and the +// newest minute any response code has a point for. The director always serves something, so that +// front is how far the 503 series is published; no front at all is no evidence. A code publishing +// late only delays a trip: a later read sees its points. +export async function readDirector503Minutes(reader, { projectId, directorService, endedAt, minutes }) { + const span = { startedAt: new Date(endedAt - minutes * MINUTE_MS), endedAt: new Date(endedAt) } + let read + try { + read = await readDirectorRequestsByCodePerMinute(reader, { config: { projectId, directorService }, span }) + } catch (error) { + // A point with an unparseable end time has no minute. + return failed503Read(`director 503 metric unreadable: ${error instanceof Error ? error.message : error}`) + } + if (read.failed) return failed503Read('director 503 metric read failed') + const published = Object.values(read.byCode).flatMap((perMinute) => Object.keys(perMinute)).sort() + return { + ok: true, + perMinute: read.byCode['503'] ?? {}, + minutes: minutesOf(span), + publishedThrough: published.at(-1) ?? null + } +} + +const LEDGER_SUMMARY_EVENT = 'orca_relay_reserve_ledger_summary' +const SEAT_BEHIND_ROW_EVENT = 'orca_relay_reserve_ledger_seat_behind_row' + +function directorLogFilter(directorService, eventClause, sinceAt) { + return [ + 'resource.type="cloud_run_revision"', + `resource.labels.service_name="${directorService}"`, + eventClause, + `timestamp>="${new Date(sinceAt).toISOString()}"` + ].join(' AND ') +} + +// Newest summary per director instance; queued plus dropped rows are the ones not in Postgres. +export async function readLedgerSample(reader, { directorService, at }) { + const read = await readLogEntries(reader, { + filter: directorLogFilter(directorService, `jsonPayload.event="${LEDGER_SUMMARY_EVENT}"`, at - RESERVE_WATCH_RULES.ledgerSummaryMaxAgeMs), + projection: 'json(timestamp,labels.instanceId,jsonPayload.pending,jsonPayload.dropped)', + limit: LOG_ENTRY_LIMIT + }) + if (read.failed) return failedRead(read.error) + if (read.entries.length >= LOG_ENTRY_LIMIT) return failedRead('ledger summary read truncated') + if (read.entries.length === 0) return failedRead('no placing director logged a ledger summary') + if (read.entries.some((entry) => !Number.isFinite(Date.parse(entry.timestamp)))) { + return failedRead('ledger summary with an unparseable timestamp') + } + const newest = new Map() + for (const entry of read.entries) { + const instance = entry.labels?.instanceId ?? 'unknown' + const previous = newest.get(instance) + if (!previous || Date.parse(entry.timestamp) > Date.parse(previous.timestamp)) newest.set(instance, entry) + } + let unwrittenRows = 0 + for (const entry of newest.values()) { + const pending = Number(entry.jsonPayload?.pending) + const dropped = Number(entry.jsonPayload?.dropped) + if (!Number.isFinite(pending) || !Number.isFinite(dropped)) return failedRead('ledger summary without counts') + unwrittenRows += pending + dropped + } + return { ok: true, unwrittenRows, directors: newest.size } +} + +// The same read also takes the ledger summary every placing director logs each minute, as a +// positive control: with no fresh summary in it, no seat-behind-row line is blindness, not zero. +export async function readSeatBehindRowSample(reader, { directorService, cellId, at }) { + const read = await readLogEntries(reader, { + filter: directorLogFilter( + directorService, + `((jsonPayload.event="${SEAT_BEHIND_ROW_EVENT}" AND jsonPayload.cellId="${cellId}") OR jsonPayload.event="${LEDGER_SUMMARY_EVENT}")`, + at - RESERVE_WATCH_RULES.duplicateSeat.lookbackMs + ), + projection: 'json(timestamp,labels.instanceId,jsonPayload.event,jsonPayload.epoch,jsonPayload.rowEpoch)', + limit: LOG_ENTRY_LIMIT + }) + if (read.failed) return failedRead(read.error) + if (read.entries.length >= LOG_ENTRY_LIMIT) return failedRead('seat-behind-row read truncated') + const reports = [] + let controlSeen = false + for (const entry of read.entries) { + const reportedAt = Date.parse(entry.timestamp) + if (!Number.isFinite(reportedAt)) return failedRead('seat-behind-row line with an unparseable timestamp') + if (entry.jsonPayload?.event === LEDGER_SUMMARY_EVENT) { + controlSeen ||= reportedAt >= at - RESERVE_WATCH_RULES.ledgerSummaryMaxAgeMs + continue + } + const { epoch, rowEpoch } = entry.jsonPayload ?? {} + if (epoch === undefined || rowEpoch === undefined) return failedRead('seat-behind-row line without epochs') + // Everything the line carries but its time, which differs on every report of one seat. A + // proxy for the host: two hosts sharing it read as one seat, so it can only over-count. + reports.push({ at: reportedAt, key: `${entry.labels?.instanceId ?? 'unknown'}/${cellId}/${epoch}:${rowEpoch}` }) + } + if (!controlSeen) return failedRead('seat-behind-row read holds no fresh ledger summary (positive control)') + return { ok: true, reports } +} + +function createReader(dependencies, projectId) { + const now = dependencies.now + return { + runGcloud: dependencies.runGcloud, + fetch: dependencies.fetchImpl, + retryDelayMs: dependencies.retryDelayMs ?? 5_000, + readTimeoutMs: 20_000, + now, + // Every sample gets its own budget, so a slow read costs one sample, not the watch. + deadlineAt: now() + SAMPLE_READ_BUDGET_MS, + projectId + } +} + +/** + * Same-cap and capacity rolls hold the Cloud SQL rollout lease for the whole run; it names no + * cell, so any live holder refuses the watch's start. Read only: holding it for hours would block + * every rollout. An unreadable lease refuses too (fail closed, as the lease action does). + */ +export async function rolloutLeaseRefusal(readRolloutLease, now) { + try { + const existing = await readRolloutLease() + if (!existing || existing.record.expires_at <= now) return null + return ( + `a rollout holds the Cloud SQL rollout lease until ${new Date(existing.record.expires_at).toISOString()} ` + + `(${existing.holder}); start the watch after it ends` + ) + } catch (error) { + return `the Cloud SQL rollout lease is unreadable (${error instanceof Error ? error.message : error}); refusing to start` + } +} + +// The lease action's own reader, resolved from the repository root because this tree's depth +// differs between the two repositories' copies. +async function readProductionRolloutLease() { + const action = new URL('.github/actions/cloud-sql-rollout-lease/', RELAY_REPOSITORY_ROOT) + const [{ CloudSqlRolloutLease, describeHolder }, { createAccessTokenSource }] = await Promise.all([ + import(new URL('storage-lease.mjs', action).href), + import(new URL('gcloud-access-token.mjs', action).href) + ]) + const lease = new CloudSqlRolloutLease({ + bucket: PRODUCTION_LEASE.bucket, + objectName: PRODUCTION_LEASE.object, + accessToken: createAccessTokenSource() + }) + const existing = await lease.read() + return existing && { record: existing.record, holder: describeHolder(existing.record) } +} + +// Only a missing file is a fresh watch. Anything else that is not a state fails the segment, which +// the outcome step reads as failed and flips back: a corrupt file must not restart or refuse the watch. +export async function loadState(path) { + const text = await readFile(path, 'utf8').catch((error) => (error?.code === 'ENOENT' ? null : Promise.reject(error))) + if (text === null) return null + const state = JSON.parse(text) + const isObject = state !== null && typeof state === 'object' && !Array.isArray(state) + if (!isObject || typeof state.state !== 'string') throw new Error(`state file ${path} holds no watch state`) + if (state.state === 'watching' && (typeof state.cellId !== 'string' || !Number.isFinite(state.startedAt))) { + throw new Error(`state file ${path} has no cell or start`) + } + return state +} + +// A segment's config. Later segments check flipped-at against the watch's start; bad inputs before +// the first sample are refused, so no flip back follows a typo. +export async function parseWatchSegment(values, now = Date.now()) { + const existing = values['state-file'] ? await loadState(values['state-file']) : null + try { + return parseWatchArguments(values, Number.isFinite(existing?.startedAt) ? existing.startedAt : now) + } catch (error) { + if (values['state-file'] && existing === null) { + await writeFile(values['state-file'], JSON.stringify({ state: 'refused', verdict: error.message })) + } + throw error + } +} + +export async function watchSegment(config, dependencies) { + const now = dependencies.now ?? Date.now + const sleep = dependencies.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))) + const log = dependencies.log ?? console.log + const deps = { ...dependencies, now, sleep, fetchImpl: dependencies.fetchImpl ?? fetch } + const { request, directorService } = config + let state = await loadState(config.stateFile) + if (state === null) { + const refusal = await rolloutLeaseRefusal(dependencies.readRolloutLease, now()) + if (refusal) { + state = { state: 'refused', verdict: refusal } + await writeFile(config.stateFile, JSON.stringify(state)) + if (config.githubOutput) await writeOutcome(config.githubOutput, state) + return state + } + state = initialWatchState({ + cellId: request.cellId, + startedAt: now(), + durationMinutes: config.durationMinutes, + background503PerMinute: config.background503PerMinute, + flippedAt: config.flippedAt + }) + } + if (state.cellId !== request.cellId) throw new Error(`state file is for ${state.cellId}, not ${request.cellId}`) + const segmentEndsAt = now() + config.segmentMinutes * MINUTE_MS + while (state.state === 'watching' && now() < segmentEndsAt) { + const at = now() + const baselineEndsAt = state.flippedAt === null ? null : minuteFloor(state.flippedAt) + if (!state.background && baselineEndsAt !== null && at >= baselineEndsAt + METRIC_DELAY_MS) { + // The minutes just before the flip, once published; retried every sample until one reads. + const read = await readDirector503Minutes(createReader(deps, request.projectId), { + projectId: request.projectId, + directorService, + endedAt: baselineEndsAt, + minutes: BACKGROUND_MINUTES + }) + state = { ...state, background: read.ok ? measuredBackground(read.perMinute, read.minutes, read.publishedThrough) : null } + } + const reader = createReader(deps, request.projectId) + // Serialised: concurrent Logging reads are what earn a 429. + const sample = { + at, + cell: await readCellSample(deps.fetchImpl, request, dependencies.idToken, sleep), + director503: await readDirector503Minutes(reader, { + projectId: request.projectId, + directorService, + // Complete minutes only, held back past the publishing delay. + endedAt: minuteFloor(at - METRIC_DELAY_MS), + minutes: RESERVE_WATCH_RULES.director503.readMinutes + }), + ledger: await readLedgerSample(reader, { directorService, at }), + seatBehindRow: await readSeatBehindRowSample(reader, { directorService, cellId: request.cellId, at }) + } + const result = evaluateWatchSample(state, sample, { unverifiableLimit: config.unverifiableLimit }) + state = result.state + log(result.line) + await writeFile(config.stateFile, JSON.stringify(state)) + if (state.state !== 'watching') break + await sleep(Math.max(0, at + SAMPLE_INTERVAL_MS - now())) + } + if (config.githubOutput) await writeOutcome(config.githubOutput, state) + return state +} + +function writeOutcome(githubOutput, { state, verdict }) { + return appendFile(githubOutput, `state=${state}\nverdict=${(verdict ?? '').replace(/[\r\n]+/g, ' ')}\n`) +} + +// The job's outcome. A watch that ended mid-segment (a crash, a timeout) records failed, which the +// workflow flips back in write mode: missing data is never healthy. +export async function recordOutcome(stateFile, githubOutput) { + const state = await loadState(stateFile).catch(() => null) + const outcome = + state && state.state !== 'watching' ? state : { state: 'failed', verdict: 'the watch stopped before a verdict' } + await writeOutcome(githubOutput, outcome) + return outcome +} + +function breakGlassInputs(request, generation) { + return { + mode: 'write', + 'cell-id': request.cellId, + set: 'admitMode=default', + 'break-glass': `${BREAK_GLASS_CONFIRMATION} ${request.cellId}`, + 'expected-generation': generation, + confirmation: `${WRITE_CONFIRMATION} ${request.cellId}` + } +} + +async function readRuntimeRetrying(fetchImpl, request, idToken, sleep, log) { + for (let attempt = 1; ; attempt += 1) { + try { + return await readRuntime(fetchImpl, request, idToken, sleep) + } catch (error) { + if (attempt >= FLIP_BACK_RUNTIME_ATTEMPTS) throw error + log(JSON.stringify({ event: 'orca_relay_reserve_watch_runtime_retry', cellId: request.cellId, attempt, message: error instanceof Error ? error.message : String(error) })) + await sleep(FLIP_BACK_BACKOFF_MS * 2 ** (attempt - 1)) + } + } +} + +/** + * The flag tool's admitMode=db write at the generation read just now. A generation conflict (some + * other write landed first) is re-read and retried while the switch file still says reserve; a + * file that already says db is done. A cell that still does not answer after the runtime retries + * needs break glass, which only an operator may type. + */ +export async function flipBack(values, dependencies) { + const log = dependencies.log ?? console.log + const fetchImpl = dependencies.fetchImpl ?? fetch + const sleep = dependencies.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))) + const base = dbFlipRequest(values) + const record = (result) => + log( + JSON.stringify({ + event: 'orca_relay_reserve_watch_flip_back', + mode: base.mode, + cellId: base.cellId, + written: result.written, + generation: result.generation ?? null, + ...(result.alreadyDb ? { alreadyDb: true } : {}) + }) + ) + for (let attempt = 1; ; attempt += 1) { + const current = await readCurrentObject(fetchImpl, base, dependencies.accessToken) + if ((current.object?.flags?.admitMode ?? 'db') !== 'reserve') { + const result = { written: false, alreadyDb: true, generation: current.generation } + record(result) + return result + } + try { + await readRuntimeRetrying(fetchImpl, base, dependencies.idToken, sleep, log) + } catch (error) { + const inputs = breakGlassInputs(base, current.generation) + log(JSON.stringify({ event: 'orca_relay_reserve_watch_break_glass_required', cellId: base.cellId, inputs })) + throw new Error( + `${base.cellId} does not answer runtime-status (${error instanceof Error ? error.message : error}), so ` + + 'the flag tool cannot flip it back. Run Operate Relay Production Cell Flags with break glass: ' + + Object.entries(inputs).map(([key, value]) => `${key}=${value}`).join(', ') + ) + } + try { + const result = await operateCellFlags( + { ...base, expectedGeneration: current.generation }, + { ...dependencies, fetchImpl, log, sleep } + ) + record(result) + return result + } catch (error) { + if (!error?.generationConflict || attempt >= FLIP_BACK_ATTEMPTS) throw error + log(JSON.stringify({ event: 'orca_relay_reserve_watch_flip_back_conflict', cellId: base.cellId, attempt, message: error.message })) + await sleep(FLIP_BACK_BACKOFF_MS * 2 ** (attempt - 1)) + } + } +} + +export const ALARM_EVENT = 'orca_relay_reserve_watch_alarm' +export const ALARM_LOG_NAME = 'orca-relay-reserve-watch' +export const ALARM_KINDS = ['refused', 'flip-back-failed'] + +// GitHub's workflow-command escaping, so a verdict cannot end the annotation or start another. +export const escapeWorkflowCommand = (text) => text.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A') +const escapeWorkflowProperty = (text) => escapeWorkflowCommand(text).replace(/:/g, '%3A').replace(/,/g, '%2C') + +/** + * Both kinds can leave the cell in reserve with nothing watching it. Only write mode pages: a + * dry-run watch never flips back, so its refusal or failure changes nothing for the cell. + */ +export async function raiseAlarm({ mode, cellId, projectId, kind, reason }, { runGcloud, log = console.log }) { + if (!ALARM_KINDS.includes(kind)) throw new Error(`--kind must be one of ${ALARM_KINDS.join(', ')}`) + const message = + kind === 'refused' + ? `${cellId} may be in reserve mode and unwatched: the watch was refused (${reason}). Rerun it, or flip the cell to db with Operate Relay Production Cell Flags.` + : `${cellId} may still be in reserve mode with no watch: the flip back did not land (${reason}). Flip it to db by hand with Operate Relay Production Cell Flags (set=admitMode=db), or with the break-glass inputs logged above if the cell does not answer.` + log(`::error title=${escapeWorkflowProperty(`Reserve watch ${kind} for ${cellId}`)}::${escapeWorkflowCommand(message)}`) + if (mode !== 'write') return { paged: false } + const entry = JSON.stringify({ event: ALARM_EVENT, cellId, kind, reason }) + try { + // The flag tool's project check: a missing --project-id must not reach gcloud as "undefined". + if (!/^[a-z][a-z0-9-]{4,62}$/.test(projectId ?? '')) throw new Error('--project-id is not a GCP project id') + await runGcloud( + ['logging', 'write', ALARM_LOG_NAME, entry, '--payload-type=json', '--severity=ERROR', '--project', projectId], + { timeoutMs: 30_000 } + ) + } catch (error) { + log(`::error title=Reserve watch page failed::${escapeWorkflowCommand(`the alarm log line was not written, so nothing pages: ${error instanceof Error ? error.message : error}`)}`) + return { paged: false } + } + return { paged: true } +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { + const [command, ...rest] = process.argv.slice(2) + const { values } = parseArgs({ + args: rest, + options: Object.fromEntries( + [ + 'mode', + 'cell-id', + 'cell-origin', + 'project-id', + 'director-origin', + 'director-service', + 'confirmation', + 'duration-minutes', + 'segment-minutes', + 'unverifiable-limit', + 'background-503-per-minute', + 'flipped-at', + 'state-file', + 'github-output', + 'verdict', + 'kind' + ].map((name) => [name, { type: 'string' }]) + ) + }) + const idToken = process.env.ORCA_RELAY_ADMIN_ID_TOKEN + const runGcloud = (args, { timeoutMs }) => + execFileAsync('gcloud', args, { maxBuffer: 64 * 1024 * 1024, timeout: timeoutMs, killSignal: 'SIGKILL' }) + try { + if (command === 'alarm') { + await raiseAlarm( + { mode: values.mode, cellId: values['cell-id'] ?? '', projectId: values['project-id'], kind: values.kind, reason: values.verdict ?? '' }, + { runGcloud } + ) + // The alarm step is red, whatever the page did. + process.exitCode = 1 + } else if (command === 'outcome') { + if (!values['state-file'] || !values['github-output']) throw new Error('--state-file and --github-output are required') + console.log(JSON.stringify({ event: 'orca_relay_reserve_watch_outcome', ...(await recordOutcome(values['state-file'], values['github-output'])) })) + } else if (!idToken) { + throw new Error('ORCA_RELAY_ADMIN_ID_TOKEN is required') + } else if (command === 'watch') { + const config = await parseWatchSegment(values) + const state = await watchSegment(config, { + idToken, + readRolloutLease: readProductionRolloutLease, + runGcloud + }) + // A cell that was never reserve is the operator's mistake; one flipped back by hand is not. + if (state.state === 'refused') { + console.log(JSON.stringify({ event: 'orca_relay_reserve_watch_refused', verdict: state.verdict })) + process.exitCode = 1 + } + } else if (command === 'flip-back') { + const accessToken = process.env.ORCA_RELAY_CONTROL_ACCESS_TOKEN + if (!accessToken) throw new Error('ORCA_RELAY_CONTROL_ACCESS_TOKEN is required') + if (!values.verdict) throw new Error('--verdict is required') + await flipBack(values, { + accessToken, + idToken, + audit: { + writtenBy: `reserve-watch/${process.env.GITHUB_TRIGGERING_ACTOR ?? process.env.GITHUB_ACTOR ?? 'unknown'}`, + runId: process.env.GITHUB_RUN_ID ?? 'local', + runAttempt: process.env.GITHUB_RUN_ATTEMPT ?? '0' + } + }) + // A trip is a failed watch even once the cell is safely back on db. + console.log(JSON.stringify({ event: 'orca_relay_reserve_watch_verdict', mode: values.mode, verdict: values.verdict })) + process.exitCode = 1 + } else { + throw new Error('command must be watch, outcome, flip-back or alarm') + } + } catch (error) { + console.log( + JSON.stringify({ + event: 'orca_relay_reserve_watch_failed', + message: error instanceof Error ? error.message : String(error) + }) + ) + process.exitCode = 1 + } +} diff --git a/cloud/dev/scripts/watch-relay-reserve-cell.test.mjs b/cloud/dev/scripts/watch-relay-reserve-cell.test.mjs new file mode 100644 index 00000000000..28d34ab4723 --- /dev/null +++ b/cloud/dev/scripts/watch-relay-reserve-cell.test.mjs @@ -0,0 +1,958 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { test } from 'node:test' +import { + evaluateWatchSample, + initialWatchState, + measuredBackground +} from './relay-reserve-watch-rules.mjs' +import { + ALARM_EVENT, + ALARM_LOG_NAME, + FLIP_BACK_ATTEMPTS, + FLIP_BACK_RUNTIME_ATTEMPTS, + flipBack, + parseWatchArguments, + parseWatchSegment, + raiseAlarm, + readDirector503Minutes, + readSeatBehindRowSample, + recordOutcome, + rolloutLeaseRefusal, + watchSegment +} from './watch-relay-reserve-cell.mjs' +import { readRelayWorkflow } from './relay-repository.mjs' + +const CELL = 'production-gce-c26' +const START = Date.parse('2026-10-14T12:00:00Z') +const MINUTE = 60_000 +const LIMIT = { unverifiableLimit: 3 } + +const healthyCell = (overrides = {}) => ({ + ok: true, + admitModeConfigured: 'reserve', + admitModeEffective: 'reserve', + draining: false, + controls: 1_000, + enforcedUnits: 1_100, + hardCap: 2_000, + ...overrides +}) +const minutesBefore = (at, count) => + Array.from({ length: count }, (_, index) => + new Date(Math.floor(at / MINUTE) * MINUTE - (count + 3 - index) * MINUTE).toISOString().slice(0, 16) + ) +const quiet503 = (at, perMinute = 10) => { + const minutes = minutesBefore(at, 3) + return { ok: true, minutes, perMinute: Object.fromEntries(minutes.map((minute) => [minute, perMinute])), publishedThrough: minutes.at(-1) } +} +const sampleAt = (at, overrides = {}) => ({ + at, + cell: healthyCell(), + director503: quiet503(at), + ledger: { ok: true, unwrittenRows: 0, directors: 5 }, + seatBehindRow: { ok: true, reports: [] }, + ...overrides +}) +const freshState = (overrides = {}) => ({ + ...initialWatchState({ cellId: CELL, startedAt: START, durationMinutes: 60, background503PerMinute: 10 }), + ...overrides +}) + +// Runs samples one minute apart; returns every result. +function run(samples, state = freshState()) { + const results = [] + for (const sample of samples) { + const result = evaluateWatchSample(state, sample, LIMIT) + results.push(result) + state = result.state + if (state.state !== 'watching') break + } + return results +} + +test('a healthy cell keeps watching and completes at the deadline', () => { + const results = run(Array.from({ length: 61 }, (_, minute) => sampleAt(START + minute * MINUTE))) + assert.equal(results.length, 61) + assert.ok(results.slice(0, 60).every((result) => result.state.state === 'watching')) + assert.equal(results.at(-1).state.state, 'completed') + const line = JSON.parse(results[0].line) + assert.equal(line.event, 'orca_relay_reserve_watch_sample') + assert.deepEqual(Object.keys(line.rules), ['deadMan', 'overrun', 'director503', 'duplicateSeat', 'hostDrop', 'ledgerDrift']) +}) + +test('503s trip only after three complete minutes over max(1.5x background, background + 20)', () => { + // Background 100: the line is 150. Two minutes over and one under is not a trip. + const state = freshState({ background: { medianPerMinute: 100, source: 'input' } }) + const series = (counts) => { + const minutes = minutesBefore(START, 3) + return { + ok: true, + minutes, + perMinute: Object.fromEntries(minutes.map((minute, index) => [minute, counts[index]])), + publishedThrough: minutes.at(-1) + } + } + const twoOver = evaluateWatchSample(state, sampleAt(START, { director503: series([151, 140, 200]) }), LIMIT) + assert.equal(twoOver.state.state, 'watching') + const atLine = evaluateWatchSample(state, sampleAt(START, { director503: series([150, 150, 150]) }), LIMIT) + assert.equal(atLine.state.state, 'watching') + const threeOver = evaluateWatchSample(state, sampleAt(START, { director503: series([151, 160, 170]) }), LIMIT) + assert.equal(threeOver.state.state, 'tripped') + assert.match(threeOver.state.verdict, /^flip back production-gce-c26: director503: /) + // A zero background still needs 20 a minute, not one 503. + const zero = freshState({ background: { medianPerMinute: 0, source: 'input' } }) + assert.equal(evaluateWatchSample(zero, sampleAt(START, { director503: series([5, 5, 5]) }), LIMIT).state.state, 'watching') + assert.equal(evaluateWatchSample(zero, sampleAt(START, { director503: series([21, 21, 21]) }), LIMIT).state.state, 'tripped') + // A published minute with no 503 point had none. + const gap = series([151, 160, 170]) + delete gap.perMinute[gap.minutes[1]] + assert.equal(evaluateWatchSample(state, sampleAt(START, { director503: gap }), LIMIT).state.state, 'watching') +}) + +test('a quiet fleet with no 503 series at all reads zero, not unverifiable', () => { + const minutes = minutesBefore(START, 10) + const quiet = { ok: true, minutes, perMinute: {}, publishedThrough: minutes.at(-1) } + const [result] = run([sampleAt(START, { director503: quiet })], freshState({ background: { medianPerMinute: 0, source: 'measured' } })) + assert.equal(result.rules.director503.status, 'pass') + assert.deepEqual(Object.values(result.rules.director503.recent), [0, 0, 0]) + assert.equal(result.state.unverifiableStreak, 0) + assert.deepEqual(measuredBackground({}, minutes, minutes.at(-1)), { medianPerMinute: 0, minutes: 10, source: 'measured' }) +}) + +test('503 minutes past the publishing front are left out, and only no front at all is unverifiable', () => { + const state = freshState({ background: { medianPerMinute: 100, source: 'input' } }) + const minutes = minutesBefore(START, 10) + const judge = (director503) => evaluateWatchSample(state, sampleAt(START, { director503 }), LIMIT) + // The front is two minutes behind: the three minutes before it are judged, the two after are not. + const over = Object.fromEntries(minutes.map((minute) => [minute, 500])) + const behind = judge({ ok: true, minutes, perMinute: over, publishedThrough: minutes[7] }) + assert.equal(behind.rules.director503.status, 'trip') + assert.deepEqual(Object.keys(behind.rules.director503.recent), minutes.slice(5, 8)) + // A sparse 503 series: minutes after its last point but within the front are zero, not late. + const sparse = judge({ ok: true, minutes, perMinute: { [minutes[2]]: 500 }, publishedThrough: minutes.at(-1) }) + assert.equal(sparse.rules.director503.status, 'pass') + assert.equal(sparse.state.unverifiableStreak, 0) + // No request series at all. + const empty = judge({ ok: true, minutes, perMinute: {}, publishedThrough: null }) + assert.equal(empty.rules.director503.status, 'unverifiable') + assert.match(empty.rules.director503.reason, /request series is empty/) + assert.equal(empty.state.unverifiableStreak, 1) + // A front too far behind to leave three published minutes in the read. + const stale = judge({ ok: true, minutes, perMinute: {}, publishedThrough: minutes[1] }) + assert.equal(stale.rules.director503.status, 'unverifiable') + assert.match(stale.rules.director503.reason, new RegExp(`published only through ${minutes[1]}`)) +}) + +const SEAT_EVENT = 'orca_relay_reserve_ledger_seat_behind_row' +const SUMMARY_EVENT = 'orca_relay_reserve_ledger_summary' +const summaryLine = (at, instanceId = 'a') => ({ + timestamp: new Date(at).toISOString(), + labels: { instanceId }, + jsonPayload: { event: SUMMARY_EVENT, pending: 0, dropped: 0 } +}) + +// A Cloud Monitoring or Cloud Logging reader over fakes, as the watch builds one. +const fakeReader = ({ fetch: fetchImpl = async () => new Response('{}'), runGcloud = async () => ({ stdout: 'token' }) } = {}) => ({ + fetch: fetchImpl, + runGcloud, + retryDelayMs: 0, + readTimeoutMs: 1_000, + now: () => START, + deadlineAt: START + 10_000, + projectId: 'onorca-cloud' +}) + +test('an unparseable timestamp makes the read unverifiable', async () => { + const badPoint = { + timeSeries: [{ points: [{ interval: { endTime: 'not a time' }, value: { int64Value: '3' } }] }] + } + const read503 = await readDirector503Minutes( + fakeReader({ fetch: async () => new Response(JSON.stringify(badPoint)) }), + { projectId: 'onorca-cloud', directorService: 'orca-cloud-relay', endedAt: START, minutes: 3 } + ) + assert.equal(read503.ok, false) + assert.match(read503.error, /unreadable/) + const seats = await readSeatBehindRowSample( + fakeReader({ runGcloud: async () => ({ stdout: JSON.stringify([summaryLine(START), { timestamp: 'garbage', jsonPayload: { epoch: 1, rowEpoch: 2 } }]) }) }), + { directorService: 'orca-cloud-relay', cellId: CELL, at: START } + ) + assert.equal(seats.ok, false) + assert.match(seats.error, /unparseable timestamp/) +}) + +test('a seat-behind-row key carries the director instance and cell beside the epochs', async () => { + const entry = (instanceId) => ({ timestamp: new Date(START).toISOString(), labels: { instanceId }, jsonPayload: { event: SEAT_EVENT, epoch: 7, rowEpoch: 8 } }) + const read = await readSeatBehindRowSample( + fakeReader({ runGcloud: async () => ({ stdout: JSON.stringify([entry('a'), summaryLine(START), entry('b')]) }) }), + { directorService: 'orca-cloud-relay', cellId: CELL, at: START } + ) + assert.deepEqual(read.reports.map((report) => report.key), [`a/${CELL}/7:8`, `b/${CELL}/7:8`]) +}) + +test('the duplicate read carries a positive control: no fresh ledger summary in it is unverifiable, not zero', async () => { + const filters = [] + const readWith = (entries) => + readSeatBehindRowSample( + fakeReader({ runGcloud: async (args) => (filters.push(args[2]), { stdout: JSON.stringify(entries) }) }), + { directorService: 'orca-cloud-relay', cellId: CELL, at: START } + ) + // One read, one window: the control rides the same filter as the seat lines. + assert.deepEqual(await readWith([summaryLine(START - 30_000)]), { ok: true, reports: [] }) + assert.match(filters[0], new RegExp(`\\(\\(jsonPayload\\.event="${SEAT_EVENT}" AND jsonPayload\\.cellId="${CELL}"\\) OR jsonPayload\\.event="${SUMMARY_EVENT}"\\)`)) + assert.match(filters[0], /resource\.labels\.service_name="orca-cloud-relay"/) + for (const entries of [[], [summaryLine(START - 10 * MINUTE)]]) { + const blind = await readWith(entries) + assert.equal(blind.ok, false) + assert.match(blind.error, /no fresh ledger summary \(positive control\)/) + } + // Blind is a gap in the sample, which counts toward the unverifiable streak. + const [gap] = run([sampleAt(START, { seatBehindRow: await readWith([]) })]) + assert.equal(gap.rules.duplicateSeat.status, 'unverifiable') + assert.equal(gap.state.unverifiableStreak, 1) +}) + +test('the measured background is the median of the minutes before the flip, published gaps as zero', () => { + const minutes = ['2026-10-14T11:47', '2026-10-14T11:48', '2026-10-14T11:49'] + const front = minutes[2] + assert.equal(measuredBackground({ '2026-10-14T11:48': 30 }, minutes, front).medianPerMinute, 0) + assert.equal(measuredBackground({ [minutes[0]]: 10, [minutes[1]]: 30, [minutes[2]]: 20 }, minutes, front).medianPerMinute, 20) + assert.equal(measuredBackground({}, minutes, '2026-10-14T11:52').medianPerMinute, 0) + // Not published through the last minute yet, or no request series: no background yet. + assert.equal(measuredBackground({ [minutes[0]]: 10 }, minutes, minutes[1]), null) + assert.equal(measuredBackground({}, minutes, null), null) + assert.equal(measuredBackground({}, [], front), null) +}) + +test('any overrun trips at once', () => { + const [result] = run([sampleAt(START, { cell: healthyCell({ enforcedUnits: 2_001 }) })]) + assert.equal(result.state.state, 'tripped') + assert.match(result.state.verdict, /overrun: ledger units 2001 over hard cap 2000/) + const [atCap] = run([sampleAt(START, { cell: healthyCell({ enforcedUnits: 2_000 }) })]) + assert.equal(atCap.state.state, 'watching') +}) + +test('a seat reported behind its row again past the grace trips; inside the grace it does not', () => { + const report = (at) => ({ at, key: '7:8' }) + const within = run([ + sampleAt(START, { seatBehindRow: { ok: true, reports: [report(START - 30_000)] } }), + sampleAt(START + MINUTE, { seatBehindRow: { ok: true, reports: [report(START - 30_000), report(START + 50_000)] } }) + ]) + assert.equal(within.at(-1).state.state, 'watching') + // Carried across samples: the second read no longer holds the first report. + const past = run([ + sampleAt(START, { seatBehindRow: { ok: true, reports: [report(START - 30_000)] } }), + sampleAt(START + 2 * MINUTE, { seatBehindRow: { ok: true, reports: [report(START + 70_000)] } }) + ]) + assert.equal(past.at(-1).state.state, 'tripped') + assert.match(past.at(-1).state.verdict, /duplicateSeat: a seat behind its ledger row \(7:8\)/) + // Another seat's report is not the same seat. + const other = run([ + sampleAt(START, { seatBehindRow: { ok: true, reports: [report(START - 30_000)] } }), + sampleAt(START + 2 * MINUTE, { seatBehindRow: { ok: true, reports: [{ at: START + 70_000, key: '3:4' }] } }) + ]) + assert.equal(other.at(-1).state.state, 'watching') +}) + +test('a host drop over 5% in 5 minutes trips outside a drain only', () => { + const controls = [1_000, 990, 980, 960, 949] + const samples = (draining) => + controls.map((count, minute) => + sampleAt(START + minute * MINUTE, { cell: healthyCell({ controls: count, draining: draining && minute === 2 }) }) + ) + assert.equal(run(samples(false)).at(-1).state.state, 'tripped') + assert.match(run(samples(false)).at(-1).state.verdict, /hostDrop: connected hosts fell 1000 -> 949/) + assert.equal(run(samples(true)).at(-1).state.state, 'watching') + // A drop spread over more than 5 minutes is not one. + const slow = [1_000, 990, 980, 970, 960, 955, 950].map((count, minute) => + sampleAt(START + minute * MINUTE, { cell: healthyCell({ controls: count }) }) + ) + assert.equal(run(slow).at(-1).state.state, 'watching') + // Under the floor on a nearly empty cell: 100 -> 92 is 8%, but only 8 hosts. + const small = [100, 92].map((count, minute) => sampleAt(START + minute * MINUTE, { cell: healthyCell({ controls: count }) })) + assert.equal(run(small).at(-1).state.state, 'watching') +}) + +test('ledger drift over 0.5% trips once it has lasted 2 minutes', () => { + const drift = (rows) => ({ ok: true, unwrittenRows: rows, directors: 5 }) + const brief = run([6, 6, 0, 6].map((rows, minute) => sampleAt(START + minute * MINUTE, { ledger: drift(rows) }))) + assert.equal(brief.at(-1).state.state, 'watching') + const sustained = run([6, 6, 6].map((rows, minute) => sampleAt(START + minute * MINUTE, { ledger: drift(rows) }))) + assert.equal(sustained.length, 3) + assert.equal(sustained.at(-1).state.state, 'tripped') + assert.match(sustained.at(-1).state.verdict, /ledgerDrift: ledger drift 0\.60% for 120 s/) + const atLine = run([5, 5, 5, 5].map((rows, minute) => sampleAt(START + minute * MINUTE, { ledger: drift(rows) }))) + assert.equal(atLine.at(-1).state.state, 'watching') +}) + +test('a tripped dead-man is a trip: Postgres still records reserve until a db write', () => { + const [result] = run([sampleAt(START, { cell: healthyCell({ admitModeEffective: 'db' }) })]) + assert.equal(result.state.state, 'tripped') + assert.match(result.state.verdict, /deadMan: the cell tripped its dead-man/) +}) + +test('unreadable data is never healthy: N samples in a row with any gap trip', () => { + const unreadable = { ok: false, error: 'cell runtime status returned 502' } + const gaps = [ + { cell: unreadable }, + { ledger: { ok: false, error: 'no placing director logged a ledger summary' } }, + { director503: { ok: false, error: 'director 503 metric read failed', perMinute: {}, minutes: [], publishedThrough: null } } + ] + const results = run(gaps.map((gap, minute) => sampleAt(START + minute * MINUTE, gap))) + assert.deepEqual(results.map((result) => result.state.unverifiableStreak), [1, 2, 3]) + assert.equal(results.at(-1).state.state, 'tripped') + assert.match(results.at(-1).state.verdict, /unverifiable: 3 samples in a row could not read director503/) + // One whole sample resets the streak. + const reset = run( + [gaps[0], {}, gaps[0], gaps[0]].map((gap, minute) => sampleAt(START + minute * MINUTE, gap)) + ) + assert.deepEqual(reset.map((result) => result.state.unverifiableStreak), [1, 0, 1, 2]) + assert.equal(reset.at(-1).state.state, 'watching') + // No background yet is a gap too. + const noBackground = freshState({ background: null }) + assert.equal(evaluateWatchSample(noBackground, sampleAt(START), LIMIT).state.unverifiableStreak, 1) + // A measured trip still wins over a gap elsewhere. + const [both] = run([sampleAt(START, { cell: healthyCell({ enforcedUnits: 2_500 }), ledger: gaps[1].ledger })]) + assert.equal(both.state.state, 'tripped') + assert.match(both.state.verdict, /overrun/) +}) + +test('a cell no longer in reserve stops the watch without a write; one never in reserve is refused', () => { + const dbCell = healthyCell({ admitModeConfigured: 'db', admitModeEffective: 'reserve' }) + const later = run([sampleAt(START), sampleAt(START + MINUTE, { cell: dbCell })]) + assert.equal(later.at(-1).state.state, 'stopped') + assert.match(later.at(-1).state.verdict, /is not in reserve mode \(switch file says db\)/) + // Never reserve within the wait: refused, which the workflow does not flip back. + const never = run(Array.from({ length: 12 }, (_, minute) => sampleAt(START + minute * MINUTE, { cell: dbCell }))) + assert.equal(never.at(-1).state.state, 'refused') + assert.equal(never.length, 11) + assert.match(never.at(-1).state.verdict, /did not report reserve mode within 10 min \(switch file says db\)/) +}) + +test('a watch dispatched before the reserve write applied waits for it, then judges as usual', () => { + const dbCell = healthyCell({ admitModeConfigured: 'db', admitModeEffective: 'db' }) + const results = run([ + sampleAt(START, { cell: dbCell, ledger: { ok: false, error: 'not yet' } }), + sampleAt(START + MINUTE, { cell: dbCell }), + sampleAt(START + 2 * MINUTE), + sampleAt(START + 3 * MINUTE, { cell: healthyCell({ enforcedUnits: 2_500 }) }) + ]) + // Waiting samples judge nothing: not a dead-man trip on a db cell, not a gap. + assert.deepEqual(results.slice(0, 2).map((result) => result.state.state), ['watching', 'watching']) + assert.equal(results[0].rules.reserve.status, 'pending') + assert.deepEqual(Object.keys(results[0].rules), ['reserve']) + assert.deepEqual(results.map((result) => result.state.unverifiableStreak), [0, 0, 0, 0]) + assert.equal(results[2].state.reserveSeenAt, START + 2 * MINUTE) + assert.equal(results[2].rules.overrun.status, 'pass') + assert.equal(results[3].state.state, 'tripped') + // Once seen in reserve, a later db reads as a hand flip back, not a refusal. + const after = run([sampleAt(START + 2 * MINUTE), sampleAt(START + 3 * MINUTE, { cell: dbCell })], results[0].state) + assert.equal(after.at(-1).state.state, 'stopped') +}) + +const watchValues = (overrides = {}) => ({ + mode: 'dry-run', + 'cell-id': CELL, + 'cell-origin': 'https://c26.relay.onorca.dev', + 'project-id': 'onorca-cloud', + 'director-origin': 'https://relay.onorca.dev', + 'director-service': 'orca-cloud-relay', + 'duration-minutes': '120', + 'segment-minutes': '55', + 'unverifiable-limit': '3', + 'background-503-per-minute': '', + 'flipped-at': new Date(START - 5 * MINUTE).toISOString(), + 'state-file': '/unused', + ...overrides +}) +const parseAt = (values) => parseWatchArguments(values, START) + +test('watch arguments reuse the flag tool checks, so a write watch needs its confirmation up front', () => { + assert.equal(parseAt(watchValues()).background503PerMinute, null) + assert.equal(parseAt(watchValues()).flippedAt, START - 5 * MINUTE) + assert.equal(parseAt(watchValues({ 'background-503-per-minute': '12.5', 'flipped-at': '' })).background503PerMinute, 12.5) + assert.throws(() => parseAt(watchValues({ mode: 'write' })), /WRITE_RELAY_CELL_FLAGS production-gce-c26/) + assert.doesNotThrow(() => parseAt(watchValues({ mode: 'write', confirmation: `WRITE_RELAY_CELL_FLAGS ${CELL}` }))) + assert.throws(() => parseAt(watchValues({ 'cell-origin': 'https://c27.relay.onorca.dev' })), /names another cell/) + assert.throws(() => parseAt(watchValues({ 'duration-minutes': '301' })), /duration-minutes/) + assert.throws(() => parseAt(watchValues({ 'segment-minutes': '56' })), /segment-minutes/) + assert.throws(() => parseAt(watchValues({ 'background-503-per-minute': '-1' })), /background/) +}) + +test('the 503 background needs flipped-at or an explicit value; flipped-at must be a recent zoned time', () => { + assert.throws(() => parseAt(watchValues({ 'flipped-at': '' })), /--flipped-at .* or --background-503-per-minute is required/) + assert.throws(() => parseAt(watchValues({ 'flipped-at': '2026-10-14T11:55:00' })), /ISO time with a zone/) + assert.throws(() => parseAt(watchValues({ 'flipped-at': '2026-10-14T12:01:00Z' })), /in the future/) + assert.throws(() => parseAt(watchValues({ 'flipped-at': '2026-10-13T11:00:00Z' })), /more than 24 hours/) + assert.equal(parseAt(watchValues({ 'flipped-at': '2026-10-14T07:55:00-04:00' })).flippedAt, START - 5 * MINUTE) +}) + +// A cell, Cloud Monitoring and Cloud Logging, answering from `script` per sample. +function fakeSources({ + cell = () => ({}), + // Logged at each sample's minute, so it stays fresh for the positive control. + ledger = (sample) => [{ timestamp: new Date(START + (sample - 1) * MINUTE).toISOString(), labels: { instanceId: 'a' }, jsonPayload: { pending: 0, dropped: 0 } }], + per503 = () => 10, + // Requests of every other code per minute; zero is a minute with no traffic at all. + perOther = () => 1_000, + seatControl = true, + rolloutLease = null +} = {}) { + let samples = 0 + const monitoringSpans = [] + const fetchImpl = async (url) => { + const text = String(url) + if (text.endsWith('/v1/admin/runtime-status')) { + samples += 1 + const overrides = cell(samples) + return new Response( + JSON.stringify({ + role: 'cell', + cellId: CELL, + draining: false, + flagsApplied: { generation: 4, flags: { admitMode: 'reserve', readinessLocal: true } }, + admitModeEffective: 'reserve', + connectionCapacity: { hardCap: 2_000 }, + runtime: { controls: 1_000, enforcedConnectionUnits: 1_100 }, + ...overrides + }) + ) + } + if (text.startsWith('https://monitoring.googleapis.com/')) { + const params = new URL(text).searchParams + const span = { startedAt: Date.parse(params.get('interval.startTime')), endedAt: Date.parse(params.get('interval.endTime')) } + monitoringSpans.push(span) + assert.equal(params.get('aggregation.groupByFields'), 'metric.label."response_code"') + assert.doesNotMatch(params.get('filter'), /response_code/) + // Cloud Monitoring returns no point for a minute with no requests of that code. + const seriesOf = (code, count) => { + const points = [] + for (let minute = span.startedAt; minute < span.endedAt; minute += MINUTE) { + if (count(minute) > 0) { + points.push({ interval: { endTime: new Date(minute + MINUTE).toISOString() }, value: { int64Value: String(count(minute)) } }) + } + } + return points.length === 0 ? [] : [{ metric: { labels: { response_code: code } }, points }] + } + return new Response(JSON.stringify({ timeSeries: [...seriesOf('200', perOther), ...seriesOf('503', per503)] })) + } + throw new Error(`unexpected fetch ${text}`) + } + const runGcloud = async (args) => { + if (args[0] === 'auth') return { stdout: 'token' } + const filter = args[2] + const summaries = ledger(samples).map((entry) => ({ ...entry, jsonPayload: { ...entry.jsonPayload, event: SUMMARY_EVENT } })) + if (filter.includes(SEAT_EVENT)) return { stdout: JSON.stringify(seatControl ? summaries : []) } + if (filter.includes(SUMMARY_EVENT)) return { stdout: JSON.stringify(ledger(samples)) } + return { stdout: '[]' } + } + return { fetchImpl, runGcloud, monitoringSpans, readRolloutLease: async () => rolloutLease } +} + +test('a segment samples once a minute, carries state to the next segment, and stops on a trip', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + let clock = START + const lines = [] + const config = parseAt( + watchValues({ 'state-file': join(directory, 'state.json'), 'github-output': join(directory, 'out'), 'segment-minutes': '3' }) + ) + const sources = fakeSources({ cell: (sample) => (sample === 5 ? { runtime: { controls: 1_000, enforcedConnectionUnits: 2_001 } } : {}) }) + const dependencies = { + ...sources, + idToken: 'id', + retryDelayMs: 0, + now: () => clock, + sleep: async (ms) => { clock += ms }, + log: (line) => lines.push(JSON.parse(line)) + } + const first = await watchSegment(config, dependencies) + assert.equal(first.state, 'watching') + assert.equal(lines.length, 3) + assert.equal(first.background.source, 'measured') + assert.equal(await readFile(join(directory, 'out'), 'utf8'), 'state=watching\nverdict=\n') + const second = await watchSegment(config, dependencies) + assert.equal(second.state, 'tripped') + assert.equal(second.samples, 5) + assert.deepEqual(lines.map((line) => line.sample), [1, 2, 3, 4, 5]) + assert.equal(lines.at(-1).rules.overrun.status, 'trip') + assert.match(await readFile(join(directory, 'out'), 'utf8'), /state=tripped\nverdict=flip back production-gce-c26: overrun: /) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('a sample that cannot read its sources counts as unverifiable rather than crashing the watch', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + let clock = START + const config = parseAt(watchValues({ 'state-file': join(directory, 'state.json'), 'segment-minutes': '10' })) + const sources = fakeSources({ ledger: () => [] }) + const state = await watchSegment(config, { + ...sources, + idToken: 'id', + retryDelayMs: 0, + now: () => clock, + sleep: async (ms) => { clock += ms }, + log: () => {} + }) + assert.equal(state.state, 'tripped') + assert.equal(state.samples, 3) + // No summary also blinds the duplicate rule, whose positive control it is. + assert.match(state.verdict, /unverifiable: 3 samples in a row could not read duplicateSeat, ledgerDrift/) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('a watch that ended without a verdict records failed, which the workflow flips back', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + const output = join(directory, 'out') + assert.equal((await recordOutcome(join(directory, 'missing.json'), output)).state, 'failed') + await writeFile(join(directory, 'watching.json'), JSON.stringify(freshState())) + assert.equal((await recordOutcome(join(directory, 'watching.json'), output)).state, 'failed') + await writeFile(join(directory, 'tripped.json'), JSON.stringify(freshState({ state: 'tripped', verdict: 'flip back x:\ny' }))) + assert.equal((await recordOutcome(join(directory, 'tripped.json'), output)).state, 'tripped') + assert.match(await readFile(output, 'utf8'), /state=tripped\nverdict=flip back x: y\n$/) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +// Google Storage and the director, as far as the flag tool's flip back reaches them. `onUpload` +// returns 'conflict' to play another writer landing first, optionally with its own admitMode. +function fakeFlipBack({ generation = '7', onUpload = () => 'ok', cellAnswers = true } = {}) { + const calls = [] + let uploads = 0 + let object = { generation: Number(generation), flags: { admitMode: 'reserve', readinessLocal: true } } + let applied = object + const fetchImpl = async (url, init = {}) => { + const text = String(url) + calls.push({ url: text, method: init.method ?? 'GET', body: init.body }) + if (text.includes('/upload/storage/v1/')) { + uploads += 1 + assert.match(text, new RegExp(`ifGenerationMatch=${object.generation}$`)) + const outcome = onUpload(uploads) + if (outcome !== 'ok') { + object = { generation: object.generation + 1, flags: { ...object.flags, admitMode: outcome.admitMode ?? 'reserve' } } + applied = object + return new Response('', { status: 412 }) + } + object = { generation: object.generation + 1, flags: JSON.parse(init.body.split('\r\n')[7]).flags } + applied = object + return new Response(JSON.stringify({ generation: String(object.generation) })) + } + if (text.includes('?alt=media')) { + return new Response(JSON.stringify({ v: 1, cellId: CELL, flags: object.flags })) + } + if (text.includes('/storage/v1/b/')) return new Response(JSON.stringify({ generation: String(object.generation) })) + if (text.endsWith('/v1/admin/runtime-status')) { + if (!cellAnswers) return new Response('', { status: 502 }) + return new Response( + JSON.stringify({ + role: 'cell', + cellId: CELL, + flagsApplied: applied, + supportedFlags: { + admitMode: { type: 'enum', values: ['db', 'reserve'] }, + readinessLocal: { type: 'boolean' } + }, + admitModeEffective: applied.flags.admitMode, + runtime: { controls: 10 } + }) + ) + } + if (text.endsWith('/v1/admin/cell-admit-mode')) { + const admitMode = JSON.parse(init.body).admitMode + return new Response(JSON.stringify({ v: 1, cellId: CELL, admitMode: admitMode ?? 'reserve', heartbeatFresh: true })) + } + throw new Error(`unexpected fetch ${text}`) + } + return { fetchImpl, calls, uploads: () => uploads } +} + +const flipDependencies = (fetchImpl, lines, sleeps = []) => { + let clock = START + return { + fetchImpl, + accessToken: 'access', + idToken: 'id', + audit: { writtenBy: 'reserve-watch/test', runId: '1', runAttempt: '1' }, + now: () => clock, + sleep: async (ms) => { sleeps.push(ms); clock += ms }, + log: (line) => lines.push(JSON.parse(line)) + } +} + +const flipValues = (overrides = {}) => ({ + mode: 'write', + 'cell-id': CELL, + 'cell-origin': 'https://c26.relay.onorca.dev', + 'project-id': 'onorca-cloud', + 'director-origin': 'https://relay.onorca.dev', + confirmation: `WRITE_RELAY_CELL_FLAGS ${CELL}`, + ...overrides +}) + +test('the flip back is the flag tool write: admitMode=db at the generation just read, then Postgres db', async () => { + const { fetchImpl, calls } = fakeFlipBack() + const lines = [] + const result = await flipBack(flipValues(), flipDependencies(fetchImpl, lines)) + assert.equal(result.written, true) + const upload = calls.find((call) => call.url.includes('/upload/storage/v1/')) + assert.match(upload.body, /"flags":\{"admitMode":"db","readinessLocal":true\}/) + assert.ok(calls.some((call) => call.url.endsWith('/v1/admin/cell-admit-mode') && JSON.parse(call.body).admitMode === 'db')) + assert.deepEqual(lines.at(-1), { + event: 'orca_relay_reserve_watch_flip_back', + mode: 'write', + cellId: CELL, + written: true, + generation: '8' + }) +}) + +test('a dry-run flip back plans the write and writes nothing', async () => { + const { fetchImpl, calls } = fakeFlipBack() + const lines = [] + const result = await flipBack(flipValues({ mode: 'dry-run', confirmation: '' }), { + fetchImpl, + accessToken: 'access', + idToken: 'id', + audit: {}, + sleep: async () => {}, + log: (line) => lines.push(JSON.parse(line)) + }) + assert.equal(result.written, false) + assert.ok(!calls.some((call) => call.url.includes('/upload/'))) + assert.ok(!calls.some((call) => call.url.endsWith('/v1/admin/cell-admit-mode') && JSON.parse(call.body).admitMode)) + assert.equal(lines.find((line) => line.event === 'orca_relay_cell_flags_plan').desired.flags.admitMode, 'db') +}) + +test('a write flip back without the typed confirmation is refused before any read', async () => { + const { fetchImpl, calls } = fakeFlipBack() + await assert.rejects( + flipBack(flipValues({ confirmation: '' }), { fetchImpl, accessToken: 'a', idToken: 'i', audit: {}, log: () => {} }), + /WRITE_RELAY_CELL_FLAGS production-gce-c26/ + ) + assert.equal(calls.length, 0) +}) + +test('a generation conflict is re-read and retried while the switch file still says reserve', async () => { + const { fetchImpl, uploads } = fakeFlipBack({ onUpload: (n) => (n < 3 ? { admitMode: 'reserve' } : 'ok') }) + const lines = [] + const sleeps = [] + const result = await flipBack(flipValues(), flipDependencies(fetchImpl, lines, sleeps)) + assert.equal(result.written, true) + assert.equal(uploads(), 3) + assert.deepEqual(lines.filter((line) => line.event === 'orca_relay_reserve_watch_flip_back_conflict').map((line) => line.attempt), [1, 2]) + assert.deepEqual(sleeps.slice(0, 1), [2_000]) + assert.equal(lines.at(-1).generation, '10') +}) + +test('a conflict whose writer already flipped the cell to db ends the flip back without a write', async () => { + const { fetchImpl, uploads } = fakeFlipBack({ onUpload: () => ({ admitMode: 'db' }) }) + const lines = [] + const result = await flipBack(flipValues(), flipDependencies(fetchImpl, lines)) + assert.equal(uploads(), 1) + assert.equal(result.written, false) + assert.equal(result.alreadyDb, true) + assert.deepEqual(lines.at(-1), { event: 'orca_relay_reserve_watch_flip_back', mode: 'write', cellId: CELL, written: false, generation: '8', alreadyDb: true }) +}) + +test('conflicts past the retry bound fail the flip back', async () => { + const { fetchImpl, uploads } = fakeFlipBack({ onUpload: () => ({ admitMode: 'reserve' }) }) + await assert.rejects(flipBack(flipValues(), flipDependencies(fetchImpl, [])), /object changed since expected-generation/) + assert.equal(uploads(), FLIP_BACK_ATTEMPTS) +}) + +test('an unreachable cell fails with the exact break-glass inputs and writes nothing', async () => { + const { fetchImpl, uploads } = fakeFlipBack({ cellAnswers: false }) + const lines = [] + await assert.rejects( + flipBack(flipValues(), flipDependencies(fetchImpl, lines)), + (error) => { + assert.match(error.message, /production-gce-c26 does not answer runtime-status \(cell runtime status returned 502\)/) + assert.match(error.message, /set=admitMode=default, break-glass=BREAK GLASS production-gce-c26, expected-generation=7/) + return true + } + ) + assert.equal(uploads(), 0) + assert.deepEqual(lines.at(-1).inputs, { + mode: 'write', + 'cell-id': CELL, + set: 'admitMode=default', + 'break-glass': `BREAK GLASS ${CELL}`, + 'expected-generation': '7', + confirmation: `WRITE_RELAY_CELL_FLAGS ${CELL}` + }) +}) + +test('the 503 background is the 10 minutes before flipped-at, read once they are published', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + let clock = START + // Flipped one minute ago: the minutes before it are not published until 3 minutes after it. + const flippedAt = START - MINUTE + 20_000 + const config = parseAt( + watchValues({ 'state-file': join(directory, 'state.json'), 'segment-minutes': '3', 'flipped-at': new Date(flippedAt).toISOString() }) + ) + const sources = fakeSources({ per503: (minute) => (minute < START - MINUTE ? 4 : 40) }) + const lines = [] + const state = await watchSegment(config, { + ...sources, + idToken: 'id', + retryDelayMs: 0, + now: () => clock, + sleep: async (ms) => { clock += ms }, + log: (line) => lines.push(JSON.parse(line)) + }) + // Pending until then, and pending is not a gap: the streak stays at zero. + assert.deepEqual(lines.map((line) => line.rules.director503.status), ['pending', 'pending', 'pass']) + assert.match(lines[0].rules.director503.reason, /background before the flip is not published yet/) + assert.deepEqual(lines.map((line) => line.unverifiableStreak), [0, 0, 0]) + const baseline = sources.monitoringSpans.find((span) => span.endedAt === START - MINUTE) + assert.equal(baseline.startedAt, START - 11 * MINUTE) + assert.deepEqual(state.background, { medianPerMinute: 4, minutes: 10, source: 'measured' }) + // An explicit value overrides the measurement. + const explicit = initialWatchState({ cellId: CELL, startedAt: START, durationMinutes: 60, background503PerMinute: 9, flippedAt }) + assert.deepEqual(explicit.background, { medianPerMinute: 9, source: 'input' }) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('a quiet fleet with zero director 503s watches on and never trips', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + let clock = START + const config = parseAt( + watchValues({ + 'state-file': join(directory, 'state.json'), + 'segment-minutes': '20', + 'flipped-at': new Date(START - 2 * MINUTE).toISOString() + }) + ) + const lines = [] + const state = await watchSegment(config, { + ...fakeSources({ per503: () => 0 }), + idToken: 'id', + retryDelayMs: 0, + now: () => clock, + sleep: async (ms) => { clock += ms }, + log: (line) => lines.push(JSON.parse(line)) + }) + assert.equal(state.state, 'watching') + assert.equal(lines.length, 20) + assert.deepEqual(state.background, { medianPerMinute: 0, minutes: 10, source: 'measured' }) + assert.ok(lines.slice(1).every((line) => line.rules.director503.status === 'pass')) + assert.ok(lines.every((line) => line.unverifiableStreak === 0)) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('no request series at all is unverifiable and trips at the limit', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + let clock = START + const config = parseAt(watchValues({ 'state-file': join(directory, 'state.json'), 'segment-minutes': '10', 'background-503-per-minute': '0', 'flipped-at': '' })) + const state = await watchSegment(config, { + ...fakeSources({ per503: () => 0, perOther: () => 0 }), + idToken: 'id', + retryDelayMs: 0, + now: () => clock, + sleep: async (ms) => { clock += ms }, + log: () => {} + }) + assert.equal(state.state, 'tripped') + assert.match(state.verdict, /unverifiable: 3 samples in a row could not read director503/) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('a later segment judges flipped-at against the watch start, so a long watch does not age it out', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + const stateFile = join(directory, 'state.json') + const values = watchValues({ 'state-file': stateFile, 'flipped-at': new Date(START - 23 * 60 * MINUTE).toISOString() }) + const later = START + 4 * 60 * MINUTE + await writeFile(stateFile, JSON.stringify(freshState())) + assert.equal((await parseWatchSegment(values, later)).flippedAt, START - 23 * 60 * MINUTE) + // The first segment (no state file yet) still refuses it, and records refused. + await rm(stateFile) + await assert.rejects(parseWatchSegment(values, later), /more than 24 hours/) + assert.equal(JSON.parse(await readFile(stateFile, 'utf8')).state, 'refused') + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('a corrupt state file fails the segment, never refuses or restarts it, and the outcome reads failed', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + const stateFile = join(directory, 'state.json') + const output = join(directory, 'out') + const values = watchValues({ 'state-file': stateFile }) + for (const corrupt of ['null', '[]', '7', JSON.stringify({ state: 'watching', cellId: 'c7' })]) { + await writeFile(stateFile, corrupt) + await assert.rejects(parseWatchSegment(values, START), /state file/) + assert.equal(await readFile(stateFile, 'utf8'), corrupt) + assert.equal((await recordOutcome(stateFile, output)).state, 'failed') + } + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +test('a slow runtime-status reply is retried before the flip back calls for break glass', async () => { + const { fetchImpl: answering, uploads } = fakeFlipBack() + let runtimeCalls = 0 + // fetchAdminOnceMore makes two tries per read; the first read's two both fail. + const fetchImpl = async (url, init) => { + if (String(url).endsWith('/v1/admin/runtime-status') && (runtimeCalls += 1) <= 2) return new Response('', { status: 503 }) + return answering(url, init) + } + const lines = [] + const sleeps = [] + const result = await flipBack(flipValues(), flipDependencies(fetchImpl, lines, sleeps)) + assert.equal(result.written, true) + assert.equal(uploads(), 1) + assert.deepEqual(lines.filter((line) => line.event === 'orca_relay_reserve_watch_runtime_retry').map((line) => line.attempt), [1]) + assert.ok(!lines.some((line) => line.event === 'orca_relay_reserve_watch_break_glass_required')) + assert.ok(sleeps.includes(2_000)) + assert.ok(FLIP_BACK_RUNTIME_ATTEMPTS > 1) +}) + +const watchWorkflow = readRelayWorkflow('operate-relay-production-reserve-watch.yml') +const jobOf = (name) => { + const start = watchWorkflow.indexOf(`\n ${name}:\n`) + assert.notEqual(start, -1, `job ${name} is missing`) + const next = watchWorkflow.slice(start + 1).search(/\n [a-z-]+:\n/) + return watchWorkflow.slice(start, next === -1 ? undefined : start + 1 + next) +} + +test('workflow: neither job takes the flag workflow group, which would let a newer run cancel a pending flip back', () => { + assert.doesNotMatch(watchWorkflow, /group: relay-production-cell-flags/) + assert.doesNotMatch(jobOf('flip-back'), /^\s+concurrency:/m) + assert.doesNotMatch(jobOf('watch'), /^\s+concurrency:/m) +}) + +test('workflow: a watch that failed or was cancelled without a verdict flips back too', () => { + const flip = jobOf('flip-back') + // A cancel re-evaluates each step: every flip-back step must run on always() too. + const steps = flip.slice(flip.indexOf('\n steps:\n')).split(/\n - /).slice(1) + assert.equal(steps.length, 7) + for (const step of steps) assert.match(step, /\n\s+if: \$\{\{ always\(\) \}\}/, step.split('\n')[0]) + assert.match(flip, /if: \$\{\{ always\(\) && vars\.ORCA_CLOUD_OPERATIONS_ENABLED == 'true' && needs\.watch\.result != 'skipped'/) + assert.match(flip, /!contains\(fromJSON\('\["completed","stopped","refused"\]'\), needs\.watch\.outputs\.state\)/) + // Every segment checks its ID token's shape, and runs the one shared command. + const segments = watchWorkflow.match(/run: bash -c "\$\{WATCH_SEGMENT\}"/g) ?? [] + assert.equal(segments.length, 6) + assert.match(jobOf('watch'), /process\.env\.ORCA_RELAY_ADMIN_ID_TOKEN/) +}) + +test('a live rollout lease refuses the watch before its first sample; an expired one does not', async () => { + const directory = await mkdtemp(join(tmpdir(), 'relay-reserve-watch-')) + try { + const stateFile = join(directory, 'state.json') + const output = join(directory, 'out') + const config = parseAt(watchValues({ 'state-file': stateFile, 'github-output': output, 'segment-minutes': '1' })) + const lease = (expiresAt) => ({ record: { expires_at: expiresAt }, holder: 'Deploy Relay Production Same-Cap (run 9)' }) + let clock = START + const dependencies = (rolloutLease) => ({ + ...fakeSources({ rolloutLease }), + idToken: 'id', + retryDelayMs: 0, + now: () => clock, + sleep: async (ms) => { clock += ms }, + log: () => {} + }) + const held = await watchSegment(config, dependencies(lease(START + MINUTE))) + assert.equal(held.state, 'refused') + assert.match(held.verdict, /a rollout holds the Cloud SQL rollout lease until .*Same-Cap \(run 9\)/) + assert.equal(JSON.parse(await readFile(stateFile, 'utf8')).state, 'refused') + assert.match(await readFile(output, 'utf8'), /^state=refused\n/) + await rm(stateFile) + assert.equal((await watchSegment(config, dependencies(lease(START - 1)))).state, 'watching') + // Only the first segment checks: a later one carries on from its state file. + assert.equal((await watchSegment(config, dependencies(lease(clock + MINUTE)))).state, 'watching') + // A lease it cannot read refuses too, as the lease action fails closed. + assert.match(await rolloutLeaseRefusal(async () => { throw new Error('403') }, START), /lease is unreadable \(403\)/) + assert.equal(await rolloutLeaseRefusal(async () => null, START), null) + } finally { + await rm(directory, { recursive: true, force: true }) + } +}) + +const stepOf = (job, name) => { + const text = jobOf(job) + const start = text.indexOf(`- name: ${name}\n`) + assert.notEqual(start, -1, `step ${name} is missing`) + const end = text.indexOf('\n - ', start + 1) + return text.slice(start, end === -1 ? undefined : end) +} +const ALARM_COMMAND = /node dev\/scripts\/watch-relay-reserve-cell\.mjs alarm --kind KIND \\\n\s+--mode "\$\{MODE\}" --cell-id "\$\{CELL_ID\}" --project-id "\$\{GCP_PROJECT_ID\}" --verdict "\$\{VERDICT_VAR\}"/ +const alarmCommand = (kind, verdict) => new RegExp(ALARM_COMMAND.source.replace('KIND', kind).replace('VERDICT_VAR', verdict)) + +test('workflow: a flip back that did not land and a refused watch both raise the alarm', () => { + const alarm = stepOf('flip-back', 'Alarm when the flip back did not land') + assert.match(alarm, /if: \$\{\{ always\(\) \}\}/) + assert.match(alarm, /grep -qs '"event":"orca_relay_reserve_watch_flip_back"' "\$\{RUNNER_TEMP\}\/relay-reserve-flip-back\.jsonl"/) + assert.match(alarm, alarmCommand('flip-back-failed', 'VERDICT')) + const refused = stepOf('watch', 'Alarm on a refused watch') + assert.match(refused, /if: \$\{\{ always\(\) && steps\.outcome\.outputs\.state == 'refused' \}\}/) + assert.match(refused, /WATCH_VERDICT: \$\{\{ steps\.outcome\.outputs\.verdict \}\}/) + assert.match(refused, alarmCommand('refused', 'WATCH_VERDICT')) + // After the outcome step, so it reads the recorded state. + assert.ok(jobOf('watch').indexOf('- name: Alarm on a refused watch') > jobOf('watch').indexOf('- id: outcome')) + // The flip-back line names only the write's own event, never its conflict retries. + assert.match(readFileSync(new URL('./watch-relay-reserve-cell.mjs', import.meta.url), 'utf8'), /event: 'orca_relay_reserve_watch_flip_back',\n/) +}) + +test('an alarm is an escaped error annotation everywhere, and in write mode a paging log line', async () => { + const calls = [] + const lines = [] + const runGcloud = async (args, options) => (calls.push({ args, options }), { stdout: '' }) + const alarm = (overrides) => + raiseAlarm( + { mode: 'write', cellId: CELL, projectId: 'onorca-cloud', kind: 'refused', reason: '50%\r\nlease held', ...overrides }, + { runGcloud, log: (line) => lines.push(line) } + ) + assert.deepEqual(await alarm(), { paged: true }) + assert.equal( + lines[0], + `::error title=Reserve watch refused for ${CELL}::${CELL} may be in reserve mode and unwatched: the watch was refused (50%25%0D%0Alease held). ` + + 'Rerun it, or flip the cell to db with Operate Relay Production Cell Flags.' + ) + assert.deepEqual(calls[0].args, [ + 'logging', 'write', ALARM_LOG_NAME, + JSON.stringify({ event: ALARM_EVENT, cellId: CELL, kind: 'refused', reason: '50%\r\nlease held' }), + '--payload-type=json', '--severity=ERROR', '--project', 'onorca-cloud' + ]) + // A dry run never flips back, so its alarm annotates but does not page. + assert.deepEqual(await alarm({ mode: 'dry-run', kind: 'flip-back-failed' }), { paged: false }) + assert.equal(calls.length, 1) + assert.match(lines[1], /^::error title=Reserve watch flip-back-failed for production-gce-c26::.*did not land/) + // A title property escapes its own separators too. + await alarm({ mode: 'dry-run', cellId: 'a:b,c' }) + assert.match(lines[2], /^::error title=Reserve watch refused for a%3Ab%2Cc::/) + // A failed log write is named, never swallowed silently. + const failed = await raiseAlarm( + { mode: 'write', cellId: CELL, projectId: 'onorca-cloud', kind: 'flip-back-failed', reason: 'x' }, + { runGcloud: async () => { throw new Error('PERMISSION_DENIED') }, log: (line) => lines.push(line) } + ) + assert.deepEqual(failed, { paged: false }) + assert.match(lines.at(-1), /^::error title=Reserve watch page failed::.*nothing pages: PERMISSION_DENIED/) + await assert.rejects(alarm({ kind: 'other' }), /--kind must be one of/) + // A missing project still annotates, then names the page it could not send. + const before = calls.length + assert.deepEqual(await alarm({ projectId: undefined }), { paged: false }) + assert.equal(calls.length, before) + assert.match(lines.at(-2), /^::error title=Reserve watch refused for /) + assert.match(lines.at(-1), /nothing pages: --project-id is not a GCP project id/) +}) diff --git a/cloud/dev/scripts/workload-identity-attribute-conditions.test.mjs b/cloud/dev/scripts/workload-identity-attribute-conditions.test.mjs index 490bfde3520..cdf3b51c565 100644 --- a/cloud/dev/scripts/workload-identity-attribute-conditions.test.mjs +++ b/cloud/dev/scripts/workload-identity-attribute-conditions.test.mjs @@ -34,7 +34,7 @@ const EXPECTED_CONDITIONS = { "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && assertion.event_name == 'workflow_dispatch' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-push-deploy.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-push-deploy.yml@refs/heads/main'", github: - "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && ((assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-fence-broker.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-director.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-multi-target.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-asia-admission.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-cell-flags.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-publish-relay-production.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-rehome.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-rehome-job.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main' && (assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml@refs/heads/main' || assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main')))", + "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && ((assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-fence-broker.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-capacity.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-director.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-multi-target.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-asia-admission.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-cell-flags.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-reserve-watch.yml@refs/heads/main' || assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-publish-relay-production.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-rehome.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-operate-relay-production-rehome-job.yml@refs/heads/main') || (assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main' && (assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap-job.yml@refs/heads/main' || assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-deploy-relay-production-same-cap.yml@refs/heads/main')))", github_monitor: "assertion.repository == 'stablyai/orca' && assertion.repository_id == '1183888342' && assertion.repository_owner_id == '127256420' && assertion.ref == 'refs/heads/main' && assertion.environment == 'production' && assertion.workflow_ref == 'stablyai/orca/.github/workflows/cloud-monitor-relay-production.yml@refs/heads/main' && assertion.job_workflow_ref == 'stablyai/orca/.github/workflows/cloud-monitor-relay-production-job.yml@refs/heads/main'", github_fence: diff --git a/cloud/docs/relay-workflows.md b/cloud/docs/relay-workflows.md index 66b56b1e901..edab96a4277 100644 --- a/cloud/docs/relay-workflows.md +++ b/cloud/docs/relay-workflows.md @@ -109,7 +109,7 @@ identity bootstrap apply. The providers require their exact workflows on `refs/heads/main` with the `production` environment. Missing values fail closed; no dedicated operations identity falls back to the shared deploy identity. -The shared production identity is restricted to seven named direct Relay callers plus the exact +The shared production identity is restricted to nine named direct Relay callers plus the exact regional-rehome and same-cap reusable wrapper/job pairs on `main` in the `production` environment. Its Artifact Registry and Cloud Run mutation permissions are scoped to the Orca repository, Relay director, and Relay fence broker; it cannot mutate the API or auth services. @@ -461,8 +461,9 @@ margin covers. Each cell is isolated, drained until restart-safe, replaced from a targeted saved plan, and restored only after a new incarnation reports the exact digest, cap, heartbeat, and rehome protocol. Restart-safe means the cell runtime itself carries nothing live (no controls, in-flight connections, reserved connection units, splices, or queued bytes) and no -migration is open, for a whole drain pace window, which every live restart-safe call must pass. -Pre-auth and total connections are printed but do not reset the window: they include +migration is open, on two consecutive samples the second of which is taken at or after the end +of the drain pace window. Every live restart-safe call supplies that window (`--pace-window-ms`). +Pre-auth and total connections are printed but do not restart the count: they include unauthenticated redials that lose nothing on a restart. Director activity leases left by hosts that already went or cannot be placed do not hold the restart; every `relay_capacity_transition_restart_progress` sample and the final verified line report them under `stranded`. The durable @@ -539,11 +540,10 @@ drained. Then read the cell's live runtime image from The `drain-pace-window-ms` input sets the window the cell spreads its drain sends over. Each drained host re-dials the director as soon as it reads `drain`, so the window sets the re-placement arrival rate: hosts / window, about 1.8-2.7 hosts/s for a US cell at the default. The window also -sets two waits: restart-safe needs an empty runtime for (ceil(window / 5 s) + 1) consecutive -5-second samples, and the drain step's overall timeout is the 15-minute migration lease plus the -window, plus the window's excess over `300000` (20 minutes at the default, 50 at `1200000`). -Because the quiet starts only once the last host has left, a drain step takes about twice the -window: about 30 minutes at `900000` and 40 at `1200000`. +sets two waits: restart-safe needs an empty runtime on two consecutive 5-second samples, the +second at or after the window's end, and the drain step's overall timeout is the 15-minute +migration lease plus the window (20 minutes at the default, 35 at `1200000`). The runtime empties +as the window closes, so a drain step takes about the window plus a few seconds to half a minute. - Allowed values are `300000` (the default, and every wave before this input), `60000`, `30000`, `900000`, and `1200000`. The wave validator refuses anything else, and each cell job @@ -634,7 +634,7 @@ monitor already excludes 503s from its director 5xx rule. - its `paceVerdict` is PASS, sealed only from a report on that cell that drained at least 400 hosts at that pace; - time to empty is within window + hosts / 50 s + 10 s. Take `settledAfterSeconds` minus the - restart-safe quiet; this is an upper bound, since it includes the isolate. + one 5-second confirming sample; this is an upper bound, since it includes the isolate. The batch check enforces the PASS. A canary that sealed anything else authorizes only `300000` batches. The 400-host floor exists because a pace is an arrival rate (hosts / @@ -801,6 +801,76 @@ It ends with the single command that finishes from where it stopped. That comman state. Each run writes a timestamped log under `~/.orca/relay-director-deploy/` (`--log-directory` overrides it). +## Reserve watch + +`Operate Relay Production Reserve Watch` watches one cell already switched to reserve mode and, on +the first step 5 stop rule it trips, flips it back to db through the flag tool's own write +(`operate-relay-cell-flags.mjs`: CAS, read-back, re-registration wait, Postgres record). It is +manual only and inert until dispatched; `dry-run` logs the flag tool's plan instead of writing. +`write` takes the flag workflow's confirmation, `WRITE_RELAY_CELL_FLAGS `. + +It samples once a minute for up to 300 minutes, re-authenticating every 55-minute segment because +an ID token lasts an hour, and logs one JSON line per sample. The 503 background must predate the +flip: give `flipped-at` (when the reserve write applied; the background is the median of the 10 +minutes before it) or `background-503-per-minute`, which overrides it. With neither, the watch is +refused. A watch dispatched before the reserve write applied waits up to 10 minutes for the cell to +report reserve, judging nothing meanwhile; a cell that never does ends the watch refused, without a +write. It also refuses to start while any run holds the Cloud SQL rollout lease (same-cap and +capacity rolls hold it for their whole run, and it names no cell), or when that lease cannot be +read. It only reads the lease: holding it for hours would block every rollout. Pausing on a roll +that starts mid-watch was not built, since a pause blinds the stop rules for the roll's length and +a flip back mid-roll races its drain, so do not start a roll on a watched cell. It trips on, in +this order: + +| Rule | Signal | Exact or proxy | +|---|---|---| +| Dead-man tripped | runtime-status `admitModeEffective=db` under a reserve switch file | Exact | +| Overrun | runtime-status `enforcedConnectionUnits` over `connectionCapacity.hardCap` | Invariant check: the ledger already refuses past its cap, so this trips only if that refusal broke. It counts seats, bookings, in-flight and phone units | +| 503s | Director `request_count` 503s above max(1.5x background, background + 20) for 3 complete minutes | Proxy that can miss: fleet-wide, since no signal attributes a 503 to a cell, so a problem on one cell can stay under the fleet's line | +| Duplicate past grace | The ledger reconcile's `orca_relay_reserve_ledger_seat_behind_row` for the cell, reported again more than 90 s (60 s demotion close + 30 s map lag) after it was first seen, keyed by director instance, cell, epoch and row epoch | Proxy that can only over-count: the line names no host, so two hosts sharing that key read as one seat | +| Host drop | runtime-status `controls` fell more than 5% (and at least 10 hosts) within 5 minutes with no `draining` sample | Exact | +| Ledger drift | Queued plus dropped rows in each director's newest `orca_relay_reserve_ledger_summary`, over the cell's controls, above 0.5% for 2 minutes | Proxy that can only over-count: the summaries are fleet-wide, so exact only while this is the only reserve cell | + +Any rule it cannot read in a sample makes that sample unverifiable; `unverifiable-limit` such +samples in a row is a trip of its own. The 503 read takes every response code in one query, and the +newest minute any code has a point for is how far the series is published: a 503 minute at or +before it with no point had no 503s, so a quiet fleet reads zero. Minutes past it are left out, not +counted; fewer than 3 published minutes in the read, no request series at all, or an unparseable +timestamp is unverifiable. This assumes the codes publish together; one code lagging the others +delays a trip by its lag but cannot hide one, because a later read sees its points. Until the +minutes before `flipped-at` are published (3 minutes past it) the 503 rule is pending, which does +not add to the streak. The duplicate read also takes `orca_relay_reserve_ledger_summary`, which +every placing director logs each minute, in the same query and window: with no summary from the +last 150 s in it, the read is blind and the sample unverifiable, not zero duplicates. The duplicate +rule keys a seat by director instance, so a director restart (a new instance id) starts its +first-seen clock over. A cell whose switch file no longer says reserve ends the watch without a +write. + +In `write` mode a watch that fails, times out or is cancelled before a verdict flips the cell back +as a trip would; the flip-back job and each of its steps run on `always()`, so a cancel during the +lease wait does not skip the switch-file write. That cancel can still kill the process before +Postgres records db, leaving the file at db and Postgres at reserve; a force-cancel can stop it +outright. The workflow's only group is +its own per-cell one, which a running run never yields. It stays out of the flag workflow's group +because GitHub keeps one pending run per group and a newer one cancels it, so a flip back queued +there could be dropped by any later flag dispatch, and a watch held there would block manual writes +for hours. The flag tool's generation check (CAS) orders it against other writes instead; on a +conflict it re-reads and retries, up to 5 times, while the switch file still says reserve, and stops +if it already says db. A cell that still does not answer runtime-status after 4 tries with backoff +cannot take the ordinary flip back: the run fails and prints the exact break-glass inputs for +`Operate Relay Production Cell Flags` (`set=admitMode=default`, `break-glass=BREAK GLASS `). + +An operator watches the run. A refused watch and a flip back that did not land (no ID token, a +failed login, a cell that does not answer) can each leave the cell in reserve with nothing watching +it, so both raise an `::error::` annotation naming the cell, as the clock-skew monitor does. In +`write` mode both also log `orca_relay_reserve_watch_alarm` (log `orca-relay-reserve-watch`, +resource `global`) with the deploy identity, and the `Orca Relay: reserve cell may be unwatched` +alert pages on any such line. A failed login cannot page: the runner then has no credentials for +`gcloud logging write`, so only the `::error::` and the operator watching the run report it. Apply +the root Terraform that adds the workflow to the shared deploy identity's provider condition and +grants that identity `roles/logging.logWriter` before the first `write` run; until then the +workflow cannot log in and no alarm line can be written. + ## Mobile push gateway `Deploy Push Gateway Production` (`.github/workflows/cloud-push-deploy.yml`) is the deploy path diff --git a/cloud/infra/terraform/environments/staging.tfvars b/cloud/infra/terraform/environments/staging.tfvars index 4bc485540b4..d74abd85b75 100644 --- a/cloud/infra/terraform/environments/staging.tfvars +++ b/cloud/infra/terraform/environments/staging.tfvars @@ -48,8 +48,8 @@ relay_gce_cells = { boot_disk_gb = 30 boot_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-stable-121-18867-528-7" capacity_requests = 4000 - # Step-5 image F (main 3c1141f2). Never 2d0f6e6: it cannot report capped telemetry. - image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:41cba995b288cab70579882ebef23e74c5ee426b6f8924cea7c986aba23bcbfe" + # Step-5 image G (main 2de73812, reports preAuthLatencyMs). Never 2d0f6e6: it cannot report capped telemetry. + image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:07b5bf67a7706b2a61ef5e9a6d8ce85ee37c2a08609496a94eb5254881b5b87d" # Step-5 staging e2e: c1 is the second US reserve cell, so it needs a cap and rehome trust. connection_hard_cap = 600 connection_unobserved_bound = 60 @@ -86,7 +86,7 @@ relay_gce_cells = { boot_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-stable-121-18867-528-21" capacity_requests = 6000 database_pool_max = 10 - image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:41cba995b288cab70579882ebef23e74c5ee426b6f8924cea7c986aba23bcbfe" + image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:07b5bf67a7706b2a61ef5e9a6d8ce85ee37c2a08609496a94eb5254881b5b87d" initially_enabled = false connection_hard_cap = 3000 connection_unobserved_bound = 60 @@ -100,7 +100,7 @@ relay_gce_cells = { boot_disk_gb = 30 boot_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-stable-121-18867-528-7" capacity_requests = 4000 - image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:41cba995b288cab70579882ebef23e74c5ee426b6f8924cea7c986aba23bcbfe" + image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:07b5bf67a7706b2a61ef5e9a6d8ce85ee37c2a08609496a94eb5254881b5b87d" connection_hard_cap = 600 connection_unobserved_bound = 60 } @@ -111,7 +111,7 @@ relay_gce_cells = { boot_disk_gb = 30 boot_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-stable-121-18867-528-7" capacity_requests = 4000 - image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:41cba995b288cab70579882ebef23e74c5ee426b6f8924cea7c986aba23bcbfe" + image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:07b5bf67a7706b2a61ef5e9a6d8ce85ee37c2a08609496a94eb5254881b5b87d" connection_hard_cap = 600 connection_unobserved_bound = 60 } @@ -122,7 +122,7 @@ relay_gce_cells = { boot_disk_gb = 30 boot_image = "https://www.googleapis.com/compute/v1/projects/cos-cloud/global/images/cos-stable-121-18867-528-7" capacity_requests = 4000 - image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:41cba995b288cab70579882ebef23e74c5ee426b6f8924cea7c986aba23bcbfe" + image = "us-central1-docker.pkg.dev/onorca-cloud-staging/orca-cloud/relay@sha256:07b5bf67a7706b2a61ef5e9a6d8ce85ee37c2a08609496a94eb5254881b5b87d" connection_hard_cap = 600 connection_unobserved_bound = 60 } diff --git a/cloud/infra/terraform/relay-github-actions.tf b/cloud/infra/terraform/relay-github-actions.tf index fe4108312e4..f7f130263db 100644 --- a/cloud/infra/terraform/relay-github-actions.tf +++ b/cloud/infra/terraform/relay-github-actions.tf @@ -20,6 +20,7 @@ locals { "deploy-relay-production.yml", "operate-relay-asia-admission.yml", "operate-relay-production-cell-flags.yml", + "operate-relay-production-reserve-watch.yml", "publish-relay-production.yml" ] @@ -380,6 +381,16 @@ resource "google_project_iam_member" "github_logging_viewer" { member = google_service_account.github_deploy[0].member } +# The reserve watch logs its paging alarm line (orca_relay_reserve_watch_alarm) from the runner. +# Accepted risk: Cloud Logging cannot narrow writes to one log, so this identity can write any line. +resource "google_project_iam_member" "github_log_writer" { + count = local.relay_create_production_ops_identity ? 1 : 0 + + project = var.project_id + role = "roles/logging.logWriter" + member = google_service_account.github_deploy[0].member +} + resource "google_project_iam_member" "github_cloudsql_viewer" { count = local.relay_create_production_ops_identity ? 1 : 0 diff --git a/cloud/infra/terraform/relay-observability.tf b/cloud/infra/terraform/relay-observability.tf index 0e867366613..5149d355040 100644 --- a/cloud/infra/terraform/relay-observability.tf +++ b/cloud/infra/terraform/relay-observability.tf @@ -1748,6 +1748,74 @@ resource "google_monitoring_alert_policy" "relay_cell_reserve_dead_man_tripped" depends_on = [google_logging_metric.relay_cell_reserve_dead_man_tripped] } +# The reserve watch workflow logs this from its runner (resource global) when a watch is refused or +# a flip back did not land in write mode: either can leave a cell in reserve unwatched. +resource "google_logging_metric" "relay_reserve_watch_alarm" { + project = var.project_id + name = "orca_relay_reserve_watch_alarm" + description = "Reserve watch runs that were refused or whose flip back to db did not land, so the cell may be in reserve mode with nothing watching it." + filter = "resource.type=\"global\" AND logName=\"projects/${var.project_id}/logs/orca-relay-reserve-watch\" AND jsonPayload.event=\"orca_relay_reserve_watch_alarm\"" + label_extractors = { + cell_id = "EXTRACT(jsonPayload.cellId)" + kind = "EXTRACT(jsonPayload.kind)" + } + + metric_descriptor { + metric_kind = "DELTA" + value_type = "INT64" + unit = "1" + + labels { + key = "cell_id" + value_type = "STRING" + description = "Durable relay cell identifier." + } + + labels { + key = "kind" + value_type = "STRING" + description = "refused or flip-back-failed." + } + } +} + +resource "google_monitoring_alert_policy" "relay_reserve_watch_alarm" { + project = var.project_id + display_name = "Orca Relay: reserve cell may be unwatched" + combiner = "OR" + enabled = true + notification_channels = var.relay_alert_notification_channels + + conditions { + display_name = "A reserve watch was refused or its flip back did not land" + + condition_threshold { + filter = "resource.type=\"global\" AND metric.type=\"logging.googleapis.com/user/orca_relay_reserve_watch_alarm\"" + comparison = "COMPARISON_GT" + threshold_value = 0 + duration = "0s" + + aggregations { + alignment_period = "300s" + per_series_aligner = "ALIGN_SUM" + cross_series_reducer = "REDUCE_SUM" + group_by_fields = ["metric.label.\"cell_id\""] + } + + trigger { + count = 1 + } + } + } + + documentation { + content = "A write-mode `Operate Relay Production Reserve Watch` run was refused (a rollout held the Cloud SQL rollout lease, the lease was unreadable, bad inputs, or the cell never reported reserve within 10 minutes) or its flip back to db did not land (no ID token, a failed login, or a cell that does not answer). The cell may be in reserve mode with nothing watching it. Read the cell's `runtime-status`; if its switch file says reserve, either rerun the watch or run `cloud-operate-relay-production-cell-flags.yml` with `admitMode=db` for that cell, using the break-glass inputs the failed run printed if the cell does not answer." + mime_type = "text/markdown" + } + + depends_on = [google_logging_metric.relay_reserve_watch_alarm] +} + # host-session-registry.ts: a flip back closed a control it could not re-register (4409), after 20 refusals. resource "google_logging_metric" "relay_cell_reregistration_gave_up" { project = var.project_id diff --git a/cloud/package.json b/cloud/package.json index 84c33de4853..8f7a8046b22 100644 --- a/cloud/package.json +++ b/cloud/package.json @@ -1,12 +1,7 @@ { "name": "orca-cloud", - "private": true, "version": "0.0.0", - "packageManager": "pnpm@10.34.6", - "engines": { - "node": ">=24 <27", - "pnpm": ">=10" - }, + "private": true, "scripts": { "build": "pnpm -r build", "dev": "pnpm --filter @orca-cloud/relay dev", @@ -21,14 +16,21 @@ "load:relay:model": "node dev/scripts/run-relay-load-model.mjs", "load:relay:recovery-gate": "node dev/scripts/run-relay-recovery-wave-gate.mjs", "ops:relay": "pnpm --filter @orca-cloud/relay-ops dev", - "pretest": "node --test dev/scripts/capture-terraform-plan-baseline.test.mjs dev/scripts/operate-relay-asia-admission.test.mjs dev/scripts/prepare-relay-asia-director-cells.test.mjs dev/scripts/prepare-relay-asia-topology-input.test.mjs dev/scripts/production-cloud-sql-rollout-lock.test.mjs dev/scripts/read-relay-serving-regional-placement-version.test.mjs dev/scripts/relay-asia-rollout-evidence.test.mjs dev/scripts/relay-asia-topology-workflow.test.mjs dev/scripts/relay-cloud-sql-connection-budget.test.mjs dev/scripts/relay-load-reader-evidence.test.mjs dev/scripts/relay-control-flag-alerts.test.mjs dev/scripts/relay-db-fault-alerts.test.mjs dev/scripts/relay-lock-contention-alerts.test.mjs dev/scripts/relay-reconnect-herd-alerts.test.mjs dev/scripts/relay-reserve-dead-man-alert.test.mjs dev/scripts/relay-region-hint-metrics.test.mjs dev/scripts/relay-staging-deploy-identity.test.mjs dev/scripts/sanitize-relay-asia-admission-result.test.mjs dev/scripts/terraform-root-partition.test.mjs dev/scripts/validate-relay-asia-topology-plan.test.mjs ../.github/actions/cloud-sql-rollout-lease/action-contract.test.mjs ../.github/actions/cloud-sql-rollout-lease/storage-lease.test.mjs", - "test": "pnpm -r test && node --test dev/scripts/check-relay-same-cap-headroom.test.mjs dev/scripts/classify-relay-production-capacity-director.test.mjs dev/scripts/classify-relay-staging-bootstrap.test.mjs dev/scripts/deploy-relay-blue-green.test.mjs dev/scripts/deploy-relay-gce-candidate.test.mjs dev/scripts/deploy-relay-gce-multi-target.test.mjs dev/scripts/drive-relay-director-deploy.test.mjs dev/scripts/github-smoke-token.test.mjs dev/scripts/infra.test.mjs dev/scripts/measure-relay-drain-disconnect-gap.test.mjs dev/scripts/operate-relay-cell-flags.test.mjs dev/scripts/operate-relay-regional-rehome.test.mjs dev/scripts/power-staging-relay.test.mjs dev/scripts/prepare-relay-capacity-canary.test.mjs dev/scripts/prepare-relay-production-capacity-canary.test.mjs dev/scripts/probe-relay-legacy-admission.test.mjs dev/scripts/probe-relay-rehome-trust.test.mjs dev/scripts/production-cell-image-digest-consistency.test.mjs dev/scripts/push-gateway-workflow.test.mjs dev/scripts/push-gateway-recovery.test.mjs dev/scripts/read-relay-production-capacity-identity.test.mjs dev/scripts/relay-admin-endpoint-retry-workflow.test.mjs dev/scripts/relay-admin-transient-retry.test.mjs dev/scripts/relay-admission-selector.test.mjs dev/scripts/relay-gce-terraform-fence.test.mjs dev/scripts/relay-load-connection-failure.test.mjs dev/scripts/relay-load-control-peer.test.mjs dev/scripts/relay-load-director-capacity-gate.test.mjs dev/scripts/relay-load-duplicate-assign.test.mjs dev/scripts/relay-load-model.test.mjs dev/scripts/relay-load-phase-barrier.test.mjs dev/scripts/relay-load-placement-boundary.test.mjs dev/scripts/relay-load-profile.test.mjs dev/scripts/relay-load-rebind-boundary.test.mjs dev/scripts/relay-load-region-behavior.test.mjs dev/scripts/relay-load-request-unit-boundary.test.mjs dev/scripts/relay-load-run-lifecycle.test.mjs dev/scripts/relay-load-splice-retry.test.mjs dev/scripts/relay-monitor-evidence.test.mjs dev/scripts/relay-production-capacity-wave.test.mjs dev/scripts/relay-production-capacity-workflow.test.mjs dev/scripts/relay-production-identity-boundaries.test.mjs dev/scripts/relay-production-same-cap-wave.test.mjs dev/scripts/relay-public-workflow-contract.test.mjs dev/scripts/relay-recovery-wave-gate.test.mjs dev/scripts/relay-region-observation-evidence.test.mjs dev/scripts/relay-rehome-aggregate-evidence.test.mjs dev/scripts/relay-repository.test.mjs dev/scripts/relay-same-cap-job-mode-conditions.test.mjs dev/scripts/relay-same-cap-script-census.test.mjs dev/scripts/relay-same-cap-shadow-gate.test.mjs dev/scripts/relay-staging-c4-refresh-workflow.test.mjs dev/scripts/relay-staging-capacity-identity.test.mjs dev/scripts/staging-relay-apply-guard.test.mjs dev/scripts/validate-relay-capacity-plan.test.mjs dev/scripts/verify-relay-capacity-transition.test.mjs dev/scripts/verify-relay-legacy-bootstrap.test.mjs dev/scripts/workload-identity-attribute-conditions.test.mjs", - "typecheck": "pnpm -r typecheck" + "pretest": "node --test dev/scripts/capture-terraform-plan-baseline.test.mjs dev/scripts/operate-relay-asia-admission.test.mjs dev/scripts/prepare-relay-asia-director-cells.test.mjs dev/scripts/prepare-relay-asia-topology-input.test.mjs dev/scripts/production-cloud-sql-rollout-lock.test.mjs dev/scripts/read-relay-serving-regional-placement-version.test.mjs dev/scripts/relay-asia-rollout-evidence.test.mjs dev/scripts/relay-asia-topology-workflow.test.mjs dev/scripts/relay-cloud-sql-connection-budget.test.mjs dev/scripts/relay-load-reader-evidence.test.mjs dev/scripts/relay-control-flag-alerts.test.mjs dev/scripts/relay-db-fault-alerts.test.mjs dev/scripts/relay-lock-contention-alerts.test.mjs dev/scripts/relay-reconnect-herd-alerts.test.mjs dev/scripts/relay-reserve-dead-man-alert.test.mjs dev/scripts/relay-region-hint-metrics.test.mjs dev/scripts/relay-staging-deploy-identity.test.mjs dev/scripts/sanitize-relay-asia-admission-result.test.mjs dev/scripts/terraform-root-partition.test.mjs dev/scripts/validate-relay-asia-topology-plan.test.mjs ../.github/actions/cloud-sql-rollout-lease/action-contract.test.mjs ../.github/actions/cloud-sql-rollout-lease/storage-lease.test.mjs && pnpm test:runner", + "test": "pnpm -r --filter \"!@orca-cloud/relay\" test && pnpm --filter @orca-cloud/relay test && node --test dev/scripts/check-relay-same-cap-headroom.test.mjs dev/scripts/classify-relay-production-capacity-director.test.mjs dev/scripts/classify-relay-staging-bootstrap.test.mjs dev/scripts/deploy-relay-blue-green.test.mjs dev/scripts/deploy-relay-gce-candidate.test.mjs dev/scripts/deploy-relay-gce-multi-target.test.mjs dev/scripts/drive-relay-director-deploy.test.mjs dev/scripts/github-smoke-token.test.mjs dev/scripts/infra.test.mjs dev/scripts/measure-relay-drain-disconnect-gap.test.mjs dev/scripts/operate-relay-cell-flags.test.mjs dev/scripts/operate-relay-regional-rehome.test.mjs dev/scripts/power-staging-relay.test.mjs dev/scripts/prepare-relay-capacity-canary.test.mjs dev/scripts/prepare-relay-production-capacity-canary.test.mjs dev/scripts/probe-relay-legacy-admission.test.mjs dev/scripts/probe-relay-rehome-trust.test.mjs dev/scripts/production-cell-image-digest-consistency.test.mjs dev/scripts/push-gateway-workflow.test.mjs dev/scripts/push-gateway-recovery.test.mjs dev/scripts/read-relay-production-capacity-identity.test.mjs dev/scripts/relay-admin-endpoint-retry-workflow.test.mjs dev/scripts/relay-admin-transient-retry.test.mjs dev/scripts/relay-admission-selector.test.mjs dev/scripts/relay-gce-terraform-fence.test.mjs dev/scripts/relay-load-connection-failure.test.mjs dev/scripts/relay-load-control-peer.test.mjs dev/scripts/relay-load-director-capacity-gate.test.mjs dev/scripts/relay-load-duplicate-assign.test.mjs dev/scripts/relay-load-model.test.mjs dev/scripts/relay-load-phase-barrier.test.mjs dev/scripts/relay-load-placement-boundary.test.mjs dev/scripts/relay-load-profile.test.mjs dev/scripts/relay-load-rebind-boundary.test.mjs dev/scripts/relay-load-region-behavior.test.mjs dev/scripts/relay-load-request-unit-boundary.test.mjs dev/scripts/relay-load-run-lifecycle.test.mjs dev/scripts/relay-load-splice-retry.test.mjs dev/scripts/relay-monitor-evidence.test.mjs dev/scripts/relay-production-capacity-wave.test.mjs dev/scripts/relay-production-capacity-workflow.test.mjs dev/scripts/relay-production-identity-boundaries.test.mjs dev/scripts/relay-production-same-cap-wave.test.mjs dev/scripts/relay-public-workflow-contract.test.mjs dev/scripts/relay-recovery-wave-gate.test.mjs dev/scripts/relay-region-observation-evidence.test.mjs dev/scripts/relay-rehome-aggregate-evidence.test.mjs dev/scripts/relay-repository.test.mjs dev/scripts/relay-same-cap-job-mode-conditions.test.mjs dev/scripts/relay-same-cap-script-census.test.mjs dev/scripts/relay-same-cap-shadow-gate.test.mjs dev/scripts/relay-staging-c4-refresh-workflow.test.mjs dev/scripts/relay-staging-capacity-identity.test.mjs dev/scripts/staging-relay-apply-guard.test.mjs dev/scripts/validate-relay-capacity-plan.test.mjs dev/scripts/verify-relay-capacity-transition.test.mjs dev/scripts/verify-relay-legacy-bootstrap.test.mjs dev/scripts/watch-relay-reserve-cell.test.mjs dev/scripts/workload-identity-attribute-conditions.test.mjs", + "typecheck": "pnpm -r typecheck", + "test:runner": "node --import tsx --conditions=orca-source --test dev/scripts/run-relay-tests.test.mjs" }, "devDependencies": { + "@orca/process-host": "link:../src/packages/process-host", "@types/node": "^24.19.0", "tsx": "^4.23.15", "typescript": "^5.9.3", "vitest": "^4.1.11" - } + }, + "engines": { + "node": ">=24 <27", + "pnpm": ">=10" + }, + "packageManager": "pnpm@10.34.6" } diff --git a/cloud/pnpm-lock.yaml b/cloud/pnpm-lock.yaml index c688889cb50..2720b9e6474 100644 --- a/cloud/pnpm-lock.yaml +++ b/cloud/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@orca/process-host': + specifier: link:../src/packages/process-host + version: link:../src/packages/process-host '@types/node': specifier: ^24.19.0 version: 24.19.0 diff --git a/config/patches/i18next-cli@1.74.2.patch b/config/patches/i18next-cli@1.74.2.patch index e4eb976d1ec..307096dd25c 100644 --- a/config/patches/i18next-cli@1.74.2.patch +++ b/config/patches/i18next-cli@1.74.2.patch @@ -1,5 +1,5 @@ diff --git a/dist/cjs/extractor/core/extractor.js b/dist/cjs/extractor/core/extractor.js -index 6d61ade2471c20bf1a253e3784bfadcb01440545..c1b001e78a215f89a8d4da1756a87562a5a6e723 100644 +index 6d61ade2471..23ad9f5ba49 100644 --- a/dist/cjs/extractor/core/extractor.js +++ b/dist/cjs/extractor/core/extractor.js @@ -143,6 +143,8 @@ const extractionSiteRegexes = new WeakMap(); @@ -20,8 +20,185 @@ index 6d61ade2471c20bf1a253e3784bfadcb01440545..c1b001e78a215f89a8d4da1756a87562 extractionSiteRegexes.set(config, re); } return re.test(code); +@@ -185,13 +187,15 @@ function mayContainExtractionSite(code, config) { + * + * @internal + */ +-async function processFile(file, plugins, astVisitors, pluginContext, config, logger$1 = new logger.ConsoleLogger(), fileErrors) { ++async function processFile(file, plugins, astVisitors, pluginContext, config, logger$1 = new logger.ConsoleLogger(), fileErrors, parsedFiles) { + try { + // Skip directories that happen to match file-extension globs (e.g. a directory named "Foo.tsx") + const fileStat = await promises.stat(file); + if (fileStat.isDirectory()) + return; +- let code = await promises.readFile(file, 'utf-8'); ++ const cached = parsedFiles?.get(file); ++ parsedFiles?.delete(file); ++ let code = cached?.code ?? await promises.readFile(file, 'utf-8'); + // Run onLoad hooks from plugins with error handling. + // Track whether any plugin actually transformed the code so we can make + // the right parse decision for non-native file types (e.g. .svelte, .vue). +@@ -241,59 +245,61 @@ async function processFile(file, plugins, astVisitors, pluginContext, config, lo + const isTypeScriptFile = effectiveExt === '.ts' || effectiveExt === '.tsx' || effectiveExt === '.mts' || effectiveExt === '.cts'; + const isTSX = effectiveExt === '.tsx'; + const isJSX = effectiveExt === '.jsx'; +- let ast; +- try { +- ast = await core.parse(code, { +- syntax: isTypeScriptFile ? 'typescript' : 'ecmascript', +- tsx: isTSX, +- jsx: isJSX, +- decorators: true, +- dynamicImport: true, +- comments: true, +- }); +- } +- catch (err) { +- // Fallback for .ts files with JSX (already present) +- if (effectiveExt === '.ts' && !isTSX) { +- try { +- ast = await core.parse(code, { +- syntax: 'typescript', +- tsx: true, +- decorators: true, +- dynamicImport: true, +- comments: true, +- }); +- logger$1.info?.(`Parsed ${file} using TSX fallback`); +- } +- catch (err2) { +- throw new validation.ExtractorError('Failed to process file', file, err2); +- } +- // Fallback for .js files with JSX ++ let ast = cached?.ast; ++ if (!cached) { ++ try { ++ ast = await core.parse(code, { ++ syntax: isTypeScriptFile ? 'typescript' : 'ecmascript', ++ tsx: isTSX, ++ jsx: isJSX, ++ decorators: true, ++ dynamicImport: true, ++ comments: true, ++ }); + } +- else if (effectiveExt === '.js' && !isJSX) { +- try { +- ast = await core.parse(code, { +- syntax: 'ecmascript', +- jsx: true, +- decorators: true, +- dynamicImport: true, +- comments: true, +- }); +- logger$1.info?.(`Parsed ${file} using JSX fallback`); ++ catch (err) { ++ // Fallback for .ts files with JSX (already present) ++ if (effectiveExt === '.ts' && !isTSX) { ++ try { ++ ast = await core.parse(code, { ++ syntax: 'typescript', ++ tsx: true, ++ decorators: true, ++ dynamicImport: true, ++ comments: true, ++ }); ++ logger$1.info?.(`Parsed ${file} using TSX fallback`); ++ } ++ catch (err2) { ++ throw new validation.ExtractorError('Failed to process file', file, err2); ++ } ++ // Fallback for .js files with JSX + } +- catch (err2) { +- throw new validation.ExtractorError('Failed to process file', file, err2); ++ else if (effectiveExt === '.js' && !isJSX) { ++ try { ++ ast = await core.parse(code, { ++ syntax: 'ecmascript', ++ jsx: true, ++ decorators: true, ++ dynamicImport: true, ++ comments: true, ++ }); ++ logger$1.info?.(`Parsed ${file} using JSX fallback`); ++ } ++ catch (err2) { ++ throw new validation.ExtractorError('Failed to process file', file, err2); ++ } ++ } ++ else { ++ throw new validation.ExtractorError('Failed to process file', file, err); + } + } +- else { +- throw new validation.ExtractorError('Failed to process file', file, err); +- } ++ // Normalize SWC span offsets so every span is a file-relative (0-based) ++ // character index. SWC accumulates byte offsets across successive parse() ++ // calls and Module.span.start points to the first token, NOT to byte 0 of ++ // the source. ++ astUtils.normalizeSpansToCharIndices(ast, code); + } +- // Normalize SWC span offsets so every span is a file-relative (0-based) +- // character index. SWC accumulates byte offsets across successive parse() +- // calls and Module.span.start points to the first token, NOT to byte 0 of +- // the source. +- astUtils.normalizeSpansToCharIndices(ast, code); + // "Wire up" the visitor's scope method to the context. + // This avoids a circular dependency while giving plugins access to the scope. + pluginContext.getVarFromScope = astVisitors.getVarFromScope.bind(astVisitors); +@@ -344,7 +350,7 @@ async function processFile(file, plugins, astVisitors, pluginContext, config, lo + * @param logger - Logger for warnings/errors + * @param fileErrors - Optional array to collect per-file error messages + */ +-async function preScanFile(file, astVisitors, config, logger$1 = new logger.ConsoleLogger(), fileErrors) { ++async function preScanFile(file, astVisitors, config, logger$1 = new logger.ConsoleLogger(), fileErrors, parsedFiles) { + try { + // Skip directories that happen to match file-extension globs (e.g. a directory named "Foo.tsx") + const fileStat = await promises.stat(file); +@@ -396,6 +402,8 @@ async function preScanFile(file, astVisitors, config, logger$1 = new logger.Cons + astUtils.normalizeSpansToCharIndices(ast, code); + astVisitors.setCurrentFile(file, code); + astVisitors.preScanForConstants(ast); ++ if (parsedFiles && mayContainExtractionSite(code, config)) ++ parsedFiles.set(file, { code, ast }); + } + catch (error) { + if (error instanceof pluginManager.ConflictError) +diff --git a/dist/cjs/extractor/core/key-finder.js b/dist/cjs/extractor/core/key-finder.js +index 1411f287157..200b6b974b6 100644 +--- a/dist/cjs/extractor/core/key-finder.js ++++ b/dist/cjs/extractor/core/key-finder.js +@@ -40,6 +40,8 @@ async function findKeys(config, logger$1 = new logger.ConsoleLogger(), fileError + const plugins = pluginsOrUndefined || []; + const sourceFiles = await processSourceFiles(config); + const allKeys = new Map(); ++ // Retain only translation-site trees; plugins can transform code between passes. ++ const parsedFiles = plugins.length === 0 ? new Map() : undefined; + // 1. Create the base context with config and logger. + const pluginContext = pluginManager.createPluginContext(allKeys, plugins, otherConfig, logger$1); + // 2. Create hooks for plugins to hook into AST +@@ -92,7 +94,7 @@ async function findKeys(config, logger$1 = new logger.ConsoleLogger(), fileError + // correctly even when the defining constants file is processed after the file that + // uses the identifier. + for (const file of sourceFiles) { +- await extractor.preScanFile(file, astVisitors$1, otherConfig, logger$1, fileErrors); ++ await extractor.preScanFile(file, astVisitors$1, otherConfig, logger$1, fileErrors, parsedFiles); + } + // 6b. Mirror declarations onto the local names they were imported as, so that + // `import { ResourceStatus as Status }` resolves like the unaliased import. +@@ -100,7 +102,7 @@ async function findKeys(config, logger$1 = new logger.ConsoleLogger(), fileError + // 7. Extraction pass: all shared tables are now fully populated, so every + // identifier reference can be resolved regardless of file order. + for (const file of sourceFiles) { +- await extractor.processFile(file, plugins, astVisitors$1, pluginContext, otherConfig, logger$1, fileErrors); ++ await extractor.processFile(file, plugins, astVisitors$1, pluginContext, otherConfig, logger$1, fileErrors, parsedFiles); + } + // 7. Run onEnd hooks + for (const plugin of plugins) { diff --git a/dist/esm/extractor/core/extractor.js b/dist/esm/extractor/core/extractor.js -index 910bdb6e25e26c01ea6127191f1a00c7089693c1..00da4a6a7acb6c3da8d4b6fe7be106c409542f8d 100644 +index 910bdb6e25e..a3868d29950 100644 --- a/dist/esm/extractor/core/extractor.js +++ b/dist/esm/extractor/core/extractor.js @@ -141,6 +141,8 @@ const extractionSiteRegexes = new WeakMap(); @@ -42,3 +219,180 @@ index 910bdb6e25e26c01ea6127191f1a00c7089693c1..00da4a6a7acb6c3da8d4b6fe7be106c4 extractionSiteRegexes.set(config, re); } return re.test(code); +@@ -183,13 +185,15 @@ function mayContainExtractionSite(code, config) { + * + * @internal + */ +-async function processFile(file, plugins, astVisitors, pluginContext, config, logger = new ConsoleLogger(), fileErrors) { ++async function processFile(file, plugins, astVisitors, pluginContext, config, logger = new ConsoleLogger(), fileErrors, parsedFiles) { + try { + // Skip directories that happen to match file-extension globs (e.g. a directory named "Foo.tsx") + const fileStat = await stat(file); + if (fileStat.isDirectory()) + return; +- let code = await readFile(file, 'utf-8'); ++ const cached = parsedFiles?.get(file); ++ parsedFiles?.delete(file); ++ let code = cached?.code ?? await readFile(file, 'utf-8'); + // Run onLoad hooks from plugins with error handling. + // Track whether any plugin actually transformed the code so we can make + // the right parse decision for non-native file types (e.g. .svelte, .vue). +@@ -239,59 +243,61 @@ async function processFile(file, plugins, astVisitors, pluginContext, config, lo + const isTypeScriptFile = effectiveExt === '.ts' || effectiveExt === '.tsx' || effectiveExt === '.mts' || effectiveExt === '.cts'; + const isTSX = effectiveExt === '.tsx'; + const isJSX = effectiveExt === '.jsx'; +- let ast; +- try { +- ast = await parse(code, { +- syntax: isTypeScriptFile ? 'typescript' : 'ecmascript', +- tsx: isTSX, +- jsx: isJSX, +- decorators: true, +- dynamicImport: true, +- comments: true, +- }); +- } +- catch (err) { +- // Fallback for .ts files with JSX (already present) +- if (effectiveExt === '.ts' && !isTSX) { +- try { +- ast = await parse(code, { +- syntax: 'typescript', +- tsx: true, +- decorators: true, +- dynamicImport: true, +- comments: true, +- }); +- logger.info?.(`Parsed ${file} using TSX fallback`); +- } +- catch (err2) { +- throw new ExtractorError('Failed to process file', file, err2); +- } +- // Fallback for .js files with JSX ++ let ast = cached?.ast; ++ if (!cached) { ++ try { ++ ast = await parse(code, { ++ syntax: isTypeScriptFile ? 'typescript' : 'ecmascript', ++ tsx: isTSX, ++ jsx: isJSX, ++ decorators: true, ++ dynamicImport: true, ++ comments: true, ++ }); + } +- else if (effectiveExt === '.js' && !isJSX) { +- try { +- ast = await parse(code, { +- syntax: 'ecmascript', +- jsx: true, +- decorators: true, +- dynamicImport: true, +- comments: true, +- }); +- logger.info?.(`Parsed ${file} using JSX fallback`); ++ catch (err) { ++ // Fallback for .ts files with JSX (already present) ++ if (effectiveExt === '.ts' && !isTSX) { ++ try { ++ ast = await parse(code, { ++ syntax: 'typescript', ++ tsx: true, ++ decorators: true, ++ dynamicImport: true, ++ comments: true, ++ }); ++ logger.info?.(`Parsed ${file} using TSX fallback`); ++ } ++ catch (err2) { ++ throw new ExtractorError('Failed to process file', file, err2); ++ } ++ // Fallback for .js files with JSX + } +- catch (err2) { +- throw new ExtractorError('Failed to process file', file, err2); ++ else if (effectiveExt === '.js' && !isJSX) { ++ try { ++ ast = await parse(code, { ++ syntax: 'ecmascript', ++ jsx: true, ++ decorators: true, ++ dynamicImport: true, ++ comments: true, ++ }); ++ logger.info?.(`Parsed ${file} using JSX fallback`); ++ } ++ catch (err2) { ++ throw new ExtractorError('Failed to process file', file, err2); ++ } ++ } ++ else { ++ throw new ExtractorError('Failed to process file', file, err); + } + } +- else { +- throw new ExtractorError('Failed to process file', file, err); +- } ++ // Normalize SWC span offsets so every span is a file-relative (0-based) ++ // character index. SWC accumulates byte offsets across successive parse() ++ // calls and Module.span.start points to the first token, NOT to byte 0 of ++ // the source. ++ normalizeSpansToCharIndices(ast, code); + } +- // Normalize SWC span offsets so every span is a file-relative (0-based) +- // character index. SWC accumulates byte offsets across successive parse() +- // calls and Module.span.start points to the first token, NOT to byte 0 of +- // the source. +- normalizeSpansToCharIndices(ast, code); + // "Wire up" the visitor's scope method to the context. + // This avoids a circular dependency while giving plugins access to the scope. + pluginContext.getVarFromScope = astVisitors.getVarFromScope.bind(astVisitors); +@@ -342,7 +348,7 @@ async function processFile(file, plugins, astVisitors, pluginContext, config, lo + * @param logger - Logger for warnings/errors + * @param fileErrors - Optional array to collect per-file error messages + */ +-async function preScanFile(file, astVisitors, config, logger = new ConsoleLogger(), fileErrors) { ++async function preScanFile(file, astVisitors, config, logger = new ConsoleLogger(), fileErrors, parsedFiles) { + try { + // Skip directories that happen to match file-extension globs (e.g. a directory named "Foo.tsx") + const fileStat = await stat(file); +@@ -394,6 +400,8 @@ async function preScanFile(file, astVisitors, config, logger = new ConsoleLogger + normalizeSpansToCharIndices(ast, code); + astVisitors.setCurrentFile(file, code); + astVisitors.preScanForConstants(ast); ++ if (parsedFiles && mayContainExtractionSite(code, config)) ++ parsedFiles.set(file, { code, ast }); + } + catch (error) { + if (error instanceof ConflictError) +diff --git a/dist/esm/extractor/core/key-finder.js b/dist/esm/extractor/core/key-finder.js +index 86612731b2d..4e724e84538 100644 +--- a/dist/esm/extractor/core/key-finder.js ++++ b/dist/esm/extractor/core/key-finder.js +@@ -38,6 +38,8 @@ async function findKeys(config, logger = new ConsoleLogger(), fileErrors) { + const plugins = pluginsOrUndefined || []; + const sourceFiles = await processSourceFiles(config); + const allKeys = new Map(); ++ // Retain only translation-site trees; plugins can transform code between passes. ++ const parsedFiles = plugins.length === 0 ? new Map() : undefined; + // 1. Create the base context with config and logger. + const pluginContext = createPluginContext(allKeys, plugins, otherConfig, logger); + // 2. Create hooks for plugins to hook into AST +@@ -90,7 +92,7 @@ async function findKeys(config, logger = new ConsoleLogger(), fileErrors) { + // correctly even when the defining constants file is processed after the file that + // uses the identifier. + for (const file of sourceFiles) { +- await preScanFile(file, astVisitors, otherConfig, logger, fileErrors); ++ await preScanFile(file, astVisitors, otherConfig, logger, fileErrors, parsedFiles); + } + // 6b. Mirror declarations onto the local names they were imported as, so that + // `import { ResourceStatus as Status }` resolves like the unaliased import. +@@ -98,7 +100,7 @@ async function findKeys(config, logger = new ConsoleLogger(), fileErrors) { + // 7. Extraction pass: all shared tables are now fully populated, so every + // identifier reference can be resolved regardless of file order. + for (const file of sourceFiles) { +- await processFile(file, plugins, astVisitors, pluginContext, otherConfig, logger, fileErrors); ++ await processFile(file, plugins, astVisitors, pluginContext, otherConfig, logger, fileErrors, parsedFiles); + } + // 7. Run onEnd hooks + for (const plugin of plugins) { diff --git a/config/renderer-layout-writer-baseline.txt b/config/renderer-layout-writer-baseline.txt index fa2387d0920..ccb3f75cbfd 100644 --- a/config/renderer-layout-writer-baseline.txt +++ b/config/renderer-layout-writer-baseline.txt @@ -15,7 +15,6 @@ 1 src/renderer/src/components/status-bar/workspace-space-decision-inputs.ts 2 src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts 3 src/renderer/src/components/tab-bar/use-tab-bar-runtime-model-worktree-write-probe.ts -23 src/renderer/src/components/tab-group/tab-drag-preview-activation.ts 1 src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.ts 1 src/renderer/src/components/terminal-pane/pty-connection-test-deps.ts 4 src/renderer/src/components/terminal-pane/terminal-recovery-ledger-test-store.ts @@ -51,7 +50,6 @@ 1 src/renderer/src/runtime/web-session-terminal-orphan-recovery-surface.ts 2 src/renderer/src/runtime/web-session-terminal-orphan-topology.ts 3 src/renderer/src/runtime/web-session-terminal-recovery-snapshot-validation.ts -9 src/renderer/src/store/repos/repo-removal.ts 10 src/renderer/src/store/slices/browser/browser-close-actions.ts 6 src/renderer/src/store/slices/browser/browser-hydration-actions.ts 5 src/renderer/src/store/slices/browser/browser-page-focus-actions.ts @@ -60,7 +58,6 @@ 4 src/renderer/src/store/slices/browser/browser-tab-focus-actions.ts 2 src/renderer/src/store/slices/browser/create-browser-slice.ts 1 src/renderer/src/store/slices/direct-ssh-terminal-recovery.ts -1 src/renderer/src/store/slices/editor/actions/chat-visual-tab-actions.ts 9 src/renderer/src/store/slices/editor/actions/check-run-details-actions.ts 7 src/renderer/src/store/slices/editor/actions/close-file-action.ts 1 src/renderer/src/store/slices/editor/actions/git-status-actions.ts @@ -82,13 +79,13 @@ 2 src/renderer/src/store/slices/editor/file-ids/hydrated-editor-file-selection.ts 1 src/renderer/src/store/slices/editor/file-ids/open-file-path-rekey.ts 4 src/renderer/src/store/slices/editor/tabs/editor-open-target-group.ts -2 src/renderer/src/store/slices/ssh-target-cleanup.ts 2 src/renderer/src/store/slices/tab-group-reference-repair.ts 4 src/renderer/src/store/slices/tab-group-state.ts 2 src/renderer/src/store/slices/tabs-hydration.ts 4 src/renderer/src/store/slices/tabs/create-tabs-slice.ts 17 src/renderer/src/store/slices/tabs/tabs-close-actions.ts 14 src/renderer/src/store/slices/tabs/tabs-create-actions.ts +23 src/renderer/src/store/slices/tabs/tabs-drag-preview-actions.ts 11 src/renderer/src/store/slices/tabs/tabs-drop-actions.ts 4 src/renderer/src/store/slices/tabs/tabs-focus-actions.ts 13 src/renderer/src/store/slices/tabs/tabs-group-actions.ts @@ -96,17 +93,15 @@ 8 src/renderer/src/store/slices/tabs/tabs-label-actions.ts 10 src/renderer/src/store/slices/tabs/tabs-move-actions.ts 9 src/renderer/src/store/slices/tabs/tabs-reconciliation.ts -1 src/renderer/src/store/slices/tabs/tabs-secondary-actions.ts 4 src/renderer/src/store/slices/tabs/tabs-session-actions.ts +1 src/renderer/src/store/slices/tabs/tabs-split-ratio-actions.ts 5 src/renderer/src/store/slices/tabs/tabs-surface.ts +33 src/renderer/src/store/slices/tabs/worktree-layout-lifecycle.ts 6 src/renderer/src/store/slices/terminal-orphan-helpers.ts 4 src/renderer/src/store/slices/terminal-tab-title-batch.ts 6 src/renderer/src/store/slices/terminals.ts -7 src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts -15 src/renderer/src/store/slices/worktrees/session/worktree-identity-rename-state.ts +5 src/renderer/src/store/slices/worktrees/session/set-active-worktree.ts 2 src/renderer/src/store/slices/worktrees/session/worktree-slice-lookups.ts -18 src/renderer/src/store/slices/worktrees/teardown/remove-worktree-store-cleanup.ts -18 src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts 5 src/renderer/src/store/terminals/terminal-layout-state.ts 2 src/renderer/src/store/terminals/terminal-pane-hibernation.ts 3 src/renderer/src/store/terminals/terminal-pty-bindings.ts diff --git a/config/scripts/build-wayland-clipboard.test.mjs b/config/scripts/build-wayland-clipboard.test.mjs index 9977a8353d8..64ebce4770f 100644 --- a/config/scripts/build-wayland-clipboard.test.mjs +++ b/config/scripts/build-wayland-clipboard.test.mjs @@ -12,6 +12,8 @@ import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { parse } from 'yaml' +import { runProcessSync } from '@orca/process-host' import { buildWaylandClipboard } from './build-wayland-clipboard.mjs' import glibcVerification from './verify-linux-glibc-floor.cjs' @@ -148,6 +150,50 @@ describe('Wayland clipboard build', () => { }) }) +describe('Wayland clipboard CI cache recovery', () => { + it.skipIf(process.platform === 'win32').each([ + ['healthy cache', true, 0, 0, 1], + ['invalid cache', true, 1, 0, 2], + ['failed replacement build', true, 1, 1, 2], + ['failed uncached build', false, 1, 1, 1] + ])('%s preserves build validation and failures', (_label, hit, initial, rebuild, calls) => { + const action = parse( + readFileSync('.github/actions/prepare-wayland-clipboard/action.yml', 'utf8') + ) + const build = action.runs.steps.find( + (step) => step.name === 'Build or validate Wayland clipboard helper' + ) + mkdirSync(join(root, 'native', 'wayland-clipboard', '.build'), { recursive: true }) + writeFileSync(join(root, 'native', 'wayland-clipboard', '.build', 'restored-cache'), '') + const script = ` + node() { + echo attempt >> calls + if [ -e native/wayland-clipboard/.build/restored-cache ]; then + return "$INITIAL_STATUS" + fi + return "$REBUILD_STATUS" + } + ${build.run} + ` + const result = runProcessSync({ + program: 'bash', + args: ['-e', '-c', script], + cwd: root, + env: { + ...process.env, + CACHE_HIT: String(hit), + INITIAL_STATUS: String(initial), + REBUILD_STATUS: String(rebuild) + } + }) + expect(result.code).toBe(initial === 0 ? 0 : hit ? rebuild : initial) + expect(readFileSync(join(root, 'calls'), 'utf8').trim().split('\n')).toHaveLength(calls) + expect(existsSync(join(root, 'native', 'wayland-clipboard', '.build', 'restored-cache'))).toBe( + calls === 1 + ) + }) +}) + describe('Wayland clipboard packaging', () => { it.each([ [1, 'x64'], diff --git a/config/scripts/check-node-runtime-pin.test.mjs b/config/scripts/check-node-runtime-pin.test.mjs index a805985d966..885bbc39776 100644 --- a/config/scripts/check-node-runtime-pin.test.mjs +++ b/config/scripts/check-node-runtime-pin.test.mjs @@ -189,7 +189,8 @@ describe('committed pin', () => { it('runs in the static analysis job', () => { const workflow = parse(readFileSync(path.join(projectDir, '.github/workflows/pr.yml'), 'utf8')) + const packageJson = JSON.parse(readFileSync(path.join(projectDir, 'package.json'), 'utf8')) const commands = workflow.jobs.preflight.steps.map((step) => step.run ?? '') - expect(commands).toContain('pnpm run check:node-runtime-pin') + expect(commands).toContain(packageJson.scripts['check:node-runtime-pin']) }) }) diff --git a/config/scripts/ci-e2e-job-selection.test.mjs b/config/scripts/ci-e2e-job-selection.test.mjs index 29d7b4c0a7a..2ecf9ea5b3c 100644 --- a/config/scripts/ci-e2e-job-selection.test.mjs +++ b/config/scripts/ci-e2e-job-selection.test.mjs @@ -97,7 +97,7 @@ it('keeps Electron build and native prerequisites for every consumer requiring t } expect(workflow.jobs['ssh-browser-network-route'].needs).toBeUndefined() for (const name of ['e2e', 'changed-e2e']) { - expect(workflow.jobs[name].needs, name).toEqual(['build', 'prepare-native-cache']) + expect(workflow.jobs[name].needs, name).toEqual(['build']) } }) @@ -132,11 +132,9 @@ it('applies allocation hints only to PRs and retains other callers and full refe default: true }) } - for (const name of ['build', 'prepare-native-cache']) { - expect(workflow.jobs[name].if).toBe( - "inputs.test_files == '' || github.event_name != 'pull_request' || inputs.needs_build" - ) - } + expect(workflow.jobs.build.if).toBe( + "inputs.test_files == '' || github.event_name != 'pull_request' || inputs.needs_build" + ) expect(workflow.jobs.e2e.if).toBe("inputs.test_files == ''") const changed = workflow.jobs['changed-e2e'] expect(changed.if).toContain("github.event_name != 'pull_request' || inputs.run_changed_e2e") diff --git a/config/scripts/ci-native-cache.test.mjs b/config/scripts/ci-native-cache.test.mjs index 91c51b7919e..4ec73876617 100644 --- a/config/scripts/ci-native-cache.test.mjs +++ b/config/scripts/ci-native-cache.test.mjs @@ -1,4 +1,4 @@ -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { parse } from 'yaml' @@ -10,6 +10,7 @@ const installerPath = '.github/actions/install-node-dependencies/action.yml' const nativePath = '.github/actions/prepare-native-runtime/action.yml' const installer = parse(readFileSync(installerPath, 'utf8')) const native = parse(readFileSync(nativePath, 'utf8')) +const e2e = parse(readFileSync('.github/workflows/e2e.yml', 'utf8')) const identity = native.runs.steps.find((step) => step.id === 'native-cache-scope') const inputs = [...identity.env.NATIVE_SOURCE_HASH.matchAll(/'([^']+)'/g)].map((match) => match[1]) const paths = [ @@ -176,4 +177,138 @@ describe('CI native cache ownership', () => { } } ) + + it('prepares both native ABIs in the E2E build before releasing its consumers', () => { + expect(e2e.jobs['prepare-native-cache']).toBeUndefined() + const { steps } = e2e.jobs.build + const install = steps.find((step) => step.id === 'deps') + const save = steps.find((step) => step.name === 'Save compiled Node native modules') + const build = steps.find((step) => step.name === 'Build E2E outputs') + const cliWait = steps.find((step) => step.wait === 'e2e-cli') + const electron = steps.find((step) => step.id === 'electron-native-runtime') + const upload = steps.find((step) => step.name === 'Upload E2E build output') + const uploadWait = steps.find((step) => step.wait === upload.id) + expect(install.with).toMatchObject({ + 'native-runtime': 'node', + 'persist-native-cache': 'false', + 'cache-electron-package': 'true' + }) + expect(save.if).toContain("steps.deps.outputs.native-cache-key != ''") + expect(save.if).toContain("steps.deps.outputs.native-cache-hit != 'true'") + expect(save.with).toEqual({ + key: '${{ steps.deps.outputs.native-cache-key }}', + path: '${{ steps.deps.outputs.native-cache-path }}' + }) + const ordered = [install, save, build, cliWait, upload, electron, uploadWait] + expect(ordered.map((step) => steps.indexOf(step))).toEqual( + ordered.map((step) => steps.indexOf(step)).sort((left, right) => left - right) + ) + expect(upload.background).toBe(true) + expect(upload.if).toBeUndefined() + expect(electron.background).toBeUndefined() + expect(electron.uses).toBe('./.github/actions/prepare-native-runtime') + expect(electron.with).toEqual({ + 'native-runtime': 'electron', + 'node-version': '${{ steps.deps.outputs.node-version }}' + }) + expect(uploadWait).toEqual({ wait: upload.id }) + for (const name of ['e2e', 'changed-e2e', 'orcad-serve-mode-switch', 'orcad-ssh-docker']) { + expect(e2e.jobs[name].needs, name).toEqual(['build']) + expect( + e2e.jobs[name].steps.find( + (step) => step.uses === './.github/actions/install-node-dependencies' + ).with['native-runtime'], + name + ).toBe('electron') + } + }) + + it.skipIf(process.platform === 'win32').each([false, true])( + 'detects shared native action availability=%s for older dispatch refs', + (available) => { + const directory = mkdtempSync(join(tmpdir(), 'orca-e2e-native-action-')) + try { + const output = join(directory, 'output') + if (available) { + mkdirSync(join(directory, '.github/actions/prepare-native-runtime'), { recursive: true }) + writeFileSync(join(directory, nativePath), '') + } + const step = e2e.jobs.build.steps.find( + (candidate) => candidate.id === 'native-runtime-action' + ) + const execution = runProcessSync({ + program: 'bash', + args: ['-e', '-o', 'pipefail', '-c', step.run], + cwd: directory, + env: { ...process.env, GITHUB_OUTPUT: output }, + timeoutMs: 10_000 + }) + expect(execution.code, execution.stderr).toBe(0) + expect(readFileSync(output, 'utf8')).toBe(`available=${available}\n`) + const fallback = e2e.jobs.build.steps.find( + (candidate) => candidate.name === 'Prepare Electron natives for older refs' + ) + expect(fallback.if).toBe( + "steps.native-runtime-action.outputs.available != 'true' || steps.deps.outputs.node-version == ''" + ) + expect(fallback.uses).toBe('./.github/actions/install-node-dependencies') + expect(fallback.with['native-runtime']).toBe('electron') + } finally { + rmSync(directory, { recursive: true, force: true }) + } + } + ) + + it.skipIf(process.platform === 'win32').each([ + ['true', 'build-essential python3', '', 0, false], + ['false', 'build-essential python3', '', 0, true], + ['true', 'build-essential python3 xvfb', '', 0, true], + ['false', 'build-essential python3', 'update', 7, true], + ['false', 'build-essential python3', 'install', 7, true] + ])( + 'tools=%s packages=%s failure=%s retains bounded setup', + (present, packages, failure, status, installs) => { + const directory = mkdtempSync(join(tmpdir(), 'orca-e2e-native-tools-')) + try { + const log = join(directory, 'commands') + writeFileSync(log, '') + const step = e2e.jobs.build.steps.find( + (candidate) => candidate.name === 'Install native build tools' + ) + const execution = runProcessSync({ + program: 'bash', + args: [ + '-e', + '-o', + 'pipefail', + '-c', + ` + command() { [ "$TOOLS_PRESENT" = true ]; } + sudo() { printf 'sudo %s\\n' "$*" >> "$COMMAND_LOG"; cat >/dev/null; } + timeout() { + printf 'timeout %s\\n' "$*" >> "$COMMAND_LOG" + if [ "$4" = "$FAIL_OPERATION" ]; then return 7; fi + } + ${step.run}` + ], + env: { + ...process.env, + TOOLS_PRESENT: present, + ORCA_E2E_APT_PACKAGES: packages, + FAIL_OPERATION: failure, + COMMAND_LOG: log + }, + timeoutMs: 10_000 + }) + expect(execution.code, execution.stderr).toBe(status) + const commands = readFileSync(log, 'utf8') + expect(commands.includes('timeout 120 sudo apt-get update')).toBe(installs) + expect(commands.includes(`timeout 300 sudo apt-get install -y ${packages}`)).toBe( + installs && failure !== 'update' + ) + } finally { + rmSync(directory, { recursive: true, force: true }) + } + } + ) }) diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index c0df38efda9..45f16bf2035 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -1,11724 +1,12327 @@ { "unit": { - "runId": "38086200856", + "runId": "38646706136", "runAttempt": "1", - "sourceSha": "311827966ba81ad0e2b40bf5b98a2a5f1b6166eb", + "sourceSha": "8d7f46198b99432efd104ac7803174173b367c66", "nodeVersion": "24.21.0", - "bunVersion": "1.4.2", + "bunVersion": "1.4.3", "metric": "module-duration-v1", "overheadMs": 0, "timings": { - "config/scripts/adhoc-build-pinned-commit.test.mjs": 226, - "config/scripts/adhoc-build-version.test.mjs": 95, - "config/scripts/agent-state-rules-bundle.test.mjs": 327, - "config/scripts/app-store-performance-plugin.test.mjs": 1010, - "config/scripts/assert-github-release-is-draft.test.mjs": 352, - "config/scripts/audit-localization-coverage.test.mjs": 270, - "config/scripts/benchmark-artifact-comparison.test.mjs": 171, - "config/scripts/benchmark-sample-summary.test.mjs": 57, - "config/scripts/build-linux-local.test.mjs": 56, - "config/scripts/build-mac-local.test.mjs": 57, - "config/scripts/build-mobile-web-app-bundle.test.mjs": 419, - "config/scripts/build-native-for-platform.test.mjs": 52, - "config/scripts/build-orcad-prebuilds.test.mjs": 105, - "config/scripts/build-orcad-template.test.mjs": 145, - "config/scripts/build-windows-cli-launcher.test.mjs": 117, - "config/scripts/casting-code-quality.test.mjs": 854, - "config/scripts/check-changed-code-quality.test.mjs": 3252, - "config/scripts/check-daemon-protocol-crossing.test.mjs": 194, - "config/scripts/check-max-lines-ratchet.test.mjs": 658, - "config/scripts/check-node-runtime-pin.test.mjs": 696, - "config/scripts/check-focus-read-ratchet.test.mjs": 86, - "config/scripts/check-process-host-imports.test.mjs": 301, - "config/scripts/check-react-doctor-changed.test.mjs": 229, - "config/scripts/check-readme-local-links.test.mjs": 414, - "config/scripts/check-reliability-gates.test.mjs": 216, - "config/scripts/check-root-directory-entries.test.mjs": 790, - "config/scripts/check-runtime-electron-ratchet-graph.test.mjs": 163, - "config/scripts/check-runtime-electron-ratchet.test.mjs": 3736, - "config/scripts/check-runtime-launcher-protocol-ratchet.test.mjs": 185, - "config/scripts/check-terminal-perf-report-budgets.test.mjs": 1481, - "config/scripts/check-ts-nocheck-ratchet.test.mjs": 68, - "config/scripts/ci-closed-pr-caches.test.mjs": 123, - "config/scripts/ci-closed-pr-cancellation.test.mjs": 128, - "config/scripts/ci-dependency-download-cache.test.mjs": 557, - "config/scripts/ci-e2e-failure-summary.test.mjs": 56, - "config/scripts/ci-e2e-job-selection.test.mjs": 326, - "config/scripts/ci-e2e-shard-selection.test.mjs": 2569, - "config/scripts/ci-native-cache.test.mjs": 267, - "config/scripts/ci-native-toolchain.test.mjs": 118, - "config/scripts/ci-pnpm-store-mode.test.mjs": 550, - "config/scripts/ci-pnpm-verification-cache.test.mjs": 331, - "config/scripts/ci-runner-metrics.test.mjs": 65, - "config/scripts/ci-shard-assignment.test.mjs": 576, - "config/scripts/ci-unit-dependency-graph.test.mjs": 340, - "config/scripts/ci-unit-exclusion-coverage.test.mjs": 936, - "config/scripts/ci-unit-import-resolution-budget.test.mjs": 270, - "config/scripts/ci-unit-plan.test.mjs": 3110, - "config/scripts/ci-unit-selection-review.test.mjs": 43, - "config/scripts/ci-unit-selection.test.mjs": 276, - "config/scripts/ci-unit-sequencer.test.mjs": 318, - "config/scripts/ci-unit-timing-reporter.test.mjs": 268, - "config/scripts/cloud-security-history-workflow.test.mjs": 259, - "config/scripts/codex-primary-home-tripwire.test.ts": 359, - "config/scripts/computer-use-smoke.test.mjs": 491, - "config/scripts/counterbalanced-benchmark-schedule.test.mjs": 57, - "config/scripts/create-draft-release.test.mjs": 71, - "config/scripts/daemon-protocol-facts.test.mjs": 57, - "config/scripts/daily-build-version.test.mjs": 90, - "config/scripts/dev-channel-base-version.test.mjs": 51, - "config/scripts/dev-cli-terminal-wrapper.test.mjs": 57, - "config/scripts/dev-electron-bundle-cache.test.ts": 49, - "config/scripts/dev-electron-bundle-identity.test.ts": 59, - "config/scripts/e2e-cli-artifact-capability.test.mjs": 262, - "config/scripts/electron-builder-config.test.mjs": 906, - "config/scripts/electron-builder-mac-channel-config.test.mjs": 396, - "config/scripts/electron-builder-native-rebuild.test.mjs": 57, - "config/scripts/electron-builder-runtime-resources.test.mjs": 540, - "config/scripts/electron-builder-speech-config.test.mjs": 322, - "config/scripts/electron-runtime-floor.test.ts": 49, - "config/scripts/electron-vite-output-contract.test.ts": 741, - "config/scripts/ensure-native-runtime-job-ownership.test.mjs": 73, - "config/scripts/ensure-native-runtime.test.mjs": 783, - "config/scripts/ephemeral-vm-runtime-store-cross-version.test.ts": 42, - "config/scripts/foreign-sqlite-reader-worker-smoke.test.mjs": 461, - "config/scripts/generate-bundled-skill-guides.test.mjs": 367, - "config/scripts/generate-monaco-associations.test.mjs": 344, - "config/scripts/generate-runtime-required-english-catalog.test.mjs": 432, - "config/scripts/generate-skill-bundle-manifest.test.mjs": 537, - "config/scripts/generate-terminal-perf-html-report.test.mjs": 107, + "config/scripts/adhoc-build-pinned-commit.test.mjs": 250, + "config/scripts/adhoc-build-version.test.mjs": 104, + "config/scripts/agent-state-rules-bundle.test.mjs": 272, + "config/scripts/anti-slop-scope-regression.test.mjs": 2281, + "config/scripts/app-store-performance-plugin.test.mjs": 855, + "config/scripts/appimage-node-runtime.test.mjs": 114, + "config/scripts/assert-github-release-is-draft.test.mjs": 208, + "config/scripts/audit-localization-coverage.test.mjs": 115, + "config/scripts/benchmark-artifact-comparison.test.mjs": 178, + "config/scripts/benchmark-sample-summary.test.mjs": 38, + "config/scripts/build-linux-local.test.mjs": 46, + "config/scripts/build-mac-local.test.mjs": 38, + "config/scripts/build-mobile-web-app-bundle.test.mjs": 430, + "config/scripts/build-native-for-platform.test.mjs": 54, + "config/scripts/build-orcad-prebuilds.test.mjs": 144, + "config/scripts/build-orcad-template.test.mjs": 180, + "config/scripts/build-wayland-clipboard.test.mjs": 351, + "config/scripts/build-windows-cli-launcher.test.mjs": 97, + "config/scripts/bump-android-apk-download-version.test.mjs": 42, + "config/scripts/casting-code-quality.test.mjs": 824, + "config/scripts/check-changed-code-quality.test.mjs": 2564, + "config/scripts/check-compat-boundary-imports.test.mjs": 124, + "config/scripts/check-daemon-protocol-crossing.test.mjs": 132, + "config/scripts/check-focus-read-ratchet.test.mjs": 58, + "config/scripts/check-import-boundaries.test.mjs": 230, + "config/scripts/check-layout-writer-ratchets.test.mjs": 143, + "config/scripts/check-main-sync-spawn-ratchet.test.mjs": 279, + "config/scripts/check-max-lines-ratchet.test.mjs": 726, + "config/scripts/check-node-runtime-pin.test.mjs": 670, + "config/scripts/check-process-host-imports.test.mjs": 165, + "config/scripts/check-react-doctor-changed.test.mjs": 218, + "config/scripts/check-readme-local-links.test.mjs": 401, + "config/scripts/check-reliability-gates.test.mjs": 166, + "config/scripts/check-root-directory-entries.test.mjs": 857, + "config/scripts/check-runtime-electron-ratchet-graph.test.mjs": 105, + "config/scripts/check-runtime-electron-ratchet.test.mjs": 4608, + "config/scripts/check-runtime-launcher-protocol-ratchet.test.mjs": 181, + "config/scripts/check-terminal-perf-report-budgets.test.mjs": 1575, + "config/scripts/check-ts-nocheck-ratchet.test.mjs": 42, + "config/scripts/ci-closed-pr-caches.test.mjs": 47, + "config/scripts/ci-closed-pr-cancellation.test.mjs": 75, + "config/scripts/ci-dependency-download-cache.test.mjs": 469, + "config/scripts/ci-e2e-failure-summary.test.mjs": 45, + "config/scripts/ci-e2e-job-selection.test.mjs": 311, + "config/scripts/ci-e2e-shard-selection.test.mjs": 2628, + "config/scripts/ci-native-cache.test.mjs": 251, + "config/scripts/ci-native-toolchain.test.mjs": 105, + "config/scripts/ci-pnpm-store-mode.test.mjs": 498, + "config/scripts/ci-pnpm-verification-cache.test.mjs": 274, + "config/scripts/ci-runner-metrics.test.mjs": 54, + "config/scripts/ci-shard-assignment.test.mjs": 505, + "config/scripts/ci-unit-dependency-graph.test.mjs": 199, + "config/scripts/ci-unit-exclusion-coverage.test.mjs": 1039, + "config/scripts/ci-unit-import-resolution-budget.test.mjs": 112, + "config/scripts/ci-unit-plan.test.mjs": 2321, + "config/scripts/ci-unit-selection-review.test.mjs": 37, + "config/scripts/ci-unit-selection.test.mjs": 139, + "config/scripts/ci-unit-sequencer.test.mjs": 304, + "config/scripts/ci-unit-timing-reporter.test.mjs": 151, + "config/scripts/cloud-security-history-workflow.test.mjs": 353, + "config/scripts/codex-primary-home-tripwire.test.ts": 332, + "config/scripts/computer-use-smoke.test.mjs": 473, + "config/scripts/counterbalanced-benchmark-schedule.test.mjs": 44, + "config/scripts/create-draft-release.test.mjs": 59, + "config/scripts/daemon-protocol-facts.test.mjs": 54, + "config/scripts/daily-build-version.test.mjs": 58, + "config/scripts/dev-channel-base-version.test.mjs": 32, + "config/scripts/dev-channel-orcad-template-workflow.test.mjs": 131, + "config/scripts/dev-cli-terminal-wrapper.test.mjs": 43, + "config/scripts/dev-electron-bundle-cache.test.ts": 45, + "config/scripts/dev-electron-bundle-identity.test.ts": 39, + "config/scripts/e2e-cli-artifact-capability.test.mjs": 188, + "config/scripts/electron-builder-config.test.mjs": 753, + "config/scripts/electron-builder-mac-channel-config.test.mjs": 251, + "config/scripts/electron-builder-native-rebuild.test.mjs": 41, + "config/scripts/electron-builder-runtime-resources.test.mjs": 2537, + "config/scripts/electron-builder-speech-config.test.mjs": 167, + "config/scripts/electron-runtime-floor.test.ts": 31, + "config/scripts/electron-vite-output-contract.test.ts": 701, + "config/scripts/ensure-native-runtime-job-ownership.test.mjs": 76, + "config/scripts/ensure-native-runtime.test.mjs": 780, + "config/scripts/ephemeral-vm-runtime-store-cross-version.test.ts": 33, + "config/scripts/foreign-sqlite-reader-worker-smoke.test.mjs": 427, + "config/scripts/generate-bundled-skill-guides.test.mjs": 458, + "config/scripts/generate-monaco-associations.test.mjs": 221, + "config/scripts/generate-runtime-required-english-catalog.test.mjs": 301, + "config/scripts/generate-skill-bundle-manifest.test.mjs": 331, + "config/scripts/generate-terminal-perf-html-report.test.mjs": 79, "config/scripts/generated-artifact-line-ending-pin.test.mjs": 54, - "config/scripts/git-pull-request-diff-base.test.mjs": 52, - "config/scripts/github-opened-issue-repository.test.ts": 1032, - "config/scripts/github-pr-assignee-repository.test.ts": 560, - "config/scripts/hang-watchdog-process-metrics.test.mjs": 102, - "config/scripts/happy-dom-mutation-observer-retention.test.ts": 206, - "config/scripts/happy-dom-offscreen-canvas.test.ts": 195, - "config/scripts/headless-detector-compiler-cache.test.mjs": 1063, - "config/scripts/headless-serve-shutdown-matrix.test.mjs": 84, - "config/scripts/hourly-build-version.test.mjs": 82, - "config/scripts/hourly-preflight-workflow.test.mjs": 182, - "config/scripts/idle-cpu-process-sampling.test.mjs": 64, - "config/scripts/install-electron-package-binary.test.mjs": 2694, - "config/scripts/install-node-dependencies-action.test.mjs": 336, - "config/scripts/lag-probe-failures.test.mjs": 60, - "config/scripts/latest-stable-release.test.mjs": 53, - "config/scripts/linear-sdk-runtime.test.mjs": 985, - "config/scripts/lint-staged-worktree-backup.test.mjs": 253, - "config/scripts/linux-package-fixture-cache.test.mjs": 204, - "config/scripts/live-freeze-bounded-history.test.mjs": 58, - "config/scripts/live-remote-bulk-open-freeze-metrics.test.mjs": 86, - "config/scripts/live-remote-freeze-rpc.test.mjs": 64, - "config/scripts/live-remote-status-watchdog.test.mjs": 284, - "config/scripts/locale-brand-prefilter.test.mjs": 215, - "config/scripts/locale-count-fragment-separator.test.mjs": 99, - "config/scripts/locale-generic-ui-terms.test.mjs": 103, - "config/scripts/locale-ko-frozen-terminal-search-keywords.test.mjs": 126, - "config/scripts/locale-ko-key-overrides.test.mjs": 68, - "config/scripts/locale-repair-catalog-missing-leaves.test.mjs": 103, - "config/scripts/locale-translation-policy-ko-round5.test.mjs": 118, - "config/scripts/locale-translation-policy.es-pr.test.mjs": 97, - "config/scripts/locale-translation-policy.es-round5.test.mjs": 78, - "config/scripts/locale-translation-policy.ja-relocalization.test.mjs": 119, - "config/scripts/locale-translation-policy.ja-round5.test.mjs": 112, + "config/scripts/git-pull-request-diff-base.test.mjs": 31, + "config/scripts/github-opened-issue-repository.test.ts": 1347, + "config/scripts/github-pr-assignee-repository.test.ts": 711, + "config/scripts/hang-watchdog-process-metrics.test.mjs": 69, + "config/scripts/happy-dom-mutation-observer-retention.test.ts": 199, + "config/scripts/happy-dom-offscreen-canvas.test.ts": 204, + "config/scripts/headless-detector-compiler-cache.test.mjs": 876, + "config/scripts/headless-serve-shutdown-matrix.test.mjs": 77, + "config/scripts/hourly-build-version.test.mjs": 63, + "config/scripts/hourly-preflight-workflow.test.mjs": 118, + "config/scripts/idle-cpu-process-sampling.test.mjs": 42, + "config/scripts/ime-enter-guard-plugin.test.mjs": 2898, + "config/scripts/install-electron-package-binary.test.mjs": 2480, + "config/scripts/install-node-dependencies-action.test.mjs": 410, + "config/scripts/lag-probe-failures.test.mjs": 54, + "config/scripts/latest-stable-release.test.mjs": 31, + "config/scripts/linear-sdk-runtime.test.mjs": 977, + "config/scripts/lint-staged-worktree-backup.test.mjs": 269, + "config/scripts/linux-package-fixture-cache.test.mjs": 206, + "config/scripts/live-freeze-bounded-history.test.mjs": 41, + "config/scripts/live-remote-bulk-open-freeze-metrics.test.mjs": 74, + "config/scripts/live-remote-freeze-rpc.test.mjs": 35, + "config/scripts/live-remote-status-watchdog.test.mjs": 290, + "config/scripts/locale-brand-prefilter.test.mjs": 245, + "config/scripts/locale-count-fragment-separator.test.mjs": 78, + "config/scripts/locale-generic-ui-terms.test.mjs": 83, + "config/scripts/locale-ko-frozen-terminal-search-keywords.test.mjs": 127, + "config/scripts/locale-repair-catalog-missing-leaves.test.mjs": 77, + "config/scripts/locale-translation-policy-ko-round5.test.mjs": 107, + "config/scripts/locale-translation-policy.es-pr.test.mjs": 77, + "config/scripts/locale-translation-policy.es-round5.test.mjs": 87, + "config/scripts/locale-translation-policy.ja-relocalization.test.mjs": 101, + "config/scripts/locale-translation-policy.ja-round5.test.mjs": 90, "config/scripts/locale-translation-policy.test.mjs": 119, - "config/scripts/locale-translation-policy.zh-round5.test.mjs": 191, - "config/scripts/locale-translation-policy.zh-status-bar-usage.test.mjs": 105, - "config/scripts/localization-extraction-call-syntax.test.mjs": 292, - "config/scripts/localization-extraction-change-scope.test.mjs": 308, - "config/scripts/mac-build-compatibility.test.mjs": 47, - "config/scripts/macos-computer-helper-owner-loss-group-recovery.test.mjs": 64, - "config/scripts/macos-computer-helper-owner-loss-processes.test.mjs": 65, - "config/scripts/macos-tcc-prompt-localization.test.mjs": 287, - "config/scripts/managed-data-account-runtime.test.ts": 1001, - "config/scripts/merge-orcad-prebuilds.test.mjs": 82, - "config/scripts/mobile-entry-static-closure-work-budget.test.mjs": 127, - "config/scripts/mobile-pairing-qrcode-import-plugin.test.mjs": 374, - "config/scripts/mobile-process-host-tooling.test.mjs": 978, - "config/scripts/mobile-release-check-scope.test.mjs": 701, - "config/scripts/mobile-test-change-scope.test.mjs": 126, - "config/scripts/mobile-web-app-agent-history-render.test.mjs": 363, - "config/scripts/mobile-web-app-browser-pane-render.test.mjs": 365, - "config/scripts/mobile-web-app-browser-pane-text-inputs.test.mjs": 374, - "config/scripts/mobile-web-app-clipboard-image-resize-render.test.mjs": 368, - "config/scripts/mobile-web-app-drawer-render.test.mjs": 365, - "config/scripts/mobile-web-app-editable-host-font-size.test.mjs": 291, - "config/scripts/mobile-web-app-external-link-seam.test.mjs": 247, - "config/scripts/mobile-web-app-external-navigation-grant.test.mjs": 68, - "config/scripts/mobile-web-app-files-external-links.test.mjs": 228, - "config/scripts/mobile-web-app-files-render.test.mjs": 356, - "config/scripts/mobile-web-app-frame-budget-sweep.test.ts": 322, - "config/scripts/mobile-web-app-handoff-grants-render.test.mjs": 395, - "config/scripts/mobile-web-app-haptics-seam.test.mjs": 295, - "config/scripts/mobile-web-app-held-press-render.test.mjs": 367, - "config/scripts/mobile-web-app-html-preview-render.test.mjs": 517, - "config/scripts/mobile-web-app-media-preview-render.test.mjs": 360, - "config/scripts/mobile-web-app-mermaid-render.test.mjs": 354, - "config/scripts/mobile-web-app-native-chat-visual-links.test.mjs": 365, - "config/scripts/mobile-web-app-native-parity-render.test.mjs": 399, - "config/scripts/mobile-web-app-page-closure-families.test.mjs": 74, - "config/scripts/mobile-web-app-page-grant-call-sites.test.mjs": 302, - "config/scripts/mobile-web-app-push-subsystem-closure.test.mjs": 64, - "config/scripts/mobile-web-app-render.test.mjs": 403, - "config/scripts/mobile-web-app-rich-markdown-render.test.mjs": 721, - "config/scripts/mobile-web-app-route-manifest.test.mjs": 164, - "config/scripts/mobile-web-app-route-snapshot.test.mjs": 121, - "config/scripts/mobile-web-app-safe-area-render.test.mjs": 365, - "config/scripts/mobile-web-app-screencast-lane-grant.test.mjs": 69, - "config/scripts/mobile-web-app-session-dictation-capture.test.mjs": 248, - "config/scripts/mobile-web-app-session-external-links.test.mjs": 197, - "config/scripts/mobile-web-app-session-media-picker.test.mjs": 270, - "config/scripts/mobile-web-app-session-render.test.mjs": 372, - "config/scripts/mobile-web-app-session-terminal-closure.test.mjs": 231, - "config/scripts/mobile-web-app-session-terminal-init.test.mjs": 352, - "config/scripts/mobile-web-app-session-text-inputs.test.mjs": 237, - "config/scripts/mobile-web-app-session-webview-consumers.test.mjs": 67, - "config/scripts/mobile-web-app-source-control-external-links.test.mjs": 215, - "config/scripts/mobile-web-app-source-control-keyboard.test.mjs": 56, - "config/scripts/mobile-web-app-source-control-render.test.mjs": 325, - "config/scripts/mobile-web-app-source-control-text-inputs.test.mjs": 334, - "config/scripts/mobile-web-app-stack-transition-render.test.mjs": 357, - "config/scripts/mobile-web-app-tasks-external-links.test.mjs": 251, - "config/scripts/mobile-web-app-tasks-render.test.mjs": 369, - "config/scripts/mobile-web-app-terminal-live-input-render.test.mjs": 332, - "config/scripts/mobile-web-app-terminal-render-fixture.test.mjs": 380, - "config/scripts/mobile-web-app-terminal-render.test.mjs": 352, - "config/scripts/mobile-web-app-working-spinner-render.test.mjs": 382, - "config/scripts/mobile-web-bundle-manifest.test.mjs": 224, - "config/scripts/mobile-web-bundle-serializer-parity.test.mjs": 237, - "config/scripts/mobile-web-page-routes.test.mjs": 250, - "config/scripts/mobile-web-source-line-endings.test.mjs": 261, - "config/scripts/mobile-web-terminal-engine-closure.test.mjs": 60, - "config/scripts/moved-code-normalization-budget.test.mjs": 125, - "config/scripts/msbuild-file-tracking.test.mjs": 44, - "config/scripts/native-chat-compaction-pagination.test.mjs": 446, - "config/scripts/node-old-space-limit.test.mjs": 50, - "config/scripts/node-server-change-scope.test.mjs": 2687, + "config/scripts/locale-translation-policy.zh-round5.test.mjs": 83, + "config/scripts/locale-translation-policy.zh-status-bar-usage.test.mjs": 73, + "config/scripts/localization-extraction-call-syntax.test.mjs": 347, + "config/scripts/localization-extraction-change-scope.test.mjs": 252, + "config/scripts/mac-build-compatibility.test.mjs": 31, + "config/scripts/macos-computer-helper-owner-loss-group-recovery.test.mjs": 40, + "config/scripts/macos-computer-helper-owner-loss-processes.test.mjs": 40, + "config/scripts/macos-folder-usage-descriptions.test.mjs": 117, + "config/scripts/macos-tcc-prompt-localization.test.mjs": 250, + "config/scripts/macos-terminal-host-bundle.test.mjs": 153, + "config/scripts/managed-data-account-runtime.test.ts": 925, + "config/scripts/merge-orcad-prebuilds.test.mjs": 66, + "config/scripts/mobile-entry-static-closure-work-budget.test.mjs": 90, + "config/scripts/mobile-pairing-qrcode-import-plugin.test.mjs": 367, + "config/scripts/mobile-process-host-tooling.test.mjs": 751, + "config/scripts/mobile-release-check-scope.test.mjs": 775, + "config/scripts/mobile-test-change-scope.test.mjs": 130, + "config/scripts/mobile-typecheck-cache.test.mjs": 70, + "config/scripts/mobile-web-app-agent-history-render.test.mjs": 453, + "config/scripts/mobile-web-app-browser-pane-render.test.mjs": 191, + "config/scripts/mobile-web-app-clipboard-image-resize-render.test.mjs": 211, + "config/scripts/mobile-web-app-drawer-render.test.mjs": 218, + "config/scripts/mobile-web-app-editable-host-font-size.test.mjs": 150, + "config/scripts/mobile-web-app-external-link-seam.test.mjs": 135, + "config/scripts/mobile-web-app-external-navigation-grant.test.mjs": 47, + "config/scripts/mobile-web-app-files-external-links.test.mjs": 112, + "config/scripts/mobile-web-app-files-render.test.mjs": 240, + "config/scripts/mobile-web-app-frame-budget-sweep.test.ts": 201, + "config/scripts/mobile-web-app-handoff-grants-render.test.mjs": 475, + "config/scripts/mobile-web-app-haptics-seam.test.mjs": 131, + "config/scripts/mobile-web-app-held-press-render.test.mjs": 257, + "config/scripts/mobile-web-app-html-preview-render.test.mjs": 291, + "config/scripts/mobile-web-app-media-preview-render.test.mjs": 205, + "config/scripts/mobile-web-app-mermaid-render.test.mjs": 197, + "config/scripts/mobile-web-app-module-closure-entrypoints.test.mjs": 79, + "config/scripts/mobile-web-app-module-entry-resolution.test.mjs": 78, + "config/scripts/mobile-web-app-native-chat-visual-links.test.mjs": 515, + "config/scripts/mobile-web-app-native-parity-render.test.mjs": 214, + "config/scripts/mobile-web-app-page-closure-families.test.mjs": 63, + "config/scripts/mobile-web-app-page-grant-call-sites.test.mjs": 127, + "config/scripts/mobile-web-app-push-subsystem-closure.test.mjs": 44, + "config/scripts/mobile-web-app-render.test.mjs": 621, + "config/scripts/mobile-web-app-rich-markdown-render.test.mjs": 620, + "config/scripts/mobile-web-app-route-manifest.test.mjs": 182, + "config/scripts/mobile-web-app-route-snapshot.test.mjs": 106, + "config/scripts/mobile-web-app-safe-area-render.test.mjs": 616, + "config/scripts/mobile-web-app-screencast-lane-grant.test.mjs": 37, + "config/scripts/mobile-web-app-session-dictation-capture.test.mjs": 207, + "config/scripts/mobile-web-app-session-external-links.test.mjs": 102, + "config/scripts/mobile-web-app-session-media-picker.test.mjs": 141, + "config/scripts/mobile-web-app-session-render.test.mjs": 188, + "config/scripts/mobile-web-app-session-terminal-closure.test.mjs": 129, + "config/scripts/mobile-web-app-session-terminal-init.test.mjs": 205, + "config/scripts/mobile-web-app-session-webview-consumers.test.mjs": 43, + "config/scripts/mobile-web-app-source-control-external-links.test.mjs": 133, + "config/scripts/mobile-web-app-source-control-keyboard.test.mjs": 49, + "config/scripts/mobile-web-app-source-control-render.test.mjs": 233, + "config/scripts/mobile-web-app-source-control-text-inputs.test.mjs": 197, + "config/scripts/mobile-web-app-stack-transition-render.test.mjs": 214, + "config/scripts/mobile-web-app-tasks-external-links.test.mjs": 121, + "config/scripts/mobile-web-app-tasks-render.test.mjs": 207, + "config/scripts/mobile-web-app-terminal-live-input-render.test.mjs": 213, + "config/scripts/mobile-web-app-terminal-render-fixture.test.mjs": 445, + "config/scripts/mobile-web-app-terminal-render.test.mjs": 241, + "config/scripts/mobile-web-app-text-input-discovery.test.mjs": 89, + "config/scripts/mobile-web-app-working-spinner-render.test.mjs": 572, + "config/scripts/mobile-web-bundle-manifest.test.mjs": 275, + "config/scripts/mobile-web-bundle-serializer-parity.test.mjs": 233, + "config/scripts/mobile-web-page-routes.test.mjs": 240, + "config/scripts/mobile-web-source-line-endings.test.mjs": 243, + "config/scripts/mobile-web-terminal-engine-closure.test.mjs": 40, + "config/scripts/moved-code-normalization-budget.test.mjs": 94, + "config/scripts/msbuild-file-tracking.test.mjs": 33, + "config/scripts/native-chat-compaction-pagination.test.mjs": 456, + "config/scripts/native-chat-design-tokens-plugin.test.mjs": 1531, + "config/scripts/node-old-space-limit.test.mjs": 41, + "config/scripts/node-server-change-scope.test.mjs": 2683, "config/scripts/node-server-concurrency.test.mjs": 173, - "config/scripts/node-server-qualification.test.mjs": 62, - "config/scripts/nsis-process-check.test.mjs": 59, - "config/scripts/orca-dev-bin.test.mjs": 118, - "config/scripts/orcad-artifact-version.test.mjs": 493, - "config/scripts/orcad-daemon-protocol-crossing.test.mjs": 57, - "config/scripts/orcad-e2e-routing.test.mjs": 184, - "config/scripts/orcad-prebuild-slot-contents.test.mjs": 81, - "config/scripts/orcad-terminal-smoke-change-scope.test.mjs": 2562, - "config/scripts/orcad-watcher-package.test.mjs": 107, - "config/scripts/orcad-windows-prebuild-cache.test.mjs": 446, - "config/scripts/orcad-windows-prebuild-workflow.test.mjs": 371, - "config/scripts/orcad-windows-process-tree.test.mjs": 94, - "config/scripts/oxc-cli-invocation.test.mjs": 102, - "config/scripts/oxlint-cli-invocation.test.mjs": 73, - "config/scripts/package-electron-runtime-contract.test.mjs": 169, - "config/scripts/package-linux-formats-appimage.test.mjs": 577, - "config/scripts/package-linux-formats.test.mjs": 315, - "config/scripts/packaged-node-pty-prebuild-prune.test.mjs": 68, - "config/scripts/packaged-orcad-template.test.mjs": 185, - "config/scripts/packaged-process-host.test.mjs": 653, - "config/scripts/packaged-source-map-prune.test.mjs": 69, - "config/scripts/patched-dependencies-frozen-install.test.mjs": 574, - "config/scripts/pdfjs-viewer-assets-stream.test.ts": 317, - "config/scripts/plain-node-entry-guard.test.ts": 680, - "config/scripts/playwright-focused-workflow-selection.test.mjs": 3051, - "config/scripts/pnpm-cli-invocation.test.mjs": 63, - "config/scripts/pr-auxiliary-preflight-admission.test.mjs": 417, - "config/scripts/pr-code-change-scope-cross-version-launch-route.test.mjs": 49, - "config/scripts/pr-code-change-scope-cross-version-send-path.test.mjs": 55, - "config/scripts/pr-code-change-scope-daemon-protocol.test.mjs": 44, - "config/scripts/pr-code-change-scope-process-host.test.mjs": 54, - "config/scripts/pr-code-change-scope.test.mjs": 551, - "config/scripts/pr-codex-unit-only-selection.test.mjs": 45, - "config/scripts/pr-e2e-gate-contract.test.mjs": 264, - "config/scripts/pr-e2e-native-only-routing.test.mjs": 153, - "config/scripts/pr-e2e-pane-close-routing.test.mjs": 61, - "config/scripts/pr-preflight-gates.test.mjs": 203, - "config/scripts/pr-ready-check-gate.test.mjs": 338, - "config/scripts/pr-ready-check-reuse.test.mjs": 171, - "config/scripts/pr-test-loc-summary.test.mjs": 179, - "config/scripts/pr-unit-support-routing.test.mjs": 64, - "config/scripts/pr-workflow-parallelism.test.mjs": 151, - "config/scripts/process-failure-message.test.mjs": 44, - "config/scripts/process-host-build-dist.test.mjs": 32253, - "config/scripts/process-host-dev-rebuild.test.ts": 9293, - "config/scripts/profile-state-worker-smoke.test.mjs": 2753, - "config/scripts/project-renderer-web-client.test.mjs": 623, - "config/scripts/pty-transcript-secret-scan.test.mjs": 168, - "config/scripts/publish-complete-draft-releases.test.mjs": 151, - "config/scripts/pullfrog-review-scope.test.mjs": 69, - "config/scripts/quadratic-buffer-concat-plugin.test.mjs": 1652, - "config/scripts/readme-downloads-badge.test.mjs": 37, - "config/scripts/rebuild-native-deps-node-pty.test.mjs": 1261, - "config/scripts/rebuild-native-deps-windows-process-tree.test.mjs": 45, - "config/scripts/rebuild-native-deps.test.mjs": 1263, - "config/scripts/reclaim-dev-electron-bundles.test.ts": 64, - "config/scripts/regenerate-xterm-patches.test.mjs": 249, - "config/scripts/windows-process-tree-prepared-addon.test.mjs": 168, - "config/scripts/release-cut-token-permissions.test.mjs": 199, - "config/scripts/release-javascript-artifact.test.mjs": 253, - "config/scripts/release-javascript-parity.test.mjs": 98, - "config/scripts/release-mac-build-workflow-dispatch.test.mjs": 56, - "config/scripts/release-policy.test.mjs": 62, - "config/scripts/release-rc-history.test.mjs": 147, - "config/scripts/release-tag-pattern-census.test.mjs": 97, - "config/scripts/renderer-boot-graph.test.mjs": 88, - "config/scripts/renderer-scrollbar-style-plugin.test.mjs": 2564, - "config/scripts/replace-cached-nsis-elevate.test.mjs": 1852, - "config/scripts/resolve-7za-path.test.mjs": 1333, - "config/scripts/run-codex-real-account-validation.test.ts": 3394, - "config/scripts/run-internal-dev-setup.test.mjs": 59, - "config/scripts/run-linux-packaged-node-pty-floor-smoke.test.mjs": 58, - "config/scripts/run-terminal-scale-perf-report-gate.test.mjs": 77, - "config/scripts/run-typecheck-projects-in-parallel.test.mjs": 59, - "config/scripts/scan-release-artifacts-antivirus.test.mjs": 68, - "config/scripts/script-entry-detection.test.mjs": 91, - "config/scripts/script-module-dependencies.test.mjs": 63, - "config/scripts/session-scanner-service-smoke.test.mjs": 549, - "config/scripts/shared-electron-dist-cache.test.ts": 105, - "config/scripts/shebang-script-line-ending-pin.test.mjs": 103, - "config/scripts/skill-description-length.test.mjs": 107, - "config/scripts/skill-recipe-shell.test.mjs": 103, - "config/scripts/sort-comparator-performance-plugin.test.mjs": 283, - "config/scripts/space-sharing-copy.test.ts": 96, - "config/scripts/ssh-browser-e2e-routing.test.mjs": 176, - "config/scripts/stable-release-tag-selection-budget.test.mjs": 149, - "config/scripts/static-appimage-package-contract.test.mjs": 83, - "config/scripts/static-module-specifiers.test.mjs": 277, - "config/scripts/structured-agent-session-start-failure-row-render.test.mjs": 2675, - "config/scripts/summarize-terminal-perf-report.test.mjs": 106, - "config/scripts/telemetry-bundle-constant-patterns.test.mjs": 60, - "config/scripts/terminal-ime-engagement-receipt.test.mjs": 60, - "config/scripts/terminal-partial-escape-native-continuation.test.mjs": 2681, - "config/scripts/terminal-perf-dependency-preparation.test.mjs": 613, - "config/scripts/terminal-perf-report-annotations.test.mjs": 59, - "config/scripts/test-only-source-path.test.mjs": 42, - "config/scripts/trim-windows-icon-source.test.mjs": 234, - "config/scripts/update-node-runtime-pin.test.mjs": 89, - "config/scripts/verify-cli-bin.test.mjs": 106, - "config/scripts/verify-dev-channel-packaging.test.mjs": 285, - "config/scripts/verify-linux-glibc-floor.test.mjs": 124, - "config/scripts/verify-linux-glibc-payloads.test.mjs": 156, - "config/scripts/verify-localization-catalog.test.mjs": 514, - "config/scripts/verify-localization-catalogs.test.mjs": 2042, - "config/scripts/verify-localization-extraction.test.mjs": 49, - "config/scripts/verify-packaged-browser-participation.test.mjs": 61, - "config/scripts/verify-packaged-daemon-entry.test.mjs": 132, - "config/scripts/verify-packaged-mobile-web-bundle.test.mjs": 330, - "config/scripts/verify-packaged-node-pty-job-ownership.test.mjs": 109, - "config/scripts/verify-packaged-orcad-template.test.mjs": 443, - "config/scripts/verify-packaged-plugin-resources.test.mjs": 91, - "config/scripts/verify-release-required-assets.test.mjs": 55, - "config/scripts/verify-skills-cli-runtime.test.mjs": 289, - "config/scripts/verify-windows-inner-signature.test.mjs": 68, - "config/scripts/verify-wsl-e2e-participation.test.mjs": 77, - "config/scripts/vitest-node-runtime-boundary.test.ts": 2060, - "config/scripts/vitest-node-runtime-pool.test.ts": 341, - "config/scripts/vitest-real-agent-home-write-guard.test.ts": 66, - "config/scripts/vitest-runtime-boundary.test.ts": 114, - "config/scripts/vitest-runtime-sequencer.test.mjs": 194, - "config/scripts/vitest-sqlite-runtime-boundary.test.ts": 3901, - "config/scripts/websocket-server-loopback-bind.test.ts": 536, - "config/scripts/win-crash-survival-e2e.test.mjs": 584, - "config/scripts/win32-test-lane-registration.test.mjs": 2948, - "config/scripts/windows-bin-spawn-predicate.test.mjs": 236, - "config/scripts/windows-cmd-shim-spawn-boundary.test.mjs": 1437, - "config/scripts/windows-pe-imports.test.mjs": 48, - "config/scripts/windows-pe-machine.test.mjs": 66, - "config/scripts/windows-process-tree-creation-time.test.mjs": 45, - "config/scripts/windows-process-tree-gyp-path.test.mjs": 39, - "config/scripts/windows-process-tree-gyp-rebuild.test.mjs": 80, - "config/scripts/windows-process-tree-patch-contract.test.mjs": 562, - "config/scripts/windows-pty-table-stress-observer.test.mjs": 97, - "config/scripts/windows-uninstaller-signing.test.mjs": 63, - "config/scripts/workflow-ref-mirror-case-safety.test.mjs": 534, - "config/scripts/workflow-ref-reachability.test.mjs": 662, - "config/scripts/workspace-source-exports.test.mjs": 120, - "config/scripts/wsl-e2e-lane-contract.test.mjs": 166, - "config/scripts/xterm-image-lifecycle-contract.test.mjs": 231, - "config/scripts/xterm-image-memory-contract.test.mjs": 380, - "config/scripts/xterm-image-resize-contract.test.mjs": 73, - "config/scripts/xterm-image-wasm-budget-contract.test.mjs": 479, - "config/scripts/zip-extractor-command.test.mjs": 76, - "src/cli/agent-context.test.ts": 125, - "src/cli/agent-session-search-format.test.ts": 71, - "src/cli/args-repeatable-flags.test.ts": 69, - "src/cli/args.test.ts": 123, - "src/cli/automation-format.test.ts": 102, - "src/cli/automation-owner-conflict-recovery.test.ts": 482, - "src/cli/browser-cookie-credentials-empty-value.test.ts": 206, - "src/cli/browser-storage-empty-value.test.ts": 185, - "src/cli/browser.test.ts": 309, + "config/scripts/node-server-packaged-chat-selection.test.mjs": 146, + "config/scripts/node-server-policy-self-tests.test.mjs": 3143, + "config/scripts/node-server-qualification.test.mjs": 43, + "config/scripts/nsis-process-check.test.mjs": 50, + "config/scripts/orca-dev-bin.test.mjs": 87, + "config/scripts/orcad-artifact-version.test.mjs": 515, + "config/scripts/orcad-daemon-protocol-crossing.test.mjs": 60, + "config/scripts/orcad-e2e-routing.test.mjs": 131, + "config/scripts/orcad-entry-build.test.mjs": 2783, + "config/scripts/orcad-prebuild-slot-contents.test.mjs": 89, + "config/scripts/orcad-terminal-smoke-change-scope.test.mjs": 2306, + "config/scripts/orcad-watcher-package.test.mjs": 91, + "config/scripts/orcad-windows-cli-launcher.test.mjs": 79, + "config/scripts/orcad-windows-prebuild-cache.test.mjs": 392, + "config/scripts/orcad-windows-prebuild-workflow.test.mjs": 368, + "config/scripts/orcad-windows-process-tree.test.mjs": 90, + "config/scripts/orcad-windows-security.test.mjs": 153, + "config/scripts/oxc-cli-invocation.test.mjs": 82, + "config/scripts/oxlint-cli-invocation.test.mjs": 76, + "config/scripts/package-electron-runtime-contract.test.mjs": 172, + "config/scripts/package-linux-formats-appimage.test.mjs": 535, + "config/scripts/package-linux-formats.test.mjs": 253, + "config/scripts/packaged-node-pty-prebuild-prune.test.mjs": 47, + "config/scripts/packaged-orcad-template.test.mjs": 196, + "config/scripts/packaged-process-host.test.mjs": 465, + "config/scripts/packaged-source-map-prune.test.mjs": 75, + "config/scripts/patched-dependencies-frozen-install.test.mjs": 623, + "config/scripts/pdfjs-viewer-assets-stream.test.ts": 275, + "config/scripts/pinned-node-downloads.test.mjs": 116, + "config/scripts/plain-node-entry-guard.test.ts": 710, + "config/scripts/playwright-focused-workflow-selection.test.mjs": 1489, + "config/scripts/pnpm-cli-invocation.test.mjs": 52, + "config/scripts/pr-auxiliary-preflight-admission.test.mjs": 2289, + "config/scripts/pr-code-change-scope-cross-version-launch-route.test.mjs": 35, + "config/scripts/pr-code-change-scope-cross-version-send-path.test.mjs": 52, + "config/scripts/pr-code-change-scope-daemon-protocol.test.mjs": 36, + "config/scripts/pr-code-change-scope-process-host.test.mjs": 43, + "config/scripts/pr-code-change-scope.test.mjs": 583, + "config/scripts/pr-codex-unit-only-selection.test.mjs": 34, + "config/scripts/pr-e2e-gate-contract.test.mjs": 224, + "config/scripts/pr-e2e-layout-oracle-routing.test.mjs": 229, + "config/scripts/pr-e2e-native-only-routing.test.mjs": 124, + "config/scripts/pr-e2e-pane-close-routing.test.mjs": 48, + "config/scripts/pr-preflight-gates.test.mjs": 606, + "config/scripts/pr-ready-check-gate.test.mjs": 378, + "config/scripts/pr-ready-check-reuse.test.mjs": 167, + "config/scripts/pr-test-loc-summary.test.mjs": 159, + "config/scripts/pr-unit-support-routing.test.mjs": 58, + "config/scripts/pr-workflow-parallelism.test.mjs": 183, + "config/scripts/process-failure-message.test.mjs": 27, + "config/scripts/process-host-build-dist.test.mjs": 12388, + "config/scripts/process-host-dev-rebuild.test.ts": 8977, + "config/scripts/profile-state-worker-smoke.test.mjs": 2936, + "config/scripts/project-renderer-web-client.test.mjs": 654, + "config/scripts/pty-transcript-secret-scan.test.mjs": 142, + "config/scripts/publish-complete-draft-releases.test.mjs": 160, + "config/scripts/pullfrog-review-scope.test.mjs": 46, + "config/scripts/quadratic-buffer-concat-plugin.test.mjs": 1492, + "config/scripts/ratchet-baseline.test.mjs": 53, + "config/scripts/readme-downloads-badge.test.mjs": 30, + "config/scripts/rebuild-native-deps-node-pty.test.mjs": 1338, + "config/scripts/rebuild-native-deps-windows-process-tree.test.mjs": 44, + "config/scripts/rebuild-native-deps.test.mjs": 1191, + "config/scripts/reclaim-dev-electron-bundles.test.ts": 168, + "config/scripts/reclaim-electron-dists.test.ts": 273, + "config/scripts/reclaim-release-test-caches.test.ts": 172, + "config/scripts/reclaim-worktree-space.test.mjs": 985, + "config/scripts/regenerate-xterm-patches.test.mjs": 240, + "config/scripts/relay-era-ref-refusal-workflow.test.mjs": 221, + "config/scripts/release-cut-token-permissions.test.mjs": 188, + "config/scripts/release-javascript-artifact.test.mjs": 174, + "config/scripts/release-javascript-parity.test.mjs": 67, + "config/scripts/release-mac-build-workflow-dispatch.test.mjs": 46, + "config/scripts/release-policy.test.mjs": 44, + "config/scripts/release-rc-history.test.mjs": 150, + "config/scripts/release-tag-pattern-census.test.mjs": 67, + "config/scripts/renderer-boot-graph.test.mjs": 87, + "config/scripts/renderer-scrollbar-style-plugin.test.mjs": 3027, + "config/scripts/replace-cached-nsis-elevate.test.mjs": 1400, + "config/scripts/required-pr-check-name.test.mjs": 852, + "config/scripts/resolve-7za-path.test.mjs": 1513, + "config/scripts/run-codex-real-account-validation.test.ts": 2916, + "config/scripts/run-internal-dev-setup.test.mjs": 33, + "config/scripts/run-linux-packaged-node-pty-floor-smoke.test.mjs": 31, + "config/scripts/run-terminal-scale-perf-report-gate.test.mjs": 64, + "config/scripts/run-typecheck-bun-queue.test.mjs": 420, + "config/scripts/run-typecheck-projects-in-parallel.test.mjs": 135, + "config/scripts/runtime-serve-terminal-smoke.test.mjs": 175, + "config/scripts/scan-release-artifacts-antivirus.test.mjs": 44, + "config/scripts/script-entry-detection.test.mjs": 73, + "config/scripts/script-module-dependencies.test.mjs": 41, + "config/scripts/session-scanner-service-smoke.test.mjs": 633, + "config/scripts/shared-electron-dist-cache.test.ts": 97, + "config/scripts/skill-description-length.test.mjs": 64, + "config/scripts/skill-recipe-shell.test.mjs": 1238, + "config/scripts/sort-comparator-performance-plugin.test.mjs": 249, + "config/scripts/space-sharing-copy.test.ts": 121, + "config/scripts/ssh-browser-e2e-routing.test.mjs": 118, + "config/scripts/stable-release-tag-selection-budget.test.mjs": 118, + "config/scripts/static-appimage-package-contract.test.mjs": 80, + "config/scripts/static-module-specifiers.test.mjs": 175, + "config/scripts/structured-agent-session-start-failure-row-render.test.mjs": 2891, + "config/scripts/summarize-terminal-perf-report.test.mjs": 62, + "config/scripts/telemetry-bundle-constant-patterns.test.mjs": 36, + "config/scripts/terminal-ime-engagement-receipt.test.mjs": 44, + "config/scripts/terminal-partial-escape-native-continuation.test.mjs": 2393, + "config/scripts/terminal-perf-dependency-preparation.test.mjs": 558, + "config/scripts/terminal-perf-report-annotations.test.mjs": 31, + "config/scripts/test-only-source-path.test.mjs": 35, + "config/scripts/trim-windows-icon-source.test.mjs": 242, + "config/scripts/update-node-runtime-pin.test.mjs": 63, + "config/scripts/verify-cli-bin.test.mjs": 100, + "config/scripts/verify-dev-channel-packaging.test.mjs": 207, + "config/scripts/verify-linux-glibc-floor.test.mjs": 143, + "config/scripts/verify-linux-glibc-payloads.test.mjs": 161, + "config/scripts/verify-localization-catalog.test.mjs": 364, + "config/scripts/verify-localization-catalogs.test.mjs": 3034, + "config/scripts/verify-localization-extraction.test.mjs": 37, + "config/scripts/verify-packaged-browser-participation.test.mjs": 46, + "config/scripts/verify-packaged-daemon-entry.test.mjs": 1866, + "config/scripts/verify-packaged-mobile-web-bundle.test.mjs": 201, + "config/scripts/verify-packaged-node-pty-job-ownership.test.mjs": 75, + "config/scripts/verify-packaged-orcad-template.test.mjs": 378, + "config/scripts/verify-packaged-plugin-resources.test.mjs": 71, + "config/scripts/verify-packaged-windows-agent-hook.test.mjs": 38, + "config/scripts/verify-release-required-assets.test.mjs": 37, + "config/scripts/verify-skill-update-roundtrip.test.mjs": 51, + "config/scripts/verify-skills-cli-runtime.test.mjs": 194, + "config/scripts/verify-windows-inner-signature.test.mjs": 56, + "config/scripts/verify-wsl-e2e-participation.test.mjs": 38, + "config/scripts/vitest-node-runtime-boundary.test.ts": 1990, + "config/scripts/vitest-node-runtime-pool.test.ts": 383, + "config/scripts/vitest-real-agent-home-write-guard.test.ts": 61, + "config/scripts/vitest-runtime-boundary.test.ts": 95, + "config/scripts/vitest-runtime-sequencer.test.mjs": 144, + "config/scripts/vitest-sqlite-runtime-boundary.test.ts": 3579, + "config/scripts/websocket-server-loopback-bind.test.ts": 405, + "config/scripts/win-crash-survival-e2e.test.mjs": 452, + "config/scripts/win32-test-lane-registration.test.mjs": 3005, + "config/scripts/windows-bin-spawn-predicate.test.mjs": 127, + "config/scripts/windows-cmd-shim-spawn-boundary.test.mjs": 1457, + "config/scripts/windows-pe-imports.test.mjs": 35, + "config/scripts/windows-pe-machine.test.mjs": 40, + "config/scripts/windows-process-tree-creation-time.test.mjs": 35, + "config/scripts/windows-process-tree-gyp-path.test.mjs": 25, + "config/scripts/windows-process-tree-gyp-rebuild.test.mjs": 76, + "config/scripts/windows-process-tree-prepared-addon.test.mjs": 152, + "config/scripts/windows-pty-table-stress-observer.test.mjs": 73, + "config/scripts/windows-uninstaller-signing.test.mjs": 54, + "config/scripts/workflow-ref-mirror-case-safety.test.mjs": 433, + "config/scripts/workflow-ref-reachability.test.mjs": 663, + "config/scripts/workspace-source-exports.test.mjs": 101, + "config/scripts/wsl-e2e-lane-contract.test.mjs": 170, + "config/scripts/xterm-image-cell-payload-contract.test.mjs": 149, + "config/scripts/xterm-image-lifecycle-contract.test.mjs": 144, + "config/scripts/xterm-image-memory-contract.test.mjs": 371, + "config/scripts/xterm-image-resize-contract.test.mjs": 46, + "config/scripts/xterm-image-wasm-budget-contract.test.mjs": 421, + "config/scripts/zip-extractor-command.test.mjs": 59, + "src/cli/agent-context.test.ts": 113, + "src/cli/agent-session-search-format.test.ts": 62, + "src/cli/args-repeatable-flags.test.ts": 58, + "src/cli/args.test.ts": 125, + "src/cli/automation-format.test.ts": 79, + "src/cli/automation-owner-conflict-recovery.test.ts": 507, + "src/cli/browser-empty-values.test.ts": 165, + "src/cli/browser.test.ts": 476, "src/cli/cli-version.test.ts": 56, - "src/cli/codex-command-classification.test.ts": 56, - "src/cli/command-suggestion-budget.test.ts": 55, - "src/cli/command-suggestion.test.ts": 53, - "src/cli/computer-format.test.ts": 102, - "src/cli/emulator-logcat-format.test.ts": 48, - "src/cli/execution-host-flag.test.ts": 94, - "src/cli/format-recovery.test.ts": 355, - "src/cli/format.test.ts": 618, - "src/cli/handler-group-manifest.test.ts": 1856, - "src/cli/handlers/account.test.ts": 924, - "src/cli/handlers/agent-hooks-load-boundary.test.ts": 533, - "src/cli/handlers/agent-hooks-prepare-codex-isolated-home.test.ts": 531, - "src/cli/handlers/agent-hooks.test.ts": 1290, - "src/cli/handlers/artifacts.test.ts": 475, - "src/cli/handlers/automation-destination-fencing.test.ts": 515, - "src/cli/handlers/automation-owner-fencing.test.ts": 506, - "src/cli/handlers/computer-action-routing.test.ts": 200, - "src/cli/handlers/computer-action-validation.test.ts": 250, - "src/cli/handlers/computer-state-formatting.test.ts": 191, - "src/cli/handlers/computer.test.ts": 224, - "src/cli/handlers/core.test.ts": 385, - "src/cli/handlers/data-account-commands.test.ts": 419, - "src/cli/handlers/emulator.test.ts": 207, - "src/cli/handlers/file-absolute-paths.test.ts": 200, - "src/cli/handlers/file.test.ts": 181, - "src/cli/handlers/interactive-login-interruption.test.ts": 69, - "src/cli/handlers/linear.test.ts": 267, - "src/cli/handlers/managed-server.test.ts": 432, - "src/cli/handlers/orchestration-caller-identity-cli.test.ts": 378, - "src/cli/handlers/orchestration-check-identity.test.ts": 473, - "src/cli/handlers/orchestration-federated-legacy-settlement.test.ts": 504, - "src/cli/handlers/orchestration-gate-cli.test.ts": 263, - "src/cli/handlers/orchestration-legacy-read-only.test.ts": 476, - "src/cli/handlers/orchestration-lifecycle-json-rejection.test.ts": 433, - "src/cli/handlers/orchestration-lifecycle-rejection.test.ts": 484, - "src/cli/handlers/orchestration-migration.test.ts": 555, - "src/cli/handlers/orchestration-module-boundaries.test.ts": 534, - "src/cli/handlers/orchestration-request-show-cli.test.ts": 442, - "src/cli/handlers/orchestration-run-cli.test.ts": 455, - "src/cli/handlers/orchestration-send-receipt-warnings.test.ts": 375, - "src/cli/handlers/orchestration-task-create-cli.test.ts": 561, - "src/cli/handlers/orchestration-task-list-brief.test.ts": 436, - "src/cli/handlers/orchestration-windows-ask-cli.test.ts": 496, - "src/cli/handlers/orchestration-worker-cli.test.ts": 472, - "src/cli/handlers/orchestration-worker-show-wait-cli.test.ts": 376, - "src/cli/handlers/orchestration.test.ts": 805, - "src/cli/handlers/orchestration/mutation-request.test.ts": 3412, - "src/cli/handlers/orchestration/worker-list-run-scope.test.ts": 534, - "src/cli/handlers/orchestration/worker-output.test.ts": 66, - "src/cli/handlers/profile-state-recovery-admission.test.ts": 2338, - "src/cli/handlers/profile-state.test.ts": 1185, - "src/cli/handlers/repo-visibility.test.ts": 584, - "src/cli/handlers/search.test.ts": 359, - "src/cli/handlers/skill-sharing.test.ts": 492, - "src/cli/handlers/terminal.test.ts": 571, - "src/cli/handlers/worktree-gitlab-link.test.ts": 347, - "src/cli/host-selector-alternatives.test.ts": 82, - "src/cli/index-automation-extra-agent-args.test.ts": 318, - "src/cli/index-automation-identifiers.test.ts": 293, - "src/cli/index-automation-schedule.test.ts": 256, - "src/cli/index-automation-session-reuse.test.ts": 312, - "src/cli/index-automation-source-context.test.ts": 254, - "src/cli/index-automation-target.test.ts": 246, - "src/cli/index-device-commands.test.ts": 180, - "src/cli/index-environment-commands.test.ts": 408, - "src/cli/index-local-command-routing-flags.test.ts": 764, - "src/cli/index-memory-diagnostics.test.ts": 244, - "src/cli/index-omitted-host-scope-selectors.test.ts": 317, - "src/cli/index-orchestration.test.ts": 292, - "src/cli/index-project-setup.test.ts": 245, - "src/cli/index-reference.test.ts": 233, - "src/cli/index-serve-command.test.ts": 444, - "src/cli/index-terminal-commands.test.ts": 233, - "src/cli/index-terminal-list-host-scope.test.ts": 229, - "src/cli/index-vm-recipe-doctor.test.ts": 469, - "src/cli/index-worktree-create-agent.test.ts": 253, - "src/cli/index-worktree-create-linear.test.ts": 235, - "src/cli/index-worktree-create-parent.test.ts": 310, - "src/cli/index-worktree-create-target.test.ts": 273, - "src/cli/index-worktree-gitlab-links.test.ts": 225, - "src/cli/index-worktree-linked-items.test.ts": 299, - "src/cli/index-worktree-selector-resolution.test.ts": 527, - "src/cli/index-worktree-set.test.ts": 281, - "src/cli/index.test.ts": 1022, - "src/cli/linear-format.test.ts": 66, - "src/cli/orchestration-dispatch-refusal-format.test.ts": 101, - "src/cli/orchestration-mutation-recovery.test.ts": 461, - "src/cli/orchestration-session-caller-cli.test.ts": 608, - "src/cli/orchestration-structured-sender-identity.test.ts": 285, - "src/cli/orchestration-structured-session-no-identity.test.ts": 448, - "src/cli/quote-stripped-json-flag.test.ts": 62, - "src/cli/reference-command-arguments.test.ts": 132, - "src/cli/retry-request-flag.test.ts": 513, - "src/cli/runtime-client-deferral.test.ts": 939, - "src/cli/runtime-client.test.ts": 532, - "src/cli/runtime/client-recovery.test.ts": 1089, - "src/cli/runtime/client-timeout-policy.test.ts": 371, - "src/cli/runtime/envelope-schema.test.ts": 261, - "src/cli/runtime/environments.test.ts": 409, - "src/cli/runtime/launch-serve-runtime.test.ts": 328, - "src/cli/runtime/launch-user-data-path.test.ts": 264, - "src/cli/runtime/launch.test.ts": 369, - "src/cli/runtime/orchestration-recovery-command.test.ts": 59, - "src/cli/runtime/profile-state-recovery-launch.test.ts": 301, - "src/cli/runtime/runtime-access-denied.test.ts": 433, - "src/cli/runtime/runtime-source-fence.test.ts": 454, - "src/cli/runtime/serve-orcad-launch.test.ts": 287, - "src/cli/runtime/serve-signal-exit-diagnostic.test.ts": 92, - "src/cli/runtime/status-caller.test.ts": 660, - "src/cli/runtime/status.test.ts": 377, - "src/cli/runtime/transport-framing.test.ts": 337, - "src/cli/runtime/transport.test.ts": 789, - "src/cli/runtime/types.test.ts": 48, - "src/cli/runtime/websocket-transport-error.test.ts": 375, - "src/cli/runtime/websocket-transport.test.ts": 552, - "src/cli/search-command-arguments.test.ts": 352, + "src/cli/codex-command-classification.test.ts": 54, + "src/cli/command-suggestion-budget.test.ts": 113, + "src/cli/command-suggestion-destructive-families.test.ts": 212, + "src/cli/command-suggestion.test.ts": 57, + "src/cli/computer-format.test.ts": 88, + "src/cli/emulator-logcat-format.test.ts": 43, + "src/cli/execution-host-flag.test.ts": 89, + "src/cli/format-recovery.test.ts": 483, + "src/cli/format.test.ts": 540, + "src/cli/handler-group-manifest.test.ts": 2724, + "src/cli/handlers/account.test.ts": 1145, + "src/cli/handlers/agent-hooks-load-boundary.test.ts": 462, + "src/cli/handlers/agent-hooks-prepare-codex-isolated-home.test.ts": 634, + "src/cli/handlers/agent-hooks.test.ts": 1543, + "src/cli/handlers/artifacts.test.ts": 596, + "src/cli/handlers/automation-destination-fencing.test.ts": 617, + "src/cli/handlers/automation-owner-fencing.test.ts": 584, + "src/cli/handlers/computer-action-routing.test.ts": 192, + "src/cli/handlers/computer-action-validation.test.ts": 222, + "src/cli/handlers/computer-state-formatting.test.ts": 218, + "src/cli/handlers/computer.test.ts": 274, + "src/cli/handlers/core.test.ts": 342, + "src/cli/handlers/data-account-commands.test.ts": 527, + "src/cli/handlers/emulator.test.ts": 202, + "src/cli/handlers/file-absolute-paths.test.ts": 223, + "src/cli/handlers/file.test.ts": 266, + "src/cli/handlers/interactive-login-interruption.test.ts": 65, + "src/cli/handlers/linear.test.ts": 300, + "src/cli/handlers/managed-server.test.ts": 543, + "src/cli/handlers/orchestration-caller-identity-cli.test.ts": 533, + "src/cli/handlers/orchestration-check-identity.test.ts": 439, + "src/cli/handlers/orchestration-federated-legacy-settlement.test.ts": 519, + "src/cli/handlers/orchestration-gate-cli.test.ts": 476, + "src/cli/handlers/orchestration-legacy-read-only.test.ts": 531, + "src/cli/handlers/orchestration-lifecycle-json-rejection.test.ts": 509, + "src/cli/handlers/orchestration-lifecycle-rejection.test.ts": 483, + "src/cli/handlers/orchestration-migration.test.ts": 527, + "src/cli/handlers/orchestration-module-boundaries.test.ts": 575, + "src/cli/handlers/orchestration-request-show-cli.test.ts": 478, + "src/cli/handlers/orchestration-run-cli.test.ts": 499, + "src/cli/handlers/orchestration-send-receipt-warnings.test.ts": 467, + "src/cli/handlers/orchestration-task-create-cli.test.ts": 563, + "src/cli/handlers/orchestration-task-list-brief.test.ts": 408, + "src/cli/handlers/orchestration-windows-ask-cli.test.ts": 467, + "src/cli/handlers/orchestration-worker-cli.test.ts": 446, + "src/cli/handlers/orchestration-worker-show-wait-cli.test.ts": 745, + "src/cli/handlers/orchestration.test.ts": 717, + "src/cli/handlers/orchestration/mutation-request.test.ts": 3679, + "src/cli/handlers/orchestration/worker-list-run-scope.test.ts": 496, + "src/cli/handlers/orchestration/worker-output.test.ts": 56, + "src/cli/handlers/profile-state-recovery-admission.test.ts": 2820, + "src/cli/handlers/profile-state.test.ts": 1205, + "src/cli/handlers/repo-visibility.test.ts": 611, + "src/cli/handlers/search.test.ts": 447, + "src/cli/handlers/serve-restore.test.ts": 73, + "src/cli/handlers/serve-service.test.ts": 157, + "src/cli/handlers/skill-sharing.test.ts": 477, + "src/cli/handlers/terminal-daemon.test.ts": 576, + "src/cli/handlers/terminal.test.ts": 585, + "src/cli/handlers/worktree-gitlab-link.test.ts": 435, + "src/cli/host-selector-alternatives.test.ts": 69, + "src/cli/index-agent-launch.test.ts": 526, + "src/cli/index-automation-extra-agent-args.test.ts": 448, + "src/cli/index-automation-identifiers.test.ts": 438, + "src/cli/index-automation-schedule.test.ts": 472, + "src/cli/index-automation-session-reuse.test.ts": 432, + "src/cli/index-automation-source-context.test.ts": 452, + "src/cli/index-automation-target.test.ts": 438, + "src/cli/index-device-commands.test.ts": 188, + "src/cli/index-environment-commands.test.ts": 368, + "src/cli/index-jira.test.ts": 451, + "src/cli/index-local-command-routing-flags.test.ts": 823, + "src/cli/index-memory-diagnostics.test.ts": 206, + "src/cli/index-omitted-host-scope-selectors.test.ts": 672, + "src/cli/index-orchestration.test.ts": 509, + "src/cli/index-project-setup.test.ts": 259, + "src/cli/index-reference-jira.test.ts": 516, + "src/cli/index-reference.test.ts": 649, + "src/cli/index-serve-command.test.ts": 467, + "src/cli/index-serve-restore.test.ts": 240, + "src/cli/index-terminal-commands.test.ts": 415, + "src/cli/index-terminal-list-host-scope.test.ts": 430, + "src/cli/index-vm-recipe-doctor.test.ts": 464, + "src/cli/index-worktree-create-agent.test.ts": 525, + "src/cli/index-worktree-create-linear.test.ts": 517, + "src/cli/index-worktree-create-parent.test.ts": 611, + "src/cli/index-worktree-create-target.test.ts": 503, + "src/cli/index-worktree-gitlab-links.test.ts": 578, + "src/cli/index-worktree-linked-items.test.ts": 523, + "src/cli/index-worktree-selector-resolution.test.ts": 1033, + "src/cli/index-worktree-set.test.ts": 464, + "src/cli/index.test.ts": 680, + "src/cli/jira-command-surface.test.ts": 132, + "src/cli/linear-format.test.ts": 67, + "src/cli/orchestration-dispatch-refusal-format.test.ts": 95, + "src/cli/orchestration-mutation-recovery.test.ts": 531, + "src/cli/orchestration-session-caller-cli.test.ts": 677, + "src/cli/orchestration-structured-sender-identity.test.ts": 479, + "src/cli/orchestration-structured-session-no-identity.test.ts": 483, + "src/cli/quote-stripped-json-flag.test.ts": 47, + "src/cli/reference-command-arguments.test.ts": 128, + "src/cli/retry-request-flag.test.ts": 577, + "src/cli/runtime-client-deferral.test.ts": 773, + "src/cli/runtime-client.test.ts": 422, + "src/cli/runtime/client-execution-host.test.ts": 390, + "src/cli/runtime/client-recovery.test.ts": 715, + "src/cli/runtime/client-timeout-policy.test.ts": 403, + "src/cli/runtime/envelope-schema.test.ts": 238, + "src/cli/runtime/environments.test.ts": 312, + "src/cli/runtime/launch-serve-preflight.test.ts": 312, + "src/cli/runtime/launch-serve-runtime.test.ts": 383, + "src/cli/runtime/launch-serve-supervise.test.ts": 1261, + "src/cli/runtime/launch-user-data-path.test.ts": 313, + "src/cli/runtime/launch.test.ts": 406, + "src/cli/runtime/orchestration-recovery-command.test.ts": 62, + "src/cli/runtime/profile-state-recovery-launch.test.ts": 277, + "src/cli/runtime/runtime-access-denied.test.ts": 375, + "src/cli/runtime/runtime-listener-probe.test.ts": 104, + "src/cli/runtime/runtime-source-fence.test.ts": 449, + "src/cli/runtime/serve-appimage-update.test.ts": 214, + "src/cli/runtime/serve-bundle-park.test.ts": 158, + "src/cli/runtime/serve-electron-preflight.test.ts": 282, + "src/cli/runtime/serve-install-hold.test.ts": 75, + "src/cli/runtime/serve-orcad-launch.test.ts": 262, + "src/cli/runtime/serve-pairing-redaction.test.ts": 122, + "src/cli/runtime/serve-service-unit.test.ts": 88, + "src/cli/runtime/serve-signal-exit-diagnostic.test.ts": 90, + "src/cli/runtime/serve-supervised-service.test.ts": 99, + "src/cli/runtime/serve-supervisor-policy.test.ts": 52, + "src/cli/runtime/serve-supervisor-relocation.test.ts": 215, + "src/cli/runtime/serve-update-supervisor-gate.test.ts": 104, + "src/cli/runtime/status-caller.test.ts": 489, + "src/cli/runtime/status.test.ts": 731, + "src/cli/runtime/transport-framing.test.ts": 313, + "src/cli/runtime/transport.test.ts": 798, + "src/cli/runtime/types.test.ts": 60, + "src/cli/runtime/websocket-transport-error.test.ts": 433, + "src/cli/runtime/websocket-transport.test.ts": 599, + "src/cli/search-command-arguments.test.ts": 359, "src/cli/serve-electron-flag-parity.test.ts": 85, - "src/cli/shell-command-quote.test.ts": 53, - "src/cli/skills-reference-selector.test.ts": 206, - "src/cli/skills.test.ts": 1353, - "src/cli/specs/account.test.ts": 94, - "src/cli/specs/bundled-guide-flags.test.ts": 201, - "src/cli/specs/file.test.ts": 97, - "src/cli/specs/profile-state.test.ts": 87, - "src/cli/specs/search.test.ts": 181, - "src/cli/terminal-format-draft.test.ts": 51, - "src/cli/terminal-format.test.ts": 78, - "src/cli/terminal-list-host-scope-format.test.ts": 80, - "src/cli/terminal-read-screen.test.ts": 215, - "src/cli/vocabulary-policy.test.ts": 110, - "src/cli/worktree-ps-terminal-verdict.test.ts": 48, - "src/cli/worktree-selector-caller-host.test.ts": 69, - "src/cli/worktree-selector-wsl-posix-path.test.ts": 78, - "src/main/acp/acp-agent-connection.test.ts": 354, - "src/main/acp/acp-compaction-journal-fixture.test.ts": 298, - "src/main/acp/acp-dialects/grok-subagents.test.ts": 307, - "src/main/acp/acp-dialects/omp-dialect.test.ts": 258, - "src/main/acp/acp-fixture-privacy.test.ts": 137, - "src/main/acp/acp-json-rpc-peer.test.ts": 274, - "src/main/acp/acp-model-catalog-probe.test.ts": 426, - "src/main/acp/acp-permission-requests.test.ts": 355, - "src/main/acp/acp-prompt-content.test.ts": 641, - "src/main/acp/acp-recovery-history.test.ts": 502, - "src/main/acp/acp-session-agent-turns.test.ts": 297, - "src/main/acp/acp-session-lifecycle.test.ts": 273, - "src/main/acp/acp-session-notifications.test.ts": 323, - "src/main/acp/acp-session-runtime.test.ts": 459, - "src/main/acp/acp-structured-auth.test.ts": 1129, - "src/main/acp/acp-structured-background-task-stop.test.ts": 4451, - "src/main/acp/acp-structured-broken-stream.test.ts": 10364, - "src/main/acp/acp-structured-child-session-end.test.ts": 2649, - "src/main/acp/acp-structured-child-stop.test.ts": 2928, - "src/main/acp/acp-structured-child-work-admission.test.ts": 1464, - "src/main/acp/acp-structured-child-work.test.ts": 2920, - "src/main/acp/acp-structured-compaction.test.ts": 895, - "src/main/acp/acp-structured-context-clear.test.ts": 490, - "src/main/acp/acp-structured-electron-free.test.ts": 276, - "src/main/acp/acp-structured-grok-subagent-recordings.test.ts": 1106, - "src/main/acp/acp-structured-host-configured-default.test.ts": 1598, - "src/main/acp/acp-structured-host-crash.test.ts": 1006, - "src/main/acp/acp-structured-host-lifecycle.test.ts": 2036, - "src/main/acp/acp-structured-host-live-listing.test.ts": 1281, - "src/main/acp/acp-structured-host-option-hang.test.ts": 1480, - "src/main/acp/acp-structured-host-restore-failed.test.ts": 1530, - "src/main/acp/acp-structured-host-start-aborts.test.ts": 1092, - "src/main/acp/acp-structured-host-stop.test.ts": 1968, - "src/main/acp/acp-structured-host-subagent-stop.test.ts": 1665, - "src/main/acp/acp-structured-image-prompts.test.ts": 806, - "src/main/acp/acp-structured-launch-resolution-omp.test.ts": 415, - "src/main/acp/acp-structured-launch-resolution-opencode.test.ts": 317, - "src/main/acp/acp-structured-launch-resolution.test.ts": 524, - "src/main/acp/acp-structured-launch-visuals.test.ts": 364, - "src/main/acp/acp-structured-missing-model.test.ts": 361, - "src/main/acp/acp-structured-omp-recordings.test.ts": 953, - "src/main/acp/acp-structured-opencode-recordings.test.ts": 690, - "src/main/acp/acp-structured-options.test.ts": 398, - "src/main/acp/acp-structured-prompt-failure.test.ts": 759, - "src/main/acp/acp-structured-request-admission.test.ts": 824, - "src/main/acp/acp-structured-session-adapter-lifecycle.test.ts": 4918, - "src/main/acp/acp-structured-session-adapter-recordings.test.ts": 684, - "src/main/acp/acp-structured-session-adapter-resume.test.ts": 5492, - "src/main/acp/acp-structured-session-adapter-teardown.test.ts": 1211, - "src/main/acp/acp-structured-session-adapter.test.ts": 1251, - "src/main/acp/acp-structured-sign-in.test.ts": 813, - "src/main/acp/acp-structured-steer.test.ts": 1740, - "src/main/acp/acp-subagent-timeline.test.ts": 476, - "src/main/acp/acp-text-message-ownership.test.ts": 1219, - "src/main/acp/acp-text-stream-identity.test.ts": 817, - "src/main/acp/acp-timeline-background-task-evidence.test.ts": 698, - "src/main/acp/acp-timeline-background-task-results.test.ts": 594, - "src/main/acp/acp-timeline-background-task-words.test.ts": 1032, - "src/main/acp/acp-timeline-background-tasks.test.ts": 1081, - "src/main/acp/acp-timeline-dialect.test.ts": 618, - "src/main/acp/acp-timeline-generic.test.ts": 974, - "src/main/acp/acp-timeline-grok-subagent-retention.test.ts": 1206, - "src/main/acp/acp-timeline-grok-subagents.test.ts": 1171, - "src/main/acp/acp-timeline-joins.test.ts": 561, - "src/main/acp/acp-timeline-open-enums.test.ts": 585, - "src/main/acp/acp-timeline-opencode-dialect.test.ts": 415, - "src/main/acp/acp-timeline-recordings.test.ts": 610, - "src/main/acp/acp-timeline-recovery.test.ts": 929, - "src/main/acp/acp-timeline-routing.test.ts": 725, - "src/main/acp/acp-timeline-turn-failures.test.ts": 863, - "src/main/acp/acp-visuals-skill.test.ts": 53, - "src/main/acp/acp-write-queue-retention.test.ts": 466, - "src/main/active-view-persistence-boundary.test.ts": 1716, - "src/main/active-view-preference-sync-flush-veto.test.ts": 784, - "src/main/active-view-preference.test.ts": 62, - "src/main/agent-auth-restart-preservation.test.ts": 75, - "src/main/agent-awake-service-platform-assertions.test.ts": 93, - "src/main/agent-awake-service.test.ts": 127, - "src/main/agent-cli-version-probe.test.ts": 63, - "src/main/agent-hooks/branch-rename-failure-output.test.ts": 51, - "src/main/agent-hooks/ended-process-reconciliation.test.ts": 638, - "src/main/agent-hooks/first-work-branch-rename.test.ts": 870, - "src/main/agent-hooks/first-work-folder-rename.test.ts": 118, - "src/main/agent-hooks/grok-replay-guard.test.ts": 149, - "src/main/agent-hooks/hook-process-capture.test.ts": 100, - "src/main/agent-hooks/hook-script-outside-orca.test.ts": 277, - "src/main/agent-hooks/hook-status-session-tabs-republish.test.ts": 647, - "src/main/agent-hooks/install-telemetry.test.ts": 50, - "src/main/agent-hooks/installer-utils-remote.test.ts": 71, - "src/main/agent-hooks/installer-utils.test.ts": 189, - "src/main/agent-hooks/local-agent-cli-presence.test.ts": 64, - "src/main/agent-hooks/local-tmux-managed-pty.test.ts": 49, - "src/main/agent-hooks/managed-agent-hook-controls.test.ts": 127, - "src/main/agent-hooks/managed-hook-command-contract.test.ts": 389, - "src/main/agent-hooks/managed-hook-detection-commands.test.ts": 68, - "src/main/agent-hooks/managed-hook-install-lock.test.ts": 332, - "src/main/agent-hooks/managed-hook-owner-identity.test.ts": 99, - "src/main/agent-hooks/managed-hook-script-refresh-main-thread.test.ts": 410, - "src/main/agent-hooks/managed-hook-script-refresh.test.ts": 498, - "src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts": 1579, - "src/main/agent-hooks/managed-hook-timeout.test.ts": 1842, - "src/main/agent-hooks/managed-toml-ownership.test.ts": 65, - "src/main/agent-hooks/manual-compact-hook-stream.test.ts": 806, - "src/main/agent-hooks/manual-compact-status-cleanup.test.ts": 635, - "src/main/agent-hooks/migration-unsupported-pty-state.test.ts": 44, - "src/main/agent-hooks/posix-hook-json-stdin-reader.test.ts": 3465, - "src/main/agent-hooks/remote-hook-service-installers.test.ts": 453, - "src/main/agent-hooks/remote-hook-service-registry-coverage.test.ts": 430, - "src/main/agent-hooks/restored-status-dead-terminal-sweep.test.ts": 821, - "src/main/agent-hooks/server-agent-presence.test.ts": 732, - "src/main/agent-hooks/server-ai-vault-liveness.test.ts": 669, - "src/main/agent-hooks/server-amp-normalization.test.ts": 693, - "src/main/agent-hooks/server-authority-evidence.test.ts": 729, - "src/main/agent-hooks/server-claude-cancel-captures.test.ts": 761, - "src/main/agent-hooks/server-claude-child-permission-lifecycle.test.ts": 713, - "src/main/agent-hooks/server-claude-normalization.test.ts": 621, - "src/main/agent-hooks/server-claude-owed-notification-expiry.test.ts": 758, - "src/main/agent-hooks/server-claude-permission-visibility.test.ts": 827, - "src/main/agent-hooks/server-claude-statusline.test.ts": 737, - "src/main/agent-hooks/server-claude-task-wakeup-lifecycle.test.ts": 6962, - "src/main/agent-hooks/server-claude-terminal-interrupt.test.ts": 728, - "src/main/agent-hooks/server-closed-tab-suppression.test.ts": 755, - "src/main/agent-hooks/server-codex-ctrl-c-inference.test.ts": 663, - "src/main/agent-hooks/server-codex-interrupt-hook.test.ts": 558, - "src/main/agent-hooks/server-codex-normalization.test.ts": 684, - "src/main/agent-hooks/server-codex-subagent-transcript.test.ts": 4877, - "src/main/agent-hooks/server-codex-turn-interruption.test.ts": 4222, - "src/main/agent-hooks/server-copilot-normalization.test.ts": 940, - "src/main/agent-hooks/server-cursor-normalization.test.ts": 653, - "src/main/agent-hooks/server-endpoint-file-lifecycle.test.ts": 770, - "src/main/agent-hooks/server-escape-navigation-inference.test.ts": 696, - "src/main/agent-hooks/server-grok-background-status.test.ts": 650, - "src/main/agent-hooks/server-grok-cancel.test.ts": 677, - "src/main/agent-hooks/server-grok-discovery.test.ts": 1216, - "src/main/agent-hooks/server-hook-http-ingest.test.ts": 742, - "src/main/agent-hooks/server-ingest-remote.test.ts": 648, - "src/main/agent-hooks/server-ingest-structured-children.test.ts": 615, - "src/main/agent-hooks/server-ingest-structured-state-clock.test.ts": 679, - "src/main/agent-hooks/server-ingest-structured-status.test.ts": 719, - "src/main/agent-hooks/server-ingest-terminal-status.test.ts": 711, - "src/main/agent-hooks/server-interrupt-inference-guards.test.ts": 580, - "src/main/agent-hooks/server-interrupt-inference-resurrection.test.ts": 644, - "src/main/agent-hooks/server-interrupt-inference-validation.test.ts": 670, - "src/main/agent-hooks/server-last-status-child-held-main-agent.test.ts": 814, - "src/main/agent-hooks/server-last-status-hydrate-confirmation.test.ts": 742, - "src/main/agent-hooks/server-last-status-hydrate-validation.test.ts": 854, - "src/main/agent-hooks/server-last-status-lead-boundary.test.ts": 852, - "src/main/agent-hooks/server-last-status-main-agent-fact.test.ts": 802, - "src/main/agent-hooks/server-last-status-restored-children.test.ts": 776, - "src/main/agent-hooks/server-last-status-write.test.ts": 1017, - "src/main/agent-hooks/server-main-agent-turn-verdicts.test.ts": 1703, - "src/main/agent-hooks/server-muse-session-log-poll.test.ts": 2581, - "src/main/agent-hooks/server-observation-provenance.test.ts": 656, - "src/main/agent-hooks/server-omp-retired-ingress.test.ts": 649, - "src/main/agent-hooks/server-omp-retirement-ack.test.ts": 606, - "src/main/agent-hooks/server-opencode-binder.test.ts": 884, - "src/main/agent-hooks/server-opencode-lifecycle.test.ts": 867, - "src/main/agent-hooks/server-opencode-tui-identity.test.ts": 914, - "src/main/agent-hooks/server-pane-authority.test.ts": 875, - "src/main/agent-hooks/server-pi-normalization.test.ts": 868, - "src/main/agent-hooks/server-process-lifetime-status.test.ts": 964, - "src/main/agent-hooks/server-prompt-sent-telemetry.test.ts": 691, - "src/main/agent-hooks/server-relay-listener-replay.test.ts": 657, - "src/main/agent-hooks/server-relayed-claude-cancel.test.ts": 960, - "src/main/agent-hooks/server-reminted-pane-key.test.ts": 739, - "src/main/agent-hooks/server-removed-worktree-foreign-authority.test.ts": 739, - "src/main/agent-hooks/server-removed-worktree-status.test.ts": 574, - "src/main/agent-hooks/server-replay-evidence-clock.test.ts": 631, - "src/main/agent-hooks/server-retired-pane-new-turn.test.ts": 650, - "src/main/agent-hooks/server-start-failure-lifecycle.test.ts": 656, - "src/main/agent-hooks/server-startup-prompt-control.test.ts": 534, - "src/main/agent-hooks/server-status-listener-fanout.test.ts": 725, - "src/main/agent-hooks/server-status-persist-throttle.test.ts": 962, - "src/main/agent-hooks/server-structured-canonical-status.test.ts": 678, - "src/main/agent-hooks/server-tmux-canonical.test.ts": 675, - "src/main/agent-hooks/server-tmux-wire.test.ts": 644, - "src/main/agent-hooks/server-transport-interference.test.ts": 11601, - "src/main/agent-hooks/server-turn-started-at.test.ts": 691, - "src/main/agent-hooks/server.claude-interactive-question.test.ts": 717, - "src/main/agent-hooks/server.test-fixtures.test.ts": 473, - "src/main/agent-hooks/server/server-row-ownership.test.ts": 612, - "src/main/agent-hooks/spool.test.ts": 974, - "src/main/agent-hooks/terminal-handle-row-identity.test.ts": 656, - "src/main/agent-hooks/windows-batch-hook-launcher.test.ts": 99, - "src/main/agent-hooks/windows-cmd-hook-command-unicode.test.ts": 124, - "src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts": 107, - "src/main/agent-hooks/windows-hook-command-hosts.test.ts": 75, - "src/main/agent-hooks/windows-hook-payload-delivery.test.ts": 129, - "src/main/agent-hooks/windows-hook-post-interpreter.test.ts": 419, - "src/main/agent-hooks/windows-powershell-hook-launcher.test.ts": 50, - "src/main/agent-hooks/wsl-guest-plugin-install.test.ts": 56, - "src/main/agent-hooks/wsl-hook-relay-launch.test.ts": 134, - "src/main/agent-hooks/wsl-hook-relay-live.integration.test.ts": 2021, - "src/main/agent-hooks/wsl-hook-relay-manager.test.ts": 2367, - "src/main/agent-hooks/wsl-hook-relay-reattach.test.ts": 971, - "src/main/agent-hooks/wsl-hook-relay-recovery.test.ts": 49, - "src/main/agent-hooks/wsl-hook-relay-sentinel.test.ts": 64, - "src/main/agent-hooks/wsl-opencode-install-race.test.ts": 869, - "src/main/agent-hooks/wsl-opencode-settings-lifetime.test.ts": 1862, - "src/main/agent-hooks/wsl-opencode-settings.test.ts": 1034, - "src/main/agent-launch/agent-launch-decided-mode-contract.test.ts": 48, - "src/main/agent-launch/agent-launch-executor.test.ts": 1177, - "src/main/agent-launch/agent-launch-legacy-host-policy.test.ts": 894, - "src/main/agent-launch/agent-launch-opencode-model.test.ts": 757, - "src/main/agent-launch/agent-launch-pane-attachment.test.ts": 273, - "src/main/agent-launch/startup-agent-worktree-create.test.ts": 1128, - "src/main/agent-state-file-reader.test.ts": 103, - "src/main/agent-trust-presets.test.ts": 300, - "src/main/agent-workspace-trust-guard-failure.test.ts": 119, - "src/main/agent-workspace-trust-spawn.test.ts": 274, - "src/main/agent-workspace-trust.test.ts": 113, - "src/main/ai-vault-search/session-search-agent-compatibility.test.ts": 319, - "src/main/ai-vault-search/session-search-child-service.test.ts": 534, - "src/main/ai-vault-search/session-search-content-hash.test.ts": 65, - "src/main/ai-vault-search/session-search-cwd-key.test.ts": 72, - "src/main/ai-vault-search/session-search-deleted-sources.test.ts": 416, - "src/main/ai-vault-search/session-search-directory-listings.test.ts": 47, - "src/main/ai-vault-search/session-search-engine.test.ts": 1393, - "src/main/ai-vault-search/session-search-file-id.test.ts": 274, - "src/main/ai-vault-search/session-search-file-write.test.ts": 500, - "src/main/ai-vault-search/session-search-fts5-contract.test.ts": 152, - "src/main/ai-vault-search/session-search-hit-ranking.test.ts": 73, - "src/main/ai-vault-search/session-search-host-registration.test.ts": 1096, - "src/main/ai-vault-search/session-search-identifier-split.test.ts": 51, - "src/main/ai-vault-search/session-search-index-consumer.test.ts": 280, - "src/main/ai-vault-search/session-search-index-generation.test.ts": 421, - "src/main/ai-vault-search/session-search-index-pass.test.ts": 468, - "src/main/ai-vault-search/session-search-index-writer.test.ts": 482, - "src/main/ai-vault-search/session-search-indexer.test.ts": 2536, - "src/main/ai-vault-search/session-search-instance.test.ts": 1147, - "src/main/ai-vault-search/session-search-lifecycle-matrix.test.ts": 2756, - "src/main/ai-vault-search/session-search-live-transcript.test.ts": 423, - "src/main/ai-vault-search/session-search-merged-roots.test.ts": 489, - "src/main/ai-vault-search/session-search-message-rows.test.ts": 149, - "src/main/ai-vault-search/session-search-native-chat-indexing.test.ts": 476, - "src/main/ai-vault-search/session-search-opencode-index.test.ts": 807, - "src/main/ai-vault-search/session-search-orphan-rows.test.ts": 235, - "src/main/ai-vault-search/session-search-paging.test.ts": 601, - "src/main/ai-vault-search/session-search-query-planner.test.ts": 59, - "src/main/ai-vault-search/session-search-read-decision.test.ts": 49, - "src/main/ai-vault-search/session-search-retention-delete.test.ts": 198, - "src/main/ai-vault-search/session-search-retention-policy.test.ts": 275, - "src/main/ai-vault-search/session-search-retrieval-statement-compilation.test.ts": 501, - "src/main/ai-vault-search/session-search-root-refresh.test.ts": 623, - "src/main/ai-vault-search/session-search-row-filter.test.ts": 195, - "src/main/ai-vault-search/session-search-row-identity.test.ts": 148, - "src/main/ai-vault-search/session-search-scan-roots.test.ts": 230, - "src/main/ai-vault-search/session-search-schema.test.ts": 531, - "src/main/ai-vault-search/session-search-scope-entry-points.test.ts": 4400, - "src/main/ai-vault-search/session-search-scope-resolution.test.ts": 106, - "src/main/ai-vault-search/session-search-scope-service.test.ts": 288, - "src/main/ai-vault-search/session-search-service-registry.test.ts": 297, - "src/main/ai-vault-search/session-search-service.test.ts": 311, - "src/main/ai-vault-search/session-search-sidebar-parity.test.ts": 241, - "src/main/ai-vault-search/session-search-snippet-marks.test.ts": 179, - "src/main/ai-vault-search/session-search-sqlite-support.test.ts": 62, - "src/main/ai-vault-search/session-search-store-is-memory.test.ts": 827, - "src/main/ai-vault-search/session-search-store-scope-catalog.test.ts": 52, - "src/main/ai-vault-search/session-search-synthetic-corpus.test.ts": 313, - "src/main/ai-vault-search/session-search-typo-policy.test.ts": 160, - "src/main/ai-vault-search/session-search-typo-scope.test.ts": 148, - "src/main/ai-vault-search/session-search-write-lifetime.test.ts": 351, - "src/main/ai-vault/ai-vault-scan-cancellation.test.ts": 46, - "src/main/ai-vault/ai-vault-scan-coordinator.test.ts": 55, - "src/main/ai-vault/antigravity-ide-history.test.ts": 322, - "src/main/ai-vault/antigravity-index-file-admission.test.ts": 115, - "src/main/ai-vault/antigravity-opening-prompt-workspace.test.ts": 157, - "src/main/ai-vault/antigravity-recorded-history.test.ts": 281, - "src/main/ai-vault/cached-session-list-wsl-probe.test.ts": 472, - "src/main/ai-vault/cached-session-list.test.ts": 626, - "src/main/ai-vault/claude-project-dir-encoding.test.ts": 52, - "src/main/ai-vault/codex-session-collection.test.ts": 149, - "src/main/ai-vault/codex-session-root-dedup.test.ts": 156, - "src/main/ai-vault/local-log-tail-reader.test.ts": 69, + "src/cli/shell-command-quote.test.ts": 50, + "src/cli/skills-reference-selector.test.ts": 169, + "src/cli/skills.test.ts": 1324, + "src/cli/specs/account.test.ts": 66, + "src/cli/specs/bundled-guide-flags.test.ts": 233, + "src/cli/specs/cli-command-names-parity.test.ts": 160, + "src/cli/specs/profile-state.test.ts": 67, + "src/cli/terminal-format.test.ts": 97, + "src/cli/terminal-list-host-scope-format.test.ts": 78, + "src/cli/terminal-read-screen.test.ts": 486, + "src/cli/vocabulary-policy.test.ts": 112, + "src/cli/worktree-ps-terminal-verdict.test.ts": 47, + "src/cli/worktree-selector-caller-host.test.ts": 61, + "src/cli/worktree-selector-wsl-posix-path.test.ts": 79, + "src/main/acp/acp-agent-connection.test.ts": 316, + "src/main/acp/acp-compaction-journal-fixture.test.ts": 313, + "src/main/acp/acp-dialects/grok-subagents.test.ts": 279, + "src/main/acp/acp-dialects/omp-dialect.test.ts": 212, + "src/main/acp/acp-fixture-privacy.test.ts": 124, + "src/main/acp/acp-json-rpc-peer.test.ts": 292, + "src/main/acp/acp-model-catalog-probe.test.ts": 676, + "src/main/acp/acp-permission-requests.test.ts": 352, + "src/main/acp/acp-prompt-content.test.ts": 470, + "src/main/acp/acp-recovery-history.test.ts": 524, + "src/main/acp/acp-session-agent-turns.test.ts": 313, + "src/main/acp/acp-session-events.test.ts": 1023, + "src/main/acp/acp-session-lifecycle.test.ts": 413, + "src/main/acp/acp-session-notifications.test.ts": 389, + "src/main/acp/acp-session-runtime.test.ts": 400, + "src/main/acp/acp-structured-auth.test.ts": 1049, + "src/main/acp/acp-structured-background-task-stop.test.ts": 4804, + "src/main/acp/acp-structured-broken-stream.test.ts": 10857, + "src/main/acp/acp-structured-child-session-end.test.ts": 2107, + "src/main/acp/acp-structured-child-stop.test.ts": 3015, + "src/main/acp/acp-structured-child-work-admission.test.ts": 864, + "src/main/acp/acp-structured-child-work.test.ts": 3411, + "src/main/acp/acp-structured-compaction.test.ts": 1845, + "src/main/acp/acp-structured-context-clear.test.ts": 689, + "src/main/acp/acp-structured-electron-free.test.ts": 212, + "src/main/acp/acp-structured-grok-subagent-recordings.test.ts": 1446, + "src/main/acp/acp-structured-host-configured-default.test.ts": 1777, + "src/main/acp/acp-structured-host-crash.test.ts": 1513, + "src/main/acp/acp-structured-host-lifecycle.test.ts": 1920, + "src/main/acp/acp-structured-host-live-listing.test.ts": 1914, + "src/main/acp/acp-structured-host-option-hang.test.ts": 1447, + "src/main/acp/acp-structured-host-restore-failed.test.ts": 2022, + "src/main/acp/acp-structured-host-start-aborts.test.ts": 1886, + "src/main/acp/acp-structured-host-stop.test.ts": 2080, + "src/main/acp/acp-structured-host-subagent-stop.test.ts": 2207, + "src/main/acp/acp-structured-image-prompts.test.ts": 848, + "src/main/acp/acp-structured-launch-resolution-omp.test.ts": 718, + "src/main/acp/acp-structured-launch-resolution-opencode.test.ts": 682, + "src/main/acp/acp-structured-launch-resolution.test.ts": 784, + "src/main/acp/acp-structured-launch-visuals.test.ts": 768, + "src/main/acp/acp-structured-missing-model.test.ts": 358, + "src/main/acp/acp-structured-omp-recordings.test.ts": 1593, + "src/main/acp/acp-structured-opencode-recordings.test.ts": 1307, + "src/main/acp/acp-structured-options.test.ts": 264, + "src/main/acp/acp-structured-prompt-failure.test.ts": 1260, + "src/main/acp/acp-structured-request-admission.test.ts": 1010, + "src/main/acp/acp-structured-session-adapter-lifecycle.test.ts": 5319, + "src/main/acp/acp-structured-session-adapter-recordings.test.ts": 1296, + "src/main/acp/acp-structured-session-adapter-resume.test.ts": 5169, + "src/main/acp/acp-structured-session-adapter-teardown.test.ts": 1688, + "src/main/acp/acp-structured-session-adapter.test.ts": 1688, + "src/main/acp/acp-structured-sign-in.test.ts": 1285, + "src/main/acp/acp-structured-steer.test.ts": 2132, + "src/main/acp/acp-text-message-ownership.test.ts": 1104, + "src/main/acp/acp-text-stream-identity.test.ts": 931, + "src/main/acp/acp-timeline-background-task-evidence.test.ts": 1539, + "src/main/acp/acp-timeline-background-task-results.test.ts": 1133, + "src/main/acp/acp-timeline-background-task-words.test.ts": 1060, + "src/main/acp/acp-timeline-background-tasks.test.ts": 1760, + "src/main/acp/acp-timeline-dialect.test.ts": 746, + "src/main/acp/acp-timeline-generic.test.ts": 1493, + "src/main/acp/acp-timeline-grok-subagent-retention.test.ts": 1665, + "src/main/acp/acp-timeline-grok-subagents.test.ts": 1432, + "src/main/acp/acp-timeline-joins.test.ts": 733, + "src/main/acp/acp-timeline-open-enums.test.ts": 743, + "src/main/acp/acp-timeline-opencode-dialect.test.ts": 733, + "src/main/acp/acp-timeline-recordings.test.ts": 1220, + "src/main/acp/acp-timeline-recovery.test.ts": 1001, + "src/main/acp/acp-timeline-routing.test.ts": 1145, + "src/main/acp/acp-timeline-turn-failures.test.ts": 1327, + "src/main/acp/acp-visuals-skill.test.ts": 58, + "src/main/acp/acp-write-queue-retention.test.ts": 579, + "src/main/active-view-persistence-boundary.test.ts": 1839, + "src/main/active-view-preference-sync-flush-veto.test.ts": 827, + "src/main/active-view-preference.test.ts": 40, + "src/main/agent-auth-restart-preservation.test.ts": 53, + "src/main/agent-awake-service-platform-assertions.test.ts": 64, + "src/main/agent-awake-service.test.ts": 111, + "src/main/agent-cli-version-probe.test.ts": 60, + "src/main/agent-hooks/agent-home-login-shell.test.ts": 57, + "src/main/agent-hooks/branch-rename-failure-output.test.ts": 33, + "src/main/agent-hooks/ended-process-reconciliation.test.ts": 658, + "src/main/agent-hooks/first-work-branch-rename-wsl-move.test.ts": 611, + "src/main/agent-hooks/first-work-branch-rename.test.ts": 1084, + "src/main/agent-hooks/first-work-folder-rename.test.ts": 114, + "src/main/agent-hooks/grok-replay-guard.test.ts": 138, + "src/main/agent-hooks/hook-process-capture.test.ts": 96, + "src/main/agent-hooks/hook-script-outside-orca.test.ts": 275, + "src/main/agent-hooks/hook-status-session-tabs-republish.test.ts": 692, + "src/main/agent-hooks/install-telemetry.test.ts": 38, + "src/main/agent-hooks/installer-utils-remote.test.ts": 48, + "src/main/agent-hooks/installer-utils.test.ts": 201, + "src/main/agent-hooks/local-agent-cli-presence.test.ts": 66, + "src/main/agent-hooks/local-tmux-managed-pty.test.ts": 35, + "src/main/agent-hooks/locked-executable-rename.test.ts": 96, + "src/main/agent-hooks/managed-agent-hook-controls.test.ts": 131, + "src/main/agent-hooks/managed-hook-command-contract.test.ts": 398, + "src/main/agent-hooks/managed-hook-detection-commands.test.ts": 61, + "src/main/agent-hooks/managed-hook-install-lock.test.ts": 307, + "src/main/agent-hooks/managed-hook-owner-identity.test.ts": 71, + "src/main/agent-hooks/managed-hook-script-refresh-main-thread.test.ts": 388, + "src/main/agent-hooks/managed-hook-script-refresh.test.ts": 455, + "src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts": 1585, + "src/main/agent-hooks/managed-hook-timeout.test.ts": 1809, + "src/main/agent-hooks/managed-toml-ownership.test.ts": 52, + "src/main/agent-hooks/manual-compact-hook-stream.test.ts": 694, + "src/main/agent-hooks/manual-compact-status-cleanup.test.ts": 669, + "src/main/agent-hooks/migration-unsupported-pty-state.test.ts": 34, + "src/main/agent-hooks/posix-hook-json-stdin-reader.test.ts": 3473, + "src/main/agent-hooks/remote-hook-service-installers.test.ts": 496, + "src/main/agent-hooks/remote-hook-service-registry-coverage.test.ts": 410, + "src/main/agent-hooks/restored-status-dead-terminal-sweep.test.ts": 889, + "src/main/agent-hooks/server-agent-presence.test.ts": 963, + "src/main/agent-hooks/server-ai-vault-liveness.test.ts": 618, + "src/main/agent-hooks/server-amp-normalization.test.ts": 682, + "src/main/agent-hooks/server-authority-evidence.test.ts": 694, + "src/main/agent-hooks/server-claude-cancel-captures.test.ts": 844, + "src/main/agent-hooks/server-claude-child-permission-lifecycle.test.ts": 819, + "src/main/agent-hooks/server-claude-normalization.test.ts": 710, + "src/main/agent-hooks/server-claude-owed-notification-expiry.test.ts": 867, + "src/main/agent-hooks/server-claude-permission-visibility.test.ts": 767, + "src/main/agent-hooks/server-claude-statusline.test.ts": 731, + "src/main/agent-hooks/server-claude-task-wakeup-lifecycle.test.ts": 6114, + "src/main/agent-hooks/server-claude-terminal-interrupt.test.ts": 838, + "src/main/agent-hooks/server-closed-tab-suppression.test.ts": 762, + "src/main/agent-hooks/server-codex-capacity.test.ts": 800, + "src/main/agent-hooks/server-codex-ctrl-c-inference.test.ts": 658, + "src/main/agent-hooks/server-codex-interrupt-hook.test.ts": 723, + "src/main/agent-hooks/server-codex-normalization.test.ts": 673, + "src/main/agent-hooks/server-codex-subagent-transcript.test.ts": 4690, + "src/main/agent-hooks/server-codex-turn-interruption.test.ts": 4334, + "src/main/agent-hooks/server-copilot-normalization.test.ts": 974, + "src/main/agent-hooks/server-cursor-normalization.test.ts": 650, + "src/main/agent-hooks/server-endpoint-file-lifecycle.test.ts": 585, + "src/main/agent-hooks/server-escape-navigation-inference.test.ts": 771, + "src/main/agent-hooks/server-grok-background-status.test.ts": 753, + "src/main/agent-hooks/server-grok-cancel.test.ts": 686, + "src/main/agent-hooks/server-grok-discovery.test.ts": 1163, + "src/main/agent-hooks/server-hook-http-ingest.test.ts": 811, + "src/main/agent-hooks/server-ingest-remote.test.ts": 702, + "src/main/agent-hooks/server-ingest-structured-children.test.ts": 704, + "src/main/agent-hooks/server-ingest-structured-state-clock.test.ts": 687, + "src/main/agent-hooks/server-ingest-structured-status.test.ts": 789, + "src/main/agent-hooks/server-ingest-terminal-status.test.ts": 633, + "src/main/agent-hooks/server-interrupt-inference-guards.test.ts": 747, + "src/main/agent-hooks/server-interrupt-inference-resurrection.test.ts": 656, + "src/main/agent-hooks/server-interrupt-inference-validation.test.ts": 759, + "src/main/agent-hooks/server-last-status-child-held-main-agent.test.ts": 1034, + "src/main/agent-hooks/server-last-status-hydrate-confirmation.test.ts": 944, + "src/main/agent-hooks/server-last-status-hydrate-validation.test.ts": 711, + "src/main/agent-hooks/server-last-status-lead-boundary.test.ts": 756, + "src/main/agent-hooks/server-last-status-main-agent-fact.test.ts": 803, + "src/main/agent-hooks/server-last-status-restored-children.test.ts": 870, + "src/main/agent-hooks/server-last-status-write.test.ts": 813, + "src/main/agent-hooks/server-main-agent-turn-verdicts.test.ts": 1813, + "src/main/agent-hooks/server-muse-session-log-poll.test.ts": 2514, + "src/main/agent-hooks/server-nested-agent-model.test.ts": 609, + "src/main/agent-hooks/server-observation-provenance.test.ts": 715, + "src/main/agent-hooks/server-omp-retired-ingress.test.ts": 620, + "src/main/agent-hooks/server-omp-retirement-ack.test.ts": 686, + "src/main/agent-hooks/server-opencode-binder.test.ts": 879, + "src/main/agent-hooks/server-opencode-lifecycle.test.ts": 835, + "src/main/agent-hooks/server-opencode-tui-identity.test.ts": 915, + "src/main/agent-hooks/server-pane-authority.test.ts": 786, + "src/main/agent-hooks/server-pi-normalization.test.ts": 883, + "src/main/agent-hooks/server-process-lifetime-status.test.ts": 800, + "src/main/agent-hooks/server-prompt-sent-telemetry.test.ts": 612, + "src/main/agent-hooks/server-relay-listener-replay.test.ts": 672, + "src/main/agent-hooks/server-relayed-claude-cancel.test.ts": 1209, + "src/main/agent-hooks/server-reminted-pane-key.test.ts": 783, + "src/main/agent-hooks/server-removed-worktree-foreign-authority.test.ts": 807, + "src/main/agent-hooks/server-removed-worktree-status.test.ts": 712, + "src/main/agent-hooks/server-replay-evidence-clock.test.ts": 639, + "src/main/agent-hooks/server-retired-pane-new-turn.test.ts": 665, + "src/main/agent-hooks/server-start-failure-lifecycle.test.ts": 733, + "src/main/agent-hooks/server-startup-prompt-control.test.ts": 783, + "src/main/agent-hooks/server-status-listener-fanout.test.ts": 738, + "src/main/agent-hooks/server-status-persist-throttle.test.ts": 1095, + "src/main/agent-hooks/server-structured-canonical-status.test.ts": 769, + "src/main/agent-hooks/server-tmux-canonical.test.ts": 649, + "src/main/agent-hooks/server-tmux-wire.test.ts": 618, + "src/main/agent-hooks/server-transport-interference.test.ts": 1870, + "src/main/agent-hooks/server-turn-started-at.test.ts": 835, + "src/main/agent-hooks/server.claude-interactive-question.test.ts": 696, + "src/main/agent-hooks/server.test-fixtures.test.ts": 764, + "src/main/agent-hooks/server/server-row-ownership.test.ts": 660, + "src/main/agent-hooks/spool.test.ts": 782, + "src/main/agent-hooks/terminal-handle-row-identity.test.ts": 860, + "src/main/agent-hooks/windows-batch-hook-launcher.test.ts": 90, + "src/main/agent-hooks/windows-cmd-hook-command-unicode.test.ts": 110, + "src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts": 90, + "src/main/agent-hooks/windows-hook-command-hosts.test.ts": 82, + "src/main/agent-hooks/windows-hook-payload-delivery.test.ts": 208, + "src/main/agent-hooks/windows-hook-post-interpreter.test.ts": 393, + "src/main/agent-hooks/windows-native-hook-exe-hosts.test.ts": 101, + "src/main/agent-hooks/windows-powershell-hook-launcher.test.ts": 44, + "src/main/agent-hooks/wsl-guest-plugin-install.test.ts": 45, + "src/main/agent-hooks/wsl-hook-relay-launch.test.ts": 96, + "src/main/agent-hooks/wsl-hook-relay-live.integration.test.ts": 1971, + "src/main/agent-hooks/wsl-hook-relay-manager.test.ts": 2166, + "src/main/agent-hooks/wsl-hook-relay-reattach.test.ts": 1178, + "src/main/agent-hooks/wsl-hook-relay-recovery.test.ts": 46, + "src/main/agent-hooks/wsl-hook-relay-sentinel.test.ts": 54, + "src/main/agent-hooks/wsl-opencode-install-race.test.ts": 1081, + "src/main/agent-hooks/wsl-opencode-settings-lifetime.test.ts": 1458, + "src/main/agent-hooks/wsl-opencode-settings.test.ts": 1055, + "src/main/agent-launch/agent-launch-account-env.test.ts": 1307, + "src/main/agent-launch/agent-launch-decided-mode-contract.test.ts": 25, + "src/main/agent-launch/agent-launch-executor.test.ts": 1401, + "src/main/agent-launch/agent-launch-legacy-host-policy.test.ts": 1463, + "src/main/agent-launch/agent-launch-opencode-model.test.ts": 1254, + "src/main/agent-launch/agent-launch-pane-attachment.test.ts": 36, + "src/main/agent-launch/startup-agent-worktree-create.test.ts": 1204, + "src/main/agent-state-file-reader.test.ts": 61, + "src/main/agent-trust-presets.test.ts": 347, + "src/main/agent-workspace-trust-guard-failure.test.ts": 132, + "src/main/agent-workspace-trust-spawn.test.ts": 345, + "src/main/agent-workspace-trust.test.ts": 169, + "src/main/ai-vault-search/session-search-agent-compatibility.test.ts": 248, + "src/main/ai-vault-search/session-search-child-service.test.ts": 376, + "src/main/ai-vault-search/session-search-content-hash.test.ts": 38, + "src/main/ai-vault-search/session-search-cwd-key.test.ts": 52, + "src/main/ai-vault-search/session-search-deleted-sources.test.ts": 395, + "src/main/ai-vault-search/session-search-dependent-transcripts.test.ts": 739, + "src/main/ai-vault-search/session-search-directory-listings.test.ts": 39, + "src/main/ai-vault-search/session-search-engine.test.ts": 1664, + "src/main/ai-vault-search/session-search-file-id.test.ts": 185, + "src/main/ai-vault-search/session-search-file-write.test.ts": 618, + "src/main/ai-vault-search/session-search-fts5-contract.test.ts": 143, + "src/main/ai-vault-search/session-search-hit-ranking.test.ts": 60, + "src/main/ai-vault-search/session-search-host-registration.test.ts": 979, + "src/main/ai-vault-search/session-search-identifier-split.test.ts": 38, + "src/main/ai-vault-search/session-search-index-consumer.test.ts": 244, + "src/main/ai-vault-search/session-search-index-generation.test.ts": 449, + "src/main/ai-vault-search/session-search-index-pass.test.ts": 507, + "src/main/ai-vault-search/session-search-index-writer.test.ts": 402, + "src/main/ai-vault-search/session-search-indexer.test.ts": 2684, + "src/main/ai-vault-search/session-search-instance.test.ts": 1182, + "src/main/ai-vault-search/session-search-lifecycle-matrix.test.ts": 3442, + "src/main/ai-vault-search/session-search-list-reparse.test.ts": 384, + "src/main/ai-vault-search/session-search-live-transcript.test.ts": 344, + "src/main/ai-vault-search/session-search-merged-roots.test.ts": 494, + "src/main/ai-vault-search/session-search-message-rows.test.ts": 139, + "src/main/ai-vault-search/session-search-native-chat-indexing.test.ts": 547, + "src/main/ai-vault-search/session-search-opencode-index.test.ts": 788, + "src/main/ai-vault-search/session-search-orphan-rows.test.ts": 244, + "src/main/ai-vault-search/session-search-paging.test.ts": 642, + "src/main/ai-vault-search/session-search-query-planner.test.ts": 42, + "src/main/ai-vault-search/session-search-read-decision.test.ts": 31, + "src/main/ai-vault-search/session-search-retention-delete.test.ts": 206, + "src/main/ai-vault-search/session-search-retention-policy.test.ts": 224, + "src/main/ai-vault-search/session-search-retrieval-statement-compilation.test.ts": 516, + "src/main/ai-vault-search/session-search-root-refresh.test.ts": 500, + "src/main/ai-vault-search/session-search-row-filter.test.ts": 205, + "src/main/ai-vault-search/session-search-row-identity.test.ts": 131, + "src/main/ai-vault-search/session-search-scan-roots.test.ts": 198, + "src/main/ai-vault-search/session-search-schema.test.ts": 403, + "src/main/ai-vault-search/session-search-scope-entry-points.test.ts": 4341, + "src/main/ai-vault-search/session-search-scope-resolution.test.ts": 98, + "src/main/ai-vault-search/session-search-scope-service.test.ts": 293, + "src/main/ai-vault-search/session-search-service-registry.test.ts": 257, + "src/main/ai-vault-search/session-search-service.test.ts": 329, + "src/main/ai-vault-search/session-search-sidebar-parity.test.ts": 318, + "src/main/ai-vault-search/session-search-snippet-marks.test.ts": 233, + "src/main/ai-vault-search/session-search-sqlite-support.test.ts": 37, + "src/main/ai-vault-search/session-search-store-is-memory.test.ts": 1107, + "src/main/ai-vault-search/session-search-store-scope-catalog.test.ts": 45, + "src/main/ai-vault-search/session-search-synthetic-corpus.test.ts": 341, + "src/main/ai-vault-search/session-search-transcript-dependencies.test.ts": 389, + "src/main/ai-vault-search/session-search-typo-policy.test.ts": 86, + "src/main/ai-vault-search/session-search-typo-scope.test.ts": 123, + "src/main/ai-vault-search/session-search-write-lifetime.test.ts": 326, + "src/main/ai-vault/ai-vault-scan-cancellation.test.ts": 31, + "src/main/ai-vault/ai-vault-scan-coordinator.test.ts": 35, + "src/main/ai-vault/ai-vault-scan-progress.test.ts": 313, + "src/main/ai-vault/antigravity-ide-history.test.ts": 344, + "src/main/ai-vault/antigravity-index-file-admission.test.ts": 129, + "src/main/ai-vault/antigravity-opening-prompt-workspace.test.ts": 173, + "src/main/ai-vault/antigravity-recorded-history.test.ts": 239, + "src/main/ai-vault/cached-session-list-wsl-probe.test.ts": 316, + "src/main/ai-vault/cached-session-list.test.ts": 383, + "src/main/ai-vault/claude-project-dir-encoding.test.ts": 40, + "src/main/ai-vault/codex-session-collection.test.ts": 91, + "src/main/ai-vault/codex-session-root-dedup.test.ts": 122, + "src/main/ai-vault/local-log-tail-reader.test.ts": 50, "src/main/ai-vault/omp-session-root.test.ts": 234, - "src/main/ai-vault/openclaw-session-layout.test.ts": 172, - "src/main/ai-vault/opencode-wsl-runtime-preparation.test.ts": 1150, - "src/main/ai-vault/remote-session-content-lines.test.ts": 84, - "src/main/ai-vault/runtime-session-scanner.test.ts": 256, - "src/main/ai-vault/runtime-session-search-call.test.ts": 44, - "src/main/ai-vault/session-cwd-bucket-layouts.test.ts": 59, - "src/main/ai-vault/session-delete-target.test.ts": 224, - "src/main/ai-vault/session-delete.test.ts": 220, - "src/main/ai-vault/session-document-stream-boundaries.test.ts": 155, - "src/main/ai-vault/session-document-stream-contract.test.ts": 681, - "src/main/ai-vault/session-document-stream-parity.test.ts": 201, - "src/main/ai-vault/session-document-stream-projection.test.ts": 145, - "src/main/ai-vault/session-first-user-prompt-opencode.test.ts": 232, - "src/main/ai-vault/session-first-user-prompt-read.test.ts": 302, - "src/main/ai-vault/session-list-result-validation.test.ts": 235, - "src/main/ai-vault/session-list-results.test.ts": 136, - "src/main/ai-vault/session-newest-files.test.ts": 120, - "src/main/ai-vault/session-parse-cache-antigravity-upgrade.test.ts": 278, - "src/main/ai-vault/session-parse-cache-bounds.test.ts": 498, - "src/main/ai-vault/session-parse-cache-devin-upgrade.test.ts": 244, - "src/main/ai-vault/session-parse-cache-persistence.test.ts": 348, - "src/main/ai-vault/session-scan-cutoff.test.ts": 125, - "src/main/ai-vault/session-scanner-agent-root-overrides.test.ts": 601, - "src/main/ai-vault/session-scanner-antigravity-parser.test.ts": 114, - "src/main/ai-vault/session-scanner-antigravity-source.test.ts": 337, - "src/main/ai-vault/session-scanner-claude-cwd-drift.test.ts": 265, - "src/main/ai-vault/session-scanner-claude-subagent-prune.test.ts": 326, - "src/main/ai-vault/session-scanner-claude-subagents.test.ts": 186, - "src/main/ai-vault/session-scanner-claude-title.test.ts": 333, - "src/main/ai-vault/session-scanner-claude-unicode-scope.test.ts": 353, - "src/main/ai-vault/session-scanner-cline-parser.test.ts": 311, - "src/main/ai-vault/session-scanner-codebuddy-scope.test.ts": 236, - "src/main/ai-vault/session-scanner-codebuddy-source.test.ts": 212, - "src/main/ai-vault/session-scanner-codex-dual-root.test.ts": 327, - "src/main/ai-vault/session-scanner-codex-fast-path.test.ts": 352, - "src/main/ai-vault/session-scanner-codex-non-user-origin.test.ts": 99, - "src/main/ai-vault/session-scanner-codex-parser.test.ts": 152, - "src/main/ai-vault/session-scanner-codex-title-index.test.ts": 255, - "src/main/ai-vault/session-scanner-codex-tool-records.test.ts": 156, - "src/main/ai-vault/session-scanner-codex-workers.test.ts": 306, - "src/main/ai-vault/session-scanner-core-parser-wsl-stall.test.ts": 318, - "src/main/ai-vault/session-scanner-cursor-chat-meta.test.ts": 433, - "src/main/ai-vault/session-scanner-cutoff.test.ts": 599, - "src/main/ai-vault/session-scanner-dedup-batches.test.ts": 510, - "src/main/ai-vault/session-scanner-devin-contention.test.ts": 367, - "src/main/ai-vault/session-scanner-devin-db.test.ts": 493, - "src/main/ai-vault/session-scanner-devin-dedup.test.ts": 383, - "src/main/ai-vault/session-scanner-devin-parser.test.ts": 127, - "src/main/ai-vault/session-scanner-directory-reader.test.ts": 97, - "src/main/ai-vault/session-scanner-discovery-wsl-gate.test.ts": 120, - "src/main/ai-vault/session-scanner-first-user-prompt.test.ts": 63, - "src/main/ai-vault/session-scanner-graph-parsers.test.ts": 138, - "src/main/ai-vault/session-scanner-grok-parser.test.ts": 124, - "src/main/ai-vault/session-scanner-grok-user-text.test.ts": 51, - "src/main/ai-vault/session-scanner-index-cache-wsl-stall.test.ts": 121, - "src/main/ai-vault/session-scanner-injected-title.test.ts": 295, - "src/main/ai-vault/session-scanner-jcode-parser.test.ts": 127, - "src/main/ai-vault/session-scanner-jsonl-reader.test.ts": 324, - "src/main/ai-vault/session-scanner-kimi-index-cache.test.ts": 119, - "src/main/ai-vault/session-scanner-kimi-parser.test.ts": 201, - "src/main/ai-vault/session-scanner-kiro-parser.test.ts": 124, - "src/main/ai-vault/session-scanner-muse.test.ts": 277, - "src/main/ai-vault/session-scanner-omp-subagent-listing.test.ts": 156, - "src/main/ai-vault/session-scanner-omp-subagent-prune.test.ts": 361, - "src/main/ai-vault/session-scanner-omp-subagent-transcripts.test.ts": 122, - "src/main/ai-vault/session-scanner-omp-title.test.ts": 161, - "src/main/ai-vault/session-scanner-opencode-cancellation.test.ts": 678, - "src/main/ai-vault/session-scanner-opencode-native-page-limit.test.ts": 64, - "src/main/ai-vault/session-scanner-opencode-native-worker.test.ts": 721, - "src/main/ai-vault/session-scanner-opencode-parser.test.ts": 126, - "src/main/ai-vault/session-scanner-opencode-process-admission.test.ts": 275, - "src/main/ai-vault/session-scanner-opencode-sources-wsl-stall.test.ts": 124, - "src/main/ai-vault/session-scanner-opencode-sources.test.ts": 53, - "src/main/ai-vault/session-scanner-opencode-sqlite-bounds.test.ts": 1475, - "src/main/ai-vault/session-scanner-opencode-sqlite-coexistence.test.ts": 398, - "src/main/ai-vault/session-scanner-opencode-sqlite-open.test.ts": 3525, - "src/main/ai-vault/session-scanner-opencode-sqlite-paths.test.ts": 53, - "src/main/ai-vault/session-scanner-opencode-sqlite-process.test.ts": 3438, - "src/main/ai-vault/session-scanner-opencode-sqlite-scan-scope.test.ts": 54, - "src/main/ai-vault/session-scanner-opencode-sqlite-worker-client.test.ts": 148, - "src/main/ai-vault/session-scanner-opencode-sqlite-worker-spawn.test.ts": 118, - "src/main/ai-vault/session-scanner-opencode-sqlite.test.ts": 432, - "src/main/ai-vault/session-scanner-opencode-wsl-client.test.ts": 144, - "src/main/ai-vault/session-scanner-opencode-wsl-routing.test.ts": 192, - "src/main/ai-vault/session-scanner-opencode2-message.test.ts": 86, - "src/main/ai-vault/session-scanner-opencode2-sqlite.test.ts": 332, - "src/main/ai-vault/session-scanner-parse-cache-agents.test.ts": 541, - "src/main/ai-vault/session-scanner-parse-cache.test.ts": 310, - "src/main/ai-vault/session-scanner-parse-wsl-stall.test.ts": 275, - "src/main/ai-vault/session-scanner-parser-stream-cleanup.test.ts": 172, - "src/main/ai-vault/session-scanner-pi-scope.test.ts": 394, - "src/main/ai-vault/session-scanner-preview-window-truncation.test.ts": 111, - "src/main/ai-vault/session-scanner-qoder.test.ts": 319, - "src/main/ai-vault/session-scanner-record-budget.test.ts": 393, - "src/main/ai-vault/session-scanner-recoverable-empty.test.ts": 338, - "src/main/ai-vault/session-scanner-roots.test.ts": 184, - "src/main/ai-vault/session-scanner-scope.test.ts": 339, - "src/main/ai-vault/session-scanner-service-cancellation.test.ts": 417, - "src/main/ai-vault/session-scanner-service-client.test.ts": 502, - "src/main/ai-vault/session-scanner-service-entry-path.test.ts": 50, - "src/main/ai-vault/session-scanner-service-entry-subagents.test.ts": 649, - "src/main/ai-vault/session-scanner-service-entry.test.ts": 813, - "src/main/ai-vault/session-scanner-service-env.test.ts": 69, - "src/main/ai-vault/session-scanner-service-restart-policy.test.ts": 54, - "src/main/ai-vault/session-scanner-service-root-request.test.ts": 50, - "src/main/ai-vault/session-scanner-service-root-response.test.ts": 219, - "src/main/ai-vault/session-scanner-service-search-roots.test.ts": 604, - "src/main/ai-vault/session-scanner-service-search.test.ts": 1251, - "src/main/ai-vault/session-scanner-service-spawn.test.ts": 501, - "src/main/ai-vault/session-scanner-timeline.test.ts": 140, - "src/main/ai-vault/session-scanner-unlimited-dedup.test.ts": 3871, - "src/main/ai-vault/session-scanner-values.test.ts": 114, - "src/main/ai-vault/session-scanner-zcode-sources.test.ts": 131, - "src/main/ai-vault/session-scanner-zcode.test.ts": 313, - "src/main/ai-vault/session-scanner.test.ts": 425, - "src/main/ai-vault/session-sidecar-stat.test.ts": 68, - "src/main/ai-vault/session-title-file-reader-wsl-stall.test.ts": 275, - "src/main/ai-vault/session-title-file-reader.test.ts": 126, - "src/main/ai-vault/session-transcript-consumers.test.ts": 386, - "src/main/ai-vault/session-transcript-every-agent-capture.test.ts": 680, - "src/main/ai-vault/session-transcript-message-content.test.ts": 212, - "src/main/ai-vault/session-transcript-record-budget.test.ts": 41, - "src/main/ai-vault/structured-session-ownership.test.ts": 283, - "src/main/amp/hook-service.test.ts": 70, - "src/main/antigravity/hook-service.test.ts": 210, - "src/main/antigravity/native-account-host.test.ts": 43, - "src/main/antigravity/native-account-launch.test.ts": 107, - "src/main/antigravity/native-account-service.test.ts": 84, - "src/main/antigravity/native-account-store.test.ts": 255, - "src/main/antigravity/native-credential-backend.test.ts": 71, + "src/main/ai-vault/openclaw-session-layout.test.ts": 154, + "src/main/ai-vault/opencode-wsl-runtime-preparation.test.ts": 1213, + "src/main/ai-vault/remote-session-content-lines.test.ts": 55, + "src/main/ai-vault/runtime-session-scanner.test.ts": 278, + "src/main/ai-vault/runtime-session-search-call.test.ts": 34, + "src/main/ai-vault/session-cwd-bucket-layouts.test.ts": 38, + "src/main/ai-vault/session-delete-target.test.ts": 169, + "src/main/ai-vault/session-delete.test.ts": 232, + "src/main/ai-vault/session-document-stream-boundaries.test.ts": 176, + "src/main/ai-vault/session-document-stream-contract.test.ts": 229, + "src/main/ai-vault/session-document-stream-parity.test.ts": 155, + "src/main/ai-vault/session-document-stream-projection.test.ts": 114, + "src/main/ai-vault/session-first-user-prompt-opencode.test.ts": 244, + "src/main/ai-vault/session-first-user-prompt-read.test.ts": 250, + "src/main/ai-vault/session-list-fingerprint.test.ts": 36, + "src/main/ai-vault/session-list-result-validation.test.ts": 208, + "src/main/ai-vault/session-list-results.test.ts": 133, + "src/main/ai-vault/session-newest-files.test.ts": 98, + "src/main/ai-vault/session-parse-cache-antigravity-upgrade.test.ts": 265, + "src/main/ai-vault/session-parse-cache-bounds.test.ts": 281, + "src/main/ai-vault/session-parse-cache-checkpoint.test.ts": 3873, + "src/main/ai-vault/session-parse-cache-devin-upgrade.test.ts": 223, + "src/main/ai-vault/session-parse-cache-metadata-checkpoint.test.ts": 299, + "src/main/ai-vault/session-parse-cache-persistence.test.ts": 378, + "src/main/ai-vault/session-parse-cache-save-lane.test.ts": 256, + "src/main/ai-vault/session-scan-cutoff.test.ts": 154, + "src/main/ai-vault/session-scanner-agent-root-overrides.test.ts": 683, + "src/main/ai-vault/session-scanner-antigravity-parser.test.ts": 130, + "src/main/ai-vault/session-scanner-antigravity-source.test.ts": 348, + "src/main/ai-vault/session-scanner-claude-cwd-drift.test.ts": 270, + "src/main/ai-vault/session-scanner-claude-subagent-prune.test.ts": 338, + "src/main/ai-vault/session-scanner-claude-subagents.test.ts": 157, + "src/main/ai-vault/session-scanner-claude-title.test.ts": 270, + "src/main/ai-vault/session-scanner-claude-unicode-scope.test.ts": 332, + "src/main/ai-vault/session-scanner-cline-parser.test.ts": 325, + "src/main/ai-vault/session-scanner-codebuddy-scope.test.ts": 301, + "src/main/ai-vault/session-scanner-codebuddy-source.test.ts": 295, + "src/main/ai-vault/session-scanner-codex-dual-root.test.ts": 369, + "src/main/ai-vault/session-scanner-codex-fast-path.test.ts": 305, + "src/main/ai-vault/session-scanner-codex-non-user-origin.test.ts": 94, + "src/main/ai-vault/session-scanner-codex-parser.test.ts": 147, + "src/main/ai-vault/session-scanner-codex-title-index.test.ts": 233, + "src/main/ai-vault/session-scanner-codex-tool-records.test.ts": 123, + "src/main/ai-vault/session-scanner-codex-workers.test.ts": 300, + "src/main/ai-vault/session-scanner-core-parser-wsl-stall.test.ts": 300, + "src/main/ai-vault/session-scanner-cursor-chat-meta.test.ts": 420, + "src/main/ai-vault/session-scanner-cutoff.test.ts": 473, + "src/main/ai-vault/session-scanner-dedup-batches.test.ts": 366, + "src/main/ai-vault/session-scanner-devin-contention.test.ts": 298, + "src/main/ai-vault/session-scanner-devin-db.test.ts": 542, + "src/main/ai-vault/session-scanner-devin-dedup.test.ts": 302, + "src/main/ai-vault/session-scanner-devin-parser.test.ts": 114, + "src/main/ai-vault/session-scanner-directory-reader.test.ts": 91, + "src/main/ai-vault/session-scanner-discovery-pipeline.test.ts": 174, + "src/main/ai-vault/session-scanner-discovery-wsl-gate.test.ts": 94, + "src/main/ai-vault/session-scanner-first-user-prompt.test.ts": 49, + "src/main/ai-vault/session-scanner-graph-parsers.test.ts": 112, + "src/main/ai-vault/session-scanner-grok-parser.test.ts": 133, + "src/main/ai-vault/session-scanner-grok-user-text.test.ts": 38, + "src/main/ai-vault/session-scanner-index-cache-wsl-stall.test.ts": 157, + "src/main/ai-vault/session-scanner-injected-title.test.ts": 296, + "src/main/ai-vault/session-scanner-jcode-parser.test.ts": 95, + "src/main/ai-vault/session-scanner-jsonl-reader.test.ts": 295, + "src/main/ai-vault/session-scanner-kimi-index-cache.test.ts": 110, + "src/main/ai-vault/session-scanner-kimi-parser.test.ts": 280, + "src/main/ai-vault/session-scanner-kiro-parser.test.ts": 137, + "src/main/ai-vault/session-scanner-muse.test.ts": 340, + "src/main/ai-vault/session-scanner-omp-subagent-listing.test.ts": 121, + "src/main/ai-vault/session-scanner-omp-subagent-prune.test.ts": 313, + "src/main/ai-vault/session-scanner-omp-subagent-transcripts.test.ts": 144, + "src/main/ai-vault/session-scanner-omp-title.test.ts": 129, + "src/main/ai-vault/session-scanner-opencode-cancellation.test.ts": 727, + "src/main/ai-vault/session-scanner-opencode-native-page-limit.test.ts": 65, + "src/main/ai-vault/session-scanner-opencode-native-worker.test.ts": 739, + "src/main/ai-vault/session-scanner-opencode-parser.test.ts": 114, + "src/main/ai-vault/session-scanner-opencode-process-admission.test.ts": 254, + "src/main/ai-vault/session-scanner-opencode-sources-wsl-stall.test.ts": 131, + "src/main/ai-vault/session-scanner-opencode-sources.test.ts": 43, + "src/main/ai-vault/session-scanner-opencode-sqlite-bounds.test.ts": 990, + "src/main/ai-vault/session-scanner-opencode-sqlite-coexistence.test.ts": 677, + "src/main/ai-vault/session-scanner-opencode-sqlite-open.test.ts": 3553, + "src/main/ai-vault/session-scanner-opencode-sqlite-paths.test.ts": 36, + "src/main/ai-vault/session-scanner-opencode-sqlite-process.test.ts": 3322, + "src/main/ai-vault/session-scanner-opencode-sqlite-scan-scope.test.ts": 45, + "src/main/ai-vault/session-scanner-opencode-sqlite-worker-client.test.ts": 107, + "src/main/ai-vault/session-scanner-opencode-sqlite-worker-spawn.test.ts": 142, + "src/main/ai-vault/session-scanner-opencode-sqlite.test.ts": 414, + "src/main/ai-vault/session-scanner-opencode-wsl-client.test.ts": 102, + "src/main/ai-vault/session-scanner-opencode-wsl-routing.test.ts": 162, + "src/main/ai-vault/session-scanner-opencode2-message.test.ts": 67, + "src/main/ai-vault/session-scanner-opencode2-sqlite.test.ts": 260, + "src/main/ai-vault/session-scanner-parse-cache-agents.test.ts": 550, + "src/main/ai-vault/session-scanner-parse-cache.test.ts": 352, + "src/main/ai-vault/session-scanner-parse-wsl-stall.test.ts": 227, + "src/main/ai-vault/session-scanner-parser-stream-cleanup.test.ts": 161, + "src/main/ai-vault/session-scanner-pi-scope.test.ts": 277, + "src/main/ai-vault/session-scanner-preview-window-truncation.test.ts": 88, + "src/main/ai-vault/session-scanner-qoder.test.ts": 271, + "src/main/ai-vault/session-scanner-record-budget.test.ts": 520, + "src/main/ai-vault/session-scanner-recoverable-empty.test.ts": 360, + "src/main/ai-vault/session-scanner-roots.test.ts": 174, + "src/main/ai-vault/session-scanner-scope.test.ts": 307, + "src/main/ai-vault/session-scanner-service-cancellation.test.ts": 372, + "src/main/ai-vault/session-scanner-service-client.test.ts": 509, + "src/main/ai-vault/session-scanner-service-entry-path.test.ts": 45, + "src/main/ai-vault/session-scanner-service-entry-subagents.test.ts": 629, + "src/main/ai-vault/session-scanner-service-entry.test.ts": 931, + "src/main/ai-vault/session-scanner-service-env.test.ts": 54, + "src/main/ai-vault/session-scanner-service-request-deadline.test.ts": 71, + "src/main/ai-vault/session-scanner-service-restart-policy.test.ts": 40, + "src/main/ai-vault/session-scanner-service-root-request.test.ts": 44, + "src/main/ai-vault/session-scanner-service-root-response.test.ts": 218, + "src/main/ai-vault/session-scanner-service-search-roots.test.ts": 625, + "src/main/ai-vault/session-scanner-service-search.test.ts": 1338, + "src/main/ai-vault/session-scanner-service-spawn.test.ts": 338, + "src/main/ai-vault/session-scanner-timeline.test.ts": 98, + "src/main/ai-vault/session-scanner-unlimited-dedup.test.ts": 3572, + "src/main/ai-vault/session-scanner-values.test.ts": 107, + "src/main/ai-vault/session-scanner-zcode-sources.test.ts": 98, + "src/main/ai-vault/session-scanner-zcode.test.ts": 406, + "src/main/ai-vault/session-scanner.test.ts": 538, + "src/main/ai-vault/session-sidecar-stat.test.ts": 44, + "src/main/ai-vault/session-title-file-reader-wsl-stall.test.ts": 253, + "src/main/ai-vault/session-title-file-reader.test.ts": 168, + "src/main/ai-vault/session-transcript-consumers.test.ts": 494, + "src/main/ai-vault/session-transcript-every-agent-capture.test.ts": 605, + "src/main/ai-vault/session-transcript-message-content.test.ts": 238, + "src/main/ai-vault/session-transcript-record-budget.test.ts": 46, + "src/main/ai-vault/shell-command-words.test.ts": 392, + "src/main/ai-vault/structured-session-ownership-acp-and-pi.test.ts": 1271, + "src/main/ai-vault/structured-session-ownership-client-audience.test.ts": 1147, + "src/main/ai-vault/structured-session-ownership-shell-commands.test.ts": 1251, + "src/main/ai-vault/structured-session-ownership.test.ts": 1243, + "src/main/amp/hook-service.test.ts": 54, + "src/main/antigravity/hook-service.test.ts": 197, + "src/main/antigravity/native-account-host.test.ts": 37, + "src/main/antigravity/native-account-launch.test.ts": 110, + "src/main/antigravity/native-account-service.test.ts": 79, + "src/main/antigravity/native-account-store.test.ts": 183, + "src/main/antigravity/native-credential-backend.test.ts": 83, "src/main/antigravity/native-credential-codec.test.ts": 44, - "src/main/antigravity/native-macos-credentials.real.test.ts": 73, - "src/main/antigravity/native-macos-credentials.test.ts": 75, - "src/main/antigravity/posix-hook-invocation.test.ts": 5420, - "src/main/antigravity/windows-hook-json-post.test.ts": 9898, - "src/main/antigravity/windows-hook-payload-delivery.test.ts": 126, - "src/main/app-icon.test.ts": 80, - "src/main/app-relaunch.test.ts": 64, - "src/main/appimage-runtime-identity.test.ts": 87, - "src/main/appkit-scene-mutation.test.ts": 49, - "src/main/artifacts/artifact-cloud-config.test.ts": 60, - "src/main/artifacts/artifact-cloud-recovery.test.ts": 577, - "src/main/artifacts/artifact-cloud-service-races.test.ts": 512, - "src/main/artifacts/artifact-cloud-service.test.ts": 1194, - "src/main/artifacts/artifact-create-intent-store.test.ts": 245, - "src/main/artifacts/artifact-recovery-directory-fsync.test.ts": 90, - "src/main/artifacts/artifact-share-record-allocation.test.ts": 179, - "src/main/artifacts/artifact-share-record-store.test.ts": 240, - "src/main/asar-transparent-fs.test.ts": 53, - "src/main/automations/automation-dispatch-host-fence.test.ts": 1715, - "src/main/automations/automation-owner-fencing.test.ts": 1763, - "src/main/automations/automation-owner-migration.test.ts": 80, - "src/main/automations/automation-run-terminal-surface.test.ts": 4629, - "src/main/automations/automation-run-writer.test.ts": 292, - "src/main/automations/automation-skip-coalescing.test.ts": 3604, - "src/main/automations/automation-ssh-readoption-migration.test.ts": 68, - "src/main/automations/automation-update-host-retarget.test.ts": 1497, - "src/main/automations/automation-worker-durability.test.ts": 5967, - "src/main/automations/automation-workspace-host-attribution.test.ts": 1786, - "src/main/automations/automation-zero-grace-tick-latency.test.ts": 1950, - "src/main/automations/dispatch-refusal.test.ts": 71, - "src/main/automations/dispatch-tokens.test.ts": 88, - "src/main/automations/external-automation-manager-cache.test.ts": 83, - "src/main/automations/external-automation-owner-guard.test.ts": 320, - "src/main/automations/external-automation-probe-scheduler.test.ts": 175, - "src/main/automations/external-manager-scoped.test.ts": 404, - "src/main/automations/external-manager.test.ts": 359, - "src/main/automations/headless-automation-agent-launch.test.ts": 845, - "src/main/automations/headless-dispatch-durability.test.ts": 414, - "src/main/automations/headless-run-terminal-retention.test.ts": 120, - "src/main/automations/headless-workspace-create.test.ts": 49, - "src/main/automations/hermes-cron-output.test.ts": 118, - "src/main/automations/precheck-runner.test.ts": 1267, - "src/main/automations/refused-manual-run.test.ts": 6693, - "src/main/automations/retained-run-reconciliation.test.ts": 1759, - "src/main/automations/run-completion-watcher.test.ts": 2643, - "src/main/automations/run-target-resolution.test.ts": 69, - "src/main/automations/runtime-automation-service-retention.test.ts": 1414, - "src/main/automations/runtime-automation-service.test.ts": 1101, - "src/main/automations/runtime-terminal-run-observer-evidence.test.ts": 64, - "src/main/automations/runtime-terminal-run-observer.test.ts": 82, - "src/main/automations/schedule-drift-report.test.ts": 63, - "src/main/automations/service-precheck.test.ts": 1669, - "src/main/automations/service.test.ts": 1985, - "src/main/azure-devops/azure-devops-api-request.test.ts": 62, - "src/main/azure-devops/client.test.ts": 566, - "src/main/azure-devops/pull-request-creation.test.ts": 351, - "src/main/azure-devops/pull-request-mappers.test.ts": 46, - "src/main/azure-devops/repository-ref.test.ts": 362, - "src/main/bitbucket/client.test.ts": 411, - "src/main/bitbucket/credential-connection.test.ts": 130, - "src/main/bitbucket/credential-store.test.ts": 178, - "src/main/bitbucket/pull-request-creation.test.ts": 420, - "src/main/bitbucket/pull-request-mappers.test.ts": 40, - "src/main/bitbucket/repository-ref.test.ts": 360, - "src/main/bitbucket/status-no-decrypt.test.ts": 196, - "src/main/browser/agent-browser-bridge-capture-paint-hold.test.ts": 581, - "src/main/browser/agent-browser-bridge-command-transport.test.ts": 563, - "src/main/browser/agent-browser-bridge-keypress-input.test.ts": 336, - "src/main/browser/agent-browser-bridge-mouse-input.test.ts": 372, - "src/main/browser/agent-browser-bridge-navigation.test.ts": 473, - "src/main/browser/agent-browser-bridge-pointer-input.test.ts": 335, - "src/main/browser/agent-browser-bridge-raw-process-retention.test.ts": 485, - "src/main/browser/agent-browser-bridge-raw-process.test.ts": 1056, - "src/main/browser/agent-browser-bridge-screenshot-format.test.ts": 318, - "src/main/browser/agent-browser-bridge-session-lifecycle.test.ts": 751, - "src/main/browser/agent-browser-bridge-tab-routing.test.ts": 488, - "src/main/browser/agent-browser-bridge-text-input.test.ts": 633, - "src/main/browser/agent-browser-orphan-sweep.test.ts": 63, - "src/main/browser/agent-browser-process-environment.test.ts": 53, - "src/main/browser/agent-browser-session-reset.test.ts": 60, - "src/main/browser/browser-certificate-trust-controller.test.ts": 436, - "src/main/browser/browser-client-download-relay.test.ts": 257, - "src/main/browser/browser-client-download-routing.test.ts": 239, - "src/main/browser/browser-client-file-channel-negotiation.test.ts": 307, - "src/main/browser/browser-client-file-channel-reconnect.test.ts": 536, - "src/main/browser/browser-client-host-attach-request.test.ts": 257, - "src/main/browser/browser-client-host-authority-replacement.test.ts": 250, - "src/main/browser/browser-client-host-command-dispatcher.test.ts": 268, - "src/main/browser/browser-client-host-command-retention.test.ts": 837, - "src/main/browser/browser-client-host-id.test.ts": 63, - "src/main/browser/browser-client-host-placement-preparation.test.ts": 95, - "src/main/browser/browser-client-host-published-url-wiring.test.ts": 537, - "src/main/browser/browser-client-host-reconciliation-command-order.test.ts": 257, - "src/main/browser/browser-client-network-route-registry.test.ts": 292, - "src/main/browser/browser-client-page-automation-runtime.test.ts": 61, - "src/main/browser/browser-client-page-command-executor-fencing.test.ts": 274, - "src/main/browser/browser-client-page-command-executor.test.ts": 311, - "src/main/browser/browser-client-page-command-integration.test.ts": 345, - "src/main/browser/browser-client-page-execution-host-supersession.test.ts": 414, - "src/main/browser/browser-client-page-inventory.test.ts": 232, - "src/main/browser/browser-client-page-metadata-transport.test.ts": 260, - "src/main/browser/browser-client-page-published-url.test.ts": 257, - "src/main/browser/browser-client-page-reconciliation-adapters.test.ts": 394, - "src/main/browser/browser-client-page-renderer-bridge.test.ts": 258, - "src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts": 840, - "src/main/browser/browser-client-page-renderer-runtime.test.ts": 230, - "src/main/browser/browser-client-page-unavailability.test.ts": 316, - "src/main/browser/browser-client-page-upload-routing.test.ts": 366, - "src/main/browser/browser-client-route-cookie-import-partition-parity.test.ts": 388, - "src/main/browser/browser-client-route-cookie-import.test.ts": 89, - "src/main/browser/browser-client-upload-buffer-retention.test.ts": 530, - "src/main/browser/browser-client-upload-command.test.ts": 261, - "src/main/browser/browser-client-upload-staging.test.ts": 311, - "src/main/browser/browser-client-upload-transfer.test.ts": 247, - "src/main/browser/browser-cookie-clear-preserve.test.ts": 136, - "src/main/browser/browser-cookie-clear-store-lifecycle.test.ts": 81, - "src/main/browser/browser-cookie-clear-store.test.ts": 83, - "src/main/browser/browser-cookie-decryption.test.ts": 67, - "src/main/browser/browser-cookie-import-clear-atomicity.test.ts": 75, - "src/main/browser/browser-cookie-import-concurrency.test.ts": 550, - "src/main/browser/browser-cookie-import-google-exclusion.test.ts": 472, - "src/main/browser/browser-cookie-import-partition-fidelity.test.ts": 603, - "src/main/browser/browser-cookie-import-partition-reland.electron.test.ts": 1241, - "src/main/browser/browser-cookie-import-partition-rollback.electron.test.ts": 643, - "src/main/browser/browser-cookie-import-partition-success.electron.test.ts": 948, - "src/main/browser/browser-cookie-import-partition.electron.test.ts": 782, - "src/main/browser/browser-cookie-import-plan-writes.test.ts": 80, - "src/main/browser/browser-cookie-import-policy.test.ts": 202, - "src/main/browser/browser-cookie-import-replace-partition-rollback.electron.test.ts": 918, - "src/main/browser/browser-cookie-import-replacement.test.ts": 539, - "src/main/browser/browser-cookie-import-route-partition-staging.test.ts": 378, - "src/main/browser/browser-cookie-import-scope.test.ts": 403, - "src/main/browser/browser-cookie-import-snapshot-ownership.test.ts": 528, - "src/main/browser/browser-cookie-import-undecryptable.test.ts": 753, - "src/main/browser/browser-cookie-import-validated-partition.electron.test.ts": 723, - "src/main/browser/browser-cookie-import-write.test.ts": 75, - "src/main/browser/browser-cookie-import.comet.test.ts": 939, - "src/main/browser/browser-cookie-import.helium.test.ts": 892, - "src/main/browser/browser-cookie-import.test.ts": 1375, - "src/main/browser/browser-cookie-public-suffix-scope.test.ts": 71, - "src/main/browser/browser-cookie-registrable-family.test.ts": 77, - "src/main/browser/browser-cookie-samesite.electron.test.ts": 796, - "src/main/browser/browser-cookie-source-partition.test.ts": 67, - "src/main/browser/browser-cookie-validation.test.ts": 79, - "src/main/browser/browser-download-destination.test.ts": 72, - "src/main/browser/browser-execution-host-storage-identity.test.ts": 256, - "src/main/browser/browser-google-auth-ua.test.ts": 52, - "src/main/browser/browser-grab-payload.test.ts": 64, - "src/main/browser/browser-guest-shortcut-forwarding.test.ts": 346, - "src/main/browser/browser-guest-wheel-zoom-scroll.test.ts": 54, - "src/main/browser/browser-host-client-identity.test.ts": 81, - "src/main/browser/browser-host-lease-reconnect-delay.test.ts": 45, - "src/main/browser/browser-identity-mode-record.test.ts": 52, - "src/main/browser/browser-identity-mode-store.test.ts": 140, - "src/main/browser/browser-manager-annotation-bridge.test.ts": 382, - "src/main/browser/browser-manager-auth-user-agent.test.ts": 378, - "src/main/browser/browser-manager-client-hosted-downloads.test.ts": 467, - "src/main/browser/browser-manager-destroyed-guest-downloads.test.ts": 499, - "src/main/browser/browser-manager-destroyed-guest.test.ts": 424, - "src/main/browser/browser-manager-downloads.test.ts": 391, - "src/main/browser/browser-manager-grab-capture.test.ts": 365, - "src/main/browser/browser-manager-grab-mode.test.ts": 397, - "src/main/browser/browser-manager-grab-selection.test.ts": 446, - "src/main/browser/browser-manager-grab-shortcuts.test.ts": 413, - "src/main/browser/browser-manager-guest-lifecycle.test.ts": 356, - "src/main/browser/browser-manager-guest-policy-profile.test.ts": 394, - "src/main/browser/browser-manager-guest-shortcuts.test.ts": 388, - "src/main/browser/browser-manager-guest-visibility.test.ts": 398, - "src/main/browser/browser-manager-load-failure-replay.test.ts": 412, - "src/main/browser/browser-manager-popup-child.test.ts": 442, - "src/main/browser/browser-manager-popup-routing.test.ts": 398, - "src/main/browser/browser-manager-tab-identity.test.ts": 387, - "src/main/browser/browser-manager-viewport-crashed-guest.test.ts": 322, - "src/main/browser/browser-manager-viewport-override.test.ts": 422, - "src/main/browser/browser-manager-viewport-ownership.test.ts": 409, - "src/main/browser/browser-manager-viewport-partial-failure.test.ts": 372, - "src/main/browser/browser-manager-viewport-touch-reset.test.ts": 317, - "src/main/browser/browser-manager-worker-request-user-agent.test.ts": 376, - "src/main/browser/browser-network-deferred-socket.test.ts": 45, - "src/main/browser/browser-network-execution-route.test.ts": 236, - "src/main/browser/browser-network-tunnel-client-memory-budget.test.ts": 93, - "src/main/browser/browser-network-tunnel-client.test.ts": 129, - "src/main/browser/browser-network-tunnel-conformance.test.ts": 169, - "src/main/browser/browser-network-tunnel-outbound-memory-budget.test.ts": 52, - "src/main/browser/browser-network-tunnel-session-aggregate-memory.test.ts": 68, - "src/main/browser/browser-network-tunnel-session-stream-scoped-failure.test.ts": 103, - "src/main/browser/browser-network-tunnel-session.test.ts": 140, - "src/main/browser/browser-page-initiated-tab-budget.test.ts": 45, - "src/main/browser/browser-popup-new-tab-intent.test.ts": 38, - "src/main/browser/browser-process-user-agent.test.ts": 55, - "src/main/browser/browser-route-dns-prefetch.electron.test.ts": 4477, - "src/main/browser/browser-route-guest-popups.test.ts": 348, - "src/main/browser/browser-route-h3-egress.electron.test.ts": 18009, - "src/main/browser/browser-route-identity.test.ts": 63, - "src/main/browser/browser-route-partition-binding-capacity.test.ts": 84, - "src/main/browser/browser-route-partition-binding-store.test.ts": 122, - "src/main/browser/browser-route-partition-migration.test.ts": 105, - "src/main/browser/browser-route-partition-stability.test.ts": 292, - "src/main/browser/browser-route-partition-storage-lifecycle.test.ts": 116, - "src/main/browser/browser-route-partition-storage-retirement.test.ts": 270, - "src/main/browser/browser-route-partition-storage-runtime.test.ts": 58, - "src/main/browser/browser-route-persisted-worker-egress.electron.test.ts": 3825, - "src/main/browser/browser-route-prepared-page-rekey.test.ts": 51, - "src/main/browser/browser-route-session-registry.test.ts": 111, - "src/main/browser/browser-route-tcp-egress.electron.test.ts": 2122, - "src/main/browser/browser-route-webcontents-registry.test.ts": 428, - "src/main/browser/browser-route-webrtc-egress.electron.test.ts": 13343, - "src/main/browser/browser-screencast-dialog-settlement.test.ts": 115, - "src/main/browser/browser-screencast-first-frame-timeout.test.ts": 93, - "src/main/browser/browser-screencast-lifecycle.test.ts": 104, - "src/main/browser/browser-screencast-live-frame-allocation.test.ts": 100, - "src/main/browser/browser-screencast-snapshot-scaling.test.ts": 202, - "src/main/browser/browser-screencast-stream.test.ts": 639, + "src/main/antigravity/native-macos-credentials.real.test.ts": 58, + "src/main/antigravity/native-macos-credentials.test.ts": 54, + "src/main/antigravity/posix-hook-invocation.test.ts": 5390, + "src/main/antigravity/windows-hook-json-post.test.ts": 9873, + "src/main/antigravity/windows-hook-payload-delivery.test.ts": 107, + "src/main/app-icon.test.ts": 66, + "src/main/app-relaunch.test.ts": 68, + "src/main/appimage-runtime-identity.test.ts": 71, + "src/main/appkit-scene-mutation.test.ts": 29, + "src/main/artifacts/artifact-cloud-config.test.ts": 45, + "src/main/artifacts/artifact-cloud-recovery.test.ts": 569, + "src/main/artifacts/artifact-cloud-service-races.test.ts": 563, + "src/main/artifacts/artifact-cloud-service.test.ts": 1030, + "src/main/artifacts/artifact-create-intent-store.test.ts": 317, + "src/main/artifacts/artifact-recovery-directory-fsync.test.ts": 96, + "src/main/artifacts/artifact-share-record-allocation.test.ts": 155, + "src/main/artifacts/artifact-share-record-store.test.ts": 194, + "src/main/asar-transparent-fs.test.ts": 41, + "src/main/automations/automation-dispatch-host-fence.test.ts": 2204, + "src/main/automations/automation-owner-fencing.test.ts": 1582, + "src/main/automations/automation-owner-migration.test.ts": 82, + "src/main/automations/automation-run-terminal-surface.test.ts": 4359, + "src/main/automations/automation-run-writer.test.ts": 251, + "src/main/automations/automation-skip-coalescing.test.ts": 2416, + "src/main/automations/automation-ssh-readoption-migration.test.ts": 44, + "src/main/automations/automation-update-host-retarget.test.ts": 1609, + "src/main/automations/automation-worker-durability.test.ts": 3774, + "src/main/automations/automation-workspace-host-attribution.test.ts": 1797, + "src/main/automations/automation-wsl-move-refusal.test.ts": 1902, + "src/main/automations/automation-zero-grace-tick-latency.test.ts": 2120, + "src/main/automations/dispatch-refusal-wsl-move.test.ts": 456, + "src/main/automations/dispatch-refusal.test.ts": 426, + "src/main/automations/dispatch-tokens.test.ts": 49, + "src/main/automations/external-automation-manager-cache.test.ts": 61, + "src/main/automations/external-automation-owner-guard.test.ts": 290, + "src/main/automations/external-automation-probe-scheduler.test.ts": 152, + "src/main/automations/external-manager-scoped.test.ts": 320, + "src/main/automations/external-manager.test.ts": 515, + "src/main/automations/headless-automation-agent-launch.test.ts": 1418, + "src/main/automations/headless-dispatch-durability.test.ts": 455, + "src/main/automations/headless-run-terminal-retention.test.ts": 72, + "src/main/automations/headless-workspace-create.test.ts": 43, + "src/main/automations/precheck-runner.test.ts": 1175, + "src/main/automations/refused-manual-run.test.ts": 8614, + "src/main/automations/retained-run-reconciliation.test.ts": 1517, + "src/main/automations/run-completion-watcher.test.ts": 2121, + "src/main/automations/run-target-resolution.test.ts": 51, + "src/main/automations/runtime-automation-service-retention.test.ts": 1498, + "src/main/automations/runtime-automation-service.test.ts": 1210, + "src/main/automations/runtime-terminal-run-observer-evidence.test.ts": 48, + "src/main/automations/runtime-terminal-run-observer.test.ts": 47, + "src/main/automations/schedule-drift-report.test.ts": 48, + "src/main/automations/service-precheck.test.ts": 1991, + "src/main/automations/service.test.ts": 2458, + "src/main/azure-devops/azure-devops-api-request.test.ts": 33, + "src/main/azure-devops/client.test.ts": 530, + "src/main/azure-devops/pull-request-creation.test.ts": 559, + "src/main/azure-devops/pull-request-mappers.test.ts": 25, + "src/main/azure-devops/repository-ref.test.ts": 536, + "src/main/bitbucket/client.test.ts": 552, + "src/main/bitbucket/credential-connection.test.ts": 128, + "src/main/bitbucket/credential-store.test.ts": 183, + "src/main/bitbucket/pull-request-creation.test.ts": 553, + "src/main/bitbucket/pull-request-mappers.test.ts": 31, + "src/main/bitbucket/repository-ref.test.ts": 631, + "src/main/bitbucket/status-no-decrypt.test.ts": 160, + "src/main/browser-sidecar-owner.test.ts": 43, + "src/main/browser/agent-browser-bridge-capture-paint-hold.test.ts": 352, + "src/main/browser/agent-browser-bridge-command-transport.test.ts": 710, + "src/main/browser/agent-browser-bridge-keypress-input.test.ts": 392, + "src/main/browser/agent-browser-bridge-mouse-input.test.ts": 329, + "src/main/browser/agent-browser-bridge-navigation.test.ts": 486, + "src/main/browser/agent-browser-bridge-pointer-input.test.ts": 588, + "src/main/browser/agent-browser-bridge-raw-process-retention.test.ts": 766, + "src/main/browser/agent-browser-bridge-raw-process.test.ts": 917, + "src/main/browser/agent-browser-bridge-screenshot-format.test.ts": 301, + "src/main/browser/agent-browser-bridge-session-lifecycle.test.ts": 785, + "src/main/browser/agent-browser-bridge-tab-routing.test.ts": 369, + "src/main/browser/agent-browser-bridge-text-input.test.ts": 607, + "src/main/browser/agent-browser-orphan-sweep.test.ts": 41, + "src/main/browser/agent-browser-process-environment.test.ts": 38, + "src/main/browser/agent-browser-session-reset.test.ts": 45, + "src/main/browser/agent-input-focus-guard.test.ts": 50, + "src/main/browser/agent-input-injection.test.ts": 287, + "src/main/browser/browser-certificate-trust-controller.test.ts": 461, + "src/main/browser/browser-client-download-relay.test.ts": 234, + "src/main/browser/browser-client-download-routing.test.ts": 243, + "src/main/browser/browser-client-file-channel-negotiation.test.ts": 333, + "src/main/browser/browser-client-file-channel-reconnect.test.ts": 570, + "src/main/browser/browser-client-host-attach-request.test.ts": 229, + "src/main/browser/browser-client-host-authority-replacement.test.ts": 235, + "src/main/browser/browser-client-host-command-dispatcher.test.ts": 249, + "src/main/browser/browser-client-host-command-retention.test.ts": 764, + "src/main/browser/browser-client-host-id.test.ts": 66, + "src/main/browser/browser-client-host-placement-preparation.test.ts": 272, + "src/main/browser/browser-client-host-published-url-wiring.test.ts": 534, + "src/main/browser/browser-client-host-reconciliation-command-order.test.ts": 246, + "src/main/browser/browser-client-network-route-registry.test.ts": 274, + "src/main/browser/browser-client-page-automation-runtime.test.ts": 31, + "src/main/browser/browser-client-page-command-executor-fencing.test.ts": 256, + "src/main/browser/browser-client-page-command-executor.test.ts": 357, + "src/main/browser/browser-client-page-command-integration.test.ts": 338, + "src/main/browser/browser-client-page-execution-host-supersession.test.ts": 323, + "src/main/browser/browser-client-page-inventory.test.ts": 266, + "src/main/browser/browser-client-page-metadata-transport.test.ts": 210, + "src/main/browser/browser-client-page-published-url.test.ts": 251, + "src/main/browser/browser-client-page-reconciliation-adapters.test.ts": 318, + "src/main/browser/browser-client-page-renderer-bridge.test.ts": 244, + "src/main/browser/browser-client-page-renderer-runtime.test.ts": 218, + "src/main/browser/browser-client-page-unavailability.test.ts": 318, + "src/main/browser/browser-client-page-upload-routing.test.ts": 334, + "src/main/browser/browser-client-route-cookie-import-partition-parity.test.ts": 337, + "src/main/browser/browser-client-route-cookie-import.test.ts": 90, + "src/main/browser/browser-client-upload-buffer-retention.test.ts": 754, + "src/main/browser/browser-client-upload-command.test.ts": 214, + "src/main/browser/browser-client-upload-staging.test.ts": 297, + "src/main/browser/browser-client-upload-transfer.test.ts": 224, + "src/main/browser/browser-cookie-clear-preserve.test.ts": 126, + "src/main/browser/browser-cookie-clear-store-lifecycle.test.ts": 78, + "src/main/browser/browser-cookie-clear-store.test.ts": 100, + "src/main/browser/browser-cookie-decryption.test.ts": 64, + "src/main/browser/browser-cookie-import-clear-atomicity.test.ts": 71, + "src/main/browser/browser-cookie-import-concurrency.test.ts": 573, + "src/main/browser/browser-cookie-import-google-exclusion.test.ts": 518, + "src/main/browser/browser-cookie-import-partition-fidelity.test.ts": 542, + "src/main/browser/browser-cookie-import-partition-reland.electron.test.ts": 1533, + "src/main/browser/browser-cookie-import-partition-rollback.electron.test.ts": 642, + "src/main/browser/browser-cookie-import-partition-success.electron.test.ts": 3984, + "src/main/browser/browser-cookie-import-partition.electron.test.ts": 769, + "src/main/browser/browser-cookie-import-plan-writes.test.ts": 71, + "src/main/browser/browser-cookie-import-policy.test.ts": 201, + "src/main/browser/browser-cookie-import-replace-partition-rollback.electron.test.ts": 1156, + "src/main/browser/browser-cookie-import-replacement.test.ts": 483, + "src/main/browser/browser-cookie-import-route-partition-staging.test.ts": 643, + "src/main/browser/browser-cookie-import-scope.test.ts": 424, + "src/main/browser/browser-cookie-import-snapshot-ownership.test.ts": 722, + "src/main/browser/browser-cookie-import-undecryptable.test.ts": 962, + "src/main/browser/browser-cookie-import-validated-partition.electron.test.ts": 722, + "src/main/browser/browser-cookie-import-write.test.ts": 69, + "src/main/browser/browser-cookie-import.comet.test.ts": 1035, + "src/main/browser/browser-cookie-import.helium.test.ts": 944, + "src/main/browser/browser-cookie-import.test.ts": 1574, + "src/main/browser/browser-cookie-partition-summary.test.ts": 65, + "src/main/browser/browser-cookie-public-suffix-scope.test.ts": 72, + "src/main/browser/browser-cookie-registrable-family.test.ts": 80, + "src/main/browser/browser-cookie-samesite.electron.test.ts": 1072, + "src/main/browser/browser-cookie-source-partition.test.ts": 54, + "src/main/browser/browser-cookie-validation.test.ts": 68, + "src/main/browser/browser-download-destination.test.ts": 43, + "src/main/browser/browser-execution-host-storage-identity.test.ts": 191, + "src/main/browser/browser-google-auth-ua.test.ts": 31, + "src/main/browser/browser-grab-payload.test.ts": 40, + "src/main/browser/browser-guest-shortcut-forwarding.test.ts": 395, + "src/main/browser/browser-guest-wheel-zoom-scroll.test.ts": 40, + "src/main/browser/browser-host-client-identity.test.ts": 74, + "src/main/browser/browser-host-lease-reconnect-delay.test.ts": 27, + "src/main/browser/browser-identity-mode-record.test.ts": 32, + "src/main/browser/browser-identity-mode-store.test.ts": 96, + "src/main/browser/browser-manager-annotation-bridge.test.ts": 383, + "src/main/browser/browser-manager-auth-user-agent.test.ts": 382, + "src/main/browser/browser-manager-client-hosted-downloads.test.ts": 540, + "src/main/browser/browser-manager-destroyed-guest-downloads.test.ts": 358, + "src/main/browser/browser-manager-destroyed-guest.test.ts": 448, + "src/main/browser/browser-manager-downloads.test.ts": 392, + "src/main/browser/browser-manager-grab-capture.test.ts": 384, + "src/main/browser/browser-manager-grab-mode.test.ts": 407, + "src/main/browser/browser-manager-grab-selection.test.ts": 433, + "src/main/browser/browser-manager-grab-shortcuts.test.ts": 385, + "src/main/browser/browser-manager-guest-lifecycle.test.ts": 361, + "src/main/browser/browser-manager-guest-policy-profile.test.ts": 380, + "src/main/browser/browser-manager-guest-shortcuts.test.ts": 382, + "src/main/browser/browser-manager-guest-visibility.test.ts": 350, + "src/main/browser/browser-manager-load-failure-replay.test.ts": 385, + "src/main/browser/browser-manager-popup-child.test.ts": 350, + "src/main/browser/browser-manager-popup-openerless-tab.test.ts": 417, + "src/main/browser/browser-manager-popup-routing.test.ts": 477, + "src/main/browser/browser-manager-tab-identity.test.ts": 390, + "src/main/browser/browser-manager-viewport-crashed-guest.test.ts": 333, + "src/main/browser/browser-manager-viewport-override.test.ts": 392, + "src/main/browser/browser-manager-viewport-ownership.test.ts": 378, + "src/main/browser/browser-manager-viewport-partial-failure.test.ts": 400, + "src/main/browser/browser-manager-viewport-touch-reset.test.ts": 355, + "src/main/browser/browser-manager-worker-request-user-agent.test.ts": 341, + "src/main/browser/browser-network-deferred-socket.test.ts": 88, + "src/main/browser/browser-network-execution-route.test.ts": 229, + "src/main/browser/browser-network-tunnel-client-memory-budget.test.ts": 81, + "src/main/browser/browser-network-tunnel-client.test.ts": 70, + "src/main/browser/browser-network-tunnel-conformance.test.ts": 141, + "src/main/browser/browser-network-tunnel-outbound-memory-budget.test.ts": 63, + "src/main/browser/browser-network-tunnel-session-aggregate-memory.test.ts": 61, + "src/main/browser/browser-network-tunnel-session-stream-scoped-failure.test.ts": 67, + "src/main/browser/browser-network-tunnel-session.test.ts": 139, + "src/main/browser/browser-page-initiated-tab-budget.test.ts": 31, + "src/main/browser/browser-popup-native-window.test.ts": 288, + "src/main/browser/browser-popup-new-tab-intent.test.ts": 50, + "src/main/browser/browser-process-user-agent.test.ts": 46, + "src/main/browser/browser-route-guest-popups.test.ts": 336, + "src/main/browser/browser-route-identity.test.ts": 33, + "src/main/browser/browser-route-partition-binding-capacity.test.ts": 78, + "src/main/browser/browser-route-partition-binding-store.test.ts": 109, + "src/main/browser/browser-route-partition-migration.test.ts": 101, + "src/main/browser/browser-route-partition-stability.test.ts": 255, + "src/main/browser/browser-route-partition-storage-lifecycle.test.ts": 95, + "src/main/browser/browser-route-partition-storage-retirement.test.ts": 245, + "src/main/browser/browser-route-partition-storage-runtime.test.ts": 39, + "src/main/browser/browser-route-persisted-worker-egress.electron.test.ts": 2086, + "src/main/browser/browser-route-prepared-page-rekey.test.ts": 33, + "src/main/browser/browser-route-session-registry.test.ts": 86, + "src/main/browser/browser-route-webcontents-registry.test.ts": 321, + "src/main/browser/browser-screencast-dialog-settlement.test.ts": 85, + "src/main/browser/browser-screencast-first-frame-timeout.test.ts": 96, + "src/main/browser/browser-screencast-lifecycle.test.ts": 89, + "src/main/browser/browser-screencast-live-frame-allocation.test.ts": 114, + "src/main/browser/browser-screencast-snapshot-scaling.test.ts": 165, + "src/main/browser/browser-screencast-stream.test.ts": 668, "src/main/browser/browser-session-cookie-staging.scoped.test.ts": 475, - "src/main/browser/browser-session-cookie-staging.test.ts": 316, - "src/main/browser/browser-session-partition-policies.test.ts": 95, - "src/main/browser/browser-session-partition-proxy-install.test.ts": 499, - "src/main/browser/browser-session-proxy.test.ts": 223, - "src/main/browser/browser-session-registry-identity.persistence.test.ts": 504, - "src/main/browser/browser-session-registry.persistence.test.ts": 887, - "src/main/browser/browser-session-registry.test.ts": 839, - "src/main/browser/browser-session-startup.test.ts": 71, - "src/main/browser/browser-session-ua-cloudflare-live.electron.test.ts": 645, - "src/main/browser/browser-session-ua-wire-identity-cross-context.electron.test.ts": 5588, - "src/main/browser/browser-session-ua-wire-identity.electron.test.ts": 6689, - "src/main/browser/browser-session-user-agent-migration-inspection.test.ts": 249, - "src/main/browser/browser-tab-identity.test.ts": 44, - "src/main/browser/browser-text-insertion.test.ts": 72, - "src/main/browser/browser-webauthn-access.test.ts": 55, - "src/main/browser/browser-webauthn-account-picker.test.ts": 70, - "src/main/browser/browser-webauthn-profile-delete.test.ts": 474, - "src/main/browser/cdp-bridge-integration.test.ts": 6649, - "src/main/browser/cdp-bridge-state.test.ts": 73, - "src/main/browser/cdp-client-response-writer.test.ts": 319, + "src/main/browser/browser-session-cookie-staging.test.ts": 335, + "src/main/browser/browser-session-partition-policies.test.ts": 75, + "src/main/browser/browser-session-partition-proxy-install.test.ts": 522, + "src/main/browser/browser-session-proxy.test.ts": 207, + "src/main/browser/browser-session-registry-identity.persistence.test.ts": 473, + "src/main/browser/browser-session-registry.persistence.test.ts": 978, + "src/main/browser/browser-session-registry.test.ts": 821, + "src/main/browser/browser-session-startup.test.ts": 44, + "src/main/browser/browser-session-ua-cloudflare-live.electron.test.ts": 272, + "src/main/browser/browser-session-ua-wire-identity-cross-context.electron.test.ts": 6773, + "src/main/browser/browser-session-ua-wire-identity.electron.test.ts": 11546, + "src/main/browser/browser-session-user-agent-migration-inspection.test.ts": 268, + "src/main/browser/browser-tab-identity.test.ts": 36, + "src/main/browser/browser-text-insertion.test.ts": 34, + "src/main/browser/browser-webauthn-access.test.ts": 25, + "src/main/browser/browser-webauthn-account-picker.test.ts": 43, + "src/main/browser/browser-webauthn-profile-delete.test.ts": 458, + "src/main/browser/cdp-bridge-integration.test.ts": 8568, + "src/main/browser/cdp-bridge-state.test.ts": 82, + "src/main/browser/cdp-client-response-writer.test.ts": 225, "src/main/browser/cdp-keyboard-us-layout.test.ts": 73, - "src/main/browser/cdp-print-to-pdf.test.ts": 64, - "src/main/browser/cdp-screenshot.test.ts": 89, - "src/main/browser/cdp-ws-proxy-focus-replay.test.ts": 293, - "src/main/browser/cdp-ws-proxy.test.ts": 2808, - "src/main/browser/chromium-cookie-snapshot.test.ts": 521, - "src/main/browser/client-route-cookie-import-source-store.test.ts": 114, - "src/main/browser/doc-preview-download-block-notice.test.ts": 65, - "src/main/browser/doc-preview-failure-notice.test.ts": 47, - "src/main/browser/doc-preview-file-reader.test.ts": 106, - "src/main/browser/doc-preview-grant-registry.test.ts": 74, - "src/main/browser/doc-preview-guest-policy.test.ts": 255, - "src/main/browser/doc-preview-protocol.test.ts": 86, - "src/main/browser/doc-preview-response-allocation.test.ts": 155, - "src/main/browser/electron-debugger-lease.test.ts": 46, - "src/main/browser/electron-probe-display-launch.test.ts": 44, - "src/main/browser/execution-route-socket-duplex.test.ts": 91, - "src/main/browser/grab-guest-script.test.ts": 91, - "src/main/browser/guest-cdp-command.test.ts": 41, - "src/main/browser/local-ssh-browser-partition-identity.test.ts": 466, - "src/main/browser/local-ssh-browser-partitions.probe-cache.test.ts": 56, - "src/main/browser/local-ssh-browser-route-target.test.ts": 50, - "src/main/browser/local-ssh-browser-route.test.ts": 84, - "src/main/browser/offscreen-browser-backend-lifecycle.test.ts": 362, - "src/main/browser/paired-runtime-browser-client-host-composition.test.ts": 261, - "src/main/browser/paired-runtime-browser-client-host-identity-wiring.test.ts": 995, - "src/main/browser/paired-runtime-browser-client-host-reconnect.test.ts": 510, - "src/main/browser/paired-runtime-browser-client-host-registry.test.ts": 79, - "src/main/browser/paired-runtime-browser-client-host-route-identity.test.ts": 574, - "src/main/browser/paired-runtime-browser-client-host-runtime.test.ts": 576, - "src/main/browser/paired-runtime-browser-client-host.test.ts": 414, - "src/main/browser/paired-runtime-browser-host-command-admission.test.ts": 266, - "src/main/browser/paired-runtime-browser-host-lease-reconnect.test.ts": 967, - "src/main/browser/paired-runtime-browser-host-lease.test.ts": 943, - "src/main/browser/paired-runtime-browser-host-reconciliation-negotiation.test.ts": 338, - "src/main/browser/paired-runtime-browser-network-route.test.ts": 1296, - "src/main/browser/popup-origin-bar-window.test.ts": 106, - "src/main/browser/remote-browser-socks-buffering.test.ts": 372, - "src/main/browser/remote-browser-socks-server.test.ts": 309, + "src/main/browser/cdp-print-to-pdf.test.ts": 50, + "src/main/browser/cdp-screenshot.test.ts": 80, + "src/main/browser/cdp-ws-proxy-focus-replay.test.ts": 327, + "src/main/browser/cdp-ws-proxy-input-focus.test.ts": 173, + "src/main/browser/cdp-ws-proxy-origin.test.ts": 141, + "src/main/browser/cdp-ws-proxy.test.ts": 2625, + "src/main/browser/chromium-cookie-snapshot.test.ts": 381, + "src/main/browser/client-route-cookie-import-source-store.test.ts": 118, + "src/main/browser/doc-preview-download-block-notice.test.ts": 52, + "src/main/browser/doc-preview-failure-notice.test.ts": 35, + "src/main/browser/doc-preview-file-reader.test.ts": 56, + "src/main/browser/doc-preview-grant-registry.test.ts": 56, + "src/main/browser/doc-preview-guest-policy.test.ts": 304, + "src/main/browser/doc-preview-protocol.test.ts": 76, + "src/main/browser/doc-preview-response-allocation.test.ts": 137, + "src/main/browser/electron-debugger-lease.test.ts": 27, + "src/main/browser/electron-probe-display-launch.test.ts": 34, + "src/main/browser/execution-route-socket-duplex.test.ts": 79, + "src/main/browser/grab-guest-overlay-follow.test.ts": 300, + "src/main/browser/grab-guest-script.test.ts": 76, + "src/main/browser/guest-cdp-command.test.ts": 44, + "src/main/browser/guest-direct-key-input.test.ts": 87, + "src/main/browser/local-ssh-browser-partition-identity.test.ts": 392, + "src/main/browser/local-ssh-browser-partitions.probe-cache.test.ts": 53, + "src/main/browser/local-ssh-browser-route-target.test.ts": 42, + "src/main/browser/local-ssh-browser-route.test.ts": 76, + "src/main/browser/offscreen-browser-backend-lifecycle.test.ts": 377, + "src/main/browser/paired-runtime-browser-client-host-composition.test.ts": 255, + "src/main/browser/paired-runtime-browser-client-host-identity-wiring.test.ts": 1509, + "src/main/browser/paired-runtime-browser-client-host-reconnect.test.ts": 541, + "src/main/browser/paired-runtime-browser-client-host-registry.test.ts": 46, + "src/main/browser/paired-runtime-browser-client-host-route-identity.test.ts": 667, + "src/main/browser/paired-runtime-browser-client-host-runtime.test.ts": 540, + "src/main/browser/paired-runtime-browser-client-host.test.ts": 403, + "src/main/browser/paired-runtime-browser-host-command-admission.test.ts": 250, + "src/main/browser/paired-runtime-browser-host-lease-reconnect.test.ts": 1082, + "src/main/browser/paired-runtime-browser-host-lease.test.ts": 1002, + "src/main/browser/paired-runtime-browser-host-reconciliation-negotiation.test.ts": 368, + "src/main/browser/paired-runtime-browser-network-route.test.ts": 1212, + "src/main/browser/popup-origin-bar-window.test.ts": 246, + "src/main/browser/remote-browser-socks-buffering.test.ts": 363, + "src/main/browser/remote-browser-socks-server.test.ts": 298, "src/main/browser/snapshot-engine-iframe-sessions.test.ts": 96, - "src/main/browser/snapshot-engine.test.ts": 60, - "src/main/browser/ssh-browser-network-execution-route.test.ts": 554, - "src/main/browser/system-ssh-socks-client-socket.test.ts": 71, - "src/main/browser/wsl-browser-network-execution-route.test.ts": 354, - "src/main/browser/wsl-browser-network-relay-launch.test.ts": 83, - "src/main/claude-accounts/claude-account-folder.test.ts": 275, - "src/main/claude-accounts/claude-account-service.test.ts": 792, - "src/main/claude-accounts/claude-command-process.test.ts": 81, - "src/main/claude-accounts/claude-credential-persistence-ratchet.test.ts": 608, - "src/main/claude-accounts/claude-duplicate-account.test.ts": 52, - "src/main/claude-accounts/claude-launch-routing.test.ts": 540, - "src/main/claude-accounts/claude-profile-history-windows.test.ts": 132, - "src/main/claude-accounts/claude-profile-history.test.ts": 107, - "src/main/claude-accounts/claude-profile-paths.test.ts": 80, - "src/main/claude-accounts/claude-profile-provisioning.test.ts": 762, - "src/main/claude-accounts/claude-profile-router.test.ts": 7997, - "src/main/claude-accounts/claude-profile-setup-worker.test.ts": 240, - "src/main/claude-accounts/claude-profile-setup.test.ts": 181, - "src/main/claude-accounts/claude-profile-shell-composition.test.ts": 72, - "src/main/claude-accounts/claude-profile-wsl-router.test.ts": 787, - "src/main/claude-accounts/claude-sign-in-browser.test.ts": 319, - "src/main/claude-accounts/claude-structured-auth-policy.test.ts": 61, - "src/main/claude-accounts/keychain-config-directory-aliases.test.ts": 57, - "src/main/claude-accounts/keychain.test.ts": 56, - "src/main/claude-accounts/runtime-auth-service.test.ts": 297, - "src/main/claude-accounts/runtime-paths.test.ts": 87, - "src/main/claude-accounts/runtime-selection.test.ts": 69, - "src/main/claude-accounts/windows-command-invocation.test.ts": 44, - "src/main/claude-usage/claude-model-pricing.test.ts": 68, - "src/main/claude-usage/claude-usage-report-aggregation.test.ts": 47, - "src/main/claude-usage/persisted-source-authority.test.ts": 320, - "src/main/claude-usage/persisted-token-columns-store.test.ts": 467, - "src/main/claude-usage/persisted-token-columns.test.ts": 91, - "src/main/claude-usage/scanner-incremental-append.test.ts": 191, - "src/main/claude-usage/scanner-incremental-ownership.test.ts": 414, - "src/main/claude-usage/scanner-incremental-reader.test.ts": 666, - "src/main/claude-usage/scanner-large-directory.test.ts": 285, - "src/main/claude-usage/scanner-scan.test.ts": 242, - "src/main/claude-usage/scanner.test.ts": 91, - "src/main/claude-usage/source-cache-compression.test.ts": 89, - "src/main/claude-usage/source-cache-write-retention.test.ts": 305, - "src/main/claude-usage/store-checkpoint-persistence.test.ts": 590, - "src/main/claude-usage/store-source-authority.test.ts": 557, - "src/main/claude-usage/store.test.ts": 403, - "src/main/claude-usage/transcript-file-discovery-errors.test.ts": 60, + "src/main/browser/snapshot-engine.test.ts": 44, + "src/main/browser/ssh-browser-network-execution-route.test.ts": 587, + "src/main/browser/system-ssh-socks-client-socket.test.ts": 50, + "src/main/browser/wsl-browser-network-execution-route.test.ts": 643, + "src/main/browser/wsl-browser-network-relay-launch.test.ts": 70, + "src/main/claude-accounts/claude-account-folder.test.ts": 269, + "src/main/claude-accounts/claude-account-service.test.ts": 1049, + "src/main/claude-accounts/claude-command-process.test.ts": 89, + "src/main/claude-accounts/claude-credential-persistence-ratchet.test.ts": 607, + "src/main/claude-accounts/claude-duplicate-account.test.ts": 42, + "src/main/claude-accounts/claude-launch-routing.test.ts": 443, + "src/main/claude-accounts/claude-profile-history-windows.test.ts": 122, + "src/main/claude-accounts/claude-profile-history.test.ts": 132, + "src/main/claude-accounts/claude-profile-paths.test.ts": 79, + "src/main/claude-accounts/claude-profile-provisioning.test.ts": 858, + "src/main/claude-accounts/claude-profile-router.test.ts": 7998, + "src/main/claude-accounts/claude-profile-setup-worker.test.ts": 242, + "src/main/claude-accounts/claude-profile-setup.test.ts": 241, + "src/main/claude-accounts/claude-profile-shell-composition.test.ts": 78, + "src/main/claude-accounts/claude-profile-wsl-router.test.ts": 835, + "src/main/claude-accounts/claude-sign-in-browser.test.ts": 323, + "src/main/claude-accounts/claude-structured-auth-policy.test.ts": 33, + "src/main/claude-accounts/keychain-config-directory-aliases.test.ts": 43, + "src/main/claude-accounts/keychain.test.ts": 35, + "src/main/claude-accounts/runtime-auth-service.test.ts": 302, + "src/main/claude-accounts/runtime-paths.test.ts": 65, + "src/main/claude-accounts/runtime-selection.test.ts": 31, + "src/main/claude-accounts/windows-command-invocation.test.ts": 33, + "src/main/claude-usage/claude-model-pricing.test.ts": 63, + "src/main/claude-usage/claude-usage-report-aggregation.test.ts": 40, + "src/main/claude-usage/persisted-source-authority.test.ts": 434, + "src/main/claude-usage/persisted-token-columns-store.test.ts": 417, + "src/main/claude-usage/persisted-token-columns.test.ts": 97, + "src/main/claude-usage/scanner-incremental-append.test.ts": 169, + "src/main/claude-usage/scanner-incremental-ownership.test.ts": 329, + "src/main/claude-usage/scanner-incremental-reader.test.ts": 665, + "src/main/claude-usage/scanner-large-directory.test.ts": 231, + "src/main/claude-usage/scanner-scan.test.ts": 210, + "src/main/claude-usage/scanner.test.ts": 74, + "src/main/claude-usage/source-cache-compression.test.ts": 61, + "src/main/claude-usage/source-cache-write-retention.test.ts": 367, + "src/main/claude-usage/store-checkpoint-persistence.test.ts": 670, + "src/main/claude-usage/store-source-authority.test.ts": 770, + "src/main/claude-usage/store.test.ts": 412, + "src/main/claude-usage/transcript-file-discovery-errors.test.ts": 48, "src/main/claude-usage/transcript-projection-integrity.test.ts": 207, - "src/main/claude-usage/transcript-reader-lifetime.test.ts": 66, - "src/main/claude-usage/transcript-record-parser-prefilter.test.ts": 57, - "src/main/claude-usage/transcript-tuple-append.test.ts": 270, - "src/main/claude-usage/worktree-attribution-scaling.test.ts": 60, - "src/main/claude/claude-adapter-capture-replay.test.ts": 669, - "src/main/claude/claude-agent-sdk-contract-pins.test.ts": 3228, - "src/main/claude/claude-agent-sdk-control-requests.test.ts": 54, - "src/main/claude/claude-agent-sdk-exit-proof-identity.test.ts": 113, - "src/main/claude/claude-agent-sdk-exit-proof.test.ts": 8144, - "src/main/claude/claude-agent-sdk-import-boundary.test.ts": 1579, - "src/main/claude/claude-agent-sdk-process-spawn.test.ts": 180, - "src/main/claude/claude-agent-sdk-root-kill-fallback.test.ts": 130, - "src/main/claude/claude-agent-sdk-user-message-queue.test.ts": 151, - "src/main/claude/claude-api-retry-idle-sweep.test.ts": 1549, - "src/main/claude/claude-api-retry-row.test.ts": 651, - "src/main/claude/claude-background-stop-records.test.ts": 828, - "src/main/claude/claude-background-task-resume.test.ts": 70, - "src/main/claude/claude-background-task-retention.test.ts": 476, - "src/main/claude/claude-background-task-row-journal.test.ts": 113, - "src/main/claude/claude-background-task-rows.test.ts": 166, - "src/main/claude/claude-background-task-terminal-frames.test.ts": 94, - "src/main/claude/claude-background-task-tracker.test.ts": 155, - "src/main/claude/claude-background-task-turn-resumption.test.ts": 376, - "src/main/claude/claude-background-wake-retention.test.ts": 1040, - "src/main/claude/claude-child-exit-proof-ladder.test.ts": 5645, - "src/main/claude/claude-child-process-environment.test.ts": 65, - "src/main/claude/claude-child-work-decoder.test.ts": 85, - "src/main/claude/claude-child-work-evidence-retention.test.ts": 483, - "src/main/claude/claude-cli-flag-prewarm.test.ts": 246, - "src/main/claude/claude-cli-flag-support.test.ts": 399, - "src/main/claude/claude-close-turn-end.test.ts": 554, - "src/main/claude/claude-command-lifecycle-frames.test.ts": 378, - "src/main/claude/claude-command-lifecycle.test.ts": 636, - "src/main/claude/claude-command-turn.test.ts": 328, - "src/main/claude/claude-config-dir-pin.test.ts": 65, - "src/main/claude/claude-context-model-switch.test.ts": 435, - "src/main/claude/claude-context-usage-restart.test.ts": 832, - "src/main/claude/claude-context-usage-unloaded-turn.test.ts": 3083, - "src/main/claude/claude-context-usage.test.ts": 298, - "src/main/claude/claude-context-window-hint.test.ts": 473, - "src/main/claude/claude-descendant-escalation-boundary.test.ts": 564, - "src/main/claude/claude-fold-steer-receipt.test.ts": 684, - "src/main/claude/claude-folder-trust-file-wsl-guest.test.ts": 60, - "src/main/claude/claude-folder-trust-file.test.ts": 737, - "src/main/claude/claude-history-window-source-budget.test.ts": 594, - "src/main/claude/claude-hook-event-install.test.ts": 142, - "src/main/claude/claude-hook-event-versions.test.ts": 87, - "src/main/claude/claude-informational-row.test.ts": 615, - "src/main/claude/claude-journal-turn-scope.test.ts": 286, - "src/main/claude/claude-model-catalog-probe.test.ts": 2112, - "src/main/claude/claude-profile-hooks.test.ts": 193, - "src/main/claude/claude-provisional-row-corrections.test.ts": 61, - "src/main/claude/claude-request-end-wait.test.ts": 49, - "src/main/claude/claude-settings-schema-install.test.ts": 181, - "src/main/claude/claude-slash-command-catalog.test.ts": 59, - "src/main/claude/claude-status-line-install.test.ts": 124, - "src/main/claude/claude-stop-ack-race.test.ts": 1348, - "src/main/claude/claude-stream-json-connection-close.test.ts": 653, - "src/main/claude/claude-stream-json-connection.test.ts": 10922, - "src/main/claude/claude-streamed-text-checkpoints.test.ts": 64, - "src/main/claude/claude-structured-applied-options.test.ts": 631, - "src/main/claude/claude-structured-auth-parity.test.ts": 458, - "src/main/claude/claude-structured-child-work-captures.test.ts": 1096, - "src/main/claude/claude-structured-child-work-producer.test.ts": 1124, - "src/main/claude/claude-structured-configured-default.test.ts": 833, - "src/main/claude/claude-structured-content-parts.test.ts": 352, - "src/main/claude/claude-structured-control-actions.test.ts": 393, - "src/main/claude/claude-structured-conversation-stop.test.ts": 562, - "src/main/claude/claude-structured-dispatch-admission.test.ts": 567, - "src/main/claude/claude-structured-dispatch-attachment-rejection.test.ts": 291, - "src/main/claude/claude-structured-dispatch-boundary.test.ts": 622, - "src/main/claude/claude-structured-dispatch-content.test.ts": 270, - "src/main/claude/claude-structured-dispatch-queue-boundary.test.ts": 659, - "src/main/claude/claude-structured-dispatch.test.ts": 1937, - "src/main/claude/claude-structured-effort-default-at-rest.test.ts": 714, - "src/main/claude/claude-structured-effort-reporting.test.ts": 715, - "src/main/claude/claude-structured-history-window.test.ts": 378, - "src/main/claude/claude-structured-in-turn-stop.test.ts": 607, - "src/main/claude/claude-structured-inbound-control.test.ts": 75, - "src/main/claude/claude-structured-journal-prompt-cards.test.ts": 60, - "src/main/claude/claude-structured-journal-prompt-retry.test.ts": 332, - "src/main/claude/claude-structured-journal-translation-background-tasks.test.ts": 506, - "src/main/claude/claude-structured-journal-translation-context-usage.test.ts": 456, - "src/main/claude/claude-structured-journal-translation-subagents.test.ts": 400, - "src/main/claude/claude-structured-journal-translation-turn-timing.test.ts": 730, - "src/main/claude/claude-structured-journal-translation.plan.test.ts": 328, - "src/main/claude/claude-structured-journal-translation.test.ts": 726, - "src/main/claude/claude-structured-launch-account-routing.test.ts": 631, - "src/main/claude/claude-structured-launch-args.test.ts": 40, - "src/main/claude/claude-structured-launch-resolution.test.ts": 647, - "src/main/claude/claude-structured-launch-visuals.test.ts": 477, - "src/main/claude/claude-structured-live-provider-process.test.ts": 587, - "src/main/claude/claude-structured-model-confirmation.test.ts": 673, - "src/main/claude/claude-structured-model-preflight.test.ts": 343, - "src/main/claude/claude-structured-option-confirmation.test.ts": 758, - "src/main/claude/claude-structured-options-context-window.test.ts": 414, - "src/main/claude/claude-structured-options.test.ts": 431, - "src/main/claude/claude-structured-owner-identity.test.ts": 90, - "src/main/claude/claude-structured-permission-mode.test.ts": 58, - "src/main/claude/claude-structured-prompt-items.test.ts": 316, - "src/main/claude/claude-structured-prompt-ownership.test.ts": 700, - "src/main/claude/claude-structured-provider-fallback.test.ts": 550, - "src/main/claude/claude-structured-queued-stop.test.ts": 538, - "src/main/claude/claude-structured-rate-limit-retry.test.ts": 264, - "src/main/claude/claude-structured-real-cli-fold.test.ts": 490, - "src/main/claude/claude-structured-real-cli-visuals.test.ts": 584, - "src/main/claude/claude-structured-real-cli.test.ts": 787, - "src/main/claude/claude-structured-reasoning-lifecycle.test.ts": 350, - "src/main/claude/claude-structured-reasoning.test.ts": 289, - "src/main/claude/claude-structured-requested-stop.test.ts": 557, - "src/main/claude/claude-structured-resume-point.test.ts": 758, - "src/main/claude/claude-structured-retired-model.test.ts": 531, - "src/main/claude/claude-structured-session-acquisition-processless.test.ts": 679, - "src/main/claude/claude-structured-session-adapter-turns.test.ts": 726, - "src/main/claude/claude-structured-session-adapter.test.ts": 824, - "src/main/claude/claude-structured-session-close.test.ts": 635, - "src/main/claude/claude-structured-session-commands.test.ts": 694, - "src/main/claude/claude-structured-session-exit-blame.test.ts": 672, - "src/main/claude/claude-structured-session-reading-control.test.ts": 886, - "src/main/claude/claude-structured-session-recovery.test.ts": 641, - "src/main/claude/claude-structured-session-startup.test.ts": 723, - "src/main/claude/claude-structured-sign-in.test.ts": 317, - "src/main/claude/claude-structured-spawn-options.test.ts": 442, - "src/main/claude/claude-structured-start-fast-mode.test.ts": 554, - "src/main/claude/claude-structured-turn-resume-point.test.ts": 695, - "src/main/claude/claude-subagent-group-row.test.ts": 55, + "src/main/claude-usage/transcript-reader-lifetime.test.ts": 89, + "src/main/claude-usage/transcript-record-parser-prefilter.test.ts": 45, + "src/main/claude-usage/transcript-tuple-append.test.ts": 346, + "src/main/claude-usage/worktree-attribution-scaling.test.ts": 42, + "src/main/claude/claude-adapter-capture-replay.test.ts": 557, + "src/main/claude/claude-agent-sdk-contract-pins.test.ts": 3570, + "src/main/claude/claude-agent-sdk-control-requests.test.ts": 40, + "src/main/claude/claude-agent-sdk-exit-proof-identity.test.ts": 94, + "src/main/claude/claude-agent-sdk-exit-proof.test.ts": 8785, + "src/main/claude/claude-agent-sdk-import-boundary.test.ts": 1558, + "src/main/claude/claude-agent-sdk-process-spawn.test.ts": 150, + "src/main/claude/claude-agent-sdk-root-kill-fallback.test.ts": 129, + "src/main/claude/claude-agent-sdk-user-message-queue.test.ts": 146, + "src/main/claude/claude-api-retry-idle-sweep.test.ts": 1807, + "src/main/claude/claude-api-retry-row.test.ts": 638, + "src/main/claude/claude-background-stop-records.test.ts": 1019, + "src/main/claude/claude-background-task-resume.test.ts": 41, + "src/main/claude/claude-background-task-retention.test.ts": 430, + "src/main/claude/claude-background-task-row-journal.test.ts": 127, + "src/main/claude/claude-background-task-rows.test.ts": 224, + "src/main/claude/claude-background-task-terminal-frames.test.ts": 70, + "src/main/claude/claude-background-task-tracker.test.ts": 150, + "src/main/claude/claude-background-task-turn-resumption.test.ts": 334, + "src/main/claude/claude-background-wake-retention.test.ts": 1429, + "src/main/claude/claude-child-exit-proof-ladder.test.ts": 5711, + "src/main/claude/claude-child-process-environment.test.ts": 38, + "src/main/claude/claude-child-work-decoder.test.ts": 74, + "src/main/claude/claude-child-work-evidence-retention.test.ts": 560, + "src/main/claude/claude-cli-flag-prewarm.test.ts": 286, + "src/main/claude/claude-cli-flag-support.test.ts": 404, + "src/main/claude/claude-close-turn-end.test.ts": 943, + "src/main/claude/claude-command-lifecycle-frames.test.ts": 334, + "src/main/claude/claude-command-lifecycle.test.ts": 789, + "src/main/claude/claude-command-turn.test.ts": 344, + "src/main/claude/claude-config-dir-pin.test.ts": 36, + "src/main/claude/claude-context-model-switch.test.ts": 302, + "src/main/claude/claude-context-usage-restart.test.ts": 831, + "src/main/claude/claude-context-usage-unloaded-turn.test.ts": 3382, + "src/main/claude/claude-context-usage.test.ts": 287, + "src/main/claude/claude-context-window-hint.test.ts": 355, + "src/main/claude/claude-descendant-escalation-boundary.test.ts": 548, + "src/main/claude/claude-fold-steer-receipt.test.ts": 901, + "src/main/claude/claude-folder-trust-file-wsl-guest.test.ts": 59, + "src/main/claude/claude-folder-trust-file.test.ts": 784, + "src/main/claude/claude-history-window-source-budget.test.ts": 603, + "src/main/claude/claude-hook-event-install.test.ts": 182, + "src/main/claude/claude-hook-event-versions.test.ts": 81, + "src/main/claude/claude-informational-row.test.ts": 704, + "src/main/claude/claude-journal-turn-scope.test.ts": 329, + "src/main/claude/claude-model-catalog-probe.test.ts": 1837, + "src/main/claude/claude-profile-hooks.test.ts": 231, + "src/main/claude/claude-provisional-row-corrections.test.ts": 38, + "src/main/claude/claude-request-end-wait.test.ts": 47, + "src/main/claude/claude-settings-schema-install.test.ts": 235, + "src/main/claude/claude-slash-command-catalog.test.ts": 50, + "src/main/claude/claude-status-line-install.test.ts": 143, + "src/main/claude/claude-status-line-windows.test.ts": 133, + "src/main/claude/claude-stop-ack-race.test.ts": 1389, + "src/main/claude/claude-stream-json-connection-close.test.ts": 752, + "src/main/claude/claude-stream-json-connection.test.ts": 8162, + "src/main/claude/claude-streamed-text-checkpoints.test.ts": 43, + "src/main/claude/claude-structured-applied-options.test.ts": 629, + "src/main/claude/claude-structured-auth-parity.test.ts": 700, + "src/main/claude/claude-structured-child-env.test.ts": 42, + "src/main/claude/claude-structured-child-work-captures.test.ts": 1184, + "src/main/claude/claude-structured-child-work-producer.test.ts": 1025, + "src/main/claude/claude-structured-configured-default.test.ts": 930, + "src/main/claude/claude-structured-content-parts.test.ts": 321, + "src/main/claude/claude-structured-control-actions.test.ts": 412, + "src/main/claude/claude-structured-conversation-stop.test.ts": 614, + "src/main/claude/claude-structured-dispatch-admission.test.ts": 400, + "src/main/claude/claude-structured-dispatch-attachment-rejection.test.ts": 371, + "src/main/claude/claude-structured-dispatch-boundary.test.ts": 586, + "src/main/claude/claude-structured-dispatch-content.test.ts": 254, + "src/main/claude/claude-structured-dispatch-queue-boundary.test.ts": 601, + "src/main/claude/claude-structured-dispatch.test.ts": 1843, + "src/main/claude/claude-structured-effort-default-at-rest.test.ts": 836, + "src/main/claude/claude-structured-effort-reporting.test.ts": 607, + "src/main/claude/claude-structured-history-window.test.ts": 742, + "src/main/claude/claude-structured-in-turn-stop.test.ts": 657, + "src/main/claude/claude-structured-inbound-control.test.ts": 46, + "src/main/claude/claude-structured-journal-prompt-cards.test.ts": 65, + "src/main/claude/claude-structured-journal-prompt-retry.test.ts": 336, + "src/main/claude/claude-structured-journal-translation-background-tasks.test.ts": 539, + "src/main/claude/claude-structured-journal-translation-context-usage.test.ts": 461, + "src/main/claude/claude-structured-journal-translation-subagents.test.ts": 365, + "src/main/claude/claude-structured-journal-translation-turn-timing.test.ts": 705, + "src/main/claude/claude-structured-journal-translation.plan.test.ts": 336, + "src/main/claude/claude-structured-journal-translation.test.ts": 677, + "src/main/claude/claude-structured-launch-account-routing.test.ts": 943, + "src/main/claude/claude-structured-launch-args.test.ts": 54, + "src/main/claude/claude-structured-launch-resolution.test.ts": 734, + "src/main/claude/claude-structured-launch-visuals.test.ts": 630, + "src/main/claude/claude-structured-live-provider-process.test.ts": 568, + "src/main/claude/claude-structured-model-confirmation.test.ts": 644, + "src/main/claude/claude-structured-model-preflight.test.ts": 323, + "src/main/claude/claude-structured-option-confirmation.test.ts": 627, + "src/main/claude/claude-structured-options-context-window.test.ts": 447, + "src/main/claude/claude-structured-options.test.ts": 532, + "src/main/claude/claude-structured-owner-identity.test.ts": 63, + "src/main/claude/claude-structured-prompt-items.test.ts": 300, + "src/main/claude/claude-structured-prompt-ownership.test.ts": 761, + "src/main/claude/claude-structured-provider-fallback.test.ts": 636, + "src/main/claude/claude-structured-queued-stop.test.ts": 599, + "src/main/claude/claude-structured-rate-limit-retry.test.ts": 326, + "src/main/claude/claude-structured-real-cli-fold.test.ts": 601, + "src/main/claude/claude-structured-real-cli-stop-task.test.ts": 526, + "src/main/claude/claude-structured-real-cli-visuals.test.ts": 789, + "src/main/claude/claude-structured-real-cli.test.ts": 487, + "src/main/claude/claude-structured-reasoning-lifecycle.test.ts": 401, + "src/main/claude/claude-structured-reasoning.test.ts": 403, + "src/main/claude/claude-structured-requested-stop.test.ts": 570, + "src/main/claude/claude-structured-resume-point.test.ts": 539, + "src/main/claude/claude-structured-retired-model.test.ts": 627, + "src/main/claude/claude-structured-session-acquisition-processless.test.ts": 527, + "src/main/claude/claude-structured-session-adapter-turns.test.ts": 666, + "src/main/claude/claude-structured-session-adapter.test.ts": 593, + "src/main/claude/claude-structured-session-close.test.ts": 636, + "src/main/claude/claude-structured-session-commands.test.ts": 604, + "src/main/claude/claude-structured-session-exit-blame.test.ts": 700, + "src/main/claude/claude-structured-session-reading-control.test.ts": 649, + "src/main/claude/claude-structured-session-recovery.test.ts": 877, + "src/main/claude/claude-structured-session-startup.test.ts": 589, + "src/main/claude/claude-structured-sign-in.test.ts": 401, + "src/main/claude/claude-structured-spawn-options.test.ts": 406, + "src/main/claude/claude-structured-start-fast-mode.test.ts": 574, + "src/main/claude/claude-structured-turn-resume-point.test.ts": 694, "src/main/claude/claude-subagent-id-aliases.test.ts": 53, - "src/main/claude/claude-subagent-journaled-roster.test.ts": 55, - "src/main/claude/claude-subagent-permission-request.test.ts": 2224, - "src/main/claude/claude-subagent-resume-after-restart.test.ts": 798, - "src/main/claude/claude-subagent-roster-groups.test.ts": 70, - "src/main/claude/claude-subagent-roster-retention.test.ts": 116, - "src/main/claude/claude-subagent-roster.test.ts": 371, - "src/main/claude/claude-subagent-task-frames.test.ts": 83, - "src/main/claude/claude-supervised-stop-real-cli.test.ts": 450, - "src/main/claude/claude-supervised-stop.integration.test.ts": 3745, - "src/main/claude/claude-transcript-branch-graph.test.ts": 46, - "src/main/claude/claude-transcript-branch-streaming.test.ts": 162, - "src/main/claude/claude-transcript-entry-uuid.test.ts": 45, - "src/main/claude/claude-transcript-marker-branch-proof.test.ts": 82, - "src/main/claude/claude-transcript-rewind-proof.test.ts": 57, - "src/main/claude/claude-turn-outcome.test.ts": 450, - "src/main/claude/claude-turn-ownership.test.ts": 743, - "src/main/claude/claude-turn-resumption.test.ts": 453, - "src/main/claude/compact-status-registration.test.ts": 220, - "src/main/claude/hook-service.test.ts": 206, - "src/main/claude/statusline-script.test.ts": 179, - "src/main/claude/windows-hook-files.test.ts": 151, - "src/main/cli/appimage-extracted-root.test.ts": 139, - "src/main/cli/appimage-extraction-pruning.test.ts": 102, - "src/main/cli/appimage-payload-removal.reentrancy.test.ts": 51, - "src/main/cli/appimage-payload-removal.test.ts": 62, - "src/main/cli/appimage-registration-lock.test.ts": 71, - "src/main/cli/appimage-stable-launcher.test.ts": 221, - "src/main/cli/cli-command-installation-races.test.ts": 152, - "src/main/cli/cli-command-privileged-transaction.test.ts": 112, - "src/main/cli/cli-installer-appimage-ownership.test.ts": 174, - "src/main/cli/cli-installer-appimage-removal.test.ts": 290, - "src/main/cli/cli-installer-command-conflicts.test.ts": 246, - "src/main/cli/cli-installer-macos-command-path.test.ts": 190, - "src/main/cli/cli-installer-windows-path.test.ts": 178, - "src/main/cli/cli-installer.test.ts": 258, - "src/main/cli/cli-privileged-processes.test.ts": 54, - "src/main/cli/keyed-promise-queue.test.ts": 55, - "src/main/cli/legacy-appimage-cli-wrapper.test.ts": 53, - "src/main/cli/linux-bare-orca-dispatcher.test.ts": 144, - "src/main/cli/linux-terminal-orca-cli-shim.test.ts": 148, - "src/main/cli/native-cli-powershell-argv-boundary.test.ts": 51, - "src/main/cli/orca-cli-child-path.test.ts": 82, - "src/main/cli/packaged-cli-assets.test.ts": 6578, - "src/main/cli/windows-user-path-registry.test.ts": 53, - "src/main/cli/wsl-cli-installer.test.ts": 298, - "src/main/cli/wsl-cli-powershell-boundary.test.ts": 97, - "src/main/cli/wsl-cli-registration-operation.test.ts": 104, - "src/main/cli/wsl-cli-registration-reconciliation.test.ts": 240, - "src/main/cli/wsl-cli-registration-registry.test.ts": 131, - "src/main/cli/wsl-managed-cli.test.ts": 64, - "src/main/cli/wsl-managed-cli.wsl.test.ts": 114, - "src/main/codebuddy/hook-service.test.ts": 102, - "src/main/codex-accounts/async-file-rename.test.ts": 1080, - "src/main/codex-accounts/codex-account-identity-api-key-guard.test.ts": 69, - "src/main/codex-accounts/codex-auth-workspace-identity.test.ts": 58, - "src/main/codex-accounts/codex-config-mirror.test.ts": 315, - "src/main/codex-accounts/codex-credential-absence-grace.test.ts": 162, - "src/main/codex-accounts/codex-login-auth-url.test.ts": 54, - "src/main/codex-accounts/codex-reset-credit-ledger.test.ts": 294, - "src/main/codex-accounts/codex-windows-interactive-login.test.ts": 464, - "src/main/codex-accounts/fs-utils.test.ts": 126, - "src/main/codex-accounts/host-codex-managed-home-ownership.test.ts": 69, - "src/main/codex-accounts/legacy-shared-auth-migration.test.ts": 156, - "src/main/codex-accounts/legacy-shared-config-compatibility.test.ts": 147, - "src/main/codex-accounts/legacy-wsl-runtime-auth-drain-apply-script.test.ts": 12427, - "src/main/codex-accounts/legacy-wsl-runtime-auth-drain-interruption-source.test.ts": 61, - "src/main/codex-accounts/legacy-wsl-runtime-auth-drain-rollback-script.test.ts": 520, - "src/main/codex-accounts/legacy-wsl-runtime-auth-drain.test.ts": 78, - "src/main/codex-accounts/managed-codex-auth-readiness.test.ts": 89, - "src/main/codex-accounts/runtime-home-catalog-account-key.test.ts": 1493, - "src/main/codex-accounts/runtime-home-catalog-probe-prep.test.ts": 381, - "src/main/codex-accounts/runtime-home-legacy-migration.test.ts": 869, - "src/main/codex-accounts/runtime-home-managed-auth-recovery.test.ts": 526, - "src/main/codex-accounts/runtime-home-mirror-login-copy.test.ts": 721, - "src/main/codex-accounts/runtime-home-mirrored-status-home.test.ts": 350, - "src/main/codex-accounts/runtime-home-per-account-homes.test.ts": 1014, - "src/main/codex-accounts/runtime-home-read-only-launch-resolution.test.ts": 480, - "src/main/codex-accounts/runtime-home-real-home-lane-routing.test.ts": 645, - "src/main/codex-accounts/runtime-home-resume-selection-gate.test.ts": 470, - "src/main/codex-accounts/runtime-home-retained-auth-provenance.test.ts": 1229, - "src/main/codex-accounts/runtime-home-service-per-account-migration.test.ts": 479, - "src/main/codex-accounts/runtime-home-session-migration-pass.test.ts": 555, - "src/main/codex-accounts/runtime-home-status-hook-independence.test.ts": 545, - "src/main/codex-accounts/runtime-home-system-default-mirror-readback.test.ts": 1406, - "src/main/codex-accounts/runtime-home-system-default-snapshot.test.ts": 949, - "src/main/codex-accounts/runtime-home-system-resource-materialization.test.ts": 604, - "src/main/codex-accounts/runtime-home-windows-profile-ownership.test.ts": 273, - "src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts": 878, - "src/main/codex-accounts/runtime-home-wsl-session-bridge.test.ts": 875, - "src/main/codex-accounts/runtime-home-wsl-system-default.test.ts": 661, - "src/main/codex-accounts/runtime-selection.test.ts": 47, - "src/main/codex-accounts/service-abandoned-login-supersede.test.ts": 1252, - "src/main/codex-accounts/service-account-add-login.test.ts": 699, - "src/main/codex-accounts/service-account-selection-and-removal.test.ts": 701, - "src/main/codex-accounts/service-add-account-from-home.test.ts": 519, - "src/main/codex-accounts/service-login-process-teardown.test.ts": 465, - "src/main/codex-accounts/service-managed-home-removal-retries.test.ts": 516, - "src/main/codex-accounts/service-quota-refresh-decoupling.test.ts": 395, - "src/main/codex-accounts/service-reauthenticate-activation.test.ts": 433, - "src/main/codex-accounts/service-reset-credit-durability.test.ts": 932, - "src/main/codex-accounts/service-reset-credit-home-ownership.test.ts": 608, - "src/main/codex-accounts/service-reset-credit-target-routing.test.ts": 652, - "src/main/codex-accounts/service-system-default-identity.test.ts": 511, - "src/main/codex-accounts/service-wsl-accounts.test.ts": 740, - "src/main/codex-accounts/service.test.ts": 615, - "src/main/codex-accounts/sta-4422-transient-ownership-error.test.ts": 525, - "src/main/codex-accounts/sta-4734-login-rollback-deletes-authenticated-home.test.ts": 465, - "src/main/codex-accounts/sta-4735-unreadable-host-lane-writes.test.ts": 528, - "src/main/codex-accounts/wsl-codex-command.test.ts": 50, - "src/main/codex-cli/codex-home-process-lock.test.ts": 53, - "src/main/codex-cli/command.test.ts": 90, - "src/main/codex-usage/codex-rollout-file-lifetime.test.ts": 160, - "src/main/codex-usage/codex-rollout-resume-state.test.ts": 75, - "src/main/codex-usage/codex-usage-cost-estimate.test.ts": 51, - "src/main/codex-usage/codex-usage-long-context-billing.test.ts": 491, - "src/main/codex-usage/scanner-attribution-memo.test.ts": 224, - "src/main/codex-usage/scanner-checkpoint-races.test.ts": 240, - "src/main/codex-usage/scanner-file-generation.test.ts": 197, - "src/main/codex-usage/scanner-file-identity.test.ts": 168, - "src/main/codex-usage/scanner-incremental-append.test.ts": 502, - "src/main/codex-usage/scanner-large-directory.test.ts": 673, - "src/main/codex-usage/scanner-paths.test.ts": 400, - "src/main/codex-usage/scanner.test.ts": 138, - "src/main/codex-usage/session-file-discovery-errors.test.ts": 71, - "src/main/codex-usage/store-automation-usage.test.ts": 385, - "src/main/codex-usage/store-model-pricing.test.ts": 431, - "src/main/codex-usage/store-orca-scope.test.ts": 404, - "src/main/codex-usage/store-persistence.test.ts": 360, - "src/main/codex-usage/store-snapshot.benchmark.test.ts": 637, - "src/main/codex/codex-account-session-bridge.test.ts": 376, - "src/main/codex/codex-account-session-index-heal.test.ts": 141, - "src/main/codex/codex-ai-vault-session-resume.test.ts": 145, - "src/main/codex/codex-app-server-capability-cache.test.ts": 65, - "src/main/codex/codex-app-server-client.test.ts": 11768, - "src/main/codex/codex-app-server-connection-exit-order.test.ts": 272, - "src/main/codex/codex-app-server-connection-tree-unproven.test.ts": 247, - "src/main/codex/codex-app-server-connection.test.ts": 1093, - "src/main/codex/codex-app-server-exit-error.test.ts": 202, - "src/main/codex/codex-app-server-session-stop.test.ts": 96, - "src/main/codex/codex-app-server-session-supervision.test.ts": 17428, - "src/main/codex/codex-app-server-session.test.ts": 278, - "src/main/codex/codex-app-server-teardown.integration.test.ts": 20731, - "src/main/codex/codex-background-command-tracker.test.ts": 377, - "src/main/codex/codex-background-task-tracker.test.ts": 326, - "src/main/codex/codex-background-terminals.test.ts": 476, - "src/main/codex/codex-child-work-evidence.test.ts": 416, - "src/main/codex/codex-child-work-translation-retention.test.ts": 424, - "src/main/codex/codex-close-turn-end.test.ts": 350, - "src/main/codex/codex-collab-call-delegation.test.ts": 435, - "src/main/codex/codex-collab-call-rows-on-positional-clients.test.ts": 522, - "src/main/codex/codex-command-turn-claim.test.ts": 332, - "src/main/codex/codex-config-mirror-hook-trust-carry.test.ts": 149, - "src/main/codex/codex-config-mirror-mcp-ownership.test.ts": 151, - "src/main/codex/codex-config-mirror.test.ts": 225, - "src/main/codex/codex-config-settings-removal.test.ts": 68, - "src/main/codex/codex-configured-launch-defaults.test.ts": 58, - "src/main/codex/codex-daemon-socket-path-guard.test.ts": 189, - "src/main/codex/codex-default-mode-subagents.test.ts": 763, - "src/main/codex/codex-goal-journal-rows.test.ts": 82, - "src/main/codex/codex-home-paths.test.ts": 116, - "src/main/codex/codex-hook-approval-first-write.test.ts": 89, - "src/main/codex/codex-hook-command-form.test.ts": 121, - "src/main/codex/codex-hook-file-entry-binary-contract.test.ts": 228, - "src/main/codex/codex-hook-hash-lookup.test.ts": 180, - "src/main/codex/codex-hook-legacy-profile-block.test.ts": 144, - "src/main/codex/codex-hook-real-home-status.test.ts": 321, - "src/main/codex/codex-hook-reconcile.test.ts": 2030, - "src/main/codex/codex-hook-retired-commands.test.ts": 81, - "src/main/codex/codex-hook-sweep-per-agent-opt-out.test.ts": 211, - "src/main/codex/codex-hook-toggle-off-wins.test.ts": 276, - "src/main/codex/codex-hook-trust-cleanup.test.ts": 140, - "src/main/codex/codex-hook-trust-derivation.test.ts": 165, - "src/main/codex/codex-hook-trust-failure-memo.test.ts": 359, - "src/main/codex/codex-hook-trust-grant.test.ts": 594, - "src/main/codex/codex-hook-trust-memo.test.ts": 170, - "src/main/codex/codex-hooks-read-denial-followup.test.ts": 284, - "src/main/codex/codex-index-heal-binary-contract.test.ts": 146, - "src/main/codex/codex-interrupt-hook-trust.test.ts": 69, - "src/main/codex/codex-legacy-session-resume.test.ts": 453, - "src/main/codex/codex-model-catalog-probe.test.ts": 3579, - "src/main/codex/codex-model-provider-config.test.ts": 55, - "src/main/codex/codex-notice-item-translation.test.ts": 341, - "src/main/codex/codex-pane-account-registry-mutations.test.ts": 48, - "src/main/codex/codex-pane-launch-account.test.ts": 58, - "src/main/codex/codex-persistent-command-retention.test.ts": 416, - "src/main/codex/codex-pinned-rollout-proof.test.ts": 87, - "src/main/codex/codex-prompt-registry-retention.test.ts": 711, - "src/main/codex/codex-provider-retry-idle-sweep.test.ts": 1708, - "src/main/codex/codex-provider-retry-row.test.ts": 419, - "src/main/codex/codex-real-home-hook-entries.test.ts": 383, - "src/main/codex/codex-real-home-hook-key-spellings.test.ts": 283, - "src/main/codex/codex-real-home-hook-removal.test.ts": 214, - "src/main/codex/codex-real-home-path.test.ts": 70, - "src/main/codex/codex-real-home-shared-entry.test.ts": 433, - "src/main/codex/codex-real-home-windows-key-spellings.test.ts": 158, - "src/main/codex/codex-requested-close-turn-timing.test.ts": 341, - "src/main/codex/codex-rollout-session-meta.test.ts": 108, - "src/main/codex/codex-server-request-disposition.test.ts": 253, - "src/main/codex/codex-session-backfill-marker.test.ts": 92, - "src/main/codex/codex-session-backfill-scan-dates.test.ts": 68, - "src/main/codex/codex-session-backfill.test.ts": 314, - "src/main/codex/codex-session-bridge.test.ts": 122, - "src/main/codex/codex-session-index-heal-state.test.ts": 506, - "src/main/codex/codex-session-index-heal.test.ts": 2477, - "src/main/codex/codex-session-migration-recent-exits.test.ts": 44, - "src/main/codex/codex-session-migration-scheduler.test.ts": 436, - "src/main/codex/codex-session-resume-home.test.ts": 217, - "src/main/codex/codex-session-resume-preparation.test.ts": 68, - "src/main/codex/codex-session-resume-windows.test.ts": 60, - "src/main/codex/codex-session-resume-wrong-account.test.ts": 75, - "src/main/codex/codex-session-source-home.test.ts": 47, - "src/main/codex/codex-shared-server-fix.test.ts": 74, - "src/main/codex/codex-shared-server-pane.test.ts": 89, - "src/main/codex/codex-shared-server-probe.test.ts": 104, - "src/main/codex/codex-short-lived-app-server-spawn.test.ts": 431, - "src/main/codex/codex-stale-pane-accounts.test.ts": 118, - "src/main/codex/codex-state-db-backfill-recovery-process.test.ts": 75, - "src/main/codex/codex-state-db-backfill-recovery-supervision.test.ts": 8520, - "src/main/codex/codex-state-db-backfill-recovery.test.ts": 286, - "src/main/codex/codex-state-db.test.ts": 127, - "src/main/codex/codex-structured-acquisition-exit-proof.test.ts": 540, - "src/main/codex/codex-structured-child-environment.test.ts": 504, - "src/main/codex/codex-structured-child-work-producer.test.ts": 625, - "src/main/codex/codex-structured-compaction-refusal.test.ts": 377, - "src/main/codex/codex-structured-conversation-stop.test.ts": 808, - "src/main/codex/codex-structured-dispatch-admission.test.ts": 772, - "src/main/codex/codex-structured-dispatch-boundary.test.ts": 696, - "src/main/codex/codex-structured-dispatch-echo.test.ts": 76, - "src/main/codex/codex-structured-item-translation.test.ts": 205, - "src/main/codex/codex-structured-journal-compactions.test.ts": 406, - "src/main/codex/codex-structured-journal-exit-convergence.test.ts": 821, - "src/main/codex/codex-structured-journal-goal-admission.test.ts": 341, - "src/main/codex/codex-structured-journal-goal-resume.test.ts": 386, - "src/main/codex/codex-structured-journal-goal-revision.test.ts": 603, - "src/main/codex/codex-structured-journal-goal-rows.test.ts": 320, - "src/main/codex/codex-structured-journal-repeated-questions.test.ts": 446, - "src/main/codex/codex-structured-journal-restore-turn-grouping.test.ts": 337, - "src/main/codex/codex-structured-journal-settlement-rollback.test.ts": 631, - "src/main/codex/codex-structured-journal-subagent-readers.test.ts": 744, - "src/main/codex/codex-structured-journal-translation-settlement.test.ts": 673, - "src/main/codex/codex-structured-journal-translation-streams.test.ts": 507, - "src/main/codex/codex-structured-journal-translation-subagent-linkage.test.ts": 337, - "src/main/codex/codex-structured-journal-translation-subagents.test.ts": 404, - "src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts": 692, - "src/main/codex/codex-structured-journal-translation-turn-settles-once.test.ts": 363, - "src/main/codex/codex-structured-journal-translation-turn-state.test.ts": 71, - "src/main/codex/codex-structured-journal-translation.test.ts": 447, - "src/main/codex/codex-structured-journal-turn-endings-replay.test.ts": 314, - "src/main/codex/codex-structured-journal-turn-settles-once-journal.test.ts": 472, - "src/main/codex/codex-structured-launch-args.test.ts": 54, - "src/main/codex/codex-structured-launch-resolution.test.ts": 482, - "src/main/codex/codex-structured-live-provider-process.test.ts": 397, - "src/main/codex/codex-structured-owner-identity.test.ts": 83, - "src/main/codex/codex-structured-permission-policy.test.ts": 73, + "src/main/claude/claude-subagent-permission-request.test.ts": 2912, + "src/main/claude/claude-subagent-resume-after-restart.test.ts": 618, + "src/main/claude/claude-subagent-task-frames.test.ts": 54, + "src/main/claude/claude-supervised-stop-real-cli.test.ts": 428, + "src/main/claude/claude-supervised-stop.integration.test.ts": 3753, + "src/main/claude/claude-transcript-branch-graph.test.ts": 37, + "src/main/claude/claude-transcript-branch-streaming.test.ts": 181, + "src/main/claude/claude-transcript-entry-uuid.test.ts": 32, + "src/main/claude/claude-transcript-marker-branch-proof.test.ts": 64, + "src/main/claude/claude-transcript-rewind-proof.test.ts": 35, + "src/main/claude/claude-turn-outcome.test.ts": 422, + "src/main/claude/claude-turn-ownership.test.ts": 750, + "src/main/claude/claude-turn-resumption.test.ts": 490, + "src/main/claude/compact-status-registration.test.ts": 270, + "src/main/claude/hook-service.test.ts": 235, + "src/main/claude/post-compact-hook-command.test.ts": 179, + "src/main/claude/statusline-script.test.ts": 257, + "src/main/claude/windows-hook-files.test.ts": 171, + "src/main/claude/windows-native-hook-registration.test.ts": 151, + "src/main/cli/appimage-copy-pre-extraction.test.ts": 329, + "src/main/cli/appimage-extracted-root.test.ts": 142, + "src/main/cli/appimage-extraction-pruning-namespace.test.ts": 106, + "src/main/cli/appimage-extraction-pruning.test.ts": 126, + "src/main/cli/appimage-generation-lease.test.ts": 64, + "src/main/cli/appimage-generation-residency.test.ts": 40, + "src/main/cli/appimage-generation-stable-launcher.test.ts": 229, + "src/main/cli/appimage-payload-removal.reentrancy.test.ts": 48, + "src/main/cli/appimage-payload-removal.test.ts": 31, + "src/main/cli/appimage-registration-lock.test.ts": 74, + "src/main/cli/appimage-serve-preparation.test.ts": 751, + "src/main/cli/appimage-stable-launcher.test.ts": 233, + "src/main/cli/cli-command-installation-races.test.ts": 150, + "src/main/cli/cli-command-privileged-transaction.test.ts": 102, + "src/main/cli/cli-installer-appimage-ownership.test.ts": 187, + "src/main/cli/cli-installer-appimage-removal.test.ts": 257, + "src/main/cli/cli-installer-command-conflicts.test.ts": 192, + "src/main/cli/cli-installer-macos-command-path.test.ts": 193, + "src/main/cli/cli-installer-windows-path.test.ts": 149, + "src/main/cli/cli-installer.test.ts": 235, + "src/main/cli/cli-privileged-processes.test.ts": 45, + "src/main/cli/keyed-promise-queue.test.ts": 41, + "src/main/cli/legacy-appimage-cli-wrapper.test.ts": 42, + "src/main/cli/linux-bare-orca-dispatcher.test.ts": 133, + "src/main/cli/linux-terminal-orca-cli-shim.test.ts": 161, + "src/main/cli/native-cli-powershell-argv-boundary.test.ts": 41, + "src/main/cli/orca-cli-child-path.test.ts": 89, + "src/main/cli/packaged-cli-assets.test.ts": 6567, + "src/main/cli/windows-user-path-registry.test.ts": 28, + "src/main/cli/wsl-cli-installer.test.ts": 314, + "src/main/cli/wsl-cli-powershell-boundary.test.ts": 85, + "src/main/cli/wsl-cli-registration-operation.test.ts": 91, + "src/main/cli/wsl-cli-registration-reconciliation.test.ts": 209, + "src/main/cli/wsl-cli-registration-registry.test.ts": 151, + "src/main/cli/wsl-managed-cli.test.ts": 56, + "src/main/cli/wsl-managed-cli.wsl.test.ts": 92, + "src/main/codebuddy/hook-service.test.ts": 131, + "src/main/codex-accounts/async-file-rename.test.ts": 1027, + "src/main/codex-accounts/codex-account-identity-api-key-guard.test.ts": 37, + "src/main/codex-accounts/codex-auth-workspace-identity.test.ts": 49, + "src/main/codex-accounts/codex-config-mirror.test.ts": 328, + "src/main/codex-accounts/codex-credential-absence-grace.test.ts": 77, + "src/main/codex-accounts/codex-login-auth-url.test.ts": 39, + "src/main/codex-accounts/codex-reset-credit-ledger.test.ts": 254, + "src/main/codex-accounts/codex-windows-interactive-login.test.ts": 506, + "src/main/codex-accounts/fs-utils.test.ts": 113, + "src/main/codex-accounts/guest-codex-account-homes.test.ts": 60, + "src/main/codex-accounts/host-codex-managed-home-ownership.test.ts": 41, + "src/main/codex-accounts/legacy-shared-auth-migration.test.ts": 103, + "src/main/codex-accounts/legacy-shared-config-compatibility.test.ts": 143, + "src/main/codex-accounts/legacy-wsl-runtime-auth-drain-apply-script.test.ts": 12778, + "src/main/codex-accounts/legacy-wsl-runtime-auth-drain-interruption-source.test.ts": 63, + "src/main/codex-accounts/legacy-wsl-runtime-auth-drain-rollback-script.test.ts": 506, + "src/main/codex-accounts/legacy-wsl-runtime-auth-drain.test.ts": 64, + "src/main/codex-accounts/managed-codex-auth-readiness.test.ts": 60, + "src/main/codex-accounts/runtime-home-catalog-account-key.test.ts": 2360, + "src/main/codex-accounts/runtime-home-catalog-probe-prep.test.ts": 560, + "src/main/codex-accounts/runtime-home-legacy-migration.test.ts": 1057, + "src/main/codex-accounts/runtime-home-managed-auth-recovery.test.ts": 566, + "src/main/codex-accounts/runtime-home-mirror-login-copy.test.ts": 815, + "src/main/codex-accounts/runtime-home-mirrored-status-home.test.ts": 309, + "src/main/codex-accounts/runtime-home-per-account-homes.test.ts": 706, + "src/main/codex-accounts/runtime-home-read-only-launch-resolution.test.ts": 573, + "src/main/codex-accounts/runtime-home-real-home-lane-routing.test.ts": 690, + "src/main/codex-accounts/runtime-home-restart-stopped-distro.test.ts": 442, + "src/main/codex-accounts/runtime-home-resume-selection-gate.test.ts": 559, + "src/main/codex-accounts/runtime-home-retained-auth-provenance.test.ts": 1166, + "src/main/codex-accounts/runtime-home-service-per-account-migration.test.ts": 536, + "src/main/codex-accounts/runtime-home-session-migration-pass.test.ts": 645, + "src/main/codex-accounts/runtime-home-status-hook-independence.test.ts": 713, + "src/main/codex-accounts/runtime-home-system-default-mirror-readback.test.ts": 1295, + "src/main/codex-accounts/runtime-home-system-default-snapshot.test.ts": 785, + "src/main/codex-accounts/runtime-home-system-resource-materialization.test.ts": 633, + "src/main/codex-accounts/runtime-home-windows-profile-ownership.test.ts": 290, + "src/main/codex-accounts/runtime-home-wsl-managed-accounts.test.ts": 982, + "src/main/codex-accounts/runtime-home-wsl-session-bridge.test.ts": 826, + "src/main/codex-accounts/runtime-home-wsl-system-default.test.ts": 649, + "src/main/codex-accounts/runtime-selection.test.ts": 37, + "src/main/codex-accounts/service-abandoned-login-supersede.test.ts": 886, + "src/main/codex-accounts/service-account-add-login.test.ts": 906, + "src/main/codex-accounts/service-account-selection-and-removal.test.ts": 952, + "src/main/codex-accounts/service-add-account-from-home.test.ts": 505, + "src/main/codex-accounts/service-login-process-teardown.test.ts": 508, + "src/main/codex-accounts/service-managed-home-removal-retries.test.ts": 425, + "src/main/codex-accounts/service-quota-refresh-decoupling.test.ts": 456, + "src/main/codex-accounts/service-reauthenticate-activation.test.ts": 493, + "src/main/codex-accounts/service-reset-credit-durability.test.ts": 845, + "src/main/codex-accounts/service-reset-credit-home-ownership.test.ts": 600, + "src/main/codex-accounts/service-reset-credit-target-routing.test.ts": 719, + "src/main/codex-accounts/service-system-default-identity.test.ts": 492, + "src/main/codex-accounts/service-wsl-accounts.test.ts": 541, + "src/main/codex-accounts/service.test.ts": 605, + "src/main/codex-accounts/sta-4422-transient-ownership-error.test.ts": 538, + "src/main/codex-accounts/sta-4734-login-rollback-deletes-authenticated-home.test.ts": 499, + "src/main/codex-accounts/sta-4735-unreadable-host-lane-writes.test.ts": 517, + "src/main/codex-accounts/wsl-codex-command.test.ts": 39, + "src/main/codex-cli/codex-home-process-lock.test.ts": 39, + "src/main/codex-cli/command.test.ts": 97, + "src/main/codex-usage/codex-rollout-file-lifetime.test.ts": 210, + "src/main/codex-usage/codex-rollout-resume-state.test.ts": 73, + "src/main/codex-usage/codex-usage-cost-estimate.test.ts": 33, + "src/main/codex-usage/codex-usage-long-context-billing.test.ts": 530, + "src/main/codex-usage/scanner-attribution-memo.test.ts": 260, + "src/main/codex-usage/scanner-checkpoint-races.test.ts": 304, + "src/main/codex-usage/scanner-file-generation.test.ts": 248, + "src/main/codex-usage/scanner-file-identity.test.ts": 166, + "src/main/codex-usage/scanner-incremental-append.test.ts": 537, + "src/main/codex-usage/scanner-large-directory.test.ts": 453, + "src/main/codex-usage/scanner-paths.test.ts": 316, + "src/main/codex-usage/scanner.test.ts": 106, + "src/main/codex-usage/session-file-discovery-errors.test.ts": 59, + "src/main/codex-usage/store-automation-usage.test.ts": 381, + "src/main/codex-usage/store-model-pricing.test.ts": 419, + "src/main/codex-usage/store-orca-scope.test.ts": 407, + "src/main/codex-usage/store-persistence.test.ts": 1203, + "src/main/codex-usage/store-snapshot.benchmark.test.ts": 447, + "src/main/codex/codex-account-home-hook.test.ts": 269, + "src/main/codex/codex-account-session-bridge.test.ts": 343, + "src/main/codex/codex-account-session-index-heal.test.ts": 173, + "src/main/codex/codex-ai-vault-session-resume.test.ts": 130, + "src/main/codex/codex-app-server-capability-cache.test.ts": 39, + "src/main/codex/codex-app-server-client.test.ts": 12524, + "src/main/codex/codex-app-server-connection-exit-order.test.ts": 293, + "src/main/codex/codex-app-server-connection-tree-unproven.test.ts": 268, + "src/main/codex/codex-app-server-connection.test.ts": 1089, + "src/main/codex/codex-app-server-exit-error.test.ts": 365, + "src/main/codex/codex-app-server-session-stop.test.ts": 87, + "src/main/codex/codex-app-server-session-supervision.test.ts": 17983, + "src/main/codex/codex-app-server-session.test.ts": 268, + "src/main/codex/codex-app-server-teardown.integration.test.ts": 21370, + "src/main/codex/codex-background-command-tracker.test.ts": 365, + "src/main/codex/codex-background-task-tracker.test.ts": 389, + "src/main/codex/codex-background-terminals.test.ts": 449, + "src/main/codex/codex-child-work-evidence.test.ts": 629, + "src/main/codex/codex-child-work-translation-retention.test.ts": 582, + "src/main/codex/codex-close-turn-end.test.ts": 321, + "src/main/codex/codex-collab-call-delegation.test.ts": 482, + "src/main/codex/codex-collab-call-rows-on-positional-clients.test.ts": 567, + "src/main/codex/codex-command-turn-claim.test.ts": 358, + "src/main/codex/codex-config-mirror-hook-trust-carry.test.ts": 258, + "src/main/codex/codex-config-mirror-mcp-ownership.test.ts": 117, + "src/main/codex/codex-config-mirror.test.ts": 264, + "src/main/codex/codex-config-settings-removal.test.ts": 57, + "src/main/codex/codex-configured-launch-defaults.test.ts": 46, + "src/main/codex/codex-daemon-socket-path-guard.test.ts": 165, + "src/main/codex/codex-default-mode-subagents.test.ts": 709, + "src/main/codex/codex-goal-journal-rows.test.ts": 72, + "src/main/codex/codex-home-paths.test.ts": 134, + "src/main/codex/codex-hook-approval-first-write.test.ts": 112, + "src/main/codex/codex-hook-command-form.test.ts": 172, + "src/main/codex/codex-hook-file-entry-binary-contract.test.ts": 266, + "src/main/codex/codex-hook-hash-lookup.test.ts": 232, + "src/main/codex/codex-hook-legacy-profile-block.test.ts": 119, + "src/main/codex/codex-hook-real-home-status.test.ts": 328, + "src/main/codex/codex-hook-reconcile.test.ts": 2218, + "src/main/codex/codex-hook-retired-commands.test.ts": 75, + "src/main/codex/codex-hook-sweep-per-agent-opt-out.test.ts": 249, + "src/main/codex/codex-hook-toggle-off-wins.test.ts": 437, + "src/main/codex/codex-hook-trust-cleanup.test.ts": 102, + "src/main/codex/codex-hook-trust-derivation.test.ts": 218, + "src/main/codex/codex-hook-trust-failure-memo.test.ts": 410, + "src/main/codex/codex-hook-trust-grant.test.ts": 568, + "src/main/codex/codex-hook-trust-memo.test.ts": 187, + "src/main/codex/codex-hooks-read-denial-followup.test.ts": 231, + "src/main/codex/codex-index-heal-binary-contract.test.ts": 167, + "src/main/codex/codex-interrupt-hook-trust.test.ts": 107, + "src/main/codex/codex-legacy-session-resume.test.ts": 246, + "src/main/codex/codex-minimum-version.test.ts": 797, + "src/main/codex/codex-model-catalog-probe.test.ts": 3355, + "src/main/codex/codex-model-provider-config.test.ts": 47, + "src/main/codex/codex-notice-item-translation.test.ts": 333, + "src/main/codex/codex-pane-account-registry-mutations.test.ts": 38, + "src/main/codex/codex-pane-launch-account.test.ts": 41, + "src/main/codex/codex-persistent-command-retention.test.ts": 498, + "src/main/codex/codex-pinned-rollout-proof.test.ts": 68, + "src/main/codex/codex-prompt-registry-retention.test.ts": 712, + "src/main/codex/codex-provider-retry-idle-sweep.test.ts": 1918, + "src/main/codex/codex-provider-retry-row.test.ts": 527, + "src/main/codex/codex-real-home-hook-entries.test.ts": 543, + "src/main/codex/codex-real-home-hook-key-spellings.test.ts": 310, + "src/main/codex/codex-real-home-hook-removal.test.ts": 325, + "src/main/codex/codex-real-home-path.test.ts": 56, + "src/main/codex/codex-real-home-shared-entry.test.ts": 472, + "src/main/codex/codex-real-home-windows-key-spellings.test.ts": 173, + "src/main/codex/codex-requested-close-turn-timing.test.ts": 373, + "src/main/codex/codex-rollout-session-meta.test.ts": 78, + "src/main/codex/codex-server-request-disposition.test.ts": 249, + "src/main/codex/codex-session-backfill-marker.test.ts": 88, + "src/main/codex/codex-session-backfill-scan-dates.test.ts": 58, + "src/main/codex/codex-session-backfill.test.ts": 351, + "src/main/codex/codex-session-bridge.test.ts": 90, + "src/main/codex/codex-session-index-heal-state.test.ts": 373, + "src/main/codex/codex-session-index-heal.test.ts": 2651, + "src/main/codex/codex-session-migration-recent-exits.test.ts": 34, + "src/main/codex/codex-session-migration-scheduler.test.ts": 423, + "src/main/codex/codex-session-resume-home.test.ts": 114, + "src/main/codex/codex-session-resume-preparation.test.ts": 41, + "src/main/codex/codex-session-resume-windows.test.ts": 33, + "src/main/codex/codex-session-resume-wrong-account.test.ts": 70, + "src/main/codex/codex-session-source-home.test.ts": 30, + "src/main/codex/codex-shared-server-fix.test.ts": 82, + "src/main/codex/codex-shared-server-pane.test.ts": 100, + "src/main/codex/codex-shared-server-probe.test.ts": 152, + "src/main/codex/codex-short-lived-app-server-spawn.test.ts": 186, + "src/main/codex/codex-stale-pane-accounts.test.ts": 128, + "src/main/codex/codex-state-db-backfill-recovery-process.test.ts": 71, + "src/main/codex/codex-state-db-backfill-recovery-supervision.test.ts": 8406, + "src/main/codex/codex-state-db-backfill-recovery.test.ts": 311, + "src/main/codex/codex-state-db.test.ts": 135, + "src/main/codex/codex-structured-acquisition-exit-proof.test.ts": 489, + "src/main/codex/codex-structured-child-environment.test.ts": 630, + "src/main/codex/codex-structured-child-work-producer.test.ts": 736, + "src/main/codex/codex-structured-compaction-refusal.test.ts": 492, + "src/main/codex/codex-structured-conversation-stop.test.ts": 834, + "src/main/codex/codex-structured-dispatch-admission.test.ts": 713, + "src/main/codex/codex-structured-dispatch-boundary.test.ts": 391, + "src/main/codex/codex-structured-dispatch-echo.test.ts": 39, + "src/main/codex/codex-structured-item-translation.test.ts": 181, + "src/main/codex/codex-structured-journal-compactions.test.ts": 430, + "src/main/codex/codex-structured-journal-context-usage.test.ts": 646, + "src/main/codex/codex-structured-journal-exit-convergence.test.ts": 964, + "src/main/codex/codex-structured-journal-goal-admission.test.ts": 349, + "src/main/codex/codex-structured-journal-goal-resume.test.ts": 440, + "src/main/codex/codex-structured-journal-goal-revision.test.ts": 600, + "src/main/codex/codex-structured-journal-goal-rows.test.ts": 262, + "src/main/codex/codex-structured-journal-repeated-questions.test.ts": 656, + "src/main/codex/codex-structured-journal-restore-turn-grouping.test.ts": 430, + "src/main/codex/codex-structured-journal-settlement-rollback.test.ts": 908, + "src/main/codex/codex-structured-journal-subagent-readers.test.ts": 738, + "src/main/codex/codex-structured-journal-translation-settlement.test.ts": 718, + "src/main/codex/codex-structured-journal-translation-streams.test.ts": 458, + "src/main/codex/codex-structured-journal-translation-subagent-linkage.test.ts": 359, + "src/main/codex/codex-structured-journal-translation-subagents.test.ts": 374, + "src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts": 579, + "src/main/codex/codex-structured-journal-translation-turn-settles-once.test.ts": 382, + "src/main/codex/codex-structured-journal-translation-turn-state.test.ts": 51, + "src/main/codex/codex-structured-journal-translation.test.ts": 468, + "src/main/codex/codex-structured-journal-turn-endings-replay.test.ts": 414, + "src/main/codex/codex-structured-journal-turn-settles-once-journal.test.ts": 790, + "src/main/codex/codex-structured-launch-args.test.ts": 61, + "src/main/codex/codex-structured-launch-resolution.test.ts": 645, + "src/main/codex/codex-structured-live-provider-process.test.ts": 485, + "src/main/codex/codex-structured-owner-identity.test.ts": 81, + "src/main/codex/codex-structured-permission-policy.test.ts": 33, "src/main/codex/codex-structured-prompt-items.test.ts": 304, - "src/main/codex/codex-structured-prompt-ownership.test.ts": 648, - "src/main/codex/codex-structured-prompt-replies.test.ts": 370, - "src/main/codex/codex-structured-question-order.test.ts": 1100, - "src/main/codex/codex-structured-real-cli-visuals.test.ts": 228, - "src/main/codex/codex-structured-reasoning-lifecycle.test.ts": 268, - "src/main/codex/codex-structured-rewind.test.ts": 502, - "src/main/codex/codex-structured-service-tier.test.ts": 649, - "src/main/codex/codex-structured-session-adapter-lifecycle.test.ts": 558, - "src/main/codex/codex-structured-session-adapter.test.ts": 645, - "src/main/codex/codex-structured-session-background-tasks.test.ts": 661, - "src/main/codex/codex-structured-session-cancel.test.ts": 566, - "src/main/codex/codex-structured-session-close.test.ts": 579, - "src/main/codex/codex-structured-session-exit-blame.test.ts": 486, - "src/main/codex/codex-structured-session-options-catalog.test.ts": 541, - "src/main/codex/codex-structured-session-options.test.ts": 609, - "src/main/codex/codex-structured-session-prompts.test.ts": 483, - "src/main/codex/codex-structured-session-shutdown.test.ts": 649, - "src/main/codex/codex-structured-sign-in.test.ts": 301, - "src/main/codex/codex-structured-thread-goal.test.ts": 339, - "src/main/codex/codex-structured-thread-open.test.ts": 392, - "src/main/codex/codex-structured-turn-end-settlement.test.ts": 1386, - "src/main/codex/codex-structured-turn-open-wait.test.ts": 1451, - "src/main/codex/codex-structured-turn-steer.test.ts": 800, - "src/main/codex/codex-structured-visuals.test.ts": 479, - "src/main/codex/codex-subagent-execution-projection.test.ts": 490, - "src/main/codex/codex-subagent-executions.test.ts": 83, - "src/main/codex/codex-subagent-interrupts.test.ts": 503, - "src/main/codex/codex-subagent-roster-retention.test.ts": 3130, - "src/main/codex/codex-subagent-roster.test.ts": 383, - "src/main/codex/codex-thread-token-totals.test.ts": 39, - "src/main/codex/codex-tool-identity-translation.test.ts": 105, - "src/main/codex/codex-trust-config-concurrent-launch.test.ts": 250, - "src/main/codex/codex-trust-config-mutation-queue.test.ts": 63, - "src/main/codex/codex-trust-grant-host.test.ts": 117, - "src/main/codex/codex-trust-grant-ledger.test.ts": 102, - "src/main/codex/codex-trust-grant-telemetry.test.ts": 161, - "src/main/codex/codex-turn-ordinals.test.ts": 89, - "src/main/codex/codex-user-hook-trust-moves.test.ts": 122, - "src/main/codex/codex-wsl-hook-install-plan.test.ts": 71, - "src/main/codex/config-features-settings-promotion.test.ts": 155, - "src/main/codex/config-plugin-registration-promotion.test.ts": 285, - "src/main/codex/config-settings-baseline-upgrade.test.ts": 169, - "src/main/codex/config-settings-promotion.test.ts": 280, - "src/main/codex/config-sync-stall.test.ts": 155, - "src/main/codex/config-toml-hook-state-duplicate-repair.test.ts": 123, - "src/main/codex/config-toml-hook-trust-scaling.test.ts": 120, - "src/main/codex/config-toml-mcp-servers.test.ts": 65, - "src/main/codex/config-toml-project-duplicate-repair.test.ts": 95, - "src/main/codex/config-toml-project-trust-spellings.test.ts": 113, - "src/main/codex/config-toml-promoted-setting-values.test.ts": 43, - "src/main/codex/config-toml-trust-hash.test.ts": 78, - "src/main/codex/config-toml-trust-hook-read.test.ts": 145, - "src/main/codex/config-toml-trust-hook-removal.test.ts": 152, - "src/main/codex/config-toml-trust-hook-upsert.test.ts": 194, - "src/main/codex/config-toml-trust-key.test.ts": 116, - "src/main/codex/config-toml-trust-loadability.test.ts": 182, - "src/main/codex/config-toml-trust-paths.test.ts": 100, - "src/main/codex/config-toml-trust-project.test.ts": 114, - "src/main/codex/config-toml-trust-stale-writes.test.ts": 71, - "src/main/codex/hook-service-concurrent-launch-install.test.ts": 1027, - "src/main/codex/hook-service-legacy-cleanup.test.ts": 907, - "src/main/codex/hook-service-managed-codex-hashes.test.ts": 646, - "src/main/codex/hook-service-managed-install.test.ts": 438, - "src/main/codex/hook-service-runtime-trust-repair.test.ts": 362, - "src/main/codex/hook-service-trust-grant.test.ts": 230, - "src/main/codex/hook-service-user-hook-mirroring.test.ts": 469, - "src/main/codex/hook-service-user-hook-trust-spellings.test.ts": 414, - "src/main/codex/hook-service-windows-key-spellings.test.ts": 220, - "src/main/codex/hook-service-wsl-runtime.test.ts": 366, - "src/main/codex/hook-trust-promotion.test.ts": 474, - "src/main/codex/retained-codex-hook-state.test.ts": 41, - "src/main/codex/sta-4735-hook-trust-provenance-overwrite.test.ts": 110, - "src/main/codex/sta-4737-unreadable-is-not-absent.test.ts": 161, - "src/main/codex/sta-4823-shared-state-survives-a-failed-read.test.ts": 193, - "src/main/codex/windows-hook-command.test.ts": 119, - "src/main/codex/windows-hook-upgrade.test.ts": 269, - "src/main/codex/wsl-codex-session-bridge.test.ts": 125, - "src/main/command-code/hook-service.test.ts": 255, - "src/main/computer/computer-action-verification-normalization.test.ts": 38, - "src/main/computer/computer-provider-lifecycle.test.ts": 152, - "src/main/computer/computer-provider-unavailable-message.test.ts": 45, - "src/main/computer/computer-sidecar-diagnostics.test.ts": 53, - "src/main/computer/desktop-script-provider-action-errors.test.ts": 149, - "src/main/computer/desktop-script-provider-actions.test.ts": 136, - "src/main/computer/desktop-script-provider-bridge.test.ts": 50, - "src/main/computer/desktop-script-provider-cache-lifecycle.test.ts": 163, - "src/main/computer/desktop-script-provider-cache.test.ts": 148, - "src/main/computer/desktop-script-provider-client.test.ts": 216, - "src/main/computer/desktop-script-provider-errors.test.ts": 290, - "src/main/computer/desktop-script-provider-paste-validation.test.ts": 123, - "src/main/computer/desktop-script-provider-runtime-host-routing.test.ts": 231, - "src/main/computer/desktop-script-runtime-host.test.ts": 248, - "src/main/computer/desktop-script-runtime-host.win32.test.ts": 80, - "src/main/computer/desktop-script-serve-channel.test.ts": 112, - "src/main/computer/macos-computer-use-permission-status.test.ts": 101, - "src/main/computer/macos-computer-use-permissions.test.ts": 115, - "src/main/computer/macos-native-provider-client.test.ts": 1179, - "src/main/computer/macos-native-provider-line-buffer.test.ts": 63, - "src/main/computer/macos-native-provider-paste-validation.test.ts": 166, - "src/main/computer/macos-native-provider-process-reaping.test.ts": 103, - "src/main/computer/macos-native-provider-reaping.integration.test.ts": 4160, - "src/main/computer/macos-native-provider-socket.test.ts": 60, - "src/main/computer/macos-native-provider-startup-cleanup.test.ts": 82, - "src/main/computer/sidecar-client.test.ts": 130, - "src/main/computer/sidecar-provider-reaping.integration.test.ts": 335, - "src/main/computer/windows-powershell-execution-policy.test.ts": 54, - "src/main/copilot/hook-service.test.ts": 112, - "src/main/crash-reporting/crash-breadcrumb-renderer-attribution.test.ts": 61, - "src/main/crash-reporting/crash-breadcrumb-store-leak.test.ts": 123, - "src/main/crash-reporting/crash-breadcrumb-store-orphan-cleanup.test.ts": 96, - "src/main/crash-reporting/crash-breadcrumb-store.test.ts": 156, - "src/main/crash-reporting/crash-report-copy-text.test.ts": 58, - "src/main/crash-reporting/crash-report-store.test.ts": 455, - "src/main/crash-reporting/crashpad-capture-read-limit.test.ts": 274, - "src/main/crash-reporting/crashpad-capture.test.ts": 102, - "src/main/crash-reporting/durable-crash-breadcrumb.test.ts": 67, - "src/main/crash-reporting/expected-teardown-state.test.ts": 57, - "src/main/crash-reporting/gpu-crash-diagnostics.test.ts": 63, - "src/main/crash-reporting/gpu-crash-fallback-decision.test.ts": 49, - "src/main/crash-reporting/gpu-crash-fallback-field-sessions.test.ts": 48, - "src/main/crash-reporting/gpu-fallback-engagement.test.ts": 78, - "src/main/crash-reporting/gpu-fallback-recovered-launch.test.ts": 53, - "src/main/crash-reporting/gpu-fallback-restart-prompt.test.ts": 55, - "src/main/crash-reporting/low-commit-oom-recovery-gate.test.ts": 61, - "src/main/crash-reporting/main-process-lifecycle-identity.test.ts": 51, - "src/main/crash-reporting/minidump-crash-signature.test.ts": 88, - "src/main/crash-reporting/minidump-file-source.test.ts": 248, - "src/main/crash-reporting/pre-gone-host-memory.test.ts": 114, - "src/main/crash-reporting/process-gone-classification.test.ts": 56, - "src/main/crash-reporting/process-gone-dedupe.test.ts": 52, - "src/main/crash-reporting/process-gone-diagnostics.test.ts": 112, - "src/main/crash-reporting/process-gone-killed-one-ordering.test.ts": 172, - "src/main/crash-reporting/process-gone-recorder.test.ts": 767, - "src/main/crash-reporting/process-gone-sibling-correlation.test.ts": 643, - "src/main/crash-reporting/renderer-recovery-circuit-breaker.test.ts": 66, - "src/main/crash-reporting/self-initiated-tree-kill-log.test.ts": 126, - "src/main/crash-reporting/suppressed-process-gone-breadcrumb.test.ts": 49, + "src/main/codex/codex-structured-prompt-ownership.test.ts": 599, + "src/main/codex/codex-structured-prompt-replies.test.ts": 271, + "src/main/codex/codex-structured-question-order.test.ts": 1568, + "src/main/codex/codex-structured-real-cli-visuals.test.ts": 312, + "src/main/codex/codex-structured-reasoning-lifecycle.test.ts": 385, + "src/main/codex/codex-structured-rewind.test.ts": 424, + "src/main/codex/codex-structured-service-tier.test.ts": 554, + "src/main/codex/codex-structured-session-adapter-lifecycle.test.ts": 478, + "src/main/codex/codex-structured-session-adapter.test.ts": 1004, + "src/main/codex/codex-structured-session-background-tasks.test.ts": 533, + "src/main/codex/codex-structured-session-cancel.test.ts": 449, + "src/main/codex/codex-structured-session-close.test.ts": 507, + "src/main/codex/codex-structured-session-exit-blame.test.ts": 529, + "src/main/codex/codex-structured-session-options-catalog.test.ts": 521, + "src/main/codex/codex-structured-session-options.test.ts": 557, + "src/main/codex/codex-structured-session-prompts.test.ts": 482, + "src/main/codex/codex-structured-session-shutdown.test.ts": 551, + "src/main/codex/codex-structured-sign-in.test.ts": 367, + "src/main/codex/codex-structured-thread-goal.test.ts": 282, + "src/main/codex/codex-structured-thread-open.test.ts": 383, + "src/main/codex/codex-structured-turn-end-settlement.test.ts": 1408, + "src/main/codex/codex-structured-turn-open-wait.test.ts": 1524, + "src/main/codex/codex-structured-turn-steer.test.ts": 744, + "src/main/codex/codex-structured-visuals.test.ts": 775, + "src/main/codex/codex-subagent-activity.test.ts": 49, + "src/main/codex/codex-subagent-execution-projection.test.ts": 384, + "src/main/codex/codex-subagent-executions.test.ts": 53, + "src/main/codex/codex-subagent-interrupts.test.ts": 580, + "src/main/codex/codex-tool-identity-translation.test.ts": 70, + "src/main/codex/codex-trust-config-concurrent-launch.test.ts": 220, + "src/main/codex/codex-trust-config-mutation-queue.test.ts": 44, + "src/main/codex/codex-trust-grant-host.test.ts": 93, + "src/main/codex/codex-trust-grant-ledger.test.ts": 72, + "src/main/codex/codex-trust-grant-telemetry.test.ts": 176, + "src/main/codex/codex-turn-ordinals.test.ts": 88, + "src/main/codex/codex-user-hook-trust-moves.test.ts": 112, + "src/main/codex/codex-wsl-hook-install-plan.test.ts": 48, + "src/main/codex/config-features-settings-promotion.test.ts": 157, + "src/main/codex/config-plugin-registration-promotion.test.ts": 313, + "src/main/codex/config-settings-baseline-upgrade.test.ts": 162, + "src/main/codex/config-settings-promotion.test.ts": 315, + "src/main/codex/config-sync-stall.test.ts": 148, + "src/main/codex/config-toml-hook-state-duplicate-repair.test.ts": 161, + "src/main/codex/config-toml-hook-trust-scaling.test.ts": 113, + "src/main/codex/config-toml-mcp-servers.test.ts": 47, + "src/main/codex/config-toml-project-duplicate-repair.test.ts": 89, + "src/main/codex/config-toml-project-trust-spellings.test.ts": 155, + "src/main/codex/config-toml-promoted-setting-values.test.ts": 47, + "src/main/codex/config-toml-trust-hash.test.ts": 92, + "src/main/codex/config-toml-trust-hook-read.test.ts": 113, + "src/main/codex/config-toml-trust-hook-removal.test.ts": 116, + "src/main/codex/config-toml-trust-hook-upsert.test.ts": 166, + "src/main/codex/config-toml-trust-key.test.ts": 111, + "src/main/codex/config-toml-trust-loadability.test.ts": 163, + "src/main/codex/config-toml-trust-paths.test.ts": 92, + "src/main/codex/config-toml-trust-project.test.ts": 105, + "src/main/codex/config-toml-trust-stale-writes.test.ts": 75, + "src/main/codex/hook-service-concurrent-launch-install.test.ts": 954, + "src/main/codex/hook-service-legacy-cleanup.test.ts": 713, + "src/main/codex/hook-service-managed-codex-hashes.test.ts": 885, + "src/main/codex/hook-service-managed-install.test.ts": 389, + "src/main/codex/hook-service-runtime-trust-repair.test.ts": 366, + "src/main/codex/hook-service-trust-grant.test.ts": 263, + "src/main/codex/hook-service-user-hook-mirroring.test.ts": 540, + "src/main/codex/hook-service-user-hook-trust-spellings.test.ts": 511, + "src/main/codex/hook-service-windows-key-spellings.test.ts": 353, + "src/main/codex/hook-service-wsl-runtime.test.ts": 443, + "src/main/codex/hook-trust-promotion.test.ts": 660, + "src/main/codex/retained-codex-hook-state.test.ts": 43, + "src/main/codex/sta-4735-hook-trust-provenance-overwrite.test.ts": 108, + "src/main/codex/sta-4737-unreadable-is-not-absent.test.ts": 160, + "src/main/codex/sta-4823-shared-state-survives-a-failed-read.test.ts": 231, + "src/main/codex/windows-hook-command.test.ts": 110, + "src/main/codex/windows-hook-upgrade.test.ts": 255, + "src/main/codex/wsl-codex-session-bridge.test.ts": 90, + "src/main/command-code/hook-service.test.ts": 269, + "src/main/computer/computer-action-verification-normalization.test.ts": 32, + "src/main/computer/computer-provider-lifecycle.test.ts": 181, + "src/main/computer/computer-provider-unavailable-message.test.ts": 33, + "src/main/computer/computer-sidecar-diagnostics.test.ts": 39, + "src/main/computer/desktop-script-provider-action-errors.test.ts": 207, + "src/main/computer/desktop-script-provider-actions.test.ts": 202, + "src/main/computer/desktop-script-provider-bridge.test.ts": 35, + "src/main/computer/desktop-script-provider-cache-lifecycle.test.ts": 95, + "src/main/computer/desktop-script-provider-cache.test.ts": 169, + "src/main/computer/desktop-script-provider-client.test.ts": 126, + "src/main/computer/desktop-script-provider-errors.test.ts": 264, + "src/main/computer/desktop-script-provider-paste-validation.test.ts": 97, + "src/main/computer/desktop-script-provider-runtime-host-routing.test.ts": 176, + "src/main/computer/desktop-script-runtime-host.test.ts": 255, + "src/main/computer/desktop-script-runtime-host.win32.test.ts": 56, + "src/main/computer/desktop-script-serve-channel.test.ts": 136, + "src/main/computer/macos-computer-use-permission-status-cleanup.integration.test.ts": 51, + "src/main/computer/macos-computer-use-permission-status.test.ts": 5094, + "src/main/computer/macos-computer-use-permissions.test.ts": 111, + "src/main/computer/macos-native-provider-client.test.ts": 1242, + "src/main/computer/macos-native-provider-line-buffer.test.ts": 44, + "src/main/computer/macos-native-provider-paste-validation.test.ts": 191, + "src/main/computer/macos-native-provider-process-reaping.test.ts": 84, + "src/main/computer/macos-native-provider-reaping.integration.test.ts": 4146, + "src/main/computer/macos-native-provider-socket.test.ts": 36, + "src/main/computer/macos-native-provider-startup-cleanup.test.ts": 44, + "src/main/computer/sidecar-client.test.ts": 88, + "src/main/computer/sidecar-provider-reaping.integration.test.ts": 295, + "src/main/computer/windows-powershell-execution-policy.test.ts": 36, + "src/main/copilot/hook-service.test.ts": 164, + "src/main/crash-reporting/appimage-crashpad-handler-exit.test.ts": 31, + "src/main/crash-reporting/crash-breadcrumb-renderer-attribution.test.ts": 58, + "src/main/crash-reporting/crash-breadcrumb-store-leak.test.ts": 61, + "src/main/crash-reporting/crash-breadcrumb-store-orphan-cleanup.test.ts": 89, + "src/main/crash-reporting/crash-breadcrumb-store.test.ts": 161, + "src/main/crash-reporting/crash-report-copy-text.test.ts": 38, + "src/main/crash-reporting/crash-report-store.test.ts": 392, + "src/main/crash-reporting/crashpad-capture-read-limit.test.ts": 253, + "src/main/crash-reporting/crashpad-capture.test.ts": 90, + "src/main/crash-reporting/durable-crash-breadcrumb.test.ts": 54, + "src/main/crash-reporting/expected-teardown-state.test.ts": 37, + "src/main/crash-reporting/gpu-crash-diagnostics.test.ts": 47, + "src/main/crash-reporting/gpu-crash-fallback-decision.test.ts": 50, + "src/main/crash-reporting/gpu-crash-fallback-field-sessions.test.ts": 34, + "src/main/crash-reporting/gpu-fallback-engagement.test.ts": 48, + "src/main/crash-reporting/gpu-fallback-recovered-launch.test.ts": 54, + "src/main/crash-reporting/gpu-fallback-restart-prompt.test.ts": 37, + "src/main/crash-reporting/low-commit-oom-recovery-gate.test.ts": 49, + "src/main/crash-reporting/main-process-lifecycle-identity.test.ts": 29, + "src/main/crash-reporting/minidump-crash-signature.test.ts": 72, + "src/main/crash-reporting/minidump-file-source.test.ts": 151, + "src/main/crash-reporting/pre-gone-host-memory.test.ts": 98, + "src/main/crash-reporting/process-gone-classification.test.ts": 44, + "src/main/crash-reporting/process-gone-dedupe.test.ts": 35, + "src/main/crash-reporting/process-gone-diagnostics.test.ts": 100, + "src/main/crash-reporting/process-gone-killed-one-ordering.test.ts": 85, + "src/main/crash-reporting/process-gone-recorder.test.ts": 846, + "src/main/crash-reporting/process-gone-sibling-correlation.test.ts": 604, + "src/main/crash-reporting/renderer-heap-allocation-profile.test.ts": 537, + "src/main/crash-reporting/renderer-heap-allocation-sites.test.ts": 52, + "src/main/crash-reporting/renderer-recovery-circuit-breaker.test.ts": 29, + "src/main/crash-reporting/self-initiated-tree-kill-log.test.ts": 96, + "src/main/crash-reporting/suppressed-process-gone-breadcrumb.test.ts": 27, "src/main/credential-file-protection.test.ts": 44, - "src/main/cursor/hook-command.live-shell.test.ts": 562, - "src/main/cursor/hook-service.test.ts": 264, - "src/main/daemon/agent-startup-prompt-latency.node-pty.test.ts": 2622, - "src/main/daemon/bash-prompt-command-composition.test.ts": 3412, - "src/main/daemon/client.test.ts": 426, - "src/main/daemon/cold-restore-payload-cache.test.ts": 58, - "src/main/daemon/daemon-adoption-telemetry-event.test.ts": 399, - "src/main/daemon/daemon-audit-eligibility-event.test.ts": 962, - "src/main/daemon/daemon-authenticated-client-activity.test.ts": 666, - "src/main/daemon/daemon-background-transient-facts.test.ts": 97, - "src/main/daemon/daemon-bundle-staleness.test.ts": 425, - "src/main/daemon/daemon-cgroup-scope.test.ts": 105, - "src/main/daemon/daemon-checkpoint-session-queue.test.ts": 227, - "src/main/daemon/daemon-client-frame-validation.test.ts": 552, - "src/main/daemon/daemon-client-notify-settlement.test.ts": 52, - "src/main/daemon/daemon-client-reader-handoff.test.ts": 61, - "src/main/daemon/daemon-client-rpc-request.test.ts": 80, - "src/main/daemon/daemon-client-stream-keepalive.test.ts": 53, - "src/main/daemon/daemon-color-query-reply-colors.test.ts": 857, - "src/main/daemon/daemon-durable-checkpoint-live-rebase.test.ts": 990, - "src/main/daemon/daemon-endpoint-ownership.test.ts": 648, - "src/main/daemon/daemon-endpoint-publish.test.ts": 219, - "src/main/daemon/daemon-endpoint-windows.test.ts": 61, - "src/main/daemon/daemon-entry-path-layouts.test.ts": 546, - "src/main/daemon/daemon-entry.test.ts": 811, - "src/main/daemon/daemon-errors.test.ts": 293, - "src/main/daemon/daemon-file-log.test.ts": 83, - "src/main/daemon/daemon-folder-access-mismatch.test.ts": 300, - "src/main/daemon/daemon-folder-access-probe-script.test.ts": 194, - "src/main/daemon/daemon-folder-access-probe.test.ts": 66, - "src/main/daemon/daemon-folder-access-reset.test.ts": 296, - "src/main/daemon/daemon-foreground-confirmation-protocol.test.ts": 60, - "src/main/daemon/daemon-health-endpoint-entry.test.ts": 121, - "src/main/daemon/daemon-health-socket-cleanup.test.ts": 124, - "src/main/daemon/daemon-health.test.ts": 3789, - "src/main/daemon/daemon-host-current-version-reclaim.test.ts": 200, - "src/main/daemon/daemon-host-entry-layout.test.ts": 175, - "src/main/daemon/daemon-host-file-hash-cache.test.ts": 164, - "src/main/daemon/daemon-host-process-package.test.ts": 315, - "src/main/daemon/daemon-host-profile-isolation.test.ts": 158, + "src/main/cursor/hook-command.live-shell.test.ts": 614, + "src/main/cursor/hook-service.test.ts": 350, + "src/main/daemon/agent-startup-prompt-latency.node-pty.test.ts": 2497, + "src/main/daemon/automatic-work-run-marker-store.test.ts": 294, + "src/main/daemon/bash-prompt-command-composition.test.ts": 3478, + "src/main/daemon/client.test.ts": 460, + "src/main/daemon/cold-restore-payload-cache.test.ts": 36, + "src/main/daemon/daemon-adoption-telemetry-event.test.ts": 353, + "src/main/daemon/daemon-appimage-copy-host.electron.test.ts": 3055, + "src/main/daemon/daemon-appimage-generation-launch.test.ts": 72, + "src/main/daemon/daemon-appimage-launch.test.ts": 115, + "src/main/daemon/daemon-audit-eligibility-event.test.ts": 825, + "src/main/daemon/daemon-authenticated-client-activity.test.ts": 620, + "src/main/daemon/daemon-automatic-work-guard.test.ts": 317, + "src/main/daemon/daemon-automatic-work-resume.test.ts": 894, + "src/main/daemon/daemon-background-transient-facts.test.ts": 86, + "src/main/daemon/daemon-bundle-staleness.test.ts": 420, + "src/main/daemon/daemon-cgroup-scope.test.ts": 101, + "src/main/daemon/daemon-checkpoint-session-queue.test.ts": 231, + "src/main/daemon/daemon-client-frame-validation.test.ts": 811, + "src/main/daemon/daemon-client-notify-settlement.test.ts": 46, + "src/main/daemon/daemon-client-reader-handoff.test.ts": 72, + "src/main/daemon/daemon-client-rpc-request.test.ts": 88, + "src/main/daemon/daemon-client-stream-keepalive.test.ts": 36, + "src/main/daemon/daemon-color-query-reply-colors.test.ts": 908, + "src/main/daemon/daemon-durable-checkpoint-live-rebase.test.ts": 989, + "src/main/daemon/daemon-electron-to-node-adoption.electron.test.ts": 3107, + "src/main/daemon/daemon-endpoint-ownership.test.ts": 664, + "src/main/daemon/daemon-endpoint-publish.test.ts": 171, + "src/main/daemon/daemon-endpoint-windows.test.ts": 43, + "src/main/daemon/daemon-entry-path-layouts.test.ts": 646, + "src/main/daemon/daemon-entry.test.ts": 680, + "src/main/daemon/daemon-errors.test.ts": 292, + "src/main/daemon/daemon-file-log.test.ts": 52, + "src/main/daemon/daemon-folder-access-mismatch.test.ts": 360, + "src/main/daemon/daemon-folder-access-probe-script.test.ts": 180, + "src/main/daemon/daemon-folder-access-probe.test.ts": 42, + "src/main/daemon/daemon-folder-access-reset.test.ts": 315, + "src/main/daemon/daemon-foreground-confirmation-protocol.test.ts": 41, + "src/main/daemon/daemon-full-disk-access-status.test.ts": 39, + "src/main/daemon/daemon-health-endpoint-entry.test.ts": 104, + "src/main/daemon/daemon-health-socket-cleanup.test.ts": 116, + "src/main/daemon/daemon-health.test.ts": 3807, + "src/main/daemon/daemon-host-current-version-reclaim.test.ts": 301, + "src/main/daemon/daemon-host-entry-layout.test.ts": 151, + "src/main/daemon/daemon-host-file-hash-cache.test.ts": 161, + "src/main/daemon/daemon-host-image.test.ts": 38, + "src/main/daemon/daemon-host-path-budget.test.ts": 120, + "src/main/daemon/daemon-host-process-package.test.ts": 436, + "src/main/daemon/daemon-host-profile-isolation.test.ts": 163, "src/main/daemon/daemon-host-relocation.test.ts": 333, - "src/main/daemon/daemon-idle-inventory-budget.test.ts": 651, - "src/main/daemon/daemon-idle-shutdown.test.ts": 9146, - "src/main/daemon/daemon-incarnation-evidence-main-thread.test.ts": 127, - "src/main/daemon/daemon-incarnation-evidence.test.ts": 130, - "src/main/daemon/daemon-init-child-readiness.test.ts": 674, - "src/main/daemon/daemon-init-child-startup-failure.test.ts": 1005, - "src/main/daemon/daemon-init-endpoint-adoption.test.ts": 1604, - "src/main/daemon/daemon-init-live-session-preservation.test.ts": 961, - "src/main/daemon/daemon-init-packaged-bundle-staleness.test.ts": 539, - "src/main/daemon/daemon-init-provider-installation.test.ts": 509, - "src/main/daemon/daemon-init-replacement-reporting.test.ts": 896, - "src/main/daemon/daemon-init-restart-sequence.test.ts": 1523, - "src/main/daemon/daemon-init-wedged-daemon-grace.test.ts": 801, - "src/main/daemon/daemon-launched-child-identity.test.ts": 82, - "src/main/daemon/daemon-launched-child-spawn.test.ts": 65, - "src/main/daemon/daemon-lifecycle-event.test.ts": 312, - "src/main/daemon/daemon-mac-code-identity.test.ts": 70, - "src/main/daemon/daemon-main.test.ts": 781, - "src/main/daemon/daemon-native-pty-exception.test.ts": 62, - "src/main/daemon/daemon-out-of-process-launcher-host-prune.test.ts": 218, - "src/main/daemon/daemon-pid-file-parse.test.ts": 44, - "src/main/daemon/daemon-preflight-client-replacement.test.ts": 927, - "src/main/daemon/daemon-process-identity-time-zone.test.ts": 57, - "src/main/daemon/daemon-process-inspection.test.ts": 95, - "src/main/daemon/daemon-provider-census.test.ts": 120, - "src/main/daemon/daemon-provider-routing.test.ts": 331, - "src/main/daemon/daemon-pty-adapter-closing-session-inventory.test.ts": 822, - "src/main/daemon/daemon-pty-adapter-cold-restore-reanchor.test.ts": 1311, - "src/main/daemon/daemon-pty-adapter-cold-restore-seed.test.ts": 1180, - "src/main/daemon/daemon-pty-adapter-concurrent-recovery.test.ts": 1076, - "src/main/daemon/daemon-pty-adapter-daemon-recovery.test.ts": 1414, - "src/main/daemon/daemon-pty-adapter-history-checkpoints.test.ts": 2079, - "src/main/daemon/daemon-pty-adapter-history-recovery.test.ts": 1304, - "src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts": 827, - "src/main/daemon/daemon-pty-adapter-live-rebased-checkpoint.test.ts": 1578, - "src/main/daemon/daemon-pty-adapter-protocol-compatibility.test.ts": 1037, - "src/main/daemon/daemon-pty-adapter-replacement-exit-race.test.ts": 870, - "src/main/daemon/daemon-pty-adapter-session-adoption.test.ts": 1432, - "src/main/daemon/daemon-pty-adapter-steady-state-compat.test.ts": 649, - "src/main/daemon/daemon-pty-adapter.test.ts": 1757, - "src/main/daemon/daemon-pty-router-history-handoff.test.ts": 1347, - "src/main/daemon/daemon-pty-router.test.ts": 485, - "src/main/daemon/daemon-pty-startup-delivery.test.ts": 3028, - "src/main/daemon/daemon-pty-upgrade-adoption.test.ts": 836, - "src/main/daemon/daemon-pty-write-settlement-recovery.test.ts": 684, - "src/main/daemon/daemon-ready-identity.test.ts": 56, - "src/main/daemon/daemon-reattach-checkpoint-isolation.test.ts": 949, - "src/main/daemon/daemon-respawn-throttle.test.ts": 106, - "src/main/daemon/daemon-restore-scrollback-depth.test.ts": 2335, - "src/main/daemon/daemon-router-retirement.test.ts": 67, - "src/main/daemon/daemon-scope-death-watch.test.ts": 89, - "src/main/daemon/daemon-self-retirement-respawn.test.ts": 1065, - "src/main/daemon/daemon-server-async-spawn-cancellation.test.ts": 1280, - "src/main/daemon/daemon-server-attach-only.test.ts": 738, - "src/main/daemon/daemon-server-attachment-lifecycle.test.ts": 919, - "src/main/daemon/daemon-server-error-handling.test.ts": 818, - "src/main/daemon/daemon-server-kill-attribution.test.ts": 693, - "src/main/daemon/daemon-server.test.ts": 1370, - "src/main/daemon/daemon-session-owner-resolution.test.ts": 89, - "src/main/daemon/daemon-session-scrollback-window.test.ts": 477, - "src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts": 856, - "src/main/daemon/daemon-shutdown-final-checkpoint-caller-deadline.test.ts": 2550, - "src/main/daemon/daemon-socket-endpoint-path.test.ts": 428, - "src/main/daemon/daemon-spawner.test.ts": 728, - "src/main/daemon/daemon-stream-backpressure-socket.test.ts": 454, - "src/main/daemon/daemon-stream-backpressure.test.ts": 71, - "src/main/daemon/daemon-stream-binary-framing.test.ts": 69, - "src/main/daemon/daemon-stream-binary-negotiation.test.ts": 550, - "src/main/daemon/daemon-stream-binary-unicode.test.ts": 60, - "src/main/daemon/daemon-stream-data-batcher.test.ts": 235, - "src/main/daemon/daemon-stream-data-split.test.ts": 700, - "src/main/daemon/daemon-stream-droppability-lifecycle.test.ts": 644, - "src/main/daemon/daemon-stream-droppable-membership.test.ts": 131, - "src/main/daemon/daemon-stream-framing-replacement.test.ts": 58, - "src/main/daemon/daemon-stream-producer-stall-watchdog.test.ts": 126, - "src/main/daemon/daemon-tcc-attribution-main-thread.test.ts": 132, - "src/main/daemon/daemon-tcc-attribution.test.ts": 101, - "src/main/daemon/daemon-transport-attachment-release.test.ts": 790, - "src/main/daemon/daemon-user-manager-lifetime.test.ts": 77, - "src/main/daemon/degraded-daemon-pty-provider.test.ts": 126, - "src/main/daemon/hangul-cell-width-agreement.test.ts": 100, - "src/main/daemon/headless-emulator-fidelity.fuzz.test.ts": 2924, - "src/main/daemon/headless-emulator-restored-osc-links.test.ts": 161, - "src/main/daemon/headless-emulator-shrink-snapshot.test.ts": 175, - "src/main/daemon/headless-emulator-snapshot-cost.bench.test.ts": 101, - "src/main/daemon/headless-emulator-unicode-width.test.ts": 141, - "src/main/daemon/headless-emulator-wide-char-repaint.test.ts": 1711, - "src/main/daemon/headless-emulator-wide-char-snapshot.test.ts": 273, - "src/main/daemon/headless-emulator.test.ts": 341, - "src/main/daemon/headless-osc-link-ranges.test.ts": 185, - "src/main/daemon/hibernation-cold-restore-repro.test.ts": 203, - "src/main/daemon/history-manager-disabled-sessions-leak.test.ts": 173, - "src/main/daemon/history-manager.test.ts": 371, - "src/main/daemon/history-reader-memory.test.ts": 440, - "src/main/daemon/history-reader.test.ts": 242, - "src/main/daemon/issue-6814-daemon-failure-classification.test.ts": 835, - "src/main/daemon/macos-login-session-death-watch.test.ts": 89, - "src/main/daemon/ndjson.test.ts": 135, - "src/main/daemon/node-pty-error-hints.test.ts": 50, - "src/main/daemon/node-pty-windows-input-error.win32.test.ts": 52, - "src/main/daemon/osc7-file-uri.test.ts": 49, - "src/main/daemon/osc7-uri-extraction.test.ts": 49, - "src/main/daemon/post-ready-flush-gate.test.ts": 67, - "src/main/daemon/process-boundary-ground.test.ts": 230, - "src/main/daemon/process-boundary-host-focus.test.ts": 296, - "src/main/daemon/pty-session-id.test.ts": 68, - "src/main/daemon/pty-subprocess-cwd-cancel-identity.test.ts": 454, - "src/main/daemon/pty-subprocess-env-inheritance.test.ts": 436, - "src/main/daemon/pty-subprocess-env-retention.test.ts": 361, - "src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts": 528, - "src/main/daemon/pty-subprocess-foreground-identity.test.ts": 719, - "src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts": 428, - "src/main/daemon/pty-subprocess-git-credential-guard.test.ts": 439, - "src/main/daemon/pty-subprocess-handle-lifecycle.test.ts": 525, - "src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts": 1307, - "src/main/daemon/pty-subprocess-io-failure-native.test.ts": 2779, - "src/main/daemon/pty-subprocess-managed-agent-env.test.ts": 559, - "src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts": 479, - "src/main/daemon/pty-subprocess-windows-shell-launch.test.ts": 434, - "src/main/daemon/pty-subprocess-wsl-launch.test.ts": 482, - "src/main/daemon/pty-subprocess.test.ts": 441, + "src/main/daemon/daemon-idle-inventory-budget.test.ts": 815, + "src/main/daemon/daemon-idle-shutdown.test.ts": 9356, + "src/main/daemon/daemon-incarnation-evidence-main-thread.test.ts": 116, + "src/main/daemon/daemon-incarnation-evidence.test.ts": 135, + "src/main/daemon/daemon-init-child-readiness.test.ts": 769, + "src/main/daemon/daemon-init-child-startup-failure.test.ts": 1151, + "src/main/daemon/daemon-init-endpoint-adoption.test.ts": 2067, + "src/main/daemon/daemon-init-live-session-preservation.test.ts": 1140, + "src/main/daemon/daemon-init-macos-stable-launch-fallback.test.ts": 914, + "src/main/daemon/daemon-init-packaged-bundle-staleness.test.ts": 819, + "src/main/daemon/daemon-init-provider-installation.test.ts": 747, + "src/main/daemon/daemon-init-replacement-reporting.test.ts": 1282, + "src/main/daemon/daemon-init-restart-sequence.test.ts": 1813, + "src/main/daemon/daemon-init-severed-host-image.test.ts": 1077, + "src/main/daemon/daemon-init-wedged-daemon-grace.test.ts": 1067, + "src/main/daemon/daemon-launched-child-identity.test.ts": 77, + "src/main/daemon/daemon-launched-child-spawn.test.ts": 120, + "src/main/daemon/daemon-lifecycle-event.test.ts": 268, + "src/main/daemon/daemon-liveness-report.test.ts": 560, + "src/main/daemon/daemon-mac-code-identity.test.ts": 36, + "src/main/daemon/daemon-main.test.ts": 673, + "src/main/daemon/daemon-native-pty-exception.test.ts": 33, + "src/main/daemon/daemon-newer-protocol-hold.test.ts": 186, + "src/main/daemon/daemon-out-of-process-launcher-host-prune.test.ts": 326, + "src/main/daemon/daemon-out-of-process-launcher-identity.test.ts": 167, + "src/main/daemon/daemon-pid-file-parse.test.ts": 32, + "src/main/daemon/daemon-pid-identity-native.win32.test.ts": 102, + "src/main/daemon/daemon-pid-identity-windows.test.ts": 74, + "src/main/daemon/daemon-preflight-client-replacement.test.ts": 746, + "src/main/daemon/daemon-process-identity-time-zone.test.ts": 39, + "src/main/daemon/daemon-process-inspection.test.ts": 96, + "src/main/daemon/daemon-protocol-cleanup.test.ts": 80, + "src/main/daemon/daemon-provider-census.test.ts": 257, + "src/main/daemon/daemon-provider-routing.test.ts": 585, + "src/main/daemon/daemon-pty-adapter-closing-session-inventory.test.ts": 1002, + "src/main/daemon/daemon-pty-adapter-cold-restore-reanchor.test.ts": 1308, + "src/main/daemon/daemon-pty-adapter-cold-restore-seed.test.ts": 1218, + "src/main/daemon/daemon-pty-adapter-concurrent-recovery.test.ts": 995, + "src/main/daemon/daemon-pty-adapter-daemon-recovery.test.ts": 1534, + "src/main/daemon/daemon-pty-adapter-full-disk-access.test.ts": 538, + "src/main/daemon/daemon-pty-adapter-history-checkpoints.test.ts": 1706, + "src/main/daemon/daemon-pty-adapter-history-recovery.test.ts": 1372, + "src/main/daemon/daemon-pty-adapter-inventory-respawn.test.ts": 917, + "src/main/daemon/daemon-pty-adapter-live-rebased-checkpoint.test.ts": 1595, + "src/main/daemon/daemon-pty-adapter-protocol-compatibility.test.ts": 899, + "src/main/daemon/daemon-pty-adapter-replacement-exit-race.test.ts": 943, + "src/main/daemon/daemon-pty-adapter-session-adoption.test.ts": 1611, + "src/main/daemon/daemon-pty-adapter-spawn-intent.test.ts": 615, + "src/main/daemon/daemon-pty-adapter-steady-state-compat.test.ts": 724, + "src/main/daemon/daemon-pty-adapter-subscription-fanout.test.ts": 38, + "src/main/daemon/daemon-pty-adapter.test.ts": 1940, + "src/main/daemon/daemon-pty-router-history-handoff.test.ts": 1121, + "src/main/daemon/daemon-pty-router-legacy-gone.test.ts": 159, + "src/main/daemon/daemon-pty-router-newer-daemon-hold.test.ts": 108, + "src/main/daemon/daemon-pty-router.test.ts": 830, + "src/main/daemon/daemon-pty-startup-delivery.test.ts": 1037, + "src/main/daemon/daemon-pty-upgrade-adoption.test.ts": 941, + "src/main/daemon/daemon-pty-write-settlement-recovery.test.ts": 582, + "src/main/daemon/daemon-ready-identity.test.ts": 35, + "src/main/daemon/daemon-reattach-checkpoint-isolation.test.ts": 1044, + "src/main/daemon/daemon-respawn-throttle.test.ts": 86, + "src/main/daemon/daemon-restore-scrollback-depth.test.ts": 2519, + "src/main/daemon/daemon-router-retirement.test.ts": 73, + "src/main/daemon/daemon-scope-death-watch.test.ts": 76, + "src/main/daemon/daemon-self-retirement-respawn.test.ts": 1119, + "src/main/daemon/daemon-server-async-spawn-cancellation.test.ts": 1516, + "src/main/daemon/daemon-server-attach-only.test.ts": 651, + "src/main/daemon/daemon-server-attachment-lifecycle.test.ts": 720, + "src/main/daemon/daemon-server-error-handling.test.ts": 731, + "src/main/daemon/daemon-server-kill-attribution.test.ts": 662, + "src/main/daemon/daemon-server.test.ts": 1396, + "src/main/daemon/daemon-session-owner-resolution.test.ts": 61, + "src/main/daemon/daemon-session-scrollback-window.test.ts": 442, + "src/main/daemon/daemon-severed-restart.test.ts": 516, + "src/main/daemon/daemon-shared-owner-incarnation-retention.test.ts": 1051, + "src/main/daemon/daemon-shutdown-final-checkpoint-caller-deadline.test.ts": 2814, + "src/main/daemon/daemon-socket-endpoint-path.test.ts": 473, + "src/main/daemon/daemon-spawner.test.ts": 762, + "src/main/daemon/daemon-stream-backpressure-socket.test.ts": 420, + "src/main/daemon/daemon-stream-backpressure.test.ts": 59, + "src/main/daemon/daemon-stream-binary-framing.test.ts": 91, + "src/main/daemon/daemon-stream-binary-negotiation.test.ts": 1188, + "src/main/daemon/daemon-stream-binary-unicode.test.ts": 85, + "src/main/daemon/daemon-stream-data-batcher.test.ts": 285, + "src/main/daemon/daemon-stream-data-split.test.ts": 617, + "src/main/daemon/daemon-stream-droppability-lifecycle.test.ts": 708, + "src/main/daemon/daemon-stream-droppable-membership.test.ts": 95, + "src/main/daemon/daemon-stream-framing-replacement.test.ts": 73, + "src/main/daemon/daemon-stream-ingest-turn-bound.test.ts": 348, + "src/main/daemon/daemon-stream-producer-stall-watchdog.test.ts": 109, + "src/main/daemon/daemon-tcc-attribution-main-thread.test.ts": 112, + "src/main/daemon/daemon-tcc-attribution.test.ts": 103, + "src/main/daemon/daemon-transport-attachment-release.test.ts": 825, + "src/main/daemon/daemon-user-manager-lifetime.test.ts": 73, + "src/main/daemon/degraded-daemon-pty-provider.test.ts": 130, + "src/main/daemon/desktop-restore-admission.test.ts": 590, + "src/main/daemon/hangul-cell-width-agreement.test.ts": 63, + "src/main/daemon/headless-emulator-fidelity.fuzz.test.ts": 2829, + "src/main/daemon/headless-emulator-restored-osc-links.test.ts": 135, + "src/main/daemon/headless-emulator-shrink-snapshot.test.ts": 157, + "src/main/daemon/headless-emulator-snapshot-cost.bench.test.ts": 93, + "src/main/daemon/headless-emulator-unicode-width.test.ts": 86, + "src/main/daemon/headless-emulator-wide-char-repaint.test.ts": 1429, + "src/main/daemon/headless-emulator-wide-char-snapshot.test.ts": 263, + "src/main/daemon/headless-emulator.test.ts": 332, + "src/main/daemon/headless-osc-link-ranges.test.ts": 195, + "src/main/daemon/hibernation-cold-restore-repro.test.ts": 219, + "src/main/daemon/history-manager-disabled-sessions-leak.test.ts": 175, + "src/main/daemon/history-manager.test.ts": 351, + "src/main/daemon/history-reader-memory.test.ts": 433, + "src/main/daemon/history-reader.test.ts": 287, + "src/main/daemon/host-terminal-ledger.test.ts": 111, + "src/main/daemon/issue-6814-daemon-failure-classification.test.ts": 681, + "src/main/daemon/legacy-restore-records.test.ts": 606, + "src/main/daemon/macos-daemon-bundle-retirement.test.ts": 66, + "src/main/daemon/macos-daemon-bundle.test.ts": 140, + "src/main/daemon/macos-daemon-job-state.test.ts": 41, + "src/main/daemon/macos-daemon-launchd.test.ts": 1494, + "src/main/daemon/macos-login-session-death-watch.test.ts": 66, + "src/main/daemon/ndjson.test.ts": 134, + "src/main/daemon/node-pty-error-hints.test.ts": 58, + "src/main/daemon/node-pty-windows-input-error.win32.test.ts": 42, + "src/main/daemon/osc7-file-uri.test.ts": 34, + "src/main/daemon/osc7-uri-extraction.test.ts": 38, + "src/main/daemon/post-ready-flush-gate.test.ts": 45, + "src/main/daemon/process-boundary-ground.test.ts": 207, + "src/main/daemon/process-boundary-host-focus.test.ts": 284, + "src/main/daemon/pty-process-ceiling.test.ts": 94, + "src/main/daemon/pty-session-id.test.ts": 59, + "src/main/daemon/pty-subprocess-cwd-cancel-identity.test.ts": 416, + "src/main/daemon/pty-subprocess-env-inheritance.test.ts": 447, + "src/main/daemon/pty-subprocess-env-retention.test.ts": 357, + "src/main/daemon/pty-subprocess-foreground-degraded-scan.test.ts": 532, + "src/main/daemon/pty-subprocess-foreground-identity.test.ts": 650, + "src/main/daemon/pty-subprocess-foreground-scan-cadence.test.ts": 485, + "src/main/daemon/pty-subprocess-git-credential-guard.test.ts": 432, + "src/main/daemon/pty-subprocess-handle-lifecycle.test.ts": 514, + "src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts": 862, + "src/main/daemon/pty-subprocess-io-failure-native.test.ts": 2756, + "src/main/daemon/pty-subprocess-managed-agent-env.test.ts": 472, + "src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts": 465, + "src/main/daemon/pty-subprocess-windows-shell-launch.test.ts": 489, + "src/main/daemon/pty-subprocess-wsl-launch.test.ts": 465, + "src/main/daemon/pty-subprocess.test.ts": 495, "src/main/daemon/pty-subprocess/native-pty-spawn-windows.test.ts": 87, - "src/main/daemon/pty-subprocess/spawn-file-foreground-rejected-agents.test.ts": 148, - "src/main/daemon/pty-subprocess/spawn-preflight-health-probe.test.ts": 91, - "src/main/daemon/reattach-snapshot.test.ts": 889, - "src/main/daemon/repro-12101-mouse-tracking-survives-agent-death.test.ts": 284, - "src/main/daemon/repro-7329-remote-snapshot-corruption.test.ts": 163, - "src/main/daemon/serialize-addon-edge-cases.test.ts": 104, - "src/main/daemon/serialize-grid-cell-descriptors.test.ts": 78, - "src/main/daemon/serialize-grid-transcript-replay.test.ts": 24288, - "src/main/daemon/serialize-grid.differential.fuzz.test.ts": 559, - "src/main/daemon/session-ingest-throughput.bench.test.ts": 203, - "src/main/daemon/session-pending-output.test.ts": 350, - "src/main/daemon/session-producer-pause.test.ts": 59, + "src/main/daemon/pty-subprocess/spawn-file-foreground-rejected-agents.test.ts": 150, + "src/main/daemon/pty-subprocess/spawn-preflight-health-probe.test.ts": 101, + "src/main/daemon/reattach-snapshot.test.ts": 1072, + "src/main/daemon/repro-12101-mouse-tracking-survives-agent-death.test.ts": 367, + "src/main/daemon/repro-7329-remote-snapshot-corruption.test.ts": 162, + "src/main/daemon/serialize-addon-edge-cases.test.ts": 79, + "src/main/daemon/serialize-grid-cell-descriptors.test.ts": 137, + "src/main/daemon/serialize-grid-transcript-replay.test.ts": 27203, + "src/main/daemon/serialize-grid.differential.fuzz.test.ts": 545, + "src/main/daemon/session-ingest-throughput.bench.test.ts": 260, + "src/main/daemon/session-pending-output.test.ts": 403, + "src/main/daemon/session-producer-pause.test.ts": 42, "src/main/daemon/session-reset-input-modes.test.ts": 243, - "src/main/daemon/session-shell-recovery.test.ts": 332, - "src/main/daemon/session-terminal-control.test.ts": 275, - "src/main/daemon/session.test.ts": 359, - "src/main/daemon/shell-ready-bash-wrapper.test.ts": 429, - "src/main/daemon/slow-daemon-session-verification.test.ts": 14993, - "src/main/daemon/startup-device-attributes-responder.test.ts": 57, - "src/main/daemon/terminal-attach-cancellation.test.ts": 40, - "src/main/daemon/terminal-checkpoint-serializer.test.ts": 255, - "src/main/daemon/terminal-checkpoint-writer-bounds.test.ts": 310, - "src/main/daemon/terminal-cursor-line-context.test.ts": 146, - "src/main/daemon/terminal-descendant-shutdown.test.ts": 124, - "src/main/daemon/terminal-history-incremental-restore.test.ts": 443, - "src/main/daemon/terminal-history-large-checkpoint-cold-restore.test.ts": 1922, - "src/main/daemon/terminal-history-log-encode-allocation.test.ts": 87, - "src/main/daemon/terminal-history-log.test.ts": 60, - "src/main/daemon/terminal-history-permissions.test.ts": 251, - "src/main/daemon/terminal-history-restorable-retention.test.ts": 55, - "src/main/daemon/terminal-history-seed-chunks.test.ts": 76, - "src/main/daemon/terminal-history-seed-segments.test.ts": 143, - "src/main/daemon/terminal-history-seed-transfer-registry.test.ts": 49, - "src/main/daemon/terminal-history-session-tombstone-retry.test.ts": 81, - "src/main/daemon/terminal-host-agent-session.test.ts": 391, - "src/main/daemon/terminal-host-attach-only.test.ts": 359, - "src/main/daemon/terminal-host-cheap-tier-ps-scan-volume.test.ts": 237, - "src/main/daemon/terminal-host-closing-session-listing.test.ts": 644, - "src/main/daemon/terminal-host-concurrent-create.test.ts": 397, - "src/main/daemon/terminal-host-cwd-readability.test.ts": 422, - "src/main/daemon/terminal-host-non-agent-foreground.test.ts": 179, - "src/main/daemon/terminal-host-process-inspection-cheap-tier.test.ts": 239, - "src/main/daemon/terminal-host-process-inspection.test.ts": 360, - "src/main/daemon/terminal-host-pty-owner-backend.test.ts": 366, - "src/main/daemon/terminal-host-readiness-reporting.test.ts": 447, - "src/main/daemon/terminal-host-session-reaping-leak.test.ts": 411, - "src/main/daemon/terminal-host-spawn-input-retention.test.ts": 641, - "src/main/daemon/terminal-host-startup-staging.test.ts": 355, - "src/main/daemon/terminal-host-startup.test.ts": 374, - "src/main/daemon/terminal-host-teardown-recreate.test.ts": 501, - "src/main/daemon/terminal-host-wsl-context.test.ts": 380, - "src/main/daemon/terminal-host.test.ts": 605, - "src/main/daemon/terminal-session-teardown.test.ts": 77, - "src/main/daemon/terminal-shell-armed-input-mode-recovery.test.ts": 256, - "src/main/daemon/terminal-shell-lifecycle-scanner.test.ts": 69, - "src/main/daemon/terminal-shell-recovery-barrier.test.ts": 1248, - "src/main/daemon/terminal-shell-recovery-clean-exit-retirement.test.ts": 72, - "src/main/daemon/terminal-snapshot-color-parity.test.ts": 136, - "src/main/daemon/terminal-snapshot-mouse-encoding.test.ts": 139, - "src/main/daemon/terminal-snapshot-osc8-roundtrip.test.ts": 130, - "src/main/daemon/terminal-snapshot-serialize-roundtrip.test.ts": 222, - "src/main/daemon/windows-conpty-warmup.test.ts": 56, - "src/main/daemon/wsl-cold-restore-cwd.test.ts": 47, - "src/main/destination-serialized-local-rename.test.ts": 2512, - "src/main/devin/hook-config-json.test.ts": 86, - "src/main/devin/hook-service.test.ts": 131, - "src/main/diagnostics/main-thread-churn-probe.test.ts": 69, - "src/main/dock/unread-badge.test.ts": 69, - "src/main/droid/hook-service.test.ts": 89, - "src/main/dsh/dsh-home-patch.test.ts": 45, - "src/main/dsh/hook-service.test.ts": 106, - "src/main/durable-file-write-syscall-proof.test.ts": 67, - "src/main/durable-file-write.test.ts": 139, - "src/main/emulator/android/adb-devices.test.ts": 65, - "src/main/emulator/android/android-app-control.test.ts": 48, - "src/main/emulator/android/android-capability-operations.test.ts": 100, - "src/main/emulator/android/android-device-inventory.test.ts": 66, - "src/main/emulator/android/android-input-commands.test.ts": 49, - "src/main/emulator/android/android-input-mapping.test.ts": 63, - "src/main/emulator/android/android-logcat.test.ts": 54, - "src/main/emulator/android/android-permissions.test.ts": 49, - "src/main/emulator/android/android-sdk-discovery.test.ts": 61, - "src/main/emulator/android/avd-manager.test.ts": 61, - "src/main/emulator/android/scrcpy-server-deploy.test.ts": 53, - "src/main/emulator/android/scrcpy-stream-session.test.ts": 419, - "src/main/emulator/android/scrcpy-video-frame-parser.test.ts": 162, - "src/main/emulator/android/uiautomator-tree.test.ts": 63, - "src/main/emulator/backends/android-emulator-backend.test.ts": 220, - "src/main/emulator/backends/ios-emulator-backend.test.ts": 74, - "src/main/emulator/emulator-availability.test.ts": 57, - "src/main/emulator/emulator-bridge.test.ts": 335, - "src/main/emulator/emulator-gesture-sender.test.ts": 191, - "src/main/emulator/emulator-session-registry.test.ts": 62, - "src/main/emulator/emulator-start-lease-registry.test.ts": 111, - "src/main/emulator/mjpeg-frame-parser.test.ts": 52, - "src/main/emulator/mjpeg-frame-stream.test.ts": 92, - "src/main/emulator/scrcpy-video-registry.test.ts": 51, - "src/main/emulator/serve-sim-accessibility-tree.test.ts": 74, - "src/main/emulator/serve-sim-ax-normalization.test.ts": 62, - "src/main/emulator/serve-sim-detached-session.test.ts": 72, - "src/main/emulator/serve-sim-execution.test.ts": 90, - "src/main/emulator/serve-sim-helper-processes.test.ts": 58, - "src/main/emulator/serve-sim-runtime-materializer.test.ts": 168, - "src/main/emulator/simctl-simulator-devices.test.ts": 66, - "src/main/ephemeral-vm-recipe-runner.test.ts": 625, - "src/main/ephemeral-vm-resume-integrity.test.ts": 271, - "src/main/ephemeral-vm-runtime-service.test.ts": 834, - "src/main/ephemeral-vm-runtime-ssh-cleanup.test.ts": 272, - "src/main/execution-host-workspace-trust.test.ts": 164, - "src/main/external-editor-launch.test.ts": 121, - "src/main/fish-history-session.test.ts": 88, - "src/main/fish-xdg-data-dirs-handoff.test.ts": 66, - "src/main/fish-xdg-data-dirs-no-config-launch.test.ts": 301, - "src/main/folder-upgrade-worktree-path.test.ts": 57, - "src/main/foreign-sqlite-readers/foreign-sqlite-reader-client-worker-lifecycle.test.ts": 768, - "src/main/foreign-sqlite-readers/foreign-sqlite-reader-client.test.ts": 245, - "src/main/foreign-sqlite-readers/foreign-sqlite-reader-dispatch.test.ts": 258, - "src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry-path.test.ts": 53, - "src/main/foreign-sqlite-readers/foreign-sqlite-reader-event-loop.test.ts": 2385, - "src/main/foreign-sqlite-readers/foreign-sqlite-reader-spawn.test.ts": 49, - "src/main/foreign-sqlite-readers/readers/cursor-profile.test.ts": 517, - "src/main/foreign-sqlite-readers/readers/opencode-binder-sessions.test.ts": 131, - "src/main/gemini/hook-service.test.ts": 87, - "src/main/ghostty/discovery.test.ts": 78, - "src/main/ghostty/index.test.ts": 118, - "src/main/ghostty/mapper-extended.test.ts": 82, - "src/main/ghostty/mapper.test.ts": 85, - "src/main/ghostty/parser.test.ts": 55, - "src/main/ghostty/theme-import.test.ts": 108, - "src/main/ghostty/theme-resolution.test.ts": 71, - "src/main/git-bash.test.ts": 61, - "src/main/git/add-sparse-worktree.test.ts": 219, - "src/main/git/admission-tier-plumbing.test.ts": 61, - "src/main/git/base-ref-search-selector.test.ts": 200, - "src/main/git/branch-rename.test.ts": 67, - "src/main/git/canonical-repo-key.test.ts": 174, - "src/main/git/check-ignored-paths.test.ts": 60, - "src/main/git/coalesced-probe.test.ts": 61, - "src/main/git/command-runner/gh-exec-file-deadline.test.ts": 151, - "src/main/git/command-runner/git-admission-output-parity.test.ts": 196, - "src/main/git/command-runner/git-admission-span.test.ts": 323, - "src/main/git/command-runner/git-admission-storm-measurement.test.ts": 940, - "src/main/git/command-runner/git-command-timeout-behavior.test.ts": 1957, - "src/main/git/command-runner/git-command-timeout.test.ts": 56, - "src/main/git/command-runner/git-exec-admission-exempt.test.ts": 124, - "src/main/git/command-runner/git-exec-admission-lifetime.test.ts": 665, - "src/main/git/command-runner/git-operation-executor.test.ts": 72, - "src/main/git/command-runner/git-spawn-admission-lifetime.test.ts": 170, - "src/main/git/command-runner/git-stream-admission-lifetime.test.ts": 263, - "src/main/git/command-runner/git-subprocess-admission.test.ts": 373, - "src/main/git/command-runner/hosted-cli-deadline-log.test.ts": 145, - "src/main/git/command-runner/spawned-command-tree-kill.test.ts": 74, - "src/main/git/command-runner/wsl-command-resolution-windows-binary.test.ts": 75, - "src/main/git/command-runner/wsl-host-failure.test.ts": 186, - "src/main/git/commit-object-ref.test.ts": 46, - "src/main/git/commit.test.ts": 231, - "src/main/git/create-event-repo-probe.test.ts": 73, - "src/main/git/exact-ref-probe.test.ts": 65, - "src/main/git/fetch-error-classification.test.ts": 53, - "src/main/git/fork-remote-refspec.test.ts": 64, - "src/main/git/fork-remote-stale-branch-refspec.test.ts": 55, - "src/main/git/gh-rate-limit-breaker.test.ts": 70, - "src/main/git/git-capability-state.test.ts": 43, - "src/main/git/git-network-safety-real-git.test.ts": 642, - "src/main/git/git-status-read-lease-owner.test.ts": 68, - "src/main/git/git-upstream-status-read-owner.test.ts": 52, - "src/main/git/git-username.test.ts": 211, - "src/main/git/hosted-remote-url.test.ts": 48, - "src/main/git/huge-folder-ignore.test.ts": 63, - "src/main/git/local-repo-ref-maintenance-probe-retry.test.ts": 244, - "src/main/git/local-repo-ref-maintenance.test.ts": 339, - "src/main/git/pack-refs-lock-ownership.test.ts": 77, - "src/main/git/porcelain-v1-records.test.ts": 132, - "src/main/git/remote-name-listing.test.ts": 307, - "src/main/git/remote-ref-probe-cache.test.ts": 342, - "src/main/git/remote-url-probe.test.ts": 376, - "src/main/git/remote.test.ts": 345, - "src/main/git/remove-worktree-branch-cleanup.test.ts": 337, - "src/main/git/remove-worktree-clean-preflight.test.ts": 188, - "src/main/git/remove-worktree.test.ts": 240, - "src/main/git/repo-branch-conflict-batched-probe.test.ts": 83, - "src/main/git/repo-branch-conflict-real-git.test.ts": 238, - "src/main/git/repo-branch-conflict.test.ts": 235, - "src/main/git/repo-clone-path.test.ts": 63, - "src/main/git/repo-default-base-timeout.test.ts": 108, - "src/main/git/repo-default-remote.test.ts": 134, - "src/main/git/repo-detection-async.test.ts": 507, - "src/main/git/repo-detection-batching.test.ts": 351, - "src/main/git/repo-detection-command.test.ts": 42, - "src/main/git/repo-detection-failure-parity.test.ts": 158, - "src/main/git/repo-detection.test.ts": 501, - "src/main/git/repo-maintenance-admission.test.ts": 647, - "src/main/git/repo-pack-index-maintenance.test.ts": 156, - "src/main/git/repo-pack-index-state.test.ts": 61, - "src/main/git/repo-ref-maintenance-real-git.test.ts": 3586, - "src/main/git/repo-remote-drift-real.test.ts": 266, - "src/main/git/repo-remote-drift.test.ts": 241, - "src/main/git/repo-search-ref-compat.test.ts": 129, - "src/main/git/repo-username.test.ts": 240, - "src/main/git/repo.test.ts": 1920, - "src/main/git/runner-buffer-cancellation.test.ts": 291, - "src/main/git/runner-command-exec.test.ts": 653, - "src/main/git/runner-gh-account-binding.test.ts": 214, - "src/main/git/runner-gh-host-args.test.ts": 189, - "src/main/git/runner-gh-rate-limit-breaker.test.ts": 217, - "src/main/git/runner-network-ssh-policy.test.ts": 159, - "src/main/git/runner-windows-host-environment.test.ts": 249, - "src/main/git/runner-wsl-direct-read.test.ts": 1856, - "src/main/git/runner-wsl-gh-fallback.test.ts": 1079, - "src/main/git/runner-wsl-linked-gitdir-timeout.test.ts": 195, - "src/main/git/runner-wsl-linked-gitdir-windows.test.ts": 248, - "src/main/git/runner-wsl-login-shell-capture.test.ts": 184, - "src/main/git/runner-wsl-network-ssh-policy.test.ts": 197, - "src/main/git/runner-wsl-read-routing.test.ts": 183, - "src/main/git/runner.test.ts": 212, - "src/main/git/saved-clone-probe-budget.test.ts": 81, - "src/main/git/saved-clone-target.test.ts": 612, - "src/main/git/settled-diff-cache-bounds.test.ts": 76, - "src/main/git/source-control/blob-absence-real-git.test.ts": 447, - "src/main/git/source-control/bulk-pathspec-command-line-budget.test.ts": 192, - "src/main/git/source-control/resolve-git-dir.test.ts": 59, - "src/main/git/source-control/stage-worktree-changes.test.ts": 1305, - "src/main/git/source-control/staging-discard-index-safety.test.ts": 941, - "src/main/git/source-control/wsl-tracked-pathspec-banner.test.ts": 193, - "src/main/git/status-branch-compare-real-ref.test.ts": 403, - "src/main/git/status-branch-compare.test.ts": 239, - "src/main/git/status-branch-line-total-exec-contract.test.ts": 892, - "src/main/git/status-branch-line-total-real-git.test.ts": 836, - "src/main/git/status-conflict-operations.test.ts": 163, - "src/main/git/status-cquoted-paths.test.ts": 374, - "src/main/git/status-diff-settled-cache.test.ts": 322, - "src/main/git/status-diff.test.ts": 260, + "src/main/daemon/session-shell-recovery.test.ts": 348, + "src/main/daemon/session-terminal-control.test.ts": 289, + "src/main/daemon/session.test.ts": 420, + "src/main/daemon/shell-ready-bash-wrapper.test.ts": 474, + "src/main/daemon/slow-daemon-session-verification.test.ts": 3611, + "src/main/daemon/startup-device-attributes-responder.test.ts": 37, + "src/main/daemon/terminal-attach-cancellation.test.ts": 31, + "src/main/daemon/terminal-checkpoint-serializer.test.ts": 257, + "src/main/daemon/terminal-checkpoint-writer-bounds.test.ts": 211, + "src/main/daemon/terminal-cursor-line-context.test.ts": 124, + "src/main/daemon/terminal-descendant-shutdown.test.ts": 120, + "src/main/daemon/terminal-history-incremental-restore.test.ts": 423, + "src/main/daemon/terminal-history-large-checkpoint-cold-restore.test.ts": 2132, + "src/main/daemon/terminal-history-log-encode-allocation.test.ts": 105, + "src/main/daemon/terminal-history-log.test.ts": 41, + "src/main/daemon/terminal-history-permissions.test.ts": 241, + "src/main/daemon/terminal-history-restorable-retention.test.ts": 47, + "src/main/daemon/terminal-history-seed-chunks.test.ts": 40, + "src/main/daemon/terminal-history-seed-segments.test.ts": 127, + "src/main/daemon/terminal-history-seed-transfer-registry.test.ts": 38, + "src/main/daemon/terminal-history-session-tombstone-retry.test.ts": 57, + "src/main/daemon/terminal-host-agent-session.test.ts": 503, + "src/main/daemon/terminal-host-attach-only.test.ts": 288, + "src/main/daemon/terminal-host-cheap-tier-ps-scan-volume.test.ts": 635, + "src/main/daemon/terminal-host-closing-session-listing.test.ts": 815, + "src/main/daemon/terminal-host-concurrent-create.test.ts": 383, + "src/main/daemon/terminal-host-cwd-readability.test.ts": 405, + "src/main/daemon/terminal-host-non-agent-foreground.test.ts": 154, + "src/main/daemon/terminal-host-process-inspection-cheap-tier.test.ts": 597, + "src/main/daemon/terminal-host-process-inspection.test.ts": 301, + "src/main/daemon/terminal-host-pty-owner-backend.test.ts": 333, + "src/main/daemon/terminal-host-readiness-reporting.test.ts": 360, + "src/main/daemon/terminal-host-session-reaping-leak.test.ts": 455, + "src/main/daemon/terminal-host-spawn-input-retention.test.ts": 761, + "src/main/daemon/terminal-host-startup-staging.test.ts": 353, + "src/main/daemon/terminal-host-startup.test.ts": 321, + "src/main/daemon/terminal-host-teardown-recreate.test.ts": 546, + "src/main/daemon/terminal-host-wsl-context.test.ts": 367, + "src/main/daemon/terminal-host.test.ts": 543, + "src/main/daemon/terminal-session-teardown.test.ts": 49, + "src/main/daemon/terminal-shell-armed-input-mode-recovery.test.ts": 302, + "src/main/daemon/terminal-shell-lifecycle-scanner.test.ts": 71, + "src/main/daemon/terminal-shell-recovery-barrier.test.ts": 1222, + "src/main/daemon/terminal-shell-recovery-clean-exit-retirement.test.ts": 54, + "src/main/daemon/terminal-snapshot-color-parity.test.ts": 107, + "src/main/daemon/terminal-snapshot-mouse-encoding.test.ts": 262, + "src/main/daemon/terminal-snapshot-osc8-roundtrip.test.ts": 74, + "src/main/daemon/terminal-snapshot-serialize-roundtrip.test.ts": 309, + "src/main/daemon/terminal-spawn-gate-legacy-restores.test.ts": 323, + "src/main/daemon/terminal-spawn-gate-shared-budget.test.ts": 467, + "src/main/daemon/terminal-spawn-gate.test.ts": 601, + "src/main/daemon/windows-conpty-warmup.test.ts": 42, + "src/main/daemon/wsl-cold-restore-cwd.test.ts": 34, + "src/main/destination-serialized-local-rename.test.ts": 1527, + "src/main/devin/hook-config-json.test.ts": 69, + "src/main/devin/hook-service.test.ts": 139, + "src/main/diagnostics/event-loop-delay-probe.test.ts": 91, + "src/main/diagnostics/main-thread-churn-probe.test.ts": 53, + "src/main/dock/unread-badge.test.ts": 26, + "src/main/droid/hook-service.test.ts": 87, + "src/main/dsh/dsh-home-patch.test.ts": 39, + "src/main/dsh/hook-service.test.ts": 103, + "src/main/durable-file-write-syscall-proof.test.ts": 50, + "src/main/durable-file-write.test.ts": 143, + "src/main/electron-updater-mac-staging-contract.test.ts": 117, + "src/main/emulator/android/adb-devices.test.ts": 53, + "src/main/emulator/android/android-app-control.test.ts": 33, + "src/main/emulator/android/android-capability-operations.test.ts": 40, + "src/main/emulator/android/android-device-inventory.test.ts": 45, + "src/main/emulator/android/android-input-commands.test.ts": 40, + "src/main/emulator/android/android-input-mapping.test.ts": 45, + "src/main/emulator/android/android-logcat.test.ts": 43, + "src/main/emulator/android/android-permissions.test.ts": 33, + "src/main/emulator/android/android-sdk-discovery.test.ts": 43, + "src/main/emulator/android/avd-manager.test.ts": 46, + "src/main/emulator/android/scrcpy-server-deploy.test.ts": 39, + "src/main/emulator/android/scrcpy-stream-session.test.ts": 405, + "src/main/emulator/android/scrcpy-video-frame-parser.test.ts": 155, + "src/main/emulator/android/uiautomator-tree.test.ts": 59, + "src/main/emulator/backends/android-emulator-backend.test.ts": 106, + "src/main/emulator/backends/ios-emulator-backend.test.ts": 132, + "src/main/emulator/emulator-availability.test.ts": 42, + "src/main/emulator/emulator-bridge.test.ts": 407, + "src/main/emulator/emulator-gesture-sender.test.ts": 163, + "src/main/emulator/emulator-session-registry.test.ts": 37, + "src/main/emulator/emulator-start-lease-registry.test.ts": 95, + "src/main/emulator/mjpeg-frame-parser.test.ts": 39, + "src/main/emulator/mjpeg-frame-stream.test.ts": 63, + "src/main/emulator/scrcpy-video-registry.test.ts": 39, + "src/main/emulator/serve-sim-accessibility-tree.test.ts": 49, + "src/main/emulator/serve-sim-ax-normalization.test.ts": 39, + "src/main/emulator/serve-sim-detached-session.test.ts": 32, + "src/main/emulator/serve-sim-execution.test.ts": 77, + "src/main/emulator/serve-sim-helper-processes.test.ts": 45, + "src/main/emulator/serve-sim-kill-contract.test.ts": 1251, + "src/main/emulator/serve-sim-runtime-materializer.test.ts": 179, + "src/main/emulator/simctl-simulator-devices.test.ts": 58, + "src/main/ephemeral-vm-recipe-runner.test.ts": 534, + "src/main/ephemeral-vm-resume-integrity.test.ts": 228, + "src/main/ephemeral-vm-runtime-environment-release.test.ts": 224, + "src/main/ephemeral-vm-runtime-service.test.ts": 979, + "src/main/ephemeral-vm-runtime-ssh-cleanup.test.ts": 325, + "src/main/ephemeral-vm-runtime-ssh.test.ts": 55, + "src/main/execution-host-workspace-trust.test.ts": 204, + "src/main/external-editor-launch.test.ts": 105, + "src/main/fish-history-session.test.ts": 70, + "src/main/fish-xdg-data-dirs-handoff.test.ts": 56, + "src/main/fish-xdg-data-dirs-no-config-launch.test.ts": 374, + "src/main/folder-upgrade-worktree-path.test.ts": 42, + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-client-worker-lifecycle.test.ts": 866, + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-client.test.ts": 281, + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-dispatch.test.ts": 305, + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-entry-path.test.ts": 50, + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-event-loop.test.ts": 2322, + "src/main/foreign-sqlite-readers/foreign-sqlite-reader-spawn.test.ts": 63, + "src/main/foreign-sqlite-readers/readers/cursor-profile.test.ts": 496, + "src/main/foreign-sqlite-readers/readers/opencode-binder-sessions.test.ts": 153, + "src/main/gemini/hook-service.test.ts": 88, + "src/main/ghostty/discovery.test.ts": 69, + "src/main/ghostty/index.test.ts": 95, + "src/main/ghostty/mapper-extended.test.ts": 71, + "src/main/ghostty/mapper.test.ts": 76, + "src/main/ghostty/parser.test.ts": 38, + "src/main/ghostty/theme-import.test.ts": 100, + "src/main/ghostty/theme-resolution.test.ts": 57, + "src/main/git-bash.test.ts": 40, + "src/main/git/add-sparse-worktree.test.ts": 162, + "src/main/git/admission-tier-plumbing.test.ts": 31, + "src/main/git/base-ref-search-selector.test.ts": 199, + "src/main/git/branch-rename.test.ts": 50, + "src/main/git/canonical-repo-key.test.ts": 183, + "src/main/git/check-ignored-paths.test.ts": 39, + "src/main/git/coalesced-probe.test.ts": 29, + "src/main/git/command-runner/gh-exec-file-deadline.test.ts": 115, + "src/main/git/command-runner/git-admission-output-parity.test.ts": 233, + "src/main/git/command-runner/git-admission-span.test.ts": 327, + "src/main/git/command-runner/git-admission-storm-measurement.test.ts": 179, + "src/main/git/command-runner/git-command-resolution-windows-checkout.test.ts": 82, + "src/main/git/command-runner/git-command-timeout-behavior.test.ts": 1935, + "src/main/git/command-runner/git-command-timeout.test.ts": 41, + "src/main/git/command-runner/git-exec-admission-exempt.test.ts": 168, + "src/main/git/command-runner/git-exec-admission-lifetime.test.ts": 647, + "src/main/git/command-runner/git-operation-executor.test.ts": 55, + "src/main/git/command-runner/git-spawn-admission-lifetime.test.ts": 180, + "src/main/git/command-runner/git-stream-admission-lifetime.test.ts": 254, + "src/main/git/command-runner/git-subprocess-admission.test.ts": 293, + "src/main/git/command-runner/hosted-cli-deadline-log.test.ts": 161, + "src/main/git/command-runner/spawned-command-tree-kill.test.ts": 57, + "src/main/git/command-runner/wsl-command-resolution-windows-binary.test.ts": 73, + "src/main/git/command-runner/wsl-host-failure.test.ts": 160, + "src/main/git/commit-object-ref.test.ts": 28, + "src/main/git/commit.test.ts": 187, + "src/main/git/create-event-repo-probe.test.ts": 86, + "src/main/git/exact-ref-probe.test.ts": 40, + "src/main/git/fetch-error-classification.test.ts": 40, + "src/main/git/fork-remote-refspec.test.ts": 46, + "src/main/git/fork-remote-stale-branch-refspec.test.ts": 44, + "src/main/git/gh-rate-limit-breaker.test.ts": 52, + "src/main/git/git-capability-state.test.ts": 48, + "src/main/git/git-network-safety-real-git.test.ts": 299, + "src/main/git/git-status-read-lease-owner.test.ts": 47, + "src/main/git/git-upstream-status-read-owner.test.ts": 42, + "src/main/git/git-username.test.ts": 181, + "src/main/git/hosted-remote-url.test.ts": 35, + "src/main/git/huge-folder-ignore.test.ts": 50, + "src/main/git/local-repo-ref-maintenance-probe-retry.test.ts": 227, + "src/main/git/local-repo-ref-maintenance.test.ts": 295, + "src/main/git/pack-refs-lock-ownership.test.ts": 58, + "src/main/git/porcelain-v1-records.test.ts": 129, + "src/main/git/remote-name-listing.test.ts": 496, + "src/main/git/remote-ref-probe-cache.test.ts": 480, + "src/main/git/remote-url-probe.test.ts": 474, + "src/main/git/remote.test.ts": 333, + "src/main/git/remove-worktree-branch-cleanup.test.ts": 310, + "src/main/git/remove-worktree-clean-preflight.test.ts": 179, + "src/main/git/remove-worktree.test.ts": 261, + "src/main/git/repo-branch-conflict-batched-probe.test.ts": 66, + "src/main/git/repo-branch-conflict-real-git.test.ts": 321, + "src/main/git/repo-branch-conflict.test.ts": 258, + "src/main/git/repo-clone-path.test.ts": 36, + "src/main/git/repo-default-base-timeout.test.ts": 110, + "src/main/git/repo-default-remote.test.ts": 106, + "src/main/git/repo-detection-async.test.ts": 517, + "src/main/git/repo-detection-batching.test.ts": 436, + "src/main/git/repo-detection-command.test.ts": 43, + "src/main/git/repo-detection-failure-parity.test.ts": 163, + "src/main/git/repo-detection.test.ts": 629, + "src/main/git/repo-maintenance-admission.test.ts": 705, + "src/main/git/repo-pack-index-maintenance.test.ts": 141, + "src/main/git/repo-pack-index-state.test.ts": 73, + "src/main/git/repo-ref-maintenance-real-git.test.ts": 2009, + "src/main/git/repo-remote-drift-real.test.ts": 240, + "src/main/git/repo-remote-drift.test.ts": 218, + "src/main/git/repo-search-ref-compat.test.ts": 78, + "src/main/git/repo-username.test.ts": 258, + "src/main/git/repo.test.ts": 2374, + "src/main/git/runner-buffer-cancellation.test.ts": 296, + "src/main/git/runner-command-exec.test.ts": 677, + "src/main/git/runner-gh-account-binding.test.ts": 223, + "src/main/git/runner-gh-host-args.test.ts": 60, + "src/main/git/runner-gh-rate-limit-breaker.test.ts": 240, + "src/main/git/runner-network-ssh-policy.test.ts": 214, + "src/main/git/runner-windows-host-environment.test.ts": 269, + "src/main/git/runner-wsl-direct-read.test.ts": 1817, + "src/main/git/runner-wsl-gh-fallback.test.ts": 1030, + "src/main/git/runner-wsl-linked-gitdir-timeout.test.ts": 158, + "src/main/git/runner-wsl-linked-gitdir-windows.test.ts": 308, + "src/main/git/runner-wsl-login-shell-capture.test.ts": 220, + "src/main/git/runner-wsl-network-ssh-policy.test.ts": 213, + "src/main/git/runner-wsl-read-routing.test.ts": 193, + "src/main/git/runner.test.ts": 192, + "src/main/git/saved-clone-probe-budget.test.ts": 70, + "src/main/git/saved-clone-target.test.ts": 970, + "src/main/git/settled-diff-cache-bounds.test.ts": 61, + "src/main/git/source-control/blob-absence-real-git.test.ts": 406, + "src/main/git/source-control/bulk-pathspec-command-line-budget.test.ts": 266, + "src/main/git/source-control/drive-checkout-suspect-paths.test.ts": 55, + "src/main/git/source-control/drive-checkout-suspect-real-git.test.ts": 235, + "src/main/git/source-control/drive-checkout-suspect-status.test.ts": 258, + "src/main/git/source-control/drive-checkout-suspect-writes-real-git.test.ts": 697, + "src/main/git/source-control/resolve-git-dir.test.ts": 43, + "src/main/git/source-control/stage-worktree-changes.test.ts": 1382, + "src/main/git/source-control/staging-discard-index-safety.test.ts": 563, + "src/main/git/source-control/wsl-tracked-pathspec-banner.test.ts": 167, + "src/main/git/status-branch-compare-real-ref.test.ts": 366, + "src/main/git/status-branch-compare.test.ts": 216, + "src/main/git/status-branch-line-total-exec-contract.test.ts": 806, + "src/main/git/status-branch-line-total-real-git.test.ts": 769, + "src/main/git/status-conflict-operations.test.ts": 223, + "src/main/git/status-cquoted-paths.test.ts": 285, + "src/main/git/status-diff-settled-cache.test.ts": 345, + "src/main/git/status-diff.test.ts": 309, "src/main/git/status-discard-and-bulk-staging.test.ts": 296, - "src/main/git/status-discard-symlink.test.ts": 544, - "src/main/git/status-line-stats-host-paths.test.ts": 77, - "src/main/git/status-pathspec-literals.test.ts": 362, - "src/main/git/status-porcelain-parser.test.ts": 78, - "src/main/git/status-read-coalescing.test.ts": 218, - "src/main/git/status-shared-symlinks.test.ts": 579, - "src/main/git/status-submodule-path-cache.test.ts": 367, - "src/main/git/status-submodule.test.ts": 264, - "src/main/git/status-symlink-probe-budget.test.ts": 179, - "src/main/git/status-upstream-negative-cache.test.ts": 718, - "src/main/git/status-upstream-probe-churn.test.ts": 280, - "src/main/git/status-upstream-ref.test.ts": 42, - "src/main/git/status-wsl-pathspecs.test.ts": 185, - "src/main/git/status.test.ts": 246, - "src/main/git/upstream-deferred-fork-remote-real.test.ts": 243, - "src/main/git/upstream.test.ts": 381, - "src/main/git/worktree-add-creation-config.test.ts": 290, - "src/main/git/worktree-add-local-base-refresh.test.ts": 291, - "src/main/git/worktree-add-local-base-suggestion.test.ts": 274, - "src/main/git/worktree-add-pending-removal.test.ts": 239, - "src/main/git/worktree-add-timeout-override.test.ts": 318, - "src/main/git/worktree-base-divergence-real-git.test.ts": 781, - "src/main/git/worktree-base-divergence.test.ts": 107, - "src/main/git/worktree-base-ref-probe.test.ts": 66, - "src/main/git/worktree-base-refresh-contention.test.ts": 239, - "src/main/git/worktree-base-refresh-lock-real-git.test.ts": 1272, - "src/main/git/worktree-branch-removal-host.test.ts": 185, - "src/main/git/worktree-common-dir-comparison.test.ts": 273, - "src/main/git/worktree-configured-paths-concurrency.test.ts": 91, - "src/main/git/worktree-create-admission-tier.test.ts": 305, - "src/main/git/worktree-create-git-executor-real-git.test.ts": 477, - "src/main/git/worktree-create-preparation-real-git.test.ts": 1597, - "src/main/git/worktree-create-preparation-real-wsl.test.ts": 327, - "src/main/git/worktree-created-description-real-git.test.ts": 1158, - "src/main/git/worktree-created-disk-witness.test.ts": 218, - "src/main/git/worktree-delete-limit.test.ts": 44, - "src/main/git/worktree-diff-stamp-guest-gitdir.test.ts": 62, - "src/main/git/worktree-diff-stamp-host-paths.test.ts": 60, - "src/main/git/worktree-git-capabilities.test.ts": 262, - "src/main/git/worktree-graph-listing.test.ts": 349, - "src/main/git/worktree-include-file.test.ts": 157, - "src/main/git/worktree-list-paths.test.ts": 464, - "src/main/git/worktree-list-porcelain.test.ts": 236, - "src/main/git/worktree-list-reader-cancellation.test.ts": 64, - "src/main/git/worktree-listing-created-sparse-distro.test.ts": 223, - "src/main/git/worktree-listing-sparse-cancellation.test.ts": 176, - "src/main/git/worktree-listing-sparse-distro.test.ts": 191, - "src/main/git/worktree-move.test.ts": 278, - "src/main/git/worktree-mutation-route-invalidation.test.ts": 177, - "src/main/git/worktree-porcelain-parsing.test.ts": 248, - "src/main/git/worktree-posix-path-case-sensitivity.test.ts": 169, - "src/main/git/worktree-preparation-add.test.ts": 194, - "src/main/git/worktree-preparation-base-oid.test.ts": 239, - "src/main/git/worktree-preparation-cancel-latency.bench.test.ts": 286, - "src/main/git/worktree-preparation-discard-real-git.test.ts": 466, - "src/main/git/worktree-preparation-fallback-cleanup-real-git.test.ts": 485, - "src/main/git/worktree-preparation-lock-real-git.test.ts": 922, - "src/main/git/worktree-preparation-lock.test.ts": 87, - "src/main/git/worktree-preparation-tip-refresh-real-git.test.ts": 488, - "src/main/git/worktree-preparation-tip-refresh.test.ts": 108, - "src/main/git/worktree-rebase-update-refs-real-git.test.ts": 273, - "src/main/git/worktree-removal-large-tree.bench.test.ts": 284, - "src/main/git/worktree-removal-real-git.test.ts": 834, - "src/main/git/worktree-remove-branch-deletion.test.ts": 258, - "src/main/git/worktree-safety-real-git.test.ts": 989, - "src/main/git/worktree-scan-cache-annotation-reuse.test.ts": 312, - "src/main/git/worktree-scan-cache-sharing.test.ts": 312, - "src/main/git/worktree-separate-git-dir.test.ts": 543, - "src/main/git/worktree-shared-directories.test.ts": 911, - "src/main/git/worktree-sparse-checkout-cache.test.ts": 206, - "src/main/git/worktree-sparse-checkout.test.ts": 574, - "src/main/git/worktree-sparse-state-host-paths.test.ts": 264, - "src/main/git/worktree-symlink-detection.test.ts": 61, - "src/main/git/wsl-direct-git-read-commands.test.ts": 72, - "src/main/git/wsl-linked-worktree-git-route-invalidation.test.ts": 61, - "src/main/git/wsl-linked-worktree-git-routing.test.ts": 118, - "src/main/git/wsl-process-group-termination.test.ts": 57, - "src/main/gitea/client-scan-invalidation.test.ts": 377, - "src/main/gitea/client.test.ts": 413, - "src/main/gitea/pull-request-creation.test.ts": 395, - "src/main/gitea/pull-request-mappers.test.ts": 44, - "src/main/gitea/repository-ref.test.ts": 373, - "src/main/github/auth-diagnose.test.ts": 66, - "src/main/github/client-create-pr.test.ts": 312, - "src/main/github/client-file-viewed.test.ts": 465, - "src/main/github/client-issue-origin-preference.test.ts": 405, - "src/main/github/client-issue-source.test.ts": 849, - "src/main/github/client-merge-queue-auto-merge.test.ts": 437, - "src/main/github/client-merged-pr-visibility.test.ts": 487, - "src/main/github/client-pr-branch-discovery.test.ts": 441, - "src/main/github/client-pr-check-details.test.ts": 427, - "src/main/github/client-pr-checks.test.ts": 464, - "src/main/github/client-pr-comment-reactions.test.ts": 449, - "src/main/github/client-pr-conflict-summary.test.ts": 478, - "src/main/github/client-pr-fallback-number.test.ts": 442, - "src/main/github/client-pr-linked-lookup.test.ts": 385, - "src/main/github/client-pr-local-runtime.test.ts": 511, - "src/main/github/client-pr-lookup-coalescing.test.ts": 622, - "src/main/github/client-pr-push-target.test.ts": 427, - "src/main/github/client-pr-state.test.ts": 408, - "src/main/github/client-rate-limit-block.test.ts": 377, - "src/main/github/client-ssh-provider-execution-boundary.test.ts": 441, - "src/main/github/client-stack-merge-guard.test.ts": 456, - "src/main/github/client-starred.test.ts": 340, - "src/main/github/client-tracked-upstream-fork-owner.test.ts": 438, - "src/main/github/client-tracked-upstream-snapshot.test.ts": 1192, - "src/main/github/client-work-item-check-summary.test.ts": 444, - "src/main/github/client-work-items-query-paging.test.ts": 651, - "src/main/github/client-work-items.test.ts": 579, - "src/main/github/comment-reactions.test.ts": 74, - "src/main/github/conflict-summary.test.ts": 97, - "src/main/github/default-branch-stale-pr.test.ts": 378, - "src/main/github/gh-account-token.test.ts": 60, - "src/main/github/gh-capability-state.test.ts": 48, - "src/main/github/gh-utils-concurrency.test.ts": 434, - "src/main/github/gh-utils.test.ts": 529, - "src/main/github/github-api-repository-validation.test.ts": 50, - "src/main/github/github-api-repository.test.ts": 563, - "src/main/github/github-enterprise-repository.test.ts": 403, - "src/main/github/github-pr-stack.test.ts": 376, - "src/main/github/github-remote-identity-parsing.test.ts": 58, - "src/main/github/github-repository-identity.fork-owner-repo.test.ts": 538, - "src/main/github/github-repository-identity.gh-account.test.ts": 408, - "src/main/github/github-repository-identity.signed-cache.test.ts": 372, - "src/main/github/github-repository-identity.ssh-host-alias.test.ts": 620, - "src/main/github/issue-mutation-source.test.ts": 436, - "src/main/github/issues.test.ts": 479, - "src/main/github/pr-head-tracking-ref.test.ts": 67, - "src/main/github/pr-mutation-review-invalidation.test.ts": 2411, - "src/main/github/pr-refresh-coordinator-active-burst-pacing.test.ts": 660, - "src/main/github/pr-refresh-coordinator-active-visible-priority.test.ts": 575, - "src/main/github/pr-refresh-coordinator-alias-coalescing.test.ts": 859, - "src/main/github/pr-refresh-coordinator-completion-order.test.ts": 602, - "src/main/github/pr-refresh-coordinator-error-cadence.test.ts": 574, - "src/main/github/pr-refresh-coordinator-foreground-pacing.test.ts": 469, - "src/main/github/pr-refresh-coordinator-rate-limit-budget.test.ts": 858, - "src/main/github/pr-refresh-coordinator-refresh-events.test.ts": 726, - "src/main/github/pr-refresh-coordinator-visibility-policy.test.ts": 1217, - "src/main/github/pr-refresh-coordinator-visible-follow-up.test.ts": 935, - "src/main/github/pr-refresh-error-classification.test.ts": 60, - "src/main/github/pr-refresh-queue-growth-bound.test.ts": 1057, - "src/main/github/pr-refresh-validation-backoff.test.ts": 56, - "src/main/github/pr-review-comment-lines.test.ts": 39, - "src/main/github/pr-start-point-compare-base.test.ts": 64, - "src/main/github/pr-start-point.test.ts": 95, + "src/main/git/status-discard-symlink.test.ts": 603, + "src/main/git/status-line-stats-host-paths.test.ts": 75, + "src/main/git/status-pathspec-literals.test.ts": 460, + "src/main/git/status-porcelain-parser.test.ts": 41, + "src/main/git/status-read-coalescing.test.ts": 192, + "src/main/git/status-shared-symlinks.test.ts": 746, + "src/main/git/status-submodule-path-cache.test.ts": 327, + "src/main/git/status-submodule.test.ts": 248, + "src/main/git/status-symlink-probe-budget.test.ts": 244, + "src/main/git/status-upstream-negative-cache.test.ts": 702, + "src/main/git/status-upstream-probe-churn.test.ts": 223, + "src/main/git/status-upstream-ref.test.ts": 39, + "src/main/git/status-wsl-pathspecs.test.ts": 208, + "src/main/git/status.test.ts": 281, + "src/main/git/upstream-deferred-fork-remote-real.test.ts": 199, + "src/main/git/upstream.test.ts": 401, + "src/main/git/worktree-add-creation-config.test.ts": 301, + "src/main/git/worktree-add-local-base-refresh.test.ts": 317, + "src/main/git/worktree-add-local-base-suggestion.test.ts": 249, + "src/main/git/worktree-add-pending-removal.test.ts": 293, + "src/main/git/worktree-add-timeout-override.test.ts": 198, + "src/main/git/worktree-base-divergence-real-git.test.ts": 1239, + "src/main/git/worktree-base-divergence.test.ts": 78, + "src/main/git/worktree-base-ref-probe.test.ts": 46, + "src/main/git/worktree-base-refresh-contention.test.ts": 262, + "src/main/git/worktree-base-refresh-lock-real-git.test.ts": 1609, + "src/main/git/worktree-branch-removal-host.test.ts": 183, + "src/main/git/worktree-common-dir-comparison.test.ts": 236, + "src/main/git/worktree-configured-paths-concurrency.test.ts": 70, + "src/main/git/worktree-create-admission-tier.test.ts": 298, + "src/main/git/worktree-create-git-executor-real-git.test.ts": 479, + "src/main/git/worktree-create-preparation-real-git.test.ts": 1746, + "src/main/git/worktree-create-preparation-real-wsl.test.ts": 287, + "src/main/git/worktree-created-description-real-git.test.ts": 975, + "src/main/git/worktree-created-disk-witness.test.ts": 190, + "src/main/git/worktree-delete-limit.test.ts": 21, + "src/main/git/worktree-diff-stamp-guest-gitdir.test.ts": 40, + "src/main/git/worktree-diff-stamp-host-paths.test.ts": 41, + "src/main/git/worktree-git-capabilities.test.ts": 288, + "src/main/git/worktree-graph-listing.test.ts": 236, + "src/main/git/worktree-include-file.test.ts": 118, + "src/main/git/worktree-list-paths.test.ts": 475, + "src/main/git/worktree-list-porcelain.test.ts": 192, + "src/main/git/worktree-list-reader-cancellation.test.ts": 73, + "src/main/git/worktree-listing-created-sparse-distro.test.ts": 251, + "src/main/git/worktree-listing-sparse-cancellation.test.ts": 222, + "src/main/git/worktree-listing-sparse-distro.test.ts": 200, + "src/main/git/worktree-move.test.ts": 226, + "src/main/git/worktree-mutation-route-invalidation.test.ts": 224, + "src/main/git/worktree-porcelain-parsing.test.ts": 260, + "src/main/git/worktree-posix-path-case-sensitivity.test.ts": 239, + "src/main/git/worktree-preparation-add.test.ts": 212, + "src/main/git/worktree-preparation-base-oid.test.ts": 210, + "src/main/git/worktree-preparation-cancel-latency.bench.test.ts": 368, + "src/main/git/worktree-preparation-discard-real-git.test.ts": 469, + "src/main/git/worktree-preparation-fallback-cleanup-real-git.test.ts": 590, + "src/main/git/worktree-preparation-lock-real-git.test.ts": 1078, + "src/main/git/worktree-preparation-lock.test.ts": 96, + "src/main/git/worktree-preparation-tip-refresh-real-git.test.ts": 797, + "src/main/git/worktree-preparation-tip-refresh.test.ts": 101, + "src/main/git/worktree-rebase-update-refs-real-git.test.ts": 224, + "src/main/git/worktree-removal-large-tree.bench.test.ts": 280, + "src/main/git/worktree-removal-real-git.test.ts": 1078, + "src/main/git/worktree-remove-branch-deletion.test.ts": 282, + "src/main/git/worktree-safety-real-git.test.ts": 528, + "src/main/git/worktree-scan-cache-annotation-reuse.test.ts": 282, + "src/main/git/worktree-scan-cache-sharing.test.ts": 313, + "src/main/git/worktree-separate-git-dir.test.ts": 537, + "src/main/git/worktree-shared-directories.test.ts": 968, + "src/main/git/worktree-sparse-checkout-cache.test.ts": 196, + "src/main/git/worktree-sparse-checkout.test.ts": 560, + "src/main/git/worktree-sparse-state-host-paths.test.ts": 257, + "src/main/git/worktree-symlink-detection.test.ts": 44, + "src/main/git/wsl-direct-git-read-commands.test.ts": 78, + "src/main/git/wsl-linked-worktree-git-route-invalidation.test.ts": 32, + "src/main/git/wsl-linked-worktree-git-routing.test.ts": 109, + "src/main/git/wsl-process-group-termination.test.ts": 51, + "src/main/git/wsl-windows-checkout-git-config.test.ts": 53, + "src/main/git/wsl-windows-checkout-git-probe.test.ts": 224, + "src/main/gitea/client-scan-invalidation.test.ts": 545, + "src/main/gitea/client.test.ts": 507, + "src/main/gitea/pull-request-creation.test.ts": 460, + "src/main/gitea/pull-request-mappers.test.ts": 38, + "src/main/gitea/repository-ref.test.ts": 469, + "src/main/github/auth-diagnose.test.ts": 35, + "src/main/github/client-create-pr.test.ts": 600, + "src/main/github/client-file-viewed.test.ts": 590, + "src/main/github/client-issue-origin-preference.test.ts": 580, + "src/main/github/client-issue-source.test.ts": 711, + "src/main/github/client-merge-queue-auto-merge.test.ts": 680, + "src/main/github/client-merged-pr-visibility.test.ts": 596, + "src/main/github/client-pr-branch-discovery.test.ts": 581, + "src/main/github/client-pr-check-details.test.ts": 573, + "src/main/github/client-pr-checks.test.ts": 611, + "src/main/github/client-pr-comment-reactions.test.ts": 622, + "src/main/github/client-pr-conflict-summary.test.ts": 644, + "src/main/github/client-pr-fallback-number.test.ts": 613, + "src/main/github/client-pr-linked-lookup.test.ts": 570, + "src/main/github/client-pr-local-runtime.test.ts": 581, + "src/main/github/client-pr-lookup-coalescing.test.ts": 597, + "src/main/github/client-pr-push-target.test.ts": 600, + "src/main/github/client-pr-state.test.ts": 543, + "src/main/github/client-rate-limit-block.test.ts": 949, + "src/main/github/client-ssh-provider-execution-boundary.test.ts": 591, + "src/main/github/client-stack-merge-guard.test.ts": 556, + "src/main/github/client-starred.test.ts": 629, + "src/main/github/client-tracked-upstream-default-branch.test.ts": 467, + "src/main/github/client-tracked-upstream-fork-owner.test.ts": 570, + "src/main/github/client-tracked-upstream-snapshot.test.ts": 907, + "src/main/github/client-work-item-check-summary.test.ts": 512, + "src/main/github/client-work-items-query-paging.test.ts": 663, + "src/main/github/client-work-items.test.ts": 518, + "src/main/github/comment-reactions.test.ts": 30, + "src/main/github/conflict-summary.test.ts": 69, + "src/main/github/default-branch-stale-pr.test.ts": 559, + "src/main/github/gh-account-token.test.ts": 50, + "src/main/github/gh-capability-state.test.ts": 33, + "src/main/github/gh-utils-concurrency.test.ts": 519, + "src/main/github/gh-utils.test.ts": 794, + "src/main/github/github-api-repository-validation.test.ts": 39, + "src/main/github/github-api-repository.test.ts": 686, + "src/main/github/github-enterprise-repository.test.ts": 548, + "src/main/github/github-pr-stack.test.ts": 780, + "src/main/github/github-remote-identity-parsing.test.ts": 34, + "src/main/github/github-repository-identity.fork-owner-repo.test.ts": 555, + "src/main/github/github-repository-identity.gh-account.test.ts": 486, + "src/main/github/github-repository-identity.signed-cache.test.ts": 524, + "src/main/github/github-repository-identity.ssh-host-alias.test.ts": 499, + "src/main/github/issue-mutation-source.test.ts": 597, + "src/main/github/issues.test.ts": 621, + "src/main/github/pr-head-tracking-ref.test.ts": 58, + "src/main/github/pr-mutation-review-invalidation.test.ts": 2422, + "src/main/github/pr-refresh-coordinator-active-burst-pacing.test.ts": 962, + "src/main/github/pr-refresh-coordinator-active-visible-priority.test.ts": 897, + "src/main/github/pr-refresh-coordinator-alias-coalescing.test.ts": 1341, + "src/main/github/pr-refresh-coordinator-completion-order.test.ts": 1083, + "src/main/github/pr-refresh-coordinator-error-cadence.test.ts": 992, + "src/main/github/pr-refresh-coordinator-foreground-pacing.test.ts": 777, + "src/main/github/pr-refresh-coordinator-rate-limit-budget.test.ts": 997, + "src/main/github/pr-refresh-coordinator-refresh-events.test.ts": 1193, + "src/main/github/pr-refresh-coordinator-visibility-policy.test.ts": 1798, + "src/main/github/pr-refresh-coordinator-visible-follow-up.test.ts": 1201, + "src/main/github/pr-refresh-error-classification.test.ts": 41, + "src/main/github/pr-refresh-queue-growth-bound.test.ts": 1418, + "src/main/github/pr-refresh-validation-backoff.test.ts": 61, + "src/main/github/pr-review-comment-lines.test.ts": 35, + "src/main/github/pr-start-point-compare-base.test.ts": 50, + "src/main/github/pr-start-point.test.ts": 86, "src/main/github/project-view-host-auth.test.ts": 102, - "src/main/github/project-view.test.ts": 482, - "src/main/github/project-view/mutations.test.ts": 63, - "src/main/github/project-view/project-field-mutations.test.ts": 51, - "src/main/github/project-view/project-view-config.test.ts": 427, - "src/main/github/project-view/project-view-table.test.ts": 461, - "src/main/github/project-view/repository-field-options.test.ts": 51, - "src/main/github/rate-limit.test.ts": 88, - "src/main/github/review-head-remote.test.ts": 60, - "src/main/github/stacked-pr-creation.test.ts": 437, - "src/main/github/work-item-details-concurrency.test.ts": 71, - "src/main/github/work-item-details-config-change.test.ts": 426, - "src/main/github/work-item-details-enterprise-host.test.ts": 450, - "src/main/github/work-item-details-file-viewed.test.ts": 468, - "src/main/github/work-item-details-issue-source.test.ts": 72, - "src/main/github/work-item-details-pr-files.test.ts": 418, - "src/main/github/work-item-details.test.ts": 472, - "src/main/github/work-item-search-fallback-environment.test.ts": 603, - "src/main/github/work-item-search-freshness.test.ts": 1299, - "src/main/github/work-item-search-isolation.test.ts": 679, - "src/main/github/work-item-search-pagination.test.ts": 501, - "src/main/github/work-item-search-semantics.test.ts": 638, - "src/main/gitlab/client-mr-auth-rate-limit.test.ts": 411, - "src/main/gitlab/client-mr-branch-lookup.test.ts": 473, - "src/main/gitlab/client-mr-job-ci.test.ts": 408, - "src/main/gitlab/client-mr-listing.test.ts": 409, - "src/main/gitlab/client-mr-review-actions.test.ts": 387, - "src/main/gitlab/client-work-items.test.ts": 451, - "src/main/gitlab/client.test.ts": 456, - "src/main/gitlab/gitlab-known-host-probe-wsl-fallback.test.ts": 375, - "src/main/gitlab/gitlab-known-host-probe.test.ts": 383, - "src/main/gitlab/gitlab-known-host-retirement.test.ts": 728, - "src/main/gitlab/gl-utils.test.ts": 452, - "src/main/gitlab/issues.test.ts": 490, - "src/main/gitlab/mappers-workitem.test.ts": 45, - "src/main/gitlab/mappers.test.ts": 71, - "src/main/gitlab/merge-request-creation.test.ts": 446, - "src/main/gitlab/mr-head-tracking-ref.test.ts": 61, - "src/main/gitlab/project-ref-parser.test.ts": 60, - "src/main/gitlab/work-item-details.test.ts": 100, - "src/main/grok-accounts/status.test.ts": 69, - "src/main/grok/grok-hook-config-file.test.ts": 153, - "src/main/grok/grok-hook-owners.test.ts": 61, - "src/main/grok/grok-hook-remnant-removal.test.ts": 89, + "src/main/github/project-view.test.ts": 529, + "src/main/github/project-view/mutations.test.ts": 48, + "src/main/github/project-view/project-field-mutations.test.ts": 35, + "src/main/github/project-view/project-view-config.test.ts": 566, + "src/main/github/project-view/project-view-table.test.ts": 560, + "src/main/github/project-view/repository-field-options.test.ts": 39, + "src/main/github/rate-limit.test.ts": 62, + "src/main/github/review-head-remote.test.ts": 41, + "src/main/github/stacked-pr-creation.test.ts": 509, + "src/main/github/work-item-details-concurrency.test.ts": 81, + "src/main/github/work-item-details-config-change.test.ts": 765, + "src/main/github/work-item-details-enterprise-host.test.ts": 571, + "src/main/github/work-item-details-file-viewed.test.ts": 542, + "src/main/github/work-item-details-issue-source.test.ts": 78, + "src/main/github/work-item-details-pr-files.test.ts": 627, + "src/main/github/work-item-details.test.ts": 516, + "src/main/github/work-item-search-fallback-environment.test.ts": 606, + "src/main/github/work-item-search-freshness.test.ts": 1165, + "src/main/github/work-item-search-isolation.test.ts": 636, + "src/main/github/work-item-search-pagination.test.ts": 646, + "src/main/github/work-item-search-semantics.test.ts": 954, + "src/main/gitlab/client-mr-auth-rate-limit.test.ts": 529, + "src/main/gitlab/client-mr-branch-lookup.test.ts": 631, + "src/main/gitlab/client-mr-job-ci.test.ts": 500, + "src/main/gitlab/client-mr-listing.test.ts": 542, + "src/main/gitlab/client-mr-review-actions.test.ts": 545, + "src/main/gitlab/client-work-items.test.ts": 544, + "src/main/gitlab/client.test.ts": 536, + "src/main/gitlab/gitlab-known-host-probe-wsl-fallback.test.ts": 608, + "src/main/gitlab/gitlab-known-host-probe.test.ts": 579, + "src/main/gitlab/gitlab-known-host-retirement.test.ts": 648, + "src/main/gitlab/gl-utils.test.ts": 589, + "src/main/gitlab/issues.test.ts": 616, + "src/main/gitlab/mappers-workitem.test.ts": 34, + "src/main/gitlab/mappers.test.ts": 62, + "src/main/gitlab/merge-request-creation.test.ts": 586, + "src/main/gitlab/mr-head-tracking-ref.test.ts": 42, + "src/main/gitlab/project-ref-parser.test.ts": 42, + "src/main/gitlab/work-item-details.test.ts": 93, + "src/main/grok-accounts/status.test.ts": 35, + "src/main/grok/grok-hook-config-file.test.ts": 160, + "src/main/grok/grok-hook-owners.test.ts": 41, + "src/main/grok/grok-hook-remnant-removal.test.ts": 110, "src/main/grok/grok-hook-stdin-no-eof.test.ts": 465, - "src/main/grok/hook-service.test.ts": 154, - "src/main/grok/windows-grok-hook-script.test.ts": 87, - "src/main/grok/windows-hook-launcher-chain.test.ts": 104, - "src/main/hang-watchdog/hang-detection-marker.test.ts": 71, - "src/main/hang-watchdog/hang-watchdog-detection-loop.test.ts": 64, - "src/main/hang-watchdog/hang-watchdog-worker-path.test.ts": 53, - "src/main/hang-watchdog/main-thread-hang-telemetry.test.ts": 299, - "src/main/hang-watchdog/main-thread-hang-watchdog-entry.test.ts": 107, - "src/main/hang-watchdog/main-thread-hang-watchdog.test.ts": 134, - "src/main/hang-watchdog/previous-hang-detection.test.ts": 118, - "src/main/hermes/hermes-config-comment-boundaries.test.ts": 513, - "src/main/hermes/hermes-config-document.test.ts": 197, - "src/main/hermes/hermes-config-flow-insertion.test.ts": 417, - "src/main/hermes/hermes-config-preservation.test.ts": 197, - "src/main/hermes/hermes-config-remote-adapter.test.ts": 186, - "src/main/hermes/hermes-config-source-edits.test.ts": 275, - "src/main/hermes/hook-service.test.ts": 307, - "src/main/hook-archive-timeout-observation.test.ts": 1099, - "src/main/hook-termination-real-process.test.ts": 8247, - "src/main/hooks-archive-exit-observation.test.ts": 277, - "src/main/hooks-effective-hook-resolution.test.ts": 471, - "src/main/hooks-issue-command.test.ts": 500, - "src/main/hooks-orca-yaml-parsing.test.ts": 485, - "src/main/hooks-runner.test.ts": 274, + "src/main/grok/hook-service.test.ts": 199, + "src/main/grok/windows-grok-hook-script.test.ts": 63, + "src/main/grok/windows-hook-launcher-chain.test.ts": 105, + "src/main/hang-watchdog/hang-detection-marker.test.ts": 41, + "src/main/hang-watchdog/hang-watchdog-detection-loop.test.ts": 48, + "src/main/hang-watchdog/hang-watchdog-worker-path.test.ts": 38, + "src/main/hang-watchdog/main-thread-hang-telemetry.test.ts": 277, + "src/main/hang-watchdog/main-thread-hang-watchdog-entry.test.ts": 68, + "src/main/hang-watchdog/main-thread-hang-watchdog.test.ts": 151, + "src/main/hang-watchdog/previous-hang-detection.test.ts": 80, + "src/main/hermes/hermes-config-comment-boundaries.test.ts": 545, + "src/main/hermes/hermes-config-document.test.ts": 218, + "src/main/hermes/hermes-config-flow-insertion.test.ts": 542, + "src/main/hermes/hermes-config-preservation.test.ts": 218, + "src/main/hermes/hermes-config-remote-adapter.test.ts": 192, + "src/main/hermes/hermes-config-source-edits.test.ts": 164, + "src/main/hermes/hook-service.test.ts": 305, + "src/main/hook-archive-timeout-observation.test.ts": 1048, + "src/main/hook-termination-real-process.test.ts": 3098, + "src/main/hooks-archive-exit-observation.test.ts": 244, + "src/main/hooks-effective-hook-resolution.test.ts": 456, + "src/main/hooks-issue-command.test.ts": 512, + "src/main/hooks-orca-yaml-parsing.test.ts": 414, + "src/main/hooks-runner.test.ts": 253, "src/main/hooks-setup-runner-script.test.ts": 238, - "src/main/hooks.test.ts": 493, - "src/main/host-tree-removal-asar.electron.test.ts": 259, - "src/main/host/deferred-secret-protection-report.test.ts": 99, - "src/main/host/electron-secret-store.test.ts": 91, - "src/main/host/secret-protection-report.test.ts": 79, - "src/main/host/structured-attention-localization.test.ts": 333, - "src/main/i18n/main-i18n-lazy-locale.test.ts": 278, - "src/main/ipc/agent-hooks.test.ts": 733, - "src/main/ipc/agent-pane-authority-ownership.test.ts": 56, - "src/main/ipc/agent-pane-retirement-ipc.test.ts": 62, - "src/main/ipc/agent-session-attachment-upload.test.ts": 7519, - "src/main/ipc/ai-vault-host-leg-cache-retention.test.ts": 553, - "src/main/ipc/ai-vault-scan-coalescing.test.ts": 1267, - "src/main/ipc/ai-vault-search-all-hosts-real-index.test.ts": 1776, - "src/main/ipc/ai-vault-search-all-hosts.test.ts": 260, - "src/main/ipc/ai-vault-search-merged-cursor.test.ts": 223, - "src/main/ipc/ai-vault-search.test.ts": 1049, - "src/main/ipc/ai-vault-unresolved-owner.test.ts": 589, - "src/main/ipc/ai-vault.test.ts": 847, - "src/main/ipc/app.test.ts": 585, - "src/main/ipc/automations-external-scope.test.ts": 481, - "src/main/ipc/bounded-warning-dedupe.test.ts": 45, - "src/main/ipc/browser-client-page-metadata-ipc.test.ts": 222, - "src/main/ipc/browser-preview-tool-authorization.test.ts": 669, - "src/main/ipc/browser-session-profile-ipc.test.ts": 342, - "src/main/ipc/browser-tab-registration-wait.test.ts": 259, - "src/main/ipc/browser.test.ts": 731, - "src/main/ipc/cli-appimage-stale-registration.test.ts": 272, - "src/main/ipc/cli.test.ts": 145, - "src/main/ipc/codex-config-sync.test.ts": 86, - "src/main/ipc/command-path-resolver.test.ts": 84, - "src/main/ipc/computer-use-permissions.test.ts": 62, - "src/main/ipc/crash-reporting-renderer-breadcrumbs.test.ts": 201, - "src/main/ipc/crash-reporting-renderer-error-report-attribution.test.ts": 95, - "src/main/ipc/crash-reporting-replay-guard-wedge-burst.test.ts": 86, - "src/main/ipc/crash-reporting.test.ts": 194, - "src/main/ipc/created-worktree-reconciliation.test.ts": 142, - "src/main/ipc/created-worktree-root-prune.test.ts": 404, - "src/main/ipc/credential-change-rate-limit-refresh.test.ts": 129, - "src/main/ipc/dashboard-payload-validation.test.ts": 200, - "src/main/ipc/dashboard-popout.test.ts": 134, - "src/main/ipc/deferred-emoji-shortcode-dataset.test.ts": 105, - "src/main/ipc/developer-permissions.test.ts": 262, - "src/main/ipc/diagnostics.test.ts": 104, - "src/main/ipc/doc-preview-grant-ipc.test.ts": 77, - "src/main/ipc/dropped-path-resolution.test.ts": 74, - "src/main/ipc/emulator-frame-stream-socket-lifetime.test.ts": 163, - "src/main/ipc/emulator-renderer-lifetime.test.ts": 117, - "src/main/ipc/emulator-stream-listener-cleanup.test.ts": 66, - "src/main/ipc/ephemeral-vm-provision-cancel.test.ts": 5904, - "src/main/ipc/ephemeral-vm-provisioned-root-ref.test.ts": 3154, - "src/main/ipc/ephemeral-vm-runtime-handler-cleanup.test.ts": 1695, - "src/main/ipc/ephemeral-vm.test.ts": 938, - "src/main/ipc/feedback-image-attachments.test.ts": 95, - "src/main/ipc/feedback-request-ownership.test.ts": 72, - "src/main/ipc/feedback.test.ts": 140, - "src/main/ipc/filesystem-allowed-roots.test.ts": 577, - "src/main/ipc/filesystem-auth.test.ts": 686, - "src/main/ipc/filesystem-branch-compare-diff.test.ts": 1287, - "src/main/ipc/filesystem-canonical-owner.test.ts": 135, - "src/main/ipc/filesystem-commit-message-generation.test.ts": 1529, - "src/main/ipc/filesystem-commit-message-model-discovery.test.ts": 2164, - "src/main/ipc/filesystem-conflict-operation-routing.test.ts": 1720, - "src/main/ipc/filesystem-content-search-cancellation.test.ts": 1714, - "src/main/ipc/filesystem-document-folder-writes.test.ts": 1153, - "src/main/ipc/filesystem-download-lifetime.test.ts": 722, - "src/main/ipc/filesystem-download-transfers.test.ts": 1825, - "src/main/ipc/filesystem-git-commit-dispatch.test.ts": 1766, - "src/main/ipc/filesystem-git-status-staging.test.ts": 1611, - "src/main/ipc/filesystem-import-local-rollback.test.ts": 87, - "src/main/ipc/filesystem-import-ssh-ops.test.ts": 1162, - "src/main/ipc/filesystem-import-ssh-path-safety.test.ts": 101, - "src/main/ipc/filesystem-import-ssh.test.ts": 1053, - "src/main/ipc/filesystem-import.test.ts": 2156, - "src/main/ipc/filesystem-list-files-handler-name-filter.test.ts": 1379, - "src/main/ipc/filesystem-list-files-name-filter.test.ts": 426, - "src/main/ipc/filesystem-list-files.test.ts": 778, - "src/main/ipc/filesystem-markdown-document-listing.test.ts": 1718, - "src/main/ipc/filesystem-mutations-runtime-upload.test.ts": 386, - "src/main/ipc/filesystem-mutations.test.ts": 1142, - "src/main/ipc/filesystem-path-containment-remote-enoent.test.ts": 46, - "src/main/ipc/filesystem-path-containment-unicode-normalization.test.ts": 492, - "src/main/ipc/filesystem-pull-request-linked-issue-lifetime.test.ts": 2316, - "src/main/ipc/filesystem-quick-open-options.integration.test.ts": 1700, - "src/main/ipc/filesystem-runtime-upload-staging.test.ts": 79, - "src/main/ipc/filesystem-search-file-paths.test.ts": 346, - "src/main/ipc/filesystem-search-rg-timeout.test.ts": 1670, - "src/main/ipc/filesystem-symlink-directory-entries.test.ts": 44, - "src/main/ipc/filesystem-watcher-canonical-root-paths.test.ts": 509, - "src/main/ipc/filesystem-watcher-document-lifetime.test.ts": 381, - "src/main/ipc/filesystem-watcher-dormant-rearm.test.ts": 384, - "src/main/ipc/filesystem-watcher-event-batch.test.ts": 44, - "src/main/ipc/filesystem-watcher-ignore-real.test.ts": 424, - "src/main/ipc/filesystem-watcher-ignore.test.ts": 67, - "src/main/ipc/filesystem-watcher-large-batch.test.ts": 173, - "src/main/ipc/filesystem-watcher-local-events.test.ts": 406, - "src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts": 1181, - "src/main/ipc/filesystem-watcher-native-capacity.test.ts": 174, - "src/main/ipc/filesystem-watcher-real.test.ts": 546, - "src/main/ipc/filesystem-watcher-remote-batch.test.ts": 198, - "src/main/ipc/filesystem-watcher-remote-cancellation.test.ts": 474, - "src/main/ipc/filesystem-watcher-remote-capacity.test.ts": 195, - "src/main/ipc/filesystem-watcher-remote-rearm.test.ts": 160, - "src/main/ipc/filesystem-watcher-removal-deadline.test.ts": 396, - "src/main/ipc/filesystem-watcher-terminal-resync.test.ts": 167, - "src/main/ipc/filesystem-watcher-unwatchable-roots.test.ts": 189, - "src/main/ipc/filesystem-watcher-wsl.test.ts": 95, - "src/main/ipc/filesystem-watcher.test.ts": 2015, - "src/main/ipc/filesystem.test.ts": 2109, - "src/main/ipc/filesystem/filesystem-source-control-ai-targets.test.ts": 50, - "src/main/ipc/filesystem/local-file-range-read.test.ts": 64, - "src/main/ipc/filesystem/local-regular-file-read.test.ts": 83, - "src/main/ipc/floating-workspace-directory.test.ts": 59, - "src/main/ipc/folder-repo-git-upgrade-lifetime.test.ts": 100, - "src/main/ipc/folder-repo-git-upgrade.test.ts": 2130, - "src/main/ipc/freebuff-detection.test.ts": 42, - "src/main/ipc/git-status-upstream-ref-watch-request.test.ts": 321, - "src/main/ipc/github-issue-source-preference.test.ts": 602, - "src/main/ipc/github-pr-refresh-routing.test.ts": 729, - "src/main/ipc/github-repo-access-guards.test.ts": 690, - "src/main/ipc/github-ssh-connection-routing.test.ts": 658, - "src/main/ipc/github-star-telemetry.test.ts": 592, - "src/main/ipc/github-work-item-args.test.ts": 56, - "src/main/ipc/github-wsl-runtime-routing.test.ts": 680, - "src/main/ipc/gitlab-repo-access.test.ts": 105, - "src/main/ipc/gitlab.test.ts": 203, - "src/main/ipc/hooks/worktree-hook-execution-host.test.ts": 783, - "src/main/ipc/hosted-review.test.ts": 668, - "src/main/ipc/jira-cancellable-requests.test.ts": 58, - "src/main/ipc/keybindings.test.ts": 57, - "src/main/ipc/linear.test.ts": 110, - "src/main/ipc/local-file-access-resolution-network-path.test.ts": 176, - "src/main/ipc/local-file-access-resolution.test.ts": 435, - "src/main/ipc/local-log-tail-lifetime.test.ts": 131, - "src/main/ipc/local-log-tail.test.ts": 65, - "src/main/ipc/local-network-connection-test.test.ts": 323, - "src/main/ipc/local-worktree-runtime-options.test.ts": 105, - "src/main/ipc/macos-keyboard-layout-change-notifications.test.ts": 66, - "src/main/ipc/macos-keyboard-layout-snapshot.test.ts": 47, - "src/main/ipc/managed-host-status-publication.test.ts": 1003, - "src/main/ipc/markdown-documents-remote-paths.test.ts": 180, - "src/main/ipc/markdown-documents-ripgrep-real.test.ts": 190, - "src/main/ipc/markdown-documents-ripgrep.test.ts": 250, - "src/main/ipc/markdown-documents.test.ts": 212, - "src/main/ipc/minimax-credentials.test.ts": 76, - "src/main/ipc/mobile.test.ts": 27111, - "src/main/ipc/native-chat-renderer-lifetime.test.ts": 910, - "src/main/ipc/native-chat-renderer-watcher-cleanup.test.ts": 1169, - "src/main/ipc/native-chat-subscribe-lifecycle.test.ts": 1282, - "src/main/ipc/native-chat.test.ts": 2590, - "src/main/ipc/notebook-project-link.test.ts": 365, - "src/main/ipc/notebook.test.ts": 507, - "src/main/ipc/notifications-custom-sound.test.ts": 328, - "src/main/ipc/notifications-delivery-gating.test.ts": 426, - "src/main/ipc/notifications-message-formatting.test.ts": 402, - "src/main/ipc/notifications-mobile-fanout.test.ts": 319, - "src/main/ipc/notifications-permission-onboarding.test.ts": 333, - "src/main/ipc/notifications-retention-lifecycle.test.ts": 506, - "src/main/ipc/notifications-subject-retirement.test.ts": 411, - "src/main/ipc/opencode-go-credentials.test.ts": 81, - "src/main/ipc/orca-profile-auth-handlers.test.ts": 473, - "src/main/ipc/orca-profile-org-members-handlers.test.ts": 61, - "src/main/ipc/orca-profiles-switch-persistence.test.ts": 2035, - "src/main/ipc/orca-profiles.test.ts": 706, - "src/main/ipc/orcad-delta-move-handlers.test.ts": 750, - "src/main/ipc/orcad-runtime-conversion-handlers.test.ts": 650, - "src/main/ipc/orcad-runtime-lifecycle-handlers.test.ts": 47, - "src/main/ipc/orcad-runtime-maintenance-handlers.test.ts": 826, - "src/main/ipc/orcad-ssh-provisioning-handlers.test.ts": 51, - "src/main/ipc/parcel-watcher-child-launch.test.ts": 66, - "src/main/ipc/parcel-watcher-child-registry.test.ts": 98, - "src/main/ipc/parcel-watcher-crash-fuse.test.ts": 48, - "src/main/ipc/parcel-watcher-disconnect-termination.test.ts": 142, - "src/main/ipc/parcel-watcher-entry-path.test.ts": 61, - "src/main/ipc/parcel-watcher-event-cancellation.test.ts": 64, - "src/main/ipc/parcel-watcher-event-delivery.test.ts": 119, - "src/main/ipc/parcel-watcher-module-loader.test.ts": 51, - "src/main/ipc/parcel-watcher-native-error-retirement.test.ts": 106, - "src/main/ipc/parcel-watcher-owned-children.test.ts": 52, - "src/main/ipc/parcel-watcher-process-dispose.test.ts": 55, - "src/main/ipc/parcel-watcher-process-entry.test.ts": 215, - "src/main/ipc/parcel-watcher-process.test.ts": 562, - "src/main/ipc/parcel-watcher-root-path-rewrite.test.ts": 114, - "src/main/ipc/parcel-watcher-shallow-subscription.test.ts": 146, - "src/main/ipc/parcel-watcher-supervisor-capacity-wait.test.ts": 173, - "src/main/ipc/parcel-watcher-supervisor-capacity.test.ts": 232, - "src/main/ipc/parcel-watcher-supervisor-disposal.test.ts": 90, - "src/main/ipc/parcel-watcher-unsubscribe-timeout.test.ts": 112, - "src/main/ipc/pet-bundle.test.ts": 57, - "src/main/ipc/pet.test.ts": 319, - "src/main/ipc/plugin-marketplaces.test.ts": 275, - "src/main/ipc/plugins.test.ts": 437, - "src/main/ipc/preflight-agent-detection-no-subprocess.test.ts": 688, - "src/main/ipc/preflight-agent-detection.test.ts": 725, - "src/main/ipc/preflight-agent-refresh.test.ts": 717, - "src/main/ipc/preflight-command-exec.test.ts": 89, - "src/main/ipc/preflight-host-cli-status.test.ts": 731, - "src/main/ipc/preflight-provider-command-selection.test.ts": 352, - "src/main/ipc/preflight-remote-ssh.test.ts": 631, - "src/main/ipc/preflight-runnable-local-cli.test.ts": 9174, - "src/main/ipc/preflight-windows-path-refresh.repro.test.ts": 77, + "src/main/hooks.test.ts": 484, + "src/main/host-tree-removal-asar.electron.test.ts": 197, + "src/main/host/deferred-secret-protection-report.test.ts": 67, + "src/main/host/electron-http-client.test.ts": 489, + "src/main/host/electron-secret-store.test.ts": 46, + "src/main/host/macos-keychain-reachability.test.ts": 65, + "src/main/host/safe-storage-availability.test.ts": 30, + "src/main/host/safe-storage-import-ratchet.test.ts": 128, + "src/main/host/secret-protection-report.test.ts": 44, + "src/main/host/serve-keychain-guard.test.ts": 48, + "src/main/host/structured-attention-localization.test.ts": 403, + "src/main/i18n/main-i18n-lazy-locale.test.ts": 282, + "src/main/integration-state-file-cache.test.ts": 65, + "src/main/integration-token-file-cache.test.ts": 128, + "src/main/ipc/agent-hooks.test.ts": 801, + "src/main/ipc/agent-pane-authority-ownership.test.ts": 36, + "src/main/ipc/agent-pane-retirement-ipc.test.ts": 42, + "src/main/ipc/agent-session-attachment-upload.test.ts": 6808, + "src/main/ipc/ai-vault-host-leg-cache-retention.test.ts": 1790, + "src/main/ipc/ai-vault-resume.test.ts": 1185, + "src/main/ipc/ai-vault-scan-coalescing.test.ts": 2026, + "src/main/ipc/ai-vault-search-all-hosts-real-index.test.ts": 1534, + "src/main/ipc/ai-vault-search-all-hosts.test.ts": 232, + "src/main/ipc/ai-vault-search-merged-cursor.test.ts": 205, + "src/main/ipc/ai-vault-search.test.ts": 1285, + "src/main/ipc/ai-vault-unresolved-owner.test.ts": 1330, + "src/main/ipc/ai-vault.test.ts": 1323, + "src/main/ipc/app.test.ts": 466, + "src/main/ipc/automations-external-scope.test.ts": 403, + "src/main/ipc/bounded-warning-dedupe.test.ts": 36, + "src/main/ipc/browser-annotation-viewport-ipc.test.ts": 195, + "src/main/ipc/browser-client-page-metadata-ipc.test.ts": 265, + "src/main/ipc/browser-preview-tool-authorization.test.ts": 862, + "src/main/ipc/browser-session-profile-ipc.test.ts": 320, + "src/main/ipc/browser-tab-registration-wait.test.ts": 262, + "src/main/ipc/browser.test.ts": 749, + "src/main/ipc/cli-appimage-stale-registration.test.ts": 283, + "src/main/ipc/cli.test.ts": 179, + "src/main/ipc/codex-config-sync.test.ts": 75, + "src/main/ipc/command-path-resolver.test.ts": 60, + "src/main/ipc/computer-use-permissions.test.ts": 30, + "src/main/ipc/crash-reporting-renderer-breadcrumbs.test.ts": 136, + "src/main/ipc/crash-reporting-renderer-error-report-attribution.test.ts": 63, + "src/main/ipc/crash-reporting-replay-guard-wedge-burst.test.ts": 49, + "src/main/ipc/crash-reporting.test.ts": 134, + "src/main/ipc/created-worktree-reconciliation.test.ts": 113, + "src/main/ipc/created-worktree-root-prune.test.ts": 498, + "src/main/ipc/credential-change-rate-limit-refresh.test.ts": 108, + "src/main/ipc/dashboard-payload-validation.test.ts": 149, + "src/main/ipc/dashboard-popout.test.ts": 89, + "src/main/ipc/deferred-emoji-shortcode-dataset.test.ts": 84, + "src/main/ipc/developer-permissions.test.ts": 208, + "src/main/ipc/diagnostics.test.ts": 71, + "src/main/ipc/doc-preview-grant-ipc.test.ts": 63, + "src/main/ipc/dropped-path-resolution.test.ts": 62, + "src/main/ipc/emulator-frame-stream-socket-lifetime.test.ts": 164, + "src/main/ipc/emulator-renderer-lifetime.test.ts": 112, + "src/main/ipc/emulator-stream-listener-cleanup.test.ts": 56, + "src/main/ipc/ephemeral-vm-managed-ssh.test.ts": 552, + "src/main/ipc/ephemeral-vm-provision-cancel.test.ts": 4199, + "src/main/ipc/ephemeral-vm-provisioned-root-ref.test.ts": 2965, + "src/main/ipc/ephemeral-vm-runtime-handler-cleanup.test.ts": 1376, + "src/main/ipc/ephemeral-vm.test.ts": 866, + "src/main/ipc/feedback-image-attachments.test.ts": 52, + "src/main/ipc/feedback-request-ownership.test.ts": 56, + "src/main/ipc/feedback.test.ts": 142, + "src/main/ipc/filesystem-allowed-roots.test.ts": 730, + "src/main/ipc/filesystem-auth.test.ts": 599, + "src/main/ipc/filesystem-branch-compare-diff.test.ts": 1385, + "src/main/ipc/filesystem-canonical-owner.test.ts": 340, + "src/main/ipc/filesystem-commit-message-generation.test.ts": 1421, + "src/main/ipc/filesystem-commit-message-model-discovery.test.ts": 1485, + "src/main/ipc/filesystem-conflict-operation-routing.test.ts": 1434, + "src/main/ipc/filesystem-content-search-cancellation.test.ts": 1939, + "src/main/ipc/filesystem-document-folder-writes.test.ts": 1256, + "src/main/ipc/filesystem-download-lifetime.test.ts": 713, + "src/main/ipc/filesystem-download-transfers.test.ts": 1450, + "src/main/ipc/filesystem-git-commit-dispatch.test.ts": 1454, + "src/main/ipc/filesystem-git-status-staging.test.ts": 1645, + "src/main/ipc/filesystem-import-local-rollback.test.ts": 86, + "src/main/ipc/filesystem-import.test.ts": 1213, + "src/main/ipc/filesystem-list-files-handler-name-filter.test.ts": 1554, + "src/main/ipc/filesystem-list-files-name-filter.test.ts": 564, + "src/main/ipc/filesystem-list-files.test.ts": 742, + "src/main/ipc/filesystem-markdown-document-listing.test.ts": 1217, + "src/main/ipc/filesystem-mutations-runtime-upload.test.ts": 605, + "src/main/ipc/filesystem-mutations.test.ts": 1536, + "src/main/ipc/filesystem-path-containment-remote-enoent.test.ts": 40, + "src/main/ipc/filesystem-path-containment-unicode-normalization.test.ts": 616, + "src/main/ipc/filesystem-pull-request-linked-issue-lifetime.test.ts": 2341, + "src/main/ipc/filesystem-quick-open-options.integration.test.ts": 1280, + "src/main/ipc/filesystem-runtime-upload-staging.test.ts": 62, + "src/main/ipc/filesystem-search-file-paths.test.ts": 361, + "src/main/ipc/filesystem-search-rg-timeout.test.ts": 1957, + "src/main/ipc/filesystem-symlink-directory-entries.test.ts": 33, + "src/main/ipc/filesystem-watcher-canonical-root-paths.test.ts": 456, + "src/main/ipc/filesystem-watcher-document-lifetime.test.ts": 388, + "src/main/ipc/filesystem-watcher-dormant-rearm.test.ts": 204, + "src/main/ipc/filesystem-watcher-event-batch.test.ts": 34, + "src/main/ipc/filesystem-watcher-ignore-real.test.ts": 441, + "src/main/ipc/filesystem-watcher-ignore.test.ts": 45, + "src/main/ipc/filesystem-watcher-large-batch.test.ts": 172, + "src/main/ipc/filesystem-watcher-local-events.test.ts": 375, + "src/main/ipc/filesystem-watcher-local-unsubscribe.test.ts": 1005, + "src/main/ipc/filesystem-watcher-native-capacity.test.ts": 186, + "src/main/ipc/filesystem-watcher-real.test.ts": 762, + "src/main/ipc/filesystem-watcher-remote-batch.test.ts": 202, + "src/main/ipc/filesystem-watcher-remote-cancellation.test.ts": 501, + "src/main/ipc/filesystem-watcher-remote-capacity.test.ts": 330, + "src/main/ipc/filesystem-watcher-remote-rearm.test.ts": 168, + "src/main/ipc/filesystem-watcher-removal-deadline.test.ts": 664, + "src/main/ipc/filesystem-watcher-terminal-resync.test.ts": 195, + "src/main/ipc/filesystem-watcher-unwatchable-roots.test.ts": 174, + "src/main/ipc/filesystem-watcher-wsl.test.ts": 107, + "src/main/ipc/filesystem-watcher.test.ts": 2082, + "src/main/ipc/filesystem.test.ts": 1823, + "src/main/ipc/filesystem/filesystem-source-control-ai-targets.test.ts": 45, + "src/main/ipc/filesystem/local-file-range-read.test.ts": 38, + "src/main/ipc/filesystem/local-regular-file-read.test.ts": 55, + "src/main/ipc/floating-workspace-directory.test.ts": 49, + "src/main/ipc/folder-repo-git-upgrade-lifetime.test.ts": 108, + "src/main/ipc/folder-repo-git-upgrade.test.ts": 2177, + "src/main/ipc/freebuff-detection.test.ts": 33, + "src/main/ipc/git-status-upstream-ref-watch-request.test.ts": 368, + "src/main/ipc/github-issue-source-preference.test.ts": 585, + "src/main/ipc/github-pr-refresh-routing.test.ts": 774, + "src/main/ipc/github-repo-access-guards.test.ts": 705, + "src/main/ipc/github-ssh-connection-routing.test.ts": 664, + "src/main/ipc/github-star-telemetry.test.ts": 551, + "src/main/ipc/github-work-item-args.test.ts": 38, + "src/main/ipc/github-wsl-runtime-routing.test.ts": 767, + "src/main/ipc/gitlab-repo-access.test.ts": 276, + "src/main/ipc/gitlab.test.ts": 362, + "src/main/ipc/hooks/worktree-hook-execution-host.test.ts": 758, + "src/main/ipc/hosted-review.test.ts": 617, + "src/main/ipc/jira-cancellable-requests.test.ts": 34, + "src/main/ipc/jira-ipc-params.test.ts": 53, + "src/main/ipc/jira-issue-summary-generation.test.ts": 232, + "src/main/ipc/jira-project-metadata-registration.test.ts": 214, + "src/main/ipc/keybindings.test.ts": 49, + "src/main/ipc/linear.test.ts": 91, + "src/main/ipc/local-file-access-resolution-network-path.test.ts": 372, + "src/main/ipc/local-file-access-resolution.test.ts": 682, + "src/main/ipc/local-log-tail-lifetime.test.ts": 150, + "src/main/ipc/local-log-tail.test.ts": 38, + "src/main/ipc/local-network-connection-test.test.ts": 291, + "src/main/ipc/local-worktree-runtime-options.test.ts": 266, + "src/main/ipc/macos-keyboard-layout-change-notifications.test.ts": 51, + "src/main/ipc/macos-keyboard-layout-snapshot.test.ts": 33, + "src/main/ipc/managed-host-status-publication.test.ts": 731, + "src/main/ipc/markdown-documents-remote-paths.test.ts": 191, + "src/main/ipc/markdown-documents-ripgrep-real.test.ts": 264, + "src/main/ipc/markdown-documents-ripgrep.test.ts": 256, + "src/main/ipc/markdown-documents.test.ts": 189, + "src/main/ipc/minimax-credentials.test.ts": 79, + "src/main/ipc/mobile.test.ts": 26150, + "src/main/ipc/native-chat-renderer-lifetime.test.ts": 924, + "src/main/ipc/native-chat-renderer-watcher-cleanup.test.ts": 1312, + "src/main/ipc/native-chat-subscribe-lifecycle.test.ts": 840, + "src/main/ipc/native-chat.test.ts": 2682, + "src/main/ipc/notebook-project-link.test.ts": 875, + "src/main/ipc/notebook.test.ts": 606, + "src/main/ipc/notifications-custom-sound.test.ts": 375, + "src/main/ipc/notifications-delivery-gating.test.ts": 453, + "src/main/ipc/notifications-macos-removal.test.ts": 338, + "src/main/ipc/notifications-message-formatting.test.ts": 331, + "src/main/ipc/notifications-mobile-fanout.test.ts": 270, + "src/main/ipc/notifications-permission-onboarding.test.ts": 457, + "src/main/ipc/notifications-retention-lifecycle.test.ts": 415, + "src/main/ipc/notifications-subject-retirement.test.ts": 285, + "src/main/ipc/notifications-workspace-click-binding.test.ts": 259, + "src/main/ipc/opencode-go-credentials.test.ts": 63, + "src/main/ipc/orca-profile-auth-handlers.test.ts": 498, + "src/main/ipc/orca-profile-org-members-handlers.test.ts": 47, + "src/main/ipc/orca-profiles-switch-persistence.test.ts": 1882, + "src/main/ipc/orca-profiles.test.ts": 595, + "src/main/ipc/orcad-delta-move-handlers.test.ts": 637, + "src/main/ipc/orcad-runtime-conversion-handlers.test.ts": 886, + "src/main/ipc/orcad-runtime-lifecycle-handlers.test.ts": 43, + "src/main/ipc/orcad-runtime-maintenance-handlers.test.ts": 856, + "src/main/ipc/orcad-ssh-provisioning-handlers.test.ts": 33, + "src/main/ipc/parcel-watcher-child-launch.test.ts": 51, + "src/main/ipc/parcel-watcher-child-registry.test.ts": 88, + "src/main/ipc/parcel-watcher-crash-fuse.test.ts": 37, + "src/main/ipc/parcel-watcher-disconnect-termination.test.ts": 146, + "src/main/ipc/parcel-watcher-entry-path.test.ts": 41, + "src/main/ipc/parcel-watcher-event-cancellation.test.ts": 45, + "src/main/ipc/parcel-watcher-event-delivery.test.ts": 112, + "src/main/ipc/parcel-watcher-module-loader.test.ts": 32, + "src/main/ipc/parcel-watcher-native-error-retirement.test.ts": 100, + "src/main/ipc/parcel-watcher-owned-children.test.ts": 50, + "src/main/ipc/parcel-watcher-process-dispose.test.ts": 36, + "src/main/ipc/parcel-watcher-process-entry.test.ts": 179, + "src/main/ipc/parcel-watcher-process.test.ts": 558, + "src/main/ipc/parcel-watcher-root-path-rewrite.test.ts": 109, + "src/main/ipc/parcel-watcher-shallow-subscription.test.ts": 113, + "src/main/ipc/parcel-watcher-supervisor-capacity-wait.test.ts": 177, + "src/main/ipc/parcel-watcher-supervisor-capacity.test.ts": 247, + "src/main/ipc/parcel-watcher-supervisor-disposal.test.ts": 91, + "src/main/ipc/parcel-watcher-unsubscribe-timeout.test.ts": 118, + "src/main/ipc/pet-bundle.test.ts": 52, + "src/main/ipc/pet.test.ts": 328, + "src/main/ipc/plugin-marketplaces.test.ts": 250, + "src/main/ipc/plugins.test.ts": 430, + "src/main/ipc/preflight-agent-detection-no-subprocess.test.ts": 598, + "src/main/ipc/preflight-agent-detection.test.ts": 554, + "src/main/ipc/preflight-agent-refresh.test.ts": 672, + "src/main/ipc/preflight-command-exec.test.ts": 77, + "src/main/ipc/preflight-host-cli-status.test.ts": 781, + "src/main/ipc/preflight-provider-command-selection.test.ts": 376, + "src/main/ipc/preflight-remote-ssh.test.ts": 635, + "src/main/ipc/preflight-runnable-local-cli.test.ts": 9234, + "src/main/ipc/preflight-windows-path-refresh.repro.test.ts": 105, "src/main/ipc/preflight-wsl-agent-detection.test.ts": 118, - "src/main/ipc/preflight-wsl-command.test.ts": 64, - "src/main/ipc/pty-activation-inventory-scope.test.ts": 1905, - "src/main/ipc/pty-agent-launch-pane-attach.test.ts": 2906, - "src/main/ipc/pty-applied-size-reporting.test.ts": 2036, - "src/main/ipc/pty-buffer-snapshot-dispatch.test.ts": 2028, - "src/main/ipc/pty-codex-account-attribution.test.ts": 1676, - "src/main/ipc/pty-controller-owner-recovery.test.ts": 1924, - "src/main/ipc/pty-controller-ownership-routing.test.ts": 2013, - "src/main/ipc/pty-controller-process-inventory.test.ts": 2060, - "src/main/ipc/pty-controller-spawn-admission.test.ts": 4498, - "src/main/ipc/pty-cumulative-ack-accounting.test.ts": 2167, - "src/main/ipc/pty-daemon-controller-teardown.test.ts": 2056, - "src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts": 2293, - "src/main/ipc/pty-daemon-spawn-codex-auth.test.ts": 2131, - "src/main/ipc/pty-daemon-spawn-session-identity.test.ts": 2097, - "src/main/ipc/pty-daemon-spawn-wsl-runtime.test.ts": 2152, - "src/main/ipc/pty-daemon-ssh-lease-lifecycle.test.ts": 1871, - "src/main/ipc/pty-dead-owner-respawn.test.ts": 2153, - "src/main/ipc/pty-delivery-health-heal.test.ts": 2042, - "src/main/ipc/pty-dispatcher-handshake-osc-answers.test.ts": 2140, - "src/main/ipc/pty-encoding.test.ts": 43, - "src/main/ipc/pty-global-renderer-credit.test.ts": 2232, - "src/main/ipc/pty-hidden-at-spawn-mark.test.ts": 5138, - "src/main/ipc/pty-hidden-delivery-gate.test.ts": 59, - "src/main/ipc/pty-ipc-hidden-delivery-gate.test.ts": 2060, - "src/main/ipc/pty-ipc-producer-flow-control.test.ts": 1883, - "src/main/ipc/pty-listener-teardown-and-orphans.test.ts": 4358, - "src/main/ipc/pty-login-shell-startup-commands.test.ts": 2204, - "src/main/ipc/pty-management.test.ts": 138, - "src/main/ipc/pty-output-batching-drain.test.ts": 1870, - "src/main/ipc/pty-output-drain-rounds.test.ts": 1637, - "src/main/ipc/pty-pane-claim-arbitration.test.ts": 7990, - "src/main/ipc/pty-pane-materialization-race.test.ts": 2686, - "src/main/ipc/pty-pane-reservation-settlement.test.ts": 2440, - "src/main/ipc/pty-pane-restart-replace.test.ts": 2428, - "src/main/ipc/pty-pending-data-drain-queue-differential.test.ts": 1638, - "src/main/ipc/pty-pending-data-drain-queue.test.ts": 65, - "src/main/ipc/pty-pending-data-drain-scheduler-differential.test.ts": 93, - "src/main/ipc/pty-pending-output-cap.test.ts": 1952, - "src/main/ipc/pty-persisted-incarnation-repair.test.ts": 1941, - "src/main/ipc/pty-producer-flow-control.test.ts": 60, - "src/main/ipc/pty-renderer-inflight-credit.test.ts": 2177, - "src/main/ipc/pty-renderer-lifecycle-delivery-reset.test.ts": 2053, - "src/main/ipc/pty-renderer-liveness-guard.test.ts": 2073, - "src/main/ipc/pty-renderer-send-failure-recovery.test.ts": 2046, - "src/main/ipc/pty-reset-input-modes.test.ts": 1487, - "src/main/ipc/pty-restore-record-seeding.test.ts": 4523, - "src/main/ipc/pty-restored-appimage-cli-shim-refresh.test.ts": 2019, - "src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts": 4718, - "src/main/ipc/pty-runtime-kill-and-exit.test.ts": 2264, - "src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts": 1912, - "src/main/ipc/pty-serializer-settlement-mapping.test.ts": 5457, - "src/main/ipc/pty-session-liveness-and-ownership.test.ts": 2154, - "src/main/ipc/pty-spawn-codex-home-unavailable.test.ts": 1758, - "src/main/ipc/pty-spawn-cwd-fallback.test.ts": 2244, - "src/main/ipc/pty-spawn-env-agent-overlays.test.ts": 2493, - "src/main/ipc/pty-spawn-env-codex-home-routing.test.ts": 2025, - "src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts": 2070, - "src/main/ipc/pty-spawn-env-terminal-basics.test.ts": 2141, - "src/main/ipc/pty-spawn-pi-owner-env.test.ts": 2128, - "src/main/ipc/pty-spawn-placement-threading.test.ts": 2049, - "src/main/ipc/pty-spawn-runtime-handle-binding.test.ts": 5122, - "src/main/ipc/pty-spawn-timing.test.ts": 57, - "src/main/ipc/pty-ssh-stop-verdict.test.ts": 2449, - "src/main/ipc/pty-ssh-undelivered-kill.test.ts": 2026, - "src/main/ipc/pty-startup-barrier-and-listing.test.ts": 2108, - "src/main/ipc/pty-startup-swap-window-presence.test.ts": 1988, - "src/main/ipc/pty-windows-shell-selection.test.ts": 2490, - "src/main/ipc/pty-write-ipc-validation.test.ts": 2150, - "src/main/ipc/pty-wsl-cwd-validation.test.ts": 2113, - "src/main/ipc/pty/configured-terminal-shell-args.test.ts": 54, - "src/main/ipc/pty/daemon-late-exit.test.ts": 4940, - "src/main/ipc/pty/delivery/attached-pty-size.test.ts": 53, - "src/main/ipc/pty/delivery/interactive-typing-burst.test.ts": 51, - "src/main/ipc/pty/host-env/codex-hook-reconcile-spawn.test.ts": 291, - "src/main/ipc/pty/host-env/omp-launch-environment.test.ts": 75, - "src/main/ipc/pty/host-env/opencode-hook-installation.test.ts": 669, - "src/main/ipc/pty/ipc/claude-old-terminal.test.ts": 48, - "src/main/ipc/pty/ipc/codex-shared-server.test.ts": 76, - "src/main/ipc/pty/ipc/leaf-move.test.ts": 628, - "src/main/ipc/pty/ipc/spawn-commit-run-facts.test.ts": 819, - "src/main/ipc/pty/ipc/spawn-commit-ssh-lease-cardinality.test.ts": 1887, - "src/main/ipc/pty/ipc/spawn-options-codex-isolation.test.ts": 880, - "src/main/ipc/pty/ipc/spawn-options-terminal-shell-args.test.ts": 1183, - "src/main/ipc/pty/ipc/spawn-push-target-materialization-real-git.test.ts": 1122, - "src/main/ipc/pty/ipc/spawn-push-target-materialization.test.ts": 77, - "src/main/ipc/pty/ipc/spawn-reattach-size-cache.test.ts": 1409, - "src/main/ipc/pty/ipc/terminal-view-attributes-ipc.test.ts": 400, - "src/main/ipc/pty/ipc/write-input-acceptance.test.ts": 77, - "src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts": 69, - "src/main/ipc/pty/ipc/write-input-user-input.test.ts": 69, - "src/main/ipc/pty/managed-data-account-spawn-environment.test.ts": 770, - "src/main/ipc/pty/opencode-legacy-spawn-deletions.test.ts": 790, - "src/main/ipc/pty/pane/agent-launch-pane-evidence.test.ts": 721, - "src/main/ipc/pty/pane/join-pane-spawn.test.ts": 924, - "src/main/ipc/pty/pane/launch-authority.test.ts": 71, - "src/main/ipc/pty/pane/pane-owner-replacement.test.ts": 1478, - "src/main/ipc/pty/pane/stable-pane-absence-death-certificate.test.ts": 5118, - "src/main/ipc/pty/pane/stable-pane-relay-absence-respawn.test.ts": 814, - "src/main/ipc/pty/provider/registry-color-query-reply-colors.test.ts": 398, - "src/main/ipc/pty/provider/registry-host-routing.test.ts": 417, - "src/main/ipc/pty/register-headless-runtime.test.ts": 43, - "src/main/ipc/pty/register-without-renderer.test.ts": 5362, - "src/main/ipc/pty/runtime/queried-host-kinds.test.ts": 512, - "src/main/ipc/pty/runtime/spawn-commit-pty-size.test.ts": 1018, - "src/main/ipc/pty/runtime/spawn-commit-run-facts.test.ts": 833, - "src/main/ipc/pty/runtime/spawn-preflight-requested-shell.test.ts": 2118, - "src/main/ipc/pty/spawn-options-agent-workspace-trust.test.ts": 1046, - "src/main/ipc/qoder-desktop-execution-host.test.ts": 1898, - "src/main/ipc/qoder-independent-renderer-spawn-review.test.ts": 1589, - "src/main/ipc/rate-limits.test.ts": 52, - "src/main/ipc/readdir-error-diagnostics.test.ts": 41, - "src/main/ipc/register-core-handlers/register-core-handlers.test.ts": 2495, - "src/main/ipc/registered-worktree-root-ownership.test.ts": 438, - "src/main/ipc/registered-worktree-root-refresh.test.ts": 184, - "src/main/ipc/registered-worktree-root-wsl-runtime.test.ts": 171, - "src/main/ipc/registered-worktree-roots-scoped-invalidation.test.ts": 89, - "src/main/ipc/remote-watcher-event-batch.test.ts": 77, - "src/main/ipc/renderer-lifetime-abort.test.ts": 58, - "src/main/ipc/renderer-shutdown-checkpoint.test.ts": 457, - "src/main/ipc/renderer-terminal-serializer-readiness.test.ts": 56, - "src/main/ipc/repos-add-linked-worktree.test.ts": 1324, - "src/main/ipc/repos-create.test.ts": 1219, - "src/main/ipc/repos-execution-host-catalog.test.ts": 1173, - "src/main/ipc/repos-local-add-and-project-setup.test.ts": 1476, - "src/main/ipc/repos-local-clone-lifecycle.test.ts": 1644, - "src/main/ipc/repos-nested-import.test.ts": 1393, - "src/main/ipc/repos-nested-scan.test.ts": 1295, - "src/main/ipc/repos-picker.test.ts": 1109, - "src/main/ipc/repos-remote-base-ref-queries.test.ts": 1223, - "src/main/ipc/repos-remote-client-events.test.ts": 1788, - "src/main/ipc/repos-remote-git-username.test.ts": 1104, - "src/main/ipc/repos-sparse-presets.test.ts": 1196, - "src/main/ipc/repos/folder-workspace-attachment-handlers.test.ts": 1525, - "src/main/ipc/repos/repo-creation-git-availability.test.ts": 310, - "src/main/ipc/runtime-agent-launch-capability.test.ts": 3143, - "src/main/ipc/runtime-environment-browser-client-host-handler.test.ts": 1143, - "src/main/ipc/runtime-environment-capability-evidence.test.ts": 46, - "src/main/ipc/runtime-environment-cli-removal-routing.test.ts": 1153, - "src/main/ipc/runtime-environment-diagnostics-broadcast.test.ts": 43, - "src/main/ipc/runtime-environment-federated-read-routing.test.ts": 1096, - "src/main/ipc/runtime-environment-federated-read-transport.bench.test.ts": 328, - "src/main/ipc/runtime-environment-host-key.test.ts": 181, - "src/main/ipc/runtime-environment-managed-tunnel.test.ts": 483, - "src/main/ipc/runtime-environment-preference.test.ts": 401, - "src/main/ipc/runtime-environment-removal-cleanup.test.ts": 53, - "src/main/ipc/runtime-environment-removal-storage.test.ts": 1576, - "src/main/ipc/runtime-environment-request-connections.test.ts": 2790, - "src/main/ipc/runtime-environment-revision-guard.test.ts": 43, - "src/main/ipc/runtime-environment-session-reconcile.test.ts": 977, - "src/main/ipc/runtime-environment-status-connection.test.ts": 1592, - "src/main/ipc/runtime-environment-status-recovery.test.ts": 1038, - "src/main/ipc/runtime-environment-status-retirement.test.ts": 1009, - "src/main/ipc/runtime-environment-subscription-identity.test.ts": 1066, - "src/main/ipc/runtime-environment-support-routing.test.ts": 1306, - "src/main/ipc/runtime-environment-transport-generation.test.ts": 42, - "src/main/ipc/runtime-environment-transport-routing-tailscale-hint.test.ts": 2342, - "src/main/ipc/runtime-environments-call-routing.test.ts": 2632, - "src/main/ipc/runtime-environments-capability-cache.test.ts": 1740, - "src/main/ipc/runtime-environments-pairing.test.ts": 2250, - "src/main/ipc/runtime-environments-status-diagnostics.test.ts": 1491, - "src/main/ipc/runtime-environments-subscription-lifecycle.test.ts": 2695, - "src/main/ipc/runtime-environments-subscription-routing.test.ts": 1823, - "src/main/ipc/runtime-environments-subscription-teardown.test.ts": 1629, - "src/main/ipc/runtime-import-limits.test.ts": 46, - "src/main/ipc/runtime-ssh-access-handlers.test.ts": 191, - "src/main/ipc/runtime-subscribe-lifecycle.test.ts": 2991, - "src/main/ipc/runtime-upload-file-stream.test.ts": 140, - "src/main/ipc/runtime-upload-slice-boundaries.test.ts": 373, - "src/main/ipc/runtime-upload-temp-sweep.test.ts": 1055, - "src/main/ipc/runtime-watcher-disposal-owners.test.ts": 51, - "src/main/ipc/runtime-watcher-pending-assignment.test.ts": 294, - "src/main/ipc/runtime-watcher-pool-shutdown.test.ts": 91, - "src/main/ipc/runtime-watcher-process-pool.test.ts": 146, - "src/main/ipc/runtime.test.ts": 3357, - "src/main/ipc/sender-scoped-request-cancellation.test.ts": 96, - "src/main/ipc/session-close-terminal-surface.test.ts": 346, - "src/main/ipc/session.test.ts": 431, - "src/main/ipc/settings.test.ts": 1109, - "src/main/ipc/shallow-watch-delivery-probe-lifetime.test.ts": 92, - "src/main/ipc/shallow-watch-delivery-probe.test.ts": 104, - "src/main/ipc/shell.test.ts": 168, - "src/main/ipc/skill-cloud-install-ipc-schemas.test.ts": 269, - "src/main/ipc/skill-cloud-ipc-handlers.test.ts": 280, - "src/main/ipc/skill-install-management-ipc-handlers.test.ts": 274, - "src/main/ipc/skill-install-progress-ipc.test.ts": 50, - "src/main/ipc/skill-ipc-main-window.test.ts": 56, - "src/main/ipc/skills.test.ts": 1180, - "src/main/ipc/source-control-ai-linked-issue.test.ts": 89, - "src/main/ipc/sparse-preset-attribution.test.ts": 43, - "src/main/ipc/speech-worker-exit-lifetime.test.ts": 529, - "src/main/ipc/speech.test.ts": 80, - "src/main/ipc/ssh-app-shutdown.test.ts": 4272, - "src/main/ipc/ssh-browse.test.ts": 706, - "src/main/ipc/ssh-connect-recheck-cancellation.test.ts": 4164, - "src/main/ipc/ssh-disconnect-cancellation.test.ts": 5166, - "src/main/ipc/ssh-handler-reregistration.test.ts": 4043, - "src/main/ipc/ssh-host-server-decision-failure.test.ts": 385, - "src/main/ipc/ssh-host-server-on-connect-wiring.test.ts": 2374, - "src/main/ipc/ssh-host-sleep-reconnect.test.ts": 424, - "src/main/ipc/ssh-managed-server-unlink-broadcast.test.ts": 487, - "src/main/ipc/ssh-passphrase.test.ts": 104, - "src/main/ipc/ssh-state-broadcast-fanout.test.ts": 4164, - "src/main/ipc/ssh-target-crud-handlers-ownership.test.ts": 560, - "src/main/ipc/ssh-target-registry.test.ts": 6430, - "src/main/ipc/ssh-test-connection-probe-registry.test.ts": 46, - "src/main/ipc/ssh-test-probe-presence.test.ts": 63, - "src/main/ipc/ssh.test.ts": 4177, - "src/main/ipc/telemetry.test.ts": 297, - "src/main/ipc/terminal-git-credential-guard.test.ts": 103, - "src/main/ipc/terminal-preview-output-stream.test.ts": 57, - "src/main/ipc/terminal-preview.test.ts": 237, - "src/main/ipc/terminal-render-desync-evidence.test.ts": 88, - "src/main/ipc/terminal-startup-color-query-replies.test.ts": 322, - "src/main/ipc/tui-agent-detection-commands.test.ts": 60, - "src/main/ipc/ui.test.ts": 86, - "src/main/ipc/usage-provider-handlers-readiness.test.ts": 597, - "src/main/ipc/usage-provider-handlers.test.ts": 59, - "src/main/ipc/watched-worktree-catalog-notification.test.ts": 74, - "src/main/ipc/watcher-event-root-path-rewrite.test.ts": 56, - "src/main/ipc/watcher-removal-gate.test.ts": 52, - "src/main/ipc/workspace-cleanup-activity.test.ts": 89, - "src/main/ipc/workspace-cleanup-broad-scan.test.ts": 686, - "src/main/ipc/workspace-cleanup-execution-host-routing.test.ts": 595, - "src/main/ipc/workspace-cleanup-local-git-routing.test.ts": 1167, - "src/main/ipc/workspace-cleanup-snapshot-ipc.test.ts": 105, - "src/main/ipc/workspace-cleanup.test.ts": 2928, - "src/main/ipc/workspace-create-error-classifier.test.ts": 52, - "src/main/ipc/workspace-ports.test.ts": 92, - "src/main/ipc/workspace-space.test.ts": 66, - "src/main/ipc/worktree-base-directory-change-collector.test.ts": 54, - "src/main/ipc/worktree-base-directory-event-filter.test.ts": 72, - "src/main/ipc/worktree-base-directory-poller-marker-fanout.test.ts": 204, - "src/main/ipc/worktree-base-directory-poller.test.ts": 2668, - "src/main/ipc/worktree-base-directory-watch-targets.test.ts": 437, - "src/main/ipc/worktree-base-directory-watcher.test.ts": 293, - "src/main/ipc/worktree-create-lineage.test.ts": 958, - "src/main/ipc/worktree-folder-rename-target.test.ts": 96, - "src/main/ipc/worktree-git-common-polling.test.ts": 1046, - "src/main/ipc/worktree-git-common-watch-reconciliation.test.ts": 112, - "src/main/ipc/worktree-git-common-watch.test.ts": 2683, - "src/main/ipc/worktree-git-status-ref-watch.test.ts": 76, - "src/main/ipc/worktree-head-identity-reader-concurrency.test.ts": 77, - "src/main/ipc/worktree-head-identity-reader-incremental.test.ts": 113, - "src/main/ipc/worktree-head-identity-reader.test.ts": 85, - "src/main/ipc/worktree-head-identity-refresh.test.ts": 87, - "src/main/ipc/worktree-head-identity-retirement.test.ts": 79, - "src/main/ipc/worktree-include-copy-budget.test.ts": 129, - "src/main/ipc/worktree-logic-created-agent.test.ts": 97, - "src/main/ipc/worktree-logic-wsl.test.ts": 104, - "src/main/ipc/worktree-logic.test.ts": 143, - "src/main/ipc/worktree-metadata-merge.test.ts": 52, - "src/main/ipc/worktree-path-deduplication.test.ts": 77, - "src/main/ipc/worktree-push-target-cleanup.test.ts": 454, - "src/main/ipc/worktree-push-target-reconciliation-real-git.test.ts": 789, - "src/main/ipc/worktree-push-target-reconciliation.test.ts": 466, - "src/main/ipc/worktree-push-target-refspec-migration.test.ts": 188, - "src/main/ipc/worktree-push-target-refspec-real-git.test.ts": 1650, - "src/main/ipc/worktree-push-target-remote-scan.test.ts": 440, - "src/main/ipc/worktree-push-target-setup.test.ts": 464, - "src/main/ipc/worktree-remote-push-target-materialization.test.ts": 772, - "src/main/ipc/worktree-symlink-reconciliation.real.test.ts": 465, - "src/main/ipc/worktree-symlinks.test.ts": 155, - "src/main/ipc/worktree-watcher-removal-binding.test.ts": 51, - "src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts": 1670, - "src/main/ipc/worktrees-background-removal.test.ts": 1866, - "src/main/ipc/worktrees-canonical-ssh-listing.test.ts": 1212, - "src/main/ipc/worktrees-create-event-timing.test.ts": 1165, - "src/main/ipc/worktrees-create-metadata-persistence.test.ts": 1531, - "src/main/ipc/worktrees-delete-pty-teardown.test.ts": 1507, + "src/main/ipc/preflight-wsl-command.test.ts": 51, + "src/main/ipc/pty-activation-inventory-scope.test.ts": 2045, + "src/main/ipc/pty-agent-launch-pane-attach.test.ts": 3145, + "src/main/ipc/pty-agent-pane-launch.test.ts": 1883, + "src/main/ipc/pty-applied-size-reporting.test.ts": 2339, + "src/main/ipc/pty-buffer-snapshot-dispatch.test.ts": 1820, + "src/main/ipc/pty-codex-account-attribution.test.ts": 1982, + "src/main/ipc/pty-controller-owner-recovery.test.ts": 2124, + "src/main/ipc/pty-controller-ownership-routing.test.ts": 2014, + "src/main/ipc/pty-controller-process-inventory.test.ts": 1998, + "src/main/ipc/pty-controller-spawn-admission.test.ts": 4679, + "src/main/ipc/pty-cumulative-ack-accounting.test.ts": 2200, + "src/main/ipc/pty-daemon-controller-teardown.test.ts": 1944, + "src/main/ipc/pty-daemon-spawn-agent-home-env.test.ts": 2206, + "src/main/ipc/pty-daemon-spawn-codex-auth.test.ts": 2270, + "src/main/ipc/pty-daemon-spawn-session-identity.test.ts": 1909, + "src/main/ipc/pty-daemon-spawn-wsl-runtime.test.ts": 2030, + "src/main/ipc/pty-daemon-ssh-lease-lifecycle.test.ts": 1727, + "src/main/ipc/pty-dead-owner-respawn.test.ts": 1858, + "src/main/ipc/pty-delivery-health-heal.test.ts": 1926, + "src/main/ipc/pty-dispatcher-handshake-osc-answers.test.ts": 2029, + "src/main/ipc/pty-encoding.test.ts": 37, + "src/main/ipc/pty-global-renderer-credit.test.ts": 2307, + "src/main/ipc/pty-hidden-at-spawn-mark.test.ts": 4605, + "src/main/ipc/pty-hidden-delivery-gate.test.ts": 35, + "src/main/ipc/pty-ipc-hidden-delivery-gate.test.ts": 2345, + "src/main/ipc/pty-ipc-producer-flow-control.test.ts": 1606, + "src/main/ipc/pty-launch-parity.test.ts": 6066, + "src/main/ipc/pty-launch-race-parity.test.ts": 2327, + "src/main/ipc/pty-listener-teardown-and-orphans.test.ts": 4257, + "src/main/ipc/pty-login-shell-startup-commands.test.ts": 2112, + "src/main/ipc/pty-management.test.ts": 122, + "src/main/ipc/pty-output-batching-drain.test.ts": 2085, + "src/main/ipc/pty-output-drain-rounds.test.ts": 2064, + "src/main/ipc/pty-pane-claim-arbitration.test.ts": 5856, + "src/main/ipc/pty-pane-materialization-race.test.ts": 2733, + "src/main/ipc/pty-pane-reservation-settlement.test.ts": 2331, + "src/main/ipc/pty-pane-restart-replace.test.ts": 2178, + "src/main/ipc/pty-pending-data-drain-queue-differential.test.ts": 1691, + "src/main/ipc/pty-pending-data-drain-queue.test.ts": 48, + "src/main/ipc/pty-pending-data-drain-scheduler-differential.test.ts": 74, + "src/main/ipc/pty-pending-output-cap.test.ts": 1806, + "src/main/ipc/pty-persisted-incarnation-repair.test.ts": 1916, + "src/main/ipc/pty-producer-flow-control.test.ts": 45, + "src/main/ipc/pty-renderer-inflight-credit.test.ts": 2088, + "src/main/ipc/pty-renderer-lifecycle-delivery-reset.test.ts": 2291, + "src/main/ipc/pty-renderer-liveness-guard.test.ts": 2009, + "src/main/ipc/pty-renderer-send-failure-recovery.test.ts": 2032, + "src/main/ipc/pty-reset-input-modes.test.ts": 1920, + "src/main/ipc/pty-restore-record-seeding.test.ts": 4510, + "src/main/ipc/pty-restored-appimage-cli-shim-refresh.test.ts": 2141, + "src/main/ipc/pty-runtime-hidden-at-spawn-mark.test.ts": 4797, + "src/main/ipc/pty-runtime-kill-and-exit.test.ts": 2167, + "src/main/ipc/pty-runtime-ssh-binding-persistence.test.ts": 2004, + "src/main/ipc/pty-serializer-settlement-mapping.test.ts": 3692, + "src/main/ipc/pty-session-liveness-and-ownership.test.ts": 1826, + "src/main/ipc/pty-spawn-codex-home-unavailable.test.ts": 2268, + "src/main/ipc/pty-spawn-cwd-fallback.test.ts": 2106, + "src/main/ipc/pty-spawn-env-agent-overlays.test.ts": 2292, + "src/main/ipc/pty-spawn-env-codex-home-routing.test.ts": 2050, + "src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts": 2201, + "src/main/ipc/pty-spawn-env-terminal-basics.test.ts": 2276, + "src/main/ipc/pty-spawn-pi-owner-env.test.ts": 1543, + "src/main/ipc/pty-spawn-placement-threading.test.ts": 1876, + "src/main/ipc/pty-spawn-runtime-handle-binding.test.ts": 7101, + "src/main/ipc/pty-spawn-timing.test.ts": 44, + "src/main/ipc/pty-ssh-stop-verdict.test.ts": 1949, + "src/main/ipc/pty-startup-barrier-and-listing.test.ts": 2134, + "src/main/ipc/pty-startup-swap-window-presence.test.ts": 1999, + "src/main/ipc/pty-windows-shell-selection.test.ts": 2114, + "src/main/ipc/pty-write-ipc-validation.test.ts": 2198, + "src/main/ipc/pty-wsl-cwd-validation.test.ts": 2138, + "src/main/ipc/pty/configured-terminal-shell-args.test.ts": 37, + "src/main/ipc/pty/daemon-late-exit.test.ts": 5049, + "src/main/ipc/pty/delivery/attached-pty-size.test.ts": 43, + "src/main/ipc/pty/delivery/interactive-typing-burst.test.ts": 38, + "src/main/ipc/pty/host-env/codex-hook-reconcile-spawn.test.ts": 424, + "src/main/ipc/pty/host-env/codex-terminal-clipboard.test.ts": 43, + "src/main/ipc/pty/host-env/omp-launch-environment.test.ts": 103, + "src/main/ipc/pty/host-env/opencode-hook-installation.test.ts": 674, + "src/main/ipc/pty/host-env/spawn-cwd-wsl-move.test.ts": 357, + "src/main/ipc/pty/ipc/claude-old-terminal.test.ts": 41, + "src/main/ipc/pty/ipc/codex-shared-server.test.ts": 86, + "src/main/ipc/pty/ipc/leaf-move.test.ts": 640, + "src/main/ipc/pty/ipc/spawn-commit-run-facts.test.ts": 1163, + "src/main/ipc/pty/ipc/spawn-options-codex-isolation.test.ts": 1015, + "src/main/ipc/pty/ipc/spawn-options-spawn-intent.test.ts": 979, + "src/main/ipc/pty/ipc/spawn-options-terminal-shell-args.test.ts": 972, + "src/main/ipc/pty/ipc/spawn-push-target-materialization-real-git.test.ts": 1079, + "src/main/ipc/pty/ipc/spawn-push-target-materialization.test.ts": 49, + "src/main/ipc/pty/ipc/spawn-reattach-size-cache.test.ts": 1063, + "src/main/ipc/pty/ipc/terminal-view-attributes-ipc.test.ts": 430, + "src/main/ipc/pty/ipc/write-input-acceptance.test.ts": 56, + "src/main/ipc/pty/ipc/write-input-chunk-yield.test.ts": 58, + "src/main/ipc/pty/ipc/write-input-user-input.test.ts": 68, + "src/main/ipc/pty/managed-data-account-spawn-environment.test.ts": 1169, + "src/main/ipc/pty/opencode-legacy-spawn-deletions.test.ts": 1041, + "src/main/ipc/pty/pane/join-pane-spawn.test.ts": 1000, + "src/main/ipc/pty/pane/launch-authority.test.ts": 30, + "src/main/ipc/pty/pane/pane-owner-replacement.test.ts": 1557, + "src/main/ipc/pty/pane/stable-pane-absence-death-certificate.test.ts": 4590, + "src/main/ipc/pty/pane/stable-pane-absence-respawn.test.ts": 1106, + "src/main/ipc/pty/provider/registry-color-query-reply-colors.test.ts": 554, + "src/main/ipc/pty/provider/registry-host-routing.test.ts": 1006, + "src/main/ipc/pty/register-headless-runtime.test.ts": 35, + "src/main/ipc/pty/register-without-renderer.test.ts": 4896, + "src/main/ipc/pty/runtime/queried-host-kinds.test.ts": 447, + "src/main/ipc/pty/runtime/spawn-commit-pty-size.test.ts": 792, + "src/main/ipc/pty/runtime/spawn-commit-run-facts.test.ts": 1071, + "src/main/ipc/pty/runtime/spawn-preflight-requested-shell.test.ts": 2152, + "src/main/ipc/pty/spawn-options-agent-workspace-trust.test.ts": 1000, + "src/main/ipc/qoder-desktop-execution-host.test.ts": 1987, + "src/main/ipc/qoder-independent-renderer-spawn-review.test.ts": 1923, + "src/main/ipc/rate-limits.test.ts": 31, + "src/main/ipc/readdir-error-diagnostics.test.ts": 39, + "src/main/ipc/register-core-handlers/register-core-handlers.test.ts": 758, + "src/main/ipc/registered-worktree-root-ownership.test.ts": 659, + "src/main/ipc/registered-worktree-root-refresh.test.ts": 321, + "src/main/ipc/registered-worktree-root-wsl-runtime.test.ts": 192, + "src/main/ipc/registered-worktree-roots-scoped-invalidation.test.ts": 239, + "src/main/ipc/remote-watcher-event-batch.test.ts": 56, + "src/main/ipc/renderer-lifetime-abort.test.ts": 47, + "src/main/ipc/renderer-shutdown-checkpoint.test.ts": 433, + "src/main/ipc/renderer-terminal-serializer-readiness.test.ts": 40, + "src/main/ipc/repos-add-linked-worktree.test.ts": 1287, + "src/main/ipc/repos-create.test.ts": 1160, + "src/main/ipc/repos-execution-host-catalog.test.ts": 1289, + "src/main/ipc/repos-local-add-and-project-setup.test.ts": 1353, + "src/main/ipc/repos-local-clone-lifecycle.test.ts": 1536, + "src/main/ipc/repos-nested-import.test.ts": 1432, + "src/main/ipc/repos-nested-scan.test.ts": 1111, + "src/main/ipc/repos-picker.test.ts": 1058, + "src/main/ipc/repos-remote-base-ref-queries.test.ts": 1422, + "src/main/ipc/repos-remote-client-events.test.ts": 1818, + "src/main/ipc/repos-remote-git-username.test.ts": 1278, + "src/main/ipc/repos-sparse-presets.test.ts": 1309, + "src/main/ipc/repos/folder-workspace-attachment-handlers.test.ts": 1735, + "src/main/ipc/repos/project-host-setup-wsl-move-fence.test.ts": 2051, + "src/main/ipc/repos/repo-creation-git-availability.test.ts": 272, + "src/main/ipc/repos/repo-ipc-wsl-move-fence.test.ts": 1840, + "src/main/ipc/runtime-agent-launch-capability.test.ts": 2779, + "src/main/ipc/runtime-environment-browser-client-host-handler.test.ts": 762, + "src/main/ipc/runtime-environment-capability-evidence.test.ts": 38, + "src/main/ipc/runtime-environment-cli-removal-routing.test.ts": 806, + "src/main/ipc/runtime-environment-diagnostics-broadcast.test.ts": 32, + "src/main/ipc/runtime-environment-federated-read-routing.test.ts": 730, + "src/main/ipc/runtime-environment-federated-read-transport.bench.test.ts": 310, + "src/main/ipc/runtime-environment-host-key.test.ts": 257, + "src/main/ipc/runtime-environment-managed-tunnel.test.ts": 518, + "src/main/ipc/runtime-environment-mobile-relay-hosts.test.ts": 479, + "src/main/ipc/runtime-environment-preference.test.ts": 456, + "src/main/ipc/runtime-environment-removal-cleanup.test.ts": 215, + "src/main/ipc/runtime-environment-removal-storage.test.ts": 1294, + "src/main/ipc/runtime-environment-request-connections.test.ts": 2310, + "src/main/ipc/runtime-environment-revision-guard.test.ts": 35, + "src/main/ipc/runtime-environment-session-reconcile.test.ts": 750, + "src/main/ipc/runtime-environment-status-connection.test.ts": 1202, + "src/main/ipc/runtime-environment-status-recovery.test.ts": 855, + "src/main/ipc/runtime-environment-status-retirement.test.ts": 905, + "src/main/ipc/runtime-environment-subscription-identity.test.ts": 931, + "src/main/ipc/runtime-environment-subscription-layout-recording.test.ts": 851, + "src/main/ipc/runtime-environment-support-routing.test.ts": 843, + "src/main/ipc/runtime-environment-transport-generation.test.ts": 31, + "src/main/ipc/runtime-environment-transport-routing-tailscale-hint.test.ts": 2039, + "src/main/ipc/runtime-environment-unreachable-hint.test.ts": 225, + "src/main/ipc/runtime-environments-call-routing.test.ts": 2173, + "src/main/ipc/runtime-environments-capability-cache.test.ts": 1268, + "src/main/ipc/runtime-environments-pairing.test.ts": 1923, + "src/main/ipc/runtime-environments-status-diagnostics.test.ts": 1104, + "src/main/ipc/runtime-environments-subscription-lifecycle.test.ts": 1561, + "src/main/ipc/runtime-environments-subscription-routing.test.ts": 1154, + "src/main/ipc/runtime-environments-subscription-teardown.test.ts": 1024, + "src/main/ipc/runtime-import-limits.test.ts": 37, + "src/main/ipc/runtime-ssh-access-handlers.test.ts": 235, + "src/main/ipc/runtime-subscribe-lifecycle.test.ts": 3297, + "src/main/ipc/runtime-upload-file-stream.test.ts": 154, + "src/main/ipc/runtime-upload-slice-boundaries.test.ts": 402, + "src/main/ipc/runtime-upload-temp-sweep.test.ts": 1037, + "src/main/ipc/runtime-watcher-disposal-owners.test.ts": 46, + "src/main/ipc/runtime-watcher-pending-assignment.test.ts": 202, + "src/main/ipc/runtime-watcher-pool-shutdown.test.ts": 73, + "src/main/ipc/runtime-watcher-process-pool.test.ts": 132, + "src/main/ipc/runtime.test.ts": 2838, + "src/main/ipc/sender-scoped-request-cancellation.test.ts": 54, + "src/main/ipc/session-close-terminal-surface.test.ts": 397, + "src/main/ipc/session.test.ts": 470, + "src/main/ipc/settings-delayed-response.test.ts": 2558, + "src/main/ipc/settings.test.ts": 1101, + "src/main/ipc/shallow-watch-delivery-probe-lifetime.test.ts": 103, + "src/main/ipc/shallow-watch-delivery-probe.test.ts": 65, + "src/main/ipc/shell-wsl-owner.test.ts": 131, + "src/main/ipc/shell.test.ts": 253, + "src/main/ipc/skill-cloud-install-ipc-schemas.test.ts": 244, + "src/main/ipc/skill-cloud-ipc-handlers.test.ts": 285, + "src/main/ipc/skill-install-management-ipc-handlers.test.ts": 257, + "src/main/ipc/skill-install-progress-ipc.test.ts": 43, + "src/main/ipc/skill-ipc-main-window.test.ts": 42, + "src/main/ipc/skills.test.ts": 980, + "src/main/ipc/source-control-ai-linked-issue.test.ts": 46, + "src/main/ipc/sparse-preset-attribution.test.ts": 41, + "src/main/ipc/speech-worker-exit-lifetime.test.ts": 662, + "src/main/ipc/speech.test.ts": 63, + "src/main/ipc/ssh-app-shutdown.test.ts": 1656, + "src/main/ipc/ssh-browse.test.ts": 772, + "src/main/ipc/ssh-connect-decision-cancellation.test.ts": 1348, + "src/main/ipc/ssh-credential-cancel.test.ts": 1371, + "src/main/ipc/ssh-disconnect-cancellation.test.ts": 1441, + "src/main/ipc/ssh-handler-reregistration.test.ts": 1334, + "src/main/ipc/ssh-host-server-decision-failure.test.ts": 107, + "src/main/ipc/ssh-host-server-on-connect-wiring.test.ts": 2114, + "src/main/ipc/ssh-host-sleep-reconnect.test.ts": 37, + "src/main/ipc/ssh-managed-host-port-forward-lifecycle.test.ts": 516, + "src/main/ipc/ssh-managed-server-unlink-broadcast.test.ts": 288, + "src/main/ipc/ssh-passphrase.test.ts": 251, + "src/main/ipc/ssh-port-forward-restore.test.ts": 95, + "src/main/ipc/ssh-state-broadcast-fanout.test.ts": 1828, + "src/main/ipc/ssh-target-crud-handlers-ownership.test.ts": 1478, + "src/main/ipc/ssh-target-registry.test.ts": 3921, + "src/main/ipc/ssh-test-connection-probe-registry.test.ts": 38, + "src/main/ipc/ssh-test-probe-presence.test.ts": 45, + "src/main/ipc/ssh.test.ts": 2105, + "src/main/ipc/telemetry.test.ts": 320, + "src/main/ipc/terminal-git-credential-guard.test.ts": 87, + "src/main/ipc/terminal-preview-output-stream.test.ts": 54, + "src/main/ipc/terminal-preview.test.ts": 228, + "src/main/ipc/terminal-render-desync-evidence.test.ts": 70, + "src/main/ipc/terminal-startup-color-query-replies.test.ts": 316, + "src/main/ipc/tui-agent-detection-commands.test.ts": 44, + "src/main/ipc/ui.test.ts": 198, + "src/main/ipc/usage-provider-handlers-readiness.test.ts": 372, + "src/main/ipc/usage-provider-handlers.test.ts": 35, + "src/main/ipc/watched-worktree-catalog-notification.test.ts": 48, + "src/main/ipc/watcher-event-root-path-rewrite.test.ts": 48, + "src/main/ipc/watcher-removal-gate.test.ts": 37, + "src/main/ipc/workspace-cleanup-activity.test.ts": 59, + "src/main/ipc/workspace-cleanup-broad-scan.test.ts": 629, + "src/main/ipc/workspace-cleanup-execution-host-routing.test.ts": 614, + "src/main/ipc/workspace-cleanup-local-git-routing.test.ts": 640, + "src/main/ipc/workspace-cleanup-snapshot-ipc.test.ts": 77, + "src/main/ipc/workspace-cleanup.test.ts": 2673, + "src/main/ipc/workspace-create-error-classifier.test.ts": 45, + "src/main/ipc/workspace-ports.test.ts": 69, + "src/main/ipc/workspace-space.test.ts": 47, + "src/main/ipc/worktree-base-directory-change-collector.test.ts": 48, + "src/main/ipc/worktree-base-directory-event-filter.test.ts": 48, + "src/main/ipc/worktree-base-directory-notifications.test.ts": 72, + "src/main/ipc/worktree-base-directory-poller-marker-fanout.test.ts": 171, + "src/main/ipc/worktree-base-directory-poller.test.ts": 2613, + "src/main/ipc/worktree-base-directory-watch-targets.test.ts": 593, + "src/main/ipc/worktree-base-directory-watcher.test.ts": 411, + "src/main/ipc/worktree-change-invalidators.test.ts": 28, + "src/main/ipc/worktree-create-lineage.test.ts": 966, + "src/main/ipc/worktree-folder-rename-target.test.ts": 76, + "src/main/ipc/worktree-git-common-polling.test.ts": 1028, + "src/main/ipc/worktree-git-common-watch-reconciliation.test.ts": 99, + "src/main/ipc/worktree-git-common-watch.test.ts": 2624, + "src/main/ipc/worktree-git-status-attribution-real-git.test.ts": 1802, + "src/main/ipc/worktree-git-status-ref-watch.test.ts": 55, + "src/main/ipc/worktree-head-identity-cached-paths.test.ts": 46, + "src/main/ipc/worktree-head-identity-reader-concurrency.test.ts": 64, + "src/main/ipc/worktree-head-identity-reader-incremental.test.ts": 157, + "src/main/ipc/worktree-head-identity-reader.test.ts": 65, + "src/main/ipc/worktree-head-identity-refresh.test.ts": 68, + "src/main/ipc/worktree-head-identity-retirement.test.ts": 80, + "src/main/ipc/worktree-include-copy-budget.test.ts": 122, + "src/main/ipc/worktree-logic-created-agent.test.ts": 102, + "src/main/ipc/worktree-logic-wsl.test.ts": 95, + "src/main/ipc/worktree-logic.test.ts": 203, + "src/main/ipc/worktree-metadata-merge.test.ts": 42, + "src/main/ipc/worktree-path-deduplication.test.ts": 63, + "src/main/ipc/worktree-push-target-cleanup.test.ts": 639, + "src/main/ipc/worktree-push-target-reconciliation-real-git.test.ts": 950, + "src/main/ipc/worktree-push-target-reconciliation.test.ts": 600, + "src/main/ipc/worktree-push-target-refspec-migration.test.ts": 212, + "src/main/ipc/worktree-push-target-refspec-real-git.test.ts": 1849, + "src/main/ipc/worktree-push-target-remote-scan.test.ts": 570, + "src/main/ipc/worktree-push-target-setup.test.ts": 568, + "src/main/ipc/worktree-remote-push-target-materialization.test.ts": 890, + "src/main/ipc/worktree-symlink-reconciliation.real.test.ts": 369, + "src/main/ipc/worktree-symlinks.test.ts": 147, + "src/main/ipc/worktree-watcher-removal-binding.test.ts": 28, + "src/main/ipc/worktrees-authoritative-local-metadata-pruning.test.ts": 1874, + "src/main/ipc/worktrees-automation-create-terminals.test.ts": 1366, + "src/main/ipc/worktrees-background-removal.test.ts": 2079, + "src/main/ipc/worktrees-canonical-ssh-listing.test.ts": 1171, + "src/main/ipc/worktrees-create-event-timing.test.ts": 1341, + "src/main/ipc/worktrees-create-metadata-persistence.test.ts": 1185, + "src/main/ipc/worktrees-delete-pty-teardown.test.ts": 1295, "src/main/ipc/worktrees-detected-scan-cache.test.ts": 2006, - "src/main/ipc/worktrees-discovery-metadata-backfill.test.ts": 1324, - "src/main/ipc/worktrees-existing-branch-checkout.test.ts": 1158, - "src/main/ipc/worktrees-failed-removal.test.ts": 1242, - "src/main/ipc/worktrees-forget-local.test.ts": 1316, - "src/main/ipc/worktrees-issue-command-overrides.test.ts": 1354, - "src/main/ipc/worktrees-lineage-hydration.test.ts": 1297, - "src/main/ipc/worktrees-listing-fallback-rows.test.ts": 1487, - "src/main/ipc/worktrees-local-base-ref-resolution.test.ts": 1454, - "src/main/ipc/worktrees-local-create-flow.test.ts": 1392, - "src/main/ipc/worktrees-mutation-scan-generation-order.test.ts": 1353, - "src/main/ipc/worktrees-orphan-directory-cleanup.test.ts": 1334, - "src/main/ipc/worktrees-removal-recovery.test.ts": 1321, - "src/main/ipc/worktrees-remove-archive-hooks.test.ts": 1873, - "src/main/ipc/worktrees-remove-preflight.test.ts": 1662, - "src/main/ipc/worktrees-setup-launch-sparse-checkout.test.ts": 1390, - "src/main/ipc/worktrees-ssh-pr-head-fetch.test.ts": 1459, - "src/main/ipc/worktrees-ssh-repo-owner-resolution.test.ts": 1671, - "src/main/ipc/worktrees-windows.test.ts": 1489, - "src/main/ipc/worktrees-wsl-runtime-routing.test.ts": 1166, - "src/main/ipc/worktrees/listing/detected-provider-listing-catalog-version.test.ts": 337, - "src/main/ipc/worktrees/listing/detected-provider-listing-meta-index.test.ts": 1057, - "src/main/ipc/worktrees/listing/detected-provider-listing-overtaken-scan.test.ts": 390, - "src/main/ipc/worktrees/listing/detected-scan-failure-authority.test.ts": 1030, - "src/main/ipc/worktrees/listing/detected-worktree-classification.test.ts": 161, - "src/main/ipc/worktrees/listing/detected-worktree-scan-hygiene-gate.test.ts": 93, - "src/main/ipc/worktrees/listing/detected-worktree-scan-listing-distro.test.ts": 559, - "src/main/ipc/worktrees/listing/detected-worktree-scan-superseded.test.ts": 68, - "src/main/ipc/worktrees/listing/register-sparse-checkout-cache-invalidation.test.ts": 197, - "src/main/ipc/zcode-plan-credentials.test.ts": 58, - "src/main/issue-command-ignore.test.ts": 620, - "src/main/jcode/daemon-prewarm.test.ts": 54, - "src/main/jcode/hook-command-quoting.test.ts": 54, - "src/main/jcode/hook-config.test.ts": 80, - "src/main/jcode/hook-gate-script.test.ts": 118, - "src/main/jcode/hook-service.test.ts": 114, - "src/main/jira/adf-markdown.test.ts": 57, - "src/main/jira/attachment-image-cache-generation.test.ts": 63, - "src/main/jira/attachment-image-cache.test.ts": 76, - "src/main/jira/attachment-images.test.ts": 80, - "src/main/jira/client.test.ts": 285, - "src/main/jira/issues.test.ts": 170, - "src/main/jira/jira-issue-mutations.test.ts": 100, - "src/main/jira/jira-issue-summary-timeout.test.ts": 80, - "src/main/jira/jira-search-abort.test.ts": 94, - "src/main/jsonl-rpc/agent-connection.test.ts": 224, - "src/main/jsonl-rpc/peer.test.ts": 223, - "src/main/jsonl-rpc/timeline-final-tail.test.ts": 523, - "src/main/jsonl-rpc/timeline-lane.test.ts": 471, - "src/main/keybindings/keybinding-file.test.ts": 185, - "src/main/keybindings/keybinding-service.test.ts": 138, - "src/main/kimi/hook-service-config-delete-race.test.ts": 90, - "src/main/kimi/hook-service-mode-read-failure.test.ts": 86, - "src/main/kimi/hook-service.test.ts": 93, - "src/main/kimi/kimi-hook-config-toml.test.ts": 109, - "src/main/kimi/kimi-runtime-home.test.ts": 66, - "src/main/kiro/hook-service.test.ts": 152, - "src/main/lib/html-to-pdf.test.ts": 79, - "src/main/lib/unread-response-body.test.ts": 60, - "src/main/line-editor-ready-output-scanner.test.ts": 49, - "src/main/linear/client.test.ts": 174, - "src/main/linear/issue-context-client.test.ts": 86, - "src/main/linear/issue-context-current.test.ts": 58, - "src/main/linear/issue-context-errors.test.ts": 45, - "src/main/linear/issue-context-includes.test.ts": 92, - "src/main/linear/issue-context-inline-media.test.ts": 85, - "src/main/linear/issue-context-relations.test.ts": 58, - "src/main/linear/issue-context.test.ts": 77, - "src/main/linear/issue-list-filter.test.ts": 55, - "src/main/linear/issue-relation-write.test.ts": 74, - "src/main/linear/issue-search-summary.test.ts": 45, - "src/main/linear/issues.test.ts": 133, - "src/main/linear/linear-description-images.test.ts": 163, - "src/main/linear/linear-issue-detail-deadline.test.ts": 225, - "src/main/linear/linear-workspace-registry.test.ts": 52, - "src/main/linear/mappers.test.ts": 43, - "src/main/linear/mcp-issue-list-pagination.test.ts": 100, - "src/main/linear/mcp-issue-list.test.ts": 142, - "src/main/linear/projects.test.ts": 394, - "src/main/linear/teams.test.ts": 85, - "src/main/linux-lid-sleep-assertion.test.ts": 95, - "src/main/linux-package-install-command.test.ts": 104, - "src/main/linux-package-install-diagnostic.test.ts": 71, - "src/main/linux-package-update-recovery.test.ts": 226, + "src/main/ipc/worktrees-discovery-metadata-backfill.test.ts": 1239, + "src/main/ipc/worktrees-existing-branch-checkout.test.ts": 1381, + "src/main/ipc/worktrees-failed-removal.test.ts": 1468, + "src/main/ipc/worktrees-forget-local.test.ts": 1309, + "src/main/ipc/worktrees-issue-command-overrides.test.ts": 1268, + "src/main/ipc/worktrees-lineage-hydration.test.ts": 1042, + "src/main/ipc/worktrees-listing-fallback-rows.test.ts": 1370, + "src/main/ipc/worktrees-local-base-ref-resolution.test.ts": 1335, + "src/main/ipc/worktrees-local-create-flow.test.ts": 1294, + "src/main/ipc/worktrees-mutation-scan-generation-order.test.ts": 1257, + "src/main/ipc/worktrees-orphan-directory-cleanup.test.ts": 1246, + "src/main/ipc/worktrees-removal-recovery.test.ts": 1450, + "src/main/ipc/worktrees-remove-archive-hooks.test.ts": 1437, + "src/main/ipc/worktrees-remove-preflight.test.ts": 1681, + "src/main/ipc/worktrees-setup-launch-sparse-checkout.test.ts": 1334, + "src/main/ipc/worktrees-ssh-pr-head-fetch.test.ts": 1358, + "src/main/ipc/worktrees-ssh-repo-owner-resolution.test.ts": 1715, + "src/main/ipc/worktrees-windows.test.ts": 1397, + "src/main/ipc/worktrees-wsl-move-fence.test.ts": 1516, + "src/main/ipc/worktrees-wsl-runtime-routing.test.ts": 1489, + "src/main/ipc/worktrees/listing/detected-provider-listing-catalog-version.test.ts": 721, + "src/main/ipc/worktrees/listing/detected-provider-listing-meta-index.test.ts": 1062, + "src/main/ipc/worktrees/listing/detected-provider-listing-overtaken-scan.test.ts": 641, + "src/main/ipc/worktrees/listing/detected-scan-failure-authority.test.ts": 1132, + "src/main/ipc/worktrees/listing/detected-worktree-classification.test.ts": 173, + "src/main/ipc/worktrees/listing/detected-worktree-scan-hygiene-gate.test.ts": 80, + "src/main/ipc/worktrees/listing/detected-worktree-scan-listing-distro.test.ts": 797, + "src/main/ipc/worktrees/listing/detected-worktree-scan-superseded.test.ts": 47, + "src/main/ipc/worktrees/listing/register-sparse-checkout-cache-invalidation.test.ts": 171, + "src/main/ipc/wsl-guest-windows-drive-path.test.ts": 56, + "src/main/ipc/wsl-host-gate-off.test.ts": 1108, + "src/main/ipc/zcode-plan-credentials.test.ts": 49, + "src/main/issue-command-ignore.test.ts": 508, + "src/main/jcode/daemon-prewarm.test.ts": 63, + "src/main/jcode/hook-command-quoting.test.ts": 31, + "src/main/jcode/hook-config.test.ts": 74, + "src/main/jcode/hook-gate-script.test.ts": 139, + "src/main/jcode/hook-service.test.ts": 113, + "src/main/jira/adf-link-mapping.test.ts": 88, + "src/main/jira/adf-markdown.test.ts": 52, + "src/main/jira/attachment-image-cache-generation.test.ts": 42, + "src/main/jira/attachment-image-cache.test.ts": 56, + "src/main/jira/attachment-image-read.test.ts": 1076, + "src/main/jira/attachment-image-references.test.ts": 74, + "src/main/jira/attachment-images.test.ts": 79, + "src/main/jira/attachment-legacy-cancellation.test.ts": 116, + "src/main/jira/authenticated-request-image-limit.test.ts": 31, + "src/main/jira/client-scoped-token.test.ts": 419, + "src/main/jira/client.test.ts": 342, + "src/main/jira/issues.test.ts": 186, + "src/main/jira/jira-agent-read.test.ts": 97, + "src/main/jira/jira-agile-field-search.test.ts": 89, + "src/main/jira/jira-agile-fields.test.ts": 56, + "src/main/jira/jira-board-column-names.test.ts": 37, + "src/main/jira/jira-cloud-planning-composition.test.ts": 70, + "src/main/jira/jira-issue-mutations.test.ts": 93, + "src/main/jira/jira-issue-summary-timeout.test.ts": 84, + "src/main/jira/jira-preset-filters.test.ts": 105, + "src/main/jira/jira-search-abort.test.ts": 86, + "src/main/jira/jira-transition-required-fields.test.ts": 70, + "src/main/jsonl-rpc/agent-connection.test.ts": 262, + "src/main/jsonl-rpc/peer.test.ts": 207, + "src/main/jsonl-rpc/timeline-final-tail.test.ts": 643, + "src/main/jsonl-rpc/timeline-lane.test.ts": 740, + "src/main/keybindings/keybinding-file.test.ts": 182, + "src/main/keybindings/keybinding-service.test.ts": 169, + "src/main/kimi/hook-service-config-delete-race.test.ts": 66, + "src/main/kimi/hook-service-mode-read-failure.test.ts": 83, + "src/main/kimi/hook-service.test.ts": 109, + "src/main/kimi/kimi-hook-config-toml.test.ts": 138, + "src/main/kimi/kimi-runtime-home.test.ts": 58, + "src/main/kiro/hook-service.test.ts": 156, + "src/main/lib/html-to-pdf.test.ts": 92, + "src/main/lib/unread-response-body.test.ts": 45, + "src/main/line-editor-ready-output-scanner.test.ts": 30, + "src/main/linear/client.test.ts": 291, + "src/main/linear/issue-context-client.test.ts": 74, + "src/main/linear/issue-context-current.test.ts": 31, + "src/main/linear/issue-context-errors.test.ts": 32, + "src/main/linear/issue-context-includes.test.ts": 74, + "src/main/linear/issue-context-inline-media.test.ts": 69, + "src/main/linear/issue-context-relations.test.ts": 46, + "src/main/linear/issue-context.test.ts": 57, + "src/main/linear/issue-list-filter.test.ts": 39, + "src/main/linear/issue-relation-write.test.ts": 56, + "src/main/linear/issue-search-summary.test.ts": 24, + "src/main/linear/issues.test.ts": 137, + "src/main/linear/linear-description-images.test.ts": 155, + "src/main/linear/linear-issue-detail-deadline.test.ts": 201, + "src/main/linear/linear-workspace-registry.test.ts": 48, + "src/main/linear/mappers.test.ts": 34, + "src/main/linear/mcp-issue-list-pagination.test.ts": 96, + "src/main/linear/mcp-issue-list.test.ts": 112, + "src/main/linear/projects.test.ts": 382, + "src/main/linear/teams.test.ts": 66, + "src/main/linux-lid-sleep-assertion.test.ts": 83, + "src/main/linux-package-install-command.test.ts": 83, + "src/main/linux-package-install-diagnostic.test.ts": 54, + "src/main/linux-package-update-recovery.test.ts": 214, "src/main/linux-update-package-type.test.ts": 140, - "src/main/local-builds/local-build-candidate.test.ts": 178, - "src/main/local-builds/local-build-compatibility-contract.test.ts": 295, - "src/main/local-builds/local-build-feed-server.test.ts": 88, - "src/main/local-downloaded-folder-promotion.test.ts": 163, - "src/main/local-worktree-filesystem-wsl-banner.wsl.test.ts": 81, - "src/main/local-worktree-filesystem.test.ts": 368, - "src/main/local-worktree-metadata-prune-gate.test.ts": 56, - "src/main/local-worktree-path-presence.test.ts": 70, - "src/main/local-worktree-removal-recovery-live.test.ts": 385, - "src/main/local-worktree-removal-recovery.test.ts": 131, - "src/main/localhost-worktree-label-proxy.test.ts": 126, - "src/main/macos-full-disk-access-status.test.ts": 49, - "src/main/macos-keychain/generic-password.test.ts": 57, - "src/main/macos-press-and-hold-default.defaults-domain.test.ts": 68, - "src/main/macos-press-and-hold-default.test.ts": 95, - "src/main/macos-system-sleep-assertion.test.ts": 71, - "src/main/macos-tcc-prompt-notice.test.ts": 96, - "src/main/macos-tcc-prompt-watch.test.ts": 94, - "src/main/macos-tcc-reset.test.ts": 67, - "src/main/macos-update-running-instances.integration.test.ts": 47, - "src/main/macos-update-running-instances.test.ts": 69, - "src/main/managed-data-accounts/launch-environment.test.ts": 342, - "src/main/managed-data-accounts/original-environment.test.ts": 316, - "src/main/managed-data-accounts/profile-removal-recovery.test.ts": 629, - "src/main/managed-data-accounts/service-enrollment-persistence.test.ts": 397, - "src/main/managed-data-accounts/service.test.ts": 1418, - "src/main/media/media-preview-protocol.test.ts": 82, - "src/main/media/media-range-response.test.ts": 78, - "src/main/memory/collector-windows-sweep.test.ts": 699, - "src/main/memory/collector.test.ts": 815, - "src/main/memory/host-memory.test.ts": 78, - "src/main/memory/hydrate-local-pty-registry-folder-workspaces.test.ts": 129, - "src/main/memory/hydrate-local-pty-registry.test.ts": 757, - "src/main/memory/memory-snapshot-buckets.test.ts": 104, - "src/main/memory/process-memory-metric.test.ts": 47, - "src/main/memory/verified-local-folder-workspaces.test.ts": 63, - "src/main/memory/windows-process-sample-parsing.test.ts": 105, - "src/main/menu/gpu-acceleration-about-panel.test.ts": 53, - "src/main/menu/register-app-menu.test.ts": 176, - "src/main/mimo/hook-service.test.ts": 120, - "src/main/minimax/minimax-api-key-store.test.ts": 107, - "src/main/minimax/minimax-cookie-store.test.ts": 113, - "src/main/muse-usage/scanner.test.ts": 218, - "src/main/muse-usage/snapshot-rollups.test.ts": 62, - "src/main/muse-usage/store-snapshot-filtering.test.ts": 705, - "src/main/muse/hook-config-json.test.ts": 81, - "src/main/muse/hook-service.test.ts": 135, - "src/main/muse/hook-settings.test.ts": 107, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-discovery-freshness.test.ts": 119, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-entry.test.ts": 52, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-learned-default.test.ts": 342, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-prewarm.test.ts": 325, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-probe-runner.test.ts": 1592, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-saved-only-read.test.ts": 87, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-service.test.ts": 299, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-store.test.ts": 609, - "src/main/native-chat/agent-model-catalog/agent-model-catalog-unavailable.test.ts": 103, - "src/main/native-chat/agent-model-catalog/agent-project-model-override.test.ts": 203, - "src/main/native-chat/agent-session-attachments/agent-session-attachment-claims.test.ts": 637, - "src/main/native-chat/agent-session-attachments/agent-session-attachment-store.test.ts": 129, - "src/main/native-chat/agent-session-attachments/agent-session-attachment-sweep.test.ts": 664, - "src/main/native-chat/agent-session-journal/command-receipt-schema.test.ts": 1186, - "src/main/native-chat/agent-session-journal/command-receipt-table.test.ts": 994, - "src/main/native-chat/agent-session-journal/command-receipt-transaction.test.ts": 595, - "src/main/native-chat/agent-session-journal/journal-approval-subject-kinds.test.ts": 215, - "src/main/native-chat/agent-session-journal/journal-atomic-terminal-settlement.test.ts": 1761, - "src/main/native-chat/agent-session-journal/journal-closed-sets.test.ts": 242, - "src/main/native-chat/agent-session-journal/journal-crash-boundary.test.ts": 626, - "src/main/native-chat/agent-session-journal/journal-cursor.test.ts": 58, - "src/main/native-chat/agent-session-journal/journal-damage.test.ts": 696, - "src/main/native-chat/agent-session-journal/journal-database.test.ts": 465, - "src/main/native-chat/agent-session-journal/journal-dispatch-answered-turn.test.ts": 366, - "src/main/native-chat/agent-session-journal/journal-dispatch-observation.test.ts": 57, - "src/main/native-chat/agent-session-journal/journal-dispatch-reason-bound.test.ts": 626, - "src/main/native-chat/agent-session-journal/journal-dispatch-settlement.test.ts": 261, - "src/main/native-chat/agent-session-journal/journal-epoch-replacement.test.ts": 596, - "src/main/native-chat/agent-session-journal/journal-host-database.test.ts": 889, - "src/main/native-chat/agent-session-journal/journal-item-identity.test.ts": 76, - "src/main/native-chat/agent-session-journal/journal-legacy-import-source-bound.test.ts": 281, - "src/main/native-chat/agent-session-journal/journal-legacy-import-tool-pairing.test.ts": 479, - "src/main/native-chat/agent-session-journal/journal-legacy-import.test.ts": 1038, - "src/main/native-chat/agent-session-journal/journal-lifecycle-batch-appender.test.ts": 781, - "src/main/native-chat/agent-session-journal/journal-lifecycle-batch-partition.test.ts": 13771, - "src/main/native-chat/agent-session-journal/journal-open-failure.test.ts": 384, - "src/main/native-chat/agent-session-journal/journal-payload-bounds-retention.test.ts": 337, - "src/main/native-chat/agent-session-journal/journal-producer-inheritance.test.ts": 561, - "src/main/native-chat/agent-session-journal/journal-queued-rejection-batch.test.ts": 513, - "src/main/native-chat/agent-session-journal/journal-reducer-producer-linkage.test.ts": 343, - "src/main/native-chat/agent-session-journal/journal-reducer-rejection-fact.test.ts": 339, - "src/main/native-chat/agent-session-journal/journal-reducer.test.ts": 439, - "src/main/native-chat/agent-session-journal/journal-rejected-message-placement.test.ts": 335, - "src/main/native-chat/agent-session-journal/journal-restart-reconciliation.test.ts": 846, - "src/main/native-chat/agent-session-journal/journal-row-context-usage.test.ts": 690, - "src/main/native-chat/agent-session-journal/journal-row-schema-version.test.ts": 524, - "src/main/native-chat/agent-session-journal/journal-row-schema.test.ts": 461, - "src/main/native-chat/agent-session-journal/journal-row-writer.test.ts": 605, - "src/main/native-chat/agent-session-journal/journal-stop-and-resume-rows.test.ts": 548, + "src/main/local-builds/local-build-candidate.test.ts": 152, + "src/main/local-builds/local-build-compatibility-contract.test.ts": 257, + "src/main/local-builds/local-build-feed-server.test.ts": 67, + "src/main/local-worktree-filesystem-wsl-banner.wsl.test.ts": 68, + "src/main/local-worktree-filesystem.test.ts": 367, + "src/main/local-worktree-metadata-prune-gate.test.ts": 42, + "src/main/local-worktree-path-presence.test.ts": 54, + "src/main/local-worktree-removal-recovery-live.test.ts": 310, + "src/main/local-worktree-removal-recovery.test.ts": 121, + "src/main/localhost-worktree-label-proxy.test.ts": 109, + "src/main/macos-full-disk-access-status.test.ts": 44, + "src/main/macos-keychain/generic-password.test.ts": 35, + "src/main/macos-press-and-hold-default.defaults-domain.test.ts": 59, + "src/main/macos-press-and-hold-default.test.ts": 114, + "src/main/macos-system-sleep-assertion.test.ts": 57, + "src/main/macos-tcc-prompt-notice.test.ts": 62, + "src/main/macos-tcc-prompt-watch.test.ts": 63, + "src/main/macos-tcc-reset.test.ts": 38, + "src/main/macos-update-running-instances.integration.test.ts": 53, + "src/main/macos-update-running-instances.test.ts": 45, + "src/main/managed-data-accounts/launch-environment.test.ts": 346, + "src/main/managed-data-accounts/original-environment.test.ts": 298, + "src/main/managed-data-accounts/profile-removal-recovery.test.ts": 648, + "src/main/managed-data-accounts/service-enrollment-persistence.test.ts": 1001, + "src/main/managed-data-accounts/service.test.ts": 1235, + "src/main/media/media-preview-protocol.test.ts": 90, + "src/main/media/media-range-response.test.ts": 72, + "src/main/memory/collector-windows-sweep.test.ts": 940, + "src/main/memory/collector.test.ts": 873, + "src/main/memory/host-memory.test.ts": 73, + "src/main/memory/hydrate-local-pty-registry-folder-workspaces.test.ts": 327, + "src/main/memory/hydrate-local-pty-registry.test.ts": 712, + "src/main/memory/memory-snapshot-buckets.test.ts": 89, + "src/main/memory/process-memory-metric.test.ts": 42, + "src/main/memory/verified-local-folder-workspaces.test.ts": 54, + "src/main/memory/windows-process-sample-parsing.test.ts": 104, + "src/main/menu/gpu-acceleration-about-panel.test.ts": 38, + "src/main/menu/register-app-menu.test.ts": 156, + "src/main/mimo/hook-service.test.ts": 106, + "src/main/minimax/minimax-api-key-store.test.ts": 120, + "src/main/minimax/minimax-cookie-store.test.ts": 114, + "src/main/muse-usage/scanner.test.ts": 202, + "src/main/muse-usage/snapshot-rollups.test.ts": 35, + "src/main/muse-usage/store-snapshot-filtering.test.ts": 821, + "src/main/muse/hook-config-json.test.ts": 64, + "src/main/muse/hook-service.test.ts": 115, + "src/main/muse/hook-settings.test.ts": 77, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-discovery-freshness.test.ts": 117, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-entry.test.ts": 27, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-learned-default.test.ts": 320, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-prewarm.test.ts": 332, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-probe-runner.test.ts": 1582, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-saved-only-read.test.ts": 72, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-service.test.ts": 296, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-store.test.ts": 598, + "src/main/native-chat/agent-model-catalog/agent-model-catalog-unavailable.test.ts": 95, + "src/main/native-chat/agent-model-catalog/agent-project-model-override.test.ts": 152, + "src/main/native-chat/agent-session-attachments/agent-session-attachment-claims.test.ts": 698, + "src/main/native-chat/agent-session-attachments/agent-session-attachment-store.test.ts": 126, + "src/main/native-chat/agent-session-attachments/agent-session-attachment-sweep.test.ts": 647, + "src/main/native-chat/agent-session-journal/command-receipt-schema.test.ts": 1268, + "src/main/native-chat/agent-session-journal/command-receipt-table.test.ts": 1511, + "src/main/native-chat/agent-session-journal/command-receipt-transaction.test.ts": 636, + "src/main/native-chat/agent-session-journal/journal-approval-subject-kinds.test.ts": 245, + "src/main/native-chat/agent-session-journal/journal-atomic-terminal-settlement.test.ts": 1440, + "src/main/native-chat/agent-session-journal/journal-closed-sets.test.ts": 270, + "src/main/native-chat/agent-session-journal/journal-crash-boundary.test.ts": 762, + "src/main/native-chat/agent-session-journal/journal-cursor.test.ts": 49, + "src/main/native-chat/agent-session-journal/journal-damage.test.ts": 818, + "src/main/native-chat/agent-session-journal/journal-database.test.ts": 545, + "src/main/native-chat/agent-session-journal/journal-dispatch-answered-turn.test.ts": 370, + "src/main/native-chat/agent-session-journal/journal-dispatch-legacy-reason.test.ts": 352, + "src/main/native-chat/agent-session-journal/journal-dispatch-observation.test.ts": 39, + "src/main/native-chat/agent-session-journal/journal-dispatch-reason-bound.test.ts": 542, + "src/main/native-chat/agent-session-journal/journal-dispatch-settlement.test.ts": 307, + "src/main/native-chat/agent-session-journal/journal-epoch-replacement.test.ts": 645, + "src/main/native-chat/agent-session-journal/journal-host-database.test.ts": 900, + "src/main/native-chat/agent-session-journal/journal-item-appender.test.ts": 331, + "src/main/native-chat/agent-session-journal/journal-item-identity.test.ts": 66, + "src/main/native-chat/agent-session-journal/journal-item-provenance.test.ts": 561, + "src/main/native-chat/agent-session-journal/journal-legacy-import-source-bound.test.ts": 693, + "src/main/native-chat/agent-session-journal/journal-legacy-import-tool-pairing.test.ts": 775, + "src/main/native-chat/agent-session-journal/journal-legacy-import.test.ts": 1244, + "src/main/native-chat/agent-session-journal/journal-lifecycle-batch-appender.test.ts": 675, + "src/main/native-chat/agent-session-journal/journal-lifecycle-batch-partition.test.ts": 1177, + "src/main/native-chat/agent-session-journal/journal-open-failure.test.ts": 481, + "src/main/native-chat/agent-session-journal/journal-payload-bounds-retention.test.ts": 345, + "src/main/native-chat/agent-session-journal/journal-pi-context-import.test.ts": 808, + "src/main/native-chat/agent-session-journal/journal-producer-inheritance.test.ts": 512, + "src/main/native-chat/agent-session-journal/journal-queued-rejection-batch.test.ts": 640, + "src/main/native-chat/agent-session-journal/journal-reducer-producer-linkage.test.ts": 390, + "src/main/native-chat/agent-session-journal/journal-reducer-rejection-fact.test.ts": 432, + "src/main/native-chat/agent-session-journal/journal-reducer.test.ts": 406, + "src/main/native-chat/agent-session-journal/journal-rejected-message-placement.test.ts": 377, + "src/main/native-chat/agent-session-journal/journal-restart-reconciliation.test.ts": 811, + "src/main/native-chat/agent-session-journal/journal-row-context-usage.test.ts": 601, + "src/main/native-chat/agent-session-journal/journal-row-schema-version.test.ts": 572, + "src/main/native-chat/agent-session-journal/journal-row-schema.test.ts": 435, + "src/main/native-chat/agent-session-journal/journal-row-writer.test.ts": 607, + "src/main/native-chat/agent-session-journal/journal-stop-and-resume-rows.test.ts": 514, "src/main/native-chat/agent-session-journal/journal-stop-turn-end.test.ts": 349, - "src/main/native-chat/agent-session-journal/journal-store-schema.test.ts": 667, - "src/main/native-chat/agent-session-journal/journal-store.test.ts": 753, - "src/main/native-chat/agent-session-journal/journal-streaming-replay.test.ts": 1439, - "src/main/native-chat/agent-session-journal/journal-subagent-liveness.test.ts": 658, - "src/main/native-chat/agent-session-journal/journal-submission-positions.test.ts": 588, - "src/main/native-chat/agent-session-journal/journal-submission-queued-link.test.ts": 547, - "src/main/native-chat/agent-session-journal/journal-terminal-settlement-validation.test.ts": 749, - "src/main/native-chat/agent-session-journal/journal-turn-scope.test.ts": 407, - "src/main/native-chat/agent-session-journal/journal-unknown-content.test.ts": 753, - "src/main/native-chat/agent-session-journal/journal-unknown-row-kind.test.ts": 652, - "src/main/native-chat/agent-session-journal/journal-unsent-send-hold.test.ts": 742, - "src/main/native-chat/agent-session-journal/journal-write-queue.test.ts": 425, - "src/main/native-chat/agent-session-journal/queued-message-bookkeeping-failure.test.ts": 663, - "src/main/native-chat/agent-session-journal/queued-message-context-clear.test.ts": 62, - "src/main/native-chat/agent-session-journal/queued-message-delivered-echo.test.ts": 638, - "src/main/native-chat/agent-session-journal/queued-message-edit-leases.test.ts": 303, - "src/main/native-chat/agent-session-journal/queued-message-pause.test.ts": 808, - "src/main/native-chat/agent-session-journal/queued-message-store.test.ts": 1498, - "src/main/native-chat/agent-session-journal/queued-message-text-update.test.ts": 346, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-attribution.test.ts": 577, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-earlier-turn-end.test.ts": 460, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-held-rows.test.ts": 483, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-items.test.ts": 840, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-order.test.ts": 704, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-refusal.test.ts": 802, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-restart.test.ts": 484, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-rows.test.ts": 857, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-session.test.ts": 773, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-stop.test.ts": 1106, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-streams.test.ts": 577, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-tool-ends.test.ts": 642, - "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-turns.test.ts": 502, - "src/main/native-chat/agent-session-timeline/provider-timeline-identity.test.ts": 64, - "src/main/native-chat/agent-session-wire/agent-session-conversation-outline.test.ts": 468, - "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer-overflow-prefix.test.ts": 101, - "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer-protected.test.ts": 79, - "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.test.ts": 74, - "src/main/native-chat/agent-session-wire/agent-session-empty-delta-retention.test.ts": 440, - "src/main/native-chat/agent-session-wire/agent-session-history-byte-accounting.test.ts": 917, - "src/main/native-chat/agent-session-wire/agent-session-history-conversation-window.test.ts": 1364, - "src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts": 1062, - "src/main/native-chat/agent-session-wire/agent-session-history-page-grouping-parity.test.ts": 229, - "src/main/native-chat/agent-session-wire/agent-session-history-page-scaling.test.ts": 78, - "src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts": 1739, - "src/main/native-chat/agent-session-wire/agent-session-stop-note-projection.test.ts": 853, - "src/main/native-chat/agent-session-wire/provider-frame-activity.test.ts": 51, - "src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts": 91, - "src/main/native-chat/agent-session-wire/provider-turn-activity-routing.test.ts": 464, - "src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts": 2781, - "src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-options.test.ts": 1351, - "src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-runtime-stamp.test.ts": 891, - "src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router-registry.test.ts": 355, - "src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.test.ts": 391, - "src/main/native-chat/agent-session-wire/structured-agent-session-adapter.test.ts": 233, - "src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts": 1223, - "src/main/native-chat/agent-session-wire/structured-agent-session-append-delivery.test.ts": 1257, - "src/main/native-chat/agent-session-wire/structured-agent-session-at-rest-commands.test.ts": 1891, - "src/main/native-chat/agent-session-wire/structured-agent-session-attach-reconciliation.test.ts": 624, - "src/main/native-chat/agent-session-wire/structured-agent-session-attachment-admission.test.ts": 1373, - "src/main/native-chat/agent-session-wire/structured-agent-session-background-task-channel.test.ts": 349, - "src/main/native-chat/agent-session-wire/structured-agent-session-child-work-selection.test.ts": 987, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-compact-stop.test.ts": 1595, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-echo-working.test.ts": 1289, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-hung-start-stop.test.ts": 1404, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-opening-send-hold.test.ts": 4766, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-option-queue.test.ts": 1212, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-queued-stop.test.ts": 4570, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-root-exit.test.ts": 1253, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-ends-session.test.ts": 3995, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-exit-ends-record.test.ts": 3545, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-note-end.test.ts": 1652, - "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-turn-end.test.ts": 4314, - "src/main/native-chat/agent-session-wire/structured-agent-session-clear-queued-command.test.ts": 1453, - "src/main/native-chat/agent-session-wire/structured-agent-session-clear-rewind.test.ts": 2367, - "src/main/native-chat/agent-session-wire/structured-agent-session-close-aborts-start.test.ts": 941, - "src/main/native-chat/agent-session-wire/structured-agent-session-close-verdict.test.ts": 1320, - "src/main/native-chat/agent-session-wire/structured-agent-session-codex-stop-note-end.test.ts": 1347, - "src/main/native-chat/agent-session-wire/structured-agent-session-codex-stop-row.test.ts": 3905, - "src/main/native-chat/agent-session-wire/structured-agent-session-command-publication.test.ts": 1042, - "src/main/native-chat/agent-session-wire/structured-agent-session-command-readers.test.ts": 459, - "src/main/native-chat/agent-session-wire/structured-agent-session-conversation-close.test.ts": 1873, - "src/main/native-chat/agent-session-wire/structured-agent-session-conversation-stop.test.ts": 3529, - "src/main/native-chat/agent-session-wire/structured-agent-session-conversations.test.ts": 746, - "src/main/native-chat/agent-session-wire/structured-agent-session-crash-mid-start.test.ts": 1635, - "src/main/native-chat/agent-session-wire/structured-agent-session-crash-turn-end.test.ts": 992, - "src/main/native-chat/agent-session-wire/structured-agent-session-create-adoption.test.ts": 1433, - "src/main/native-chat/agent-session-wire/structured-agent-session-create.test.ts": 1584, - "src/main/native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement.test.ts": 843, - "src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.test.ts": 201, - "src/main/native-chat/agent-session-wire/structured-agent-session-drivability.test.ts": 985, - "src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.test.ts": 876, - "src/main/native-chat/agent-session-wire/structured-agent-session-eviction.test.ts": 434, - "src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-owner-verdict.test.ts": 1316, - "src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-sink-release.test.ts": 1186, - "src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts": 223, - "src/main/native-chat/agent-session-wire/structured-agent-session-final-tail-reservation.test.ts": 40, - "src/main/native-chat/agent-session-wire/structured-agent-session-forget-status.test.ts": 1449, - "src/main/native-chat/agent-session-wire/structured-agent-session-grouped-prompt.test.ts": 1196, - "src/main/native-chat/agent-session-wire/structured-agent-session-held-child-lease-renewal.test.ts": 580, - "src/main/native-chat/agent-session-wire/structured-agent-session-host-held-child-renewal.test.ts": 915, - "src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.test.ts": 699, - "src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.test.ts": 473, - "src/main/native-chat/agent-session-wire/structured-agent-session-host-test-abandon.test.ts": 1168, - "src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts": 1737, - "src/main/native-chat/agent-session-wire/structured-agent-session-idle-sweep.test.ts": 2591, - "src/main/native-chat/agent-session-wire/structured-agent-session-interrupted-continuation.test.ts": 3775, - "src/main/native-chat/agent-session-wire/structured-agent-session-journal-corruption.test.ts": 957, - "src/main/native-chat/agent-session-wire/structured-agent-session-late-proof-tool-ends.test.ts": 486, - "src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts": 1481, - "src/main/native-chat/agent-session-wire/structured-agent-session-launch-env.test.ts": 355, - "src/main/native-chat/agent-session-wire/structured-agent-session-launch-send-after-create.test.ts": 1218, - "src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.test.ts": 706, - "src/main/native-chat/agent-session-wire/structured-agent-session-legacy-handoff-record.test.ts": 1500, - "src/main/native-chat/agent-session-wire/structured-agent-session-live-turn-window.test.ts": 1117, - "src/main/native-chat/agent-session-wire/structured-agent-session-logger.test.ts": 190, - "src/main/native-chat/agent-session-wire/structured-agent-session-main-agent-working-agreement.test.ts": 1078, - "src/main/native-chat/agent-session-wire/structured-agent-session-mutation-context.test.ts": 1098, - "src/main/native-chat/agent-session-wire/structured-agent-session-newer-content-host.test.ts": 1244, - "src/main/native-chat/agent-session-wire/structured-agent-session-operation-settlement.test.ts": 1201, - "src/main/native-chat/agent-session-wire/structured-agent-session-operation-start-refusal.test.ts": 519, - "src/main/native-chat/agent-session-wire/structured-agent-session-option-settlement.test.ts": 1248, - "src/main/native-chat/agent-session-wire/structured-agent-session-options-read.test.ts": 365, - "src/main/native-chat/agent-session-wire/structured-agent-session-orca-stop-row-readers.test.ts": 198, - "src/main/native-chat/agent-session-wire/structured-agent-session-owed-work-release.test.ts": 3318, - "src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts": 917, - "src/main/native-chat/agent-session-wire/structured-agent-session-processless-reservation.test.ts": 1125, - "src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel-repeat.test.ts": 782, - "src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel.test.ts": 914, - "src/main/native-chat/agent-session-wire/structured-agent-session-provider-child-record.test.ts": 3062, - "src/main/native-chat/agent-session-wire/structured-agent-session-provider-restore.test.ts": 1138, - "src/main/native-chat/agent-session-wire/structured-agent-session-provider-started-catalog.test.ts": 758, - "src/main/native-chat/agent-session-wire/structured-agent-session-provider-started.test.ts": 1956, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-card-holds.test.ts": 5719, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-command.test.ts": 1912, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-compact.test.ts": 5019, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-edit.test.ts": 1908, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-gate.test.ts": 1540, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.test.ts": 5700, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-newer-orca.test.ts": 1096, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-pause-lift.test.ts": 6327, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-published-bytes.test.ts": 957, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-quit.test.ts": 1221, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-repeat-press.test.ts": 1544, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-stop-row.test.ts": 4507, - "src/main/native-chat/agent-session-wire/structured-agent-session-queued-withdrawn-draft.test.ts": 1569, - "src/main/native-chat/agent-session-wire/structured-agent-session-read-restore.test.ts": 628, - "src/main/native-chat/agent-session-wire/structured-agent-session-readable-restorer.test.ts": 543, - "src/main/native-chat/agent-session-wire/structured-agent-session-reasoning-sweep.test.ts": 461, - "src/main/native-chat/agent-session-wire/structured-agent-session-recovered-turn-clock.test.ts": 1364, - "src/main/native-chat/agent-session-wire/structured-agent-session-recovery-exits.test.ts": 1017, - "src/main/native-chat/agent-session-wire/structured-agent-session-recovery-resolution.test.ts": 4093, - "src/main/native-chat/agent-session-wire/structured-agent-session-refusal-details.test.ts": 360, - "src/main/native-chat/agent-session-wire/structured-agent-session-refusal-retry.test.ts": 1144, - "src/main/native-chat/agent-session-wire/structured-agent-session-registered-definition.test.ts": 314, - "src/main/native-chat/agent-session-wire/structured-agent-session-registry.test.ts": 47, - "src/main/native-chat/agent-session-wire/structured-agent-session-repeat-press.test.ts": 999, - "src/main/native-chat/agent-session-wire/structured-agent-session-repeated-stop.test.ts": 1072, - "src/main/native-chat/agent-session-wire/structured-agent-session-replaced-server-turn.test.ts": 1207, - "src/main/native-chat/agent-session-wire/structured-agent-session-resend-answer.test.ts": 2268, - "src/main/native-chat/agent-session-wire/structured-agent-session-resend-ledger-refusal.test.ts": 1204, - "src/main/native-chat/agent-session-wire/structured-agent-session-reservation-directory.test.ts": 235, - "src/main/native-chat/agent-session-wire/structured-agent-session-resolved-append.test.ts": 576, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-audience.test.ts": 1011, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-cap.test.ts": 686, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-child-work.test.ts": 1345, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-context-clear.test.ts": 399, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation-wait.test.ts": 242, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.test.ts": 518, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-failure-filing.test.ts": 1326, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-after-reattach.test.ts": 1335, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-endings.test.ts": 1285, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-newer-orca.test.ts": 1212, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-withdrawal.test.ts": 2691, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-ownership.test.ts": 2611, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-provider-output.test.ts": 2206, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-reconcile.test.ts": 269, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-recovery-output.test.ts": 1503, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-restore-gate.test.ts": 67, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-restore.test.ts": 649, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-progress.test.ts": 1297, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.ts": 632, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-status-publication.test.ts": 1232, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-stream-duration.test.ts": 1339, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-teardown.test.ts": 1379, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-undrivable.test.ts": 959, - "src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts": 434, - "src/main/native-chat/agent-session-wire/structured-agent-session-reveal.test.ts": 427, - "src/main/native-chat/agent-session-wire/structured-agent-session-rewind-at-rest.test.ts": 1079, - "src/main/native-chat/agent-session-wire/structured-agent-session-rewind-live-send.test.ts": 1097, - "src/main/native-chat/agent-session-wire/structured-agent-session-rewind.test.ts": 2657, - "src/main/native-chat/agent-session-wire/structured-agent-session-send-block.test.ts": 280, - "src/main/native-chat/agent-session-wire/structured-agent-session-send-idempotency.test.ts": 796, - "src/main/native-chat/agent-session-wire/structured-agent-session-send-open-stale-turn.test.ts": 1279, - "src/main/native-chat/agent-session-wire/structured-agent-session-send-preparation.test.ts": 3251, - "src/main/native-chat/agent-session-wire/structured-agent-session-send-restarts-failed-start.test.ts": 2021, - "src/main/native-chat/agent-session-wire/structured-agent-session-send-settlement.test.ts": 337, - "src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts": 2285, - "src/main/native-chat/agent-session-wire/structured-agent-session-settled-attach-retry.test.ts": 1282, - "src/main/native-chat/agent-session-wire/structured-agent-session-shown-work.test.ts": 478, - "src/main/native-chat/agent-session-wire/structured-agent-session-shutdown-cut-row.test.ts": 1364, - "src/main/native-chat/agent-session-wire/structured-agent-session-signed-out-child.test.ts": 1452, - "src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-open.test.ts": 1043, - "src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts": 1011, - "src/main/native-chat/agent-session-wire/structured-agent-session-start-failure-writer.test.ts": 1029, - "src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-host.test.ts": 3748, - "src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-registrations.test.ts": 2188, - "src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt.test.ts": 64, - "src/main/native-chat/agent-session-wire/structured-agent-session-startup-failure-exit.test.ts": 392, - "src/main/native-chat/agent-session-wire/structured-agent-session-startup-reconcile-failure.test.ts": 1817, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child-work.test.ts": 570, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child.test.ts": 454, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-children.test.ts": 643, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-clock.test.ts": 787, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-conversation-name.test.ts": 595, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-launch-directory.test.ts": 649, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-stopping.test.ts": 524, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.test.ts": 904, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-first-input-identity.test.ts": 470, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-ownership.test.ts": 251, - "src/main/native-chat/agent-session-wire/structured-agent-session-status-reentry.test.ts": 1002, - "src/main/native-chat/agent-session-wire/structured-agent-session-steer-while-stopping.test.ts": 1701, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-aborts-start.test.ts": 890, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-binding.test.ts": 2907, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-entries.test.ts": 5698, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-restart.test.ts": 1979, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event.test.ts": 2801, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-never-waits.test.ts": 1766, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-note-opened-turn.test.ts": 701, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-note-withdrawn-send.test.ts": 513, - "src/main/native-chat/agent-session-wire/structured-agent-session-stop-settle-binding.test.ts": 1319, - "src/main/native-chat/agent-session-wire/structured-agent-session-stopping-journal.test.ts": 457, - "src/main/native-chat/agent-session-wire/structured-agent-session-stopping.test.ts": 3973, - "src/main/native-chat/agent-session-wire/structured-agent-session-subagent-recency.test.ts": 1318, - "src/main/native-chat/agent-session-wire/structured-agent-session-subscriber-background-tasks.test.ts": 643, - "src/main/native-chat/agent-session-wire/structured-agent-session-subscriber-journal-retention.test.ts": 5915, - "src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts": 1011, - "src/main/native-chat/agent-session-wire/structured-agent-session-surface-lifetime.test.ts": 2430, - "src/main/native-chat/agent-session-wire/structured-agent-session-task-queue.test.ts": 98, - "src/main/native-chat/agent-session-wire/structured-agent-session-thread-goal.test.ts": 847, - "src/main/native-chat/agent-session-wire/structured-agent-session-transition.test.ts": 829, - "src/main/native-chat/agent-session-wire/structured-agent-session-turn-completion-feed.test.ts": 328, - "src/main/native-chat/agent-session-wire/structured-agent-session-turns.test.ts": 1030, - "src/main/native-chat/agent-session-wire/structured-agent-session-unanswered-dispatch-release.test.ts": 276, - "src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.ts": 480, - "src/main/native-chat/agent-session-wire/structured-agent-session-unopened-send-withdrawal.test.ts": 195, - "src/main/native-chat/agent-session-wire/structured-agent-session-unopened-tab.test.ts": 968, - "src/main/native-chat/agent-session-wire/structured-agent-session-unrun-send-hold.test.ts": 1496, - "src/main/native-chat/agent-session-wire/structured-agent-session-unsent-send-hold.test.ts": 5952, - "src/main/native-chat/agent-session-wire/structured-agent-session-view-start-after-failed-start.test.ts": 1354, - "src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts": 1592, - "src/main/native-chat/agent-session-wire/structured-agent-session-wire-admission.test.ts": 922, - "src/main/native-chat/agent-session-wire/structured-conversation-command-admission.test.ts": 326, - "src/main/native-chat/agent-session-wire/structured-conversation-command-child-work.test.ts": 939, - "src/main/native-chat/agent-session-wire/structured-conversation-command-outcome.test.ts": 196, - "src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts": 1502, - "src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts": 2102, - "src/main/native-chat/agent-session-wire/structured-rewind-journal-body.test.ts": 273, - "src/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.test.ts": 234, - "src/main/native-chat/agent-session-wire/unhandled-provider-frame.test.ts": 332, - "src/main/native-chat/host-readable-transcript-path-fs-gate.test.ts": 359, - "src/main/native-chat/host-readable-transcript-path.test.ts": 160, - "src/main/native-chat/native-chat-file-provenance.test.ts": 575, - "src/main/native-chat/native-chat-inline-visuals-setting.test.ts": 536, - "src/main/native-chat/native-chat-visual-file-read.test.ts": 85, - "src/main/native-chat/native-chat-visuals-delivery.test.ts": 343, - "src/main/native-chat/native-chat-visuals-sweep.test.ts": 78, - "src/main/native-chat/session-file-resolver-claude-roots.test.ts": 142, - "src/main/native-chat/session-file-resolver-codex-roots.test.ts": 136, - "src/main/native-chat/session-file-resolver-wsl-scan-gate.test.ts": 141, - "src/main/native-chat/session-file-resolver-wsl.test.ts": 139, - "src/main/native-chat/session-file-resolver.test.ts": 217, - "src/main/native-chat/ssh-transcript-path.test.ts": 40, - "src/main/native-chat/structured-agent-command-resolution.test.ts": 86, - "src/main/native-chat/structured-agent-configured-args.test.ts": 48, - "src/main/native-chat/structured-agent-session-create-support.test.ts": 54, - "src/main/native-chat/structured-agent-session-history-adoption.test.ts": 279, - "src/main/native-chat/structured-chat-naming-command-first.test.ts": 1258, - "src/main/native-chat/structured-chat-naming-status-hook.test.ts": 2387, - "src/main/native-chat/structured-chat-naming.test.ts": 225, - "src/main/native-chat/subagent-entry-id-bounds.test.ts": 51, - "src/main/native-chat/subagent-roster-retention.test.ts": 88, - "src/main/native-chat/transcript-fallback-id.test.ts": 52, - "src/main/native-chat/transcript-file-version.test.ts": 67, - "src/main/native-chat/transcript-interruption-message.test.ts": 100, - "src/main/native-chat/transcript-line-decoders-claude-command-output.test.ts": 96, - "src/main/native-chat/transcript-line-decoders-claude-control-bytes.test.ts": 104, - "src/main/native-chat/transcript-line-decoders-claude-image-companion.test.ts": 102, - "src/main/native-chat/transcript-line-decoders-claude-pasted-content.test.ts": 108, + "src/main/native-chat/agent-session-journal/journal-store-schema.test.ts": 590, + "src/main/native-chat/agent-session-journal/journal-store.test.ts": 888, + "src/main/native-chat/agent-session-journal/journal-streaming-replay.test.ts": 1277, + "src/main/native-chat/agent-session-journal/journal-subagent-item.test.ts": 566, + "src/main/native-chat/agent-session-journal/journal-subagent-liveness.test.ts": 636, + "src/main/native-chat/agent-session-journal/journal-submission-positions.test.ts": 591, + "src/main/native-chat/agent-session-journal/journal-submission-queued-link.test.ts": 591, + "src/main/native-chat/agent-session-journal/journal-terminal-settlement-validation.test.ts": 1021, + "src/main/native-chat/agent-session-journal/journal-turn-scope.test.ts": 413, + "src/main/native-chat/agent-session-journal/journal-unknown-content.test.ts": 849, + "src/main/native-chat/agent-session-journal/journal-unknown-row-kind.test.ts": 637, + "src/main/native-chat/agent-session-journal/journal-unsent-send-hold.test.ts": 974, + "src/main/native-chat/agent-session-journal/journal-write-queue.test.ts": 687, + "src/main/native-chat/agent-session-journal/queued-message-bookkeeping-failure.test.ts": 676, + "src/main/native-chat/agent-session-journal/queued-message-context-clear.test.ts": 58, + "src/main/native-chat/agent-session-journal/queued-message-delivered-echo.test.ts": 757, + "src/main/native-chat/agent-session-journal/queued-message-edit-leases.test.ts": 317, + "src/main/native-chat/agent-session-journal/queued-message-pause.test.ts": 881, + "src/main/native-chat/agent-session-journal/queued-message-store.test.ts": 1156, + "src/main/native-chat/agent-session-journal/queued-message-text-update.test.ts": 325, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-attribution.test.ts": 802, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-earlier-turn-end.test.ts": 594, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-held-rows.test.ts": 686, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-items.test.ts": 675, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-order.test.ts": 879, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-refusal.test.ts": 933, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-restart.test.ts": 532, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-rows.test.ts": 1147, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-session.test.ts": 761, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-stop.test.ts": 1711, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-streams.test.ts": 568, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-tool-ends.test.ts": 758, + "src/main/native-chat/agent-session-timeline/provider-timeline-assembler-turns.test.ts": 657, + "src/main/native-chat/agent-session-timeline/provider-timeline-identity.test.ts": 35, + "src/main/native-chat/agent-session-wire/agent-session-bounded-frames.test.ts": 3078, + "src/main/native-chat/agent-session-wire/agent-session-conversation-outline.test.ts": 512, + "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer-overflow-prefix.test.ts": 104, + "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer-protected.test.ts": 69, + "src/main/native-chat/agent-session-wire/agent-session-delta-coalescer.test.ts": 44, + "src/main/native-chat/agent-session-wire/agent-session-empty-delta-retention.test.ts": 420, + "src/main/native-chat/agent-session-wire/agent-session-history-byte-accounting.test.ts": 689, + "src/main/native-chat/agent-session-wire/agent-session-history-conversation-window.test.ts": 1608, + "src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts": 954, + "src/main/native-chat/agent-session-wire/agent-session-history-page-grouping-parity.test.ts": 210, + "src/main/native-chat/agent-session-wire/agent-session-history-page-scaling.test.ts": 70, + "src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts": 1878, + "src/main/native-chat/agent-session-wire/agent-session-stop-note-projection.test.ts": 1211, + "src/main/native-chat/agent-session-wire/hook-failure-provider-frame.test.ts": 252, + "src/main/native-chat/agent-session-wire/json-string-shortening.test.ts": 58, + "src/main/native-chat/agent-session-wire/provider-frame-activity.test.ts": 41, + "src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts": 86, + "src/main/native-chat/agent-session-wire/provider-turn-activity-routing.test.ts": 361, + "src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts": 3549, + "src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-options.test.ts": 1536, + "src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-runtime-stamp.test.ts": 1084, + "src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router-registry.test.ts": 378, + "src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.test.ts": 359, + "src/main/native-chat/agent-session-wire/structured-agent-session-adapter.test.ts": 261, + "src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts": 2552, + "src/main/native-chat/agent-session-wire/structured-agent-session-append-delivery.test.ts": 1472, + "src/main/native-chat/agent-session-wire/structured-agent-session-at-rest-commands.test.ts": 2212, + "src/main/native-chat/agent-session-wire/structured-agent-session-attach-reconciliation.test.ts": 703, + "src/main/native-chat/agent-session-wire/structured-agent-session-attachment-admission.test.ts": 1723, + "src/main/native-chat/agent-session-wire/structured-agent-session-background-stop-unreadable.test.ts": 1548, + "src/main/native-chat/agent-session-wire/structured-agent-session-background-task-channel.test.ts": 499, + "src/main/native-chat/agent-session-wire/structured-agent-session-child-work-selection.test.ts": 1063, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-compact-stop.test.ts": 1758, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-echo-working.test.ts": 1791, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-hung-start-stop.test.ts": 1955, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-opening-send-hold.test.ts": 5597, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-option-queue.test.ts": 1723, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-queued-stop.test.ts": 2143, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-root-exit.test.ts": 891, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-ends-session.test.ts": 3972, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-exit-ends-record.test.ts": 3799, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-note-end.test.ts": 2032, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-stop-turn-end.test.ts": 1804, + "src/main/native-chat/agent-session-wire/structured-agent-session-claude-subagent-row.test.ts": 3710, + "src/main/native-chat/agent-session-wire/structured-agent-session-clear-queued-command.test.ts": 1689, + "src/main/native-chat/agent-session-wire/structured-agent-session-clear-rewind.test.ts": 2692, + "src/main/native-chat/agent-session-wire/structured-agent-session-close-aborts-start.test.ts": 1480, + "src/main/native-chat/agent-session-wire/structured-agent-session-close-verdict.test.ts": 1703, + "src/main/native-chat/agent-session-wire/structured-agent-session-codex-stop-note-end.test.ts": 2393, + "src/main/native-chat/agent-session-wire/structured-agent-session-codex-stop-row.test.ts": 4109, + "src/main/native-chat/agent-session-wire/structured-agent-session-codex-version-recovery.test.ts": 1626, + "src/main/native-chat/agent-session-wire/structured-agent-session-command-publication.test.ts": 883, + "src/main/native-chat/agent-session-wire/structured-agent-session-command-readers.test.ts": 497, + "src/main/native-chat/agent-session-wire/structured-agent-session-conversation-close.test.ts": 2355, + "src/main/native-chat/agent-session-wire/structured-agent-session-conversation-stop.test.ts": 4235, + "src/main/native-chat/agent-session-wire/structured-agent-session-conversations.test.ts": 799, + "src/main/native-chat/agent-session-wire/structured-agent-session-crash-mid-start.test.ts": 1852, + "src/main/native-chat/agent-session-wire/structured-agent-session-crash-turn-end.test.ts": 1629, + "src/main/native-chat/agent-session-wire/structured-agent-session-create-adoption.test.ts": 1495, + "src/main/native-chat/agent-session-wire/structured-agent-session-create.test.ts": 2354, + "src/main/native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement.test.ts": 983, + "src/main/native-chat/agent-session-wire/structured-agent-session-delivery-loop.test.ts": 238, + "src/main/native-chat/agent-session-wire/structured-agent-session-drivability.test.ts": 1303, + "src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.test.ts": 798, + "src/main/native-chat/agent-session-wire/structured-agent-session-eviction.test.ts": 459, + "src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-owner-verdict.test.ts": 1604, + "src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-sink-release.test.ts": 1418, + "src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts": 312, + "src/main/native-chat/agent-session-wire/structured-agent-session-final-tail-reservation.test.ts": 35, + "src/main/native-chat/agent-session-wire/structured-agent-session-forget-status.test.ts": 1131, + "src/main/native-chat/agent-session-wire/structured-agent-session-grouped-prompt.test.ts": 1275, + "src/main/native-chat/agent-session-wire/structured-agent-session-held-child-lease-renewal.test.ts": 798, + "src/main/native-chat/agent-session-wire/structured-agent-session-host-held-child-renewal.test.ts": 1317, + "src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.test.ts": 588, + "src/main/native-chat/agent-session-wire/structured-agent-session-host-teardown.test.ts": 589, + "src/main/native-chat/agent-session-wire/structured-agent-session-host-test-abandon.test.ts": 1605, + "src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts": 2537, + "src/main/native-chat/agent-session-wire/structured-agent-session-idle-sweep.test.ts": 2933, + "src/main/native-chat/agent-session-wire/structured-agent-session-interrupted-continuation.test.ts": 1805, + "src/main/native-chat/agent-session-wire/structured-agent-session-journal-corruption.test.ts": 1402, + "src/main/native-chat/agent-session-wire/structured-agent-session-late-proof-tool-ends.test.ts": 707, + "src/main/native-chat/agent-session-wire/structured-agent-session-late-settlement.test.ts": 1845, + "src/main/native-chat/agent-session-wire/structured-agent-session-launch-env.test.ts": 332, + "src/main/native-chat/agent-session-wire/structured-agent-session-launch-permission.test.ts": 313, + "src/main/native-chat/agent-session-wire/structured-agent-session-launch-send-after-create.test.ts": 1249, + "src/main/native-chat/agent-session-wire/structured-agent-session-lease-renewer.test.ts": 774, + "src/main/native-chat/agent-session-wire/structured-agent-session-legacy-handoff-record.test.ts": 1676, + "src/main/native-chat/agent-session-wire/structured-agent-session-live-turn-window.test.ts": 1952, + "src/main/native-chat/agent-session-wire/structured-agent-session-logger.test.ts": 263, + "src/main/native-chat/agent-session-wire/structured-agent-session-main-agent-working-agreement.test.ts": 1594, + "src/main/native-chat/agent-session-wire/structured-agent-session-mutation-context.test.ts": 1320, + "src/main/native-chat/agent-session-wire/structured-agent-session-newer-content-host.test.ts": 1448, + "src/main/native-chat/agent-session-wire/structured-agent-session-operation-settlement.test.ts": 1290, + "src/main/native-chat/agent-session-wire/structured-agent-session-operation-start-refusal.test.ts": 909, + "src/main/native-chat/agent-session-wire/structured-agent-session-option-settlement.test.ts": 1571, + "src/main/native-chat/agent-session-wire/structured-agent-session-options-read.test.ts": 517, + "src/main/native-chat/agent-session-wire/structured-agent-session-orca-stop-row-readers.test.ts": 284, + "src/main/native-chat/agent-session-wire/structured-agent-session-outgrown-child.test.ts": 1833, + "src/main/native-chat/agent-session-wire/structured-agent-session-owed-work-release.test.ts": 3411, + "src/main/native-chat/agent-session-wire/structured-agent-session-permission-mode.test.ts": 2186, + "src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts": 1412, + "src/main/native-chat/agent-session-wire/structured-agent-session-processless-reservation.test.ts": 1227, + "src/main/native-chat/agent-session-wire/structured-agent-session-prompt-answer-receipts.test.ts": 2036, + "src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel-repeat.test.ts": 1049, + "src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel.test.ts": 1033, + "src/main/native-chat/agent-session-wire/structured-agent-session-provider-child-record.test.ts": 3521, + "src/main/native-chat/agent-session-wire/structured-agent-session-provider-restore.test.ts": 1247, + "src/main/native-chat/agent-session-wire/structured-agent-session-provider-started-catalog.test.ts": 1659, + "src/main/native-chat/agent-session-wire/structured-agent-session-provider-started.test.ts": 1846, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-action-receipts.test.ts": 3551, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-card-holds.test.ts": 3891, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-command.test.ts": 2383, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-compact.test.ts": 4502, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-edit.test.ts": 2454, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-gate.test.ts": 1796, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.test.ts": 6375, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-newer-orca.test.ts": 1559, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-pause-lift.test.ts": 6142, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-published-bytes.test.ts": 1468, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-quit.test.ts": 1572, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-repeat-press.test.ts": 1854, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-stop-row.test.ts": 2829, + "src/main/native-chat/agent-session-wire/structured-agent-session-queued-withdrawn-draft.test.ts": 1837, + "src/main/native-chat/agent-session-wire/structured-agent-session-read-restore.test.ts": 589, + "src/main/native-chat/agent-session-wire/structured-agent-session-readable-restorer.test.ts": 575, + "src/main/native-chat/agent-session-wire/structured-agent-session-reasoning-sweep.test.ts": 690, + "src/main/native-chat/agent-session-wire/structured-agent-session-recovered-turn-clock.test.ts": 1211, + "src/main/native-chat/agent-session-wire/structured-agent-session-recovery-exits.test.ts": 1336, + "src/main/native-chat/agent-session-wire/structured-agent-session-recovery-resolution.test.ts": 4135, + "src/main/native-chat/agent-session-wire/structured-agent-session-refusal-details.test.ts": 519, + "src/main/native-chat/agent-session-wire/structured-agent-session-refusal-retry.test.ts": 1373, + "src/main/native-chat/agent-session-wire/structured-agent-session-registered-definition.test.ts": 347, + "src/main/native-chat/agent-session-wire/structured-agent-session-registry.test.ts": 24, + "src/main/native-chat/agent-session-wire/structured-agent-session-repeat-press.test.ts": 1697, + "src/main/native-chat/agent-session-wire/structured-agent-session-repeated-stop.test.ts": 1280, + "src/main/native-chat/agent-session-wire/structured-agent-session-replaced-server-turn.test.ts": 1672, + "src/main/native-chat/agent-session-wire/structured-agent-session-resend-answer.test.ts": 2850, + "src/main/native-chat/agent-session-wire/structured-agent-session-resend-ledger-refusal.test.ts": 1528, + "src/main/native-chat/agent-session-wire/structured-agent-session-reservation-directory.test.ts": 305, + "src/main/native-chat/agent-session-wire/structured-agent-session-resolved-append.test.ts": 593, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-audience.test.ts": 1607, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-cap.test.ts": 787, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-child-work.test.ts": 1341, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-context-clear.test.ts": 469, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation-wait.test.ts": 349, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.test.ts": 541, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-failure-filing.test.ts": 1603, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-after-reattach.test.ts": 1318, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-endings.test.ts": 1421, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-newer-orca.test.ts": 1839, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-withdrawal.test.ts": 3680, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-ownership.test.ts": 3324, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-provider-output.test.ts": 2721, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-reconcile.test.ts": 260, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-recovery-output.test.ts": 1996, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-restore-gate.test.ts": 33, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-restore.test.ts": 614, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-progress.test.ts": 1877, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.ts": 639, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-status-publication.test.ts": 1485, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-stream-duration.test.ts": 1813, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-teardown.test.ts": 1630, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-undrivable.test.ts": 1222, + "src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts": 552, + "src/main/native-chat/agent-session-wire/structured-agent-session-reveal.test.ts": 568, + "src/main/native-chat/agent-session-wire/structured-agent-session-rewind-at-rest.test.ts": 1360, + "src/main/native-chat/agent-session-wire/structured-agent-session-rewind-live-send.test.ts": 1724, + "src/main/native-chat/agent-session-wire/structured-agent-session-rewind.test.ts": 2365, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-block.test.ts": 387, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-idempotency.test.ts": 627, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-open-stale-turn.test.ts": 1669, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-options.test.ts": 2572, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-preparation.test.ts": 3260, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-restarts-failed-start.test.ts": 1922, + "src/main/native-chat/agent-session-wire/structured-agent-session-send-settlement.test.ts": 328, + "src/main/native-chat/agent-session-wire/structured-agent-session-send.test.ts": 2632, + "src/main/native-chat/agent-session-wire/structured-agent-session-settled-attach-retry.test.ts": 1619, + "src/main/native-chat/agent-session-wire/structured-agent-session-shown-work.test.ts": 463, + "src/main/native-chat/agent-session-wire/structured-agent-session-shutdown-cut-row.test.ts": 1597, + "src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-open.test.ts": 1539, + "src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts": 1202, + "src/main/native-chat/agent-session-wire/structured-agent-session-start-failure-writer.test.ts": 1574, + "src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-host.test.ts": 2563, + "src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-registrations.test.ts": 2613, + "src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt.test.ts": 68, + "src/main/native-chat/agent-session-wire/structured-agent-session-startup-failure-exit.test.ts": 461, + "src/main/native-chat/agent-session-wire/structured-agent-session-startup-reconcile-failure.test.ts": 2288, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child-work.test.ts": 617, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child.test.ts": 661, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-children.test.ts": 833, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-clock.test.ts": 633, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-conversation-name.test.ts": 690, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-launch-directory.test.ts": 669, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-stopping.test.ts": 725, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.test.ts": 1201, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-first-input-identity.test.ts": 703, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-ownership.test.ts": 4923, + "src/main/native-chat/agent-session-wire/structured-agent-session-status-reentry.test.ts": 1131, + "src/main/native-chat/agent-session-wire/structured-agent-session-steer-while-stopping.test.ts": 2161, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-aborts-start.test.ts": 1542, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-binding.test.ts": 2987, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-entries.test.ts": 5972, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event-restart.test.ts": 2339, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-event.test.ts": 3184, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-note-opened-turn.test.ts": 718, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-note-withdrawn-send.test.ts": 788, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-receipts.test.ts": 1967, + "src/main/native-chat/agent-session-wire/structured-agent-session-stop-settle-binding.test.ts": 1732, + "src/main/native-chat/agent-session-wire/structured-agent-session-stopped-history.test.ts": 1593, + "src/main/native-chat/agent-session-wire/structured-agent-session-stopping-journal.test.ts": 649, + "src/main/native-chat/agent-session-wire/structured-agent-session-stopping.test.ts": 4697, + "src/main/native-chat/agent-session-wire/structured-agent-session-subagent-recency.test.ts": 1194, + "src/main/native-chat/agent-session-wire/structured-agent-session-subagent-rows.test.ts": 711, + "src/main/native-chat/agent-session-wire/structured-agent-session-subscriber-background-tasks.test.ts": 686, + "src/main/native-chat/agent-session-wire/structured-agent-session-subscriber-journal-retention.test.ts": 5985, + "src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts": 993, + "src/main/native-chat/agent-session-wire/structured-agent-session-surface-lifetime.test.ts": 2991, + "src/main/native-chat/agent-session-wire/structured-agent-session-task-queue.test.ts": 90, + "src/main/native-chat/agent-session-wire/structured-agent-session-thread-goal.test.ts": 757, + "src/main/native-chat/agent-session-wire/structured-agent-session-transition.test.ts": 1084, + "src/main/native-chat/agent-session-wire/structured-agent-session-turn-completion-feed.test.ts": 344, + "src/main/native-chat/agent-session-wire/structured-agent-session-turns.test.ts": 877, + "src/main/native-chat/agent-session-wire/structured-agent-session-unanswered-dispatch-release.test.ts": 278, + "src/main/native-chat/agent-session-wire/structured-agent-session-unexpected-exit.test.ts": 494, + "src/main/native-chat/agent-session-wire/structured-agent-session-unnamed-stop-races.test.ts": 7136, + "src/main/native-chat/agent-session-wire/structured-agent-session-unopened-send-withdrawal.test.ts": 259, + "src/main/native-chat/agent-session-wire/structured-agent-session-unopened-tab.test.ts": 1285, + "src/main/native-chat/agent-session-wire/structured-agent-session-unrun-send-hold.test.ts": 1501, + "src/main/native-chat/agent-session-wire/structured-agent-session-unsent-send-hold.test.ts": 6554, + "src/main/native-chat/agent-session-wire/structured-agent-session-view-start-after-failed-start.test.ts": 1637, + "src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts": 1607, + "src/main/native-chat/agent-session-wire/structured-agent-session-wire-admission.test.ts": 1083, + "src/main/native-chat/agent-session-wire/structured-conversation-command-admission.test.ts": 319, + "src/main/native-chat/agent-session-wire/structured-conversation-command-child-work.test.ts": 1226, + "src/main/native-chat/agent-session-wire/structured-conversation-command-outcome.test.ts": 294, + "src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts": 2238, + "src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts": 1915, + "src/main/native-chat/agent-session-wire/structured-rewind-journal-body.test.ts": 258, + "src/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.test.ts": 342, + "src/main/native-chat/agent-session-wire/unhandled-provider-frame.test.ts": 276, + "src/main/native-chat/host-readable-transcript-path-fs-gate.test.ts": 385, + "src/main/native-chat/host-readable-transcript-path.test.ts": 114, + "src/main/native-chat/native-chat-file-provenance.test.ts": 472, + "src/main/native-chat/native-chat-inline-visuals-setting.test.ts": 760, + "src/main/native-chat/native-chat-visual-file-read.test.ts": 99, + "src/main/native-chat/native-chat-visuals-delivery.test.ts": 634, + "src/main/native-chat/native-chat-visuals-sweep.test.ts": 170, + "src/main/native-chat/session-file-resolver-claude-roots.test.ts": 154, + "src/main/native-chat/session-file-resolver-codex-roots.test.ts": 135, + "src/main/native-chat/session-file-resolver-wsl-scan-gate.test.ts": 120, + "src/main/native-chat/session-file-resolver-wsl.test.ts": 213, + "src/main/native-chat/session-file-resolver.test.ts": 171, + "src/main/native-chat/ssh-transcript-path.test.ts": 41, + "src/main/native-chat/structured-agent-cli-conversation-adoption.test.ts": 1893, + "src/main/native-chat/structured-agent-command-guest-path.test.ts": 54, + "src/main/native-chat/structured-agent-command-resolution.test.ts": 79, + "src/main/native-chat/structured-agent-configured-args.test.ts": 58, + "src/main/native-chat/structured-agent-session-create-support.test.ts": 41, + "src/main/native-chat/structured-agent-session-history-adoption.test.ts": 316, + "src/main/native-chat/structured-chat-naming-command-first.test.ts": 1484, + "src/main/native-chat/structured-chat-naming-status-hook.test.ts": 2515, + "src/main/native-chat/structured-chat-naming.test.ts": 294, + "src/main/native-chat/subagent-entry-id-bounds.test.ts": 26, + "src/main/native-chat/transcript-fallback-id.test.ts": 30, + "src/main/native-chat/transcript-file-version.test.ts": 72, + "src/main/native-chat/transcript-interruption-message.test.ts": 78, + "src/main/native-chat/transcript-line-decoders-claude-command-output.test.ts": 93, + "src/main/native-chat/transcript-line-decoders-claude-control-bytes.test.ts": 74, + "src/main/native-chat/transcript-line-decoders-claude-image-companion.test.ts": 103, + "src/main/native-chat/transcript-line-decoders-claude-pasted-content.test.ts": 103, "src/main/native-chat/transcript-line-decoders-codex-skill-context.test.ts": 97, - "src/main/native-chat/transcript-line-decoders.grok.test.ts": 132, - "src/main/native-chat/transcript-line-decoders.omp.test.ts": 142, - "src/main/native-chat/transcript-opencode-database.test.ts": 107, - "src/main/native-chat/transcript-opencode-read-bounds.test.ts": 589, - "src/main/native-chat/transcript-opencode-semantic-fidelity.test.ts": 2910, - "src/main/native-chat/transcript-opencode-sqlite-query.test.ts": 594, - "src/main/native-chat/transcript-opencode-subscribe.test.ts": 5611, - "src/main/native-chat/transcript-opencode.test.ts": 405, - "src/main/native-chat/transcript-opencode2-sqlite.test.ts": 10052, - "src/main/native-chat/transcript-read-cache-inflight.test.ts": 1054, - "src/main/native-chat/transcript-read-cache-refusal-recovery.test.ts": 580, - "src/main/native-chat/transcript-read-cache-wsl-stall.test.ts": 758, - "src/main/native-chat/transcript-read-cache.test.ts": 652, - "src/main/native-chat/transcript-reader-codex-history-mode.test.ts": 636, - "src/main/native-chat/transcript-reader-wsl-stall.test.ts": 637, - "src/main/native-chat/transcript-reader.test.ts": 607, - "src/main/native-chat/transcript-record-copy-counts.test.ts": 487, - "src/main/native-chat/transcript-stream-lines.test.ts": 103, - "src/main/native-chat/transcript-tail-reader-cancellation.test.ts": 701, - "src/main/native-chat/transcript-tail-reader-wsl-gate.test.ts": 611, - "src/main/native-chat/transcript-tool-result-identity.test.ts": 537, - "src/main/native-chat/transcript-turn-lifecycle.test.ts": 105, - "src/main/native-chat/transcript-watch-engine-wsl-lifecycle.test.ts": 730, - "src/main/native-chat/transcript-watch-error.test.ts": 2634, - "src/main/native-chat/transcript-watch-inplace-drain-race.test.ts": 960, - "src/main/native-chat/transcript-watch-liveness.test.ts": 1270, - "src/main/native-chat/transcript-watch-resolve-poll.test.ts": 784, - "src/main/native-chat/transcript-watch-unflushed-settle.test.ts": 820, - "src/main/native-chat/transcript-watch-unsubscribe-race.test.ts": 624, - "src/main/native-chat/transcript-watch-wsl-exact-path.test.ts": 594, - "src/main/native-chat/transcript-watch-wsl-stall.test.ts": 693, - "src/main/native-chat/transcript-watch.test.ts": 2600, - "src/main/native-chat/transcript-window-tool-attribution.test.ts": 616, - "src/main/native-chat/wsl-session-path-scan.test.ts": 107, - "src/main/native-chat/wsl-transcript-fs-access.test.ts": 161, - "src/main/native-chat/wsl-transcript-fs-gate.test.ts": 585, - "src/main/native-chat/wsl-transcript-fs-process-client.test.ts": 115, - "src/main/native-chat/wsl-transcript-fs-process-operations.test.ts": 55, - "src/main/native-chat/wsl-transcript-fs-route-quarantine.test.ts": 98, - "src/main/native-chat/wsl-transcript-fs-route.test.ts": 44, - "src/main/native-chat/wsl-transcript-running-observer.test.ts": 104, - "src/main/nested-worktree-removal-plan.test.ts": 87, - "src/main/network/electron-proxy-request-guard.test.ts": 381, - "src/main/network/macos-system-resolver-health.test.ts": 71, - "src/main/network/macos-tailscale-dns-diagnostic.test.ts": 58, - "src/main/network/macos-tailscale-dns-probe-admission.test.ts": 225, - "src/main/network/proxy-settings-session.test.ts": 883, - "src/main/network/proxy-settings.test.ts": 120, - "src/main/notebook/notebook-kernel.test.ts": 148, - "src/main/notebook/python-environments.test.ts": 126, - "src/main/notifications/announced-notification-registry.test.ts": 253, - "src/main/notifications/desktop-away-state.test.ts": 62, - "src/main/notifications/notification-delivery-service.test.ts": 87, - "src/main/observability/bundle.test.ts": 487, - "src/main/observability/diagnostic-upload-http.test.ts": 48, - "src/main/observability/instrumentation.test.ts": 235, - "src/main/observability/local-file-sink-memory.test.ts": 135, - "src/main/observability/local-file-sink.test.ts": 81, - "src/main/observability/redactor-environment-lines.test.ts": 217, - "src/main/observability/redactor.test.ts": 86, - "src/main/observability/tracer.test.ts": 91, - "src/main/opencode-usage/cache-read-reproduction.test.ts": 244, - "src/main/opencode-usage/cache-write-accounting.test.ts": 197, - "src/main/opencode-usage/opencode2-session-usage.test.ts": 906, - "src/main/opencode-usage/scanner-windows-data-directory.test.ts": 207, - "src/main/opencode-usage/scanner-wsl-gate.test.ts": 98, - "src/main/opencode-usage/scanner.test.ts": 349, - "src/main/opencode-usage/store.test.ts": 424, - "src/main/opencode/hook-plugin-background-child-completion.test.ts": 375, - "src/main/opencode/hook-plugin-child-attention.test.ts": 759, - "src/main/opencode/hook-plugin-dispose-host.test.ts": 404, - "src/main/opencode/hook-plugin-fail-open-ownership.test.ts": 553, - "src/main/opencode/hook-plugin-legacy-tui-ownership.test.ts": 2096, - "src/main/opencode/hook-plugin-lifecycle-delivery.test.ts": 1063, - "src/main/opencode/hook-plugin-message-part-throttle.test.ts": 266, - "src/main/opencode/hook-plugin-module-contract.test.ts": 682, - "src/main/opencode/hook-plugin-opencode2-session-lineage.test.ts": 1037, - "src/main/opencode/hook-plugin-opencode2-setup.test.ts": 2250, - "src/main/opencode/hook-plugin-opencode2-tui-ownership.test.ts": 1275, - "src/main/opencode/hook-plugin-run-process-session-start.test.ts": 183, - "src/main/opencode/hook-service-invalid-tui-config.test.ts": 111, - "src/main/opencode/hook-service-legacy-acl.test.ts": 251, - "src/main/opencode/hook-service-overlay-confinement.test.ts": 113, - "src/main/opencode/hook-service-startup-refresh.test.ts": 89, - "src/main/opencode/hook-service.test.ts": 350, - "src/main/opencode/legacy-shared-config-dir.test.ts": 54, - "src/main/opencode/opencode-acp-stored-messages.test.ts": 134, - "src/main/opencode/opencode-cli-version.test.ts": 53, - "src/main/opencode/opencode-client-sweep.test.ts": 119, - "src/main/opencode/opencode-credential-backend.test.ts": 79, - "src/main/opencode/opencode-data-directory.test.ts": 50, - "src/main/opencode/opencode-go-api-key-store.test.ts": 97, - "src/main/opencode/opencode-launch-capabilities.test.ts": 67, - "src/main/opencode/opencode-launch-model-config.test.ts": 217, - "src/main/opencode/opencode-launch-model-context-cleanup.test.ts": 4256, - "src/main/opencode/opencode-launch-model-context-supervision.test.ts": 2443, - "src/main/opencode/opencode-launch-model-context.test.ts": 201, - "src/main/opencode/opencode-model-availability.test.ts": 71, - "src/main/opencode/opencode-model-catalog-listing.test.ts": 190, - "src/main/opencode/opencode-model-startup-plan.test.ts": 263, - "src/main/opencode/opencode-model-version-policy.test.ts": 41, - "src/main/opencode/opencode-overlay-manifest.test.ts": 39, - "src/main/opencode/opencode-pty-launch.test.ts": 321, - "src/main/opencode/opencode-session-binder.test.ts": 118, - "src/main/opencode/opencode-startup-prompt-claims.test.ts": 59, - "src/main/opencode/opencode-startup-prompt-installer.test.ts": 181, - "src/main/opencode/opencode-startup-prompt-owner.test.ts": 81, - "src/main/opencode/opencode-startup-prompt-runtime-identity.test.ts": 4479, - "src/main/opencode/opencode-startup-prompt-source.test.ts": 4927, + "src/main/native-chat/transcript-line-decoders.grok.test.ts": 136, + "src/main/native-chat/transcript-line-decoders.omp.test.ts": 133, + "src/main/native-chat/transcript-opencode-database.test.ts": 123, + "src/main/native-chat/transcript-opencode-read-bounds.test.ts": 505, + "src/main/native-chat/transcript-opencode-semantic-fidelity.test.ts": 2041, + "src/main/native-chat/transcript-opencode-sqlite-query.test.ts": 927, + "src/main/native-chat/transcript-opencode-subscribe.test.ts": 5551, + "src/main/native-chat/transcript-opencode.test.ts": 386, + "src/main/native-chat/transcript-opencode2-sqlite.test.ts": 1767, + "src/main/native-chat/transcript-read-cache-inflight.test.ts": 964, + "src/main/native-chat/transcript-read-cache-refusal-recovery.test.ts": 608, + "src/main/native-chat/transcript-read-cache-wsl-stall.test.ts": 399, + "src/main/native-chat/transcript-read-cache.test.ts": 627, + "src/main/native-chat/transcript-reader-codex-history-mode.test.ts": 548, + "src/main/native-chat/transcript-reader-wsl-stall.test.ts": 506, + "src/main/native-chat/transcript-reader.test.ts": 551, + "src/main/native-chat/transcript-record-copy-counts.test.ts": 538, + "src/main/native-chat/transcript-stream-lines.test.ts": 77, + "src/main/native-chat/transcript-tail-reader-cancellation.test.ts": 699, + "src/main/native-chat/transcript-tail-reader-wsl-gate.test.ts": 512, + "src/main/native-chat/transcript-tool-result-identity.test.ts": 523, + "src/main/native-chat/transcript-turn-lifecycle.test.ts": 114, + "src/main/native-chat/transcript-watch-engine-wsl-lifecycle.test.ts": 570, + "src/main/native-chat/transcript-watch-error.test.ts": 2617, + "src/main/native-chat/transcript-watch-inplace-drain-race.test.ts": 939, + "src/main/native-chat/transcript-watch-liveness.test.ts": 1082, + "src/main/native-chat/transcript-watch-resolve-poll.test.ts": 478, + "src/main/native-chat/transcript-watch-unflushed-settle.test.ts": 537, + "src/main/native-chat/transcript-watch-unsubscribe-race.test.ts": 465, + "src/main/native-chat/transcript-watch-wsl-exact-path.test.ts": 462, + "src/main/native-chat/transcript-watch-wsl-stall.test.ts": 579, + "src/main/native-chat/transcript-watch.test.ts": 2407, + "src/main/native-chat/transcript-window-tool-attribution.test.ts": 489, + "src/main/native-chat/transcript-zcode.test.ts": 822, + "src/main/native-chat/wsl-session-path-scan.test.ts": 76, + "src/main/native-chat/wsl-transcript-fs-access.test.ts": 127, + "src/main/native-chat/wsl-transcript-fs-gate.test.ts": 580, + "src/main/native-chat/wsl-transcript-fs-process-client.test.ts": 81, + "src/main/native-chat/wsl-transcript-fs-process-operations.test.ts": 45, + "src/main/native-chat/wsl-transcript-fs-route-quarantine.test.ts": 55, + "src/main/native-chat/wsl-transcript-fs-route.test.ts": 35, + "src/main/native-chat/wsl-transcript-running-observer.test.ts": 94, + "src/main/nested-worktree-removal-plan.test.ts": 101, + "src/main/network/electron-proxy-request-guard.test.ts": 357, + "src/main/network/macos-system-resolver-health.test.ts": 48, + "src/main/network/macos-tailscale-dns-diagnostic.test.ts": 33, + "src/main/network/macos-tailscale-dns-probe-admission.test.ts": 246, + "src/main/network/proxy-settings-session.test.ts": 847, + "src/main/network/proxy-settings.test.ts": 128, + "src/main/node-runtime/appimage-bundled-node-runtime.test.ts": 53, + "src/main/node-runtime/pinned-runtime-materializer-node.test.ts": 136, + "src/main/node-runtime/pinned-runtime-materializer.test.ts": 3143, + "src/main/node-runtime/runtime-archive-download.test.ts": 97, + "src/main/notebook/notebook-kernel.test.ts": 134, + "src/main/notebook/python-environments.test.ts": 68, + "src/main/notifications/announced-notification-registry.test.ts": 276, + "src/main/notifications/desktop-away-state.test.ts": 32, + "src/main/notifications/notification-delivery-service.test.ts": 77, + "src/main/observability/bundle.test.ts": 452, + "src/main/observability/diagnostic-upload-http.test.ts": 36, + "src/main/observability/instrumentation.test.ts": 179, + "src/main/observability/local-file-sink-memory.test.ts": 153, + "src/main/observability/local-file-sink.test.ts": 63, + "src/main/observability/redactor-environment-lines.test.ts": 224, + "src/main/observability/redactor.test.ts": 84, + "src/main/observability/tracer.test.ts": 59, + "src/main/omp/omp-session-cwd-lookup.test.ts": 143, + "src/main/opencode-usage/cache-read-reproduction.test.ts": 170, + "src/main/opencode-usage/cache-write-accounting.test.ts": 219, + "src/main/opencode-usage/opencode2-session-usage.test.ts": 781, + "src/main/opencode-usage/scanner-windows-data-directory.test.ts": 211, + "src/main/opencode-usage/scanner-wsl-gate.test.ts": 61, + "src/main/opencode-usage/scanner.test.ts": 388, + "src/main/opencode-usage/store.test.ts": 435, + "src/main/opencode/hook-plugin-background-child-completion.test.ts": 338, + "src/main/opencode/hook-plugin-child-attention.test.ts": 694, + "src/main/opencode/hook-plugin-dispose-host.test.ts": 417, + "src/main/opencode/hook-plugin-fail-open-ownership.test.ts": 595, + "src/main/opencode/hook-plugin-legacy-tui-ownership.test.ts": 2178, + "src/main/opencode/hook-plugin-lifecycle-delivery.test.ts": 1062, + "src/main/opencode/hook-plugin-message-part-throttle.test.ts": 256, + "src/main/opencode/hook-plugin-module-contract.test.ts": 672, + "src/main/opencode/hook-plugin-opencode2-session-lineage.test.ts": 1083, + "src/main/opencode/hook-plugin-opencode2-setup.test.ts": 2532, + "src/main/opencode/hook-plugin-opencode2-tui-ownership.test.ts": 1667, + "src/main/opencode/hook-plugin-run-process-session-start.test.ts": 173, + "src/main/opencode/hook-service-invalid-tui-config.test.ts": 125, + "src/main/opencode/hook-service-legacy-acl.test.ts": 315, + "src/main/opencode/hook-service-overlay-confinement.test.ts": 80, + "src/main/opencode/hook-service-startup-refresh.test.ts": 114, + "src/main/opencode/hook-service.test.ts": 324, + "src/main/opencode/legacy-shared-config-dir.test.ts": 37, + "src/main/opencode/opencode-acp-stored-messages.test.ts": 135, + "src/main/opencode/opencode-cli-version.test.ts": 55, + "src/main/opencode/opencode-client-sweep.test.ts": 101, + "src/main/opencode/opencode-credential-backend.test.ts": 65, + "src/main/opencode/opencode-data-directory.test.ts": 36, + "src/main/opencode/opencode-go-api-key-store.test.ts": 121, + "src/main/opencode/opencode-launch-capabilities.test.ts": 98, + "src/main/opencode/opencode-launch-model-config.test.ts": 201, + "src/main/opencode/opencode-launch-model-context-cleanup.test.ts": 4232, + "src/main/opencode/opencode-launch-model-context-supervision.test.ts": 2461, + "src/main/opencode/opencode-launch-model-context.test.ts": 310, + "src/main/opencode/opencode-model-availability.test.ts": 48, + "src/main/opencode/opencode-model-catalog-listing.test.ts": 227, + "src/main/opencode/opencode-model-startup-plan.test.ts": 307, + "src/main/opencode/opencode-model-version-policy.test.ts": 29, + "src/main/opencode/opencode-overlay-manifest.test.ts": 33, + "src/main/opencode/opencode-pty-launch.test.ts": 314, + "src/main/opencode/opencode-session-binder.test.ts": 90, + "src/main/opencode/opencode-startup-prompt-claims.test.ts": 44, + "src/main/opencode/opencode-startup-prompt-installer.test.ts": 174, + "src/main/opencode/opencode-startup-prompt-owner.test.ts": 80, + "src/main/opencode/opencode-startup-prompt-runtime-identity.test.ts": 3956, + "src/main/opencode/opencode-startup-prompt-source.test.ts": 4775, "src/main/opencode/opencode-status-plugin-startup-refresh.test.ts": 52, - "src/main/opencode/opencode-structured-account-home.test.ts": 198, - "src/main/opencode/opencode-wsl-probe-environment.test.ts": 148, - "src/main/opencode/overlay-dir-gc-lifecycle.test.ts": 48, - "src/main/opencode/overlay-dir-gc.test.ts": 131, - "src/main/opencode/overlay-source-ownership.test.ts": 64, - "src/main/opencode/status-plugin-shared-server-capability.test.ts": 43, - "src/main/opencode2/status-plugin-setup-lifetime.test.ts": 98, - "src/main/orca-profiles/other-profile-workspace-catalog.test.ts": 385, - "src/main/orca-profiles/profile-active-transfer-worker.test.ts": 2466, - "src/main/orca-profiles/profile-active-transfer.test.ts": 1924, - "src/main/orca-profiles/profile-artifact-cloud-cleanup.test.ts": 640, - "src/main/orca-profiles/profile-cloud-auth-config.test.ts": 65, - "src/main/orca-profiles/profile-cloud-auth-status.test.ts": 72, - "src/main/orca-profiles/profile-cloud-client.test.ts": 148, - "src/main/orca-profiles/profile-cloud-dev-service.test.ts": 511, - "src/main/orca-profiles/profile-cloud-org-members-client.test.ts": 75, - "src/main/orca-profiles/profile-cloud-org-members-service.test.ts": 478, - "src/main/orca-profiles/profile-cloud-pkce.test.ts": 615, - "src/main/orca-profiles/profile-cloud-refresh-replay-guard.test.ts": 91, - "src/main/orca-profiles/profile-cloud-service-auth-retry.test.ts": 535, - "src/main/orca-profiles/profile-cloud-service-connect-overlap.test.ts": 688, - "src/main/orca-profiles/profile-cloud-service-refresh.test.ts": 548, - "src/main/orca-profiles/profile-cloud-service-sign-out-connect-race.test.ts": 529, - "src/main/orca-profiles/profile-cloud-service.test.ts": 541, - "src/main/orca-profiles/profile-cloud-session-mutation.test.ts": 96, - "src/main/orca-profiles/profile-cloud-session-refresh.test.ts": 86, - "src/main/orca-profiles/profile-cloud-session-store.test.ts": 176, - "src/main/orca-profiles/profile-index-store.test.ts": 1403, - "src/main/orca-profiles/profile-persistence-deadline.test.ts": 61, - "src/main/orca-profiles/profile-project-domain-transfer.test.ts": 2843, - "src/main/orca-profiles/profile-project-presence.test.ts": 680, - "src/main/orca-profiles/profile-project-session-field-disposition.test.ts": 367, - "src/main/orca-profiles/profile-project-session-state.test.ts": 326, - "src/main/orca-profiles/profile-project-state-file.test.ts": 384, - "src/main/orca-profiles/profile-project-transfer-migration.test.ts": 1054, - "src/main/orca-profiles/profile-project-transfer.test.ts": 1132, - "src/main/orca-profiles/profile-session-owner-transfer.test.ts": 268, - "src/main/orca-profiles/profile-telemetry-consent-seed.test.ts": 537, - "src/main/orca-profiles/profile-ui-scope.test.ts": 57, - "src/main/orcad-migration-export-holds.test.ts": 286, - "src/main/orcad/electron-serve-browser-process.test.ts": 2791, - "src/main/orcad/electron-serve-provider-selection.test.ts": 808, - "src/main/orcad/electron-sidecar-method-routing.test.ts": 304, - "src/main/orcad/electron-sidecar-tab-registry.test.ts": 207, - "src/main/orcad/external-chromium-browser-process.integration.test.ts": 429, - "src/main/orcad/external-chromium-browser-process.test.ts": 235, - "src/main/orcad/external-chromium-browser-session.test.ts": 239, - "src/main/orcad/main-preflight-order.test.ts": 808, - "src/main/orcad/native-host-abi.test.ts": 50, - "src/main/orcad/node-pty-prebuilt-slot.test.ts": 302, - "src/main/orcad/node-pty-precondition.test.ts": 614, - "src/main/orcad/orcad-app-paths.test.ts": 47, - "src/main/orcad/orcad-artifact-cache-retention.test.ts": 300, - "src/main/orcad/orcad-artifact-preflight.test.ts": 458, - "src/main/orcad/orcad-automations.test.ts": 2225, - "src/main/orcad/orcad-bind-address.test.ts": 41, - "src/main/orcad/orcad-browser-provider.test.ts": 420, - "src/main/orcad/orcad-browser-startup-cancellation.test.ts": 65, - "src/main/orcad/orcad-browser-startup.test.ts": 61, - "src/main/orcad/orcad-bundle-native-load-order.test.ts": 2425, - "src/main/orcad/orcad-bundled-runtime.integration.test.ts": 4817, - "src/main/orcad/orcad-bundled-runtime.test.ts": 97, - "src/main/orcad/orcad-cli-launcher.test.ts": 116, - "src/main/orcad/orcad-cross-runtime-daemon-adoption.integration.test.ts": 397, - "src/main/orcad/orcad-daemon-retirement.test.ts": 186, - "src/main/orcad/orcad-daemon-supervision.test.ts": 63, - "src/main/orcad/orcad-entry.test.ts": 583, - "src/main/orcad/orcad-forked-child-paths.test.ts": 1742, - "src/main/orcad/orcad-health.test.ts": 269, - "src/main/orcad/orcad-help.integration.test.ts": 4302, - "src/main/orcad/orcad-idle-exit-monitor.test.ts": 66, - "src/main/orcad/orcad-idle-stop-record.test.ts": 265, - "src/main/orcad/orcad-instance-lock.test.ts": 302, - "src/main/orcad/orcad-launch-contract.test.ts": 1502, - "src/main/orcad/orcad-launcher-isolation.integration.test.ts": 728, - "src/main/orcad/orcad-lifecycle-host.test.ts": 313, - "src/main/orcad/orcad-local-serve-selection.test.ts": 651, - "src/main/orcad/orcad-managed-idle-exit.test.ts": 230, - "src/main/orcad/orcad-managed-stop-cancel-race.test.ts": 255, - "src/main/orcad/orcad-managed-stop-cancellation.test.ts": 246, - "src/main/orcad/orcad-managed-stop.test.ts": 1323, - "src/main/orcad/orcad-native-preflight.test.ts": 259, - "src/main/orcad/orcad-node-launcher.integration.test.ts": 67, - "src/main/orcad/orcad-observability.test.ts": 280, - "src/main/orcad/orcad-packaged-node-pty.integration.test.ts": 80, - "src/main/orcad/orcad-process-start-time.test.ts": 62, - "src/main/orcad/orcad-profile-preflight.test.ts": 376, - "src/main/orcad/orcad-profile-state-startup.test.ts": 97, - "src/main/orcad/orcad-profile-state-telemetry.test.ts": 49, - "src/main/orcad/orcad-push-startup.test.ts": 2094, - "src/main/orcad/orcad-runtime-entry-artifacts.test.ts": 2239, - "src/main/orcad/orcad-runtime-lifetime.test.ts": 58, - "src/main/orcad/orcad-runtime-native-preflight.test.ts": 108, - "src/main/orcad/orcad-serve-parity.integration.test.ts": 355, - "src/main/orcad/orcad-sidecar-runtime-client.test.ts": 227, - "src/main/orcad/orcad-stop-request-listener.test.ts": 755, - "src/main/orcad/orcad-stop-request-shutdown.integration.test.ts": 482, - "src/main/orcad/orcad-terminal-census.test.ts": 2253, - "src/main/orcad/orcad-windows-conpty-breakaway.integration.test.ts": 74, - "src/main/orcad/serve-profile-ssh-targets.test.ts": 476, - "src/main/own-chromium-tree-kill-guard.test.ts": 158, - "src/main/persistence-async-write-syscalls.test.ts": 3814, - "src/main/persistence-automation-extra-agent-args.test.ts": 1200, - "src/main/persistence-automations.test.ts": 2450, - "src/main/persistence-clipboard-selection-migration.test.ts": 1767, - "src/main/persistence-cohort-and-identity-migration.test.ts": 1422, - "src/main/persistence-cross-host-pane-identity.test.ts": 1564, - "src/main/persistence-deregistered-repo-residue.test.ts": 1665, - "src/main/persistence-duplicate-repo-id-host-scope.test.ts": 1664, - "src/main/persistence-feature-interaction-broadcast.benchmark.test.ts": 1812, - "src/main/persistence-floating-terminal-trust.test.ts": 1766, - "src/main/persistence-flush-and-save-scheduling.test.ts": 2502, - "src/main/persistence-folder-scan-project-groups.test.ts": 1419, - "src/main/persistence-folder-workspace-notes.test.ts": 1820, - "src/main/persistence-host-admitted-terminal-membership.test.ts": 1706, - "src/main/persistence-host-partitioned-sessions.test.ts": 1731, - "src/main/persistence-host-partitioned-ssh-pty-bindings.test.ts": 1409, - "src/main/persistence-initial-load.test.ts": 1921, - "src/main/persistence-layout-binding-recovery.test.ts": 1504, - "src/main/persistence-load-repair-durability.test.ts": 1479, - "src/main/persistence-loading-store-extraction.test.ts": 1733, - "src/main/persistence-loading-store-write-risks.test.ts": 1592, - "src/main/persistence-native-chat-appearance.test.ts": 1702, - "src/main/persistence-native-chat-tab-view-mode.test.ts": 1389, - "src/main/persistence-native-chat-upgrade-tip-audience.test.ts": 1987, - "src/main/persistence-orcad-migration-catalog.test.ts": 1607, - "src/main/persistence-orcad-repository-cache-reimport.test.ts": 1240, - "src/main/persistence-pane-alias-selective-saves.test.ts": 1406, - "src/main/persistence-pane-identity-migration.test.ts": 10029, - "src/main/persistence-protected-secret-fail-closed.test.ts": 1679, - "src/main/persistence-protected-secret-write-race.test.ts": 1450, - "src/main/persistence-proxy-secret-recovery.test.ts": 1456, - "src/main/persistence-pty-binding-leaf-tab-resolution.test.ts": 1655, - "src/main/persistence-pty-binding-reconciliation.test.ts": 1737, - "src/main/persistence-remote-session-startup.test.ts": 1716, - "src/main/persistence-repo-lifecycle.test.ts": 2658, - "src/main/persistence-right-sidebar-tab.test.ts": 1623, - "src/main/persistence-settings-ui-defaults.test.ts": 2488, - "src/main/persistence-settings-update.test.ts": 1778, - "src/main/persistence-single-serialize.test.ts": 1683, - "src/main/persistence-source-control-ai-migration.test.ts": 1814, - "src/main/persistence-split-pane-incarnation.test.ts": 1439, - "src/main/persistence-ssh-lease-reattach-reclaim.test.ts": 1505, - "src/main/persistence-ssh-lease-tombstone-retention.test.ts": 1687, - "src/main/persistence-ssh-pending-pty-kill.test.ts": 2151, - "src/main/persistence-ssh-readoption-automation-migration.test.ts": 1910, - "src/main/persistence-ssh-remote-pty-binding-replay.test.ts": 1287, - "src/main/persistence-ssh-remote-pty-leases.test.ts": 1649, - "src/main/persistence-ssh-targets-and-pane-keys.test.ts": 1785, - "src/main/persistence-status-bar-compact-change-notice.test.ts": 1422, - "src/main/persistence-terminal-option-key-migration.test.ts": 1484, - "src/main/persistence-ui-state.test.ts": 2968, - "src/main/persistence-update-repo.test.ts": 1699, - "src/main/persistence-workspace-attachments.test.ts": 1648, - "src/main/persistence-workspace-pinned-automation-fence.test.ts": 1841, - "src/main/persistence-workspace-repin-automation-owner.test.ts": 2087, - "src/main/persistence-workspace-session-scrollback.test.ts": 1556, - "src/main/persistence-workspace-status-workflow.test.ts": 1917, - "src/main/persistence-worktree-card-properties.test.ts": 1715, - "src/main/persistence-worktree-deletion-fencing.test.ts": 1851, - "src/main/persistence-worktree-lineage-and-backups.test.ts": 1621, - "src/main/persistence-worktree-meta-and-folder-workspaces.test.ts": 1769, - "src/main/persistence-worktree-name-retirement.test.ts": 2258, - "src/main/persistence-worktree-visibility.test.ts": 1878, - "src/main/persistence/applying-settings/notification-settings-normalization.test.ts": 48, - "src/main/persistence/applying-settings/settings-update-machine-name.test.ts": 161, - "src/main/persistence/applying-settings/settings-update-terminal-contrast.test.ts": 174, - "src/main/persistence/applying-settings/terminal-settings-migrations.test.ts": 74, - "src/main/persistence/atomic-snapshot-writer.test.ts": 76, - "src/main/persistence/closed-terminal-tab-write-fence.test.ts": 5551, - "src/main/persistence/leasing-ssh-ptys/ssh-normalization-orcad-fence.test.ts": 200, - "src/main/persistence/leasing-ssh-ptys/ssh-pty-binding-cleanup.test.ts": 373, - "src/main/persistence/loading-store/durable-settings-write.test.ts": 1968, - "src/main/persistence/loading-store/legacy-opencode-go-api-key-migration.test.ts": 62, - "src/main/persistence/loading-store/metadata-lineage-batch-pruning.test.ts": 516, - "src/main/persistence/loading-store/normalize-loaded-explorer-root-migration.test.ts": 289, - "src/main/persistence/loading-store/normalize-loaded-global-settings.test.ts": 369, - "src/main/persistence/loading-store/normalize-loaded-project-catalog.test.ts": 370, - "src/main/persistence/loading-store/persisted-state-redundancy.test.ts": 2095, - "src/main/persistence/loading-store/profile-state-caller-cancellation.test.ts": 1569, - "src/main/persistence/loading-store/profile-state-checkpoints.test.ts": 1426, - "src/main/persistence/loading-store/profile-state-direct-flush.test.ts": 1898, - "src/main/persistence/loading-store/profile-state-import-lifetime.test.ts": 1339, - "src/main/persistence/loading-store/profile-state-maintenance-compatibility.test.ts": 1841, - "src/main/persistence/loading-store/profile-state-maintenance-final-failure.test.ts": 2130, - "src/main/persistence/loading-store/profile-state-maintenance-recovery.test.ts": 2887, - "src/main/persistence/loading-store/profile-state-maintenance.test.ts": 3136, - "src/main/persistence/loading-store/profile-state-pty-retirement-finalization.test.ts": 3327, - "src/main/persistence/loading-store/profile-state-selective-serialization.test.ts": 361, - "src/main/persistence/loading-store/profile-state-settings-writes.test.ts": 1962, - "src/main/persistence/loading-store/profile-state-sqlite-authority.test.ts": 3947, - "src/main/persistence/loading-store/profile-state-store-backups.test.ts": 1838, - "src/main/persistence/loading-store/profile-state-temp-cleanup-lifetime.test.ts": 1843, - "src/main/persistence/loading-store/profile-state-update-quit.test.ts": 2031, - "src/main/persistence/loading-store/profile-state-worker-coordination.test.ts": 1931, - "src/main/persistence/loading-store/profile-state-worker-secret-retention.test.ts": 1546, - "src/main/persistence/loading-store/profile-state-write-batching.test.ts": 1637, - "src/main/persistence/loading-store/pty-binding-async-durability.test.ts": 2161, - "src/main/persistence/loading-store/pty-binding-created-surface-rollback.test.ts": 1817, - "src/main/persistence/loading-store/pty-binding-fast-lane.test.ts": 406, - "src/main/persistence/loading-store/pty-binding-placement-inertness.test.ts": 1843, - "src/main/persistence/loading-store/pty-binding-restart-retirement.test.ts": 1599, - "src/main/persistence/loading-store/pty-binding-span.test.ts": 118, - "src/main/persistence/loading-store/pty-reattach-failure-routing.test.ts": 5933, - "src/main/persistence/loading-store/pty-retirement-async-durability.test.ts": 2298, - "src/main/persistence/loading-store/pty-retirement-publication-during-read.test.ts": 5281, - "src/main/persistence/loading-store/pty-spawn-exit-durability.test.ts": 5496, - "src/main/persistence/loading-store/pty-spawn-handle-publication.test.ts": 5526, - "src/main/persistence/loading-store/pty-spawn-replacement-durability.test.ts": 5757, - "src/main/persistence/loading-store/pty-spawn-restore-durability.test.ts": 7219, - "src/main/persistence/loading-store/secret-sentinel-substitution.test.ts": 99, - "src/main/persistence/loading-store/ssh-lease-async-durability.test.ts": 1576, - "src/main/persistence/loading-store/state-write-round-trip.test.ts": 1937, - "src/main/persistence/loading-store/store-prune-gate-signals.test.ts": 1724, - "src/main/persistence/loading-store/store-runtime-authored-session-writes.test.ts": 1738, - "src/main/persistence/loading-store/store-workspace-session-host-removal.test.ts": 1405, - "src/main/persistence/loading-store/terminal-close-async-durability.test.ts": 5695, - "src/main/persistence/loading-store/terminal-tab-pty-ownership.test.ts": 68, - "src/main/persistence/loading-store/workspace-session-partitions.test.ts": 435, - "src/main/persistence/loading-store/workspace-session-terminal-binding-replay.test.ts": 73, - "src/main/persistence/loading-store/workspace-session-write-listeners.test.ts": 1588, - "src/main/persistence/loading-store/worktree-meta-alias-projection.test.ts": 2168, - "src/main/persistence/loading-store/worktree-meta-write-normalization.test.ts": 98, - "src/main/persistence/migrating-orcad-catalog/orcad-catalog-receipt-ledger.test.ts": 365, - "src/main/persistence/migrating-orcad-catalog/orcad-destination-client-state.test.ts": 405, - "src/main/persistence/migrating-orcad-catalog/orcad-destination-workspace-session.test.ts": 395, - "src/main/persistence/migrating-orcad-catalog/orcad-dormant-metadata-defaults.test.ts": 358, - "src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts": 412, - "src/main/persistence/migrating-orcad-catalog/orcad-source-client-focus-census.test.ts": 603, + "src/main/opencode/opencode-structured-account-home.test.ts": 235, + "src/main/opencode/opencode-wsl-probe-environment.test.ts": 154, + "src/main/opencode/overlay-dir-gc-lifecycle.test.ts": 41, + "src/main/opencode/overlay-dir-gc.test.ts": 121, + "src/main/opencode/overlay-source-ownership.test.ts": 43, + "src/main/opencode/status-plugin-shared-server-capability.test.ts": 38, + "src/main/opencode2/status-plugin-setup-lifetime.test.ts": 77, + "src/main/orca-profiles/other-profile-workspace-catalog.test.ts": 518, + "src/main/orca-profiles/profile-active-transfer-worker.test.ts": 2278, + "src/main/orca-profiles/profile-active-transfer.test.ts": 2002, + "src/main/orca-profiles/profile-artifact-cloud-cleanup.test.ts": 579, + "src/main/orca-profiles/profile-cloud-auth-characterization.test.ts": 870, + "src/main/orca-profiles/profile-cloud-auth-config.test.ts": 62, + "src/main/orca-profiles/profile-cloud-auth-status.test.ts": 73, + "src/main/orca-profiles/profile-cloud-client.test.ts": 122, + "src/main/orca-profiles/profile-cloud-dev-service.test.ts": 509, + "src/main/orca-profiles/profile-cloud-org-members-client.test.ts": 59, + "src/main/orca-profiles/profile-cloud-org-members-service.test.ts": 488, + "src/main/orca-profiles/profile-cloud-pkce.test.ts": 649, + "src/main/orca-profiles/profile-cloud-refresh-replay-guard.test.ts": 73, + "src/main/orca-profiles/profile-cloud-service-auth-retry.test.ts": 462, + "src/main/orca-profiles/profile-cloud-service-connect-overlap.test.ts": 449, + "src/main/orca-profiles/profile-cloud-service-refresh.test.ts": 538, + "src/main/orca-profiles/profile-cloud-service-sign-out-connect-race.test.ts": 624, + "src/main/orca-profiles/profile-cloud-service.test.ts": 601, + "src/main/orca-profiles/profile-cloud-session-refresh-rotation.test.ts": 515, + "src/main/orca-profiles/profile-cloud-session-refresh.test.ts": 61, + "src/main/orca-profiles/profile-cloud-session-store.test.ts": 143, + "src/main/orca-profiles/profile-index-store.test.ts": 1170, + "src/main/orca-profiles/profile-persistence-deadline.test.ts": 48, + "src/main/orca-profiles/profile-project-domain-transfer.test.ts": 2842, + "src/main/orca-profiles/profile-project-presence.test.ts": 509, + "src/main/orca-profiles/profile-project-session-field-disposition.test.ts": 356, + "src/main/orca-profiles/profile-project-session-state.test.ts": 345, + "src/main/orca-profiles/profile-project-state-file.test.ts": 425, + "src/main/orca-profiles/profile-project-transfer-migration.test.ts": 1434, + "src/main/orca-profiles/profile-project-transfer.test.ts": 1495, + "src/main/orca-profiles/profile-session-owner-transfer.test.ts": 282, + "src/main/orca-profiles/profile-telemetry-consent-seed.test.ts": 533, + "src/main/orca-profiles/profile-ui-scope.test.ts": 43, + "src/main/orcad-migration-export-holds.test.ts": 282, + "src/main/orcad/electron-serve-browser-process.test.ts": 2915, + "src/main/orcad/electron-serve-provider-selection.test.ts": 763, + "src/main/orcad/electron-sidecar-method-routing.test.ts": 260, + "src/main/orcad/electron-sidecar-tab-registry.test.ts": 189, + "src/main/orcad/external-chromium-browser-process.integration.test.ts": 417, + "src/main/orcad/external-chromium-browser-process.test.ts": 248, + "src/main/orcad/external-chromium-browser-session.test.ts": 216, + "src/main/orcad/main-preflight-order.test.ts": 859, + "src/main/orcad/native-host-abi.test.ts": 48, + "src/main/orcad/node-pty-prebuilt-slot.test.ts": 306, + "src/main/orcad/node-pty-precondition.test.ts": 560, + "src/main/orcad/orcad-app-paths.test.ts": 45, + "src/main/orcad/orcad-artifact-cache-retention.test.ts": 354, + "src/main/orcad/orcad-artifact-materializer.test.ts": 1039, + "src/main/orcad/orcad-artifact-preflight.test.ts": 462, + "src/main/orcad/orcad-automations.test.ts": 2628, + "src/main/orcad/orcad-bind-address.test.ts": 51, + "src/main/orcad/orcad-browser-provider.test.ts": 365, + "src/main/orcad/orcad-browser-startup-cancellation.test.ts": 49, + "src/main/orcad/orcad-browser-startup.test.ts": 60, + "src/main/orcad/orcad-bundle-native-load-order.test.ts": 2460, + "src/main/orcad/orcad-bundled-runtime.integration.test.ts": 4851, + "src/main/orcad/orcad-bundled-runtime.test.ts": 89, + "src/main/orcad/orcad-cli-launcher.integration.test.ts": 331, + "src/main/orcad/orcad-cli-launcher.test.ts": 143, + "src/main/orcad/orcad-cli-orchestration.integration.test.ts": 217, + "src/main/orcad/orcad-codex-account-homes.test.ts": 574, + "src/main/orcad/orcad-compile-cache.test.ts": 40, + "src/main/orcad/orcad-cross-runtime-daemon-adoption.integration.test.ts": 430, + "src/main/orcad/orcad-daemon-retirement.test.ts": 176, + "src/main/orcad/orcad-daemon-supervision.test.ts": 83, + "src/main/orcad/orcad-entry.test.ts": 627, + "src/main/orcad/orcad-forked-child-paths.test.ts": 90, + "src/main/orcad/orcad-health.test.ts": 250, + "src/main/orcad/orcad-help.integration.test.ts": 3822, + "src/main/orcad/orcad-host-account-launch-env-store.test.ts": 219, + "src/main/orcad/orcad-host-unavailable.test.ts": 350, + "src/main/orcad/orcad-idle-exit-monitor.test.ts": 94, + "src/main/orcad/orcad-idle-stop-record.test.ts": 261, + "src/main/orcad/orcad-instance-lock.test.ts": 308, + "src/main/orcad/orcad-launch-contract.test.ts": 1772, + "src/main/orcad/orcad-launcher-isolation.integration.test.ts": 560, + "src/main/orcad/orcad-lifecycle-host.test.ts": 372, + "src/main/orcad/orcad-local-serve-selection.test.ts": 517, + "src/main/orcad/orcad-managed-idle-exit-chat-provider.test.ts": 1607, + "src/main/orcad/orcad-managed-idle-exit-provider-race.test.ts": 321, + "src/main/orcad/orcad-managed-idle-exit.test.ts": 284, + "src/main/orcad/orcad-managed-stop-cancel-race.test.ts": 293, + "src/main/orcad/orcad-managed-stop-cancellation.test.ts": 317, + "src/main/orcad/orcad-managed-stop-identity.test.ts": 339, + "src/main/orcad/orcad-managed-stop.test.ts": 1593, + "src/main/orcad/orcad-migration-wsl-destination.test.ts": 37, + "src/main/orcad/orcad-native-preflight.test.ts": 278, + "src/main/orcad/orcad-node-launcher.integration.test.ts": 69, + "src/main/orcad/orcad-observability.test.ts": 347, + "src/main/orcad/orcad-packaged-node-pty.integration.test.ts": 54, + "src/main/orcad/orcad-packaged-structured-chat.integration.test.ts": 384, + "src/main/orcad/orcad-process-start-time.test.ts": 55, + "src/main/orcad/orcad-profile-preflight.test.ts": 347, + "src/main/orcad/orcad-profile-state-startup.test.ts": 52, + "src/main/orcad/orcad-profile-state-telemetry.test.ts": 41, + "src/main/orcad/orcad-push-startup.test.ts": 2144, + "src/main/orcad/orcad-runtime-entry-artifacts.test.ts": 2502, + "src/main/orcad/orcad-runtime-lifetime.test.ts": 48, + "src/main/orcad/orcad-runtime-native-preflight.test.ts": 86, + "src/main/orcad/orcad-secret-protection-gap.test.ts": 28, + "src/main/orcad/orcad-serve-parity.integration.test.ts": 243, + "src/main/orcad/orcad-sidecar-runtime-client.test.ts": 259, + "src/main/orcad/orcad-stop-request-listener.test.ts": 771, + "src/main/orcad/orcad-stop-request-shutdown.integration.test.ts": 578, + "src/main/orcad/orcad-terminal-census.test.ts": 628, + "src/main/orcad/orcad-windows-conpty-breakaway.integration.test.ts": 54, + "src/main/orcad/orcad-wsl-guest-shell-path.test.ts": 51, + "src/main/orcad/orcad-wsl-host-login-environment.test.ts": 65, + "src/main/orcad/serve-profile-ssh-targets.test.ts": 429, + "src/main/own-chromium-tree-kill-guard.test.ts": 113, + "src/main/persistence-async-write-syscalls.test.ts": 2801, + "src/main/persistence-automation-extra-agent-args.test.ts": 1694, + "src/main/persistence-automations.test.ts": 2080, + "src/main/persistence-clipboard-selection-migration.test.ts": 1954, + "src/main/persistence-cohort-and-identity-migration.test.ts": 1714, + "src/main/persistence-cross-host-pane-identity.test.ts": 1627, + "src/main/persistence-deregistered-repo-residue.test.ts": 1689, + "src/main/persistence-duplicate-repo-id-host-scope.test.ts": 1946, + "src/main/persistence-empty-root-layout-load.test.ts": 1567, + "src/main/persistence-feature-interaction-broadcast.benchmark.test.ts": 1585, + "src/main/persistence-floating-terminal-trust.test.ts": 1893, + "src/main/persistence-flush-and-save-scheduling.test.ts": 2736, + "src/main/persistence-folder-scan-project-groups.test.ts": 1641, + "src/main/persistence-folder-workspace-notes.test.ts": 1578, + "src/main/persistence-host-partitioned-sessions.test.ts": 1852, + "src/main/persistence-host-partitioned-ssh-pty-bindings.test.ts": 1580, + "src/main/persistence-initial-load.test.ts": 1852, + "src/main/persistence-jira-project.test.ts": 1400, + "src/main/persistence-jira-saved-filters.test.ts": 1531, + "src/main/persistence-layout-binding-recovery.test.ts": 1550, + "src/main/persistence-load-repair-durability.test.ts": 1535, + "src/main/persistence-loading-store-extraction.test.ts": 1773, + "src/main/persistence-loading-store-write-risks.test.ts": 1640, + "src/main/persistence-native-chat-appearance.test.ts": 1369, + "src/main/persistence-native-chat-tab-view-mode.test.ts": 1365, + "src/main/persistence-native-chat-upgrade-tip-audience.test.ts": 1983, + "src/main/persistence-orcad-migration-catalog.test.ts": 2096, + "src/main/persistence-orcad-repository-cache-reimport.test.ts": 1587, + "src/main/persistence-pane-alias-selective-saves.test.ts": 1608, + "src/main/persistence-pane-identity-migration.test.ts": 4881, + "src/main/persistence-protected-secret-fail-closed.test.ts": 2040, + "src/main/persistence-protected-secret-write-race.test.ts": 1639, + "src/main/persistence-proxy-secret-recovery.test.ts": 1569, + "src/main/persistence-pty-binding-leaf-tab-resolution.test.ts": 1418, + "src/main/persistence-pty-binding-reconciliation.test.ts": 1631, + "src/main/persistence-remote-session-startup.test.ts": 1481, + "src/main/persistence-repo-lifecycle.test.ts": 3167, + "src/main/persistence-right-sidebar-tab.test.ts": 1444, + "src/main/persistence-runtime-admitted-terminal-membership.test.ts": 1419, + "src/main/persistence-settings-ui-defaults.test.ts": 2214, + "src/main/persistence-settings-update.test.ts": 2072, + "src/main/persistence-single-serialize.test.ts": 1630, + "src/main/persistence-source-control-ai-migration.test.ts": 1770, + "src/main/persistence-split-pane-incarnation.test.ts": 1586, + "src/main/persistence-ssh-lease-tombstone-retention.test.ts": 1552, + "src/main/persistence-ssh-pending-pty-kill.test.ts": 1505, + "src/main/persistence-ssh-readoption-automation-migration.test.ts": 2132, + "src/main/persistence-ssh-remote-pty-binding-replay.test.ts": 1537, + "src/main/persistence-ssh-remote-pty-leases.test.ts": 1759, + "src/main/persistence-ssh-targets-and-pane-keys.test.ts": 1788, + "src/main/persistence-status-bar-compact-change-notice.test.ts": 1688, + "src/main/persistence-terminal-option-key-migration.test.ts": 1645, + "src/main/persistence-ui-state.test.ts": 3132, + "src/main/persistence-update-repo.test.ts": 1760, + "src/main/persistence-workspace-attachments.test.ts": 1584, + "src/main/persistence-workspace-pinned-automation-fence.test.ts": 2063, + "src/main/persistence-workspace-repin-automation-owner.test.ts": 2056, + "src/main/persistence-workspace-session-scrollback.test.ts": 1638, + "src/main/persistence-workspace-status-workflow.test.ts": 1750, + "src/main/persistence-worktree-card-properties.test.ts": 1708, + "src/main/persistence-worktree-deletion-fencing.test.ts": 1210, + "src/main/persistence-worktree-lineage-and-backups.test.ts": 1734, + "src/main/persistence-worktree-meta-and-folder-workspaces.test.ts": 1544, + "src/main/persistence-worktree-name-retirement.test.ts": 2492, + "src/main/persistence-worktree-visibility.test.ts": 1839, + "src/main/persistence/applying-settings/notification-settings-normalization.test.ts": 56, + "src/main/persistence/applying-settings/settings-update-machine-name.test.ts": 149, + "src/main/persistence/applying-settings/settings-update-terminal-contrast.test.ts": 153, + "src/main/persistence/applying-settings/settings-update-terminal-default-shell.test.ts": 164, + "src/main/persistence/applying-settings/settings-update-terminal-link-preference.test.ts": 1536, + "src/main/persistence/applying-settings/terminal-settings-migrations.test.ts": 76, + "src/main/persistence/atomic-snapshot-writer.test.ts": 341, + "src/main/persistence/closed-terminal-tab-write-fence.test.ts": 5851, + "src/main/persistence/leasing-ssh-ptys/ssh-normalization-orcad-fence.test.ts": 219, + "src/main/persistence/leasing-ssh-ptys/ssh-pty-binding-cleanup.test.ts": 336, + "src/main/persistence/loading-store/durable-settings-write.test.ts": 1952, + "src/main/persistence/loading-store/legacy-opencode-go-api-key-migration.test.ts": 74, + "src/main/persistence/loading-store/metadata-lineage-batch-pruning.test.ts": 609, + "src/main/persistence/loading-store/normalize-loaded-explorer-root-migration.test.ts": 403, + "src/main/persistence/loading-store/normalize-loaded-global-settings.test.ts": 402, + "src/main/persistence/loading-store/normalize-loaded-project-catalog.test.ts": 369, + "src/main/persistence/loading-store/persisted-state-redundancy.test.ts": 1976, + "src/main/persistence/loading-store/profile-state-caller-cancellation.test.ts": 1931, + "src/main/persistence/loading-store/profile-state-checkpoints.test.ts": 1815, + "src/main/persistence/loading-store/profile-state-direct-flush.test.ts": 1619, + "src/main/persistence/loading-store/profile-state-import-lifetime.test.ts": 1535, + "src/main/persistence/loading-store/profile-state-maintenance-compatibility.test.ts": 1904, + "src/main/persistence/loading-store/profile-state-maintenance-final-failure.test.ts": 1809, + "src/main/persistence/loading-store/profile-state-maintenance-recovery.test.ts": 2380, + "src/main/persistence/loading-store/profile-state-maintenance.test.ts": 2759, + "src/main/persistence/loading-store/profile-state-pty-retirement-finalization.test.ts": 4119, + "src/main/persistence/loading-store/profile-state-selective-serialization.test.ts": 431, + "src/main/persistence/loading-store/profile-state-settings-writes.test.ts": 1770, + "src/main/persistence/loading-store/profile-state-sqlite-authority.test.ts": 2255, + "src/main/persistence/loading-store/profile-state-store-backups.test.ts": 1967, + "src/main/persistence/loading-store/profile-state-temp-cleanup-lifetime.test.ts": 1900, + "src/main/persistence/loading-store/profile-state-update-quit.test.ts": 1829, + "src/main/persistence/loading-store/profile-state-worker-coordination.test.ts": 1887, + "src/main/persistence/loading-store/profile-state-worker-secret-retention.test.ts": 1551, + "src/main/persistence/loading-store/profile-state-write-batching.test.ts": 1790, + "src/main/persistence/loading-store/pty-binding-async-durability.test.ts": 1820, + "src/main/persistence/loading-store/pty-binding-created-surface-rollback.test.ts": 1573, + "src/main/persistence/loading-store/pty-binding-fast-lane.test.ts": 293, + "src/main/persistence/loading-store/pty-binding-placement-inertness.test.ts": 2073, + "src/main/persistence/loading-store/pty-binding-restart-retirement.test.ts": 1611, + "src/main/persistence/loading-store/pty-binding-span.test.ts": 93, + "src/main/persistence/loading-store/pty-reattach-failure-routing.test.ts": 5492, + "src/main/persistence/loading-store/pty-retirement-async-durability.test.ts": 1872, + "src/main/persistence/loading-store/pty-retirement-publication-during-read.test.ts": 6062, + "src/main/persistence/loading-store/pty-spawn-exit-durability.test.ts": 8031, + "src/main/persistence/loading-store/pty-spawn-handle-publication.test.ts": 6193, + "src/main/persistence/loading-store/pty-spawn-replacement-durability.test.ts": 5838, + "src/main/persistence/loading-store/pty-spawn-restore-durability.test.ts": 7144, + "src/main/persistence/loading-store/secret-sentinel-substitution.test.ts": 89, + "src/main/persistence/loading-store/state-write-round-trip.test.ts": 1562, + "src/main/persistence/loading-store/store-host-partition-before-unload.test.ts": 1450, + "src/main/persistence/loading-store/store-prune-gate-signals.test.ts": 1466, + "src/main/persistence/loading-store/store-runtime-authored-session-writes.test.ts": 1513, + "src/main/persistence/loading-store/store-workspace-session-host-removal.test.ts": 1533, + "src/main/persistence/loading-store/terminal-close-async-durability.test.ts": 5272, + "src/main/persistence/loading-store/terminal-tab-pty-ownership.test.ts": 36, + "src/main/persistence/loading-store/workspace-session-partitions.test.ts": 379, + "src/main/persistence/loading-store/workspace-session-terminal-binding-replay.test.ts": 72, + "src/main/persistence/loading-store/workspace-session-write-listeners.test.ts": 1677, + "src/main/persistence/loading-store/worktree-meta-alias-projection.test.ts": 2365, + "src/main/persistence/loading-store/worktree-meta-write-normalization.test.ts": 103, + "src/main/persistence/migrating-orcad-catalog/orcad-catalog-receipt-ledger.test.ts": 429, + "src/main/persistence/migrating-orcad-catalog/orcad-destination-client-state.test.ts": 403, + "src/main/persistence/migrating-orcad-catalog/orcad-destination-workspace-session.test.ts": 462, + "src/main/persistence/migrating-orcad-catalog/orcad-dormant-metadata-defaults.test.ts": 489, + "src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts": 459, + "src/main/persistence/migrating-orcad-catalog/orcad-source-client-focus-census.test.ts": 626, "src/main/persistence/migrating-orcad-catalog/orcad-source-client-state.test.ts": 472, - "src/main/persistence/migrating-orcad-catalog/orcad-source-client-subtraction.test.ts": 390, - "src/main/persistence/migrating-orcad-catalog/orcad-source-dependency-census.test.ts": 475, - "src/main/persistence/migrating-orcad-catalog/orcad-source-export.test.ts": 1403, - "src/main/persistence/migrating-orcad-catalog/orcad-source-host-qualified-scope.test.ts": 581, - "src/main/persistence/migrating-orcad-catalog/orcad-source-legacy-key-scope.test.ts": 660, - "src/main/persistence/migrating-orcad-catalog/orcad-source-ownership.test.ts": 404, - "src/main/persistence/migrating-orcad-catalog/orcad-source-partial-partition.test.ts": 486, - "src/main/persistence/migrating-orcad-catalog/orcad-source-scope.test.ts": 71, - "src/main/persistence/migrating-orcad-catalog/orcad-source-scrollback-state.test.ts": 222, - "src/main/persistence/migrating-orcad-catalog/orcad-staged-catalog-claims.test.ts": 440, - "src/main/persistence/profile-state-cutover-fixture.test.ts": 1623, - "src/main/persistence/profile-state-cutover-soak.test.ts": 2103, - "src/main/persistence/profile-state/legacy-json/profile-state-versioned-export.test.ts": 134, - "src/main/persistence/profile-state/profile-state-access-identity.test.ts": 166, - "src/main/persistence/profile-state/profile-state-access-process.test.ts": 592, - "src/main/persistence/profile-state/profile-state-access-windows-native.win32.test.ts": 102, - "src/main/persistence/profile-state/profile-state-access-windows.test.ts": 195, - "src/main/persistence/profile-state/profile-state-access.test.ts": 277, - "src/main/persistence/profile-state/profile-state-authority-bootstrap.test.ts": 1809, - "src/main/persistence/profile-state/profile-state-authority-export-fencing.test.ts": 225, - "src/main/persistence/profile-state/profile-state-authority-marker-writes.test.ts": 541, - "src/main/persistence/profile-state/profile-state-authority-marker.test.ts": 1460, - "src/main/persistence/profile-state/profile-state-automation-runs-equality.test.ts": 203, - "src/main/persistence/profile-state/profile-state-automation-runs-serialization.test.ts": 416, - "src/main/persistence/profile-state/profile-state-automation-storage-upgrade.test.ts": 2903, - "src/main/persistence/profile-state/profile-state-backup-path.test.ts": 68, - "src/main/persistence/profile-state/profile-state-backup-rotation.test.ts": 730, - "src/main/persistence/profile-state/profile-state-backup-temporary-files.test.ts": 94, - "src/main/persistence/profile-state/profile-state-backup-worker.test.ts": 1602, - "src/main/persistence/profile-state/profile-state-bootstrap-publication-race.test.ts": 1679, - "src/main/persistence/profile-state/profile-state-complete-domain-writes.test.ts": 379, + "src/main/persistence/migrating-orcad-catalog/orcad-source-client-subtraction.test.ts": 497, + "src/main/persistence/migrating-orcad-catalog/orcad-source-dependency-census.test.ts": 712, + "src/main/persistence/migrating-orcad-catalog/orcad-source-export.test.ts": 1730, + "src/main/persistence/migrating-orcad-catalog/orcad-source-host-qualified-scope.test.ts": 684, + "src/main/persistence/migrating-orcad-catalog/orcad-source-legacy-key-scope.test.ts": 663, + "src/main/persistence/migrating-orcad-catalog/orcad-source-ownership.test.ts": 453, + "src/main/persistence/migrating-orcad-catalog/orcad-source-partial-partition.test.ts": 698, + "src/main/persistence/migrating-orcad-catalog/orcad-source-release-upgrade.test.ts": 715, + "src/main/persistence/migrating-orcad-catalog/orcad-source-scope.test.ts": 75, + "src/main/persistence/migrating-orcad-catalog/orcad-source-scrollback-state.test.ts": 236, + "src/main/persistence/migrating-orcad-catalog/orcad-staged-catalog-claims.test.ts": 443, + "src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.test.ts": 386, + "src/main/persistence/moving-wsl-projects/wsl-move-cutover-journal.test.ts": 684, + "src/main/persistence/moving-wsl-projects/wsl-move-exit-proof.test.ts": 69, + "src/main/persistence/moving-wsl-projects/wsl-move-fence.test.ts": 421, + "src/main/persistence/moving-wsl-projects/wsl-move-preflight.test.ts": 63, + "src/main/persistence/moving-wsl-projects/wsl-move-quiesce.test.ts": 416, + "src/main/persistence/moving-wsl-projects/wsl-move-restore-hints.test.ts": 61, + "src/main/persistence/moving-wsl-projects/wsl-move-scope-guard.test.ts": 568, + "src/main/persistence/moving-wsl-projects/wsl-move-scope.test.ts": 103, + "src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.test.ts": 1620, + "src/main/persistence/moving-wsl-projects/wsl-move-state-projection-scope.test.ts": 360, + "src/main/persistence/moving-wsl-projects/wsl-move-state-projection.test.ts": 424, + "src/main/persistence/moving-wsl-projects/wsl-move-terminal-gate.test.ts": 241, + "src/main/persistence/profile-state-cutover-fixture.test.ts": 1568, + "src/main/persistence/profile-state-cutover-soak.test.ts": 2160, + "src/main/persistence/profile-state/legacy-json/profile-state-versioned-export.test.ts": 133, + "src/main/persistence/profile-state/profile-state-access-identity.test.ts": 121, + "src/main/persistence/profile-state/profile-state-access-process.test.ts": 671, + "src/main/persistence/profile-state/profile-state-access-windows-native.win32.test.ts": 112, + "src/main/persistence/profile-state/profile-state-access-windows.test.ts": 148, + "src/main/persistence/profile-state/profile-state-access.test.ts": 340, + "src/main/persistence/profile-state/profile-state-authority-bootstrap.test.ts": 1854, + "src/main/persistence/profile-state/profile-state-authority-export-fencing.test.ts": 179, + "src/main/persistence/profile-state/profile-state-authority-marker-writes.test.ts": 547, + "src/main/persistence/profile-state/profile-state-authority-marker.test.ts": 1646, + "src/main/persistence/profile-state/profile-state-automation-runs-equality.test.ts": 235, + "src/main/persistence/profile-state/profile-state-automation-runs-serialization.test.ts": 439, + "src/main/persistence/profile-state/profile-state-automation-storage-upgrade.test.ts": 2632, + "src/main/persistence/profile-state/profile-state-backup-path.test.ts": 74, + "src/main/persistence/profile-state/profile-state-backup-rotation.test.ts": 627, + "src/main/persistence/profile-state/profile-state-backup-temporary-files.test.ts": 90, + "src/main/persistence/profile-state/profile-state-backup-worker.test.ts": 1733, + "src/main/persistence/profile-state/profile-state-bootstrap-publication-race.test.ts": 1530, + "src/main/persistence/profile-state/profile-state-complete-domain-writes.test.ts": 347, "src/main/persistence/profile-state/profile-state-crash-recovery.test.ts": 596, - "src/main/persistence/profile-state/profile-state-cross-runtime.integration.test.ts": 316, - "src/main/persistence/profile-state/profile-state-current-json-command.test.ts": 2938, - "src/main/persistence/profile-state/profile-state-current-sqlite-command.test.ts": 1748, - "src/main/persistence/profile-state/profile-state-database-export-crash.test.ts": 203, - "src/main/persistence/profile-state/profile-state-database-recovery.test.ts": 1415, - "src/main/persistence/profile-state/profile-state-database-rollback-export.test.ts": 1597, - "src/main/persistence/profile-state/profile-state-database-snapshot.test.ts": 454, - "src/main/persistence/profile-state/profile-state-database.test.ts": 243, - "src/main/persistence/profile-state/profile-state-documents.test.ts": 792, - "src/main/persistence/profile-state/profile-state-domain-equality.test.ts": 282, - "src/main/persistence/profile-state/profile-state-domain-reader.test.ts": 178, - "src/main/persistence/profile-state/profile-state-domain-writes.test.ts": 562, - "src/main/persistence/profile-state/profile-state-fragment-validation.test.ts": 1697, - "src/main/persistence/profile-state/profile-state-json-compatibility-recovery.test.ts": 1489, - "src/main/persistence/profile-state/profile-state-large-recovery-crash.test.ts": 3444, - "src/main/persistence/profile-state/profile-state-live-store-factory.test.ts": 2219, - "src/main/persistence/profile-state/profile-state-offline-settings.test.ts": 422, - "src/main/persistence/profile-state/profile-state-parsed-snapshot.test.ts": 229, - "src/main/persistence/profile-state/profile-state-read-concurrency.test.ts": 200, - "src/main/persistence/profile-state/profile-state-recovery-batch.test.ts": 164, - "src/main/persistence/profile-state/profile-state-recovery-copy.test.ts": 111, - "src/main/persistence/profile-state/profile-state-recovery-crash-boundaries.test.ts": 11352, - "src/main/persistence/profile-state/profile-state-recovery-crash-process.test.ts": 228, - "src/main/persistence/profile-state/profile-state-runtime-preflight.test.ts": 602, - "src/main/persistence/profile-state/profile-state-selected-profile-recovery.test.ts": 1692, - "src/main/persistence/profile-state/profile-state-startup-authority.test.ts": 2001, - "src/main/persistence/profile-state/profile-state-startup-failure.test.ts": 1463, - "src/main/persistence/profile-state/profile-state-startup-recovery-dialog.test.ts": 266, - "src/main/persistence/profile-state/profile-state-startup-secrets.test.ts": 1720, - "src/main/persistence/profile-state/profile-state-startup-snapshot.test.ts": 1603, - "src/main/persistence/profile-state/profile-state-store-factory.test.ts": 2673, - "src/main/persistence/profile-state/profile-state-streaming-validation.test.ts": 282, - "src/main/persistence/profile-state/profile-state-worker-authority.test.ts": 1651, - "src/main/persistence/profile-state/profile-state-worker-export-failures.test.ts": 440, - "src/main/persistence/profile-state/profile-state-write-transaction.test.ts": 74, - "src/main/persistence/profile-state/profile-state-writer-capacity-errors.test.ts": 66, - "src/main/persistence/profile-state/profile-state-writer-close.test.ts": 86, - "src/main/persistence/profile-state/profile-state-writer-protocol-faults.test.ts": 936, - "src/main/persistence/profile-state/profile-state-writer-sleep.test.ts": 782, - "src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts": 8846, - "src/main/persistence/profile-state/profile-state-writer-worker.test.ts": 1542, - "src/main/persistence/restoring-sessions/pane-alias-normalization-scaling.test.ts": 85, - "src/main/persistence/restoring-sessions/pane-alias-normalization.test.ts": 921, - "src/main/persistence/restoring-sessions/pane-key-remapping.test.ts": 50, - "src/main/persistence/restoring-sessions/session-owner-fields.test.ts": 333, - "src/main/persistence/restoring-sessions/session-worktree-ownership.test.ts": 348, - "src/main/persistence/restoring-sessions/terminal-layout-normalization.test.ts": 60, - "src/main/persistence/restoring-sessions/workspace-pane-normalization-index.test.ts": 101, - "src/main/persistence/runtime-authored-workspace-session-fields.test.ts": 49, - "src/main/persistence/scheduling-automations/automation-run-operations.test.ts": 143, - "src/main/persistence/terminal-topology/terminal-late-bind-layout-rules.test.ts": 1562, - "src/main/persistence/terminal-topology/terminal-leaf-move-concurrent-close.test.ts": 1529, - "src/main/persistence/terminal-topology/terminal-leaf-move.test.ts": 1619, - "src/main/persistence/terminal-topology/terminal-owner-invariants.test.ts": 299, - "src/main/persistence/terminal-topology/terminal-owner-report-only.test.ts": 1399, - "src/main/persistence/terminal-topology/terminal-sleep-quit-resume.test.ts": 1626, - "src/main/persistence/terminal-topology/terminal-stale-pane-tree-client.test.ts": 4000, - "src/main/persistence/terminal-topology/terminal-topology-commit.test.ts": 280, - "src/main/persistence/terminal-topology/terminal-topology-model.test.ts": 2243, - "src/main/persistence/terminal-topology/workspace-layout-rules.test.ts": 304, - "src/main/persistence/tracking-repos/local-worktree-metadata-batch-pruning.bench.test.ts": 409, - "src/main/persistence/tracking-repos/local-worktree-metadata-scan-expectation.test.ts": 418, - "src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts": 503, - "src/main/persistence/tracking-repos/probeable-local-worktree-metadata-candidates.test.ts": 400, - "src/main/persistence/tracking-repos/project-group-session-batch.test.ts": 353, - "src/main/persistence/tracking-repos/project-host-compatibility.test.ts": 61, - "src/main/persistence/tracking-repos/repo-update-host-guard.test.ts": 424, - "src/main/persistence/tracking-repos/worktree-identity-migration-field-coverage.test.ts": 351, - "src/main/persistence/tracking-repos/worktree-metadata-normalization.test.ts": 327, - "src/main/pi/agent-status-completion-delivery.test.ts": 1338, - "src/main/pi/agent-status-extension-async-subagents.test.ts": 1604, - "src/main/pi/agent-status-extension-nested-run-settlement.test.ts": 486, - "src/main/pi/agent-status-extension-omp-lifecycle.test.ts": 2614, - "src/main/pi/agent-status-extension-omp-model.test.ts": 1167, - "src/main/pi/agent-status-extension-omp-prefill.test.ts": 698, - "src/main/pi/agent-status-extension-session-change.test.ts": 2226, - "src/main/pi/agent-status-extension-source.test.ts": 2691, - "src/main/pi/agent-status-input-redaction.test.ts": 1660, - "src/main/pi/agent-status-omp-approval-forwarding.test.ts": 914, - "src/main/pi/agent-status-owner-recovery.test.ts": 1128, - "src/main/pi/agent-status-reload-delivery.test.ts": 680, - "src/main/pi/agent-status-ui-prompt.test.ts": 1662, - "src/main/pi/omp-model-command-source.test.ts": 789, - "src/main/pi/omp-session-status-owner.test.ts": 1010, - "src/main/pi/omp-transcript-metadata.test.ts": 1375, - "src/main/pi/rpc-capture-replay.test.ts": 518, - "src/main/pi/rpc-configured-default.test.ts": 1225, - "src/main/pi/rpc-launch-resolution.test.ts": 810, - "src/main/pi/rpc-launch.test.ts": 432, - "src/main/pi/rpc-location.test.ts": 1032, - "src/main/pi/rpc-message-dialog-shapes.test.ts": 205, - "src/main/pi/rpc-model-catalog-probe.test.ts": 481, - "src/main/pi/rpc-options.test.ts": 208, - "src/main/pi/rpc-prompt-delivery.test.ts": 300, - "src/main/pi/rpc-prompt.test.ts": 253, - "src/main/pi/rpc-runtime-registration.test.ts": 452, - "src/main/pi/rpc-session-adapter.test.ts": 1081, - "src/main/pi/rpc-turn-races.test.ts": 714, - "src/main/pi/titlebar-extension-overlay-path.test.ts": 83, - "src/main/pi/titlebar-extension-service.test.ts": 230, - "src/main/pi/titlebar-extension-source.test.ts": 1009, - "src/main/plugins/plugin-agent-status-event.test.ts": 227, - "src/main/plugins/plugin-atomic-file-write.test.ts": 2063, - "src/main/plugins/plugin-audit-log-scaling.test.ts": 405, - "src/main/plugins/plugin-audit-log.test.ts": 66, - "src/main/plugins/plugin-bundled-bootstrap-coordinator.test.ts": 425, - "src/main/plugins/plugin-bundled-bootstrap.test.ts": 486, - "src/main/plugins/plugin-command-registry.test.ts": 345, - "src/main/plugins/plugin-content-pack-registry.test.ts": 302, - "src/main/plugins/plugin-content-safety.test.ts": 850, - "src/main/plugins/plugin-dev-watcher-unknown-identity.test.ts": 252, - "src/main/plugins/plugin-dev-watcher.test.ts": 509, - "src/main/plugins/plugin-discovery.test.ts": 325, - "src/main/plugins/plugin-enablement.test.ts": 366, - "src/main/plugins/plugin-host-methods.test.ts": 300, + "src/main/persistence/profile-state/profile-state-cross-runtime.integration.test.ts": 322, + "src/main/persistence/profile-state/profile-state-current-json-command.test.ts": 2774, + "src/main/persistence/profile-state/profile-state-current-sqlite-command.test.ts": 1884, + "src/main/persistence/profile-state/profile-state-database-export-crash.test.ts": 219, + "src/main/persistence/profile-state/profile-state-database-recovery.test.ts": 1224, + "src/main/persistence/profile-state/profile-state-database-rollback-export.test.ts": 1462, + "src/main/persistence/profile-state/profile-state-database-snapshot.test.ts": 589, + "src/main/persistence/profile-state/profile-state-database.test.ts": 293, + "src/main/persistence/profile-state/profile-state-documents.test.ts": 637, + "src/main/persistence/profile-state/profile-state-domain-equality.test.ts": 206, + "src/main/persistence/profile-state/profile-state-domain-reader.test.ts": 212, + "src/main/persistence/profile-state/profile-state-domain-writes.test.ts": 556, + "src/main/persistence/profile-state/profile-state-fragment-validation.test.ts": 1366, + "src/main/persistence/profile-state/profile-state-json-compatibility-recovery.test.ts": 1584, + "src/main/persistence/profile-state/profile-state-large-recovery-crash.test.ts": 2706, + "src/main/persistence/profile-state/profile-state-live-store-factory.test.ts": 2473, + "src/main/persistence/profile-state/profile-state-offline-settings.test.ts": 471, + "src/main/persistence/profile-state/profile-state-parsed-snapshot.test.ts": 191, + "src/main/persistence/profile-state/profile-state-read-concurrency.test.ts": 246, + "src/main/persistence/profile-state/profile-state-recovery-batch.test.ts": 171, + "src/main/persistence/profile-state/profile-state-recovery-copy.test.ts": 105, + "src/main/persistence/profile-state/profile-state-recovery-crash-boundaries.test.ts": 8210, + "src/main/persistence/profile-state/profile-state-recovery-crash-process.test.ts": 255, + "src/main/persistence/profile-state/profile-state-runtime-preflight.test.ts": 622, + "src/main/persistence/profile-state/profile-state-selected-profile-recovery.test.ts": 1483, + "src/main/persistence/profile-state/profile-state-startup-authority.test.ts": 2391, + "src/main/persistence/profile-state/profile-state-startup-failure.test.ts": 1495, + "src/main/persistence/profile-state/profile-state-startup-recovery-dialog.test.ts": 223, + "src/main/persistence/profile-state/profile-state-startup-secrets.test.ts": 1956, + "src/main/persistence/profile-state/profile-state-startup-snapshot.test.ts": 1683, + "src/main/persistence/profile-state/profile-state-store-factory.test.ts": 2366, + "src/main/persistence/profile-state/profile-state-streaming-validation.test.ts": 281, + "src/main/persistence/profile-state/profile-state-worker-authority.test.ts": 1960, + "src/main/persistence/profile-state/profile-state-worker-export-failures.test.ts": 512, + "src/main/persistence/profile-state/profile-state-write-transaction.test.ts": 71, + "src/main/persistence/profile-state/profile-state-writer-capacity-errors.test.ts": 83, + "src/main/persistence/profile-state/profile-state-writer-close.test.ts": 101, + "src/main/persistence/profile-state/profile-state-writer-protocol-faults.test.ts": 967, + "src/main/persistence/profile-state/profile-state-writer-sleep.test.ts": 828, + "src/main/persistence/profile-state/profile-state-writer-worker.test.ts": 1419, + "src/main/persistence/restoring-sessions/pane-alias-normalization-scaling.test.ts": 77, + "src/main/persistence/restoring-sessions/pane-alias-normalization.test.ts": 793, + "src/main/persistence/restoring-sessions/pane-key-remapping.test.ts": 58, + "src/main/persistence/restoring-sessions/session-owner-fields.test.ts": 281, + "src/main/persistence/restoring-sessions/session-worktree-ownership.test.ts": 311, + "src/main/persistence/restoring-sessions/terminal-layout-normalization.test.ts": 70, + "src/main/persistence/restoring-sessions/workspace-pane-normalization-index.test.ts": 88, + "src/main/persistence/runtime-authored-workspace-session-fields.test.ts": 45, + "src/main/persistence/scheduling-automations/automation-run-operations.test.ts": 127, + "src/main/persistence/terminal-topology/terminal-late-bind-layout-rules.test.ts": 1639, + "src/main/persistence/terminal-topology/terminal-leaf-move-concurrent-close.test.ts": 1779, + "src/main/persistence/terminal-topology/terminal-leaf-move.test.ts": 1803, + "src/main/persistence/terminal-topology/terminal-owner-invariants.test.ts": 285, + "src/main/persistence/terminal-topology/terminal-owner-report-only.test.ts": 1617, + "src/main/persistence/terminal-topology/terminal-sleep-quit-resume.test.ts": 1542, + "src/main/persistence/terminal-topology/terminal-stale-pane-tree-client.test.ts": 5032, + "src/main/persistence/terminal-topology/terminal-topology-commit.test.ts": 508, + "src/main/persistence/terminal-topology/terminal-topology-model.test.ts": 2977, + "src/main/persistence/terminal-topology/workspace-layout-rules.test.ts": 318, + "src/main/persistence/tracking-repos/local-worktree-metadata-batch-pruning.bench.test.ts": 539, + "src/main/persistence/tracking-repos/local-worktree-metadata-scan-expectation.test.ts": 358, + "src/main/persistence/tracking-repos/missing-local-worktree-metadata-pruning.test.ts": 551, + "src/main/persistence/tracking-repos/probeable-local-worktree-metadata-candidates.test.ts": 373, + "src/main/persistence/tracking-repos/project-group-session-batch.test.ts": 490, + "src/main/persistence/tracking-repos/project-host-compatibility.test.ts": 59, + "src/main/persistence/tracking-repos/repo-update-host-guard.test.ts": 468, + "src/main/persistence/tracking-repos/worktree-identity-migration-field-coverage.test.ts": 376, + "src/main/persistence/tracking-repos/worktree-metadata-normalization.test.ts": 288, + "src/main/pi/agent-status-completion-delivery.test.ts": 1000, + "src/main/pi/agent-status-extension-async-subagents.test.ts": 1328, + "src/main/pi/agent-status-extension-nested-run-settlement.test.ts": 288, + "src/main/pi/agent-status-extension-omp-lifecycle.test.ts": 2175, + "src/main/pi/agent-status-extension-omp-model.test.ts": 832, + "src/main/pi/agent-status-extension-omp-prefill.test.ts": 613, + "src/main/pi/agent-status-extension-session-change.test.ts": 2157, + "src/main/pi/agent-status-extension-source.test.ts": 2756, + "src/main/pi/agent-status-input-redaction.test.ts": 1445, + "src/main/pi/agent-status-omp-approval-forwarding.test.ts": 598, + "src/main/pi/agent-status-owner-recovery.test.ts": 883, + "src/main/pi/agent-status-reload-delivery.test.ts": 494, + "src/main/pi/agent-status-ui-prompt.test.ts": 1748, + "src/main/pi/omp-model-command-source.test.ts": 615, + "src/main/pi/omp-session-status-owner.test.ts": 823, + "src/main/pi/omp-transcript-metadata.test.ts": 1326, + "src/main/pi/rpc-capture-replay.test.ts": 632, + "src/main/pi/rpc-configured-default.test.ts": 1020, + "src/main/pi/rpc-launch-resolution.test.ts": 1078, + "src/main/pi/rpc-launch.test.ts": 438, + "src/main/pi/rpc-location.test.ts": 1332, + "src/main/pi/rpc-message-dialog-shapes.test.ts": 266, + "src/main/pi/rpc-model-catalog-probe.test.ts": 457, + "src/main/pi/rpc-options.test.ts": 251, + "src/main/pi/rpc-prompt-delivery.test.ts": 312, + "src/main/pi/rpc-prompt.test.ts": 269, + "src/main/pi/rpc-runtime-registration.test.ts": 643, + "src/main/pi/rpc-session-adapter.test.ts": 1035, + "src/main/pi/rpc-turn-races.test.ts": 895, + "src/main/pi/titlebar-extension-overlay-path.test.ts": 80, + "src/main/pi/titlebar-extension-service.test.ts": 251, + "src/main/pi/titlebar-extension-source.test.ts": 923, + "src/main/plugins/plugin-agent-status-event.test.ts": 186, + "src/main/plugins/plugin-atomic-file-write.test.ts": 2036, + "src/main/plugins/plugin-audit-log-scaling.test.ts": 226, + "src/main/plugins/plugin-audit-log.test.ts": 41, + "src/main/plugins/plugin-bundled-bootstrap-coordinator.test.ts": 446, + "src/main/plugins/plugin-bundled-bootstrap.test.ts": 565, + "src/main/plugins/plugin-command-registry.test.ts": 319, + "src/main/plugins/plugin-content-pack-registry.test.ts": 330, + "src/main/plugins/plugin-content-safety.test.ts": 867, + "src/main/plugins/plugin-dev-watcher-unknown-identity.test.ts": 208, + "src/main/plugins/plugin-dev-watcher.test.ts": 442, + "src/main/plugins/plugin-discovery.test.ts": 320, + "src/main/plugins/plugin-enablement.test.ts": 319, + "src/main/plugins/plugin-host-methods.test.ts": 291, "src/main/plugins/plugin-host-process.test.ts": 249, "src/main/plugins/plugin-host-runtime.test.ts": 258, - "src/main/plugins/plugin-install-trust.test.ts": 464, - "src/main/plugins/plugin-install.test.ts": 819, - "src/main/plugins/plugin-kill-list-content-revocation.test.ts": 541, + "src/main/plugins/plugin-install-trust.test.ts": 470, + "src/main/plugins/plugin-install.test.ts": 810, + "src/main/plugins/plugin-kill-list-content-revocation.test.ts": 556, "src/main/plugins/plugin-kill-list-service.test.ts": 292, - "src/main/plugins/plugin-language-pack-registry.test.ts": 352, - "src/main/plugins/plugin-launch-content.test.ts": 464, - "src/main/plugins/plugin-list-projection.test.ts": 320, - "src/main/plugins/plugin-marketplace-installer.test.ts": 425, - "src/main/plugins/plugin-marketplace-service.test.ts": 526, - "src/main/plugins/plugin-marketplace-store.test.ts": 269, - "src/main/plugins/plugin-panel-controller.test.ts": 337, - "src/main/plugins/plugin-panel-owner-lifecycle.test.ts": 51, - "src/main/plugins/plugin-panel-sessions.test.ts": 56, - "src/main/plugins/plugin-private-marketplace.integration.test.ts": 715, - "src/main/plugins/plugin-secrets-store.test.ts": 295, - "src/main/plugins/plugin-service-housekeeping.test.ts": 63, - "src/main/plugins/plugin-service-integrity.test.ts": 555, - "src/main/plugins/plugin-service-reconciliation.test.ts": 841, - "src/main/plugins/plugin-startup-budget.test.ts": 757, - "src/main/plugins/plugin-storage-store.test.ts": 286, - "src/main/plugins/plugin-uninstall-lifecycle.test.ts": 994, - "src/main/plugins/plugin-uninstall-log-retention.test.ts": 1223, - "src/main/plugins/plugin-uninstall-ordering.test.ts": 892, - "src/main/plugins/plugin-vm-recipe-registry.test.ts": 426, - "src/main/plugins/plugin-worker-controller.test.ts": 371, - "src/main/plugins/plugin-worker-env.test.ts": 48, - "src/main/plugins/plugin-worker-manager.test.ts": 311, - "src/main/plugins/plugin-worker-output-buffer.test.ts": 49, - "src/main/plugins/plugin-worker-output-retention.test.ts": 379, - "src/main/plugins/plugin-worker-supervision.integration.test.ts": 8486, + "src/main/plugins/plugin-language-pack-registry.test.ts": 310, + "src/main/plugins/plugin-launch-content.test.ts": 475, + "src/main/plugins/plugin-list-projection.test.ts": 321, + "src/main/plugins/plugin-marketplace-installer.test.ts": 423, + "src/main/plugins/plugin-marketplace-service.test.ts": 513, + "src/main/plugins/plugin-marketplace-store.test.ts": 297, + "src/main/plugins/plugin-panel-controller.test.ts": 301, + "src/main/plugins/plugin-panel-owner-lifecycle.test.ts": 32, + "src/main/plugins/plugin-panel-sessions.test.ts": 33, + "src/main/plugins/plugin-private-marketplace.integration.test.ts": 768, + "src/main/plugins/plugin-secrets-store.test.ts": 284, + "src/main/plugins/plugin-service-housekeeping.test.ts": 42, + "src/main/plugins/plugin-service-integrity.test.ts": 814, + "src/main/plugins/plugin-service-reconciliation.test.ts": 1041, + "src/main/plugins/plugin-startup-budget.test.ts": 944, + "src/main/plugins/plugin-storage-store.test.ts": 236, + "src/main/plugins/plugin-uninstall-lifecycle.test.ts": 699, + "src/main/plugins/plugin-uninstall-log-retention.test.ts": 1284, + "src/main/plugins/plugin-uninstall-ordering.test.ts": 1022, + "src/main/plugins/plugin-vm-recipe-registry.test.ts": 318, + "src/main/plugins/plugin-worker-controller.test.ts": 415, + "src/main/plugins/plugin-worker-env.test.ts": 37, + "src/main/plugins/plugin-worker-manager.test.ts": 245, + "src/main/plugins/plugin-worker-output-buffer.test.ts": 29, + "src/main/plugins/plugin-worker-output-retention.test.ts": 322, + "src/main/plugins/plugin-worker-supervision.integration.test.ts": 8259, "src/main/ports/advertised-url-retention.test.ts": 145, - "src/main/ports/advertised-url-watcher-pid-validation.test.ts": 66, - "src/main/ports/advertised-url-watcher.test.ts": 91, - "src/main/ports/local-workspace-port-scanner.test.ts": 144, - "src/main/ports/port-scan-command-client.test.ts": 1724, - "src/main/ports/port-scan-command-execution.test.ts": 5262, - "src/main/ports/ssh-advertised-url-enrichment.test.ts": 83, - "src/main/ports/workspace-port-execution-host.test.ts": 59, - "src/main/ports/workspace-port-ownership.test.ts": 62, - "src/main/powershell-osc133-bootstrap-windows-clm.test.ts": 109, - "src/main/powershell-osc133-bootstrap.test.ts": 159, - "src/main/preflight/workspace-agent-detection.test.ts": 575, - "src/main/project-groups/folder-workspace-path-status.test.ts": 73, - "src/main/project-groups/nested-repo-discovery-queue.test.ts": 208, - "src/main/project-groups/nested-repo-discovery.test.ts": 271, - "src/main/project-groups/nested-repo-glob-budget.test.ts": 391, - "src/main/project-groups/nested-repo-import-target.test.ts": 56, - "src/main/project-groups/nested-repo-import.test.ts": 63, - "src/main/project-groups/nested-repo-scan-rules.test.ts": 52, - "src/main/project-runtime-git-options.test.ts": 118, - "src/main/protected-secret-persistence-concurrency.test.ts": 61, - "src/main/protected-secret-persistence.test.ts": 77, - "src/main/provider-process/managed-provider-process-fallback-tree.test.ts": 98, - "src/main/provider-process/managed-provider-process-grace.test.ts": 108, - "src/main/provider-process/managed-provider-process-output.test.ts": 101, - "src/main/provider-process/managed-provider-process-root-only.test.ts": 105, - "src/main/provider-process/managed-provider-process.test.ts": 121, - "src/main/provider-process/provider-acquisition-starts.test.ts": 46, - "src/main/provider-process/provider-process-launch.test.ts": 46, - "src/main/provider-process/provider-process-supervisor.integration.test.ts": 14407, - "src/main/provider-process/provider-process-supervisor.test.ts": 65, - "src/main/provider-process/provider-process-teardown.test.ts": 140, - "src/main/provider-process/provider-spawn-failure-report.test.ts": 51, - "src/main/provider-process/provider-stdio-write-queue.test.ts": 48, - "src/main/providers/agent-foreground-command-line.test.ts": 157, - "src/main/providers/agent-foreground-process-batch.test.ts": 89, - "src/main/providers/agent-foreground-process-git-bash-launcher.test.ts": 137, - "src/main/providers/agent-foreground-process-git-bash.win32.test.ts": 445, - "src/main/providers/agent-foreground-process-pi.test.ts": 136, - "src/main/providers/agent-foreground-process-ps-scan-volume.test.ts": 155, - "src/main/providers/agent-foreground-process-real-rows.test.ts": 141, - "src/main/providers/agent-foreground-process-remote-evidence.test.ts": 108, - "src/main/providers/agent-foreground-process-shell-path.test.ts": 131, - "src/main/providers/agent-foreground-process-windows-job-rejection.test.ts": 107, - "src/main/providers/agent-foreground-process-wsl-shell-proof.test.ts": 125, - "src/main/providers/agent-foreground-process.test.ts": 186, - "src/main/providers/execution-host-provider-dispatch.test.ts": 404, - "src/main/providers/local-filesystem-provider.test.ts": 206, - "src/main/providers/local-git-provider.test.ts": 77, - "src/main/providers/local-pty-child-process-verdict.test.ts": 498, - "src/main/providers/local-pty-default-shell.test.ts": 168, - "src/main/providers/local-pty-foreground-inspection-cheap-tier.test.ts": 202, - "src/main/providers/local-pty-pending-native-spawn.test.ts": 306, - "src/main/providers/local-pty-pi-owner-env.test.ts": 257, - "src/main/providers/local-pty-provider-foreground-process.test.ts": 645, - "src/main/providers/local-pty-provider-io-events.test.ts": 438, - "src/main/providers/local-pty-provider-session-inventory.test.ts": 475, - "src/main/providers/local-pty-provider-shell-readiness.test.ts": 521, - "src/main/providers/local-pty-provider-shutdown.test.ts": 714, - "src/main/providers/local-pty-provider-spawn-cwd-safety.test.ts": 501, - "src/main/providers/local-pty-provider-spawn-env.test.ts": 493, - "src/main/providers/local-pty-provider-spawn-session.test.ts": 539, - "src/main/providers/local-pty-provider-windows-shell-launch.test.ts": 571, - "src/main/providers/local-pty-shell-ready-marker-scan.test.ts": 82, - "src/main/providers/local-pty-shell-ready-startup-command.test.ts": 74, - "src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts": 491, - "src/main/providers/local-pty-shell-startup-command.node-pty.test.ts": 310, - "src/main/providers/local-pty-utils-windows-fallback.test.ts": 79, - "src/main/providers/local-pty-utils.test.ts": 91, - "src/main/providers/macos-login-session-pty-probe.test.ts": 54, - "src/main/providers/macos-tcc-login-shell.test.ts": 148, - "src/main/providers/posix-pane-foreground-fingerprint.test.ts": 62, - "src/main/providers/process-cwd.test.ts": 136, - "src/main/providers/provider-dispatch.test.ts": 2275, - "src/main/providers/pty-default-cwd.test.ts": 64, - "src/main/providers/pty-process-inspection.test.ts": 55, - "src/main/providers/pty-process-list-admission.test.ts": 150, - "src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts": 1987, - "src/main/providers/ssh-filesystem-dispatch.test.ts": 52, - "src/main/providers/ssh-git-dispatch.test.ts": 54, - "src/main/providers/ssh-git-provider-commit-message.test.ts": 122, - "src/main/providers/ssh-git-provider-diff.test.ts": 140, - "src/main/providers/ssh-git-provider-exec.test.ts": 126, - "src/main/providers/ssh-git-provider-merge.test.ts": 103, - "src/main/providers/ssh-git-provider-remote-sync.test.ts": 115, - "src/main/providers/ssh-git-provider-staging.test.ts": 163, - "src/main/providers/ssh-git-provider-status-lease.test.ts": 102, - "src/main/providers/ssh-git-provider-status.test.ts": 112, - "src/main/providers/ssh-git-provider-upstream-lease.test.ts": 110, - "src/main/providers/ssh-git-provider-worktree.test.ts": 149, - "src/main/providers/ssh-git-worktree-list-dedupe.test.ts": 125, - "src/main/providers/ssh-worktree-catalog-authority.test.ts": 1068, - "src/main/providers/stable-foreground-process.test.ts": 68, - "src/main/providers/startup-line-typed-length.live-shell.test.ts": 165, - "src/main/providers/terminal-path-existence-batch.integration.test.ts": 150, - "src/main/providers/windows-agent-foreground-command-line.test.ts": 102, - "src/main/providers/windows-agent-foreground-process-scan-volume.test.ts": 107, - "src/main/providers/windows-cached-agent-revalidation.test.ts": 55, - "src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts": 59, - "src/main/providers/windows-console-attached-processes.test.ts": 56, - "src/main/providers/windows-foreground-process-inspection-cost.test.ts": 105, - "src/main/providers/windows-foreground-process-rows.test.ts": 90, - "src/main/providers/windows-powershell-executable.test.ts": 76, - "src/main/providers/windows-powershell.test.ts": 42, - "src/main/providers/windows-pty-job-membership.test.ts": 68, - "src/main/providers/windows-shell-args.test.ts": 192, - "src/main/providers/windows-shell-fallback-chain.test.ts": 131, - "src/main/providers/windows-shell-preflight-runtime.windows.test.ts": 123, - "src/main/providers/working-directory-validation-retention.test.ts": 132, - "src/main/providers/working-directory-validation.test.ts": 118, - "src/main/provisioned-root-ssh-adoption.test.ts": 332, - "src/main/pty-descendant-exit-verification.test.ts": 139, - "src/main/pty-descendant-termination.test.ts": 173, - "src/main/pty/appimage-terminal-env.test.ts": 68, - "src/main/pty/build-mode-env.test.ts": 49, - "src/main/pty/chromium-session-bus-env.test.ts": 202, - "src/main/pty/codex-home-wsl-env.test.ts": 44, - "src/main/pty/codex-launch-shell-wrapping.test.ts": 180, - "src/main/pty/codex-no-daemon-binary-contract.test.ts": 44, - "src/main/pty/codex-no-daemon-launch-command.test.ts": 96, - "src/main/pty/codex-preflight-profile-path-rewrite.test.ts": 137, - "src/main/pty/codex-shell-launch-preflight.test.ts": 2497, - "src/main/pty/codex-shell-no-daemon.test.ts": 7782, - "src/main/pty/conda-activation-env.test.ts": 64, - "src/main/pty/legacy-terminal-shim-dir.test.ts": 287, - "src/main/pty/legacy-terminal-windows-tombstone.test.ts": 85, - "src/main/pty/managed-ssh-host-login-shell.test.ts": 179, - "src/main/pty/node-pty-master-fd-retirement.test.ts": 2373, - "src/main/pty/node-pty-self-exit-pseudoconsole-close.test.ts": 55, - "src/main/pty/omp-sqlite-overlay.test.ts": 51, - "src/main/pty/overlay-mirror.test.ts": 69, - "src/main/pty/pi-process-owner-env.test.ts": 909, - "src/main/pty/posix-pty-foreground-group.test.ts": 65, - "src/main/pty/posix-pty-process-groups.integration.test.ts": 167, - "src/main/pty/posix-pty-process-groups.test.ts": 105, - "src/main/pty/powershell-profile-env.test.ts": 98, - "src/main/pty/shell-startup-env.test.ts": 131, - "src/main/pty/terminal-color-env.test.ts": 46, - "src/main/pty/windows-environment-path-main-loop.test.ts": 430, - "src/main/pty/windows-environment-path.test.ts": 118, - "src/main/pty/windows-path-registry-change.test.ts": 49, - "src/main/pty/windows-path-registry-fallback.test.ts": 69, - "src/main/pty/windows-path-registry-reader.test.ts": 55, + "src/main/ports/advertised-url-watcher-pid-validation.test.ts": 45, + "src/main/ports/advertised-url-watcher.test.ts": 69, + "src/main/ports/local-workspace-port-scanner.test.ts": 174, + "src/main/ports/port-scan-command-client.test.ts": 1705, + "src/main/ports/port-scan-command-execution.test.ts": 5255, + "src/main/ports/ssh-advertised-url-enrichment.test.ts": 86, + "src/main/ports/workspace-port-execution-host.test.ts": 61, + "src/main/ports/workspace-port-ownership.test.ts": 54, + "src/main/powershell-osc133-bootstrap-windows-clm.test.ts": 136, + "src/main/powershell-osc133-bootstrap.test.ts": 145, + "src/main/preflight/agent-command-detection.test.ts": 96, + "src/main/preflight/codex-cli-installation.test.ts": 68, + "src/main/preflight/codex-workspace-relative-path.test.ts": 1298, + "src/main/preflight/workspace-agent-detection.test.ts": 608, + "src/main/project-groups/folder-workspace-path-status.test.ts": 57, + "src/main/project-groups/nested-repo-discovery-queue.test.ts": 232, + "src/main/project-groups/nested-repo-discovery.test.ts": 305, + "src/main/project-groups/nested-repo-glob-budget.test.ts": 302, + "src/main/project-groups/nested-repo-import-target.test.ts": 35, + "src/main/project-groups/nested-repo-import.test.ts": 54, + "src/main/project-groups/nested-repo-scan-rules.test.ts": 40, + "src/main/project-runtime-git-options.test.ts": 291, + "src/main/protected-secret-persistence-concurrency.test.ts": 51, + "src/main/protected-secret-persistence.test.ts": 55, + "src/main/provider-process/managed-provider-process-fallback-tree.test.ts": 83, + "src/main/provider-process/managed-provider-process-grace.test.ts": 101, + "src/main/provider-process/managed-provider-process-output.test.ts": 106, + "src/main/provider-process/managed-provider-process-root-only.test.ts": 88, + "src/main/provider-process/managed-provider-process.test.ts": 114, + "src/main/provider-process/provider-acquisition-starts.test.ts": 35, + "src/main/provider-process/provider-process-launch.test.ts": 39, + "src/main/provider-process/provider-process-supervisor.integration.test.ts": 7553, + "src/main/provider-process/provider-process-supervisor.test.ts": 42, + "src/main/provider-process/provider-process-teardown.test.ts": 121, + "src/main/provider-process/provider-spawn-failure-report.test.ts": 38, + "src/main/provider-process/provider-stdio-write-queue.test.ts": 47, + "src/main/providers/agent-foreground-command-line.test.ts": 125, + "src/main/providers/agent-foreground-process-batch.test.ts": 106, + "src/main/providers/agent-foreground-process-git-bash-launcher.test.ts": 132, + "src/main/providers/agent-foreground-process-git-bash.win32.test.ts": 423, + "src/main/providers/agent-foreground-process-pi.test.ts": 116, + "src/main/providers/agent-foreground-process-ps-scan-volume.test.ts": 213, + "src/main/providers/agent-foreground-process-real-rows.test.ts": 115, + "src/main/providers/agent-foreground-process-remote-evidence.test.ts": 112, + "src/main/providers/agent-foreground-process-shell-path.test.ts": 103, + "src/main/providers/agent-foreground-process-windows-job-rejection.test.ts": 108, + "src/main/providers/agent-foreground-process-wsl-shell-proof.test.ts": 81, + "src/main/providers/agent-foreground-process.test.ts": 204, + "src/main/providers/execution-host-provider-dispatch.test.ts": 561, + "src/main/providers/filesystem-markdown-listing.test.ts": 77, + "src/main/providers/local-filesystem-provider.test.ts": 365, + "src/main/providers/local-git-provider.test.ts": 103, + "src/main/providers/local-pty-child-process-verdict.test.ts": 605, + "src/main/providers/local-pty-default-shell.test.ts": 157, + "src/main/providers/local-pty-foreground-inspection-cheap-tier.test.ts": 185, + "src/main/providers/local-pty-pending-native-spawn.test.ts": 341, + "src/main/providers/local-pty-pi-owner-env.test.ts": 251, + "src/main/providers/local-pty-provider-foreground-process.test.ts": 588, + "src/main/providers/local-pty-provider-io-events.test.ts": 526, + "src/main/providers/local-pty-provider-session-inventory.test.ts": 490, + "src/main/providers/local-pty-provider-shell-readiness.test.ts": 501, + "src/main/providers/local-pty-provider-shutdown.test.ts": 811, + "src/main/providers/local-pty-provider-spawn-cwd-safety.test.ts": 457, + "src/main/providers/local-pty-provider-spawn-env.test.ts": 554, + "src/main/providers/local-pty-provider-spawn-session.test.ts": 864, + "src/main/providers/local-pty-provider-windows-shell-launch.test.ts": 543, + "src/main/providers/local-pty-shell-ready-marker-scan.test.ts": 66, + "src/main/providers/local-pty-shell-ready-startup-command.test.ts": 51, + "src/main/providers/local-pty-shell-ready-wrapper-generation.test.ts": 472, + "src/main/providers/local-pty-shell-startup-command.node-pty.test.ts": 233, + "src/main/providers/local-pty-utils-windows-fallback.test.ts": 63, + "src/main/providers/local-pty-utils.test.ts": 78, + "src/main/providers/macos-login-session-pty-probe.test.ts": 61, + "src/main/providers/macos-tcc-login-shell.test.ts": 162, + "src/main/providers/posix-pane-foreground-fingerprint.test.ts": 70, + "src/main/providers/process-cwd.test.ts": 121, + "src/main/providers/provider-dispatch.test.ts": 2388, + "src/main/providers/pty-default-cwd.test.ts": 30, + "src/main/providers/pty-process-inspection.test.ts": 34, + "src/main/providers/pty-process-list-admission.test.ts": 320, + "src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts": 2007, + "src/main/providers/stable-foreground-process.test.ts": 75, + "src/main/providers/startup-line-typed-length.live-shell.test.ts": 135, + "src/main/providers/terminal-path-existence-batch.integration.test.ts": 109, + "src/main/providers/windows-agent-foreground-command-line.test.ts": 85, + "src/main/providers/windows-agent-foreground-process-scan-volume.test.ts": 125, + "src/main/providers/windows-cached-agent-revalidation.test.ts": 49, + "src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts": 52, + "src/main/providers/windows-console-attached-processes.test.ts": 48, + "src/main/providers/windows-foreground-process-inspection-cost.test.ts": 107, + "src/main/providers/windows-foreground-process-rows.test.ts": 66, + "src/main/providers/windows-powershell-executable.test.ts": 44, + "src/main/providers/windows-powershell.test.ts": 38, + "src/main/providers/windows-pty-job-membership.test.ts": 64, + "src/main/providers/windows-shell-args.test.ts": 198, + "src/main/providers/windows-shell-fallback-chain.test.ts": 113, + "src/main/providers/windows-shell-preflight-runtime.windows.test.ts": 114, + "src/main/providers/working-directory-validation-retention.test.ts": 152, + "src/main/providers/working-directory-validation.test.ts": 144, + "src/main/provisioned-root-checkout.test.ts": 103, + "src/main/provisioned-root-ssh-adoption.test.ts": 408, + "src/main/pty-descendant-exit-verification.test.ts": 148, + "src/main/pty-descendant-termination.test.ts": 170, + "src/main/pty/appimage-terminal-env.test.ts": 45, + "src/main/pty/build-mode-env.test.ts": 41, + "src/main/pty/chromium-session-bus-env.test.ts": 319, + "src/main/pty/codex-home-wsl-env.test.ts": 30, + "src/main/pty/codex-launch-shell-wrapping.test.ts": 114, + "src/main/pty/codex-no-daemon-binary-contract.test.ts": 26, + "src/main/pty/codex-no-daemon-launch-command.test.ts": 92, + "src/main/pty/codex-preflight-profile-path-rewrite.test.ts": 122, + "src/main/pty/codex-shell-launch-preflight.test.ts": 2405, + "src/main/pty/codex-shell-no-daemon.test.ts": 11219, + "src/main/pty/conda-activation-env.test.ts": 40, + "src/main/pty/legacy-terminal-shim-dir.test.ts": 258, + "src/main/pty/legacy-terminal-windows-tombstone.test.ts": 58, + "src/main/pty/managed-ssh-host-login-shell.test.ts": 199, + "src/main/pty/node-pty-master-fd-retirement.test.ts": 2422, + "src/main/pty/node-pty-self-exit-pseudoconsole-close.test.ts": 57, + "src/main/pty/overlay-mirror.test.ts": 51, + "src/main/pty/pi-process-owner-env.test.ts": 821, + "src/main/pty/posix-pty-foreground-group.test.ts": 43, + "src/main/pty/posix-pty-process-groups.integration.test.ts": 156, + "src/main/pty/posix-pty-process-groups.test.ts": 95, + "src/main/pty/powershell-profile-env.test.ts": 80, + "src/main/pty/shell-startup-env.test.ts": 84, + "src/main/pty/terminal-color-env.test.ts": 29, + "src/main/pty/windows-environment-path-main-loop.test.ts": 425, + "src/main/pty/windows-environment-path.test.ts": 127, + "src/main/pty/windows-path-registry-change.test.ts": 62, + "src/main/pty/windows-path-registry-fallback.test.ts": 35, + "src/main/pty/windows-path-registry-reader.test.ts": 46, "src/main/pty/wsl-orca-env.test.ts": 107, - "src/main/pwsh.test.ts": 78, - "src/main/qoder/hook-service.test.ts": 146, - "src/main/qoder/real-cli.test.ts": 709, - "src/main/quit-path-durable-write-blocking.test.ts": 3220, - "src/main/quit-teardown-deadline.test.ts": 54, - "src/main/quit-teardown-start-gate.test.ts": 38, - "src/main/rate-limits/account-runtime-target-sync.test.ts": 277, - "src/main/rate-limits/antigravity-usage-error.test.ts": 52, - "src/main/rate-limits/antigravity-usage-fetcher.test.ts": 103, - "src/main/rate-limits/antigravity-usage-real-cli.test.ts": 124, - "src/main/rate-limits/antigravity-usage-response.test.ts": 66, - "src/main/rate-limits/auth-filesystem-operation-retention.test.ts": 140, - "src/main/rate-limits/auth-filesystem-operation.test.ts": 95, - "src/main/rate-limits/claude-oauth-usage-error.test.ts": 44, - "src/main/rate-limits/claude-oauth-usage-extra-usage.test.ts": 84, - "src/main/rate-limits/claude-profile-usage.test.ts": 275, - "src/main/rate-limits/claude-usage-error-classification.test.ts": 58, - "src/main/rate-limits/codex-auth-presence.test.ts": 63, - "src/main/rate-limits/codex-fetcher-auth-errors.test.ts": 120, - "src/main/rate-limits/codex-fetcher-backend.test.ts": 267, - "src/main/rate-limits/codex-fetcher-credit-balance.test.ts": 111, - "src/main/rate-limits/codex-fetcher-probe-shutdown.test.ts": 166, - "src/main/rate-limits/codex-fetcher-process-contract.test.ts": 237, - "src/main/rate-limits/codex-fetcher-rpc-exit-diagnostics.test.ts": 358, - "src/main/rate-limits/codex-fetcher-runtime-pairing.test.ts": 136, - "src/main/rate-limits/codex-fetcher-session-supplement.test.ts": 152, - "src/main/rate-limits/codex-fetcher.test.ts": 265, - "src/main/rate-limits/codex-probe-termination.test.ts": 81, - "src/main/rate-limits/codex-rate-limit-window-classification.test.ts": 41, - "src/main/rate-limits/codex-rpc-rate-limit-probe.test.ts": 156, - "src/main/rate-limits/cursor-auth.test.ts": 220, - "src/main/rate-limits/cursor-fetcher.test.ts": 370, - "src/main/rate-limits/cursor-session-token.test.ts": 214, - "src/main/rate-limits/cursor-usage-mapping.test.ts": 247, - "src/main/rate-limits/gemini-bucket-formatting.test.ts": 60, - "src/main/rate-limits/gemini-usage-fetcher.fallback.test.ts": 88, - "src/main/rate-limits/gemini-usage-fetcher.test.ts": 112, - "src/main/rate-limits/grok-auth.test.ts": 103, - "src/main/rate-limits/grok-fetcher.test.ts": 97, - "src/main/rate-limits/grok-weekly-zero.test.ts": 180, - "src/main/rate-limits/initial-account-rate-limit-target.test.ts": 295, - "src/main/rate-limits/kimi-fetcher-wsl-home.test.ts": 107, - "src/main/rate-limits/kimi-fetcher.test.ts": 175, - "src/main/rate-limits/minimax/minimax-fetcher.test.ts": 122, - "src/main/rate-limits/minimax/minimax-request-context.test.ts": 97, - "src/main/rate-limits/opencode-go-api-key-source.test.ts": 452, - "src/main/rate-limits/opencode-go-managed-account-source.test.ts": 514, - "src/main/rate-limits/opencode-go-status-parsing.test.ts": 56, - "src/main/rate-limits/opencode-go-usage-api.test.ts": 77, - "src/main/rate-limits/opencode-go-usage-fetcher.test.ts": 136, - "src/main/rate-limits/opencode-go-usage-source-selection.test.ts": 107, - "src/main/rate-limits/service-account-target-selection.test.ts": 1032, - "src/main/rate-limits/service-antigravity-usage.test.ts": 139, - "src/main/rate-limits/service-cursor-usage.test.ts": 430, - "src/main/rate-limits/service-inactive-account-previews.test.ts": 158, - "src/main/rate-limits/service-live-claude-usage.test.ts": 188, - "src/main/rate-limits/service-minimax-usage.test.ts": 159, - "src/main/rate-limits/service-opencode-go-credentials.test.ts": 136, - "src/main/rate-limits/service-refresh-orchestration.test.ts": 177, - "src/main/rate-limits/service-window-activation.test.ts": 363, - "src/main/rate-limits/service-zcode-usage.test.ts": 322, - "src/main/rate-limits/zcode-usage-fetcher.test.ts": 77, - "src/main/refused-tree-kill-root-termination.test.ts": 293, - "src/main/repo-git-remote-avatar-refresh.test.ts": 135, - "src/main/repo-git-remote-identity-enrichment.test.ts": 119, - "src/main/repo-git-remote-identity.test.ts": 402, - "src/main/repo-git-username-enrichment.test.ts": 65, - "src/main/repo-icon-autodetect.test.ts": 826, - "src/main/repo-icon-file-detection.test.ts": 455, - "src/main/repo-icon-source-href.test.ts": 81, - "src/main/repo-maintenance-idle-gate.test.ts": 219, - "src/main/repo-worktrees.test.ts": 372, - "src/main/ripgrep/bundled-ripgrep-path.test.ts": 107, - "src/main/ripgrep/bundled-ripgrep-real.test.ts": 166, - "src/main/ripgrep/bundled-ripgrep-spawn.test.ts": 80, - "src/main/ripgrep/bundled-ripgrep-stop.test.ts": 84, - "src/main/ripgrep/bundled-ripgrep-text-search.test.ts": 207, - "src/main/ripgrep/fd-exhausted-spawn.test.ts": 75, - "src/main/ripgrep/ripgrep-filename-identity-real.test.ts": 209, - "src/main/ripgrep/synchronous-spawn-failure.test.ts": 69, - "src/main/ripgrep/text-search-unicode-real.test.ts": 228, - "src/main/ripgrep/wsl-missing-cwd-exit.test.ts": 124, - "src/main/runtime/acknowledged-terminal-tab-retirement.test.ts": 5402, - "src/main/runtime/agent-detection-host-resolution.test.ts": 3351, - "src/main/runtime/agent-launch-record-warmup-gate.test.ts": 42, - "src/main/runtime/agent-launch-typed-line-shell.test.ts": 50, - "src/main/runtime/agent-prompt-line-settle.test.ts": 4010, - "src/main/runtime/agent-prompt-receipt-correlation.test.ts": 4528, - "src/main/runtime/agent-prompt-request-correlation.test.ts": 46, - "src/main/runtime/agent-prompt-submission-runtime-hook-and-generation.test.ts": 5685, - "src/main/runtime/agent-prompt-submission-runtime.test.ts": 6589, - "src/main/runtime/agent-prompt-submission-verification.test.ts": 140, - "src/main/runtime/agent-prompt-submission-windows-submit-delay.test.ts": 4945, - "src/main/runtime/agent-session-claim-identity.test.ts": 67, - "src/main/runtime/agent-session-conversation-clear-commit.test.ts": 705, - "src/main/runtime/agent-session-conversation-name-store.test.ts": 703, - "src/main/runtime/agent-session-death-evidence-persistence.test.ts": 730, - "src/main/runtime/agent-session-launch-directory.test.ts": 583, - "src/main/runtime/agent-session-launch-env-admission.test.ts": 812, - "src/main/runtime/agent-session-lease-renewal.test.ts": 706, - "src/main/runtime/agent-session-operation-admission.test.ts": 308, - "src/main/runtime/agent-session-process-identity-probe-windows-batch.test.ts": 88, - "src/main/runtime/agent-session-process-identity-probe.test.ts": 93, - "src/main/runtime/agent-session-provider-handle-transition.test.ts": 241, - "src/main/runtime/agent-session-record-conversation-name.test.ts": 296, - "src/main/runtime/agent-session-record-options.test.ts": 741, - "src/main/runtime/agent-session-record-store-slot.test.ts": 1888, - "src/main/runtime/agent-session-record-store-tab-table.test.ts": 847, - "src/main/runtime/agent-session-record-store.test.ts": 1364, - "src/main/runtime/agent-session-recovery-capsule.test.ts": 1090, - "src/main/runtime/agent-session-released-reservation-replay.test.ts": 851, - "src/main/runtime/agent-session-replaced-runtime.test.ts": 238, - "src/main/runtime/agent-session-reservation-admission.test.ts": 291, - "src/main/runtime/agent-session-runtime-attribution.test.ts": 799, - "src/main/runtime/agent-session-runtime-end-record.test.ts": 314, - "src/main/runtime/agent-session-spawn-token-process-scan.test.ts": 82, - "src/main/runtime/agent-session-spawn-token-readback.test.ts": 82, - "src/main/runtime/agent-session-store-transactions.test.ts": 575, - "src/main/runtime/agent-session-surface-release-transition.test.ts": 240, - "src/main/runtime/agent-session-unsaved-creation-supersession.test.ts": 730, - "src/main/runtime/agent-state-rules/agent-state-rule-regions.test.ts": 354, - "src/main/runtime/agent-state-rules/agent-state-rules-bundle.test.ts": 239, - "src/main/runtime/agent-state-rules/agent-state-rules-engine.test.ts": 325, - "src/main/runtime/agent-state-rules/agent-state-rules-live-update.test.ts": 470, - "src/main/runtime/agent-state-rules/agent-state-text-anchors.test.ts": 209, - "src/main/runtime/agent-terminal-launch-host.test.ts": 4780, - "src/main/runtime/agent-terminal-startup-prompt.test.ts": 4385, - "src/main/runtime/antigravity-readiness-transcripts.test.ts": 9127, - "src/main/runtime/antigravity-terminal-readiness.test.ts": 318, - "src/main/runtime/automatic-activation-host-sleep.test.ts": 5209, - "src/main/runtime/automation-change-publication.test.ts": 13953, - "src/main/runtime/browser-client-download-transfers.test.ts": 341, - "src/main/runtime/browser-dialog-settlement.test.ts": 415, - "src/main/runtime/browser-execution-host-key-resolution.test.ts": 4603, - "src/main/runtime/browser-host-capability-selection.test.ts": 270, - "src/main/runtime/browser-host-client-page-adoption-grants.test.ts": 56, - "src/main/runtime/browser-host-client-page-adoption.test.ts": 53, - "src/main/runtime/browser-host-client-page-creation.test.ts": 298, - "src/main/runtime/browser-host-command-ledger-capacity.test.ts": 233, - "src/main/runtime/browser-host-command-ledger.test.ts": 267, - "src/main/runtime/browser-host-file-channel-admission.test.ts": 228, - "src/main/runtime/browser-host-lease-download-transfer-cleanup.test.ts": 407, - "src/main/runtime/browser-host-lease-fenced-page-release.test.ts": 271, - "src/main/runtime/browser-host-lease-placement-retirement.test.ts": 299, - "src/main/runtime/browser-host-lease-registry.test.ts": 279, - "src/main/runtime/browser-host-page-placement-replacement.test.ts": 299, - "src/main/runtime/browser-host-page-placement.test.ts": 299, - "src/main/runtime/browser-host-page-reconciliation-executor.test.ts": 89, - "src/main/runtime/browser-host-page-reconciliation-orchestration.test.ts": 512, - "src/main/runtime/browser-host-page-reconciliation-plan.test.ts": 83, - "src/main/runtime/browser-host-page-reconciliation-preconditions.test.ts": 277, - "src/main/runtime/browser-host-page-retirement.test.ts": 58, - "src/main/runtime/browser-network-tunnel-paired-runtime.integration.test.ts": 5322, - "src/main/runtime/browser-screencast-driver-attribution.test.ts": 4877, - "src/main/runtime/browser-screencast-driver-scope.test.ts": 46, - "src/main/runtime/browser-screencast-ghost-subscriber-eviction.test.ts": 67, - "src/main/runtime/browser-screencast-remote-viewer-retention.test.ts": 5765, - "src/main/runtime/browser-screencast-renderer-throttle-lease.test.ts": 5601, - "src/main/runtime/browser-session-tab-selection-snapshot.test.ts": 60, - "src/main/runtime/browser-tab-create-caller-navigation.test.ts": 4661, - "src/main/runtime/browser-tab-create-folder-workspace.test.ts": 407, - "src/main/runtime/browser-tab-create-publication.test.ts": 550, - "src/main/runtime/claude-agent-teams-pty-exit-leak.test.ts": 4658, - "src/main/runtime/claude-agent-teams-service.test.ts": 92, - "src/main/runtime/claude-agent-teams-shim-env.test.ts": 79, - "src/main/runtime/claude-structured-exit-mid-response.test.ts": 1386, - "src/main/runtime/claude-structured-failed-start-resume.test.ts": 1942, - "src/main/runtime/claude-structured-idle-releases-unknown.test.ts": 2200, - "src/main/runtime/claude-structured-resumed-start-failure.test.ts": 1787, - "src/main/runtime/claude-structured-retired-saved-model.test.ts": 1984, - "src/main/runtime/claude-structured-send-during-startup.test.ts": 2343, - "src/main/runtime/claude-structured-send-restart-dies-before-dispatch.test.ts": 2482, - "src/main/runtime/claude-structured-session-integration.test.ts": 3307, - "src/main/runtime/claude-structured-startup-fault-is-not-an-exit.test.ts": 1987, - "src/main/runtime/claude-structured-startup-unanswered-control-request.test.ts": 2142, - "src/main/runtime/claude-trust-dialog-transcript.test.ts": 3395, - "src/main/runtime/cli-terminal-create-host-session-binding.test.ts": 4725, - "src/main/runtime/client-hosted-browser-page-persistence.integration.test.ts": 4478, - "src/main/runtime/client-hosted-browser-page-persistence.test.ts": 369, - "src/main/runtime/client-hosted-browser-row-projection.test.ts": 54, - "src/main/runtime/client-hosted-browser-row-publication.test.ts": 77, - "src/main/runtime/client-hosted-browser-row-push.integration.test.ts": 4403, - "src/main/runtime/client-hosted-page-reconciliation-hold.integration.test.ts": 4758, - "src/main/runtime/client-hosted-page-reconciliation-window.test.ts": 44, - "src/main/runtime/client-session-tab-selection.test.ts": 68, - "src/main/runtime/cline-screen-readiness-transcripts.test.ts": 8000, - "src/main/runtime/codex-header-readiness-transcripts.test.ts": 5220, - "src/main/runtime/codex-quiet-ready-screen.test.ts": 8412, - "src/main/runtime/codex-startup-paste-transcripts.test.ts": 503, - "src/main/runtime/created-mobile-session-tab-caller-selection.test.ts": 4175, - "src/main/runtime/decorative-title-fact-emission.test.ts": 48, - "src/main/runtime/draft-paste-ready-scanner-chunking.test.ts": 1720, - "src/main/runtime/dsb-terminal-runtime.test.ts": 4441, - "src/main/runtime/dsh-readiness-transcript.test.ts": 70, - "src/main/runtime/e2ee-keypair.test.ts": 92, - "src/main/runtime/exit-provenance-audit.test.ts": 5831, - "src/main/runtime/expired-ssh-lease-pane-candidacy.test.ts": 6032, - "src/main/runtime/external-worktree-paired-client-discovery.integration.test.ts": 5078, - "src/main/runtime/fetch-remote-cache.test.ts": 4636, - "src/main/runtime/file-watcher-host.test.ts": 393, - "src/main/runtime/fit-override-integration.test.ts": 4532, - "src/main/runtime/floating-terminal-inventory-ownership.test.ts": 4508, - "src/main/runtime/folder-workspace-pty-identity.test.ts": 4443, - "src/main/runtime/graph-sync-deletion-fence.test.ts": 4769, - "src/main/runtime/graph-sync-live-daemon-pty-tab-preservation.test.ts": 4728, - "src/main/runtime/graph-sync-mobile-snapshot-gating.test.ts": 4118, - "src/main/runtime/graph-sync-payload-partition.test.ts": 4542, - "src/main/runtime/headless-close-keeps-publication-epoch.test.ts": 4883, - "src/main/runtime/headless-hydration-ownership.test.ts": 6383, - "src/main/runtime/headless-placeholder-graph-surface-standing.test.ts": 4474, - "src/main/runtime/headless-runtime-graph.test.ts": 4867, - "src/main/runtime/headless-seed-ownership.test.ts": 6450, - "src/main/runtime/headless-tab-group-selection.test.ts": 39, - "src/main/runtime/headless-tab-order-stability.test.ts": 5018, - "src/main/runtime/headless-terminal-dispose-write-ordering.test.ts": 4223, - "src/main/runtime/headless-terminal-query-reply-policy.test.ts": 55, - "src/main/runtime/headless-terminal-split-layout.test.ts": 62, - "src/main/runtime/hidden-output-restored-provider-snapshot.test.ts": 4953, - "src/main/runtime/host-descriptor.test.ts": 299, - "src/main/runtime/host-terminal-close-persistence-durability.test.ts": 1889, - "src/main/runtime/launch-agent-terminal-followup.test.ts": 4203, - "src/main/runtime/launched-agent-composer-readiness-claude.test.ts": 4003, - "src/main/runtime/launched-agent-composer-readiness.test.ts": 581, - "src/main/runtime/launched-agent-crash-guard.live-shell.test.ts": 209, - "src/main/runtime/launched-agent-exit-delivery.test.ts": 342, - "src/main/runtime/launched-agent-foreground.test.ts": 92, - "src/main/runtime/launched-agent-shell-handoff-readiness.test.ts": 101, - "src/main/runtime/launched-agent-write-guard.test.ts": 63, - "src/main/runtime/leaf-pty-verdict-expiry.test.ts": 5707, - "src/main/runtime/linear-save-issue.test.ts": 4330, - "src/main/runtime/managed-worktree-create-execution-host.test.ts": 4606, - "src/main/runtime/managed-worktree-create-telemetry.test.ts": 4411, - "src/main/runtime/missing-worktree-terminal-reconciliation.test.ts": 347, - "src/main/runtime/mobile-agent-status-permission-renewal.test.ts": 4833, + "src/main/pwsh.test.ts": 62, + "src/main/qoder/hook-service.test.ts": 166, + "src/main/qoder/real-cli.test.ts": 684, + "src/main/quit-path-durable-write-blocking.test.ts": 3083, + "src/main/quit-teardown-deadline.test.ts": 57, + "src/main/quit-teardown-start-gate.test.ts": 52, + "src/main/rate-limits/account-runtime-target-sync.test.ts": 407, + "src/main/rate-limits/antigravity-usage-error.test.ts": 43, + "src/main/rate-limits/antigravity-usage-fetcher.test.ts": 110, + "src/main/rate-limits/antigravity-usage-real-cli.test.ts": 92, + "src/main/rate-limits/antigravity-usage-response.test.ts": 50, + "src/main/rate-limits/auth-filesystem-operation-retention.test.ts": 119, + "src/main/rate-limits/auth-filesystem-operation.test.ts": 36, + "src/main/rate-limits/claude-managed-usage-credential-source.test.ts": 282, + "src/main/rate-limits/claude-oauth-usage-error.test.ts": 38, + "src/main/rate-limits/claude-oauth-usage-extra-usage.test.ts": 49, + "src/main/rate-limits/claude-profile-usage.test.ts": 307, + "src/main/rate-limits/claude-usage-error-classification.test.ts": 60, + "src/main/rate-limits/codex-auth-presence.test.ts": 62, + "src/main/rate-limits/codex-fetcher-auth-errors.test.ts": 129, + "src/main/rate-limits/codex-fetcher-backend.test.ts": 337, + "src/main/rate-limits/codex-fetcher-credit-balance.test.ts": 169, + "src/main/rate-limits/codex-fetcher-probe-shutdown.test.ts": 179, + "src/main/rate-limits/codex-fetcher-process-contract.test.ts": 315, + "src/main/rate-limits/codex-fetcher-rpc-exit-diagnostics.test.ts": 401, + "src/main/rate-limits/codex-fetcher-runtime-pairing.test.ts": 156, + "src/main/rate-limits/codex-fetcher-session-supplement.test.ts": 177, + "src/main/rate-limits/codex-fetcher.test.ts": 209, + "src/main/rate-limits/codex-probe-termination.test.ts": 77, + "src/main/rate-limits/codex-rate-limit-window-classification.test.ts": 43, + "src/main/rate-limits/codex-rpc-failure-kind.test.ts": 31, + "src/main/rate-limits/codex-rpc-rate-limit-probe.test.ts": 113, + "src/main/rate-limits/cursor-auth.test.ts": 194, + "src/main/rate-limits/cursor-fetcher.test.ts": 281, + "src/main/rate-limits/cursor-session-token.test.ts": 228, + "src/main/rate-limits/cursor-usage-mapping.test.ts": 252, + "src/main/rate-limits/gemini-bucket-formatting.test.ts": 39, + "src/main/rate-limits/gemini-usage-fetcher.fallback.test.ts": 102, + "src/main/rate-limits/gemini-usage-fetcher.test.ts": 70, + "src/main/rate-limits/grok-auth.test.ts": 96, + "src/main/rate-limits/grok-fetcher.test.ts": 88, + "src/main/rate-limits/grok-weekly-zero.test.ts": 144, + "src/main/rate-limits/initial-account-rate-limit-target.test.ts": 277, + "src/main/rate-limits/kimi-fetcher-wsl-home.test.ts": 112, + "src/main/rate-limits/kimi-fetcher.test.ts": 126, + "src/main/rate-limits/minimax/minimax-fetcher.test.ts": 141, + "src/main/rate-limits/minimax/minimax-request-context.test.ts": 107, + "src/main/rate-limits/opencode-go-api-key-source.test.ts": 477, + "src/main/rate-limits/opencode-go-managed-account-source.test.ts": 546, + "src/main/rate-limits/opencode-go-status-parsing.test.ts": 58, + "src/main/rate-limits/opencode-go-usage-api.test.ts": 56, + "src/main/rate-limits/opencode-go-usage-fetcher.test.ts": 155, + "src/main/rate-limits/opencode-go-usage-source-selection.test.ts": 68, + "src/main/rate-limits/service-account-target-selection.test.ts": 1005, + "src/main/rate-limits/service-antigravity-usage.test.ts": 306, + "src/main/rate-limits/service-cursor-usage.test.ts": 494, + "src/main/rate-limits/service-inactive-account-previews.test.ts": 323, + "src/main/rate-limits/service-inactive-claude-cooldown.test.ts": 592, + "src/main/rate-limits/service-live-claude-usage.test.ts": 351, + "src/main/rate-limits/service-minimax-usage.test.ts": 325, + "src/main/rate-limits/service-opencode-go-credentials.test.ts": 281, + "src/main/rate-limits/service-refresh-orchestration.test.ts": 341, + "src/main/rate-limits/service-window-activation.test.ts": 532, + "src/main/rate-limits/service-zcode-usage.test.ts": 570, + "src/main/rate-limits/usage-request-failure-kind.test.ts": 35, + "src/main/rate-limits/zcode-usage-fetcher.test.ts": 290, + "src/main/refused-tree-kill-root-termination.test.ts": 334, + "src/main/remote-layout-snapshot/remote-layout-snapshot-persistence.test.ts": 1277, + "src/main/remote-layout-snapshot/remote-layout-snapshot-recorder.test.ts": 1617, + "src/main/repo-backfill-wsl-move.test.ts": 1562, + "src/main/repo-git-remote-avatar-refresh.test.ts": 501, + "src/main/repo-git-remote-identity-enrichment.test.ts": 464, + "src/main/repo-git-remote-identity.test.ts": 519, + "src/main/repo-git-username-enrichment.test.ts": 49, + "src/main/repo-icon-autodetect.test.ts": 755, + "src/main/repo-icon-file-detection.test.ts": 552, + "src/main/repo-icon-source-href.test.ts": 104, + "src/main/repo-maintenance-idle-gate.test.ts": 170, + "src/main/repo-worktrees.test.ts": 564, + "src/main/ripgrep/bundled-ripgrep-path.test.ts": 91, + "src/main/ripgrep/bundled-ripgrep-real.test.ts": 196, + "src/main/ripgrep/bundled-ripgrep-spawn.test.ts": 61, + "src/main/ripgrep/bundled-ripgrep-stop.test.ts": 63, + "src/main/ripgrep/bundled-ripgrep-text-search.test.ts": 205, + "src/main/ripgrep/bundled-ripgrep-wsl-routing.test.ts": 73, + "src/main/ripgrep/ripgrep-filename-identity-real.test.ts": 234, + "src/main/ripgrep/synchronous-spawn-failure.test.ts": 64, + "src/main/ripgrep/text-search-unicode-real.test.ts": 218, + "src/main/ripgrep/wsl-missing-cwd-exit.test.ts": 71, + "src/main/runtime/acknowledged-terminal-tab-retirement.test.ts": 4261, + "src/main/runtime/agent-detection-host-resolution.test.ts": 5077, + "src/main/runtime/agent-launch-record-warmup-gate.test.ts": 35, + "src/main/runtime/agent-launch-scope-runtime.test.ts": 270, + "src/main/runtime/agent-launch-typed-line-shell.test.ts": 34, + "src/main/runtime/agent-prompt-line-settle.test.ts": 4905, + "src/main/runtime/agent-prompt-receipt-correlation.test.ts": 4665, + "src/main/runtime/agent-prompt-request-correlation.test.ts": 31, + "src/main/runtime/agent-prompt-submission-runtime-hook-and-generation.test.ts": 4319, + "src/main/runtime/agent-prompt-submission-runtime.test.ts": 5371, + "src/main/runtime/agent-prompt-submission-verification.test.ts": 120, + "src/main/runtime/agent-prompt-submission-windows-submit-delay.test.ts": 4179, + "src/main/runtime/agent-session-claim-identity.test.ts": 213, + "src/main/runtime/agent-session-conversation-clear-commit.test.ts": 726, + "src/main/runtime/agent-session-conversation-name-store.test.ts": 734, + "src/main/runtime/agent-session-death-evidence-persistence.test.ts": 902, + "src/main/runtime/agent-session-launch-directory.test.ts": 818, + "src/main/runtime/agent-session-launch-env-admission.test.ts": 621, + "src/main/runtime/agent-session-lease-renewal.test.ts": 790, + "src/main/runtime/agent-session-operation-admission.test.ts": 239, + "src/main/runtime/agent-session-permission-backfill.test.ts": 827, + "src/main/runtime/agent-session-process-identity-probe-windows-batch.test.ts": 65, + "src/main/runtime/agent-session-process-identity-probe.test.ts": 92, + "src/main/runtime/agent-session-provider-handle-transition.test.ts": 271, + "src/main/runtime/agent-session-record-conversation-name.test.ts": 270, + "src/main/runtime/agent-session-record-options.test.ts": 793, + "src/main/runtime/agent-session-record-store-slot.test.ts": 1914, + "src/main/runtime/agent-session-record-store-tab-table.test.ts": 792, + "src/main/runtime/agent-session-record-store.test.ts": 1144, + "src/main/runtime/agent-session-recovery-capsule.test.ts": 1153, + "src/main/runtime/agent-session-released-reservation-replay.test.ts": 906, + "src/main/runtime/agent-session-replaced-runtime.test.ts": 276, + "src/main/runtime/agent-session-reservation-admission.test.ts": 278, + "src/main/runtime/agent-session-runtime-attribution.test.ts": 979, + "src/main/runtime/agent-session-runtime-end-record.test.ts": 391, + "src/main/runtime/agent-session-spawn-token-process-scan.test.ts": 36, + "src/main/runtime/agent-session-spawn-token-readback.test.ts": 53, + "src/main/runtime/agent-session-store-transactions.test.ts": 860, + "src/main/runtime/agent-session-surface-release-transition.test.ts": 273, + "src/main/runtime/agent-session-unsaved-creation-supersession.test.ts": 745, + "src/main/runtime/agent-state-rules/agent-state-rule-regions.test.ts": 361, + "src/main/runtime/agent-state-rules/agent-state-rules-bundle.test.ts": 280, + "src/main/runtime/agent-state-rules/agent-state-rules-engine.test.ts": 344, + "src/main/runtime/agent-state-rules/agent-state-rules-live-update.test.ts": 544, + "src/main/runtime/agent-state-rules/agent-state-text-anchors.test.ts": 314, + "src/main/runtime/agent-terminal-launch-host.test.ts": 4836, + "src/main/runtime/agent-terminal-startup-prompt.test.ts": 4094, + "src/main/runtime/antigravity-readiness-transcripts.test.ts": 4952, + "src/main/runtime/antigravity-terminal-readiness.test.ts": 313, + "src/main/runtime/automatic-activation-host-sleep.test.ts": 5992, + "src/main/runtime/automation-change-publication.test.ts": 21921, + "src/main/runtime/browser-client-download-transfers.test.ts": 288, + "src/main/runtime/browser-dialog-settlement.test.ts": 38, + "src/main/runtime/browser-execution-host-key-resolution.test.ts": 4671, + "src/main/runtime/browser-host-capability-selection.test.ts": 242, + "src/main/runtime/browser-host-client-page-adoption-grants.test.ts": 43, + "src/main/runtime/browser-host-client-page-adoption.test.ts": 47, + "src/main/runtime/browser-host-client-page-creation.test.ts": 283, + "src/main/runtime/browser-host-command-ledger-capacity.test.ts": 247, + "src/main/runtime/browser-host-command-ledger.test.ts": 245, + "src/main/runtime/browser-host-file-channel-admission.test.ts": 202, + "src/main/runtime/browser-host-lease-download-transfer-cleanup.test.ts": 331, + "src/main/runtime/browser-host-lease-fenced-page-release.test.ts": 234, + "src/main/runtime/browser-host-lease-placement-retirement.test.ts": 270, + "src/main/runtime/browser-host-lease-registry.test.ts": 284, + "src/main/runtime/browser-host-page-placement-replacement.test.ts": 254, + "src/main/runtime/browser-host-page-placement.test.ts": 267, + "src/main/runtime/browser-host-page-reconciliation-executor.test.ts": 46, + "src/main/runtime/browser-host-page-reconciliation-orchestration.test.ts": 557, + "src/main/runtime/browser-host-page-reconciliation-plan.test.ts": 57, + "src/main/runtime/browser-host-page-reconciliation-preconditions.test.ts": 240, + "src/main/runtime/browser-host-page-retirement.test.ts": 36, + "src/main/runtime/browser-network-tunnel-paired-runtime.integration.test.ts": 6195, + "src/main/runtime/browser-screencast-driver-attribution.test.ts": 4732, + "src/main/runtime/browser-screencast-driver-scope.test.ts": 38, + "src/main/runtime/browser-screencast-ghost-subscriber-eviction.test.ts": 40, + "src/main/runtime/browser-screencast-remote-viewer-retention.test.ts": 5842, + "src/main/runtime/browser-screencast-renderer-throttle-lease.test.ts": 5631, + "src/main/runtime/browser-screencast-subscription-id.test.ts": 45, + "src/main/runtime/browser-session-tab-selection-snapshot.test.ts": 34, + "src/main/runtime/browser-tab-create-caller-navigation.test.ts": 4715, + "src/main/runtime/browser-tab-create-folder-workspace.test.ts": 496, + "src/main/runtime/browser-tab-create-publication.test.ts": 476, + "src/main/runtime/browser/browser-screencasts.test.ts": 459, + "src/main/runtime/claude-agent-teams-pty-exit-leak.test.ts": 4455, + "src/main/runtime/claude-agent-teams-service.test.ts": 81, + "src/main/runtime/claude-agent-teams-shim-env.test.ts": 68, + "src/main/runtime/claude-structured-exit-mid-response.test.ts": 1908, + "src/main/runtime/claude-structured-failed-start-resume.test.ts": 2049, + "src/main/runtime/claude-structured-idle-releases-unknown.test.ts": 2253, + "src/main/runtime/claude-structured-resumed-start-failure.test.ts": 2253, + "src/main/runtime/claude-structured-retired-saved-model.test.ts": 1947, + "src/main/runtime/claude-structured-send-during-startup.test.ts": 2303, + "src/main/runtime/claude-structured-send-restart-dies-before-dispatch.test.ts": 2722, + "src/main/runtime/claude-structured-session-integration.test.ts": 4018, + "src/main/runtime/claude-structured-startup-fault-is-not-an-exit.test.ts": 2124, + "src/main/runtime/claude-structured-startup-unanswered-control-request.test.ts": 2119, + "src/main/runtime/claude-trust-dialog-transcript.test.ts": 4432, + "src/main/runtime/cli-terminal-create-host-session-binding.test.ts": 4504, + "src/main/runtime/client-hosted-browser-page-persistence.integration.test.ts": 4697, + "src/main/runtime/client-hosted-browser-page-persistence.test.ts": 311, + "src/main/runtime/client-hosted-browser-row-projection.test.ts": 32, + "src/main/runtime/client-hosted-browser-row-publication.test.ts": 55, + "src/main/runtime/client-hosted-browser-row-push.integration.test.ts": 4420, + "src/main/runtime/client-hosted-page-reconciliation-hold.integration.test.ts": 4121, + "src/main/runtime/client-hosted-page-reconciliation-window.test.ts": 39, + "src/main/runtime/client-session-tab-selection.test.ts": 41, + "src/main/runtime/cline-screen-readiness-transcripts.test.ts": 6415, + "src/main/runtime/codex-capacity-terminal-status.test.ts": 3982, + "src/main/runtime/codex-header-readiness-transcripts.test.ts": 6041, + "src/main/runtime/codex-quiet-ready-screen.test.ts": 8507, + "src/main/runtime/codex-startup-paste-transcripts.test.ts": 535, + "src/main/runtime/created-mobile-session-tab-caller-selection.test.ts": 5880, + "src/main/runtime/decorative-title-fact-emission.test.ts": 33, + "src/main/runtime/delegated-mobile-device.integration.test.ts": 5919, + "src/main/runtime/device-key-fork.test.ts": 87, + "src/main/runtime/device-principal.test.ts": 3240, + "src/main/runtime/draft-paste-ready-scanner-chunking.test.ts": 1537, + "src/main/runtime/dsb-terminal-runtime.test.ts": 4611, + "src/main/runtime/dsh-readiness-transcript.test.ts": 74, + "src/main/runtime/e2ee-keypair.test.ts": 71, + "src/main/runtime/ephemeral-device-rows.test.ts": 81, + "src/main/runtime/exit-provenance-audit.test.ts": 6026, + "src/main/runtime/expired-ssh-lease-pane-candidacy.test.ts": 9671, + "src/main/runtime/external-worktree-paired-client-discovery.integration.test.ts": 5250, + "src/main/runtime/fetch-remote-cache.test.ts": 4747, + "src/main/runtime/file-watcher-host.test.ts": 460, + "src/main/runtime/files/search/file-search.test.ts": 471, + "src/main/runtime/files/search/mobile-path-search-expiry.test.ts": 236, + "src/main/runtime/files/search/mobile-path-search.test.ts": 46, + "src/main/runtime/fit-override-integration.test.ts": 4650, + "src/main/runtime/floating-terminal-inventory-ownership.test.ts": 5150, + "src/main/runtime/folder-workspace-pty-identity.test.ts": 4471, + "src/main/runtime/graph-sync-deletion-fence.test.ts": 4351, + "src/main/runtime/graph-sync-live-daemon-pty-tab-preservation.test.ts": 4439, + "src/main/runtime/graph-sync-mobile-snapshot-gating.test.ts": 4458, + "src/main/runtime/graph-sync-payload-partition.test.ts": 4480, + "src/main/runtime/headless-close-keeps-publication-epoch.test.ts": 4509, + "src/main/runtime/headless-hydration-ownership.test.ts": 8048, + "src/main/runtime/headless-placeholder-graph-surface-standing.test.ts": 4175, + "src/main/runtime/headless-runtime-graph.test.ts": 4519, + "src/main/runtime/headless-seed-ownership.test.ts": 6549, + "src/main/runtime/headless-tab-group-selection.test.ts": 26, + "src/main/runtime/headless-tab-order-stability.test.ts": 4260, + "src/main/runtime/headless-terminal-dispose-write-ordering.test.ts": 4631, + "src/main/runtime/headless-terminal-query-reply-policy.test.ts": 36, + "src/main/runtime/headless-terminal-split-layout.test.ts": 32, + "src/main/runtime/hidden-output-restored-provider-snapshot.test.ts": 5446, + "src/main/runtime/host-account-launch-env.test.ts": 4485, + "src/main/runtime/host-descriptor.test.ts": 296, + "src/main/runtime/host-terminal-close-persistence-durability.test.ts": 1608, + "src/main/runtime/launch-agent-terminal-followup.test.ts": 3644, + "src/main/runtime/launched-agent-composer-readiness-claude.test.ts": 4535, + "src/main/runtime/launched-agent-composer-readiness.test.ts": 671, + "src/main/runtime/launched-agent-crash-guard.live-shell.test.ts": 203, + "src/main/runtime/launched-agent-exit-delivery.test.ts": 386, + "src/main/runtime/launched-agent-foreground.test.ts": 112, + "src/main/runtime/launched-agent-shell-handoff-readiness.test.ts": 133, + "src/main/runtime/launched-agent-write-guard.test.ts": 51, + "src/main/runtime/leaf-pty-verdict-expiry.test.ts": 4521, + "src/main/runtime/linear-save-issue.test.ts": 4300, + "src/main/runtime/managed-worktree-create-execution-host.test.ts": 4345, + "src/main/runtime/managed-worktree-create-telemetry.test.ts": 4498, + "src/main/runtime/missing-worktree-terminal-reconciliation.test.ts": 379, + "src/main/runtime/mobile-agent-status-permission-renewal.test.ts": 4057, + "src/main/runtime/mobile-desktop-relay/hosted-browser-page-route.test.ts": 831, + "src/main/runtime/mobile-desktop-relay/mobile-desktop-relay.integration.test.ts": 7777, + "src/main/runtime/mobile-desktop-relay/mobile-desktop-relay.test.ts": 1233, + "src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.integration.test.ts": 4800, + "src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.test.ts": 251, + "src/main/runtime/mobile-desktop-relay/relayed-phone-frames.test.ts": 241, + "src/main/runtime/mobile-desktop-relay/relayed-stream-carriers.test.ts": 3047, "src/main/runtime/mobile-notification-dismissal-read-failure.test.ts": 268, - "src/main/runtime/mobile-notification-dismissal-store.test.ts": 293, - "src/main/runtime/mobile-notification-replay.test.ts": 52, - "src/main/runtime/mobile-pairing-qr.test.ts": 1059, - "src/main/runtime/mobile-pairing-userdata-path.test.ts": 2373, - "src/main/runtime/mobile-presence-lock.test.ts": 16927, - "src/main/runtime/mobile-rpc-allowlist.test.ts": 3585, - "src/main/runtime/mobile-session-tab-close-refusal-message.test.ts": 46, - "src/main/runtime/mobile-session-tabs-agent-status-heartbeat.test.ts": 65, - "src/main/runtime/mobile-session-tabs-churn-coalescing.test.ts": 4509, - "src/main/runtime/mobile-session-tabs-notify-coalescer.test.ts": 67, - "src/main/runtime/mobile-session-terminal-persistence-retirement.test.ts": 331, - "src/main/runtime/mobile-session-terminal-retirement-proof.test.ts": 52, - "src/main/runtime/mobile-session-terminal-retirement.test.ts": 253, - "src/main/runtime/mobile-store-orphaned-temp-cleanup.test.ts": 381, - "src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts": 5383, - "src/main/runtime/mobile-subscribe-integration.test.ts": 18007, - "src/main/runtime/multi-client-navigation-isolation.integration.test.ts": 7135, - "src/main/runtime/native-chat-visuals-workspace-verdict.test.ts": 323, - "src/main/runtime/omp-readiness-transcripts.test.ts": 8110, - "src/main/runtime/opencode-agent-row-enter-transcript.test.ts": 559, - "src/main/runtime/opencode-agent-row-structure-transcript.test.ts": 333, - "src/main/runtime/opencode-finished-session-authority.test.ts": 5338, - "src/main/runtime/opencode-model-worktree-create.test.ts": 964, - "src/main/runtime/opencode-run-lifetime-runtime.test.ts": 4511, - "src/main/runtime/opencode-run-lifetime-status.test.ts": 131, - "src/main/runtime/orca-runtime-agent-session-operation.test.ts": 4398, - "src/main/runtime/orca-runtime-agent-skill-share.test.ts": 5202, - "src/main/runtime/orca-runtime-automations.test.ts": 4377, - "src/main/runtime/orca-runtime-browser-client-hosted.test.ts": 573, - "src/main/runtime/orca-runtime-browser-ghost-session-row-close.test.ts": 519, - "src/main/runtime/orca-runtime-browser-headless.test.ts": 448, - "src/main/runtime/orca-runtime-browser-screencast-fanout.test.ts": 543, - "src/main/runtime/orca-runtime-browser.test.ts": 656, - "src/main/runtime/orca-runtime-cold-restore-terminal-membership.test.ts": 4342, - "src/main/runtime/orca-runtime-create-base-prefetch.test.ts": 4314, - "src/main/runtime/orca-runtime-default-tab-agent-status.test.ts": 4293, - "src/main/runtime/orca-runtime-emulator-folder-workspace.test.ts": 4356, - "src/main/runtime/orca-runtime-exact-worker-provider-session.test.ts": 2713, - "src/main/runtime/orca-runtime-files-mobile-explorer-reads.test.ts": 549, - "src/main/runtime/orca-runtime-files-preview-budget.test.ts": 634, - "src/main/runtime/orca-runtime-files-rename-authority.test.ts": 542, - "src/main/runtime/orca-runtime-files-search.test.ts": 622, - "src/main/runtime/orca-runtime-files-ssh-chunk-reads.test.ts": 636, - "src/main/runtime/orca-runtime-files-terminal-artifact-grants.test.ts": 663, - "src/main/runtime/orca-runtime-files-terminal-artifact-io.test.ts": 635, - "src/main/runtime/orca-runtime-files-terminal-artifact-swap-detection.test.ts": 617, - "src/main/runtime/orca-runtime-files-terminal-link-host-translation.test.ts": 590, - "src/main/runtime/orca-runtime-files-terminal-path-resolution.test.ts": 674, - "src/main/runtime/orca-runtime-files-watch-host-scope.test.ts": 466, - "src/main/runtime/orca-runtime-files-watch.test.ts": 533, - "src/main/runtime/orca-runtime-git-branch-diff.test.ts": 1006, - "src/main/runtime/orca-runtime-git-diff-budget.test.ts": 952, - "src/main/runtime/orca-runtime-git.test.ts": 1054, - "src/main/runtime/orca-runtime-headless-hydration-repo-gate.test.ts": 4653, - "src/main/runtime/orca-runtime-headless-terminal-close.test.ts": 4240, - "src/main/runtime/orca-runtime-hook-agent-status-projection.test.ts": 4572, - "src/main/runtime/orca-runtime-linked-issue-live-meta.integration.test.ts": 4470, - "src/main/runtime/orca-runtime-mobile-agent-status-title-truthfulness.test.ts": 4519, - "src/main/runtime/orca-runtime-mobile-close-preserved-resurrection.test.ts": 4450, - "src/main/runtime/orca-runtime-on-pty-data-redraw.test.ts": 4103, - "src/main/runtime/orca-runtime-path-candidate-history.test.ts": 64, - "src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts": 5125, - "src/main/runtime/orca-runtime-provider-reattach-launch-identity.test.ts": 4630, - "src/main/runtime/orca-runtime-quick-open-capabilities.test.ts": 618, - "src/main/runtime/orca-runtime-skill-recovery.test.ts": 4922, - "src/main/runtime/orca-runtime-startup-draft-launch-parity.test.ts": 5171, - "src/main/runtime/orca-runtime-structured-agent-registration-seam.test.ts": 4301, - "src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts": 4720, - "src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts": 4032, - "src/main/runtime/orca-runtime-structured-session-restore.test.ts": 4748, - "src/main/runtime/orca-runtime-structured-status-sink-wiring.test.ts": 4449, - "src/main/runtime/orca-runtime-tab-id-collision.test.ts": 4395, - "src/main/runtime/orca-runtime-tail-wait-memo.test.ts": 303, - "src/main/runtime/orca-runtime-terminal-close-agent-launch.test.ts": 3986, - "src/main/runtime/orca-runtime-terminal-close-continuity.test.ts": 4919, - "src/main/runtime/orca-runtime-terminal-close-no-widening.test.ts": 4813, - "src/main/runtime/orca-runtime-terminal-close-records.test.ts": 6684, - "src/main/runtime/orca-runtime-terminal-close-spill-partition.test.ts": 4512, - "src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts": 4787, - "src/main/runtime/orca-runtime-terminal-cwd.test.ts": 4884, - "src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts": 5490, - "src/main/runtime/orca-runtime-terminal-rename-retention.test.ts": 6075, - "src/main/runtime/orca-runtime-terminal-retirement-host-partition.test.ts": 4772, - "src/main/runtime/orca-runtime-terminal-retirement.test.ts": 4562, - "src/main/runtime/orca-runtime-terminal-split-authority.test.ts": 4555, - "src/main/runtime/orca-runtime-terminal-surface-close.test.ts": 5472, - "src/main/runtime/orca-runtime-workspace-ports-host.test.ts": 3791, - "src/main/runtime/orca-runtime-worktree-change-invalidation.test.ts": 4736, - "src/main/runtime/orca-runtime-worktree-provisioning-availability.test.ts": 5541, - "src/main/runtime/orca-runtime.test.ts": 36218, - "src/main/runtime/orcad-migration-catalog-import.test.ts": 45, - "src/main/runtime/orchestration-cli-subprocess.test.ts": 5861, - "src/main/runtime/orchestration-codex-completion-title.test.ts": 5337, - "src/main/runtime/orchestration-codex-real-pty.integration.test.ts": 6330, - "src/main/runtime/orchestration-compatibility-authority.test.ts": 4711, - "src/main/runtime/orchestration-configured-agent-target.test.ts": 4510, - "src/main/runtime/orchestration-dispatch-mailbox-delivery.test.ts": 6140, - "src/main/runtime/orchestration-live-agent-title-restore.test.ts": 5202, - "src/main/runtime/orchestration-mailbox-cold-park-idle.test.ts": 5151, - "src/main/runtime/orchestration-mailbox-crash-recovery.test.ts": 5866, - "src/main/runtime/orchestration-mailbox-detached-routing.test.ts": 5444, - "src/main/runtime/orchestration-mailbox-filtered-waiters.test.ts": 6051, - "src/main/runtime/orchestration-mailbox-notification-consistency.test.ts": 19774, - "src/main/runtime/orchestration-mailbox-pointer-cli-command.test.ts": 5540, - "src/main/runtime/orchestration-mailbox-routing-races.test.ts": 8786, - "src/main/runtime/orchestration-mailbox-transport-settlement.test.ts": 5363, - "src/main/runtime/orchestration-message-delivery-identity.test.ts": 6283, - "src/main/runtime/orchestration-messages-fake-parity.test.ts": 619, - "src/main/runtime/orchestration-opencode-model-host.test.ts": 4210, - "src/main/runtime/orchestration-worker-workspace-resolution.test.ts": 4914, - "src/main/runtime/orchestration/agent-facing-parity.test.ts": 866, - "src/main/runtime/orchestration/chat-assignee.test.ts": 5175, - "src/main/runtime/orchestration/cli-command.test.ts": 55, - "src/main/runtime/orchestration/configured-worker-agent-selector.test.ts": 73, - "src/main/runtime/orchestration/coordinator-decision-gates.test.ts": 589, - "src/main/runtime/orchestration/coordinator-dispatch-unobserved-prompt.test.ts": 614, - "src/main/runtime/orchestration/coordinator-drift-probe-coalescing.test.ts": 652, - "src/main/runtime/orchestration/coordinator-escalation-triage.test.ts": 533, - "src/main/runtime/orchestration/coordinator-stale-base-flag.test.ts": 41, - "src/main/runtime/orchestration/coordinator-terminal-census-unavailable.test.ts": 590, - "src/main/runtime/orchestration/coordinator.test.ts": 961, - "src/main/runtime/orchestration/db-empty-dispatch-shortcircuit.benchmark.test.ts": 609, - "src/main/runtime/orchestration/db-heartbeat-straggler-guard.test.ts": 599, - "src/main/runtime/orchestration/db-message-timestamp.test.ts": 562, - "src/main/runtime/orchestration/db-messages.test.ts": 551, - "src/main/runtime/orchestration/db-stopping-worker-task-guard.test.ts": 640, - "src/main/runtime/orchestration/db-task-create-readiness.test.ts": 786, - "src/main/runtime/orchestration/db-task-dispatch-invariant.test.ts": 1161, - "src/main/runtime/orchestration/db-task-dispatch-lifecycle-guards.test.ts": 1315, - "src/main/runtime/orchestration/db-task-dispatch-races.test.ts": 762, - "src/main/runtime/orchestration/db-task-promotion-projection.test.ts": 662, - "src/main/runtime/orchestration/db-undelivered-mailboxes.test.ts": 573, - "src/main/runtime/orchestration/db.test.ts": 1313, - "src/main/runtime/orchestration/db/attempt-outcome-projection.test.ts": 673, - "src/main/runtime/orchestration/db/decision-gate-lifecycle.test.ts": 527, - "src/main/runtime/orchestration/db/dispatch-depth.test.ts": 834, - "src/main/runtime/orchestration/db/dispatch-mailbox-consumer-fencing.test.ts": 627, - "src/main/runtime/orchestration/db/federated-worker-report-outcome.test.ts": 48, - "src/main/runtime/orchestration/db/federation/federated-dispatch-observation-fence.test.ts": 560, - "src/main/runtime/orchestration/db/federation/remote-dispatch-attachment-release.test.ts": 633, - "src/main/runtime/orchestration/db/hot-path-statement-compilation.test.ts": 597, - "src/main/runtime/orchestration/db/legacy-question-identity.test.ts": 52, - "src/main/runtime/orchestration/db/lifecycle-rejection-marker.test.ts": 51, - "src/main/runtime/orchestration/db/lifecycle-transition.test.ts": 572, - "src/main/runtime/orchestration/db/messages/mailbox-consumer-lifecycle-fencing.test.ts": 809, - "src/main/runtime/orchestration/db/pane-key-match.test.ts": 51, - "src/main/runtime/orchestration/db/remaining-dispatch-statement-compilation.test.ts": 1139, - "src/main/runtime/orchestration/db/row-column-lists.test.ts": 554, - "src/main/runtime/orchestration/db/run-list-cursor.test.ts": 46, - "src/main/runtime/orchestration/db/runs/run-coordinator-orca-session-binding.test.ts": 720, - "src/main/runtime/orchestration/db/schema/derived-delivery-migration.test.ts": 810, - "src/main/runtime/orchestration/db/schema/federated-home-run-migration.test.ts": 581, - "src/main/runtime/orchestration/db/schema/structured-pointer-schema-migration.test.ts": 620, - "src/main/runtime/orchestration/db/schema/structured-worker-orca-session-backfill.test.ts": 630, - "src/main/runtime/orchestration/db/worker-dispatch/worker-dispatch-abandon.test.ts": 627, - "src/main/runtime/orchestration/db/worker-dispatch/worker-dispatch-assignee-orca-session.test.ts": 575, - "src/main/runtime/orchestration/db/writer-run-required.test.ts": 659, - "src/main/runtime/orchestration/dispatch-consumer-generation-migration.test.ts": 720, - "src/main/runtime/orchestration/dispatch-creator-identity-migration.test.ts": 649, - "src/main/runtime/orchestration/dispatch-failure-idempotency.test.ts": 693, - "src/main/runtime/orchestration/federation-ack-checkpoints.test.ts": 38, - "src/main/runtime/orchestration/federation-acknowledgment-integrity.test.ts": 561, - "src/main/runtime/orchestration/federation-acknowledgment-migration.test.ts": 684, - "src/main/runtime/orchestration/federation-lifecycle-settlement.test.ts": 39, - "src/main/runtime/orchestration/federation-sync.test.ts": 5859, - "src/main/runtime/orchestration/federation-terminal-recovery.test.ts": 4138, - "src/main/runtime/orchestration/formatter.test.ts": 75, - "src/main/runtime/orchestration/groups.test.ts": 61, - "src/main/runtime/orchestration/lifecycle-caller-edges.test.ts": 653, - "src/main/runtime/orchestration/lifecycle-reconciliation.test.ts": 799, - "src/main/runtime/orchestration/lightweight-run-worker-exit-escalation.test.ts": 5376, - "src/main/runtime/orchestration/mailbox-pointer-eligibility.test.ts": 666, - "src/main/runtime/orchestration/mailbox-pointer-release-query.test.ts": 591, - "src/main/runtime/orchestration/mailbox-pointer-stage.test.ts": 691, - "src/main/runtime/orchestration/mailbox-pointer-submit.test.ts": 981, - "src/main/runtime/orchestration/message-batch-atomicity.test.ts": 641, - "src/main/runtime/orchestration/mutation-receipt-capacity.test.ts": 638, - "src/main/runtime/orchestration/nested-worker-depth-migration.test.ts": 676, - "src/main/runtime/orchestration/orchestration-adopted-run-binding.test.ts": 995, - "src/main/runtime/orchestration/orchestration-all-start-versions-migration.test.ts": 1990, - "src/main/runtime/orchestration/orchestration-creator-authority-performance.test.ts": 944, - "src/main/runtime/orchestration/orchestration-db-permissions.test.ts": 573, - "src/main/runtime/orchestration/orchestration-db-retention-pagination.test.ts": 776, - "src/main/runtime/orchestration/orchestration-delivery-consumption.test.ts": 643, + "src/main/runtime/mobile-notification-dismissal-store.test.ts": 275, + "src/main/runtime/mobile-notification-replay.test.ts": 42, + "src/main/runtime/mobile-pairing-qr.test.ts": 1063, + "src/main/runtime/mobile-pairing-userdata-path.test.ts": 2695, + "src/main/runtime/mobile-presence-lock.test.ts": 17896, + "src/main/runtime/mobile-rpc-allowlist.test.ts": 3443, + "src/main/runtime/mobile-session-tab-close-refusal-message.test.ts": 30, + "src/main/runtime/mobile-session-tabs-agent-status-heartbeat.test.ts": 59, + "src/main/runtime/mobile-session-tabs-churn-coalescing.test.ts": 4066, + "src/main/runtime/mobile-session-tabs-notify-coalescer.test.ts": 36, + "src/main/runtime/mobile-session-terminal-persistence-retirement.test.ts": 233, + "src/main/runtime/mobile-session-terminal-retirement-proof.test.ts": 34, + "src/main/runtime/mobile-session-terminal-retirement.test.ts": 300, + "src/main/runtime/mobile-store-orphaned-temp-cleanup.test.ts": 453, + "src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts": 5902, + "src/main/runtime/mobile-subscribe-integration.test.ts": 22684, + "src/main/runtime/multi-client-navigation-isolation.integration.test.ts": 5282, + "src/main/runtime/native-chat-visuals-workspace-verdict.test.ts": 326, + "src/main/runtime/omp-readiness-transcripts.test.ts": 7797, + "src/main/runtime/opencode-agent-row-enter-transcript.test.ts": 562, + "src/main/runtime/opencode-agent-row-structure-transcript.test.ts": 347, + "src/main/runtime/opencode-finished-session-authority.test.ts": 5180, + "src/main/runtime/opencode-run-lifetime-runtime.test.ts": 5346, + "src/main/runtime/opencode-run-lifetime-status.test.ts": 143, + "src/main/runtime/opencode-wsl-folder-launch-host.test.ts": 4927, + "src/main/runtime/orca-runtime-agent-session-account-env.test.ts": 3829, + "src/main/runtime/orca-runtime-agent-session-operation.test.ts": 5689, + "src/main/runtime/orca-runtime-agent-skill-share.test.ts": 3916, + "src/main/runtime/orca-runtime-automations.test.ts": 5397, + "src/main/runtime/orca-runtime-browser-client-hosted.test.ts": 555, + "src/main/runtime/orca-runtime-browser-ghost-session-row-close.test.ts": 514, + "src/main/runtime/orca-runtime-browser-headless.test.ts": 530, + "src/main/runtime/orca-runtime-browser-screencast-e2ee-backlog.test.ts": 523, + "src/main/runtime/orca-runtime-browser-screencast-fanout.test.ts": 448, + "src/main/runtime/orca-runtime-browser-screencast-pending-frame-retry.test.ts": 518, + "src/main/runtime/orca-runtime-browser.test.ts": 551, + "src/main/runtime/orca-runtime-cold-restore-terminal-membership.test.ts": 5056, + "src/main/runtime/orca-runtime-create-base-prefetch.test.ts": 4973, + "src/main/runtime/orca-runtime-default-tab-agent-status.test.ts": 3898, + "src/main/runtime/orca-runtime-emulator-folder-workspace.test.ts": 4520, + "src/main/runtime/orca-runtime-exact-worker-provider-session.test.ts": 3927, + "src/main/runtime/orca-runtime-files-artifact-facade.test.ts": 4613, + "src/main/runtime/orca-runtime-files-chunk-reads.test.ts": 508, + "src/main/runtime/orca-runtime-files-mobile-explorer-reads.test.ts": 670, + "src/main/runtime/orca-runtime-files-preview-budget.test.ts": 950, + "src/main/runtime/orca-runtime-files-rename-authority.test.ts": 646, + "src/main/runtime/orca-runtime-files-search.test.ts": 749, + "src/main/runtime/orca-runtime-files-terminal-artifact-grants.test.ts": 747, + "src/main/runtime/orca-runtime-files-terminal-artifact-io.test.ts": 762, + "src/main/runtime/orca-runtime-files-terminal-artifact-swap-detection.test.ts": 725, + "src/main/runtime/orca-runtime-files-terminal-link-host-translation.test.ts": 628, + "src/main/runtime/orca-runtime-files-terminal-path-resolution.test.ts": 642, + "src/main/runtime/orca-runtime-files-watch-host-scope.test.ts": 673, + "src/main/runtime/orca-runtime-files-watch.test.ts": 765, + "src/main/runtime/orca-runtime-git-branch-diff.test.ts": 988, + "src/main/runtime/orca-runtime-git-diff-budget.test.ts": 1013, + "src/main/runtime/orca-runtime-git.test.ts": 1002, + "src/main/runtime/orca-runtime-headless-hydration-repo-gate.test.ts": 4466, + "src/main/runtime/orca-runtime-headless-terminal-close.test.ts": 4999, + "src/main/runtime/orca-runtime-hook-agent-status-projection.test.ts": 4679, + "src/main/runtime/orca-runtime-linked-issue-live-meta.integration.test.ts": 4413, + "src/main/runtime/orca-runtime-mobile-agent-status-title-truthfulness.test.ts": 4243, + "src/main/runtime/orca-runtime-mobile-close-preserved-resurrection.test.ts": 4606, + "src/main/runtime/orca-runtime-move-headless-mobile-session-tab.test.ts": 4208, + "src/main/runtime/orca-runtime-on-pty-data-redraw.test.ts": 4052, + "src/main/runtime/orca-runtime-path-candidate-history.test.ts": 42, + "src/main/runtime/orca-runtime-process-incarnation-liveness.test.ts": 4495, + "src/main/runtime/orca-runtime-provider-reattach-launch-identity.test.ts": 4550, + "src/main/runtime/orca-runtime-quick-open-capabilities.test.ts": 631, + "src/main/runtime/orca-runtime-serve-browser-restore.test.ts": 5565, + "src/main/runtime/orca-runtime-skill-recovery.test.ts": 4382, + "src/main/runtime/orca-runtime-startup-draft-launch-parity.test.ts": 5622, + "src/main/runtime/orca-runtime-structured-agent-registration-seam.test.ts": 4257, + "src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts": 4533, + "src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts": 3753, + "src/main/runtime/orca-runtime-structured-session-restore.test.ts": 4449, + "src/main/runtime/orca-runtime-structured-status-sink-wiring.test.ts": 4891, + "src/main/runtime/orca-runtime-tab-id-collision.test.ts": 4432, + "src/main/runtime/orca-runtime-tail-wait-memo.test.ts": 272, + "src/main/runtime/orca-runtime-terminal-close-agent-launch.test.ts": 4325, + "src/main/runtime/orca-runtime-terminal-close-continuity.test.ts": 4477, + "src/main/runtime/orca-runtime-terminal-close-no-widening.test.ts": 4931, + "src/main/runtime/orca-runtime-terminal-close-records.test.ts": 7567, + "src/main/runtime/orca-runtime-terminal-close-spill-partition.test.ts": 4231, + "src/main/runtime/orca-runtime-terminal-create-idempotency.test.ts": 4450, + "src/main/runtime/orca-runtime-terminal-cwd.test.ts": 4526, + "src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts": 5738, + "src/main/runtime/orca-runtime-terminal-rename-retention.test.ts": 6378, + "src/main/runtime/orca-runtime-terminal-retirement-host-partition.test.ts": 4778, + "src/main/runtime/orca-runtime-terminal-retirement.test.ts": 4644, + "src/main/runtime/orca-runtime-terminal-split-authority.test.ts": 4668, + "src/main/runtime/orca-runtime-terminal-surface-close.test.ts": 6591, + "src/main/runtime/orca-runtime-terminal-title-saved-layout.test.ts": 5455, + "src/main/runtime/orca-runtime-workspace-ports-host.test.ts": 4496, + "src/main/runtime/orca-runtime-worktree-change-invalidation.test.ts": 4183, + "src/main/runtime/orca-runtime-worktree-provisioning-availability.test.ts": 5891, + "src/main/runtime/orca-runtime.test.ts": 37405, + "src/main/runtime/orcad-migration-catalog-import.test.ts": 28, + "src/main/runtime/orchestration-cli-subprocess.test.ts": 6191, + "src/main/runtime/orchestration-codex-completion-title.test.ts": 5949, + "src/main/runtime/orchestration-codex-real-pty.integration.test.ts": 4837, + "src/main/runtime/orchestration-compatibility-authority.test.ts": 4511, + "src/main/runtime/orchestration-configured-agent-target.test.ts": 4312, + "src/main/runtime/orchestration-dispatch-mailbox-delivery.test.ts": 5037, + "src/main/runtime/orchestration-live-agent-title-restore.test.ts": 5381, + "src/main/runtime/orchestration-mailbox-cold-park-idle.test.ts": 5009, + "src/main/runtime/orchestration-mailbox-crash-recovery.test.ts": 6728, + "src/main/runtime/orchestration-mailbox-detached-routing.test.ts": 6903, + "src/main/runtime/orchestration-mailbox-filtered-waiters.test.ts": 5482, + "src/main/runtime/orchestration-mailbox-notification-consistency.test.ts": 19969, + "src/main/runtime/orchestration-mailbox-pointer-cli-command.test.ts": 5744, + "src/main/runtime/orchestration-mailbox-routing-races.test.ts": 5632, + "src/main/runtime/orchestration-mailbox-transport-settlement.test.ts": 5651, + "src/main/runtime/orchestration-message-delivery-identity.test.ts": 9401, + "src/main/runtime/orchestration-messages-fake-parity.test.ts": 631, + "src/main/runtime/orchestration-opencode-model-host.test.ts": 4141, + "src/main/runtime/orchestration-worker-workspace-resolution.test.ts": 4454, + "src/main/runtime/orchestration/agent-facing-parity.test.ts": 1063, + "src/main/runtime/orchestration/chat-assignee.test.ts": 5367, + "src/main/runtime/orchestration/cli-command.test.ts": 37, + "src/main/runtime/orchestration/configured-worker-agent-selector.test.ts": 54, + "src/main/runtime/orchestration/coordinator-decision-gates.test.ts": 577, + "src/main/runtime/orchestration/coordinator-dispatch-unobserved-prompt.test.ts": 536, + "src/main/runtime/orchestration/coordinator-drift-probe-coalescing.test.ts": 662, + "src/main/runtime/orchestration/coordinator-escalation-triage.test.ts": 544, + "src/main/runtime/orchestration/coordinator-stale-base-flag.test.ts": 44, + "src/main/runtime/orchestration/coordinator-terminal-census-unavailable.test.ts": 528, + "src/main/runtime/orchestration/coordinator.test.ts": 900, + "src/main/runtime/orchestration/db-empty-dispatch-shortcircuit.benchmark.test.ts": 573, + "src/main/runtime/orchestration/db-heartbeat-straggler-guard.test.ts": 635, + "src/main/runtime/orchestration/db-message-timestamp.test.ts": 559, + "src/main/runtime/orchestration/db-messages.test.ts": 655, + "src/main/runtime/orchestration/db-stopping-worker-task-guard.test.ts": 626, + "src/main/runtime/orchestration/db-task-create-readiness.test.ts": 699, + "src/main/runtime/orchestration/db-task-dispatch-invariant.test.ts": 1058, + "src/main/runtime/orchestration/db-task-dispatch-lifecycle-guards.test.ts": 1072, + "src/main/runtime/orchestration/db-task-dispatch-races.test.ts": 610, + "src/main/runtime/orchestration/db-task-promotion-projection.test.ts": 757, + "src/main/runtime/orchestration/db-undelivered-mailboxes.test.ts": 611, + "src/main/runtime/orchestration/db.test.ts": 1135, + "src/main/runtime/orchestration/db/attempt-outcome-projection.test.ts": 942, + "src/main/runtime/orchestration/db/decision-gate-lifecycle.test.ts": 601, + "src/main/runtime/orchestration/db/dispatch-depth.test.ts": 790, + "src/main/runtime/orchestration/db/dispatch-mailbox-consumer-fencing.test.ts": 636, + "src/main/runtime/orchestration/db/federated-worker-report-outcome.test.ts": 33, + "src/main/runtime/orchestration/db/federation/federated-dispatch-observation-fence.test.ts": 599, + "src/main/runtime/orchestration/db/federation/remote-dispatch-attachment-release.test.ts": 690, + "src/main/runtime/orchestration/db/hot-path-statement-compilation.test.ts": 592, + "src/main/runtime/orchestration/db/legacy-question-identity.test.ts": 36, + "src/main/runtime/orchestration/db/lifecycle-rejection-marker.test.ts": 32, + "src/main/runtime/orchestration/db/lifecycle-transition.test.ts": 647, + "src/main/runtime/orchestration/db/messages/mailbox-consumer-lifecycle-fencing.test.ts": 838, + "src/main/runtime/orchestration/db/pane-key-match.test.ts": 27, + "src/main/runtime/orchestration/db/remaining-dispatch-statement-compilation.test.ts": 1055, + "src/main/runtime/orchestration/db/row-column-lists.test.ts": 612, + "src/main/runtime/orchestration/db/run-list-cursor.test.ts": 35, + "src/main/runtime/orchestration/db/runs/run-coordinator-orca-session-binding.test.ts": 644, + "src/main/runtime/orchestration/db/schema/derived-delivery-migration.test.ts": 734, + "src/main/runtime/orchestration/db/schema/federated-home-run-migration.test.ts": 615, + "src/main/runtime/orchestration/db/schema/structured-pointer-schema-migration.test.ts": 560, + "src/main/runtime/orchestration/db/schema/structured-worker-orca-session-backfill.test.ts": 586, + "src/main/runtime/orchestration/db/worker-dispatch/failed-start-circuit-breaker.test.ts": 603, + "src/main/runtime/orchestration/db/worker-dispatch/worker-dispatch-abandon.test.ts": 742, + "src/main/runtime/orchestration/db/worker-dispatch/worker-dispatch-assignee-orca-session.test.ts": 616, + "src/main/runtime/orchestration/db/writer-run-required.test.ts": 607, + "src/main/runtime/orchestration/dispatch-consumer-generation-migration.test.ts": 715, + "src/main/runtime/orchestration/dispatch-creator-identity-migration.test.ts": 584, + "src/main/runtime/orchestration/dispatch-failure-idempotency.test.ts": 558, + "src/main/runtime/orchestration/federation-ack-checkpoints.test.ts": 31, + "src/main/runtime/orchestration/federation-acknowledgment-integrity.test.ts": 633, + "src/main/runtime/orchestration/federation-acknowledgment-migration.test.ts": 613, + "src/main/runtime/orchestration/federation-lifecycle-settlement.test.ts": 31, + "src/main/runtime/orchestration/federation-sync.test.ts": 4893, + "src/main/runtime/orchestration/federation-terminal-recovery.test.ts": 4724, + "src/main/runtime/orchestration/formatter.test.ts": 226, + "src/main/runtime/orchestration/groups.test.ts": 58, + "src/main/runtime/orchestration/lifecycle-caller-edges.test.ts": 583, + "src/main/runtime/orchestration/lifecycle-reconciliation.test.ts": 823, + "src/main/runtime/orchestration/lightweight-run-worker-exit-escalation.test.ts": 5766, + "src/main/runtime/orchestration/mailbox-pointer-eligibility.test.ts": 655, + "src/main/runtime/orchestration/mailbox-pointer-release-query.test.ts": 600, + "src/main/runtime/orchestration/mailbox-pointer-stage.test.ts": 687, + "src/main/runtime/orchestration/mailbox-pointer-submit.test.ts": 921, + "src/main/runtime/orchestration/message-batch-atomicity.test.ts": 639, + "src/main/runtime/orchestration/mutation-receipt-capacity.test.ts": 650, + "src/main/runtime/orchestration/nested-worker-depth-migration.test.ts": 715, + "src/main/runtime/orchestration/orchestration-adopted-run-binding.test.ts": 1143, + "src/main/runtime/orchestration/orchestration-all-start-versions-migration.test.ts": 2102, + "src/main/runtime/orchestration/orchestration-creator-authority-performance.test.ts": 902, + "src/main/runtime/orchestration/orchestration-db-permissions.test.ts": 586, + "src/main/runtime/orchestration/orchestration-db-retention-pagination.test.ts": 716, + "src/main/runtime/orchestration/orchestration-delivery-consumption.test.ts": 613, "src/main/runtime/orchestration/orchestration-derived-delivery.test.ts": 567, - "src/main/runtime/orchestration/orchestration-federated-legacy-probe.test.ts": 868, - "src/main/runtime/orchestration/orchestration-legacy-coordinator-authority-db.test.ts": 956, - "src/main/runtime/orchestration/orchestration-legacy-question-migration-db.test.ts": 1066, - "src/main/runtime/orchestration/orchestration-legacy-storage-db.test.ts": 2229, - "src/main/runtime/orchestration/orchestration-legacy-worker-terminal-recovery.test.ts": 347, - "src/main/runtime/orchestration/orchestration-mutation-question-db.test.ts": 660, - "src/main/runtime/orchestration/orchestration-orca-session-column-migration.test.ts": 957, - "src/main/runtime/orchestration/orchestration-party-cli-address.test.ts": 281, - "src/main/runtime/orchestration/orchestration-party-location.test.ts": 616, - "src/main/runtime/orchestration/orchestration-peer-capability-cache.test.ts": 79, - "src/main/runtime/orchestration/orchestration-reset-db.test.ts": 588, - "src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts": 768, - "src/main/runtime/orchestration/orchestration-run-list-compatibility.test.ts": 593, - "src/main/runtime/orchestration/orchestration-version-skew-migration.test.ts": 1120, - "src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts": 822, - "src/main/runtime/orchestration/preamble.test.ts": 174, - "src/main/runtime/orchestration/r1-identity-migration.test.ts": 634, - "src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts": 691, - "src/main/runtime/orchestration/run-coordinator-orca-session-address.test.ts": 693, - "src/main/runtime/orchestration/send-agent-turn-host.test.ts": 1639, - "src/main/runtime/orchestration/send-agent-turn.test.ts": 267, - "src/main/runtime/orchestration/settled-question-threads-migration.test.ts": 570, - "src/main/runtime/orchestration/setup-completion-signal.test.ts": 54, - "src/main/runtime/orchestration/structured-mail-source.test.ts": 302, - "src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts": 664, - "src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts": 401, - "src/main/runtime/orchestration/structured-pointer-operation-id.test.ts": 91, - "src/main/runtime/orchestration/structured-session-mail-target.test.ts": 4683, - "src/main/runtime/orchestration/structured-session-pointer-delivery.test.ts": 288, - "src/main/runtime/orchestration/structured-worker-direct-mailbox-target.test.ts": 4219, - "src/main/runtime/orchestration/structured-worker-group-addressing.test.ts": 513, - "src/main/runtime/orchestration/structured-worker-journal-archive.test.ts": 354, - "src/main/runtime/orchestration/structured-worker-journal-page.test.ts": 978, - "src/main/runtime/orchestration/task-deps-flag.test.ts": 70, - "src/main/runtime/orchestration/worker-attention-context.test.ts": 89, - "src/main/runtime/orchestration/worker-dispatch-repair-safety.test.ts": 2467, - "src/main/runtime/orchestration/worker-dispatch-settlement.test.ts": 1181, - "src/main/runtime/orchestration/worker-output-archive-bounding.test.ts": 794, - "src/main/runtime/orchestration/worker-output-archive.test.ts": 801, - "src/main/runtime/orchestration/worker-output-cursor.test.ts": 51, - "src/main/runtime/orchestration/worker-provider-session.test.ts": 59, - "src/main/runtime/orchestration/worker-start-unobserved-prompt-settlement.test.ts": 506, - "src/main/runtime/orchestration/worker-terminal-process-liveness.test.ts": 58, - "src/main/runtime/orchestration/worker-transcript-payload.test.ts": 319, - "src/main/runtime/orchestration/worker-transcript-read.test.ts": 844, - "src/main/runtime/orchestration/worker-transcript-remote-read.test.ts": 668, - "src/main/runtime/paired-close-retirement-proof-publication-order.test.ts": 4291, - "src/main/runtime/pairing-endpoint.test.ts": 264, - "src/main/runtime/prime-agent-screen-readiness-transcripts.test.ts": 7440, - "src/main/runtime/project-host-setup-delete-guard.test.ts": 387, - "src/main/runtime/proven-absent-leaf-pty-verdicts.test.ts": 41, - "src/main/runtime/pty-exit-agent-status-reconciliation.test.ts": 4014, - "src/main/runtime/pty-exit-record-wait.test.ts": 4307, - "src/main/runtime/pty-inventory-lifecycle-admission.test.ts": 4897, - "src/main/runtime/pty-inventory-lifecycle-scope.test.ts": 5629, - "src/main/runtime/pty-inventory-liveness-verdict.test.ts": 4512, - "src/main/runtime/pty-inventory-partial-relay-liveness.test.ts": 4555, - "src/main/runtime/pty-lifecycle-generation-retention.test.ts": 5736, - "src/main/runtime/pty-recorded-surface-topology.test.ts": 47, - "src/main/runtime/pty-shell-ownership-mirror.test.ts": 1615, - "src/main/runtime/pty-transcript-prune-wait-cache.test.ts": 4405, - "src/main/runtime/public-ssh-state.test.ts": 42, - "src/main/runtime/push/desktop-push-service-unreadable-outbox.test.ts": 369, - "src/main/runtime/push/desktop-push-service.test.ts": 455, - "src/main/runtime/push/push-agent-state.test.ts": 61, - "src/main/runtime/push/push-cleanup-auth-expiry.test.ts": 392, - "src/main/runtime/push/push-delivery-policy.test.ts": 75, - "src/main/runtime/push/push-device-registration-persistence.test.ts": 129, - "src/main/runtime/push/push-dispatcher.test.ts": 84, - "src/main/runtime/push/push-gateway-client.test.ts": 415, - "src/main/runtime/push/push-gateway-session.test.ts": 392, - "src/main/runtime/push/push-host-proof-vector.test.ts": 60, - "src/main/runtime/push/push-host-proof.test.ts": 155, - "src/main/runtime/push/push-outcome-counters.test.ts": 39, - "src/main/runtime/push/push-policy-pipeline.integration.test.ts": 358, - "src/main/runtime/push/push-preferences.test.ts": 66, - "src/main/runtime/push/push-registration-races.test.ts": 398, - "src/main/runtime/push/push-registration-rpc.test.ts": 5420, - "src/main/runtime/push/push-unpair-persistence.test.ts": 5651, - "src/main/runtime/push/push-unregister-outbox.test.ts": 91, - "src/main/runtime/qoder-independent-spawn-review.test.ts": 4429, - "src/main/runtime/qoder-mobile-resume-adoption.test.ts": 6484, - "src/main/runtime/qoder-terminal-command-resolution.test.ts": 148, - "src/main/runtime/quarter-circle-title-send-authorization.test.ts": 16020, - "src/main/runtime/queued-terminal-graph-exit.test.ts": 5194, - "src/main/runtime/recent-pty-output-buffer.test.ts": 137, - "src/main/runtime/recent-pty-output-retention.test.ts": 160, - "src/main/runtime/relay/desktop-relay-service-broker-liveness.test.ts": 306, - "src/main/runtime/relay/desktop-relay-service-installer.test.ts": 48, - "src/main/runtime/relay/desktop-relay-service.test.ts": 743, - "src/main/runtime/relay/mobile-relay-e2ee.integration.test.ts": 526, - "src/main/runtime/relay/relay-auth-coordinator-readiness.test.ts": 526, - "src/main/runtime/relay/relay-auth-coordinator-recovery.test.ts": 326, + "src/main/runtime/orchestration/orchestration-federated-legacy-probe.test.ts": 1087, + "src/main/runtime/orchestration/orchestration-legacy-coordinator-authority-db.test.ts": 1143, + "src/main/runtime/orchestration/orchestration-legacy-question-migration-db.test.ts": 902, + "src/main/runtime/orchestration/orchestration-legacy-storage-db.test.ts": 1695, + "src/main/runtime/orchestration/orchestration-legacy-worker-terminal-recovery.test.ts": 294, + "src/main/runtime/orchestration/orchestration-mutation-question-db.test.ts": 569, + "src/main/runtime/orchestration/orchestration-orca-session-column-migration.test.ts": 912, + "src/main/runtime/orchestration/orchestration-party-cli-address.test.ts": 315, + "src/main/runtime/orchestration/orchestration-party-location.test.ts": 617, + "src/main/runtime/orchestration/orchestration-peer-capability-cache.test.ts": 267, + "src/main/runtime/orchestration/orchestration-reset-db.test.ts": 587, + "src/main/runtime/orchestration/orchestration-run-delivery-db.test.ts": 704, + "src/main/runtime/orchestration/orchestration-run-list-compatibility.test.ts": 558, + "src/main/runtime/orchestration/orchestration-version-skew-migration.test.ts": 1124, + "src/main/runtime/orchestration/orchestration-worker-dispatch-db.test.ts": 846, + "src/main/runtime/orchestration/preamble.test.ts": 148, + "src/main/runtime/orchestration/r1-identity-migration.test.ts": 842, + "src/main/runtime/orchestration/run-coordinator-handle-migration.test.ts": 688, + "src/main/runtime/orchestration/run-coordinator-orca-session-address.test.ts": 751, + "src/main/runtime/orchestration/send-agent-turn-host.test.ts": 2017, + "src/main/runtime/orchestration/send-agent-turn.test.ts": 261, + "src/main/runtime/orchestration/settled-question-threads-migration.test.ts": 585, + "src/main/runtime/orchestration/setup-completion-signal.test.ts": 47, + "src/main/runtime/orchestration/structured-mail-source.test.ts": 324, + "src/main/runtime/orchestration/structured-mailbox-pointer-delivery.test.ts": 574, + "src/main/runtime/orchestration/structured-mailbox-pointer-host.test.ts": 337, + "src/main/runtime/orchestration/structured-pointer-operation-id.test.ts": 266, + "src/main/runtime/orchestration/structured-session-mail-target.test.ts": 4901, + "src/main/runtime/orchestration/structured-session-pointer-delivery.test.ts": 305, + "src/main/runtime/orchestration/structured-worker-direct-mailbox-target.test.ts": 3717, + "src/main/runtime/orchestration/structured-worker-group-addressing.test.ts": 461, + "src/main/runtime/orchestration/structured-worker-journal-archive.test.ts": 340, + "src/main/runtime/orchestration/structured-worker-journal-page.test.ts": 1086, + "src/main/runtime/orchestration/task-deps-flag.test.ts": 53, + "src/main/runtime/orchestration/worker-attention-context.test.ts": 38, + "src/main/runtime/orchestration/worker-dispatch-repair-safety.test.ts": 2611, + "src/main/runtime/orchestration/worker-dispatch-settlement.test.ts": 1004, + "src/main/runtime/orchestration/worker-output-archive-bounding.test.ts": 798, + "src/main/runtime/orchestration/worker-output-archive.test.ts": 685, + "src/main/runtime/orchestration/worker-output-cursor.test.ts": 37, + "src/main/runtime/orchestration/worker-provider-session.test.ts": 37, + "src/main/runtime/orchestration/worker-start-unobserved-prompt-settlement.test.ts": 570, + "src/main/runtime/orchestration/worker-terminal-process-liveness.test.ts": 33, + "src/main/runtime/orchestration/worker-terminal-redaction.test.ts": 58, + "src/main/runtime/orchestration/worker-transcript-payload.test.ts": 305, + "src/main/runtime/orchestration/worker-transcript-read.test.ts": 619, + "src/main/runtime/paired-close-retirement-proof-publication-order.test.ts": 5259, + "src/main/runtime/pairing-endpoint.test.ts": 242, + "src/main/runtime/prime-agent-screen-readiness-transcripts.test.ts": 7502, + "src/main/runtime/project-host-setup-delete-guard.test.ts": 752, + "src/main/runtime/proven-absent-leaf-pty-verdicts.test.ts": 40, + "src/main/runtime/pty-exit-agent-status-reconciliation.test.ts": 4257, + "src/main/runtime/pty-exit-record-wait.test.ts": 4417, + "src/main/runtime/pty-inventory-lifecycle-admission.test.ts": 4218, + "src/main/runtime/pty-inventory-lifecycle-scope.test.ts": 3977, + "src/main/runtime/pty-inventory-liveness-verdict.test.ts": 4048, + "src/main/runtime/pty-inventory-partial-relay-liveness.test.ts": 4199, + "src/main/runtime/pty-lifecycle-generation-retention.test.ts": 5257, + "src/main/runtime/pty-recorded-surface-topology.test.ts": 37, + "src/main/runtime/pty-shell-ownership-mirror.test.ts": 1597, + "src/main/runtime/pty-transcript-prune-wait-cache.test.ts": 5374, + "src/main/runtime/public-ssh-state.test.ts": 28, + "src/main/runtime/push/desktop-push-service-unreadable-outbox.test.ts": 299, + "src/main/runtime/push/desktop-push-service.test.ts": 413, + "src/main/runtime/push/push-agent-state.test.ts": 38, + "src/main/runtime/push/push-cleanup-auth-expiry.test.ts": 512, + "src/main/runtime/push/push-delivery-policy.test.ts": 41, + "src/main/runtime/push/push-device-registration-persistence.test.ts": 76, + "src/main/runtime/push/push-dispatcher.test.ts": 65, + "src/main/runtime/push/push-gateway-client.test.ts": 412, + "src/main/runtime/push/push-gateway-session.test.ts": 299, + "src/main/runtime/push/push-host-proof-vector.test.ts": 36, + "src/main/runtime/push/push-host-proof.test.ts": 157, + "src/main/runtime/push/push-outcome-counters.test.ts": 46, + "src/main/runtime/push/push-policy-pipeline.integration.test.ts": 379, + "src/main/runtime/push/push-preferences.test.ts": 39, + "src/main/runtime/push/push-registration-races.test.ts": 423, + "src/main/runtime/push/push-registration-rpc.test.ts": 5720, + "src/main/runtime/push/push-unpair-persistence.test.ts": 5266, + "src/main/runtime/push/push-unregister-outbox.test.ts": 54, + "src/main/runtime/qoder-independent-spawn-review.test.ts": 4131, + "src/main/runtime/qoder-mobile-resume-adoption.test.ts": 7211, + "src/main/runtime/qoder-terminal-command-resolution.test.ts": 137, + "src/main/runtime/quarter-circle-title-send-authorization.test.ts": 19646, + "src/main/runtime/queued-terminal-graph-exit.test.ts": 4535, + "src/main/runtime/recent-pty-output-buffer.test.ts": 121, + "src/main/runtime/recent-pty-output-retention.test.ts": 179, + "src/main/runtime/relay/desktop-relay-service-broker-liveness.test.ts": 323, + "src/main/runtime/relay/desktop-relay-service-installer.test.ts": 40, + "src/main/runtime/relay/desktop-relay-service.test.ts": 690, + "src/main/runtime/relay/mobile-relay-e2ee.integration.test.ts": 481, + "src/main/runtime/relay/relay-auth-coordinator-readiness.test.ts": 507, + "src/main/runtime/relay/relay-auth-coordinator-recovery.test.ts": 296, "src/main/runtime/relay/relay-auth-coordinator.test.ts": 1435, - "src/main/runtime/relay/relay-auth-host-close-reason.test.ts": 355, - "src/main/runtime/relay/relay-control-client.test.ts": 788, - "src/main/runtime/relay/relay-control-close-reason.test.ts": 339, - "src/main/runtime/relay/relay-control-origin.test.ts": 284, - "src/main/runtime/relay/relay-control-request-retirement.test.ts": 296, - "src/main/runtime/relay/relay-control-timeout-classification.test.ts": 244, - "src/main/runtime/relay/relay-demand-ledger.test.ts": 98, - "src/main/runtime/relay/relay-host-proof.test.ts": 166, - "src/main/runtime/relay/relay-http-client.test.ts": 279, - "src/main/runtime/relay/relay-region-correction.test.ts": 312, - "src/main/runtime/relay/relay-region-preference.test.ts": 352, - "src/main/runtime/relay/relay-region-probe-log.test.ts": 275, - "src/main/runtime/relay/relay-region-refresh.test.ts": 249, - "src/main/runtime/relay/relay-renewal-jitter.test.ts": 254, - "src/main/runtime/relay/relay-revoke-outbox.test.ts": 102, - "src/main/runtime/relay/relay-session-broker.test.ts": 859, - "src/main/runtime/remote-agent-session-host-authority.integration.test.ts": 5984, - "src/main/runtime/remote-browser-screencast-frame-admission.test.ts": 409, - "src/main/runtime/remote-desktop-driver.test.ts": 4594, - "src/main/runtime/remote-desktop-terminal-floor-reconnect.test.ts": 46, - "src/main/runtime/remote-runtime-close-intent.integration.test.ts": 4011, - "src/main/runtime/remote-runtime-request-connection.integration.test.ts": 3568, - "src/main/runtime/remote-server-updater.test.ts": 51, - "src/main/runtime/renderer-browser-session-reconciliation.test.ts": 2554, - "src/main/runtime/repo-icon-fork-backfill.test.ts": 4230, - "src/main/runtime/repo-worktree-admin-fingerprint.test.ts": 813, - "src/main/runtime/repo-worktree-resolution-scan.test.ts": 54, - "src/main/runtime/repo-worktree-row-resolution.test.ts": 231, - "src/main/runtime/retained-tail-redraw-window.equivalence.test.ts": 724, - "src/main/runtime/ripgrep-filename-identity.test.ts": 68, - "src/main/runtime/rpc/core-typed-method-contract.test.ts": 257, - "src/main/runtime/rpc/dispatcher-browser-client-automation.test.ts": 482, - "src/main/runtime/rpc/dispatcher-computer-errors.test.ts": 559, - "src/main/runtime/rpc/dispatcher-feature-interactions.test.ts": 601, - "src/main/runtime/rpc/dispatcher-request-parsing.test.ts": 296, - "src/main/runtime/rpc/dispatcher-transport-parity.test.ts": 5920, - "src/main/runtime/rpc/e2ee-channel-text-backpressure.test.ts": 300, - "src/main/runtime/rpc/e2ee-channel-v2.test.ts": 412, - "src/main/runtime/rpc/e2ee-channel.test.ts": 659, - "src/main/runtime/rpc/e2ee-crypto.test.ts": 139, - "src/main/runtime/rpc/e2ee-integration.test.ts": 357, - "src/main/runtime/rpc/errors.test.ts": 357, - "src/main/runtime/rpc/methods/accounts.test.ts": 248, - "src/main/runtime/rpc/methods/agent-hooks.test.ts": 233, - "src/main/runtime/rpc/methods/agent-launch-caller-renders.test.ts": 1188, - "src/main/runtime/rpc/methods/agent-launch-caller-selection.test.ts": 1753, - "src/main/runtime/rpc/methods/agent-launch-floating-workspace.test.ts": 4476, - "src/main/runtime/rpc/methods/agent-launch-folder-workspace-create.test.ts": 1776, - "src/main/runtime/rpc/methods/agent-launch-instant-tab.test.ts": 2435, - "src/main/runtime/rpc/methods/agent-launch-pane-reservation.test.ts": 2058, - "src/main/runtime/rpc/methods/agent-launch-prestart-failure.test.ts": 2073, - "src/main/runtime/rpc/methods/agent-launch-prompt-carry.test.ts": 1280, - "src/main/runtime/rpc/methods/agent-launch-replay.test.ts": 1990, - "src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts": 5221, - "src/main/runtime/rpc/methods/agent-launch-session-reservation.test.ts": 2077, - "src/main/runtime/rpc/methods/agent-launch-structured-prompt.test.ts": 245, - "src/main/runtime/rpc/methods/agent-launch-terminal-identity.test.ts": 1355, - "src/main/runtime/rpc/methods/agent-launch-terminal-preferences.test.ts": 1325, - "src/main/runtime/rpc/methods/agent-launch-terminal-prompt.test.ts": 385, - "src/main/runtime/rpc/methods/agent-launch.test.ts": 1715, - "src/main/runtime/rpc/methods/agent-session.test.ts": 699, - "src/main/runtime/rpc/methods/agent-skill-sharing-capability-grant.test.ts": 4568, - "src/main/runtime/rpc/methods/ai-vault-search.test.ts": 4950, - "src/main/runtime/rpc/methods/ai-vault-structured-ownership.test.ts": 568, - "src/main/runtime/rpc/methods/ai-vault.test.ts": 5161, - "src/main/runtime/rpc/methods/artifact-sharing-capability-grant.test.ts": 4562, - "src/main/runtime/rpc/methods/artifacts.test.ts": 371, - "src/main/runtime/rpc/methods/automation-scoped-list-methods.test.ts": 533, - "src/main/runtime/rpc/methods/automations.test.ts": 607, - "src/main/runtime/rpc/methods/browser-client-host-attach-adoption.test.ts": 690, - "src/main/runtime/rpc/methods/browser-client-host-reconciliation.test.ts": 684, - "src/main/runtime/rpc/methods/browser-client-host.test.ts": 609, - "src/main/runtime/rpc/methods/browser-client-page-metadata.test.ts": 637, - "src/main/runtime/rpc/methods/browser-identity-rpc.test.ts": 562, - "src/main/runtime/rpc/methods/browser-network-tunnel.test.ts": 798, - "src/main/runtime/rpc/methods/browser-tab-create-schema.test.ts": 235, - "src/main/runtime/rpc/methods/browser.test.ts": 704, - "src/main/runtime/rpc/methods/client-events.test.ts": 3207, - "src/main/runtime/rpc/methods/client-native-chat-settings.test.ts": 654, - "src/main/runtime/rpc/methods/client-ui-pairing-local-fields.test.ts": 675, - "src/main/runtime/rpc/methods/client-ui-task-resume-state.test.ts": 659, - "src/main/runtime/rpc/methods/client-ui.test.ts": 661, - "src/main/runtime/rpc/methods/clipboard-image-upload-decoding.test.ts": 680, - "src/main/runtime/rpc/methods/clipboard.test.ts": 627, - "src/main/runtime/rpc/methods/computer-actions.test.ts": 326, - "src/main/runtime/rpc/methods/computer.test.ts": 296, - "src/main/runtime/rpc/methods/diagnostics.test.ts": 471, - "src/main/runtime/rpc/methods/file-watch-event-batcher.test.ts": 57, - "src/main/runtime/rpc/methods/files-base64-padding.test.ts": 626, - "src/main/runtime/rpc/methods/files-doc-preview.test.ts": 568, - "src/main/runtime/rpc/methods/files-list-all-page-size.test.ts": 647, - "src/main/runtime/rpc/methods/files-open-navigation.test.ts": 597, - "src/main/runtime/rpc/methods/files-path-search.test.ts": 624, - "src/main/runtime/rpc/methods/files-preview-transport-budget.test.ts": 547, - "src/main/runtime/rpc/methods/files-read-dir-remote-budget.test.ts": 4567, - "src/main/runtime/rpc/methods/files-terminal-path-resolution.test.ts": 568, - "src/main/runtime/rpc/methods/files-watch-cancellation.test.ts": 672, - "src/main/runtime/rpc/methods/files-watch-cleanup.test.ts": 565, - "src/main/runtime/rpc/methods/files.test.ts": 815, - "src/main/runtime/rpc/methods/git-diff-transport-budget.test.ts": 1484, - "src/main/runtime/rpc/methods/git.test.ts": 596, - "src/main/runtime/rpc/methods/github-pr-refresh-reason.test.ts": 538, - "src/main/runtime/rpc/methods/github.test.ts": 619, - "src/main/runtime/rpc/methods/gitlab.test.ts": 600, - "src/main/runtime/rpc/methods/host-capabilities.test.ts": 508, - "src/main/runtime/rpc/methods/hosted-review.test.ts": 1004, - "src/main/runtime/rpc/methods/jira.test.ts": 584, - "src/main/runtime/rpc/methods/linear-agent-access.test.ts": 4650, - "src/main/runtime/rpc/methods/linear-agent-project-access.test.ts": 5785, - "src/main/runtime/rpc/methods/linear.test.ts": 555, - "src/main/runtime/rpc/methods/managed-server.test.ts": 3128, - "src/main/runtime/rpc/methods/mobile-web-bundle-range.test.ts": 3197, - "src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts": 3452, - "src/main/runtime/rpc/methods/mobile-web-bundle-window-refusals.test.ts": 3539, - "src/main/runtime/rpc/methods/mobile-web-bundle.test.ts": 2747, - "src/main/runtime/rpc/methods/native-chat-opencode-semantic-pages.test.ts": 699, - "src/main/runtime/rpc/methods/native-chat-opencode-wire-page-budget.test.ts": 2650, - "src/main/runtime/rpc/methods/native-chat-rpc-block-sanitize.test.ts": 69, - "src/main/runtime/rpc/methods/native-chat-ssh-transcript.test.ts": 1634, + "src/main/runtime/relay/relay-auth-host-close-reason.test.ts": 323, + "src/main/runtime/relay/relay-control-client.test.ts": 735, + "src/main/runtime/relay/relay-control-close-reason.test.ts": 291, + "src/main/runtime/relay/relay-control-origin.test.ts": 230, + "src/main/runtime/relay/relay-control-request-retirement.test.ts": 239, + "src/main/runtime/relay/relay-control-timeout-classification.test.ts": 241, + "src/main/runtime/relay/relay-demand-ledger.test.ts": 89, + "src/main/runtime/relay/relay-host-proof.test.ts": 128, + "src/main/runtime/relay/relay-http-client.test.ts": 271, + "src/main/runtime/relay/relay-region-correction.test.ts": 263, + "src/main/runtime/relay/relay-region-preference.test.ts": 327, + "src/main/runtime/relay/relay-region-probe-log.test.ts": 284, + "src/main/runtime/relay/relay-region-refresh.test.ts": 229, + "src/main/runtime/relay/relay-renewal-jitter.test.ts": 278, + "src/main/runtime/relay/relay-revoke-outbox.test.ts": 50, + "src/main/runtime/relay/relay-session-broker.test.ts": 982, + "src/main/runtime/remote-agent-session-host-authority.integration.test.ts": 7059, + "src/main/runtime/remote-browser-screencast-frame-admission.test.ts": 394, + "src/main/runtime/remote-desktop-driver.test.ts": 4641, + "src/main/runtime/remote-desktop-terminal-floor-reconnect.test.ts": 37, + "src/main/runtime/remote-runtime-close-intent.integration.test.ts": 3427, + "src/main/runtime/remote-runtime-request-connection.integration.test.ts": 3495, + "src/main/runtime/remote-server-updater.test.ts": 34, + "src/main/runtime/renderer-browser-session-reconciliation.test.ts": 2697, + "src/main/runtime/repo-icon-fork-backfill.test.ts": 4918, + "src/main/runtime/repo-worktree-admin-fingerprint.test.ts": 564, + "src/main/runtime/repo-worktree-resolution-scan.test.ts": 40, + "src/main/runtime/repo-worktree-row-resolution.test.ts": 538, + "src/main/runtime/retained-tail-redraw-window.equivalence.test.ts": 812, + "src/main/runtime/ripgrep-filename-identity.test.ts": 81, + "src/main/runtime/rpc/core-typed-method-contract.test.ts": 235, + "src/main/runtime/rpc/device-key-auth.test.ts": 581, + "src/main/runtime/rpc/dispatcher-browser-client-automation.test.ts": 581, + "src/main/runtime/rpc/dispatcher-computer-errors.test.ts": 604, + "src/main/runtime/rpc/dispatcher-feature-interactions.test.ts": 637, + "src/main/runtime/rpc/dispatcher-request-parsing.test.ts": 290, + "src/main/runtime/rpc/dispatcher-transport-parity.test.ts": 7600, + "src/main/runtime/rpc/e2ee-channel-text-backpressure.test.ts": 333, + "src/main/runtime/rpc/e2ee-channel-v2.test.ts": 463, + "src/main/runtime/rpc/e2ee-channel.test.ts": 694, + "src/main/runtime/rpc/e2ee-crypto.test.ts": 116, + "src/main/runtime/rpc/e2ee-integration.test.ts": 393, + "src/main/runtime/rpc/errors.test.ts": 452, + "src/main/runtime/rpc/methods/accounts.test.ts": 259, + "src/main/runtime/rpc/methods/agent-hooks.test.ts": 217, + "src/main/runtime/rpc/methods/agent-launch-caller-renders.test.ts": 1318, + "src/main/runtime/rpc/methods/agent-launch-caller-selection.test.ts": 1682, + "src/main/runtime/rpc/methods/agent-launch-floating-workspace.test.ts": 5095, + "src/main/runtime/rpc/methods/agent-launch-folder-workspace-create.test.ts": 2026, + "src/main/runtime/rpc/methods/agent-launch-instant-tab.test.ts": 2830, + "src/main/runtime/rpc/methods/agent-launch-pane-reservation.test.ts": 2039, + "src/main/runtime/rpc/methods/agent-launch-prestart-failure.test.ts": 1848, + "src/main/runtime/rpc/methods/agent-launch-prompt-carry.test.ts": 1443, + "src/main/runtime/rpc/methods/agent-launch-replay.test.ts": 2421, + "src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts": 4550, + "src/main/runtime/rpc/methods/agent-launch-session-reservation.test.ts": 2135, + "src/main/runtime/rpc/methods/agent-launch-structured-prompt.test.ts": 230, + "src/main/runtime/rpc/methods/agent-launch-terminal-identity.test.ts": 1495, + "src/main/runtime/rpc/methods/agent-launch-terminal-preferences.test.ts": 1617, + "src/main/runtime/rpc/methods/agent-launch-terminal-prompt.test.ts": 380, + "src/main/runtime/rpc/methods/agent-launch.test.ts": 1882, + "src/main/runtime/rpc/methods/agent-session.test.ts": 1680, + "src/main/runtime/rpc/methods/agent-skill-sharing-capability-grant.test.ts": 4489, + "src/main/runtime/rpc/methods/ai-vault-search.test.ts": 4340, + "src/main/runtime/rpc/methods/ai-vault-structured-ownership.test.ts": 1479, + "src/main/runtime/rpc/methods/ai-vault.test.ts": 4712, + "src/main/runtime/rpc/methods/artifact-sharing-capability-grant.test.ts": 4296, + "src/main/runtime/rpc/methods/artifacts.test.ts": 361, + "src/main/runtime/rpc/methods/automatic-work.test.ts": 4674, + "src/main/runtime/rpc/methods/automation-scoped-list-methods.test.ts": 635, + "src/main/runtime/rpc/methods/automations.test.ts": 617, + "src/main/runtime/rpc/methods/browser-client-host-attach-adoption.test.ts": 855, + "src/main/runtime/rpc/methods/browser-client-host-reconciliation.test.ts": 614, + "src/main/runtime/rpc/methods/browser-client-host.test.ts": 681, + "src/main/runtime/rpc/methods/browser-client-page-metadata.test.ts": 909, + "src/main/runtime/rpc/methods/browser-identity-rpc.test.ts": 582, + "src/main/runtime/rpc/methods/browser-network-tunnel.test.ts": 801, + "src/main/runtime/rpc/methods/browser-tab-create-schema.test.ts": 233, + "src/main/runtime/rpc/methods/browser.test.ts": 594, + "src/main/runtime/rpc/methods/client-events.test.ts": 3014, + "src/main/runtime/rpc/methods/client-native-chat-settings.test.ts": 630, + "src/main/runtime/rpc/methods/client-ui-pairing-local-fields.test.ts": 774, + "src/main/runtime/rpc/methods/client-ui-task-resume-state.test.ts": 660, + "src/main/runtime/rpc/methods/client-ui.test.ts": 810, + "src/main/runtime/rpc/methods/clipboard-image-upload-decoding.test.ts": 713, + "src/main/runtime/rpc/methods/clipboard.test.ts": 630, + "src/main/runtime/rpc/methods/computer-actions.test.ts": 316, + "src/main/runtime/rpc/methods/computer.test.ts": 268, + "src/main/runtime/rpc/methods/diagnostics.test.ts": 460, + "src/main/runtime/rpc/methods/file-watch-event-batcher.test.ts": 49, + "src/main/runtime/rpc/methods/files-base64-padding.test.ts": 498, + "src/main/runtime/rpc/methods/files-doc-preview.test.ts": 676, + "src/main/runtime/rpc/methods/files-list-all-page-size.test.ts": 572, + "src/main/runtime/rpc/methods/files-open-navigation.test.ts": 523, + "src/main/runtime/rpc/methods/files-path-search.test.ts": 680, + "src/main/runtime/rpc/methods/files-preview-transport-budget.test.ts": 615, + "src/main/runtime/rpc/methods/files-read-dir-remote-budget.test.ts": 4393, + "src/main/runtime/rpc/methods/files-terminal-path-resolution.test.ts": 636, + "src/main/runtime/rpc/methods/files-watch-cancellation.test.ts": 625, + "src/main/runtime/rpc/methods/files-watch-cleanup.test.ts": 614, + "src/main/runtime/rpc/methods/files.test.ts": 865, + "src/main/runtime/rpc/methods/git-diff-transport-budget.test.ts": 601, + "src/main/runtime/rpc/methods/git.test.ts": 747, + "src/main/runtime/rpc/methods/github-pr-refresh-reason.test.ts": 547, + "src/main/runtime/rpc/methods/github.test.ts": 675, + "src/main/runtime/rpc/methods/gitlab.test.ts": 618, + "src/main/runtime/rpc/methods/host-capabilities.test.ts": 555, + "src/main/runtime/rpc/methods/hosted-review.test.ts": 1241, + "src/main/runtime/rpc/methods/jira-attachment-image.test.ts": 572, + "src/main/runtime/rpc/methods/jira-board-filter-rpc.test.ts": 576, + "src/main/runtime/rpc/methods/jira-saved-filter-state.test.ts": 524, + "src/main/runtime/rpc/methods/jira.test.ts": 639, + "src/main/runtime/rpc/methods/linear-agent-access.test.ts": 4572, + "src/main/runtime/rpc/methods/linear-agent-project-access.test.ts": 7359, + "src/main/runtime/rpc/methods/linear.test.ts": 673, + "src/main/runtime/rpc/methods/managed-server.test.ts": 3191, + "src/main/runtime/rpc/methods/mobile-web-bundle-range.test.ts": 2902, + "src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts": 2455, + "src/main/runtime/rpc/methods/mobile-web-bundle-window-refusals.test.ts": 2455, + "src/main/runtime/rpc/methods/mobile-web-bundle.test.ts": 2575, + "src/main/runtime/rpc/methods/native-chat-opencode-semantic-pages.test.ts": 607, + "src/main/runtime/rpc/methods/native-chat-opencode-wire-page-budget.test.ts": 2617, + "src/main/runtime/rpc/methods/native-chat-rpc-block-sanitize.test.ts": 48, + "src/main/runtime/rpc/methods/native-chat-ssh-transcript.test.ts": 449, "src/main/runtime/rpc/methods/native-chat-subscription-token.test.ts": 212, - "src/main/runtime/rpc/methods/native-chat.test.ts": 293, - "src/main/runtime/rpc/methods/notification-preferences.test.ts": 330, - "src/main/runtime/rpc/methods/notification-reconnect-cooldown.test.ts": 297, - "src/main/runtime/rpc/methods/orcad-migration.test.ts": 3207, - "src/main/runtime/rpc/methods/orcad-terminal-census.test.ts": 214, - "src/main/runtime/rpc/methods/orchestration-dispatch-error-codes.test.ts": 6710, - "src/main/runtime/rpc/methods/orchestration-structured-worker-abandon.test.ts": 3849, - "src/main/runtime/rpc/methods/orchestration-structured-worker-lifecycle.test.ts": 395, - "src/main/runtime/rpc/methods/orchestration-structured-worker-redrive.test.ts": 5166, - "src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts": 462, - "src/main/runtime/rpc/methods/orchestration-structured-worker-start-failure.test.ts": 1247, - "src/main/runtime/rpc/methods/orchestration-worker-mode-opacity.test.ts": 5434, - "src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts": 5547, - "src/main/runtime/rpc/methods/orchestration-worker-start-mode-selection.test.ts": 5253, - "src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts": 1089, - "src/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.ts": 1242, - "src/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.ts": 920, - "src/main/runtime/rpc/methods/orchestration-worker-support-unknown.test.ts": 1109, - "src/main/runtime/rpc/methods/orchestration/cli-runtime-boundary.test.ts": 5257, - "src/main/runtime/rpc/methods/orchestration/federation/federated-attach-receipt.test.ts": 54, - "src/main/runtime/rpc/methods/orchestration/federation/federated-fleet-snapshot.test.ts": 580, - "src/main/runtime/rpc/methods/orchestration/federation/federated-message-targeting.test.ts": 5648, - "src/main/runtime/rpc/methods/orchestration/federation/federated-release-safety.test.ts": 5387, - "src/main/runtime/rpc/methods/orchestration/federation/federated-transport-safety.test.ts": 4379, - "src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.test.ts": 1057, - "src/main/runtime/rpc/methods/orchestration/federation/federated-worker-start-receipt.test.ts": 5244, - "src/main/runtime/rpc/methods/orchestration/federation/federation-agent-launch.test.ts": 5782, - "src/main/runtime/rpc/methods/orchestration/federation/federation-control-mail.test.ts": 6612, - "src/main/runtime/rpc/methods/orchestration/federation/federation-effects.test.ts": 49, - "src/main/runtime/rpc/methods/orchestration/federation/federation-folder-placement.test.ts": 5952, - "src/main/runtime/rpc/methods/orchestration/federation/federation-launched-agent-readiness.test.ts": 5779, - "src/main/runtime/rpc/methods/orchestration/federation/federation-lifecycle-settlement.test.ts": 10158, - "src/main/runtime/rpc/methods/orchestration/federation/federation-liveness-verdict.test.ts": 5824, - "src/main/runtime/rpc/methods/orchestration/federation/federation-output.test.ts": 6850, - "src/main/runtime/rpc/methods/orchestration/federation/federation-setup.test.ts": 5357, - "src/main/runtime/rpc/methods/orchestration/federation/federation-start-prompt-budget.test.ts": 5348, - "src/main/runtime/rpc/methods/orchestration/federation/federation-start-schema.test.ts": 230, - "src/main/runtime/rpc/methods/orchestration/federation/federation-worker-brief-agent-in-front.test.ts": 5354, - "src/main/runtime/rpc/methods/orchestration/federation/federation.test.ts": 6454, - "src/main/runtime/rpc/methods/orchestration/gates/gate-run-authorization.test.ts": 6061, - "src/main/runtime/rpc/methods/orchestration/gates/gates.test.ts": 4295, - "src/main/runtime/rpc/methods/orchestration/messaging/ask.test.ts": 4846, - "src/main/runtime/rpc/methods/orchestration/messaging/check-worker-consumer-fencing.test.ts": 5619, - "src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts": 5245, - "src/main/runtime/rpc/methods/orchestration/messaging/check.test.ts": 4797, - "src/main/runtime/rpc/methods/orchestration/messaging/recipient-routing.test.ts": 4757, - "src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-history.test.ts": 4560, - "src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-interleavings.test.ts": 4411, - "src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-safety.test.ts": 4829, - "src/main/runtime/rpc/methods/orchestration/messaging/send-dispatch-authority.test.ts": 4949, - "src/main/runtime/rpc/methods/orchestration/messaging/send-group.test.ts": 4642, - "src/main/runtime/rpc/methods/orchestration/messaging/send-invalid-type.test.ts": 4314, - "src/main/runtime/rpc/methods/orchestration/messaging/send.test.ts": 4897, - "src/main/runtime/rpc/methods/orchestration/runs/migration-behavior.test.ts": 5370, - "src/main/runtime/rpc/methods/orchestration/runs/run-receipt.test.ts": 223, - "src/main/runtime/rpc/methods/orchestration/runs/runs.test.ts": 4878, - "src/main/runtime/rpc/methods/orchestration/runs/tasks-dispatch.test.ts": 4979, - "src/main/runtime/rpc/methods/orchestration/worker/agent-status-producer-census.test.ts": 6689, - "src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts": 5051, - "src/main/runtime/rpc/methods/orchestration/worker/fleet-status-observed-identity.test.ts": 3341, - "src/main/runtime/rpc/methods/orchestration/worker/fleet-status-terminal-identity.test.ts": 3237, - "src/main/runtime/rpc/methods/orchestration/worker/manual-dispatch-observation.test.ts": 5925, - "src/main/runtime/rpc/methods/orchestration/worker/manual-dispatch-release.test.ts": 5791, - "src/main/runtime/rpc/methods/orchestration/worker/structured-worker-launch-seed-options.test.ts": 332, - "src/main/runtime/rpc/methods/orchestration/worker/worker-interactive-wait.test.ts": 5697, - "src/main/runtime/rpc/methods/orchestration/worker/worker-launch-preferences.test.ts": 289, - "src/main/runtime/rpc/methods/orchestration/worker/worker-list-pagination.test.ts": 5550, - "src/main/runtime/rpc/methods/orchestration/worker/worker-list-run-scope-rpc.test.ts": 5064, - "src/main/runtime/rpc/methods/orchestration/worker/worker-observation.test.ts": 492, - "src/main/runtime/rpc/methods/orchestration/worker/worker-output.test.ts": 732, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-archive.test.ts": 4946, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-inventory.test.ts": 4973, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-liveness-verdict.test.ts": 829, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-mobile-report.test.ts": 4843, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-ownership-guard.test.ts": 4849, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-recovery.test.ts": 5546, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release-runtime-incarnation.test.ts": 4885, - "src/main/runtime/rpc/methods/orchestration/worker/worker-release.test.ts": 5097, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-agent-placement-teardown.test.ts": 1020, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-budgets.test.ts": 48, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-launch-executor-parity.test.ts": 1191, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-outcome-classification.test.ts": 418, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-budget.test.ts": 99, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-contract.test.ts": 6316, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-readiness-settlement.test.ts": 350, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-terminal-target.test.ts": 6240, - "src/main/runtime/rpc/methods/orchestration/worker/worker-start-turn-observation.test.ts": 60, - "src/main/runtime/rpc/methods/orchestration/worker/worker-stop-capability.test.ts": 482, - "src/main/runtime/rpc/methods/orchestration/worker/worker-stop-exit-race.test.ts": 4286, - "src/main/runtime/rpc/methods/orchestration/worker/worker-stop-liveness-verdict.test.ts": 5777, - "src/main/runtime/rpc/methods/orchestration/worker/worker-terminal-custody-at-creation.test.ts": 5458, - "src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts": 5092, - "src/main/runtime/rpc/methods/orchestration/worker/workers-recovery.test.ts": 5732, - "src/main/runtime/rpc/methods/paired-caller-host-id.test.ts": 65, - "src/main/runtime/rpc/methods/pairing.test.ts": 4533, - "src/main/runtime/rpc/methods/plugins.test.ts": 411, - "src/main/runtime/rpc/methods/preflight.test.ts": 520, - "src/main/runtime/rpc/methods/project-host-setup-self-host-stamp.test.ts": 256, - "src/main/runtime/rpc/methods/repo-badge-color.test.ts": 605, - "src/main/runtime/rpc/methods/repo.test.ts": 4674, - "src/main/runtime/rpc/methods/runtime-client-capabilities.test.ts": 565, - "src/main/runtime/rpc/methods/session-tab-agent-capability-mutations.test.ts": 615, - "src/main/runtime/rpc/methods/session-tab-agent-status-projection.test.ts": 88, - "src/main/runtime/rpc/methods/session-tab-browser-placement-mutations.test.ts": 643, - "src/main/runtime/rpc/methods/session-tab-browser-placement-projection.test.ts": 66, - "src/main/runtime/rpc/methods/session-tab-registered-agent-projection.test.ts": 69, - "src/main/runtime/rpc/methods/session-tabs-inventory-census-race.test.ts": 3462, - "src/main/runtime/rpc/methods/session-tabs-inventory-rpc.test.ts": 4327, - "src/main/runtime/rpc/methods/session-tabs-move-validation.test.ts": 4892, - "src/main/runtime/rpc/methods/session-tabs-pane-layout-intent.test.ts": 604, - "src/main/runtime/rpc/methods/session-tabs-retirement-proof-delta.test.ts": 578, - "src/main/runtime/rpc/methods/session-tabs-schemas.test.ts": 255, - "src/main/runtime/rpc/methods/session-tabs-structured-restore.test.ts": 540, - "src/main/runtime/rpc/methods/session-tabs-subscribe-admission.test.ts": 559, - "src/main/runtime/rpc/methods/session-tabs-unsubscribe.test.ts": 639, - "src/main/runtime/rpc/methods/session-tabs.test.ts": 676, - "src/main/runtime/rpc/methods/skills.test.ts": 341, - "src/main/runtime/rpc/methods/speech.test.ts": 550, - "src/main/runtime/rpc/methods/ssh.test.ts": 558, - "src/main/runtime/rpc/methods/status-machine-name-publish-budget.test.ts": 4652, - "src/main/runtime/rpc/methods/status.test.ts": 4704, - "src/main/runtime/rpc/methods/structured-agent-session-admission.test.ts": 3034, - "src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts": 5689, - "src/main/runtime/rpc/methods/structured-agent-session-agents.test.ts": 1000, - "src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts": 6678, - "src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts": 2917, - "src/main/runtime/rpc/methods/structured-agent-session-attention-ack.test.ts": 3201, - "src/main/runtime/rpc/methods/structured-agent-session-background-task-capability.test.ts": 3064, - "src/main/runtime/rpc/methods/structured-agent-session-conversation-outline.test.ts": 3264, - "src/main/runtime/rpc/methods/structured-agent-session-create-message.test.ts": 1688, - "src/main/runtime/rpc/methods/structured-agent-session-create-tab-publication.test.ts": 290, - "src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts": 5963, - "src/main/runtime/rpc/methods/structured-agent-session-optional-model-capability.test.ts": 2875, - "src/main/runtime/rpc/methods/structured-agent-session-options-read.test.ts": 3814, - "src/main/runtime/rpc/methods/structured-agent-session-pi-dialog-capability.test.ts": 2971, - "src/main/runtime/rpc/methods/structured-agent-session-policy.test.ts": 89, - "src/main/runtime/rpc/methods/structured-agent-session-precommit-refusal.test.ts": 862, - "src/main/runtime/rpc/methods/structured-agent-session-queued-edit.test.ts": 3153, - "src/main/runtime/rpc/methods/structured-agent-session-respond-params.test.ts": 2947, - "src/main/runtime/rpc/methods/structured-agent-session-restart-dismiss-fence.test.ts": 3237, - "src/main/runtime/rpc/methods/structured-agent-session-restart-resume.test.ts": 3111, - "src/main/runtime/rpc/methods/structured-agent-session-restart-unregistered-agent.test.ts": 3764, - "src/main/runtime/rpc/methods/structured-agent-session-send-compatibility.test.ts": 2815, - "src/main/runtime/rpc/methods/structured-agent-session-turn-completion-stream.test.ts": 2944, - "src/main/runtime/rpc/methods/structured-agent-session-turn-item-capability.test.ts": 3068, - "src/main/runtime/rpc/methods/structured-agent-session-unsubscribe.test.ts": 5112, - "src/main/runtime/rpc/methods/structured-agent-session-visual.test.ts": 3030, - "src/main/runtime/rpc/methods/structured-agent-session.test.ts": 3424, - "src/main/runtime/rpc/methods/structured-chat-tab-table.test.ts": 6245, - "src/main/runtime/rpc/methods/structured-worker-read-cursor.test.ts": 447, - "src/main/runtime/rpc/methods/structured-worker-stop-receipt.test.ts": 5290, - "src/main/runtime/rpc/methods/structured-worker-tab-retirement.test.ts": 4358, - "src/main/runtime/rpc/methods/task-provider-identity.test.ts": 247, - "src/main/runtime/rpc/methods/terminal-create-idempotency.test.ts": 312, - "src/main/runtime/rpc/methods/terminal-legacy-stream-id-characterization.test.ts": 853, - "src/main/runtime/rpc/methods/terminal-manifest-characterization.test.ts": 378, - "src/main/runtime/rpc/methods/terminal-navigation.test.ts": 830, - "src/main/runtime/rpc/methods/terminal-read-screen-cursor.test.ts": 340, - "src/main/runtime/rpc/methods/terminal-stream-extraction-characterization.test.ts": 83, - "src/main/runtime/rpc/methods/terminal/terminal-input-sequence-ledger.test.ts": 82, - "src/main/runtime/rpc/methods/terminal/terminal-inspect-process-params.test.ts": 266, - "src/main/runtime/rpc/methods/terminal/terminal-snapshot-json-byte-budget.test.ts": 190, - "src/main/runtime/rpc/methods/terminal/terminal-snapshot-scrollback-rows.test.ts": 51, - "src/main/runtime/rpc/methods/terminal/terminal-subscription-registration.test.ts": 122, - "src/main/runtime/rpc/methods/terminal/terminal-viewer-colors-method.test.ts": 583, - "src/main/runtime/rpc/methods/updater.test.ts": 275, - "src/main/runtime/rpc/methods/workspace-cleanup-ui-schema.test.ts": 304, - "src/main/runtime/rpc/methods/workspace-layout.test.ts": 3003, - "src/main/runtime/rpc/methods/workspace-ports.test.ts": 559, - "src/main/runtime/rpc/methods/worktree-catalog-snapshot-method.test.ts": 1589, - "src/main/runtime/rpc/methods/worktree-catalog-version-stamp.test.ts": 1496, - "src/main/runtime/rpc/methods/worktree-create-args.test.ts": 351, - "src/main/runtime/rpc/methods/worktree-create-launch-source.test.ts": 1520, - "src/main/runtime/rpc/methods/worktree-create-navigation.test.ts": 1623, - "src/main/runtime/rpc/methods/worktree-create-startup-agent.test.ts": 1540, - "src/main/runtime/rpc/methods/worktree-github-pr-suppression.test.ts": 1481, - "src/main/runtime/rpc/methods/worktree-missing-terminal-teardown.test.ts": 1296, - "src/main/runtime/rpc/methods/worktree-retired-names.test.ts": 1517, - "src/main/runtime/rpc/methods/worktree-rm-background-reply.test.ts": 1604, - "src/main/runtime/rpc/methods/worktree-rm-host-qualification.test.ts": 1546, - "src/main/runtime/rpc/methods/worktree-rm-pty-waiver.test.ts": 1498, - "src/main/runtime/rpc/methods/worktree-schemas.test.ts": 356, - "src/main/runtime/rpc/methods/worktree.test.ts": 1713, - "src/main/runtime/rpc/mobile-auth-acl-critical-path.test.ts": 835, - "src/main/runtime/rpc/mobile-clipboard-image-provenance.test.ts": 72, - "src/main/runtime/rpc/mobile-e2ee-outbound-memory-budget.test.ts": 52, - "src/main/runtime/rpc/mobile-e2ee-v2-desktop-session.test.ts": 102, - "src/main/runtime/rpc/mobile-e2ee-v2-key-schedule.test.ts": 103, - "src/main/runtime/rpc/mobile-socket-wiring.test.ts": 402, - "src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts": 11255, - "src/main/runtime/rpc/orchestration-caller-show.test.ts": 5152, - "src/main/runtime/rpc/orchestration-check-wait-session.test.ts": 5368, - "src/main/runtime/rpc/orchestration-commit-notify-characterization.test.ts": 6014, - "src/main/runtime/rpc/orchestration-contract-fence.test.ts": 5967, - "src/main/runtime/rpc/orchestration-current-authority-precedence.test.ts": 5673, - "src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher.test.ts": 5854, - "src/main/runtime/rpc/orchestration-legacy-coordinator-race.test.ts": 6448, - "src/main/runtime/rpc/orchestration-legacy-fence-jurisdiction.test.ts": 5939, - "src/main/runtime/rpc/orchestration-legacy-question-takeover.test.ts": 5215, - "src/main/runtime/rpc/orchestration-legacy-run-routing.test.ts": 5130, - "src/main/runtime/rpc/orchestration-legacy-session-address-migration.test.ts": 5254, - "src/main/runtime/rpc/orchestration-legacy-takeover-current-authority.test.ts": 4232, - "src/main/runtime/rpc/orchestration-legacy-takeover-delivery.test.ts": 5733, - "src/main/runtime/rpc/orchestration-legacy-takeover-dispatcher.test.ts": 5702, - "src/main/runtime/rpc/orchestration-mutation-executor.test.ts": 5127, - "src/main/runtime/rpc/orchestration-mutation-ledger.test.ts": 5979, - "src/main/runtime/rpc/orchestration-mutation-request-show-legacy.test.ts": 5437, - "src/main/runtime/rpc/orchestration-mutation-request-show.test.ts": 5333, - "src/main/runtime/rpc/orchestration-party-addressing.test.ts": 5971, - "src/main/runtime/rpc/orchestration-party-agreement.test.ts": 5225, - "src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts": 5613, - "src/main/runtime/rpc/orchestration-session-caller.test.ts": 5634, - "src/main/runtime/rpc/orchestration-session-coordinator.test.ts": 6501, - "src/main/runtime/rpc/orchestration-session-recipient.test.ts": 5755, - "src/main/runtime/rpc/orchestration-session-run-binding.test.ts": 5278, - "src/main/runtime/rpc/orchestration-task-dispatch-invariant.test.ts": 5900, - "src/main/runtime/rpc/relay-transport.test.ts": 193, - "src/main/runtime/rpc/remote-runtime-server-heartbeat-missed-probe-tolerance.test.ts": 67, - "src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts": 82, - "src/main/runtime/rpc/rpc-caller-scope.test.ts": 5768, - "src/main/runtime/rpc/runtime-client-capabilities.test.ts": 40, - "src/main/runtime/rpc/runtime-close-attribution-topology.test.ts": 565, - "src/main/runtime/rpc/runtime-close-attribution.test.ts": 937, - "src/main/runtime/rpc/runtime-source-admission.test.ts": 4337, - "src/main/runtime/rpc/schemas.test.ts": 1500, - "src/main/runtime/rpc/static-web-client-handler-stream.test.ts": 1767, - "src/main/runtime/rpc/streaming.test.ts": 690, - "src/main/runtime/rpc/terminal-agent-prompt-send.test.ts": 611, - "src/main/runtime/rpc/terminal-agent-send-guard.test.ts": 612, - "src/main/runtime/rpc/terminal-geometry-stale-handle.test.ts": 706, - "src/main/runtime/rpc/terminal-lease-stale-handle-survival.test.ts": 861, - "src/main/runtime/rpc/terminal-list-host-scope-transport.test.ts": 531, - "src/main/runtime/rpc/terminal-multiplex-ack-output-budget.test.ts": 1151, - "src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery-history.test.ts": 703, - "src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery.test.ts": 2045, - "src/main/runtime/rpc/terminal-multiplex-desktop-resize-routing.test.ts": 1317, - "src/main/runtime/rpc/terminal-multiplex-end-verdict.test.ts": 3540, - "src/main/runtime/rpc/terminal-multiplex-escape-tail.test.ts": 633, - "src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts": 90, - "src/main/runtime/rpc/terminal-multiplex-initial-snapshot-buffering.test.ts": 945, - "src/main/runtime/rpc/terminal-multiplex-input-sequence-replay.test.ts": 1902, - "src/main/runtime/rpc/terminal-multiplex-input-write-rejection.test.ts": 1429, - "src/main/runtime/rpc/terminal-multiplex-output-pause-and-viewport.test.ts": 1047, - "src/main/runtime/rpc/terminal-multiplex-pty-wait-capacity.test.ts": 1137, - "src/main/runtime/rpc/terminal-multiplex-resync-replay-trim.test.ts": 641, - "src/main/runtime/rpc/terminal-multiplex-round-robin.test.ts": 49, - "src/main/runtime/rpc/terminal-multiplex-snapshot-serialization.test.ts": 1278, - "src/main/runtime/rpc/terminal-multiplex-subscribe-slot-recovery.test.ts": 790, - "src/main/runtime/rpc/terminal-opencode-send-guard.integration.test.ts": 4307, - "src/main/runtime/rpc/terminal-output-batching.test.ts": 965, - "src/main/runtime/rpc/terminal-output-frame-chunks.test.ts": 66, - "src/main/runtime/rpc/terminal-prompt-delivery-receipt.test.ts": 6663, - "src/main/runtime/rpc/terminal-provider-snapshot-sequence.test.ts": 807, - "src/main/runtime/rpc/terminal-requested-snapshot-unavailability.test.ts": 1018, - "src/main/runtime/rpc/terminal-send-launch-draft-resolution.test.ts": 543, - "src/main/runtime/rpc/terminal-send.test.ts": 832, - "src/main/runtime/rpc/terminal-stream-byte-length.test.ts": 450, - "src/main/runtime/rpc/terminal-subscribe-admission.test.ts": 5270, - "src/main/runtime/rpc/terminal-subscribe-blank-mount.test.ts": 685, - "src/main/runtime/rpc/terminal-subscribe-buffer.test.ts": 1142, - "src/main/runtime/rpc/terminal-subscribe-lease-only.test.ts": 734, - "src/main/runtime/rpc/terminal-subscribe-mount-replay.test.ts": 844, - "src/main/runtime/rpc/terminal-subscribe-mounted-pane.test.ts": 631, - "src/main/runtime/rpc/terminal-subscribe-ownership.test.ts": 1030, - "src/main/runtime/rpc/terminal-subscribe-reconnect-rebind.test.ts": 792, - "src/main/runtime/rpc/terminal-subscribe-relay-drop-lease-survival.test.ts": 4313, - "src/main/runtime/rpc/terminal-subscribe-renderer-recovery-output.test.ts": 674, - "src/main/runtime/rpc/unix-socket-transport.test.ts": 86, - "src/main/runtime/rpc/unpaired-device-auth-throttle.test.ts": 45, - "src/main/runtime/rpc/worktree-catalog-snapshot.test.ts": 64, - "src/main/runtime/rpc/worktree-removal-marker-projection.test.ts": 289, - "src/main/runtime/rpc/ws-empty-payload-frame-write.test.ts": 118, - "src/main/runtime/rpc/ws-fallback-port-store.test.ts": 51, - "src/main/runtime/rpc/ws-transport-accept-order.test.ts": 78, - "src/main/runtime/rpc/ws-transport-static-web.test.ts": 99, - "src/main/runtime/rpc/ws-transport-transient-packet-loss.test.ts": 691, - "src/main/runtime/rpc/ws-transport.test.ts": 1073, - "src/main/runtime/run-terminal-shell-alone.node-pty.test.ts": 2371, - "src/main/runtime/run-terminal-shell-alone.test.ts": 114, - "src/main/runtime/runtime-agent-row-main-agent-projection.test.ts": 94, - "src/main/runtime/runtime-binary-message-router.test.ts": 49, - "src/main/runtime/runtime-browser-client-automation.test.ts": 301, - "src/main/runtime/runtime-browser-client-page-adoption.test.ts": 275, - "src/main/runtime/runtime-browser-client-page-creation.test.ts": 254, - "src/main/runtime/runtime-browser-client-page-recovery.test.ts": 302, - "src/main/runtime/runtime-browser-client-page-restored-recovery.test.ts": 237, - "src/main/runtime/runtime-browser-network-execution-host.test.ts": 45, - "src/main/runtime/runtime-browser-page-registry.test.ts": 55, - "src/main/runtime/runtime-client-settings-linear-team-projection.test.ts": 776, - "src/main/runtime/runtime-client-settings-minimax-projection.test.ts": 568, - "src/main/runtime/runtime-client-settings-source-control-projection.test.ts": 645, - "src/main/runtime/runtime-client-settings-terminal-copy-projection.test.ts": 605, - "src/main/runtime/runtime-created-worktree-terminal-target.test.ts": 4338, - "src/main/runtime/runtime-extraction-regressions.test.ts": 4478, - "src/main/runtime/runtime-failed-local-worktree-removal.test.ts": 723, - "src/main/runtime/runtime-file-listing-producer-budgets.test.ts": 528, - "src/main/runtime/runtime-file-target-execution-host.test.ts": 4300, - "src/main/runtime/runtime-file-upload-decode.test.ts": 421, - "src/main/runtime/runtime-folder-workspace.test.ts": 65, - "src/main/runtime/runtime-folder-worktree-create.test.ts": 132, - "src/main/runtime/runtime-git-api-contract.test.ts": 982, - "src/main/runtime/runtime-git-branch-compare-admission.test.ts": 525, - "src/main/runtime/runtime-git-command-target.test.ts": 630, - "src/main/runtime/runtime-git-conflict-operation-routing.test.ts": 561, - "src/main/runtime/runtime-git-execution-host-ownership.test.ts": 959, - "src/main/runtime/runtime-git-generation-admission.test.ts": 551, - "src/main/runtime/runtime-git-stage-worktree-scope.test.ts": 959, - "src/main/runtime/runtime-git-status-admission.test.ts": 1309, - "src/main/runtime/runtime-git-sync-commands.test.ts": 995, - "src/main/runtime/runtime-git-target-execution-host.test.ts": 5048, - "src/main/runtime/runtime-graph-reload-lifecycle.test.ts": 51, - "src/main/runtime/runtime-hook-agent-row-selection.test.ts": 99, - "src/main/runtime/runtime-interrupted-local-worktree-removal.test.ts": 1473, - "src/main/runtime/runtime-legacy-worker-missing-workspace-recovery.test.ts": 81, - "src/main/runtime/runtime-legacy-worker-recovery-lifetime.test.ts": 150, - "src/main/runtime/runtime-legacy-worker-terminal-recovery-persistence.test.ts": 399, - "src/main/runtime/runtime-legacy-worker-terminal-recovery-retry.test.ts": 96, - "src/main/runtime/runtime-linear-read-commands.test.ts": 161, - "src/main/runtime/runtime-local-create-rearm-ordering.test.ts": 4256, - "src/main/runtime/runtime-local-worktree-create.test.ts": 588, - "src/main/runtime/runtime-local-worktree-materialization.test.ts": 130, - "src/main/runtime/runtime-local-worktree-setup.test.ts": 373, - "src/main/runtime/runtime-local-worktree-terminal-startup.test.ts": 81, - "src/main/runtime/runtime-machine-name.test.ts": 78, - "src/main/runtime/runtime-managed-worktree-metadata-sweep.test.ts": 1804, - "src/main/runtime/runtime-managed-worktree-metadata.test.ts": 94, - "src/main/runtime/runtime-managed-worktree-queries.test.ts": 543, - "src/main/runtime/runtime-metadata-ownership-watch.test.ts": 116, - "src/main/runtime/runtime-metadata.test.ts": 380, - "src/main/runtime/runtime-mobile-agent-status-builder.test.ts": 91, - "src/main/runtime/files/search/mobile-path-search-expiry.test.ts": 223, - "src/main/runtime/files/search/mobile-path-search.test.ts": 75, - "src/main/runtime/runtime-mobile-session-incarnation-projection.test.ts": 80, - "src/main/runtime/runtime-orchestration-sender-names.test.ts": 948, - "src/main/runtime/runtime-owned-terminal-publication-lineage.test.ts": 5659, - "src/main/runtime/runtime-pairing-desktop-control-grant.test.ts": 5025, - "src/main/runtime/runtime-project-group-controller-folder-create.test.ts": 57, - "src/main/runtime/runtime-project-group-controller-folder-delete.test.ts": 56, - "src/main/runtime/runtime-project-host-setup-controller.test.ts": 410, - "src/main/runtime/runtime-reference-agents.test.ts": 274, + "src/main/runtime/rpc/methods/native-chat.test.ts": 274, + "src/main/runtime/rpc/methods/notification-preferences.test.ts": 272, + "src/main/runtime/rpc/methods/notification-reconnect-cooldown.test.ts": 281, + "src/main/runtime/rpc/methods/orcad-migration.test.ts": 2684, + "src/main/runtime/rpc/methods/orcad-terminal-census.test.ts": 213, + "src/main/runtime/rpc/methods/orchestration-dispatch-error-codes.test.ts": 6440, + "src/main/runtime/rpc/methods/orchestration-structured-worker-abandon.test.ts": 4782, + "src/main/runtime/rpc/methods/orchestration-structured-worker-lifecycle.test.ts": 473, + "src/main/runtime/rpc/methods/orchestration-structured-worker-redrive.test.ts": 5450, + "src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts": 546, + "src/main/runtime/rpc/methods/orchestration-structured-worker-start-failure.test.ts": 1492, + "src/main/runtime/rpc/methods/orchestration-worker-mode-opacity.test.ts": 6174, + "src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts": 5582, + "src/main/runtime/rpc/methods/orchestration-worker-start-mode-selection.test.ts": 6301, + "src/main/runtime/rpc/methods/orchestration-worker-start-mode.test.ts": 1106, + "src/main/runtime/rpc/methods/orchestration-worker-start-receipt-wording.test.ts": 1227, + "src/main/runtime/rpc/methods/orchestration-worker-start-registered-agent.test.ts": 1237, + "src/main/runtime/rpc/methods/orchestration-worker-support-unknown.test.ts": 1193, + "src/main/runtime/rpc/methods/orchestration/cli-runtime-boundary.test.ts": 5949, + "src/main/runtime/rpc/methods/orchestration/federation/federated-attach-receipt.test.ts": 45, + "src/main/runtime/rpc/methods/orchestration/federation/federated-fleet-snapshot.test.ts": 463, + "src/main/runtime/rpc/methods/orchestration/federation/federated-message-targeting.test.ts": 5468, + "src/main/runtime/rpc/methods/orchestration/federation/federated-release-safety.test.ts": 4364, + "src/main/runtime/rpc/methods/orchestration/federation/federated-transport-safety.test.ts": 4461, + "src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.test.ts": 1205, + "src/main/runtime/rpc/methods/orchestration/federation/federated-worker-start-receipt.test.ts": 5806, + "src/main/runtime/rpc/methods/orchestration/federation/federation-agent-launch.test.ts": 5149, + "src/main/runtime/rpc/methods/orchestration/federation/federation-control-mail.test.ts": 6085, + "src/main/runtime/rpc/methods/orchestration/federation/federation-effects.test.ts": 27, + "src/main/runtime/rpc/methods/orchestration/federation/federation-folder-placement.test.ts": 5796, + "src/main/runtime/rpc/methods/orchestration/federation/federation-launched-agent-readiness.test.ts": 5382, + "src/main/runtime/rpc/methods/orchestration/federation/federation-lifecycle-settlement.test.ts": 7705, + "src/main/runtime/rpc/methods/orchestration/federation/federation-liveness-verdict.test.ts": 5800, + "src/main/runtime/rpc/methods/orchestration/federation/federation-output.test.ts": 7774, + "src/main/runtime/rpc/methods/orchestration/federation/federation-setup.test.ts": 4269, + "src/main/runtime/rpc/methods/orchestration/federation/federation-start-prompt-budget.test.ts": 4274, + "src/main/runtime/rpc/methods/orchestration/federation/federation-start-schema.test.ts": 204, + "src/main/runtime/rpc/methods/orchestration/federation/federation-worker-brief-agent-in-front.test.ts": 5603, + "src/main/runtime/rpc/methods/orchestration/federation/federation.test.ts": 8450, + "src/main/runtime/rpc/methods/orchestration/gates/gate-run-authorization.test.ts": 7032, + "src/main/runtime/rpc/methods/orchestration/gates/gates.test.ts": 4535, + "src/main/runtime/rpc/methods/orchestration/messaging/ask.test.ts": 4467, + "src/main/runtime/rpc/methods/orchestration/messaging/check-worker-consumer-fencing.test.ts": 4128, + "src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts": 5193, + "src/main/runtime/rpc/methods/orchestration/messaging/check.test.ts": 4820, + "src/main/runtime/rpc/methods/orchestration/messaging/recipient-routing.test.ts": 4865, + "src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-history.test.ts": 4739, + "src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-interleavings.test.ts": 4788, + "src/main/runtime/rpc/methods/orchestration/messaging/run-bound-mailbox-safety.test.ts": 4888, + "src/main/runtime/rpc/methods/orchestration/messaging/send-dispatch-authority.test.ts": 4634, + "src/main/runtime/rpc/methods/orchestration/messaging/send-group.test.ts": 5287, + "src/main/runtime/rpc/methods/orchestration/messaging/send-invalid-type.test.ts": 4022, + "src/main/runtime/rpc/methods/orchestration/messaging/send.test.ts": 4934, + "src/main/runtime/rpc/methods/orchestration/runs/migration-behavior.test.ts": 5974, + "src/main/runtime/rpc/methods/orchestration/runs/run-receipt.test.ts": 288, + "src/main/runtime/rpc/methods/orchestration/runs/runs.test.ts": 4221, + "src/main/runtime/rpc/methods/orchestration/runs/tasks-dispatch.test.ts": 4612, + "src/main/runtime/rpc/methods/orchestration/worker/agent-status-producer-census.test.ts": 6840, + "src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts": 4444, + "src/main/runtime/rpc/methods/orchestration/worker/failed-start-retry-release.test.ts": 5155, + "src/main/runtime/rpc/methods/orchestration/worker/fleet-status-observed-identity.test.ts": 2855, + "src/main/runtime/rpc/methods/orchestration/worker/fleet-status-terminal-identity.test.ts": 3069, + "src/main/runtime/rpc/methods/orchestration/worker/manual-dispatch-observation.test.ts": 5231, + "src/main/runtime/rpc/methods/orchestration/worker/manual-dispatch-release.test.ts": 6413, + "src/main/runtime/rpc/methods/orchestration/worker/structured-worker-launch-seed-options.test.ts": 308, + "src/main/runtime/rpc/methods/orchestration/worker/worker-interactive-wait.test.ts": 5356, + "src/main/runtime/rpc/methods/orchestration/worker/worker-launch-preferences.test.ts": 294, + "src/main/runtime/rpc/methods/orchestration/worker/worker-list-pagination.test.ts": 5228, + "src/main/runtime/rpc/methods/orchestration/worker/worker-list-run-scope-rpc.test.ts": 4762, + "src/main/runtime/rpc/methods/orchestration/worker/worker-observation.test.ts": 426, + "src/main/runtime/rpc/methods/orchestration/worker/worker-output.test.ts": 582, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-archive.test.ts": 4438, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-inventory.test.ts": 4297, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-liveness-verdict.test.ts": 716, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-mobile-report.test.ts": 4344, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-ownership-guard.test.ts": 4874, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-recovery.test.ts": 5609, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release-runtime-incarnation.test.ts": 4203, + "src/main/runtime/rpc/methods/orchestration/worker/worker-release.test.ts": 5571, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-agent-placement-teardown.test.ts": 1262, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-budgets.test.ts": 31, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-launch-executor-parity.test.ts": 1242, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-outcome-classification.test.ts": 444, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-budget.test.ts": 100, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-prompt-contract.test.ts": 6233, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-readiness-settlement.test.ts": 432, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-terminal-target.test.ts": 5653, + "src/main/runtime/rpc/methods/orchestration/worker/worker-start-turn-observation.test.ts": 50, + "src/main/runtime/rpc/methods/orchestration/worker/worker-stop-capability.test.ts": 471, + "src/main/runtime/rpc/methods/orchestration/worker/worker-stop-exit-race.test.ts": 4485, + "src/main/runtime/rpc/methods/orchestration/worker/worker-stop-liveness-verdict.test.ts": 5582, + "src/main/runtime/rpc/methods/orchestration/worker/worker-terminal-custody-at-creation.test.ts": 5330, + "src/main/runtime/rpc/methods/orchestration/worker/workers-new-worktree.test.ts": 6057, + "src/main/runtime/rpc/methods/orchestration/worker/workers-recovery.test.ts": 5706, + "src/main/runtime/rpc/methods/paired-caller-host-id.test.ts": 49, + "src/main/runtime/rpc/methods/pairing.test.ts": 4388, + "src/main/runtime/rpc/methods/plugins.test.ts": 420, + "src/main/runtime/rpc/methods/preflight.test.ts": 576, + "src/main/runtime/rpc/methods/project-host-setup-self-host-stamp.test.ts": 278, + "src/main/runtime/rpc/methods/repo-badge-color.test.ts": 674, + "src/main/runtime/rpc/methods/repo.test.ts": 4446, + "src/main/runtime/rpc/methods/runtime-client-capabilities.test.ts": 688, + "src/main/runtime/rpc/methods/session-tab-agent-capability-mutations.test.ts": 1495, + "src/main/runtime/rpc/methods/session-tab-agent-status-projection.test.ts": 195, + "src/main/runtime/rpc/methods/session-tab-browser-placement-mutations.test.ts": 1426, + "src/main/runtime/rpc/methods/session-tab-browser-placement-projection.test.ts": 247, + "src/main/runtime/rpc/methods/session-tab-registered-agent-projection.test.ts": 356, + "src/main/runtime/rpc/methods/session-tabs-inventory-census-race.test.ts": 4614, + "src/main/runtime/rpc/methods/session-tabs-inventory-rpc.test.ts": 4621, + "src/main/runtime/rpc/methods/session-tabs-move-validation.test.ts": 4095, + "src/main/runtime/rpc/methods/session-tabs-pane-layout-intent.test.ts": 1502, + "src/main/runtime/rpc/methods/session-tabs-retirement-proof-delta.test.ts": 1394, + "src/main/runtime/rpc/methods/session-tabs-schemas.test.ts": 261, + "src/main/runtime/rpc/methods/session-tabs-structured-restore.test.ts": 1543, + "src/main/runtime/rpc/methods/session-tabs-subscribe-admission.test.ts": 1595, + "src/main/runtime/rpc/methods/session-tabs-unsubscribe.test.ts": 1385, + "src/main/runtime/rpc/methods/session-tabs.test.ts": 1498, + "src/main/runtime/rpc/methods/skills.test.ts": 344, + "src/main/runtime/rpc/methods/speech.test.ts": 540, + "src/main/runtime/rpc/methods/ssh-target-management.test.ts": 4753, + "src/main/runtime/rpc/methods/ssh.test.ts": 692, + "src/main/runtime/rpc/methods/status-machine-name-publish-budget.test.ts": 4290, + "src/main/runtime/rpc/methods/status.test.ts": 4333, + "src/main/runtime/rpc/methods/structured-agent-session-admission.test.ts": 2947, + "src/main/runtime/rpc/methods/structured-agent-session-adoption-replay.test.ts": 5335, + "src/main/runtime/rpc/methods/structured-agent-session-agents.test.ts": 1228, + "src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts": 7709, + "src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts": 3379, + "src/main/runtime/rpc/methods/structured-agent-session-attention-ack.test.ts": 3131, + "src/main/runtime/rpc/methods/structured-agent-session-background-task-capability.test.ts": 2901, + "src/main/runtime/rpc/methods/structured-agent-session-bounded-frames-rpc.test.ts": 3127, + "src/main/runtime/rpc/methods/structured-agent-session-conversation-outline.test.ts": 3215, + "src/main/runtime/rpc/methods/structured-agent-session-create-message.test.ts": 1984, + "src/main/runtime/rpc/methods/structured-agent-session-create-tab-publication.test.ts": 266, + "src/main/runtime/rpc/methods/structured-agent-session-hold.test.ts": 6384, + "src/main/runtime/rpc/methods/structured-agent-session-optional-model-capability.test.ts": 2992, + "src/main/runtime/rpc/methods/structured-agent-session-options-read.test.ts": 3254, + "src/main/runtime/rpc/methods/structured-agent-session-options-workspace-scope.test.ts": 5673, + "src/main/runtime/rpc/methods/structured-agent-session-permission-create.test.ts": 2743, + "src/main/runtime/rpc/methods/structured-agent-session-pi-dialog-capability.test.ts": 3102, + "src/main/runtime/rpc/methods/structured-agent-session-policy.test.ts": 231, + "src/main/runtime/rpc/methods/structured-agent-session-precommit-refusal.test.ts": 791, + "src/main/runtime/rpc/methods/structured-agent-session-queued-edit.test.ts": 3203, + "src/main/runtime/rpc/methods/structured-agent-session-respond-params.test.ts": 3517, + "src/main/runtime/rpc/methods/structured-agent-session-restart-dismiss-fence.test.ts": 3145, + "src/main/runtime/rpc/methods/structured-agent-session-restart-resume.test.ts": 3513, + "src/main/runtime/rpc/methods/structured-agent-session-restart-unregistered-agent.test.ts": 4737, + "src/main/runtime/rpc/methods/structured-agent-session-send-compatibility.test.ts": 3326, + "src/main/runtime/rpc/methods/structured-agent-session-turn-completion-stream.test.ts": 3315, + "src/main/runtime/rpc/methods/structured-agent-session-turn-item-capability.test.ts": 3252, + "src/main/runtime/rpc/methods/structured-agent-session-unsubscribe.test.ts": 4980, + "src/main/runtime/rpc/methods/structured-agent-session-visual.test.ts": 6517, + "src/main/runtime/rpc/methods/structured-agent-session.test.ts": 3051, + "src/main/runtime/rpc/methods/structured-chat-tab-table.test.ts": 7243, + "src/main/runtime/rpc/methods/structured-worker-read-cursor.test.ts": 419, + "src/main/runtime/rpc/methods/structured-worker-stop-receipt.test.ts": 5551, + "src/main/runtime/rpc/methods/structured-worker-tab-retirement.test.ts": 4638, + "src/main/runtime/rpc/methods/task-provider-identity.test.ts": 258, + "src/main/runtime/rpc/methods/terminal-create-idempotency.test.ts": 1464, + "src/main/runtime/rpc/methods/terminal-legacy-stream-id-characterization.test.ts": 1602, + "src/main/runtime/rpc/methods/terminal-manifest-characterization.test.ts": 1262, + "src/main/runtime/rpc/methods/terminal-navigation.test.ts": 1520, + "src/main/runtime/rpc/methods/terminal-read-screen-cursor.test.ts": 1340, + "src/main/runtime/rpc/methods/terminal-stream-extraction-characterization.test.ts": 61, + "src/main/runtime/rpc/methods/terminal/terminal-input-sequence-ledger.test.ts": 68, + "src/main/runtime/rpc/methods/terminal/terminal-inspect-process-params.test.ts": 232, + "src/main/runtime/rpc/methods/terminal/terminal-snapshot-json-byte-budget.test.ts": 187, + "src/main/runtime/rpc/methods/terminal/terminal-snapshot-scrollback-rows.test.ts": 47, + "src/main/runtime/rpc/methods/terminal/terminal-subscription-registration.test.ts": 100, + "src/main/runtime/rpc/methods/terminal/terminal-viewer-colors-method.test.ts": 1541, + "src/main/runtime/rpc/methods/updater.test.ts": 241, + "src/main/runtime/rpc/methods/workspace-cleanup-ui-schema.test.ts": 273, + "src/main/runtime/rpc/methods/workspace-layout.test.ts": 3590, + "src/main/runtime/rpc/methods/workspace-ports.test.ts": 535, + "src/main/runtime/rpc/methods/worktree-catalog-snapshot-method.test.ts": 1579, + "src/main/runtime/rpc/methods/worktree-catalog-version-stamp.test.ts": 1698, + "src/main/runtime/rpc/methods/worktree-create-args.test.ts": 310, + "src/main/runtime/rpc/methods/worktree-create-launch-source.test.ts": 1894, + "src/main/runtime/rpc/methods/worktree-create-navigation.test.ts": 1814, + "src/main/runtime/rpc/methods/worktree-create-setup-choice.test.ts": 320, + "src/main/runtime/rpc/methods/worktree-create-startup-agent.test.ts": 1603, + "src/main/runtime/rpc/methods/worktree-github-pr-suppression.test.ts": 1770, + "src/main/runtime/rpc/methods/worktree-missing-terminal-teardown.test.ts": 2004, + "src/main/runtime/rpc/methods/worktree-retired-names.test.ts": 1603, + "src/main/runtime/rpc/methods/worktree-rm-background-reply.test.ts": 1709, + "src/main/runtime/rpc/methods/worktree-rm-host-qualification.test.ts": 1665, + "src/main/runtime/rpc/methods/worktree-rm-pty-waiver.test.ts": 1841, + "src/main/runtime/rpc/methods/worktree-schemas.test.ts": 369, + "src/main/runtime/rpc/methods/worktree.test.ts": 1682, + "src/main/runtime/rpc/mobile-auth-acl-critical-path.test.ts": 499, + "src/main/runtime/rpc/mobile-clipboard-image-provenance.test.ts": 45, + "src/main/runtime/rpc/mobile-e2ee-auth-classification.test.ts": 204, + "src/main/runtime/rpc/mobile-e2ee-outbound-memory-budget.test.ts": 46, + "src/main/runtime/rpc/mobile-e2ee-v2-desktop-session.test.ts": 81, + "src/main/runtime/rpc/mobile-socket-wiring.test.ts": 488, + "src/main/runtime/rpc/orchestration-11745-regression-verification.test.ts": 7446, + "src/main/runtime/rpc/orchestration-caller-show.test.ts": 5080, + "src/main/runtime/rpc/orchestration-check-wait-session.test.ts": 5916, + "src/main/runtime/rpc/orchestration-commit-notify-characterization.test.ts": 5082, + "src/main/runtime/rpc/orchestration-contract-fence.test.ts": 5716, + "src/main/runtime/rpc/orchestration-current-authority-precedence.test.ts": 5533, + "src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher.test.ts": 5911, + "src/main/runtime/rpc/orchestration-legacy-coordinator-race.test.ts": 11342, + "src/main/runtime/rpc/orchestration-legacy-fence-jurisdiction.test.ts": 4933, + "src/main/runtime/rpc/orchestration-legacy-question-takeover.test.ts": 5765, + "src/main/runtime/rpc/orchestration-legacy-run-routing.test.ts": 5826, + "src/main/runtime/rpc/orchestration-legacy-session-address-migration.test.ts": 5520, + "src/main/runtime/rpc/orchestration-legacy-takeover-current-authority.test.ts": 4786, + "src/main/runtime/rpc/orchestration-legacy-takeover-delivery.test.ts": 6856, + "src/main/runtime/rpc/orchestration-legacy-takeover-dispatcher.test.ts": 6087, + "src/main/runtime/rpc/orchestration-mutation-executor.test.ts": 5308, + "src/main/runtime/rpc/orchestration-mutation-ledger.test.ts": 5880, + "src/main/runtime/rpc/orchestration-mutation-request-show-legacy.test.ts": 6000, + "src/main/runtime/rpc/orchestration-mutation-request-show.test.ts": 5473, + "src/main/runtime/rpc/orchestration-party-addressing.test.ts": 6675, + "src/main/runtime/rpc/orchestration-party-agreement.test.ts": 4678, + "src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts": 5101, + "src/main/runtime/rpc/orchestration-session-caller.test.ts": 5492, + "src/main/runtime/rpc/orchestration-session-coordinator.test.ts": 5828, + "src/main/runtime/rpc/orchestration-session-recipient.test.ts": 6145, + "src/main/runtime/rpc/orchestration-session-run-binding.test.ts": 5700, + "src/main/runtime/rpc/orchestration-task-dispatch-invariant.test.ts": 6327, + "src/main/runtime/rpc/relay-transport.test.ts": 196, + "src/main/runtime/rpc/remote-runtime-server-heartbeat-missed-probe-tolerance.test.ts": 55, + "src/main/runtime/rpc/remote-runtime-server-heartbeat.test.ts": 38, + "src/main/runtime/rpc/rpc-caller-scope.test.ts": 4947, + "src/main/runtime/rpc/runtime-client-capabilities.test.ts": 28, + "src/main/runtime/rpc/runtime-close-attribution-topology.test.ts": 1679, + "src/main/runtime/rpc/runtime-close-attribution.test.ts": 1401, + "src/main/runtime/rpc/runtime-source-admission.test.ts": 4516, + "src/main/runtime/rpc/schemas.test.ts": 1798, + "src/main/runtime/rpc/static-web-client-handler-stream.test.ts": 1818, + "src/main/runtime/rpc/streaming.test.ts": 1486, + "src/main/runtime/rpc/terminal-agent-prompt-send.test.ts": 1451, + "src/main/runtime/rpc/terminal-agent-send-guard.test.ts": 1417, + "src/main/runtime/rpc/terminal-geometry-stale-handle.test.ts": 1529, + "src/main/runtime/rpc/terminal-lease-stale-handle-survival.test.ts": 1722, + "src/main/runtime/rpc/terminal-list-host-scope-transport.test.ts": 1584, + "src/main/runtime/rpc/terminal-multiplex-ack-output-budget.test.ts": 1767, + "src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery-history.test.ts": 1667, + "src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery.test.ts": 2140, + "src/main/runtime/rpc/terminal-multiplex-desktop-resize-routing.test.ts": 2187, + "src/main/runtime/rpc/terminal-multiplex-end-verdict.test.ts": 3046, + "src/main/runtime/rpc/terminal-multiplex-escape-tail.test.ts": 1473, + "src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts": 40, + "src/main/runtime/rpc/terminal-multiplex-initial-snapshot-buffering.test.ts": 1757, + "src/main/runtime/rpc/terminal-multiplex-input-sequence-replay.test.ts": 2667, + "src/main/runtime/rpc/terminal-multiplex-input-write-rejection.test.ts": 2375, + "src/main/runtime/rpc/terminal-multiplex-output-pause-and-viewport.test.ts": 2491, + "src/main/runtime/rpc/terminal-multiplex-pty-wait-capacity.test.ts": 1780, + "src/main/runtime/rpc/terminal-multiplex-resync-replay-trim.test.ts": 1478, + "src/main/runtime/rpc/terminal-multiplex-round-robin.test.ts": 27, + "src/main/runtime/rpc/terminal-multiplex-snapshot-serialization.test.ts": 2130, + "src/main/runtime/rpc/terminal-multiplex-source-range-request.test.ts": 1729, + "src/main/runtime/rpc/terminal-multiplex-subscribe-slot-recovery.test.ts": 1673, + "src/main/runtime/rpc/terminal-opencode-send-guard.integration.test.ts": 4427, + "src/main/runtime/rpc/terminal-output-batching.test.ts": 1826, + "src/main/runtime/rpc/terminal-output-frame-chunks.test.ts": 61, + "src/main/runtime/rpc/terminal-prompt-delivery-receipt.test.ts": 6691, + "src/main/runtime/rpc/terminal-provider-snapshot-sequence.test.ts": 1648, + "src/main/runtime/rpc/terminal-requested-snapshot-unavailability.test.ts": 2084, + "src/main/runtime/rpc/terminal-send-launch-draft-resolution.test.ts": 1532, + "src/main/runtime/rpc/terminal-send.test.ts": 1621, + "src/main/runtime/rpc/terminal-stream-byte-length.test.ts": 509, + "src/main/runtime/rpc/terminal-subscribe-admission.test.ts": 5440, + "src/main/runtime/rpc/terminal-subscribe-blank-mount.test.ts": 1511, + "src/main/runtime/rpc/terminal-subscribe-buffer.test.ts": 1899, + "src/main/runtime/rpc/terminal-subscribe-lease-only.test.ts": 1700, + "src/main/runtime/rpc/terminal-subscribe-mount-replay.test.ts": 1691, + "src/main/runtime/rpc/terminal-subscribe-mounted-pane.test.ts": 1478, + "src/main/runtime/rpc/terminal-subscribe-ownership.test.ts": 1775, + "src/main/runtime/rpc/terminal-subscribe-reconnect-rebind.test.ts": 1448, + "src/main/runtime/rpc/terminal-subscribe-relay-drop-lease-survival.test.ts": 5109, + "src/main/runtime/rpc/terminal-subscribe-renderer-recovery-output.test.ts": 1550, + "src/main/runtime/rpc/unix-socket-transport.test.ts": 66, + "src/main/runtime/rpc/unpaired-device-auth-throttle.test.ts": 32, + "src/main/runtime/rpc/web-session-pairing.test.ts": 511, + "src/main/runtime/rpc/worktree-catalog-snapshot.test.ts": 45, + "src/main/runtime/rpc/worktree-removal-marker-projection.test.ts": 502, + "src/main/runtime/rpc/ws-empty-payload-frame-write.test.ts": 121, + "src/main/runtime/rpc/ws-fallback-port-store.test.ts": 40, + "src/main/runtime/rpc/ws-transport-accept-order.test.ts": 62, + "src/main/runtime/rpc/ws-transport-static-web.test.ts": 104, + "src/main/runtime/rpc/ws-transport-transient-packet-loss.test.ts": 655, + "src/main/runtime/rpc/ws-transport.test.ts": 999, + "src/main/runtime/run-terminal-shell-alone.node-pty.test.ts": 1526, + "src/main/runtime/run-terminal-shell-alone.test.ts": 116, + "src/main/runtime/runtime-agent-row-main-agent-projection.test.ts": 70, + "src/main/runtime/runtime-agent-terminal-startup.test.ts": 396, + "src/main/runtime/runtime-binary-message-router.test.ts": 33, + "src/main/runtime/runtime-browser-client-automation.test.ts": 286, + "src/main/runtime/runtime-browser-client-page-adoption.test.ts": 310, + "src/main/runtime/runtime-browser-client-page-creation.test.ts": 268, + "src/main/runtime/runtime-browser-client-page-recovery.test.ts": 269, + "src/main/runtime/runtime-browser-client-page-restored-recovery.test.ts": 195, + "src/main/runtime/runtime-browser-network-execution-host.test.ts": 44, + "src/main/runtime/runtime-browser-page-registry.test.ts": 53, + "src/main/runtime/runtime-client-agent-launch-settings-durability.test.ts": 1693, + "src/main/runtime/runtime-client-agent-launch-settings.test.ts": 423, + "src/main/runtime/runtime-client-settings-linear-team-projection.test.ts": 651, + "src/main/runtime/runtime-client-settings-minimax-projection.test.ts": 693, + "src/main/runtime/runtime-client-settings-source-control-projection.test.ts": 660, + "src/main/runtime/runtime-client-settings-terminal-copy-projection.test.ts": 708, + "src/main/runtime/runtime-created-worktree-terminal-target.test.ts": 5015, + "src/main/runtime/runtime-extraction-regressions.test.ts": 3812, + "src/main/runtime/runtime-failed-local-worktree-removal.test.ts": 975, + "src/main/runtime/runtime-file-listing-producer-budgets.test.ts": 756, + "src/main/runtime/runtime-file-target-execution-host.test.ts": 5186, + "src/main/runtime/runtime-file-upload-decode.test.ts": 419, + "src/main/runtime/runtime-folder-workspace.test.ts": 52, + "src/main/runtime/runtime-folder-worktree-create.test.ts": 111, + "src/main/runtime/runtime-git-api-contract.test.ts": 1210, + "src/main/runtime/runtime-git-branch-compare-admission.test.ts": 507, + "src/main/runtime/runtime-git-command-target.test.ts": 615, + "src/main/runtime/runtime-git-conflict-operation-routing.test.ts": 639, + "src/main/runtime/runtime-git-execution-host-ownership.test.ts": 1020, + "src/main/runtime/runtime-git-generation-admission.test.ts": 534, + "src/main/runtime/runtime-git-stage-worktree-scope.test.ts": 895, + "src/main/runtime/runtime-git-status-admission.test.ts": 1084, + "src/main/runtime/runtime-git-sync-commands.test.ts": 887, + "src/main/runtime/runtime-git-target-execution-host.test.ts": 4325, + "src/main/runtime/runtime-graph-mirror-capture.test.ts": 4691, + "src/main/runtime/runtime-graph-reload-lifecycle.test.ts": 36, + "src/main/runtime/runtime-hook-agent-row-selection.test.ts": 75, + "src/main/runtime/runtime-interrupted-local-worktree-removal.test.ts": 1818, + "src/main/runtime/runtime-leaf-record-projection.test.ts": 91, + "src/main/runtime/runtime-legacy-worker-missing-workspace-recovery.test.ts": 75, + "src/main/runtime/runtime-legacy-worker-recovery-lifetime.test.ts": 168, + "src/main/runtime/runtime-legacy-worker-terminal-recovery-persistence.test.ts": 838, + "src/main/runtime/runtime-legacy-worker-terminal-recovery-retry.test.ts": 99, + "src/main/runtime/runtime-linear-read-commands.test.ts": 471, + "src/main/runtime/runtime-local-create-rearm-ordering.test.ts": 4288, + "src/main/runtime/runtime-local-worktree-create.test.ts": 707, + "src/main/runtime/runtime-local-worktree-materialization.test.ts": 116, + "src/main/runtime/runtime-local-worktree-setup.test.ts": 364, + "src/main/runtime/runtime-local-worktree-terminal-startup.test.ts": 54, + "src/main/runtime/runtime-machine-name.test.ts": 60, + "src/main/runtime/runtime-managed-worktree-metadata-sweep.test.ts": 1835, + "src/main/runtime/runtime-managed-worktree-metadata-wsl-move.test.ts": 1423, + "src/main/runtime/runtime-managed-worktree-metadata.test.ts": 360, + "src/main/runtime/runtime-managed-worktree-queries.test.ts": 859, + "src/main/runtime/runtime-metadata-ownership-watch.test.ts": 122, + "src/main/runtime/runtime-metadata.test.ts": 428, + "src/main/runtime/runtime-migration-catalog-capabilities.test.ts": 5201, + "src/main/runtime/runtime-mobile-agent-status-builder.test.ts": 72, + "src/main/runtime/runtime-mobile-session-incarnation-projection.test.ts": 95, + "src/main/runtime/runtime-orchestration-sender-names.test.ts": 879, + "src/main/runtime/runtime-owned-terminal-publication-lineage.test.ts": 4992, + "src/main/runtime/runtime-pairing-desktop-control-grant.test.ts": 5698, + "src/main/runtime/runtime-project-group-controller-folder-create.test.ts": 450, + "src/main/runtime/runtime-project-group-controller-folder-delete.test.ts": 389, + "src/main/runtime/runtime-project-host-setup-controller.test.ts": 688, + "src/main/runtime/runtime-project-host-setup-publication.test.ts": 719, + "src/main/runtime/runtime-provisioned-root-adoption.test.ts": 520, + "src/main/runtime/runtime-reference-agents.test.ts": 279, "src/main/runtime/runtime-reference-catalog.test.ts": 97, - "src/main/runtime/runtime-reference-find.test.ts": 264, - "src/main/runtime/runtime-reference-integration.test.ts": 4869, - "src/main/runtime/runtime-remote-fetch-ref-maintenance.test.ts": 506, - "src/main/runtime/runtime-remove-project-host-scope.test.ts": 5466, - "src/main/runtime/runtime-repository-issue-command.test.ts": 769, - "src/main/runtime/runtime-repository-ref-queries.test.ts": 196, - "src/main/runtime/runtime-resolved-worktree-cache.test.ts": 53, - "src/main/runtime/runtime-rpc-browser-host-admission.test.ts": 5730, - "src/main/runtime/runtime-rpc-client-activity.test.ts": 5083, - "src/main/runtime/runtime-rpc-device-revocation.test.ts": 5464, - "src/main/runtime/runtime-rpc-dispatch-inject-long-poll.test.ts": 5309, - "src/main/runtime/runtime-rpc-long-poll-transport.test.ts": 10349, - "src/main/runtime/runtime-rpc-metadata-lifecycle.test.ts": 5255, - "src/main/runtime/runtime-rpc-mobile-method-allowlist.test.ts": 3159, - "src/main/runtime/runtime-rpc-mobile-terminal-streaming.test.ts": 5792, - "src/main/runtime/runtime-rpc-mobile-unknown-method-scope-refusal.test.ts": 3140, - "src/main/runtime/runtime-rpc-orchestration-db-migration.test.ts": 625, - "src/main/runtime/runtime-rpc-pairing-mode-persistence.test.ts": 5329, - "src/main/runtime/runtime-rpc-pairing-offer.test.ts": 4839, - "src/main/runtime/runtime-rpc-relay-pairing.test.ts": 6365, - "src/main/runtime/runtime-rpc-relay-provider-install.test.ts": 5568, - "src/main/runtime/runtime-rpc-request-authorization.test.ts": 6177, - "src/main/runtime/runtime-rpc-startup-failure.test.ts": 93, - "src/main/runtime/runtime-rpc-terminal-list.test.ts": 5576, - "src/main/runtime/runtime-rpc-websocket-bind-host.test.ts": 5192, - "src/main/runtime/runtime-rpc-websocket-fallback-port.test.ts": 5232, - "src/main/runtime/runtime-rpc-websocket-long-poll-caps.test.ts": 7485, - "src/main/runtime/runtime-rpc-websocket-reply-size-limit.test.ts": 5190, - "src/main/runtime/runtime-rpc-worktree-queries.test.ts": 5245, - "src/main/runtime/runtime-search-line-fragments.test.ts": 572, - "src/main/runtime/runtime-server-git-availability.test.ts": 44, - "src/main/runtime/runtime-session-search-settings.test.ts": 236, - "src/main/runtime/runtime-skill-install-authority.test.ts": 102, - "src/main/runtime/runtime-skill-install-commands.test.ts": 673, - "src/main/runtime/runtime-skill-install-queries.test.ts": 752, - "src/main/runtime/runtime-socket-sweep.test.ts": 50, - "src/main/runtime/runtime-subscription-registry.test.ts": 138, - "src/main/runtime/runtime-terminal-client-use.test.ts": 4273, - "src/main/runtime/runtime-terminal-idle-polls.test.ts": 404, - "src/main/runtime/runtime-terminal-orphan-topology-validation.test.ts": 58, - "src/main/runtime/runtime-terminal-spawn-push-target-materialization.test.ts": 77, - "src/main/runtime/runtime-terminal-writer.test.ts": 85, - "src/main/runtime/runtime-worktree-agent-rows-structured.test.ts": 650, - "src/main/runtime/runtime-worktree-agent-rows-verdict.test.ts": 1049, - "src/main/runtime/runtime-worktree-agent-sources.test.ts": 62, - "src/main/runtime/runtime-worktree-agent-startup.test.ts": 451, - "src/main/runtime/runtime-worktree-ps-summaries.test.ts": 93, - "src/main/runtime/runtime-worktree-ps-unverifiable-terminals.test.ts": 45, - "src/main/runtime/runtime-worktree-selection.test.ts": 62, - "src/main/runtime/runtime-worktree-startup-readiness.test.ts": 140, - "src/main/runtime/runtime-worktree-structured-agent-rows-liveness.test.ts": 1126, - "src/main/runtime/runtime-worktree-terminal-provisioning.test.ts": 67, - "src/main/runtime/saved-structured-agent-session-restoration.test.ts": 40, - "src/main/runtime/selected-review-branch.test.ts": 61, - "src/main/runtime/session-tabs-empty-worktree-publication.test.ts": 4763, - "src/main/runtime/session-tabs-inventory-publication.test.ts": 4384, - "src/main/runtime/settled-worker-process-replacement.test.ts": 5611, - "src/main/runtime/stale-title-completion-recovery.test.ts": 5144, - "src/main/runtime/stale-title-display-surfaces.test.ts": 5236, - "src/main/runtime/structured-agent-account-kind.test.ts": 58, - "src/main/runtime/structured-agent-create-support-opencode-rig.test.ts": 981, - "src/main/runtime/structured-agent-launch-args-wiring.test.ts": 1637, - "src/main/runtime/structured-agent-launch-support-probe-omp.test.ts": 978, - "src/main/runtime/structured-agent-launch-support-probe.test.ts": 1015, - "src/main/runtime/structured-agent-launch-support.test.ts": 958, - "src/main/runtime/structured-agent-model-catalog-discovery.test.ts": 987, - "src/main/runtime/structured-agent-model-catalog-first-frame.test.ts": 1196, - "src/main/runtime/structured-agent-runtime-registrations-acp.test.ts": 996, - "src/main/runtime/structured-agent-session-attachment-wiring.test.ts": 257, - "src/main/runtime/structured-agent-session-close.test.ts": 357, - "src/main/runtime/structured-agent-session-codex-compact-exit.test.ts": 3778, - "src/main/runtime/structured-agent-session-codex-interrupt-order.test.ts": 2081, - "src/main/runtime/structured-agent-session-codex-opening-send-hold.test.ts": 9062, - "src/main/runtime/structured-agent-session-codex-settlement-order.test.ts": 1766, - "src/main/runtime/structured-agent-session-codex-stopped-send-order.test.ts": 12290, - "src/main/runtime/structured-agent-session-codex-turn-end-settlement.test.ts": 5239, - "src/main/runtime/structured-agent-session-failure-logging.test.ts": 1791, - "src/main/runtime/structured-agent-session-held-signal.test.ts": 1930, - "src/main/runtime/structured-agent-session-host-refusal.test.ts": 5070, - "src/main/runtime/structured-agent-session-install-failure.test.ts": 1569, - "src/main/runtime/structured-agent-session-integration-replay.test.ts": 4482, - "src/main/runtime/structured-agent-session-integration.test.ts": 6902, - "src/main/runtime/structured-agent-session-legacy-send-behind-command.test.ts": 3540, - "src/main/runtime/structured-agent-session-lifecycle-delivery.test.ts": 205, - "src/main/runtime/structured-agent-session-mobile-attention.test.ts": 212, - "src/main/runtime/structured-agent-session-mobile-restore.test.ts": 1047, - "src/main/runtime/structured-agent-session-mobile-retirement.test.ts": 315, - "src/main/runtime/structured-agent-session-paired-caller.test.ts": 3386, - "src/main/runtime/structured-agent-session-rollback-compatibility.test.ts": 740, - "src/main/runtime/structured-agent-session-runtime-exit.test.ts": 1929, - "src/main/runtime/structured-agent-session-runtime-model-catalog-lifetime.test.ts": 1605, - "src/main/runtime/structured-agent-session-runtime-provider-started.test.ts": 2396, - "src/main/runtime/structured-agent-session-runtime-teardown.test.ts": 253, - "src/main/runtime/structured-agent-session-runtime.test.ts": 2060, - "src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts": 1655, - "src/main/runtime/structured-agent-session-startup-reconcile.test.ts": 4757, - "src/main/runtime/structured-agent-session-startup-tab-restore.test.ts": 5325, - "src/main/runtime/structured-agent-session-support-probe.test.ts": 4686, - "src/main/runtime/structured-agent-session-tab-close-cause.test.ts": 51, - "src/main/runtime/structured-agent-shell-environment.test.ts": 91, - "src/main/runtime/structured-agent-stored-definitions.test.ts": 334, - "src/main/runtime/structured-chat-coordinator-mail-queue.test.ts": 7105, - "src/main/runtime/structured-chat-coordinator-mail.test.ts": 13367, - "src/main/runtime/structured-chat-dispatch-worker-lifecycle.test.ts": 5228, - "src/main/runtime/structured-chat-dispatch-worker-mail.test.ts": 6477, - "src/main/runtime/structured-chat-dispatch-worker.test.ts": 5004, - "src/main/runtime/structured-chat-naming-runtime.test.ts": 440, - "src/main/runtime/structured-chat-worker-start.test.ts": 4438, - "src/main/runtime/structured-child-records-switch.test.ts": 2097, - "src/main/runtime/structured-claude-attachment-grant.test.ts": 1678, - "src/main/runtime/structured-claude-auth-policy-wiring.test.ts": 1703, - "src/main/runtime/structured-claude-pending-rewind.test.ts": 1486, - "src/main/runtime/structured-claude-thinking-display-refusal.test.ts": 865, - "src/main/runtime/structured-codex-child-work-runtime.test.ts": 1600, - "src/main/runtime/structured-conversation-tab-title.test.ts": 38, - "src/main/runtime/structured-foreground-command-strip.test.ts": 2430, - "src/main/runtime/structured-session-child-identity-env.test.ts": 312, - "src/main/runtime/structured-session-cli-login-shell.test.ts": 334, - "src/main/runtime/structured-session-mail-redrive-wiring.test.ts": 4818, - "src/main/runtime/structured-session-worktree-teardown.test.ts": 452, - "src/main/runtime/structured-worker-agent-presence.test.ts": 345, - "src/main/runtime/structured-worker-at-rest.test.ts": 1193, - "src/main/runtime/structured-worker-authority.test.ts": 332, - "src/main/runtime/structured-worker-cleared-session.test.ts": 5252, - "src/main/runtime/structured-worker-hook-attestation.test.ts": 3190, - "src/main/runtime/structured-worker-identity.test.ts": 433, - "src/main/runtime/structured-worker-mail-routing.test.ts": 3023, - "src/main/runtime/structured-worker-takeover-pane-key.test.ts": 4488, - "src/main/runtime/structured-worker-terminal-read.test.ts": 2993, - "src/main/runtime/structured-worker-terminal-refusal.test.ts": 357, - "src/main/runtime/terminal-ansi-pending-retention.test.ts": 405, - "src/main/runtime/terminal-chat-owner-persistence.test.ts": 6950, - "src/main/runtime/terminal-close-observed-exit.test.ts": 4756, - "src/main/runtime/terminal-close-slow-agent-exit.test.ts": 6616, - "src/main/runtime/terminal-command-paint.test.ts": 56, - "src/main/runtime/terminal-focus-navigation-coalescer.test.ts": 66, - "src/main/runtime/terminal-foreground-group.test.ts": 78, - "src/main/runtime/terminal-identity-probe.test.ts": 50, - "src/main/runtime/terminal-intentional-stop-exit.test.ts": 4300, - "src/main/runtime/terminal-intentional-stops.test.ts": 42, - "src/main/runtime/terminal-interactive-wait-visibility.test.ts": 5553, - "src/main/runtime/terminal-leaf-tab-resolution.test.ts": 64, - "src/main/runtime/terminal-list-execution-host-scope.test.ts": 6664, - "src/main/runtime/terminal-list-payload-size.test.ts": 4739, - "src/main/runtime/terminal-list-pending-pty-registration.test.ts": 4304, - "src/main/runtime/terminal-list-stale-leaf-liveness.test.ts": 4787, - "src/main/runtime/terminal-list-surface-lost-orphan.test.ts": 4463, - "src/main/runtime/terminal-mobile-subscribe-tab-mount.test.ts": 3776, - "src/main/runtime/terminal-model-query-authority.test.ts": 60, - "src/main/runtime/terminal-orphan-owner.test.ts": 41, - "src/main/runtime/terminal-orphan-topology.test.ts": 52, - "src/main/runtime/terminal-pane-recovery-liveness-gate.test.ts": 5332, - "src/main/runtime/terminal-projection.test.ts": 54, - "src/main/runtime/terminal-pty-exit-waiter.test.ts": 4535, - "src/main/runtime/terminal-query-responder.test.ts": 15573, - "src/main/runtime/terminal-restore-record-seed.test.ts": 4386, - "src/main/runtime/terminal-retirement-async-durability.test.ts": 5424, - "src/main/runtime/terminal-retirement-proof-emitted-frame.test.ts": 5172, - "src/main/runtime/terminal-retirement-proof-publication.test.ts": 4203, - "src/main/runtime/terminal-run-facts-input.test.ts": 4384, - "src/main/runtime/terminal-run-facts.test.ts": 55, - "src/main/runtime/terminal-send-stale-leaf-liveness.test.ts": 4372, - "src/main/runtime/terminal-shell-override-host-support.test.ts": 55, - "src/main/runtime/terminal-subscribe-exit-waiter-leak.test.ts": 4589, - "src/main/runtime/terminal-subscriber-driven-daemon-attach.test.ts": 4236, - "src/main/runtime/terminal-surface-close.test.ts": 203, - "src/main/runtime/terminal-tail-buffer.test.ts": 6881, - "src/main/runtime/terminal-tail-redraw-row.equivalence.test.ts": 1562, - "src/main/runtime/terminal-tail-redraw-scaling.test.ts": 344, - "src/main/runtime/terminal-tail-redraw-spans.test.ts": 251, - "src/main/runtime/terminal-tail-redraw-wall-time.test.ts": 21650, - "src/main/runtime/terminal-tail-row-retention.test.ts": 1528, - "src/main/runtime/terminal-tail-sentinel-index.test.ts": 3542, - "src/main/runtime/terminal-tail-whitespace.test.ts": 276, - "src/main/runtime/terminal-vertical-control-scan.test.ts": 307, - "src/main/runtime/terminal-wait-detection.test.ts": 350, - "src/main/runtime/terminal-wait-name-only-idle.test.ts": 4243, - "src/main/runtime/terminal-wait-results.test.ts": 38, - "src/main/runtime/terminal-wait-tail-window.test.ts": 263, - "src/main/runtime/tui-idle-claude-task-wakeup.test.ts": 282, - "src/main/runtime/tui-idle-command-paint.test.ts": 4068, - "src/main/runtime/tui-idle-delivery-and-quiescence.test.ts": 5085, - "src/main/runtime/tui-idle-evidence-source.test.ts": 3277, - "src/main/runtime/tui-idle-evidence.test.ts": 374, - "src/main/runtime/tui-idle-hook-lane-runtime.test.ts": 7403, - "src/main/runtime/tui-idle-hook-lane.test.ts": 301, - "src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts": 35459, - "src/main/runtime/tui-idle-weak-evidence-seam.test.ts": 3901, - "src/main/runtime/unreadable-secret-store-preservation.win32.test.ts": 54, - "src/main/runtime/wait-blocked-check-state.test.ts": 90, - "src/main/runtime/wait-blocked-keyword-carry-retention.test.ts": 3758, - "src/main/runtime/windows-default-route-interfaces.test.ts": 62, - "src/main/runtime/windows-drive-listing.test.ts": 54, - "src/main/runtime/windows-firewall-remote-scope.test.ts": 81, - "src/main/runtime/windows-mobile-firewall.test.ts": 99, - "src/main/runtime/workspace-layout-stream.test.ts": 257, - "src/main/runtime/workspace-session-membership-scaling.test.ts": 99, - "src/main/runtime/workspace-session-worktree-id.test.ts": 254, - "src/main/runtime/worktree-launch-host-repo.test.ts": 52, - "src/main/runtime/worktree-list-host-scope.test.ts": 1039, - "src/main/runtime/worktree-path-selector-wsl-posix.test.ts": 4715, - "src/main/runtime/worktree-ps-degraded-repo-scan.test.ts": 5316, - "src/main/runtime/worktree-ps-host-scope.test.ts": 4255, - "src/main/runtime/worktree-ps-unverifiable-terminals.test.ts": 4242, - "src/main/runtime/worktree-rm-id-selector-path-spelling.test.ts": 4532, - "src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts": 4765, - "src/main/runtime/worktree-scan-execution-host-routing.test.ts": 4500, - "src/main/runtime/worktree-scan-repeated-path.test.ts": 3815, - "src/main/runtime/worktree-teardown-unstopped-pty.test.ts": 554, - "src/main/runtime/worktree-teardown.test.ts": 863, - "src/main/runtime/worktree-terminal-mutation-lock.test.ts": 57, - "src/main/serve-update-handoff.app-environment.test.ts": 1475, - "src/main/serve-update-handoff.test.ts": 58, - "src/main/server/serve-readiness.test.ts": 69, - "src/main/server/serve-stdout-boundary.test.ts": 47, - "src/main/shell-prompt-readiness-probe.test.ts": 94, - "src/main/shell-startup-identity-scanner.test.ts": 43, - "src/main/shell-startup-output-scanner.test.ts": 84, - "src/main/shell-wrapper-content-address.test.ts": 56, - "src/main/shell-wrapper-generated-file-snapshot.test.ts": 118, - "src/main/sidecar-snapshot-file.test.ts": 59, - "src/main/skills/agent-skill-selection.test.ts": 238, - "src/main/skills/antigravity-skill-discovery.test.ts": 95, - "src/main/skills/claude-plugin-skill-sources-wsl.test.ts": 106, - "src/main/skills/claude-plugin-skill-sources.test.ts": 95, - "src/main/skills/discovery-filter-sharing.test.ts": 247, - "src/main/skills/discovery-source-filter-order.test.ts": 103, - "src/main/skills/discovery.test.ts": 187, + "src/main/runtime/runtime-reference-find.test.ts": 215, + "src/main/runtime/runtime-reference-integration.test.ts": 4784, + "src/main/runtime/runtime-remote-fetch-ref-maintenance.test.ts": 528, + "src/main/runtime/runtime-remove-project-host-scope.test.ts": 4900, + "src/main/runtime/runtime-repository-issue-command.test.ts": 489, + "src/main/runtime/runtime-repository-ref-queries.test.ts": 176, + "src/main/runtime/runtime-resolved-worktree-cache.test.ts": 40, + "src/main/runtime/runtime-rpc-browser-host-admission.test.ts": 4866, + "src/main/runtime/runtime-rpc-client-activity.test.ts": 5291, + "src/main/runtime/runtime-rpc-device-key-dispatch.test.ts": 3122, + "src/main/runtime/runtime-rpc-device-revocation.test.ts": 6347, + "src/main/runtime/runtime-rpc-dispatch-inject-long-poll.test.ts": 5541, + "src/main/runtime/runtime-rpc-long-poll-transport.test.ts": 9425, + "src/main/runtime/runtime-rpc-metadata-lifecycle.test.ts": 5413, + "src/main/runtime/runtime-rpc-mobile-legacy-socket-relay-refusal.test.ts": 2909, + "src/main/runtime/runtime-rpc-mobile-method-allowlist.test.ts": 2953, + "src/main/runtime/runtime-rpc-mobile-terminal-streaming.test.ts": 6412, + "src/main/runtime/runtime-rpc-mobile-unknown-method-scope-refusal.test.ts": 2997, + "src/main/runtime/runtime-rpc-orchestration-db-migration.test.ts": 536, + "src/main/runtime/runtime-rpc-pairing-mode-persistence.test.ts": 6252, + "src/main/runtime/runtime-rpc-pairing-offer.test.ts": 5913, + "src/main/runtime/runtime-rpc-relay-pairing.test.ts": 6508, + "src/main/runtime/runtime-rpc-relay-provider-install.test.ts": 6621, + "src/main/runtime/runtime-rpc-request-authorization.test.ts": 6790, + "src/main/runtime/runtime-rpc-startup-failure.test.ts": 72, + "src/main/runtime/runtime-rpc-terminal-list.test.ts": 5719, + "src/main/runtime/runtime-rpc-websocket-bind-host.test.ts": 5661, + "src/main/runtime/runtime-rpc-websocket-fallback-port.test.ts": 5395, + "src/main/runtime/runtime-rpc-websocket-long-poll-caps.test.ts": 9511, + "src/main/runtime/runtime-rpc-websocket-reply-size-limit.test.ts": 4833, + "src/main/runtime/runtime-rpc-worktree-queries.test.ts": 5679, + "src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-routing.test.ts": 42, + "src/main/runtime/runtime-search-line-fragments.test.ts": 547, + "src/main/runtime/runtime-server-git-availability.test.ts": 33, + "src/main/runtime/runtime-session-search-settings.test.ts": 213, + "src/main/runtime/runtime-skill-install-authority.test.ts": 87, + "src/main/runtime/runtime-skill-install-commands.test.ts": 617, + "src/main/runtime/runtime-skill-install-queries.test.ts": 790, + "src/main/runtime/runtime-socket-sweep.test.ts": 34, + "src/main/runtime/runtime-sort-order-scan-cache.test.ts": 5489, + "src/main/runtime/runtime-subscription-registry.test.ts": 127, + "src/main/runtime/runtime-terminal-client-use.test.ts": 4251, + "src/main/runtime/runtime-terminal-idle-polls.test.ts": 361, + "src/main/runtime/runtime-terminal-orphan-topology-validation.test.ts": 62, + "src/main/runtime/runtime-terminal-spawn-push-target-materialization.test.ts": 31, + "src/main/runtime/runtime-terminal-writer.test.ts": 77, + "src/main/runtime/runtime-visible-catalog.test.ts": 45, + "src/main/runtime/runtime-worktree-agent-rows-structured.test.ts": 685, + "src/main/runtime/runtime-worktree-agent-rows-verdict.test.ts": 1068, + "src/main/runtime/runtime-worktree-agent-sources.test.ts": 52, + "src/main/runtime/runtime-worktree-agent-startup.test.ts": 454, + "src/main/runtime/runtime-worktree-create-git.test.ts": 633, + "src/main/runtime/runtime-worktree-ps-summaries.test.ts": 59, + "src/main/runtime/runtime-worktree-ps-unverifiable-terminals.test.ts": 33, + "src/main/runtime/runtime-worktree-selection.test.ts": 42, + "src/main/runtime/runtime-worktree-startup-readiness.test.ts": 161, + "src/main/runtime/runtime-worktree-structured-agent-rows-liveness.test.ts": 1189, + "src/main/runtime/runtime-worktree-terminal-provisioning.test.ts": 62, + "src/main/runtime/saved-structured-agent-session-restoration.test.ts": 39, + "src/main/runtime/selected-review-branch.test.ts": 40, + "src/main/runtime/serve-browser-tab-layout.test.ts": 348, + "src/main/runtime/session-tabs-empty-worktree-publication.test.ts": 4143, + "src/main/runtime/session-tabs-inventory-publication.test.ts": 4481, + "src/main/runtime/session-tabs/headless-browser-group-persistence.test.ts": 4669, + "src/main/runtime/session-tabs/session-tab-prototype-dispatch.test.ts": 4643, + "src/main/runtime/session-tabs/session-tabs-inventory-publication.test.ts": 4662, + "src/main/runtime/settled-worker-process-replacement.test.ts": 4613, + "src/main/runtime/stale-title-completion-recovery.test.ts": 5362, + "src/main/runtime/stale-title-display-surfaces.test.ts": 5546, + "src/main/runtime/structured-agent-account-kind.test.ts": 48, + "src/main/runtime/structured-agent-create-support-opencode-rig.test.ts": 1062, + "src/main/runtime/structured-agent-install-refresh.test.ts": 643, + "src/main/runtime/structured-agent-launch-args-wiring.test.ts": 1870, + "src/main/runtime/structured-agent-launch-parity-coverage.test.ts": 1087, + "src/main/runtime/structured-agent-launch-parity.test.ts": 4863, + "src/main/runtime/structured-agent-launch-support-probe-omp.test.ts": 1480, + "src/main/runtime/structured-agent-launch-support-probe.test.ts": 1239, + "src/main/runtime/structured-agent-launch-support.test.ts": 1440, + "src/main/runtime/structured-agent-model-catalog-discovery.test.ts": 1265, + "src/main/runtime/structured-agent-model-catalog-first-frame.test.ts": 1155, + "src/main/runtime/structured-agent-new-chat-effort-at-rest.test.ts": 732, + "src/main/runtime/structured-agent-runtime-registrations-acp.test.ts": 1166, + "src/main/runtime/structured-agent-session-attachment-wiring.test.ts": 273, + "src/main/runtime/structured-agent-session-close.test.ts": 306, + "src/main/runtime/structured-agent-session-codex-compact-exit.test.ts": 4454, + "src/main/runtime/structured-agent-session-codex-interrupt-order.test.ts": 2483, + "src/main/runtime/structured-agent-session-codex-opening-send-hold.test.ts": 12784, + "src/main/runtime/structured-agent-session-codex-settlement-order.test.ts": 2190, + "src/main/runtime/structured-agent-session-codex-stopped-send-order.test.ts": 13028, + "src/main/runtime/structured-agent-session-codex-turn-end-settlement.test.ts": 5209, + "src/main/runtime/structured-agent-session-failure-logging.test.ts": 2288, + "src/main/runtime/structured-agent-session-held-signal.test.ts": 2279, + "src/main/runtime/structured-agent-session-host-refusal.test.ts": 5023, + "src/main/runtime/structured-agent-session-install-failure.test.ts": 2058, + "src/main/runtime/structured-agent-session-integration-replay.test.ts": 3906, + "src/main/runtime/structured-agent-session-integration.test.ts": 2913, + "src/main/runtime/structured-agent-session-legacy-send-behind-command.test.ts": 3938, + "src/main/runtime/structured-agent-session-lifecycle-delivery.test.ts": 258, + "src/main/runtime/structured-agent-session-mobile-attention.test.ts": 260, + "src/main/runtime/structured-agent-session-mobile-restore.test.ts": 1554, + "src/main/runtime/structured-agent-session-mobile-retirement.test.ts": 330, + "src/main/runtime/structured-agent-session-paired-caller.test.ts": 3844, + "src/main/runtime/structured-agent-session-rollback-compatibility.test.ts": 674, + "src/main/runtime/structured-agent-session-runtime-exit.test.ts": 2410, + "src/main/runtime/structured-agent-session-runtime-model-catalog-lifetime.test.ts": 1893, + "src/main/runtime/structured-agent-session-runtime-provider-started.test.ts": 2429, + "src/main/runtime/structured-agent-session-runtime-teardown.test.ts": 285, + "src/main/runtime/structured-agent-session-runtime.test.ts": 2056, + "src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts": 2281, + "src/main/runtime/structured-agent-session-startup-reconcile.test.ts": 5466, + "src/main/runtime/structured-agent-session-startup-tab-restore.test.ts": 4920, + "src/main/runtime/structured-agent-session-support-probe.test.ts": 4474, + "src/main/runtime/structured-agent-session-tab-close-cause.test.ts": 29, + "src/main/runtime/structured-agent-session-windowless-layout-host.test.ts": 5678, + "src/main/runtime/structured-agent-session-windowless-layout.test.ts": 4850, + "src/main/runtime/structured-agent-shell-environment.test.ts": 646, + "src/main/runtime/structured-agent-stored-definitions.test.ts": 388, + "src/main/runtime/structured-chat-coordinator-mail-queue.test.ts": 7480, + "src/main/runtime/structured-chat-coordinator-mail.test.ts": 30995, + "src/main/runtime/structured-chat-dispatch-worker-lifecycle.test.ts": 5718, + "src/main/runtime/structured-chat-dispatch-worker-mail.test.ts": 5975, + "src/main/runtime/structured-chat-dispatch-worker.test.ts": 5546, + "src/main/runtime/structured-chat-naming-runtime.test.ts": 486, + "src/main/runtime/structured-chat-worker-start.test.ts": 5608, + "src/main/runtime/structured-child-records-switch.test.ts": 3148, + "src/main/runtime/structured-claude-attachment-grant.test.ts": 2010, + "src/main/runtime/structured-claude-auth-policy-wiring.test.ts": 1794, + "src/main/runtime/structured-claude-pending-rewind.test.ts": 1750, + "src/main/runtime/structured-claude-thinking-display-refusal.test.ts": 806, + "src/main/runtime/structured-codex-child-work-runtime.test.ts": 4653, + "src/main/runtime/structured-conversation-tab-title.test.ts": 36, + "src/main/runtime/structured-foreground-command-strip.test.ts": 3120, + "src/main/runtime/structured-session-child-env.test.ts": 310, + "src/main/runtime/structured-session-cli-login-shell.test.ts": 433, + "src/main/runtime/structured-session-mail-redrive-wiring.test.ts": 5320, + "src/main/runtime/structured-session-worktree-teardown.test.ts": 424, + "src/main/runtime/structured-worker-agent-presence.test.ts": 278, + "src/main/runtime/structured-worker-at-rest.test.ts": 1745, + "src/main/runtime/structured-worker-authority.test.ts": 346, + "src/main/runtime/structured-worker-cleared-session.test.ts": 4498, + "src/main/runtime/structured-worker-hook-attestation.test.ts": 2655, + "src/main/runtime/structured-worker-identity.test.ts": 411, + "src/main/runtime/structured-worker-mail-routing.test.ts": 2912, + "src/main/runtime/structured-worker-takeover-pane-key.test.ts": 4447, + "src/main/runtime/structured-worker-terminal-read.test.ts": 3267, + "src/main/runtime/structured-worker-terminal-refusal.test.ts": 321, + "src/main/runtime/terminal-ansi-pending-retention.test.ts": 316, + "src/main/runtime/terminal-chat-owner-persistence.test.ts": 4944, + "src/main/runtime/terminal-close-observed-exit.test.ts": 5503, + "src/main/runtime/terminal-close-slow-agent-exit.test.ts": 7870, + "src/main/runtime/terminal-command-paint.test.ts": 42, + "src/main/runtime/terminal-focus-navigation-coalescer.test.ts": 45, + "src/main/runtime/terminal-foreground-group.test.ts": 121, + "src/main/runtime/terminal-identity-probe.test.ts": 32, + "src/main/runtime/terminal-intentional-stop-exit.test.ts": 5289, + "src/main/runtime/terminal-intentional-stops.test.ts": 45, + "src/main/runtime/terminal-interactive-wait-visibility.test.ts": 4225, + "src/main/runtime/terminal-leaf-tab-resolution.test.ts": 40, + "src/main/runtime/terminal-list-execution-host-scope.test.ts": 7247, + "src/main/runtime/terminal-list-payload-size.test.ts": 4546, + "src/main/runtime/terminal-list-pending-pty-registration.test.ts": 3947, + "src/main/runtime/terminal-list-stale-leaf-liveness.test.ts": 4165, + "src/main/runtime/terminal-list-surface-lost-orphan.test.ts": 3974, + "src/main/runtime/terminal-mobile-subscribe-tab-mount.test.ts": 5041, + "src/main/runtime/terminal-model-query-authority.test.ts": 41, + "src/main/runtime/terminal-orphan-owner.test.ts": 30, + "src/main/runtime/terminal-orphan-topology.test.ts": 40, + "src/main/runtime/terminal-pane-recovery-liveness-gate.test.ts": 6266, + "src/main/runtime/terminal-projection.test.ts": 35, + "src/main/runtime/terminal-pty-exit-waiter.test.ts": 4167, + "src/main/runtime/terminal-query-responder.test.ts": 19211, + "src/main/runtime/terminal-restore-record-seed.test.ts": 4619, + "src/main/runtime/terminal-retirement-async-durability.test.ts": 5733, + "src/main/runtime/terminal-retirement-proof-emitted-frame.test.ts": 5801, + "src/main/runtime/terminal-retirement-proof-publication.test.ts": 4802, + "src/main/runtime/terminal-run-facts-input.test.ts": 4586, + "src/main/runtime/terminal-run-facts.test.ts": 51, + "src/main/runtime/terminal-screen-row-metadata.test.ts": 193, + "src/main/runtime/terminal-send-stale-leaf-liveness.test.ts": 4542, + "src/main/runtime/terminal-shell-override-host-support.test.ts": 45, + "src/main/runtime/terminal-subscribe-exit-waiter-leak.test.ts": 4671, + "src/main/runtime/terminal-subscriber-driven-daemon-attach.test.ts": 6357, + "src/main/runtime/terminal-surface-close.test.ts": 245, + "src/main/runtime/terminal-tail-buffer.test.ts": 7350, + "src/main/runtime/terminal-tail-crlf.test.ts": 4828, + "src/main/runtime/terminal-tail-redraw-row.equivalence.test.ts": 1862, + "src/main/runtime/terminal-tail-redraw-scaling.test.ts": 463, + "src/main/runtime/terminal-tail-redraw-spans.test.ts": 287, + "src/main/runtime/terminal-tail-redraw-wall-time.test.ts": 41259, + "src/main/runtime/terminal-tail-row-retention.test.ts": 1561, + "src/main/runtime/terminal-tail-sentinel-index.test.ts": 3453, + "src/main/runtime/terminal-tail-whitespace.test.ts": 284, + "src/main/runtime/terminal-vertical-control-scan.test.ts": 342, + "src/main/runtime/terminal-wait-detection.test.ts": 301, + "src/main/runtime/terminal-wait-name-only-idle.test.ts": 4498, + "src/main/runtime/terminal-wait-results.test.ts": 35, + "src/main/runtime/terminal-wait-tail-window.test.ts": 251, + "src/main/runtime/terminal/artifacts/file-grants.test.ts": 560, + "src/main/runtime/tui-idle-claude-task-wakeup.test.ts": 335, + "src/main/runtime/tui-idle-command-paint.test.ts": 4275, + "src/main/runtime/tui-idle-delivery-and-quiescence.test.ts": 4142, + "src/main/runtime/tui-idle-evidence-source.test.ts": 4203, + "src/main/runtime/tui-idle-evidence.test.ts": 461, + "src/main/runtime/tui-idle-hook-lane-runtime.test.ts": 6744, + "src/main/runtime/tui-idle-hook-lane.test.ts": 316, + "src/main/runtime/tui-idle-name-only-real-pty.integration.test.ts": 35446, + "src/main/runtime/tui-idle-weak-evidence-seam.test.ts": 4548, + "src/main/runtime/unreadable-secret-store-preservation.win32.test.ts": 45, + "src/main/runtime/wait-blocked-check-state.test.ts": 94, + "src/main/runtime/wait-blocked-keyword-carry-retention.test.ts": 3823, + "src/main/runtime/windows-default-route-interfaces.test.ts": 41, + "src/main/runtime/windows-drive-listing.test.ts": 38, + "src/main/runtime/windows-firewall-remote-scope.test.ts": 61, + "src/main/runtime/windows-mobile-firewall.test.ts": 66, + "src/main/runtime/worker-terminal-archive-release.test.ts": 7746, + "src/main/runtime/worker-terminal-archive-screen.test.ts": 6002, + "src/main/runtime/worker-terminal-wrap-provenance.test.ts": 6757, + "src/main/runtime/workspace-layout-stream.test.ts": 425, + "src/main/runtime/workspace-session-membership-scaling.test.ts": 77, + "src/main/runtime/workspace-session-worktree-id.test.ts": 229, + "src/main/runtime/workspaces/workspace-query-prototype.test.ts": 5484, + "src/main/runtime/worktree-launch-host-repo.test.ts": 40, + "src/main/runtime/worktree-list-host-scope.test.ts": 1474, + "src/main/runtime/worktree-path-selector-wsl-posix.test.ts": 4150, + "src/main/runtime/worktree-ps-degraded-repo-scan.test.ts": 3674, + "src/main/runtime/worktree-ps-host-scope.test.ts": 3663, + "src/main/runtime/worktree-ps-unverifiable-terminals.test.ts": 4273, + "src/main/runtime/worktree-rm-id-selector-path-spelling.test.ts": 4381, + "src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts": 4510, + "src/main/runtime/worktree-scan-execution-host-routing.test.ts": 4398, + "src/main/runtime/worktree-scan-repeated-path.test.ts": 4897, + "src/main/runtime/worktree-teardown-unstopped-pty.test.ts": 860, + "src/main/runtime/worktree-teardown.test.ts": 1009, + "src/main/runtime/worktree-terminal-mutation-lock.test.ts": 46, + "src/main/serve-update-handoff.app-environment.test.ts": 1353, + "src/main/serve-update-handoff.test.ts": 49, + "src/main/server/serve-readiness.test.ts": 49, + "src/main/server/serve-stdout-boundary.test.ts": 43, + "src/main/shell-prompt-readiness-probe.test.ts": 74, + "src/main/shell-startup-identity-scanner.test.ts": 24, + "src/main/shell-startup-output-scanner.test.ts": 56, + "src/main/shell-wrapper-content-address.test.ts": 45, + "src/main/shell-wrapper-generated-file-snapshot.test.ts": 105, + "src/main/sidecar-snapshot-file.test.ts": 52, + "src/main/skills/agent-skill-selection.test.ts": 246, + "src/main/skills/antigravity-skill-discovery.test.ts": 85, + "src/main/skills/claude-plugin-skill-sources-wsl.test.ts": 80, + "src/main/skills/claude-plugin-skill-sources.test.ts": 89, + "src/main/skills/discovery-filter-sharing.test.ts": 242, + "src/main/skills/discovery-source-filter-order.test.ts": 94, + "src/main/skills/discovery.test.ts": 194, "src/main/skills/skill-bundle-artifacts.test.ts": 285, - "src/main/skills/skill-bundle-creation.test.ts": 636, - "src/main/skills/skill-bundle-install-service.test.ts": 447, - "src/main/skills/skill-bundle-observability-summary.test.ts": 39, - "src/main/skills/skill-bundle-process-termination-child.test.ts": 276, - "src/main/skills/skill-bundle-process-termination.integration.test.ts": 294, - "src/main/skills/skill-candidate-concurrency.test.ts": 67, - "src/main/skills/skill-client-mediated-transfer-cancellation.test.ts": 278, - "src/main/skills/skill-client-mediated-transfer.test.ts": 295, - "src/main/skills/skill-cloud-direct-upload.test.ts": 110, - "src/main/skills/skill-cloud-grant-installation.test.ts": 253, - "src/main/skills/skill-cloud-install-target.test.ts": 54, - "src/main/skills/skill-cloud-request.test.ts": 62, - "src/main/skills/skill-cloud-service.test.ts": 512, + "src/main/skills/skill-bundle-creation.test.ts": 686, + "src/main/skills/skill-bundle-install-service.test.ts": 565, + "src/main/skills/skill-bundle-observability-summary.test.ts": 31, + "src/main/skills/skill-bundle-process-termination-child.test.ts": 299, + "src/main/skills/skill-bundle-process-termination.integration.test.ts": 287, + "src/main/skills/skill-candidate-concurrency.test.ts": 46, + "src/main/skills/skill-client-mediated-transfer-cancellation.test.ts": 271, + "src/main/skills/skill-client-mediated-transfer.test.ts": 296, + "src/main/skills/skill-cloud-direct-upload.test.ts": 106, + "src/main/skills/skill-cloud-grant-installation.test.ts": 225, + "src/main/skills/skill-cloud-install-target.test.ts": 38, + "src/main/skills/skill-cloud-request.test.ts": 41, + "src/main/skills/skill-cloud-service.test.ts": 479, "src/main/skills/skill-delete/plan.test.ts": 217, - "src/main/skills/skill-delete/recovery.test.ts": 266, - "src/main/skills/skill-delete/roots.test.ts": 98, - "src/main/skills/skill-delete/service.test.ts": 497, - "src/main/skills/skill-delete/staging-visibility.test.ts": 124, - "src/main/skills/skill-delete/wsl-enumeration-protocol.test.ts": 98, - "src/main/skills/skill-discovery-concurrency.test.ts": 152, - "src/main/skills/skill-discovery-order.test.ts": 267, - "src/main/skills/skill-discovery-source-filter.test.ts": 41, - "src/main/skills/skill-discovery-target.test.ts": 121, - "src/main/skills/skill-discovery-wsl-alias-sharing.test.ts": 124, - "src/main/skills/skill-discovery-wsl-bash-filter.test.ts": 2071, - "src/main/skills/skill-discovery-wsl-plugins.test.ts": 93, - "src/main/skills/skill-discovery-wsl-script-roundtrip.test.ts": 112, - "src/main/skills/skill-discovery-wsl.test.ts": 111, - "src/main/skills/skill-freshness-eligibility.test.ts": 84, - "src/main/skills/skill-freshness-inventory-limits.test.ts": 254, - "src/main/skills/skill-freshness-inventory.test.ts": 902, - "src/main/skills/skill-git-tree-identity.test.ts": 79, - "src/main/skills/skill-install-destinations.test.ts": 68, - "src/main/skills/skill-install-discovery-verification.test.ts": 129, - "src/main/skills/skill-install-lock-release.test.ts": 48, - "src/main/skills/skill-install-lock.test.ts": 438, - "src/main/skills/skill-install-management-service.test.ts": 319, - "src/main/skills/skill-install-provenance.test.ts": 85, - "src/main/skills/skill-install-recovery.test.ts": 513, - "src/main/skills/skill-install-request-service.test.ts": 714, - "src/main/skills/skill-install-service.test.ts": 1015, - "src/main/skills/skill-install-transaction.test.ts": 1418, - "src/main/skills/skill-operation-observability.test.ts": 330, - "src/main/skills/skill-package-creation.test.ts": 502, - "src/main/skills/skill-package-deterministic-gzip.test.ts": 1040, - "src/main/skills/skill-package-download-response-lifetime.test.ts": 904, - "src/main/skills/skill-package-download.test.ts": 318, - "src/main/skills/skill-package-identity.test.ts": 117, - "src/main/skills/skill-package-tar.test.ts": 422, - "src/main/skills/skill-placement-alias-repair.test.ts": 238, - "src/main/skills/skill-placement-copy-drift.test.ts": 211, - "src/main/skills/skill-placement-reconciliation.test.ts": 280, - "src/main/skills/skill-placement-transaction.test.ts": 322, - "src/main/skills/skill-plugin-cache-scan.test.ts": 325, - "src/main/skills/skill-process-contention-child.test.ts": 259, - "src/main/skills/skill-process-contention.integration.test.ts": 245, - "src/main/skills/skill-process-termination-child.test.ts": 270, - "src/main/skills/skill-process-termination.integration.test.ts": 310, - "src/main/skills/skill-provider-destinations.test.ts": 64, - "src/main/skills/skill-provider-runtime-roots.test.ts": 79, - "src/main/skills/skill-remote-error-category-parity.test.ts": 264, - "src/main/skills/skill-remote-install-cancellation.test.ts": 46, - "src/main/skills/skill-remote-install-service.test.ts": 273, - "src/main/skills/skill-remove-transaction.test.ts": 852, - "src/main/skills/skill-root-file-walk.test.ts": 67, - "src/main/skills/skill-runtime-capability.test.ts": 77, - "src/main/skills/skill-scan-coalescer.test.ts": 62, - "src/main/skills/skill-share-preparation-service.test.ts": 426, - "src/main/skills/skill-transaction-startup-recovery.test.ts": 471, - "src/main/skills/skill-update-convergence.test.ts": 73, - "src/main/skills/skill-update-outcome.test.ts": 313, - "src/main/skills/skill-update-registration.test.ts": 51, - "src/main/skills/skill-update-run.test.ts": 154, - "src/main/skills/skill-upload-process-restart-child.test.ts": 222, - "src/main/skills/skill-upload-process-restart.integration.test.ts": 244, - "src/main/skills/skill-upload-session-admission-regression.test.ts": 301, - "src/main/skills/skill-upload-session-service.test.ts": 390, - "src/main/skills/skill-windows-rename-contention.integration.test.ts": 43, - "src/main/skills/skill-windows-workspace.integration.test.ts": 345, - "src/main/skills/skill-wsl-delete.integration.test.ts": 70, - "src/main/skills/skill-wsl-install-filesystem.test.ts": 82, - "src/main/skills/skill-wsl-install-transaction.integration.test.ts": 254, - "src/main/skills/skill-wsl-posix-semantics.integration.test.ts": 326, - "src/main/skills/skill-wsl-provider-detection.test.ts": 84, - "src/main/source-control/forge-provider.test.ts": 418, - "src/main/source-control/hosted-review-azure-devops.integration.test.ts": 842, - "src/main/source-control/hosted-review-base-ref-suffix.test.ts": 428, - "src/main/source-control/hosted-review-bitbucket.integration.test.ts": 2640, - "src/main/source-control/hosted-review-branch-cache.test.ts": 216, - "src/main/source-control/hosted-review-creation-eligibility.test.ts": 488, - "src/main/source-control/hosted-review-creation-gitlab-self-hosted.test.ts": 507, - "src/main/source-control/hosted-review-creation-shared-symlinks.test.ts": 488, - "src/main/source-control/hosted-review-creation.test.ts": 487, - "src/main/source-control/hosted-review-dirty-preflight-wsl-paths.test.ts": 440, - "src/main/source-control/hosted-review-execution-host-routing.test.ts": 406, - "src/main/source-control/hosted-review-gitea.integration.test.ts": 771, - "src/main/source-control/hosted-review-inflight-invalidation.test.ts": 131, - "src/main/source-control/hosted-review-inflight-lookups.test.ts": 49, - "src/main/source-control/hosted-review.test.ts": 465, - "src/main/source-control/pull-request-linked-issue.test.ts": 63, - "src/main/source-control/repo-default-branch.test.ts": 137, - "src/main/source-control/stacked-hosted-review-creation.test.ts": 51, - "src/main/speech/model-catalog.test.ts": 48, - "src/main/speech/model-manager-download-error.test.ts": 152, + "src/main/skills/skill-delete/recovery.test.ts": 378, + "src/main/skills/skill-delete/roots.test.ts": 64, + "src/main/skills/skill-delete/service.test.ts": 579, + "src/main/skills/skill-delete/staging-visibility.test.ts": 95, + "src/main/skills/skill-delete/wsl-enumeration-protocol.test.ts": 45, + "src/main/skills/skill-discovery-concurrency.test.ts": 151, + "src/main/skills/skill-discovery-order.test.ts": 244, + "src/main/skills/skill-discovery-source-filter.test.ts": 33, + "src/main/skills/skill-discovery-target.test.ts": 112, + "src/main/skills/skill-discovery-wsl-alias-sharing.test.ts": 110, + "src/main/skills/skill-discovery-wsl-bash-filter.test.ts": 2072, + "src/main/skills/skill-discovery-wsl-plugins.test.ts": 74, + "src/main/skills/skill-discovery-wsl-script-roundtrip.test.ts": 136, + "src/main/skills/skill-discovery-wsl.test.ts": 85, + "src/main/skills/skill-freshness-eligibility.test.ts": 48, + "src/main/skills/skill-freshness-inventory-limits.test.ts": 292, + "src/main/skills/skill-freshness-inventory.test.ts": 951, + "src/main/skills/skill-install-destinations.test.ts": 57, + "src/main/skills/skill-install-discovery-verification.test.ts": 98, + "src/main/skills/skill-install-lock-release.test.ts": 31, + "src/main/skills/skill-install-lock.test.ts": 465, + "src/main/skills/skill-install-management-service.test.ts": 266, + "src/main/skills/skill-install-provenance.test.ts": 56, + "src/main/skills/skill-install-recovery.test.ts": 463, + "src/main/skills/skill-install-request-service.test.ts": 572, + "src/main/skills/skill-install-service.test.ts": 1431, + "src/main/skills/skill-install-transaction.test.ts": 1483, + "src/main/skills/skill-lock-sources.test.ts": 40, + "src/main/skills/skill-operation-observability.test.ts": 341, + "src/main/skills/skill-package-creation.test.ts": 616, + "src/main/skills/skill-package-deterministic-gzip.test.ts": 977, + "src/main/skills/skill-package-download-response-lifetime.test.ts": 975, + "src/main/skills/skill-package-download.test.ts": 273, + "src/main/skills/skill-package-identity.test.ts": 100, + "src/main/skills/skill-package-tar.test.ts": 400, + "src/main/skills/skill-placement-alias-repair.test.ts": 242, + "src/main/skills/skill-placement-copy-drift.test.ts": 250, + "src/main/skills/skill-placement-reconciliation.test.ts": 263, + "src/main/skills/skill-placement-transaction.test.ts": 301, + "src/main/skills/skill-plugin-cache-scan.test.ts": 352, + "src/main/skills/skill-process-contention-child.test.ts": 285, + "src/main/skills/skill-process-contention.integration.test.ts": 226, + "src/main/skills/skill-process-termination-child.test.ts": 289, + "src/main/skills/skill-process-termination.integration.test.ts": 326, + "src/main/skills/skill-provider-destinations.test.ts": 36, + "src/main/skills/skill-provider-runtime-roots.test.ts": 80, + "src/main/skills/skill-remote-error-category-parity.test.ts": 284, + "src/main/skills/skill-remote-install-cancellation.test.ts": 40, + "src/main/skills/skill-remote-install-service.test.ts": 276, + "src/main/skills/skill-remove-transaction.test.ts": 662, + "src/main/skills/skill-root-file-walk.test.ts": 62, + "src/main/skills/skill-runtime-capability.test.ts": 48, + "src/main/skills/skill-scan-coalescer.test.ts": 49, + "src/main/skills/skill-share-preparation-service.test.ts": 425, + "src/main/skills/skill-transaction-startup-recovery.test.ts": 521, + "src/main/skills/skill-update-outcome.test.ts": 48, + "src/main/skills/skill-update-run.test.ts": 160, + "src/main/skills/skill-upload-process-restart-child.test.ts": 210, + "src/main/skills/skill-upload-process-restart.integration.test.ts": 232, + "src/main/skills/skill-upload-session-admission-regression.test.ts": 263, + "src/main/skills/skill-upload-session-service.test.ts": 368, + "src/main/skills/skill-windows-rename-contention.integration.test.ts": 27, + "src/main/skills/skill-windows-workspace.integration.test.ts": 313, + "src/main/skills/skill-wsl-delete.integration.test.ts": 60, + "src/main/skills/skill-wsl-install-filesystem.test.ts": 49, + "src/main/skills/skill-wsl-install-transaction.integration.test.ts": 295, + "src/main/skills/skill-wsl-posix-semantics.integration.test.ts": 343, + "src/main/skills/skill-wsl-provider-detection.test.ts": 41, + "src/main/source-control/forge-provider.test.ts": 516, + "src/main/source-control/hosted-review-azure-devops.integration.test.ts": 795, + "src/main/source-control/hosted-review-base-ref-suffix.test.ts": 579, + "src/main/source-control/hosted-review-bitbucket.integration.test.ts": 2944, + "src/main/source-control/hosted-review-branch-cache.test.ts": 198, + "src/main/source-control/hosted-review-creation-eligibility.test.ts": 606, + "src/main/source-control/hosted-review-creation-gitlab-self-hosted.test.ts": 543, + "src/main/source-control/hosted-review-creation-shared-symlinks.test.ts": 595, + "src/main/source-control/hosted-review-creation.test.ts": 610, + "src/main/source-control/hosted-review-dirty-preflight-wsl-paths.test.ts": 539, + "src/main/source-control/hosted-review-execution-host-routing.test.ts": 556, + "src/main/source-control/hosted-review-gitea.integration.test.ts": 897, + "src/main/source-control/hosted-review-inflight-invalidation.test.ts": 124, + "src/main/source-control/hosted-review-inflight-lookups.test.ts": 42, + "src/main/source-control/hosted-review.test.ts": 600, + "src/main/source-control/pull-request-linked-issue.test.ts": 32, + "src/main/source-control/repo-default-branch.test.ts": 151, + "src/main/source-control/repo-remote-head-branch.test.ts": 102, + "src/main/source-control/stacked-hosted-review-creation.test.ts": 38, + "src/main/speech/model-catalog.test.ts": 33, + "src/main/speech/model-manager-download-error.test.ts": 109, "src/main/speech/model-manager-download-resume.test.ts": 3126, - "src/main/speech/model-manager-progress-callback.test.ts": 65, - "src/main/speech/model-manager-stream-cleanup.test.ts": 124, - "src/main/speech/model-manager-windows-path.test.ts": 170, - "src/main/speech/model-manager.test.ts": 90, - "src/main/speech/openai-api-key-store.test.ts": 82, - "src/main/speech/openai-transcription-client.test.ts": 49, - "src/main/speech/speech-model-deletion.test.ts": 292, - "src/main/speech/speech-model-download-response.test.ts": 49, - "src/main/speech/stt-offline-audio-chunker.test.ts": 222, - "src/main/speech/stt-service.test.ts": 109, - "src/main/speech/stt-worker-model-config.test.ts": 76, - "src/main/speech/stt-worker.test.ts": 49, - "src/main/sqlite/node-sqlite-reader-api.test.ts": 54, - "src/main/sqlite/sqlite-read-failure.test.ts": 67, - "src/main/sqlite/sync-database-concurrent-backup.test.ts": 1201, - "src/main/sqlite/sync-database-portability.test.ts": 114, - "src/main/sqlite/sync-database.test.ts": 895, - "src/main/ssh-expired-lease-pane-readoption.test.ts": 1865, - "src/main/ssh-reattach-pane-cardinality.test.ts": 1839, - "src/main/ssh/managed-server-fence-recheck.test.ts": 459, - "src/main/ssh/orcad-activation-crash-recovery.test.ts": 754, - "src/main/ssh/orcad-activation-fence-generation.test.ts": 645, + "src/main/speech/model-manager-progress-callback.test.ts": 57, + "src/main/speech/model-manager-stream-cleanup.test.ts": 171, + "src/main/speech/model-manager-windows-path.test.ts": 115, + "src/main/speech/model-manager.test.ts": 102, + "src/main/speech/openai-api-key-store.test.ts": 73, + "src/main/speech/openai-transcription-client.test.ts": 42, + "src/main/speech/speech-model-deletion.test.ts": 251, + "src/main/speech/speech-model-download-response.test.ts": 33, + "src/main/speech/stt-offline-audio-chunker.test.ts": 234, + "src/main/speech/stt-service.test.ts": 148, + "src/main/speech/stt-worker-model-config.test.ts": 41, + "src/main/speech/stt-worker.test.ts": 21, + "src/main/sqlite/node-sqlite-reader-api.test.ts": 64, + "src/main/sqlite/sqlite-read-failure.test.ts": 68, + "src/main/sqlite/sync-database-concurrent-backup.test.ts": 1286, + "src/main/sqlite/sync-database-portability.test.ts": 127, + "src/main/sqlite/sync-database.test.ts": 877, + "src/main/ssh-expired-lease-pane-readoption.test.ts": 1555, + "src/main/ssh-reattach-pane-cardinality.test.ts": 1274, + "src/main/ssh/legacy-relay-bridge/legacy-relay-bridge-teardown.test.ts": 326, + "src/main/ssh/legacy-relay-bridge/legacy-relay-client.test.ts": 196, + "src/main/ssh/legacy-relay-bridge/legacy-relay-endpoints.test.ts": 173, + "src/main/ssh/legacy-relay-bridge/legacy-relay-target-bridge.test.ts": 74, + "src/main/ssh/managed-server-fence-recheck.test.ts": 484, + "src/main/ssh/orcad-activation-crash-recovery.test.ts": 1032, + "src/main/ssh/orcad-activation-fence-generation.test.ts": 826, "src/main/ssh/orcad-activation-fence-hold.test.ts": 397, - "src/main/ssh/orcad-activation-gate.test.ts": 62, - "src/main/ssh/orcad-activation-outcome-log.test.ts": 55, - "src/main/ssh/orcad-activation-record.test.ts": 254, - "src/main/ssh/orcad-activation-transaction.test.ts": 398, - "src/main/orcad/orcad-artifact-materializer.test.ts": 722, - "src/main/ssh/orcad-decommission-released-peer-recovery.test.ts": 295, - "src/main/ssh/orcad-decommission-transaction.test.ts": 232, - "src/main/ssh/orcad-deployment-target.test.ts": 234, - "src/main/ssh/orcad-exited-own-fence-mutation-race.test.ts": 775, - "src/main/ssh/orcad-exited-own-fence.test.ts": 5738, - "src/main/ssh/orcad-exited-own-install-lock.test.ts": 4492, - "src/main/ssh/orcad-exited-own-lock-windows.test.ts": 350, - "src/main/ssh/orcad-fence-lost-classification.test.ts": 287, - "src/main/orcad/orcad-host-unavailable.test.ts": 325, - "src/main/ssh/orcad-managed-auto-update.test.ts": 319, - "src/main/ssh/orcad-managed-bound-port.test.ts": 386, - "src/main/ssh/orcad-managed-serving.test.ts": 480, - "src/main/ssh/orcad-managed-tunnel-identity-restart.test.ts": 4422, - "src/main/ssh/orcad-managed-tunnel-identity.test.ts": 403, - "src/main/ssh/orcad-managed-tunnel-target.test.ts": 401, - "src/main/ssh/orcad-managed-tunnel-transport.test.ts": 367, - "src/main/ssh/orcad-managed-tunnel.test.ts": 901, - "src/main/ssh/orcad-managed-update-deferrals.test.ts": 37, - "src/main/ssh/orcad-managed-update-on-restore.test.ts": 334, - "src/main/ssh/orcad-managed-version-gc.test.ts": 375, - "src/main/ssh/orcad-managed-wake.test.ts": 467, - "src/main/ssh/orcad-migration-catalog-client-extra-agent-args.test.ts": 339, - "src/main/ssh/orcad-migration-catalog-client.test.ts": 294, - "src/main/ssh/orcad-migration-cutover-coordinator.test.ts": 1549, - "src/main/ssh/orcad-migration-cutover-journal.test.ts": 342, - "src/main/ssh/orcad-migration-delta-move.test.ts": 3105, - "src/main/ssh/orcad-migration-delta-snapshot.test.ts": 1922, - "src/main/ssh/orcad-migration-manifest-export.test.ts": 384, - "src/main/ssh/orcad-migration-refusal-reason.test.ts": 40, - "src/main/ssh/orcad-migration-scrollback-retention-wiring.test.ts": 59, - "src/main/ssh/orcad-migration-snapshot-coordinator.test.ts": 309, - "src/main/ssh/orcad-migration-snapshot-resume.test.ts": 1220, - "src/main/ssh/orcad-migration-source-fence.test.ts": 1306, - "src/main/ssh/orcad-migration-source-retention.test.ts": 385, - "src/main/ssh/orcad-migration-terminal-gate-host-wide.test.ts": 238, - "src/main/ssh/orcad-migration-terminal-gate.test.ts": 60, - "src/main/ssh/orcad-remote-build-hash.integration.test.ts": 404, - "src/main/ssh/orcad-remote-decommission.test.ts": 387, - "src/main/ssh/orcad-remote-deploy.test.ts": 603, - "src/main/ssh/orcad-remote-gc.test.ts": 468, - "src/main/ssh/orcad-remote-install-termination.test.ts": 495, - "src/main/ssh/orcad-remote-launch-file-modes.test.ts": 421, - "src/main/ssh/orcad-remote-launch.test.ts": 348, - "src/main/ssh/orcad-remote-lifecycle-windows.test.ts": 454, - "src/main/ssh/orcad-remote-log-tail.test.ts": 409, - "src/main/ssh/orcad-remote-node-runtime-promotion-lock.test.ts": 185, - "src/main/ssh/orcad-remote-node-runtime-slow-hash.test.ts": 1212, - "src/main/ssh/orcad-remote-node-runtime-windows.test.ts": 221, - "src/main/ssh/orcad-remote-node-runtime.test.ts": 220, - "src/main/ssh/orcad-remote-primitives.test.ts": 429, - "src/main/ssh/orcad-remote-readiness-wait.test.ts": 1837, - "src/main/ssh/orcad-remote-rollback.test.ts": 570, - "src/main/ssh/orcad-remote-runtime-control.test.ts": 281, - "src/main/ssh/orcad-remote-runtime.test.ts": 680, - "src/main/ssh/orcad-remote-shell-commands.integration.test.ts": 10068, - "src/main/ssh/orcad-remote-windows-commands.test.ts": 374, - "src/main/ssh/orcad-remote-windows-decommission.test.ts": 448, - "src/main/ssh/orcad-remote-windows-scripts.test.ts": 3448, - "src/main/ssh/orcad-retained-fence-takeover.test.ts": 194, - "src/main/ssh/orcad-retained-source-lists.test.ts": 325, - "src/main/ssh/orcad-runtime-conversion.test.ts": 2065, - "src/main/ssh/orcad-runtime-decommission.test.ts": 886, - "src/main/ssh/orcad-runtime-deployment.test.ts": 767, - "src/main/ssh/orcad-runtime-forget.test.ts": 558, - "src/main/ssh/orcad-runtime-maintenance.test.ts": 744, - "src/main/ssh/orcad-runtime-target.test.ts": 393, - "src/main/ssh/orcad-ssh-provisioning.test.ts": 279, - "src/main/ssh/orcad-state-mutation-exec.test.ts": 343, - "src/main/ssh/orcad-state-mutation-fence-heartbeat.test.ts": 17438, - "src/main/ssh/orcad-state-mutation-timeout.test.ts": 460, - "src/main/ssh/orcad-state-snapshot-shell.test.ts": 726, - "src/main/ssh/orcad-state-snapshot.test.ts": 382, - "src/main/ssh/orcad-stdio-bridge-provider.test.ts": 686, - "src/main/ssh/orcad-stdio-bridge.test.ts": 502, - "src/main/ssh/orcad-terminal-census-client.test.ts": 188, - "src/main/ssh/orcad-tunneled-pairing.test.ts": 209, - "src/main/ssh/orcad-unreachable-setup-release.test.ts": 1751, - "src/main/ssh/orcad-unreachable-setup-stop.test.ts": 424, - "src/main/ssh/orcad-update-plan.test.ts": 2663, - "src/main/ssh/orcad-windows-host-fence-ops.test.ts": 763, - "src/main/ssh/orcad-windows-host-lane.test.ts": 745, - "src/main/ssh/orcad-windows-host-state-ops.test.ts": 2035, - "src/main/node-runtime/pinned-runtime-materializer-node.test.ts": 114, - "src/main/node-runtime/pinned-runtime-materializer.test.ts": 3192, - "src/main/ssh/relay-bundle-paths.test.ts": 62, - "src/main/ssh/relay-protocol.test.ts": 120, - "src/main/ssh/relay-socket-path-limit-shell.integration.test.ts": 107, - "src/main/ssh/relay-socket-path-limit.test.ts": 559, - "src/main/ssh/relay-version-dir-liveness.test.ts": 577, - "src/main/ssh/remote-install-gc-termination.test.ts": 267, - "src/main/ssh/remote-install-model.test.ts": 199, - "src/main/ssh/remote-install-previous-version.test.ts": 187, - "src/main/ssh/remote-node-runtime-store-gc.test.ts": 2001, - "src/main/ssh/remote-node-runtime-store-lock-host-release.test.ts": 2390, - "src/main/ssh/remote-node-runtime-store-windows.test.ts": 12643, - "src/main/ssh/removed-ssh-target-tombstone-retention.test.ts": 54, - "src/main/node-runtime/runtime-archive-download.test.ts": 99, - "src/main/ssh/runtime-ssh-access-verification.test.ts": 228, - "src/main/ssh/runtime-ssh-access.test.ts": 548, - "src/main/ssh/sftp-namespace-resolution.test.ts": 91, - "src/main/ssh/sftp-stream-late-error.test.ts": 232, - "src/main/ssh/sftp-upload.test.ts": 298, - "src/main/ssh/ssh-agent-hook-interrupt-reconciliation.test.ts": 530, - "src/main/ssh/ssh-agent-identity-filter.test.ts": 67, - "src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts": 103, - "src/main/ssh/ssh-channel-multiplexer-saturation-wedge.test.ts": 72, - "src/main/ssh/ssh-channel-multiplexer-settlement.test.ts": 61, - "src/main/ssh/ssh-channel-multiplexer.test.ts": 133, - "src/main/ssh/ssh-config-alias-claim.test.ts": 87, - "src/main/ssh/ssh-config-host-picker.test.ts": 200, - "src/main/ssh/ssh-config-loader-regression.test.ts": 134, - "src/main/ssh/ssh-config-loader.test.ts": 117, - "src/main/ssh/ssh-config-parser-host-patterns.test.ts": 66, - "src/main/ssh/ssh-config-parser.test.ts": 327, - "src/main/ssh/ssh-config-resolver.test.ts": 72, - "src/main/ssh/ssh-connect-attempt-cancellation.test.ts": 94, - "src/main/ssh/ssh-connection-attribution.test.ts": 53, - "src/main/ssh/ssh-connection-auth-fallback.test.ts": 266, - "src/main/ssh/ssh-connection-channel-error.test.ts": 144, - "src/main/ssh/ssh-connection-channel-lifetime.test.ts": 41, - "src/main/ssh/ssh-connection-channel-open.test.ts": 932, - "src/main/ssh/ssh-connection-direct-startup.test.ts": 141, - "src/main/ssh/ssh-connection-file-transfer.test.ts": 178, - "src/main/ssh/ssh-connection-generation.test.ts": 52, - "src/main/ssh/ssh-connection-github-probe.test.ts": 196, - "src/main/ssh/ssh-connection-gssapi-fallback.test.ts": 254, - "src/main/ssh/ssh-connection-host-key-store-wiring.test.ts": 579, - "src/main/ssh/ssh-connection-host-key-verification.test.ts": 201, - "src/main/ssh/ssh-connection-manager-registry.test.ts": 258, - "src/main/ssh/ssh-connection-manager.test.ts": 73, - "src/main/ssh/ssh-connection-reconnect-handshake-loss.test.ts": 225, - "src/main/ssh/ssh-connection-reconnect-ladder.test.ts": 382, - "src/main/ssh/ssh-connection-sftp-namespace.test.ts": 991, - "src/main/ssh/ssh-connection-sftp-wire.test.ts": 368, - "src/main/ssh/ssh-connection-store-orcad-ownership.test.ts": 234, - "src/main/ssh/ssh-connection-store.test.ts": 320, - "src/main/ssh/ssh-connection-system-transport.test.ts": 241, - "src/main/ssh/ssh-connection-utils.test.ts": 217, - "src/main/ssh/ssh-connection.test.ts": 1261, - "src/main/ssh/ssh-control-socket.test.ts": 62, - "src/main/ssh/ssh-exec-rc-stdout-noise.test.ts": 255, - "src/main/ssh/ssh-exec-stdin-file-transfer.test.ts": 246, - "src/main/ssh/ssh-file-stream-inactivity-deadline.test.ts": 58, - "src/main/ssh/ssh-file-transfer-abort.test.ts": 105, - "src/main/ssh/ssh-g-config-resolution.test.ts": 112, - "src/main/ssh/ssh-git-response-pending-retention.test.ts": 238, - "src/main/ssh/ssh-git-response-stream-reader.test.ts": 59, - "src/main/ssh/ssh-git-stream-idle-timer.test.ts": 109, - "src/main/ssh/ssh-host-key-decision.test.ts": 74, - "src/main/ssh/ssh-host-key-store.test.ts": 193, - "src/main/ssh/ssh-host-key-verifier.test.ts": 92, - "src/main/ssh/ssh-host-platform-memo.test.ts": 40, - "src/main/ssh/ssh-host-relay-census-for-target.test.ts": 53, - "src/main/ssh/ssh-host-relay-endpoint-census.test.ts": 112, - "src/main/ssh/ssh-host-relay-terminals-on-connect.test.ts": 576, - "src/main/ssh/ssh-host-relay-windows-census.test.ts": 209, - "src/main/ssh/ssh-host-relay-windows-inventory.test.ts": 179, - "src/main/ssh/ssh-host-server-on-connect-stopped-elsewhere.test.ts": 674, - "src/main/ssh/ssh-host-server-on-connect-telemetry.test.ts": 547, - "src/main/ssh/ssh-host-server-on-connect.test.ts": 539, - "src/main/ssh/ssh-host-server-telemetry.test.ts": 228, - "src/main/ssh/ssh-hostile-host-cells.test.ts": 137, - "src/main/ssh/ssh-hostile-host-local-sshd.test.ts": 69, - "src/main/ssh/ssh-keyboard-interactive-wire.test.ts": 290, - "src/main/ssh/ssh-keyboard-interactive.test.ts": 72, - "src/main/ssh/ssh-known-hosts-source.test.ts": 128, - "src/main/ssh/ssh-known-hosts.test.ts": 96, - "src/main/ssh/ssh-listing-response-retention.test.ts": 77, - "src/main/ssh/ssh-multi-factor-authentication.test.ts": 168, - "src/main/ssh/ssh-multi-key-authentication.test.ts": 126, - "src/main/ssh/ssh-multiplexer-transport-writer.test.ts": 89, - "src/main/ssh/ssh-multiplexer-writer-retention.test.ts": 480, - "src/main/ssh/ssh-port-forward.test.ts": 253, - "src/main/ssh/ssh-posix-command-wrapper.test.ts": 140, - "src/main/ssh/ssh-provider-authority.test.ts": 67, - "src/main/ssh/ssh-proxy-command.test.ts": 120, - "src/main/ssh/ssh-quit-abort-upload.test.ts": 256, - "src/main/ssh/ssh-reconnect-error-classification.test.ts": 123, - "src/main/ssh/ssh-reconnect-ladder.test.ts": 96, - "src/main/ssh/ssh-relay-answered-failure-producers.test.ts": 301, - "src/main/ssh/ssh-relay-sentinel.test.ts": 571, - "src/main/ssh/ssh-relay-endpoint-incumbent-shell.integration.test.ts": 2228, - "src/main/ssh/ssh-relay-endpoint-incumbent.test.ts": 115, - "src/main/ssh/ssh-relay-endpoint-pty-count.test.ts": 695, - "src/main/ssh/ssh-relay-endpoint-runtime.test.ts": 102, - "src/main/ssh/ssh-relay-endpoint-takeover.test.ts": 141, - "src/main/ssh/ssh-relay-exec-input.test.ts": 91, - "src/main/ssh/ssh-relay-gc-claim-termination.test.ts": 261, - "src/main/ssh/ssh-relay-host-answered-failure.test.ts": 100, - "src/main/ssh/ssh-relay-incumbent-process.test.ts": 5678, - "src/main/ssh/ssh-relay-install-lock-exited-owner.test.ts": 324, - "src/main/ssh/ssh-relay-install-lock-termination.test.ts": 245, - "src/main/ssh/ssh-relay-install-lock.test.ts": 178, - "src/main/ssh/ssh-relay-install-namespace.test.ts": 227, - "src/main/ssh/ssh-relay-install-noisy-sftp-wire.test.ts": 292, - "src/main/ssh/ssh-relay-install-transfers-exec-fallback.test.ts": 113, - "src/main/ssh/ssh-relay-native-deps-cache-gc-real.test.ts": 706, - "src/main/ssh/ssh-relay-native-deps-cache-shell.test.ts": 421, - "src/main/ssh/ssh-relay-native-deps-cache.test.ts": 220, - "src/main/ssh/ssh-relay-pinned-node.integration.test.ts": 352, - "src/main/ssh/ssh-relay-pinned-node.test.ts": 421, - "src/main/ssh/ssh-relay-runtime-ladder.test.ts": 346, - "src/main/ssh/ssh-relay-runtime-never-worse.test.ts": 422, - "src/main/ssh/ssh-relay-runtime-resolution.test.ts": 438, - "src/main/ssh/ssh-relay-runtime-self-test.test.ts": 655, - "src/main/ssh/ssh-relay-sentinel-copy-budget.test.ts": 165, - "src/main/ssh/ssh-relay-superseded-endpoints.test.ts": 146, - "src/main/ssh/ssh-relay-versioned-install-termination.test.ts": 239, - "src/main/ssh/ssh-relay-versioned-install.test.ts": 327, - "src/main/ssh/ssh-relay-windows-launch-command.test.ts": 151, - "src/main/ssh/ssh-remote-commands.test.ts": 7310, - "src/main/ssh/ssh-remote-node-resolution.test.ts": 318, - "src/main/ssh/ssh-remote-node-toolchain-probe.test.ts": 173, - "src/main/ssh/ssh-remote-platform-detection.test.ts": 218, - "src/main/ssh/ssh-remote-platform.test.ts": 58, - "src/main/ssh/ssh-remote-powershell.test.ts": 328, - "src/main/ssh/ssh-remote-runtime-telemetry.test.ts": 286, - "src/main/ssh/ssh-remote-windows-command-line-limit.test.ts": 208, - "src/main/ssh/ssh-request-outcome-verdict.test.ts": 52, - "src/main/ssh/ssh-response-pending-frames.test.ts": 96, - "src/main/ssh/ssh-security-key-identity.test.ts": 156, - "src/main/ssh/ssh-session-limit-error.test.ts": 49, - "src/main/ssh/ssh-system-fallback.test.ts": 307, - "src/main/ssh/ssh-system-transport.integration.test.ts": 1301, - "src/main/ssh/ssh-target-id-migration.test.ts": 62, - "src/main/ssh/ssh-target-orcad-claims.test.ts": 221, - "src/main/ssh/ssh-target-orcad-dependents.test.ts": 247, - "src/main/ssh/ssh-target-orcad-preflight.test.ts": 1492, - "src/main/ssh/ssh-target-readoption.test.ts": 56, - "src/main/ssh/ssh-tcp-forwarding-probe.test.ts": 109, - "src/main/ssh/ssh-windows-host-cells.test.ts": 67, - "src/main/ssh/ssh2-port-forward-listen.test.ts": 49, - "src/main/ssh/system-ssh-binary.test.ts": 62, - "src/main/ssh/system-ssh-dynamic-forward-process.test.ts": 236, - "src/main/ssh/system-ssh-forward-process.test.ts": 228, - "src/main/ssh/system-ssh-sftp-args.test.ts": 57, - "src/main/ssh/system-ssh-sftp-path.test.ts": 54, - "src/main/ssh/system-ssh-upload-plan-budget.test.ts": 344, - "src/main/ssh/system-ssh-windows-upload.test.ts": 379, - "src/main/ssh/system-ssh-windows-write-capabilities.test.ts": 87, - "src/main/ssh/vscode-ssh-authority.test.ts": 71, - "src/main/star-nag/service-direct-star.test.ts": 381, - "src/main/star-nag/service-force-show-races.test.ts": 346, - "src/main/star-nag/service-outcome-telemetry.test.ts": 353, - "src/main/star-nag/service-prompt-moments.test.ts": 340, - "src/main/star-nag/service-threshold-prompt.test.ts": 410, - "src/main/startup/bootstrap-fatal-exit-guard.test.ts": 55, - "src/main/startup/branch-rename-hook-structured-session.test.ts": 197, - "src/main/startup/browser-process-user-agent-ordering.test.ts": 5756, - "src/main/startup/cli-launch-redirect.test.ts": 114, - "src/main/startup/codex-account-restart-resume.test.ts": 292, - "src/main/startup/codex-hook-startup-wiring.test.ts": 6575, - "src/main/startup/codex-launch-per-agent-hook-opt-out.test.ts": 187, - "src/main/startup/codex-session-resume-daemon-guard.test.ts": 143, - "src/main/startup/configure-process-dev-parent-shutdown.test.ts": 113, - "src/main/startup/configure-process-profile-state.test.ts": 143, - "src/main/startup/configure-process.test.ts": 235, - "src/main/startup/desktop-profile-instance-lock.test.ts": 219, - "src/main/startup/dev-education-suppression.test.ts": 306, - "src/main/startup/dev-instance-identity.test.ts": 57, - "src/main/startup/ensure-virtual-display.test.ts": 77, - "src/main/startup/first-window-startup-services.test.ts": 83, - "src/main/startup/gpu-fallback-marker.test.ts": 75, - "src/main/startup/gpu-fallback-switches.test.ts": 52, - "src/main/startup/gpu-lifecycle-install-dir-acl-guard.test.ts": 546, - "src/main/startup/headless-serve-ssh-registration.test.ts": 4078, - "src/main/startup/http1-compatibility-profile-state.test.ts": 90, - "src/main/startup/hydrate-shell-path.test.ts": 451, - "src/main/startup/hydrate-shell-path.windows.test.ts": 322, - "src/main/startup/legacy-worker-renderer-recovery.test.ts": 100, - "src/main/startup/linux-dev-shm-policy.test.ts": 74, - "src/main/startup/linux-keyring-backend-selection.test.ts": 73, - "src/main/startup/linux-secret-service-probe.test.ts": 51, - "src/main/startup/login-shell-environment-cache.test.ts": 90, - "src/main/startup/login-shell-environment.test.ts": 104, - "src/main/startup/main-process-error-guards.test.ts": 108, - "src/main/startup/main-process-observers-hang-breadcrumb.test.ts": 160, - "src/main/startup/main-process-quit-pending-writer.test.ts": 118, - "src/main/startup/main-process-quit-update-veto.test.ts": 91, - "src/main/startup/main-process-ready-activation-failure.test.ts": 261, - "src/main/startup/main-process-ready-identity-write.test.ts": 1168, - "src/main/startup/main-process-ready-persistence-cleanup.test.ts": 213, - "src/main/startup/main-process-ready-phase-ordering.test.ts": 235, - "src/main/startup/main-process-runtime-launch-activation.test.ts": 5532, - "src/main/startup/main-window-actions-user-quit.test.ts": 193, - "src/main/startup/main-window-core-services.test.ts": 87, - "src/main/startup/main-window-structured-status-filter.test.ts": 195, - "src/main/startup/os-opened-document-arguments.test.ts": 167, - "src/main/startup/os-opened-document-delivery.test.ts": 168, - "src/main/startup/os-opened-document-resolution.test.ts": 254, - "src/main/startup/os-opened-documents.test.ts": 180, - "src/main/startup/profile-state-recovery-preflight.test.ts": 690, - "src/main/startup/profile-state-save-delay.test.ts": 53, - "src/main/startup/profile-state-write-failure.test.ts": 446, - "src/main/startup/renderer-heap-headroom.test.ts": 54, + "src/main/ssh/orcad-activation-gate.test.ts": 40, + "src/main/ssh/orcad-activation-outcome-log.test.ts": 40, + "src/main/ssh/orcad-activation-record.test.ts": 245, + "src/main/ssh/orcad-activation-transaction.test.ts": 451, + "src/main/ssh/orcad-decommission-released-peer-recovery.test.ts": 266, + "src/main/ssh/orcad-decommission-transaction.test.ts": 309, + "src/main/ssh/orcad-deployment-target.test.ts": 263, + "src/main/ssh/orcad-exited-own-fence-mutation-race.test.ts": 822, + "src/main/ssh/orcad-exited-own-fence.test.ts": 5949, + "src/main/ssh/orcad-exited-own-install-lock.test.ts": 4491, + "src/main/ssh/orcad-exited-own-lock-windows.test.ts": 422, + "src/main/ssh/orcad-fence-lost-classification.test.ts": 374, + "src/main/ssh/orcad-managed-auto-update.test.ts": 438, + "src/main/ssh/orcad-managed-bound-port.test.ts": 442, + "src/main/ssh/orcad-managed-serving.test.ts": 489, + "src/main/ssh/orcad-managed-tunnel-bridge-port.test.ts": 414, + "src/main/ssh/orcad-managed-tunnel-identity-restart.test.ts": 5282, + "src/main/ssh/orcad-managed-tunnel-identity.test.ts": 447, + "src/main/ssh/orcad-managed-tunnel-target.test.ts": 558, + "src/main/ssh/orcad-managed-tunnel-transport.test.ts": 437, + "src/main/ssh/orcad-managed-tunnel.test.ts": 914, + "src/main/ssh/orcad-managed-update-deferrals.test.ts": 30, + "src/main/ssh/orcad-managed-update-on-restore.test.ts": 422, + "src/main/ssh/orcad-managed-version-gc.test.ts": 380, + "src/main/ssh/orcad-managed-wake.test.ts": 567, + "src/main/ssh/orcad-migration-catalog-client-extra-agent-args.test.ts": 351, + "src/main/ssh/orcad-migration-catalog-client-source-kind.test.ts": 378, + "src/main/ssh/orcad-migration-catalog-client.test.ts": 373, + "src/main/ssh/orcad-migration-cutover-coordinator.test.ts": 2115, + "src/main/ssh/orcad-migration-cutover-journal.test.ts": 426, + "src/main/ssh/orcad-migration-delta-move.test.ts": 3104, + "src/main/ssh/orcad-migration-delta-snapshot.test.ts": 2081, + "src/main/ssh/orcad-migration-manifest-export.test.ts": 584, + "src/main/ssh/orcad-migration-manifest-golden.test.ts": 517, + "src/main/ssh/orcad-migration-path-rewrite.test.ts": 461, + "src/main/ssh/orcad-migration-refusal-reason.test.ts": 30, + "src/main/ssh/orcad-migration-scrollback-retention-wiring.test.ts": 62, + "src/main/ssh/orcad-migration-snapshot-coordinator.test.ts": 353, + "src/main/ssh/orcad-migration-snapshot-resume.test.ts": 1625, + "src/main/ssh/orcad-migration-source-fence.test.ts": 1790, + "src/main/ssh/orcad-migration-source-retention.test.ts": 476, + "src/main/ssh/orcad-migration-ssh-relay-source.test.ts": 1623, + "src/main/ssh/orcad-migration-terminal-gate-host-wide.test.ts": 240, + "src/main/ssh/orcad-migration-terminal-gate.test.ts": 43, + "src/main/ssh/orcad-remote-build-hash.integration.test.ts": 375, + "src/main/ssh/orcad-remote-decommission.test.ts": 459, + "src/main/ssh/orcad-remote-deploy.test.ts": 574, + "src/main/ssh/orcad-remote-gc.test.ts": 422, + "src/main/ssh/orcad-remote-install-termination.test.ts": 411, + "src/main/ssh/orcad-remote-launch-file-modes.test.ts": 401, + "src/main/ssh/orcad-remote-launch-setsid.test.ts": 401, + "src/main/ssh/orcad-remote-launch.test.ts": 334, + "src/main/ssh/orcad-remote-lifecycle-windows.test.ts": 474, + "src/main/ssh/orcad-remote-log-tail.test.ts": 443, + "src/main/ssh/orcad-remote-node-runtime-promotion-lock.test.ts": 182, + "src/main/ssh/orcad-remote-node-runtime-recorded-probe.test.ts": 242, + "src/main/ssh/orcad-remote-node-runtime-slow-hash.test.ts": 1230, + "src/main/ssh/orcad-remote-node-runtime-windows.test.ts": 234, + "src/main/ssh/orcad-remote-node-runtime.test.ts": 212, + "src/main/ssh/orcad-remote-primitives.test.ts": 459, + "src/main/ssh/orcad-remote-readiness-wait.test.ts": 3110, + "src/main/ssh/orcad-remote-rollback.test.ts": 575, + "src/main/ssh/orcad-remote-runtime-control.test.ts": 365, + "src/main/ssh/orcad-remote-runtime.test.ts": 702, + "src/main/ssh/orcad-remote-shell-commands.integration.test.ts": 11199, + "src/main/ssh/orcad-remote-windows-commands.test.ts": 403, + "src/main/ssh/orcad-remote-windows-decommission.test.ts": 457, + "src/main/ssh/orcad-remote-windows-scripts.test.ts": 3486, + "src/main/ssh/orcad-retained-fence-takeover.test.ts": 200, + "src/main/ssh/orcad-retained-source-lists.test.ts": 504, + "src/main/ssh/orcad-running-version-adoption.test.ts": 383, + "src/main/ssh/orcad-runtime-conversion.test.ts": 2187, + "src/main/ssh/orcad-runtime-decommission.test.ts": 877, + "src/main/ssh/orcad-runtime-deployment.test.ts": 873, + "src/main/ssh/orcad-runtime-forget.test.ts": 814, + "src/main/ssh/orcad-runtime-maintenance.test.ts": 894, + "src/main/ssh/orcad-runtime-target.test.ts": 217, + "src/main/ssh/orcad-ssh-provisioning.test.ts": 271, + "src/main/ssh/orcad-state-mutation-exec.test.ts": 360, + "src/main/ssh/orcad-state-mutation-fence-heartbeat.test.ts": 4403, + "src/main/ssh/orcad-state-mutation-timeout.test.ts": 434, + "src/main/ssh/orcad-state-snapshot-shell.test.ts": 832, + "src/main/ssh/orcad-state-snapshot.test.ts": 334, + "src/main/ssh/orcad-stdio-bridge-provider.test.ts": 705, + "src/main/ssh/orcad-stdio-bridge.test.ts": 597, + "src/main/ssh/orcad-terminal-census-client.test.ts": 302, + "src/main/ssh/orcad-tunneled-pairing.test.ts": 257, + "src/main/ssh/orcad-unreachable-setup-release.test.ts": 1871, + "src/main/ssh/orcad-unreachable-setup-stop.test.ts": 421, + "src/main/ssh/orcad-update-plan.test.ts": 557, + "src/main/ssh/orcad-windows-host-fence-ops.test.ts": 995, + "src/main/ssh/orcad-windows-host-lane.test.ts": 647, + "src/main/ssh/orcad-windows-host-state-ops.test.ts": 2049, + "src/main/ssh/pinned-runtime-host-target.test.ts": 51, + "src/main/ssh/relay-bundle-paths.test.ts": 32, + "src/main/ssh/relay-install-artifacts.test.ts": 37, + "src/main/ssh/relay-protocol.test.ts": 113, + "src/main/ssh/remote-install-gc-termination.test.ts": 187, + "src/main/ssh/remote-install-model.test.ts": 184, + "src/main/ssh/remote-node-runtime-store-gc.test.ts": 1625, + "src/main/ssh/remote-node-runtime-store-lock-host-release.test.ts": 2252, + "src/main/ssh/remote-node-runtime-store-windows.test.ts": 7184, + "src/main/ssh/remote-versioned-install.test.ts": 283, + "src/main/ssh/removed-ssh-target-tombstone-retention.test.ts": 72, + "src/main/ssh/runtime-ssh-access-verification.test.ts": 290, + "src/main/ssh/runtime-ssh-access.test.ts": 621, + "src/main/ssh/sftp-namespace-resolution.test.ts": 66, + "src/main/ssh/sftp-stream-late-error.test.ts": 166, + "src/main/ssh/sftp-upload.test.ts": 292, + "src/main/ssh/ssh-agent-hook-interrupt-reconciliation.test.ts": 729, + "src/main/ssh/ssh-agent-identity-filter.test.ts": 47, + "src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts": 99, + "src/main/ssh/ssh-channel-multiplexer-saturation-wedge.test.ts": 61, + "src/main/ssh/ssh-channel-multiplexer-settlement.test.ts": 37, + "src/main/ssh/ssh-channel-multiplexer.test.ts": 120, + "src/main/ssh/ssh-config-alias-claim.test.ts": 58, + "src/main/ssh/ssh-config-host-picker.test.ts": 223, + "src/main/ssh/ssh-config-loader-regression.test.ts": 132, + "src/main/ssh/ssh-config-loader.test.ts": 114, + "src/main/ssh/ssh-config-parser-host-patterns.test.ts": 49, + "src/main/ssh/ssh-config-parser.test.ts": 331, + "src/main/ssh/ssh-config-resolver.test.ts": 40, + "src/main/ssh/ssh-connect-attempt-cancellation.test.ts": 85, + "src/main/ssh/ssh-connection-attribution.test.ts": 34, + "src/main/ssh/ssh-connection-auth-fallback.test.ts": 269, + "src/main/ssh/ssh-connection-channel-error.test.ts": 142, + "src/main/ssh/ssh-connection-channel-lifetime.test.ts": 58, + "src/main/ssh/ssh-connection-channel-open.test.ts": 930, + "src/main/ssh/ssh-connection-direct-startup.test.ts": 128, + "src/main/ssh/ssh-connection-file-transfer.test.ts": 142, + "src/main/ssh/ssh-connection-generation.test.ts": 41, + "src/main/ssh/ssh-connection-github-probe.test.ts": 174, + "src/main/ssh/ssh-connection-gssapi-fallback.test.ts": 237, + "src/main/ssh/ssh-connection-host-key-store-wiring.test.ts": 524, + "src/main/ssh/ssh-connection-host-key-verification.test.ts": 216, + "src/main/ssh/ssh-connection-manager-registry.test.ts": 402, + "src/main/ssh/ssh-connection-manager-sign-in-reuse.test.ts": 522, + "src/main/ssh/ssh-connection-manager.test.ts": 274, + "src/main/ssh/ssh-connection-reconnect-handshake-loss.test.ts": 254, + "src/main/ssh/ssh-connection-reconnect-ladder.test.ts": 354, + "src/main/ssh/ssh-connection-sftp-namespace.test.ts": 1227, + "src/main/ssh/ssh-connection-sftp-wire.test.ts": 254, + "src/main/ssh/ssh-connection-store-orcad-ownership.test.ts": 293, + "src/main/ssh/ssh-connection-store.test.ts": 337, + "src/main/ssh/ssh-connection-system-transport.test.ts": 206, + "src/main/ssh/ssh-connection-utils.test.ts": 244, + "src/main/ssh/ssh-connection.test.ts": 1226, + "src/main/ssh/ssh-control-socket.test.ts": 50, + "src/main/ssh/ssh-credential-decline.test.ts": 252, + "src/main/ssh/ssh-exec-rc-stdout-noise.test.ts": 260, + "src/main/ssh/ssh-exec-stdin-file-transfer.test.ts": 334, + "src/main/ssh/ssh-file-stream-inactivity-deadline.test.ts": 38, + "src/main/ssh/ssh-file-transfer-abort.test.ts": 77, + "src/main/ssh/ssh-g-config-resolution.test.ts": 128, + "src/main/ssh/ssh-host-key-decision.test.ts": 67, + "src/main/ssh/ssh-host-key-store.test.ts": 207, + "src/main/ssh/ssh-host-key-verifier.test.ts": 77, + "src/main/ssh/ssh-host-platform-memo.test.ts": 51, + "src/main/ssh/ssh-host-relay-census-for-target.test.ts": 43, + "src/main/ssh/ssh-host-relay-endpoint-census.test.ts": 133, + "src/main/ssh/ssh-host-relay-terminals-on-connect.test.ts": 272, + "src/main/ssh/ssh-host-relay-windows-census.test.ts": 247, + "src/main/ssh/ssh-host-relay-windows-inventory.test.ts": 201, + "src/main/ssh/ssh-host-server-on-connect-stopped-elsewhere.test.ts": 865, + "src/main/ssh/ssh-host-server-on-connect-telemetry.test.ts": 597, + "src/main/ssh/ssh-host-server-on-connect.test.ts": 553, + "src/main/ssh/ssh-host-server-telemetry.test.ts": 316, + "src/main/ssh/ssh-hostile-host-cells.test.ts": 105, + "src/main/ssh/ssh-hostile-host-local-sshd.test.ts": 71, + "src/main/ssh/ssh-hostile-hosts.docker.test.ts": 574, + "src/main/ssh/ssh-keyboard-interactive-wire.test.ts": 261, + "src/main/ssh/ssh-keyboard-interactive.test.ts": 41, + "src/main/ssh/ssh-known-hosts-source.test.ts": 106, + "src/main/ssh/ssh-known-hosts.test.ts": 89, + "src/main/ssh/ssh-multi-factor-authentication.test.ts": 109, + "src/main/ssh/ssh-multi-key-authentication.test.ts": 87, + "src/main/ssh/ssh-multiplexer-transport-writer.test.ts": 75, + "src/main/ssh/ssh-multiplexer-writer-retention.test.ts": 511, + "src/main/ssh/ssh-port-forward.test.ts": 261, + "src/main/ssh/ssh-posix-command-wrapper.test.ts": 122, + "src/main/ssh/ssh-provider-authority.test.ts": 50, + "src/main/ssh/ssh-proxy-command.test.ts": 108, + "src/main/ssh/ssh-quit-abort-upload.test.ts": 252, + "src/main/ssh/ssh-reconnect-error-classification.test.ts": 98, + "src/main/ssh/ssh-reconnect-ladder.test.ts": 75, + "src/main/ssh/ssh-relay-answered-failure-producers.test.ts": 321, + "src/main/ssh/ssh-relay-endpoint-incumbent-shell.integration.test.ts": 1326, + "src/main/ssh/ssh-relay-endpoint-incumbent.test.ts": 100, + "src/main/ssh/ssh-relay-endpoint-pty-count.test.ts": 90, + "src/main/ssh/ssh-relay-endpoint-runtime.test.ts": 69, + "src/main/ssh/ssh-relay-exec-closed-stdin.test.ts": 89, + "src/main/ssh/ssh-relay-exec-command.test.ts": 130, + "src/main/ssh/ssh-relay-exec-input.test.ts": 92, + "src/main/ssh/ssh-relay-gc-claim-termination.test.ts": 249, + "src/main/ssh/ssh-relay-host-answered-failure.test.ts": 101, + "src/main/ssh/ssh-relay-incumbent-process.test.ts": 5329, + "src/main/ssh/ssh-relay-install-lock-exited-owner.test.ts": 380, + "src/main/ssh/ssh-relay-install-lock-termination.test.ts": 276, + "src/main/ssh/ssh-relay-install-lock.test.ts": 157, + "src/main/ssh/ssh-relay-install-noisy-sftp-wire.test.ts": 299, + "src/main/ssh/ssh-relay-install-transfers-exec-fallback.test.ts": 101, + "src/main/ssh/ssh-relay-sentinel-copy-budget.test.ts": 114, + "src/main/ssh/ssh-relay-sentinel.test.ts": 400, + "src/main/ssh/ssh-relay-versioned-install-termination.test.ts": 236, + "src/main/ssh/ssh-remote-commands.test.ts": 8588, + "src/main/ssh/ssh-remote-node-resolution.test.ts": 317, + "src/main/ssh/ssh-remote-node-toolchain-probe.test.ts": 133, + "src/main/ssh/ssh-remote-platform-detection.test.ts": 206, + "src/main/ssh/ssh-remote-platform.test.ts": 34, + "src/main/ssh/ssh-remote-powershell.test.ts": 267, + "src/main/ssh/ssh-remote-windows-command-line-limit.test.ts": 179, + "src/main/ssh/ssh-request-outcome-verdict.test.ts": 37, + "src/main/ssh/ssh-response-pending-frames.test.ts": 76, + "src/main/ssh/ssh-security-key-identity.test.ts": 147, + "src/main/ssh/ssh-session-limit-error.test.ts": 27, + "src/main/ssh/ssh-system-fallback.test.ts": 275, + "src/main/ssh/ssh-system-transport.integration.test.ts": 277, + "src/main/ssh/ssh-target-id-migration.test.ts": 46, + "src/main/ssh/ssh-target-orcad-claims.test.ts": 306, + "src/main/ssh/ssh-target-orcad-dependents.test.ts": 269, + "src/main/ssh/ssh-target-orcad-preflight.test.ts": 1582, + "src/main/ssh/ssh-target-readoption.test.ts": 52, + "src/main/ssh/ssh-tcp-forwarding-probe.test.ts": 810, + "src/main/ssh/ssh-windows-host-cells.test.ts": 39, + "src/main/ssh/ssh2-port-forward-listen.test.ts": 30, + "src/main/ssh/system-ssh-binary.test.ts": 35, + "src/main/ssh/system-ssh-dynamic-forward-process.test.ts": 240, + "src/main/ssh/system-ssh-forward-process.test.ts": 251, + "src/main/ssh/system-ssh-orphaned-forward.test.ts": 86, + "src/main/ssh/system-ssh-sftp-args.test.ts": 44, + "src/main/ssh/system-ssh-sftp-path.test.ts": 38, + "src/main/ssh/system-ssh-upload-plan-budget.test.ts": 336, + "src/main/ssh/system-ssh-windows-upload.test.ts": 343, + "src/main/ssh/system-ssh-windows-write-capabilities.test.ts": 35, + "src/main/ssh/vscode-ssh-authority.test.ts": 48, + "src/main/star-nag/service-direct-star.test.ts": 323, + "src/main/star-nag/service-force-show-races.test.ts": 322, + "src/main/star-nag/service-outcome-telemetry.test.ts": 354, + "src/main/star-nag/service-prompt-moments.test.ts": 297, + "src/main/star-nag/service-threshold-prompt.test.ts": 295, + "src/main/startup/bootstrap-fatal-exit-guard.test.ts": 25, + "src/main/startup/branch-rename-hook-structured-session.test.ts": 515, + "src/main/startup/browser-process-user-agent-ordering.test.ts": 2454, + "src/main/startup/cli-launch-redirect.test.ts": 108, + "src/main/startup/cloud-session-shutdown-flush.test.ts": 37, + "src/main/startup/codex-account-restart-resume.test.ts": 327, + "src/main/startup/codex-hook-startup-wiring.test.ts": 8111, + "src/main/startup/codex-launch-per-agent-hook-opt-out.test.ts": 175, + "src/main/startup/codex-session-resume-daemon-guard.test.ts": 139, + "src/main/startup/configure-process-dev-parent-shutdown.test.ts": 103, + "src/main/startup/configure-process-profile-state.test.ts": 99, + "src/main/startup/configure-process.test.ts": 237, + "src/main/startup/desktop-profile-instance-lock.test.ts": 245, + "src/main/startup/dev-education-suppression.test.ts": 268, + "src/main/startup/dev-instance-identity.test.ts": 32, + "src/main/startup/ensure-virtual-display.test.ts": 99, + "src/main/startup/first-window-startup-services.test.ts": 36, + "src/main/startup/gpu-fallback-marker.test.ts": 58, + "src/main/startup/gpu-fallback-switches.test.ts": 40, + "src/main/startup/gpu-lifecycle-install-dir-acl-guard.test.ts": 427, + "src/main/startup/headless-serve-ssh-registration.test.ts": 1124, + "src/main/startup/http1-compatibility-profile-state.test.ts": 66, + "src/main/startup/hydrate-shell-path.test.ts": 436, + "src/main/startup/hydrate-shell-path.windows.test.ts": 327, + "src/main/startup/legacy-worker-renderer-recovery.test.ts": 52, + "src/main/startup/linux-dev-shm-policy.test.ts": 49, + "src/main/startup/linux-keyring-backend-selection.test.ts": 40, + "src/main/startup/linux-secret-service-probe.test.ts": 47, + "src/main/startup/login-shell-environment-cache.test.ts": 113, + "src/main/startup/login-shell-environment.test.ts": 75, + "src/main/startup/main-process-error-guards.test.ts": 76, + "src/main/startup/main-process-observers-hang-breadcrumb.test.ts": 214, + "src/main/startup/main-process-quit-pending-writer.test.ts": 711, + "src/main/startup/main-process-quit-update-veto.test.ts": 384, + "src/main/startup/main-process-ready-activation-failure.test.ts": 273, + "src/main/startup/main-process-ready-identity-write.test.ts": 1211, + "src/main/startup/main-process-ready-persistence-cleanup.test.ts": 196, + "src/main/startup/main-process-ready-phase-ordering.test.ts": 196, + "src/main/startup/main-process-runtime-launch-activation.test.ts": 8361, + "src/main/startup/main-window-actions-menu-target.test.ts": 204, + "src/main/startup/main-window-actions-user-quit.test.ts": 214, + "src/main/startup/main-window-core-services.test.ts": 88, + "src/main/startup/main-window-structured-status-filter.test.ts": 227, + "src/main/startup/os-opened-document-arguments.test.ts": 235, + "src/main/startup/os-opened-document-delivery.test.ts": 190, + "src/main/startup/os-opened-document-resolution.test.ts": 211, + "src/main/startup/os-opened-documents.test.ts": 211, + "src/main/startup/profile-state-recovery-preflight.test.ts": 790, + "src/main/startup/profile-state-save-delay.test.ts": 45, + "src/main/startup/profile-state-write-failure.test.ts": 426, + "src/main/startup/renderer-heap-headroom.test.ts": 50, "src/main/startup/run-electron-vite-dev-web.test.ts": 541, - "src/main/startup/run-electron-vite-dev.test.ts": 914, - "src/main/startup/select-linux-keyring-backend.test.ts": 50, - "src/main/startup/serve-desktop-activation.test.ts": 48, - "src/main/startup/serve-mode-argv-cli-redirect-order.test.ts": 85, - "src/main/startup/serve-mode-argv.test.ts": 88, - "src/main/startup/serve-native-quit-guard.test.ts": 55, - "src/main/startup/serve-options.test.ts": 104, - "src/main/startup/serve-signal-handlers.test.ts": 53, - "src/main/startup/serve-user-data-path-guard.test.ts": 165, - "src/main/startup/single-instance-lock-exit.electron.test.ts": 402, - "src/main/startup/single-instance-lock.test.ts": 80, - "src/main/startup/skill-share-deep-link-state.test.ts": 52, - "src/main/startup/startup-diagnostics.test.ts": 51, - "src/main/startup/window-all-closed-quit-policy.test.ts": 48, - "src/main/startup/windows-app-data-path.test.ts": 53, - "src/main/startup/windows-desktop-shell-path-startup.test.ts": 99, - "src/main/startup/windows-install-dir-acl-probe.test.ts": 144, - "src/main/startup/windows-install-dir-acl-recovery.test.ts": 578, - "src/main/startup/windows-install-dir-acl-repair.win32.test.ts": 124, - "src/main/startup/windows-install-dir-package-acl-repair.test.ts": 153, - "src/main/startup/windows-shell-path-hydration.test.ts": 388, - "src/main/startup/windows-shell-path-ownership.test.ts": 56, - "src/main/startup/windows-shell-path-restoration.windows.test.ts": 48, - "src/main/startup/windows-user-data-acl.test.ts": 73, - "src/main/startup/wsl-cli-reconciliation-startup-barrier.test.ts": 58, - "src/main/stats/agent-session-transition-recorder.test.ts": 604, - "src/main/stats/collector-async-save.test.ts": 513, - "src/main/synthetic-title-frame-routing.test.ts": 42, - "src/main/synthetic-title-spinner.test.ts": 51, - "src/main/synthetic-title-visibility.test.ts": 37, - "src/main/system-fonts.test.ts": 80, - "src/main/system-power-lifecycle.test.ts": 43, - "src/main/system-resume-broadcast.test.ts": 68, - "src/main/telemetry/burst-cap.test.ts": 343, - "src/main/telemetry/classify-error.test.ts": 48, - "src/main/telemetry/client-lifecycle.test.ts": 359, - "src/main/telemetry/client.test.ts": 344, - "src/main/telemetry/cohort-classifier.test.ts": 42, - "src/main/telemetry/consent.test.ts": 71, - "src/main/telemetry/install-id.test.ts": 79, - "src/main/telemetry/onboarding-cohort-classifier.test.ts": 70, - "src/main/telemetry/onboarding-feature-setup-validator.test.ts": 277, - "src/main/telemetry/validator-warn-cache.test.ts": 290, - "src/main/telemetry/validator.test.ts": 364, - "src/main/terminal-history-async-delete.test.ts": 402, - "src/main/terminal-history-gc-fs-call-count.test.ts": 176, - "src/main/terminal-history-gc.test.ts": 821, - "src/main/terminal-history-tombstone-retry.test.ts": 764, - "src/main/terminal-history-tombstone-scan-cost.test.ts": 319, - "src/main/terminal-history.test.ts": 153, - "src/main/text-generation/agent-failure-output.test.ts": 57, - "src/main/text-generation/command-environment-process.test.ts": 915, - "src/main/text-generation/command-environment-wsl.test.ts": 83, - "src/main/text-generation/commit-message-agent-environment.test.ts": 73, - "src/main/text-generation/commit-message-command-backslash-mode.test.ts": 499, - "src/main/text-generation/commit-message-text-generation-branch-name.test.ts": 498, - "src/main/text-generation/commit-message-text-generation-cancellation.test.ts": 1070, - "src/main/text-generation/commit-message-text-generation-failure-sanitization.test.ts": 406, - "src/main/text-generation/commit-message-text-generation-generated-output.test.ts": 426, - "src/main/text-generation/commit-message-text-generation-linked-issue.test.ts": 401, - "src/main/text-generation/commit-message-text-generation-local-subprocess.test.ts": 463, - "src/main/text-generation/commit-message-text-generation-model-discovery.test.ts": 504, - "src/main/text-generation/commit-message-text-generation-regression.test.ts": 463, - "src/main/text-generation/commit-message-text-generation-remote-execution.test.ts": 418, - "src/main/text-generation/commit-message-text-generation-settings.test.ts": 451, - "src/main/text-generation/conversation-name-generation-request.test.ts": 355, - "src/main/text-generation/omp-model-discovery-policy.test.ts": 251, - "src/main/text-generation/opencode-generation-events.test.ts": 351, - "src/main/text-generation/pull-request-context-errors.test.ts": 71, - "src/main/text-generation/pull-request-context.test.ts": 89, - "src/main/text-generation/source-control-agent-launch.test.ts": 1915, - "src/main/text-generation/source-control-local-process.test.ts": 1522, - "src/main/tray/system-tray.test.ts": 178, - "src/main/tray/tray-attention-icon.test.ts": 63, - "src/main/tray/tray-dev-badge.test.ts": 56, - "src/main/update-install-exit-watchdog.test.ts": 77, - "src/main/updater-agent-state-rules-release.test.ts": 103, - "src/main/updater-changelog.test.ts": 63, - "src/main/updater-events.test.ts": 227, - "src/main/updater-lifecycle-diagnostics.test.ts": 57, - "src/main/updater-linux-package-recovery-actions.test.ts": 4060, + "src/main/startup/run-electron-vite-dev.test.ts": 916, + "src/main/startup/select-linux-keyring-backend.test.ts": 53, + "src/main/startup/serve-desktop-activation.test.ts": 33, + "src/main/startup/serve-mode-argv-cli-redirect-order.test.ts": 73, + "src/main/startup/serve-mode-argv.test.ts": 69, + "src/main/startup/serve-native-quit-guard.test.ts": 47, + "src/main/startup/serve-options.test.ts": 66, + "src/main/startup/serve-signal-handlers.test.ts": 42, + "src/main/startup/serve-user-data-path-guard.test.ts": 91, + "src/main/startup/single-instance-lock-exit.electron.test.ts": 306, + "src/main/startup/single-instance-lock.test.ts": 58, + "src/main/startup/skill-share-deep-link-state.test.ts": 43, + "src/main/startup/startup-diagnostics.test.ts": 34, + "src/main/startup/window-all-closed-quit-policy.test.ts": 52, + "src/main/startup/windows-app-data-path.test.ts": 42, + "src/main/startup/windows-desktop-shell-path-startup.test.ts": 40, + "src/main/startup/windows-install-dir-acl-probe.test.ts": 102, + "src/main/startup/windows-install-dir-acl-recovery.test.ts": 581, + "src/main/startup/windows-install-dir-acl-repair.win32.test.ts": 73, + "src/main/startup/windows-install-dir-package-acl-grant.test.ts": 66, + "src/main/startup/windows-install-dir-package-acl-repair.test.ts": 124, + "src/main/startup/windows-shell-path-hydration.test.ts": 381, + "src/main/startup/windows-shell-path-ownership.test.ts": 31, + "src/main/startup/windows-shell-path-restoration.windows.test.ts": 52, + "src/main/startup/windows-user-data-acl.test.ts": 58, + "src/main/startup/wsl-cli-reconciliation-startup-barrier.test.ts": 56, + "src/main/startup/wsl-orcad-account-env-wiring.test.ts": 402, + "src/main/stats/agent-session-transition-recorder.test.ts": 757, + "src/main/stats/collector-async-save.test.ts": 554, + "src/main/synthetic-title-frame-routing.test.ts": 37, + "src/main/synthetic-title-spinner.test.ts": 34, + "src/main/synthetic-title-visibility.test.ts": 28, + "src/main/system-fonts.test.ts": 58, + "src/main/system-power-lifecycle.test.ts": 34, + "src/main/system-resume-broadcast.test.ts": 70, + "src/main/telemetry/burst-cap.test.ts": 322, + "src/main/telemetry/classify-error.test.ts": 32, + "src/main/telemetry/client-lifecycle.test.ts": 445, + "src/main/telemetry/client.test.ts": 294, + "src/main/telemetry/cohort-classifier.test.ts": 34, + "src/main/telemetry/consent.test.ts": 49, + "src/main/telemetry/install-id.test.ts": 43, + "src/main/telemetry/onboarding-cohort-classifier.test.ts": 35, + "src/main/telemetry/onboarding-feature-setup-validator.test.ts": 273, + "src/main/telemetry/validator-warn-cache.test.ts": 355, + "src/main/telemetry/validator.test.ts": 517, + "src/main/terminal-history-async-delete.test.ts": 288, + "src/main/terminal-history-gc-fs-call-count.test.ts": 152, + "src/main/terminal-history-gc.test.ts": 692, + "src/main/terminal-history-tombstone-retry.test.ts": 676, + "src/main/terminal-history-tombstone-scan-cost.test.ts": 294, + "src/main/terminal-history.test.ts": 106, + "src/main/text-generation/agent-failure-output.test.ts": 41, + "src/main/text-generation/command-environment-process.test.ts": 875, + "src/main/text-generation/command-environment-wsl.test.ts": 100, + "src/main/text-generation/commit-message-agent-environment.test.ts": 87, + "src/main/text-generation/commit-message-command-backslash-mode.test.ts": 342, + "src/main/text-generation/commit-message-text-generation-branch-name.test.ts": 418, + "src/main/text-generation/commit-message-text-generation-cancellation.test.ts": 1109, + "src/main/text-generation/commit-message-text-generation-failure-sanitization.test.ts": 371, + "src/main/text-generation/commit-message-text-generation-generated-output.test.ts": 393, + "src/main/text-generation/commit-message-text-generation-linked-issue.test.ts": 405, + "src/main/text-generation/commit-message-text-generation-local-subprocess.test.ts": 433, + "src/main/text-generation/commit-message-text-generation-model-discovery.test.ts": 476, + "src/main/text-generation/commit-message-text-generation-regression.test.ts": 442, + "src/main/text-generation/commit-message-text-generation-remote-execution.test.ts": 446, + "src/main/text-generation/commit-message-text-generation-settings.test.ts": 419, + "src/main/text-generation/conversation-name-generation-request.test.ts": 439, + "src/main/text-generation/jira-issue-summary-requests.test.ts": 259, + "src/main/text-generation/jira-issue-summary-text-generation.test.ts": 375, + "src/main/text-generation/omp-model-discovery-policy.test.ts": 242, + "src/main/text-generation/opencode-generation-events.test.ts": 336, + "src/main/text-generation/pull-request-context-errors.test.ts": 61, + "src/main/text-generation/pull-request-context.test.ts": 100, + "src/main/text-generation/source-control-agent-launch.test.ts": 1928, + "src/main/text-generation/source-control-local-process.test.ts": 1508, + "src/main/tray/system-tray.test.ts": 288, + "src/main/tray/tray-attention-icon.test.ts": 40, + "src/main/tray/tray-dev-badge.test.ts": 40, + "src/main/update-install-exit-watchdog.test.ts": 60, + "src/main/updater-agent-state-rules-release.test.ts": 76, + "src/main/updater-changelog.test.ts": 56, + "src/main/updater-events.test.ts": 250, + "src/main/updater-lifecycle-diagnostics.test.ts": 44, + "src/main/updater-linux-package-recovery-actions.test.ts": 3886, "src/main/updater-mac-quit-guard.test.ts": 40, - "src/main/updater-nudge.test.ts": 247, + "src/main/updater-nudge.test.ts": 234, "src/main/updater-prerelease-feed-readiness.test.ts": 156, - "src/main/updater-prerelease-feed.test.ts": 237, - "src/main/updater-release-api-token.test.ts": 63, - "src/main/updater-release-build-cache.test.ts": 58, - "src/main/updater-release-builds.test.ts": 95, - "src/main/updater-sqlite-profile-downgrade.test.ts": 1672, - "src/main/updater.build-channel-selection.test.ts": 3459, - "src/main/updater.check-failure.test.ts": 3131, - "src/main/updater.check-preflight.test.ts": 4281, - "src/main/updater.check-settlement.test.ts": 3437, - "src/main/updater.fallback.test.ts": 54, - "src/main/updater.feed-attempt-lifetime.test.ts": 2886, - "src/main/updater.headless-serve-install.test.ts": 431, - "src/main/updater.install-failure-cause.test.ts": 3737, - "src/main/updater.linux-externally-managed.test.ts": 2482, - "src/main/updater.linux-root-package-install.test.ts": 2779, - "src/main/updater.mac-install.test.ts": 55, - "src/main/updater.nudge-campaign.test.ts": 2950, - "src/main/updater.prerelease-fallback.test.ts": 4430, - "src/main/updater.publishing-window-feed.test.ts": 4333, - "src/main/updater.quit-and-install.test.ts": 4367, - "src/main/updater.startup-scheduling.test.ts": 3238, - "src/main/updater/rollout-flags.test.ts": 338, - "src/main/usage-worktree-canonicalizer.test.ts": 47, - "src/main/usage-worktree-metadata.test.ts": 60, - "src/main/usage/agent-token-usage-reporter.test.ts": 802, - "src/main/usage/agent-token-usage.test.ts": 46, - "src/main/usage/analytics-session-id-store.test.ts": 247, - "src/main/usage/highest-usage-key.test.ts": 68, - "src/main/usage/jsonl-file-reader.test.ts": 87, - "src/main/usage/jsonl-file-snapshot.test.ts": 73, - "src/main/usage/jsonl-line-offsets.test.ts": 67, - "src/main/usage/usage-breakdown-scaling.test.ts": 93, - "src/main/usage/usage-calendar-range.test.ts": 59, - "src/main/usage/usage-provider-store-initialization.test.ts": 734, - "src/main/usage/usage-provider-store-lifecycle.test.ts": 751, - "src/main/usage/usage-scan-worker-cache-cleanup.test.ts": 130, - "src/main/usage/usage-scan-worker-client.test.ts": 98, - "src/main/usage/usage-scan-worker-event-loop.test.ts": 2406, - "src/main/usage/usage-source-cache-file.test.ts": 77, - "src/main/usage/usage-worktree-refs.test.ts": 43, - "src/main/usage/usage-worktree-resolver.test.ts": 125, - "src/main/warp-themes/discovery.test.ts": 76, - "src/main/warp-themes/index.test.ts": 685, - "src/main/warp-themes/manual-warp-theme-files.test.ts": 61, - "src/main/warp-themes/parser-runner.test.ts": 55, - "src/main/warp-themes/parser.test.ts": 108, - "src/main/warp-themes/theme-file-scanner.test.ts": 70, - "src/main/win32-utils-async-acl.test.ts": 56, - "src/main/win32-utils.test.ts": 70, - "src/main/window/attach-main-window-services.test.ts": 1420, - "src/main/window/clipboard-agent-session-attachment-paste.test.ts": 438, - "src/main/window/clipboard-copied-file-paths.test.ts": 75, - "src/main/window/clipboard-dashboard-popout-access.test.ts": 1333, - "src/main/window/clipboard-file-copy.test.ts": 68, - "src/main/window/clipboard-image-source.test.ts": 60, - "src/main/window/clipboard-image-temp-file.test.ts": 61, - "src/main/window/clipboard-image-thumbnail.test.ts": 44, - "src/main/window/clipboard-ipc-handlers.test.ts": 477, - "src/main/window/clipboard-remote-file-cleanup.bench.test.ts": 46, - "src/main/window/clipboard-remote-file-copy.test.ts": 82, - "src/main/window/clipboard-remote-file-staging-fs.test.ts": 59, - "src/main/window/clipboard-remote-file-staging.test.ts": 107, - "src/main/window/clipboard-runtime-owned-ssh-paste.test.ts": 341, - "src/main/window/clipboard-text-write-verify.test.ts": 62, - "src/main/window/clipboard-windows-image-file.test.ts": 91, - "src/main/window/createMainWindow-close-confirmation.test.ts": 2132, - "src/main/window/createMainWindow-low-commit-oom-recovery.test.ts": 1748, - "src/main/window/createMainWindow-markdown-editor-focus.test.ts": 1897, - "src/main/window/createMainWindow-recovery-reload-watchdog.test.ts": 2346, - "src/main/window/createMainWindow-renderer-crash-recovery.test.ts": 2303, - "src/main/window/createMainWindow-renderer-launch-failure.test.ts": 1598, - "src/main/window/createMainWindow-startup-reveal.test.ts": 2157, - "src/main/window/createMainWindow-system-resume-relay.test.ts": 2342, - "src/main/window/createMainWindow-terminal-focus-shortcuts.test.ts": 1846, - "src/main/window/createMainWindow-tray-minimize-close.test.ts": 1969, - "src/main/window/createMainWindow-user-quit-close.test.ts": 2017, - "src/main/window/createMainWindow-zoom-and-tab-switch-shortcuts.test.ts": 1751, - "src/main/window/createMainWindow.test.ts": 2184, - "src/main/window/darwin-user-temp-dir.test.ts": 56, - "src/main/window/dashboard-popout-window.test.ts": 122, - "src/main/window/dragged-temp-file-copy.test.ts": 189, - "src/main/window/dropped-path-preparation-ipc.test.ts": 103, - "src/main/window/dropped-path-preparation.test.ts": 89, - "src/main/window/editable-context-menu.test.ts": 80, - "src/main/window/focus-existing-window.test.ts": 70, - "src/main/window/foreground-activation-policy.test.ts": 59, - "src/main/window/history-gc-worktree-ids.test.ts": 311, - "src/main/window/host-frame-navigation-guard.test.ts": 237, - "src/main/window/macos-app-activation.test.ts": 67, - "src/main/window/macos-tahoe-release.test.ts": 55, - "src/main/window/main-window-state-lifecycle.test.ts": 54, - "src/main/window/main-window-updater-deferred-setup.test.ts": 146, - "src/main/window/main-window-visibility.test.ts": 56, - "src/main/window/main-window-webview-security.test.ts": 344, - "src/main/window/mobile-markdown-request-relay.test.ts": 61, - "src/main/window/native-chat-paste-files.test.ts": 497, - "src/main/window/privileged-window-navigation.test.ts": 266, - "src/main/window/renderer-document-navigation.test.ts": 60, - "src/main/window/renderer-launch-failure-backoff.test.ts": 37, - "src/main/window/renderer-launch-failure-probe.spawn.test.ts": 71, - "src/main/window/renderer-launch-failure-probe.test.ts": 69, - "src/main/window/renderer-publication-throttle.test.ts": 55, - "src/main/window/renderer-recovery-prompt.test.ts": 109, - "src/main/window/renderer-recovery-reload-watchdog.test.ts": 76, - "src/main/window/runtime-renderer-notification-sender.test.ts": 47, - "src/main/window/runtime-window-lifecycle-file-open.test.ts": 57, - "src/main/window/session-tab-close-request-relay.test.ts": 74, - "src/main/window/terminal-tab-close-request-relay.test.ts": 63, - "src/main/window/updater-package-recovery-ipc.test.ts": 7052, - "src/main/window/window-close-decision.test.ts": 52, - "src/main/windows-live-tree-kill.win32.test.ts": 60, - "src/main/windows-process-tree-kill.test.ts": 81, - "src/main/windows-pty-root-identity.test.ts": 129, - "src/main/windows-registry-addon.test.ts": 41, - "src/main/windows/windows-command-line-recovery-health.test.ts": 50, - "src/main/windows/windows-host-job.win32.test.ts": 44, - "src/main/windows/windows-msys-job.win32.test.ts": 60, - "src/main/windows/windows-process-creation-time.test.ts": 80, - "src/main/windows/windows-process-table-cim-scan.test.ts": 63, - "src/main/windows/windows-process-table-native-addon.win32.test.ts": 60, - "src/main/windows/windows-process-table.test.ts": 177, - "src/main/windows/windows-process-tree-command-line-patch.test.ts": 61, - "src/main/windows/windows-pty-job.test.ts": 74, - "src/main/windows/windows-pty-job.win32.test.ts": 71, - "src/main/worker-thread-request-queue.test.ts": 94, - "src/main/workspace-cleanup-removal-snapshot-prune.test.ts": 78, - "src/main/workspace-cleanup-scan-snapshot.test.ts": 166, - "src/main/workspace-create-event-fields.test.ts": 300, - "src/main/workspace-snapshot-pruning.bench.test.ts": 53, - "src/main/workspace-space-analysis-capacity.test.ts": 475, - "src/main/workspace-space-analysis-du-timeout.test.ts": 587, - "src/main/workspace-space-analysis-snapshot.test.ts": 130, - "src/main/workspace-space-analysis.test.ts": 652, - "src/main/workspace-space-repo-scan.test.ts": 388, - "src/main/workspace-space-scan-control.test.ts": 55, - "src/main/worktree-background-removal-records.test.ts": 417, - "src/main/worktree-background-removal.test.ts": 346, - "src/main/worktree-create-base-prefetch.test.ts": 308, - "src/main/worktree-create-base.test.ts": 50, - "src/main/worktree-create-candidates.test.ts": 49, - "src/main/worktree-create-concurrency.test.ts": 67, - "src/main/worktree-create-execution-host-route.test.ts": 385, - "src/main/worktree-create-preparation-activity.test.ts": 43, - "src/main/worktree-create-preparation-cancellation.test.ts": 353, - "src/main/worktree-create-preparation-claim.test.ts": 55, - "src/main/worktree-create-preparation-contention.test.ts": 363, - "src/main/worktree-create-preparation-timing.test.ts": 316, - "src/main/worktree-create-preparation-wsl-root.test.ts": 348, - "src/main/worktree-create-preparation.test.ts": 299, - "src/main/worktree-create-timing.test.ts": 119, - "src/main/worktree-failed-removal.test.ts": 400, - "src/main/worktree-identity-persistence.test.ts": 1751, - "src/main/worktree-lineage-pruning.test.ts": 76, - "src/main/worktree-metadata-ownership.test.ts": 46, - "src/main/worktree-name-retirement.test.ts": 175, - "src/main/worktree-preparation-discard-retry.test.ts": 245, - "src/main/worktree-preparation-fetch-barrier.test.ts": 343, - "src/main/worktree-preparation-tip-refresh.test.ts": 500, - "src/main/worktree-prunable-git-file.test.ts": 88, - "src/main/worktree-removal-authority.test.ts": 104, - "src/main/worktree-removal-close-records.test.ts": 1393, - "src/main/worktree-removal-execution-host-route.test.ts": 397, - "src/main/worktree-removal-home-guard.test.ts": 94, - "src/main/worktree-removal-repo-owner.test.ts": 49, - "src/main/worktree-removal-safety.test.ts": 177, - "src/main/worktree-removal-session-partition-fencing.test.ts": 1465, - "src/main/worktree-retirement-backfill-scan.test.ts": 76, - "src/main/worktree-retirement-backfill-stall.test.ts": 141, - "src/main/worktree-retirement-discovery-wsl.test.ts": 184, - "src/main/worktree-retirement-discovery.test.ts": 174, - "src/main/worktree-retirement-namespace.test.ts": 84, - "src/main/worktree-root-preparation.test.ts": 116, - "src/main/worktree-trash.test.ts": 138, - "src/main/wsl-approved-root-race.wsl.test.ts": 65, - "src/main/wsl-availability-missing-kernel.test.ts": 68, - "src/main/wsl-bash-command.test.ts": 47, - "src/main/wsl-distro-list-output.test.ts": 40, - "src/main/wsl-distro-list-single-flight.test.ts": 103, - "src/main/wsl-fish-history-cleanup.test.ts": 94, - "src/main/wsl-interop-spawn-directory.test.ts": 54, - "src/main/wsl-managed-cli-path-restore.test.ts": 71, - "src/main/wsl-running-distros.test.ts": 217, - "src/main/wsl-running-path-filter.test.ts": 52, - "src/main/wsl-unc-delete-symlink-repro.test.ts": 78, - "src/main/wsl-unc-delete.test.ts": 92, - "src/main/wsl-unc-delete.wsl.test.ts": 71, - "src/main/wsl.test.ts": 152, - "src/main/wsl/wsl-executable-path.win32.test.ts": 44, - "src/main/wsl/wsl-guest-environment.test.ts": 109, - "src/main/wsl/wsl-pinned-runtime.test.ts": 413, - "src/main/wsl/wsl-runner.test.ts": 86, - "src/main/wsl/wsl-runner.wsl.test.ts": 71, - "src/main/wsl/wsl-w1-w3-contract.test.ts": 79, - "src/main/zcode/hook-service.test.ts": 130, - "src/main/zcode/interactive-capability.live.test.ts": 61, - "src/main/zcode/zcode-plan-api-key-store.test.ts": 72, - "src/packages/process-host/src/bounded-output-sink.test.ts": 98, - "src/packages/process-host/src/close-process-registry.test.ts": 51, - "src/packages/process-host/src/growing-byte-buffer.test.ts": 119, - "src/packages/process-host/src/posix-lsof-probe.test.ts": 4335, - "src/packages/process-host/src/process-tree-termination.test.ts": 86, - "src/packages/process-host/src/retryable-process-exit-proof.test.ts": 53, - "src/packages/process-host/src/run-process-bytes.test.ts": 2227, - "src/packages/process-host/src/run-process-termination-failure.test.ts": 85, - "src/packages/process-host/src/run-process.test.ts": 7249, - "src/packages/process-host/src/spawn-observer.test.ts": 181, - "src/packages/process-host/src/spawn-process-stdio.test.ts": 247, - "src/packages/process-host/src/windows-command-line.test.ts": 81, - "src/preload/api/notification-sound-playback.test.ts": 52, - "src/preload/api/platform-bridge.test.ts": 71, - "src/preload/app-restart-checkpoint-routing.test.ts": 1800, - "src/preload/browser-client-page-renderer-requests.test.ts": 262, - "src/preload/browser-find-subscriptions.test.ts": 58, - "src/preload/browser-window-close.test.ts": 45, - "src/preload/close-active-tab-payload-admission.test.ts": 54, - "src/preload/doc-preview-link-interception.test.ts": 213, - "src/preload/opencode-go-credentials.test.ts": 43, - "src/preload/preload-file-drop-absence.test.ts": 293, - "src/preload/pty-snapshot-capability-ipc.test.ts": 992, - "src/preload/renderer-heap-statistics-reader.test.ts": 58, - "src/preload/renderer-process-memory-reader.test.ts": 56, - "src/preload/renderer-restart-wiring.test.ts": 84, - "src/preload/runtime-environment-subscriptions.test.ts": 73, - "src/preload/ssh-authority-forwarding.test.ts": 1025, - "src/preload/updater-package-recovery.test.ts": 867, - "src/preload/usage-provider-api.test.ts": 52, - "src/renderer/src/app-shell/app-command-handlers-child-workspaces.test.ts": 1064, - "src/renderer/src/app-shell/app-command-handlers-tab-rename.test.ts": 1261, - "src/renderer/src/app-shell/app-command-handlers-workspace-delete.test.ts": 646, - "src/renderer/src/app-shell/app-root-surface-settings.test.tsx": 455, - "src/renderer/src/app-shell/app-window-chrome.test.ts": 37, - "src/renderer/src/app-shell/reconcile-hydrated-workspace-tab-models.test.ts": 40, - "src/renderer/src/app-shell/shutdown-checkpoint-persist.test.ts": 62, - "src/renderer/src/app-shell/shutdown-checkpoint-restart-lifecycle.test.ts": 171, - "src/renderer/src/app-shell/startup-actions-selector.test.ts": 78, - "src/renderer/src/app-shell/use-document-appearance.test.tsx": 1472, - "src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx": 454, - "src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx": 291, - "src/renderer/src/app-shell/use-terminal-viewer-color-publication.test.tsx": 1708, - "src/renderer/src/app-shell/use-visible-hosted-review-refresh.test.tsx": 338, - "src/renderer/src/app-shell/visible-review-reporting.test.ts": 1357, - "src/renderer/src/app-shell/window-visibility-actions-selector.test.ts": 228, - "src/renderer/src/app-shell/workspace-view-cross-client-sync.test.tsx": 927, - "src/renderer/src/attention/agent-attention-policy.test.ts": 58, - "src/renderer/src/attention/notification-sources.test.ts": 1870, - "src/renderer/src/components/AgentStateDot.test.ts": 179, - "src/renderer/src/components/AgentWorkingSpinner.test.tsx": 249, - "src/renderer/src/components/ButtonKeyHint.test.tsx": 348, - "src/renderer/src/components/LinuxPackageInstallRecoveryCard.test.tsx": 868, - "src/renderer/src/components/NativeChatResumeOnRestartGroups.selection.test.tsx": 2230, - "src/renderer/src/components/NativeChatResumeOnRestartGroups.test.tsx": 2007, - "src/renderer/src/components/NativeChatResumeOnRestartModal.list.test.tsx": 2572, - "src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx": 3280, - "src/renderer/src/components/NewWorkspaceComposerCard.file-drop.test.tsx": 884, - "src/renderer/src/components/NewWorkspaceComposerCard.quick-file-drop.test.tsx": 3372, - "src/renderer/src/components/NewWorkspaceComposerCard.set-location-warm.test.tsx": 1056, - "src/renderer/src/components/NewWorkspaceComposerCard.set-location.test.tsx": 1064, - "src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx": 929, - "src/renderer/src/components/NewWorkspaceComposerCard.test.tsx": 1755, - "src/renderer/src/components/QuickOpen.mount-gating.test.tsx": 1998, - "src/renderer/src/components/QuickOpen.selection.test.tsx": 720, - "src/renderer/src/components/SelectedTextCopyMenu.test.tsx": 343, - "src/renderer/src/components/StarNagCard.test.tsx": 322, - "src/renderer/src/components/StateIndicatorTooltip.test.tsx": 76, - "src/renderer/src/components/TerminalSearch.test.tsx": 518, - "src/renderer/src/components/TerminalTitlebarTabs.test.tsx": 222, - "src/renderer/src/components/TerminalWorkbenchContainer.test.tsx": 293, - "src/renderer/src/components/UpdateCard.error-card.test.tsx": 1835, - "src/renderer/src/components/UpdateCard.test.ts": 1484, - "src/renderer/src/components/WorktreeBaseFallbackDialog.test.tsx": 1751, - "src/renderer/src/components/WorktreeJumpPalette.linear-url.test.tsx": 3680, - "src/renderer/src/components/WorktreeJumpPalette.mount-gating.test.tsx": 2974, - "src/renderer/src/components/WorktreeJumpPalette.recent-tabs.behavior.test.tsx": 3247, - "src/renderer/src/components/WorktreeJumpPalette.recent-tabs.test.tsx": 5538, - "src/renderer/src/components/WorktreeJumpPalette.test.tsx": 3096, - "src/renderer/src/components/activity/ActivityPrototypePage.filter-focus-shortcut.test.ts": 54, - "src/renderer/src/components/activity/ActivityPrototypePage.test.ts": 443, - "src/renderer/src/components/activity/ActivityPrototypePage.thread-grouping.test.ts": 1500, - "src/renderer/src/components/activity/ActivityThreadOptionsMenu.test.tsx": 2356, - "src/renderer/src/components/activity/activity-answered-ask-timeline.test.tsx": 1673, - "src/renderer/src/components/activity/activity-auto-mark-read-loop.react185.test.tsx": 3249, - "src/renderer/src/components/activity/activity-clear-completed-hmr.test.ts": 567, - "src/renderer/src/components/activity/activity-clear-completed-state-lifetime.test.ts": 2446, - "src/renderer/src/components/activity/activity-clear-completed.test.ts": 406, - "src/renderer/src/components/activity/activity-event-builder.identity-reuse.test.ts": 438, - "src/renderer/src/components/activity/activity-portal-churn-budget.test.ts": 47, - "src/renderer/src/components/activity/activity-portal-readiness-decay.react185.test.tsx": 200, - "src/renderer/src/components/activity/activity-portal-readiness-loop.react185.test.tsx": 372, - "src/renderer/src/components/activity/activity-portal-readiness-oscillation.test.ts": 62, - "src/renderer/src/components/activity/activity-portal-readiness-subscription-churn.react185.test.tsx": 303, - "src/renderer/src/components/activity/activity-portal-thread-reconciliation.test.ts": 55, - "src/renderer/src/components/activity/activity-scope-filter.test.ts": 588, - "src/renderer/src/components/activity/activity-search-query-work-budget.test.tsx": 1691, - "src/renderer/src/components/activity/activity-terminal-portal-publication-loop.react185.test.tsx": 169, - "src/renderer/src/components/activity/activity-terminal-portal.test.tsx": 162, - "src/renderer/src/components/activity/activity-thread-actions.test.ts": 739, - "src/renderer/src/components/activity/activity-thread-context-menu.test.tsx": 2485, - "src/renderer/src/components/activity/activity-thread-grouping.status-order.test.ts": 435, - "src/renderer/src/components/activity/activity-thread-hover-card.test.tsx": 3220, - "src/renderer/src/components/activity/activity-thread-list-pane-collapsible.test.tsx": 3450, - "src/renderer/src/components/activity/activity-thread-list-pane.multi-select.test.tsx": 3049, - "src/renderer/src/components/activity/activity-thread-list-pane.virtualization.test.tsx": 3728, - "src/renderer/src/components/activity/dev-activity-fixture.test.ts": 62, - "src/renderer/src/components/activity/event-time-clock-refresh.test.tsx": 1553, - "src/renderer/src/components/activity/use-activity-thread-action-bindings.test.tsx": 1820, - "src/renderer/src/components/activity/useActivityUnreadCount.freshness.test.tsx": 1645, - "src/renderer/src/components/activity/useActivityUnreadCount.test.ts": 1635, - "src/renderer/src/components/agent-child-row-parity.test.tsx": 641, - "src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.test.tsx": 376, - "src/renderer/src/components/agent-session-continuation/agent-session-continuation-selection.test.ts": 55, - "src/renderer/src/components/agent/AgentCombobox.test.tsx": 1407, - "src/renderer/src/components/agent/AgentSettingsDialog.test.tsx": 555, - "src/renderer/src/components/agent/agent-combobox-command-state.test.ts": 45, - "src/renderer/src/components/artifacts/ArtifactCollection.test.tsx": 1101, - "src/renderer/src/components/artifacts/ArtifactPreview.test.tsx": 381, - "src/renderer/src/components/artifacts/ArtifactPublishButton.test.tsx": 616, - "src/renderer/src/components/artifacts/ArtifactsPage.test.tsx": 1568, - "src/renderer/src/components/artifacts/artifact-display-labels.test.ts": 88, - "src/renderer/src/components/artifacts/artifact-list-search.test.ts": 104, - "src/renderer/src/components/artifacts/artifact-list-windowing.test.tsx": 3096, - "src/renderer/src/components/artifacts/artifact-publish-flow.test.ts": 125, - "src/renderer/src/components/artifacts/artifact-published-link-client.test.ts": 51, - "src/renderer/src/components/automations/AutomationDestinationField.test.tsx": 622, - "src/renderer/src/components/automations/AutomationDetail.host.test.tsx": 751, - "src/renderer/src/components/automations/AutomationDetail.test.tsx": 681, - "src/renderer/src/components/automations/AutomationEditorPromptEditor.test.tsx": 318, - "src/renderer/src/components/automations/AutomationEditorSettingsSidebar.test.tsx": 655, - "src/renderer/src/components/automations/AutomationHostBadges.test.tsx": 332, - "src/renderer/src/components/automations/AutomationHostFilterNotice.test.tsx": 379, - "src/renderer/src/components/automations/AutomationListEmptyView.test.tsx": 244, - "src/renderer/src/components/automations/AutomationListHostGroups.test.tsx": 579, - "src/renderer/src/components/automations/AutomationListLocalRows.test.tsx": 1000, - "src/renderer/src/components/automations/AutomationListSearchField.test.tsx": 345, - "src/renderer/src/components/automations/AutomationListTableHeader.test.tsx": 448, - "src/renderer/src/components/automations/AutomationOwnerConflictNotice.test.tsx": 476, - "src/renderer/src/components/automations/AutomationPromptDisclosure.test.tsx": 1051, - "src/renderer/src/components/automations/AutomationRunHistory.test.tsx": 1017, - "src/renderer/src/components/automations/AutomationRunsDashboardSurface.test.tsx": 351, - "src/renderer/src/components/automations/AutomationRunsTable.test.tsx": 540, - "src/renderer/src/components/automations/AutomationSchedulePicker.render.test.tsx": 962, - "src/renderer/src/components/automations/AutomationSchedulePicker.test.ts": 875, - "src/renderer/src/components/automations/AutomationTimeField.test.tsx": 593, - "src/renderer/src/components/automations/AutomationsDetailPane.run-count.test.tsx": 2326, - "src/renderer/src/components/automations/AutomationsDetailPane.test.tsx": 2585, - "src/renderer/src/components/automations/AutomationsListPanel.test.tsx": 1015, - "src/renderer/src/components/automations/AutomationsPage.create-destination.test.tsx": 2412, - "src/renderer/src/components/automations/AutomationsPage.cross-authority-actions.test.tsx": 2046, - "src/renderer/src/components/automations/AutomationsPage.escape-precedence.test.tsx": 1344, - "src/renderer/src/components/automations/AutomationsPage.external-scope.test.tsx": 1416, - "src/renderer/src/components/automations/AutomationsPage.notice-recovery.test.tsx": 2091, - "src/renderer/src/components/automations/AutomationsPage.refresh-selection.test.tsx": 1405, - "src/renderer/src/components/automations/AutomationsPage.run-visibility.test.tsx": 1871, - "src/renderer/src/components/automations/AutomationsPage.save-visibility.test.tsx": 1326, - "src/renderer/src/components/automations/AutomationsPage.strict-mode-save-visibility.test.tsx": 2040, - "src/renderer/src/components/automations/AutomationsPage.test.tsx": 2751, - "src/renderer/src/components/automations/AutomationsPageBreadcrumb.test.tsx": 304, - "src/renderer/src/components/automations/ExternalAutomationManagers.test.tsx": 897, - "src/renderer/src/components/automations/automation-authority-identity.test.ts": 54, - "src/renderer/src/components/automations/automation-captured-owner.test.ts": 177, - "src/renderer/src/components/automations/automation-create-destination.test.ts": 115, - "src/renderer/src/components/automations/automation-create-projects.test.ts": 45, - "src/renderer/src/components/automations/automation-detail-tab-navigation.test.ts": 153, - "src/renderer/src/components/automations/automation-editor-prompt-options.test.ts": 44, - "src/renderer/src/components/automations/automation-external-target-match.test.ts": 56, - "src/renderer/src/components/automations/automation-extra-agent-args-capability.test.ts": 115, - "src/renderer/src/components/automations/automation-host-cache-controller.test.ts": 185, - "src/renderer/src/components/automations/automation-host-cache-health.test.ts": 63, - "src/renderer/src/components/automations/automation-host-cache.test.ts": 71, - "src/renderer/src/components/automations/automation-host-catalog-generation.test.ts": 113, - "src/renderer/src/components/automations/automation-host-catalog-order.test.ts": 57, - "src/renderer/src/components/automations/automation-host-catalog-source.test.ts": 87, - "src/renderer/src/components/automations/automation-host-catalog.test.ts": 97, - "src/renderer/src/components/automations/automation-host-client.test.ts": 97, - "src/renderer/src/components/automations/automation-host-detail-display.test.ts": 54, - "src/renderer/src/components/automations/automation-host-diagnostics.test.ts": 206, - "src/renderer/src/components/automations/automation-host-filter-resolution.test.ts": 107, - "src/renderer/src/components/automations/automation-host-health.test.ts": 55, - "src/renderer/src/components/automations/automation-host-invalidation-window-events.test.ts": 78, - "src/renderer/src/components/automations/automation-host-invalidation.test.ts": 63, - "src/renderer/src/components/automations/automation-host-list-rows.test.ts": 181, - "src/renderer/src/components/automations/automation-host-orphan-entry.test.ts": 212, - "src/renderer/src/components/automations/automation-host-picker-groups.test.ts": 50, - "src/renderer/src/components/automations/automation-host-recovery.test.ts": 65, - "src/renderer/src/components/automations/automation-host-scale-gates.test.ts": 329, - "src/renderer/src/components/automations/automation-host-scheduler.test.ts": 271, - "src/renderer/src/components/automations/automation-host-status-descriptors.test.ts": 125, - "src/renderer/src/components/automations/automation-legacy-schedule-editing.test.ts": 1590, - "src/renderer/src/components/automations/automation-list-empty-state.test.ts": 120, - "src/renderer/src/components/automations/automation-list-focus-recovery.test.ts": 52, - "src/renderer/src/components/automations/automation-list-keyboard-navigation.test.ts": 80, - "src/renderer/src/components/automations/automation-list-last-run.test.ts": 111, - "src/renderer/src/components/automations/automation-list-row-identity.test.ts": 513, - "src/renderer/src/components/automations/automation-list-search-rows.test.ts": 483, - "src/renderer/src/components/automations/automation-list-search.test.ts": 78, - "src/renderer/src/components/automations/automation-list-view-sort.test.ts": 447, - "src/renderer/src/components/automations/automation-list-view.test.ts": 127, - "src/renderer/src/components/automations/automation-project-groups.test.ts": 57, - "src/renderer/src/components/automations/automation-run-completion-evidence.test.ts": 425, - "src/renderer/src/components/automations/automation-run-context.test.ts": 55, - "src/renderer/src/components/automations/automation-run-history-keyboard-navigation.test.ts": 383, - "src/renderer/src/components/automations/automation-run-open-target.test.ts": 319, - "src/renderer/src/components/automations/automation-run-output-snapshot-equivalence.test.ts": 121, - "src/renderer/src/components/automations/automation-run-output-snapshot.test.ts": 101, - "src/renderer/src/components/automations/automation-run-view-state.test.ts": 55, - "src/renderer/src/components/automations/automation-run-workspace-action.paired-server.test.ts": 425, - "src/renderer/src/components/automations/automation-runs-dashboard-model.test.ts": 52, - "src/renderer/src/components/automations/automation-scoped-list-client.test.ts": 188, - "src/renderer/src/components/automations/automation-setup-decision.test.ts": 1440, - "src/renderer/src/components/automations/automation-source-display.test.ts": 52, - "src/renderer/src/components/automations/automation-target-availability.test.ts": 77, - "src/renderer/src/components/automations/automation-usage-model.test.ts": 53, - "src/renderer/src/components/automations/automation-write-invalidation.test.ts": 52, - "src/renderer/src/components/automations/external-automation-display.test.ts": 67, - "src/renderer/src/components/automations/external-automation-list-entries.test.ts": 53, - "src/renderer/src/components/automations/external-automation-run-table-state.test.ts": 50, - "src/renderer/src/components/automations/external-automation-scope-gating.test.ts": 68, - "src/renderer/src/components/automations/external-automation-scope-keys.test.ts": 76, - "src/renderer/src/components/automations/external-automation-source-availability.test.ts": 56, - "src/renderer/src/components/automations/hermes-cron-output-parse.test.ts": 75, - "src/renderer/src/components/automations/use-automation-host-catalog-readoption.test.tsx": 305, - "src/renderer/src/components/automations/use-automation-host-catalog.test.tsx": 341, - "src/renderer/src/components/automations/use-automation-list-focus-recovery.test.tsx": 294, - "src/renderer/src/components/automations/use-automation-list-search.test.tsx": 598, - "src/renderer/src/components/automations/use-automation-runs-dashboard-payload.test.tsx": 1210, - "src/renderer/src/components/automations/use-automation-runs-dashboard.test.tsx": 631, - "src/renderer/src/components/automations/use-external-automation-scope-retention.test.tsx": 227, - "src/renderer/src/components/automations/use-selected-automation-run-history.test.tsx": 434, - "src/renderer/src/components/browser-cookie-import-google-disclosure.test.tsx": 886, - "src/renderer/src/components/browser-favicon.test.tsx": 383, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.chrome-parity.test.tsx": 2452, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.dead-guest.test.tsx": 2788, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.download-notices.test.tsx": 2232, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.drag-focus.test.tsx": 2135, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.failure-remount.test.tsx": 2806, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.markup.test.tsx": 2693, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.popup-notices.test.tsx": 2474, - "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.test.tsx": 2380, - "src/renderer/src/components/browser-pane/ReopenBrowserPageOnServerButton.test.tsx": 384, - "src/renderer/src/components/browser-pane/annotate/BrowserAnnotationSendMenuContent.test.tsx": 59, - "src/renderer/src/components/browser-pane/annotate/GrabConfirmationSheet.test.ts": 150, - "src/renderer/src/components/browser-pane/annotate/MarkupOverlay.test.tsx": 606, - "src/renderer/src/components/browser-pane/annotate/browser-annotation-output.test.ts": 105, - "src/renderer/src/components/browser-pane/annotate/browser-grab-toast-owner-cleanup.test.tsx": 921, - "src/renderer/src/components/browser-pane/annotate/browser-page-annotation-tray.test.tsx": 2518, - "src/renderer/src/components/browser-pane/annotate/guest-annotation-viewport-bridge.test.ts": 52, - "src/renderer/src/components/browser-pane/annotate/markup-drawing-model.test.ts": 69, - "src/renderer/src/components/browser-pane/annotate/markup-gesture.test.ts": 110, - "src/renderer/src/components/browser-pane/annotate/markup-screenshot-compose.test.ts": 78, - "src/renderer/src/components/browser-pane/annotate/markup-shape-hit-test.test.ts": 57, - "src/renderer/src/components/browser-pane/annotate/markup-shape-render.test.ts": 55, - "src/renderer/src/components/browser-pane/annotate/use-browser-page-annotation-send.test.tsx": 883, - "src/renderer/src/components/browser-pane/annotate/use-browser-page-grab-annotations.test.tsx": 1134, - "src/renderer/src/components/browser-pane/annotate/use-markup-draw-hint.test.ts": 281, - "src/renderer/src/components/browser-pane/annotate/useGrabMode.test.ts": 497, - "src/renderer/src/components/browser-pane/annotate/useMarkupEditor.test.ts": 290, - "src/renderer/src/components/browser-pane/annotate/useMarkupPointerHandlers.test.ts": 246, - "src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.test.tsx": 545, - "src/renderer/src/components/browser-pane/assemble-chrome/BrowserFind.test.tsx": 413, - "src/renderer/src/components/browser-pane/assemble-chrome/BrowserPane.webview-preferences.test.ts": 404, - "src/renderer/src/components/browser-pane/assemble-chrome/BrowserPaneOverlayLayer.test.tsx": 1274, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-expansion.test.ts": 50, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-suggestions.test.ts": 348, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-chrome-folded-tools.test.tsx": 350, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-chrome-toolbar.test.tsx": 456, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-deferred-lifecycle.test.tsx": 1800, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-detected-browsers-summary.test.ts": 38, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-egress-indicator.test.tsx": 2010, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-import-hint-visibility.test.ts": 64, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-navigation-control-row.test.tsx": 421, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-page-zoom-indicator.test.tsx": 276, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-pane-page-selection.test.ts": 48, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.adoption-chrome.test.tsx": 16057, - "src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.retention-props.test.tsx": 1906, - "src/renderer/src/components/browser-pane/assemble-chrome/ssh-routed-browser-page-gate.test.tsx": 1675, + "src/main/updater-prerelease-feed.test.ts": 219, + "src/main/updater-release-api-token.test.ts": 47, + "src/main/updater-release-build-cache.test.ts": 46, + "src/main/updater-release-builds.test.ts": 113, + "src/main/updater-sqlite-profile-downgrade.test.ts": 2081, + "src/main/updater.build-channel-selection.test.ts": 3230, + "src/main/updater.check-failure.test.ts": 3474, + "src/main/updater.check-preflight.test.ts": 4950, + "src/main/updater.check-settlement.test.ts": 3854, + "src/main/updater.fallback.test.ts": 39, + "src/main/updater.feed-attempt-lifetime.test.ts": 2718, + "src/main/updater.headless-serve-install.test.ts": 604, + "src/main/updater.install-failure-cause.test.ts": 4875, + "src/main/updater.linux-externally-managed.test.ts": 2325, + "src/main/updater.linux-root-package-install.test.ts": 3113, + "src/main/updater.mac-install.test.ts": 1719, + "src/main/updater.nudge-campaign.test.ts": 3409, + "src/main/updater.prerelease-fallback.test.ts": 4456, + "src/main/updater.publishing-window-feed.test.ts": 4837, + "src/main/updater.quit-and-install.test.ts": 4994, + "src/main/updater.startup-scheduling.test.ts": 3378, + "src/main/updater/mac-browser-sidecar-recovery.test.ts": 81, + "src/main/updater/rollout-flags.test.ts": 285, + "src/main/updater/serve-supervisor-install-park.test.ts": 151, + "src/main/updater/shipit-leftover-recovery.test.ts": 103, + "src/main/usage-worktree-canonicalizer.test.ts": 32, + "src/main/usage-worktree-metadata.test.ts": 44, + "src/main/usage/agent-token-usage-reporter.test.ts": 335, + "src/main/usage/agent-token-usage.test.ts": 34, + "src/main/usage/analytics-session-id-store.test.ts": 343, + "src/main/usage/highest-usage-key.test.ts": 41, + "src/main/usage/jsonl-file-reader.test.ts": 81, + "src/main/usage/jsonl-file-snapshot.test.ts": 52, + "src/main/usage/jsonl-line-offsets.test.ts": 55, + "src/main/usage/usage-breakdown-scaling.test.ts": 90, + "src/main/usage/usage-calendar-range.test.ts": 44, + "src/main/usage/usage-provider-store-initialization.test.ts": 669, + "src/main/usage/usage-provider-store-lifecycle.test.ts": 868, + "src/main/usage/usage-scan-worker-cache-cleanup.test.ts": 139, + "src/main/usage/usage-scan-worker-client.test.ts": 83, + "src/main/usage/usage-scan-worker-event-loop.test.ts": 2396, + "src/main/usage/usage-source-cache-file.test.ts": 76, + "src/main/usage/usage-worktree-refs.test.ts": 33, + "src/main/usage/usage-worktree-resolver.test.ts": 120, + "src/main/warp-themes/discovery.test.ts": 55, + "src/main/warp-themes/index.test.ts": 625, + "src/main/warp-themes/manual-warp-theme-files.test.ts": 60, + "src/main/warp-themes/parser-runner.test.ts": 45, + "src/main/warp-themes/parser.test.ts": 91, + "src/main/warp-themes/theme-file-scanner.test.ts": 46, + "src/main/win32-utils-async-acl.test.ts": 53, + "src/main/win32-utils.test.ts": 75, + "src/main/window/attach-main-window-services.test.ts": 741, + "src/main/window/clipboard-agent-session-attachment-paste.test.ts": 582, + "src/main/window/clipboard-copied-file-paths.test.ts": 59, + "src/main/window/clipboard-dashboard-popout-access.test.ts": 1186, + "src/main/window/clipboard-file-copy.test.ts": 46, + "src/main/window/clipboard-image-source.test.ts": 61, + "src/main/window/clipboard-image-temp-file.test.ts": 46, + "src/main/window/clipboard-image-thumbnail.test.ts": 29, + "src/main/window/clipboard-ipc-handlers.test.ts": 553, + "src/main/window/clipboard-macos-image-paste.test.ts": 932, + "src/main/window/clipboard-platform-routing.test.ts": 59, + "src/main/window/clipboard-publisher-executable.test.ts": 48, + "src/main/window/clipboard-remote-file-cleanup.bench.test.ts": 26, + "src/main/window/clipboard-remote-file-staging-fs.test.ts": 44, + "src/main/window/clipboard-remote-file-staging.test.ts": 100, + "src/main/window/clipboard-runtime-owned-ssh-paste.test.ts": 303, + "src/main/window/clipboard-text-publisher.test.ts": 344, + "src/main/window/clipboard-text-write-verify.test.ts": 38, + "src/main/window/clipboard-windows-image-file.test.ts": 92, + "src/main/window/createMainWindow-close-confirmation.test.ts": 1826, + "src/main/window/createMainWindow-low-commit-oom-recovery.test.ts": 1701, + "src/main/window/createMainWindow-markdown-editor-focus.test.ts": 1756, + "src/main/window/createMainWindow-recovery-reload-watchdog.test.ts": 2387, + "src/main/window/createMainWindow-renderer-crash-recovery.test.ts": 1674, + "src/main/window/createMainWindow-renderer-launch-failure.test.ts": 1750, + "src/main/window/createMainWindow-startup-reveal.test.ts": 1903, + "src/main/window/createMainWindow-system-resume-relay.test.ts": 1871, + "src/main/window/createMainWindow-terminal-focus-shortcuts.test.ts": 2155, + "src/main/window/createMainWindow-tray-minimize-close.test.ts": 1657, + "src/main/window/createMainWindow-user-quit-close.test.ts": 1615, + "src/main/window/createMainWindow-zoom-and-tab-switch-shortcuts.test.ts": 1676, + "src/main/window/createMainWindow.test.ts": 1790, + "src/main/window/darwin-user-temp-dir.test.ts": 54, + "src/main/window/dashboard-popout-window.test.ts": 111, + "src/main/window/dragged-temp-file-copy.test.ts": 220, + "src/main/window/dropped-path-preparation-ipc.test.ts": 70, + "src/main/window/dropped-path-preparation.test.ts": 65, + "src/main/window/editable-context-menu.test.ts": 60, + "src/main/window/focus-existing-window.test.ts": 62, + "src/main/window/foreground-activation-policy.test.ts": 46, + "src/main/window/history-gc-worktree-ids.test.ts": 421, + "src/main/window/host-frame-navigation-guard.test.ts": 268, + "src/main/window/macos-app-activation.test.ts": 52, + "src/main/window/macos-tahoe-release.test.ts": 34, + "src/main/window/main-window-state-lifecycle.test.ts": 36, + "src/main/window/main-window-updater-deferred-setup.test.ts": 176, + "src/main/window/main-window-updater-headless-serve.test.ts": 41, + "src/main/window/main-window-visibility.test.ts": 32, + "src/main/window/main-window-webview-security.test.ts": 277, + "src/main/window/mobile-markdown-request-relay.test.ts": 35, + "src/main/window/native-chat-paste-files.test.ts": 815, + "src/main/window/privileged-window-navigation.test.ts": 240, + "src/main/window/renderer-document-navigation.test.ts": 59, + "src/main/window/renderer-launch-failure-backoff.test.ts": 21, + "src/main/window/renderer-launch-failure-probe.spawn.test.ts": 77, + "src/main/window/renderer-launch-failure-probe.test.ts": 48, + "src/main/window/renderer-publication-throttle.test.ts": 34, + "src/main/window/renderer-recovery-prompt.test.ts": 80, + "src/main/window/renderer-recovery-reload-watchdog.test.ts": 45, + "src/main/window/runtime-renderer-notification-sender.test.ts": 36, + "src/main/window/runtime-window-lifecycle-file-open.test.ts": 46, + "src/main/window/session-tab-close-request-relay.test.ts": 57, + "src/main/window/terminal-tab-close-request-relay.test.ts": 51, + "src/main/window/updater-package-recovery-ipc.test.ts": 3623, + "src/main/window/window-close-decision.test.ts": 31, + "src/main/window/worktree-sleep-request-relay.test.ts": 67, + "src/main/windows-live-tree-kill.win32.test.ts": 78, + "src/main/windows-process-tree-kill.test.ts": 68, + "src/main/windows-pty-root-identity.test.ts": 120, + "src/main/windows-registry-addon.test.ts": 32, + "src/main/windows/windows-administrator-restart.test.ts": 37, + "src/main/windows/windows-administrator-restart.win32.test.ts": 59, + "src/main/windows/windows-administrator-status.test.ts": 56, + "src/main/windows/windows-administrator-status.win32.test.ts": 47, + "src/main/windows/windows-command-line-recovery-health.test.ts": 36, + "src/main/windows/windows-host-job.win32.test.ts": 30, + "src/main/windows/windows-msys-job.win32.test.ts": 50, + "src/main/windows/windows-process-creation-time.test.ts": 83, + "src/main/windows/windows-process-table-cim-scan.test.ts": 48, + "src/main/windows/windows-process-table-native-addon.win32.test.ts": 63, + "src/main/windows/windows-process-table.test.ts": 142, + "src/main/windows/windows-process-tree-command-line-patch.test.ts": 40, + "src/main/windows/windows-pty-job.test.ts": 48, + "src/main/windows/windows-pty-job.win32.test.ts": 74, + "src/main/worker-thread-child-process/worker-thread-child-process.test.ts": 452, + "src/main/worker-thread-request-queue.test.ts": 83, + "src/main/workspace-cleanup-removal-snapshot-prune.test.ts": 37, + "src/main/workspace-cleanup-scan-snapshot.test.ts": 156, + "src/main/workspace-create-event-fields.test.ts": 358, + "src/main/workspace-snapshot-pruning.bench.test.ts": 50, + "src/main/workspace-space-analysis-capacity.test.ts": 488, + "src/main/workspace-space-analysis-du-timeout.test.ts": 770, + "src/main/workspace-space-analysis-snapshot.test.ts": 140, + "src/main/workspace-space-analysis.test.ts": 492, + "src/main/workspace-space-repo-scan.test.ts": 675, + "src/main/workspace-space-scan-control.test.ts": 36, + "src/main/worktree-background-removal-records.test.ts": 501, + "src/main/worktree-background-removal.test.ts": 360, + "src/main/worktree-create-base-prefetch.test.ts": 286, + "src/main/worktree-create-base.test.ts": 33, + "src/main/worktree-create-candidates.test.ts": 40, + "src/main/worktree-create-concurrency.test.ts": 38, + "src/main/worktree-create-execution-host-route.test.ts": 525, + "src/main/worktree-create-preparation-activity.test.ts": 31, + "src/main/worktree-create-preparation-cancellation.test.ts": 325, + "src/main/worktree-create-preparation-claim.test.ts": 74, + "src/main/worktree-create-preparation-contention.test.ts": 519, + "src/main/worktree-create-preparation-timing.test.ts": 335, + "src/main/worktree-create-preparation-wsl-root.test.ts": 314, + "src/main/worktree-create-preparation.test.ts": 264, + "src/main/worktree-create-timing.test.ts": 106, + "src/main/worktree-failed-removal.test.ts": 614, + "src/main/worktree-identity-persistence.test.ts": 1812, + "src/main/worktree-lineage-pruning.test.ts": 388, + "src/main/worktree-metadata-ownership.test.ts": 27, + "src/main/worktree-name-retirement.test.ts": 275, + "src/main/worktree-preparation-discard-retry.test.ts": 315, + "src/main/worktree-preparation-fetch-barrier.test.ts": 322, + "src/main/worktree-preparation-tip-refresh.test.ts": 585, + "src/main/worktree-prunable-git-file.test.ts": 55, + "src/main/worktree-removal-authority.test.ts": 92, + "src/main/worktree-removal-close-records.test.ts": 1345, + "src/main/worktree-removal-execution-host-route.test.ts": 534, + "src/main/worktree-removal-home-guard.test.ts": 69, + "src/main/worktree-removal-repo-owner.test.ts": 40, + "src/main/worktree-removal-safety.test.ts": 114, + "src/main/worktree-removal-session-partition-fencing.test.ts": 1472, + "src/main/worktree-retirement-backfill-scan.test.ts": 65, + "src/main/worktree-retirement-backfill-stall.test.ts": 303, + "src/main/worktree-retirement-discovery-wsl.test.ts": 256, + "src/main/worktree-retirement-discovery.test.ts": 319, + "src/main/worktree-retirement-namespace.test.ts": 71, + "src/main/worktree-root-preparation.test.ts": 310, + "src/main/worktree-trash.test.ts": 108, + "src/main/wsl-approved-root-race.wsl.test.ts": 61, + "src/main/wsl-availability-missing-kernel.test.ts": 51, + "src/main/wsl-background-distro-gate.test.ts": 95, + "src/main/wsl-bash-command.test.ts": 30, + "src/main/wsl-distro-list-output.test.ts": 33, + "src/main/wsl-distro-list-single-flight.test.ts": 75, + "src/main/wsl-fish-history-cleanup.test.ts": 125, + "src/main/wsl-interop-spawn-directory.test.ts": 44, + "src/main/wsl-managed-cli-path-restore.test.ts": 68, + "src/main/wsl-move-incidental-writes.test.ts": 1045, + "src/main/wsl-move-scan-pruning.test.ts": 1921, + "src/main/wsl-running-distro-stop.test.ts": 39, + "src/main/wsl-running-distros.test.ts": 171, + "src/main/wsl-running-path-filter.test.ts": 37, + "src/main/wsl-unc-delete-symlink-repro.test.ts": 52, + "src/main/wsl-unc-delete.test.ts": 66, + "src/main/wsl-unc-delete.wsl.test.ts": 74, + "src/main/wsl.test.ts": 110, + "src/main/wsl/wsl-command-loop.test.ts": 6064, + "src/main/wsl/wsl-directory-upload.test.ts": 15460, + "src/main/wsl/wsl-distro-holder.test.ts": 5207, + "src/main/wsl/wsl-distro-registry-id.test.ts": 61, + "src/main/wsl/wsl-executable-path.win32.test.ts": 29, + "src/main/wsl/wsl-guest-environment.test.ts": 99, + "src/main/wsl/wsl-guest-process-identity.test.ts": 85, + "src/main/wsl/wsl-host-distro-list.test.ts": 78, + "src/main/wsl/wsl-host-server-stop.test.ts": 482, + "src/main/wsl/wsl-host-service.test.ts": 978, + "src/main/wsl/wsl-interop-health.test.ts": 224, + "src/main/wsl/wsl-interop-registration.test.ts": 45, + "src/main/wsl/wsl-orcad-account-env.test.ts": 189, + "src/main/wsl/wsl-orcad-connection.test.ts": 776, + "src/main/wsl/wsl-orcad-deploy.test.ts": 666, + "src/main/wsl/wsl-orcad-host-account-push.test.ts": 235, + "src/main/wsl/wsl-orcad-host-environments.test.ts": 33, + "src/main/wsl/wsl-orcad-host-transport-loops.test.ts": 223, + "src/main/wsl/wsl-orcad-host-transport.test.ts": 197, + "src/main/wsl/wsl-orcad-incumbent.test.ts": 540, + "src/main/wsl/wsl-orcad-instance-adoption.test.ts": 406, + "src/main/wsl/wsl-orcad-liveness-supervisor.test.ts": 132, + "src/main/wsl/wsl-orcad-liveness-verdict.test.ts": 33, + "src/main/wsl/wsl-orcad-real-distro-lane.test.ts": 639, + "src/main/wsl/wsl-orcad-reconnect-backoff.test.ts": 44, + "src/main/wsl/wsl-orcad-stdio-bridge.test.ts": 546, + "src/main/wsl/wsl-orcad-watch.test.ts": 551, + "src/main/wsl/wsl-pinned-runtime.test.ts": 294, + "src/main/wsl/wsl-runner.test.ts": 80, + "src/main/wsl/wsl-runner.wsl.test.ts": 53, + "src/main/wsl/wsl-w1-w3-contract.test.ts": 57, + "src/main/zcode/hook-service.test.ts": 176, + "src/main/zcode/interactive-capability.live.test.ts": 55, + "src/main/zcode/zcode-plan-api-key-store-native-windows.test.ts": 30, + "src/main/zcode/zcode-plan-api-key-store-plaintext-rollback.test.ts": 113, + "src/main/zcode/zcode-plan-api-key-store-publication-race.test.ts": 78, + "src/main/zcode/zcode-plan-api-key-store.test.ts": 96, + "src/main/zcode/zcode-v2-personal-overlay-raw-json.test.ts": 339, + "src/main/zcode/zcode-v2-plan-credentials.test.ts": 997, + "src/main/zod-schema-compilation.test.ts": 166, + "src/packages/process-host/src/bounded-output-sink.test.ts": 70, + "src/packages/process-host/src/close-process-registry.test.ts": 25, + "src/packages/process-host/src/growing-byte-buffer.test.ts": 91, + "src/packages/process-host/src/posix-lsof-probe.test.ts": 4280, + "src/packages/process-host/src/process-tree-termination.test.ts": 58, + "src/packages/process-host/src/retryable-process-exit-proof.test.ts": 37, + "src/packages/process-host/src/run-process-bytes.test.ts": 2236, + "src/packages/process-host/src/run-process-termination-failure.test.ts": 67, + "src/packages/process-host/src/run-process.test.ts": 7365, + "src/packages/process-host/src/spawn-observer.test.ts": 258, + "src/packages/process-host/src/spawn-process-stdio.test.ts": 257, + "src/packages/process-host/src/windows-command-line.test.ts": 60, + "src/preload/api/notification-sound-playback.test.ts": 56, + "src/preload/api/platform-bridge.test.ts": 47, + "src/preload/api/wsl-hosts-api.test.ts": 31, + "src/preload/app-restart-checkpoint-routing.test.ts": 1729, + "src/preload/browser-client-page-renderer-requests.test.ts": 263, + "src/preload/browser-find-subscriptions.test.ts": 41, + "src/preload/browser-window-close.test.ts": 29, + "src/preload/close-active-tab-payload-admission.test.ts": 30, + "src/preload/doc-preview-link-interception.test.ts": 208, + "src/preload/opencode-go-credentials.test.ts": 28, + "src/preload/preload-file-drop-absence.test.ts": 349, + "src/preload/pty-snapshot-capability-ipc.test.ts": 868, + "src/preload/renderer-heap-statistics-reader.test.ts": 27, + "src/preload/renderer-process-memory-reader.test.ts": 51, + "src/preload/renderer-restart-wiring.test.ts": 51, + "src/preload/runtime-environment-subscriptions.test.ts": 47, + "src/preload/ssh-authority-forwarding.test.ts": 1277, + "src/preload/updater-package-recovery.test.ts": 491, + "src/preload/usage-provider-api.test.ts": 37, + "src/renderer/src/app-shell/app-command-handlers-child-workspaces.test.ts": 1123, + "src/renderer/src/app-shell/app-command-handlers-tab-rename.test.ts": 1224, + "src/renderer/src/app-shell/app-command-handlers-workspace-delete.test.ts": 514, + "src/renderer/src/app-shell/app-root-surface-settings.test.tsx": 515, + "src/renderer/src/app-shell/app-window-chrome.test.ts": 32, + "src/renderer/src/app-shell/reconcile-hydrated-workspace-tab-models.test.ts": 48, + "src/renderer/src/app-shell/shutdown-checkpoint-persist.test.ts": 61, + "src/renderer/src/app-shell/shutdown-checkpoint-restart-lifecycle.test.ts": 190, + "src/renderer/src/app-shell/startup-actions-selector.test.ts": 65, + "src/renderer/src/app-shell/startup-window-parity.test.ts": 1484, + "src/renderer/src/app-shell/use-document-appearance.test.tsx": 1441, + "src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx": 333, + "src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx": 276, + "src/renderer/src/app-shell/use-terminal-viewer-color-publication.test.tsx": 1679, + "src/renderer/src/app-shell/use-visible-hosted-review-refresh.test.tsx": 339, + "src/renderer/src/app-shell/visible-review-reporting.test.ts": 1158, + "src/renderer/src/app-shell/window-visibility-actions-selector.test.ts": 250, + "src/renderer/src/app-shell/workspace-view-cross-client-sync.test.tsx": 938, + "src/renderer/src/attention/agent-attention-policy.test.ts": 37, + "src/renderer/src/attention/notification-sources.test.ts": 1558, + "src/renderer/src/components/AgentStateDot.test.ts": 113, + "src/renderer/src/components/AgentWorkingSpinner.test.tsx": 220, + "src/renderer/src/components/ButtonKeyHint.test.tsx": 267, + "src/renderer/src/components/JiraIssueWorkspace.assignee-source.test.tsx": 1928, + "src/renderer/src/components/JiraIssueWorkspace.optimistic-edit-reopen.test.tsx": 1828, + "src/renderer/src/components/JiraIssueWorkspace.optimistic-edit-rollback.test.tsx": 949, + "src/renderer/src/components/JiraIssueWorkspace.optimistic-edit.test.tsx": 1551, + "src/renderer/src/components/JiraIssueWorkspace.status-queue.test.tsx": 1181, + "src/renderer/src/components/JiraIssueWorkspace.transition-source.test.tsx": 1146, + "src/renderer/src/components/LinuxPackageInstallRecoveryCard.test.tsx": 888, + "src/renderer/src/components/NativeChatResumeOnRestartGroups.selection.test.tsx": 2871, + "src/renderer/src/components/NativeChatResumeOnRestartGroups.test.tsx": 2266, + "src/renderer/src/components/NativeChatResumeOnRestartModal.list.test.tsx": 2270, + "src/renderer/src/components/NativeChatResumeOnRestartModal.test.tsx": 3190, + "src/renderer/src/components/NewWorkspaceComposerCard.file-drop.test.tsx": 630, + "src/renderer/src/components/NewWorkspaceComposerCard.quick-file-drop.test.tsx": 3819, + "src/renderer/src/components/NewWorkspaceComposerCard.set-location-warm.test.tsx": 1037, + "src/renderer/src/components/NewWorkspaceComposerCard.set-location.test.tsx": 1164, + "src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx": 1008, + "src/renderer/src/components/NewWorkspaceComposerCard.test.tsx": 1575, + "src/renderer/src/components/QuickOpen.mount-gating.test.tsx": 1589, + "src/renderer/src/components/QuickOpen.selection.test.tsx": 866, + "src/renderer/src/components/SelectedTextCopyMenu.test.tsx": 347, + "src/renderer/src/components/StarNagCard.test.tsx": 277, + "src/renderer/src/components/StateIndicatorTooltip.test.tsx": 43, + "src/renderer/src/components/TaskPageJiraSavedFilters.test.tsx": 1317, + "src/renderer/src/components/TaskPageJiraStatusFilters.test.tsx": 664, + "src/renderer/src/components/TerminalSearch.test.tsx": 485, + "src/renderer/src/components/TerminalTitlebarTabs.test.tsx": 188, + "src/renderer/src/components/TerminalWorkbenchContainer.test.tsx": 279, + "src/renderer/src/components/UpdateCard.error-card.test.tsx": 2563, + "src/renderer/src/components/UpdateCard.test.ts": 529, + "src/renderer/src/components/WorktreeBaseFallbackDialog.test.tsx": 1562, + "src/renderer/src/components/WorktreeJumpPalette.linear-url.test.tsx": 3255, + "src/renderer/src/components/WorktreeJumpPalette.mount-gating.test.tsx": 3045, + "src/renderer/src/components/WorktreeJumpPalette.recent-tabs.behavior.test.tsx": 2766, + "src/renderer/src/components/WorktreeJumpPalette.recent-tabs.test.tsx": 4413, + "src/renderer/src/components/WorktreeJumpPalette.task-search.test.tsx": 4356, + "src/renderer/src/components/WorktreeJumpPalette.test.tsx": 3062, + "src/renderer/src/components/activity/ActivityPrototypePage.filter-focus-shortcut.test.ts": 42, + "src/renderer/src/components/activity/ActivityPrototypePage.test.ts": 532, + "src/renderer/src/components/activity/ActivityPrototypePage.thread-grouping.test.ts": 1405, + "src/renderer/src/components/activity/ActivityThreadOptionsMenu.test.tsx": 2278, + "src/renderer/src/components/activity/activity-answered-ask-timeline.test.tsx": 1793, + "src/renderer/src/components/activity/activity-auto-mark-read-loop.react185.test.tsx": 3027, + "src/renderer/src/components/activity/activity-clear-completed-hmr.test.ts": 579, + "src/renderer/src/components/activity/activity-clear-completed-state-lifetime.test.ts": 2320, + "src/renderer/src/components/activity/activity-clear-completed.test.ts": 428, + "src/renderer/src/components/activity/activity-event-builder.identity-reuse.test.ts": 445, + "src/renderer/src/components/activity/activity-portal-churn-budget.test.ts": 45, + "src/renderer/src/components/activity/activity-portal-readiness-decay.react185.test.tsx": 236, + "src/renderer/src/components/activity/activity-portal-readiness-loop.react185.test.tsx": 365, + "src/renderer/src/components/activity/activity-portal-readiness-oscillation.test.ts": 54, + "src/renderer/src/components/activity/activity-portal-readiness-subscription-churn.react185.test.tsx": 324, + "src/renderer/src/components/activity/activity-portal-thread-reconciliation.test.ts": 40, + "src/renderer/src/components/activity/activity-scope-filter.test.ts": 442, + "src/renderer/src/components/activity/activity-search-query-work-budget.test.tsx": 1763, + "src/renderer/src/components/activity/activity-terminal-portal-publication-loop.react185.test.tsx": 191, + "src/renderer/src/components/activity/activity-terminal-portal.test.tsx": 174, + "src/renderer/src/components/activity/activity-thread-actions.test.ts": 602, + "src/renderer/src/components/activity/activity-thread-context-menu.test.tsx": 2316, + "src/renderer/src/components/activity/activity-thread-grouping.status-order.test.ts": 530, + "src/renderer/src/components/activity/activity-thread-hover-card-intent.test.tsx": 476, + "src/renderer/src/components/activity/activity-thread-hover-card.test.tsx": 2348, + "src/renderer/src/components/activity/activity-thread-list-pane-collapsible.test.tsx": 2783, + "src/renderer/src/components/activity/activity-thread-list-pane.multi-select.test.tsx": 4208, + "src/renderer/src/components/activity/activity-thread-list-pane.virtualization.test.tsx": 3196, + "src/renderer/src/components/activity/dev-activity-fixture.test.ts": 43, + "src/renderer/src/components/activity/event-time-clock-refresh.test.tsx": 1622, + "src/renderer/src/components/activity/use-activity-thread-action-bindings.test.tsx": 1305, + "src/renderer/src/components/activity/useActivityUnreadCount.freshness.test.tsx": 1542, + "src/renderer/src/components/activity/useActivityUnreadCount.test.ts": 1418, + "src/renderer/src/components/agent-child-row-parity.test.tsx": 547, + "src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.test.tsx": 279, + "src/renderer/src/components/agent-session-continuation/agent-session-continuation-selection.test.ts": 40, + "src/renderer/src/components/agent/AgentCombobox.test.tsx": 1534, + "src/renderer/src/components/agent/AgentSettingsDialog.test.tsx": 585, + "src/renderer/src/components/agent/agent-combobox-command-state.test.ts": 29, + "src/renderer/src/components/artifacts/ArtifactCollection.test.tsx": 1447, + "src/renderer/src/components/artifacts/ArtifactPreview.test.tsx": 316, + "src/renderer/src/components/artifacts/ArtifactPublishButton.test.tsx": 605, + "src/renderer/src/components/artifacts/ArtifactsPage.test.tsx": 1594, + "src/renderer/src/components/artifacts/artifact-display-labels.test.ts": 71, + "src/renderer/src/components/artifacts/artifact-list-search.test.ts": 76, + "src/renderer/src/components/artifacts/artifact-list-windowing.test.tsx": 3481, + "src/renderer/src/components/artifacts/artifact-publish-flow.test.ts": 86, + "src/renderer/src/components/artifacts/artifact-published-link-client.test.ts": 40, + "src/renderer/src/components/automations/AutomationDestinationField.test.tsx": 577, + "src/renderer/src/components/automations/AutomationDetail.host.test.tsx": 608, + "src/renderer/src/components/automations/AutomationDetail.test.tsx": 716, + "src/renderer/src/components/automations/AutomationEditorPromptEditor.test.tsx": 306, + "src/renderer/src/components/automations/AutomationEditorSettingsSidebar.test.tsx": 796, + "src/renderer/src/components/automations/AutomationHostBadges.test.tsx": 382, + "src/renderer/src/components/automations/AutomationHostFilterNotice.test.tsx": 358, + "src/renderer/src/components/automations/AutomationListEmptyView.test.tsx": 278, + "src/renderer/src/components/automations/AutomationListHostGroups.test.tsx": 707, + "src/renderer/src/components/automations/AutomationListLocalRows.test.tsx": 1082, + "src/renderer/src/components/automations/AutomationListSearchField.test.tsx": 322, + "src/renderer/src/components/automations/AutomationListTableHeader.test.tsx": 434, + "src/renderer/src/components/automations/AutomationOwnerConflictNotice.test.tsx": 278, + "src/renderer/src/components/automations/AutomationPromptDisclosure.test.tsx": 770, + "src/renderer/src/components/automations/AutomationRunDetailsPage.test.tsx": 1763, + "src/renderer/src/components/automations/AutomationRunHistory.test.tsx": 1111, + "src/renderer/src/components/automations/AutomationRunsDashboardSurface.test.tsx": 505, + "src/renderer/src/components/automations/AutomationRunsTable.test.tsx": 535, + "src/renderer/src/components/automations/AutomationSchedulePicker.render.test.tsx": 1042, + "src/renderer/src/components/automations/AutomationSchedulePicker.test.ts": 946, + "src/renderer/src/components/automations/AutomationTimeField.test.tsx": 556, + "src/renderer/src/components/automations/AutomationsDetailPane.run-count.test.tsx": 786, + "src/renderer/src/components/automations/AutomationsDetailPane.test.tsx": 1040, + "src/renderer/src/components/automations/AutomationsListPanel.test.tsx": 1103, + "src/renderer/src/components/automations/AutomationsPage.create-destination.test.tsx": 2462, + "src/renderer/src/components/automations/AutomationsPage.cross-authority-actions.test.tsx": 2016, + "src/renderer/src/components/automations/AutomationsPage.escape-precedence.test.tsx": 1322, + "src/renderer/src/components/automations/AutomationsPage.external-scope.test.tsx": 2202, + "src/renderer/src/components/automations/AutomationsPage.notice-recovery.test.tsx": 2264, + "src/renderer/src/components/automations/AutomationsPage.refresh-selection.test.tsx": 1648, + "src/renderer/src/components/automations/AutomationsPage.run-visibility.test.tsx": 1240, + "src/renderer/src/components/automations/AutomationsPage.save-visibility.test.tsx": 1505, + "src/renderer/src/components/automations/AutomationsPage.strict-mode-save-visibility.test.tsx": 1331, + "src/renderer/src/components/automations/AutomationsPage.test.tsx": 2709, + "src/renderer/src/components/automations/AutomationsPageBreadcrumb.test.tsx": 265, + "src/renderer/src/components/automations/ExternalAutomationManagers.test.tsx": 785, + "src/renderer/src/components/automations/automation-authority-identity.test.ts": 42, + "src/renderer/src/components/automations/automation-captured-owner.test.ts": 335, + "src/renderer/src/components/automations/automation-create-destination.test.ts": 113, + "src/renderer/src/components/automations/automation-create-projects.test.ts": 31, + "src/renderer/src/components/automations/automation-detail-tab-navigation.test.ts": 187, + "src/renderer/src/components/automations/automation-editor-prompt-options.test.ts": 43, + "src/renderer/src/components/automations/automation-extra-agent-args-capability.test.ts": 292, + "src/renderer/src/components/automations/automation-host-cache-controller.test.ts": 366, + "src/renderer/src/components/automations/automation-host-cache-health.test.ts": 43, + "src/renderer/src/components/automations/automation-host-cache.test.ts": 62, + "src/renderer/src/components/automations/automation-host-catalog-generation.test.ts": 109, + "src/renderer/src/components/automations/automation-host-catalog-order.test.ts": 39, + "src/renderer/src/components/automations/automation-host-catalog-source.test.ts": 205, + "src/renderer/src/components/automations/automation-host-catalog.test.ts": 58, + "src/renderer/src/components/automations/automation-host-client.test.ts": 228, + "src/renderer/src/components/automations/automation-host-detail-display.test.ts": 39, + "src/renderer/src/components/automations/automation-host-diagnostics.test.ts": 405, + "src/renderer/src/components/automations/automation-host-filter-resolution.test.ts": 71, + "src/renderer/src/components/automations/automation-host-health.test.ts": 36, + "src/renderer/src/components/automations/automation-host-invalidation-window-events.test.ts": 59, + "src/renderer/src/components/automations/automation-host-invalidation.test.ts": 41, + "src/renderer/src/components/automations/automation-host-list-rows.test.ts": 393, + "src/renderer/src/components/automations/automation-host-orphan-entry.test.ts": 446, + "src/renderer/src/components/automations/automation-host-picker-groups.test.ts": 44, + "src/renderer/src/components/automations/automation-host-recovery.test.ts": 50, + "src/renderer/src/components/automations/automation-host-scale-gates.test.ts": 570, + "src/renderer/src/components/automations/automation-host-scheduler.test.ts": 381, + "src/renderer/src/components/automations/automation-host-status-descriptors.test.ts": 105, + "src/renderer/src/components/automations/automation-legacy-schedule-editing.test.ts": 1599, + "src/renderer/src/components/automations/automation-list-empty-state.test.ts": 107, + "src/renderer/src/components/automations/automation-list-focus-recovery.test.ts": 46, + "src/renderer/src/components/automations/automation-list-keyboard-navigation.test.ts": 60, + "src/renderer/src/components/automations/automation-list-last-run.test.ts": 88, + "src/renderer/src/components/automations/automation-list-row-identity.test.ts": 576, + "src/renderer/src/components/automations/automation-list-search-rows.test.ts": 446, + "src/renderer/src/components/automations/automation-list-search.test.ts": 51, + "src/renderer/src/components/automations/automation-list-view-sort.test.ts": 501, + "src/renderer/src/components/automations/automation-list-view.test.ts": 100, + "src/renderer/src/components/automations/automation-project-groups.test.ts": 55, + "src/renderer/src/components/automations/automation-run-completion-evidence.test.ts": 331, + "src/renderer/src/components/automations/automation-run-context.test.ts": 39, + "src/renderer/src/components/automations/automation-run-history-keyboard-navigation.test.ts": 423, + "src/renderer/src/components/automations/automation-run-open-target.test.ts": 316, + "src/renderer/src/components/automations/automation-run-output-snapshot-equivalence.test.ts": 131, + "src/renderer/src/components/automations/automation-run-output-snapshot.test.ts": 86, + "src/renderer/src/components/automations/automation-run-view-state.test.ts": 51, + "src/renderer/src/components/automations/automation-run-workspace-action.paired-server.test.ts": 396, + "src/renderer/src/components/automations/automation-runs-dashboard-model.test.ts": 33, + "src/renderer/src/components/automations/automation-scoped-list-client.test.ts": 282, + "src/renderer/src/components/automations/automation-setup-decision.test.ts": 1337, + "src/renderer/src/components/automations/automation-source-display.test.ts": 40, + "src/renderer/src/components/automations/automation-target-availability.test.ts": 243, + "src/renderer/src/components/automations/automation-usage-model.test.ts": 35, + "src/renderer/src/components/automations/automation-write-invalidation.test.ts": 48, + "src/renderer/src/components/automations/external-automation-list-entries.test.ts": 49, + "src/renderer/src/components/automations/external-automation-scope-gating.test.ts": 53, + "src/renderer/src/components/automations/external-automation-scope-keys.test.ts": 41, + "src/renderer/src/components/automations/external-automation-source-availability.test.ts": 40, + "src/renderer/src/components/automations/use-automation-host-catalog-readoption.test.tsx": 641, + "src/renderer/src/components/automations/use-automation-host-catalog.test.tsx": 551, + "src/renderer/src/components/automations/use-automation-list-focus-recovery.test.tsx": 201, + "src/renderer/src/components/automations/use-automation-list-search.test.tsx": 627, + "src/renderer/src/components/automations/use-automation-runs-dashboard-payload.test.tsx": 1297, + "src/renderer/src/components/automations/use-automation-runs-dashboard.test.tsx": 667, + "src/renderer/src/components/automations/use-automations-page-setup-state.test.tsx": 1694, + "src/renderer/src/components/automations/use-automations-page-store-state.folder.test.tsx": 1325, + "src/renderer/src/components/automations/use-external-automation-scope-retention.test.tsx": 167, + "src/renderer/src/components/automations/use-selected-automation-run-history.test.tsx": 572, + "src/renderer/src/components/browser-cookie-import-google-disclosure.test.tsx": 713, + "src/renderer/src/components/browser-favicon.test.tsx": 288, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.chrome-parity.test.tsx": 2840, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.dead-guest.test.tsx": 2567, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.download-notices.test.tsx": 2156, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.drag-focus.test.tsx": 2322, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.failure-remount.test.tsx": 2055, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.markup.test.tsx": 3462, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.popup-notices.test.tsx": 2096, + "src/renderer/src/components/browser-pane/ClientHostedBrowserPagePane.test.tsx": 1921, + "src/renderer/src/components/browser-pane/ReopenBrowserPageOnServerButton.test.tsx": 365, + "src/renderer/src/components/browser-pane/annotate/BrowserAnnotationSendMenuContent.test.tsx": 33, + "src/renderer/src/components/browser-pane/annotate/GrabConfirmationSheet.test.ts": 109, + "src/renderer/src/components/browser-pane/annotate/MarkupOverlay.test.tsx": 627, + "src/renderer/src/components/browser-pane/annotate/browser-annotation-output.test.ts": 62, + "src/renderer/src/components/browser-pane/annotate/browser-grab-toast-owner-cleanup.test.tsx": 885, + "src/renderer/src/components/browser-pane/annotate/browser-page-annotation-tray.test.tsx": 2284, + "src/renderer/src/components/browser-pane/annotate/guest-annotation-viewport-bridge.test.ts": 44, + "src/renderer/src/components/browser-pane/annotate/markup-drawing-model.test.ts": 52, + "src/renderer/src/components/browser-pane/annotate/markup-gesture.test.ts": 75, + "src/renderer/src/components/browser-pane/annotate/markup-screenshot-compose.test.ts": 33, + "src/renderer/src/components/browser-pane/annotate/markup-shape-hit-test.test.ts": 55, + "src/renderer/src/components/browser-pane/annotate/markup-shape-render.test.ts": 44, + "src/renderer/src/components/browser-pane/annotate/use-browser-page-annotation-send.test.tsx": 773, + "src/renderer/src/components/browser-pane/annotate/use-browser-page-grab-annotations.test.tsx": 875, + "src/renderer/src/components/browser-pane/annotate/use-markup-draw-hint.test.ts": 209, + "src/renderer/src/components/browser-pane/annotate/useGrabMode.test.ts": 467, + "src/renderer/src/components/browser-pane/annotate/useMarkupEditor.test.ts": 327, + "src/renderer/src/components/browser-pane/annotate/useMarkupPointerHandlers.test.ts": 236, + "src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.test.tsx": 649, + "src/renderer/src/components/browser-pane/assemble-chrome/BrowserFind.test.tsx": 398, + "src/renderer/src/components/browser-pane/assemble-chrome/BrowserPane.webview-preferences.test.ts": 375, + "src/renderer/src/components/browser-pane/assemble-chrome/BrowserPaneOverlayLayer.test.tsx": 1151, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-expansion.test.ts": 31, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-suggestions.test.ts": 323, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-chrome-folded-tools.test.tsx": 324, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-chrome-toolbar.test.tsx": 317, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-deferred-lifecycle.test.tsx": 1345, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-detected-browsers-summary.test.ts": 25, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-egress-indicator.test.tsx": 1917, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-import-hint-visibility.test.ts": 37, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-navigation-control-row.test.tsx": 382, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-page-zoom-indicator.test.tsx": 262, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-pane-page-selection.test.ts": 28, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.adoption-chrome.test.tsx": 17595, + "src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.retention-props.test.tsx": 2189, + "src/renderer/src/components/browser-pane/assemble-chrome/ssh-routed-browser-page-gate.test.tsx": 1630, "src/renderer/src/components/browser-pane/assemble-chrome/use-browser-chrome-tool-fold.test.tsx": 319, - "src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-chrome-focus.test.tsx": 1619, - "src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-find-shortcuts.test.tsx": 1680, - "src/renderer/src/components/browser-pane/browser-client-page-attachment-scan-budget.test.ts": 382, - "src/renderer/src/components/browser-pane/browser-client-page-metadata-publisher.test.ts": 317, - "src/renderer/src/components/browser-pane/browser-client-page-position-driver.test.ts": 398, - "src/renderer/src/components/browser-pane/browser-client-page-renderer-installation.test.ts": 333, - "src/renderer/src/components/browser-pane/browser-client-page-retained-drag-passthrough.test.ts": 394, - "src/renderer/src/components/browser-pane/browser-client-page-retained-registry.test.ts": 435, - "src/renderer/src/components/browser-pane/browser-download-destination-toast.test.ts": 53, - "src/renderer/src/components/browser-pane/browser-reopen-on-server.test.ts": 287, - "src/renderer/src/components/browser-pane/client-hosted-browser-unavailable-notice.test.tsx": 359, - "src/renderer/src/components/browser-pane/describe-page/browser-annotation-geometry.test.ts": 119, - "src/renderer/src/components/browser-pane/describe-page/browser-artifact-upload.test.ts": 1571, - "src/renderer/src/components/browser-pane/describe-page/browser-favicon-url.test.ts": 48, - "src/renderer/src/components/browser-pane/describe-page/browser-overlay-shortcut-target.test.ts": 1332, - "src/renderer/src/components/browser-pane/describe-page/browser-page-url-display.test.ts": 1382, - "src/renderer/src/components/browser-pane/describe-page/live-browser-url-registry.test.ts": 42, - "src/renderer/src/components/browser-pane/host-guest/browser-automation-visibility.test.ts": 52, - "src/renderer/src/components/browser-pane/host-guest/browser-focus.test.ts": 149, - "src/renderer/src/components/browser-pane/host-guest/browser-guest-eviction-veto.test.ts": 1390, - "src/renderer/src/components/browser-pane/host-guest/browser-guest-page-id-identity.test.ts": 1745, - "src/renderer/src/components/browser-pane/host-guest/browser-guest-paint-retention.test.ts": 2307, - "src/renderer/src/components/browser-pane/host-guest/browser-guest-worktree-retention.test.ts": 2078, - "src/renderer/src/components/browser-pane/host-guest/browser-keyboard.test.ts": 151, - "src/renderer/src/components/browser-pane/host-guest/browser-page-annotation-document.test.ts": 1878, - "src/renderer/src/components/browser-pane/host-guest/browser-page-evicted-guest-recovery.test.ts": 553, - "src/renderer/src/components/browser-pane/host-guest/browser-page-favicon-retention.test.ts": 1737, - "src/renderer/src/components/browser-pane/host-guest/browser-page-guest-recovery.test.ts": 514, - "src/renderer/src/components/browser-pane/host-guest/browser-page-paintability.test.ts": 48, - "src/renderer/src/components/browser-pane/host-guest/browser-page-registration-ownership.test.ts": 684, - "src/renderer/src/components/browser-pane/host-guest/browser-page-viewport.test.ts": 179, - "src/renderer/src/components/browser-pane/host-guest/browser-page-webview-surface.test.ts": 397, - "src/renderer/src/components/browser-pane/host-guest/browser-page-webview.test.ts": 388, - "src/renderer/src/components/browser-pane/host-guest/browser-page-zoom.test.ts": 74, - "src/renderer/src/components/browser-pane/host-guest/browser-system-resume.test.ts": 155, - "src/renderer/src/components/browser-pane/host-guest/browser-worktree-surface-paintability.test.ts": 55, - "src/renderer/src/components/browser-pane/host-guest/use-browser-page-keyboard-shortcuts.split.test.tsx": 2135, - "src/renderer/src/components/browser-pane/host-guest/use-browser-page-slot-viewport.test.ts": 316, - "src/renderer/src/components/browser-pane/host-guest/use-browser-page-viewport-scroll-reporting.test.tsx": 243, - "src/renderer/src/components/browser-pane/host-guest/use-webview-drag-passthrough-active.test.tsx": 246, - "src/renderer/src/components/browser-pane/host-guest/webview-registry.test.ts": 107, - "src/renderer/src/components/browser-pane/navigate/browser-address-bar-navigation.test.ts": 319, - "src/renderer/src/components/browser-pane/navigate/browser-download-progress.test.ts": 147, - "src/renderer/src/components/browser-pane/navigate/browser-load-failure-overlay.test.tsx": 808, - "src/renderer/src/components/browser-pane/navigate/browser-notices.test.ts": 101, - "src/renderer/src/components/browser-pane/navigate/browser-page-download-activity.test.ts": 64, - "src/renderer/src/components/browser-pane/navigate/browser-reload-action.test.ts": 54, - "src/renderer/src/components/browser-pane/navigate/browser-webview-load-failure.test.ts": 290, - "src/renderer/src/components/browser-pane/navigate/chromium-error-page-poll-visibility.test.ts": 1762, - "src/renderer/src/components/browser-pane/navigate/chromium-error-page-polling.test.ts": 48, - "src/renderer/src/components/browser-pane/navigate/navigate-browser-page-notebook-link.test.ts": 510, - "src/renderer/src/components/browser-pane/navigate/use-browser-page-navigation-downloads.file-drop.test.tsx": 1699, - "src/renderer/src/components/browser-pane/navigate/use-browser-page-reload-actions.test.tsx": 1593, - "src/renderer/src/components/browser-pane/restored-client-hosted-recovery-window.test.tsx": 2547, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-frame-style.test.ts": 49, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-keyboard.test.ts": 41, + "src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-chrome-focus.test.tsx": 1676, + "src/renderer/src/components/browser-pane/assemble-chrome/use-browser-page-find-shortcuts.test.tsx": 1557, + "src/renderer/src/components/browser-pane/browser-client-page-attachment-scan-budget.test.ts": 494, + "src/renderer/src/components/browser-pane/browser-client-page-metadata-publisher.test.ts": 297, + "src/renderer/src/components/browser-pane/browser-client-page-position-driver.test.ts": 447, + "src/renderer/src/components/browser-pane/browser-client-page-renderer-installation.test.ts": 400, + "src/renderer/src/components/browser-pane/browser-client-page-retained-drag-passthrough.test.ts": 405, + "src/renderer/src/components/browser-pane/browser-client-page-retained-registry.test.ts": 445, + "src/renderer/src/components/browser-pane/browser-download-destination-toast.test.ts": 37, + "src/renderer/src/components/browser-pane/browser-reopen-on-server.test.ts": 232, + "src/renderer/src/components/browser-pane/client-hosted-browser-unavailable-notice.test.tsx": 553, + "src/renderer/src/components/browser-pane/describe-page/browser-annotation-geometry.test.ts": 92, + "src/renderer/src/components/browser-pane/describe-page/browser-artifact-upload.test.ts": 1417, + "src/renderer/src/components/browser-pane/describe-page/browser-favicon-url.test.ts": 30, + "src/renderer/src/components/browser-pane/describe-page/browser-overlay-shortcut-target.test.ts": 1497, + "src/renderer/src/components/browser-pane/describe-page/browser-page-url-display.test.ts": 1509, + "src/renderer/src/components/browser-pane/describe-page/live-browser-url-registry.test.ts": 45, + "src/renderer/src/components/browser-pane/host-guest/agent-input-focus-guard.test.ts": 159, + "src/renderer/src/components/browser-pane/host-guest/browser-automation-visibility.test.ts": 33, + "src/renderer/src/components/browser-pane/host-guest/browser-focus.test.ts": 157, + "src/renderer/src/components/browser-pane/host-guest/browser-guest-eviction-veto.test.ts": 1259, + "src/renderer/src/components/browser-pane/host-guest/browser-guest-page-id-identity.test.ts": 1300, + "src/renderer/src/components/browser-pane/host-guest/browser-guest-paint-retention.test.ts": 3557, + "src/renderer/src/components/browser-pane/host-guest/browser-guest-worktree-retention.test.ts": 1937, + "src/renderer/src/components/browser-pane/host-guest/browser-keyboard.test.ts": 190, + "src/renderer/src/components/browser-pane/host-guest/browser-page-annotation-document.test.ts": 1555, + "src/renderer/src/components/browser-pane/host-guest/browser-page-evicted-guest-recovery.test.ts": 804, + "src/renderer/src/components/browser-pane/host-guest/browser-page-favicon-retention.test.ts": 2623, + "src/renderer/src/components/browser-pane/host-guest/browser-page-guest-recovery.test.ts": 496, + "src/renderer/src/components/browser-pane/host-guest/browser-page-mount-admission.test.ts": 1485, + "src/renderer/src/components/browser-pane/host-guest/browser-page-paintability.test.ts": 40, + "src/renderer/src/components/browser-pane/host-guest/browser-page-registration-ownership.test.ts": 688, + "src/renderer/src/components/browser-pane/host-guest/browser-page-viewport.test.ts": 209, + "src/renderer/src/components/browser-pane/host-guest/browser-page-webview-surface.test.ts": 454, + "src/renderer/src/components/browser-pane/host-guest/browser-page-webview.test.ts": 404, + "src/renderer/src/components/browser-pane/host-guest/browser-page-zoom.test.ts": 65, + "src/renderer/src/components/browser-pane/host-guest/browser-system-resume.test.ts": 163, + "src/renderer/src/components/browser-pane/host-guest/browser-worktree-surface-paintability.test.ts": 48, + "src/renderer/src/components/browser-pane/host-guest/use-browser-page-keyboard-shortcuts.split.test.tsx": 1419, + "src/renderer/src/components/browser-pane/host-guest/use-browser-page-slot-viewport.test.ts": 231, + "src/renderer/src/components/browser-pane/host-guest/use-browser-page-viewport-scroll-reporting.test.tsx": 244, + "src/renderer/src/components/browser-pane/host-guest/use-webview-drag-passthrough-active.test.tsx": 258, + "src/renderer/src/components/browser-pane/host-guest/webview-registry.test.ts": 110, + "src/renderer/src/components/browser-pane/navigate/browser-address-bar-navigation.test.ts": 262, + "src/renderer/src/components/browser-pane/navigate/browser-download-progress.test.ts": 49, + "src/renderer/src/components/browser-pane/navigate/browser-file-editor-open.test.ts": 1310, + "src/renderer/src/components/browser-pane/navigate/browser-load-failure-overlay.test.tsx": 725, + "src/renderer/src/components/browser-pane/navigate/browser-notices.test.ts": 92, + "src/renderer/src/components/browser-pane/navigate/browser-page-download-activity.test.ts": 44, + "src/renderer/src/components/browser-pane/navigate/browser-reload-action.test.ts": 37, + "src/renderer/src/components/browser-pane/navigate/browser-webview-load-failure.test.ts": 276, + "src/renderer/src/components/browser-pane/navigate/chromium-error-page-poll-visibility.test.ts": 1449, + "src/renderer/src/components/browser-pane/navigate/chromium-error-page-polling.test.ts": 33, + "src/renderer/src/components/browser-pane/navigate/navigate-browser-page-notebook-link.test.ts": 526, + "src/renderer/src/components/browser-pane/navigate/use-browser-page-navigation-downloads.file-drop.test.tsx": 1590, + "src/renderer/src/components/browser-pane/navigate/use-browser-page-reload-actions.test.tsx": 1545, + "src/renderer/src/components/browser-pane/restored-client-hosted-recovery-window.test.tsx": 1767, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-frame-style.test.ts": 31, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-keyboard.test.ts": 37, "src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-input-model.test.ts": 49, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-pane.address-bar.test.tsx": 2342, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-pane.chrome-chords.test.tsx": 2187, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-service-failure.test.ts": 390, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-errors.test.ts": 132, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-lifecycle.test.ts": 409, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-restart-scheduler.test.ts": 69, - "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-status.test.ts": 80, - "src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-page-navigation.test.ts": 1962, - "src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-stream-activation.test.ts": 338, - "src/renderer/src/components/browser-pane/use-ssh-workspace-browser-route.host-connection.test.tsx": 1836, - "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx": 3852, - "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx": 3045, - "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts": 280, - "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-identity.test.ts": 62, - "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-external-link-confirmation.test.ts": 146, - "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-webview-attach.test.ts": 154, - "src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.lazy-ref.test.tsx": 272, - "src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts": 191, - "src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx": 210, - "src/renderer/src/components/browser-webauthn-account-dialog.test.tsx": 541, - "src/renderer/src/components/cmd-j/palette-duplicate-key-ghost-rows.test.tsx": 191, - "src/renderer/src/components/cmd-j/palette-filter-option-list.test.ts": 49, - "src/renderer/src/components/cmd-j/palette-filter-options.test.ts": 144, - "src/renderer/src/components/cmd-j/palette-filter.test.ts": 92, - "src/renderer/src/components/cmd-j/palette-focus-restore-target.test.ts": 162, - "src/renderer/src/components/cmd-j/palette-host-badge.test.ts": 120, - "src/renderer/src/components/cmd-j/palette-list-entry-render-keys.test.ts": 53, - "src/renderer/src/components/cmd-j/palette-live-status.test.tsx": 1819, - "src/renderer/src/components/cmd-j/palette-query-tokens.test.ts": 66, - "src/renderer/src/components/cmd-j/palette-results.test.ts": 266, - "src/renderer/src/components/cmd-j/palette-section-render-cap.test.ts": 79, - "src/renderer/src/components/cmd-j/palette-session-age.test.ts": 79, - "src/renderer/src/components/cmd-j/plugin-quick-actions.test.ts": 123, - "src/renderer/src/components/cmd-j/quick-action-context.test.ts": 1708, - "src/renderer/src/components/cmd-j/worktree-checks-review-index.test.ts": 163, - "src/renderer/src/components/cmd-j/worktree-palette-cache-inputs.test.ts": 58, - "src/renderer/src/components/codex-restart-chip.test.tsx": 1738, - "src/renderer/src/components/comment-code-context-state.test.ts": 56, - "src/renderer/src/components/comment-reply-target-state.test.ts": 54, - "src/renderer/src/components/confirmation-dialog.test.tsx": 833, - "src/renderer/src/components/confirmation-skip-preference.test.ts": 155, - "src/renderer/src/components/connect-dialog-outside-dismiss.test.tsx": 839, - "src/renderer/src/components/contextual-tours/ContextualTourControl.test.ts": 1374, - "src/renderer/src/components/contextual-tours/ContextualTourOverlay.remeasure.test.tsx": 2051, - "src/renderer/src/components/contextual-tours/ContextualTourOverlay.test.tsx": 1496, - "src/renderer/src/components/contextual-tours/ContextualTourOverlay.visibility.test.tsx": 4049, - "src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.localization.test.tsx": 1962, - "src/renderer/src/components/contextual-tours/contextual-tour-floating-position.test.ts": 2054, - "src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts": 69, - "src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts": 1941, - "src/renderer/src/components/contextual-tours/contextual-tour-step-actions.test.ts": 68, - "src/renderer/src/components/contextual-tours/request-contextual-tour-when-ready.test.ts": 1339, - "src/renderer/src/components/contextual-tours/use-contextual-tour.test.ts": 1428, - "src/renderer/src/components/contextual-tours/workspace-creation-tour-handoff.test.ts": 1400, - "src/renderer/src/components/crash-report/CrashReportDialogSurface.overflow.test.tsx": 544, - "src/renderer/src/components/crash-report/crash-report-submit-notice.test.ts": 103, - "src/renderer/src/components/crash-report/use-crash-report-copy.test.tsx": 301, - "src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx": 1462, - "src/renderer/src/components/dashboard-popout/AgentKanbanCard.test.tsx": 630, - "src/renderer/src/components/dashboard-popout/AgentTerminalDialog.test.tsx": 732, - "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.clipboard-routes.test.tsx": 2011, - "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.frame-lifecycle.test.tsx": 2133, - "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.option-dead-key.test.tsx": 1271, - "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-geometry.test.tsx": 1908, - "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-lifetime.test.tsx": 2148, - "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.test.tsx": 2784, - "src/renderer/src/components/dashboard-popout/DashboardHostBadge.test.tsx": 470, - "src/renderer/src/components/dashboard-popout/DashboardPopoutRoot.test.tsx": 323, - "src/renderer/src/components/dashboard-popout/agent-board-filtering.test.ts": 56, - "src/renderer/src/components/dashboard-popout/dashboard-agent-status-patch.test.ts": 67, - "src/renderer/src/components/dashboard-popout/preview-grid-claim.test.ts": 275, - "src/renderer/src/components/dashboard-popout/preview-terminal-box-fit-lifecycle.test.ts": 189, - "src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts": 439, - "src/renderer/src/components/dashboard-popout/preview-terminal-options.test.ts": 155, - "src/renderer/src/components/dashboard-popout/preview-terminal-right-click-paste.test.ts": 1585, - "src/renderer/src/components/dashboard-popout/preview-terminal-shortcuts.test.ts": 175, - "src/renderer/src/components/dashboard-popout/preview-terminal-snapshot-replay.test.ts": 58, - "src/renderer/src/components/dashboard-popout/remote-runtime-terminal-preview-api.test.ts": 107, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-pane.address-bar.test.tsx": 1659, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-pane.chrome-chords.test.tsx": 2326, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-service-failure.test.ts": 342, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-errors.test.ts": 87, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-lifecycle.test.ts": 422, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-restart-scheduler.test.ts": 52, + "src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-status.test.ts": 60, + "src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-page-navigation.test.ts": 1472, + "src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-stream-activation.test.ts": 274, + "src/renderer/src/components/browser-pane/use-ssh-workspace-browser-route.host-connection.test.tsx": 1677, + "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.failure-message.test.tsx": 2588, + "src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.toolbar.test.tsx": 3892, + "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-artifact-share.test.ts": 360, + "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.test.ts": 254, + "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-identity.test.ts": 31, + "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-external-link-confirmation.test.ts": 123, + "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-share-key-parity.test.ts": 1124, + "src/renderer/src/components/browser-pane/workspace-doc/doc-preview-webview-attach.test.ts": 198, + "src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.lazy-ref.test.tsx": 275, + "src/renderer/src/components/browser-pane/workspace-doc/use-doc-preview-guest-tools.test.ts": 256, + "src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.test.tsx": 194, + "src/renderer/src/components/browser-webauthn-account-dialog.test.tsx": 461, + "src/renderer/src/components/cmd-j/palette-duplicate-key-ghost-rows.test.tsx": 216, + "src/renderer/src/components/cmd-j/palette-filter-option-list.test.ts": 46, + "src/renderer/src/components/cmd-j/palette-filter-options.test.ts": 338, + "src/renderer/src/components/cmd-j/palette-filter.test.ts": 75, + "src/renderer/src/components/cmd-j/palette-focus-restore-target.test.ts": 167, + "src/renderer/src/components/cmd-j/palette-host-badge.test.ts": 313, + "src/renderer/src/components/cmd-j/palette-list-entry-render-keys.test.ts": 35, + "src/renderer/src/components/cmd-j/palette-live-status.test.tsx": 1575, + "src/renderer/src/components/cmd-j/palette-query-tokens.test.ts": 40, + "src/renderer/src/components/cmd-j/palette-results.test.ts": 185, + "src/renderer/src/components/cmd-j/palette-section-render-cap.test.ts": 48, + "src/renderer/src/components/cmd-j/palette-session-age.test.ts": 67, + "src/renderer/src/components/cmd-j/plugin-quick-actions.test.ts": 101, + "src/renderer/src/components/cmd-j/quick-action-context.test.ts": 1337, + "src/renderer/src/components/cmd-j/task-search-activation.test.ts": 1085, + "src/renderer/src/components/cmd-j/task-search-requests.test.ts": 1413, + "src/renderer/src/components/cmd-j/worktree-checks-review-index.test.ts": 145, + "src/renderer/src/components/cmd-j/worktree-palette-cache-inputs.test.ts": 32, + "src/renderer/src/components/codex-restart-chip.test.tsx": 1731, + "src/renderer/src/components/comment-code-context-state.test.ts": 36, + "src/renderer/src/components/comment-reply-target-state.test.ts": 29, + "src/renderer/src/components/confirmation-dialog.test.tsx": 780, + "src/renderer/src/components/confirmation-skip-preference.test.ts": 149, + "src/renderer/src/components/connect-dialog-outside-dismiss.test.tsx": 1070, + "src/renderer/src/components/contextual-tours/ContextualTourControl.test.ts": 1266, + "src/renderer/src/components/contextual-tours/ContextualTourOverlay.remeasure.test.tsx": 1744, + "src/renderer/src/components/contextual-tours/ContextualTourOverlay.test.tsx": 1645, + "src/renderer/src/components/contextual-tours/ContextualTourOverlay.visibility.test.tsx": 3997, + "src/renderer/src/components/contextual-tours/ContextualTourOverlaySurface.localization.test.tsx": 1757, + "src/renderer/src/components/contextual-tours/contextual-tour-floating-position.test.ts": 2095, + "src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts": 49, + "src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts": 1641, + "src/renderer/src/components/contextual-tours/contextual-tour-step-actions.test.ts": 46, + "src/renderer/src/components/contextual-tours/request-contextual-tour-when-ready.test.ts": 1364, + "src/renderer/src/components/contextual-tours/use-contextual-tour.test.ts": 1349, + "src/renderer/src/components/contextual-tours/workspace-creation-tour-handoff.test.ts": 1377, + "src/renderer/src/components/crash-report/CrashReportDialogSurface.overflow.test.tsx": 400, + "src/renderer/src/components/crash-report/crash-report-submit-notice.test.ts": 89, + "src/renderer/src/components/crash-report/use-crash-report-copy.test.tsx": 290, + "src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx": 1126, + "src/renderer/src/components/dashboard-popout/AgentKanbanCard.test.tsx": 745, + "src/renderer/src/components/dashboard-popout/AgentTerminalDialog.test.tsx": 687, + "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.clipboard-routes.test.tsx": 2694, + "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.frame-lifecycle.test.tsx": 1920, + "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.option-dead-key.test.tsx": 929, + "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-geometry.test.tsx": 1839, + "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.settings-lifetime.test.tsx": 2118, + "src/renderer/src/components/dashboard-popout/AgentTerminalPreview.test.tsx": 2357, + "src/renderer/src/components/dashboard-popout/DashboardHostBadge.test.tsx": 398, + "src/renderer/src/components/dashboard-popout/DashboardPopoutRoot.test.tsx": 290, + "src/renderer/src/components/dashboard-popout/agent-board-filtering.test.ts": 42, + "src/renderer/src/components/dashboard-popout/dashboard-agent-status-patch.test.ts": 63, + "src/renderer/src/components/dashboard-popout/preview-grid-claim.test.ts": 200, + "src/renderer/src/components/dashboard-popout/preview-terminal-box-fit-lifecycle.test.ts": 221, + "src/renderer/src/components/dashboard-popout/preview-terminal-ime-bridge-kitty-bytes.test.ts": 424, + "src/renderer/src/components/dashboard-popout/preview-terminal-options.test.ts": 185, + "src/renderer/src/components/dashboard-popout/preview-terminal-right-click-paste.test.ts": 1528, + "src/renderer/src/components/dashboard-popout/preview-terminal-shortcuts.test.ts": 213, + "src/renderer/src/components/dashboard-popout/preview-terminal-snapshot-replay.test.ts": 53, + "src/renderer/src/components/dashboard-popout/remote-runtime-terminal-preview-api.test.ts": 59, "src/renderer/src/components/dashboard-popout/terminal-preview-unavailable-message.test.ts": 77, - "src/renderer/src/components/dashboard-popout/useDashboardSnapshot.test.tsx": 290, - "src/renderer/src/components/dashboard/AgentDashboardDrawer.test.tsx": 2233, - "src/renderer/src/components/dashboard/AgentDashboardSettingsMenu.test.tsx": 595, - "src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.test.tsx": 1822, - "src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx": 1741, - "src/renderer/src/components/dashboard/agent-dashboard-open-mode.test.ts": 48, - "src/renderer/src/components/dashboard/agent-finished-timestamp.test.ts": 58, - "src/renderer/src/components/dashboard/agent-row-lineage-model.test.ts": 134, - "src/renderer/src/components/dashboard/agent-row-pane-live-title.test.ts": 52, - "src/renderer/src/components/dashboard/build-dashboard-bucket-counts.allocation.test.ts": 381, - "src/renderer/src/components/dashboard/build-dashboard-bucket-counts.cache.test.ts": 230, - "src/renderer/src/components/dashboard/build-dashboard-bucket-counts.equivalence.test.ts": 296, - "src/renderer/src/components/dashboard/build-dashboard-snapshot-folder-workspace.test.ts": 1527, - "src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts": 1528, - "src/renderer/src/components/dashboard/build-dashboard-snapshot.rows-cache.test.ts": 1542, - "src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts": 1528, - "src/renderer/src/components/dashboard/dashboard-card-context.test.ts": 76, - "src/renderer/src/components/dashboard/dashboard-card-labels.test.ts": 77, - "src/renderer/src/components/dashboard/dashboard-card-terminal-input.test.ts": 1338, - "src/renderer/src/components/dashboard/dashboard-worktree-launch-options.test.ts": 86, - "src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts": 60, - "src/renderer/src/components/dashboard/reveal-dashboard-agent.test.ts": 56, - "src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts": 106, - "src/renderer/src/components/dashboard/useAgentBucketCounts.gate.test.ts": 346, - "src/renderer/src/components/dashboard/useAgentBucketCounts.test.tsx": 273, - "src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx": 1283, - "src/renderer/src/components/dashboard/useLiveDashboardSnapshot.test.ts": 1818, - "src/renderer/src/components/dashboard/useRetainedAgents.test.ts": 1671, - "src/renderer/src/components/dashboard/useRetainedAgentsSync.test.ts": 1484, - "src/renderer/src/components/dictation/DictationIndicator.localization.test.ts": 257, - "src/renderer/src/components/dictation/DictationIndicator.test.tsx": 461, - "src/renderer/src/components/dictation/dictation-audio-meter.test.ts": 44, - "src/renderer/src/components/dictation/dictation-final-segments.test.ts": 48, - "src/renderer/src/components/dictation/dictation-insertion-target.test.ts": 337, - "src/renderer/src/components/dictation/dictation-meter-store.test.tsx": 305, - "src/renderer/src/components/dictation/dictation-stopped-sessions.test.ts": 54, - "src/renderer/src/components/dictation/microphone-devices.test.ts": 309, - "src/renderer/src/components/dictation/use-hold-dictation-gesture.test.tsx": 261, - "src/renderer/src/components/diff-comments/DiffCommentCard.resize.test.tsx": 460, - "src/renderer/src/components/diff-comments/DiffCommentDraftCard.test.tsx": 2015, - "src/renderer/src/components/diff-comments/diff-comment-draft-zone.test.tsx": 2153, - "src/renderer/src/components/diff-comments/diff-comment-drag-auto-scroll.test.ts": 47, - "src/renderer/src/components/diff-comments/diff-comment-line-range.test.ts": 61, - "src/renderer/src/components/diff-comments/diff-comment-popover-outside-click.test.tsx": 1638, - "src/renderer/src/components/diff-comments/diff-comment-popover-position.test.ts": 52, - "src/renderer/src/components/diff-comments/diff-comment-range-drag.test.ts": 9586, - "src/renderer/src/components/diff-comments/diff-comment-zone-mouse-events.test.ts": 54, - "src/renderer/src/components/diff-comments/use-visible-worktree-diff-comments.test.ts": 263, - "src/renderer/src/components/diff-comments/useDiffCommentDecorator.commentable-lines.test.tsx": 2092, - "src/renderer/src/components/diff-comments/useDiffCommentDecorator.model-lifecycle.test.tsx": 8367, - "src/renderer/src/components/diff-comments/useDiffCommentDecorator.range-selection.test.tsx": 5475, - "src/renderer/src/components/editor/ChangesModeView.test.tsx": 345, - "src/renderer/src/components/editor/CheckRunCopyButton.test.tsx": 362, - "src/renderer/src/components/editor/CheckRunDetailsPanel.copy.test.tsx": 1906, - "src/renderer/src/components/editor/ConflictComponents.test.tsx": 213, - "src/renderer/src/components/editor/DiffSectionBody.word-wrap-lifecycle.test.tsx": 405, - "src/renderer/src/components/editor/DiffViewer.word-wrap-lifecycle.test.tsx": 376, - "src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx": 1799, - "src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx": 2378, - "src/renderer/src/components/editor/EditorContent.test.tsx": 2816, - "src/renderer/src/components/editor/EditorEditFileSurface.monaco-containment.test.tsx": 1937, - "src/renderer/src/components/editor/EditorFileLoadErrorView.test.tsx": 388, - "src/renderer/src/components/editor/EditorPanel.markdown-classification-memoization.test.tsx": 3015, - "src/renderer/src/components/editor/EditorPanel.markdown-review-tools-setting.test.tsx": 2316, - "src/renderer/src/components/editor/EditorPanelHeader.test.tsx": 1367, - "src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx": 2121, - "src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.test.tsx": 177, - "src/renderer/src/components/editor/EditorPanelShell.header.test.tsx": 193, - "src/renderer/src/components/editor/ExternalFileChangeBanner.test.tsx": 521, - "src/renderer/src/components/editor/ExternalFileChangeCompareDialog.test.tsx": 719, - "src/renderer/src/components/editor/ImageViewer.test.tsx": 148, - "src/renderer/src/components/editor/IpynbCellEditor.external-sync.test.tsx": 1507, - "src/renderer/src/components/editor/IpynbKernelToolbar.test.tsx": 676, - "src/renderer/src/components/editor/IpynbRendering.test.tsx": 2202, - "src/renderer/src/components/editor/LargeDiffLoadPrompt.test.tsx": 338, - "src/renderer/src/components/editor/MarkdownPreview.image-access.test.tsx": 1100, - "src/renderer/src/components/editor/MarkdownPreview.link-routing.interaction.test.tsx": 1077, - "src/renderer/src/components/editor/MarkdownPreview.test.ts": 2492, - "src/renderer/src/components/editor/MarkdownPreview.toc-visibility-gate.test.tsx": 967, - "src/renderer/src/components/editor/MarkdownPreviewBody.test.tsx": 342, - "src/renderer/src/components/editor/MarkdownTableOfContentsPanel.test.tsx": 1650, - "src/renderer/src/components/editor/MediaViewer.test.tsx": 221, - "src/renderer/src/components/editor/MermaidBlock.test.tsx": 522, - "src/renderer/src/components/editor/MonacoEditor.content-owner.test.tsx": 5776, - "src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx": 6247, - "src/renderer/src/components/editor/MonacoEditor.owner-view-state.test.tsx": 3008, - "src/renderer/src/components/editor/MonacoEditor.system-theme.test.tsx": 2679, - "src/renderer/src/components/editor/NotesSendMenu.test.tsx": 212, - "src/renderer/src/components/editor/PdfFind.test.tsx": 582, - "src/renderer/src/components/editor/ReviewNotesSendMenuContent.test.tsx": 215, - "src/renderer/src/components/editor/RichMarkdownErrorBoundary.lazy-chunk.test.tsx": 344, - "src/renderer/src/components/editor/RichMarkdownLinkBubble.render.test.tsx": 375, - "src/renderer/src/components/editor/RichMarkdownLinkBubble.test.ts": 151, - "src/renderer/src/components/editor/RichMarkdownSearchBar.ime-enter.test.tsx": 423, - "src/renderer/src/components/editor/RichMarkdownSlashMenu.test.tsx": 168, - "src/renderer/src/components/editor/RichMarkdownTableControls.test.tsx": 2455, - "src/renderer/src/components/editor/check-annotation-open.test.ts": 110, - "src/renderer/src/components/editor/check-annotation-path.test.ts": 59, - "src/renderer/src/components/editor/check-job-step-status.test.ts": 49, - "src/renderer/src/components/editor/check-run-clipboard-text.test.ts": 48, - "src/renderer/src/components/editor/check-run-details-fix-with-ai.test.ts": 549, - "src/renderer/src/components/editor/check-run-details-tab.test.ts": 43, - "src/renderer/src/components/editor/closed-editor-model-lifetime.test.ts": 5487, - "src/renderer/src/components/editor/closed-editor-model-registry.test.ts": 12691, - "src/renderer/src/components/editor/closed-editor-model-shell.test.tsx": 3197, - "src/renderer/src/components/editor/closed-editor-tab-cache-sweep.test.ts": 51, - "src/renderer/src/components/editor/closed-editor-tab-disposal.test.ts": 111, - "src/renderer/src/components/editor/closed-editor-view-state-retention.test.ts": 5815, - "src/renderer/src/components/editor/combined-diff-on-demand-load.test.ts": 60, - "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-collapsed-work.test.tsx": 693, - "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-render.test.tsx": 1027, - "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-row-drag.test.tsx": 287, - "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-windowing.test.tsx": 775, - "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree.test.ts": 71, - "src/renderer/src/components/editor/combined-diff/browse-files/use-combined-diff-file-tree-resize.test.tsx": 273, - "src/renderer/src/components/editor/combined-diff/browse-files/use-combined-diff-tree-navigation.test.ts": 255, - "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-initial-section-load.test.ts": 49, - "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-load-scheduler.test.ts": 55, - "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-section-connection.test.ts": 1148, - "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-section-load-state.test.ts": 58, - "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-stale-analysis-work-budget.test.tsx": 769, - "src/renderer/src/components/editor/combined-diff/remember-view/use-combined-diff-view-restore.test.tsx": 731, - "src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-entries.test.ts": 71, - "src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-section-cache-match.test.ts": 55, - "src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-section-scaling.test.tsx": 681, - "src/renderer/src/components/editor/combined-diff/resolve-changes/use-combined-diff-section-index-map.test.tsx": 265, - "src/renderer/src/components/editor/combined-diff/review-controls/combined-diff-commit-message.test.ts": 59, - "src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-render-range.test.ts": 47, - "src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-restore-signal-equivalence.test.tsx": 740, - "src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-scrollbar-drag.test.ts": 68, - "src/renderer/src/components/editor/conflict-review-file-tree-windowing.test.tsx": 1135, - "src/renderer/src/components/editor/csv/CsvGrid.test.tsx": 765, - "src/renderer/src/components/editor/csv/CsvViewer.delimiter.test.tsx": 2535, - "src/renderer/src/components/editor/csv/CsvViewer.editing.test.tsx": 3037, - "src/renderer/src/components/editor/csv/csv-byte-index.test.ts": 1733, - "src/renderer/src/components/editor/csv/csv-cell-links.test.tsx": 294, - "src/renderer/src/components/editor/csv/csv-column-width-preferences.test.ts": 144, - "src/renderer/src/components/editor/csv/csv-paged-preview.test.ts": 94, - "src/renderer/src/components/editor/csv/csv-parse-budget.test.ts": 100, - "src/renderer/src/components/editor/csv/csv-parse.test.ts": 125, - "src/renderer/src/components/editor/csv/csv-pending-save.test.ts": 1349, - "src/renderer/src/components/editor/csv/csv-preview-save-safety.test.ts": 1483, - "src/renderer/src/components/editor/csv/csv-table-interactions.test.ts": 118, - "src/renderer/src/components/editor/csv/csv-text-document.test.ts": 139, - "src/renderer/src/components/editor/csv/useCsvPagedPreview.test.tsx": 396, - "src/renderer/src/components/editor/details-markdown-html.test.ts": 78, - "src/renderer/src/components/editor/diff-editor-hide-unchanged-options.test.ts": 54, - "src/renderer/src/components/editor/diff-editor-line-number-options.test.ts": 73, - "src/renderer/src/components/editor/diff-editor-shift-wheel-scroll.test.ts": 155, - "src/renderer/src/components/editor/diff-editor-whitespace-options.test.ts": 48, - "src/renderer/src/components/editor/diff-editor-word-wrap-options.test.ts": 180, - "src/renderer/src/components/editor/diff-line-stats.test.ts": 58, - "src/renderer/src/components/editor/diff-model-swap-view-state.test.ts": 145, - "src/renderer/src/components/editor/diff-monaco-model-disposal.test.ts": 51, - "src/renderer/src/components/editor/diff-navigation-context.test.tsx": 1732, - "src/renderer/src/components/editor/diff-section-comment-submit.test.ts": 99, - "src/renderer/src/components/editor/diff-section-layout.test.ts": 87, - "src/renderer/src/components/editor/diff-section-preview.test.ts": 1498, - "src/renderer/src/components/editor/diff-viewer-large-diff-save-action.test.ts": 93, - "src/renderer/src/components/editor/drag-selection-guard-lifetime.test.ts": 563, - "src/renderer/src/components/editor/editor-autosave-conflict-flow.test.ts": 634, - "src/renderer/src/components/editor/editor-autosave-controller.test.ts": 652, - "src/renderer/src/components/editor/editor-autosave.test.ts": 279, - "src/renderer/src/components/editor/editor-cmd-save-target.test.ts": 258, - "src/renderer/src/components/editor/editor-content-dirty-state.test.ts": 83, - "src/renderer/src/components/editor/editor-dropped-file-open.escaping-link.test.ts": 362, - "src/renderer/src/components/editor/editor-dropped-file-open.test.ts": 1242, - "src/renderer/src/components/editor/editor-external-watch-path-index.test.ts": 1264, - "src/renderer/src/components/editor/editor-file-save-attempt.test.ts": 57, - "src/renderer/src/components/editor/editor-header-file-rename.test.ts": 1748, - "src/renderer/src/components/editor/editor-header.test.ts": 59, - "src/renderer/src/components/editor/editor-labels.test.ts": 54, - "src/renderer/src/components/editor/editor-model-host-isolation.test.ts": 2625, - "src/renderer/src/components/editor/editor-model-owner.test.ts": 309, - "src/renderer/src/components/editor/editor-model-uri.test.ts": 131, - "src/renderer/src/components/editor/editor-panel-diff-reload.test.ts": 46, - "src/renderer/src/components/editor/editor-panel-draft-selector.test.ts": 64, - "src/renderer/src/components/editor/editor-panel-git-entry-selector.test.ts": 54, - "src/renderer/src/components/editor/editor-panel-render-model.test.ts": 1971, - "src/renderer/src/components/editor/editor-path-display.test.ts": 52, - "src/renderer/src/components/editor/editor-path-move-inflight.test.ts": 54, - "src/renderer/src/components/editor/editor-pending-flush.test.ts": 38, - "src/renderer/src/components/editor/editor-restored-tab-conflict-scan.test.ts": 624, - "src/renderer/src/components/editor/editor-save-user-named-tab.test.ts": 547, - "src/renderer/src/components/editor/editor-self-write-registry.test.ts": 93, - "src/renderer/src/components/editor/editor-shortcuts.test.ts": 225, - "src/renderer/src/components/editor/editor-untitled-dont-save-dialogs.test.tsx": 668, - "src/renderer/src/components/editor/editor-untitled-save-lifecycle.test.ts": 837, - "src/renderer/src/components/editor/export-active-markdown.test.ts": 128, - "src/renderer/src/components/editor/file-editor-word-wrap-options.test.ts": 55, - "src/renderer/src/components/editor/image-viewer-zoom.test.ts": 63, - "src/renderer/src/components/editor/ipynb-ansi.test.ts": 70, - "src/renderer/src/components/editor/ipynb-code-cell-lines.test.ts": 142, - "src/renderer/src/components/editor/ipynb-kernel-outputs.test.ts": 318, - "src/renderer/src/components/editor/ipynb-kernel-session-start-lifetime.test.ts": 113, - "src/renderer/src/components/editor/ipynb-kernel-session.test.ts": 182, - "src/renderer/src/components/editor/ipynb-kernel-setup-commands.test.ts": 58, - "src/renderer/src/components/editor/ipynb-parse.test.ts": 80, - "src/renderer/src/components/editor/large-diff-render-limit.test.ts": 168, - "src/renderer/src/components/editor/large-markdown-render-freeze.test.tsx": 2232, - "src/renderer/src/components/editor/line-copy-path.test.ts": 45, - "src/renderer/src/components/editor/local-log-tail-decoder.test.ts": 113, - "src/renderer/src/components/editor/markdown-artifact-upload.test.ts": 68, - "src/renderer/src/components/editor/markdown-dirty-state.test.ts": 274, - "src/renderer/src/components/editor/markdown-doc-completions.test.ts": 59, - "src/renderer/src/components/editor/markdown-doc-links.test.ts": 139, - "src/renderer/src/components/editor/markdown-document-list-request.test.ts": 379, - "src/renderer/src/components/editor/markdown-document-worktree-path-selector.test.ts": 1527, - "src/renderer/src/components/editor/markdown-export-extract.test.ts": 218, - "src/renderer/src/components/editor/markdown-export-html.test.ts": 58, - "src/renderer/src/components/editor/markdown-frontmatter.test.ts": 53, - "src/renderer/src/components/editor/markdown-heading-slug.test.ts": 42, - "src/renderer/src/components/editor/markdown-internal-links.test.ts": 74, - "src/renderer/src/components/editor/markdown-preview-anchor-navigation.test.ts": 135, - "src/renderer/src/components/editor/markdown-preview-annotation-shortcut.test.ts": 177, - "src/renderer/src/components/editor/markdown-preview-controls.test.ts": 91, - "src/renderer/src/components/editor/markdown-preview-document-client.test.ts": 58, - "src/renderer/src/components/editor/markdown-preview-document-search-budget.test.ts": 357, - "src/renderer/src/components/editor/markdown-preview-document-search-cache.test.ts": 552, - "src/renderer/src/components/editor/markdown-preview-document-search-scope.test.ts": 385, - "src/renderer/src/components/editor/markdown-preview-document.test.ts": 1525, - "src/renderer/src/components/editor/markdown-preview-links.test.ts": 67, - "src/renderer/src/components/editor/markdown-preview-local-images.test.ts": 1715, - "src/renderer/src/components/editor/markdown-preview-review-frame-lifecycle.test.tsx": 325, - "src/renderer/src/components/editor/markdown-preview-row-measurements.test.ts": 167, - "src/renderer/src/components/editor/markdown-preview-search-crash.test.tsx": 230, - "src/renderer/src/components/editor/markdown-preview-search-memory.test.ts": 171, - "src/renderer/src/components/editor/markdown-preview-search-offsets.test.ts": 518, - "src/renderer/src/components/editor/markdown-preview-search.test.ts": 79, - "src/renderer/src/components/editor/markdown-preview-table-chunks.test.ts": 6270, - "src/renderer/src/components/editor/markdown-preview-url-transform.test.ts": 95, - "src/renderer/src/components/editor/markdown-reference-link-normalization.test.ts": 77, - "src/renderer/src/components/editor/markdown-render-mode.test.ts": 50, - "src/renderer/src/components/editor/markdown-rich-comment-scan.test.ts": 323, - "src/renderer/src/components/editor/markdown-rich-line-scan.test.ts": 1628, - "src/renderer/src/components/editor/markdown-rich-mode-eligibility-cache.test.ts": 3909, - "src/renderer/src/components/editor/markdown-rich-mode.test.ts": 2831, - "src/renderer/src/components/editor/markdown-rich-size-limit.test.ts": 340, - "src/renderer/src/components/editor/markdown-round-trip.test.ts": 2101, - "src/renderer/src/components/editor/markdown-scanner-boundaries.test.ts": 100, - "src/renderer/src/components/editor/markdown-table-of-contents.test.ts": 166, - "src/renderer/src/components/editor/markdown-toc-collapse-state.test.ts": 53, - "src/renderer/src/components/editor/markdown-toc-visibility-gate.test.ts": 105, - "src/renderer/src/components/editor/markdown-tokenizer-start.test.ts": 97, - "src/renderer/src/components/editor/mermaid-config.test.ts": 44, - "src/renderer/src/components/editor/monaco-auto-height.test.ts": 114, - "src/renderer/src/components/editor/monaco-codebase-search.test.ts": 52, - "src/renderer/src/components/editor/monaco-conflict-decorations.test.ts": 68, - "src/renderer/src/components/editor/monaco-content-sync.test.ts": 67, - "src/renderer/src/components/editor/monaco-content-sync.undo-history.test.ts": 1193, - "src/renderer/src/components/editor/monaco-context-menu-paste.test.ts": 208, - "src/renderer/src/components/editor/monaco-cursor-line-lifetime.test.ts": 1102, - "src/renderer/src/components/editor/monaco-find-options.test.ts": 44, - "src/renderer/src/components/editor/monaco-find-widget.test.ts": 167, - "src/renderer/src/components/editor/monaco-large-text-paste.test.ts": 208, - "src/renderer/src/components/editor/monaco-markdown-doc-completions-retention.test.ts": 64, - "src/renderer/src/components/editor/monaco-markdown-doc-link-decorations.offset-scan.test.ts": 267, - "src/renderer/src/components/editor/monaco-markdown-doc-link-decorations.test.ts": 82, - "src/renderer/src/components/editor/monaco-markdown-selection-annotation.test.ts": 49, - "src/renderer/src/components/editor/monaco-programmatic-sync.test.ts": 54, - "src/renderer/src/components/editor/monaco-reveal-range.test.ts": 45, - "src/renderer/src/components/editor/monaco-view-state-persistence.test.ts": 55, - "src/renderer/src/components/editor/pdf-js-document-options.test.ts": 46, - "src/renderer/src/components/editor/pdf-scale-preference-storage.test.ts": 65, - "src/renderer/src/components/editor/pdf-scale-preference.test.ts": 71, - "src/renderer/src/components/editor/pdf-view-position.test.ts": 73, - "src/renderer/src/components/editor/pdf-viewer-failed-load-recovery.test.tsx": 2246, - "src/renderer/src/components/editor/pdf-viewer-zoom-ownership.test.tsx": 933, - "src/renderer/src/components/editor/pdf-zoom-request.test.ts": 137, - "src/renderer/src/components/editor/pending-editor-focus-request.test.ts": 34, - "src/renderer/src/components/editor/position-stable-node-view-update.test.ts": 46, - "src/renderer/src/components/editor/raw-markdown-comment-scan.test.ts": 82, - "src/renderer/src/components/editor/raw-markdown-html-line-start.test.ts": 62, - "src/renderer/src/components/editor/raw-markdown-html.blank-run.test.ts": 195, - "src/renderer/src/components/editor/raw-markdown-html.details-fences.test.ts": 64, - "src/renderer/src/components/editor/restored-editor-owner-save-lifecycle.test.ts": 2235, - "src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.test.tsx": 1466, - "src/renderer/src/components/editor/rich-markdown-async-image-paste.test.ts": 1484, - "src/renderer/src/components/editor/rich-markdown-async-image-table.test.ts": 1133, - "src/renderer/src/components/editor/rich-markdown-auto-focus.test.ts": 197, - "src/renderer/src/components/editor/rich-markdown-code-block-languages.test.ts": 92, - "src/renderer/src/components/editor/rich-markdown-commands.test.ts": 1846, - "src/renderer/src/components/editor/rich-markdown-comment-blocks.test.ts": 2049, - "src/renderer/src/components/editor/rich-markdown-context-command-routing.test.ts": 1887, - "src/renderer/src/components/editor/rich-markdown-cut.test.ts": 293, - "src/renderer/src/components/editor/rich-markdown-details-keyboard.test.ts": 3545, - "src/renderer/src/components/editor/rich-markdown-doc-link-keyboard.test.ts": 306, - "src/renderer/src/components/editor/rich-markdown-doc-link-resolution.test.ts": 248, - "src/renderer/src/components/editor/rich-markdown-doc-link.test.ts": 51, - "src/renderer/src/components/editor/rich-markdown-editor-click-routing.test.ts": 695, - "src/renderer/src/components/editor/rich-markdown-editor-config.test.ts": 1952, - "src/renderer/src/components/editor/rich-markdown-empty-paragraph-delete.test.ts": 208, - "src/renderer/src/components/editor/rich-markdown-escape-fidelity.test.ts": 1526, - "src/renderer/src/components/editor/rich-markdown-extension.test.ts": 260, - "src/renderer/src/components/editor/rich-markdown-html-superscript-link.test.ts": 3213, - "src/renderer/src/components/editor/rich-markdown-image-context.test.ts": 675, - "src/renderer/src/components/editor/rich-markdown-image-cut.test.ts": 2226, - "src/renderer/src/components/editor/rich-markdown-image-insert-code-block.test.ts": 1165, - "src/renderer/src/components/editor/rich-markdown-image-insert.test.ts": 770, - "src/renderer/src/components/editor/rich-markdown-image-insertion-target.test.ts": 2190, - "src/renderer/src/components/editor/rich-markdown-inline-source-tokenization.test.ts": 2521, - "src/renderer/src/components/editor/rich-markdown-key-handler.test.ts": 2196, - "src/renderer/src/components/editor/rich-markdown-large-html.test.ts": 3625, - "src/renderer/src/components/editor/rich-markdown-large-text-paste.fidelity.test.ts": 1997, - "src/renderer/src/components/editor/rich-markdown-large-text-paste.selection.test.ts": 2208, - "src/renderer/src/components/editor/rich-markdown-large-text-paste.test.ts": 1824, - "src/renderer/src/components/editor/rich-markdown-link-clipboard.test.ts": 140, - "src/renderer/src/components/editor/rich-markdown-link-shortcut.test.ts": 1507, - "src/renderer/src/components/editor/rich-markdown-list-continuation.test.ts": 150, - "src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts": 87, - "src/renderer/src/components/editor/rich-markdown-literal-serialization.test.ts": 160, - "src/renderer/src/components/editor/rich-markdown-local-image.test.ts": 1491, - "src/renderer/src/components/editor/rich-markdown-lowlight-cache.test.ts": 149, - "src/renderer/src/components/editor/rich-markdown-lowlight.test.ts": 1932, - "src/renderer/src/components/editor/rich-markdown-normalize.test.ts": 138, - "src/renderer/src/components/editor/rich-markdown-paragraph.test.ts": 90, - "src/renderer/src/components/editor/rich-markdown-paste-handler.test.ts": 74, - "src/renderer/src/components/editor/rich-markdown-paste-image.test.ts": 494, - "src/renderer/src/components/editor/rich-markdown-range-bounds.test.ts": 94, - "src/renderer/src/components/editor/rich-markdown-review-annotations.test.ts": 109, - "src/renderer/src/components/editor/rich-markdown-review-note-layout.test.ts": 58, - "src/renderer/src/components/editor/rich-markdown-review-rail-benchmark.test.ts": 1687, - "src/renderer/src/components/editor/rich-markdown-review-rail-blocks.test.ts": 2390, - "src/renderer/src/components/editor/rich-markdown-review-text-ranges.test.ts": 74, - "src/renderer/src/components/editor/rich-markdown-save-benchmark.test.ts": 1604, - "src/renderer/src/components/editor/rich-markdown-search-focus.adversarial.test.ts": 316, - "src/renderer/src/components/editor/rich-markdown-search-focus.test.ts": 226, + "src/renderer/src/components/dashboard-popout/useDashboardSnapshot.test.tsx": 268, + "src/renderer/src/components/dashboard/AgentDashboardDrawer.test.tsx": 1576, + "src/renderer/src/components/dashboard/AgentDashboardSettingsMenu.test.tsx": 554, + "src/renderer/src/components/dashboard/DashboardAgentChildDisclosure.test.tsx": 2019, + "src/renderer/src/components/dashboard/DashboardAgentRow.test.tsx": 1603, + "src/renderer/src/components/dashboard/agent-dashboard-open-mode.test.ts": 39, + "src/renderer/src/components/dashboard/agent-finished-timestamp.test.ts": 43, + "src/renderer/src/components/dashboard/agent-row-lineage-model.test.ts": 73, + "src/renderer/src/components/dashboard/agent-row-pane-live-title.test.ts": 43, + "src/renderer/src/components/dashboard/build-dashboard-bucket-counts.allocation.test.ts": 388, + "src/renderer/src/components/dashboard/build-dashboard-bucket-counts.cache.test.ts": 179, + "src/renderer/src/components/dashboard/build-dashboard-bucket-counts.equivalence.test.ts": 261, + "src/renderer/src/components/dashboard/build-dashboard-snapshot-folder-workspace.test.ts": 1517, + "src/renderer/src/components/dashboard/build-dashboard-snapshot-orchestration-routing.test.ts": 1465, + "src/renderer/src/components/dashboard/build-dashboard-snapshot.rows-cache.test.ts": 1574, + "src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts": 1506, + "src/renderer/src/components/dashboard/dashboard-card-context.test.ts": 49, + "src/renderer/src/components/dashboard/dashboard-card-labels.test.ts": 76, + "src/renderer/src/components/dashboard/dashboard-card-terminal-input.test.ts": 2024, + "src/renderer/src/components/dashboard/dashboard-worktree-launch-options.test.ts": 49, + "src/renderer/src/components/dashboard/launch-dashboard-agent.test.ts": 46, + "src/renderer/src/components/dashboard/reveal-dashboard-agent.test.ts": 41, + "src/renderer/src/components/dashboard/use-agent-row-conversation-name.test.ts": 69, + "src/renderer/src/components/dashboard/useAgentBucketCounts.gate.test.ts": 297, + "src/renderer/src/components/dashboard/useAgentBucketCounts.test.tsx": 225, + "src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx": 1392, + "src/renderer/src/components/dashboard/useLiveDashboardSnapshot.test.ts": 1789, + "src/renderer/src/components/dashboard/useRetainedAgents.test.ts": 1551, + "src/renderer/src/components/dashboard/useRetainedAgentsSync.test.ts": 1326, + "src/renderer/src/components/dictation/DictationIndicator.localization.test.ts": 294, + "src/renderer/src/components/dictation/DictationIndicator.test.tsx": 406, + "src/renderer/src/components/dictation/dictation-audio-meter.test.ts": 38, + "src/renderer/src/components/dictation/dictation-final-segments.test.ts": 32, + "src/renderer/src/components/dictation/dictation-insertion-target.test.ts": 297, + "src/renderer/src/components/dictation/dictation-meter-store.test.tsx": 249, + "src/renderer/src/components/dictation/dictation-stopped-sessions.test.ts": 39, + "src/renderer/src/components/dictation/microphone-devices.test.ts": 279, + "src/renderer/src/components/dictation/use-hold-dictation-gesture.test.tsx": 225, + "src/renderer/src/components/diff-comments/DiffCommentCard.resize.test.tsx": 537, + "src/renderer/src/components/diff-comments/DiffCommentDraftCard.test.tsx": 2007, + "src/renderer/src/components/diff-comments/diff-comment-draft-zone.test.tsx": 2250, + "src/renderer/src/components/diff-comments/diff-comment-drag-auto-scroll.test.ts": 41, + "src/renderer/src/components/diff-comments/diff-comment-line-range.test.ts": 47, + "src/renderer/src/components/diff-comments/diff-comment-popover-outside-click.test.tsx": 1433, + "src/renderer/src/components/diff-comments/diff-comment-popover-position.test.ts": 41, + "src/renderer/src/components/diff-comments/diff-comment-range-drag.test.ts": 5126, + "src/renderer/src/components/diff-comments/diff-comment-zone-mouse-events.test.ts": 40, + "src/renderer/src/components/diff-comments/use-visible-worktree-diff-comments.test.ts": 225, + "src/renderer/src/components/diff-comments/useDiffCommentDecorator.commentable-lines.test.tsx": 1316, + "src/renderer/src/components/diff-comments/useDiffCommentDecorator.model-lifecycle.test.tsx": 6697, + "src/renderer/src/components/diff-comments/useDiffCommentDecorator.range-selection.test.tsx": 3708, + "src/renderer/src/components/editor/ChangesModeView.test.tsx": 283, + "src/renderer/src/components/editor/CheckRunCopyButton.test.tsx": 338, + "src/renderer/src/components/editor/CheckRunDetailsPanel.copy.test.tsx": 2165, + "src/renderer/src/components/editor/ConflictComponents.test.tsx": 150, + "src/renderer/src/components/editor/DiagramViewerModal.test.tsx": 2151, + "src/renderer/src/components/editor/DiffSectionBody.word-wrap-lifecycle.test.tsx": 385, + "src/renderer/src/components/editor/DiffViewer.word-wrap-lifecycle.test.tsx": 345, + "src/renderer/src/components/editor/EditorContent.markdown-classification.test.tsx": 1788, + "src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx": 1679, + "src/renderer/src/components/editor/EditorContent.test.tsx": 2324, + "src/renderer/src/components/editor/EditorEditFileSurface.monaco-containment.test.tsx": 1661, + "src/renderer/src/components/editor/EditorFileLoadErrorView.test.tsx": 451, + "src/renderer/src/components/editor/EditorPanel.markdown-classification-memoization.test.tsx": 2435, + "src/renderer/src/components/editor/EditorPanel.markdown-review-tools-setting.test.tsx": 2232, + "src/renderer/src/components/editor/EditorPanelHeader.test.tsx": 1207, + "src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx": 2241, + "src/renderer/src/components/editor/EditorPanelMarkdownActionsMenu.test.tsx": 88, + "src/renderer/src/components/editor/EditorPanelShell.header.test.tsx": 178, + "src/renderer/src/components/editor/ExternalFileChangeBanner.test.tsx": 467, + "src/renderer/src/components/editor/ExternalFileChangeCompareDialog.test.tsx": 852, + "src/renderer/src/components/editor/ImageViewer.test.tsx": 141, + "src/renderer/src/components/editor/IpynbCellEditor.external-sync.test.tsx": 1548, + "src/renderer/src/components/editor/IpynbKernelToolbar.test.tsx": 713, + "src/renderer/src/components/editor/IpynbRendering.test.tsx": 2222, + "src/renderer/src/components/editor/LargeDiffLoadPrompt.test.tsx": 311, + "src/renderer/src/components/editor/MarkdownPreview.image-access.test.tsx": 1020, + "src/renderer/src/components/editor/MarkdownPreview.link-routing.interaction.test.tsx": 1040, + "src/renderer/src/components/editor/MarkdownPreview.test.ts": 1339, + "src/renderer/src/components/editor/MarkdownPreview.toc-visibility-gate.test.tsx": 936, + "src/renderer/src/components/editor/MarkdownPreviewBody.test.tsx": 247, + "src/renderer/src/components/editor/MarkdownTableOfContentsPanel.test.tsx": 1741, + "src/renderer/src/components/editor/MediaViewer.test.tsx": 195, + "src/renderer/src/components/editor/MermaidBlock.test.tsx": 673, + "src/renderer/src/components/editor/MonacoEditor.content-owner.test.tsx": 8350, + "src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx": 5368, + "src/renderer/src/components/editor/MonacoEditor.owner-view-state.test.tsx": 2904, + "src/renderer/src/components/editor/MonacoEditor.system-theme.test.tsx": 2265, + "src/renderer/src/components/editor/NotesSendMenu.test.tsx": 145, + "src/renderer/src/components/editor/PdfFind.test.tsx": 509, + "src/renderer/src/components/editor/ReviewNotesSendMenuContent.test.tsx": 182, + "src/renderer/src/components/editor/RichMarkdownErrorBoundary.lazy-chunk.test.tsx": 373, + "src/renderer/src/components/editor/RichMarkdownLinkBubble.render.test.tsx": 355, + "src/renderer/src/components/editor/RichMarkdownLinkBubble.test.ts": 96, + "src/renderer/src/components/editor/RichMarkdownSearchBar.ime-enter.test.tsx": 364, + "src/renderer/src/components/editor/RichMarkdownSlashMenu.test.tsx": 148, + "src/renderer/src/components/editor/RichMarkdownTableControls.test.tsx": 2038, + "src/renderer/src/components/editor/check-annotation-open.test.ts": 80, + "src/renderer/src/components/editor/check-annotation-path.test.ts": 46, + "src/renderer/src/components/editor/check-job-step-status.test.ts": 36, + "src/renderer/src/components/editor/check-run-clipboard-text.test.ts": 32, + "src/renderer/src/components/editor/check-run-details-fix-with-ai.test.ts": 601, + "src/renderer/src/components/editor/check-run-details-tab.test.ts": 35, + "src/renderer/src/components/editor/closed-editor-model-lifetime.test.ts": 8979, + "src/renderer/src/components/editor/closed-editor-model-shell.test.tsx": 4156, + "src/renderer/src/components/editor/closed-editor-tab-cache-sweep.test.ts": 43, + "src/renderer/src/components/editor/closed-editor-tab-disposal.test.ts": 120, + "src/renderer/src/components/editor/closed-editor-view-state-retention.test.ts": 3232, + "src/renderer/src/components/editor/combined-diff-on-demand-load.test.ts": 47, + "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-collapsed-work.test.tsx": 617, + "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-render.test.tsx": 867, + "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-row-drag.test.tsx": 256, + "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree-windowing.test.tsx": 743, + "src/renderer/src/components/editor/combined-diff/browse-files/combined-diff-file-tree.test.ts": 94, + "src/renderer/src/components/editor/combined-diff/browse-files/use-combined-diff-file-tree-resize.test.tsx": 228, + "src/renderer/src/components/editor/combined-diff/browse-files/use-combined-diff-tree-navigation.test.ts": 297, + "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-initial-section-load.test.ts": 41, + "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-load-scheduler.test.ts": 43, + "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-section-connection.test.ts": 1457, + "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-section-load-state.test.ts": 40, + "src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-stale-analysis-work-budget.test.tsx": 853, + "src/renderer/src/components/editor/combined-diff/remember-view/use-combined-diff-view-restore.test.tsx": 530, + "src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-entries.test.ts": 53, + "src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-section-cache-match.test.ts": 47, + "src/renderer/src/components/editor/combined-diff/resolve-changes/combined-diff-section-scaling.test.tsx": 648, + "src/renderer/src/components/editor/combined-diff/resolve-changes/use-combined-diff-section-index-map.test.tsx": 240, + "src/renderer/src/components/editor/combined-diff/review-controls/combined-diff-commit-message.test.ts": 41, + "src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-render-range.test.ts": 36, + "src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-restore-signal-equivalence.test.tsx": 718, + "src/renderer/src/components/editor/combined-diff/scroll-viewport/combined-diff-scrollbar-drag.test.ts": 38, + "src/renderer/src/components/editor/conflict-review-file-tree-windowing.test.tsx": 1460, + "src/renderer/src/components/editor/csv/CsvGrid.test.tsx": 736, + "src/renderer/src/components/editor/csv/CsvViewer.delimiter.test.tsx": 2556, + "src/renderer/src/components/editor/csv/CsvViewer.editing.test.tsx": 3100, + "src/renderer/src/components/editor/csv/csv-byte-index.test.ts": 1486, + "src/renderer/src/components/editor/csv/csv-cell-links.test.tsx": 296, + "src/renderer/src/components/editor/csv/csv-column-width-preferences.test.ts": 191, + "src/renderer/src/components/editor/csv/csv-paged-preview.test.ts": 106, + "src/renderer/src/components/editor/csv/csv-parse-budget.test.ts": 82, + "src/renderer/src/components/editor/csv/csv-parse.test.ts": 123, + "src/renderer/src/components/editor/csv/csv-pending-save.test.ts": 1407, + "src/renderer/src/components/editor/csv/csv-preview-save-safety.test.ts": 1578, + "src/renderer/src/components/editor/csv/csv-table-interactions.test.ts": 142, + "src/renderer/src/components/editor/csv/csv-text-document.test.ts": 118, + "src/renderer/src/components/editor/csv/useCsvPagedPreview.test.tsx": 406, + "src/renderer/src/components/editor/details-markdown-html.test.ts": 66, + "src/renderer/src/components/editor/diagram-pan-zoom.test.ts": 45, + "src/renderer/src/components/editor/diff-editor-hide-unchanged-options.test.ts": 39, + "src/renderer/src/components/editor/diff-editor-line-number-options.test.ts": 33, + "src/renderer/src/components/editor/diff-editor-shift-wheel-scroll.test.ts": 192, + "src/renderer/src/components/editor/diff-editor-whitespace-options.test.ts": 31, + "src/renderer/src/components/editor/diff-editor-word-wrap-options.test.ts": 179, + "src/renderer/src/components/editor/diff-line-stats.test.ts": 34, + "src/renderer/src/components/editor/diff-model-swap-view-state.test.ts": 149, + "src/renderer/src/components/editor/diff-monaco-model-disposal.test.ts": 46, + "src/renderer/src/components/editor/diff-navigation-context.test.tsx": 1605, + "src/renderer/src/components/editor/diff-section-comment-submit.test.ts": 63, + "src/renderer/src/components/editor/diff-section-layout.test.ts": 77, + "src/renderer/src/components/editor/diff-section-preview.test.ts": 1232, + "src/renderer/src/components/editor/diff-viewer-large-diff-save-action.test.ts": 60, + "src/renderer/src/components/editor/drag-selection-guard-lifetime.test.ts": 615, + "src/renderer/src/components/editor/editor-artifact-upload.test.ts": 35, + "src/renderer/src/components/editor/editor-autosave-conflict-flow.test.ts": 626, + "src/renderer/src/components/editor/editor-autosave-controller.test.ts": 794, + "src/renderer/src/components/editor/editor-autosave.test.ts": 265, + "src/renderer/src/components/editor/editor-cmd-save-target.test.ts": 272, + "src/renderer/src/components/editor/editor-content-dirty-state.test.ts": 44, + "src/renderer/src/components/editor/editor-dropped-file-open.escaping-link.test.ts": 394, + "src/renderer/src/components/editor/editor-dropped-file-open.test.ts": 1367, + "src/renderer/src/components/editor/editor-external-watch-path-index.test.ts": 1270, + "src/renderer/src/components/editor/editor-file-save-attempt.test.ts": 39, + "src/renderer/src/components/editor/editor-header-file-rename.test.ts": 1642, + "src/renderer/src/components/editor/editor-header.test.ts": 52, + "src/renderer/src/components/editor/editor-labels.test.ts": 40, + "src/renderer/src/components/editor/editor-model-host-isolation.test.ts": 2119, + "src/renderer/src/components/editor/editor-model-owner.test.ts": 322, + "src/renderer/src/components/editor/editor-model-uri.test.ts": 62, + "src/renderer/src/components/editor/editor-panel-diff-reload.test.ts": 44, + "src/renderer/src/components/editor/editor-panel-draft-selector.test.ts": 51, + "src/renderer/src/components/editor/editor-panel-git-entry-selector.test.ts": 39, + "src/renderer/src/components/editor/editor-panel-render-model.test.ts": 1484, + "src/renderer/src/components/editor/editor-path-display.test.ts": 40, + "src/renderer/src/components/editor/editor-path-move-inflight.test.ts": 33, + "src/renderer/src/components/editor/editor-pending-flush.test.ts": 48, + "src/renderer/src/components/editor/editor-restored-tab-conflict-scan.test.ts": 525, + "src/renderer/src/components/editor/editor-save-user-named-tab.test.ts": 603, + "src/renderer/src/components/editor/editor-self-write-registry.test.ts": 62, + "src/renderer/src/components/editor/editor-shortcuts.test.ts": 227, + "src/renderer/src/components/editor/editor-untitled-dont-save-dialogs.test.tsx": 733, + "src/renderer/src/components/editor/editor-untitled-save-lifecycle.test.ts": 946, + "src/renderer/src/components/editor/export-active-markdown.test.ts": 164, + "src/renderer/src/components/editor/file-editor-word-wrap-options.test.ts": 38, + "src/renderer/src/components/editor/image-viewer-zoom.test.ts": 49, + "src/renderer/src/components/editor/ipynb-ansi.test.ts": 56, + "src/renderer/src/components/editor/ipynb-code-cell-lines.test.ts": 70, + "src/renderer/src/components/editor/ipynb-kernel-outputs.test.ts": 302, + "src/renderer/src/components/editor/ipynb-kernel-session-start-lifetime.test.ts": 99, + "src/renderer/src/components/editor/ipynb-kernel-session.test.ts": 197, + "src/renderer/src/components/editor/ipynb-kernel-setup-commands.test.ts": 36, + "src/renderer/src/components/editor/ipynb-parse.test.ts": 67, + "src/renderer/src/components/editor/large-diff-render-limit.test.ts": 106, + "src/renderer/src/components/editor/large-markdown-render-freeze.test.tsx": 2035, + "src/renderer/src/components/editor/line-copy-path.test.ts": 32, + "src/renderer/src/components/editor/local-log-tail-decoder.test.ts": 85, + "src/renderer/src/components/editor/markdown-dirty-state.test.ts": 217, + "src/renderer/src/components/editor/markdown-doc-completions.test.ts": 39, + "src/renderer/src/components/editor/markdown-doc-links.test.ts": 149, + "src/renderer/src/components/editor/markdown-document-list-request.test.ts": 361, + "src/renderer/src/components/editor/markdown-document-worktree-path-selector.test.ts": 1743, + "src/renderer/src/components/editor/markdown-export-extract.test.ts": 254, + "src/renderer/src/components/editor/markdown-export-html.test.ts": 76, + "src/renderer/src/components/editor/markdown-frontmatter.test.ts": 40, + "src/renderer/src/components/editor/markdown-heading-slug.test.ts": 32, + "src/renderer/src/components/editor/markdown-internal-links.test.ts": 44, + "src/renderer/src/components/editor/markdown-preview-anchor-navigation.test.ts": 145, + "src/renderer/src/components/editor/markdown-preview-annotation-shortcut.test.ts": 230, + "src/renderer/src/components/editor/markdown-preview-controls.test.ts": 73, + "src/renderer/src/components/editor/markdown-preview-document-client.test.ts": 57, + "src/renderer/src/components/editor/markdown-preview-document-search-budget.test.ts": 391, + "src/renderer/src/components/editor/markdown-preview-document-search-cache.test.ts": 593, + "src/renderer/src/components/editor/markdown-preview-document-search-scope.test.ts": 462, + "src/renderer/src/components/editor/markdown-preview-document.test.ts": 1549, + "src/renderer/src/components/editor/markdown-preview-links.test.ts": 53, + "src/renderer/src/components/editor/markdown-preview-local-images.test.ts": 1443, + "src/renderer/src/components/editor/markdown-preview-review-frame-lifecycle.test.tsx": 384, + "src/renderer/src/components/editor/markdown-preview-row-measurements.test.ts": 184, + "src/renderer/src/components/editor/markdown-preview-search-crash.test.tsx": 224, + "src/renderer/src/components/editor/markdown-preview-search-memory.test.ts": 163, + "src/renderer/src/components/editor/markdown-preview-search-offsets.test.ts": 609, + "src/renderer/src/components/editor/markdown-preview-search.test.ts": 83, + "src/renderer/src/components/editor/markdown-preview-table-chunks.test.ts": 6179, + "src/renderer/src/components/editor/markdown-preview-url-transform.test.ts": 78, + "src/renderer/src/components/editor/markdown-reference-link-normalization.test.ts": 64, + "src/renderer/src/components/editor/markdown-render-mode.test.ts": 49, + "src/renderer/src/components/editor/markdown-rich-comment-scan.test.ts": 351, + "src/renderer/src/components/editor/markdown-rich-mode-eligibility-cache.test.ts": 3887, + "src/renderer/src/components/editor/markdown-rich-mode.test.ts": 2727, + "src/renderer/src/components/editor/markdown-rich-size-limit.test.ts": 244, + "src/renderer/src/components/editor/markdown-round-trip.test.ts": 1977, + "src/renderer/src/components/editor/markdown-scanner-boundaries.test.ts": 69, + "src/renderer/src/components/editor/markdown-table-of-contents.test.ts": 121, + "src/renderer/src/components/editor/markdown-toc-collapse-state.test.ts": 39, + "src/renderer/src/components/editor/markdown-toc-visibility-gate.test.ts": 98, + "src/renderer/src/components/editor/markdown-tokenizer-start.test.ts": 69, + "src/renderer/src/components/editor/mermaid-config.test.ts": 512, + "src/renderer/src/components/editor/monaco-auto-height.test.ts": 95, + "src/renderer/src/components/editor/monaco-codebase-search.test.ts": 37, + "src/renderer/src/components/editor/monaco-conflict-decorations.test.ts": 50, + "src/renderer/src/components/editor/monaco-content-sync.test.ts": 37, + "src/renderer/src/components/editor/monaco-content-sync.undo-history.test.ts": 1119, + "src/renderer/src/components/editor/monaco-context-menu-paste.test.ts": 213, + "src/renderer/src/components/editor/monaco-cursor-line-lifetime.test.ts": 862, + "src/renderer/src/components/editor/monaco-find-options.test.ts": 33, + "src/renderer/src/components/editor/monaco-find-widget.test.ts": 183, + "src/renderer/src/components/editor/monaco-large-text-paste.test.ts": 178, + "src/renderer/src/components/editor/monaco-markdown-doc-completions-retention.test.ts": 60, + "src/renderer/src/components/editor/monaco-markdown-doc-link-decorations.offset-scan.test.ts": 194, + "src/renderer/src/components/editor/monaco-markdown-doc-link-decorations.test.ts": 87, + "src/renderer/src/components/editor/monaco-markdown-selection-annotation.test.ts": 42, + "src/renderer/src/components/editor/monaco-programmatic-sync.test.ts": 34, + "src/renderer/src/components/editor/monaco-reveal-range.test.ts": 31, + "src/renderer/src/components/editor/monaco-view-state-persistence.test.ts": 43, + "src/renderer/src/components/editor/pdf-js-document-options.test.ts": 35, + "src/renderer/src/components/editor/pdf-scale-preference-storage.test.ts": 43, + "src/renderer/src/components/editor/pdf-scale-preference.test.ts": 44, + "src/renderer/src/components/editor/pdf-view-position.test.ts": 54, + "src/renderer/src/components/editor/pdf-viewer-failed-load-recovery.test.tsx": 2210, + "src/renderer/src/components/editor/pdf-viewer-zoom-ownership.test.tsx": 726, + "src/renderer/src/components/editor/pdf-zoom-request.test.ts": 192, + "src/renderer/src/components/editor/pending-editor-focus-request.test.ts": 27, + "src/renderer/src/components/editor/position-stable-node-view-update.test.ts": 33, + "src/renderer/src/components/editor/raw-markdown-comment-scan.test.ts": 68, + "src/renderer/src/components/editor/raw-markdown-html-line-start.test.ts": 37, + "src/renderer/src/components/editor/raw-markdown-html.blank-run.test.ts": 172, + "src/renderer/src/components/editor/raw-markdown-html.details-fences.test.ts": 75, + "src/renderer/src/components/editor/restored-editor-owner-save-lifecycle.test.ts": 2045, + "src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.test.tsx": 1559, + "src/renderer/src/components/editor/rich-markdown-async-image-paste.test.ts": 1363, + "src/renderer/src/components/editor/rich-markdown-async-image-table.test.ts": 999, + "src/renderer/src/components/editor/rich-markdown-auto-focus.test.ts": 180, + "src/renderer/src/components/editor/rich-markdown-code-block-languages.test.ts": 89, + "src/renderer/src/components/editor/rich-markdown-commands.test.ts": 1799, + "src/renderer/src/components/editor/rich-markdown-comment-blocks.test.ts": 1955, + "src/renderer/src/components/editor/rich-markdown-context-command-routing.test.ts": 1769, + "src/renderer/src/components/editor/rich-markdown-cut.test.ts": 308, + "src/renderer/src/components/editor/rich-markdown-details-keyboard.test.ts": 3294, + "src/renderer/src/components/editor/rich-markdown-doc-link-keyboard.test.ts": 381, + "src/renderer/src/components/editor/rich-markdown-doc-link-resolution.test.ts": 274, + "src/renderer/src/components/editor/rich-markdown-doc-link.test.ts": 34, + "src/renderer/src/components/editor/rich-markdown-editor-click-routing.test.ts": 841, + "src/renderer/src/components/editor/rich-markdown-editor-config.test.ts": 2004, + "src/renderer/src/components/editor/rich-markdown-empty-paragraph-delete.test.ts": 177, + "src/renderer/src/components/editor/rich-markdown-escape-fidelity.test.ts": 2084, + "src/renderer/src/components/editor/rich-markdown-extension.test.ts": 206, + "src/renderer/src/components/editor/rich-markdown-html-superscript-link.test.ts": 2646, + "src/renderer/src/components/editor/rich-markdown-image-context.test.ts": 481, + "src/renderer/src/components/editor/rich-markdown-image-cut.test.ts": 1999, + "src/renderer/src/components/editor/rich-markdown-image-insert-code-block.test.ts": 990, + "src/renderer/src/components/editor/rich-markdown-image-insert.test.ts": 930, + "src/renderer/src/components/editor/rich-markdown-image-insertion-target.test.ts": 1953, + "src/renderer/src/components/editor/rich-markdown-inline-source-tokenization.test.ts": 2383, + "src/renderer/src/components/editor/rich-markdown-key-handler.test.ts": 2146, + "src/renderer/src/components/editor/rich-markdown-large-html.test.ts": 4403, + "src/renderer/src/components/editor/rich-markdown-large-text-paste.fidelity.test.ts": 1802, + "src/renderer/src/components/editor/rich-markdown-large-text-paste.selection.test.ts": 1894, + "src/renderer/src/components/editor/rich-markdown-large-text-paste.test.ts": 1462, + "src/renderer/src/components/editor/rich-markdown-link-clipboard.test.ts": 159, + "src/renderer/src/components/editor/rich-markdown-link-shortcut.test.ts": 1814, + "src/renderer/src/components/editor/rich-markdown-list-continuation.test.ts": 121, + "src/renderer/src/components/editor/rich-markdown-list-tokenizers.test.ts": 64, + "src/renderer/src/components/editor/rich-markdown-literal-serialization.test.ts": 167, + "src/renderer/src/components/editor/rich-markdown-local-image.test.ts": 1802, + "src/renderer/src/components/editor/rich-markdown-lowlight-cache.test.ts": 171, + "src/renderer/src/components/editor/rich-markdown-lowlight.test.ts": 2336, + "src/renderer/src/components/editor/rich-markdown-normalize.test.ts": 198, + "src/renderer/src/components/editor/rich-markdown-paragraph.test.ts": 61, + "src/renderer/src/components/editor/rich-markdown-paste-handler.test.ts": 51, + "src/renderer/src/components/editor/rich-markdown-paste-image.test.ts": 559, + "src/renderer/src/components/editor/rich-markdown-range-bounds.test.ts": 58, + "src/renderer/src/components/editor/rich-markdown-review-annotations.test.ts": 108, + "src/renderer/src/components/editor/rich-markdown-review-note-layout.test.ts": 36, + "src/renderer/src/components/editor/rich-markdown-review-rail-benchmark.test.ts": 1794, + "src/renderer/src/components/editor/rich-markdown-review-rail-blocks.test.ts": 2001, + "src/renderer/src/components/editor/rich-markdown-review-text-ranges.test.ts": 62, + "src/renderer/src/components/editor/rich-markdown-save-benchmark.test.ts": 1466, + "src/renderer/src/components/editor/rich-markdown-search-focus.adversarial.test.ts": 279, + "src/renderer/src/components/editor/rich-markdown-search-focus.test.ts": 239, "src/renderer/src/components/editor/rich-markdown-search-matches-cache.test.ts": 116, - "src/renderer/src/components/editor/rich-markdown-search.test.ts": 103, - "src/renderer/src/components/editor/rich-markdown-serialization-commit.test.ts": 1408, - "src/renderer/src/components/editor/rich-markdown-signed-images.test.ts": 1568, - "src/renderer/src/components/editor/rich-markdown-slash-command-filter.test.ts": 47, - "src/renderer/src/components/editor/rich-markdown-source-eol-allocation.test.ts": 285, - "src/renderer/src/components/editor/rich-markdown-source-reconcile.test.ts": 1937, - "src/renderer/src/components/editor/rich-markdown-tab-key-handler.test.ts": 2528, - "src/renderer/src/components/editor/rich-markdown-table-actions.test.ts": 2439, - "src/renderer/src/components/editor/rich-markdown-table-control-layout.test.ts": 51, - "src/renderer/src/components/editor/rich-markdown-table-keyboard.test.ts": 1783, - "src/renderer/src/components/editor/rich-markdown-table.test.ts": 84, - "src/renderer/src/components/editor/rich-markdown-terminal-path-paste.test.ts": 150, - "src/renderer/src/components/editor/rich-markdown-toc-heading-target.test.ts": 168, - "src/renderer/src/components/editor/rich-markdown-trailing-paragraph.test.ts": 2081, - "src/renderer/src/components/editor/selection-copy.test.ts": 63, - "src/renderer/src/components/editor/setup-contextual-copy.test.ts": 93, - "src/renderer/src/components/editor/untitled-file-rename-path.test.ts": 48, - "src/renderer/src/components/editor/use-editor-content-change-handler.test.tsx": 1784, - "src/renderer/src/components/editor/use-editor-group-file-drop-owner.remote.test.tsx": 2275, - "src/renderer/src/components/editor/use-editor-group-file-drop-owner.test.tsx": 1048, - "src/renderer/src/components/editor/use-markdown-preview-document-search.test.tsx": 228, - "src/renderer/src/components/editor/use-markdown-preview-document.test.tsx": 290, - "src/renderer/src/components/editor/use-markdown-preview-navigation.test.tsx": 512, - "src/renderer/src/components/editor/use-markdown-preview-search-reveal.test.tsx": 372, - "src/renderer/src/components/editor/use-monaco-editor-decorations.doc-link-refresh.test.tsx": 206, - "src/renderer/src/components/editor/use-monaco-editor-mount.cursor-owner.test.tsx": 1084, - "src/renderer/src/components/editor/use-pdf-viewer-find-shortcut.test.tsx": 740, - "src/renderer/src/components/editor/use-quick-open-history-visit.test.tsx": 225, - "src/renderer/src/components/editor/use-rich-markdown-table-context-menu.test.tsx": 2188, - "src/renderer/src/components/editor/use-rich-markdown-table-control-target.test.tsx": 2157, - "src/renderer/src/components/editor/useDiffViewerLargeDiffLifecycle.test.tsx": 273, - "src/renderer/src/components/editor/useEditorCmdSaveRequest.test.tsx": 455, - "src/renderer/src/components/editor/useEditorPanelContentState.test.tsx": 1443, - "src/renderer/src/components/editor/useEditorPanelExternalContentEvents.test.tsx": 461, - "src/renderer/src/components/editor/useEditorPanelFileLoadRetry.host-connection.test.tsx": 1687, - "src/renderer/src/components/editor/useEditorPanelFileLoadRetry.test.tsx": 1705, - "src/renderer/src/components/editor/useEditorPanelFloatingTabContentState.test.tsx": 615, - "src/renderer/src/components/editor/useEditorPanelHostUnresolvedContentState.test.tsx": 784, - "src/renderer/src/components/editor/useEditorPanelRemoteSiblingContentState.test.tsx": 579, - "src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx": 660, - "src/renderer/src/components/editor/useIpynbCellExecution.test.tsx": 431, - "src/renderer/src/components/editor/useIpynbDocumentEditing.test.tsx": 349, - "src/renderer/src/components/editor/useLinkBubble.test.tsx": 356, - "src/renderer/src/components/editor/useLocalImagePick.test.tsx": 1197, - "src/renderer/src/components/editor/useLocalImageSrc.test.ts": 587, - "src/renderer/src/components/editor/useLocalLogTail.test.tsx": 511, - "src/renderer/src/components/editor/useMarkdownDocuments.navigation.test.ts": 573, - "src/renderer/src/components/editor/useMarkdownDocuments.ownership.test.tsx": 1885, - "src/renderer/src/components/editor/useMarkdownDocuments.test.ts": 1520, - "src/renderer/src/components/editor/useMarkdownDocuments.watch.test.ts": 438, - "src/renderer/src/components/editor/useRichMarkdownEditorInstance.integration.test.tsx": 340, - "src/renderer/src/components/editor/useRichMarkdownEditorInstance.test.tsx": 265, - "src/renderer/src/components/editor/useRichMarkdownPendingFocus.test.ts": 249, - "src/renderer/src/components/editor/useRichMarkdownProgrammaticSync.test.ts": 1933, - "src/renderer/src/components/editor/useRichMarkdownReviewController.open-guard.test.ts": 418, - "src/renderer/src/components/editor/useRichMarkdownReviewData.test.ts": 302, - "src/renderer/src/components/editor/useRichMarkdownSearch.adversarial.test.tsx": 617, - "src/renderer/src/components/editor/useRichMarkdownSearch.editing.test.tsx": 577, - "src/renderer/src/components/editor/useRichMarkdownSearch.reuse.test.tsx": 355, - "src/renderer/src/components/editor/useRichMarkdownSearch.test.tsx": 1817, - "src/renderer/src/components/emulator-pane/MobileEmulatorTabIntroCallout.test.tsx": 386, - "src/renderer/src/components/emulator-pane/emulator-device-frame-layout.test.ts": 54, - "src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx": 843, - "src/renderer/src/components/emulator-pane/emulator-device-frame.input.test.tsx": 577, - "src/renderer/src/components/emulator-pane/emulator-device-state.test.ts": 55, - "src/renderer/src/components/emulator-pane/emulator-keyboard-paste.test.ts": 198, - "src/renderer/src/components/emulator-pane/emulator-screen-gesture.test.ts": 83, - "src/renderer/src/components/emulator-pane/emulator-screen-stream-content.test.tsx": 371, - "src/renderer/src/components/emulator-pane/mobile-emulator-agent-setup-visibility.test.ts": 46, - "src/renderer/src/components/emulator-pane/mobile-emulator-hidden-toast.test.tsx": 214, - "src/renderer/src/components/emulator-pane/mobile-emulator-tab-intro-visibility.test.ts": 45, - "src/renderer/src/components/emulator-pane/use-emulator-pane-session.test.tsx": 1737, - "src/renderer/src/components/emulator-pane/use-mobile-emulator-tab-intro-actions.test.tsx": 1585, - "src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.lazy-chunk.test.tsx": 289, - "src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.never-landed-reload.test.tsx": 296, - "src/renderer/src/components/error-boundaries/react-185-bystander-attribution.test.tsx": 391, - "src/renderer/src/components/feature-tips/CliSkillSetupTerminal.test.tsx": 1870, - "src/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.test.tsx": 325, - "src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.test.tsx": 169, - "src/renderer/src/components/feature-tips/NativeChatUpgradeTipDialog.test.tsx": 1996, - "src/renderer/src/components/feature-tips/SessionSearchTipDialog.test.tsx": 374, - "src/renderer/src/components/feature-tips/VoiceDictationFeatureTipVisual.test.tsx": 338, - "src/renderer/src/components/feature-tips/VoiceDictationTipDialog.test.tsx": 269, - "src/renderer/src/components/feature-tips/feature-tip-cli-install-action.test.ts": 56, - "src/renderer/src/components/feature-tips/feature-tip-modal-state.test.ts": 269, - "src/renderer/src/components/feature-tips/feature-tip-startup-gate.test.ts": 299, - "src/renderer/src/components/feature-tips/feature-tip-telemetry.test.ts": 66, - "src/renderer/src/components/feature-tips/use-session-search-tip-setup.test.tsx": 441, - "src/renderer/src/components/feature-wall/AgentCapabilitiesSetupAction.test.ts": 1436, - "src/renderer/src/components/feature-wall/ConnectIntegrationsList.test.tsx": 749, - "src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx": 1754, - "src/renderer/src/components/feature-wall/FullDiskAccessSetupPrompt.test.ts": 330, - "src/renderer/src/components/feature-wall/KeepAwakeCard.test.tsx": 641, - "src/renderer/src/components/feature-wall/agents-orchestration/UsageAccountsCard.test.tsx": 383, - "src/renderer/src/components/feature-wall/browser-animated-visual-sequence.test.ts": 62, - "src/renderer/src/components/feature-wall/feature-wall-animation-visibility.test.tsx": 638, - "src/renderer/src/components/feature-wall/feature-wall-rail-navigation.test.ts": 57, - "src/renderer/src/components/feature-wall/feature-wall-setup-checklist-localized-copy.test.ts": 552, - "src/renderer/src/components/feature-wall/feature-wall-setup-progress.test.ts": 61, - "src/renderer/src/components/feature-wall/feature-wall-shortcut-labels.test.tsx": 1718, - "src/renderer/src/components/feature-wall/feature-wall-usage-tracking.test.ts": 91, - "src/renderer/src/components/feature-wall/use-feature-wall-completion.test.ts": 85, - "src/renderer/src/components/feature-wall/use-feature-wall-tour-telemetry.test.ts": 55, - "src/renderer/src/components/feature-wall/use-integration-connection-status.test.ts": 2054, - "src/renderer/src/components/feature-wall/workbench-terminal-storyboard-sequence.test.ts": 54, - "src/renderer/src/components/file-path-cursor-tooltip.test.ts": 177, - "src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.freshness.test.tsx": 491, - "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.bounds.test.tsx": 4024, - "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.focus.test.tsx": 7004, - "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.shortcuts.test.tsx": 2710, - "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tab-lifecycle.test.tsx": 4440, - "src/renderer/src/components/floating-terminal/FloatingTerminalToggleButton.test.tsx": 126, - "src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.test.tsx": 433, - "src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.test.ts": 57, - "src/renderer/src/components/floating-terminal/floating-terminal-panel-inputs.test.ts": 313, - "src/renderer/src/components/floating-terminal/floating-terminal-panel-view-state.test.ts": 172, - "src/renderer/src/components/floating-terminal/floating-terminal-trigger-position.test.ts": 68, - "src/renderer/src/components/floating-terminal/floating-workspace-surface-model.test.ts": 228, - "src/renderer/src/components/floating-terminal/floating-workspace-tab-reorder.test.ts": 1744, - "src/renderer/src/components/floating-terminal/use-floating-terminal-close-actions.test.tsx": 2068, - "src/renderer/src/components/github-body-draft-state.test.ts": 54, - "src/renderer/src/components/github-checks-tab-state.test.ts": 61, - "src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section.assignee-identity.test.tsx": 414, - "src/renderer/src/components/github-item-dialog/inspect-pull-request/pr-files-combined-diff-section-index.test.tsx": 330, - "src/renderer/src/components/github-item-dialog/load-item-details/work-item-details-issue-target.test.ts": 69, - "src/renderer/src/components/github-link-copy-state.test.ts": 45, - "src/renderer/src/components/github-pr-merge-state.test.ts": 99, - "src/renderer/src/components/github-pr-reviewer-display.test.ts": 61, - "src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts": 180, - "src/renderer/src/components/github-project/ProjectBoard.test.tsx": 554, - "src/renderer/src/components/github-project/ProjectRoadmap.test.tsx": 2087, - "src/renderer/src/components/github-project/ProjectViewStates.test.tsx": 459, - "src/renderer/src/components/github-project/github-project-picker-filter.test.ts": 67, - "src/renderer/src/components/github-project/group-sort.test.ts": 89, - "src/renderer/src/components/github-project/project-dialog-state.test.ts": 46, - "src/renderer/src/components/github-project/project-picker-browse-cache.test.ts": 52, - "src/renderer/src/components/github-project/project-picker-input.test.ts": 1461, - "src/renderer/src/components/github-project/project-row-filtering.test.ts": 68, - "src/renderer/src/components/github-project/project-view-wrapper-source-context-boundary.test.ts": 64, - "src/renderer/src/components/github-project/project-visible-table-cache.test.ts": 57, - "src/renderer/src/components/github/PRFilterPickers.test.ts": 173, - "src/renderer/src/components/github/github-markdown-image-url.test.ts": 53, - "src/renderer/src/components/github/github-mention-option-filter.test.ts": 75, - "src/renderer/src/components/github/github-pr-reviewer-candidate-filter.test.ts": 66, - "src/renderer/src/components/github/github-user-avatar.test.tsx": 238, - "src/renderer/src/components/github/github-work-item-assignee-filter.test.ts": 70, - "src/renderer/src/components/github/github-work-item-issue-target.test.ts": 163, - "src/renderer/src/components/github/github-work-item-label-filter.test.ts": 75, - "src/renderer/src/components/github/github-work-item-mutation-target.test.ts": 144, - "src/renderer/src/components/github/pr-comment-code-context.test.ts": 79, - "src/renderer/src/components/github/pr-file-content-size.test.ts": 57, - "src/renderer/src/components/github/use-image-input.test.ts": 2248, - "src/renderer/src/components/jira-create-adf.test.ts": 59, - "src/renderer/src/components/jira-issue-workspace-request-lifetime.test.tsx": 444, - "src/renderer/src/components/jira-project-picker-filter.test.ts": 59, - "src/renderer/src/components/jira-self-user.test.ts": 58, - "src/renderer/src/components/landing-preflight-dismissal.test.ts": 66, - "src/renderer/src/components/landing-preflight-issues.test.ts": 107, - "src/renderer/src/components/landing-preflight-runtime-boundary.test.ts": 1928, - "src/renderer/src/components/landing-preflight-unverifiable-host.test.tsx": 1541, - "src/renderer/src/components/linear-api-key-dialog-state.test.ts": 49, - "src/renderer/src/components/linear-issue-attribute-filter-coverage-agreement.test.ts": 633, - "src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx": 1593, - "src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts": 63, - "src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts": 83, - "src/renderer/src/components/linear-issue-project-selector.test.tsx": 453, - "src/renderer/src/components/linear-issue-text-draft-state.test.ts": 54, - "src/renderer/src/components/linear-issue-text-save-plan.test.ts": 52, - "src/renderer/src/components/linear-issue-view-storage.test.ts": 174, - "src/renderer/src/components/linear-issue-workspace-detail-state.test.tsx": 222, - "src/renderer/src/components/linear-issue-workspace-text.test.ts": 109, - "src/renderer/src/components/linear-project-presentation.test.ts": 65, - "src/renderer/src/components/linear-project-search-query.test.ts": 49, - "src/renderer/src/components/linear-project-view-surfaces.test.tsx": 504, - "src/renderer/src/components/linear-scope-selector.test.ts": 211, - "src/renderer/src/components/link-actions/LinkActionPopover.test.tsx": 545, - "src/renderer/src/components/maintenance/update-card/update-card-error-model.test.ts": 121, - "src/renderer/src/components/mobile/MobileHero.test.tsx": 2284, - "src/renderer/src/components/mobile/MobilePage.test.tsx": 1610, - "src/renderer/src/components/mobile/MobilePageToolbar.test.tsx": 313, - "src/renderer/src/components/mobile/NetworkInterfacePicker.test.tsx": 516, - "src/renderer/src/components/mobile/WindowsFirewallNotice.test.tsx": 461, - "src/renderer/src/components/mobile/mobile-page-stage.test.ts": 39, - "src/renderer/src/components/mobile/paired-mobile-devices.test.ts": 155, - "src/renderer/src/components/mobile/use-mobile-page-paired-devices.test.ts": 298, - "src/renderer/src/components/mobile/use-mobile-pairing-address-preference.test.tsx": 309, - "src/renderer/src/components/native-chat-launch-resume-decision.test.tsx": 1492, - "src/renderer/src/components/native-chat-restart-action-notifications.test.ts": 129, - "src/renderer/src/components/native-chat-resume-activity-label.test.ts": 107, - "src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx": 2316, - "src/renderer/src/components/native-chat-resume-failed-chat-watch.test.ts": 1826, - "src/renderer/src/components/native-chat-resume-failure-guidance.test.ts": 102, - "src/renderer/src/components/native-chat-resume-finished-run.test.tsx": 2925, - "src/renderer/src/components/native-chat-resume-on-restart-grouping.test.ts": 106, - "src/renderer/src/components/native-chat-resume-on-restart-outcome-toast.test.ts": 1502, - "src/renderer/src/components/native-chat-resume-run.test.ts": 99, - "src/renderer/src/components/native-chat-resume-skipped-progress.test.tsx": 2367, - "src/renderer/src/components/native-chat/NativeChatAgentMessageRow.test.tsx": 1911, - "src/renderer/src/components/native-chat/NativeChatAgentMessageSenders.test.tsx": 1766, - "src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx": 2314, - "src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.test.tsx": 428, - "src/renderer/src/components/native-chat/NativeChatBackgroundTaskRun.test.tsx": 456, - "src/renderer/src/components/native-chat/NativeChatBackgroundTasksStatus.test.tsx": 682, - "src/renderer/src/components/native-chat/NativeChatCodeBlock.load-failure.test.tsx": 482, - "src/renderer/src/components/native-chat/NativeChatCodeBlock.test.tsx": 4855, - "src/renderer/src/components/native-chat/NativeChatComposer.paste-remount.test.tsx": 2164, - "src/renderer/src/components/native-chat/NativeChatComposer.test.tsx": 1040, - "src/renderer/src/components/native-chat/NativeChatComposerActions.test.tsx": 494, - "src/renderer/src/components/native-chat/NativeChatComposerNotices.test.tsx": 450, - "src/renderer/src/components/native-chat/NativeChatContextUsageRing.test.tsx": 681, - "src/renderer/src/components/native-chat/NativeChatEmptyState.test.tsx": 399, - "src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.test.tsx": 1768, - "src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx": 1710, - "src/renderer/src/components/native-chat/NativeChatInterruptedContinue.test.tsx": 673, - "src/renderer/src/components/native-chat/NativeChatJumpToLatest.test.tsx": 363, - "src/renderer/src/components/native-chat/NativeChatMarkdown.mermaid.test.tsx": 1267, - "src/renderer/src/components/native-chat/NativeChatMarkdown.test.tsx": 1968, - "src/renderer/src/components/native-chat/NativeChatMarkdown.visual.test.tsx": 2370, - "src/renderer/src/components/native-chat/NativeChatMessageList.growth-windowing.test.tsx": 5931, - "src/renderer/src/components/native-chat/NativeChatMessageList.live-reasoning.test.tsx": 4308, - "src/renderer/src/components/native-chat/NativeChatMessageList.older-history.test.tsx": 4124, - "src/renderer/src/components/native-chat/NativeChatMessageList.opening-send.test.tsx": 3124, - "src/renderer/src/components/native-chat/NativeChatMessageList.provider-frame.test.tsx": 1738, - "src/renderer/src/components/native-chat/NativeChatMessageList.queued-turn-order.test.tsx": 3019, - "src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx": 16129, - "src/renderer/src/components/native-chat/NativeChatMessageList.reader-navigation.test.tsx": 5316, - "src/renderer/src/components/native-chat/NativeChatMessageList.rewind-rows.test.tsx": 2450, - "src/renderer/src/components/native-chat/NativeChatMessageList.stream-render.perf.test.tsx": 3416, - "src/renderer/src/components/native-chat/NativeChatMessageList.subagent-sections.test.tsx": 4465, - "src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx": 3209, - "src/renderer/src/components/native-chat/NativeChatMessageList.tool-stream-cost.test.tsx": 6211, - "src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx": 2860, - "src/renderer/src/components/native-chat/NativeChatMessageList.turn-membership.test.tsx": 3458, - "src/renderer/src/components/native-chat/NativeChatMessageList.turn-status.test.tsx": 3074, - "src/renderer/src/components/native-chat/NativeChatMessageList.unsent-message.test.tsx": 2773, - "src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx": 7809, - "src/renderer/src/components/native-chat/NativeChatMessageList.work-run-memo.test.tsx": 2520, - "src/renderer/src/components/native-chat/NativeChatMessageRail.test.tsx": 975, - "src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx": 2763, - "src/renderer/src/components/native-chat/NativeChatMessageRow.work-run.test.tsx": 2752, - "src/renderer/src/components/native-chat/NativeChatMessageTimestamp.test.tsx": 530, - "src/renderer/src/components/native-chat/NativeChatModelCombobox.test.tsx": 922, - "src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx": 2999, - "src/renderer/src/components/native-chat/NativeChatPacedMarkdown.file-links.test.tsx": 2066, - "src/renderer/src/components/native-chat/NativeChatPacedMarkdown.mermaid.test.tsx": 2028, - "src/renderer/src/components/native-chat/NativeChatPromptEditor.test.tsx": 670, - "src/renderer/src/components/native-chat/NativeChatQuestionCard.test.tsx": 565, - "src/renderer/src/components/native-chat/NativeChatQueuedMessageEditor.test.tsx": 2144, - "src/renderer/src/components/native-chat/NativeChatQueuedMessageList.inline-edit.test.tsx": 1044, - "src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx": 1795, - "src/renderer/src/components/native-chat/NativeChatReasoningRow.test.tsx": 2689, - "src/renderer/src/components/native-chat/NativeChatResolutionReceipt.empty-answer.test.tsx": 397, - "src/renderer/src/components/native-chat/NativeChatResolutionReceipt.test.tsx": 1020, - "src/renderer/src/components/native-chat/NativeChatResolvedView.context-menu.test.tsx": 3415, - "src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx": 3672, - "src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx": 3511, - "src/renderer/src/components/native-chat/NativeChatResolvedView.reveal-latest.test.tsx": 2755, - "src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx": 3611, - "src/renderer/src/components/native-chat/NativeChatRewindAction.target.test.tsx": 3255, - "src/renderer/src/components/native-chat/NativeChatRewindAction.test.tsx": 2974, - "src/renderer/src/components/native-chat/NativeChatSelectionQuote.test.tsx": 589, - "src/renderer/src/components/native-chat/NativeChatSessionOptionPickers.focus-return.test.tsx": 1366, - "src/renderer/src/components/native-chat/NativeChatSessionOptionPickers.test.tsx": 1137, - "src/renderer/src/components/native-chat/NativeChatStructuredQuestionCard.test.tsx": 491, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.approval-and-start-failure.test.tsx": 2905, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.availability-notice.test.tsx": 2847, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.goal-dock.test.tsx": 3314, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.history-read-state.test.tsx": 2758, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.host-outage.test.tsx": 3114, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsx": 3169, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.notice-card.test.tsx": 2787, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.refusal-notices.test.tsx": 2576, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.stopping.test.tsx": 2687, - "src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx": 2836, - "src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.test.tsx": 459, - "src/renderer/src/components/native-chat/NativeChatSubagentRun.test.tsx": 1152, - "src/renderer/src/components/native-chat/NativeChatTaskList.test.tsx": 489, - "src/renderer/src/components/native-chat/NativeChatThreadGoalBanner.test.tsx": 594, - "src/renderer/src/components/native-chat/NativeChatToolFileTarget.test.tsx": 502, - "src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx": 1353, - "src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx": 1214, - "src/renderer/src/components/native-chat/NativeChatToolRun.identity.test.tsx": 1029, - "src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx": 1212, - "src/renderer/src/components/native-chat/NativeChatToolRunCallCounts.test.tsx": 841, - "src/renderer/src/components/native-chat/NativeChatToolRunMemberList.identity.test.tsx": 786, - "src/renderer/src/components/native-chat/NativeChatTranscriptChrome.test.tsx": 1913, - "src/renderer/src/components/native-chat/NativeChatTranscriptRow.work-run.test.tsx": 2772, - "src/renderer/src/components/native-chat/NativeChatTurnActivityLine.test.tsx": 2341, - "src/renderer/src/components/native-chat/NativeChatUserMessageFold.test.tsx": 2829, - "src/renderer/src/components/native-chat/NativeChatView.test.tsx": 481, - "src/renderer/src/components/native-chat/NativeChatVisualFrame.test.tsx": 869, - "src/renderer/src/components/native-chat/NativeChatVisualTab.test.tsx": 1828, - "src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.test.tsx": 2038, - "src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.test.tsx": 378, - "src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.launch-directory.test.tsx": 1686, - "src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx": 1946, - "src/renderer/src/components/native-chat/agent-session-failure-words-text.test.ts": 1046, - "src/renderer/src/components/native-chat/agent-session-write-notice-text.test.ts": 684, - "src/renderer/src/components/native-chat/background-task-words-text.test.ts": 158, - "src/renderer/src/components/native-chat/claude-model-switch-confirmation.test.ts": 1797, - "src/renderer/src/components/native-chat/claude-terminal-session-options.test.ts": 84, - "src/renderer/src/components/native-chat/native-chat-active-rail-item.test.ts": 53, - "src/renderer/src/components/native-chat/native-chat-agent-message-sender-host.test.tsx": 3468, - "src/renderer/src/components/native-chat/native-chat-agent-message-sender-identity.test.tsx": 1721, - "src/renderer/src/components/native-chat/native-chat-appearance-palette.test.ts": 592, - "src/renderer/src/components/native-chat/native-chat-appearance-style.test.ts": 324, - "src/renderer/src/components/native-chat/native-chat-appearance-write.test.ts": 346, - "src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts": 1810, - "src/renderer/src/components/native-chat/native-chat-autoscroll.test.ts": 66, - "src/renderer/src/components/native-chat/native-chat-availability.test.ts": 62, - "src/renderer/src/components/native-chat/native-chat-claude-sign-in.test.tsx": 2665, - "src/renderer/src/components/native-chat/native-chat-command-language.test.ts": 58, - "src/renderer/src/components/native-chat/native-chat-compaction-visibility.test.ts": 450, - "src/renderer/src/components/native-chat/native-chat-composer-autogrow.test.tsx": 1883, - "src/renderer/src/components/native-chat/native-chat-composer-composition.test.tsx": 2116, - "src/renderer/src/components/native-chat/native-chat-composer-draft-addition.test.ts": 223, - "src/renderer/src/components/native-chat/native-chat-composer-draft-comparison.test.ts": 48, - "src/renderer/src/components/native-chat/native-chat-composer-draft-indexeddb.test.ts": 118, - "src/renderer/src/components/native-chat/native-chat-composer-draft-lifecycle.test.tsx": 1926, - "src/renderer/src/components/native-chat/native-chat-composer-draft-store.test.ts": 1553, - "src/renderer/src/components/native-chat/native-chat-composer-drop-scope.test.tsx": 1112, - "src/renderer/src/components/native-chat/native-chat-composer-field-resume.test.tsx": 1838, - "src/renderer/src/components/native-chat/native-chat-composer-ime-file-drop.test.tsx": 1111, - "src/renderer/src/components/native-chat/native-chat-composer-placeholder.test.ts": 54, - "src/renderer/src/components/native-chat/native-chat-composer-primary-action.test.ts": 40, - "src/renderer/src/components/native-chat/native-chat-composer-prompt-recall.test.tsx": 1254, - "src/renderer/src/components/native-chat/native-chat-composer-reveal-focus.test.tsx": 256, - "src/renderer/src/components/native-chat/native-chat-composer-state.test.ts": 123, - "src/renderer/src/components/native-chat/native-chat-composer-upload-completion.test.tsx": 1909, - "src/renderer/src/components/native-chat/native-chat-composer-workspace-file-drop.test.tsx": 954, - "src/renderer/src/components/native-chat/native-chat-context-command.test.tsx": 501, - "src/renderer/src/components/native-chat/native-chat-context-usage-summary.test.ts": 52, - "src/renderer/src/components/native-chat/native-chat-diff.test.ts": 69, - "src/renderer/src/components/native-chat/native-chat-dismiss-key.test.ts": 50, - "src/renderer/src/components/native-chat/native-chat-draft-cache.test.ts": 92, - "src/renderer/src/components/native-chat/native-chat-element-file-drop.test.tsx": 1865, - "src/renderer/src/components/native-chat/native-chat-external-attachments-destination.test.tsx": 594, - "src/renderer/src/components/native-chat/native-chat-file-link-existence.test.ts": 1427, - "src/renderer/src/components/native-chat/native-chat-file-link.test.ts": 366, - "src/renderer/src/components/native-chat/native-chat-find-visibility.test.ts": 135, - "src/renderer/src/components/native-chat/native-chat-floating-file-drop.test.tsx": 773, - "src/renderer/src/components/native-chat/native-chat-font-size.test.ts": 82, - "src/renderer/src/components/native-chat/native-chat-http-link-source-owner.test.ts": 56, - "src/renderer/src/components/native-chat/native-chat-image-paste.test.ts": 69, - "src/renderer/src/components/native-chat/native-chat-image-runtime-context.test.ts": 1523, - "src/renderer/src/components/native-chat/native-chat-incremental-assembler.test.ts": 301, - "src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts": 115, - "src/renderer/src/components/native-chat/native-chat-launch-default-adoption.test.ts": 129, + "src/renderer/src/components/editor/rich-markdown-search.test.ts": 71, + "src/renderer/src/components/editor/rich-markdown-serialization-commit.test.ts": 1507, + "src/renderer/src/components/editor/rich-markdown-signed-images.test.ts": 1642, + "src/renderer/src/components/editor/rich-markdown-slash-command-filter.test.ts": 37, + "src/renderer/src/components/editor/rich-markdown-source-eol-allocation.test.ts": 302, + "src/renderer/src/components/editor/rich-markdown-source-reconcile.test.ts": 2146, + "src/renderer/src/components/editor/rich-markdown-tab-key-handler.test.ts": 1914, + "src/renderer/src/components/editor/rich-markdown-table-actions.test.ts": 2341, + "src/renderer/src/components/editor/rich-markdown-table-control-layout.test.ts": 29, + "src/renderer/src/components/editor/rich-markdown-table-keyboard.test.ts": 1642, + "src/renderer/src/components/editor/rich-markdown-table.test.ts": 58, + "src/renderer/src/components/editor/rich-markdown-terminal-path-paste.test.ts": 159, + "src/renderer/src/components/editor/rich-markdown-toc-heading-target.test.ts": 175, + "src/renderer/src/components/editor/rich-markdown-trailing-paragraph.test.ts": 1992, + "src/renderer/src/components/editor/selection-copy.test.ts": 42, + "src/renderer/src/components/editor/setup-contextual-copy.test.ts": 53, + "src/renderer/src/components/editor/untitled-file-rename-path.test.ts": 39, + "src/renderer/src/components/editor/use-editor-content-change-handler.test.tsx": 1646, + "src/renderer/src/components/editor/use-editor-group-file-drop-owner.remote.test.tsx": 2011, + "src/renderer/src/components/editor/use-editor-group-file-drop-owner.test.tsx": 891, + "src/renderer/src/components/editor/use-markdown-preview-document-search.test.tsx": 220, + "src/renderer/src/components/editor/use-markdown-preview-document.test.tsx": 340, + "src/renderer/src/components/editor/use-markdown-preview-navigation.test.tsx": 461, + "src/renderer/src/components/editor/use-markdown-preview-search-reveal.test.tsx": 307, + "src/renderer/src/components/editor/use-monaco-editor-decorations.doc-link-refresh.test.tsx": 266, + "src/renderer/src/components/editor/use-monaco-editor-mount.cursor-owner.test.tsx": 996, + "src/renderer/src/components/editor/use-monaco-reveal-scheduler.test.tsx": 195, + "src/renderer/src/components/editor/use-pdf-viewer-find-shortcut.test.tsx": 801, + "src/renderer/src/components/editor/use-quick-open-history-visit.test.tsx": 202, + "src/renderer/src/components/editor/use-rich-markdown-table-context-menu.test.tsx": 1891, + "src/renderer/src/components/editor/use-rich-markdown-table-control-target.test.tsx": 1887, + "src/renderer/src/components/editor/useDiffViewerLargeDiffLifecycle.test.tsx": 237, + "src/renderer/src/components/editor/useEditorCmdSaveRequest.test.tsx": 412, + "src/renderer/src/components/editor/useEditorPanelContentState.test.tsx": 1655, + "src/renderer/src/components/editor/useEditorPanelExternalContentEvents.test.tsx": 446, + "src/renderer/src/components/editor/useEditorPanelFileLoadRetry.host-connection.test.tsx": 1574, + "src/renderer/src/components/editor/useEditorPanelFileLoadRetry.test.tsx": 1575, + "src/renderer/src/components/editor/useEditorPanelFloatingTabContentState.test.tsx": 632, + "src/renderer/src/components/editor/useEditorPanelHostUnresolvedContentState.test.tsx": 808, + "src/renderer/src/components/editor/useEditorPanelRemoteSiblingContentState.test.tsx": 537, + "src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx": 911, + "src/renderer/src/components/editor/useIpynbCellExecution.test.tsx": 427, + "src/renderer/src/components/editor/useIpynbDocumentEditing.test.tsx": 255, + "src/renderer/src/components/editor/useLinkBubble.test.tsx": 288, + "src/renderer/src/components/editor/useLocalImagePick.test.tsx": 1051, + "src/renderer/src/components/editor/useLocalImageSrc.test.ts": 551, + "src/renderer/src/components/editor/useLocalLogTail.test.tsx": 466, + "src/renderer/src/components/editor/useMarkdownDocuments.navigation.test.ts": 566, + "src/renderer/src/components/editor/useMarkdownDocuments.ownership.test.tsx": 1603, + "src/renderer/src/components/editor/useMarkdownDocuments.test.ts": 1294, + "src/renderer/src/components/editor/useMarkdownDocuments.watch.test.ts": 525, + "src/renderer/src/components/editor/useRichMarkdownEditorInstance.integration.test.tsx": 335, + "src/renderer/src/components/editor/useRichMarkdownEditorInstance.test.tsx": 235, + "src/renderer/src/components/editor/useRichMarkdownPendingFocus.test.ts": 253, + "src/renderer/src/components/editor/useRichMarkdownProgrammaticSync.test.ts": 1892, + "src/renderer/src/components/editor/useRichMarkdownReviewController.open-guard.test.ts": 355, + "src/renderer/src/components/editor/useRichMarkdownReviewData.test.ts": 292, + "src/renderer/src/components/editor/useRichMarkdownSearch.adversarial.test.tsx": 751, + "src/renderer/src/components/editor/useRichMarkdownSearch.editing.test.tsx": 439, + "src/renderer/src/components/editor/useRichMarkdownSearch.reuse.test.tsx": 324, + "src/renderer/src/components/editor/useRichMarkdownSearch.test.tsx": 1597, + "src/renderer/src/components/emulator-pane/MobileEmulatorTabIntroCallout.test.tsx": 295, + "src/renderer/src/components/emulator-pane/emulator-device-frame-layout.test.ts": 52, + "src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx": 719, + "src/renderer/src/components/emulator-pane/emulator-device-frame.input.test.tsx": 551, + "src/renderer/src/components/emulator-pane/emulator-device-state.test.ts": 34, + "src/renderer/src/components/emulator-pane/emulator-keyboard-paste.test.ts": 204, + "src/renderer/src/components/emulator-pane/emulator-screen-gesture.test.ts": 57, + "src/renderer/src/components/emulator-pane/emulator-screen-stream-content.test.tsx": 295, + "src/renderer/src/components/emulator-pane/mobile-emulator-agent-setup-visibility.test.ts": 38, + "src/renderer/src/components/emulator-pane/mobile-emulator-hidden-toast.test.tsx": 216, + "src/renderer/src/components/emulator-pane/mobile-emulator-tab-intro-visibility.test.ts": 27, + "src/renderer/src/components/emulator-pane/use-emulator-pane-session.test.tsx": 1678, + "src/renderer/src/components/emulator-pane/use-mobile-emulator-tab-intro-actions.test.tsx": 1366, + "src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.lazy-chunk.test.tsx": 320, + "src/renderer/src/components/error-boundaries/RecoverableRenderErrorBoundary.never-landed-reload.test.tsx": 264, + "src/renderer/src/components/error-boundaries/react-185-bystander-attribution.test.tsx": 337, + "src/renderer/src/components/feature-tips/CliSkillSetupTerminal.test.tsx": 1476, + "src/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.test.tsx": 313, + "src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.test.tsx": 116, + "src/renderer/src/components/feature-tips/NativeChatUpgradeTipDialog.test.tsx": 2110, + "src/renderer/src/components/feature-tips/SessionSearchTipDialog.test.tsx": 370, + "src/renderer/src/components/feature-tips/VoiceDictationFeatureTipVisual.test.tsx": 330, + "src/renderer/src/components/feature-tips/VoiceDictationTipDialog.test.tsx": 325, + "src/renderer/src/components/feature-tips/feature-tip-cli-install-action.test.ts": 34, + "src/renderer/src/components/feature-tips/feature-tip-modal-state.test.ts": 321, + "src/renderer/src/components/feature-tips/feature-tip-startup-gate.test.ts": 270, + "src/renderer/src/components/feature-tips/feature-tip-telemetry.test.ts": 51, + "src/renderer/src/components/feature-tips/use-session-search-tip-setup.test.tsx": 422, + "src/renderer/src/components/feature-wall/AgentCapabilitiesSetupAction.test.ts": 1407, + "src/renderer/src/components/feature-wall/ConnectIntegrationsList.test.tsx": 767, + "src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx": 1588, + "src/renderer/src/components/feature-wall/FullDiskAccessSetupPrompt.test.ts": 351, + "src/renderer/src/components/feature-wall/KeepAwakeCard.test.tsx": 710, + "src/renderer/src/components/feature-wall/agents-orchestration/UsageAccountsCard.test.tsx": 345, + "src/renderer/src/components/feature-wall/browser-animated-visual-sequence.test.ts": 41, + "src/renderer/src/components/feature-wall/feature-wall-animation-visibility.test.tsx": 724, + "src/renderer/src/components/feature-wall/feature-wall-rail-navigation.test.ts": 36, + "src/renderer/src/components/feature-wall/feature-wall-setup-checklist-localized-copy.test.ts": 861, + "src/renderer/src/components/feature-wall/feature-wall-setup-progress.test.ts": 51, + "src/renderer/src/components/feature-wall/feature-wall-shortcut-labels.test.tsx": 1548, + "src/renderer/src/components/feature-wall/feature-wall-usage-tracking.test.ts": 67, + "src/renderer/src/components/feature-wall/use-feature-wall-completion.test.ts": 64, + "src/renderer/src/components/feature-wall/use-feature-wall-tour-telemetry.test.ts": 30, + "src/renderer/src/components/feature-wall/use-integration-connection-status.test.ts": 1373, + "src/renderer/src/components/feature-wall/workbench-terminal-storyboard-sequence.test.ts": 55, + "src/renderer/src/components/file-path-cursor-tooltip.test.ts": 184, + "src/renderer/src/components/floating-terminal/FloatingTerminalOrchestrationDialog.freshness.test.tsx": 561, + "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.bounds.test.tsx": 8255, + "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.focus.test.tsx": 11451, + "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.shortcuts.test.tsx": 8034, + "src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tab-lifecycle.test.tsx": 9448, + "src/renderer/src/components/floating-terminal/FloatingTerminalToggleButton.test.tsx": 121, + "src/renderer/src/components/floating-terminal/FloatingTerminalWindowControls.test.tsx": 327, + "src/renderer/src/components/floating-terminal/floating-terminal-panel-bounds.test.ts": 45, + "src/renderer/src/components/floating-terminal/floating-terminal-panel-inputs.test.ts": 268, + "src/renderer/src/components/floating-terminal/floating-terminal-panel-view-state.test.ts": 161, + "src/renderer/src/components/floating-terminal/floating-terminal-trigger-position.test.ts": 58, + "src/renderer/src/components/floating-terminal/floating-workspace-surface-model.test.ts": 266, + "src/renderer/src/components/floating-terminal/floating-workspace-tab-reorder.test.ts": 1521, + "src/renderer/src/components/floating-terminal/use-floating-terminal-close-actions.test.tsx": 1731, + "src/renderer/src/components/github-body-draft-state.test.ts": 34, + "src/renderer/src/components/github-checks-tab-state.test.ts": 33, + "src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section.assignee-identity.test.tsx": 461, + "src/renderer/src/components/github-item-dialog/inspect-pull-request/pr-files-combined-diff-section-index.test.tsx": 373, + "src/renderer/src/components/github-item-dialog/load-item-details/work-item-details-issue-target.test.ts": 62, + "src/renderer/src/components/github-link-copy-state.test.ts": 29, + "src/renderer/src/components/github-pr-merge-state.test.ts": 80, + "src/renderer/src/components/github-pr-reviewer-display.test.ts": 52, + "src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts": 126, + "src/renderer/src/components/github-project/ProjectBoard.test.tsx": 595, + "src/renderer/src/components/github-project/ProjectRoadmap.test.tsx": 1861, + "src/renderer/src/components/github-project/ProjectViewStates.test.tsx": 360, + "src/renderer/src/components/github-project/github-project-picker-filter.test.ts": 52, + "src/renderer/src/components/github-project/group-sort.test.ts": 52, + "src/renderer/src/components/github-project/project-dialog-state.test.ts": 37, + "src/renderer/src/components/github-project/project-picker-browse-cache.test.ts": 51, + "src/renderer/src/components/github-project/project-picker-input.test.ts": 235, + "src/renderer/src/components/github-project/project-row-filtering.test.ts": 55, + "src/renderer/src/components/github-project/project-view-wrapper-source-context-boundary.test.ts": 38, + "src/renderer/src/components/github-project/project-visible-table-cache.test.ts": 36, + "src/renderer/src/components/github/PRFilterPickers.test.ts": 133, + "src/renderer/src/components/github/github-markdown-image-url.test.ts": 43, + "src/renderer/src/components/github/github-mention-option-filter.test.ts": 52, + "src/renderer/src/components/github/github-pr-reviewer-candidate-filter.test.ts": 37, + "src/renderer/src/components/github/github-user-avatar.test.tsx": 216, + "src/renderer/src/components/github/github-work-item-assignee-filter.test.ts": 50, + "src/renderer/src/components/github/github-work-item-issue-target.test.ts": 352, + "src/renderer/src/components/github/github-work-item-label-filter.test.ts": 49, + "src/renderer/src/components/github/github-work-item-mutation-target.test.ts": 252, + "src/renderer/src/components/github/pr-comment-code-context.test.ts": 48, + "src/renderer/src/components/github/pr-file-content-size.test.ts": 40, + "src/renderer/src/components/github/use-image-input.test.ts": 1923, + "src/renderer/src/components/jira-attachment-image.test.tsx": 400, + "src/renderer/src/components/jira-connect-scoped-token.test.tsx": 811, + "src/renderer/src/components/jira-create-adf.test.ts": 37, + "src/renderer/src/components/jira-issue-assignee-control.test.tsx": 2518, + "src/renderer/src/components/jira-issue-transition-control.test.tsx": 633, + "src/renderer/src/components/jira-issue-workspace-actions.test.ts": 68, + "src/renderer/src/components/jira-issue-workspace-image-owner.test.tsx": 317, + "src/renderer/src/components/jira-issue-workspace-request-lifetime.test.tsx": 588, + "src/renderer/src/components/jira-list-columns.test.ts": 197, + "src/renderer/src/components/jira-project-picker-filter.test.ts": 45, + "src/renderer/src/components/jira-project-picker.test.tsx": 463, + "src/renderer/src/components/jira-prompt-templates.test.ts": 46, + "src/renderer/src/components/jira-self-user.test.ts": 43, + "src/renderer/src/components/landing-preflight-dismissal.test.ts": 41, + "src/renderer/src/components/landing-preflight-issues.test.ts": 90, + "src/renderer/src/components/landing-preflight-runtime-boundary.test.ts": 1620, + "src/renderer/src/components/landing-preflight-unverifiable-host.test.tsx": 1679, + "src/renderer/src/components/linear-api-key-dialog-state.test.ts": 40, + "src/renderer/src/components/linear-issue-attribute-filter-coverage-agreement.test.ts": 688, + "src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx": 1684, + "src/renderer/src/components/linear-issue-attribute-filter-primary-team.test.ts": 37, + "src/renderer/src/components/linear-issue-attribute-filter-team-ids.test.ts": 60, + "src/renderer/src/components/linear-issue-project-selector.test.tsx": 476, + "src/renderer/src/components/linear-issue-text-draft-state.test.ts": 33, + "src/renderer/src/components/linear-issue-text-save-plan.test.ts": 32, + "src/renderer/src/components/linear-issue-view-storage.test.ts": 181, + "src/renderer/src/components/linear-issue-workspace-detail-state.test.tsx": 223, + "src/renderer/src/components/linear-issue-workspace-text.test.ts": 99, + "src/renderer/src/components/linear-project-presentation.test.ts": 29, + "src/renderer/src/components/linear-project-search-query.test.ts": 42, + "src/renderer/src/components/linear-project-view-surfaces.test.tsx": 422, + "src/renderer/src/components/linear-scope-selector.test.ts": 133, + "src/renderer/src/components/link-actions/LinkActionPopover.test.tsx": 562, + "src/renderer/src/components/maintenance/update-card/update-card-error-model.test.ts": 94, + "src/renderer/src/components/mobile/MobileHero.test.tsx": 2453, + "src/renderer/src/components/mobile/MobilePage.test.tsx": 1616, + "src/renderer/src/components/mobile/MobilePageToolbar.test.tsx": 257, + "src/renderer/src/components/mobile/NetworkInterfacePicker.test.tsx": 413, + "src/renderer/src/components/mobile/WindowsFirewallNotice.test.tsx": 456, + "src/renderer/src/components/mobile/mobile-page-stage.test.ts": 36, + "src/renderer/src/components/mobile/paired-mobile-devices.test.ts": 187, + "src/renderer/src/components/mobile/use-mobile-page-paired-devices.test.ts": 302, + "src/renderer/src/components/mobile/use-mobile-pairing-address-preference.test.tsx": 243, + "src/renderer/src/components/native-chat-launch-resume-decision.test.tsx": 1509, + "src/renderer/src/components/native-chat-restart-action-notifications.test.ts": 107, + "src/renderer/src/components/native-chat-resume-activity-label.test.ts": 73, + "src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx": 2577, + "src/renderer/src/components/native-chat-resume-failed-chat-watch.test.ts": 1492, + "src/renderer/src/components/native-chat-resume-failure-guidance.test.ts": 67, + "src/renderer/src/components/native-chat-resume-finished-run.test.tsx": 3568, + "src/renderer/src/components/native-chat-resume-on-restart-grouping.test.ts": 92, + "src/renderer/src/components/native-chat-resume-on-restart-outcome-toast.test.ts": 1413, + "src/renderer/src/components/native-chat-resume-result-view.test.tsx": 2313, + "src/renderer/src/components/native-chat-resume-run.test.ts": 45, + "src/renderer/src/components/native-chat-resume-skipped-progress.test.tsx": 2665, + "src/renderer/src/components/native-chat/NativeChatAgentMessageRow.test.tsx": 2507, + "src/renderer/src/components/native-chat/NativeChatAgentMessageSenders.test.tsx": 1661, + "src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx": 2282, + "src/renderer/src/components/native-chat/NativeChatAutocompleteMenus.test.tsx": 468, + "src/renderer/src/components/native-chat/NativeChatBackgroundTaskRun.test.tsx": 396, + "src/renderer/src/components/native-chat/NativeChatBackgroundTasksStatus.test.tsx": 693, + "src/renderer/src/components/native-chat/NativeChatCodeBlock.load-failure.test.tsx": 431, + "src/renderer/src/components/native-chat/NativeChatCodeBlock.test.tsx": 4272, + "src/renderer/src/components/native-chat/NativeChatComposer.paste-remount.test.tsx": 1842, + "src/renderer/src/components/native-chat/NativeChatComposer.test.tsx": 1112, + "src/renderer/src/components/native-chat/NativeChatComposerActions.test.tsx": 695, + "src/renderer/src/components/native-chat/NativeChatComposerNotices.test.tsx": 370, + "src/renderer/src/components/native-chat/NativeChatContextUsageRing.test.tsx": 927, + "src/renderer/src/components/native-chat/NativeChatEmptyState.test.tsx": 420, + "src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.test.tsx": 1836, + "src/renderer/src/components/native-chat/NativeChatInlineVisual.open-as-file.test.tsx": 1984, + "src/renderer/src/components/native-chat/NativeChatInteractiveCard.test.tsx": 1564, + "src/renderer/src/components/native-chat/NativeChatInterruptedContinue.test.tsx": 786, + "src/renderer/src/components/native-chat/NativeChatJumpToLatest.test.tsx": 313, + "src/renderer/src/components/native-chat/NativeChatMarkdown.code-wrap.test.tsx": 2290, + "src/renderer/src/components/native-chat/NativeChatMarkdown.mermaid.test.tsx": 1634, + "src/renderer/src/components/native-chat/NativeChatMarkdown.test.tsx": 1822, + "src/renderer/src/components/native-chat/NativeChatMarkdown.visual.test.tsx": 2953, + "src/renderer/src/components/native-chat/NativeChatMessageList.growth-windowing.test.tsx": 5930, + "src/renderer/src/components/native-chat/NativeChatMessageList.live-reasoning.test.tsx": 9840, + "src/renderer/src/components/native-chat/NativeChatMessageList.older-history.test.tsx": 4463, + "src/renderer/src/components/native-chat/NativeChatMessageList.opening-send.test.tsx": 3299, + "src/renderer/src/components/native-chat/NativeChatMessageList.provider-frame.test.tsx": 1653, + "src/renderer/src/components/native-chat/NativeChatMessageList.queued-turn-order.test.tsx": 3018, + "src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx": 17796, + "src/renderer/src/components/native-chat/NativeChatMessageList.reader-navigation.test.tsx": 5187, + "src/renderer/src/components/native-chat/NativeChatMessageList.rewind-rows.test.tsx": 2401, + "src/renderer/src/components/native-chat/NativeChatMessageList.stream-render.perf.test.tsx": 3720, + "src/renderer/src/components/native-chat/NativeChatMessageList.subagent-sections.test.tsx": 4144, + "src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx": 3031, + "src/renderer/src/components/native-chat/NativeChatMessageList.tool-stream-cost.test.tsx": 8326, + "src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx": 3007, + "src/renderer/src/components/native-chat/NativeChatMessageList.turn-membership.test.tsx": 3090, + "src/renderer/src/components/native-chat/NativeChatMessageList.turn-status.test.tsx": 3275, + "src/renderer/src/components/native-chat/NativeChatMessageList.unsent-message.test.tsx": 2272, + "src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx": 5544, + "src/renderer/src/components/native-chat/NativeChatMessageList.work-run-memo.test.tsx": 3104, + "src/renderer/src/components/native-chat/NativeChatMessageRail.test.tsx": 1052, + "src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx": 2306, + "src/renderer/src/components/native-chat/NativeChatMessageRow.work-run.test.tsx": 2250, + "src/renderer/src/components/native-chat/NativeChatMessageTimestamp.test.tsx": 527, + "src/renderer/src/components/native-chat/NativeChatModelCombobox.test.tsx": 608, + "src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx": 3357, + "src/renderer/src/components/native-chat/NativeChatPacedMarkdown.file-links.test.tsx": 1757, + "src/renderer/src/components/native-chat/NativeChatPacedMarkdown.mermaid.test.tsx": 2152, + "src/renderer/src/components/native-chat/NativeChatPermissionModePicker.test.tsx": 672, + "src/renderer/src/components/native-chat/NativeChatPromptEditor.paragraph-selection.test.tsx": 387, + "src/renderer/src/components/native-chat/NativeChatPromptEditor.test.tsx": 598, + "src/renderer/src/components/native-chat/NativeChatQuestionCard.test.tsx": 582, + "src/renderer/src/components/native-chat/NativeChatQueuedMessageEditor.test.tsx": 2160, + "src/renderer/src/components/native-chat/NativeChatQueuedMessageList.inline-edit.test.tsx": 1003, + "src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx": 2098, + "src/renderer/src/components/native-chat/NativeChatReasoningRow.test.tsx": 2024, + "src/renderer/src/components/native-chat/NativeChatResolutionReceipt.empty-answer.test.tsx": 466, + "src/renderer/src/components/native-chat/NativeChatResolutionReceipt.test.tsx": 941, + "src/renderer/src/components/native-chat/NativeChatResolvedView.context-menu.test.tsx": 3343, + "src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-composer.test.tsx": 3683, + "src/renderer/src/components/native-chat/NativeChatResolvedView.prompt-card-input.test.tsx": 3003, + "src/renderer/src/components/native-chat/NativeChatResolvedView.reveal-latest.test.tsx": 2183, + "src/renderer/src/components/native-chat/NativeChatResolvedView.turn-status.test.tsx": 3452, + "src/renderer/src/components/native-chat/NativeChatRewindAction.target.test.tsx": 2869, + "src/renderer/src/components/native-chat/NativeChatRewindAction.test.tsx": 2594, + "src/renderer/src/components/native-chat/NativeChatSelectionQuote.test.tsx": 541, + "src/renderer/src/components/native-chat/NativeChatSessionOptionPickers.focus-return.test.tsx": 2644, + "src/renderer/src/components/native-chat/NativeChatSessionOptionPickers.test.tsx": 1353, + "src/renderer/src/components/native-chat/NativeChatStructuredQuestionCard.test.tsx": 402, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.approval-and-start-failure.test.tsx": 2229, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.availability-notice.test.tsx": 2310, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.goal-dock.test.tsx": 2740, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.history-read-state.test.tsx": 1974, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.host-outage.test.tsx": 2864, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsx": 3067, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.notice-card.test.tsx": 2620, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.orca-stop-continue.test.tsx": 2142, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.refusal-notices.test.tsx": 2657, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.stopping.test.tsx": 2060, + "src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx": 2965, + "src/renderer/src/components/native-chat/NativeChatStructuredSessionStatus.test.tsx": 405, + "src/renderer/src/components/native-chat/NativeChatSubagentRun.test.tsx": 801, + "src/renderer/src/components/native-chat/NativeChatTaskList.test.tsx": 430, + "src/renderer/src/components/native-chat/NativeChatThreadGoalBanner.test.tsx": 545, + "src/renderer/src/components/native-chat/NativeChatToolFileTarget.test.tsx": 487, + "src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx": 1198, + "src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx": 1299, + "src/renderer/src/components/native-chat/NativeChatToolRun.identity.test.tsx": 1011, + "src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx": 1259, + "src/renderer/src/components/native-chat/NativeChatToolRunCallCounts.test.tsx": 810, + "src/renderer/src/components/native-chat/NativeChatToolRunMemberList.identity.test.tsx": 899, + "src/renderer/src/components/native-chat/NativeChatTranscriptChrome.test.tsx": 1962, + "src/renderer/src/components/native-chat/NativeChatTranscriptRow.work-run.test.tsx": 2712, + "src/renderer/src/components/native-chat/NativeChatTurnActivityLine.test.tsx": 2044, + "src/renderer/src/components/native-chat/NativeChatUserMessageFold.test.tsx": 2446, + "src/renderer/src/components/native-chat/NativeChatView.test.tsx": 440, + "src/renderer/src/components/native-chat/NativeChatVisualFrame.test.tsx": 804, + "src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.test.tsx": 2280, + "src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.test.tsx": 326, + "src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.launch-directory.test.tsx": 1487, + "src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.test.tsx": 2246, + "src/renderer/src/components/native-chat/agent-session-failure-words-text.test.ts": 1624, + "src/renderer/src/components/native-chat/agent-session-write-notice-text.test.ts": 858, + "src/renderer/src/components/native-chat/background-task-words-text.test.ts": 137, + "src/renderer/src/components/native-chat/claude-model-switch-confirmation.test.ts": 1457, + "src/renderer/src/components/native-chat/claude-terminal-session-options.test.ts": 63, + "src/renderer/src/components/native-chat/code-fence-language-label.test.ts": 61, + "src/renderer/src/components/native-chat/native-chat-active-rail-item.test.ts": 49, + "src/renderer/src/components/native-chat/native-chat-agent-message-sender-host.test.tsx": 3120, + "src/renderer/src/components/native-chat/native-chat-agent-message-sender-identity.test.tsx": 1661, + "src/renderer/src/components/native-chat/native-chat-appearance-palette.test.ts": 534, + "src/renderer/src/components/native-chat/native-chat-appearance-style.test.ts": 307, + "src/renderer/src/components/native-chat/native-chat-appearance-write.test.ts": 310, + "src/renderer/src/components/native-chat/native-chat-attachment-upload.test.ts": 1396, + "src/renderer/src/components/native-chat/native-chat-autoscroll.test.ts": 34, + "src/renderer/src/components/native-chat/native-chat-availability.test.ts": 55, + "src/renderer/src/components/native-chat/native-chat-claude-sign-in.test.tsx": 2233, + "src/renderer/src/components/native-chat/native-chat-command-language.test.ts": 47, + "src/renderer/src/components/native-chat/native-chat-compaction-visibility.test.ts": 422, + "src/renderer/src/components/native-chat/native-chat-composer-autogrow.test.tsx": 1741, + "src/renderer/src/components/native-chat/native-chat-composer-composition.test.tsx": 1995, + "src/renderer/src/components/native-chat/native-chat-composer-draft-addition.test.ts": 314, + "src/renderer/src/components/native-chat/native-chat-composer-draft-comparison.test.ts": 29, + "src/renderer/src/components/native-chat/native-chat-composer-draft-indexeddb.test.ts": 123, + "src/renderer/src/components/native-chat/native-chat-composer-draft-lifecycle.test.tsx": 2070, + "src/renderer/src/components/native-chat/native-chat-composer-draft-store.test.ts": 1584, + "src/renderer/src/components/native-chat/native-chat-composer-drop-scope.test.tsx": 1138, + "src/renderer/src/components/native-chat/native-chat-composer-field-resume.test.tsx": 2040, + "src/renderer/src/components/native-chat/native-chat-composer-ime-file-drop.test.tsx": 1077, + "src/renderer/src/components/native-chat/native-chat-composer-placeholder.test.ts": 40, + "src/renderer/src/components/native-chat/native-chat-composer-primary-action.test.ts": 35, + "src/renderer/src/components/native-chat/native-chat-composer-prompt-recall.test.tsx": 1622, + "src/renderer/src/components/native-chat/native-chat-composer-reveal-focus.test.tsx": 265, + "src/renderer/src/components/native-chat/native-chat-composer-state.test.ts": 82, + "src/renderer/src/components/native-chat/native-chat-composer-upload-completion.test.tsx": 1869, + "src/renderer/src/components/native-chat/native-chat-composer-workspace-file-drop.test.tsx": 1012, + "src/renderer/src/components/native-chat/native-chat-context-command.test.tsx": 584, + "src/renderer/src/components/native-chat/native-chat-context-usage-summary.test.ts": 29, + "src/renderer/src/components/native-chat/native-chat-diff.test.ts": 73, + "src/renderer/src/components/native-chat/native-chat-dismiss-key.test.ts": 40, + "src/renderer/src/components/native-chat/native-chat-draft-cache.test.ts": 96, + "src/renderer/src/components/native-chat/native-chat-element-file-drop.test.tsx": 1759, + "src/renderer/src/components/native-chat/native-chat-external-attachments-destination.test.tsx": 660, + "src/renderer/src/components/native-chat/native-chat-file-link-existence.test.ts": 1384, + "src/renderer/src/components/native-chat/native-chat-file-link.test.ts": 301, + "src/renderer/src/components/native-chat/native-chat-find-visibility.test.ts": 176, + "src/renderer/src/components/native-chat/native-chat-floating-file-drop.test.tsx": 630, + "src/renderer/src/components/native-chat/native-chat-font-size.test.ts": 77, + "src/renderer/src/components/native-chat/native-chat-http-link-source-owner.test.ts": 40, + "src/renderer/src/components/native-chat/native-chat-image-paste.test.ts": 50, + "src/renderer/src/components/native-chat/native-chat-image-runtime-context.test.ts": 1265, + "src/renderer/src/components/native-chat/native-chat-incremental-assembler.test.ts": 309, + "src/renderer/src/components/native-chat/native-chat-interactive-prompt.test.ts": 114, + "src/renderer/src/components/native-chat/native-chat-launch-default-adoption.test.ts": 103, "src/renderer/src/components/native-chat/native-chat-launch-draft-resolution.test.ts": 51, - "src/renderer/src/components/native-chat/native-chat-launch-draft-send.test.ts": 63, - "src/renderer/src/components/native-chat/native-chat-layout-actions.test.ts": 1604, - "src/renderer/src/components/native-chat/native-chat-leaf-routing.test.ts": 60, - "src/renderer/src/components/native-chat/native-chat-live-message-preparation.test.ts": 314, - "src/renderer/src/components/native-chat/native-chat-live-status.test.ts": 79, - "src/renderer/src/components/native-chat/native-chat-message-grouping.test.ts": 280, - "src/renderer/src/components/native-chat/native-chat-message-list-projection.test.ts": 298, - "src/renderer/src/components/native-chat/native-chat-message-rail-items.test.ts": 388, - "src/renderer/src/components/native-chat/native-chat-observed-send.test.ts": 82, - "src/renderer/src/components/native-chat/native-chat-owner-selectors.test.tsx": 505, - "src/renderer/src/components/native-chat/native-chat-pagination.test.ts": 43, - "src/renderer/src/components/native-chat/native-chat-pane-file-drop.test.tsx": 435, - "src/renderer/src/components/native-chat/native-chat-pane-resolution.test.ts": 65, - "src/renderer/src/components/native-chat/native-chat-paste-routing.test.tsx": 1916, - "src/renderer/src/components/native-chat/native-chat-pending-attachment-cache.test.ts": 83, - "src/renderer/src/components/native-chat/native-chat-pending-occurrence.test.ts": 63, - "src/renderer/src/components/native-chat/native-chat-pending-pasted-content.test.ts": 74, - "src/renderer/src/components/native-chat/native-chat-pending.test.ts": 133, - "src/renderer/src/components/native-chat/native-chat-pinned-rows.test.ts": 60, - "src/renderer/src/components/native-chat/native-chat-preassembled-session-parity.test.ts": 471, - "src/renderer/src/components/native-chat/native-chat-prompt-document-text.test.ts": 57, - "src/renderer/src/components/native-chat/native-chat-prompt-editor-store-value.test.tsx": 2153, - "src/renderer/src/components/native-chat/native-chat-pty-retired-model.test.ts": 118, - "src/renderer/src/components/native-chat/native-chat-pty-session-options.test.ts": 435, - "src/renderer/src/components/native-chat/native-chat-queue-send-confirm.test.tsx": 2050, - "src/renderer/src/components/native-chat/native-chat-rail-outline-parity.test.ts": 363, - "src/renderer/src/components/native-chat/native-chat-reader-scroll-input.test.tsx": 276, - "src/renderer/src/components/native-chat/native-chat-resolved-path-ownership.test.ts": 51, - "src/renderer/src/components/native-chat/native-chat-retire-persisted-model.test.ts": 346, - "src/renderer/src/components/native-chat/native-chat-row-height-estimate.test.ts": 101, - "src/renderer/src/components/native-chat/native-chat-runtime-owner-selector.test.ts": 371, - "src/renderer/src/components/native-chat/native-chat-runtime-owner.test.ts": 326, - "src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts": 1348, - "src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts": 222, - "src/renderer/src/components/native-chat/native-chat-scrape-fallback.test.ts": 45, - "src/renderer/src/components/native-chat/native-chat-send-eligibility.test.ts": 38, - "src/renderer/src/components/native-chat/native-chat-send.test.ts": 54, - "src/renderer/src/components/native-chat/native-chat-sent-prompt-history.test.ts": 252, - "src/renderer/src/components/native-chat/native-chat-session-assembler.test.ts": 321, - "src/renderer/src/components/native-chat/native-chat-session-option-discovery.test.ts": 1181, - "src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts": 1681, - "src/renderer/src/components/native-chat/native-chat-session-option-labels.test.ts": 60, - "src/renderer/src/components/native-chat/native-chat-session-transport.test.ts": 123, - "src/renderer/src/components/native-chat/native-chat-shared-copy-matches-catalog.test.ts": 246, - "src/renderer/src/components/native-chat/native-chat-shortcut.test.ts": 60, - "src/renderer/src/components/native-chat/native-chat-split-shortcut.test.ts": 68, - "src/renderer/src/components/native-chat/native-chat-structured-send-composition-clear.test.tsx": 1857, - "src/renderer/src/components/native-chat/native-chat-structured-session-notices.command-failure.test.tsx": 1836, - "src/renderer/src/components/native-chat/native-chat-structured-session-notices.create-message.test.tsx": 1788, - "src/renderer/src/components/native-chat/native-chat-subagent-delegation.test.ts": 61, - "src/renderer/src/components/native-chat/native-chat-subagent-group-words-text.test.ts": 159, - "src/renderer/src/components/native-chat/native-chat-subagent-handoff-copy-budget.test.ts": 339, - "src/renderer/src/components/native-chat/native-chat-subagent-section-work-runs.test.ts": 377, - "src/renderer/src/components/native-chat/native-chat-subagent-sections-retention.test.ts": 774, - "src/renderer/src/components/native-chat/native-chat-subagent-sections.test.ts": 444, - "src/renderer/src/components/native-chat/native-chat-submission-navigation.test.tsx": 789, - "src/renderer/src/components/native-chat/native-chat-tab-agent-entry.test.ts": 47, - "src/renderer/src/components/native-chat/native-chat-tab-directory.test.ts": 280, - "src/renderer/src/components/native-chat/native-chat-task-list-history.test.ts": 69, - "src/renderer/src/components/native-chat/native-chat-task-list-state.test.ts": 68, - "src/renderer/src/components/native-chat/native-chat-terminal-turn.test.ts": 73, - "src/renderer/src/components/native-chat/native-chat-thread-goal-rows.test.ts": 54, - "src/renderer/src/components/native-chat/native-chat-tool-fold.test.ts": 72, - "src/renderer/src/components/native-chat/native-chat-tool-summary.test.ts": 54, - "src/renderer/src/components/native-chat/native-chat-transcript-slots.test.ts": 355, - "src/renderer/src/components/native-chat/native-chat-transcript-work-runs.test.ts": 328, + "src/renderer/src/components/native-chat/native-chat-launch-draft-send.test.ts": 32, + "src/renderer/src/components/native-chat/native-chat-layout-actions.test.ts": 1455, + "src/renderer/src/components/native-chat/native-chat-leaf-routing.test.ts": 63, + "src/renderer/src/components/native-chat/native-chat-live-message-preparation.test.ts": 267, + "src/renderer/src/components/native-chat/native-chat-live-status.test.ts": 49, + "src/renderer/src/components/native-chat/native-chat-message-grouping.test.ts": 289, + "src/renderer/src/components/native-chat/native-chat-message-list-projection.test.ts": 288, + "src/renderer/src/components/native-chat/native-chat-message-rail-items.test.ts": 314, + "src/renderer/src/components/native-chat/native-chat-observed-send.test.ts": 62, + "src/renderer/src/components/native-chat/native-chat-owner-selectors.test.tsx": 611, + "src/renderer/src/components/native-chat/native-chat-pagination.test.ts": 42, + "src/renderer/src/components/native-chat/native-chat-pane-file-drop.test.tsx": 353, + "src/renderer/src/components/native-chat/native-chat-pane-resolution.test.ts": 44, + "src/renderer/src/components/native-chat/native-chat-paste-routing.test.tsx": 1918, + "src/renderer/src/components/native-chat/native-chat-pending-attachment-cache.test.ts": 70, + "src/renderer/src/components/native-chat/native-chat-pending-occurrence.test.ts": 53, + "src/renderer/src/components/native-chat/native-chat-pending-pasted-content.test.ts": 51, + "src/renderer/src/components/native-chat/native-chat-pending.test.ts": 109, + "src/renderer/src/components/native-chat/native-chat-pinned-rows.test.ts": 33, + "src/renderer/src/components/native-chat/native-chat-preassembled-session-parity.test.ts": 511, + "src/renderer/src/components/native-chat/native-chat-prompt-document-text.test.ts": 45, + "src/renderer/src/components/native-chat/native-chat-prompt-document.test.ts": 132, + "src/renderer/src/components/native-chat/native-chat-prompt-editor-store-value.test.tsx": 691, + "src/renderer/src/components/native-chat/native-chat-pty-retired-model.test.ts": 95, + "src/renderer/src/components/native-chat/native-chat-pty-session-options.test.ts": 438, + "src/renderer/src/components/native-chat/native-chat-queue-send-confirm.test.tsx": 2051, + "src/renderer/src/components/native-chat/native-chat-rail-outline-parity.test.ts": 550, + "src/renderer/src/components/native-chat/native-chat-reader-scroll-input.test.tsx": 293, + "src/renderer/src/components/native-chat/native-chat-reply-reveals.test.tsx": 206, + "src/renderer/src/components/native-chat/native-chat-resolved-path-ownership.test.ts": 38, + "src/renderer/src/components/native-chat/native-chat-retire-persisted-model.test.ts": 313, + "src/renderer/src/components/native-chat/native-chat-row-height-estimate.test.ts": 88, + "src/renderer/src/components/native-chat/native-chat-runtime-owner-selector.test.ts": 301, + "src/renderer/src/components/native-chat/native-chat-runtime-owner.test.ts": 300, + "src/renderer/src/components/native-chat/native-chat-runtime-send-launch-draft.test.ts": 1277, + "src/renderer/src/components/native-chat/native-chat-runtime-send.test.ts": 234, + "src/renderer/src/components/native-chat/native-chat-scrape-fallback.test.ts": 34, + "src/renderer/src/components/native-chat/native-chat-send-eligibility.test.ts": 28, + "src/renderer/src/components/native-chat/native-chat-send.test.ts": 45, + "src/renderer/src/components/native-chat/native-chat-sent-prompt-history.test.ts": 283, + "src/renderer/src/components/native-chat/native-chat-session-assembler.test.ts": 265, + "src/renderer/src/components/native-chat/native-chat-session-option-discovery.test.ts": 1142, + "src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts": 1735, + "src/renderer/src/components/native-chat/native-chat-session-option-labels.test.ts": 40, + "src/renderer/src/components/native-chat/native-chat-session-transport.test.ts": 272, + "src/renderer/src/components/native-chat/native-chat-shared-copy-matches-catalog.test.ts": 123, + "src/renderer/src/components/native-chat/native-chat-shortcut.test.ts": 32, + "src/renderer/src/components/native-chat/native-chat-split-shortcut.test.ts": 63, + "src/renderer/src/components/native-chat/native-chat-structured-send-composition-clear.test.tsx": 1367, + "src/renderer/src/components/native-chat/native-chat-structured-session-notices.command-failure.test.tsx": 1588, + "src/renderer/src/components/native-chat/native-chat-structured-session-notices.create-message.test.tsx": 1640, + "src/renderer/src/components/native-chat/native-chat-subagent-delegation.test.ts": 37, + "src/renderer/src/components/native-chat/native-chat-subagent-group-words-text.test.ts": 133, + "src/renderer/src/components/native-chat/native-chat-subagent-handoff-copy-budget.test.ts": 371, + "src/renderer/src/components/native-chat/native-chat-subagent-section-work-runs.test.ts": 291, + "src/renderer/src/components/native-chat/native-chat-subagent-sections-retention.test.ts": 712, + "src/renderer/src/components/native-chat/native-chat-subagent-sections.test.ts": 429, + "src/renderer/src/components/native-chat/native-chat-submission-navigation.test.tsx": 868, + "src/renderer/src/components/native-chat/native-chat-tab-agent-entry.test.ts": 38, + "src/renderer/src/components/native-chat/native-chat-tab-directory.test.ts": 325, + "src/renderer/src/components/native-chat/native-chat-task-list-history.test.ts": 48, + "src/renderer/src/components/native-chat/native-chat-task-list-state.test.ts": 46, + "src/renderer/src/components/native-chat/native-chat-terminal-turn.test.ts": 63, + "src/renderer/src/components/native-chat/native-chat-thread-goal-rows.test.ts": 45, + "src/renderer/src/components/native-chat/native-chat-tool-fold.test.ts": 75, + "src/renderer/src/components/native-chat/native-chat-tool-summary.test.ts": 40, + "src/renderer/src/components/native-chat/native-chat-transcript-slots.test.ts": 363, + "src/renderer/src/components/native-chat/native-chat-transcript-work-runs.test.ts": 389, "src/renderer/src/components/native-chat/native-chat-turn-diffs.test.ts": 328, - "src/renderer/src/components/native-chat/native-chat-typing-redirect.test.ts": 62, - "src/renderer/src/components/native-chat/native-chat-view-file-drop.test.tsx": 1879, - "src/renderer/src/components/native-chat/native-chat-view-state.test.ts": 54, - "src/renderer/src/components/native-chat/native-chat-visual-markdown-syntax.test.tsx": 1741, - "src/renderer/src/components/native-chat/native-chat-visual-read-client.test.ts": 258, - "src/renderer/src/components/native-chat/native-chat-waiting-partition-budget.test.ts": 653, + "src/renderer/src/components/native-chat/native-chat-typing-redirect.test.ts": 47, + "src/renderer/src/components/native-chat/native-chat-view-file-drop.test.tsx": 1553, + "src/renderer/src/components/native-chat/native-chat-view-state.test.ts": 46, + "src/renderer/src/components/native-chat/native-chat-visual-markdown-syntax.test.tsx": 1581, + "src/renderer/src/components/native-chat/native-chat-visual-read-client.test.ts": 259, + "src/renderer/src/components/native-chat/native-chat-waiting-partition-budget.test.ts": 912, "src/renderer/src/components/native-chat/native-chat-web-link-actions.test.ts": 330, - "src/renderer/src/components/native-chat/native-chat-work-run-pairing.test.ts": 419, - "src/renderer/src/components/native-chat/native-chat-working-status-shared-clock.test.tsx": 300, - "src/renderer/src/components/native-chat/native-chat-working-suppression.test.ts": 53, - "src/renderer/src/components/native-chat/omp-model-switch-capability.test.ts": 130, - "src/renderer/src/components/native-chat/serialized-screen-background-rows.test.ts": 124, - "src/renderer/src/components/native-chat/structured-agent-question-projection.test.ts": 306, - "src/renderer/src/components/native-chat/structured-agent-registered-agent-surface.test.ts": 435, - "src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.kept-card.test.ts": 369, - "src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.test.ts": 392, - "src/renderer/src/components/native-chat/structured-agent-session-legacy-outbox.test.ts": 360, - "src/renderer/src/components/native-chat/structured-agent-session-message-hand-back.test.ts": 218, - "src/renderer/src/components/native-chat/structured-agent-session-message-projection.rejected-in-place.test.ts": 405, - "src/renderer/src/components/native-chat/structured-agent-session-message-projection.test.ts": 325, - "src/renderer/src/components/native-chat/structured-agent-session-message-sender.test.ts": 419, - "src/renderer/src/components/native-chat/structured-agent-session-queue-request.test.ts": 52, - "src/renderer/src/components/native-chat/structured-agent-session-queued-cards.test.ts": 219, - "src/renderer/src/components/native-chat/structured-agent-session-rail-outline.test.ts": 49, - "src/renderer/src/components/native-chat/structured-agent-session-read-transport.test.ts": 294, - "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-clock.test.tsx": 1549, - "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-failed-start.test.tsx": 1883, - "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-stopping.test.tsx": 1608, - "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-unconfirmed-start.test.tsx": 1818, + "src/renderer/src/components/native-chat/native-chat-work-run-pairing.test.ts": 300, + "src/renderer/src/components/native-chat/native-chat-working-status-shared-clock.test.tsx": 237, + "src/renderer/src/components/native-chat/native-chat-working-suppression.test.ts": 28, + "src/renderer/src/components/native-chat/omp-model-switch-capability.test.ts": 118, + "src/renderer/src/components/native-chat/serialized-screen-background-rows.test.ts": 100, + "src/renderer/src/components/native-chat/structured-agent-question-projection.test.ts": 300, + "src/renderer/src/components/native-chat/structured-agent-registered-agent-surface.test.ts": 378, + "src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.kept-card.test.ts": 345, + "src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.test.ts": 346, + "src/renderer/src/components/native-chat/structured-agent-session-legacy-outbox.test.ts": 655, + "src/renderer/src/components/native-chat/structured-agent-session-message-hand-back.test.ts": 184, + "src/renderer/src/components/native-chat/structured-agent-session-message-projection.rejected-in-place.test.ts": 355, + "src/renderer/src/components/native-chat/structured-agent-session-message-projection.test.ts": 320, + "src/renderer/src/components/native-chat/structured-agent-session-message-sender.test.ts": 449, + "src/renderer/src/components/native-chat/structured-agent-session-queue-request.test.ts": 32, + "src/renderer/src/components/native-chat/structured-agent-session-queued-cards.test.ts": 274, + "src/renderer/src/components/native-chat/structured-agent-session-rail-outline.test.ts": 37, + "src/renderer/src/components/native-chat/structured-agent-session-read-transport.test.ts": 326, + "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-clock.test.tsx": 1445, + "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-failed-start.test.tsx": 1749, + "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-stopping.test.tsx": 1514, + "src/renderer/src/components/native-chat/structured-agent-session-status-bridge-unconfirmed-start.test.tsx": 1493, "src/renderer/src/components/native-chat/structured-agent-session-transcript-order.test.ts": 364, - "src/renderer/src/components/native-chat/structured-attention-dispatch.test.ts": 1277, - "src/renderer/src/components/native-chat/structured-attention-surface.test.ts": 1681, - "src/renderer/src/components/native-chat/structured-child-records-switch.test.tsx": 1858, - "src/renderer/src/components/native-chat/structured-conversation-command-send.test.ts": 785, - "src/renderer/src/components/native-chat/structured-session-takeover-report.test.tsx": 588, - "src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsx": 1720, - "src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.unavailable.test.tsx": 1718, - "src/renderer/src/components/native-chat/use-native-chat-appearance-style.test.tsx": 564, - "src/renderer/src/components/native-chat/use-native-chat-availability-notice.test.tsx": 602, - "src/renderer/src/components/native-chat/use-native-chat-composer-app-menu-selection.test.tsx": 297, - "src/renderer/src/components/native-chat/use-native-chat-composer-attachment-previews.test.tsx": 189, - "src/renderer/src/components/native-chat/use-native-chat-composer-attachments.test.tsx": 1797, + "src/renderer/src/components/native-chat/structured-attention-dispatch.test.ts": 1534, + "src/renderer/src/components/native-chat/structured-attention-surface.test.ts": 1328, + "src/renderer/src/components/native-chat/structured-child-records-switch.test.tsx": 1952, + "src/renderer/src/components/native-chat/structured-conversation-command-send.test.ts": 550, + "src/renderer/src/components/native-chat/structured-session-takeover-report.test.tsx": 568, + "src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.test.tsx": 1628, + "src/renderer/src/components/native-chat/use-host-model-catalog-upgrade.unavailable.test.tsx": 1634, + "src/renderer/src/components/native-chat/use-native-chat-appearance-style.test.tsx": 454, + "src/renderer/src/components/native-chat/use-native-chat-availability-notice.test.tsx": 588, + "src/renderer/src/components/native-chat/use-native-chat-composer-app-menu-selection.test.tsx": 275, + "src/renderer/src/components/native-chat/use-native-chat-composer-attachment-previews.test.tsx": 205, + "src/renderer/src/components/native-chat/use-native-chat-composer-attachments.test.tsx": 1752, "src/renderer/src/components/native-chat/use-native-chat-composer-catalog.test.tsx": 523, - "src/renderer/src/components/native-chat/use-native-chat-composer-image-input.test.tsx": 1613, - "src/renderer/src/components/native-chat/use-native-chat-composer-keydown.steer.test.tsx": 457, - "src/renderer/src/components/native-chat/use-native-chat-composer-keydown.test.tsx": 614, - "src/renderer/src/components/native-chat/use-native-chat-composer-notice.test.tsx": 309, - "src/renderer/src/components/native-chat/use-native-chat-composer-paste-session-attachments.test.tsx": 279, - "src/renderer/src/components/native-chat/use-native-chat-composer-paste.test.tsx": 446, - "src/renderer/src/components/native-chat/use-native-chat-composer-preview-lifetime.test.tsx": 1616, - "src/renderer/src/components/native-chat/use-native-chat-composer-recall.test.tsx": 466, - "src/renderer/src/components/native-chat/use-native-chat-composer-submit.test.tsx": 332, - "src/renderer/src/components/native-chat/use-native-chat-context-menu.test.tsx": 2301, - "src/renderer/src/components/native-chat/use-native-chat-external-attachments.test.tsx": 702, - "src/renderer/src/components/native-chat/use-native-chat-file-attachment-actions.test.tsx": 259, - "src/renderer/src/components/native-chat/use-native-chat-file-link-click.test.tsx": 1131, - "src/renderer/src/components/native-chat/use-native-chat-file-link-existence.test.tsx": 706, - "src/renderer/src/components/native-chat/use-native-chat-find.test.tsx": 1343, - "src/renderer/src/components/native-chat/use-native-chat-font-size.test.tsx": 660, - "src/renderer/src/components/native-chat/use-native-chat-hook-status.test.ts": 1604, - "src/renderer/src/components/native-chat/use-native-chat-host-outage.test.tsx": 1609, - "src/renderer/src/components/native-chat/use-native-chat-interactive-prompt-card.test.ts": 1685, - "src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx": 388, - "src/renderer/src/components/native-chat/use-native-chat-launch-draft-adoption.test.tsx": 368, - "src/renderer/src/components/native-chat/use-native-chat-link-actions.test.tsx": 844, - "src/renderer/src/components/native-chat/use-native-chat-live-session-pending.test.ts": 1561, - "src/renderer/src/components/native-chat/use-native-chat-live-session-visibility.test.ts": 1650, - "src/renderer/src/components/native-chat/use-native-chat-live-session.test.ts": 1567, - "src/renderer/src/components/native-chat/use-native-chat-mention-files.test.tsx": 1739, - "src/renderer/src/components/native-chat/use-native-chat-menu-paste-lifetime.test.tsx": 1722, - "src/renderer/src/components/native-chat/use-native-chat-message-rail.test.ts": 778, - "src/renderer/src/components/native-chat/use-native-chat-paced-text.remount.test.tsx": 288, - "src/renderer/src/components/native-chat/use-native-chat-paced-text.test.ts": 45, - "src/renderer/src/components/native-chat/use-native-chat-paste-lifetime.test.tsx": 317, - "src/renderer/src/components/native-chat/use-native-chat-pending-delivery.test.tsx": 350, - "src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.test.tsx": 1548, - "src/renderer/src/components/native-chat/use-native-chat-picker-state.test.tsx": 295, - "src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts": 1743, - "src/renderer/src/components/native-chat/use-native-chat-provisional-launch.test.tsx": 1968, - "src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.test.tsx": 431, - "src/renderer/src/components/native-chat/use-native-chat-rail-history-jump.test.ts": 586, - "src/renderer/src/components/native-chat/use-native-chat-retained-session.test.ts": 217, - "src/renderer/src/components/native-chat/use-native-chat-rewind.test.tsx": 725, - "src/renderer/src/components/native-chat/use-native-chat-row-typography.test.tsx": 1793, - "src/renderer/src/components/native-chat/use-native-chat-send-lifecycle.test.tsx": 252, - "src/renderer/src/components/native-chat/use-native-chat-session-option-command.test.tsx": 1640, - "src/renderer/src/components/native-chat/use-native-chat-session-options.test.ts": 805, - "src/renderer/src/components/native-chat/use-native-chat-skills.react.test.tsx": 972, - "src/renderer/src/components/native-chat/use-native-chat-skills.test.ts": 1523, - "src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.test.tsx": 983, - "src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.surface-owner.test.ts": 590, - "src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.test.ts": 1442, - "src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.first-reply.test.tsx": 261, - "src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.test.tsx": 355, - "src/renderer/src/components/native-chat/use-native-chat-transcript-window.options.test.tsx": 684, - "src/renderer/src/components/native-chat/use-structured-agent-session-command-send-gate.test.tsx": 1939, - "src/renderer/src/components/native-chat/use-structured-agent-session-command-start-failure.test.tsx": 2211, - "src/renderer/src/components/native-chat/use-structured-agent-session-context-usage.test.tsx": 2614, - "src/renderer/src/components/native-chat/use-structured-agent-session-delivery-notices.test.tsx": 575, - "src/renderer/src/components/native-chat/use-structured-agent-session-hold.test.tsx": 533, - "src/renderer/src/components/native-chat/use-structured-agent-session-legacy-recovery.test.tsx": 2508, - "src/renderer/src/components/native-chat/use-structured-agent-session-messages.test.tsx": 547, - "src/renderer/src/components/native-chat/use-structured-agent-session-mutate.test.tsx": 876, - "src/renderer/src/components/native-chat/use-structured-agent-session-options.first-frame.test.tsx": 2125, - "src/renderer/src/components/native-chat/use-structured-agent-session-options.learned-default.test.tsx": 1942, - "src/renderer/src/components/native-chat/use-structured-agent-session-options.preloaded-agents.test.tsx": 1799, - "src/renderer/src/components/native-chat/use-structured-agent-session-options.test.tsx": 2745, - "src/renderer/src/components/native-chat/use-structured-agent-session-paired-runtime.test.tsx": 2294, - "src/renderer/src/components/native-chat/use-structured-agent-session-prompt-cancel.test.tsx": 2071, - "src/renderer/src/components/native-chat/use-structured-agent-session-prompt-state.test.tsx": 2502, - "src/renderer/src/components/native-chat/use-structured-agent-session-provisional.test.tsx": 2577, - "src/renderer/src/components/native-chat/use-structured-agent-session-question-answers.test.tsx": 1996, - "src/renderer/src/components/native-chat/use-structured-agent-session-queued-edit.test.tsx": 2429, - "src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.clear.test.tsx": 1709, - "src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.resume.test.tsx": 1659, - "src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.test.tsx": 1691, - "src/renderer/src/components/native-chat/use-structured-agent-session-rail-outline.test.tsx": 564, - "src/renderer/src/components/native-chat/use-structured-agent-session-read.test.tsx": 2334, - "src/renderer/src/components/native-chat/use-structured-agent-session-sends.test.tsx": 553, - "src/renderer/src/components/native-chat/use-structured-agent-session-start-failure-facts.test.ts": 523, - "src/renderer/src/components/native-chat/use-structured-agent-session-stop-press.test.tsx": 247, - "src/renderer/src/components/native-chat/use-structured-agent-session-stop.test.tsx": 2147, - "src/renderer/src/components/native-chat/use-structured-agent-session-thread-goal.test.tsx": 312, - "src/renderer/src/components/native-chat/use-structured-agent-session-transport.test.tsx": 1868, - "src/renderer/src/components/native-chat/use-structured-agent-session-write-refusal.test.tsx": 2124, - "src/renderer/src/components/native-chat/use-structured-agent-session.clear-send.test.tsx": 1896, - "src/renderer/src/components/native-chat/use-structured-agent-session.cut-turn-notice.test.tsx": 2024, - "src/renderer/src/components/native-chat/use-structured-agent-session.queue-while-stopping.test.tsx": 2061, - "src/renderer/src/components/native-chat/use-structured-agent-session.queued-gating.test.tsx": 2287, - "src/renderer/src/components/native-chat/use-structured-agent-session.rejected-card.test.tsx": 2042, - "src/renderer/src/components/native-chat/use-structured-agent-session.rewind.test.tsx": 2472, - "src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx": 3082, - "src/renderer/src/components/native-chat/use-structured-agent-turn-timing.test.tsx": 471, - "src/renderer/src/components/native-chat/use-structured-native-chat-pane-commands.test.tsx": 540, - "src/renderer/src/components/native-chat/use-structured-native-chat-submit-reveal.test.ts": 295, - "src/renderer/src/components/network/CustomAddressDialog.test.tsx": 516, - "src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.test.tsx": 596, - "src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.test.tsx": 773, - "src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx": 926, - "src/renderer/src/components/new-workspace/ProjectCombobox.test.tsx": 563, - "src/renderer/src/components/new-workspace/ProjectComboboxRow.test.tsx": 324, - "src/renderer/src/components/new-workspace/SetProjectLocationDialog.test.tsx": 1306, - "src/renderer/src/components/new-workspace/SmartWorkspaceNameField-provider-boundaries.test.ts": 54, - "src/renderer/src/components/new-workspace/SmartWorkspaceNameField-repo-slug-routing.test.ts": 97, - "src/renderer/src/components/new-workspace/SmartWorkspaceNameField.ime-enter.test.tsx": 635, - "src/renderer/src/components/new-workspace/SmartWorkspaceNameField.jira-accessibility.test.tsx": 1078, - "src/renderer/src/components/new-workspace/base-ref-picker-visibility.test.ts": 53, - "src/renderer/src/components/new-workspace/project-combobox-matching.test.ts": 76, - "src/renderer/src/components/new-workspace/smart-workspace-command-value.test.ts": 53, - "src/renderer/src/components/new-workspace/smart-workspace-localized-options.test.ts": 185, - "src/renderer/src/components/new-workspace/smart-workspace-source-popover-focus.test.ts": 179, - "src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts": 125, - "src/renderer/src/components/new-workspace/use-jira-source-connection.test.tsx": 323, - "src/renderer/src/components/new-workspace/use-jira-url-source.test.tsx": 293, - "src/renderer/src/components/new-workspace/use-linear-source-connection.test.tsx": 878, - "src/renderer/src/components/new-workspace/use-recent-project-ids.test.ts": 1462, - "src/renderer/src/components/new-workspace/use-smart-workspace-field-focus-controls.test.tsx": 272, - "src/renderer/src/components/new-workspace/use-smart-workspace-name-field-foundation.source.test.tsx": 663, - "src/renderer/src/components/new-workspace/work-item-source-selection.test.ts": 1480, - "src/renderer/src/components/onboarding/AgentStep.test.tsx": 289, - "src/renderer/src/components/onboarding/FeatureSetupInlineTerminal.test.tsx": 1700, - "src/renderer/src/components/onboarding/NotificationStep.test.tsx": 1612, - "src/renderer/src/components/onboarding/OnboardingFlow.test.tsx": 14464, - "src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.command-finished.test.tsx": 369, - "src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.test.ts": 9982, - "src/renderer/src/components/onboarding/ThemeStep.test.ts": 132, - "src/renderer/src/components/onboarding/WindowsTerminalStep.test.tsx": 558, - "src/renderer/src/components/onboarding/agent-picked-payload.test.ts": 68, - "src/renderer/src/components/onboarding/onboarding-dismiss-target.test.ts": 51, - "src/renderer/src/components/onboarding/onboarding-feature-setup.test.ts": 1391, - "src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts": 332, - "src/renderer/src/components/onboarding/onboarding-settings-hydration.test.ts": 60, - "src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.ts": 1673, - "src/renderer/src/components/onboarding/use-onboarding-flow.test.ts": 159, - "src/renderer/src/components/onboarding/windows-terminal-onboarding-telemetry.test.ts": 42, - "src/renderer/src/components/orca-profiles/OrcaProfileSignOutConfirmDialog.test.tsx": 250, - "src/renderer/src/components/pet/PetOverlay.frame-durations.test.tsx": 269, - "src/renderer/src/components/pet/PetOverlay.keyframes.test.tsx": 372, - "src/renderer/src/components/pet/PetOverlay.pet-switch.test.tsx": 245, - "src/renderer/src/components/pet/PetOverlay.pointer-interaction.test.tsx": 348, - "src/renderer/src/components/pet/PetOverlay.test.ts": 1488, - "src/renderer/src/components/pet/pet-agent-state.test.ts": 95, - "src/renderer/src/components/pet/pet-blob-cache.test.ts": 66, - "src/renderer/src/components/pet/pet-overlay-hit-area.test.tsx": 283, - "src/renderer/src/components/pet/pet-overlay-position.test.ts": 1490, - "src/renderer/src/components/pet/pet-overlay-visibility.test.ts": 49, - "src/renderer/src/components/pet/sprite-animation-css.test.ts": 57, - "src/renderer/src/components/pet/usePetPointerInteraction.test.ts": 319, - "src/renderer/src/components/pet/usePetUrl.test.tsx": 355, - "src/renderer/src/components/ports/WorkspacePortScanner.test.tsx": 2253, - "src/renderer/src/components/pr-check-counts.test.ts": 84, - "src/renderer/src/components/pr-checks-fix-prompt.test.ts": 61, - "src/renderer/src/components/pr-comments-resolution-prompt.test.ts": 48, - "src/renderer/src/components/provider-check-classification-parity.test.ts": 129, - "src/renderer/src/components/pull-request-page/cache/file-content.test.ts": 129, - "src/renderer/src/components/pull-request-page/files/combined-diff-section-index.test.tsx": 495, - "src/renderer/src/components/pull-request-page/mentions/options.test.ts": 59, - "src/renderer/src/components/pull-request-page/mentions/query.test.ts": 54, - "src/renderer/src/components/pull-request-page/presentation/state-badge.test.ts": 155, - "src/renderer/src/components/quick-open-file-history.test.ts": 1540, - "src/renderer/src/components/quick-open-file-list-host-name-filter.react.test.tsx": 1753, - "src/renderer/src/components/quick-open-file-list-remote-catalog.react.test.tsx": 1792, - "src/renderer/src/components/quick-open-file-list.react.test.tsx": 1934, - "src/renderer/src/components/quick-open-file-list.test.ts": 1601, - "src/renderer/src/components/quick-open-file-navigation.test.ts": 335, - "src/renderer/src/components/quick-open-history-ranking.test.ts": 59, - "src/renderer/src/components/quick-open-install-rg-guidance.render.test.tsx": 314, - "src/renderer/src/components/quick-open-install-rg-guidance.test.ts": 133, - "src/renderer/src/components/quick-open-recent-lifetime.react.test.tsx": 1679, - "src/renderer/src/components/quick-open-recent-validation.test.ts": 109, - "src/renderer/src/components/quick-open-search.test.ts": 275, - "src/renderer/src/components/quick-open-selection-owner.react.test.tsx": 1849, - "src/renderer/src/components/repo/CreateFromPicker.test.tsx": 677, - "src/renderer/src/components/repo/NestedRepoChecklist.test.tsx": 186, - "src/renderer/src/components/repo/NestedRepoScanLimitNotice.test.ts": 177, - "src/renderer/src/components/repo/repo-icon.emoji-centering.test.tsx": 333, - "src/renderer/src/components/repo/repo-icon.test.tsx": 399, - "src/renderer/src/components/right-sidebar/ActionButton.test.tsx": 143, - "src/renderer/src/components/right-sidebar/AiVaultPanel.legacy-filter.test.tsx": 3070, - "src/renderer/src/components/right-sidebar/AiVaultPanelHeader.focus.test.tsx": 385, - "src/renderer/src/components/right-sidebar/AiVaultPanelSearch.test.tsx": 541, - "src/renderer/src/components/right-sidebar/AiVaultSearchEvidence.test.tsx": 2086, - "src/renderer/src/components/right-sidebar/AiVaultSessionActionMenuItems.test.tsx": 482, - "src/renderer/src/components/right-sidebar/AiVaultSessionListBar.test.tsx": 696, - "src/renderer/src/components/right-sidebar/AiVaultSessionRow.structured-title.test.tsx": 2145, - "src/renderer/src/components/right-sidebar/AiVaultSessionRow.test.tsx": 2248, - "src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.test.tsx": 2264, - "src/renderer/src/components/right-sidebar/AiVaultShowMoreSessionsRow.test.tsx": 376, - "src/renderer/src/components/right-sidebar/AiVaultVirtualRow.test.tsx": 2999, - "src/renderer/src/components/right-sidebar/ChecksPanel.review-header.test.tsx": 5337, - "src/renderer/src/components/right-sidebar/ChecksPanel.updated-at-metadata.test.tsx": 249, - "src/renderer/src/components/right-sidebar/CommitArea.chevron-spinner.test.tsx": 2229, - "src/renderer/src/components/right-sidebar/CommitArea.generate.test.tsx": 1890, - "src/renderer/src/components/right-sidebar/CommitArea.primary-icons.test.tsx": 3363, - "src/renderer/src/components/right-sidebar/CommitArea.test.tsx": 6748, - "src/renderer/src/components/right-sidebar/FileExplorerFilesTreePane.errors.test.tsx": 1674, - "src/renderer/src/components/right-sidebar/FileExplorerNameFilter.test.tsx": 156, - "src/renderer/src/components/right-sidebar/FileExplorerRow.actions.test.tsx": 432, - "src/renderer/src/components/right-sidebar/FileExplorerRow.load-error.test.tsx": 1657, - "src/renderer/src/components/right-sidebar/FileExplorerScopeNotice.test.tsx": 538, - "src/renderer/src/components/right-sidebar/FileExplorerToolbar.test.tsx": 1512, - "src/renderer/src/components/right-sidebar/FileExplorerViewSwitch.test.tsx": 98, - "src/renderer/src/components/right-sidebar/FileExplorerVirtualRows.row-handlers.test.tsx": 1604, - "src/renderer/src/components/right-sidebar/FileExplorerVirtualRowsAddProject.test.tsx": 1525, - "src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.test.tsx": 566, - "src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.test.tsx": 428, - "src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.test.tsx": 334, - "src/renderer/src/components/right-sidebar/GitHistoryPanel.test.tsx": 1611, - "src/renderer/src/components/right-sidebar/GitHubPRStackMap.test.tsx": 359, - "src/renderer/src/components/right-sidebar/HostedReviewActions.draft.test.tsx": 1176, + "src/renderer/src/components/native-chat/use-native-chat-composer-image-input.test.tsx": 1658, + "src/renderer/src/components/native-chat/use-native-chat-composer-keydown.steer.test.tsx": 482, + "src/renderer/src/components/native-chat/use-native-chat-composer-keydown.test.tsx": 622, + "src/renderer/src/components/native-chat/use-native-chat-composer-notice.test.tsx": 197, + "src/renderer/src/components/native-chat/use-native-chat-composer-paste-session-attachments.test.tsx": 278, + "src/renderer/src/components/native-chat/use-native-chat-composer-paste.test.tsx": 427, + "src/renderer/src/components/native-chat/use-native-chat-composer-preview-lifetime.test.tsx": 1298, + "src/renderer/src/components/native-chat/use-native-chat-composer-recall.test.tsx": 509, + "src/renderer/src/components/native-chat/use-native-chat-composer-submit.test.tsx": 300, + "src/renderer/src/components/native-chat/use-native-chat-context-menu.test.tsx": 2180, + "src/renderer/src/components/native-chat/use-native-chat-external-attachments.test.tsx": 609, + "src/renderer/src/components/native-chat/use-native-chat-file-attachment-actions.test.tsx": 237, + "src/renderer/src/components/native-chat/use-native-chat-file-link-click.test.tsx": 1025, + "src/renderer/src/components/native-chat/use-native-chat-file-link-existence.test.tsx": 603, + "src/renderer/src/components/native-chat/use-native-chat-find.test.tsx": 1353, + "src/renderer/src/components/native-chat/use-native-chat-font-size.test.tsx": 658, + "src/renderer/src/components/native-chat/use-native-chat-hook-status.test.ts": 1369, + "src/renderer/src/components/native-chat/use-native-chat-host-outage.test.tsx": 1473, + "src/renderer/src/components/native-chat/use-native-chat-interactive-prompt-card.test.ts": 1729, + "src/renderer/src/components/native-chat/use-native-chat-interactive-send.test.tsx": 367, + "src/renderer/src/components/native-chat/use-native-chat-launch-draft-adoption.test.tsx": 264, + "src/renderer/src/components/native-chat/use-native-chat-link-actions.test.tsx": 728, + "src/renderer/src/components/native-chat/use-native-chat-live-session-pending.test.ts": 1702, + "src/renderer/src/components/native-chat/use-native-chat-live-session-visibility.test.ts": 1616, + "src/renderer/src/components/native-chat/use-native-chat-live-session.test.ts": 1863, + "src/renderer/src/components/native-chat/use-native-chat-mention-files.test.tsx": 1474, + "src/renderer/src/components/native-chat/use-native-chat-menu-paste-lifetime.test.tsx": 1809, + "src/renderer/src/components/native-chat/use-native-chat-message-rail.test.ts": 768, + "src/renderer/src/components/native-chat/use-native-chat-paced-text.remount.test.tsx": 331, + "src/renderer/src/components/native-chat/use-native-chat-paced-text.test.ts": 42, + "src/renderer/src/components/native-chat/use-native-chat-paste-lifetime.test.tsx": 328, + "src/renderer/src/components/native-chat/use-native-chat-pending-delivery.test.tsx": 349, + "src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.test.tsx": 1635, + "src/renderer/src/components/native-chat/use-native-chat-picker-state.test.tsx": 236, + "src/renderer/src/components/native-chat/use-native-chat-prompt-card-presentation.test.ts": 1666, + "src/renderer/src/components/native-chat/use-native-chat-provisional-launch.test.tsx": 1846, + "src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.test.tsx": 519, + "src/renderer/src/components/native-chat/use-native-chat-rail-history-jump.test.ts": 554, + "src/renderer/src/components/native-chat/use-native-chat-retained-session.test.ts": 181, + "src/renderer/src/components/native-chat/use-native-chat-rewind.test.tsx": 721, + "src/renderer/src/components/native-chat/use-native-chat-row-typography.test.tsx": 1671, + "src/renderer/src/components/native-chat/use-native-chat-send-lifecycle.test.tsx": 251, + "src/renderer/src/components/native-chat/use-native-chat-session-option-command.test.tsx": 1521, + "src/renderer/src/components/native-chat/use-native-chat-session-options.test.ts": 814, + "src/renderer/src/components/native-chat/use-native-chat-skills.react.test.tsx": 884, + "src/renderer/src/components/native-chat/use-native-chat-skills.test.ts": 1322, + "src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.test.tsx": 857, + "src/renderer/src/components/native-chat/use-native-chat-structured-composer-transport.test.tsx": 211, + "src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.surface-owner.test.ts": 690, + "src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.test.ts": 1283, + "src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.first-reply.test.tsx": 290, + "src/renderer/src/components/native-chat/use-native-chat-transcript-scroll.test.tsx": 275, + "src/renderer/src/components/native-chat/use-native-chat-transcript-window.options.test.tsx": 673, + "src/renderer/src/components/native-chat/use-structured-agent-session-command-send-gate.test.tsx": 1764, + "src/renderer/src/components/native-chat/use-structured-agent-session-command-start-failure.test.tsx": 1954, + "src/renderer/src/components/native-chat/use-structured-agent-session-context-usage.test.tsx": 1734, + "src/renderer/src/components/native-chat/use-structured-agent-session-delivery-notices.test.tsx": 629, + "src/renderer/src/components/native-chat/use-structured-agent-session-hold.test.tsx": 517, + "src/renderer/src/components/native-chat/use-structured-agent-session-legacy-recovery.test.tsx": 2183, + "src/renderer/src/components/native-chat/use-structured-agent-session-messages.test.tsx": 620, + "src/renderer/src/components/native-chat/use-structured-agent-session-mutate.test.tsx": 811, + "src/renderer/src/components/native-chat/use-structured-agent-session-options.first-frame.test.tsx": 2060, + "src/renderer/src/components/native-chat/use-structured-agent-session-options.learned-default.test.tsx": 1700, + "src/renderer/src/components/native-chat/use-structured-agent-session-options.permission-mode.test.tsx": 1882, + "src/renderer/src/components/native-chat/use-structured-agent-session-options.preloaded-agents.test.tsx": 1688, + "src/renderer/src/components/native-chat/use-structured-agent-session-options.test.tsx": 2989, + "src/renderer/src/components/native-chat/use-structured-agent-session-paired-runtime.test.tsx": 2276, + "src/renderer/src/components/native-chat/use-structured-agent-session-prompt-cancel.test.tsx": 1790, + "src/renderer/src/components/native-chat/use-structured-agent-session-prompt-state.test.tsx": 1860, + "src/renderer/src/components/native-chat/use-structured-agent-session-provisional.test.tsx": 1464, + "src/renderer/src/components/native-chat/use-structured-agent-session-question-answers.test.tsx": 1897, + "src/renderer/src/components/native-chat/use-structured-agent-session-queued-edit.test.tsx": 2428, + "src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.clear.test.tsx": 1786, + "src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.resume.test.tsx": 2036, + "src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.test.tsx": 1570, + "src/renderer/src/components/native-chat/use-structured-agent-session-rail-outline.test.tsx": 516, + "src/renderer/src/components/native-chat/use-structured-agent-session-read.test.tsx": 2516, + "src/renderer/src/components/native-chat/use-structured-agent-session-send-options.test.tsx": 302, + "src/renderer/src/components/native-chat/use-structured-agent-session-sends.test.tsx": 563, + "src/renderer/src/components/native-chat/use-structured-agent-session-start-failure-facts.test.ts": 549, + "src/renderer/src/components/native-chat/use-structured-agent-session-stop-press.test.tsx": 239, + "src/renderer/src/components/native-chat/use-structured-agent-session-stop.test.tsx": 2037, + "src/renderer/src/components/native-chat/use-structured-agent-session-thread-goal.test.tsx": 222, + "src/renderer/src/components/native-chat/use-structured-agent-session-transport.test.tsx": 1648, + "src/renderer/src/components/native-chat/use-structured-agent-session-write-refusal.test.tsx": 1956, + "src/renderer/src/components/native-chat/use-structured-agent-session.clear-send.test.tsx": 2029, + "src/renderer/src/components/native-chat/use-structured-agent-session.cut-turn-notice.test.tsx": 1712, + "src/renderer/src/components/native-chat/use-structured-agent-session.queue-while-stopping.test.tsx": 1881, + "src/renderer/src/components/native-chat/use-structured-agent-session.queued-gating.test.tsx": 2336, + "src/renderer/src/components/native-chat/use-structured-agent-session.rejected-card.test.tsx": 1871, + "src/renderer/src/components/native-chat/use-structured-agent-session.rewind.test.tsx": 2345, + "src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx": 3405, + "src/renderer/src/components/native-chat/use-structured-agent-turn-timing.test.tsx": 441, + "src/renderer/src/components/native-chat/use-structured-native-chat-pane-commands.test.tsx": 502, + "src/renderer/src/components/native-chat/use-structured-native-chat-submit-reveal.test.ts": 238, + "src/renderer/src/components/network/CustomAddressDialog.test.tsx": 389, + "src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.test.tsx": 621, + "src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.test.tsx": 725, + "src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx": 860, + "src/renderer/src/components/new-workspace/ProjectCombobox.test.tsx": 647, + "src/renderer/src/components/new-workspace/ProjectComboboxRow.test.tsx": 254, + "src/renderer/src/components/new-workspace/RunTargetCombobox.host-identity.test.tsx": 590, + "src/renderer/src/components/new-workspace/SetProjectLocationDialog.host-identity.test.tsx": 820, + "src/renderer/src/components/new-workspace/SetProjectLocationDialog.test.tsx": 1460, + "src/renderer/src/components/new-workspace/SmartWorkspaceNameField-provider-boundaries.test.ts": 39, + "src/renderer/src/components/new-workspace/SmartWorkspaceNameField-repo-slug-routing.test.ts": 282, + "src/renderer/src/components/new-workspace/SmartWorkspaceNameField.ime-enter.test.tsx": 918, + "src/renderer/src/components/new-workspace/SmartWorkspaceNameField.jira-accessibility.test.tsx": 1007, + "src/renderer/src/components/new-workspace/base-ref-picker-visibility.test.ts": 38, + "src/renderer/src/components/new-workspace/project-combobox-matching.test.ts": 82, + "src/renderer/src/components/new-workspace/smart-workspace-command-value.test.ts": 58, + "src/renderer/src/components/new-workspace/smart-workspace-localized-options.test.ts": 165, + "src/renderer/src/components/new-workspace/smart-workspace-source-popover-focus.test.ts": 190, + "src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts": 85, + "src/renderer/src/components/new-workspace/use-jira-composer-project.test.tsx": 230, + "src/renderer/src/components/new-workspace/use-jira-source-connection.test.tsx": 249, + "src/renderer/src/components/new-workspace/use-jira-url-source.test.tsx": 438, + "src/renderer/src/components/new-workspace/use-linear-source-connection.test.tsx": 850, + "src/renderer/src/components/new-workspace/use-recent-project-ids.test.ts": 1492, + "src/renderer/src/components/new-workspace/use-smart-workspace-field-focus-controls.test.tsx": 223, + "src/renderer/src/components/new-workspace/use-smart-workspace-name-field-foundation.source.test.tsx": 816, + "src/renderer/src/components/new-workspace/work-item-source-selection.test.ts": 1410, + "src/renderer/src/components/onboarding/AgentStep.test.tsx": 199, + "src/renderer/src/components/onboarding/FeatureSetupInlineTerminal.test.tsx": 1420, + "src/renderer/src/components/onboarding/NotificationStep.test.tsx": 1599, + "src/renderer/src/components/onboarding/OnboardingFlow.test.tsx": 11176, + "src/renderer/src/components/onboarding/OnboardingInlineCommandTerminal.command-finished.test.tsx": 337, + "src/renderer/src/components/onboarding/ThemeStep.test.ts": 98, + "src/renderer/src/components/onboarding/WindowsTerminalStep.test.tsx": 487, + "src/renderer/src/components/onboarding/agent-picked-payload.test.ts": 34, + "src/renderer/src/components/onboarding/onboarding-dismiss-target.test.ts": 37, + "src/renderer/src/components/onboarding/onboarding-feature-setup.test.ts": 1183, + "src/renderer/src/components/onboarding/onboarding-folder-agent-startup.test.ts": 345, + "src/renderer/src/components/onboarding/onboarding-settings-hydration.test.ts": 42, + "src/renderer/src/components/onboarding/use-onboarding-flow-persistence.test.ts": 1521, + "src/renderer/src/components/onboarding/use-onboarding-flow.test.ts": 79, + "src/renderer/src/components/onboarding/windows-terminal-onboarding-telemetry.test.ts": 33, + "src/renderer/src/components/orca-profiles/OrcaProfileSignOutConfirmDialog.test.tsx": 228, + "src/renderer/src/components/pet/PetOverlay.frame-durations.test.tsx": 238, + "src/renderer/src/components/pet/PetOverlay.keyframes.test.tsx": 388, + "src/renderer/src/components/pet/PetOverlay.pet-switch.test.tsx": 282, + "src/renderer/src/components/pet/PetOverlay.pointer-interaction.test.tsx": 316, + "src/renderer/src/components/pet/PetOverlay.test.ts": 1373, + "src/renderer/src/components/pet/pet-agent-state.test.ts": 94, + "src/renderer/src/components/pet/pet-blob-cache.test.ts": 42, + "src/renderer/src/components/pet/pet-overlay-hit-area.test.tsx": 289, + "src/renderer/src/components/pet/pet-overlay-position.test.ts": 1466, + "src/renderer/src/components/pet/pet-overlay-visibility.test.ts": 34, + "src/renderer/src/components/pet/sprite-animation-css.test.ts": 38, + "src/renderer/src/components/pet/usePetPointerInteraction.test.ts": 309, + "src/renderer/src/components/pet/usePetUrl.test.tsx": 246, + "src/renderer/src/components/ports/WorkspacePortScanner.test.tsx": 1880, + "src/renderer/src/components/pr-check-counts.test.ts": 53, + "src/renderer/src/components/pr-checks-fix-prompt.test.ts": 60, + "src/renderer/src/components/pr-comments-resolution-prompt.test.ts": 36, + "src/renderer/src/components/provider-check-classification-parity.test.ts": 89, + "src/renderer/src/components/pull-request-page/cache/file-content.test.ts": 274, + "src/renderer/src/components/pull-request-page/files/combined-diff-section-index.test.tsx": 603, + "src/renderer/src/components/pull-request-page/mentions/options.test.ts": 37, + "src/renderer/src/components/pull-request-page/mentions/query.test.ts": 38, + "src/renderer/src/components/pull-request-page/presentation/state-badge.test.ts": 83, + "src/renderer/src/components/quick-open-file-history.test.ts": 1431, + "src/renderer/src/components/quick-open-file-list-host-name-filter.react.test.tsx": 1521, + "src/renderer/src/components/quick-open-file-list-remote-catalog.react.test.tsx": 1527, + "src/renderer/src/components/quick-open-file-list.react.test.tsx": 1687, + "src/renderer/src/components/quick-open-file-list.test.ts": 1696, + "src/renderer/src/components/quick-open-file-navigation.test.ts": 443, + "src/renderer/src/components/quick-open-history-ranking.test.ts": 36, + "src/renderer/src/components/quick-open-install-rg-guidance.render.test.tsx": 294, + "src/renderer/src/components/quick-open-install-rg-guidance.test.ts": 76, + "src/renderer/src/components/quick-open-recent-lifetime.react.test.tsx": 1413, + "src/renderer/src/components/quick-open-recent-validation.test.ts": 76, + "src/renderer/src/components/quick-open-search.test.ts": 263, + "src/renderer/src/components/quick-open-selection-owner.react.test.tsx": 1714, + "src/renderer/src/components/repo/CreateFromPicker.test.tsx": 706, + "src/renderer/src/components/repo/NestedRepoChecklist.test.tsx": 126, + "src/renderer/src/components/repo/NestedRepoScanLimitNotice.test.ts": 114, + "src/renderer/src/components/repo/repo-icon.emoji-centering.test.tsx": 314, + "src/renderer/src/components/repo/repo-icon.test.tsx": 312, + "src/renderer/src/components/right-sidebar/ActionButton.test.tsx": 82, + "src/renderer/src/components/right-sidebar/AiVaultPanel.legacy-filter.test.tsx": 3333, + "src/renderer/src/components/right-sidebar/AiVaultPanelHeader.focus.test.tsx": 326, + "src/renderer/src/components/right-sidebar/AiVaultPanelSearch.test.tsx": 632, + "src/renderer/src/components/right-sidebar/AiVaultSearchEvidence.test.tsx": 1823, + "src/renderer/src/components/right-sidebar/AiVaultSessionActionMenuItems.test.tsx": 511, + "src/renderer/src/components/right-sidebar/AiVaultSessionListBar.test.tsx": 627, + "src/renderer/src/components/right-sidebar/AiVaultSessionRow.structured-title.test.tsx": 1980, + "src/renderer/src/components/right-sidebar/AiVaultSessionRow.test.tsx": 2285, + "src/renderer/src/components/right-sidebar/AiVaultSessionSubagents.test.tsx": 2067, + "src/renderer/src/components/right-sidebar/AiVaultShowMoreSessionsRow.test.tsx": 363, + "src/renderer/src/components/right-sidebar/AiVaultVirtualRow.test.tsx": 2985, + "src/renderer/src/components/right-sidebar/ChecksPanel.review-header.test.tsx": 6315, + "src/renderer/src/components/right-sidebar/ChecksPanel.updated-at-metadata.test.tsx": 204, + "src/renderer/src/components/right-sidebar/CommitArea.chevron-spinner.test.tsx": 2448, + "src/renderer/src/components/right-sidebar/CommitArea.generate.test.tsx": 1621, + "src/renderer/src/components/right-sidebar/CommitArea.primary-icons.test.tsx": 2452, + "src/renderer/src/components/right-sidebar/CommitArea.test.tsx": 10499, + "src/renderer/src/components/right-sidebar/FileExplorerFilesTreePane.errors.test.tsx": 1777, + "src/renderer/src/components/right-sidebar/FileExplorerNameFilter.test.tsx": 103, + "src/renderer/src/components/right-sidebar/FileExplorerRow.actions.test.tsx": 353, + "src/renderer/src/components/right-sidebar/FileExplorerRow.load-error.test.tsx": 1966, + "src/renderer/src/components/right-sidebar/FileExplorerScopeNotice.test.tsx": 558, + "src/renderer/src/components/right-sidebar/FileExplorerToolbar.test.tsx": 1314, + "src/renderer/src/components/right-sidebar/FileExplorerViewSwitch.test.tsx": 76, + "src/renderer/src/components/right-sidebar/FileExplorerVirtualRows.row-handlers.test.tsx": 1853, + "src/renderer/src/components/right-sidebar/FileExplorerVirtualRowsAddProject.test.tsx": 1440, + "src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksPanel.test.tsx": 674, + "src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.test.tsx": 312, + "src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.test.tsx": 356, + "src/renderer/src/components/right-sidebar/GitHistoryPanel.test.tsx": 1304, + "src/renderer/src/components/right-sidebar/GitHubPRStackMap.test.tsx": 314, + "src/renderer/src/components/right-sidebar/HostedReviewActions.draft.test.tsx": 1486, "src/renderer/src/components/right-sidebar/PluginPanel.test.tsx": 559, - "src/renderer/src/components/right-sidebar/PortsPanel.test.tsx": 2438, - "src/renderer/src/components/right-sidebar/PullRequestComposer.generate-tooltip.test.tsx": 902, - "src/renderer/src/components/right-sidebar/SearchResultItems.test.tsx": 197, - "src/renderer/src/components/right-sidebar/SessionRowTrailingActions.test.tsx": 280, - "src/renderer/src/components/right-sidebar/SourceControl.branch-line-total.test.tsx": 2329, - "src/renderer/src/components/right-sidebar/SourceControl.commit-drafts.test.ts": 133, - "src/renderer/src/components/right-sidebar/SourceControl.commit-failure-recovery.test.ts": 70, - "src/renderer/src/components/right-sidebar/SourceControl.commit-generation-records.test.ts": 63, - "src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts": 182, - "src/renderer/src/components/right-sidebar/SourceControl.hosted-review-header-link.test.tsx": 146, - "src/renderer/src/components/right-sidebar/SourceControl.open-file-highlight.test.tsx": 3578, - "src/renderer/src/components/right-sidebar/SourceControl.pr-generation-records.test.ts": 70, - "src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx": 3147, - "src/renderer/src/components/right-sidebar/SourceControl.push-failure-recovery.test.ts": 45, - "src/renderer/src/components/right-sidebar/SourceControl.remote-action-errors.test.ts": 46, - "src/renderer/src/components/right-sidebar/SourceControl.resolve-conflicts-prompt.test.ts": 98, - "src/renderer/src/components/right-sidebar/SourceControl.virtual-file-list.test.tsx": 3961, - "src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx": 2686, - "src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.test.tsx": 258, - "src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialog.test.ts": 376, - "src/renderer/src/components/right-sidebar/active-checks-status.test.ts": 1468, - "src/renderer/src/components/right-sidebar/activity-bar-buttons.test.tsx": 328, - "src/renderer/src/components/right-sidebar/activity-bar-overflow.test.ts": 53, - "src/renderer/src/components/right-sidebar/ai-vault-first-prompt-card.test.tsx": 419, - "src/renderer/src/components/right-sidebar/ai-vault-host-scope.test.ts": 201, - "src/renderer/src/components/right-sidebar/ai-vault-original-pane-index.test.ts": 79, - "src/renderer/src/components/right-sidebar/ai-vault-original-pane.test.ts": 66, - "src/renderer/src/components/right-sidebar/ai-vault-panel-session-list-request.test.tsx": 2271, - "src/renderer/src/components/right-sidebar/ai-vault-scan-issue-state.test.ts": 69, - "src/renderer/src/components/right-sidebar/ai-vault-scope-paths.test.ts": 72, - "src/renderer/src/components/right-sidebar/ai-vault-scope-state.test.ts": 52, - "src/renderer/src/components/right-sidebar/ai-vault-search-scope-identity.test.tsx": 448, - "src/renderer/src/components/right-sidebar/ai-vault-search-session.test.ts": 268, - "src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.test.ts": 77, - "src/renderer/src/components/right-sidebar/ai-vault-session-continuation.test.ts": 69, - "src/renderer/src/components/right-sidebar/ai-vault-session-deletability.test.ts": 306, - "src/renderer/src/components/right-sidebar/ai-vault-session-display.test.ts": 59, - "src/renderer/src/components/right-sidebar/ai-vault-session-filters.test.ts": 103, - "src/renderer/src/components/right-sidebar/ai-vault-session-identity.test.ts": 57, - "src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.test.ts": 1623, - "src/renderer/src/components/right-sidebar/ai-vault-session-log-open.test.ts": 337, - "src/renderer/src/components/right-sidebar/ai-vault-session-path-actions.test.ts": 330, - "src/renderer/src/components/right-sidebar/ai-vault-session-projects.test.ts": 101, - "src/renderer/src/components/right-sidebar/ai-vault-session-publication-gate.test.ts": 156, - "src/renderer/src/components/right-sidebar/ai-vault-session-refresh.test.ts": 1511, - "src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.test.ts": 1041, - "src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.test.ts": 519, - "src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat.test.ts": 84, - "src/renderer/src/components/right-sidebar/ai-vault-session-resume.test.ts": 1709, - "src/renderer/src/components/right-sidebar/ai-vault-session-surface-switch.test.ts": 1609, - "src/renderer/src/components/right-sidebar/ai-vault-session-worktree-map.test.tsx": 1626, - "src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts": 109, - "src/renderer/src/components/right-sidebar/ai-vault-view-defaults.test.ts": 70, - "src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.test.ts": 68, - "src/renderer/src/components/right-sidebar/branch-line-total-request-gate.test.ts": 42, - "src/renderer/src/components/right-sidebar/check-details-resize.test.ts": 48, - "src/renderer/src/components/right-sidebar/checks-entry-refresh.test.ts": 72, - "src/renderer/src/components/right-sidebar/checks-list-expanded-details.test.tsx": 701, - "src/renderer/src/components/right-sidebar/checks-panel-async-result-key.test.ts": 55, - "src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx": 1908, - "src/renderer/src/components/right-sidebar/checks-panel-empty-state.test.ts": 138, - "src/renderer/src/components/right-sidebar/checks-panel-git-status-snapshot.test.ts": 66, - "src/renderer/src/components/right-sidebar/checks-panel-hosted-review-click-routing.test.ts": 56, - "src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-breadcrumb.test.ts": 61, - "src/renderer/src/components/right-sidebar/checks-panel-review-creation.test.ts": 72, - "src/renderer/src/components/right-sidebar/checks-panel-review-lookup-authority.test.ts": 74, - "src/renderer/src/components/right-sidebar/checks-panel-review.test.ts": 58, - "src/renderer/src/components/right-sidebar/checks-panel-terminal-worktree.test.ts": 72, - "src/renderer/src/components/right-sidebar/checks-panel/checks-detail-polling-policy.test.ts": 49, - "src/renderer/src/components/right-sidebar/checks-panel/gitlab-review-client.test.ts": 103, - "src/renderer/src/components/right-sidebar/checks-panel/panel-content-rendering.test.tsx": 6495, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-check-and-review-actions.test.tsx": 2148, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-create-review.test.tsx": 1647, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-foreground-effects.test.tsx": 228, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-generation.test.tsx": 1760, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-git-status-effects.test.tsx": 241, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-manual-refresh.test.tsx": 1661, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-polling-stability.test.tsx": 1756, - "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-polling.test.tsx": 1865, - "src/renderer/src/components/right-sidebar/coalesced-poll-runner.test.ts": 65, - "src/renderer/src/components/right-sidebar/commit-failure-dialog-state.test.ts": 56, - "src/renderer/src/components/right-sidebar/commit-failure-summary.test.ts": 66, - "src/renderer/src/components/right-sidebar/create-review-draft-title.test.ts": 60, - "src/renderer/src/components/right-sidebar/diff-comments-clear-dialog-state.test.ts": 59, - "src/renderer/src/components/right-sidebar/discard-all-sequence.test.ts": 76, - "src/renderer/src/components/right-sidebar/file-explorer-add-project-action.test.ts": 56, - "src/renderer/src/components/right-sidebar/file-explorer-batch-deletion.test.ts": 136, - "src/renderer/src/components/right-sidebar/file-explorer-copy-file-owner.test.ts": 76, - "src/renderer/src/components/right-sidebar/file-explorer-delete-editor-owner.test.tsx": 1672, - "src/renderer/src/components/right-sidebar/file-explorer-dir-load-tracker.test.ts": 50, - "src/renderer/src/components/right-sidebar/file-explorer-dir-toggle-timing.test.ts": 167, - "src/renderer/src/components/right-sidebar/file-explorer-directory-filename-identity.test.ts": 1416, - "src/renderer/src/components/right-sidebar/file-explorer-directory-listing.test.ts": 1622, - "src/renderer/src/components/right-sidebar/file-explorer-display-root.test.ts": 1628, - "src/renderer/src/components/right-sidebar/file-explorer-drag-scroll-marker.test.tsx": 2169, - "src/renderer/src/components/right-sidebar/file-explorer-entries.test.ts": 58, - "src/renderer/src/components/right-sidebar/file-explorer-expanded-dirs-refresh.test.ts": 1581, - "src/renderer/src/components/right-sidebar/file-explorer-inline-input-ime-enter.test.tsx": 361, - "src/renderer/src/components/right-sidebar/file-explorer-inline-input-outside-click.test.tsx": 348, - "src/renderer/src/components/right-sidebar/file-explorer-inline-rename-flow.test.tsx": 910, - "src/renderer/src/components/right-sidebar/file-explorer-instant-dir-toggle.test.ts": 389, - "src/renderer/src/components/right-sidebar/file-explorer-keyboard-navigation.test.ts": 68, - "src/renderer/src/components/right-sidebar/file-explorer-linked-recovery.test.tsx": 1794, - "src/renderer/src/components/right-sidebar/file-explorer-linked-watch.test.ts": 1465, - "src/renderer/src/components/right-sidebar/file-explorer-name-filter-projection.test.ts": 70, - "src/renderer/src/components/right-sidebar/file-explorer-operation-generation.test.ts": 1460, - "src/renderer/src/components/right-sidebar/file-explorer-operation-owner.test.ts": 349, - "src/renderer/src/components/right-sidebar/file-explorer-paths.test.ts": 61, - "src/renderer/src/components/right-sidebar/file-explorer-refresh-concurrency.test.ts": 49, - "src/renderer/src/components/right-sidebar/file-explorer-replace-selection-order-budget.test.tsx": 338, - "src/renderer/src/components/right-sidebar/file-explorer-reset.test.ts": 52, - "src/renderer/src/components/right-sidebar/file-explorer-row-context-menu.test.tsx": 433, - "src/renderer/src/components/right-sidebar/file-explorer-row-projection.test.ts": 53, - "src/renderer/src/components/right-sidebar/file-explorer-selection.test.ts": 55, - "src/renderer/src/components/right-sidebar/file-explorer-stale-dir-cache.test.ts": 48, - "src/renderer/src/components/right-sidebar/file-explorer-stale-retry.test.tsx": 1729, - "src/renderer/src/components/right-sidebar/file-explorer-watch-drive-root.test.ts": 1350, - "src/renderer/src/components/right-sidebar/file-explorer-watch-filename-identity.test.ts": 1562, - "src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.test.ts": 1632, - "src/renderer/src/components/right-sidebar/file-explorer-watch-refresh-scheduler.test.ts": 99, - "src/renderer/src/components/right-sidebar/file-explorer-watch-selected-host.test.ts": 1457, - "src/renderer/src/components/right-sidebar/file-search-include-pattern.test.ts": 61, - "src/renderer/src/components/right-sidebar/folder-workspace-attached-worktrees.test.ts": 104, - "src/renderer/src/components/right-sidebar/fork-push-target-label.test.ts": 61, - "src/renderer/src/components/right-sidebar/git-status-push-signal-refresh.test.ts": 76, - "src/renderer/src/components/right-sidebar/git-status-refresh-branch-line-total.test.ts": 318, - "src/renderer/src/components/right-sidebar/git-status-refresh-scheduler.test.ts": 80, - "src/renderer/src/components/right-sidebar/git-status-refresh.test.ts": 430, - "src/renderer/src/components/right-sidebar/github-pr-link-modal.test.ts": 48, - "src/renderer/src/components/right-sidebar/github-pr-stack-merge.test.ts": 91, - "src/renderer/src/components/right-sidebar/gitlab-mr-merge-state.test.ts": 112, - "src/renderer/src/components/right-sidebar/local-workspace-port-sections.test.ts": 55, - "src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts": 224, - "src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.test.ts": 129, - "src/renderer/src/components/right-sidebar/parent-pr-checks-rows.test.ts": 133, - "src/renderer/src/components/right-sidebar/plugin-panel-activity-items.test.ts": 111, - "src/renderer/src/components/right-sidebar/plugin-panel-bridge-host.test.ts": 354, - "src/renderer/src/components/right-sidebar/plugin-panel-watchdog-visibility.test.ts": 404, - "src/renderer/src/components/right-sidebar/plugin-panel-watchdog.test.ts": 296, - "src/renderer/src/components/right-sidebar/ports-panel-host.test.ts": 319, - "src/renderer/src/components/right-sidebar/pr-comment-ack-summary.test.ts": 49, - "src/renderer/src/components/right-sidebar/pr-comment-presentation.test.ts": 167, - "src/renderer/src/components/right-sidebar/pr-comment-snapshotted-thread-resolver.test.ts": 58, - "src/renderer/src/components/right-sidebar/pr-comment-thread-resolution.test.ts": 51, - "src/renderer/src/components/right-sidebar/pr-comments-ai-launch-ack.test.ts": 113, - "src/renderer/src/components/right-sidebar/pr-comments-list-selection.test.tsx": 2618, - "src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.test.ts": 390, - "src/renderer/src/components/right-sidebar/review-cache-entry-selection.test.ts": 49, - "src/renderer/src/components/right-sidebar/right-panel-comment-composer.ime-enter.test.tsx": 297, - "src/renderer/src/components/right-sidebar/right-panel-comment-focus-timers.test.ts": 45, - "src/renderer/src/components/right-sidebar/right-sidebar-activity-visibility.test.ts": 106, - "src/renderer/src/components/right-sidebar/right-sidebar-effective-tab.test.ts": 50, - "src/renderer/src/components/right-sidebar/right-sidebar-primary-action-layout.test.ts": 52, - "src/renderer/src/components/right-sidebar/right-sidebar-titlebar-drag-regions.render.test.tsx": 718, - "src/renderer/src/components/right-sidebar/right-sidebar-titlebar-drag-regions.test.ts": 44, - "src/renderer/src/components/right-sidebar/right-sidebar-width.test.ts": 46, - "src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.test.ts": 218, - "src/renderer/src/components/right-sidebar/search-match-open.test.ts": 64, - "src/renderer/src/components/right-sidebar/search-rows.test.ts": 56, - "src/renderer/src/components/right-sidebar/source-control-action-recipe-match.test.ts": 270, - "src/renderer/src/components/right-sidebar/source-control-actions.test.ts": 53, - "src/renderer/src/components/right-sidebar/source-control-active-open-file-keys.test.ts": 47, - "src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.test.ts": 134, - "src/renderer/src/components/right-sidebar/source-control-branch-context-row.test.tsx": 259, - "src/renderer/src/components/right-sidebar/source-control-branch-context-stats.test.ts": 86, - "src/renderer/src/components/right-sidebar/source-control-branch-section-heading.test.tsx": 1703, - "src/renderer/src/components/right-sidebar/source-control-commit-eligibility.test.ts": 51, - "src/renderer/src/components/right-sidebar/source-control-commit-message-rows.test.ts": 110, - "src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.test.ts": 78, - "src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.test.ts": 93, - "src/renderer/src/components/right-sidebar/source-control-created-review-link.test.ts": 49, - "src/renderer/src/components/right-sidebar/source-control-discard-confirmation.test.ts": 87, - "src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx": 194, - "src/renderer/src/components/right-sidebar/source-control-discard-localization.test.ts": 353, - "src/renderer/src/components/right-sidebar/source-control-dropdown-items.create-pr-intent.test.ts": 131, - "src/renderer/src/components/right-sidebar/source-control-dropdown-items.test.ts": 160, - "src/renderer/src/components/right-sidebar/source-control-entry-actions.test.ts": 48, - "src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx": 420, - "src/renderer/src/components/right-sidebar/source-control-file-filter.test.ts": 61, - "src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx": 372, - "src/renderer/src/components/right-sidebar/source-control-header-toolbar.test.ts": 211, - "src/renderer/src/components/right-sidebar/source-control-hosted-review-creation-eligibility-snapshot.test.ts": 71, - "src/renderer/src/components/right-sidebar/source-control-hosted-review-push-target.test.ts": 79, - "src/renderer/src/components/right-sidebar/source-control-huge-repo-warning-dismissals.test.ts": 154, - "src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.test.ts": 434, - "src/renderer/src/components/right-sidebar/source-control-manual-review-url.test.ts": 71, - "src/renderer/src/components/right-sidebar/source-control-primary-action-tooltip.test.ts": 113, - "src/renderer/src/components/right-sidebar/source-control-primary-action.create-pr-intent.test.ts": 143, - "src/renderer/src/components/right-sidebar/source-control-primary-action.test.ts": 148, - "src/renderer/src/components/right-sidebar/source-control-push-recovery.test.ts": 59, - "src/renderer/src/components/right-sidebar/source-control-section-order.test.ts": 67, - "src/renderer/src/components/right-sidebar/source-control-split-open.test.ts": 53, - "src/renderer/src/components/right-sidebar/source-control-status-sort.test.ts": 56, - "src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts": 72, - "src/renderer/src/components/right-sidebar/source-control-text-generation-defaults.test.ts": 270, - "src/renderer/src/components/right-sidebar/source-control-too-many-changes-banner.test.tsx": 421, - "src/renderer/src/components/right-sidebar/source-control-tree.test.ts": 75, - "src/renderer/src/components/right-sidebar/source-control/commit/discard-all-failure-description.test.tsx": 1485, - "src/renderer/src/components/right-sidebar/source-control/commit/rename-mutation-paths.test.ts": 55, - "src/renderer/src/components/right-sidebar/source-control/commit/source-control-discard-editor-owner.test.tsx": 315, - "src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts": 226, - "src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-mutation-failures.test.tsx": 323, - "src/renderer/src/components/right-sidebar/source-control/commit/use-bulk-actions-capped-stage.test.tsx": 320, - "src/renderer/src/components/right-sidebar/source-control/listing/section-action-buttons.test.tsx": 2110, - "src/renderer/src/components/right-sidebar/source-control/listing/section-header.test.tsx": 417, - "src/renderer/src/components/right-sidebar/source-control/listing/use-file-projection-work.test.tsx": 331, - "src/renderer/src/components/right-sidebar/source-control/listing/use-store-actions.store-subscriptions.test.tsx": 1360, - "src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.test.tsx": 300, - "src/renderer/src/components/right-sidebar/source-control/review/suppressed-github-pr.test.ts": 50, - "src/renderer/src/components/right-sidebar/source-control/review/use-action-model.test.tsx": 303, - "src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-creation.test.tsx": 1779, - "src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-state.test.tsx": 306, - "src/renderer/src/components/right-sidebar/source-control/review/use-pull-request-generation.test.tsx": 1604, - "src/renderer/src/components/right-sidebar/source-control/review/use-review-push-target.test.tsx": 251, - "src/renderer/src/components/right-sidebar/source-control/sync/git-history-format.test.ts": 69, - "src/renderer/src/components/right-sidebar/source-control/sync/git-history-graph-svg.test.tsx": 98, - "src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.local-ignore.test.tsx": 164, - "src/renderer/src/components/right-sidebar/status-display.test.ts": 51, - "src/renderer/src/components/right-sidebar/use-ai-vault-search-focus-request.test.tsx": 1381, - "src/renderer/src/components/right-sidebar/use-ai-vault-search.test.tsx": 453, - "src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.test.ts": 1614, - "src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.runtime-listing.test.tsx": 1666, - "src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.test.ts": 1698, - "src/renderer/src/components/right-sidebar/use-file-explorer-root-navigation.test.tsx": 297, - "src/renderer/src/components/right-sidebar/use-git-status-upstream-ref-watch.test.ts": 53, - "src/renderer/src/components/right-sidebar/use-hosted-review-actions.test.tsx": 297, - "src/renderer/src/components/right-sidebar/use-installed-plugin-route-reconciliation.test.tsx": 357, - "src/renderer/src/components/right-sidebar/use-persisted-ai-vault-view-options.test.tsx": 341, - "src/renderer/src/components/right-sidebar/use-plugin-panel-theme-revision.test.tsx": 480, - "src/renderer/src/components/right-sidebar/use-right-sidebar-tab-routing.effective-tab.test.tsx": 266, - "src/renderer/src/components/right-sidebar/use-source-control-ai.test.ts": 1903, - "src/renderer/src/components/right-sidebar/use-source-control-branch-compare.test.tsx": 421, - "src/renderer/src/components/right-sidebar/use-source-control-git-history.test.tsx": 303, - "src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.repo-default.test.ts": 1571, - "src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.test.ts": 1533, - "src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.os-file-drop.test.tsx": 2146, - "src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.test.ts": 52, - "src/renderer/src/components/right-sidebar/useFileExplorerHandlers.test.ts": 1433, - "src/renderer/src/components/right-sidebar/useFileExplorerImport.committed-scope.test.tsx": 291, - "src/renderer/src/components/right-sidebar/useFileExplorerImport.os-file-drop.test.tsx": 514, - "src/renderer/src/components/right-sidebar/useFileExplorerImport.remote.test.tsx": 1732, - "src/renderer/src/components/right-sidebar/useFileExplorerKeys.test.ts": 1412, - "src/renderer/src/components/right-sidebar/useFileExplorerReveal.scoped-root.test.tsx": 316, - "src/renderer/src/components/right-sidebar/useFileExplorerRowDrag.test.ts": 64, - "src/renderer/src/components/right-sidebar/useFileExplorerTree.lifecycle.test.tsx": 1983, - "src/renderer/src/components/right-sidebar/useFileExplorerTree.refresh-projection-churn.test.tsx": 1825, - "src/renderer/src/components/right-sidebar/useFileExplorerTree.scoped-root.test.tsx": 1837, - "src/renderer/src/components/right-sidebar/useFileExplorerTree.stale-dirs.test.tsx": 1690, - "src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.debounce.test.tsx": 1954, - "src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.test.ts": 1433, - "src/renderer/src/components/right-sidebar/useFileExplorerWatch.broker.test.tsx": 286, - "src/renderer/src/components/right-sidebar/useFileExplorerWatch.host-root.test.tsx": 569, - "src/renderer/src/components/right-sidebar/useFileExplorerWatch.pending-refresh.test.tsx": 346, - "src/renderer/src/components/right-sidebar/useFileExplorerWatch.test.ts": 1444, - "src/renderer/src/components/right-sidebar/useFileSearchPanel.lifecycle.test.tsx": 1892, - "src/renderer/src/components/right-sidebar/useFileSearchRunner.test.tsx": 582, - "src/renderer/src/components/right-sidebar/useGitStatusPolling.rerender.test.ts": 1604, - "src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts": 2655, - "src/renderer/src/components/right-sidebar/useHostedReviewStackParent.test.tsx": 296, - "src/renderer/src/components/right-sidebar/useSourceControlSelection.test.ts": 63, - "src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.test.tsx": 254, - "src/renderer/src/components/settings/AccountsPane.owner-scope.test.tsx": 1292, - "src/renderer/src/components/settings/AccountsPane.section-lifetime.test.tsx": 2440, - "src/renderer/src/components/settings/AccountsPane.test.tsx": 2349, - "src/renderer/src/components/settings/AdvancedNetworkSettingsSection.interaction.test.tsx": 1600, - "src/renderer/src/components/settings/AdvancedNetworkSettingsSection.test.ts": 1496, - "src/renderer/src/components/settings/AdvancedPane.test.tsx": 496, - "src/renderer/src/components/settings/AgentSkillSetupPanel.freshness.test.tsx": 1728, - "src/renderer/src/components/settings/AgentSkillSetupPanel.test.tsx": 2128, - "src/renderer/src/components/settings/AgentsPane.test.tsx": 2229, - "src/renderer/src/components/settings/AntigravityAccountsSection.test.tsx": 743, - "src/renderer/src/components/settings/AppearanceChatContrastControls.draft.test.tsx": 697, - "src/renderer/src/components/settings/AppearanceChatContrastControls.test.tsx": 1066, - "src/renderer/src/components/settings/AppearanceChatSection.test.tsx": 1335, - "src/renderer/src/components/settings/AppearancePane.test.tsx": 1175, - "src/renderer/src/components/settings/ArtifactsSettingsPane.test.tsx": 859, - "src/renderer/src/components/settings/AutomationsSettingsPane.test.tsx": 735, - "src/renderer/src/components/settings/BranchPrefixFeedback.test.tsx": 116, - "src/renderer/src/components/settings/BrowserClientHostedRemoteSetting.test.tsx": 710, - "src/renderer/src/components/settings/BrowserPane.search-catalog.test.tsx": 564, - "src/renderer/src/components/settings/BrowserPane.test.ts": 49, - "src/renderer/src/components/settings/BrowserSshWorkspaceRoutingSetting.test.tsx": 619, - "src/renderer/src/components/settings/BrowserUseSkillStep.test.tsx": 162, - "src/renderer/src/components/settings/BrowserUserAgentSetting.test.tsx": 1795, - "src/renderer/src/components/settings/ChatNamingSetting.test.tsx": 1784, - "src/renderer/src/components/settings/ChatSettingsSection.inline-visuals.test.tsx": 1723, - "src/renderer/src/components/settings/ChatSettingsSection.navigation.test.tsx": 1343, - "src/renderer/src/components/settings/ChatSettingsSection.test.tsx": 1939, - "src/renderer/src/components/settings/CliSection.install-failure.test.tsx": 2113, - "src/renderer/src/components/settings/CliSection.test.tsx": 1847, - "src/renderer/src/components/settings/CliSkillRuntimeSetup.test.tsx": 1626, - "src/renderer/src/components/settings/CloudVmSetupGuide.test.tsx": 1517, - "src/renderer/src/components/settings/CodexLoginLinkNotice.test.tsx": 463, - "src/renderer/src/components/settings/CommitMessageAiPane.test.tsx": 1859, - "src/renderer/src/components/settings/DefaultWindowsProjectRuntimeSetting.test.tsx": 459, - "src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx": 1979, - "src/renderer/src/components/settings/DiffShowWhitespaceSetting.test.tsx": 558, - "src/renderer/src/components/settings/EditorWordWrapSetting.test.tsx": 685, - "src/renderer/src/components/settings/EphemeralVmRuntimesSection.test.tsx": 786, - "src/renderer/src/components/settings/EphemeralVmsPane.test.tsx": 7418, - "src/renderer/src/components/settings/ExperimentalPane.test.tsx": 1697, - "src/renderer/src/components/settings/FloatingWorkspacePane.test.tsx": 1674, - "src/renderer/src/components/settings/GeneralPane.section-lifetime.test.tsx": 1367, - "src/renderer/src/components/settings/GeneralPane.test.ts": 9929, - "src/renderer/src/components/settings/GeneralRemoteServerUpdates.test.tsx": 306, - "src/renderer/src/components/settings/GeneralUpdateSettingsSection.test.tsx": 1953, - "src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.test.tsx": 1880, - "src/renderer/src/components/settings/GhosttyImportModal.test.ts": 213, - "src/renderer/src/components/settings/GitPane.test.ts": 1505, - "src/renderer/src/components/settings/GitPane.test.tsx": 1506, - "src/renderer/src/components/settings/GrokAccountsSection.test.tsx": 384, - "src/renderer/src/components/settings/HiddenExperimentalGroup.test.tsx": 326, - "src/renderer/src/components/settings/KagiSessionLinkForm.test.ts": 1476, - "src/renderer/src/components/settings/LinearAgentSkillGuide.test.tsx": 227, - "src/renderer/src/components/settings/LinearAgentSkillNotes.test.tsx": 152, - "src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx": 672, - "src/renderer/src/components/settings/LocalNetworkConnectionTest.test.tsx": 432, - "src/renderer/src/components/settings/MachineNameField.test.tsx": 531, - "src/renderer/src/components/settings/ManagedServerRow.test.tsx": 401, - "src/renderer/src/components/settings/McpConfigSection.host-owner.test.tsx": 1848, - "src/renderer/src/components/settings/MobileEmulatorAgentControlRow.test.tsx": 1509, - "src/renderer/src/components/settings/MobilePairingConnectionOptions.test.tsx": 551, - "src/renderer/src/components/settings/MobilePairingQrSection.test.tsx": 539, - "src/renderer/src/components/settings/MobilePairingSetupSection.test.tsx": 1031, - "src/renderer/src/components/settings/MobilePane.test.tsx": 1213, - "src/renderer/src/components/settings/MobilePaneAddressSearch.test.tsx": 635, - "src/renderer/src/components/settings/NativeChatExperimentalSetting.test.tsx": 2559, - "src/renderer/src/components/settings/NativeChatSupportedAgents.test.tsx": 564, - "src/renderer/src/components/settings/NotificationHostToggles.test.tsx": 610, - "src/renderer/src/components/settings/NotificationsPane.test.tsx": 2062, - "src/renderer/src/components/settings/OpenInMenuSetting.test.ts": 233, - "src/renderer/src/components/settings/OrcaAccountSettingsPane.test.tsx": 516, - "src/renderer/src/components/settings/OrchestrationPane.test.tsx": 1871, - "src/renderer/src/components/settings/OrchestrationSkillAgentCoverage.test.tsx": 207, - "src/renderer/src/components/settings/PluginConsentDialog.test.tsx": 2096, - "src/renderer/src/components/settings/PluginInstallDialog.test.tsx": 803, - "src/renderer/src/components/settings/PluginKeybindingConsentPreview.test.ts": 1327, - "src/renderer/src/components/settings/PluginMarketplaceBrowser.test.tsx": 804, - "src/renderer/src/components/settings/PluginMarketplaceSourceDialog.test.tsx": 533, - "src/renderer/src/components/settings/PluginSettingsRow.test.tsx": 642, - "src/renderer/src/components/settings/PluginsSettingsSection.lifecycle.test.tsx": 1128, - "src/renderer/src/components/settings/PrivacyPane.test.ts": 1570, - "src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.test.tsx": 709, - "src/renderer/src/components/settings/QuickCommandsList.test.tsx": 604, - "src/renderer/src/components/settings/QuickCommandsPane.test.ts": 1851, - "src/renderer/src/components/settings/RemoteServerUpdateDialog.test.tsx": 335, - "src/renderer/src/components/settings/RepositoryForkSyncSection.test.tsx": 685, - "src/renderer/src/components/settings/RepositoryGitHubAccountSection.inventory-lifetime.test.tsx": 475, - "src/renderer/src/components/settings/RepositoryGitHubAccountSection.test.tsx": 479, - "src/renderer/src/components/settings/RepositoryHooksSection.test.ts": 799, - "src/renderer/src/components/settings/RepositoryHostSetupsSection.settings-entry.test.tsx": 1925, - "src/renderer/src/components/settings/RepositoryHostSetupsSection.test.tsx": 2009, - "src/renderer/src/components/settings/RepositoryHostSetupsSection.workspace-window.test.tsx": 1574, - "src/renderer/src/components/settings/RepositoryIconEmojiPicker.test.tsx": 262, - "src/renderer/src/components/settings/RepositoryIconPicker.github-avatar-refresh.test.tsx": 542, - "src/renderer/src/components/settings/RepositoryPane.test.ts": 2984, - "src/renderer/src/components/settings/RepositoryPaneDraftInput.test.tsx": 258, - "src/renderer/src/components/settings/RepositorySourceControlAiSection.test.ts": 466, + "src/renderer/src/components/right-sidebar/PortsPanel.test.tsx": 1357, + "src/renderer/src/components/right-sidebar/PullRequestComposer.generate-tooltip.test.tsx": 982, + "src/renderer/src/components/right-sidebar/SearchResultItems.test.tsx": 144, + "src/renderer/src/components/right-sidebar/SessionRowTrailingActions.test.tsx": 270, + "src/renderer/src/components/right-sidebar/SourceControl.branch-line-total.test.tsx": 3151, + "src/renderer/src/components/right-sidebar/SourceControl.commit-drafts.test.ts": 141, + "src/renderer/src/components/right-sidebar/SourceControl.commit-failure-recovery.test.ts": 56, + "src/renderer/src/components/right-sidebar/SourceControl.commit-generation-records.test.ts": 39, + "src/renderer/src/components/right-sidebar/SourceControl.compare-summary.test.ts": 148, + "src/renderer/src/components/right-sidebar/SourceControl.hosted-review-header-link.test.tsx": 56, + "src/renderer/src/components/right-sidebar/SourceControl.open-file-highlight.test.tsx": 3419, + "src/renderer/src/components/right-sidebar/SourceControl.pr-generation-records.test.ts": 105, + "src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx": 2053, + "src/renderer/src/components/right-sidebar/SourceControl.push-failure-recovery.test.ts": 31, + "src/renderer/src/components/right-sidebar/SourceControl.remote-action-errors.test.ts": 43, + "src/renderer/src/components/right-sidebar/SourceControl.resolve-conflicts-prompt.test.ts": 48, + "src/renderer/src/components/right-sidebar/SourceControl.virtual-file-list.test.tsx": 4347, + "src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.test.tsx": 2372, + "src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.test.tsx": 181, + "src/renderer/src/components/right-sidebar/SourceControlTextGenerationDialog.test.ts": 410, + "src/renderer/src/components/right-sidebar/active-checks-status.test.ts": 1406, + "src/renderer/src/components/right-sidebar/activity-bar-buttons.test.tsx": 278, + "src/renderer/src/components/right-sidebar/activity-bar-overflow.test.ts": 37, + "src/renderer/src/components/right-sidebar/ai-vault-first-prompt-card.test.tsx": 414, + "src/renderer/src/components/right-sidebar/ai-vault-host-scope.test.ts": 1625, + "src/renderer/src/components/right-sidebar/ai-vault-original-pane-actions.test.ts": 313, + "src/renderer/src/components/right-sidebar/ai-vault-original-pane-index.test.ts": 55, + "src/renderer/src/components/right-sidebar/ai-vault-original-pane.test.ts": 51, + "src/renderer/src/components/right-sidebar/ai-vault-panel-session-list-request.test.tsx": 2030, + "src/renderer/src/components/right-sidebar/ai-vault-scan-issue-state.test.ts": 49, + "src/renderer/src/components/right-sidebar/ai-vault-scope-paths.test.ts": 76, + "src/renderer/src/components/right-sidebar/ai-vault-scope-state.test.ts": 40, + "src/renderer/src/components/right-sidebar/ai-vault-search-scope-identity.test.tsx": 450, + "src/renderer/src/components/right-sidebar/ai-vault-search-session.test.ts": 245, + "src/renderer/src/components/right-sidebar/ai-vault-session-chat-action.test.ts": 1255, + "src/renderer/src/components/right-sidebar/ai-vault-session-cli-fork.test.ts": 1496, + "src/renderer/src/components/right-sidebar/ai-vault-session-conditional-refresh-hook.test.tsx": 1235, + "src/renderer/src/components/right-sidebar/ai-vault-session-conditional-refresh.test.ts": 189, + "src/renderer/src/components/right-sidebar/ai-vault-session-continuation.test.ts": 55, + "src/renderer/src/components/right-sidebar/ai-vault-session-deletability.test.ts": 328, + "src/renderer/src/components/right-sidebar/ai-vault-session-display.test.ts": 32, + "src/renderer/src/components/right-sidebar/ai-vault-session-filters.test.ts": 87, + "src/renderer/src/components/right-sidebar/ai-vault-session-identity.test.ts": 35, + "src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.test.ts": 1558, + "src/renderer/src/components/right-sidebar/ai-vault-session-log-open.test.ts": 376, + "src/renderer/src/components/right-sidebar/ai-vault-session-path-actions.test.ts": 271, + "src/renderer/src/components/right-sidebar/ai-vault-session-projects.test.ts": 85, + "src/renderer/src/components/right-sidebar/ai-vault-session-publication-gate.test.ts": 178, + "src/renderer/src/components/right-sidebar/ai-vault-session-refresh.test.ts": 1830, + "src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-launch.test.ts": 1298, + "src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.test.ts": 617, + "src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat.test.ts": 77, + "src/renderer/src/components/right-sidebar/ai-vault-session-resume.test.ts": 1442, + "src/renderer/src/components/right-sidebar/ai-vault-session-surface-switch.test.ts": 1331, + "src/renderer/src/components/right-sidebar/ai-vault-session-worktree-map.test.tsx": 1595, + "src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts": 149, + "src/renderer/src/components/right-sidebar/ai-vault-view-defaults.test.ts": 45, + "src/renderer/src/components/right-sidebar/ai-vault-view-options-persistence.test.ts": 57, + "src/renderer/src/components/right-sidebar/branch-line-total-request-gate.test.ts": 29, + "src/renderer/src/components/right-sidebar/check-details-resize.test.ts": 26, + "src/renderer/src/components/right-sidebar/checks-entry-refresh.test.ts": 38, + "src/renderer/src/components/right-sidebar/checks-list-expanded-details.test.tsx": 819, + "src/renderer/src/components/right-sidebar/checks-panel-async-result-key.test.ts": 37, + "src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx": 2011, + "src/renderer/src/components/right-sidebar/checks-panel-empty-state.test.ts": 169, + "src/renderer/src/components/right-sidebar/checks-panel-git-status-snapshot.test.ts": 53, + "src/renderer/src/components/right-sidebar/checks-panel-hosted-review-click-routing.test.ts": 46, + "src/renderer/src/components/right-sidebar/checks-panel-pr-refresh-breadcrumb.test.ts": 55, + "src/renderer/src/components/right-sidebar/checks-panel-review-creation.test.ts": 46, + "src/renderer/src/components/right-sidebar/checks-panel-review-lookup-authority.test.ts": 46, + "src/renderer/src/components/right-sidebar/checks-panel-review.test.ts": 106, + "src/renderer/src/components/right-sidebar/checks-panel-terminal-worktree.test.ts": 67, + "src/renderer/src/components/right-sidebar/checks-panel/checks-detail-polling-policy.test.ts": 44, + "src/renderer/src/components/right-sidebar/checks-panel/gitlab-review-client.test.ts": 260, + "src/renderer/src/components/right-sidebar/checks-panel/panel-content-rendering.test.tsx": 4621, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-check-and-review-actions.test.tsx": 2052, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-create-review.test.tsx": 1694, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-foreground-effects.test.tsx": 278, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-generation.test.tsx": 1614, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-git-status-effects.test.tsx": 220, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-manual-refresh.test.tsx": 1654, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-polling-stability.test.tsx": 1612, + "src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-polling.test.tsx": 1996, + "src/renderer/src/components/right-sidebar/coalesced-poll-runner.test.ts": 50, + "src/renderer/src/components/right-sidebar/commit-failure-dialog-state.test.ts": 38, + "src/renderer/src/components/right-sidebar/commit-failure-summary.test.ts": 44, + "src/renderer/src/components/right-sidebar/create-review-draft-title.test.ts": 86, + "src/renderer/src/components/right-sidebar/diff-comments-clear-dialog-state.test.ts": 29, + "src/renderer/src/components/right-sidebar/discard-all-sequence.test.ts": 54, + "src/renderer/src/components/right-sidebar/file-explorer-add-project-action.test.ts": 50, + "src/renderer/src/components/right-sidebar/file-explorer-batch-deletion.test.ts": 124, + "src/renderer/src/components/right-sidebar/file-explorer-copy-file-owner.test.ts": 39, + "src/renderer/src/components/right-sidebar/file-explorer-delete-editor-owner.test.tsx": 1512, + "src/renderer/src/components/right-sidebar/file-explorer-dir-load-tracker.test.ts": 36, + "src/renderer/src/components/right-sidebar/file-explorer-dir-toggle-timing.test.ts": 171, + "src/renderer/src/components/right-sidebar/file-explorer-directory-filename-identity.test.ts": 1252, + "src/renderer/src/components/right-sidebar/file-explorer-directory-listing.test.ts": 1394, + "src/renderer/src/components/right-sidebar/file-explorer-display-root.test.ts": 1453, + "src/renderer/src/components/right-sidebar/file-explorer-drag-scroll-marker.test.tsx": 1874, + "src/renderer/src/components/right-sidebar/file-explorer-entries.test.ts": 43, + "src/renderer/src/components/right-sidebar/file-explorer-expanded-dirs-refresh.test.ts": 1394, + "src/renderer/src/components/right-sidebar/file-explorer-inline-input-ime-enter.test.tsx": 650, + "src/renderer/src/components/right-sidebar/file-explorer-inline-input-outside-click.test.tsx": 277, + "src/renderer/src/components/right-sidebar/file-explorer-inline-rename-flow.test.tsx": 738, + "src/renderer/src/components/right-sidebar/file-explorer-instant-dir-toggle.test.ts": 386, + "src/renderer/src/components/right-sidebar/file-explorer-keyboard-navigation.test.ts": 55, + "src/renderer/src/components/right-sidebar/file-explorer-linked-recovery.test.tsx": 1586, + "src/renderer/src/components/right-sidebar/file-explorer-linked-watch.test.ts": 1472, + "src/renderer/src/components/right-sidebar/file-explorer-name-filter-projection.test.ts": 74, + "src/renderer/src/components/right-sidebar/file-explorer-operation-generation.test.ts": 1376, + "src/renderer/src/components/right-sidebar/file-explorer-operation-owner.test.ts": 405, + "src/renderer/src/components/right-sidebar/file-explorer-paths.test.ts": 38, + "src/renderer/src/components/right-sidebar/file-explorer-read-error.test.ts": 35, + "src/renderer/src/components/right-sidebar/file-explorer-refresh-concurrency.test.ts": 38, + "src/renderer/src/components/right-sidebar/file-explorer-replace-selection-order-budget.test.tsx": 351, + "src/renderer/src/components/right-sidebar/file-explorer-reset.test.ts": 33, + "src/renderer/src/components/right-sidebar/file-explorer-row-context-menu.test.tsx": 437, + "src/renderer/src/components/right-sidebar/file-explorer-row-projection.test.ts": 42, + "src/renderer/src/components/right-sidebar/file-explorer-selection.test.ts": 40, + "src/renderer/src/components/right-sidebar/file-explorer-stale-dir-cache.test.ts": 39, + "src/renderer/src/components/right-sidebar/file-explorer-stale-retry.test.tsx": 1573, + "src/renderer/src/components/right-sidebar/file-explorer-watch-drive-root.test.ts": 1295, + "src/renderer/src/components/right-sidebar/file-explorer-watch-filename-identity.test.ts": 1279, + "src/renderer/src/components/right-sidebar/file-explorer-watch-reconcile.test.ts": 1602, + "src/renderer/src/components/right-sidebar/file-explorer-watch-refresh-scheduler.test.ts": 103, + "src/renderer/src/components/right-sidebar/file-explorer-watch-selected-host.test.ts": 1277, + "src/renderer/src/components/right-sidebar/file-search-include-pattern.test.ts": 49, + "src/renderer/src/components/right-sidebar/folder-workspace-attached-worktrees.test.ts": 59, + "src/renderer/src/components/right-sidebar/fork-push-target-label.test.ts": 28, + "src/renderer/src/components/right-sidebar/git-status-push-signal-refresh.test.ts": 109, + "src/renderer/src/components/right-sidebar/git-status-refresh-branch-line-total.test.ts": 364, + "src/renderer/src/components/right-sidebar/git-status-refresh-scheduler.test.ts": 63, + "src/renderer/src/components/right-sidebar/git-status-refresh.test.ts": 348, + "src/renderer/src/components/right-sidebar/github-pr-link-modal.test.ts": 32, + "src/renderer/src/components/right-sidebar/github-pr-stack-merge.test.ts": 71, + "src/renderer/src/components/right-sidebar/gitlab-mr-merge-state.test.ts": 54, + "src/renderer/src/components/right-sidebar/local-workspace-port-sections.test.ts": 46, + "src/renderer/src/components/right-sidebar/parent-pr-checks-projection-selector.test.ts": 291, + "src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.test.ts": 122, + "src/renderer/src/components/right-sidebar/parent-pr-checks-rows.test.ts": 159, + "src/renderer/src/components/right-sidebar/plugin-panel-activity-items.test.ts": 50, + "src/renderer/src/components/right-sidebar/plugin-panel-bridge-host.test.ts": 300, + "src/renderer/src/components/right-sidebar/plugin-panel-watchdog-visibility.test.ts": 415, + "src/renderer/src/components/right-sidebar/plugin-panel-watchdog.test.ts": 256, + "src/renderer/src/components/right-sidebar/ports-panel-host.test.ts": 293, + "src/renderer/src/components/right-sidebar/pr-comment-ack-summary.test.ts": 47, + "src/renderer/src/components/right-sidebar/pr-comment-presentation.test.ts": 178, + "src/renderer/src/components/right-sidebar/pr-comment-snapshotted-thread-resolver.test.ts": 51, + "src/renderer/src/components/right-sidebar/pr-comment-thread-resolution.test.ts": 35, + "src/renderer/src/components/right-sidebar/pr-comments-ai-launch-ack.test.ts": 84, + "src/renderer/src/components/right-sidebar/pr-comments-list-selection.test.tsx": 2722, + "src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.test.ts": 312, + "src/renderer/src/components/right-sidebar/review-cache-entry-selection.test.ts": 44, + "src/renderer/src/components/right-sidebar/right-panel-comment-composer.ime-enter.test.tsx": 329, + "src/renderer/src/components/right-sidebar/right-panel-comment-focus-timers.test.ts": 28, + "src/renderer/src/components/right-sidebar/right-sidebar-activity-visibility.test.ts": 53, + "src/renderer/src/components/right-sidebar/right-sidebar-effective-tab.test.ts": 51, + "src/renderer/src/components/right-sidebar/right-sidebar-primary-action-layout.test.ts": 34, + "src/renderer/src/components/right-sidebar/right-sidebar-titlebar-drag-regions.render.test.tsx": 946, + "src/renderer/src/components/right-sidebar/right-sidebar-titlebar-drag-regions.test.ts": 34, + "src/renderer/src/components/right-sidebar/right-sidebar-width.test.ts": 35, + "src/renderer/src/components/right-sidebar/runSourceControlAgentActionStart.test.ts": 198, + "src/renderer/src/components/right-sidebar/search-match-open.test.ts": 41, + "src/renderer/src/components/right-sidebar/search-rows.test.ts": 35, + "src/renderer/src/components/right-sidebar/source-control-action-recipe-match.test.ts": 277, + "src/renderer/src/components/right-sidebar/source-control-actions.test.ts": 28, + "src/renderer/src/components/right-sidebar/source-control-active-open-file-keys.test.ts": 42, + "src/renderer/src/components/right-sidebar/source-control-agent-action-dialog-support.test.ts": 104, + "src/renderer/src/components/right-sidebar/source-control-branch-context-row.test.tsx": 224, + "src/renderer/src/components/right-sidebar/source-control-branch-context-stats.test.ts": 73, + "src/renderer/src/components/right-sidebar/source-control-branch-section-heading.test.tsx": 1485, + "src/renderer/src/components/right-sidebar/source-control-commit-eligibility.test.ts": 35, + "src/renderer/src/components/right-sidebar/source-control-commit-message-rows.test.ts": 75, + "src/renderer/src/components/right-sidebar/source-control-create-pr-intent-flow.test.ts": 52, + "src/renderer/src/components/right-sidebar/source-control-create-review-blocked-action.test.ts": 77, + "src/renderer/src/components/right-sidebar/source-control-created-review-link.test.ts": 42, + "src/renderer/src/components/right-sidebar/source-control-discard-confirmation.test.ts": 77, + "src/renderer/src/components/right-sidebar/source-control-discard-dialog.test.tsx": 127, + "src/renderer/src/components/right-sidebar/source-control-discard-localization.test.ts": 350, + "src/renderer/src/components/right-sidebar/source-control-dropdown-items.create-pr-intent.test.ts": 98, + "src/renderer/src/components/right-sidebar/source-control-dropdown-items.test.ts": 234, + "src/renderer/src/components/right-sidebar/source-control-entry-actions.test.ts": 40, + "src/renderer/src/components/right-sidebar/source-control-entry-context-menu.test.tsx": 407, + "src/renderer/src/components/right-sidebar/source-control-file-filter.test.ts": 49, + "src/renderer/src/components/right-sidebar/source-control-header-toolbar-identity.test.tsx": 226, + "src/renderer/src/components/right-sidebar/source-control-header-toolbar.test.ts": 156, + "src/renderer/src/components/right-sidebar/source-control-hosted-review-creation-eligibility-snapshot.test.ts": 58, + "src/renderer/src/components/right-sidebar/source-control-hosted-review-push-target.test.ts": 52, + "src/renderer/src/components/right-sidebar/source-control-huge-repo-warning-dismissals.test.ts": 134, + "src/renderer/src/components/right-sidebar/source-control-launch-agent-args-applicability.test.ts": 403, + "src/renderer/src/components/right-sidebar/source-control-manual-review-url.test.ts": 63, + "src/renderer/src/components/right-sidebar/source-control-primary-action-tooltip.test.ts": 95, + "src/renderer/src/components/right-sidebar/source-control-primary-action.create-pr-intent.test.ts": 128, + "src/renderer/src/components/right-sidebar/source-control-primary-action.test.ts": 139, + "src/renderer/src/components/right-sidebar/source-control-push-recovery.test.ts": 49, + "src/renderer/src/components/right-sidebar/source-control-section-order.test.ts": 51, + "src/renderer/src/components/right-sidebar/source-control-split-open.test.ts": 42, + "src/renderer/src/components/right-sidebar/source-control-status-sort.test.ts": 36, + "src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts": 70, + "src/renderer/src/components/right-sidebar/source-control-text-generation-defaults.test.ts": 265, + "src/renderer/src/components/right-sidebar/source-control-too-many-changes-banner.test.tsx": 411, + "src/renderer/src/components/right-sidebar/source-control-tree.test.ts": 51, + "src/renderer/src/components/right-sidebar/source-control/commit/discard-all-failure-description.test.tsx": 1645, + "src/renderer/src/components/right-sidebar/source-control/commit/rename-mutation-paths.test.ts": 44, + "src/renderer/src/components/right-sidebar/source-control/commit/source-control-discard-editor-owner.test.tsx": 534, + "src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-failure-toast.test.ts": 209, + "src/renderer/src/components/right-sidebar/source-control/commit/source-control-entry-mutation-failures.test.tsx": 601, + "src/renderer/src/components/right-sidebar/source-control/commit/use-bulk-actions-capped-stage.test.tsx": 1610, + "src/renderer/src/components/right-sidebar/source-control/listing/section-action-buttons.test.tsx": 1771, + "src/renderer/src/components/right-sidebar/source-control/listing/section-header.test.tsx": 362, + "src/renderer/src/components/right-sidebar/source-control/listing/use-file-projection-work.test.tsx": 322, + "src/renderer/src/components/right-sidebar/source-control/listing/use-store-actions.store-subscriptions.test.tsx": 1484, + "src/renderer/src/components/right-sidebar/source-control/notes/use-diff-comment-notes.test.tsx": 272, + "src/renderer/src/components/right-sidebar/source-control/review/suppressed-github-pr.test.ts": 33, + "src/renderer/src/components/right-sidebar/source-control/review/use-action-model.test.tsx": 299, + "src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-jira-title.test.tsx": 1786, + "src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-creation.test.tsx": 1590, + "src/renderer/src/components/right-sidebar/source-control/review/use-hosted-review-state.test.tsx": 262, + "src/renderer/src/components/right-sidebar/source-control/review/use-pull-request-generation.test.tsx": 1676, + "src/renderer/src/components/right-sidebar/source-control/review/use-review-push-target.test.tsx": 250, + "src/renderer/src/components/right-sidebar/source-control/sync/git-history-format.test.ts": 46, + "src/renderer/src/components/right-sidebar/source-control/sync/git-history-graph-svg.test.tsx": 97, + "src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.local-ignore.test.tsx": 221, + "src/renderer/src/components/right-sidebar/status-display.test.ts": 37, + "src/renderer/src/components/right-sidebar/use-ai-vault-search-focus-request.test.tsx": 1727, + "src/renderer/src/components/right-sidebar/use-ai-vault-search.test.tsx": 509, + "src/renderer/src/components/right-sidebar/use-checks-panel-terminal-worktree.test.ts": 1576, + "src/renderer/src/components/right-sidebar/use-create-jira-review-title.test.tsx": 1440, + "src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.runtime-listing.test.tsx": 1694, + "src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.test.ts": 1548, + "src/renderer/src/components/right-sidebar/use-file-explorer-root-navigation.test.tsx": 312, + "src/renderer/src/components/right-sidebar/use-generate-before-create-pull-request-context.test.tsx": 1281, + "src/renderer/src/components/right-sidebar/use-git-status-upstream-ref-watch.test.ts": 60, + "src/renderer/src/components/right-sidebar/use-hosted-review-actions.test.tsx": 508, + "src/renderer/src/components/right-sidebar/use-installed-plugin-route-reconciliation.test.tsx": 408, + "src/renderer/src/components/right-sidebar/use-persisted-ai-vault-view-options.test.tsx": 296, + "src/renderer/src/components/right-sidebar/use-plugin-panel-theme-revision.test.tsx": 477, + "src/renderer/src/components/right-sidebar/use-right-sidebar-tab-routing.effective-tab.test.tsx": 239, + "src/renderer/src/components/right-sidebar/use-source-control-ai.test.ts": 1543, + "src/renderer/src/components/right-sidebar/use-source-control-branch-compare.test.tsx": 600, + "src/renderer/src/components/right-sidebar/use-source-control-git-history.test.tsx": 1641, + "src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.repo-default.test.ts": 1606, + "src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.test.ts": 1517, + "src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.os-file-drop.test.tsx": 1659, + "src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.test.ts": 32, + "src/renderer/src/components/right-sidebar/useFileExplorerHandlers.test.ts": 1682, + "src/renderer/src/components/right-sidebar/useFileExplorerImport.committed-scope.test.tsx": 372, + "src/renderer/src/components/right-sidebar/useFileExplorerImport.os-file-drop.test.tsx": 398, + "src/renderer/src/components/right-sidebar/useFileExplorerImport.remote.test.tsx": 1727, + "src/renderer/src/components/right-sidebar/useFileExplorerKeys.test.ts": 1454, + "src/renderer/src/components/right-sidebar/useFileExplorerReveal.scoped-root.test.tsx": 323, + "src/renderer/src/components/right-sidebar/useFileExplorerRowDrag.test.ts": 72, + "src/renderer/src/components/right-sidebar/useFileExplorerTree.lifecycle.test.tsx": 1748, + "src/renderer/src/components/right-sidebar/useFileExplorerTree.refresh-projection-churn.test.tsx": 1939, + "src/renderer/src/components/right-sidebar/useFileExplorerTree.scoped-root.test.tsx": 1330, + "src/renderer/src/components/right-sidebar/useFileExplorerTree.stale-dirs.test.tsx": 1625, + "src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.debounce.test.tsx": 1722, + "src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.test.ts": 1421, + "src/renderer/src/components/right-sidebar/useFileExplorerWatch.broker.test.tsx": 317, + "src/renderer/src/components/right-sidebar/useFileExplorerWatch.host-root.test.tsx": 596, + "src/renderer/src/components/right-sidebar/useFileExplorerWatch.pending-refresh.test.tsx": 301, + "src/renderer/src/components/right-sidebar/useFileExplorerWatch.test.ts": 1400, + "src/renderer/src/components/right-sidebar/useFileSearchPanel.lifecycle.test.tsx": 1764, + "src/renderer/src/components/right-sidebar/useFileSearchRunner.test.tsx": 645, + "src/renderer/src/components/right-sidebar/useGitStatusPolling.rerender.test.ts": 1659, + "src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts": 2419, + "src/renderer/src/components/right-sidebar/useHostedReviewStackParent.test.tsx": 282, + "src/renderer/src/components/right-sidebar/useSourceControlSelection.test.ts": 45, + "src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.test.tsx": 1633, + "src/renderer/src/components/settings/AccountsPane.owner-scope.test.tsx": 1189, + "src/renderer/src/components/settings/AccountsPane.section-lifetime.test.tsx": 2420, + "src/renderer/src/components/settings/AccountsPane.test.tsx": 2623, + "src/renderer/src/components/settings/AdvancedNetworkSettingsSection.interaction.test.tsx": 1666, + "src/renderer/src/components/settings/AdvancedNetworkSettingsSection.test.ts": 1263, + "src/renderer/src/components/settings/AdvancedPane.test.tsx": 460, + "src/renderer/src/components/settings/AgentLaunchDefaultsEditor.test.tsx": 568, + "src/renderer/src/components/settings/AgentLaunchSettingsSection.test.tsx": 588, + "src/renderer/src/components/settings/AgentSkillSetupPanel.freshness.test.tsx": 1484, + "src/renderer/src/components/settings/AgentSkillSetupPanel.test.tsx": 1770, + "src/renderer/src/components/settings/AgentsPane.test.tsx": 1928, + "src/renderer/src/components/settings/AntigravityAccountsSection.test.tsx": 661, + "src/renderer/src/components/settings/AppearanceChatContrastControls.draft.test.tsx": 828, + "src/renderer/src/components/settings/AppearanceChatContrastControls.test.tsx": 1178, + "src/renderer/src/components/settings/AppearanceChatSection.test.tsx": 1282, + "src/renderer/src/components/settings/AppearancePane.test.tsx": 946, + "src/renderer/src/components/settings/ArtifactsSettingsPane.test.tsx": 845, + "src/renderer/src/components/settings/AutomationsSettingsPane.test.tsx": 748, + "src/renderer/src/components/settings/BranchPrefixFeedback.test.tsx": 68, + "src/renderer/src/components/settings/BrowserClientHostedRemoteSetting.test.tsx": 601, + "src/renderer/src/components/settings/BrowserPane.search-catalog.test.tsx": 600, + "src/renderer/src/components/settings/BrowserPane.test.ts": 25, + "src/renderer/src/components/settings/BrowserSshWorkspaceRoutingSetting.test.tsx": 558, + "src/renderer/src/components/settings/BrowserUseSkillStep.test.tsx": 116, + "src/renderer/src/components/settings/BrowserUserAgentSetting.test.tsx": 1791, + "src/renderer/src/components/settings/ChatBehaviorSetting.test.tsx": 2255, + "src/renderer/src/components/settings/ChatNamingSetting.test.tsx": 1643, + "src/renderer/src/components/settings/ChatPermissionSetting.test.tsx": 701, + "src/renderer/src/components/settings/ChatSettingsSection.inline-visuals.test.tsx": 1725, + "src/renderer/src/components/settings/ChatSettingsSection.navigation.test.tsx": 1526, + "src/renderer/src/components/settings/ChatSettingsSection.test.tsx": 2327, + "src/renderer/src/components/settings/CliSection.install-failure.test.tsx": 1756, + "src/renderer/src/components/settings/CliSection.test.tsx": 1761, + "src/renderer/src/components/settings/CliSkillRuntimeSetup.test.tsx": 1408, + "src/renderer/src/components/settings/CloudVmSetupGuide.test.tsx": 1506, + "src/renderer/src/components/settings/CodexLoginLinkNotice.test.tsx": 384, + "src/renderer/src/components/settings/CommitMessageAiPane.test.tsx": 1994, + "src/renderer/src/components/settings/DefaultWindowsProjectRuntimeSetting.test.tsx": 409, + "src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx": 1714, + "src/renderer/src/components/settings/DiffShowWhitespaceSetting.test.tsx": 533, + "src/renderer/src/components/settings/EditorWordWrapSetting.test.tsx": 591, + "src/renderer/src/components/settings/EphemeralVmRuntimesSection.test.tsx": 578, + "src/renderer/src/components/settings/EphemeralVmsPane.test.tsx": 1248, + "src/renderer/src/components/settings/ExperimentalPane.test.tsx": 1165, + "src/renderer/src/components/settings/FloatingWorkspacePane.test.tsx": 1528, + "src/renderer/src/components/settings/GeneralPane.section-lifetime.test.tsx": 1377, + "src/renderer/src/components/settings/GeneralPane.test.ts": 1394, + "src/renderer/src/components/settings/GeneralRemoteServerUpdates.test.tsx": 243, + "src/renderer/src/components/settings/GeneralUpdateSettingsSection.test.tsx": 1689, + "src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.test.tsx": 1801, + "src/renderer/src/components/settings/GhosttyImportModal.test.ts": 157, + "src/renderer/src/components/settings/GitPane.test.ts": 1655, + "src/renderer/src/components/settings/GitPane.test.tsx": 1442, + "src/renderer/src/components/settings/GrokAccountsSection.test.tsx": 373, + "src/renderer/src/components/settings/HiddenExperimentalGroup.test.tsx": 361, + "src/renderer/src/components/settings/HostAgentLaunchSettings.test.tsx": 2818, + "src/renderer/src/components/settings/KagiSessionLinkForm.test.ts": 1230, + "src/renderer/src/components/settings/LinearAgentSkillGuide.test.tsx": 173, + "src/renderer/src/components/settings/LinearAgentSkillNotes.test.tsx": 123, + "src/renderer/src/components/settings/LinearAgentSkillPane.test.tsx": 687, + "src/renderer/src/components/settings/LocalNetworkConnectionTest.test.tsx": 374, + "src/renderer/src/components/settings/MachineNameField.test.tsx": 479, + "src/renderer/src/components/settings/ManagedServerRow.test.tsx": 503, + "src/renderer/src/components/settings/McpConfigSection.host-owner.test.tsx": 1738, + "src/renderer/src/components/settings/MobileEmulatorAgentControlRow.test.tsx": 1506, + "src/renderer/src/components/settings/MobilePairingConnectionOptions.test.tsx": 622, + "src/renderer/src/components/settings/MobilePairingQrSection.test.tsx": 477, + "src/renderer/src/components/settings/MobilePairingSetupSection.test.tsx": 1086, + "src/renderer/src/components/settings/MobilePane.test.tsx": 1559, + "src/renderer/src/components/settings/MobilePaneAddressSearch.test.tsx": 551, + "src/renderer/src/components/settings/NativeChatSupportedAgents.test.tsx": 608, + "src/renderer/src/components/settings/NotificationHostToggles.test.tsx": 603, + "src/renderer/src/components/settings/NotificationsPane.test.tsx": 1361, + "src/renderer/src/components/settings/OpenInMenuSetting.test.ts": 179, + "src/renderer/src/components/settings/OrcaAccountSettingsPane.test.tsx": 487, + "src/renderer/src/components/settings/OrchestrationPane.test.tsx": 2064, + "src/renderer/src/components/settings/OrchestrationSkillAgentCoverage.test.tsx": 168, + "src/renderer/src/components/settings/PluginConsentDialog.test.tsx": 2027, + "src/renderer/src/components/settings/PluginInstallDialog.test.tsx": 780, + "src/renderer/src/components/settings/PluginKeybindingConsentPreview.test.ts": 1415, + "src/renderer/src/components/settings/PluginMarketplaceBrowser.test.tsx": 689, + "src/renderer/src/components/settings/PluginMarketplaceSourceDialog.test.tsx": 493, + "src/renderer/src/components/settings/PluginSettingsRow.test.tsx": 582, + "src/renderer/src/components/settings/PluginsSettingsSection.lifecycle.test.tsx": 1212, + "src/renderer/src/components/settings/PrivacyPane.test.ts": 1509, + "src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.test.tsx": 724, + "src/renderer/src/components/settings/QuickCommandsList.test.tsx": 442, + "src/renderer/src/components/settings/QuickCommandsPane.test.ts": 1701, + "src/renderer/src/components/settings/RemoteServerUpdateDialog.test.tsx": 333, + "src/renderer/src/components/settings/RemoteServerUpdateStatus.test.ts": 75, + "src/renderer/src/components/settings/RepositoryForkSyncSection.test.tsx": 750, + "src/renderer/src/components/settings/RepositoryGitHubAccountSection.inventory-lifetime.test.tsx": 578, + "src/renderer/src/components/settings/RepositoryGitHubAccountSection.test.tsx": 506, + "src/renderer/src/components/settings/RepositoryHooksSection.test.ts": 776, + "src/renderer/src/components/settings/RepositoryHostSetupsSection.settings-entry.test.tsx": 1648, + "src/renderer/src/components/settings/RepositoryHostSetupsSection.test.tsx": 1935, + "src/renderer/src/components/settings/RepositoryHostSetupsSection.workspace-window.test.tsx": 1452, + "src/renderer/src/components/settings/RepositoryIconEmojiPicker.test.tsx": 252, + "src/renderer/src/components/settings/RepositoryIconPicker.github-avatar-refresh.test.tsx": 494, + "src/renderer/src/components/settings/RepositoryJiraProjectSection.test.tsx": 309, + "src/renderer/src/components/settings/RepositoryPane.test.ts": 3208, + "src/renderer/src/components/settings/RepositoryPaneDraftInput.test.tsx": 318, + "src/renderer/src/components/settings/RepositorySourceControlAiSection.test.ts": 357, "src/renderer/src/components/settings/RepositoryWorktreeDefaultsSection.test.tsx": 415, - "src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts": 120, - "src/renderer/src/components/settings/RuntimeHostAccessForm.test.tsx": 405, - "src/renderer/src/components/settings/RuntimePairingGeneratorForm.test.tsx": 315, - "src/renderer/src/components/settings/RuntimePairingUrlGenerator.test.tsx": 335, - "src/renderer/src/components/settings/SessionHistoryComputerRow.test.tsx": 967, - "src/renderer/src/components/settings/SessionHistorySettingsPane.test.tsx": 1217, - "src/renderer/src/components/settings/Settings.load-performance.test.ts": 54, - "src/renderer/src/components/settings/SettingsConstants.test.ts": 285, - "src/renderer/src/components/settings/SettingsFormControls.font-autocomplete.test.tsx": 797, - "src/renderer/src/components/settings/SettingsFormControls.number-field.test.tsx": 622, - "src/renderer/src/components/settings/SettingsFormControls.segmented-control.test.tsx": 662, - "src/renderer/src/components/settings/SettingsSidebar.test.tsx": 2372, - "src/renderer/src/components/settings/ShareSkillsSettingsPane.test.tsx": 832, - "src/renderer/src/components/settings/ShortcutCommandBlock.test.tsx": 1771, - "src/renderer/src/components/settings/ShortcutFilterRail.test.ts": 215, - "src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.test.tsx": 399, - "src/renderer/src/components/settings/SparsePresetSettingsSection.test.tsx": 633, - "src/renderer/src/components/settings/SshHostDeltaMoveDialog.test.tsx": 450, - "src/renderer/src/components/settings/SshTargetCard.test.tsx": 430, - "src/renderer/src/components/settings/SshTargetForm.test.tsx": 1002, - "src/renderer/src/components/settings/TaskSourceLinearSetup.test.tsx": 484, - "src/renderer/src/components/settings/TaskSourceProviderCard.test.tsx": 376, - "src/renderer/src/components/settings/TaskSourceShowInTasksStep.test.tsx": 196, - "src/renderer/src/components/settings/TaskSourceSimpleSetup.test.tsx": 355, - "src/renderer/src/components/settings/TasksPane.test.tsx": 926, - "src/renderer/src/components/settings/TerminalAdvancedSection.test.tsx": 1998, - "src/renderer/src/components/settings/TerminalAppearanceSection.ghostty.test.ts": 499, - "src/renderer/src/components/settings/TerminalContrastSetting.test.tsx": 692, - "src/renderer/src/components/settings/TerminalFontSizeSetting.test.tsx": 1643, - "src/renderer/src/components/settings/TerminalPane.pwsh.test.ts": 489, - "src/renderer/src/components/settings/TerminalSettingsPreview.lifecycle.test.tsx": 407, - "src/renderer/src/components/settings/TerminalTccAttributionNotice.test.tsx": 322, - "src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts": 1446, - "src/renderer/src/components/settings/UnsealedCredentialNotice.test.tsx": 313, - "src/renderer/src/components/settings/VoiceMicrophoneSetting.test.tsx": 945, - "src/renderer/src/components/settings/VoicePane.test.tsx": 902, - "src/renderer/src/components/settings/VoiceSpeechModelSection.test.tsx": 552, - "src/renderer/src/components/settings/WorkspaceDirectorySetting.test.tsx": 1671, - "src/renderer/src/components/settings/ZcodePlanAccountsSection.test.tsx": 873, - "src/renderer/src/components/settings/accounts-pane-claude-section.test.tsx": 1765, - "src/renderer/src/components/settings/accounts-pane-opencode-credentials.test.ts": 435, - "src/renderer/src/components/settings/accounts-search.test.ts": 61, - "src/renderer/src/components/settings/agent-availability-settings.test.ts": 297, - "src/renderer/src/components/settings/agent-default-env-draft.test.ts": 60, - "src/renderer/src/components/settings/appearance-interface-summary.test.ts": 321, - "src/renderer/src/components/settings/appearance-search.test.ts": 556, - "src/renderer/src/components/settings/appearance-status-bar-search.test.ts": 117, - "src/renderer/src/components/settings/appearance-usage-percentage-search.test.ts": 109, - "src/renderer/src/components/settings/bitbucket-integration-card.test.tsx": 466, - "src/renderer/src/components/settings/browser-cookie-import-label.test.ts": 150, - "src/renderer/src/components/settings/browser-link-routing-localization.test.ts": 307, - "src/renderer/src/components/settings/browser-search.test.ts": 234, - "src/renderer/src/components/settings/browser-session-host-selection.test.ts": 47, - "src/renderer/src/components/settings/chat-naming-persistence.test.tsx": 3409, - "src/renderer/src/components/settings/chat-settings-unsaved-guard.test.tsx": 1734, - "src/renderer/src/components/settings/cli-install-failure.test.ts": 60, - "src/renderer/src/components/settings/cli-source-control-integration-cards.test.tsx": 414, - "src/renderer/src/components/settings/codex-account-auth-warning.test.ts": 60, - "src/renderer/src/components/settings/codex-config-sync-warning.test.ts": 42, - "src/renderer/src/components/settings/codex-session-source-home-control.test.tsx": 164, - "src/renderer/src/components/settings/developer-permissions-search.test.ts": 318, - "src/renderer/src/components/settings/general-editor-search.test.ts": 140, - "src/renderer/src/components/settings/general-preview-tab-search.test.ts": 101, - "src/renderer/src/components/settings/host-scoped-setting-scope.test.ts": 60, - "src/renderer/src/components/settings/integrations-pane-status.test.ts": 45, - "src/renderer/src/components/settings/jira-integration-card.test.tsx": 467, - "src/renderer/src/components/settings/linear-agent-skill-install-cta.test.tsx": 1710, - "src/renderer/src/components/settings/managed-server-copy.test.ts": 85, - "src/renderer/src/components/settings/mobile-network-interface-selection.test.ts": 71, - "src/renderer/src/components/settings/mobile-pairing-device-polling.test.ts": 67, - "src/renderer/src/components/settings/mobile-pane-search.test.ts": 92, - "src/renderer/src/components/settings/native-chat-experimental-search-entry.test.ts": 112, - "src/renderer/src/components/settings/notification-host-pending-save.test.tsx": 1929, - "src/renderer/src/components/settings/osc52-clipboard-copy.test.ts": 277, - "src/renderer/src/components/settings/plugin-install-source.test.ts": 297, - "src/renderer/src/components/settings/provider-account-scope.test.ts": 86, - "src/renderer/src/components/settings/provider-account-visibility.test.ts": 59, - "src/renderer/src/components/settings/provider-rate-limit-scope-panels.test.tsx": 248, - "src/renderer/src/components/settings/repository-hook-settings-draft.test.ts": 290, - "src/renderer/src/components/settings/repository-host-setup-options.test.ts": 290, - "src/renderer/src/components/settings/repository-icon-github.test.ts": 62, - "src/renderer/src/components/settings/repository-source-control-ai-global-ux.test.ts": 429, - "src/renderer/src/components/settings/repository-source-control-ai-persist-queue.test.ts": 94, - "src/renderer/src/components/settings/runtime-server-row-unverifiable-host.test.tsx": 1730, - "src/renderer/src/components/settings/runtime-server-workflow-sections.test.tsx": 377, - "src/renderer/src/components/settings/session-history-status-copy.test.ts": 282, - "src/renderer/src/components/settings/session-search-computer-rollup.test.ts": 45, - "src/renderer/src/components/settings/setting-ownership.test.ts": 96, - "src/renderer/src/components/settings/settings-deep-link-target-watcher.test.ts": 187, - "src/renderer/src/components/settings/settings-form-option-filter.test.ts": 69, - "src/renderer/src/components/settings/settings-host-scope.test.ts": 94, - "src/renderer/src/components/settings/settings-project-checkout-target.test.ts": 70, - "src/renderer/src/components/settings/settings-project-list.test.ts": 94, - "src/renderer/src/components/settings/settings-project-regrouping.test.ts": 550, - "src/renderer/src/components/settings/settings-search-keywords.test.ts": 137, - "src/renderer/src/components/settings/settings-search.test.ts": 63, - "src/renderer/src/components/settings/settings-setup-guide-progress-hook.test.tsx": 95, - "src/renderer/src/components/settings/settings-setup-guide-progress.test.ts": 1583, - "src/renderer/src/components/settings/shortcut-binding-list-mutations.test.ts": 57, - "src/renderer/src/components/settings/shortcut-definition-catalog.test.ts": 316, - "src/renderer/src/components/settings/shortcut-groups.test.ts": 143, - "src/renderer/src/components/settings/shortcut-recording-state.test.ts": 51, - "src/renderer/src/components/settings/shortcut-row-visibility.test.ts": 179, - "src/renderer/src/components/settings/source-control-ai-settings-persistence.test.tsx": 3192, - "src/renderer/src/components/settings/sparse-preset-date.test.ts": 66, - "src/renderer/src/components/settings/sparse-preset-operation-error.test.ts": 53, - "src/renderer/src/ssh/ssh-host-server-status-copy.test.ts": 107, - "src/renderer/src/components/settings/ssh-target-action-state.test.ts": 47, - "src/renderer/src/components/settings/ssh-target-draft.test.ts": 66, - "src/renderer/src/components/settings/ssh-target-save-payload.test.ts": 114, - "src/renderer/src/components/settings/task-source-setup-state.test.ts": 62, - "src/renderer/src/components/settings/task-tracker-integration-cards.test.tsx": 565, - "src/renderer/src/components/settings/terminal-preview-content.test.ts": 55, - "src/renderer/src/components/settings/terminal-search.test.ts": 304, - "src/renderer/src/components/settings/use-debounced-settings-text-draft.test.ts": 297, - "src/renderer/src/components/settings/use-mac-captured-digit-chords.test.ts": 706, - "src/renderer/src/components/settings/use-published-machine-name.test.tsx": 269, - "src/renderer/src/components/settings/use-repository-hook-settings-draft.test.tsx": 461, - "src/renderer/src/components/settings/use-runtime-environment-catalog.test.tsx": 1802, - "src/renderer/src/components/settings/use-session-search-status.test.tsx": 560, - "src/renderer/src/components/settings/use-task-source-provider-readiness.test.tsx": 245, - "src/renderer/src/components/settings/use-zcode-plan-credentials.test.tsx": 890, - "src/renderer/src/components/settings/useGhosttyImport.test.ts": 84, - "src/renderer/src/components/settings/useWarpThemeImport.test.ts": 111, - "src/renderer/src/components/settings/worktree-symlink-path-filter.scan-budget.test.ts": 104, - "src/renderer/src/components/settings/worktree-symlink-path-filter.test.ts": 56, - "src/renderer/src/components/setup-guide/SetupGuideModal.mount-gating.test.tsx": 1712, - "src/renderer/src/components/setup-guide/use-setup-guide-progress.test.ts": 1636, - "src/renderer/src/components/setup-guide/use-setup-guide-telemetry.test.ts": 57, - "src/renderer/src/components/shared/MacFolderAccessFixDialog.test.tsx": 1061, - "src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts": 2003, - "src/renderer/src/components/shared/useDaemonActions.test.tsx": 448, - "src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx": 195, - "src/renderer/src/components/sidebar/AddRemoteHostDialog.config-picker.test.tsx": 1551, - "src/renderer/src/components/sidebar/AddRemoteHostFields.test.tsx": 541, - "src/renderer/src/components/sidebar/AddRepoCreateStep.test.tsx": 317, - "src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts": 59, - "src/renderer/src/components/sidebar/AddRepoDialogChrome.test.tsx": 632, - "src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx": 558, - "src/renderer/src/components/sidebar/AddRepoHostSelector.test.tsx": 228, - "src/renderer/src/components/sidebar/AddRepoNestedImportStep.test.tsx": 492, - "src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx": 659, - "src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts": 141, - "src/renderer/src/components/sidebar/AgentDashboardSidebarHost.test.tsx": 1722, - "src/renderer/src/components/sidebar/AutoRenameFailedDialog.test.tsx": 505, - "src/renderer/src/components/sidebar/CacheTimer.test.tsx": 154, - "src/renderer/src/components/sidebar/CommentMarkdown.github-attachment-image.test.tsx": 1722, - "src/renderer/src/components/sidebar/CommentMarkdown.github-callout.test.tsx": 2046, - "src/renderer/src/components/sidebar/CommentMarkdown.growing.test.tsx": 2136, - "src/renderer/src/components/sidebar/CommentMarkdown.link-click.test.tsx": 2032, - "src/renderer/src/components/sidebar/CommentMarkdown.ordered-list.test.tsx": 2756, - "src/renderer/src/components/sidebar/CommentMarkdown.test.tsx": 2088, - "src/renderer/src/components/sidebar/CommentMarkdown.word-fade.test.tsx": 1979, - "src/renderer/src/components/sidebar/CommentMermaidBlock.system-theme.test.tsx": 290, - "src/renderer/src/components/sidebar/DeleteNestedWorktreesDialog.test.tsx": 878, - "src/renderer/src/components/sidebar/DeleteWorktreeDialog.test.tsx": 1146, - "src/renderer/src/components/sidebar/DeleteWorktreeDialogFooter.test.tsx": 613, - "src/renderer/src/components/sidebar/DeleteWorktreeTargetPreview.test.tsx": 699, - "src/renderer/src/components/sidebar/FilterToggleRow.test.tsx": 307, - "src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.test.tsx": 1535, - "src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.reminder-toast.test.tsx": 1917, - "src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.test.tsx": 2535, - "src/renderer/src/components/sidebar/LocalGitToolchainScanBanner.test.tsx": 1490, - "src/renderer/src/components/sidebar/MarkdownImageLightbox.test.tsx": 589, - "src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx": 1727, - "src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx": 1115, - "src/renderer/src/components/sidebar/NoticeHostGlyph.test.tsx": 1056, - "src/renderer/src/components/sidebar/OrcaYamlTrustDialog.test.tsx": 94, - "src/renderer/src/components/sidebar/ProjectAddedDialog.test.tsx": 108, - "src/renderer/src/components/sidebar/ProjectGroupDeleteDialog.test.tsx": 561, - "src/renderer/src/components/sidebar/ProjectHeaderActions.test.tsx": 194, - "src/renderer/src/components/sidebar/RemoteFileBrowser.paste.test.tsx": 510, - "src/renderer/src/components/sidebar/RemoveFolderDialog.test.tsx": 425, - "src/renderer/src/components/sidebar/SetupGuideSidebarEntry.test.tsx": 353, - "src/renderer/src/components/sidebar/SetupScriptPromptCard.test.ts": 54, - "src/renderer/src/components/sidebar/SetupScriptPromptCardShell.test.tsx": 484, - "src/renderer/src/components/sidebar/Sidebar.test.tsx": 840, - "src/renderer/src/components/sidebar/SidebarAgentsList.test.tsx": 2753, - "src/renderer/src/components/sidebar/SidebarFeedbackDialog.test.tsx": 3074, - "src/renderer/src/components/sidebar/SidebarGroupByToggle.test.tsx": 456, - "src/renderer/src/components/sidebar/SidebarHeader.test.tsx": 407, - "src/renderer/src/components/sidebar/SidebarNav.test.tsx": 1772, - "src/renderer/src/components/sidebar/SidebarRepositoryFilterSection.test.tsx": 598, - "src/renderer/src/components/sidebar/SidebarSettingsHelpMenu.test.tsx": 605, - "src/renderer/src/components/sidebar/SidebarToolbar.test.tsx": 392, - "src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.test.tsx": 459, - "src/renderer/src/components/sidebar/StatusIndicator.test.ts": 201, - "src/renderer/src/components/sidebar/WorkspaceAttachmentOrigins.test.tsx": 589, - "src/renderer/src/components/sidebar/WorkspaceAttachmentSearch.resolution.test.tsx": 2470, - "src/renderer/src/components/sidebar/WorkspaceKanbanCard.host-identity.test.tsx": 367, - "src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.mount-gating.test.tsx": 1879, - "src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.search.test.tsx": 2008, - "src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.task-status-sync.test.tsx": 1680, - "src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.test.tsx": 1665, - "src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.test.tsx": 304, - "src/renderer/src/components/sidebar/WorkspaceKanbanLaneGrid.test.tsx": 387, - "src/renderer/src/components/sidebar/WorkspaceKanbanSearchField.test.tsx": 379, - "src/renderer/src/components/sidebar/WorkspaceKanbanSettingsMenu.test.tsx": 826, - "src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.test.tsx": 339, - "src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx": 1949, - "src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx": 3248, - "src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx": 1908, - "src/renderer/src/components/sidebar/WorktreeCard.delete-failed.test.tsx": 1127, - "src/renderer/src/components/sidebar/WorktreeCard.host-badge-toggle.test.tsx": 1664, - "src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx": 1698, - "src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx": 1106, - "src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx": 1239, - "src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx": 1332, - "src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx": 1162, - "src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx": 1696, - "src/renderer/src/components/sidebar/WorktreeCard.single-reference-details.test.tsx": 1881, - "src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx": 1238, - "src/renderer/src/components/sidebar/WorktreeCard.test.ts": 3227, - "src/renderer/src/components/sidebar/WorktreeCardAgents.acknowledgement-emphasis.test.tsx": 1133, - "src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx": 965, - "src/renderer/src/components/sidebar/WorktreeCardAgents.expansion-remount.test.tsx": 1237, - "src/renderer/src/components/sidebar/WorktreeCardAgents.send-target.test.tsx": 1319, - "src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx": 977, - "src/renderer/src/components/sidebar/WorktreeCardAutomationDetailSection.test.tsx": 418, - "src/renderer/src/components/sidebar/WorktreeCardDisplayMenuSection.test.tsx": 473, - "src/renderer/src/components/sidebar/WorktreeCardLinkedItemsSection.test.tsx": 2662, - "src/renderer/src/components/sidebar/WorktreeCardMeta.interaction.test.tsx": 2731, - "src/renderer/src/components/sidebar/WorktreeCardMeta.test.tsx": 2991, - "src/renderer/src/components/sidebar/WorktreeCardPorts.test.tsx": 1396, - "src/renderer/src/components/sidebar/WorktreeCardSshHostControl.test.tsx": 1913, - "src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx": 387, - "src/renderer/src/components/sidebar/WorktreeContextMenu.copy-worktree-name.test.tsx": 1913, - "src/renderer/src/components/sidebar/WorktreeContextMenu.delete-shortcut.test.tsx": 1311, - "src/renderer/src/components/sidebar/WorktreeContextMenu.react185-bystander.test.tsx": 2062, - "src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts": 2108, - "src/renderer/src/components/sidebar/WorktreeDeveloperMenu.test.tsx": 146, - "src/renderer/src/components/sidebar/WorktreeDeveloperMenuReveal.test.tsx": 586, - "src/renderer/src/components/sidebar/WorktreeList.card-memo-stability.test.tsx": 2031, - "src/renderer/src/components/sidebar/WorktreeList.empty-project-rows.test.ts": 2233, - "src/renderer/src/components/sidebar/WorktreeList.folder-workspace-rows.test.ts": 2667, - "src/renderer/src/components/sidebar/WorktreeList.group-headers.test.ts": 3532, - "src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx": 4327, - "src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts": 3044, - "src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx": 1805, - "src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx": 2913, - "src/renderer/src/components/sidebar/WorktreeMetaDialog.host-scope.test.tsx": 2087, - "src/renderer/src/components/sidebar/WorktreeMetaDialog.test.tsx": 6094, - "src/renderer/src/components/sidebar/WorktreeOpenInMenu.test.tsx": 195, - "src/renderer/src/components/sidebar/WorktreeParentPickerPopover.test.ts": 1624, - "src/renderer/src/components/sidebar/WorktreeReferenceStack.test.tsx": 414, - "src/renderer/src/components/sidebar/WorktreeTitleInlineRename.begin-editing.test.tsx": 120, - "src/renderer/src/components/sidebar/WorktreeTitleInlineRename.editor-lifecycle.test.tsx": 439, - "src/renderer/src/components/sidebar/WorktreeTitleInlineRename.test.tsx": 265, - "src/renderer/src/components/sidebar/WorktreeVisibilityDialog.test.tsx": 2342, - "src/renderer/src/components/sidebar/WorktreeVisibilityHelpPopover.test.tsx": 450, - "src/renderer/src/components/sidebar/WorktreeVisibilitySourceList.test.tsx": 460, - "src/renderer/src/components/sidebar/active-worktree-focus-after-delete.test.ts": 84, - "src/renderer/src/components/sidebar/add-remote-host-ssh-actions.test.ts": 139, - "src/renderer/src/components/sidebar/add-repo-browse-authority.test.ts": 49, - "src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.test.ts": 59, - "src/renderer/src/components/sidebar/add-repo-skip-finalization.test.ts": 81, - "src/renderer/src/components/sidebar/agent-send-picker-collapse.test.tsx": 1815, - "src/renderer/src/components/sidebar/child-workspaces-toggle-target.test.ts": 1879, - "src/renderer/src/components/sidebar/child-workspaces-toggle-visibility.test.ts": 2328, - "src/renderer/src/components/sidebar/clone-defaults.test.ts": 59, - "src/renderer/src/components/sidebar/create-project-defaults.test.ts": 53, - "src/renderer/src/components/sidebar/default-branch-folder-project-root.test.ts": 1426, - "src/renderer/src/components/sidebar/default-branch-visible-under-hide-sleeping.test.ts": 1666, - "src/renderer/src/components/sidebar/delete-worktree-always-force-preference.test.tsx": 2527, - "src/renderer/src/components/sidebar/delete-worktree-dirty-change-counts.test.ts": 81, - "src/renderer/src/components/sidebar/delete-worktree-dirty-change-hydration.test.tsx": 644, - "src/renderer/src/components/sidebar/delete-worktree-failure-toast.test.tsx": 2640, - "src/renderer/src/components/sidebar/delete-worktree-flow.test.ts": 914, - "src/renderer/src/components/sidebar/delete-worktree-parallel-flow.test.ts": 521, - "src/renderer/src/components/sidebar/delete-worktree-toast.test.ts": 81, - "src/renderer/src/components/sidebar/empty-project-placeholder-repos.test.ts": 63, - "src/renderer/src/components/sidebar/focused-agent-row-highlight.test.ts": 1445, - "src/renderer/src/components/sidebar/folder-workspace-card-pr-display.test.ts": 130, - "src/renderer/src/components/sidebar/folder-workspace-composer-expiry.test.tsx": 2194, - "src/renderer/src/components/sidebar/folder-workspace-composer-helpers.test.ts": 91, - "src/renderer/src/components/sidebar/folder-workspace-composer-path-status.test.tsx": 1765, - "src/renderer/src/components/sidebar/folder-workspace-composer-submit-host-platform.test.ts": 2163, - "src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts": 2015, - "src/renderer/src/components/sidebar/folder-workspace-linked-startup-plan.test.ts": 1762, - "src/renderer/src/components/sidebar/host-header-drag.test.tsx": 275, - "src/renderer/src/components/sidebar/host-header-menu-items.test.ts": 53, - "src/renderer/src/components/sidebar/host-rename-remove.test.ts": 53, - "src/renderer/src/components/sidebar/host-section-collapse-folders.test.ts": 184, - "src/renderer/src/components/sidebar/host-section-collapse.test.ts": 195, - "src/renderer/src/components/sidebar/host-section-folder-workspace-counts.test.ts": 53, - "src/renderer/src/components/sidebar/host-section-order.test.ts": 47, - "src/renderer/src/components/sidebar/host-section-reveal.test.tsx": 2061, - "src/renderer/src/components/sidebar/host-section-rows.test.ts": 179, - "src/renderer/src/components/sidebar/hovered-workspace-delete.test.ts": 2113, - "src/renderer/src/components/sidebar/imported-worktrees-card-actions.test.ts": 63, - "src/renderer/src/components/sidebar/imported-worktrees-card-candidates.test.ts": 75, - "src/renderer/src/components/sidebar/kanban-empty-selection-prune-budget.test.tsx": 1098, - "src/renderer/src/components/sidebar/linear-agent-skill-runtime.shell-override.test.ts": 82, - "src/renderer/src/components/sidebar/linear-agent-skill-setup-reminders.test.ts": 44, + "src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts": 280, + "src/renderer/src/components/settings/RuntimeHostAccessForm.test.tsx": 334, + "src/renderer/src/components/settings/RuntimePairingGeneratorForm.test.tsx": 260, + "src/renderer/src/components/settings/RuntimePairingUrlGenerator.test.tsx": 365, + "src/renderer/src/components/settings/SessionHistoryComputerRow.test.tsx": 816, + "src/renderer/src/components/settings/SessionHistorySettingsPane.test.tsx": 1332, + "src/renderer/src/components/settings/Settings.load-performance.test.ts": 43, + "src/renderer/src/components/settings/SettingsConstants.test.ts": 267, + "src/renderer/src/components/settings/SettingsFormControls.font-autocomplete.test.tsx": 850, + "src/renderer/src/components/settings/SettingsFormControls.number-field.test.tsx": 571, + "src/renderer/src/components/settings/SettingsFormControls.segmented-control.test.tsx": 598, + "src/renderer/src/components/settings/SettingsSidebar.test.tsx": 2294, + "src/renderer/src/components/settings/ShareSkillsSettingsPane.test.tsx": 848, + "src/renderer/src/components/settings/ShortcutCommandBlock.test.tsx": 1568, + "src/renderer/src/components/settings/ShortcutFilterRail.test.ts": 139, + "src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.test.tsx": 418, + "src/renderer/src/components/settings/SparsePresetSettingsSection.test.tsx": 653, + "src/renderer/src/components/settings/SshHostDeltaMoveDialog.test.tsx": 364, + "src/renderer/src/components/settings/SshPane.test.tsx": 931, + "src/renderer/src/components/settings/SshTargetCard.test.tsx": 453, + "src/renderer/src/components/settings/SshTargetForm.test.tsx": 975, + "src/renderer/src/components/settings/TaskSourceLinearSetup.test.tsx": 434, + "src/renderer/src/components/settings/TaskSourceProviderCard.test.tsx": 301, + "src/renderer/src/components/settings/TaskSourceShowInTasksStep.test.tsx": 174, + "src/renderer/src/components/settings/TaskSourceSimpleSetup.test.tsx": 321, + "src/renderer/src/components/settings/TasksPane.test.tsx": 694, + "src/renderer/src/components/settings/TerminalAdvancedSection.test.tsx": 1811, + "src/renderer/src/components/settings/TerminalAppearanceSection.ghostty.test.ts": 461, + "src/renderer/src/components/settings/TerminalContrastSetting.test.tsx": 761, + "src/renderer/src/components/settings/TerminalFontSizeSetting.test.tsx": 1648, + "src/renderer/src/components/settings/TerminalPane.pwsh.test.ts": 441, + "src/renderer/src/components/settings/TerminalSettingsPreview.lifecycle.test.tsx": 376, + "src/renderer/src/components/settings/TerminalTccAttributionNotice.test.tsx": 293, + "src/renderer/src/components/settings/TerminalThemeSections.lifecycle.test.ts": 1568, + "src/renderer/src/components/settings/UnsealedCredentialNotice.test.tsx": 270, + "src/renderer/src/components/settings/VoiceDictationSettingsSection.test.tsx": 593, + "src/renderer/src/components/settings/VoiceMicrophoneSetting.test.tsx": 982, + "src/renderer/src/components/settings/VoicePane.test.tsx": 801, + "src/renderer/src/components/settings/VoiceSpeechModelSection.test.tsx": 467, + "src/renderer/src/components/settings/WorkspaceDirectorySetting.test.tsx": 1697, + "src/renderer/src/components/settings/ZcodeCliSetupSection.test.tsx": 609, + "src/renderer/src/components/settings/ZcodePlanAccountsSection.test.tsx": 990, + "src/renderer/src/components/settings/accounts-pane-claude-section.test.tsx": 1317, + "src/renderer/src/components/settings/accounts-pane-opencode-credentials.test.ts": 396, + "src/renderer/src/components/settings/accounts-search.test.ts": 48, + "src/renderer/src/components/settings/agent-availability-settings.test.ts": 287, + "src/renderer/src/components/settings/agent-default-env-draft.test.ts": 31, + "src/renderer/src/components/settings/agent-launch-settings-transport.test.ts": 43, + "src/renderer/src/components/settings/appearance-interface-summary.test.ts": 302, + "src/renderer/src/components/settings/appearance-search.test.ts": 1011, + "src/renderer/src/components/settings/appearance-status-bar-search.test.ts": 52, + "src/renderer/src/components/settings/appearance-usage-percentage-search.test.ts": 57, + "src/renderer/src/components/settings/bitbucket-integration-card.test.tsx": 384, + "src/renderer/src/components/settings/browser-link-routing-localization.test.ts": 268, + "src/renderer/src/components/settings/browser-search.test.ts": 298, + "src/renderer/src/components/settings/browser-session-host-selection.test.ts": 40, + "src/renderer/src/components/settings/chat-naming-persistence.test.tsx": 2985, + "src/renderer/src/components/settings/chat-settings-unsaved-guard.test.tsx": 1585, + "src/renderer/src/components/settings/cli-install-failure.test.ts": 36, + "src/renderer/src/components/settings/cli-source-control-integration-cards.test.tsx": 343, + "src/renderer/src/components/settings/codex-account-auth-warning.test.ts": 30, + "src/renderer/src/components/settings/codex-config-sync-warning.test.ts": 39, + "src/renderer/src/components/settings/codex-session-source-home-control.test.tsx": 132, + "src/renderer/src/components/settings/developer-permissions-search.test.ts": 307, + "src/renderer/src/components/settings/general-editor-search.test.ts": 104, + "src/renderer/src/components/settings/general-preview-tab-search.test.ts": 95, + "src/renderer/src/components/settings/git-description-templates.test.tsx": 2060, + "src/renderer/src/components/settings/host-scoped-setting-scope.test.ts": 35, + "src/renderer/src/components/settings/integrations-pane-status.test.ts": 33, + "src/renderer/src/components/settings/jira-integration-card.test.tsx": 493, + "src/renderer/src/components/settings/linear-agent-skill-install-cta.test.tsx": 1610, + "src/renderer/src/components/settings/managed-server-copy.test.ts": 62, + "src/renderer/src/components/settings/mobile-network-interface-selection.test.ts": 56, + "src/renderer/src/components/settings/mobile-pairing-device-polling.test.ts": 38, + "src/renderer/src/components/settings/mobile-pane-search.test.ts": 80, + "src/renderer/src/components/settings/native-chat-experimental-search-entry.test.ts": 75, + "src/renderer/src/components/settings/notification-host-pending-save.test.tsx": 1629, + "src/renderer/src/components/settings/plugin-install-source.test.ts": 227, + "src/renderer/src/components/settings/provider-account-scope.test.ts": 75, + "src/renderer/src/components/settings/provider-account-visibility.test.ts": 44, + "src/renderer/src/components/settings/provider-rate-limit-scope-panels.test.tsx": 320, + "src/renderer/src/components/settings/repository-description-templates.test.tsx": 783, + "src/renderer/src/components/settings/repository-hook-settings-draft.test.ts": 409, + "src/renderer/src/components/settings/repository-host-setup-options.test.ts": 268, + "src/renderer/src/components/settings/repository-icon-github.test.ts": 49, + "src/renderer/src/components/settings/repository-source-control-ai-global-ux.test.ts": 389, + "src/renderer/src/components/settings/repository-source-control-ai-persist-queue.test.ts": 86, + "src/renderer/src/components/settings/runtime-server-row-unverifiable-host.test.tsx": 1776, + "src/renderer/src/components/settings/runtime-server-workflow-sections.test.tsx": 300, + "src/renderer/src/components/settings/session-history-status-copy.test.ts": 243, + "src/renderer/src/components/settings/session-search-computer-rollup.test.ts": 38, + "src/renderer/src/components/settings/settings-deep-link-target-watcher.test.ts": 210, + "src/renderer/src/components/settings/settings-form-option-filter.test.ts": 46, + "src/renderer/src/components/settings/settings-host-scope.test.ts": 38, + "src/renderer/src/components/settings/settings-project-checkout-target.test.ts": 55, + "src/renderer/src/components/settings/settings-project-list.test.ts": 72, + "src/renderer/src/components/settings/settings-project-regrouping.test.ts": 500, + "src/renderer/src/components/settings/settings-search-keywords.test.ts": 106, + "src/renderer/src/components/settings/settings-search.test.ts": 37, + "src/renderer/src/components/settings/settings-setup-guide-progress-hook.test.tsx": 47, + "src/renderer/src/components/settings/settings-setup-guide-progress.test.ts": 1451, + "src/renderer/src/components/settings/shortcut-binding-list-mutations.test.ts": 42, + "src/renderer/src/components/settings/shortcut-definition-catalog.test.ts": 361, + "src/renderer/src/components/settings/shortcut-groups.test.ts": 84, + "src/renderer/src/components/settings/shortcut-recording-state.test.ts": 37, + "src/renderer/src/components/settings/shortcut-row-visibility.test.ts": 150, + "src/renderer/src/components/settings/source-control-ai-settings-persistence.test.tsx": 3088, + "src/renderer/src/components/settings/sparse-preset-date.test.ts": 42, + "src/renderer/src/components/settings/sparse-preset-operation-error.test.ts": 35, + "src/renderer/src/components/settings/ssh-target-action-state.test.ts": 38, + "src/renderer/src/components/settings/ssh-target-draft.test.ts": 40, + "src/renderer/src/components/settings/ssh-target-save-payload.test.ts": 68, + "src/renderer/src/components/settings/task-source-setup-state.test.ts": 50, + "src/renderer/src/components/settings/task-tracker-integration-cards.test.tsx": 918, + "src/renderer/src/components/settings/terminal-preview-content.test.ts": 26, + "src/renderer/src/components/settings/terminal-search.test.ts": 282, + "src/renderer/src/components/settings/use-debounced-settings-text-draft.test.ts": 287, + "src/renderer/src/components/settings/use-mac-captured-digit-chords.test.ts": 666, + "src/renderer/src/components/settings/use-published-machine-name.test.tsx": 232, + "src/renderer/src/components/settings/use-repository-hook-settings-draft.test.tsx": 483, + "src/renderer/src/components/settings/use-runtime-environment-catalog.test.tsx": 1805, + "src/renderer/src/components/settings/use-session-search-status.test.tsx": 522, + "src/renderer/src/components/settings/use-task-source-provider-readiness.test.tsx": 218, + "src/renderer/src/components/settings/use-zcode-plan-credentials.test.tsx": 1054, + "src/renderer/src/components/settings/useGhosttyImport.test.ts": 60, + "src/renderer/src/components/settings/useWarpThemeImport.test.ts": 115, + "src/renderer/src/components/settings/worktree-symlink-path-filter.scan-budget.test.ts": 68, + "src/renderer/src/components/settings/worktree-symlink-path-filter.test.ts": 50, + "src/renderer/src/components/setup-guide/SetupGuideModal.mount-gating.test.tsx": 1815, + "src/renderer/src/components/setup-guide/use-setup-guide-progress.test.ts": 1323, + "src/renderer/src/components/setup-guide/use-setup-guide-telemetry.test.ts": 56, + "src/renderer/src/components/shared/MacFolderAccessFixDialog.test.tsx": 1298, + "src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts": 1354, + "src/renderer/src/components/shared/useDaemonActions.test.tsx": 434, + "src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx": 153, + "src/renderer/src/components/sidebar/AddRemoteHostDialog.config-picker.test.tsx": 1286, + "src/renderer/src/components/sidebar/AddRemoteHostFields.test.tsx": 376, + "src/renderer/src/components/sidebar/AddRepoCreateStep.test.tsx": 478, + "src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts": 34, + "src/renderer/src/components/sidebar/AddRepoDialogChrome.test.tsx": 652, + "src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx": 1847, + "src/renderer/src/components/sidebar/AddRepoHostSelector.test.tsx": 324, + "src/renderer/src/components/sidebar/AddRepoNestedImportStep.test.tsx": 473, + "src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx": 814, + "src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts": 147, + "src/renderer/src/components/sidebar/AddWslHostFormPanel.test.tsx": 196, + "src/renderer/src/components/sidebar/AgentDashboardSidebarHost.test.tsx": 1463, + "src/renderer/src/components/sidebar/AutoRenameFailedDialog.test.tsx": 498, + "src/renderer/src/components/sidebar/CacheTimer.test.tsx": 136, + "src/renderer/src/components/sidebar/CommentMarkdown.code-block-renderer.test.tsx": 370, + "src/renderer/src/components/sidebar/CommentMarkdown.file-link-cases.test.tsx": 1548, + "src/renderer/src/components/sidebar/CommentMarkdown.github-attachment-image.test.tsx": 1588, + "src/renderer/src/components/sidebar/CommentMarkdown.github-callout.test.tsx": 1895, + "src/renderer/src/components/sidebar/CommentMarkdown.growing.test.tsx": 1964, + "src/renderer/src/components/sidebar/CommentMarkdown.jira-links.test.tsx": 1557, + "src/renderer/src/components/sidebar/CommentMarkdown.link-click.test.tsx": 1746, + "src/renderer/src/components/sidebar/CommentMarkdown.ordered-list.test.tsx": 2451, + "src/renderer/src/components/sidebar/CommentMarkdown.test.tsx": 1526, + "src/renderer/src/components/sidebar/CommentMarkdown.word-fade.test.tsx": 1644, + "src/renderer/src/components/sidebar/CommentMermaidBlock.system-theme.test.tsx": 212, + "src/renderer/src/components/sidebar/DeleteNestedWorktreesDialog.test.tsx": 956, + "src/renderer/src/components/sidebar/DeleteWorktreeDialog.test.tsx": 871, + "src/renderer/src/components/sidebar/DeleteWorktreeDialogFooter.test.tsx": 482, + "src/renderer/src/components/sidebar/DeleteWorktreeTargetPreview.test.tsx": 863, + "src/renderer/src/components/sidebar/FilterToggleRow.test.tsx": 265, + "src/renderer/src/components/sidebar/ImportedWorktreesVisibilityLine.test.tsx": 1372, + "src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.reminder-toast.test.tsx": 2023, + "src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.test.tsx": 3036, + "src/renderer/src/components/sidebar/LocalGitToolchainScanBanner.test.tsx": 1649, + "src/renderer/src/components/sidebar/MarkdownImageLightbox.test.tsx": 630, + "src/renderer/src/components/sidebar/NewExternalWorktreesInboxLine.test.tsx": 1486, + "src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx": 1043, + "src/renderer/src/components/sidebar/NoticeHostGlyph.test.tsx": 903, + "src/renderer/src/components/sidebar/OrcaYamlTrustDialog.test.tsx": 76, + "src/renderer/src/components/sidebar/ProjectAddedDialog.test.tsx": 77, + "src/renderer/src/components/sidebar/ProjectGroupDeleteDialog.test.tsx": 528, + "src/renderer/src/components/sidebar/ProjectHeaderActions.test.tsx": 184, + "src/renderer/src/components/sidebar/RemoteFileBrowser.paste.test.tsx": 721, + "src/renderer/src/components/sidebar/RemoveFolderDialog.test.tsx": 433, + "src/renderer/src/components/sidebar/SetupGuideSidebarEntry.test.tsx": 288, + "src/renderer/src/components/sidebar/SetupScriptPromptCard.test.ts": 38, + "src/renderer/src/components/sidebar/SetupScriptPromptCardShell.test.tsx": 334, + "src/renderer/src/components/sidebar/SeveredTerminalDaemonBanner.test.tsx": 462, + "src/renderer/src/components/sidebar/Sidebar.test.tsx": 736, + "src/renderer/src/components/sidebar/SidebarAgentsList.test.tsx": 1978, + "src/renderer/src/components/sidebar/SidebarFeedbackDialog.test.tsx": 3348, + "src/renderer/src/components/sidebar/SidebarGroupByToggle.test.tsx": 508, + "src/renderer/src/components/sidebar/SidebarHeader.test.tsx": 467, + "src/renderer/src/components/sidebar/SidebarNav.test.tsx": 1674, + "src/renderer/src/components/sidebar/SidebarRepositoryFilterSection.test.tsx": 837, + "src/renderer/src/components/sidebar/SidebarSettingsHelpMenu.test.tsx": 497, + "src/renderer/src/components/sidebar/SidebarToolbar.test.tsx": 396, + "src/renderer/src/components/sidebar/SidebarWorkspaceFilterSection.test.tsx": 419, + "src/renderer/src/components/sidebar/StatusIndicator.test.ts": 171, + "src/renderer/src/components/sidebar/WorkspaceAttachmentOrigins.test.tsx": 552, + "src/renderer/src/components/sidebar/WorkspaceAttachmentSearch.resolution.test.tsx": 2578, + "src/renderer/src/components/sidebar/WorkspaceKanbanCard.host-identity.test.tsx": 320, + "src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.mount-gating.test.tsx": 2063, + "src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.search.test.tsx": 1818, + "src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.task-status-sync.test.tsx": 1917, + "src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.test.tsx": 1575, + "src/renderer/src/components/sidebar/WorkspaceKanbanLaneCardList.test.tsx": 301, + "src/renderer/src/components/sidebar/WorkspaceKanbanLaneGrid.test.tsx": 391, + "src/renderer/src/components/sidebar/WorkspaceKanbanSearchField.test.tsx": 363, + "src/renderer/src/components/sidebar/WorkspaceKanbanSettingsMenu.test.tsx": 844, + "src/renderer/src/components/sidebar/WorkspaceKanbanStatusLane.test.tsx": 374, + "src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx": 2039, + "src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx": 2328, + "src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx": 1608, + "src/renderer/src/components/sidebar/WorktreeCard.delete-failed.test.tsx": 936, + "src/renderer/src/components/sidebar/WorktreeCard.host-badge-toggle.test.tsx": 998, + "src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx": 1720, + "src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx": 1440, + "src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx": 1732, + "src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx": 1170, + "src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx": 1193, + "src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx": 1534, + "src/renderer/src/components/sidebar/WorktreeCard.single-reference-details.test.tsx": 1519, + "src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx": 863, + "src/renderer/src/components/sidebar/WorktreeCard.test.ts": 31, + "src/renderer/src/components/sidebar/WorktreeCardAgents.acknowledgement-emphasis.test.tsx": 1109, + "src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx": 885, + "src/renderer/src/components/sidebar/WorktreeCardAgents.expansion-remount.test.tsx": 1342, + "src/renderer/src/components/sidebar/WorktreeCardAgents.send-target.test.tsx": 1272, + "src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx": 845, + "src/renderer/src/components/sidebar/WorktreeCardAutomationDetailSection.test.tsx": 558, + "src/renderer/src/components/sidebar/WorktreeCardDisplayMenuSection.test.tsx": 402, + "src/renderer/src/components/sidebar/WorktreeCardLinkedItemsSection.test.tsx": 2028, + "src/renderer/src/components/sidebar/WorktreeCardMeta.interaction.test.tsx": 2591, + "src/renderer/src/components/sidebar/WorktreeCardMeta.test.tsx": 2152, + "src/renderer/src/components/sidebar/WorktreeCardPorts.test.tsx": 1152, + "src/renderer/src/components/sidebar/WorktreeCardSshHostControl.test.tsx": 1797, + "src/renderer/src/components/sidebar/WorktreeCardStatusSlot.test.tsx": 264, + "src/renderer/src/components/sidebar/WorktreeContextMenu.copy-worktree-name.test.tsx": 1696, + "src/renderer/src/components/sidebar/WorktreeContextMenu.delete-shortcut.test.tsx": 1315, + "src/renderer/src/components/sidebar/WorktreeContextMenu.react185-bystander.test.tsx": 2115, + "src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts": 122, + "src/renderer/src/components/sidebar/WorktreeDeveloperMenu.test.tsx": 75, + "src/renderer/src/components/sidebar/WorktreeDeveloperMenuReveal.test.tsx": 646, + "src/renderer/src/components/sidebar/WorktreeList.card-memo-stability.test.tsx": 2150, + "src/renderer/src/components/sidebar/WorktreeList.empty-project-rows.test.ts": 1561, + "src/renderer/src/components/sidebar/WorktreeList.folder-workspace-rows.test.ts": 1830, + "src/renderer/src/components/sidebar/WorktreeList.group-headers.test.ts": 1966, + "src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx": 3218, + "src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts": 3256, + "src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx": 2091, + "src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx": 2729, + "src/renderer/src/components/sidebar/WorktreeMetaDialog.host-scope.test.tsx": 1889, + "src/renderer/src/components/sidebar/WorktreeMetaDialog.test.tsx": 5615, + "src/renderer/src/components/sidebar/WorktreeOpenInMenu.test.tsx": 199, + "src/renderer/src/components/sidebar/WorktreeParentPickerPopover.test.ts": 1699, + "src/renderer/src/components/sidebar/WorktreeReferenceStack.test.tsx": 392, + "src/renderer/src/components/sidebar/WorktreeTitleInlineRename.begin-editing.test.tsx": 95, + "src/renderer/src/components/sidebar/WorktreeTitleInlineRename.editor-lifecycle.test.tsx": 387, + "src/renderer/src/components/sidebar/WorktreeTitleInlineRename.test.tsx": 177, + "src/renderer/src/components/sidebar/WorktreeVisibilityDialog.test.tsx": 2116, + "src/renderer/src/components/sidebar/WorktreeVisibilityHelpPopover.test.tsx": 425, + "src/renderer/src/components/sidebar/WorktreeVisibilitySourceList.test.tsx": 401, + "src/renderer/src/components/sidebar/active-worktree-focus-after-delete.test.ts": 67, + "src/renderer/src/components/sidebar/add-remote-host-ssh-actions.test.ts": 110, + "src/renderer/src/components/sidebar/add-repo-browse-authority.test.ts": 34, + "src/renderer/src/components/sidebar/add-repo-existing-workspaces-telemetry.test.ts": 39, + "src/renderer/src/components/sidebar/add-repo-skip-finalization.test.ts": 50, + "src/renderer/src/components/sidebar/add-repo-wsl-host-folder-picker.test.ts": 80, + "src/renderer/src/components/sidebar/agent-send-picker-collapse.test.tsx": 1453, + "src/renderer/src/components/sidebar/child-workspaces-toggle-target.test.ts": 1788, + "src/renderer/src/components/sidebar/child-workspaces-toggle-visibility.test.ts": 2237, + "src/renderer/src/components/sidebar/clone-defaults.test.ts": 42, + "src/renderer/src/components/sidebar/create-project-defaults.test.ts": 39, + "src/renderer/src/components/sidebar/default-branch-folder-project-root.test.ts": 1259, + "src/renderer/src/components/sidebar/default-branch-visible-under-hide-sleeping.test.ts": 1375, + "src/renderer/src/components/sidebar/delete-worktree-always-force-preference.test.tsx": 2230, + "src/renderer/src/components/sidebar/delete-worktree-dirty-change-counts.test.ts": 71, + "src/renderer/src/components/sidebar/delete-worktree-dirty-change-hydration.test.tsx": 845, + "src/renderer/src/components/sidebar/delete-worktree-failure-toast.test.tsx": 1579, + "src/renderer/src/components/sidebar/delete-worktree-flow.test.ts": 769, + "src/renderer/src/components/sidebar/delete-worktree-parallel-flow.test.ts": 439, + "src/renderer/src/components/sidebar/delete-worktree-toast.test.ts": 77, + "src/renderer/src/components/sidebar/empty-project-placeholder-repos.test.ts": 32, + "src/renderer/src/components/sidebar/focused-agent-row-highlight.test.ts": 1233, + "src/renderer/src/components/sidebar/folder-workspace-card-pr-display.test.ts": 92, + "src/renderer/src/components/sidebar/folder-workspace-composer-expiry.test.tsx": 2002, + "src/renderer/src/components/sidebar/folder-workspace-composer-helpers.test.ts": 94, + "src/renderer/src/components/sidebar/folder-workspace-composer-path-status.test.tsx": 1490, + "src/renderer/src/components/sidebar/folder-workspace-composer-submit-host-platform.test.ts": 2067, + "src/renderer/src/components/sidebar/folder-workspace-composer-submit.test.ts": 2132, + "src/renderer/src/components/sidebar/folder-workspace-linked-startup-plan.test.ts": 1757, + "src/renderer/src/components/sidebar/host-header-drag.test.tsx": 260, + "src/renderer/src/components/sidebar/host-header-menu-items.test.ts": 31, + "src/renderer/src/components/sidebar/host-rename-remove.test.ts": 30, + "src/renderer/src/components/sidebar/host-section-collapse-folders.test.ts": 187, + "src/renderer/src/components/sidebar/host-section-collapse.test.ts": 146, + "src/renderer/src/components/sidebar/host-section-folder-workspace-counts.test.ts": 35, + "src/renderer/src/components/sidebar/host-section-order.test.ts": 25, + "src/renderer/src/components/sidebar/host-section-reveal.test.tsx": 1645, + "src/renderer/src/components/sidebar/host-section-rows.test.ts": 108, + "src/renderer/src/components/sidebar/hovered-workspace-delete.test.ts": 1636, + "src/renderer/src/components/sidebar/imported-worktrees-card-actions.test.ts": 52, + "src/renderer/src/components/sidebar/imported-worktrees-card-candidates.test.ts": 77, + "src/renderer/src/components/sidebar/kanban-empty-selection-prune-budget.test.tsx": 996, + "src/renderer/src/components/sidebar/linear-agent-skill-runtime.shell-override.test.ts": 60, + "src/renderer/src/components/sidebar/linear-agent-skill-setup-reminders.test.ts": 49, "src/renderer/src/components/sidebar/local-base-ref-suggestion-toast.test.tsx": 246, - "src/renderer/src/components/sidebar/manual-sort-switch-toast.test.ts": 1537, - "src/renderer/src/components/sidebar/markdown-top-level-blocks.test.ts": 466, - "src/renderer/src/components/sidebar/mobile-sidebar-onboarding-badge.test.ts": 234, - "src/renderer/src/components/sidebar/natural-worktree-ids.test.ts": 136, - "src/renderer/src/components/sidebar/new-external-worktrees-inbox-actions.test.ts": 105, - "src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.test.ts": 58, - "src/renderer/src/components/sidebar/pinned-section-worktrees.test.ts": 460, - "src/renderer/src/components/sidebar/preserved-branch-batch-toast.test.tsx": 524, - "src/renderer/src/components/sidebar/preserved-branch-toast.test.tsx": 357, - "src/renderer/src/components/sidebar/project-added-default-checkout.test.ts": 92, - "src/renderer/src/components/sidebar/project-filter-reveal.test.ts": 47, - "src/renderer/src/components/sidebar/project-group-header-drag-commit.test.ts": 201, - "src/renderer/src/components/sidebar/project-group-header-drag-start.test.ts": 155, - "src/renderer/src/components/sidebar/project-group-header-drop.test.ts": 136, - "src/renderer/src/components/sidebar/project-header-action-selector-lockstep.test.ts": 172, - "src/renderer/src/components/sidebar/project-header-color.test.ts": 281, - "src/renderer/src/components/sidebar/project-header-drag-commit.test.ts": 154, - "src/renderer/src/components/sidebar/project-header-drag-start.test.ts": 159, - "src/renderer/src/components/sidebar/project-header-drag.test.ts": 201, - "src/renderer/src/components/sidebar/project-header-drop.test.ts": 169, - "src/renderer/src/components/sidebar/project-order-manual-default-notice-visibility.test.ts": 53, - "src/renderer/src/components/sidebar/prompt-cache-countdown-clock.test.ts": 51, - "src/renderer/src/components/sidebar/prompt-cache-timer-selection.test.ts": 54, - "src/renderer/src/components/sidebar/remote-file-browser-drive-paths.test.ts": 56, - "src/renderer/src/components/sidebar/remote-file-browser-helpers.test.ts": 143, - "src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.test.ts": 1479, - "src/renderer/src/components/sidebar/repo-header-create-state.test.ts": 86, - "src/renderer/src/components/sidebar/sidebar-empty-state-gate.test.ts": 53, - "src/renderer/src/components/sidebar/sidebar-filter-state.test.ts": 1362, - "src/renderer/src/components/sidebar/sidebar-host-options.test.ts": 119, - "src/renderer/src/components/sidebar/sidebar-lineage-ancestors.test.ts": 220, - "src/renderer/src/components/sidebar/sidebar-project-drop.test.ts": 93, - "src/renderer/src/components/sidebar/sidebar-workspace-option-items.test.ts": 386, - "src/renderer/src/components/sidebar/sleep-worktree-activation-race.test.ts": 112, - "src/renderer/src/components/sidebar/sleep-worktree-flow.test.ts": 129, - "src/renderer/src/components/sidebar/smart-attention-host-observed-end.test.ts": 93, - "src/renderer/src/components/sidebar/smart-attention.test.ts": 136, - "src/renderer/src/components/sidebar/smart-sort.test.ts": 158, - "src/renderer/src/components/sidebar/ssh-host-remove-resolution.test.ts": 54, - "src/renderer/src/components/sidebar/ssh-target-duplicate.test.ts": 60, - "src/renderer/src/components/sidebar/ssh-workspace-forget-resolution.test.ts": 51, - "src/renderer/src/components/sidebar/stale-agent-row-unverifiable.test.ts": 1936, - "src/renderer/src/components/sidebar/subagent-child-row-fallback-pixels.test.tsx": 650, - "src/renderer/src/components/sidebar/truncated-sidebar-label.test.tsx": 200, - "src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts": 318, - "src/renderer/src/components/sidebar/use-add-repo-hosted-controller.test.ts": 68, - "src/renderer/src/components/sidebar/use-feedback-image-drop.test.tsx": 330, - "src/renderer/src/components/sidebar/use-sidebar-feedback-images.test.tsx": 334, - "src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.test.tsx": 1548, - "src/renderer/src/components/sidebar/use-workspace-kanban-card-pointer-drag.test.ts": 76, - "src/renderer/src/components/sidebar/use-workspace-kanban-drawer-lingering.test.tsx": 257, - "src/renderer/src/components/sidebar/use-workspace-kanban-outside-dismiss.test.ts": 64, - "src/renderer/src/components/sidebar/use-workspace-kanban-selection.test.tsx": 210, - "src/renderer/src/components/sidebar/use-workspace-reference-details.test.tsx": 1076, - "src/renderer/src/components/sidebar/use-workspace-reveal-body-redirect.test.tsx": 194, - "src/renderer/src/components/sidebar/use-workspace-status-drop.test.ts": 140, - "src/renderer/src/components/sidebar/use-worktree-activity-status.test.tsx": 185, - "src/renderer/src/components/sidebar/use-worktree-activity-statuses.test.ts": 1394, - "src/renderer/src/components/sidebar/use-worktree-card-secondary-details.store-subscriptions.test.tsx": 2686, - "src/renderer/src/components/sidebar/use-worktree-sleep-state.test.tsx": 148, - "src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts": 141, - "src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts": 169, - "src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts": 139, - "src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts": 73, - "src/renderer/src/components/sidebar/useCreateProjectDefaults.test.ts": 116, - "src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts": 139, - "src/renderer/src/components/sidebar/useRenderedSetupScriptPromptState.test.ts": 253, - "src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.test.tsx": 1633, - "src/renderer/src/components/sidebar/useSidebarProjectDrop.os-file-drop.test.tsx": 647, - "src/renderer/src/components/sidebar/useWorkspaceBoardPanel.test.tsx": 245, - "src/renderer/src/components/sidebar/useWorktreeAgentRows.test.ts": 176, - "src/renderer/src/components/sidebar/visible-worktree-activity-inputs.test.ts": 58, - "src/renderer/src/components/sidebar/visible-worktree-indexes.test.ts": 1791, - "src/renderer/src/components/sidebar/visible-worktrees-manual-lineage-order.test.ts": 1627, - "src/renderer/src/components/sidebar/visible-worktrees.test.ts": 1757, - "src/renderer/src/components/sidebar/workspace-attachment-source-label.test.ts": 80, - "src/renderer/src/components/sidebar/workspace-attachment-source-result.test.ts": 1721, - "src/renderer/src/components/sidebar/workspace-board-task-status-sync.test.ts": 127, - "src/renderer/src/components/sidebar/workspace-creator-visibility.test.ts": 84, - "src/renderer/src/components/sidebar/workspace-delete-lineage.test.ts": 52, - "src/renderer/src/components/sidebar/workspace-hover-attachments.test.ts": 119, - "src/renderer/src/components/sidebar/workspace-kanban-area-selection.test.ts": 181, - "src/renderer/src/components/sidebar/workspace-kanban-card-pointer-drag-dom.test.ts": 57, - "src/renderer/src/components/sidebar/workspace-kanban-filtered-drop-index.test.ts": 51, - "src/renderer/src/components/sidebar/workspace-kanban-search.test.ts": 153, - "src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.test.ts": 172, - "src/renderer/src/components/sidebar/workspace-kanban-virtual-lane-layout.test.ts": 64, - "src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.test.ts": 164, - "src/renderer/src/components/sidebar/workspace-lineage-menu-actions.test.ts": 59, - "src/renderer/src/components/sidebar/workspace-reference-detail-read.test.ts": 310, - "src/renderer/src/components/sidebar/workspace-reference-refresh-targets.test.ts": 582, - "src/renderer/src/components/sidebar/workspace-status.test.ts": 151, - "src/renderer/src/components/sidebar/worktree-agent-activity-summary.test.ts": 165, - "src/renderer/src/components/sidebar/worktree-agent-freshness-selector.test.ts": 126, - "src/renderer/src/components/sidebar/worktree-agent-host-attribution.test.ts": 64, - "src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts": 79, - "src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts": 120, - "src/renderer/src/components/sidebar/worktree-agent-remote-attribution.test.ts": 953, - "src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts": 82, + "src/renderer/src/components/sidebar/manual-sort-switch-toast.test.ts": 1416, + "src/renderer/src/components/sidebar/markdown-top-level-blocks.test.ts": 716, + "src/renderer/src/components/sidebar/mobile-sidebar-onboarding-badge.test.ts": 200, + "src/renderer/src/components/sidebar/natural-worktree-ids.test.ts": 90, + "src/renderer/src/components/sidebar/new-external-worktrees-inbox-actions.test.ts": 74, + "src/renderer/src/components/sidebar/new-external-worktrees-inbox-candidates.test.ts": 44, + "src/renderer/src/components/sidebar/pinned-section-worktrees.test.ts": 408, + "src/renderer/src/components/sidebar/preserved-branch-batch-toast.test.tsx": 514, + "src/renderer/src/components/sidebar/preserved-branch-toast.test.tsx": 252, + "src/renderer/src/components/sidebar/project-added-default-checkout.test.ts": 82, + "src/renderer/src/components/sidebar/project-filter-reveal.test.ts": 26, + "src/renderer/src/components/sidebar/project-group-header-drag-commit.test.ts": 162, + "src/renderer/src/components/sidebar/project-group-header-drag-start.test.ts": 171, + "src/renderer/src/components/sidebar/project-group-header-drop.test.ts": 177, + "src/renderer/src/components/sidebar/project-header-action-selector-lockstep.test.ts": 159, + "src/renderer/src/components/sidebar/project-header-color.test.ts": 235, + "src/renderer/src/components/sidebar/project-header-drag-commit.test.ts": 186, + "src/renderer/src/components/sidebar/project-header-drag-start.test.ts": 181, + "src/renderer/src/components/sidebar/project-header-drag.test.ts": 184, + "src/renderer/src/components/sidebar/project-header-drop.test.ts": 176, + "src/renderer/src/components/sidebar/project-order-manual-default-notice-visibility.test.ts": 32, + "src/renderer/src/components/sidebar/prompt-cache-countdown-clock.test.ts": 37, + "src/renderer/src/components/sidebar/prompt-cache-timer-selection.test.ts": 46, + "src/renderer/src/components/sidebar/remote-file-browser-drive-paths.test.ts": 39, + "src/renderer/src/components/sidebar/remote-file-browser-helpers.test.ts": 137, + "src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.test.ts": 1450, + "src/renderer/src/components/sidebar/repo-header-create-state.test.ts": 90, + "src/renderer/src/components/sidebar/sidebar-empty-state-gate.test.ts": 40, + "src/renderer/src/components/sidebar/sidebar-filter-state.test.ts": 1409, + "src/renderer/src/components/sidebar/sidebar-host-options.test.ts": 334, + "src/renderer/src/components/sidebar/sidebar-lineage-ancestors.test.ts": 143, + "src/renderer/src/components/sidebar/sidebar-project-drop.test.ts": 88, + "src/renderer/src/components/sidebar/sidebar-workspace-option-items.test.ts": 303, + "src/renderer/src/components/sidebar/sleep-worktree-activation-race.test.ts": 92, + "src/renderer/src/components/sidebar/sleep-worktree-flow.test.ts": 102, + "src/renderer/src/components/sidebar/smart-attention-host-observed-end.test.ts": 61, + "src/renderer/src/components/sidebar/smart-attention.test.ts": 145, + "src/renderer/src/components/sidebar/smart-sort.test.ts": 150, + "src/renderer/src/components/sidebar/ssh-host-remove-resolution.test.ts": 41, + "src/renderer/src/components/sidebar/ssh-target-duplicate.test.ts": 34, + "src/renderer/src/components/sidebar/ssh-workspace-forget-resolution.test.ts": 41, + "src/renderer/src/components/sidebar/stale-agent-row-unverifiable.test.ts": 1311, + "src/renderer/src/components/sidebar/subagent-child-row-fallback-pixels.test.tsx": 496, + "src/renderer/src/components/sidebar/truncated-sidebar-label.test.tsx": 231, + "src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts": 308, + "src/renderer/src/components/sidebar/use-add-repo-hosted-controller.test.ts": 41, + "src/renderer/src/components/sidebar/use-add-repo-wsl-host-paths.test.tsx": 388, + "src/renderer/src/components/sidebar/use-feedback-image-drop.test.tsx": 419, + "src/renderer/src/components/sidebar/use-remote-file-browser-listing.test.ts": 200, + "src/renderer/src/components/sidebar/use-sidebar-feedback-images.test.tsx": 283, + "src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.test.tsx": 1676, + "src/renderer/src/components/sidebar/use-workspace-kanban-card-pointer-drag.test.ts": 48, + "src/renderer/src/components/sidebar/use-workspace-kanban-drawer-lingering.test.tsx": 226, + "src/renderer/src/components/sidebar/use-workspace-kanban-outside-dismiss.test.ts": 36, + "src/renderer/src/components/sidebar/use-workspace-kanban-selection.test.tsx": 219, + "src/renderer/src/components/sidebar/use-workspace-reference-details.test.tsx": 983, + "src/renderer/src/components/sidebar/use-workspace-reveal-body-redirect.test.tsx": 176, + "src/renderer/src/components/sidebar/use-workspace-status-drop.test.ts": 94, + "src/renderer/src/components/sidebar/use-worktree-activity-status.test.tsx": 136, + "src/renderer/src/components/sidebar/use-worktree-activity-statuses.test.ts": 1381, + "src/renderer/src/components/sidebar/use-worktree-card-secondary-details.store-subscriptions.test.tsx": 2311, + "src/renderer/src/components/sidebar/use-worktree-sleep-state.test.tsx": 99, + "src/renderer/src/components/sidebar/use-wsl-distro-list.test.ts": 510, + "src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts": 113, + "src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts": 194, + "src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts": 134, + "src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts": 85, + "src/renderer/src/components/sidebar/useCreateProjectDefaults.test.ts": 80, + "src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts": 102, + "src/renderer/src/components/sidebar/useRenderedSetupScriptPromptState.test.ts": 213, + "src/renderer/src/components/sidebar/useSetupScriptPromptRevalidation.test.tsx": 1570, + "src/renderer/src/components/sidebar/useSidebarProjectDrop.os-file-drop.test.tsx": 834, + "src/renderer/src/components/sidebar/useWorkspaceBoardPanel.test.tsx": 248, + "src/renderer/src/components/sidebar/useWorktreeAgentRows.test.ts": 180, + "src/renderer/src/components/sidebar/visible-worktree-activity-inputs.test.ts": 55, + "src/renderer/src/components/sidebar/visible-worktree-indexes.test.ts": 1283, + "src/renderer/src/components/sidebar/visible-worktrees-manual-lineage-order.test.ts": 1378, + "src/renderer/src/components/sidebar/visible-worktrees.test.ts": 1260, + "src/renderer/src/components/sidebar/workspace-attachment-source-label.test.ts": 76, + "src/renderer/src/components/sidebar/workspace-attachment-source-result.test.ts": 1483, + "src/renderer/src/components/sidebar/workspace-board-task-status-sync.test.ts": 243, + "src/renderer/src/components/sidebar/workspace-creator-visibility.test.ts": 69, + "src/renderer/src/components/sidebar/workspace-delete-lineage.test.ts": 46, + "src/renderer/src/components/sidebar/workspace-hover-attachments.test.ts": 106, + "src/renderer/src/components/sidebar/workspace-kanban-area-selection.test.ts": 186, + "src/renderer/src/components/sidebar/workspace-kanban-card-pointer-drag-dom.test.ts": 44, + "src/renderer/src/components/sidebar/workspace-kanban-filtered-drop-index.test.ts": 40, + "src/renderer/src/components/sidebar/workspace-kanban-search.test.ts": 162, + "src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.test.ts": 195, + "src/renderer/src/components/sidebar/workspace-kanban-virtual-lane-layout.test.ts": 55, + "src/renderer/src/components/sidebar/workspace-kanban-worktree-groups.test.ts": 105, + "src/renderer/src/components/sidebar/workspace-lineage-menu-actions.test.ts": 49, + "src/renderer/src/components/sidebar/workspace-reference-detail-read.test.ts": 327, + "src/renderer/src/components/sidebar/workspace-reference-refresh-targets.test.ts": 635, + "src/renderer/src/components/sidebar/workspace-status.test.ts": 108, + "src/renderer/src/components/sidebar/worktree-agent-activity-summary.test.ts": 157, + "src/renderer/src/components/sidebar/worktree-agent-freshness-selector.test.ts": 102, + "src/renderer/src/components/sidebar/worktree-agent-host-attribution.test.ts": 45, + "src/renderer/src/components/sidebar/worktree-agent-orchestration-batch.test.ts": 82, + "src/renderer/src/components/sidebar/worktree-agent-orchestration-index.test.ts": 121, + "src/renderer/src/components/sidebar/worktree-agent-remote-attribution.test.ts": 862, + "src/renderer/src/components/sidebar/worktree-agent-row-selectors.test.ts": 65, "src/renderer/src/components/sidebar/worktree-attachment-editing.test.ts": 83, - "src/renderer/src/components/sidebar/worktree-card-agent-ack-inputs.test.tsx": 204, - "src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts": 1707, - "src/renderer/src/components/sidebar/worktree-card-compact-agent-row-stopping.test.ts": 1763, - "src/renderer/src/components/sidebar/worktree-card-compact-agent-row.stable-message.test.tsx": 520, - "src/renderer/src/components/sidebar/worktree-card-details-hover-state.test.tsx": 172, - "src/renderer/src/components/sidebar/worktree-card-dom-events.test.ts": 158, - "src/renderer/src/components/sidebar/worktree-card-jira-issue-display.test.ts": 45, - "src/renderer/src/components/sidebar/worktree-card-pr-display.test.ts": 65, - "src/renderer/src/components/sidebar/worktree-card-send-target-inputs.test.ts": 65, - "src/renderer/src/components/sidebar/worktree-card-status-inputs.test.ts": 64, - "src/renderer/src/components/sidebar/worktree-card-title-display.test.ts": 47, - "src/renderer/src/components/sidebar/worktree-context-menu-delete-intent.test.ts": 72, - "src/renderer/src/components/sidebar/worktree-delete-host-qualification.test.ts": 1033, - "src/renderer/src/components/sidebar/worktree-delete-position-scaling.test.ts": 306, - "src/renderer/src/components/sidebar/worktree-delete-request.test.ts": 52, - "src/renderer/src/components/sidebar/worktree-drag-units.test.ts": 162, - "src/renderer/src/components/sidebar/worktree-filter-visibility.test.ts": 536, - "src/renderer/src/components/sidebar/worktree-header-section-boundaries.test.ts": 171, - "src/renderer/src/components/sidebar/worktree-keyboard-cycle.test.ts": 182, - "src/renderer/src/components/sidebar/worktree-lineage-drag-drop.test.ts": 179, - "src/renderer/src/components/sidebar/worktree-lineage-expansion.performance.test.ts": 126, - "src/renderer/src/components/sidebar/worktree-lineage-projection.test.ts": 52, - "src/renderer/src/components/sidebar/worktree-lineage-toggle-handler-cache.test.ts": 56, - "src/renderer/src/components/sidebar/worktree-list-groups-host-collision.test.ts": 204, - "src/renderer/src/components/sidebar/worktree-list-groups-host-labels.test.ts": 185, - "src/renderer/src/components/sidebar/worktree-list-groups-imported-worktrees.test.ts": 206, - "src/renderer/src/components/sidebar/worktree-list-groups-lineage-nesting.test.ts": 220, - "src/renderer/src/components/sidebar/worktree-list-groups-nested-project-groups.test.ts": 176, - "src/renderer/src/components/sidebar/worktree-list-groups-notice-host-labels.test.ts": 195, - "src/renderer/src/components/sidebar/worktree-list-groups-pending-creations.test.ts": 184, - "src/renderer/src/components/sidebar/worktree-list-groups-pinned-host-labels.test.ts": 208, - "src/renderer/src/components/sidebar/worktree-list-groups-project-groups.test.ts": 209, - "src/renderer/src/components/sidebar/worktree-list-groups-project-host-setups.test.ts": 216, - "src/renderer/src/components/sidebar/worktree-list-groups-project-order.test.ts": 873, - "src/renderer/src/components/sidebar/worktree-list-groups-section-label-disambiguation.test.ts": 190, - "src/renderer/src/components/sidebar/worktree-list/drag/pointer-flush.test.ts": 420, - "src/renderer/src/components/sidebar/worktree-list/drag/row-state.test.ts": 53, - "src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.folder-workspace-lanes.test.ts": 213, - "src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.test.ts": 224, - "src/renderer/src/components/sidebar/worktree-list/grouping/host-labels.test.ts": 53, - "src/renderer/src/components/sidebar/worktree-list/grouping/imported-rows.test.ts": 1443, - "src/renderer/src/components/sidebar/worktree-list/grouping/recent-project-rank-scan-budget.test.ts": 201, - "src/renderer/src/components/sidebar/worktree-list/listing/host-filtering.test.ts": 58, - "src/renderer/src/components/sidebar/worktree-list/listing/pending-worktree-creation-keys.test.ts": 39, - "src/renderer/src/components/sidebar/worktree-list/listing/review-cache-inputs.test.ts": 50, - "src/renderer/src/components/sidebar/worktree-list/listing/use-sort-order.test.tsx": 1814, - "src/renderer/src/components/sidebar/worktree-list/listing/use-visible-worktrees.test.tsx": 1702, - "src/renderer/src/components/sidebar/worktree-list/navigation/active-descendant-option.test.ts": 181, - "src/renderer/src/components/sidebar/worktree-list/navigation/folder-reveal.test.ts": 181, - "src/renderer/src/components/sidebar/worktree-list/navigation/folder-row-host-identity.test.ts": 170, - "src/renderer/src/components/sidebar/worktree-list/navigation/render-row-lookup.folder-workspace.test.ts": 189, - "src/renderer/src/components/sidebar/worktree-list/navigation/use-keyboard.focus.test.tsx": 517, - "src/renderer/src/components/sidebar/worktree-list/navigation/use-keyboard.host-identity.test.tsx": 754, - "src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx": 528, - "src/renderer/src/components/sidebar/worktree-list/navigation/use-selection-host-collision.test.tsx": 1905, - "src/renderer/src/components/sidebar/worktree-list/rows/FolderPathStatusIndicator.test.tsx": 313, - "src/renderer/src/components/sidebar/worktree-list/rows/RepoScanUnavailableIndicator.test.tsx": 1640, - "src/renderer/src/components/sidebar/worktree-list/rows/indentation.test.ts": 56, - "src/renderer/src/components/sidebar/worktree-list/rows/option-dom-host-collision.test.ts": 179, - "src/renderer/src/components/sidebar/worktree-list/rows/use-project-group-dialogs-owner-host.test.tsx": 259, - "src/renderer/src/components/sidebar/worktree-list/viewport/hard-scroll-up.test.ts": 59, - "src/renderer/src/components/sidebar/worktree-list/viewport/scroll-adjustment.test.ts": 2225, - "src/renderer/src/components/sidebar/worktree-list/viewport/sticky-headers.test.ts": 235, - "src/renderer/src/components/sidebar/worktree-list/viewport/use-row-removal-animation.test.ts": 94, - "src/renderer/src/components/sidebar/worktree-list/viewport/use-scroll-to-top.test.ts": 274, - "src/renderer/src/components/sidebar/worktree-list/viewport/use-scroll-to-top.test.tsx": 274, - "src/renderer/src/components/sidebar/worktree-list/viewport/virtual-rows.test.ts": 207, - "src/renderer/src/components/sidebar/worktree-list/viewport/visible-refresh.test.ts": 1414, - "src/renderer/src/components/sidebar/worktree-manual-order-catalog.test.ts": 152, - "src/renderer/src/components/sidebar/worktree-manual-order-store-write.test.ts": 1611, - "src/renderer/src/components/sidebar/worktree-manual-order.test.ts": 306, - "src/renderer/src/components/sidebar/worktree-name-suggestions.test.ts": 164, - "src/renderer/src/components/sidebar/worktree-parent-eligibility.test.ts": 62, - "src/renderer/src/components/sidebar/worktree-review-helpers.test.tsx": 178, - "src/renderer/src/components/sidebar/worktree-scroll-to-current-button.test.ts": 171, - "src/renderer/src/components/sidebar/worktree-sidebar-drag-autoscroll.test.ts": 69, - "src/renderer/src/components/sidebar/worktree-sidebar-drag-geometry.test.ts": 78, - "src/renderer/src/components/sidebar/worktree-sidebar-drop-preview.test.ts": 65, - "src/renderer/src/components/sidebar/worktree-sidebar-pointer-drag-dom.test.ts": 142, - "src/renderer/src/components/sidebar/worktree-sidebar-reveal-scroll-settle.test.ts": 41, - "src/renderer/src/components/sidebar/worktree-sidebar-reveal.test.ts": 207, - "src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts": 199, - "src/renderer/src/components/sidebar/worktree-sidebar-running-children.test.ts": 122, - "src/renderer/src/components/sidebar/worktree-sort-label-ordering.test.ts": 159, - "src/renderer/src/components/sidebar/worktree-subagent-child-rows.test.ts": 62, - "src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts": 184, - "src/renderer/src/components/sidebar/worktree-unambiguous-id-index.test.ts": 52, - "src/renderer/src/components/sidebar/worktree-unnest.test.ts": 90, - "src/renderer/src/components/sidebar/worktree-visibility-source-provenance.test.ts": 64, - "src/renderer/src/components/skills/SkillFreshnessNudge.test.tsx": 371, - "src/renderer/src/components/skills/SkillFreshnessStatusPill.test.tsx": 343, - "src/renderer/src/components/skills/SkillFreshnessUpdateDialog.test.tsx": 782, - "src/renderer/src/components/skills/SkillInstallAgentPicker.test.tsx": 690, - "src/renderer/src/components/skills/SkillInstallDialog.test.tsx": 3927, - "src/renderer/src/components/skills/SkillInstallManagementDialog.test.tsx": 3175, - "src/renderer/src/components/skills/SkillInstallTargetFields.test.tsx": 903, - "src/renderer/src/components/skills/SkillInstallWorkspaceCombobox.test.tsx": 705, - "src/renderer/src/components/skills/SkillShareDialog.test.tsx": 2543, - "src/renderer/src/components/skills/SkillSharedLinkRow.test.tsx": 665, - "src/renderer/src/components/skills/SkillsPage.test.tsx": 3940, - "src/renderer/src/components/skills/skill-agent-filter.test.ts": 156, - "src/renderer/src/components/skills/skill-bundle-name.test.ts": 233, - "src/renderer/src/components/skills/skill-bundle-retry-selection.test.ts": 55, - "src/renderer/src/components/skills/skill-delete-copy.test.ts": 110, - "src/renderer/src/components/skills/skill-delete-selection.test.ts": 321, - "src/renderer/src/components/skills/skill-description-length.test.ts": 57, - "src/renderer/src/components/skills/skill-freshness-grouping.test.ts": 63, - "src/renderer/src/components/skills/skill-freshness-skipped-reason.test.ts": 93, - "src/renderer/src/components/skills/skill-install-progress-state.test.tsx": 281, - "src/renderer/src/components/skills/skill-install-provider-groups.test.ts": 62, - "src/renderer/src/components/skills/skill-install-workspace-choices.test.ts": 49, - "src/renderer/src/components/skills/skill-package-checklist-items.test.ts": 102, - "src/renderer/src/components/skills/skill-package-digest.test.ts": 44, - "src/renderer/src/components/skills/skill-package-install-risk.test.ts": 50, - "src/renderer/src/components/skills/skill-share-link.test.ts": 48, - "src/renderer/src/components/skills/skill-share-package-selection.test.ts": 63, - "src/renderer/src/components/skills/skill-share-preview-summary.test.ts": 95, - "src/renderer/src/components/skills/skill-share-selection.test.ts": 103, - "src/renderer/src/components/skills/skill-source-inventory.test.ts": 60, - "src/renderer/src/components/skills/skills-filter.test.ts": 87, - "src/renderer/src/components/source-control/SourceControlActionVariableChips.test.tsx": 184, + "src/renderer/src/components/sidebar/worktree-card-agent-ack-inputs.test.tsx": 196, + "src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts": 1403, + "src/renderer/src/components/sidebar/worktree-card-compact-agent-row-stopping.test.ts": 1555, + "src/renderer/src/components/sidebar/worktree-card-compact-agent-row.stable-message.test.tsx": 413, + "src/renderer/src/components/sidebar/worktree-card-details-hover-state.test.tsx": 171, + "src/renderer/src/components/sidebar/worktree-card-dom-events.test.ts": 161, + "src/renderer/src/components/sidebar/worktree-card-jira-issue-display.test.ts": 38, + "src/renderer/src/components/sidebar/worktree-card-pr-display.test.ts": 48, + "src/renderer/src/components/sidebar/worktree-card-send-target-inputs.test.ts": 42, + "src/renderer/src/components/sidebar/worktree-card-status-inputs.test.ts": 44, + "src/renderer/src/components/sidebar/worktree-card-title-display.test.ts": 39, + "src/renderer/src/components/sidebar/worktree-context-menu-delete-intent.test.ts": 52, + "src/renderer/src/components/sidebar/worktree-delete-host-qualification.test.ts": 1056, + "src/renderer/src/components/sidebar/worktree-delete-position-scaling.test.ts": 235, + "src/renderer/src/components/sidebar/worktree-delete-request.test.ts": 36, + "src/renderer/src/components/sidebar/worktree-drag-units.test.ts": 98, + "src/renderer/src/components/sidebar/worktree-filter-visibility.test.ts": 532, + "src/renderer/src/components/sidebar/worktree-header-section-boundaries.test.ts": 144, + "src/renderer/src/components/sidebar/worktree-keyboard-cycle.test.ts": 133, + "src/renderer/src/components/sidebar/worktree-lineage-drag-drop.test.ts": 205, + "src/renderer/src/components/sidebar/worktree-lineage-expansion.performance.test.ts": 136, + "src/renderer/src/components/sidebar/worktree-lineage-projection.test.ts": 47, + "src/renderer/src/components/sidebar/worktree-lineage-toggle-handler-cache.test.ts": 40, + "src/renderer/src/components/sidebar/worktree-list-groups-host-collision.test.ts": 141, + "src/renderer/src/components/sidebar/worktree-list-groups-host-labels.test.ts": 212, + "src/renderer/src/components/sidebar/worktree-list-groups-imported-worktrees.test.ts": 166, + "src/renderer/src/components/sidebar/worktree-list-groups-lineage-nesting.test.ts": 165, + "src/renderer/src/components/sidebar/worktree-list-groups-nested-project-groups.test.ts": 125, + "src/renderer/src/components/sidebar/worktree-list-groups-notice-host-labels.test.ts": 126, + "src/renderer/src/components/sidebar/worktree-list-groups-pending-creations.test.ts": 125, + "src/renderer/src/components/sidebar/worktree-list-groups-pinned-host-labels.test.ts": 140, + "src/renderer/src/components/sidebar/worktree-list-groups-project-groups.test.ts": 128, + "src/renderer/src/components/sidebar/worktree-list-groups-project-host-setups.test.ts": 159, + "src/renderer/src/components/sidebar/worktree-list-groups-project-order.test.ts": 923, + "src/renderer/src/components/sidebar/worktree-list-groups-section-label-disambiguation.test.ts": 153, + "src/renderer/src/components/sidebar/worktree-list/drag/pointer-flush.test.ts": 343, + "src/renderer/src/components/sidebar/worktree-list/drag/row-state.test.ts": 29, + "src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.folder-workspace-lanes.test.ts": 160, + "src/renderer/src/components/sidebar/worktree-list/grouping/build-rows.test.ts": 177, + "src/renderer/src/components/sidebar/worktree-list/grouping/host-labels.test.ts": 35, + "src/renderer/src/components/sidebar/worktree-list/grouping/imported-rows.test.ts": 1313, + "src/renderer/src/components/sidebar/worktree-list/grouping/recent-project-rank-scan-budget.test.ts": 189, + "src/renderer/src/components/sidebar/worktree-list/listing/host-filtering.test.ts": 84, + "src/renderer/src/components/sidebar/worktree-list/listing/pending-worktree-creation-keys.test.ts": 31, + "src/renderer/src/components/sidebar/worktree-list/listing/review-cache-inputs.test.ts": 38, + "src/renderer/src/components/sidebar/worktree-list/listing/use-sort-order.test.tsx": 1713, + "src/renderer/src/components/sidebar/worktree-list/listing/use-visible-worktrees.test.tsx": 1568, + "src/renderer/src/components/sidebar/worktree-list/navigation/active-descendant-option.test.ts": 151, + "src/renderer/src/components/sidebar/worktree-list/navigation/folder-reveal.test.ts": 138, + "src/renderer/src/components/sidebar/worktree-list/navigation/folder-row-host-identity.test.ts": 179, + "src/renderer/src/components/sidebar/worktree-list/navigation/render-row-lookup.folder-workspace.test.ts": 107, + "src/renderer/src/components/sidebar/worktree-list/navigation/use-keyboard.focus.test.tsx": 605, + "src/renderer/src/components/sidebar/worktree-list/navigation/use-keyboard.host-identity.test.tsx": 315, + "src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx": 634, + "src/renderer/src/components/sidebar/worktree-list/navigation/use-selection-host-collision.test.tsx": 1580, + "src/renderer/src/components/sidebar/worktree-list/rows/FolderPathStatusIndicator.test.tsx": 253, + "src/renderer/src/components/sidebar/worktree-list/rows/RepoScanUnavailableIndicator.test.tsx": 1646, + "src/renderer/src/components/sidebar/worktree-list/rows/indentation.test.ts": 51, + "src/renderer/src/components/sidebar/worktree-list/rows/option-dom-host-collision.test.ts": 156, + "src/renderer/src/components/sidebar/worktree-list/rows/use-project-group-dialogs-owner-host.test.tsx": 223, + "src/renderer/src/components/sidebar/worktree-list/viewport/hard-scroll-up.test.ts": 60, + "src/renderer/src/components/sidebar/worktree-list/viewport/scroll-adjustment.test.ts": 2083, + "src/renderer/src/components/sidebar/worktree-list/viewport/sticky-headers.test.ts": 158, + "src/renderer/src/components/sidebar/worktree-list/viewport/use-row-removal-animation.test.ts": 78, + "src/renderer/src/components/sidebar/worktree-list/viewport/use-scroll-to-top.test.ts": 262, + "src/renderer/src/components/sidebar/worktree-list/viewport/use-scroll-to-top.test.tsx": 260, + "src/renderer/src/components/sidebar/worktree-list/viewport/virtual-rows.test.ts": 163, + "src/renderer/src/components/sidebar/worktree-list/viewport/visible-refresh.test.ts": 1464, + "src/renderer/src/components/sidebar/worktree-manual-order-catalog.test.ts": 179, + "src/renderer/src/components/sidebar/worktree-manual-order-store-write.test.ts": 1565, + "src/renderer/src/components/sidebar/worktree-manual-order.test.ts": 315, + "src/renderer/src/components/sidebar/worktree-name-suggestions.test.ts": 183, + "src/renderer/src/components/sidebar/worktree-parent-eligibility.test.ts": 40, + "src/renderer/src/components/sidebar/worktree-review-helpers.test.tsx": 87, + "src/renderer/src/components/sidebar/worktree-scroll-to-current-button.test.ts": 160, + "src/renderer/src/components/sidebar/worktree-sidebar-drag-autoscroll.test.ts": 91, + "src/renderer/src/components/sidebar/worktree-sidebar-drag-geometry.test.ts": 90, + "src/renderer/src/components/sidebar/worktree-sidebar-drop-preview.test.ts": 42, + "src/renderer/src/components/sidebar/worktree-sidebar-pointer-drag-dom.test.ts": 166, + "src/renderer/src/components/sidebar/worktree-sidebar-reveal-scroll-settle.test.ts": 29, + "src/renderer/src/components/sidebar/worktree-sidebar-reveal.test.ts": 120, + "src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts": 119, + "src/renderer/src/components/sidebar/worktree-sidebar-running-children.test.ts": 78, + "src/renderer/src/components/sidebar/worktree-sort-label-ordering.test.ts": 140, + "src/renderer/src/components/sidebar/worktree-subagent-child-rows.test.ts": 52, + "src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.test.ts": 177, + "src/renderer/src/components/sidebar/worktree-unambiguous-id-index.test.ts": 22, + "src/renderer/src/components/sidebar/worktree-unnest.test.ts": 76, + "src/renderer/src/components/sidebar/worktree-visibility-source-provenance.test.ts": 57, + "src/renderer/src/components/sidebar/wsl-host-removal.test.ts": 84, + "src/renderer/src/components/sidebar/wsl-host-row-status.test.ts": 289, + "src/renderer/src/components/skills/SkillFreshnessNudge.test.tsx": 285, + "src/renderer/src/components/skills/SkillFreshnessStatusPill.test.tsx": 290, + "src/renderer/src/components/skills/SkillFreshnessUpdateDialog.test.tsx": 972, + "src/renderer/src/components/skills/SkillInstallAgentPicker.test.tsx": 644, + "src/renderer/src/components/skills/SkillInstallDialog.test.tsx": 4101, + "src/renderer/src/components/skills/SkillInstallManagementDialog.test.tsx": 3205, + "src/renderer/src/components/skills/SkillInstallTargetFields.test.tsx": 764, + "src/renderer/src/components/skills/SkillInstallWorkspaceCombobox.test.tsx": 867, + "src/renderer/src/components/skills/SkillShareDialog.test.tsx": 2091, + "src/renderer/src/components/skills/SkillSharedLinkRow.test.tsx": 705, + "src/renderer/src/components/skills/SkillUpdateRow.test.tsx": 324, + "src/renderer/src/components/skills/SkillsPage.test.tsx": 3659, + "src/renderer/src/components/skills/skill-agent-filter.test.ts": 164, + "src/renderer/src/components/skills/skill-bundle-name.test.ts": 221, + "src/renderer/src/components/skills/skill-bundle-retry-selection.test.ts": 25, + "src/renderer/src/components/skills/skill-delete-copy.test.ts": 101, + "src/renderer/src/components/skills/skill-delete-selection.test.ts": 260, + "src/renderer/src/components/skills/skill-description-length.test.ts": 35, + "src/renderer/src/components/skills/skill-freshness-grouping.test.ts": 53, + "src/renderer/src/components/skills/skill-freshness-skipped-reason.test.ts": 59, + "src/renderer/src/components/skills/skill-install-progress-state.test.tsx": 275, + "src/renderer/src/components/skills/skill-install-provider-groups.test.ts": 49, + "src/renderer/src/components/skills/skill-install-workspace-choices.test.ts": 41, + "src/renderer/src/components/skills/skill-package-checklist-items.test.ts": 68, + "src/renderer/src/components/skills/skill-package-digest.test.ts": 28, + "src/renderer/src/components/skills/skill-package-install-risk.test.ts": 42, + "src/renderer/src/components/skills/skill-share-link.test.ts": 45, + "src/renderer/src/components/skills/skill-share-package-selection.test.ts": 35, + "src/renderer/src/components/skills/skill-share-preview-summary.test.ts": 77, + "src/renderer/src/components/skills/skill-share-selection.test.ts": 76, + "src/renderer/src/components/skills/skill-source-inventory.test.ts": 41, + "src/renderer/src/components/skills/skills-filter.test.ts": 69, + "src/renderer/src/components/source-control/SourceControlActionVariableChips.test.tsx": 131, "src/renderer/src/components/sparse/SparseCheckoutPresetDraftForm.test.tsx": 559, - "src/renderer/src/components/sparse/SparsePresetChooser.test.tsx": 521, - "src/renderer/src/components/sparse/sparse-directory-browse.test.ts": 59, - "src/renderer/src/components/sparse/sparse-directory-entry-input.test.ts": 123, - "src/renderer/src/components/star-nag/StarNagAgentValueMomentObserver.test.tsx": 1675, - "src/renderer/src/components/star-nag/StarNagToastHost.test.tsx": 373, - "src/renderer/src/components/stats/CodexUsagePane.test.tsx": 615, - "src/renderer/src/components/stats/GrokUsagePane.test.tsx": 430, - "src/renderer/src/components/stats/UsageBreakdownSection.test.tsx": 406, - "src/renderer/src/components/stats/UsageTrackingPaneShell.test.tsx": 566, - "src/renderer/src/components/stats/usage-daily-chart.test.tsx": 231, - "src/renderer/src/components/stats/usage-overview-model.test.ts": 361, - "src/renderer/src/components/status-bar/CaffeinateStatusSegment.localization.test.tsx": 907, - "src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx": 2014, - "src/renderer/src/components/status-bar/PortsStatusSegment.host-routing.test.tsx": 602, - "src/renderer/src/components/status-bar/PortsStatusSegment.render-stability.test.tsx": 2028, - "src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx": 1713, - "src/renderer/src/components/status-bar/RuntimeHostStatusRow.test.tsx": 422, - "src/renderer/src/components/status-bar/SkillUpdateStatusSegment.test.tsx": 270, - "src/renderer/src/components/status-bar/SshStatusSegment.test.ts": 1449, - "src/renderer/src/components/status-bar/StatusBarUsageChangeNotices.test.tsx": 561, - "src/renderer/src/components/status-bar/UpdateStatusSegment.test.tsx": 1978, - "src/renderer/src/components/status-bar/UsagePercentageDisplayChangeNotice.test.tsx": 391, - "src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx": 677, - "src/renderer/src/components/status-bar/claude-status-sign-in.test.tsx": 1075, - "src/renderer/src/components/status-bar/codex-restart-status-summary.test.ts": 46, - "src/renderer/src/components/status-bar/codex-status-sign-in.test.tsx": 1775, - "src/renderer/src/components/status-bar/cursor-usage-headline.test.tsx": 282, - "src/renderer/src/components/status-bar/icons.test.tsx": 81, - "src/renderer/src/components/status-bar/inline-usage-bars.test.tsx": 1440, - "src/renderer/src/components/status-bar/mergeSnapshotAndSessions.test.ts": 133, - "src/renderer/src/components/status-bar/ports-status-popover-rows.test.tsx": 651, - "src/renderer/src/components/status-bar/provider-account-sync-key.test.ts": 49, - "src/renderer/src/components/status-bar/provider-segment-balance.test.tsx": 1233, - "src/renderer/src/components/status-bar/provider-segment-monthly-window.test.tsx": 1225, - "src/renderer/src/components/status-bar/remote-host-connection-status.test.ts": 90, - "src/renderer/src/components/status-bar/resource-manager-exit-render-budget.test.tsx": 1982, - "src/renderer/src/components/status-bar/resource-manager-terminal-copy.test.ts": 58, - "src/renderer/src/components/status-bar/resource-manager-worktree-target.test.ts": 46, - "src/renderer/src/components/status-bar/resource-memory-metric-copy.test.ts": 67, - "src/renderer/src/components/status-bar/resource-session-bindings.test.ts": 52, - "src/renderer/src/components/status-bar/resource-session-inventory.test.ts": 80, - "src/renderer/src/components/status-bar/resource-session-kill-confirmation.test.ts": 62, - "src/renderer/src/components/status-bar/resource-session-navigation.test.ts": 61, - "src/renderer/src/components/status-bar/resource-session-removal-render-budget.test.tsx": 352, - "src/renderer/src/components/status-bar/resource-usage-folder-merge.test.ts": 100, - "src/renderer/src/components/status-bar/resource-usage-open-slices.test.ts": 1354, - "src/renderer/src/components/status-bar/resource-usage-space-scan-ready.test.ts": 62, - "src/renderer/src/components/status-bar/runtime-host-resources.test.ts": 52, - "src/renderer/src/components/status-bar/ssh-status-segment-copy.test.ts": 48, - "src/renderer/src/components/status-bar/status-bar-agent-gating.test.ts": 45, - "src/renderer/src/components/status-bar/status-bar-antigravity-segment.test.tsx": 1543, - "src/renderer/src/components/status-bar/status-bar-context-menu-policy.test.ts": 54, - "src/renderer/src/components/status-bar/status-bar-copy-localization.test.tsx": 961, - "src/renderer/src/components/status-bar/status-bar-cursor-segment.test.tsx": 1640, - "src/renderer/src/components/status-bar/status-bar-density.test.ts": 61, - "src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx": 1470, - "src/renderer/src/components/status-bar/status-bar-provider-visibility.test.ts": 76, - "src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts": 2360, - "src/renderer/src/components/status-bar/status-bar-usage-collapse.test.ts": 184, - "src/renderer/src/components/status-bar/tooltip.test.ts": 167, - "src/renderer/src/components/status-bar/usage-error-copy.test.ts": 52, - "src/renderer/src/components/status-bar/usage-percentage-label.localization.test.ts": 134, - "src/renderer/src/components/status-bar/usage-percentage-label.test.ts": 44, - "src/renderer/src/components/status-bar/usage-provider-settings-target.test.ts": 45, - "src/renderer/src/components/status-bar/usage-roster-formatting.test.ts": 64, - "src/renderer/src/components/status-bar/usage-roster-row-state.test.ts": 54, - "src/renderer/src/components/status-bar/usage-unit-label-parity.test.tsx": 344, - "src/renderer/src/components/status-bar/usage-unit-label.test.ts": 135, - "src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx": 1000, - "src/renderer/src/components/status-bar/use-resource-usage-actions.test.tsx": 536, - "src/renderer/src/components/status-bar/use-resource-usage-derived-model.test.tsx": 666, - "src/renderer/src/components/status-bar/use-workspace-space-decision-projection.test.tsx": 1770, - "src/renderer/src/components/status-bar/workspace-space-breakdown-list.test.tsx": 403, - "src/renderer/src/components/status-bar/workspace-space-delete-host-routing.test.ts": 52, - "src/renderer/src/components/status-bar/workspace-space-format.test.ts": 103, - "src/renderer/src/components/status-bar/workspace-space-layout.test.ts": 54, - "src/renderer/src/components/status-bar/workspace-space-presentation.test.ts": 320, - "src/renderer/src/components/tab-bar/BrowserTab.test.tsx": 1750, - "src/renderer/src/components/tab-bar/ClientHostedBrowserTabRows.test.tsx": 1122, - "src/renderer/src/components/tab-bar/CopyTabIdMenuItem.test.tsx": 449, - "src/renderer/src/components/tab-bar/EditorFileTab.test.tsx": 1281, - "src/renderer/src/components/tab-bar/EditorFileTabContextMenu.test.tsx": 1810, - "src/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsx": 746, - "src/renderer/src/components/tab-bar/QuickLaunchButton.test.ts": 437, - "src/renderer/src/components/tab-bar/RecentTabSwitcher.test.tsx": 329, - "src/renderer/src/components/tab-bar/SortableTab.rename-shortcut.test.tsx": 849, - "src/renderer/src/components/tab-bar/SortableTab.update-depth-probe.test.tsx": 2331, - "src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx": 914, - "src/renderer/src/components/tab-bar/TabBar.client-hosted-row-active-state.test.ts": 2769, - "src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts": 4026, - "src/renderer/src/components/tab-bar/TabBar.os-file-drop.test.tsx": 1358, - "src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts": 2501, - "src/renderer/src/components/tab-bar/TabBar.windows-shell-remote-runtime.test.ts": 1701, - "src/renderer/src/components/tab-bar/TabBar.windows-shell-ssh-host.test.ts": 2447, - "src/renderer/src/components/tab-bar/TabBar.worktree-write-gate.test.tsx": 1338, - "src/renderer/src/components/tab-bar/TabBar.worktree-write-gate.windows.test.tsx": 1434, - "src/renderer/src/components/tab-bar/TabBarCreateEntry.history.test.tsx": 1393, - "src/renderer/src/components/tab-bar/TabBarCreateEntry.keyboard.test.tsx": 2288, - "src/renderer/src/components/tab-bar/TabBarCreateEntry.search.test.tsx": 2607, - "src/renderer/src/components/tab-bar/TabBarCreateEntry.tab-results.test.tsx": 2010, - "src/renderer/src/components/tab-bar/TabBarCreateEntryRow.test.tsx": 662, - "src/renderer/src/components/tab-bar/TabBarItemRow.test.tsx": 1712, - "src/renderer/src/components/tab-bar/TabBarQuickCommandItem.test.tsx": 352, - "src/renderer/src/components/tab-bar/TabBarQuickCommandsMenu.keyboard.test.ts": 139, - "src/renderer/src/components/tab-bar/TabSessionSurfaceSwitchMenuItems.test.tsx": 1749, - "src/renderer/src/components/tab-bar/TabStripScrollIndicator.test.tsx": 313, - "src/renderer/src/components/tab-bar/TabStripTooltipProvider.test.tsx": 592, - "src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.state-tooltip.test.tsx": 220, - "src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.test.tsx": 371, - "src/renderer/src/components/tab-bar/client-hosted-browser-row-strip-placement.test.ts": 47, - "src/renderer/src/components/tab-bar/drop-indicator.test.ts": 53, - "src/renderer/src/components/tab-bar/group-tab-order.test.ts": 2175, - "src/renderer/src/components/tab-bar/middle-button-default-guard.test.ts": 45, - "src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.test.ts": 92, - "src/renderer/src/components/tab-bar/open-tab-entry-dedupe.test.ts": 301, - "src/renderer/src/components/tab-bar/open-tab-search-retention.test.ts": 535, - "src/renderer/src/components/tab-bar/open-tab-search.test.ts": 408, - "src/renderer/src/components/tab-bar/open-tab-selection-routing.test.ts": 211, - "src/renderer/src/components/tab-bar/query-token-match.test.ts": 46, - "src/renderer/src/components/tab-bar/recent-tab-switching.test.ts": 142, - "src/renderer/src/components/tab-bar/reconcile-order.test.ts": 52, - "src/renderer/src/components/tab-bar/tab-agent-launch-options.test.ts": 190, - "src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts": 66, - "src/renderer/src/components/tab-bar/tab-bar-item-model.git-status.test.ts": 1809, - "src/renderer/src/components/tab-bar/tab-create-entry-action.test.ts": 1664, - "src/renderer/src/components/tab-bar/tab-create-entry-classifier.runtime-listing.test.tsx": 1788, - "src/renderer/src/components/tab-bar/tab-create-entry-classifier.test.ts": 1647, - "src/renderer/src/components/tab-bar/tab-create-entry-file-matches.test.ts": 226, - "src/renderer/src/components/tab-bar/tab-create-entry-forced-search.test.ts": 53, - "src/renderer/src/components/tab-bar/tab-create-entry-history-placement.test.ts": 312, - "src/renderer/src/components/tab-bar/tab-create-entry-local-path.test.ts": 1511, - "src/renderer/src/components/tab-bar/tab-create-entry-search-ranking.test.ts": 313, - "src/renderer/src/components/tab-bar/tab-create-entry-url-classification.test.ts": 324, - "src/renderer/src/components/tab-bar/tab-create-menu-options.test.ts": 125, - "src/renderer/src/components/tab-bar/tab-move-to-pane-column.test.ts": 1394, - "src/renderer/src/components/tab-bar/tab-session-history-switch.parity.test.ts": 1955, - "src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts": 1808, - "src/renderer/src/components/tab-bar/tab-strip-content-resize-observers.test.ts": 171, - "src/renderer/src/components/tab-bar/tab-strip-growth-scroll.test.tsx": 407, - "src/renderer/src/components/tab-bar/tab-strip-overflow-navigation.test.ts": 60, - "src/renderer/src/components/tab-bar/tab-strip-pointer-activation.test.tsx": 1868, - "src/renderer/src/components/tab-bar/tab-strip-scroll-metrics.test.ts": 67, - "src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx": 1644, - "src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts": 202, + "src/renderer/src/components/sparse/SparsePresetChooser.test.tsx": 556, + "src/renderer/src/components/sparse/sparse-directory-browse.test.ts": 35, + "src/renderer/src/components/sparse/sparse-directory-entry-input.test.ts": 66, + "src/renderer/src/components/star-nag/StarNagAgentValueMomentObserver.test.tsx": 1527, + "src/renderer/src/components/star-nag/StarNagToastHost.test.tsx": 461, + "src/renderer/src/components/stats/CodexUsagePane.test.tsx": 457, + "src/renderer/src/components/stats/GrokUsagePane.test.tsx": 387, + "src/renderer/src/components/stats/UsageBreakdownSection.test.tsx": 328, + "src/renderer/src/components/stats/UsageTrackingPaneShell.test.tsx": 644, + "src/renderer/src/components/stats/usage-daily-chart.test.tsx": 229, + "src/renderer/src/components/stats/usage-overview-model.test.ts": 294, + "src/renderer/src/components/status-bar/CaffeinateStatusSegment.localization.test.tsx": 738, + "src/renderer/src/components/status-bar/NativeChatResumeStatusSegment.test.tsx": 1741, + "src/renderer/src/components/status-bar/PortsStatusSegment.host-routing.test.tsx": 686, + "src/renderer/src/components/status-bar/PortsStatusSegment.render-stability.test.tsx": 1808, + "src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx": 1634, + "src/renderer/src/components/status-bar/RuntimeHostStatusRow.test.tsx": 401, + "src/renderer/src/components/status-bar/SkillUpdateStatusSegment.test.tsx": 258, + "src/renderer/src/components/status-bar/SshStatusSegment.managed-hosts.test.tsx": 713, + "src/renderer/src/components/status-bar/SshStatusSegment.test.ts": 1278, + "src/renderer/src/components/status-bar/StatusBar.language-change.test.tsx": 1867, + "src/renderer/src/components/status-bar/StatusBarUsageChangeNotices.test.tsx": 510, + "src/renderer/src/components/status-bar/UpdateStatusSegment.test.tsx": 1734, + "src/renderer/src/components/status-bar/UsagePercentageDisplayChangeNotice.test.tsx": 415, + "src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx": 703, + "src/renderer/src/components/status-bar/claude-status-sign-in.test.tsx": 1108, + "src/renderer/src/components/status-bar/codex-restart-status-summary.test.ts": 34, + "src/renderer/src/components/status-bar/codex-status-sign-in.test.tsx": 1914, + "src/renderer/src/components/status-bar/cursor-usage-headline.test.tsx": 131, + "src/renderer/src/components/status-bar/icons.test.tsx": 62, + "src/renderer/src/components/status-bar/inline-usage-bars.test.tsx": 1407, + "src/renderer/src/components/status-bar/mergeSnapshotAndSessions.test.ts": 143, + "src/renderer/src/components/status-bar/ports-status-popover-rows.test.tsx": 631, + "src/renderer/src/components/status-bar/provider-account-sync-key.test.ts": 43, + "src/renderer/src/components/status-bar/provider-segment-balance.test.tsx": 1104, + "src/renderer/src/components/status-bar/provider-segment-monthly-window.test.tsx": 990, + "src/renderer/src/components/status-bar/remote-host-connection-status.test.ts": 70, + "src/renderer/src/components/status-bar/resource-manager-exit-render-budget.test.tsx": 1244, + "src/renderer/src/components/status-bar/resource-manager-terminal-copy.test.ts": 52, + "src/renderer/src/components/status-bar/resource-manager-worktree-target.test.ts": 29, + "src/renderer/src/components/status-bar/resource-memory-metric-copy.test.ts": 48, + "src/renderer/src/components/status-bar/resource-session-bindings.test.ts": 38, + "src/renderer/src/components/status-bar/resource-session-inventory.test.ts": 34, + "src/renderer/src/components/status-bar/resource-session-kill-confirmation.test.ts": 30, + "src/renderer/src/components/status-bar/resource-session-navigation.test.ts": 33, + "src/renderer/src/components/status-bar/resource-session-removal-render-budget.test.tsx": 386, + "src/renderer/src/components/status-bar/resource-usage-folder-merge.test.ts": 73, + "src/renderer/src/components/status-bar/resource-usage-open-slices.test.ts": 1445, + "src/renderer/src/components/status-bar/resource-usage-space-scan-ready.test.ts": 34, + "src/renderer/src/components/status-bar/runtime-host-resources.test.ts": 43, + "src/renderer/src/components/status-bar/ssh-status-segment-copy.test.ts": 34, + "src/renderer/src/components/status-bar/status-bar-agent-gating.test.ts": 35, + "src/renderer/src/components/status-bar/status-bar-antigravity-segment.test.tsx": 1423, + "src/renderer/src/components/status-bar/status-bar-context-menu-policy.test.ts": 38, + "src/renderer/src/components/status-bar/status-bar-copy-localization.test.tsx": 755, + "src/renderer/src/components/status-bar/status-bar-cursor-segment.test.tsx": 1500, + "src/renderer/src/components/status-bar/status-bar-density.test.ts": 35, + "src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx": 1327, + "src/renderer/src/components/status-bar/status-bar-provider-visibility.test.ts": 70, + "src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts": 365, + "src/renderer/src/components/status-bar/status-bar-usage-collapse.test.ts": 176, + "src/renderer/src/components/status-bar/tooltip.test.ts": 154, + "src/renderer/src/components/status-bar/usage-error-copy.test.ts": 42, + "src/renderer/src/components/status-bar/usage-percentage-label.localization.test.ts": 119, + "src/renderer/src/components/status-bar/usage-percentage-label.test.ts": 22, + "src/renderer/src/components/status-bar/usage-provider-settings-target.test.ts": 30, + "src/renderer/src/components/status-bar/usage-roster-formatting.test.ts": 34, + "src/renderer/src/components/status-bar/usage-roster-row-state.test.ts": 44, + "src/renderer/src/components/status-bar/usage-unit-label-parity.test.tsx": 267, + "src/renderer/src/components/status-bar/usage-unit-label.test.ts": 84, + "src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx": 985, + "src/renderer/src/components/status-bar/use-resource-usage-actions.test.tsx": 532, + "src/renderer/src/components/status-bar/use-resource-usage-derived-model.test.tsx": 553, + "src/renderer/src/components/status-bar/use-workspace-space-decision-projection.test.tsx": 1901, + "src/renderer/src/components/status-bar/workspace-space-breakdown-list.test.tsx": 371, + "src/renderer/src/components/status-bar/workspace-space-delete-host-routing.test.ts": 41, + "src/renderer/src/components/status-bar/workspace-space-format.test.ts": 67, + "src/renderer/src/components/status-bar/workspace-space-layout.test.ts": 37, + "src/renderer/src/components/status-bar/workspace-space-presentation.test.ts": 319, + "src/renderer/src/components/tab-bar/BrowserTab.test.tsx": 1505, + "src/renderer/src/components/tab-bar/ClientHostedBrowserTabRows.test.tsx": 701, + "src/renderer/src/components/tab-bar/CopyTabIdMenuItem.test.tsx": 328, + "src/renderer/src/components/tab-bar/EditorFileTab.test.tsx": 667, + "src/renderer/src/components/tab-bar/EditorFileTabContextMenu.test.tsx": 835, + "src/renderer/src/components/tab-bar/QuickLaunchButton.launch-status.test.tsx": 688, + "src/renderer/src/components/tab-bar/QuickLaunchButton.test.ts": 402, + "src/renderer/src/components/tab-bar/RecentTabSwitcher.test.tsx": 290, + "src/renderer/src/components/tab-bar/SortableTab.rename-shortcut.test.tsx": 473, + "src/renderer/src/components/tab-bar/SortableTab.update-depth-probe.test.tsx": 1688, + "src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx": 656, + "src/renderer/src/components/tab-bar/TabBar.client-hosted-row-active-state.test.ts": 2936, + "src/renderer/src/components/tab-bar/TabBar.context-menu.test.ts": 3906, + "src/renderer/src/components/tab-bar/TabBar.os-file-drop.test.tsx": 875, + "src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts": 2782, + "src/renderer/src/components/tab-bar/TabBar.windows-shell-remote-runtime.test.ts": 2214, + "src/renderer/src/components/tab-bar/TabBar.windows-shell-ssh-host.test.ts": 1702, + "src/renderer/src/components/tab-bar/TabBar.worktree-write-gate.test.tsx": 814, + "src/renderer/src/components/tab-bar/TabBar.worktree-write-gate.windows.test.tsx": 799, + "src/renderer/src/components/tab-bar/TabBarCreateEntry.history.test.tsx": 1474, + "src/renderer/src/components/tab-bar/TabBarCreateEntry.keyboard.test.tsx": 2256, + "src/renderer/src/components/tab-bar/TabBarCreateEntry.search.test.tsx": 2785, + "src/renderer/src/components/tab-bar/TabBarCreateEntry.tab-results.test.tsx": 1973, + "src/renderer/src/components/tab-bar/TabBarCreateEntryRow.test.tsx": 666, + "src/renderer/src/components/tab-bar/TabBarItemRow.test.tsx": 1510, + "src/renderer/src/components/tab-bar/TabBarQuickCommandItem.test.tsx": 360, + "src/renderer/src/components/tab-bar/TabBarQuickCommandsMenu.keyboard.test.ts": 105, + "src/renderer/src/components/tab-bar/TabSessionSurfaceSwitchMenuItems.test.tsx": 1981, + "src/renderer/src/components/tab-bar/TabStripScrollIndicator.test.tsx": 320, + "src/renderer/src/components/tab-bar/TabStripTooltipProvider.test.tsx": 531, + "src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.state-tooltip.test.tsx": 230, + "src/renderer/src/components/tab-bar/TerminalTabLeadingIcon.test.tsx": 220, + "src/renderer/src/components/tab-bar/client-hosted-browser-row-strip-placement.test.ts": 36, + "src/renderer/src/components/tab-bar/drop-indicator.test.ts": 37, + "src/renderer/src/components/tab-bar/group-tab-order.test.ts": 1476, + "src/renderer/src/components/tab-bar/middle-button-default-guard.test.ts": 27, + "src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.test.ts": 60, + "src/renderer/src/components/tab-bar/open-tab-entry-dedupe.test.ts": 273, + "src/renderer/src/components/tab-bar/open-tab-search-retention.test.ts": 590, + "src/renderer/src/components/tab-bar/open-tab-search.test.ts": 405, + "src/renderer/src/components/tab-bar/open-tab-selection-routing.test.ts": 222, + "src/renderer/src/components/tab-bar/query-token-match.test.ts": 40, + "src/renderer/src/components/tab-bar/recent-tab-switching.test.ts": 41, + "src/renderer/src/components/tab-bar/reconcile-order.test.ts": 30, + "src/renderer/src/components/tab-bar/tab-agent-launch-options.test.ts": 216, + "src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.test.ts": 62, + "src/renderer/src/components/tab-bar/tab-bar-item-model.git-status.test.ts": 1450, + "src/renderer/src/components/tab-bar/tab-create-entry-action.test.ts": 1394, + "src/renderer/src/components/tab-bar/tab-create-entry-classifier.runtime-listing.test.tsx": 1638, + "src/renderer/src/components/tab-bar/tab-create-entry-classifier.test.ts": 1329, + "src/renderer/src/components/tab-bar/tab-create-entry-file-matches.test.ts": 178, + "src/renderer/src/components/tab-bar/tab-create-entry-forced-search.test.ts": 33, + "src/renderer/src/components/tab-bar/tab-create-entry-history-placement.test.ts": 284, + "src/renderer/src/components/tab-bar/tab-create-entry-local-path.test.ts": 1603, + "src/renderer/src/components/tab-bar/tab-create-entry-search-ranking.test.ts": 383, + "src/renderer/src/components/tab-bar/tab-create-entry-url-classification.test.ts": 308, + "src/renderer/src/components/tab-bar/tab-create-menu-options.test.ts": 81, + "src/renderer/src/components/tab-bar/tab-move-to-pane-column.test.ts": 1265, + "src/renderer/src/components/tab-bar/tab-session-history-switch.parity.test.ts": 1664, + "src/renderer/src/components/tab-bar/tab-session-history-switch.test.ts": 1738, + "src/renderer/src/components/tab-bar/tab-strip-content-resize-observers.test.ts": 173, + "src/renderer/src/components/tab-bar/tab-strip-growth-scroll.test.tsx": 380, + "src/renderer/src/components/tab-bar/tab-strip-overflow-navigation.test.ts": 29, + "src/renderer/src/components/tab-bar/tab-strip-pointer-activation.test.tsx": 1697, + "src/renderer/src/components/tab-bar/tab-strip-scroll-metrics.test.ts": 36, + "src/renderer/src/components/tab-bar/tab-title-tooltip.test.tsx": 1711, + "src/renderer/src/components/tab-bar/terminal-tab-activity-status.test.ts": 154, "src/renderer/src/components/tab-bar/terminal-tab-spinner-launch-agent.test.ts": 103, - "src/renderer/src/components/tab-bar/use-open-tab-search.test.ts": 1793, - "src/renderer/src/components/tab-bar/use-tab-bar-item-projection.test.tsx": 2025, - "src/renderer/src/components/tab-bar/use-tab-bar-quick-command-search-input.test.tsx": 367, - "src/renderer/src/components/tab-bar/windows-shell-launch.test.ts": 51, - "src/renderer/src/components/tab-bar/windows-shell-menu-visibility.test.ts": 50, - "src/renderer/src/components/tab-group/RetainedPaneHost.test.tsx": 360, - "src/renderer/src/components/tab-group/TabGroupPanel.context-menu.test.ts": 43, - "src/renderer/src/components/tab-group/TabGroupPanel.os-file-drop.test.tsx": 1712, + "src/renderer/src/components/tab-bar/use-open-tab-search.test.ts": 1896, + "src/renderer/src/components/tab-bar/use-tab-bar-item-projection.test.tsx": 1972, + "src/renderer/src/components/tab-bar/use-tab-bar-quick-command-search-input.test.tsx": 362, + "src/renderer/src/components/tab-bar/windows-shell-launch.test.ts": 26, + "src/renderer/src/components/tab-bar/windows-shell-menu-visibility.test.ts": 33, + "src/renderer/src/components/tab-group/RetainedPaneHost.test.tsx": 336, + "src/renderer/src/components/tab-group/TabGroupPanel.context-menu.test.ts": 33, + "src/renderer/src/components/tab-group/TabGroupPanel.os-file-drop.test.tsx": 1463, "src/renderer/src/components/tab-group/TabGroupSplitLayout.drag.test.tsx": 485, - "src/renderer/src/components/tab-group/TabGroupSplitLayout.test.ts": 241, - "src/renderer/src/components/tab-group/TabPaneColumnSplitDragOverlay.test.tsx": 99, - "src/renderer/src/components/tab-group/tab-drag-pointer-sensor.test.ts": 145, - "src/renderer/src/components/tab-group/tab-drag-pointer.test.ts": 48, - "src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts": 1235, - "src/renderer/src/components/tab-group/tab-drag-retained-guest-passthrough.test.tsx": 1438, - "src/renderer/src/components/tab-group/tab-drop-zone.test.ts": 50, - "src/renderer/src/components/tab-group/tab-group-body-anchor.test.ts": 48, - "src/renderer/src/components/tab-group/tab-group-body-geometry.test.ts": 168, - "src/renderer/src/components/tab-group/tab-group-panel-split-target.test.ts": 74, - "src/renderer/src/components/tab-group/tab-insertion.test.ts": 67, - "src/renderer/src/components/tab-group/useTabDragSplit.test.ts": 1883, - "src/renderer/src/components/tab-group/useTabGroupCreationCommands.local-shell.test.ts": 998, - "src/renderer/src/components/tab-group/useTabGroupTabCloseCommands.structured-session.test.ts": 543, - "src/renderer/src/components/tab-group/useTabGroupTabCloseCommands.test.tsx": 1900, - "src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts": 1213, - "src/renderer/src/components/tab-group/workspace-tab-close-commands.floating-reclaim.test.ts": 1573, - "src/renderer/src/components/task-page-cache-selectors.test.ts": 71, - "src/renderer/src/components/task-page-checks-pill.test.ts": 46, - "src/renderer/src/components/task-page-default-repo-selection.test.ts": 80, - "src/renderer/src/components/task-page-empty-state.test.ts": 89, - "src/renderer/src/components/task-page-github-dialog-state-authority.test.ts": 191, - "src/renderer/src/components/task-page-github-list-scroll-restore.test.ts": 206, - "src/renderer/src/components/task-page-github-resume-cache.test.ts": 52, - "src/renderer/src/components/task-page-github-reviewer-suggestions.test.ts": 75, - "src/renderer/src/components/task-page-github-status-actions.test.ts": 49, - "src/renderer/src/components/task-page-github-status-state.test.ts": 56, - "src/renderer/src/components/task-page-github-task-kind.test.ts": 70, - "src/renderer/src/components/task-page-github-work-item-filter-membership.test.ts": 47, - "src/renderer/src/components/task-page-github-work-item-mutation-patches.test.ts": 62, - "src/renderer/src/components/task-page-github-work-item-mutation-regressions.test.ts": 98, - "src/renderer/src/components/task-page-github-work-item-mutations.test.ts": 168, - "src/renderer/src/components/task-page-github-work-item-status.test.ts": 58, - "src/renderer/src/components/task-page-gitlab-task-filters.test.ts": 1459, - "src/renderer/src/components/task-page-initial-selection-scaling.test.tsx": 1832, - "src/renderer/src/components/task-page-jira-cache-selectors.test.ts": 49, - "src/renderer/src/components/task-page-jira-create-fields.test.ts": 84, - "src/renderer/src/components/task-page-jira-grouping.test.ts": 577, - "src/renderer/src/components/task-page-jira-item-source-context.test.ts": 60, - "src/renderer/src/components/task-page-jira-load-state.test.ts": 54, - "src/renderer/src/components/task-page-jira-project-selection.test.ts": 49, - "src/renderer/src/components/task-page-jira-search.test.ts": 89, - "src/renderer/src/components/task-page-jira-sort-controls.test.tsx": 702, - "src/renderer/src/components/task-page-jira-sorting.test.ts": 134, - "src/renderer/src/components/task-page-jira-text-fallback-notice.test.tsx": 429, - "src/renderer/src/components/task-page-linear-in-orca-issues.test.ts": 94, - "src/renderer/src/components/task-page-linear-issue-empty-state.test.ts": 47, - "src/renderer/src/components/task-page-linear-issue-grouping.test.ts": 1537, - "src/renderer/src/components/task-page-linear-issue-request.test.ts": 60, - "src/renderer/src/components/task-page-linear-team-selection.test.ts": 48, - "src/renderer/src/components/task-page-list-chrome-visibility.test.ts": 58, - "src/renderer/src/components/task-page-localized-options.test.ts": 256, - "src/renderer/src/components/task-page-mutation-page-allocation.test.ts": 86, - "src/renderer/src/components/task-page-new-issue-draft.test.ts": 60, - "src/renderer/src/components/task-page-pagination-page-numbers.test.ts": 63, - "src/renderer/src/components/task-page-pr-check-summary.test.ts": 45, - "src/renderer/src/components/task-page-repo-source-context.test.ts": 1529, - "src/renderer/src/components/task-page-repo-source-divergence.test.ts": 1378, - "src/renderer/src/components/task-page-task-source-host-availability.test.ts": 1464, - "src/renderer/src/components/task-page-work-item-pagination.test.ts": 84, - "src/renderer/src/components/task-page/jira/IssueAssigneeField.test.tsx": 1049, - "src/renderer/src/components/task-source-context-summary.test.ts": 92, - "src/renderer/src/components/task-source-provider-availability.test.ts": 64, - "src/renderer/src/components/terminal-agent-tab-shortcut.test.ts": 1561, - "src/renderer/src/components/terminal-pane/CloseTerminalDialog.test.tsx": 388, - "src/renderer/src/components/terminal-pane/MobileDriverOverlay.test.tsx": 181, - "src/renderer/src/components/terminal-pane/PinnedTabCloseDialog.test.tsx": 1699, - "src/renderer/src/components/terminal-pane/RunningTerminalCloseDialog.test.tsx": 1847, - "src/renderer/src/components/terminal-pane/SessionRestoredBanner.test.tsx": 233, - "src/renderer/src/components/terminal-pane/TerminalAgentSessionForkDialog.test.tsx": 171, - "src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx": 1769, - "src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts": 626, - "src/renderer/src/components/terminal-pane/TerminalOverlaySlot.emptied-reaction.test.tsx": 439, - "src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.test.tsx": 1019, - "src/renderer/src/components/terminal-pane/TerminalPaneNativeChatPortal.test.tsx": 327, - "src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.react185.test.tsx": 1492, - "src/renderer/src/components/terminal-pane/TerminalProcessExitOverlay.test.tsx": 370, - "src/renderer/src/components/terminal-pane/TerminalRemoteRuntimeReconnectBanner.test.tsx": 380, - "src/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.test.tsx": 1832, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-attention-dispatch.test.ts": 223, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-completion-replay-guard.test.ts": 206, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-dispose-leak.test.ts": 219, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-done-quiet-window.test.ts": 196, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-title-precedence.test.ts": 204, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-monitoring-turn-end.test.ts": 341, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts": 251, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts": 270, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-exit-turn-boundary.test.ts": 191, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts": 175, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-boundary.test.ts": 227, - "src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-replay.test.ts": 208, - "src/renderer/src/components/terminal-pane/agent-completion-no-evidence-cadence.test.ts": 1486, - "src/renderer/src/components/terminal-pane/agent-completion-poll-interval.test.ts": 111, - "src/renderer/src/components/terminal-pane/agent-completion-stale-evidence-backoff.test.ts": 147, - "src/renderer/src/components/terminal-pane/agent-completion-steady-state-opt-in.test.ts": 188, - "src/renderer/src/components/terminal-pane/agent-hook-terminal-lifecycle.test.ts": 45, - "src/renderer/src/components/terminal-pane/agent-interrupt-inference.test.ts": 155, - "src/renderer/src/components/terminal-pane/agent-launch-pane-notice.test.ts": 43, - "src/renderer/src/components/terminal-pane/agent-process-inspection-queue-rejection-containment.test.ts": 53, - "src/renderer/src/components/terminal-pane/agent-process-inspection-round.test.ts": 61, - "src/renderer/src/components/terminal-pane/agent-question-answered-inference.test.ts": 108, - "src/renderer/src/components/terminal-pane/cache-timer-seeding.test.ts": 100, - "src/renderer/src/components/terminal-pane/claude-old-terminal-banner.test.tsx": 1667, - "src/renderer/src/components/terminal-pane/closed-terminal-leaf-notice.test.ts": 1078, - "src/renderer/src/components/terminal-pane/codex-backfill-error-detector.test.ts": 31, - "src/renderer/src/components/terminal-pane/codex-detached-pane-restart.test.ts": 1928, - "src/renderer/src/components/terminal-pane/codex-shared-server-banner.test.tsx": 2177, - "src/renderer/src/components/terminal-pane/codex-shared-settings-notice.test.ts": 245, - "src/renderer/src/components/terminal-pane/codex-terminal-presence.test.ts": 83, - "src/renderer/src/components/terminal-pane/codex-terminal-server-isolation-notice.test.ts": 232, - "src/renderer/src/components/terminal-pane/command-code-done-settle.test.ts": 53, - "src/renderer/src/components/terminal-pane/command-code-output-ownership.test.ts": 44, - "src/renderer/src/components/terminal-pane/compose-active-terminal-theme.test.ts": 135, - "src/renderer/src/components/terminal-pane/deferred-reattach-live-data-queue.test.ts": 193, - "src/renderer/src/components/terminal-pane/deferred-split-pane-handoff.test.ts": 75, - "src/renderer/src/components/terminal-pane/desktop-fit-fallback.test.ts": 85, - "src/renderer/src/components/terminal-pane/direct-ssh-hidden-output-restore-unavailable-banner.test.ts": 1301, - "src/renderer/src/components/terminal-pane/edge-wrapped-terminal-http-links.test.ts": 2360, - "src/renderer/src/components/terminal-pane/expand-collapse-render-stability.test.ts": 2055, - "src/renderer/src/components/terminal-pane/expand-collapse.test.ts": 101, - "src/renderer/src/components/terminal-pane/floating-file-link-selection.test.ts": 2261, - "src/renderer/src/components/terminal-pane/focus-terminal-pane-event.test.ts": 55, - "src/renderer/src/components/terminal-pane/focused-pane-rim-flash.test.ts": 46, - "src/renderer/src/components/terminal-pane/git-bash-console-capacity.test.ts": 52, - "src/renderer/src/components/terminal-pane/hard-wrapped-terminal-http-links.test.ts": 90, - "src/renderer/src/components/terminal-pane/hidden-output-restore-scheduler.test.ts": 54, - "src/renderer/src/components/terminal-pane/ipc-pty-opencode-model.test.ts": 263, - "src/renderer/src/components/terminal-pane/issue-12112-agent-pane-startup-color-reply-leak.repro.test.ts": 530, - "src/renderer/src/components/terminal-pane/issue-21762-tracking-mode-middle-click-double-paste.repro.test.ts": 349, - "src/renderer/src/components/terminal-pane/issue-4631-terminal-hover-link-provider.repro.test.ts": 919, - "src/renderer/src/components/terminal-pane/keyboard-handlers-ime-composing-chord.test.tsx": 2146, - "src/renderer/src/components/terminal-pane/keyboard-handlers-ime-enter-keyup.test.tsx": 1944, - "src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx": 2229, - "src/renderer/src/components/terminal-pane/keyboard-handlers.test.ts": 1877, - "src/renderer/src/components/terminal-pane/layout-serialization.test.ts": 96, - "src/renderer/src/components/terminal-pane/leaf-scrollback-resolution.test.ts": 263, - "src/renderer/src/components/terminal-pane/manual-terminal-worktree-park-eligibility.test.ts": 1422, - "src/renderer/src/components/terminal-pane/merge-captured-leaf-state.test.ts": 56, - "src/renderer/src/components/terminal-pane/mobile-driver-overlay-collapse.test.ts": 47, - "src/renderer/src/components/terminal-pane/mobile-driver-overlay-focus.test.ts": 45, - "src/renderer/src/components/terminal-pane/mobile-driver-overlay-visibility.test.ts": 38, - "src/renderer/src/components/terminal-pane/mouse-hide-while-typing.test.ts": 48, - "src/renderer/src/components/terminal-pane/native-chat-leaf-title-agent.test.ts": 87, - "src/renderer/src/components/terminal-pane/osc52-clipboard-default-on-notice.test.ts": 278, - "src/renderer/src/components/terminal-pane/osc52-clipboard-toast.test.ts": 1039, - "src/renderer/src/components/terminal-pane/osc52-clipboard.test.ts": 170, - "src/renderer/src/components/terminal-pane/override-affected-panes.test.ts": 48, - "src/renderer/src/components/terminal-pane/paired-parking-unverifiable-host.test.ts": 93, - "src/renderer/src/components/terminal-pane/paired-reconnect-multi-pane-materialization.test.ts": 1915, - "src/renderer/src/components/terminal-pane/pane-agent-session-id.test.ts": 45, - "src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.test.ts": 349, - "src/renderer/src/components/terminal-pane/pane-foreground-inspect-observation-identity.test.ts": 100, - "src/renderer/src/components/terminal-pane/pane-foreground-process-exit-retire.test.ts": 223, - "src/renderer/src/components/terminal-pane/pane-helpers.test.ts": 73, - "src/renderer/src/components/terminal-pane/pane-restart-transport-handoff.test.ts": 556, - "src/renderer/src/components/terminal-pane/pane-title-overlay-rects.test.ts": 58, - "src/renderer/src/components/terminal-pane/pane-title-update-global-scan-budget.test.tsx": 3072, - "src/renderer/src/components/terminal-pane/parked-terminal-buffer-capture.test.ts": 308, - "src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.test.ts": 2465, - "src/renderer/src/components/terminal-pane/parked-terminal-command-status.test.ts": 428, - "src/renderer/src/components/terminal-pane/parked-terminal-tab-capture-episodes.test.ts": 301, - "src/renderer/src/components/terminal-pane/parse-osc7.test.ts": 50, - "src/renderer/src/components/terminal-pane/pending-pane-close-confirmation.test.ts": 1589, - "src/renderer/src/components/terminal-pane/pending-runtime-pane-close.test.ts": 1223, - "src/renderer/src/components/terminal-pane/pending-split-close.test.ts": 1550, - "src/renderer/src/components/terminal-pane/pty-buffer-serializer.test.ts": 50, - "src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts": 2046, - "src/renderer/src/components/terminal-pane/pty-connection-cold-restore-agent-resume.test.ts": 1677, - "src/renderer/src/components/terminal-pane/pty-connection-cold-restore-repaint.test.ts": 1145, - "src/renderer/src/components/terminal-pane/pty-connection-cold-restore-resume-command.test.ts": 1608, - "src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts": 7322, - "src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts": 1868, - "src/renderer/src/components/terminal-pane/pty-connection-daemon-snapshot-replay.test.ts": 2005, - "src/renderer/src/components/terminal-pane/pty-connection-deferred-reattach-live-output.test.ts": 1774, - "src/renderer/src/components/terminal-pane/pty-connection-deliberate-sleep-guard.test.ts": 1469, - "src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts": 1745, - "src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts": 2594, - "src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.test.ts": 879, - "src/renderer/src/components/terminal-pane/pty-connection-foreground-routing-confirmation.test.ts": 1378, - "src/renderer/src/components/terminal-pane/pty-connection-foreground-write-path.test.ts": 2002, - "src/renderer/src/components/terminal-pane/pty-connection-fresh-spawn-guards.test.ts": 2467, - "src/renderer/src/components/terminal-pane/pty-connection-hibernation-wake.test.ts": 2008, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.test.ts": 2262, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.test.ts": 1815, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-snapshot.test.ts": 2838, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.test.ts": 1760, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-delivery-gate.test.ts": 2566, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-output-restore.test.ts": 1595, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-query-snapshot-restore.test.ts": 1664, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-restore-fit-overflow.test.ts": 914, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-live-overlap.test.ts": 1148, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-resize-signals.test.ts": 2595, - "src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.test.ts": 1107, - "src/renderer/src/components/terminal-pane/pty-connection-hook-completion-bell-arbitration.test.ts": 1929, - "src/renderer/src/components/terminal-pane/pty-connection-hook-completion-side-effects.test.ts": 1620, - "src/renderer/src/components/terminal-pane/pty-connection-hook-idle-arbitration.test.ts": 1272, - "src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.test.ts": 2067, - "src/renderer/src/components/terminal-pane/pty-connection-kitty-keyboard-restore.test.ts": 1436, - "src/renderer/src/components/terminal-pane/pty-connection-main-side-effect-authority.test.ts": 2527, - "src/renderer/src/components/terminal-pane/pty-connection-mode-2031-subscriptions.test.ts": 2233, - "src/renderer/src/components/terminal-pane/pty-connection-notification-settings-gating.test.ts": 1856, - "src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts": 922, - "src/renderer/src/components/terminal-pane/pty-connection-post-dispose-restore-termination.test.ts": 1239, - "src/renderer/src/components/terminal-pane/pty-connection-pty-exit-teardown.test.ts": 4853, - "src/renderer/src/components/terminal-pane/pty-connection-queued-startup-consume.test.ts": 1191, - "src/renderer/src/components/terminal-pane/pty-connection-reattach-binding.test.ts": 4133, - "src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.test.ts": 2681, - "src/renderer/src/components/terminal-pane/pty-connection-remote-runtime-attach.test.ts": 2433, - "src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts": 1154, - "src/renderer/src/components/terminal-pane/pty-connection-renderer-risk-repaint.test.ts": 1895, - "src/renderer/src/components/terminal-pane/pty-connection-replay-payload-handling.test.ts": 2078, - "src/renderer/src/components/terminal-pane/pty-connection-restored-baseline-shortfall.test.ts": 1232, - "src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts": 1812, - "src/renderer/src/components/terminal-pane/pty-connection-session-liveness.test.ts": 2447, - "src/renderer/src/components/terminal-pane/pty-connection-setup-split-spawn.test.ts": 1546, + "src/renderer/src/components/tab-group/TabGroupSplitLayout.test.ts": 133, + "src/renderer/src/components/tab-group/TabPaneColumnSplitDragOverlay.test.tsx": 62, + "src/renderer/src/components/tab-group/tab-drag-pointer-sensor.test.ts": 177, + "src/renderer/src/components/tab-group/tab-drag-pointer.test.ts": 38, + "src/renderer/src/components/tab-group/tab-drag-preview-activation.test.ts": 1356, + "src/renderer/src/components/tab-group/tab-drag-retained-guest-passthrough.test.tsx": 1738, + "src/renderer/src/components/tab-group/tab-drop-zone.test.ts": 35, + "src/renderer/src/components/tab-group/tab-group-body-anchor.test.ts": 30, + "src/renderer/src/components/tab-group/tab-group-body-geometry.test.ts": 138, + "src/renderer/src/components/tab-group/tab-group-panel-split-target.test.ts": 58, + "src/renderer/src/components/tab-group/tab-insertion.test.ts": 52, + "src/renderer/src/components/tab-group/useTabDragSplit.test.ts": 1673, + "src/renderer/src/components/tab-group/useTabGroupCreationCommands.local-shell.test.ts": 836, + "src/renderer/src/components/tab-group/useTabGroupTabCloseCommands.structured-session.test.ts": 430, + "src/renderer/src/components/tab-group/useTabGroupTabCloseCommands.test.tsx": 1711, + "src/renderer/src/components/tab-group/useTabGroupWorkspaceModel.focus.test.ts": 676, + "src/renderer/src/components/tab-group/workspace-tab-close-commands.floating-reclaim.test.ts": 1405, + "src/renderer/src/components/task-page-cache-selectors.test.ts": 69, + "src/renderer/src/components/task-page-checks-pill.test.ts": 39, + "src/renderer/src/components/task-page-default-repo-selection.test.ts": 54, + "src/renderer/src/components/task-page-empty-state.test.ts": 62, + "src/renderer/src/components/task-page-github-dialog-state-authority.test.ts": 67, + "src/renderer/src/components/task-page-github-list-scroll-restore.test.ts": 163, + "src/renderer/src/components/task-page-github-resume-cache.test.ts": 39, + "src/renderer/src/components/task-page-github-reviewer-suggestions.test.ts": 78, + "src/renderer/src/components/task-page-github-status-actions.test.ts": 35, + "src/renderer/src/components/task-page-github-status-state.test.ts": 33, + "src/renderer/src/components/task-page-github-task-kind.test.ts": 61, + "src/renderer/src/components/task-page-github-work-item-filter-membership.test.ts": 31, + "src/renderer/src/components/task-page-github-work-item-mutation-patches.test.ts": 44, + "src/renderer/src/components/task-page-github-work-item-mutation-regressions.test.ts": 96, + "src/renderer/src/components/task-page-github-work-item-mutations.test.ts": 159, + "src/renderer/src/components/task-page-github-work-item-status.test.ts": 37, + "src/renderer/src/components/task-page-gitlab-task-filters.test.ts": 1370, + "src/renderer/src/components/task-page-initial-selection-scaling.test.tsx": 1587, + "src/renderer/src/components/task-page-jira-board-detail-integration.test.tsx": 2017, + "src/renderer/src/components/task-page-jira-board-sections.test.ts": 1498, + "src/renderer/src/components/task-page-jira-board-transitions.test.ts": 49, + "src/renderer/src/components/task-page-jira-board-view.test.tsx": 1803, + "src/renderer/src/components/task-page-jira-board.test.tsx": 1631, + "src/renderer/src/components/task-page-jira-cache-selectors.test.ts": 32, + "src/renderer/src/components/task-page-jira-create-fields.test.ts": 65, + "src/renderer/src/components/task-page-jira-grouping.test.ts": 2252, + "src/renderer/src/components/task-page-jira-item-source-context.test.ts": 33, + "src/renderer/src/components/task-page-jira-linked-issues.test.ts": 57, + "src/renderer/src/components/task-page-jira-load-state.test.ts": 36, + "src/renderer/src/components/task-page-jira-project-selection.test.ts": 31, + "src/renderer/src/components/task-page-jira-quick-copy.test.tsx": 3451, + "src/renderer/src/components/task-page-jira-saved-filter-application.test.tsx": 1859, + "src/renderer/src/components/task-page-jira-search.test.ts": 73, + "src/renderer/src/components/task-page-jira-sort-controls.test.tsx": 715, + "src/renderer/src/components/task-page-jira-sorting.test.ts": 148, + "src/renderer/src/components/task-page-jira-status-catalog.test.ts": 28, + "src/renderer/src/components/task-page-jira-status-order-session.test.tsx": 1881, + "src/renderer/src/components/task-page-jira-text-fallback-notice.test.tsx": 478, + "src/renderer/src/components/task-page-jira-workspace-actions.test.tsx": 2107, + "src/renderer/src/components/task-page-linear-in-orca-issues.test.ts": 81, + "src/renderer/src/components/task-page-linear-issue-empty-state.test.ts": 29, + "src/renderer/src/components/task-page-linear-issue-grouping.test.ts": 1344, + "src/renderer/src/components/task-page-linear-issue-request.test.ts": 52, + "src/renderer/src/components/task-page-linear-team-selection.test.ts": 39, + "src/renderer/src/components/task-page-list-chrome-visibility.test.ts": 36, + "src/renderer/src/components/task-page-localized-options.test.ts": 188, + "src/renderer/src/components/task-page-mutation-page-allocation.test.ts": 95, + "src/renderer/src/components/task-page-new-issue-draft.test.ts": 53, + "src/renderer/src/components/task-page-pagination-page-numbers.test.ts": 39, + "src/renderer/src/components/task-page-pr-check-summary.test.ts": 40, + "src/renderer/src/components/task-page-repo-source-context.test.ts": 1360, + "src/renderer/src/components/task-page-repo-source-divergence.test.ts": 1310, + "src/renderer/src/components/task-page-task-source-host-availability.test.ts": 1553, + "src/renderer/src/components/task-page-work-item-pagination.test.ts": 76, + "src/renderer/src/components/task-page/jira/Content.board-owner.test.tsx": 2237, + "src/renderer/src/components/task-page/jira/Filters.linked-mode.test.tsx": 3095, + "src/renderer/src/components/task-page/jira/IssueAssigneeField.test.tsx": 1121, + "src/renderer/src/components/task-source-context-summary.test.ts": 89, + "src/renderer/src/components/task-source-provider-availability.test.ts": 43, + "src/renderer/src/components/terminal-agent-tab-shortcut.test.ts": 1372, + "src/renderer/src/components/terminal-pane/CloseTerminalDialog.test.tsx": 529, + "src/renderer/src/components/terminal-pane/MobileDriverOverlay.test.tsx": 139, + "src/renderer/src/components/terminal-pane/PinnedTabCloseDialog.test.tsx": 1747, + "src/renderer/src/components/terminal-pane/RunningTerminalCloseDialog.test.tsx": 1887, + "src/renderer/src/components/terminal-pane/SessionRestoredBanner.test.tsx": 184, + "src/renderer/src/components/terminal-pane/TerminalAgentSessionForkDialog.test.tsx": 98, + "src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx": 1496, + "src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts": 691, + "src/renderer/src/components/terminal-pane/TerminalOverlaySlot.emptied-reaction.test.tsx": 484, + "src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.test.tsx": 1228, + "src/renderer/src/components/terminal-pane/TerminalPaneNativeChatPortal.test.tsx": 323, + "src/renderer/src/components/terminal-pane/TerminalPaneOverlayLayer.react185.test.tsx": 794, + "src/renderer/src/components/terminal-pane/TerminalPaneResumeInChatButton.test.tsx": 1281, + "src/renderer/src/components/terminal-pane/TerminalProcessExitOverlay.test.tsx": 352, + "src/renderer/src/components/terminal-pane/TerminalRemoteRuntimeReconnectBanner.test.tsx": 1816, + "src/renderer/src/components/terminal-pane/TerminalSleepingPaneOverlay.test.tsx": 387, + "src/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.test.tsx": 1462, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-attention-dispatch.test.ts": 127, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-completion-replay-guard.test.ts": 157, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-dispose-leak.test.ts": 162, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-done-quiet-window.test.ts": 163, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-hook-title-precedence.test.ts": 185, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-monitoring-turn-end.test.ts": 263, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-pending-title-inspection.test.ts": 169, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-cadence.test.ts": 202, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-process-exit-turn-boundary.test.ts": 175, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-queued-inspection-disposal.test.ts": 179, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-boundary.test.ts": 157, + "src/renderer/src/components/terminal-pane/agent-completion-coordinator-stamped-turn-replay.test.ts": 182, + "src/renderer/src/components/terminal-pane/agent-completion-no-evidence-cadence.test.ts": 1217, + "src/renderer/src/components/terminal-pane/agent-completion-poll-interval.test.ts": 114, + "src/renderer/src/components/terminal-pane/agent-completion-stale-evidence-backoff.test.ts": 98, + "src/renderer/src/components/terminal-pane/agent-completion-steady-state-opt-in.test.ts": 158, + "src/renderer/src/components/terminal-pane/agent-hook-terminal-lifecycle.test.ts": 28, + "src/renderer/src/components/terminal-pane/agent-interrupt-inference.test.ts": 145, + "src/renderer/src/components/terminal-pane/agent-launch-pane-notice.test.ts": 35, + "src/renderer/src/components/terminal-pane/agent-process-inspection-queue-rejection-containment.test.ts": 46, + "src/renderer/src/components/terminal-pane/agent-process-inspection-round.test.ts": 43, + "src/renderer/src/components/terminal-pane/agent-question-answered-inference.test.ts": 93, + "src/renderer/src/components/terminal-pane/cache-timer-seeding.test.ts": 94, + "src/renderer/src/components/terminal-pane/claude-old-terminal-banner.test.tsx": 1615, + "src/renderer/src/components/terminal-pane/closed-terminal-leaf-notice.test.ts": 781, + "src/renderer/src/components/terminal-pane/codex-backfill-error-detector.test.ts": 24, + "src/renderer/src/components/terminal-pane/codex-detached-pane-restart.test.ts": 1659, + "src/renderer/src/components/terminal-pane/codex-shared-server-banner.test.tsx": 2300, + "src/renderer/src/components/terminal-pane/codex-shared-settings-notice.test.ts": 270, + "src/renderer/src/components/terminal-pane/codex-terminal-presence.test.ts": 78, + "src/renderer/src/components/terminal-pane/codex-terminal-server-isolation-notice.test.ts": 264, + "src/renderer/src/components/terminal-pane/command-code-done-settle.test.ts": 32, + "src/renderer/src/components/terminal-pane/command-code-output-ownership.test.ts": 38, + "src/renderer/src/components/terminal-pane/compose-active-terminal-theme.test.ts": 119, + "src/renderer/src/components/terminal-pane/deferred-reattach-live-data-queue.test.ts": 197, + "src/renderer/src/components/terminal-pane/deferred-split-pane-handoff.test.ts": 53, + "src/renderer/src/components/terminal-pane/desktop-fit-fallback.test.ts": 66, + "src/renderer/src/components/terminal-pane/direct-ssh-hidden-output-restore-unavailable-banner.test.ts": 1395, + "src/renderer/src/components/terminal-pane/edge-wrapped-terminal-http-links.test.ts": 1883, + "src/renderer/src/components/terminal-pane/expand-collapse-render-stability.test.ts": 2208, + "src/renderer/src/components/terminal-pane/expand-collapse.test.ts": 97, + "src/renderer/src/components/terminal-pane/floating-file-link-selection.test.ts": 2004, + "src/renderer/src/components/terminal-pane/focus-terminal-pane-event.test.ts": 43, + "src/renderer/src/components/terminal-pane/focused-pane-rim-flash.test.ts": 33, + "src/renderer/src/components/terminal-pane/git-bash-console-capacity.test.ts": 45, + "src/renderer/src/components/terminal-pane/hard-wrapped-terminal-http-links.test.ts": 45, + "src/renderer/src/components/terminal-pane/hidden-output-restore-scheduler.test.ts": 44, + "src/renderer/src/components/terminal-pane/ipc-pty-opencode-model.test.ts": 279, + "src/renderer/src/components/terminal-pane/issue-12112-agent-pane-startup-color-reply-leak.repro.test.ts": 464, + "src/renderer/src/components/terminal-pane/issue-21762-tracking-mode-middle-click-double-paste.repro.test.ts": 287, + "src/renderer/src/components/terminal-pane/issue-4631-terminal-hover-link-provider.repro.test.ts": 886, + "src/renderer/src/components/terminal-pane/keyboard-handlers-ime-composing-chord.test.tsx": 1676, + "src/renderer/src/components/terminal-pane/keyboard-handlers-ime-enter-keyup.test.tsx": 1589, + "src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx": 2130, + "src/renderer/src/components/terminal-pane/keyboard-handlers.test.ts": 1272, + "src/renderer/src/components/terminal-pane/launch-parity-window-tab.test.ts": 4136, + "src/renderer/src/components/terminal-pane/layout-serialization.test.ts": 89, + "src/renderer/src/components/terminal-pane/leaf-scrollback-resolution.test.ts": 241, + "src/renderer/src/components/terminal-pane/legacy-relay-bridge-transport.test.ts": 271, + "src/renderer/src/components/terminal-pane/manual-terminal-worktree-park-eligibility.test.ts": 1216, + "src/renderer/src/components/terminal-pane/merge-captured-leaf-state.test.ts": 33, + "src/renderer/src/components/terminal-pane/mobile-driver-overlay-collapse.test.ts": 30, + "src/renderer/src/components/terminal-pane/mobile-driver-overlay-focus.test.ts": 36, + "src/renderer/src/components/terminal-pane/mobile-driver-overlay-visibility.test.ts": 28, + "src/renderer/src/components/terminal-pane/mouse-hide-while-typing.test.ts": 34, + "src/renderer/src/components/terminal-pane/native-chat-leaf-title-agent.test.ts": 67, + "src/renderer/src/components/terminal-pane/newer-daemon-hold-banner.test.tsx": 381, + "src/renderer/src/components/terminal-pane/osc52-clipboard-default-on-notice.test.ts": 248, + "src/renderer/src/components/terminal-pane/osc52-clipboard-toast.test.ts": 875, + "src/renderer/src/components/terminal-pane/osc52-clipboard.test.ts": 142, + "src/renderer/src/components/terminal-pane/override-affected-panes.test.ts": 39, + "src/renderer/src/components/terminal-pane/paired-parking-unverifiable-host.test.ts": 217, + "src/renderer/src/components/terminal-pane/paired-reconnect-multi-pane-materialization.test.ts": 1671, + "src/renderer/src/components/terminal-pane/pane-agent-session-id.test.ts": 42, + "src/renderer/src/components/terminal-pane/pane-delayed-agent-startup.test.ts": 138, + "src/renderer/src/components/terminal-pane/pane-foreground-agent-tracker.test.ts": 388, + "src/renderer/src/components/terminal-pane/pane-foreground-inspect-observation-identity.test.ts": 95, + "src/renderer/src/components/terminal-pane/pane-foreground-process-exit-retire.test.ts": 180, + "src/renderer/src/components/terminal-pane/pane-helpers.test.ts": 52, + "src/renderer/src/components/terminal-pane/pane-restart-transport-handoff.test.ts": 545, + "src/renderer/src/components/terminal-pane/pane-title-overlay-rects.test.ts": 46, + "src/renderer/src/components/terminal-pane/pane-title-update-global-scan-budget.test.tsx": 3319, + "src/renderer/src/components/terminal-pane/parked-terminal-buffer-capture.test.ts": 257, + "src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.test.ts": 2110, + "src/renderer/src/components/terminal-pane/parked-terminal-command-status.test.ts": 1014, + "src/renderer/src/components/terminal-pane/parked-terminal-tab-capture-episodes.test.ts": 272, + "src/renderer/src/components/terminal-pane/parse-osc7.test.ts": 31, + "src/renderer/src/components/terminal-pane/pending-pane-close-confirmation.test.ts": 2271, + "src/renderer/src/components/terminal-pane/pending-runtime-pane-close.test.ts": 1846, + "src/renderer/src/components/terminal-pane/pending-split-close.test.ts": 1758, + "src/renderer/src/components/terminal-pane/pty-buffer-serializer.test.ts": 41, + "src/renderer/src/components/terminal-pane/pty-connection-agent-session-resume.test.ts": 2230, + "src/renderer/src/components/terminal-pane/pty-connection-cold-restore-agent-resume.test.ts": 1894, + "src/renderer/src/components/terminal-pane/pty-connection-cold-restore-repaint.test.ts": 1199, + "src/renderer/src/components/terminal-pane/pty-connection-cold-restore-resume-command.test.ts": 1548, + "src/renderer/src/components/terminal-pane/pty-connection-command-finished-cleanup.test.ts": 5478, + "src/renderer/src/components/terminal-pane/pty-connection-command-finished-late-done.test.ts": 2024, + "src/renderer/src/components/terminal-pane/pty-connection-daemon-snapshot-replay.test.ts": 2341, + "src/renderer/src/components/terminal-pane/pty-connection-deferred-reattach-live-output.test.ts": 1988, + "src/renderer/src/components/terminal-pane/pty-connection-deliberate-sleep-guard.test.ts": 1631, + "src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-routing.test.ts": 2049, + "src/renderer/src/components/terminal-pane/pty-connection-foreground-agent-sampling.test.ts": 2466, + "src/renderer/src/components/terminal-pane/pty-connection-foreground-process-exit.test.ts": 834, + "src/renderer/src/components/terminal-pane/pty-connection-foreground-routing-confirmation.test.ts": 1532, + "src/renderer/src/components/terminal-pane/pty-connection-foreground-write-path.test.ts": 2036, + "src/renderer/src/components/terminal-pane/pty-connection-fresh-spawn-guards.test.ts": 2516, + "src/renderer/src/components/terminal-pane/pty-connection-hibernation-wake.test.ts": 2098, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-atlas-recovery.test.ts": 2328, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.test.ts": 2014, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-snapshot.test.ts": 2810, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.test.ts": 2345, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-delivery-gate.test.ts": 2669, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-output-restore.test.ts": 1500, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-query-snapshot-restore.test.ts": 1710, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-restore-fit-overflow.test.ts": 1000, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-live-overlap.test.ts": 1033, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-snapshot-resize-signals.test.ts": 2741, + "src/renderer/src/components/terminal-pane/pty-connection-hidden-tui-snapshot-replay.test.ts": 992, + "src/renderer/src/components/terminal-pane/pty-connection-hook-completion-bell-arbitration.test.ts": 1703, + "src/renderer/src/components/terminal-pane/pty-connection-hook-completion-side-effects.test.ts": 1942, + "src/renderer/src/components/terminal-pane/pty-connection-hook-idle-arbitration.test.ts": 1121, + "src/renderer/src/components/terminal-pane/pty-connection-interrupt-inference.test.ts": 2610, + "src/renderer/src/components/terminal-pane/pty-connection-kitty-keyboard-restore.test.ts": 1488, + "src/renderer/src/components/terminal-pane/pty-connection-main-side-effect-authority.test.ts": 2332, + "src/renderer/src/components/terminal-pane/pty-connection-mode-2031-subscriptions.test.ts": 2007, + "src/renderer/src/components/terminal-pane/pty-connection-notification-settings-gating.test.ts": 2023, + "src/renderer/src/components/terminal-pane/pty-connection-park-reveal-model-snapshot.test.ts": 1018, + "src/renderer/src/components/terminal-pane/pty-connection-pending-spawn.test.ts": 1083, + "src/renderer/src/components/terminal-pane/pty-connection-pi-synchronized-input.test.ts": 828, + "src/renderer/src/components/terminal-pane/pty-connection-post-dispose-restore-termination.test.ts": 1011, + "src/renderer/src/components/terminal-pane/pty-connection-process-exit-row-retire.test.ts": 3709, + "src/renderer/src/components/terminal-pane/pty-connection-pty-exit-teardown.test.ts": 4219, + "src/renderer/src/components/terminal-pane/pty-connection-queued-startup-consume.test.ts": 1307, + "src/renderer/src/components/terminal-pane/pty-connection-reattach-binding.test.ts": 2228, + "src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.test.ts": 2958, + "src/renderer/src/components/terminal-pane/pty-connection-remote-runtime-attach.test.ts": 2363, + "src/renderer/src/components/terminal-pane/pty-connection-remote-snapshot-source-grid.test.ts": 989, + "src/renderer/src/components/terminal-pane/pty-connection-renderer-risk-repaint.test.ts": 1994, + "src/renderer/src/components/terminal-pane/pty-connection-replay-payload-handling.test.ts": 2406, + "src/renderer/src/components/terminal-pane/pty-connection-restored-baseline-shortfall.test.ts": 1107, + "src/renderer/src/components/terminal-pane/pty-connection-runtime-owner-spawn-routing.test.ts": 1800, + "src/renderer/src/components/terminal-pane/pty-connection-session-liveness.test.ts": 2413, + "src/renderer/src/components/terminal-pane/pty-connection-setup-split-spawn.test.ts": 1980, "src/renderer/src/components/terminal-pane/pty-connection-sleeping-resume-banner.test.ts": 1242, - "src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts": 1408, - "src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts": 950, - "src/renderer/src/components/terminal-pane/pty-connection-stalled-hidden-restore.test.ts": 1743, - "src/renderer/src/components/terminal-pane/pty-connection-startup-command-delivery.test.ts": 1856, - "src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts": 2005, - "src/renderer/src/components/terminal-pane/pty-connection-terminal-input-gating.test.ts": 3182, - "src/renderer/src/components/terminal-pane/pty-connection-typed-agent-identity.test.ts": 2416, - "src/renderer/src/components/terminal-pane/pty-connection-unanswered-command-finish.test.ts": 1067, - "src/renderer/src/components/terminal-pane/pty-connection-visibility-resume-size.test.ts": 2445, - "src/renderer/src/components/terminal-pane/pty-connection-visible-pane-output-pause-latch.test.ts": 1123, - "src/renderer/src/components/terminal-pane/pty-connection-windows-cjk-repaint.test.ts": 1970, - "src/renderer/src/components/terminal-pane/pty-connection-windows-keyboard-reset.test.ts": 2271, - "src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-live-bookmark.test.ts": 132, - "src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-startup-snapshot.test.ts": 65, - "src/renderer/src/components/terminal-pane/pty-connection/disposed-spawn-retention.test.ts": 77, - "src/renderer/src/components/terminal-pane/pty-connection/empty-reattach-retire-evidence.test.ts": 60, - "src/renderer/src/components/terminal-pane/pty-connection/foreground-output-budgets.test.ts": 60, - "src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.test.ts": 99, - "src/renderer/src/components/terminal-pane/pty-connection/foreground-renderer-risk-scan-tail.test.ts": 112, - "src/renderer/src/components/terminal-pane/pty-connection/pane-launch-agent-candidate.test.ts": 51, - "src/renderer/src/components/terminal-pane/pty-connection/pane-pty-layout-binding.test.ts": 39, - "src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-register.test.ts": 139, - "src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.test.ts": 69, - "src/renderer/src/components/terminal-pane/pty-connection/pty-exit-hibernate.test.ts": 1416, - "src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.test.ts": 1490, - "src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler-park-reveal-verdict.test.ts": 189, - "src/renderer/src/components/terminal-pane/pty-connection/reattach-success-settle.test.ts": 226, - "src/renderer/src/components/terminal-pane/pty-connection/ssh-session-gone-verdict.test.ts": 46, - "src/renderer/src/components/terminal-pane/pty-connection/terminal-tab-id.test.ts": 46, - "src/renderer/src/components/terminal-pane/pty-connection/unbound-pane-spawn-recovery.test.ts": 50, - "src/renderer/src/components/terminal-pane/pty-connection/write-pty-output-to-xterm-startup-callback.test.ts": 73, - "src/renderer/src/components/terminal-pane/pty-dispatcher-delivery-interest.test.ts": 258, - "src/renderer/src/components/terminal-pane/pty-dispatcher-delivery-resync.test.ts": 155, - "src/renderer/src/components/terminal-pane/pty-dispatcher-pi-routing.test.ts": 857, - "src/renderer/src/components/terminal-pane/pty-dispatcher-push-reattach.test.ts": 160, - "src/renderer/src/components/terminal-pane/pty-input-write-queue.test.ts": 752, - "src/renderer/src/components/terminal-pane/pty-model-restore-channel.test.ts": 53, - "src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-reentrancy.test.ts": 57, - "src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-retention.test.ts": 130, - "src/renderer/src/components/terminal-pane/pty-pre-handler-buffer-warn-eviction.test.ts": 69, - "src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts": 115, - "src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.test.ts": 85, - "src/renderer/src/components/terminal-pane/pty-renderer-delivery-claims.test.ts": 79, - "src/renderer/src/components/terminal-pane/pty-shutdown-data-suspension.test.ts": 61, - "src/renderer/src/components/terminal-pane/pty-shutdown-exit-deferral.test.ts": 195, - "src/renderer/src/components/terminal-pane/pty-shutdown-output-queue.test.ts": 824, - "src/renderer/src/components/terminal-pane/pty-side-effect-pending-census.test.ts": 551, - "src/renderer/src/components/terminal-pane/pty-size-reassertion.test.ts": 56, - "src/renderer/src/components/terminal-pane/pty-size-reconcile.test.ts": 73, - "src/renderer/src/components/terminal-pane/pty-transport-connect-spawn.test.ts": 1160, - "src/renderer/src/components/terminal-pane/pty-transport-detach-attach-handoff.test.ts": 814, - "src/renderer/src/components/terminal-pane/pty-transport-eager-buffer-replay.test.ts": 1299, - "src/renderer/src/components/terminal-pane/pty-transport-handler-suspension.test.ts": 738, - "src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts": 2267, - "src/renderer/src/components/terminal-pane/pty-transport-output-side-effects.test.ts": 1185, - "src/renderer/src/components/terminal-pane/pty-transport-pi-coalesce.test.ts": 484, - "src/renderer/src/components/terminal-pane/pty-transport-pi-spinner.test.ts": 500, - "src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts": 1113, - "src/renderer/src/components/terminal-pane/pty-transport-recycled-pty-incarnation.test.ts": 591, - "src/renderer/src/components/terminal-pane/pty-transport-spawn-errors.test.ts": 773, - "src/renderer/src/components/terminal-pane/pty-transport-user-input.test.ts": 539, - "src/renderer/src/components/terminal-pane/regular-terminal-focus-ownership.test.ts": 185, - "src/renderer/src/components/terminal-pane/remote-desktop-viewport-claim.test.ts": 46, - "src/renderer/src/components/terminal-pane/remote-execution-host-pty.test.ts": 1319, - "src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts": 1795, - "src/renderer/src/components/terminal-pane/remote-hidden-output-restore-unavailable-banner.repro.test.ts": 1450, - "src/renderer/src/components/terminal-pane/remote-layout-scrollback-retention.test.ts": 64, - "src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts": 71, - "src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts": 988, - "src/renderer/src/components/terminal-pane/remote-runtime-connect-failure-recovery.test.ts": 1519, - "src/renderer/src/components/terminal-pane/remote-runtime-disconnected-input-grace-retry.test.ts": 2324, - "src/renderer/src/components/terminal-pane/remote-runtime-disconnected-input-grace.test.ts": 43, - "src/renderer/src/components/terminal-pane/remote-runtime-error-surface-dismissal.test.ts": 1823, - "src/renderer/src/components/terminal-pane/remote-runtime-input-journal.test.ts": 70, - "src/renderer/src/components/terminal-pane/remote-runtime-outage-toast-flood-and-stuck-reconnect.test.ts": 2091, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-batching.test.ts": 105, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-deadline-reattach.test.ts": 3257, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-latched-pane-retention.test.ts": 5440, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-query-reply-immediate.test.ts": 2376, + "src/renderer/src/components/terminal-pane/pty-connection-spawn-deferred-sleep.test.ts": 1332, + "src/renderer/src/components/terminal-pane/pty-connection-spawn-left-pane-unbound.test.ts": 1152, + "src/renderer/src/components/terminal-pane/pty-connection-split-cwd-resolution.test.ts": 837, + "src/renderer/src/components/terminal-pane/pty-connection-stalled-hidden-restore.test.ts": 2042, + "src/renderer/src/components/terminal-pane/pty-connection-startup-command-delivery.test.ts": 1958, + "src/renderer/src/components/terminal-pane/pty-connection-startup-draft-delivery.test.ts": 1095, + "src/renderer/src/components/terminal-pane/pty-connection-task-complete-dispatch.test.ts": 2131, + "src/renderer/src/components/terminal-pane/pty-connection-terminal-input-gating.test.ts": 2601, + "src/renderer/src/components/terminal-pane/pty-connection-typed-agent-identity.test.ts": 2782, + "src/renderer/src/components/terminal-pane/pty-connection-unanswered-command-finish.test.ts": 1232, + "src/renderer/src/components/terminal-pane/pty-connection-unserved-ssh-pane.test.ts": 1465, + "src/renderer/src/components/terminal-pane/pty-connection-visibility-resume-size.test.ts": 2668, + "src/renderer/src/components/terminal-pane/pty-connection-visible-pane-output-pause-latch.test.ts": 1046, + "src/renderer/src/components/terminal-pane/pty-connection-windows-cjk-repaint.test.ts": 2028, + "src/renderer/src/components/terminal-pane/pty-connection-windows-keyboard-reset.test.ts": 2312, + "src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-live-bookmark.test.ts": 298, + "src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-newer-daemon-hold.test.ts": 558, + "src/renderer/src/components/terminal-pane/pty-connection/deferred-session-reattach-startup-snapshot.test.ts": 82, + "src/renderer/src/components/terminal-pane/pty-connection/disposed-spawn-retention.test.ts": 59, + "src/renderer/src/components/terminal-pane/pty-connection/empty-reattach-retire-evidence.test.ts": 36, + "src/renderer/src/components/terminal-pane/pty-connection/foreground-output-budgets.test.ts": 28, + "src/renderer/src/components/terminal-pane/pty-connection/foreground-output-scan.test.ts": 84, + "src/renderer/src/components/terminal-pane/pty-connection/foreground-renderer-risk-scan-tail.test.ts": 98, + "src/renderer/src/components/terminal-pane/pty-connection/pane-launch-agent-candidate.test.ts": 70, + "src/renderer/src/components/terminal-pane/pty-connection/pane-pty-layout-binding.test.ts": 25, + "src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-register.test.ts": 128, + "src/renderer/src/components/terminal-pane/pty-connection/park-reveal-snapshot-verdict.test.ts": 53, + "src/renderer/src/components/terminal-pane/pty-connection/pty-exit-hibernate.test.ts": 1307, + "src/renderer/src/components/terminal-pane/pty-connection/reattach-payload-context.test.ts": 1411, + "src/renderer/src/components/terminal-pane/pty-connection/reattach-result-handler-park-reveal-verdict.test.ts": 144, + "src/renderer/src/components/terminal-pane/pty-connection/reattach-success-settle.test.ts": 145, + "src/renderer/src/components/terminal-pane/pty-connection/ssh-session-gone-verdict.test.ts": 42, + "src/renderer/src/components/terminal-pane/pty-connection/terminal-tab-id.test.ts": 25, + "src/renderer/src/components/terminal-pane/pty-connection/unbound-pane-spawn-recovery.test.ts": 48, + "src/renderer/src/components/terminal-pane/pty-connection/write-pty-output-to-xterm-startup-callback.test.ts": 81, + "src/renderer/src/components/terminal-pane/pty-dispatcher-delivery-interest.test.ts": 162, + "src/renderer/src/components/terminal-pane/pty-dispatcher-delivery-resync.test.ts": 125, + "src/renderer/src/components/terminal-pane/pty-dispatcher-pi-routing.test.ts": 901, + "src/renderer/src/components/terminal-pane/pty-dispatcher-push-reattach.test.ts": 125, + "src/renderer/src/components/terminal-pane/pty-input-write-queue.test.ts": 849, + "src/renderer/src/components/terminal-pane/pty-model-restore-channel.test.ts": 35, + "src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-reentrancy.test.ts": 30, + "src/renderer/src/components/terminal-pane/pty-output-side-effect-queue-retention.test.ts": 114, + "src/renderer/src/components/terminal-pane/pty-pre-handler-buffer-warn-eviction.test.ts": 46, + "src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts": 95, + "src/renderer/src/components/terminal-pane/pty-preconnect-input-buffer.test.ts": 87, + "src/renderer/src/components/terminal-pane/pty-renderer-delivery-claims.test.ts": 50, + "src/renderer/src/components/terminal-pane/pty-shutdown-data-suspension.test.ts": 45, + "src/renderer/src/components/terminal-pane/pty-shutdown-exit-deferral.test.ts": 168, + "src/renderer/src/components/terminal-pane/pty-shutdown-output-queue.test.ts": 799, + "src/renderer/src/components/terminal-pane/pty-side-effect-pending-census.test.ts": 220, + "src/renderer/src/components/terminal-pane/pty-size-reassertion.test.ts": 57, + "src/renderer/src/components/terminal-pane/pty-size-reconcile.test.ts": 60, + "src/renderer/src/components/terminal-pane/pty-transport-connect-spawn.test.ts": 1450, + "src/renderer/src/components/terminal-pane/pty-transport-detach-attach-handoff.test.ts": 876, + "src/renderer/src/components/terminal-pane/pty-transport-eager-buffer-replay.test.ts": 1129, + "src/renderer/src/components/terminal-pane/pty-transport-handler-suspension.test.ts": 851, + "src/renderer/src/components/terminal-pane/pty-transport-input-write.test.ts": 2740, + "src/renderer/src/components/terminal-pane/pty-transport-output-side-effects.test.ts": 909, + "src/renderer/src/components/terminal-pane/pty-transport-pi-coalesce.test.ts": 495, + "src/renderer/src/components/terminal-pane/pty-transport-pi-spinner.test.ts": 513, + "src/renderer/src/components/terminal-pane/pty-transport-reattach-admission.test.ts": 983, + "src/renderer/src/components/terminal-pane/pty-transport-recycled-pty-incarnation.test.ts": 585, + "src/renderer/src/components/terminal-pane/pty-transport-spawn-errors.test.ts": 796, + "src/renderer/src/components/terminal-pane/pty-transport-spawn-intent.test.ts": 446, + "src/renderer/src/components/terminal-pane/pty-transport-user-input.test.ts": 627, + "src/renderer/src/components/terminal-pane/regular-terminal-focus-ownership.test.ts": 175, + "src/renderer/src/components/terminal-pane/remote-desktop-viewport-claim.test.ts": 32, + "src/renderer/src/components/terminal-pane/remote-execution-host-pty.test.ts": 1322, + "src/renderer/src/components/terminal-pane/remote-hidden-output-restore-outcomes.test.ts": 1881, + "src/renderer/src/components/terminal-pane/remote-hidden-output-restore-unavailable-banner.repro.test.ts": 1319, + "src/renderer/src/components/terminal-pane/remote-layout-scrollback-retention.test.ts": 60, + "src/renderer/src/components/terminal-pane/remote-pane-layout-push.test.ts": 52, + "src/renderer/src/components/terminal-pane/remote-park-reveal-unverifiable-retry.test.ts": 896, + "src/renderer/src/components/terminal-pane/remote-runtime-connect-failure-recovery.test.ts": 1728, + "src/renderer/src/components/terminal-pane/remote-runtime-disconnected-input-grace-retry.test.ts": 2507, + "src/renderer/src/components/terminal-pane/remote-runtime-disconnected-input-grace.test.ts": 29, + "src/renderer/src/components/terminal-pane/remote-runtime-error-surface-dismissal.test.ts": 1408, + "src/renderer/src/components/terminal-pane/remote-runtime-input-journal.test.ts": 59, + "src/renderer/src/components/terminal-pane/remote-runtime-outage-toast-flood-and-stuck-reconnect.test.ts": 2228, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-batching.test.ts": 86, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-deadline-reattach.test.ts": 3465, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-latched-pane-retention.test.ts": 6446, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-query-reply-immediate.test.ts": 2001, "src/renderer/src/components/terminal-pane/remote-runtime-pty-recovery-state.test.ts": 70, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-snapshot-escape-tail.test.ts": 1605, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-snapshot-source-grid.test.ts": 1617, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-activation-inventory-fallback.test.ts": 2219, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-attach-subscription.test.ts": 3968, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-handoff.test.ts": 4354, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-outcome-recovery.test.ts": 3437, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-end-verdict.test.ts": 1769, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-expired-pane-recovery.test.ts": 2693, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-absence-evidence.test.ts": 1553, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-session-launch.test.ts": 3458, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-surface-replacement.test.ts": 9187, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-coalescing.test.ts": 4890, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-fallback.test.ts": 2895, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-replay.test.ts": 8016, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pairing-rotation.test.ts": 3856, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-binding-ownership.test.ts": 2736, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-handle-resolution.test.ts": 4764, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pending-host-surface-attach.test.ts": 3084, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-reattach-type-ahead.test.ts": 1622, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-snapshot-replay.test.ts": 2962, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-split-leaf-activation.test.ts": 3183, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stale-handle-recovery.test.ts": 2943, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-sticky-replacement-policy.test.ts": 2889, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stream-reconnect.test.ts": 5167, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-unpublished-host-graph.test.ts": 3061, - "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-web-mirror-recovery.test.ts": 3266, - "src/renderer/src/components/terminal-pane/remote-runtime-recovery-input-hold.test.ts": 275, - "src/renderer/src/components/terminal-pane/remote-runtime-resubscribe-failure-recovery-routing.test.ts": 3078, - "src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-host-chain.test.ts": 10249, - "src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-replay.test.ts": 1906, - "src/renderer/src/components/terminal-pane/remote-snapshot-history-seam.test.ts": 3139, - "src/renderer/src/components/terminal-pane/renderer-owned-agent-status-registry.test.ts": 49, - "src/renderer/src/components/terminal-pane/replay-guard.test.ts": 127, - "src/renderer/src/components/terminal-pane/replayed-scrollback-store-release.test.ts": 303, - "src/renderer/src/components/terminal-pane/repro-8299-shift-space-input-source.test.ts": 122, - "src/renderer/src/components/terminal-pane/repro-8832-url-next-line.test.ts": 1708, - "src/renderer/src/components/terminal-pane/request-active-terminal-pane-close.test.ts": 178, - "src/renderer/src/components/terminal-pane/resolve-split-cwd.test.ts": 1372, - "src/renderer/src/components/terminal-pane/restored-snapshot-coverage.test.ts": 50, - "src/renderer/src/components/terminal-pane/scan22-ssh-new-tab-disposed-spawn-kill.repro.test.ts": 412, - "src/renderer/src/components/terminal-pane/session-restored-banner-pane-state.test.tsx": 270, - "src/renderer/src/components/terminal-pane/shutdown-buffer-captures.test.ts": 50, - "src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.test.ts": 60, - "src/renderer/src/components/terminal-pane/split-right-white-screen.test.ts": 58, - "src/renderer/src/components/terminal-pane/stale-document-visibility.test.ts": 90, - "src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.test.ts": 74, - "src/renderer/src/components/terminal-pane/terminal-agent-session-continuation.test.ts": 70, - "src/renderer/src/components/terminal-pane/terminal-agent-session-fork.test.ts": 1153, - "src/renderer/src/components/terminal-pane/terminal-alternate-screen-parse.test.ts": 82, - "src/renderer/src/components/terminal-pane/terminal-appearance.test.ts": 341, - "src/renderer/src/components/terminal-pane/terminal-attention-surface.test.ts": 1370, - "src/renderer/src/components/terminal-pane/terminal-bracketed-paste.test.ts": 68, - "src/renderer/src/components/terminal-pane/terminal-capability-replies.test.ts": 147, - "src/renderer/src/components/terminal-pane/terminal-capped-buffer-retention.test.ts": 646, - "src/renderer/src/components/terminal-pane/terminal-captured-input-dispatch.test.ts": 54, - "src/renderer/src/components/terminal-pane/terminal-chat-ownership-lifecycle.test.tsx": 834, - "src/renderer/src/components/terminal-pane/terminal-cjk-cursor-cell-placement.test.ts": 325, - "src/renderer/src/components/terminal-pane/terminal-clipboard-event-paste.test.ts": 55, - "src/renderer/src/components/terminal-pane/terminal-clipboard-paste.test.ts": 117, - "src/renderer/src/components/terminal-pane/terminal-clipboard-selection-text.test.ts": 67, - "src/renderer/src/components/terminal-pane/terminal-cold-park-exempt-flip.react185.test.tsx": 1752, - "src/renderer/src/components/terminal-pane/terminal-cold-park-pre-gate-loop.react185.test.tsx": 906, - "src/renderer/src/components/terminal-pane/terminal-cold-park-recheck-deadlines.test.ts": 1436, - "src/renderer/src/components/terminal-pane/terminal-cold-park-subscription-narrowing.react185.test.tsx": 556, - "src/renderer/src/components/terminal-pane/terminal-cold-park-tab-model-identity.react185.test.tsx": 890, - "src/renderer/src/components/terminal-pane/terminal-cold-park-timer-rearm.test.tsx": 566, - "src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx": 583, - "src/renderer/src/components/terminal-pane/terminal-cold-park-withheld-tabs.test.ts": 69, - "src/renderer/src/components/terminal-pane/terminal-command-lifecycle.test.ts": 42, - "src/renderer/src/components/terminal-pane/terminal-context-menu-dismiss.test.ts": 44, - "src/renderer/src/components/terminal-pane/terminal-copy-rejection-handling.test.ts": 43, - "src/renderer/src/components/terminal-pane/terminal-copy-shortcut.test.ts": 1732, - "src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.test.ts": 1612, - "src/renderer/src/components/terminal-pane/terminal-ctrl-enter.test.ts": 107, - "src/renderer/src/components/terminal-pane/terminal-cursor-appearance-precedence.test.ts": 317, - "src/renderer/src/components/terminal-pane/terminal-cursor-inactive-style.test.ts": 309, - "src/renderer/src/components/terminal-pane/terminal-dead-session-reconcile.test.ts": 61, - "src/renderer/src/components/terminal-pane/terminal-delivery-watchdog.test.ts": 117, - "src/renderer/src/components/terminal-pane/terminal-drop-handler.test.ts": 436, - "src/renderer/src/components/terminal-pane/terminal-drop-image-path.test.ts": 64, - "src/renderer/src/components/terminal-pane/terminal-drop-internal-handler.test.ts": 568, - "src/renderer/src/components/terminal-pane/terminal-drop-local-workspace.test.ts": 574, - "src/renderer/src/components/terminal-pane/terminal-drop-pane-resolution.test.ts": 141, - "src/renderer/src/components/terminal-pane/terminal-drop-path-writer.test.ts": 120, - "src/renderer/src/components/terminal-pane/terminal-drop-runtime-catalog-owner.test.ts": 679, - "src/renderer/src/components/terminal-pane/terminal-drop-shell.test.ts": 60, - "src/renderer/src/components/terminal-pane/terminal-drop-source-owner.test.ts": 509, - "src/renderer/src/components/terminal-pane/terminal-drop-transport-owner.test.ts": 338, - "src/renderer/src/components/terminal-pane/terminal-drop-upload-report.test.ts": 50, - "src/renderer/src/components/terminal-pane/terminal-drop-worktree-path.test.ts": 317, - "src/renderer/src/components/terminal-pane/terminal-drop-write-failure.test.ts": 52, - "src/renderer/src/components/terminal-pane/terminal-error-accumulation.test.ts": 127, - "src/renderer/src/components/terminal-pane/terminal-error-remote-closed-localization.test.ts": 93, - "src/renderer/src/components/terminal-pane/terminal-eviction-exempt-tabs.test.ts": 880, - "src/renderer/src/components/terminal-pane/terminal-file-link-actions.test.ts": 369, - "src/renderer/src/components/terminal-pane/terminal-file-link-target.test.ts": 1292, - "src/renderer/src/components/terminal-pane/terminal-fit-restore.test.ts": 88, - "src/renderer/src/components/terminal-pane/terminal-freeze-report.test.ts": 107, - "src/renderer/src/components/terminal-pane/terminal-handle-copy.test.ts": 63, - "src/renderer/src/components/terminal-pane/terminal-handle-links.test.ts": 412, - "src/renderer/src/components/terminal-pane/terminal-hidden-restore-scrollback.test.ts": 32, - "src/renderer/src/components/terminal-pane/terminal-hidden-view-parking.test.ts": 1508, - "src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.test.ts": 1920, - "src/renderer/src/components/terminal-pane/terminal-http-link-activation.test.ts": 369, - "src/renderer/src/components/terminal-pane/terminal-http-link-source-owner.test.ts": 95, - "src/renderer/src/components/terminal-pane/terminal-http-url-extraction.test.ts": 47, - "src/renderer/src/components/terminal-pane/terminal-ime-candidate-key-release-guard.test.ts": 58, - "src/renderer/src/components/terminal-pane/terminal-ime-composer-placeholder-mask.test.ts": 780, - "src/renderer/src/components/terminal-pane/terminal-ime-composition-route.test.ts": 157, - "src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts": 147, - "src/renderer/src/components/terminal-pane/terminal-ime-composition-transaction-ownership.test.ts": 165, - "src/renderer/src/components/terminal-pane/terminal-ime-deferred-chord.test.ts": 159, - "src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.test.ts": 230, - "src/renderer/src/components/terminal-pane/terminal-ime-forwarder-space-claim.test.ts": 226, - "src/renderer/src/components/terminal-pane/terminal-ime-hangul-syllable-flush.test.ts": 688, - "src/renderer/src/components/terminal-pane/terminal-ime-hangul-terminating-digit.test.ts": 211, - "src/renderer/src/components/terminal-pane/terminal-ime-input-context-refresh.test.ts": 147, - "src/renderer/src/components/terminal-pane/terminal-ime-linux-candidate-state.test.ts": 77, - "src/renderer/src/components/terminal-pane/terminal-ime-linux-claimed-preedit-release.test.ts": 174, - "src/renderer/src/components/terminal-pane/terminal-ime-linux-claimed-preedit-selector.test.ts": 81, - "src/renderer/src/components/terminal-pane/terminal-ime-macos-keybinding-dict-trace.test.ts": 266, - "src/renderer/src/components/terminal-pane/terminal-ime-native-text-forwarder.test.ts": 233, - "src/renderer/src/components/terminal-pane/terminal-ime-substituted-text-commit.test.ts": 438, - "src/renderer/src/components/terminal-pane/terminal-ime-user-input-provenance.test.ts": 337, - "src/renderer/src/components/terminal-pane/terminal-ime-won-composition-order.test.ts": 454, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-adversarial.test.ts": 2315, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-cancelled-preedit-visibility.test.ts": 385, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-cancel.test.ts": 462, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts": 1414, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-consumed-key-commit.test.ts": 532, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-korean-enter-commit-order.test.ts": 382, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-linux-native-trace-replay.test.ts": 4041, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-midline-preedit-tail.test.ts": 876, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-preedit-cell-grid.test.ts": 1043, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-resumed-preedit-visibility.test.ts": 420, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-trailing-preedit-occlusion.test.ts": 586, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-transaction-events.test.ts": 550, - "src/renderer/src/components/terminal-pane/terminal-ime-xterm-windows-resumed-preedit-trace.test.ts": 728, - "src/renderer/src/components/terminal-pane/terminal-inline-image-da1.test.ts": 123, - "src/renderer/src/components/terminal-pane/terminal-input-host-platform.test.ts": 1406, - "src/renderer/src/components/terminal-pane/terminal-input-quarantine.test.ts": 53, - "src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit-coexistence.test.ts": 786, - "src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit-device-trace.test.ts": 562, - "src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit.test.ts": 634, - "src/renderer/src/components/terminal-pane/terminal-jis-yen-input.test.ts": 47, - "src/renderer/src/components/terminal-pane/terminal-keyboard-event-handlers-focus.test.ts": 2037, - "src/renderer/src/components/terminal-pane/terminal-keyboard-native-chat-cover.test.ts": 1797, - "src/renderer/src/components/terminal-pane/terminal-keyboard-pane-resolution.test.ts": 140, - "src/renderer/src/components/terminal-pane/terminal-keyboard-protocol-pane-agent.test.ts": 57, - "src/renderer/src/components/terminal-pane/terminal-keyboard-search-dispatch.test.ts": 1809, - "src/renderer/src/components/terminal-pane/terminal-kitty-csi-u-encoding.test.ts": 69, - "src/renderer/src/components/terminal-pane/terminal-layout-duplicate-pty-replay.test.ts": 73, - "src/renderer/src/components/terminal-pane/terminal-layout-gesture-persist.test.tsx": 356, - "src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.test.ts": 56, - "src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.test.ts": 57, - "src/renderer/src/components/terminal-pane/terminal-layout-overlay-focus.test.tsx": 190, - "src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership-depth.test.ts": 98, - "src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.test.ts": 118, - "src/renderer/src/components/terminal-pane/terminal-link-action-routing.test.ts": 1850, - "src/renderer/src/components/terminal-pane/terminal-link-activation.test.ts": 319, - "src/renderer/src/components/terminal-pane/terminal-link-click-behavior.test.ts": 55, - "src/renderer/src/components/terminal-pane/terminal-link-click-fallback.test.ts": 770, - "src/renderer/src/components/terminal-pane/terminal-link-client-local-file.test.ts": 704, - "src/renderer/src/components/terminal-pane/terminal-link-escaping-file-open.test.ts": 588, - "src/renderer/src/components/terminal-pane/terminal-link-file-open-routing.test.ts": 780, - "src/renderer/src/components/terminal-pane/terminal-link-host-workspace-file.test.ts": 879, - "src/renderer/src/components/terminal-pane/terminal-link-missing-file-open.test.ts": 820, - "src/renderer/src/components/terminal-pane/terminal-link-open-hints.test.ts": 297, - "src/renderer/src/components/terminal-pane/terminal-link-osc-file-targets.test.ts": 767, - "src/renderer/src/components/terminal-pane/terminal-link-osc-url-routing.test.ts": 645, - "src/renderer/src/components/terminal-pane/terminal-link-pointer-gesture.test.ts": 485, - "src/renderer/src/components/terminal-pane/terminal-link-provider-batching.test.ts": 737, - "src/renderer/src/components/terminal-pane/terminal-link-provider-link-detection.test.ts": 929, - "src/renderer/src/components/terminal-pane/terminal-link-pty-mouse-suppression.test.ts": 461, - "src/renderer/src/components/terminal-pane/terminal-link-remote-runtime-ssh-open.test.ts": 509, - "src/renderer/src/components/terminal-pane/terminal-link-same-id-ssh-file-host.test.ts": 735, - "src/renderer/src/components/terminal-pane/terminal-link-worktree-root-activation.test.ts": 763, - "src/renderer/src/components/terminal-pane/terminal-link-wsl-path-mapping.test.ts": 723, - "src/renderer/src/components/terminal-pane/terminal-linkifier-click-priming.test.ts": 317, - "src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.test.ts": 1358, - "src/renderer/src/components/terminal-pane/terminal-native-copy-gutter.test.ts": 159, - "src/renderer/src/components/terminal-pane/terminal-non-latin-control-chord.test.ts": 59, - "src/renderer/src/components/terminal-pane/terminal-notification-state.test.ts": 1566, - "src/renderer/src/components/terminal-pane/terminal-option-kitty-release.test.ts": 66, - "src/renderer/src/components/terminal-pane/terminal-option-punctuation.test.ts": 74, - "src/renderer/src/components/terminal-pane/terminal-output-visibility.test.ts": 56, - "src/renderer/src/components/terminal-pane/terminal-pane-attention-subscriptions.test.ts": 67, - "src/renderer/src/components/terminal-pane/terminal-pane-close-identity.test.ts": 55, - "src/renderer/src/components/terminal-pane/terminal-pane-element-file-drop.test.tsx": 1023, - "src/renderer/src/components/terminal-pane/terminal-pane-host-state-memo.test.ts": 1887, - "src/renderer/src/components/terminal-pane/terminal-pane-host-state.test.ts": 1520, - "src/renderer/src/components/terminal-pane/terminal-pane-manager-options-gesture.test.ts": 2408, - "src/renderer/src/components/terminal-pane/terminal-pane-recovery-unsettled-fallback.test.ts": 568, - "src/renderer/src/components/terminal-pane/terminal-pane-recovery.test.ts": 488, - "src/renderer/src/components/terminal-pane/terminal-pane-spawn-placement.test.ts": 559, - "src/renderer/src/components/terminal-pane/terminal-pane-split-completion.test.ts": 47, - "src/renderer/src/components/terminal-pane/terminal-pane-split-request-routing.test.ts": 151, - "src/renderer/src/components/terminal-pane/terminal-pane-split-with-inherited-cwd.test.ts": 87, - "src/renderer/src/components/terminal-pane/terminal-pane-split-writer-paths.test.ts": 1735, - "src/renderer/src/components/terminal-pane/terminal-pane-store-subscription-budget.test.tsx": 6838, - "src/renderer/src/components/terminal-pane/terminal-pane-surface-ownership.test.ts": 335, - "src/renderer/src/components/terminal-pane/terminal-pane-tab-detach-commit.test.ts": 118, - "src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts": 109, - "src/renderer/src/components/terminal-pane/terminal-park-verdict-flip-telemetry.test.ts": 132, - "src/renderer/src/components/terminal-pane/terminal-park-verdict-worktree-driver-loop.test.tsx": 467, - "src/renderer/src/components/terminal-pane/terminal-parked-pane-candidates.test.ts": 1644, - "src/renderer/src/components/terminal-pane/terminal-parked-tab-eviction-exemption.test.ts": 342, - "src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers-batch-sync.test.ts": 454, - "src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.test.ts": 464, - "src/renderer/src/components/terminal-pane/terminal-parked-watcher-partial-reconciliation.test.ts": 415, - "src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts": 307, - "src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.test.ts": 341, - "src/renderer/src/components/terminal-pane/terminal-parked-watcher-sleep-preserved-exit.test.ts": 404, - "src/renderer/src/components/terminal-pane/terminal-parked-watcher-sole-newborn-exit.test.ts": 368, - "src/renderer/src/components/terminal-pane/terminal-parking-e2e-overrides.test.ts": 2849, - "src/renderer/src/components/terminal-pane/terminal-parser-handler-guard.test.ts": 85, - "src/renderer/src/components/terminal-pane/terminal-paste-coordinator.test.ts": 153, - "src/renderer/src/components/terminal-pane/terminal-paste-executor-default-yield.test.ts": 58, - "src/renderer/src/components/terminal-pane/terminal-paste-multiline-policy.test.ts": 68, - "src/renderer/src/components/terminal-pane/terminal-paste-operation-order.test.ts": 1390, - "src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts": 59, - "src/renderer/src/components/terminal-pane/terminal-paste-target-state.test.ts": 58, - "src/renderer/src/components/terminal-pane/terminal-path-existence-batch.test.ts": 70, - "src/renderer/src/components/terminal-pane/terminal-process-exit-restart.test.ts": 42, - "src/renderer/src/components/terminal-pane/terminal-programmatic-text-paste.test.ts": 1433, - "src/renderer/src/components/terminal-pane/terminal-pty-ack-gate.test.ts": 155, - "src/renderer/src/components/terminal-pane/terminal-pty-paste-writer.test.ts": 57, - "src/renderer/src/components/terminal-pane/terminal-quick-command-dispatch.test.ts": 61, - "src/renderer/src/components/terminal-pane/terminal-remote-runtime-recovery-ui-state.test.ts": 42, - "src/renderer/src/components/terminal-pane/terminal-render-desync-evidence-persistence.test.ts": 49, - "src/renderer/src/components/terminal-pane/terminal-render-desync-frame.test.ts": 68, - "src/renderer/src/components/terminal-pane/terminal-render-desync-sentinel.test.ts": 458, - "src/renderer/src/components/terminal-pane/terminal-render-desync-weight-probe.test.ts": 63, - "src/renderer/src/components/terminal-pane/terminal-renderer-policy.test.ts": 118, - "src/renderer/src/components/terminal-pane/terminal-replay-application-continuity.test.ts": 393, - "src/renderer/src/components/terminal-pane/terminal-replay-cursor-state.test.ts": 130, - "src/renderer/src/components/terminal-pane/terminal-restore-sgr-latch.test.ts": 155, - "src/renderer/src/components/terminal-pane/terminal-restored-viewport.test.ts": 66, - "src/renderer/src/components/terminal-pane/terminal-retention-churn-pins.test.tsx": 643, - "src/renderer/src/components/terminal-pane/terminal-retention-exempt-growth.test.ts": 2874, - "src/renderer/src/components/terminal-pane/terminal-runtime-host-link-routing.test.ts": 2218, - "src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts": 1451, - "src/renderer/src/components/terminal-pane/terminal-selection-fit-guard.test.ts": 45, - "src/renderer/src/components/terminal-pane/terminal-shortcut-ctrl-arrow.test.ts": 81, - "src/renderer/src/components/terminal-pane/terminal-shortcut-option-compose.test.ts": 154, - "src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts": 132, - "src/renderer/src/components/terminal-pane/terminal-shortcut-select-all.test.ts": 72, - "src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts": 209, - "src/renderer/src/components/terminal-pane/terminal-side-effect-facts-handler.test.ts": 79, - "src/renderer/src/components/terminal-pane/terminal-snapshot-replay-paint.test.ts": 481, - "src/renderer/src/components/terminal-pane/terminal-spawn-error-display.test.ts": 52, - "src/renderer/src/components/terminal-pane/terminal-startup-command-classifier.test.ts": 53, - "src/renderer/src/components/terminal-pane/terminal-startup-grid-settle.test.ts": 64, - "src/renderer/src/components/terminal-pane/terminal-startup-timing-scheduler.test.ts": 116, - "src/renderer/src/components/terminal-pane/terminal-startup-timing.test.ts": 64, - "src/renderer/src/components/terminal-pane/terminal-tab-agent-type-index-retention.test.ts": 2034, - "src/renderer/src/components/terminal-pane/terminal-tab-agent-type-index.test.ts": 71, - "src/renderer/src/components/terminal-pane/terminal-tab-lookup.test.ts": 44, - "src/renderer/src/components/terminal-pane/terminal-title-evidence.test.ts": 102, - "src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts": 449, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-snapshot-escape-tail.test.ts": 1542, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-snapshot-source-grid.test.ts": 1472, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-activation-inventory-fallback.test.ts": 2816, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-attach-subscription.test.ts": 4180, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-handoff.test.ts": 4075, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-create-outcome-recovery.test.ts": 3481, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-end-verdict.test.ts": 1667, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-expired-pane-recovery.test.ts": 2665, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-absence-evidence.test.ts": 1536, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-session-launch.test.ts": 3381, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-host-surface-replacement.test.ts": 8290, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-coalescing.test.ts": 4827, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-fallback.test.ts": 2959, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-input-replay.test.ts": 8142, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pairing-rotation.test.ts": 6272, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-binding-ownership.test.ts": 2821, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pane-handle-resolution.test.ts": 5252, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-pending-host-surface-attach.test.ts": 3028, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-reattach-type-ahead.test.ts": 4289, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-snapshot-replay.test.ts": 3189, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-spawn-intent.test.ts": 1814, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-split-leaf-activation.test.ts": 3239, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stale-handle-recovery.test.ts": 3486, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-sticky-replacement-policy.test.ts": 3006, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-stream-reconnect.test.ts": 5774, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-unmounted-host-pane.test.ts": 2049, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-unpublished-host-graph.test.ts": 2668, + "src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-web-mirror-recovery.test.ts": 3376, + "src/renderer/src/components/terminal-pane/remote-runtime-recovery-input-hold.test.ts": 346, + "src/renderer/src/components/terminal-pane/remote-runtime-resubscribe-failure-recovery-routing.test.ts": 3138, + "src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-host-chain.test.ts": 9361, + "src/renderer/src/components/terminal-pane/remote-snapshot-alt-screen-replay.test.ts": 2220, + "src/renderer/src/components/terminal-pane/remote-snapshot-history-seam.test.ts": 3278, + "src/renderer/src/components/terminal-pane/renderer-owned-agent-status-registry.test.ts": 40, + "src/renderer/src/components/terminal-pane/replay-guard.test.ts": 80, + "src/renderer/src/components/terminal-pane/replayed-scrollback-store-release.test.ts": 278, + "src/renderer/src/components/terminal-pane/repro-8299-shift-space-input-source.test.ts": 95, + "src/renderer/src/components/terminal-pane/repro-8832-url-next-line.test.ts": 1712, + "src/renderer/src/components/terminal-pane/request-active-terminal-pane-close.test.ts": 169, + "src/renderer/src/components/terminal-pane/resolve-split-cwd.test.ts": 1275, + "src/renderer/src/components/terminal-pane/restored-snapshot-coverage.test.ts": 39, + "src/renderer/src/components/terminal-pane/scan22-ssh-new-tab-disposed-spawn-kill.repro.test.ts": 415, + "src/renderer/src/components/terminal-pane/session-restored-banner-pane-state.test.tsx": 229, + "src/renderer/src/components/terminal-pane/shutdown-buffer-captures.test.ts": 28, + "src/renderer/src/components/terminal-pane/sleeping-record-park-exemption.test.ts": 54, + "src/renderer/src/components/terminal-pane/split-right-white-screen.test.ts": 74, + "src/renderer/src/components/terminal-pane/stale-document-visibility.test.ts": 61, + "src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.test.ts": 66, + "src/renderer/src/components/terminal-pane/terminal-agent-session-continuation.test.ts": 66, + "src/renderer/src/components/terminal-pane/terminal-agent-session-fork.test.ts": 1061, + "src/renderer/src/components/terminal-pane/terminal-alternate-screen-parse.test.ts": 65, + "src/renderer/src/components/terminal-pane/terminal-appearance.test.ts": 384, + "src/renderer/src/components/terminal-pane/terminal-attention-surface.test.ts": 1469, + "src/renderer/src/components/terminal-pane/terminal-bracketed-paste.test.ts": 42, + "src/renderer/src/components/terminal-pane/terminal-capability-replies.test.ts": 108, + "src/renderer/src/components/terminal-pane/terminal-capped-buffer-retention.test.ts": 716, + "src/renderer/src/components/terminal-pane/terminal-captured-input-dispatch.test.ts": 41, + "src/renderer/src/components/terminal-pane/terminal-chat-ownership-lifecycle.test.tsx": 850, + "src/renderer/src/components/terminal-pane/terminal-cjk-cursor-cell-placement.test.ts": 298, + "src/renderer/src/components/terminal-pane/terminal-clipboard-event-paste.test.ts": 42, + "src/renderer/src/components/terminal-pane/terminal-clipboard-paste.test.ts": 73, + "src/renderer/src/components/terminal-pane/terminal-clipboard-selection-text.test.ts": 47, + "src/renderer/src/components/terminal-pane/terminal-cold-park-exempt-flip.react185.test.tsx": 1079, + "src/renderer/src/components/terminal-pane/terminal-cold-park-pre-gate-loop.react185.test.tsx": 723, + "src/renderer/src/components/terminal-pane/terminal-cold-park-recheck-deadlines.test.ts": 1409, + "src/renderer/src/components/terminal-pane/terminal-cold-park-subscription-narrowing.react185.test.tsx": 548, + "src/renderer/src/components/terminal-pane/terminal-cold-park-tab-model-identity.react185.test.tsx": 861, + "src/renderer/src/components/terminal-pane/terminal-cold-park-timer-rearm.test.tsx": 465, + "src/renderer/src/components/terminal-pane/terminal-cold-park-verdict-loop.test.tsx": 565, + "src/renderer/src/components/terminal-pane/terminal-cold-park-withheld-tabs.test.ts": 66, + "src/renderer/src/components/terminal-pane/terminal-command-lifecycle.test.ts": 53, + "src/renderer/src/components/terminal-pane/terminal-context-menu-dismiss.test.ts": 33, + "src/renderer/src/components/terminal-pane/terminal-copy-rejection-handling.test.ts": 44, + "src/renderer/src/components/terminal-pane/terminal-copy-shortcut.test.ts": 1457, + "src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.test.ts": 1372, + "src/renderer/src/components/terminal-pane/terminal-ctrl-enter.test.ts": 92, + "src/renderer/src/components/terminal-pane/terminal-cursor-appearance-precedence.test.ts": 296, + "src/renderer/src/components/terminal-pane/terminal-cursor-inactive-style.test.ts": 337, + "src/renderer/src/components/terminal-pane/terminal-dead-session-reconcile.test.ts": 49, + "src/renderer/src/components/terminal-pane/terminal-delivery-watchdog.test.ts": 113, + "src/renderer/src/components/terminal-pane/terminal-drop-handler.test.ts": 542, + "src/renderer/src/components/terminal-pane/terminal-drop-image-path.test.ts": 52, + "src/renderer/src/components/terminal-pane/terminal-drop-internal-handler.test.ts": 715, + "src/renderer/src/components/terminal-pane/terminal-drop-local-workspace.test.ts": 652, + "src/renderer/src/components/terminal-pane/terminal-drop-pane-resolution.test.ts": 163, + "src/renderer/src/components/terminal-pane/terminal-drop-path-writer.test.ts": 87, + "src/renderer/src/components/terminal-pane/terminal-drop-runtime-catalog-owner.test.ts": 681, + "src/renderer/src/components/terminal-pane/terminal-drop-shell.test.ts": 43, + "src/renderer/src/components/terminal-pane/terminal-drop-source-owner.test.ts": 532, + "src/renderer/src/components/terminal-pane/terminal-drop-transport-owner.test.ts": 290, + "src/renderer/src/components/terminal-pane/terminal-drop-upload-report.test.ts": 40, + "src/renderer/src/components/terminal-pane/terminal-drop-worktree-path.test.ts": 344, + "src/renderer/src/components/terminal-pane/terminal-drop-write-failure.test.ts": 39, + "src/renderer/src/components/terminal-pane/terminal-error-accumulation.test.ts": 168, + "src/renderer/src/components/terminal-pane/terminal-error-remote-closed-localization.test.ts": 49, + "src/renderer/src/components/terminal-pane/terminal-eviction-exempt-tabs.test.ts": 750, + "src/renderer/src/components/terminal-pane/terminal-file-link-actions.test.ts": 313, + "src/renderer/src/components/terminal-pane/terminal-file-link-target.test.ts": 1371, + "src/renderer/src/components/terminal-pane/terminal-fit-restore.test.ts": 48, + "src/renderer/src/components/terminal-pane/terminal-freeze-report.test.ts": 83, + "src/renderer/src/components/terminal-pane/terminal-handle-copy.test.ts": 40, + "src/renderer/src/components/terminal-pane/terminal-handle-links.test.ts": 429, + "src/renderer/src/components/terminal-pane/terminal-hidden-restore-scrollback.test.ts": 22, + "src/renderer/src/components/terminal-pane/terminal-hidden-view-parking.test.ts": 1272, + "src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.test.ts": 1822, + "src/renderer/src/components/terminal-pane/terminal-http-link-activation.test.ts": 301, + "src/renderer/src/components/terminal-pane/terminal-http-link-source-owner.test.ts": 102, + "src/renderer/src/components/terminal-pane/terminal-http-url-extraction.test.ts": 36, + "src/renderer/src/components/terminal-pane/terminal-ime-candidate-key-release-guard.test.ts": 47, + "src/renderer/src/components/terminal-pane/terminal-ime-composer-placeholder-mask.test.ts": 769, + "src/renderer/src/components/terminal-pane/terminal-ime-composition-route.test.ts": 179, + "src/renderer/src/components/terminal-pane/terminal-ime-composition-tracker.test.ts": 218, + "src/renderer/src/components/terminal-pane/terminal-ime-composition-transaction-ownership.test.ts": 170, + "src/renderer/src/components/terminal-pane/terminal-ime-deferred-chord.test.ts": 168, + "src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.test.ts": 218, + "src/renderer/src/components/terminal-pane/terminal-ime-forwarder-space-claim.test.ts": 240, + "src/renderer/src/components/terminal-pane/terminal-ime-hangul-syllable-flush.test.ts": 841, + "src/renderer/src/components/terminal-pane/terminal-ime-hangul-terminating-digit.test.ts": 518, + "src/renderer/src/components/terminal-pane/terminal-ime-input-context-refresh.test.ts": 185, + "src/renderer/src/components/terminal-pane/terminal-ime-linux-fcitx4-trace-replay.test.ts": 1506, + "src/renderer/src/components/terminal-pane/terminal-ime-linux-kitty-orphan-keyup.test.ts": 761, + "src/renderer/src/components/terminal-pane/terminal-ime-macos-keybinding-dict-trace.test.ts": 375, + "src/renderer/src/components/terminal-pane/terminal-ime-native-text-forwarder.test.ts": 244, + "src/renderer/src/components/terminal-pane/terminal-ime-substituted-text-commit.test.ts": 475, + "src/renderer/src/components/terminal-pane/terminal-ime-user-input-provenance.test.ts": 413, + "src/renderer/src/components/terminal-pane/terminal-ime-won-composition-order.test.ts": 559, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-adversarial.test.ts": 2323, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-cancelled-preedit-visibility.test.ts": 417, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-cancel.test.ts": 423, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-composition-deduplication.test.ts": 1563, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-consumed-key-commit.test.ts": 654, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-korean-enter-commit-order.test.ts": 407, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-linux-native-trace-replay.test.ts": 4785, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-midline-preedit-tail.test.ts": 967, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-preedit-cell-grid.test.ts": 1178, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-resumed-preedit-visibility.test.ts": 432, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-trailing-preedit-occlusion.test.ts": 519, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-transaction-events.test.ts": 562, + "src/renderer/src/components/terminal-pane/terminal-ime-xterm-windows-resumed-preedit-trace.test.ts": 720, + "src/renderer/src/components/terminal-pane/terminal-inline-image-da1.test.ts": 85, + "src/renderer/src/components/terminal-pane/terminal-input-host-platform.test.ts": 1425, + "src/renderer/src/components/terminal-pane/terminal-input-quarantine.test.ts": 41, + "src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit-coexistence.test.ts": 636, + "src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit-device-trace.test.ts": 653, + "src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit.test.ts": 818, + "src/renderer/src/components/terminal-pane/terminal-jis-yen-input.test.ts": 36, + "src/renderer/src/components/terminal-pane/terminal-keyboard-event-handlers-focus.test.ts": 1553, + "src/renderer/src/components/terminal-pane/terminal-keyboard-native-chat-cover.test.ts": 1520, + "src/renderer/src/components/terminal-pane/terminal-keyboard-pane-resolution.test.ts": 195, + "src/renderer/src/components/terminal-pane/terminal-keyboard-protocol-pane-agent.test.ts": 36, + "src/renderer/src/components/terminal-pane/terminal-keyboard-search-dispatch.test.ts": 1621, + "src/renderer/src/components/terminal-pane/terminal-kitty-csi-u-encoding.test.ts": 58, + "src/renderer/src/components/terminal-pane/terminal-layout-duplicate-pty-replay.test.ts": 71, + "src/renderer/src/components/terminal-pane/terminal-layout-gesture-persist.test.tsx": 488, + "src/renderer/src/components/terminal-pane/terminal-layout-leaf-detach.test.ts": 55, + "src/renderer/src/components/terminal-pane/terminal-layout-leaf-ids.test.ts": 63, + "src/renderer/src/components/terminal-pane/terminal-layout-overlay-focus.test.tsx": 205, + "src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership-depth.test.ts": 73, + "src/renderer/src/components/terminal-pane/terminal-layout-pty-ownership.test.ts": 103, + "src/renderer/src/components/terminal-pane/terminal-link-action-routing.test.ts": 1819, + "src/renderer/src/components/terminal-pane/terminal-link-activation.test.ts": 300, + "src/renderer/src/components/terminal-pane/terminal-link-click-behavior.test.ts": 33, + "src/renderer/src/components/terminal-pane/terminal-link-click-fallback.test.ts": 781, + "src/renderer/src/components/terminal-pane/terminal-link-client-local-file.test.ts": 713, + "src/renderer/src/components/terminal-pane/terminal-link-escaping-file-open.test.ts": 707, + "src/renderer/src/components/terminal-pane/terminal-link-file-open-routing.test.ts": 725, + "src/renderer/src/components/terminal-pane/terminal-link-host-granted-file-link.test.ts": 685, + "src/renderer/src/components/terminal-pane/terminal-link-host-workspace-file.test.ts": 758, + "src/renderer/src/components/terminal-pane/terminal-link-missing-file-open.test.ts": 762, + "src/renderer/src/components/terminal-pane/terminal-link-open-hints.test.ts": 266, + "src/renderer/src/components/terminal-pane/terminal-link-osc-file-targets.test.ts": 878, + "src/renderer/src/components/terminal-pane/terminal-link-osc-url-routing.test.ts": 779, + "src/renderer/src/components/terminal-pane/terminal-link-pointer-gesture.test.ts": 487, + "src/renderer/src/components/terminal-pane/terminal-link-provider-batching.test.ts": 710, + "src/renderer/src/components/terminal-pane/terminal-link-provider-candidate-failures.test.ts": 767, + "src/renderer/src/components/terminal-pane/terminal-link-provider-chat-cases.test.ts": 1013, + "src/renderer/src/components/terminal-pane/terminal-link-provider-link-detection.test.ts": 739, + "src/renderer/src/components/terminal-pane/terminal-link-pty-mouse-suppression.test.ts": 501, + "src/renderer/src/components/terminal-pane/terminal-link-remote-runtime-ssh-open.test.ts": 510, + "src/renderer/src/components/terminal-pane/terminal-link-same-id-ssh-file-host.test.ts": 790, + "src/renderer/src/components/terminal-pane/terminal-link-worktree-root-activation.test.ts": 753, + "src/renderer/src/components/terminal-pane/terminal-link-wsl-path-mapping.test.ts": 769, + "src/renderer/src/components/terminal-pane/terminal-linkifier-click-priming.test.ts": 258, + "src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.test.ts": 1374, + "src/renderer/src/components/terminal-pane/terminal-native-copy-gutter.test.ts": 177, + "src/renderer/src/components/terminal-pane/terminal-non-latin-control-chord.test.ts": 45, + "src/renderer/src/components/terminal-pane/terminal-notification-state.test.ts": 1578, + "src/renderer/src/components/terminal-pane/terminal-option-kitty-release.test.ts": 48, + "src/renderer/src/components/terminal-pane/terminal-option-punctuation.test.ts": 84, + "src/renderer/src/components/terminal-pane/terminal-output-visibility.test.ts": 39, + "src/renderer/src/components/terminal-pane/terminal-pane-attention-subscriptions.test.ts": 47, + "src/renderer/src/components/terminal-pane/terminal-pane-close-identity.test.ts": 25, + "src/renderer/src/components/terminal-pane/terminal-pane-element-file-drop.test.tsx": 1996, + "src/renderer/src/components/terminal-pane/terminal-pane-host-state-memo.test.ts": 1945, + "src/renderer/src/components/terminal-pane/terminal-pane-host-state.test.ts": 1437, + "src/renderer/src/components/terminal-pane/terminal-pane-manager-options-gesture.test.ts": 2444, + "src/renderer/src/components/terminal-pane/terminal-pane-recovery-unsettled-fallback.test.ts": 691, + "src/renderer/src/components/terminal-pane/terminal-pane-recovery.test.ts": 698, + "src/renderer/src/components/terminal-pane/terminal-pane-spawn-placement.test.ts": 597, + "src/renderer/src/components/terminal-pane/terminal-pane-split-completion.test.ts": 43, + "src/renderer/src/components/terminal-pane/terminal-pane-split-request-routing.test.ts": 180, + "src/renderer/src/components/terminal-pane/terminal-pane-split-with-inherited-cwd.test.ts": 63, + "src/renderer/src/components/terminal-pane/terminal-pane-split-writer-paths.test.ts": 658, + "src/renderer/src/components/terminal-pane/terminal-pane-store-subscription-budget.test.tsx": 7104, + "src/renderer/src/components/terminal-pane/terminal-pane-surface-ownership.test.ts": 362, + "src/renderer/src/components/terminal-pane/terminal-pane-tab-detach-commit.test.ts": 148, + "src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.test.ts": 152, + "src/renderer/src/components/terminal-pane/terminal-park-verdict-flip-telemetry.test.ts": 106, + "src/renderer/src/components/terminal-pane/terminal-park-verdict-worktree-driver-loop.test.tsx": 645, + "src/renderer/src/components/terminal-pane/terminal-parked-pane-candidates.test.ts": 1531, + "src/renderer/src/components/terminal-pane/terminal-parked-tab-eviction-exemption.test.ts": 352, + "src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers-batch-sync.test.ts": 449, + "src/renderer/src/components/terminal-pane/terminal-parked-tab-watchers.test.ts": 429, + "src/renderer/src/components/terminal-pane/terminal-parked-watcher-partial-reconciliation.test.ts": 355, + "src/renderer/src/components/terminal-pane/terminal-parked-watcher-reconciliation.test.ts": 306, + "src/renderer/src/components/terminal-pane/terminal-parked-watcher-registry.test.ts": 239, + "src/renderer/src/components/terminal-pane/terminal-parked-watcher-sleep-preserved-exit.test.ts": 400, + "src/renderer/src/components/terminal-pane/terminal-parked-watcher-sole-newborn-exit.test.ts": 376, + "src/renderer/src/components/terminal-pane/terminal-parking-e2e-overrides.test.ts": 2938, + "src/renderer/src/components/terminal-pane/terminal-parser-handler-guard.test.ts": 44, + "src/renderer/src/components/terminal-pane/terminal-paste-coordinator.test.ts": 148, + "src/renderer/src/components/terminal-pane/terminal-paste-executor-default-yield.test.ts": 48, + "src/renderer/src/components/terminal-pane/terminal-paste-multiline-policy.test.ts": 40, + "src/renderer/src/components/terminal-pane/terminal-paste-operation-order.test.ts": 1511, + "src/renderer/src/components/terminal-pane/terminal-paste-runtime.test.ts": 35, + "src/renderer/src/components/terminal-pane/terminal-paste-target-state.test.ts": 38, + "src/renderer/src/components/terminal-pane/terminal-path-existence-batch.test.ts": 44, + "src/renderer/src/components/terminal-pane/terminal-process-exit-restart.test.ts": 36, + "src/renderer/src/components/terminal-pane/terminal-programmatic-text-paste.test.ts": 124, + "src/renderer/src/components/terminal-pane/terminal-pty-ack-gate.test.ts": 180, + "src/renderer/src/components/terminal-pane/terminal-pty-paste-writer.test.ts": 32, + "src/renderer/src/components/terminal-pane/terminal-quick-command-dispatch.test.ts": 39, + "src/renderer/src/components/terminal-pane/terminal-remote-runtime-recovery-ui-state.test.ts": 26, + "src/renderer/src/components/terminal-pane/terminal-render-desync-evidence-persistence.test.ts": 42, + "src/renderer/src/components/terminal-pane/terminal-render-desync-frame.test.ts": 40, + "src/renderer/src/components/terminal-pane/terminal-render-desync-sentinel.test.ts": 463, + "src/renderer/src/components/terminal-pane/terminal-render-desync-weight-probe.test.ts": 38, + "src/renderer/src/components/terminal-pane/terminal-renderer-policy.test.ts": 116, + "src/renderer/src/components/terminal-pane/terminal-replay-application-continuity.test.ts": 365, + "src/renderer/src/components/terminal-pane/terminal-replay-cursor-state.test.ts": 91, + "src/renderer/src/components/terminal-pane/terminal-restore-sgr-latch.test.ts": 147, + "src/renderer/src/components/terminal-pane/terminal-restored-viewport.test.ts": 38, + "src/renderer/src/components/terminal-pane/terminal-retention-churn-pins.test.tsx": 822, + "src/renderer/src/components/terminal-pane/terminal-retention-exempt-growth.test.ts": 1827, + "src/renderer/src/components/terminal-pane/terminal-runtime-host-link-routing.test.ts": 1832, + "src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts": 1213, + "src/renderer/src/components/terminal-pane/terminal-selection-fit-guard.test.ts": 26, + "src/renderer/src/components/terminal-pane/terminal-shortcut-ctrl-arrow.test.ts": 87, + "src/renderer/src/components/terminal-pane/terminal-shortcut-option-compose.test.ts": 118, + "src/renderer/src/components/terminal-pane/terminal-shortcut-policy.test.ts": 134, + "src/renderer/src/components/terminal-pane/terminal-shortcut-select-all.test.ts": 61, + "src/renderer/src/components/terminal-pane/terminal-shutdown-layout-capture.test.ts": 188, + "src/renderer/src/components/terminal-pane/terminal-side-effect-facts-handler.test.ts": 48, + "src/renderer/src/components/terminal-pane/terminal-snapshot-replay-paint.test.ts": 492, + "src/renderer/src/components/terminal-pane/terminal-spawn-error-display.test.ts": 55, + "src/renderer/src/components/terminal-pane/terminal-startup-command-classifier.test.ts": 100, + "src/renderer/src/components/terminal-pane/terminal-startup-grid-settle.test.ts": 50, + "src/renderer/src/components/terminal-pane/terminal-startup-timing-scheduler.test.ts": 120, + "src/renderer/src/components/terminal-pane/terminal-startup-timing.test.ts": 49, + "src/renderer/src/components/terminal-pane/terminal-tab-agent-type-index-retention.test.ts": 1803, + "src/renderer/src/components/terminal-pane/terminal-tab-agent-type-index.test.ts": 49, + "src/renderer/src/components/terminal-pane/terminal-tab-lookup.test.ts": 38, + "src/renderer/src/components/terminal-pane/terminal-title-evidence.test.ts": 95, + "src/renderer/src/components/terminal-pane/terminal-title-tracker-parity.test.ts": 192, "src/renderer/src/components/terminal-pane/terminal-unified-tab-lookup.test.ts": 59, - "src/renderer/src/components/terminal-pane/terminal-url-link-click.test.ts": 1895, - "src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.test.ts": 1787, - "src/renderer/src/components/terminal-pane/terminal-user-input-signal.test.ts": 72, - "src/renderer/src/components/terminal-pane/terminal-view-attributes-publisher.test.ts": 369, - "src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts": 1475, - "src/renderer/src/components/terminal-pane/terminal-webgl-atlas-recovery.test.ts": 67, - "src/renderer/src/components/terminal-pane/terminal-webgl-diagnostics-breadcrumbs.test.ts": 75, - "src/renderer/src/components/terminal-pane/terminal-windows-shift-enter.test.ts": 105, - "src/renderer/src/components/terminal-pane/terminal-worktree-path-link.test.ts": 1473, - "src/renderer/src/components/terminal-pane/title-agent-identity.test.ts": 69, - "src/renderer/src/components/terminal-pane/use-manual-terminal-worktree-parking.test.ts": 1577, - "src/renderer/src/components/terminal-pane/use-mobile-overlay-ticks.perf.test.tsx": 337, - "src/renderer/src/components/terminal-pane/use-notification-dispatch-verdict.test.ts": 438, - "src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts": 477, - "src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx": 495, - "src/renderer/src/components/terminal-pane/use-system-prefers-dark.test.ts": 47, - "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-active-pty-reporting.test.ts": 1361, - "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-paste-events.test.ts": 1425, - "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-sync-fit-registration.test.ts": 1495, - "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-visibility-resume.test.ts": 1703, - "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-window-focus-recovery.test.ts": 1618, - "src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.test.ts": 2584, - "src/renderer/src/components/terminal-pane/use-terminal-park-mount-intent.react185.test.tsx": 441, - "src/renderer/src/components/terminal-pane/use-terminal-scroll-visibility-memory.test.ts": 77, - "src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts": 746, - "src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts": 306, - "src/renderer/src/components/terminal-pane/use-visible-terminal-tab-claim.test.ts": 54, - "src/renderer/src/components/terminal-pane/useSessionRestoredBannerDismiss.test.tsx": 206, - "src/renderer/src/components/terminal-pane/useTerminalFontZoom.test.ts": 180, - "src/renderer/src/components/terminal-pane/wrapped-terminal-link-ranges.test.ts": 65, - "src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts": 87, - "src/renderer/src/components/terminal-pane/xterm-bypass-policy-ios.test.ts": 100, - "src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts": 102, - "src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts": 102, - "src/renderer/src/components/terminal-parked-watcher-sync-entries.test.tsx": 1186, - "src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx": 903, - "src/renderer/src/components/terminal-quick-commands/terminal-quick-command-agent-options.test.ts": 128, - "src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.test.ts": 182, - "src/renderer/src/components/terminal-scrollback-decoration-eviction.test.ts": 504, - "src/renderer/src/components/terminal-search-decoration-leak.test.ts": 2052, - "src/renderer/src/components/terminal-search-long-wrapped-line.test.ts": 4844, - "src/renderer/src/components/terminal-search-safe-find.test.ts": 49, - "src/renderer/src/components/terminal-split-workspace-surfaces-startup-hold.test.tsx": 174, - "src/renderer/src/components/terminal-startup-hold-placeholder.test.tsx": 636, - "src/renderer/src/components/terminal-workspace-keydown.test.ts": 591, - "src/renderer/src/components/terminal-workspace-surface-ids.test.tsx": 2255, - "src/renderer/src/components/terminal/activation-deferred-tab-admission.test.ts": 61, - "src/renderer/src/components/terminal/active-terminal-repair-loop.react185.test.tsx": 1667, - "src/renderer/src/components/terminal/active-terminal-repair.test.ts": 57, - "src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts": 1773, - "src/renderer/src/components/terminal/background-terminal-worktree-visibility.test.ts": 51, - "src/renderer/src/components/terminal/cold-activation-deferral-stranding.test.ts": 1948, - "src/renderer/src/components/terminal/initial-terminal-admission-wait.test.tsx": 1079, - "src/renderer/src/components/terminal/initial-terminal-structured-launch.test.tsx": 461, - "src/renderer/src/components/terminal/initial-terminal.test.ts": 60, - "src/renderer/src/components/terminal/pty-running-work-probe-child-evidence.test.ts": 57, - "src/renderer/src/components/terminal/running-terminal-close-guard.test.ts": 108, - "src/renderer/src/components/terminal/split-group-mount.test.ts": 67, - "src/renderer/src/components/terminal/startup-terminal-tab-hold.test.ts": 2061, - "src/renderer/src/components/terminal/tab-type-cycle.test.ts": 49, - "src/renderer/src/components/terminal/terminal-close-confirm-keyboard-vs-mouse.test.ts": 389, - "src/renderer/src/components/terminal/terminal-close-copy-kind.test.ts": 51, - "src/renderer/src/components/terminal/terminal-close-incarnation.test.ts": 122, - "src/renderer/src/components/terminal/terminal-provider-snapshot-bound-pty-ids.test.ts": 1808, - "src/renderer/src/components/terminal/terminal-provider-snapshot-capability-resettlement.test.ts": 1356, + "src/renderer/src/components/terminal-pane/terminal-url-link-click.test.ts": 1659, + "src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.test.ts": 1825, + "src/renderer/src/components/terminal-pane/terminal-user-input-signal.test.ts": 46, + "src/renderer/src/components/terminal-pane/terminal-view-attributes-publisher.test.ts": 331, + "src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts": 1476, + "src/renderer/src/components/terminal-pane/terminal-webgl-atlas-recovery.test.ts": 43, + "src/renderer/src/components/terminal-pane/terminal-webgl-diagnostics-breadcrumbs.test.ts": 52, + "src/renderer/src/components/terminal-pane/terminal-windows-shift-enter.test.ts": 106, + "src/renderer/src/components/terminal-pane/terminal-worktree-path-link.test.ts": 1398, + "src/renderer/src/components/terminal-pane/title-agent-identity.test.ts": 138, + "src/renderer/src/components/terminal-pane/use-manual-terminal-worktree-parking.test.ts": 1497, + "src/renderer/src/components/terminal-pane/use-mobile-overlay-ticks.perf.test.tsx": 311, + "src/renderer/src/components/terminal-pane/use-notification-dispatch-verdict.test.ts": 418, + "src/renderer/src/components/terminal-pane/use-notification-dispatch.test.ts": 464, + "src/renderer/src/components/terminal-pane/use-parked-terminal-watcher-synchronization.react185.test.tsx": 530, + "src/renderer/src/components/terminal-pane/use-system-prefers-dark.test.ts": 33, + "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-active-pty-reporting.test.ts": 1392, + "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-paste-events.test.ts": 1307, + "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-sync-fit-registration.test.ts": 1400, + "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-visibility-resume.test.ts": 1561, + "src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects-window-focus-recovery.test.ts": 1516, + "src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.test.ts": 1466, + "src/renderer/src/components/terminal-pane/use-terminal-park-mount-intent.react185.test.tsx": 413, + "src/renderer/src/components/terminal-pane/use-terminal-scroll-visibility-memory.test.ts": 79, + "src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts": 625, + "src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts": 316, + "src/renderer/src/components/terminal-pane/use-visible-terminal-tab-claim.test.ts": 42, + "src/renderer/src/components/terminal-pane/use-wsl-host-unavailable-reason.test.ts": 1236, + "src/renderer/src/components/terminal-pane/useSessionRestoredBannerDismiss.test.tsx": 199, + "src/renderer/src/components/terminal-pane/useTerminalFontZoom.test.ts": 163, + "src/renderer/src/components/terminal-pane/wrapped-terminal-link-ranges.test.ts": 60, + "src/renderer/src/components/terminal-pane/xterm-bypass-policy-interrupt.test.ts": 76, + "src/renderer/src/components/terminal-pane/xterm-bypass-policy-ios.test.ts": 69, + "src/renderer/src/components/terminal-pane/xterm-bypass-policy-non-mac.test.ts": 111, + "src/renderer/src/components/terminal-pane/xterm-bypass-policy.test.ts": 127, + "src/renderer/src/components/terminal-parked-watcher-sync-entries.test.tsx": 1122, + "src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx": 1203, + "src/renderer/src/components/terminal-quick-commands/terminal-quick-command-agent-options.test.ts": 131, + "src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.test.ts": 153, + "src/renderer/src/components/terminal-scrollback-decoration-eviction.test.ts": 617, + "src/renderer/src/components/terminal-search-decoration-leak.test.ts": 2155, + "src/renderer/src/components/terminal-search-long-wrapped-line.test.ts": 4767, + "src/renderer/src/components/terminal-search-safe-find.test.ts": 37, + "src/renderer/src/components/terminal-split-workspace-surfaces-startup-hold.test.tsx": 198, + "src/renderer/src/components/terminal-startup-hold-placeholder.test.tsx": 742, + "src/renderer/src/components/terminal-workspace-keydown.test.ts": 744, + "src/renderer/src/components/terminal-workspace-surface-ids.test.tsx": 1959, + "src/renderer/src/components/terminal/activation-deferred-tab-admission.test.ts": 41, + "src/renderer/src/components/terminal/active-terminal-repair-loop.react185.test.tsx": 1554, + "src/renderer/src/components/terminal/active-terminal-repair.test.ts": 35, + "src/renderer/src/components/terminal/background-terminal-worktree-mount.test.ts": 1397, + "src/renderer/src/components/terminal/background-terminal-worktree-visibility.test.ts": 45, + "src/renderer/src/components/terminal/cold-activation-deferral-stranding.test.ts": 1846, + "src/renderer/src/components/terminal/initial-terminal-admission-wait.test.tsx": 876, + "src/renderer/src/components/terminal/initial-terminal-structured-launch.test.tsx": 408, + "src/renderer/src/components/terminal/initial-terminal.test.ts": 30, + "src/renderer/src/components/terminal/pty-running-work-probe-child-evidence.test.ts": 36, + "src/renderer/src/components/terminal/running-terminal-close-guard.test.ts": 112, + "src/renderer/src/components/terminal/split-group-mount.test.ts": 53, + "src/renderer/src/components/terminal/startup-terminal-tab-hold.test.ts": 1725, + "src/renderer/src/components/terminal/tab-type-cycle.test.ts": 41, + "src/renderer/src/components/terminal/terminal-close-confirm-keyboard-vs-mouse.test.ts": 338, + "src/renderer/src/components/terminal/terminal-close-copy-kind.test.ts": 35, + "src/renderer/src/components/terminal/terminal-close-incarnation.test.ts": 64, + "src/renderer/src/components/terminal/terminal-provider-snapshot-bound-pty-ids.test.ts": 1537, + "src/renderer/src/components/terminal/terminal-provider-snapshot-capability-resettlement.test.ts": 1472, "src/renderer/src/components/terminal/terminal-provider-snapshot-capability.test.ts": 77, - "src/renderer/src/components/terminal/terminal-tab-actions-kill-all.test.ts": 414, - "src/renderer/src/components/terminal/terminal-tab-actions-unified-close.test.ts": 1714, - "src/renderer/src/components/terminal/terminal-tab-actions-unrouted-close.test.ts": 375, - "src/renderer/src/components/terminal/terminal-tab-actions.test.ts": 441, - "src/renderer/src/components/terminal/terminal-tab-close-running-confirm.test.ts": 440, - "src/renderer/src/components/terminal/unsaved-close-queue.test.ts": 54, + "src/renderer/src/components/terminal/terminal-tab-actions-kill-all.test.ts": 482, + "src/renderer/src/components/terminal/terminal-tab-actions-unified-close.test.ts": 1414, + "src/renderer/src/components/terminal/terminal-tab-actions-unrouted-close.test.ts": 391, + "src/renderer/src/components/terminal/terminal-tab-actions.test.ts": 427, + "src/renderer/src/components/terminal/terminal-tab-close-running-confirm.test.ts": 404, + "src/renderer/src/components/terminal/unsaved-close-queue.test.ts": 41, "src/renderer/src/components/terminal/use-terminal-provider-snapshot-capability.test.tsx": 309, - "src/renderer/src/components/terminal/use-worktree-files.test.tsx": 349, - "src/renderer/src/components/terminal/window-close-running-work.test.ts": 118, - "src/renderer/src/components/ui/color-picker.test.tsx": 331, - "src/renderer/src/components/ui/popover-wheel-scroll.test.tsx": 436, - "src/renderer/src/components/ui/repo-multi-combobox.test.ts": 204, - "src/renderer/src/components/ui/scroll-area.test.tsx": 249, - "src/renderer/src/components/ui/slider.test.tsx": 606, - "src/renderer/src/components/ui/switch.test.tsx": 102, - "src/renderer/src/components/unexpected-signout/unexpected-signout-card.test.tsx": 1501, - "src/renderer/src/components/unexpected-signout/unexpected-signout-visibility.test.ts": 61, - "src/renderer/src/components/use-github-task-search-commit.test.ts": 287, - "src/renderer/src/components/use-jira-create-assignee.test.tsx": 250, - "src/renderer/src/components/use-quick-open-interaction.test.tsx": 1646, - "src/renderer/src/components/use-task-creation-draft-retention.test.ts": 562, - "src/renderer/src/components/use-task-page-gitlab-loading.test.tsx": 1645, - "src/renderer/src/components/use-task-page-jira-list-effects.test.ts": 1835, - "src/renderer/src/components/use-terminal-create-actions.test.tsx": 313, - "src/renderer/src/components/use-terminal-editor-close-foundation.window-close.test.tsx": 1581, - "src/renderer/src/components/use-terminal-editor-close-queue.reveal.test.tsx": 646, - "src/renderer/src/components/use-terminal-parking-pass.test.tsx": 1936, - "src/renderer/src/components/use-terminal-window-lifecycle.lazy-ref.test.tsx": 240, - "src/renderer/src/components/use-worktree-jump-palette-browser-ownership.test.ts": 1489, - "src/renderer/src/components/virtualized-list.test.tsx": 391, - "src/renderer/src/components/window-close-request-coordinator.test.ts": 222, - "src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.mount-gating.test.tsx": 2431, - "src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.stale-while-revalidate.test.tsx": 2563, - "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-browse-state.test.tsx": 266, - "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-facet-rows.test.tsx": 389, - "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.test.tsx": 349, - "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-row-order.test.ts": 267, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-active-facets.test.ts": 70, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal-late-settlement.test.ts": 1523, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal-snapshot-batch.test.ts": 1294, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal.test.ts": 189, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-list.test.tsx": 2157, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.test.ts": 124, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.test.tsx": 2357, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.test.tsx": 532, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-deletion-phases.test.ts": 85, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-notices.test.tsx": 363, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.test.tsx": 601, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-filter.test.ts": 174, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.test.ts": 123, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-bar.test.tsx": 537, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-flat-list.test.tsx": 2613, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-evidence.test.ts": 91, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.test.ts": 130, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-candidates.test.ts": 97, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-settlement.test.ts": 95, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-timeout-recovery.test.ts": 1542, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-scanned-host-confirmation-removal.test.tsx": 3533, - "src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.test.tsx": 439, - "src/renderer/src/components/workspace-emoji/WorkspaceEmojiSuggestionPopover.test.tsx": 390, - "src/renderer/src/components/workspace-emoji/useWorkspaceEmojiShortcodeInput.test.tsx": 376, - "src/renderer/src/components/workspace-surface-projection.test.ts": 97, - "src/renderer/src/components/worktree-creation/WorktreeCreationPanel.test.tsx": 315, - "src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx": 2544, - "src/renderer/src/components/worktree-jump-palette-primitives.test.tsx": 357, - "src/renderer/src/components/worktree-jump-palette-quick-action-availability.test.tsx": 998, - "src/renderer/src/components/worktree-jump-palette-recent-inclusion.test.ts": 124, - "src/renderer/src/components/worktree-jump-palette-sleeping-filter.test.ts": 1424, - "src/renderer/src/components/worktree-jump-palette-status-inputs.test.ts": 65, - "src/renderer/src/hooks/agent-auto-ack-targets.test.ts": 1504, - "src/renderer/src/hooks/agent-hook-completion-background-turn-notifications.test.ts": 351, - "src/renderer/src/hooks/agent-hook-completion-fresh-working.test.ts": 187, - "src/renderer/src/hooks/agent-hook-completion-grok-notifications.test.ts": 454, - "src/renderer/src/hooks/agent-hook-completion-notifications-import.test.ts": 237, - "src/renderer/src/hooks/agent-hook-completion-notifications.test.ts": 504, - "src/renderer/src/hooks/agent-hook-completion-store-sync.test.ts": 75, - "src/renderer/src/hooks/automation-agent-status-entry-change.test.ts": 50, - "src/renderer/src/hooks/automation-dispatch-unverifiable-loss.test.ts": 178, - "src/renderer/src/hooks/automations-changed-event-attribution.test.ts": 1801, - "src/renderer/src/hooks/composer-branch-selection.test.ts": 75, - "src/renderer/src/hooks/composer-drop-failure-toast.test.ts": 92, - "src/renderer/src/hooks/composer-drop-result.test.ts": 47, - "src/renderer/src/hooks/composer-native-file-drop.test.ts": 52, - "src/renderer/src/hooks/composer-state/attachment-drop-failure.test.tsx": 633, - "src/renderer/src/hooks/composer-state/attachment-drop-lifetime.test.tsx": 790, - "src/renderer/src/hooks/composer-state/branch-start-point-actions.test.ts": 323, - "src/renderer/src/hooks/composer-state/composer-attachment-target.test.ts": 70, - "src/renderer/src/hooks/composer-state/composer-name-source-selection.test.ts": 2007, - "src/renderer/src/hooks/composer-state/composer-remote-server-drop.test.tsx": 639, - "src/renderer/src/hooks/composer-state/draft-target-sync.test.ts": 265, - "src/renderer/src/hooks/composer-state/multiple-create-reset.test.ts": 1678, - "src/renderer/src/hooks/composer-state/provider-runtime-sync.test.ts": 1661, - "src/renderer/src/hooks/composer-state/quick-creation-request.test.ts": 110, - "src/renderer/src/hooks/direct-ssh-runtime-wake-isolation.test.tsx": 1605, - "src/renderer/src/hooks/direct-ssh-state-routing.test.ts": 183, - "src/renderer/src/hooks/editor-external-watch-fallback-explorer.test.ts": 355, - "src/renderer/src/hooks/editor-external-watch-selected-host.test.ts": 324, - "src/renderer/src/hooks/editor-runtime-file-watch.test.ts": 55, - "src/renderer/src/hooks/fork-push-warning.test.ts": 65, - "src/renderer/src/hooks/installed-agent-skill-discovery-cache.test.ts": 67, - "src/renderer/src/hooks/installed-agent-skill-discovery.test.ts": 73, - "src/renderer/src/hooks/installed-agent-skill-runtime-eviction.test.ts": 54, - "src/renderer/src/hooks/installed-agent-skill-verdict.test.ts": 1648, - "src/renderer/src/hooks/ipc-events/agent-status-pane-routing-index-retention.test.ts": 2083, - "src/renderer/src/hooks/ipc-events/agent-status-pane-routing-index.test.ts": 1458, - "src/renderer/src/hooks/ipc-events/agent-status-pending-retry-gate.test.ts": 563, - "src/renderer/src/hooks/ipc-events/agent-status-terminal-title-tab-write.test.ts": 1686, - "src/renderer/src/hooks/ipc-events/agent-status-unavailable.test.ts": 82, - "src/renderer/src/hooks/ipc-events/browser-automation-bootstrap-lease.test.ts": 144, - "src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.owner.test.ts": 487, - "src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.profile-switch.test.ts": 535, - "src/renderer/src/hooks/ipc-events/browser-state-capture-paint-hold.test.ts": 293, - "src/renderer/src/hooks/ipc-events/browser-state-open-link-profile.test.ts": 323, - "src/renderer/src/hooks/ipc-events/direct-ssh-connect-reply-routing.test.ts": 1461, - "src/renderer/src/hooks/ipc-events/direct-ssh-hydration-fanout.test.ts": 1432, - "src/renderer/src/hooks/ipc-events/direct-ssh-hydration-target-metadata.test.ts": 1473, - "src/renderer/src/hooks/ipc-events/mobile-terminal-close-ipc-bridge.test.ts": 1564, - "src/renderer/src/hooks/ipc-events/normalize-agent-status-event.test.ts": 60, - "src/renderer/src/hooks/ipc-events/orca-profile-auth-ipc-bridge.test.ts": 1416, - "src/renderer/src/hooks/ipc-events/os-markdown-file-open-bridge.test.ts": 308, - "src/renderer/src/hooks/ipc-events/runtime-client-ipc-bridge-ownership.test.ts": 1295, - "src/renderer/src/hooks/ipc-events/runtime-reconnect-host-status.test.ts": 1629, - "src/renderer/src/hooks/ipc-events/ssh-managed-server-converted-catalogs.test.ts": 2199, - "src/renderer/src/hooks/ipc-events/ssh-managed-server-state-effects.test.ts": 646, - "src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge-placement.test.ts": 1359, - "src/renderer/src/hooks/ipc-events/terminal-ui-routing-ipc-bridge-split.test.ts": 261, - "src/renderer/src/hooks/ipc-events/updater-status-ipc-bridge.test.ts": 51, - "src/renderer/src/hooks/ipc-events/worktree-event-runtime-activation.test.ts": 1461, - "src/renderer/src/hooks/ipc-tab-switch-group-order-hydration.test.ts": 79, - "src/renderer/src/hooks/ipc-tab-switch-structured-tab-cycle.test.ts": 76, - "src/renderer/src/hooks/ipc-tab-switch.test.ts": 81, - "src/renderer/src/hooks/legacy-worker-terminal-recovery-event.test.ts": 67, - "src/renderer/src/hooks/macos-tcc-prompt-notice-subscription.test.ts": 61, - "src/renderer/src/hooks/metadata-request-cache.test.ts": 140, - "src/renderer/src/hooks/mobile-terminal-reveal-tab-adoption.test.ts": 2706, - "src/renderer/src/hooks/modal-return-focus-action.test.ts": 53, - "src/renderer/src/hooks/programmatic-scroll-marks.test.ts": 46, - "src/renderer/src/hooks/runtime-client-events-sync-ownership.test.ts": 70, - "src/renderer/src/hooks/runtime-client-events-sync.test.ts": 114, - "src/renderer/src/hooks/runtime-project-refresh-scheduler.test.ts": 99, - "src/renderer/src/hooks/shortcut-label-cache.test.tsx": 2077, - "src/renderer/src/hooks/structured-session-completion-focus.test.ts": 1950, - "src/renderer/src/hooks/unpaired-device-auth-notification.test.ts": 47, - "src/renderer/src/hooks/use-audio-capture.capture-loss.test.ts": 350, - "src/renderer/src/hooks/use-clipboard-text-copy-feedback.test.ts": 262, - "src/renderer/src/hooks/use-document-dark-theme.test.tsx": 268, - "src/renderer/src/hooks/use-list-multi-selection.test.tsx": 243, - "src/renderer/src/hooks/use-now.test.ts": 250, - "src/renderer/src/hooks/use-os-file-drop-owner.test.tsx": 410, - "src/renderer/src/hooks/use-palette-search-evaluation-context.test.ts": 272, - "src/renderer/src/hooks/use-task-page-github-work-item-mutation-host.test.ts": 1506, - "src/renderer/src/hooks/use-terminal-quick-command-hosts.test.ts": 434, - "src/renderer/src/hooks/use-window-stream-visibility.test.ts": 274, - "src/renderer/src/hooks/useActiveProjectSkillRuntime.test.tsx": 1752, - "src/renderer/src/hooks/useAgentDetectionTarget.test.ts": 1369, - "src/renderer/src/hooks/useAppMenuPaste.test.tsx": 225, - "src/renderer/src/hooks/useAppMenuSelectionActions.test.tsx": 270, - "src/renderer/src/hooks/useReadOnScreenAttention.away.test.ts": 1687, - "src/renderer/src/hooks/useReadOnScreenAttention.clock-skew.test.ts": 1904, - "src/renderer/src/hooks/useReadOnScreenAttention.floating-panel.test.ts": 1665, - "src/renderer/src/hooks/useReadOnScreenAttention.focus-group.test.ts": 1914, - "src/renderer/src/hooks/useReadOnScreenAttention.mixed-workspace.test.ts": 1866, - "src/renderer/src/hooks/useReadOnScreenAttention.test.ts": 1475, - "src/renderer/src/hooks/useAutomationDispatchEvents.test.ts": 1410, - "src/renderer/src/hooks/useComposerState-decisions.test.ts": 59, - "src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts": 118, - "src/renderer/src/hooks/useComposerState-host-retarget.test.ts": 98, - "src/renderer/src/hooks/useComposerState.integration.test.ts": 3120, - "src/renderer/src/hooks/useDetectedAgents.test.tsx": 1507, - "src/renderer/src/hooks/useEditorExternalWatch-complexity.test.ts": 745, - "src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts": 517, - "src/renderer/src/hooks/useEditorExternalWatch-subscriptions.test.tsx": 272, - "src/renderer/src/hooks/useEditorExternalWatch-targets.test.ts": 376, - "src/renderer/src/hooks/useEditorExternalWatch-wsl-repro.test.ts": 579, - "src/renderer/src/hooks/useEditorExternalWatch.test.ts": 590, - "src/renderer/src/hooks/useEphemeralVmRecipeOptions.test.tsx": 219, - "src/renderer/src/hooks/useGitHubSlugMetadata.test.tsx": 224, - "src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx": 1476, - "src/renderer/src/hooks/useInstalledAgentSkills.test.ts": 1366, - "src/renderer/src/hooks/useIpcEvents-agent-dashboard-shortcut.test.ts": 48, - "src/renderer/src/hooks/useIpcEvents-agent-launch-closed-tab.test.ts": 1906, - "src/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.ts": 3396, - "src/renderer/src/hooks/useIpcEvents-agent-status-connection-attribution.test.ts": 3402, - "src/renderer/src/hooks/useIpcEvents-agent-status-hook-titles.test.ts": 4162, - "src/renderer/src/hooks/useIpcEvents-agent-status-pane-teardown.test.ts": 2945, - "src/renderer/src/hooks/useIpcEvents-agent-status-queue-ordering.test.ts": 2132, - "src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-hydration.test.ts": 10394, - "src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-replay.test.ts": 2794, - "src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts": 3533, - "src/renderer/src/hooks/useIpcEvents-agent-status-turn-completion.test.ts": 1596, - "src/renderer/src/hooks/useIpcEvents-browser-certificate-failure.test.ts": 2422, - "src/renderer/src/hooks/useIpcEvents-browser-navigation.test.ts": 1521, - "src/renderer/src/hooks/useIpcEvents-browser-tab-create.test.ts": 1398, - "src/renderer/src/hooks/useIpcEvents-cli-worktree-activation.test.ts": 1748, - "src/renderer/src/hooks/useIpcEvents-client-hosted-browser-rows.test.ts": 2627, - "src/renderer/src/hooks/useIpcEvents-close-routing-active-browser-tab.test.ts": 3432, - "src/renderer/src/hooks/useIpcEvents-close-routing-browser-pages.test.ts": 2641, - "src/renderer/src/hooks/useIpcEvents-close-routing-floating-guest.test.ts": 1892, - "src/renderer/src/hooks/useIpcEvents-close-routing-session-tabs.test.ts": 4606, - "src/renderer/src/hooks/useIpcEvents-cmd-j-digit-chord.test.ts": 2911, - "src/renderer/src/hooks/useIpcEvents-lifecycle.test.ts": 1437, - "src/renderer/src/hooks/useIpcEvents-new-workspace-shortcut.test.ts": 72, - "src/renderer/src/hooks/useIpcEvents-rate-limit-hydration.test.ts": 1595, - "src/renderer/src/hooks/useIpcEvents-repos-changed-catalogs.test.ts": 2869, - "src/renderer/src/hooks/useIpcEvents-runtime-environment-selectors.test.ts": 325, - "src/renderer/src/hooks/useIpcEvents-session-tab-close-request.test.ts": 2114, - "src/renderer/src/hooks/useIpcEvents-silent-terminal-adoption.test.ts": 1752, - "src/renderer/src/hooks/useIpcEvents-ssh-disconnect-cleanup.test.ts": 1524, - "src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts": 1746, - "src/renderer/src/hooks/useIpcEvents-updater-status.test.ts": 1988, - "src/renderer/src/hooks/useIssueMetadata.test.tsx": 262, - "src/renderer/src/hooks/useLinearProviderConnected.test.tsx": 1249, - "src/renderer/src/hooks/useMacTccAttributionSeveredNotice.test.tsx": 2312, - "src/renderer/src/hooks/useMacosTccPromptNotice.test.tsx": 1916, - "src/renderer/src/hooks/useMetadataListRequest.test.tsx": 277, - "src/renderer/src/hooks/useModalReturnFocus.test.tsx": 1441, - "src/renderer/src/hooks/usePrimarySelectionPaste.terminal-native-paste.test.tsx": 199, - "src/renderer/src/hooks/usePrimarySelectionPaste.test.tsx": 282, - "src/renderer/src/hooks/useResetCountdownClock.test.ts": 303, - "src/renderer/src/hooks/useRetiredWorktreeNames.test.ts": 600, - "src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx": 966, - "src/renderer/src/hooks/useSettingsNavigationMetadata.language-switch.test.tsx": 2576, - "src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts": 2176, - "src/renderer/src/hooks/useSidebarResize.test.ts": 59, - "src/renderer/src/hooks/useSkillFreshness.test.tsx": 273, - "src/renderer/src/hooks/useUnreadDockBadge.test.ts": 1844, - "src/renderer/src/hooks/useVirtualizedScrollAnchor.listener-deps.test.ts": 88, - "src/renderer/src/hooks/useVirtualizedScrollAnchor.marks-restore-signal.test.ts": 144, - "src/renderer/src/hooks/useVirtualizedScrollAnchor.test.ts": 53, - "src/renderer/src/hooks/useVirtualizedScrollAnchor.test.tsx": 260, - "src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.test.tsx": 226, - "src/renderer/src/hooks/viewport-size-change-listener.test.ts": 56, - "src/renderer/src/hooks/worktree-change-refresh-queue.test.ts": 61, - "src/renderer/src/hooks/worktree-head-identity-apply.test.ts": 52, - "src/renderer/src/hooks/zoom-routing.test.ts": 686, - "src/renderer/src/i18n/I18nProvider.test.tsx": 1693, - "src/renderer/src/i18n/intl-locale.test.ts": 148, - "src/renderer/src/i18n/ja-technical-literal-mistranslations.test.ts": 105, - "src/renderer/src/i18n/ko-ui-semantic-mistranslations.test.ts": 101, - "src/renderer/src/i18n/lazy-locale.test.ts": 194, - "src/renderer/src/i18n/locale-english-regression.test.ts": 825, - "src/renderer/src/i18n/native-chat-locales.test.ts": 952, - "src/renderer/src/i18n/plugin-chrome-allowlist.test.ts": 96, - "src/renderer/src/i18n/provider-credit-balance-locales.test.ts": 364, - "src/renderer/src/i18n/pseudo-localization.test.ts": 80, - "src/renderer/src/i18n/relative-duration-copy-localization.test.ts": 693, - "src/renderer/src/i18n/relative-time-format.test.ts": 61, - "src/renderer/src/i18n/runtime-required-catalog.test.ts": 664, - "src/renderer/src/i18n/settings-status-label-localization.test.ts": 288, - "src/renderer/src/i18n/technical-literal-catalog-values.test.ts": 250, - "src/renderer/src/i18n/weekday-names.test.ts": 86, - "src/renderer/src/i18n/zh-technical-literal-mistranslations.test.ts": 135, - "src/renderer/src/lazy-modal-mount-state.test.ts": 57, - "src/renderer/src/lib/activate-ai-vault-structured-session-host.test.ts": 1143, - "src/renderer/src/lib/activate-ai-vault-structured-session-reveal.test.ts": 1773, - "src/renderer/src/lib/activate-ai-vault-structured-session.test.ts": 1867, - "src/renderer/src/lib/activate-tab-and-focus-pane.test.ts": 59, - "src/renderer/src/lib/active-agent-note-send-explicit-target.test.ts": 966, - "src/renderer/src/lib/active-agent-note-send-focused-session.test.ts": 638, - "src/renderer/src/lib/active-agent-note-send-runtime-error-codes.test.ts": 80, - "src/renderer/src/lib/active-agent-note-send-target-detection.test.ts": 525, - "src/renderer/src/lib/active-view-persist.test.ts": 46, - "src/renderer/src/lib/activity-thread-display.test.ts": 68, - "src/renderer/src/lib/agent-background-session-launch-host.test.ts": 1472, - "src/renderer/src/lib/agent-detection-target-inventory.test.ts": 1465, - "src/renderer/src/lib/agent-draft-readiness.test.ts": 142, - "src/renderer/src/lib/agent-followup-delivery.test.ts": 464, - "src/renderer/src/lib/agent-hibernation-confirmation.test.ts": 48, - "src/renderer/src/lib/agent-hibernation-coordinator.test.ts": 1779, - "src/renderer/src/lib/agent-hibernation-output-activity.test.ts": 48, - "src/renderer/src/lib/agent-hibernation-pane-age.test.ts": 56, - "src/renderer/src/lib/agent-hibernation-planner.test.ts": 207, - "src/renderer/src/lib/agent-hibernation-runtime-liveness-race.test.ts": 1423, - "src/renderer/src/lib/agent-hibernation-visibility.test.ts": 1582, - "src/renderer/src/lib/agent-launch-caller-permissions.test.ts": 1006, - "src/renderer/src/lib/agent-launch-caller-placement.test.ts": 1164, - "src/renderer/src/lib/agent-launch-caller-prompt-transport.test.ts": 1248, - "src/renderer/src/lib/agent-launch-caller-routing.test.ts": 1132, - "src/renderer/src/lib/agent-launch-pane-spawn-hold.test.ts": 47, - "src/renderer/src/lib/agent-launch-pane-verdict-application.test.ts": 1221, - "src/renderer/src/lib/agent-launch-prompt-delivery.test.ts": 82, - "src/renderer/src/lib/agent-launch-route-connection-fallback.test.ts": 1819, - "src/renderer/src/lib/agent-launch-route-input.test.ts": 1635, - "src/renderer/src/lib/agent-launch-route-registered-agents.test.ts": 1571, - "src/renderer/src/lib/agent-launch-routing.test.ts": 83, - "src/renderer/src/lib/agent-launch-tab-publication.test.ts": 1452, - "src/renderer/src/lib/agent-launch-through-host.test.ts": 1481, - "src/renderer/src/lib/agent-message-send.test.ts": 251, - "src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts": 175, - "src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts": 194, - "src/renderer/src/lib/agent-paste-draft.test.ts": 324, - "src/renderer/src/lib/agent-picker-search.test.ts": 167, - "src/renderer/src/lib/agent-ready-wait.test.ts": 125, - "src/renderer/src/lib/agent-resume-launch-target.test.ts": 3321, - "src/renderer/src/lib/agent-row-primary-text.test.ts": 61, - "src/renderer/src/lib/agent-row-tool-preview.test.ts": 59, - "src/renderer/src/lib/agent-send-title-status.test.ts": 98, - "src/renderer/src/lib/agent-session-fork-context.test.ts": 68, - "src/renderer/src/lib/agent-session-launch-plan.test.ts": 1740, - "src/renderer/src/lib/agent-skill-cli-prerequisite.test.ts": 143, - "src/renderer/src/lib/agent-skill-nav-install-status.test.ts": 1387, - "src/renderer/src/lib/agent-startup-delayed-delivery-perf.test.ts": 1373, - "src/renderer/src/lib/agent-status-connection-ownership.test.ts": 127, - "src/renderer/src/lib/agent-status-count.test.ts": 101, - "src/renderer/src/lib/agent-status-epoch-clock.test.ts": 52, - "src/renderer/src/lib/agent-status-evidence-clock.test.ts": 89, - "src/renderer/src/lib/agent-status-terminal-title.test.ts": 117, - "src/renderer/src/lib/agent-status-worktree-attribution.test.ts": 57, - "src/renderer/src/lib/agent-status.test.ts": 489, - "src/renderer/src/lib/agent-tab-shortcuts.test.ts": 78, - "src/renderer/src/lib/ai-vault-antigravity-reference-host.test.ts": 450, - "src/renderer/src/lib/ai-vault-omp-cold-resume.test.ts": 1762, - "src/renderer/src/lib/ai-vault-resume-command.drop-repin.test.ts": 447, - "src/renderer/src/lib/ai-vault-resume-command.resumable-agent.test.ts": 459, - "src/renderer/src/lib/ai-vault-resume-command.test.ts": 412, - "src/renderer/src/lib/ai-vault-resume-shell.test.ts": 466, - "src/renderer/src/lib/ai-vault-resume-target.test.ts": 110, - "src/renderer/src/lib/ai-vault-session-drag.test.ts": 67, - "src/renderer/src/lib/ai-vault-session-fork-startup.test.ts": 413, - "src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts": 64, - "src/renderer/src/lib/ai-vault-tab-title-sync-inputs.test.ts": 423, - "src/renderer/src/lib/ai-vault-tab-title-sync.test.ts": 957, - "src/renderer/src/lib/app-command-dispatch.test.ts": 79, - "src/renderer/src/lib/app-font-family.test.ts": 270, - "src/renderer/src/lib/app-menu-paste.test.ts": 209, - "src/renderer/src/lib/app-menu-selection-actions.test.ts": 122, - "src/renderer/src/lib/automation-session-observer.test.ts": 135, - "src/renderer/src/lib/automation-session-reuse.test.ts": 91, - "src/renderer/src/lib/automation-terminal-ownership.test.ts": 1337, - "src/renderer/src/lib/bounded-generation-map.test.ts": 46, - "src/renderer/src/lib/browser-cookie-import-toast.test.ts": 102, - "src/renderer/src/lib/browser-history-match.performance.test.ts": 344, - "src/renderer/src/lib/browser-history-match.test.ts": 334, - "src/renderer/src/lib/browser-page-conversion-history.test.ts": 59, - "src/renderer/src/lib/browser-page-palette-activation.test.ts": 1521, - "src/renderer/src/lib/browser-palette-page-entries.test.ts": 417, - "src/renderer/src/lib/browser-palette-search.test.ts": 322, - "src/renderer/src/lib/browser-uuid.test.ts": 54, - "src/renderer/src/lib/browser-workspace-source-resolution.test.ts": 48, - "src/renderer/src/lib/browser-workspace-tab-activation.test.ts": 1497, - "src/renderer/src/lib/client-creation-action-policy.test.ts": 298, - "src/renderer/src/lib/cmd-j-github-url-lookup.test.ts": 54, - "src/renderer/src/lib/cmd-j-host-qualified-candidate-ownership.test.ts": 422, - "src/renderer/src/lib/cmd-j-linear-issue-intent.test.ts": 55, + "src/renderer/src/components/terminal/use-worktree-files.test.tsx": 301, + "src/renderer/src/components/terminal/window-close-running-work.test.ts": 85, + "src/renderer/src/components/ui/color-picker.test.tsx": 353, + "src/renderer/src/components/ui/popover-wheel-scroll.test.tsx": 507, + "src/renderer/src/components/ui/repo-multi-combobox.test.ts": 161, + "src/renderer/src/components/ui/scroll-area.test.tsx": 205, + "src/renderer/src/components/ui/slider.test.tsx": 396, + "src/renderer/src/components/ui/switch.test.tsx": 80, + "src/renderer/src/components/unexpected-signout/unexpected-signout-card.test.tsx": 1940, + "src/renderer/src/components/unexpected-signout/unexpected-signout-visibility.test.ts": 49, + "src/renderer/src/components/use-github-task-search-commit.test.ts": 260, + "src/renderer/src/components/use-jira-create-assignee.test.tsx": 236, + "src/renderer/src/components/use-jira-project-inventory.test.tsx": 601, + "src/renderer/src/components/use-quick-open-interaction.test.tsx": 1634, + "src/renderer/src/components/use-task-creation-draft-retention.test.ts": 383, + "src/renderer/src/components/use-task-page-gitlab-loading.test.tsx": 1771, + "src/renderer/src/components/use-task-page-jira-list-effects.test.ts": 2405, + "src/renderer/src/components/use-task-page-jira-list-state.owner.test.tsx": 562, + "src/renderer/src/components/use-task-page-jira-project-scope.test.tsx": 1702, + "src/renderer/src/components/use-task-page-jira-quick-filters.test.tsx": 1272, + "src/renderer/src/components/use-task-page-jira-saved-filters.test.tsx": 1357, + "src/renderer/src/components/use-task-page-jira-summary-generation.test.tsx": 362, + "src/renderer/src/components/use-terminal-create-actions.test.tsx": 282, + "src/renderer/src/components/use-terminal-editor-close-foundation.window-close.test.tsx": 1550, + "src/renderer/src/components/use-terminal-editor-close-queue.reveal.test.tsx": 606, + "src/renderer/src/components/use-terminal-parking-pass.test.tsx": 1779, + "src/renderer/src/components/use-terminal-window-lifecycle.lazy-ref.test.tsx": 233, + "src/renderer/src/components/use-worktree-jump-palette-browser-ownership.test.ts": 1613, + "src/renderer/src/components/use-worktree-jump-palette-task-search.test.tsx": 1788, + "src/renderer/src/components/virtualized-list.test.tsx": 543, + "src/renderer/src/components/window-close-request-coordinator.test.ts": 246, + "src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.mount-gating.test.tsx": 2698, + "src/renderer/src/components/workspace-cleanup/WorkspaceCleanupDialog.stale-while-revalidate.test.tsx": 2451, + "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-browse-state.test.tsx": 233, + "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-facet-rows.test.tsx": 550, + "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-git-evidence.test.tsx": 316, + "src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-row-order.test.ts": 236, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-active-facets.test.ts": 52, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal-late-settlement.test.ts": 1481, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal-snapshot-batch.test.ts": 1386, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-background-removal.test.ts": 131, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-list.test.tsx": 1878, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row-data.test.ts": 114, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-candidate-row.test.tsx": 1930, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.test.tsx": 407, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-deletion-phases.test.ts": 67, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-dialog-notices.test.tsx": 432, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-controls.test.tsx": 702, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-filter.test.ts": 137, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-facet-sort.test.ts": 91, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-filter-bar.test.tsx": 672, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-flat-list.test.tsx": 2272, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-git-evidence.test.ts": 85, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-presentation.test.ts": 121, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-candidates.test.ts": 65, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-settlement.test.ts": 73, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-removal-timeout-recovery.test.ts": 1337, + "src/renderer/src/components/workspace-cleanup/workspace-cleanup-sort-header.test.tsx": 375, + "src/renderer/src/components/workspace-emoji/WorkspaceEmojiSuggestionPopover.test.tsx": 456, + "src/renderer/src/components/workspace-emoji/useWorkspaceEmojiShortcodeInput.test.tsx": 290, + "src/renderer/src/components/workspace-surface-projection.test.ts": 66, + "src/renderer/src/components/worktree-creation/WorktreeCreationPanel.test.tsx": 314, + "src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx": 3913, + "src/renderer/src/components/worktree-jump-palette-primitives.test.tsx": 355, + "src/renderer/src/components/worktree-jump-palette-quick-action-availability.test.tsx": 752, + "src/renderer/src/components/worktree-jump-palette-recent-inclusion.test.ts": 133, + "src/renderer/src/components/worktree-jump-palette-sleeping-filter.test.ts": 1266, + "src/renderer/src/components/worktree-jump-palette-status-inputs.test.ts": 43, + "src/renderer/src/hooks/agent-auto-ack-targets.test.ts": 1391, + "src/renderer/src/hooks/agent-hook-completion-background-turn-notifications.test.ts": 282, + "src/renderer/src/hooks/agent-hook-completion-fresh-working.test.ts": 179, + "src/renderer/src/hooks/agent-hook-completion-grok-notifications.test.ts": 417, + "src/renderer/src/hooks/agent-hook-completion-notifications-import.test.ts": 164, + "src/renderer/src/hooks/agent-hook-completion-notifications.test.ts": 471, + "src/renderer/src/hooks/agent-hook-completion-store-sync.test.ts": 38, + "src/renderer/src/hooks/automation-agent-status-entry-change.test.ts": 39, + "src/renderer/src/hooks/automation-dispatch-unverifiable-loss.test.ts": 154, + "src/renderer/src/hooks/automations-changed-event-attribution.test.ts": 1857, + "src/renderer/src/hooks/composer-branch-selection.test.ts": 60, + "src/renderer/src/hooks/composer-drop-failure-toast.test.ts": 87, + "src/renderer/src/hooks/composer-drop-result.test.ts": 29, + "src/renderer/src/hooks/composer-native-file-drop.test.ts": 44, + "src/renderer/src/hooks/composer-state/attachment-drop-failure.test.tsx": 579, + "src/renderer/src/hooks/composer-state/attachment-drop-lifetime.test.tsx": 726, + "src/renderer/src/hooks/composer-state/branch-start-point-actions.test.ts": 221, + "src/renderer/src/hooks/composer-state/composer-attachment-target.test.ts": 58, + "src/renderer/src/hooks/composer-state/composer-deferred-source-name.test.ts": 1570, + "src/renderer/src/hooks/composer-state/composer-name-ownership.test.ts": 1396, + "src/renderer/src/hooks/composer-state/composer-name-source-selection.test.ts": 1817, + "src/renderer/src/hooks/composer-state/composer-remote-server-drop.test.tsx": 666, + "src/renderer/src/hooks/composer-state/composer-review-base-fencing.test.ts": 2084, + "src/renderer/src/hooks/composer-state/composer-review-base-submit-cache.test.ts": 1951, + "src/renderer/src/hooks/composer-state/composer-submit-source-ownership.test.ts": 1917, + "src/renderer/src/hooks/composer-state/composer-target-state.jira-mapping.test.tsx": 1690, + "src/renderer/src/hooks/composer-state/draft-target-sync.test.ts": 234, + "src/renderer/src/hooks/composer-state/initial-target-state.jira-mapping.test.tsx": 265, + "src/renderer/src/hooks/composer-state/jira-mapped-owner-flow.integration.test.tsx": 3172, + "src/renderer/src/hooks/composer-state/multiple-create-reset.test.ts": 1589, + "src/renderer/src/hooks/composer-state/provider-runtime-sync.test.ts": 1548, + "src/renderer/src/hooks/composer-state/quick-creation-request.test.ts": 116, + "src/renderer/src/hooks/composer-state/server-ssh-repo-connect.test.ts": 708, + "src/renderer/src/hooks/direct-ssh-runtime-wake-isolation.test.tsx": 1591, + "src/renderer/src/hooks/direct-ssh-state-routing.test.ts": 164, + "src/renderer/src/hooks/editor-external-watch-fallback-explorer.test.ts": 333, + "src/renderer/src/hooks/editor-external-watch-selected-host.test.ts": 309, + "src/renderer/src/hooks/editor-runtime-file-watch.test.ts": 61, + "src/renderer/src/hooks/fork-push-warning.test.ts": 44, + "src/renderer/src/hooks/installed-agent-skill-discovery-cache.test.ts": 43, + "src/renderer/src/hooks/installed-agent-skill-discovery.test.ts": 63, + "src/renderer/src/hooks/installed-agent-skill-runtime-eviction.test.ts": 39, + "src/renderer/src/hooks/installed-agent-skill-verdict.test.ts": 1343, + "src/renderer/src/hooks/ipc-events/agent-status-pane-routing-index-retention.test.ts": 2123, + "src/renderer/src/hooks/ipc-events/agent-status-pane-routing-index.test.ts": 1512, + "src/renderer/src/hooks/ipc-events/agent-status-pending-retry-gate.test.ts": 371, + "src/renderer/src/hooks/ipc-events/agent-status-terminal-title-tab-write.test.ts": 1744, + "src/renderer/src/hooks/ipc-events/agent-status-unavailable.test.ts": 83, + "src/renderer/src/hooks/ipc-events/browser-automation-bootstrap-lease.test.ts": 169, + "src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.owner.test.ts": 464, + "src/renderer/src/hooks/ipc-events/browser-request-ipc-bridge.profile-switch.test.ts": 517, + "src/renderer/src/hooks/ipc-events/browser-state-capture-paint-hold.test.ts": 252, + "src/renderer/src/hooks/ipc-events/browser-state-open-link-profile.test.ts": 253, + "src/renderer/src/hooks/ipc-events/direct-ssh-connect-reply-routing.test.ts": 1418, + "src/renderer/src/hooks/ipc-events/direct-ssh-hydration-fanout.test.ts": 1356, + "src/renderer/src/hooks/ipc-events/direct-ssh-hydration-target-metadata.test.ts": 1280, + "src/renderer/src/hooks/ipc-events/floating-workspace-reveal-routing.test.ts": 1589, + "src/renderer/src/hooks/ipc-events/mobile-terminal-close-ipc-bridge.test.ts": 1566, + "src/renderer/src/hooks/ipc-events/normalize-agent-status-event.test.ts": 33, + "src/renderer/src/hooks/ipc-events/orca-profile-auth-ipc-bridge.test.ts": 1487, + "src/renderer/src/hooks/ipc-events/os-markdown-file-open-bridge.test.ts": 291, + "src/renderer/src/hooks/ipc-events/runtime-client-ipc-bridge-ownership.test.ts": 1203, + "src/renderer/src/hooks/ipc-events/runtime-file-position.test.ts": 1522, + "src/renderer/src/hooks/ipc-events/runtime-reconnect-host-status.test.ts": 1584, + "src/renderer/src/hooks/ipc-events/ssh-managed-server-converted-catalogs.test.ts": 2158, + "src/renderer/src/hooks/ipc-events/ssh-managed-server-state-effects.test.ts": 464, + "src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.test.ts": 79, + "src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge-placement.test.ts": 1354, + "src/renderer/src/hooks/ipc-events/terminal-ui-routing-ipc-bridge-split.test.ts": 488, + "src/renderer/src/hooks/ipc-events/updater-status-ipc-bridge.test.ts": 31, + "src/renderer/src/hooks/ipc-events/worktree-event-lineage-refresh.test.ts": 1383, + "src/renderer/src/hooks/ipc-events/worktree-event-runtime-activation.test.ts": 1322, + "src/renderer/src/hooks/ipc-events/wsl-account-notice-ipc-bridge.test.ts": 53, + "src/renderer/src/hooks/ipc-events/wsl-host-ipc-bridge.test.ts": 1388, + "src/renderer/src/hooks/ipc-tab-switch-group-order-hydration.test.ts": 54, + "src/renderer/src/hooks/ipc-tab-switch-structured-tab-cycle.test.ts": 54, + "src/renderer/src/hooks/ipc-tab-switch.test.ts": 58, + "src/renderer/src/hooks/legacy-worker-terminal-recovery-event.test.ts": 50, + "src/renderer/src/hooks/macos-tcc-prompt-notice-subscription.test.ts": 53, + "src/renderer/src/hooks/metadata-request-cache.test.ts": 157, + "src/renderer/src/hooks/mobile-terminal-reveal-tab-adoption.test.ts": 2077, + "src/renderer/src/hooks/modal-return-focus-action.test.ts": 42, + "src/renderer/src/hooks/programmatic-scroll-marks.test.ts": 34, + "src/renderer/src/hooks/runtime-client-events-sync-ownership.test.ts": 58, + "src/renderer/src/hooks/runtime-client-events-sync.test.ts": 118, + "src/renderer/src/hooks/runtime-project-refresh-scheduler.test.ts": 76, + "src/renderer/src/hooks/shortcut-label-cache.test.tsx": 1760, + "src/renderer/src/hooks/structured-session-completion-focus.test.ts": 1490, + "src/renderer/src/hooks/unpaired-device-auth-notification.test.ts": 46, + "src/renderer/src/hooks/use-audio-capture.capture-loss.test.ts": 249, + "src/renderer/src/hooks/use-clipboard-text-copy-feedback.test.ts": 253, + "src/renderer/src/hooks/use-document-dark-theme.test.tsx": 234, + "src/renderer/src/hooks/use-list-multi-selection.test.tsx": 231, + "src/renderer/src/hooks/use-now.test.ts": 235, + "src/renderer/src/hooks/use-os-file-drop-owner.test.tsx": 366, + "src/renderer/src/hooks/use-palette-search-evaluation-context.test.ts": 231, + "src/renderer/src/hooks/use-task-page-github-work-item-mutation-host.test.ts": 1575, + "src/renderer/src/hooks/use-terminal-quick-command-hosts.test.ts": 505, + "src/renderer/src/hooks/use-window-stream-visibility.test.ts": 278, + "src/renderer/src/hooks/useActiveProjectSkillRuntime.test.tsx": 1638, + "src/renderer/src/hooks/useAgentDetectionTarget.test.ts": 1335, + "src/renderer/src/hooks/useAppMenuPaste.test.tsx": 205, + "src/renderer/src/hooks/useAppMenuSelectionActions.test.tsx": 235, + "src/renderer/src/hooks/useAutomationDispatchEvents.test.ts": 1529, + "src/renderer/src/hooks/useComposerState-decisions.test.ts": 73, + "src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts": 74, + "src/renderer/src/hooks/useComposerState-host-retarget.test.ts": 51, + "src/renderer/src/hooks/useComposerState.integration.test.ts": 2293, + "src/renderer/src/hooks/useDetectedAgents.re-pair.test.tsx": 1433, + "src/renderer/src/hooks/useDetectedAgents.test.tsx": 1598, + "src/renderer/src/hooks/useEditorExternalWatch-complexity.test.ts": 606, + "src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts": 513, + "src/renderer/src/hooks/useEditorExternalWatch-subscriptions.test.tsx": 242, + "src/renderer/src/hooks/useEditorExternalWatch-targets.test.ts": 314, + "src/renderer/src/hooks/useEditorExternalWatch-wsl-repro.test.ts": 547, + "src/renderer/src/hooks/useEditorExternalWatch.test.ts": 658, + "src/renderer/src/hooks/useEphemeralVmRecipeOptions.test.tsx": 184, + "src/renderer/src/hooks/useGitHubSlugMetadata.test.tsx": 196, + "src/renderer/src/hooks/useInstalledAgentSkills.react.test.tsx": 1698, + "src/renderer/src/hooks/useInstalledAgentSkills.test.ts": 1414, + "src/renderer/src/hooks/useIpcEvents-agent-dashboard-shortcut.test.ts": 38, + "src/renderer/src/hooks/useIpcEvents-agent-launch-closed-tab.test.ts": 1176, + "src/renderer/src/hooks/useIpcEvents-agent-status-batch-projection.test.ts": 3518, + "src/renderer/src/hooks/useIpcEvents-agent-status-connection-attribution.test.ts": 2736, + "src/renderer/src/hooks/useIpcEvents-agent-status-hook-titles.test.ts": 3370, + "src/renderer/src/hooks/useIpcEvents-agent-status-pane-teardown.test.ts": 2298, + "src/renderer/src/hooks/useIpcEvents-agent-status-queue-ordering.test.ts": 1785, + "src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-hydration.test.ts": 5155, + "src/renderer/src/hooks/useIpcEvents-agent-status-snapshot-replay.test.ts": 2163, + "src/renderer/src/hooks/useIpcEvents-agent-status-ssh-authority.test.ts": 2461, + "src/renderer/src/hooks/useIpcEvents-agent-status-turn-completion.test.ts": 1170, + "src/renderer/src/hooks/useIpcEvents-browser-certificate-failure.test.ts": 1445, + "src/renderer/src/hooks/useIpcEvents-browser-navigation.test.ts": 1091, + "src/renderer/src/hooks/useIpcEvents-browser-tab-create.test.ts": 1229, + "src/renderer/src/hooks/useIpcEvents-cli-worktree-activation.test.ts": 1415, + "src/renderer/src/hooks/useIpcEvents-client-hosted-browser-rows.test.ts": 1782, + "src/renderer/src/hooks/useIpcEvents-close-routing-active-browser-tab.test.ts": 2166, + "src/renderer/src/hooks/useIpcEvents-close-routing-browser-pages.test.ts": 1525, + "src/renderer/src/hooks/useIpcEvents-close-routing-floating-guest.test.ts": 1638, + "src/renderer/src/hooks/useIpcEvents-close-routing-session-tabs.test.ts": 3732, + "src/renderer/src/hooks/useIpcEvents-cmd-j-digit-chord.test.ts": 1530, + "src/renderer/src/hooks/useIpcEvents-lifecycle.test.ts": 1363, + "src/renderer/src/hooks/useIpcEvents-new-workspace-shortcut.test.ts": 54, + "src/renderer/src/hooks/useIpcEvents-rate-limit-hydration.test.ts": 1194, + "src/renderer/src/hooks/useIpcEvents-repos-changed-catalogs.test.ts": 2739, + "src/renderer/src/hooks/useIpcEvents-runtime-environment-selectors.test.ts": 456, + "src/renderer/src/hooks/useIpcEvents-session-tab-close-request.test.ts": 1256, + "src/renderer/src/hooks/useIpcEvents-silent-terminal-adoption.test.ts": 1382, + "src/renderer/src/hooks/useIpcEvents-ssh-disconnect-cleanup.test.ts": 1238, + "src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts": 995, + "src/renderer/src/hooks/useIpcEvents-updater-status.test.ts": 1443, + "src/renderer/src/hooks/useIssueMetadata.test.tsx": 387, + "src/renderer/src/hooks/useLinearProviderConnected.test.tsx": 1342, + "src/renderer/src/hooks/useMacTccAttributionSeveredNotice.test.tsx": 2244, + "src/renderer/src/hooks/useMacosTccPromptNotice.test.tsx": 1945, + "src/renderer/src/hooks/useMetadataListRequest.test.tsx": 222, + "src/renderer/src/hooks/useModalReturnFocus.test.tsx": 1582, + "src/renderer/src/hooks/usePrefersReducedMotion.test.ts": 34, + "src/renderer/src/hooks/usePrimarySelectionPaste.terminal-native-paste.test.tsx": 227, + "src/renderer/src/hooks/usePrimarySelectionPaste.test.tsx": 238, + "src/renderer/src/hooks/useReadOnScreenAttention.away.test.ts": 1680, + "src/renderer/src/hooks/useReadOnScreenAttention.clock-skew.test.ts": 1402, + "src/renderer/src/hooks/useReadOnScreenAttention.floating-panel.test.ts": 1634, + "src/renderer/src/hooks/useReadOnScreenAttention.focus-group.test.ts": 1709, + "src/renderer/src/hooks/useReadOnScreenAttention.mixed-workspace.test.ts": 1490, + "src/renderer/src/hooks/useReadOnScreenAttention.test.ts": 1280, + "src/renderer/src/hooks/useResetCountdownClock.test.ts": 246, + "src/renderer/src/hooks/useRetiredWorktreeNames.test.ts": 586, + "src/renderer/src/hooks/useSettingsNavigationMetadata.capability-owner.test.tsx": 709, + "src/renderer/src/hooks/useSettingsNavigationMetadata.language-switch.test.tsx": 3222, + "src/renderer/src/hooks/useSettingsNavigationMetadata.test.ts": 2270, + "src/renderer/src/hooks/useSidebarResize.test.ts": 36, + "src/renderer/src/hooks/useSkillFreshness.test.tsx": 259, + "src/renderer/src/hooks/useUnreadDockBadge.test.ts": 1321, + "src/renderer/src/hooks/useVirtualizedScrollAnchor.listener-deps.test.ts": 56, + "src/renderer/src/hooks/useVirtualizedScrollAnchor.marks-restore-signal.test.ts": 106, + "src/renderer/src/hooks/useVirtualizedScrollAnchor.test.ts": 30, + "src/renderer/src/hooks/useVirtualizedScrollAnchor.test.tsx": 285, + "src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.test.tsx": 446, + "src/renderer/src/hooks/viewport-size-change-listener.test.ts": 33, + "src/renderer/src/hooks/worktree-change-refresh-queue.test.ts": 51, + "src/renderer/src/hooks/worktree-head-identity-apply.test.ts": 35, + "src/renderer/src/hooks/zoom-routing.test.ts": 636, + "src/renderer/src/i18n/I18nProvider.test.tsx": 1676, + "src/renderer/src/i18n/catalog-nested-settings.test.ts": 336, + "src/renderer/src/i18n/intl-locale.test.ts": 119, + "src/renderer/src/i18n/lazy-locale.test.ts": 200, + "src/renderer/src/i18n/plugin-chrome-allowlist.test.ts": 111, + "src/renderer/src/i18n/project-runtime-repair-message.test.ts": 91, + "src/renderer/src/i18n/provider-credit-balance-locales.test.ts": 696, + "src/renderer/src/i18n/pseudo-localization.test.ts": 56, + "src/renderer/src/i18n/relative-time-format.test.ts": 41, + "src/renderer/src/i18n/runtime-required-catalog.test.ts": 409, + "src/renderer/src/i18n/weekday-names.test.ts": 56, + "src/renderer/src/lazy-modal-mount-state.test.ts": 31, + "src/renderer/src/lib/activate-ai-vault-structured-session-host.test.ts": 1179, + "src/renderer/src/lib/activate-ai-vault-structured-session-reveal.test.ts": 1657, + "src/renderer/src/lib/activate-ai-vault-structured-session.test.ts": 1581, + "src/renderer/src/lib/activate-tab-and-focus-pane.test.ts": 48, + "src/renderer/src/lib/active-agent-note-send-explicit-target.test.ts": 956, + "src/renderer/src/lib/active-agent-note-send-focused-session.test.ts": 541, + "src/renderer/src/lib/active-agent-note-send-runtime-error-codes.test.ts": 262, + "src/renderer/src/lib/active-agent-note-send-target-detection.test.ts": 529, + "src/renderer/src/lib/active-view-persist.test.ts": 33, + "src/renderer/src/lib/activity-thread-display.test.ts": 85, + "src/renderer/src/lib/agent-background-session-launch-host.test.ts": 1363, + "src/renderer/src/lib/agent-detection-target-inventory.test.ts": 1428, + "src/renderer/src/lib/agent-draft-readiness.test.ts": 93, + "src/renderer/src/lib/agent-followup-delivery.test.ts": 91, + "src/renderer/src/lib/agent-hibernation-confirmation.test.ts": 38, + "src/renderer/src/lib/agent-hibernation-coordinator.test.ts": 1458, + "src/renderer/src/lib/agent-hibernation-output-activity.test.ts": 34, + "src/renderer/src/lib/agent-hibernation-pane-age.test.ts": 35, + "src/renderer/src/lib/agent-hibernation-planner.test.ts": 171, + "src/renderer/src/lib/agent-hibernation-runtime-liveness-race.test.ts": 1324, + "src/renderer/src/lib/agent-hibernation-visibility.test.ts": 1519, + "src/renderer/src/lib/agent-launch-caller-permissions.test.ts": 1399, + "src/renderer/src/lib/agent-launch-caller-placement.test.ts": 1025, + "src/renderer/src/lib/agent-launch-caller-prompt-transport.test.ts": 1100, + "src/renderer/src/lib/agent-launch-caller-routing.test.ts": 1302, + "src/renderer/src/lib/agent-launch-pane-start.test.ts": 33, + "src/renderer/src/lib/agent-launch-pane-verdict-application.test.ts": 1491, + "src/renderer/src/lib/agent-launch-prompt-delivery.test.ts": 55, + "src/renderer/src/lib/agent-launch-route-connection-fallback.test.ts": 1604, + "src/renderer/src/lib/agent-launch-route-input.test.ts": 1556, + "src/renderer/src/lib/agent-launch-route-registered-agents.test.ts": 1398, + "src/renderer/src/lib/agent-launch-routing.test.ts": 250, + "src/renderer/src/lib/agent-launch-tab-publication.test.ts": 1473, + "src/renderer/src/lib/agent-launch-through-host.test.ts": 1552, + "src/renderer/src/lib/agent-message-send.test.ts": 264, + "src/renderer/src/lib/agent-paste-draft-readiness-budget.test.ts": 176, + "src/renderer/src/lib/agent-paste-draft-submit-retry.test.ts": 172, + "src/renderer/src/lib/agent-paste-draft.test.ts": 352, + "src/renderer/src/lib/agent-picker-search.test.ts": 123, + "src/renderer/src/lib/agent-ready-wait.test.ts": 124, + "src/renderer/src/lib/agent-resume-launch-target.test.ts": 1607, + "src/renderer/src/lib/agent-row-primary-text.test.ts": 44, + "src/renderer/src/lib/agent-row-tool-preview.test.ts": 42, + "src/renderer/src/lib/agent-send-title-status.test.ts": 64, + "src/renderer/src/lib/agent-session-fork-context.test.ts": 74, + "src/renderer/src/lib/agent-session-launch-plan.test.ts": 1485, + "src/renderer/src/lib/agent-skill-cli-prerequisite.test.ts": 136, + "src/renderer/src/lib/agent-skill-nav-install-status.test.ts": 1402, + "src/renderer/src/lib/agent-startup-delayed-delivery-perf.test.ts": 1506, + "src/renderer/src/lib/agent-status-connection-ownership.test.ts": 88, + "src/renderer/src/lib/agent-status-count.test.ts": 91, + "src/renderer/src/lib/agent-status-epoch-clock.test.ts": 36, + "src/renderer/src/lib/agent-status-evidence-clock.test.ts": 80, + "src/renderer/src/lib/agent-status-terminal-title.test.ts": 81, + "src/renderer/src/lib/agent-status-worktree-attribution.test.ts": 33, + "src/renderer/src/lib/agent-status.test.ts": 180, + "src/renderer/src/lib/agent-tab-shortcuts.test.ts": 67, + "src/renderer/src/lib/ai-vault-antigravity-reference-host.test.ts": 537, + "src/renderer/src/lib/ai-vault-omp-cold-resume.test.ts": 1410, + "src/renderer/src/lib/ai-vault-resume-command.drop-repin.test.ts": 441, + "src/renderer/src/lib/ai-vault-resume-command.resumable-agent.test.ts": 404, + "src/renderer/src/lib/ai-vault-resume-command.test.ts": 483, + "src/renderer/src/lib/ai-vault-resume-shell.test.ts": 436, + "src/renderer/src/lib/ai-vault-resume-target.test.ts": 76, + "src/renderer/src/lib/ai-vault-session-drag.test.ts": 61, + "src/renderer/src/lib/ai-vault-session-fork-startup.test.ts": 397, + "src/renderer/src/lib/ai-vault-session-resume-preparation.test.ts": 58, + "src/renderer/src/lib/ai-vault-tab-title-sync-inputs.test.ts": 384, + "src/renderer/src/lib/ai-vault-tab-title-sync.test.ts": 837, + "src/renderer/src/lib/app-command-dispatch.test.ts": 68, + "src/renderer/src/lib/app-font-family.test.ts": 274, + "src/renderer/src/lib/app-menu-paste.test.ts": 211, + "src/renderer/src/lib/app-menu-selection-actions.test.ts": 134, + "src/renderer/src/lib/automation-session-observer.test.ts": 114, + "src/renderer/src/lib/automation-session-reuse.test.ts": 34, + "src/renderer/src/lib/automation-terminal-ownership.test.ts": 1336, + "src/renderer/src/lib/bounded-generation-map.test.ts": 33, + "src/renderer/src/lib/browser-cookie-import-toast.test.ts": 169, + "src/renderer/src/lib/browser-history-match.performance.test.ts": 300, + "src/renderer/src/lib/browser-history-match.test.ts": 293, + "src/renderer/src/lib/browser-page-conversion-history.test.ts": 47, + "src/renderer/src/lib/browser-page-palette-activation.test.ts": 1597, + "src/renderer/src/lib/browser-palette-page-entries.test.ts": 382, + "src/renderer/src/lib/browser-palette-search.test.ts": 318, + "src/renderer/src/lib/browser-uuid.test.ts": 38, + "src/renderer/src/lib/browser-workspace-source-resolution.test.ts": 33, + "src/renderer/src/lib/browser-workspace-tab-activation.test.ts": 1604, + "src/renderer/src/lib/client-creation-action-policy.test.ts": 327, + "src/renderer/src/lib/cmd-j-github-url-lookup.test.ts": 42, + "src/renderer/src/lib/cmd-j-host-qualified-candidate-ownership.test.ts": 395, + "src/renderer/src/lib/cmd-j-linear-issue-intent.test.ts": 30, "src/renderer/src/lib/cmd-j-section-leadership.test.ts": 77, - "src/renderer/src/lib/codex-account-display-label.test.ts": 55, - "src/renderer/src/lib/codex-account-restart-startup.test.ts": 1537, - "src/renderer/src/lib/codex-pane-restart-eligibility.test.ts": 81, - "src/renderer/src/lib/codex-pane-selection-lane.test.ts": 379, - "src/renderer/src/lib/codex-session-restart-account-recheck.test.ts": 1182, - "src/renderer/src/lib/codex-session-restart-shell-flap.test.ts": 1412, - "src/renderer/src/lib/codex-session-restart.test.ts": 3754, - "src/renderer/src/lib/codex-stale-pane-account-identity.test.ts": 1583, - "src/renderer/src/lib/codex-stale-pane-sweep.test.ts": 1535, - "src/renderer/src/lib/comment-body-line-count.test.ts": 115, - "src/renderer/src/lib/comment-body-submit-state.test.ts": 85, - "src/renderer/src/lib/composer-issue-command.test.ts": 1472, - "src/renderer/src/lib/composer-submit-cancellation.test.ts": 54, - "src/renderer/src/lib/connection-context.test.ts": 1611, - "src/renderer/src/lib/crash-diagnostics.test.ts": 136, - "src/renderer/src/lib/create-untitled-markdown.test.ts": 444, - "src/renderer/src/lib/default-creation-host.test.ts": 48, - "src/renderer/src/lib/default-terminal-tabs-applied-persist.test.ts": 1617, - "src/renderer/src/lib/desktop-window-chrome.test.ts": 56, - "src/renderer/src/lib/diff-comment-compat.test.ts": 50, - "src/renderer/src/lib/diff-comments-format.test.ts": 47, - "src/renderer/src/lib/direct-ssh-target-scope.test.ts": 70, - "src/renderer/src/lib/doc-preview-grants.test.ts": 370, - "src/renderer/src/lib/document-theme.test.ts": 55, - "src/renderer/src/lib/editable-target.test.ts": 175, - "src/renderer/src/lib/editor-file-operation-owner.test.ts": 1442, - "src/renderer/src/lib/editor-font-zoom.test.ts": 77, - "src/renderer/src/lib/empty-workspace-default-agent-chat.test.ts": 1422, - "src/renderer/src/lib/ensure-hooks-confirmed.test.ts": 589, - "src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts": 652, - "src/renderer/src/lib/ensure-simulator-tab.test.ts": 47, - "src/renderer/src/lib/ephemeral-vm-failed-create-cleanup.test.ts": 49, - "src/renderer/src/lib/ephemeral-vm-runtime-cleanup.test.ts": 82, - "src/renderer/src/lib/ephemeral-vm-workspace-target.integration.test.ts": 297, - "src/renderer/src/lib/ephemeral-vm-workspace-target.test.ts": 317, - "src/renderer/src/lib/ephemeral-vm-worktree-creation.test.ts": 102, - "src/renderer/src/lib/execute-open-editor-path-move.test.ts": 1738, - "src/renderer/src/lib/external-editor-open-capability.test.ts": 53, - "src/renderer/src/lib/feature-education-telemetry.test.ts": 63, - "src/renderer/src/lib/feedback-image-attachments.test.ts": 314, - "src/renderer/src/lib/feedback-image-shrink.test.ts": 53, - "src/renderer/src/lib/file-owner-runtime-target.test.ts": 284, - "src/renderer/src/lib/file-preview-action-visibility.test.tsx": 826, - "src/renderer/src/lib/file-preview.test.ts": 588, - "src/renderer/src/lib/file-search-result-owner.test.ts": 56, - "src/renderer/src/lib/file-search-selection.test.ts": 52, - "src/renderer/src/lib/file-search-shortcut-policy.test.ts": 181, - "src/renderer/src/lib/file-type-icons.test.ts": 108, - "src/renderer/src/lib/find-query-bounds.test.ts": 49, + "src/renderer/src/lib/codex-account-display-label.test.ts": 36, + "src/renderer/src/lib/codex-account-restart-startup.test.ts": 1480, + "src/renderer/src/lib/codex-pane-restart-eligibility.test.ts": 77, + "src/renderer/src/lib/codex-pane-selection-lane.test.ts": 576, + "src/renderer/src/lib/codex-session-restart-account-recheck.test.ts": 1365, + "src/renderer/src/lib/codex-session-restart-shell-flap.test.ts": 1503, + "src/renderer/src/lib/codex-session-restart.test.ts": 1561, + "src/renderer/src/lib/codex-stale-pane-account-identity.test.ts": 1418, + "src/renderer/src/lib/codex-stale-pane-sweep.test.ts": 1243, + "src/renderer/src/lib/comment-body-line-count.test.ts": 68, + "src/renderer/src/lib/comment-body-submit-state.test.ts": 33, + "src/renderer/src/lib/composer-issue-command.test.ts": 1235, + "src/renderer/src/lib/composer-submit-cancellation.test.ts": 40, + "src/renderer/src/lib/connection-context.test.ts": 1403, + "src/renderer/src/lib/crash-diagnostics.test.ts": 140, + "src/renderer/src/lib/create-untitled-markdown.test.ts": 373, + "src/renderer/src/lib/default-creation-host.test.ts": 44, + "src/renderer/src/lib/default-terminal-tabs-applied-persist.test.ts": 1437, + "src/renderer/src/lib/desktop-window-chrome.test.ts": 41, + "src/renderer/src/lib/diff-comment-compat.test.ts": 41, + "src/renderer/src/lib/diff-comments-format.test.ts": 36, + "src/renderer/src/lib/direct-ssh-target-scope.test.ts": 52, + "src/renderer/src/lib/doc-preview-grants.test.ts": 386, + "src/renderer/src/lib/document-theme.test.ts": 37, + "src/renderer/src/lib/editable-target.test.ts": 174, + "src/renderer/src/lib/editor-file-operation-owner.test.ts": 1375, + "src/renderer/src/lib/editor-font-zoom.test.ts": 40, + "src/renderer/src/lib/empty-workspace-default-agent-chat.test.ts": 1363, + "src/renderer/src/lib/ensure-hooks-confirmed.test.ts": 756, + "src/renderer/src/lib/ensure-simulator-tab-behavior.test.ts": 933, + "src/renderer/src/lib/ensure-simulator-tab.test.ts": 35, + "src/renderer/src/lib/ephemeral-vm-failed-create-cleanup.test.ts": 24, + "src/renderer/src/lib/ephemeral-vm-runtime-cleanup.test.ts": 55, + "src/renderer/src/lib/ephemeral-vm-workspace-target.integration.test.ts": 323, + "src/renderer/src/lib/ephemeral-vm-workspace-target.test.ts": 303, + "src/renderer/src/lib/ephemeral-vm-worktree-creation.test.ts": 75, + "src/renderer/src/lib/execute-open-editor-path-move.test.ts": 1893, + "src/renderer/src/lib/external-editor-open-capability.test.ts": 40, + "src/renderer/src/lib/feature-education-telemetry.test.ts": 69, + "src/renderer/src/lib/feedback-image-attachments.test.ts": 312, + "src/renderer/src/lib/feedback-image-shrink.test.ts": 40, + "src/renderer/src/lib/file-artifact-source-key.test.ts": 412, + "src/renderer/src/lib/file-owner-runtime-target.test.ts": 340, + "src/renderer/src/lib/file-preview-action-visibility.test.tsx": 835, + "src/renderer/src/lib/file-preview.test.ts": 589, + "src/renderer/src/lib/file-search-result-owner.test.ts": 53, + "src/renderer/src/lib/file-search-selection.test.ts": 43, + "src/renderer/src/lib/file-search-shortcut-policy.test.ts": 212, + "src/renderer/src/lib/file-type-icons.test.ts": 62, + "src/renderer/src/lib/find-query-bounds.test.ts": 43, "src/renderer/src/lib/finished-agent-resume-resurrection.test.ts": 1532, - "src/renderer/src/lib/fix-checks-agent-launch.test.ts": 500, - "src/renderer/src/lib/flatten-retained-slice.test.ts": 177, - "src/renderer/src/lib/floating-terminal.test.ts": 41, - "src/renderer/src/lib/floating-workspace-panel-reveal.test.ts": 291, - "src/renderer/src/lib/floating-workspace-terminal-actions.test.ts": 449, - "src/renderer/src/lib/floating-workspace-tour-interaction-snapshot.test.ts": 68, - "src/renderer/src/lib/focus-terminal-tab-surface.split-focus.test.ts": 131, - "src/renderer/src/lib/focus-terminal-tab-surface.test.ts": 71, - "src/renderer/src/lib/folder-workspace-path-status.test.ts": 108, - "src/renderer/src/lib/foreground-terminal-tabs.test.ts": 60, - "src/renderer/src/lib/github-links.test.ts": 69, - "src/renderer/src/lib/github-pr-start-point.test.ts": 91, - "src/renderer/src/lib/github-source-runtime-context.test.ts": 55, - "src/renderer/src/lib/github-work-item-source-lookup.test.ts": 93, - "src/renderer/src/lib/github-work-item-workspace-attachment.test.ts": 98, - "src/renderer/src/lib/gitlab-links.test.ts": 64, - "src/renderer/src/lib/gitlab-work-item-source-lookup.test.ts": 139, - "src/renderer/src/lib/hook-command-delayed-delivery-perf.test.ts": 1322, - "src/renderer/src/lib/hook-command-delayed-delivery.test.ts": 1644, - "src/renderer/src/lib/host-mirror-handle-gap-drain.test.ts": 1339, - "src/renderer/src/lib/host-mirror-handle-gap-landed-handle.test.ts": 1317, - "src/renderer/src/lib/host-mirror-handle-gap-resume.test.ts": 1612, - "src/renderer/src/lib/host-mirror-handle-gap-snapshot-lifetime.test.ts": 2086, - "src/renderer/src/lib/host-mirror-handle-gap-subscription-lifetime.test.ts": 1885, - "src/renderer/src/lib/host-mirror-handle-gap-teardown.test.ts": 1675, - "src/renderer/src/lib/host-mirror-handle-gap-verdict-union.test.ts": 1490, - "src/renderer/src/lib/host-mirrored-pane-resume-replay.test.ts": 1451, - "src/renderer/src/lib/http-link-destinations.test.ts": 308, - "src/renderer/src/lib/http-link-modifier-routing.test.ts": 307, - "src/renderer/src/lib/http-link-routing.test.ts": 326, - "src/renderer/src/lib/image-blob-png.test.ts": 234, - "src/renderer/src/lib/ime-composition-keyboard-event.test.ts": 299, - "src/renderer/src/lib/ime-overlay-escape.test.tsx": 647, - "src/renderer/src/lib/ime-text-field.test.tsx": 599, - "src/renderer/src/lib/ios-web-platform.test.ts": 53, - "src/renderer/src/lib/ipc-error.test.ts": 53, - "src/renderer/src/lib/jira-source-host.test.ts": 52, - "src/renderer/src/lib/keyboard-layout/detect-option-as-alt.test.ts": 60, - "src/renderer/src/lib/keyboard-layout/input-source-id.test.ts": 52, - "src/renderer/src/lib/keyboard-layout/layout-base-character.test.ts": 52, - "src/renderer/src/lib/keyboard-layout/option-as-alt-probe.test.ts": 98, - "src/renderer/src/lib/keyboard-layout/option-key-location-state.test.ts": 44, - "src/renderer/src/lib/language-detect.test.ts": 97, - "src/renderer/src/lib/large-text-control-paste.test.ts": 178, - "src/renderer/src/lib/launch-agent-background-session-extra-args.test.ts": 465, - "src/renderer/src/lib/launch-agent-background-session-remote.test.ts": 684, - "src/renderer/src/lib/launch-agent-background-session.test.ts": 556, - "src/renderer/src/lib/launch-agent-execution-context.test.ts": 1461, - "src/renderer/src/lib/launch-agent-fresh-new-tab-host-route.test.ts": 154, - "src/renderer/src/lib/launch-agent-in-new-tab-cwd.test.ts": 1001, - "src/renderer/src/lib/launch-agent-in-new-tab-fresh-host.test.ts": 1688, - "src/renderer/src/lib/launch-agent-in-new-tab-host-agents.test.ts": 591, - "src/renderer/src/lib/launch-agent-in-new-tab-host-resolution.test.ts": 965, - "src/renderer/src/lib/launch-agent-in-new-tab-main-surface.test.ts": 1838, - "src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts": 993, - "src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts": 1042, - "src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts": 1018, - "src/renderer/src/lib/launch-agent-in-new-tab.test.ts": 1018, - "src/renderer/src/lib/launch-agent-new-tab-host-route-readiness.test.ts": 337, - "src/renderer/src/lib/launch-agent-new-tab-host-route.test.ts": 1582, - "src/renderer/src/lib/launch-agent-session-continuation.test.ts": 71, - "src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts": 1111, - "src/renderer/src/lib/launch-ai-vault-session.test.ts": 61, - "src/renderer/src/lib/launch-structured-agent-session.test.ts": 2251, - "src/renderer/src/lib/launch-work-item-direct-agent-routing.test.ts": 1691, - "src/renderer/src/lib/launch-work-item-direct-agent.test.ts": 163, - "src/renderer/src/lib/launch-work-item-direct-messages.test.ts": 48, - "src/renderer/src/lib/launch-work-item-direct.test.ts": 1033, - "src/renderer/src/lib/lazy-chunk-recovery-reload.test.ts": 200, - "src/renderer/src/lib/lazy-with-retry.never-landed-reload.test.ts": 172, - "src/renderer/src/lib/lazy-with-retry.test.ts": 232, - "src/renderer/src/lib/left-sidebar-appearance.test.ts": 314, - "src/renderer/src/lib/linear-agent-skill-update-command.test.ts": 1484, - "src/renderer/src/lib/linear-board-drag-payload.test.ts": 63, - "src/renderer/src/lib/linear-issue-context-snapshot.test.ts": 95, - "src/renderer/src/lib/linear-issue-url-lookup.test.ts": 100, - "src/renderer/src/lib/linear-issue-workspace-attachment.test.ts": 88, - "src/renderer/src/lib/linear-issue-workspace-open.test.ts": 144, - "src/renderer/src/lib/linear-linked-work-item.test.ts": 81, - "src/renderer/src/lib/link-source-local-open-owner.test.ts": 295, - "src/renderer/src/lib/linked-work-item-context.test.ts": 67, - "src/renderer/src/lib/linked-work-item-provider.test.ts": 1457, - "src/renderer/src/lib/list-multi-selection.test.ts": 49, - "src/renderer/src/lib/list-row-interaction.test.ts": 141, - "src/renderer/src/lib/local-file-access.test.ts": 1545, - "src/renderer/src/lib/local-path-open-guard.test.ts": 87, - "src/renderer/src/lib/local-preflight-context-cache.test.ts": 397, - "src/renderer/src/lib/local-preflight-context.test.ts": 327, - "src/renderer/src/lib/locale-text-collators.test.ts": 48, - "src/renderer/src/lib/manual-terminal-worktree-parking.test.ts": 182, - "src/renderer/src/lib/markdown-comment-blocks.test.ts": 50, - "src/renderer/src/lib/markdown-document-templates.test.ts": 438, - "src/renderer/src/lib/markdown-review-note-copy.test.ts": 60, - "src/renderer/src/lib/markdown-review-notes-setting.test.ts": 47, - "src/renderer/src/lib/markdown-review-notes.test.ts": 81, - "src/renderer/src/lib/migration-unsupported-agent-entry.test.ts": 45, - "src/renderer/src/lib/mobile-terminal-tab-mount.test.ts": 55, - "src/renderer/src/lib/monaco-delayer-cancellation-guard.test.ts": 116, - "src/renderer/src/lib/monaco-diff-editor-disposal.test.ts": 49, - "src/renderer/src/lib/monaco-filename-language.test.ts": 68, - "src/renderer/src/lib/monaco-languages/monarch-embed-entry-recursion.test.ts": 530, - "src/renderer/src/lib/monaco-languages/monarch-upstream-mdx-recursion.test.ts": 2807, - "src/renderer/src/lib/monaco-languages/register-astro.test.ts": 152, - "src/renderer/src/lib/monaco-languages/register-jsonl.test.ts": 345, - "src/renderer/src/lib/monaco-languages/register-nim.test.ts": 69, - "src/renderer/src/lib/monaco-languages/register-quarto-adversarial.test.ts": 171, - "src/renderer/src/lib/monaco-languages/register-quarto.test.ts": 153, - "src/renderer/src/lib/monaco-languages/register-shell-markdown-aliases.test.ts": 50, - "src/renderer/src/lib/monaco-languages/register-svelte.test.ts": 169, - "src/renderer/src/lib/monaco-languages/register-typst.test.ts": 64, - "src/renderer/src/lib/monaco-languages/register-vue.test.ts": 325, - "src/renderer/src/lib/monaco-languages/textmate-language-registration.test.ts": 60, - "src/renderer/src/lib/monaco-languages/textmate-token-provider.test.ts": 287, - "src/renderer/src/lib/monaco-peek-preview-options.test.ts": 911, - "src/renderer/src/lib/monaco-setup-steps.test.ts": 58, - "src/renderer/src/lib/monospace-font-family.test.ts": 54, - "src/renderer/src/lib/native-chat-launch-draft-mirrorability.test.ts": 298, - "src/renderer/src/lib/native-chat-transcript-readability.test.ts": 50, - "src/renderer/src/lib/native-file-drop-rejection-message.test.ts": 89, - "src/renderer/src/lib/nested-repo-selected-paths.test.ts": 50, - "src/renderer/src/lib/new-agent-prompt-outcome.test.ts": 641, - "src/renderer/src/lib/new-workspace-composer-repo.test.ts": 64, - "src/renderer/src/lib/new-workspace-create-gates.test.ts": 54, - "src/renderer/src/lib/new-workspace-enter-guard.test.ts": 53, - "src/renderer/src/lib/new-workspace-project-options.test.ts": 74, - "src/renderer/src/lib/new-workspace-ssh-gate.test.ts": 52, - "src/renderer/src/lib/new-workspace.test.ts": 4504, - "src/renderer/src/lib/non-secure-context-crypto.repro.test.ts": 1464, - "src/renderer/src/lib/notes-send-agent-targets.test.ts": 369, - "src/renderer/src/lib/onboarding-project-checklist.test.ts": 54, - "src/renderer/src/lib/open-agent-message-sender.test.ts": 437, - "src/renderer/src/lib/open-document-in-floating-workspace.test.ts": 272, - "src/renderer/src/lib/open-mobile-emulator-tab.test.ts": 371, - "src/renderer/src/lib/open-tab-occupant-agent.test.ts": 123, - "src/renderer/src/lib/orca-hook-trust.test.ts": 53, - "src/renderer/src/lib/orca-yaml-trust-prompt-slot-eviction.test.ts": 796, - "src/renderer/src/lib/orchestration-setup-state.test.ts": 51, - "src/renderer/src/lib/orchestration-skill-coverage.test.ts": 142, + "src/renderer/src/lib/fix-checks-agent-launch.test.ts": 474, + "src/renderer/src/lib/flatten-retained-slice.test.ts": 220, + "src/renderer/src/lib/floating-terminal.test.ts": 39, + "src/renderer/src/lib/floating-workspace-panel-reveal.test.ts": 271, + "src/renderer/src/lib/floating-workspace-terminal-actions.test.ts": 437, + "src/renderer/src/lib/floating-workspace-tour-interaction-snapshot.test.ts": 35, + "src/renderer/src/lib/focus-terminal-tab-surface.split-focus.test.ts": 188, + "src/renderer/src/lib/focus-terminal-tab-surface.test.ts": 49, + "src/renderer/src/lib/folder-workspace-path-status.test.ts": 104, + "src/renderer/src/lib/foreground-terminal-tabs.test.ts": 33, + "src/renderer/src/lib/github-links.test.ts": 48, + "src/renderer/src/lib/github-pr-start-point.test.ts": 212, + "src/renderer/src/lib/github-source-runtime-context.test.ts": 33, + "src/renderer/src/lib/github-work-item-source-lookup.test.ts": 274, + "src/renderer/src/lib/github-work-item-workspace-attachment.test.ts": 95, + "src/renderer/src/lib/gitlab-links.test.ts": 56, + "src/renderer/src/lib/gitlab-work-item-source-lookup.test.ts": 291, + "src/renderer/src/lib/hook-command-delayed-delivery-perf.test.ts": 1364, + "src/renderer/src/lib/hook-command-delayed-delivery.test.ts": 1217, + "src/renderer/src/lib/host-mirror-handle-gap-drain.test.ts": 1366, + "src/renderer/src/lib/host-mirror-handle-gap-landed-handle.test.ts": 1357, + "src/renderer/src/lib/host-mirror-handle-gap-resume.test.ts": 1621, + "src/renderer/src/lib/host-mirror-handle-gap-snapshot-lifetime.test.ts": 2211, + "src/renderer/src/lib/host-mirror-handle-gap-subscription-lifetime.test.ts": 1726, + "src/renderer/src/lib/host-mirror-handle-gap-teardown.test.ts": 1356, + "src/renderer/src/lib/host-mirror-handle-gap-verdict-union.test.ts": 1443, + "src/renderer/src/lib/host-mirrored-pane-resume-replay.test.ts": 1464, + "src/renderer/src/lib/http-link-destinations.test.ts": 317, + "src/renderer/src/lib/http-link-modifier-routing.test.ts": 299, + "src/renderer/src/lib/http-link-routing.test.ts": 366, + "src/renderer/src/lib/image-blob-png.test.ts": 319, + "src/renderer/src/lib/ime-composition-keyboard-event.test.ts": 251, + "src/renderer/src/lib/ime-overlay-escape.test.tsx": 714, + "src/renderer/src/lib/ime-text-field.test.tsx": 512, + "src/renderer/src/lib/ios-web-platform.test.ts": 36, + "src/renderer/src/lib/ipc-error.test.ts": 49, + "src/renderer/src/lib/jira-board-drag-payload.test.ts": 46, + "src/renderer/src/lib/jira-issue-workspace-attachment.test.ts": 107, + "src/renderer/src/lib/jira-source-host.test.ts": 34, + "src/renderer/src/lib/keyboard-layout/detect-option-as-alt.test.ts": 51, + "src/renderer/src/lib/keyboard-layout/input-source-id.test.ts": 40, + "src/renderer/src/lib/keyboard-layout/layout-base-character.test.ts": 37, + "src/renderer/src/lib/keyboard-layout/option-as-alt-probe.test.ts": 91, + "src/renderer/src/lib/keyboard-layout/option-key-location-state.test.ts": 31, + "src/renderer/src/lib/language-detect.test.ts": 58, + "src/renderer/src/lib/large-text-control-paste.test.ts": 192, + "src/renderer/src/lib/launch-agent-background-session-extra-args.test.ts": 614, + "src/renderer/src/lib/launch-agent-background-session-launch-parity.test.ts": 1960, + "src/renderer/src/lib/launch-agent-background-session-remote.test.ts": 743, + "src/renderer/src/lib/launch-agent-background-session.test.ts": 1041, + "src/renderer/src/lib/launch-agent-execution-context.test.ts": 1405, + "src/renderer/src/lib/launch-agent-in-new-tab-cwd.test.ts": 940, + "src/renderer/src/lib/launch-agent-in-new-tab-host-agents.test.ts": 576, + "src/renderer/src/lib/launch-agent-in-new-tab-host-resolution.test.ts": 1014, + "src/renderer/src/lib/launch-agent-in-new-tab-main-surface.test.ts": 1768, + "src/renderer/src/lib/launch-agent-in-new-tab-placement.test.ts": 1188, + "src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts": 838, + "src/renderer/src/lib/launch-agent-in-new-tab-windows-quoting.test.ts": 1006, + "src/renderer/src/lib/launch-agent-in-new-tab.test.ts": 1109, + "src/renderer/src/lib/launch-agent-new-tab-host-route-readiness.test.ts": 381, + "src/renderer/src/lib/launch-agent-new-tab-host-route.test.ts": 51, + "src/renderer/src/lib/launch-agent-session-continuation.test.ts": 87, + "src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts": 1177, + "src/renderer/src/lib/launch-ai-vault-session.test.ts": 55, + "src/renderer/src/lib/launch-parity-plus-planner.test.ts": 1409, + "src/renderer/src/lib/launch-structured-agent-session.test.ts": 2191, + "src/renderer/src/lib/launch-work-item-direct-agent-routing.test.ts": 1508, + "src/renderer/src/lib/launch-work-item-direct-agent.test.ts": 130, + "src/renderer/src/lib/launch-work-item-direct-messages.test.ts": 35, + "src/renderer/src/lib/launch-work-item-direct-owner-row.test.ts": 989, + "src/renderer/src/lib/launch-work-item-direct.test.ts": 1020, + "src/renderer/src/lib/lazy-chunk-recovery-reload.test.ts": 215, + "src/renderer/src/lib/lazy-with-retry.never-landed-reload.test.ts": 157, + "src/renderer/src/lib/lazy-with-retry.test.ts": 259, + "src/renderer/src/lib/left-sidebar-appearance.test.ts": 272, + "src/renderer/src/lib/linear-agent-skill-update-command.test.ts": 1375, + "src/renderer/src/lib/linear-board-drag-payload.test.ts": 50, + "src/renderer/src/lib/linear-issue-context-snapshot.test.ts": 56, + "src/renderer/src/lib/linear-issue-url-lookup.test.ts": 248, + "src/renderer/src/lib/linear-issue-workspace-attachment.test.ts": 106, + "src/renderer/src/lib/linear-issue-workspace-open.test.ts": 137, + "src/renderer/src/lib/linear-linked-work-item.test.ts": 54, + "src/renderer/src/lib/link-source-local-open-owner.test.ts": 294, + "src/renderer/src/lib/linked-work-item-context.test.ts": 48, + "src/renderer/src/lib/linked-work-item-provider.test.ts": 1379, + "src/renderer/src/lib/list-multi-selection.test.ts": 46, + "src/renderer/src/lib/list-row-interaction.test.ts": 165, + "src/renderer/src/lib/local-file-access.test.ts": 1221, + "src/renderer/src/lib/local-path-open-guard.test.ts": 70, + "src/renderer/src/lib/local-preflight-context-cache.test.ts": 302, + "src/renderer/src/lib/local-preflight-context.test.ts": 425, + "src/renderer/src/lib/locale-text-collators.test.ts": 56, + "src/renderer/src/lib/manual-terminal-worktree-parking.test.ts": 169, + "src/renderer/src/lib/markdown-comment-blocks.test.ts": 35, + "src/renderer/src/lib/markdown-document-templates.test.ts": 388, + "src/renderer/src/lib/markdown-review-note-copy.test.ts": 53, + "src/renderer/src/lib/markdown-review-notes-setting.test.ts": 33, + "src/renderer/src/lib/markdown-review-notes.test.ts": 65, + "src/renderer/src/lib/migration-unsupported-agent-entry.test.ts": 28, + "src/renderer/src/lib/mobile-terminal-tab-mount.test.ts": 49, + "src/renderer/src/lib/monaco-delayer-cancellation-guard.test.ts": 184, + "src/renderer/src/lib/monaco-diff-editor-disposal.test.ts": 37, + "src/renderer/src/lib/monaco-editor-gutter-disposal.test.ts": 301, + "src/renderer/src/lib/monaco-filename-language.test.ts": 73, + "src/renderer/src/lib/monaco-languages/monarch-embed-entry-recursion.test.ts": 649, + "src/renderer/src/lib/monaco-languages/monarch-upstream-mdx-recursion.test.ts": 3810, + "src/renderer/src/lib/monaco-languages/register-astro.test.ts": 120, + "src/renderer/src/lib/monaco-languages/register-jsonl.test.ts": 104, + "src/renderer/src/lib/monaco-languages/register-nim.test.ts": 49, + "src/renderer/src/lib/monaco-languages/register-quarto-adversarial.test.ts": 366, + "src/renderer/src/lib/monaco-languages/register-quarto.test.ts": 343, + "src/renderer/src/lib/monaco-languages/register-shell-markdown-aliases.test.ts": 28, + "src/renderer/src/lib/monaco-languages/register-svelte.test.ts": 415, + "src/renderer/src/lib/monaco-languages/register-typst.test.ts": 56, + "src/renderer/src/lib/monaco-languages/register-vue.test.ts": 119, + "src/renderer/src/lib/monaco-languages/textmate-language-registration.test.ts": 43, + "src/renderer/src/lib/monaco-languages/textmate-token-provider.test.ts": 206, + "src/renderer/src/lib/monaco-peek-preview-options.test.ts": 975, + "src/renderer/src/lib/monaco-setup-steps.test.ts": 32, + "src/renderer/src/lib/monospace-font-family.test.ts": 41, + "src/renderer/src/lib/native-chat-launch-draft-mirrorability.test.ts": 406, + "src/renderer/src/lib/native-chat-transcript-readability.test.ts": 31, + "src/renderer/src/lib/native-file-drop-rejection-message.test.ts": 71, + "src/renderer/src/lib/nested-repo-selected-paths.test.ts": 43, + "src/renderer/src/lib/new-agent-prompt-outcome.test.ts": 849, + "src/renderer/src/lib/new-workspace-composer-repo.test.ts": 42, + "src/renderer/src/lib/new-workspace-create-gates.test.ts": 35, + "src/renderer/src/lib/new-workspace-enter-guard.test.ts": 41, + "src/renderer/src/lib/new-workspace-project-options.test.ts": 84, + "src/renderer/src/lib/new-workspace-ssh-gate.test.ts": 42, + "src/renderer/src/lib/new-workspace.test.ts": 4502, + "src/renderer/src/lib/non-secure-context-crypto.repro.test.ts": 1468, + "src/renderer/src/lib/notes-send-agent-targets.test.ts": 341, + "src/renderer/src/lib/onboarding-project-checklist.test.ts": 40, + "src/renderer/src/lib/open-agent-message-sender.test.ts": 457, + "src/renderer/src/lib/open-document-in-floating-workspace.test.ts": 237, + "src/renderer/src/lib/open-mobile-emulator-tab.test.ts": 389, + "src/renderer/src/lib/open-tab-occupant-agent.test.ts": 106, + "src/renderer/src/lib/orca-hook-trust.test.ts": 27, + "src/renderer/src/lib/orca-yaml-trust-prompt-slot-eviction.test.ts": 787, + "src/renderer/src/lib/orchestration-setup-state.test.ts": 26, + "src/renderer/src/lib/orchestration-skill-coverage.test.ts": 144, "src/renderer/src/lib/order-empty-query-worktrees.test.ts": 62, - "src/renderer/src/lib/os-file-drop-cancellation-guard.test.ts": 173, - "src/renderer/src/lib/os-file-drop-preload-integration.test.tsx": 489, - "src/renderer/src/lib/outside-dismiss-guard.test.ts": 50, - "src/renderer/src/lib/paired-web-client-host.test.ts": 405, - "src/renderer/src/lib/palette-match/cmd-j-ranking-contract.test.ts": 83, - "src/renderer/src/lib/palette-match/match-field-allocation.test.ts": 154, - "src/renderer/src/lib/palette-match/palette-match-core.test.ts": 121, - "src/renderer/src/lib/palette-match/palette-match-performance.test.ts": 6685, - "src/renderer/src/lib/palette-match/palette-phrase-placement.test.ts": 80, - "src/renderer/src/lib/palette-match/palette-ranking.test.ts": 63, - "src/renderer/src/lib/palette-repo-resolution.test.ts": 59, - "src/renderer/src/lib/palette-type-alias-match.test.ts": 51, - "src/renderer/src/lib/pane-agent-evidence.test.ts": 92, - "src/renderer/src/lib/pane-manager/browser-mobile-driver-state.test.ts": 61, - "src/renderer/src/lib/pane-manager/browser-remote-viewer-state.test.ts": 43, - "src/renderer/src/lib/pane-manager/client-hosted-browser-row-ephemerality.test.ts": 1413, - "src/renderer/src/lib/pane-manager/client-hosted-browser-row-state.test.ts": 63, - "src/renderer/src/lib/pane-manager/focus-follows-mouse.test.ts": 49, - "src/renderer/src/lib/pane-manager/mobile-driver-state.test.ts": 43, - "src/renderer/src/lib/pane-manager/mobile-fit-overrides-hydration.test.ts": 54, - "src/renderer/src/lib/pane-manager/mobile-fit-overrides.test.ts": 66, - "src/renderer/src/lib/pane-manager/pane-container-listener-lifecycle.test.ts": 607, - "src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.test.ts": 478, - "src/renderer/src/lib/pane-manager/pane-divider-capture-loss.test.ts": 59, - "src/renderer/src/lib/pane-manager/pane-divider-stray-touch.test.ts": 58, - "src/renderer/src/lib/pane-manager/pane-divider.test.ts": 78, - "src/renderer/src/lib/pane-manager/pane-dom-creation.test.ts": 274, - "src/renderer/src/lib/pane-manager/pane-dom-focus-class-sync-lifecycle.test.ts": 482, - "src/renderer/src/lib/pane-manager/pane-drag-reorder.test.ts": 118, - "src/renderer/src/lib/pane-manager/pane-drop-no-op.test.ts": 235, - "src/renderer/src/lib/pane-manager/pane-fit-resize-observer.test.ts": 105, - "src/renderer/src/lib/pane-manager/pane-fit.test.ts": 139, - "src/renderer/src/lib/pane-manager/pane-initial-fit-lifecycle.test.ts": 197, - "src/renderer/src/lib/pane-manager/pane-inline-image-recovery.test.ts": 76, - "src/renderer/src/lib/pane-manager/pane-inline-images.test.ts": 97, - "src/renderer/src/lib/pane-manager/pane-key-resolution.test.ts": 50, - "src/renderer/src/lib/pane-manager/pane-layout-geometry-apply.test.ts": 384, - "src/renderer/src/lib/pane-manager/pane-layout-gesture.test.ts": 228, - "src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts": 243, - "src/renderer/src/lib/pane-manager/pane-manager-registry.test.ts": 70, - "src/renderer/src/lib/pane-manager/pane-manager-spawn-placement.test.ts": 458, - "src/renderer/src/lib/pane-manager/pane-metric-options-deferral.test.ts": 63, - "src/renderer/src/lib/pane-manager/pane-overlay-focus.test.ts": 387, - "src/renderer/src/lib/pane-manager/pane-pointer-focus.test.ts": 52, - "src/renderer/src/lib/pane-manager/pane-pty-resize-hold.test.ts": 47, - "src/renderer/src/lib/pane-manager/pane-reveal-fit.test.ts": 62, - "src/renderer/src/lib/pane-manager/pane-reveal-lifetime.test.ts": 1002, - "src/renderer/src/lib/pane-manager/pane-reveal-repaint.test.ts": 350, - "src/renderer/src/lib/pane-manager/pane-scroll.test.ts": 179, - "src/renderer/src/lib/pane-manager/pane-split-close.test.ts": 67, - "src/renderer/src/lib/pane-manager/pane-split-scroll.test.ts": 85, - "src/renderer/src/lib/pane-manager/pane-terminal-foreground-render-settle.test.ts": 54, - "src/renderer/src/lib/pane-manager/pane-terminal-gpu-acceleration.test.ts": 108, + "src/renderer/src/lib/os-file-drop-cancellation-guard.test.ts": 175, + "src/renderer/src/lib/os-file-drop-preload-integration.test.tsx": 497, + "src/renderer/src/lib/outside-dismiss-guard.test.ts": 34, + "src/renderer/src/lib/paired-web-client-host.test.ts": 327, + "src/renderer/src/lib/palette-match/cmd-j-ranking-contract.test.ts": 80, + "src/renderer/src/lib/palette-match/match-field-allocation.test.ts": 146, + "src/renderer/src/lib/palette-match/palette-match-core.test.ts": 105, + "src/renderer/src/lib/palette-match/palette-match-performance.test.ts": 6044, + "src/renderer/src/lib/palette-match/palette-phrase-placement.test.ts": 73, + "src/renderer/src/lib/palette-match/palette-ranking.test.ts": 46, + "src/renderer/src/lib/palette-repo-resolution.test.ts": 47, + "src/renderer/src/lib/pane-agent-evidence.test.ts": 71, + "src/renderer/src/lib/pane-manager/browser-mobile-driver-state.test.ts": 37, + "src/renderer/src/lib/pane-manager/browser-remote-viewer-state.test.ts": 34, + "src/renderer/src/lib/pane-manager/client-hosted-browser-row-ephemerality.test.ts": 1254, + "src/renderer/src/lib/pane-manager/client-hosted-browser-row-state.test.ts": 42, + "src/renderer/src/lib/pane-manager/focus-follows-mouse.test.ts": 33, + "src/renderer/src/lib/pane-manager/mobile-driver-state.test.ts": 42, + "src/renderer/src/lib/pane-manager/mobile-fit-overrides-hydration.test.ts": 39, + "src/renderer/src/lib/pane-manager/mobile-fit-overrides.test.ts": 58, + "src/renderer/src/lib/pane-manager/pane-container-listener-lifecycle.test.ts": 311, + "src/renderer/src/lib/pane-manager/pane-cursor-blink-suspension.test.ts": 553, + "src/renderer/src/lib/pane-manager/pane-divider-capture-loss.test.ts": 40, + "src/renderer/src/lib/pane-manager/pane-divider-stray-touch.test.ts": 47, + "src/renderer/src/lib/pane-manager/pane-divider.test.ts": 46, + "src/renderer/src/lib/pane-manager/pane-dom-creation.test.ts": 309, + "src/renderer/src/lib/pane-manager/pane-dom-focus-class-sync-lifecycle.test.ts": 472, + "src/renderer/src/lib/pane-manager/pane-drag-reorder.test.ts": 95, + "src/renderer/src/lib/pane-manager/pane-drop-no-op.test.ts": 233, + "src/renderer/src/lib/pane-manager/pane-fit-resize-observer.test.ts": 95, + "src/renderer/src/lib/pane-manager/pane-fit.test.ts": 146, + "src/renderer/src/lib/pane-manager/pane-initial-fit-lifecycle.test.ts": 146, + "src/renderer/src/lib/pane-manager/pane-inline-image-recovery.test.ts": 39, + "src/renderer/src/lib/pane-manager/pane-inline-images.test.ts": 78, + "src/renderer/src/lib/pane-manager/pane-key-resolution.test.ts": 34, + "src/renderer/src/lib/pane-manager/pane-layout-geometry-apply.test.ts": 433, + "src/renderer/src/lib/pane-manager/pane-layout-gesture.test.ts": 195, + "src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts": 359, + "src/renderer/src/lib/pane-manager/pane-manager-registry.test.ts": 60, + "src/renderer/src/lib/pane-manager/pane-manager-spawn-placement.test.ts": 709, + "src/renderer/src/lib/pane-manager/pane-metric-options-deferral.test.ts": 41, + "src/renderer/src/lib/pane-manager/pane-overlay-focus.test.ts": 345, + "src/renderer/src/lib/pane-manager/pane-pointer-focus.test.ts": 30, + "src/renderer/src/lib/pane-manager/pane-pty-resize-hold.test.ts": 32, + "src/renderer/src/lib/pane-manager/pane-reveal-fit.test.ts": 52, + "src/renderer/src/lib/pane-manager/pane-reveal-lifetime.test.ts": 1263, + "src/renderer/src/lib/pane-manager/pane-reveal-repaint.test.ts": 232, + "src/renderer/src/lib/pane-manager/pane-scroll.test.ts": 166, + "src/renderer/src/lib/pane-manager/pane-split-close.test.ts": 54, + "src/renderer/src/lib/pane-manager/pane-split-scroll.test.ts": 49, + "src/renderer/src/lib/pane-manager/pane-terminal-foreground-render-settle.test.ts": 34, + "src/renderer/src/lib/pane-manager/pane-terminal-gpu-acceleration.test.ts": 62, "src/renderer/src/lib/pane-manager/pane-terminal-mouse-wheel.test.ts": 82, - "src/renderer/src/lib/pane-manager/pane-terminal-output-queue-chunks.test.ts": 52, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-ack-credit.test.ts": 167, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-backlog-cap.test.ts": 160, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-disposed-writes.test.ts": 102, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-foreground-refresh.test.ts": 158, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-queue-retention.test.ts": 279, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-synchronized-frames.test.ts": 190, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-throughput.bench.test.ts": 324, - "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.test.ts": 208, - "src/renderer/src/lib/pane-manager/pane-tree-equalization-parity.test.ts": 676, + "src/renderer/src/lib/pane-manager/pane-terminal-output-queue-chunks.test.ts": 39, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-ack-credit.test.ts": 178, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-backlog-cap.test.ts": 171, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-disposed-writes.test.ts": 87, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-foreground-refresh.test.ts": 187, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-queue-retention.test.ts": 335, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-synchronized-frames.test.ts": 207, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler-throughput.bench.test.ts": 318, + "src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.test.ts": 251, + "src/renderer/src/lib/pane-manager/pane-tree-equalization-parity.test.ts": 803, "src/renderer/src/lib/pane-manager/pane-tree-equalization-scaling.test.ts": 200, - "src/renderer/src/lib/pane-manager/pane-tree-ops.test.ts": 148, - "src/renderer/src/lib/pane-manager/pane-tree-reparent-frame.test.ts": 80, - "src/renderer/src/lib/pane-manager/pane-viewport-present-lifetime.test.ts": 779, - "src/renderer/src/lib/pane-manager/pane-webgl-context-recovery.test.ts": 124, - "src/renderer/src/lib/pane-manager/pane-webgl-refresh-lifecycle.test.ts": 159, - "src/renderer/src/lib/pane-manager/pane-webgl-renderer.test.ts": 356, - "src/renderer/src/lib/pane-manager/terminal-arabic-shaping-joiner.test.ts": 73, - "src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.test.ts": 53, - "src/renderer/src/lib/pane-manager/terminal-complex-script.test.ts": 75, - "src/renderer/src/lib/pane-manager/terminal-delivery-credit.test.ts": 43, - "src/renderer/src/lib/pane-manager/terminal-dom-block-fill.test.ts": 283, - "src/renderer/src/lib/pane-manager/terminal-foreground-repair-convergence.test.ts": 217, - "src/renderer/src/lib/pane-manager/terminal-ime-anchor.test.ts": 47, - "src/renderer/src/lib/pane-manager/terminal-ime-candidate-anchor.test.ts": 206, - "src/renderer/src/lib/pane-manager/terminal-keyboard-protocol.test.ts": 63, - "src/renderer/src/lib/pane-manager/terminal-ligatures-addon.test.ts": 83, - "src/renderer/src/lib/pane-manager/terminal-link-provider-guard.test.ts": 63, - "src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset-on-mouseleave.test.ts": 56, - "src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset-on-write.test.ts": 56, - "src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset.test.ts": 43, - "src/renderer/src/lib/pane-manager/terminal-mouse-encoding-tracker.test.ts": 126, - "src/renderer/src/lib/pane-manager/terminal-render-pause-release-parked-resize.test.ts": 60, - "src/renderer/src/lib/pane-manager/terminal-render-pause-release.test.ts": 59, - "src/renderer/src/lib/pane-manager/terminal-scroll-intent-input-resync.test.ts": 71, - "src/renderer/src/lib/pane-manager/terminal-scroll-intent-key-retention.test.ts": 106, - "src/renderer/src/lib/pane-manager/terminal-scroll-intent-structural-transitions.test.ts": 56, - "src/renderer/src/lib/pane-manager/terminal-scroll-intent.test.ts": 93, - "src/renderer/src/lib/pane-manager/terminal-structural-replay-coordinator.test.ts": 76, - "src/renderer/src/lib/pane-manager/terminal-webgl-auto-policy.test.ts": 55, - "src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.test.ts": 365, - "src/renderer/src/lib/pane-manager/terminal-windows-ctrl-alt-chord-classification.test.ts": 196, - "src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.test.ts": 73, - "src/renderer/src/lib/pane-manager/windows-pty-compatibility.test.ts": 76, - "src/renderer/src/lib/pane-manager/xterm-instance-disposed.test.ts": 86, - "src/renderer/src/lib/pane-manager/xterm-user-scrolling-contract.test.ts": 2444, - "src/renderer/src/lib/pane-manager/xterm-write-callback-guard.test.ts": 72, - "src/renderer/src/lib/parked-terminal-host-hydration.test.ts": 323, - "src/renderer/src/lib/passive-macos-app-data-access.test.ts": 49, - "src/renderer/src/lib/paste-payload-metadata.test.ts": 60, - "src/renderer/src/lib/path-head-elision.test.ts": 56, - "src/renderer/src/lib/path.test.ts": 59, - "src/renderer/src/lib/pending-worktree-creation.test.ts": 40, - "src/renderer/src/lib/pi-live-session-no-duplicate-tab.test.ts": 1634, - "src/renderer/src/lib/pi-session-resume-wake.test.ts": 1408, - "src/renderer/src/lib/plugin-command-execution.test.ts": 165, - "src/renderer/src/lib/plugin-command-keybindings.test.ts": 121, - "src/renderer/src/lib/pr-bot-author-overrides.test.ts": 302, - "src/renderer/src/lib/pr-comment-action-state.test.ts": 54, - "src/renderer/src/lib/pr-comment-reactions.test.ts": 44, - "src/renderer/src/lib/primary-selection-paste.test.ts": 247, - "src/renderer/src/lib/primary-selection.test.ts": 70, - "src/renderer/src/lib/project-clone-url-prefill.test.ts": 59, - "src/renderer/src/lib/project-host-clone-url.test.ts": 49, - "src/renderer/src/lib/project-host-setup-options.test.ts": 325, - "src/renderer/src/lib/project-host-workspace-target-managed-host.test.ts": 340, - "src/renderer/src/lib/project-host-workspace-target.test.ts": 67, - "src/renderer/src/lib/project-skill-runtime.test.ts": 82, - "src/renderer/src/lib/provisioned-root-create-options.test.ts": 52, - "src/renderer/src/lib/quick-workspace-agent-selection.test.ts": 141, - "src/renderer/src/lib/react-commit-cascade-observer.react185.test.tsx": 271, - "src/renderer/src/lib/react-commit-cascade-observer.test.ts": 180, - "src/renderer/src/lib/react-commit-cascade-store-write-samples.test.ts": 66, - "src/renderer/src/lib/react-commit-cascade-telemetry.test.ts": 164, - "src/renderer/src/lib/react-error-boundary-reporting.test.ts": 73, - "src/renderer/src/lib/react-grab-dev-gate.test.ts": 54, - "src/renderer/src/lib/react-renderer-root.test.ts": 54, - "src/renderer/src/lib/recent-workspace-tab-rows.test.ts": 121, - "src/renderer/src/lib/remap-open-editor-tabs-for-path-change.test.ts": 1646, - "src/renderer/src/lib/rename-user-named-tab-access.test.ts": 1706, - "src/renderer/src/lib/renderer-agent-status-observation-ingress.test.ts": 115, - "src/renderer/src/lib/renderer-app-platform.test.ts": 156, - "src/renderer/src/lib/renderer-memory-profile.test.ts": 75, - "src/renderer/src/lib/repo-display-labels.test.ts": 60, - "src/renderer/src/lib/repo-runtime-owner.test.ts": 90, - "src/renderer/src/lib/repo-search.test.ts": 67, - "src/renderer/src/lib/repo-slug-cache.test.ts": 56, - "src/renderer/src/lib/repo-slug-index.test.ts": 2468, - "src/renderer/src/lib/resolve-owner.test.ts": 320, - "src/renderer/src/lib/resolved-worktree-execution-host.test.ts": 282, - "src/renderer/src/lib/resume-sleeping-agent-session-direct-ssh-hydration-gap.test.ts": 1548, - "src/renderer/src/lib/resume-sleeping-agent-session-execution-host-scope.test.ts": 1708, - "src/renderer/src/lib/resume-sleeping-agent-session-provider-claim.test.ts": 1682, - "src/renderer/src/lib/resume-sleeping-agent-session-replay.test.ts": 1680, - "src/renderer/src/lib/resume-sleeping-agent-session-slept-pane-recovery.test.ts": 1876, - "src/renderer/src/lib/resume-sleeping-agent-session-suppress-navigation.test.ts": 1521, - "src/renderer/src/lib/resume-sleeping-agent-session.test.ts": 1401, - "src/renderer/src/lib/resume-stale-structured-agent-session.test.ts": 1623, - "src/renderer/src/lib/reveal-in-file-manager.test.ts": 220, - "src/renderer/src/lib/right-sidebar-visibility.test.ts": 61, - "src/renderer/src/lib/run-quick-command-in-new-tab.test.ts": 72, - "src/renderer/src/lib/running-agent-targets.test.ts": 341, - "src/renderer/src/lib/runtime-host-unverifiable-probe-readers.test.ts": 396, - "src/renderer/src/lib/runtime-pane-title-leaf-id.test.ts": 47, - "src/renderer/src/lib/runtime-session-mirror-targets.test.ts": 63, - "src/renderer/src/lib/runtime-session-mirror-unverifiable-host.test.ts": 53, - "src/renderer/src/lib/runtime-workspace-file-route.test.ts": 319, - "src/renderer/src/lib/screen-submit-shortcut.test.ts": 62, - "src/renderer/src/lib/script-textarea-rows.test.ts": 106, - "src/renderer/src/lib/scroll-cache.test.ts": 56, - "src/renderer/src/lib/serve-desktop-promotion-session-continuity.test.ts": 1625, - "src/renderer/src/lib/session-write-subscriber-acknowledgment.test.ts": 1407, - "src/renderer/src/lib/session-write-subscriber-allocation.test.ts": 427, - "src/renderer/src/lib/session-write-subscriber-deferred-persist.test.ts": 1534, - "src/renderer/src/lib/session-write-subscriber.test.ts": 1444, - "src/renderer/src/lib/settled-worker-wake-policy.test.ts": 1691, - "src/renderer/src/lib/setup-runner.test.ts": 57, - "src/renderer/src/lib/setup-script-prompt.test.ts": 289, - "src/renderer/src/lib/setup-split-activation-single-owner.test.ts": 1879, - "src/renderer/src/lib/sha256.test.ts": 55, - "src/renderer/src/lib/shutdown-checkpoint-guard.test.ts": 201, - "src/renderer/src/lib/sidebar-worktree-activation.test.ts": 87, - "src/renderer/src/lib/simulator-launch-coordination.test.ts": 114, - "src/renderer/src/lib/simulator-palette-search.test.ts": 335, - "src/renderer/src/lib/simulator-pane-shutdown-scheduler.test.ts": 1417, - "src/renderer/src/lib/simulator-tab-palette-activation.test.ts": 1343, - "src/renderer/src/lib/skill-freshness-display-status.test.ts": 60, - "src/renderer/src/lib/sleeping-agent-pane-ownership.test.ts": 53, - "src/renderer/src/lib/sleeping-agent-session-launch-windows-quoting.test.ts": 524, - "src/renderer/src/lib/smart-github-submit.test.ts": 104, - "src/renderer/src/lib/solidity-language-id-registration.test.ts": 48, - "src/renderer/src/lib/source-control-agent-action-launch-check.test.ts": 1358, - "src/renderer/src/lib/source-control-generation-plan.test.ts": 114, - "src/renderer/src/lib/source-control-launch-platform.test.ts": 58, - "src/renderer/src/lib/source-control-remote-error.test.ts": 62, - "src/renderer/src/lib/sparse-preset-draft.test.ts": 168, - "src/renderer/src/lib/ssh-mutation-expectation.test.ts": 1556, - "src/renderer/src/lib/ssh-workspace-browser-route-eligibility.test.ts": 47, - "src/renderer/src/lib/startup-draft-input-kind.test.ts": 464, - "src/renderer/src/lib/startup-ui-hydration.test.ts": 270, - "src/renderer/src/lib/state-collection-byte-estimate.test.ts": 113, - "src/renderer/src/lib/structured-agent-launch-settlement.test.ts": 64, - "src/renderer/src/lib/structured-agent-session-launch-admission.test.ts": 3534, - "src/renderer/src/lib/structured-agent-session-launch-after-failure.test.ts": 585, - "src/renderer/src/lib/structured-agent-session-launch-cancellation.test.ts": 1639, - "src/renderer/src/lib/structured-agent-session-launch-close-race.test.ts": 663, - "src/renderer/src/lib/structured-agent-session-launch-create-message.test.ts": 1863, - "src/renderer/src/lib/structured-agent-session-launch-different-request.test.ts": 607, - "src/renderer/src/lib/structured-agent-session-launch-exited-owner.test.ts": 944, - "src/renderer/src/lib/structured-agent-session-launch-join-delivery.test.ts": 508, - "src/renderer/src/lib/structured-agent-session-launch-options.test.ts": 602, - "src/renderer/src/lib/structured-agent-session-launch-persistence.test.ts": 374, - "src/renderer/src/lib/structured-agent-session-launch-probe-outage.test.ts": 1677, - "src/renderer/src/lib/structured-agent-session-launch-prompt-stop.test.ts": 511, - "src/renderer/src/lib/structured-agent-session-launch-prompt.test.ts": 481, - "src/renderer/src/lib/structured-agent-session-launch-record.test.ts": 203, - "src/renderer/src/lib/structured-agent-session-launch-registered-agent.test.ts": 610, - "src/renderer/src/lib/structured-agent-session-launch-reload.test.ts": 573, - "src/renderer/src/lib/structured-agent-session-launch-resume-identity.test.ts": 602, - "src/renderer/src/lib/structured-agent-session-launch-unconfirmed-or-retried.test.ts": 539, - "src/renderer/src/lib/structured-agent-session-launch.test.ts": 698, - "src/renderer/src/lib/structured-agent-session-new-chat-own-launch.test.ts": 656, - "src/renderer/src/lib/structured-agent-session-provisional-tab.test.ts": 1115, - "src/renderer/src/lib/structured-agent-session-tab-activation.test.ts": 59, - "src/renderer/src/lib/structured-chat-tab-index.test.ts": 42, - "src/renderer/src/lib/syntax-highlighting/incremental-syntax-tokenizer.test.ts": 553, - "src/renderer/src/lib/syntax-highlighting/syntax-highlight-scheduler.test.ts": 151, - "src/renderer/src/lib/syntax-highlighting/syntax-highlighter.test.ts": 476, - "src/renderer/src/lib/tab-agent-identity-decision-table.test.ts": 542, - "src/renderer/src/lib/tab-agent-status-index.test.ts": 224, - "src/renderer/src/lib/tab-agent.test.ts": 127, - "src/renderer/src/lib/tab-has-live-pty.test.ts": 62, - "src/renderer/src/lib/tab-number-shortcuts.test.ts": 1566, - "src/renderer/src/lib/task-source-runtime-target.test.ts": 47, - "src/renderer/src/lib/terminal-contrast-correction.test.ts": 64, - "src/renderer/src/lib/terminal-file-uri-link.test.ts": 74, - "src/renderer/src/lib/terminal-input-activity-coalescing.test.ts": 58, + "src/renderer/src/lib/pane-manager/pane-tree-ops.test.ts": 124, + "src/renderer/src/lib/pane-manager/pane-tree-reparent-frame.test.ts": 76, + "src/renderer/src/lib/pane-manager/pane-viewport-present-lifetime.test.ts": 971, + "src/renderer/src/lib/pane-manager/pane-webgl-context-recovery.test.ts": 160, + "src/renderer/src/lib/pane-manager/pane-webgl-refresh-lifecycle.test.ts": 297, + "src/renderer/src/lib/pane-manager/pane-webgl-renderer.test.ts": 322, + "src/renderer/src/lib/pane-manager/terminal-arabic-shaping-joiner.test.ts": 65, + "src/renderer/src/lib/pane-manager/terminal-canvas-dpr-repair.test.ts": 43, + "src/renderer/src/lib/pane-manager/terminal-complex-script.test.ts": 69, + "src/renderer/src/lib/pane-manager/terminal-delivery-credit.test.ts": 30, + "src/renderer/src/lib/pane-manager/terminal-dom-block-fill.test.ts": 321, + "src/renderer/src/lib/pane-manager/terminal-foreground-repair-convergence.test.ts": 275, + "src/renderer/src/lib/pane-manager/terminal-grid-size-for-box.test.ts": 170, + "src/renderer/src/lib/pane-manager/terminal-ime-anchor.test.ts": 39, + "src/renderer/src/lib/pane-manager/terminal-ime-candidate-anchor.test.ts": 283, + "src/renderer/src/lib/pane-manager/terminal-keyboard-protocol.test.ts": 50, + "src/renderer/src/lib/pane-manager/terminal-ligatures-addon.test.ts": 44, + "src/renderer/src/lib/pane-manager/terminal-link-provider-guard.test.ts": 42, + "src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset-on-mouseleave.test.ts": 38, + "src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset-on-write.test.ts": 54, + "src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset.test.ts": 34, + "src/renderer/src/lib/pane-manager/terminal-mouse-encoding-tracker.test.ts": 84, + "src/renderer/src/lib/pane-manager/terminal-render-pause-release-parked-resize.test.ts": 38, + "src/renderer/src/lib/pane-manager/terminal-render-pause-release.test.ts": 45, + "src/renderer/src/lib/pane-manager/terminal-scroll-intent-input-resync.test.ts": 61, + "src/renderer/src/lib/pane-manager/terminal-scroll-intent-key-retention.test.ts": 130, + "src/renderer/src/lib/pane-manager/terminal-scroll-intent-structural-transitions.test.ts": 38, + "src/renderer/src/lib/pane-manager/terminal-scroll-intent.test.ts": 89, + "src/renderer/src/lib/pane-manager/terminal-structural-replay-coordinator.test.ts": 54, + "src/renderer/src/lib/pane-manager/terminal-webgl-auto-policy.test.ts": 46, + "src/renderer/src/lib/pane-manager/terminal-webgl-hidden-retention.test.ts": 541, + "src/renderer/src/lib/pane-manager/terminal-windows-ctrl-alt-chord-classification.test.ts": 203, + "src/renderer/src/lib/pane-manager/terminal-write-pipeline-health.test.ts": 46, + "src/renderer/src/lib/pane-manager/windows-pty-compatibility.test.ts": 51, + "src/renderer/src/lib/pane-manager/xterm-instance-disposed.test.ts": 41, + "src/renderer/src/lib/pane-manager/xterm-user-scrolling-contract.test.ts": 2372, + "src/renderer/src/lib/pane-manager/xterm-write-callback-guard.test.ts": 45, + "src/renderer/src/lib/parked-terminal-host-hydration.test.ts": 294, + "src/renderer/src/lib/passive-macos-app-data-access.test.ts": 37, + "src/renderer/src/lib/paste-payload-metadata.test.ts": 45, + "src/renderer/src/lib/path-head-elision.test.ts": 37, + "src/renderer/src/lib/path.test.ts": 42, + "src/renderer/src/lib/pending-worktree-creation.test.ts": 52, + "src/renderer/src/lib/pi-live-session-no-duplicate-tab.test.ts": 1341, + "src/renderer/src/lib/pi-session-resume-wake.test.ts": 1359, + "src/renderer/src/lib/plugin-command-execution.test.ts": 174, + "src/renderer/src/lib/plugin-command-keybindings.test.ts": 100, + "src/renderer/src/lib/pr-bot-author-overrides.test.ts": 284, + "src/renderer/src/lib/pr-comment-action-state.test.ts": 38, + "src/renderer/src/lib/pr-comment-reactions.test.ts": 27, + "src/renderer/src/lib/primary-selection-paste.test.ts": 252, + "src/renderer/src/lib/primary-selection.test.ts": 53, + "src/renderer/src/lib/project-clone-url-prefill.test.ts": 55, + "src/renderer/src/lib/project-host-clone-url.test.ts": 43, + "src/renderer/src/lib/project-host-setup-options.test.ts": 372, + "src/renderer/src/lib/project-host-workspace-target-managed-host.test.ts": 345, + "src/renderer/src/lib/project-host-workspace-target.jira-mapping.test.ts": 55, + "src/renderer/src/lib/project-host-workspace-target.test.ts": 74, + "src/renderer/src/lib/project-skill-runtime.test.ts": 67, + "src/renderer/src/lib/provisioned-root-create-options.test.ts": 27, + "src/renderer/src/lib/quick-workspace-agent-selection.test.ts": 149, + "src/renderer/src/lib/react-commit-cascade-observer.react185.test.tsx": 280, + "src/renderer/src/lib/react-commit-cascade-observer.test.ts": 188, + "src/renderer/src/lib/react-commit-cascade-store-write-samples.test.ts": 47, + "src/renderer/src/lib/react-commit-cascade-telemetry.test.ts": 182, + "src/renderer/src/lib/react-error-boundary-reporting.test.ts": 40, + "src/renderer/src/lib/react-grab-dev-gate.test.ts": 29, + "src/renderer/src/lib/react-renderer-root.test.ts": 44, + "src/renderer/src/lib/recent-workspace-tab-rows.test.ts": 120, + "src/renderer/src/lib/recipe-vm-managed-host.test.ts": 31, + "src/renderer/src/lib/remap-open-editor-tabs-for-path-change.test.ts": 1238, + "src/renderer/src/lib/rename-user-named-tab-access.test.ts": 1628, + "src/renderer/src/lib/renderer-agent-status-observation-ingress.test.ts": 137, + "src/renderer/src/lib/renderer-app-platform.test.ts": 177, + "src/renderer/src/lib/renderer-memory-profile.test.ts": 64, + "src/renderer/src/lib/repo-display-labels.test.ts": 50, + "src/renderer/src/lib/repo-runtime-owner.test.ts": 61, + "src/renderer/src/lib/repo-search.test.ts": 43, + "src/renderer/src/lib/repo-slug-cache.test.ts": 37, + "src/renderer/src/lib/repo-slug-index.test.ts": 1967, + "src/renderer/src/lib/repo-ssh-connection.test.ts": 102, + "src/renderer/src/lib/resolve-owner.test.ts": 347, + "src/renderer/src/lib/resolved-worktree-execution-host.test.ts": 268, + "src/renderer/src/lib/resume-sleeping-agent-session-deferred-pane.test.ts": 1635, + "src/renderer/src/lib/resume-sleeping-agent-session-direct-ssh-hydration-gap.test.ts": 1451, + "src/renderer/src/lib/resume-sleeping-agent-session-execution-host-scope.test.ts": 1527, + "src/renderer/src/lib/resume-sleeping-agent-session-provider-claim.test.ts": 1301, + "src/renderer/src/lib/resume-sleeping-agent-session-replay.test.ts": 1480, + "src/renderer/src/lib/resume-sleeping-agent-session-slept-pane-recovery.test.ts": 1297, + "src/renderer/src/lib/resume-sleeping-agent-session-suppress-navigation.test.ts": 1294, + "src/renderer/src/lib/resume-sleeping-agent-session.test.ts": 1785, + "src/renderer/src/lib/resume-stale-structured-agent-session.test.ts": 1351, + "src/renderer/src/lib/reveal-in-file-manager.test.ts": 274, + "src/renderer/src/lib/right-sidebar-visibility.test.ts": 49, + "src/renderer/src/lib/run-quick-command-in-new-tab.test.ts": 73, + "src/renderer/src/lib/running-agent-targets.test.ts": 301, + "src/renderer/src/lib/runtime-host-unverifiable-probe-readers.test.ts": 370, + "src/renderer/src/lib/runtime-pane-title-leaf-id.test.ts": 45, + "src/renderer/src/lib/runtime-session-mirror-targets.test.ts": 39, + "src/renderer/src/lib/runtime-session-mirror-unverifiable-host.test.ts": 46, + "src/renderer/src/lib/runtime-workspace-file-route.test.ts": 312, + "src/renderer/src/lib/screen-submit-shortcut.test.ts": 34, + "src/renderer/src/lib/script-textarea-rows.test.ts": 80, + "src/renderer/src/lib/scroll-cache.test.ts": 43, + "src/renderer/src/lib/serve-desktop-promotion-session-continuity.test.ts": 1474, + "src/renderer/src/lib/session-write-subscriber-acknowledgment.test.ts": 1414, + "src/renderer/src/lib/session-write-subscriber-allocation.test.ts": 404, + "src/renderer/src/lib/session-write-subscriber.test.ts": 1363, + "src/renderer/src/lib/settled-worker-wake-policy.test.ts": 1297, + "src/renderer/src/lib/setup-runner.test.ts": 48, + "src/renderer/src/lib/setup-script-prompt.test.ts": 293, + "src/renderer/src/lib/setup-split-activation-single-owner.test.ts": 1817, + "src/renderer/src/lib/sha256.test.ts": 38, + "src/renderer/src/lib/shutdown-checkpoint-guard.test.ts": 189, + "src/renderer/src/lib/sidebar-worktree-activation.test.ts": 58, + "src/renderer/src/lib/simulator-launch-coordination.test.ts": 95, + "src/renderer/src/lib/simulator-palette-search.test.ts": 293, + "src/renderer/src/lib/simulator-pane-shutdown-scheduler.test.ts": 1355, + "src/renderer/src/lib/simulator-tab-palette-activation.test.ts": 1479, + "src/renderer/src/lib/skill-freshness-display-status.test.ts": 54, + "src/renderer/src/lib/sleeping-agent-pane-ownership.test.ts": 43, + "src/renderer/src/lib/sleeping-agent-session-launch-windows-quoting.test.ts": 459, + "src/renderer/src/lib/smart-github-submit.test.ts": 81, + "src/renderer/src/lib/solidity-language-id-registration.test.ts": 39, + "src/renderer/src/lib/source-control-agent-action-launch-check.test.ts": 1403, + "src/renderer/src/lib/source-control-generation-plan.test.ts": 98, + "src/renderer/src/lib/source-control-launch-platform.test.ts": 38, + "src/renderer/src/lib/source-control-remote-error.test.ts": 51, + "src/renderer/src/lib/sparse-preset-draft.test.ts": 158, + "src/renderer/src/lib/ssh-mutation-expectation.test.ts": 1261, + "src/renderer/src/lib/ssh-workspace-browser-route-eligibility.test.ts": 224, + "src/renderer/src/lib/startup-draft-input-kind.test.ts": 612, + "src/renderer/src/lib/startup-ui-hydration.test.ts": 261, + "src/renderer/src/lib/state-collection-byte-estimate.test.ts": 114, + "src/renderer/src/lib/structured-agent-launch-settlement.test.ts": 43, + "src/renderer/src/lib/structured-agent-session-launch-admission.test.ts": 3161, + "src/renderer/src/lib/structured-agent-session-launch-after-failure.test.ts": 606, + "src/renderer/src/lib/structured-agent-session-launch-cancellation.test.ts": 1447, + "src/renderer/src/lib/structured-agent-session-launch-close-race.test.ts": 747, + "src/renderer/src/lib/structured-agent-session-launch-create-message.test.ts": 1817, + "src/renderer/src/lib/structured-agent-session-launch-different-request.test.ts": 730, + "src/renderer/src/lib/structured-agent-session-launch-exited-owner.test.ts": 1103, + "src/renderer/src/lib/structured-agent-session-launch-join-delivery.test.ts": 611, + "src/renderer/src/lib/structured-agent-session-launch-options.test.ts": 611, + "src/renderer/src/lib/structured-agent-session-launch-persistence.test.ts": 421, + "src/renderer/src/lib/structured-agent-session-launch-probe-outage.test.ts": 1687, + "src/renderer/src/lib/structured-agent-session-launch-prompt-stop.test.ts": 433, + "src/renderer/src/lib/structured-agent-session-launch-prompt.test.ts": 513, + "src/renderer/src/lib/structured-agent-session-launch-record.test.ts": 279, + "src/renderer/src/lib/structured-agent-session-launch-registered-agent.test.ts": 643, + "src/renderer/src/lib/structured-agent-session-launch-reload.test.ts": 637, + "src/renderer/src/lib/structured-agent-session-launch-resume-identity.test.ts": 679, + "src/renderer/src/lib/structured-agent-session-launch-unconfirmed-or-retried.test.ts": 656, + "src/renderer/src/lib/structured-agent-session-launch.test.ts": 822, + "src/renderer/src/lib/structured-agent-session-new-chat-own-launch.test.ts": 701, + "src/renderer/src/lib/structured-agent-session-provisional-tab.test.ts": 829, + "src/renderer/src/lib/structured-agent-session-tab-activation.test.ts": 50, + "src/renderer/src/lib/structured-chat-tab-index.test.ts": 27, + "src/renderer/src/lib/syntax-highlighting/incremental-syntax-tokenizer.test.ts": 711, + "src/renderer/src/lib/syntax-highlighting/syntax-highlight-scheduler.test.ts": 133, + "src/renderer/src/lib/syntax-highlighting/syntax-highlighter.test.ts": 546, + "src/renderer/src/lib/syntax-highlighting/textmate-core.test.ts": 41, + "src/renderer/src/lib/tab-agent-identity-decision-table.test.ts": 560, + "src/renderer/src/lib/tab-agent-status-index.test.ts": 215, + "src/renderer/src/lib/tab-agent.test.ts": 98, + "src/renderer/src/lib/tab-has-live-pty.test.ts": 24, + "src/renderer/src/lib/tab-number-shortcuts.test.ts": 1334, + "src/renderer/src/lib/task-source-runtime-target.test.ts": 34, + "src/renderer/src/lib/terminal-contrast-correction.test.ts": 44, + "src/renderer/src/lib/terminal-file-uri-link.test.ts": 43, + "src/renderer/src/lib/terminal-input-activity-coalescing.test.ts": 38, "src/renderer/src/lib/terminal-links-cjk.test.ts": 72, - "src/renderer/src/lib/terminal-links-redos.test.ts": 74, - "src/renderer/src/lib/terminal-links.test.ts": 282, - "src/renderer/src/lib/terminal-pane-title-sanitization.test.ts": 49, - "src/renderer/src/lib/terminal-path-normalization.test.ts": 50, - "src/renderer/src/lib/terminal-quick-command-project-scope.test.ts": 58, - "src/renderer/src/lib/terminal-quick-command-score-budget.test.ts": 57, - "src/renderer/src/lib/terminal-quick-command-search.test.ts": 70, - "src/renderer/src/lib/terminal-reveal-identity.test.ts": 42, - "src/renderer/src/lib/terminal-shortcut-capture-notification.test.tsx": 1656, - "src/renderer/src/lib/terminal-tab-for-pty-id.test.ts": 63, - "src/renderer/src/lib/terminal-tab-owner-activation.test.ts": 50, - "src/renderer/src/lib/terminal-theme.test.ts": 84, - "src/renderer/src/lib/terminal-worktree-route.test.ts": 312, - "src/renderer/src/lib/text-control-paste-ownership.test.ts": 190, - "src/renderer/src/lib/text-control-paste.test.ts": 166, - "src/renderer/src/lib/titlebar-left-chrome.test.ts": 51, - "src/renderer/src/lib/titlebar-worktree-history-controls.test.ts": 54, - "src/renderer/src/lib/tui-agent-startup.test.ts": 155, - "src/renderer/src/lib/typing-latency/diagnostic-lifecycle.test.ts": 183, - "src/renderer/src/lib/typing-latency/diagnostic-summary.test.ts": 61, - "src/renderer/src/lib/typing-latency/echo-instrumentation.test.ts": 145, - "src/renderer/src/lib/typing-latency/input-events.test.ts": 205, - "src/renderer/src/lib/typing-latency/input-source.test.ts": 77, - "src/renderer/src/lib/typing-latency/sample-window.test.ts": 53, - "src/renderer/src/lib/ui-zoom.test.ts": 73, - "src/renderer/src/lib/unified-tab-anchor-insertion.test.ts": 48, - "src/renderer/src/lib/unread-badge-count-migration.test.ts": 306, - "src/renderer/src/lib/unread-badge-count-selector.test.ts": 1368, - "src/renderer/src/lib/unread-badge-count.test.ts": 190, - "src/renderer/src/lib/update-check-click-options.test.ts": 49, - "src/renderer/src/lib/updater-beforeunload.test.ts": 58, - "src/renderer/src/lib/use-tab-agent-observed-signal-dispatch.test.tsx": 1424, - "src/renderer/src/lib/use-tab-agent-opencode-native-title.test.ts": 1734, - "src/renderer/src/lib/use-tab-agent-pi-identity.test.ts": 125, - "src/renderer/src/lib/use-tab-agent-process-signals.test.ts": 1363, - "src/renderer/src/lib/use-tab-agent-remote-pty-selector.test.ts": 159, - "src/renderer/src/lib/use-tab-agent-retained-identity.test.ts": 1903, - "src/renderer/src/lib/use-tab-agent-sleeping-session.test.ts": 1563, - "src/renderer/src/lib/use-tab-agent.test.ts": 1826, - "src/renderer/src/lib/user-opened-local-path.test.ts": 1464, - "src/renderer/src/lib/visible-overlay.test.ts": 165, - "src/renderer/src/lib/wake-sleeping-agents-in-background.test.ts": 203, - "src/renderer/src/lib/wake-sleeping-agents-live-done.test.ts": 161, - "src/renderer/src/lib/web-client-location.test.ts": 51, - "src/renderer/src/lib/window-label-formatter.test.ts": 44, - "src/renderer/src/lib/window-visibility-interval.test.ts": 66, - "src/renderer/src/lib/window-visibility-timeout-poller.test.ts": 173, - "src/renderer/src/lib/windows-terminal-capabilities-race.test.ts": 220, - "src/renderer/src/lib/windows-terminal-capabilities.test.ts": 282, - "src/renderer/src/lib/windows-terminal-capability-reprobe.test.ts": 190, - "src/renderer/src/lib/work-item-link-query-bounds.test.ts": 55, - "src/renderer/src/lib/work-item-lookup-text.test.ts": 57, - "src/renderer/src/lib/worker-terminal-takeover-report.test.ts": 299, - "src/renderer/src/lib/workspace-activation-path-gate.test.ts": 1672, - "src/renderer/src/lib/workspace-activation-terminal-focus.test.ts": 340, - "src/renderer/src/lib/workspace-attachment-terminal-origin.test.ts": 88, - "src/renderer/src/lib/workspace-browser-tab-open.test.ts": 398, - "src/renderer/src/lib/workspace-composer-initial-focus.test.ts": 148, - "src/renderer/src/lib/workspace-create-error-format.test.ts": 82, - "src/renderer/src/lib/workspace-directory.test.ts": 359, - "src/renderer/src/lib/workspace-doc-address-input.test.ts": 283, - "src/renderer/src/lib/workspace-emoji-shortcodes.lazy.test.ts": 95, - "src/renderer/src/lib/workspace-emoji-shortcodes.test.ts": 95, - "src/renderer/src/lib/workspace-file-drag-source-runtime.test.ts": 62, - "src/renderer/src/lib/workspace-file-drag.test.ts": 80, - "src/renderer/src/lib/workspace-port-groups.test.ts": 44, - "src/renderer/src/lib/workspace-port-host-availability.test.ts": 63, - "src/renderer/src/lib/workspace-port-host-scoped-scan.test.ts": 337, - "src/renderer/src/lib/workspace-port-scan-debounce.test.ts": 51, - "src/renderer/src/lib/workspace-port-scan-publish.test.ts": 445, - "src/renderer/src/lib/workspace-session-browser-history.test.ts": 331, - "src/renderer/src/lib/workspace-session-editor-drafts.test.ts": 312, - "src/renderer/src/lib/workspace-session-host-contention.test.ts": 357, - "src/renderer/src/lib/workspace-session-host-persistence.test.ts": 333, - "src/renderer/src/lib/workspace-session-host-split.test.ts": 316, - "src/renderer/src/lib/workspace-session-hydration-keys.test.ts": 58, - "src/renderer/src/lib/workspace-session-liveness.test.ts": 284, - "src/renderer/src/lib/workspace-session-patch.test.ts": 292, - "src/renderer/src/lib/workspace-session-persistence-gate.test.ts": 287, - "src/renderer/src/lib/workspace-session-relevant-fields.test.ts": 325, - "src/renderer/src/lib/workspace-session-ssh-partition-ownership.test.ts": 338, - "src/renderer/src/lib/workspace-session-ssh-partition-round-trip.test.ts": 347, - "src/renderer/src/lib/workspace-session-staged-browser-tabs.test.ts": 48, - "src/renderer/src/lib/workspace-session.test.ts": 318, - "src/renderer/src/lib/workspace-tab-agent-metadata.test.ts": 56, - "src/renderer/src/lib/workspace-tab-palette-activation.store.test.ts": 1688, - "src/renderer/src/lib/workspace-tab-palette-activation.test.ts": 294, - "src/renderer/src/lib/workspace-tab-palette-results.test.ts": 350, - "src/renderer/src/lib/workspace-tab-palette-search.test.ts": 423, - "src/renderer/src/lib/workspace-terminal-host-authority.test.ts": 1753, - "src/renderer/src/lib/worktree-activation-agent-startup.test.ts": 2012, - "src/renderer/src/lib/worktree-activation-automation-filter.test.ts": 1819, - "src/renderer/src/lib/worktree-activation-created-agent.test.ts": 2057, - "src/renderer/src/lib/worktree-activation-default-agent-chat.test.ts": 1659, - "src/renderer/src/lib/worktree-activation-default-tabs.test.ts": 1571, - "src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts": 1934, - "src/renderer/src/lib/worktree-activation-empty-remote.test.ts": 1634, - "src/renderer/src/lib/worktree-activation-issue-command.test.ts": 1693, - "src/renderer/src/lib/worktree-activation-launch-awaiting-host.test.ts": 1657, - "src/renderer/src/lib/worktree-activation-pty-inventory.test.ts": 321, - "src/renderer/src/lib/worktree-activation-reveal.test.ts": 1759, - "src/renderer/src/lib/worktree-activation-setup-script.test.ts": 1905, - "src/renderer/src/lib/worktree-activation-structured-chat-surface.test.ts": 1865, - "src/renderer/src/lib/worktree-activation-web-runtime.test.ts": 2050, - "src/renderer/src/lib/worktree-activity-state.test.ts": 66, - "src/renderer/src/lib/worktree-agent-activation-gate.test.ts": 1647, - "src/renderer/src/lib/worktree-agent-activation-seam.test.ts": 1849, - "src/renderer/src/lib/worktree-agent-structured-inventory.test.ts": 282, - "src/renderer/src/lib/worktree-attachment-label.test.ts": 57, - "src/renderer/src/lib/worktree-creation-agent-seeding.test.ts": 1201, - "src/renderer/src/lib/worktree-creation-agent-seeds.test.ts": 1452, - "src/renderer/src/lib/worktree-creation-backend-startup-focus.test.ts": 1881, - "src/renderer/src/lib/worktree-creation-background-mount.test.ts": 2059, - "src/renderer/src/lib/worktree-creation-chat-setup.test.ts": 2093, - "src/renderer/src/lib/worktree-creation-flow-dedupe.test.ts": 1253, - "src/renderer/src/lib/worktree-creation-flow-ready-toast.test.ts": 1699, - "src/renderer/src/lib/worktree-creation-flow-stranded-surface.test.ts": 1183, - "src/renderer/src/lib/worktree-creation-flow.test.ts": 1987, - "src/renderer/src/lib/worktree-creation-hook-flow.test.ts": 1338, - "src/renderer/src/lib/worktree-creation-hook-preparation.test.ts": 185, - "src/renderer/src/lib/worktree-creation-ready-toast.test.tsx": 173, - "src/renderer/src/lib/worktree-creation-structured-session-host-decline.test.ts": 1994, - "src/renderer/src/lib/worktree-creation-structured-session.test.ts": 1041, - "src/renderer/src/lib/worktree-creation-surface.test.ts": 54, - "src/renderer/src/lib/worktree-default-display-name.test.ts": 52, - "src/renderer/src/lib/worktree-display-name-order.test.ts": 47, - "src/renderer/src/lib/worktree-draft-startup-view-mode.test.ts": 1335, - "src/renderer/src/lib/worktree-git-identity-display.test.ts": 46, - "src/renderer/src/lib/worktree-host-connection-phase.test.ts": 1513, - "src/renderer/src/lib/worktree-initial-terminal-close-records.test.ts": 1617, - "src/renderer/src/lib/worktree-jump-navigation.test.ts": 354, - "src/renderer/src/lib/worktree-live-terminal-surface-owners.test.ts": 314, - "src/renderer/src/lib/worktree-operation-generation.test.ts": 379, - "src/renderer/src/lib/worktree-operation-route.test.ts": 356, - "src/renderer/src/lib/worktree-palette-attachment-urls.test.ts": 109, - "src/renderer/src/lib/worktree-palette-comment-snippet.test.ts": 50, - "src/renderer/src/lib/worktree-palette-create-action.test.ts": 63, - "src/renderer/src/lib/worktree-palette-multi-keyword.test.ts": 235, - "src/renderer/src/lib/worktree-palette-review-match.test.ts": 194, - "src/renderer/src/lib/worktree-palette-runtime-owner-identity.test.ts": 402, - "src/renderer/src/lib/worktree-palette-search.test.ts": 236, - "src/renderer/src/lib/worktree-palette-task-url-match.test.ts": 133, - "src/renderer/src/lib/worktree-reactivation-preserved-pane-replacement.test.ts": 1546, - "src/renderer/src/lib/worktree-reactivation-runtime-owned-resume-deferral.test.ts": 1410, - "src/renderer/src/lib/worktree-reactivation-tab-forkbomb.test.ts": 1937, - "src/renderer/src/lib/worktree-row-open-in-owner.test.ts": 364, - "src/renderer/src/lib/worktree-runtime-owner-index.detected-perf.test.ts": 298, - "src/renderer/src/lib/worktree-runtime-owner-index.detected.test.ts": 102, - "src/renderer/src/lib/worktree-runtime-owner-index.test.ts": 49, - "src/renderer/src/lib/worktree-runtime-owner.test.ts": 356, - "src/renderer/src/lib/worktree-sort-order-host-split.test.ts": 320, - "src/renderer/src/lib/worktree-sort-order-persistence.test.ts": 342, - "src/renderer/src/lib/worktree-status-spinner-launch-agent.test.ts": 127, - "src/renderer/src/lib/worktree-status-terminal-layout-roots.test.ts": 115, - "src/renderer/src/lib/worktree-status.interrupted.test.ts": 106, - "src/renderer/src/lib/worktree-status.test.ts": 121, - "src/renderer/src/lib/worktree-visit-recency.test.ts": 45, - "src/renderer/src/runtime/abortable-runtime-environment-call.test.ts": 181, - "src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts": 70, - "src/renderer/src/runtime/agent-session-keyboard-capability.test.ts": 96, - "src/renderer/src/runtime/agent-session-operation-id.test.ts": 87, - "src/renderer/src/runtime/antigravity-generation-compatibility.test.ts": 349, - "src/renderer/src/runtime/browser-client-host-identity.test.ts": 52, - "src/renderer/src/runtime/browser-workspace-tab-close-plan.test.ts": 55, - "src/renderer/src/runtime/browser-workspace-tab-close.test.ts": 238, - "src/renderer/src/runtime/client-hosted-browser-close-intent-replay.test.ts": 374, - "src/renderer/src/runtime/client-hosted-browser-close-intents.test.ts": 234, - "src/renderer/src/runtime/client-hosted-browser-row-close.test.ts": 264, - "src/renderer/src/runtime/close-mirrored-editor-tab.test.ts": 224, - "src/renderer/src/runtime/file-explorer-delete-owner-provenance.test.ts": 1862, - "src/renderer/src/runtime/focus-runtime-terminal-surface-chat-view.test.ts": 570, - "src/renderer/src/runtime/focus-runtime-terminal-surface-readiness.test.ts": 437, - "src/renderer/src/runtime/github-check-details-timeout.test.ts": 64, - "src/renderer/src/runtime/gitlab-job-trace-client.test.ts": 137, - "src/renderer/src/runtime/gitlab-owner-api.test.ts": 111, - "src/renderer/src/runtime/host-model-catalog-snapshots.test.ts": 1805, - "src/renderer/src/runtime/host-route-call.test.ts": 338, - "src/renderer/src/runtime/host-session-mirror-empty-inventory-settle.test.ts": 944, - "src/renderer/src/runtime/host-session-mirror-flap-hydration.test.ts": 384, - "src/renderer/src/runtime/host-session-mirror-hydration-drain.test.ts": 343, - "src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx": 2135, - "src/renderer/src/runtime/host-session-mirror-settle-receipt-frames.test.tsx": 2168, - "src/renderer/src/runtime/host-session-snapshot-authority-client-hosted.test.ts": 54, - "src/renderer/src/runtime/host-structured-agents.test.ts": 537, - "src/renderer/src/runtime/local-runtime-capabilities.test.ts": 139, - "src/renderer/src/runtime/local-session-tab-close-owner.test.ts": 44, - "src/renderer/src/runtime/local-structured-chats.test.ts": 69, - "src/renderer/src/runtime/local-structured-session-empty-worktree-visibility.test.ts": 1662, - "src/renderer/src/runtime/local-structured-session-retired-epoch-repair.test.ts": 1933, - "src/renderer/src/runtime/local-structured-session-reveal-visibility.test.ts": 2162, - "src/renderer/src/runtime/local-structured-session-tabs-host-isolation.test.ts": 1048, - "src/renderer/src/runtime/local-structured-session-tabs-sync-hook.test.ts": 1933, - "src/renderer/src/runtime/local-structured-session-tabs-sync.test.ts": 2619, - "src/renderer/src/runtime/local-structured-session-tabs-sync/retired-epoch-repair.test.ts": 63, - "src/renderer/src/runtime/local-structured-session-unverifiable-inventory.test.ts": 1704, - "src/renderer/src/runtime/mirrored-agent-status-clock-skew.test.ts": 1534, - "src/renderer/src/runtime/mobile-markdown-bridge-save-guards.test.ts": 1255, - "src/renderer/src/runtime/mobile-markdown-bridge.test.ts": 2156, - "src/renderer/src/runtime/native-chat-launch-draft-runtime-resolution.test.ts": 53, - "src/renderer/src/runtime/paired-agent-row-turn-outcome-arms.test.ts": 1736, - "src/renderer/src/runtime/paired-host-client-capabilities.test.ts": 251, - "src/renderer/src/runtime/paired-host-structured-launch-bookkeeping.test.ts": 487, - "src/renderer/src/runtime/paired-reconnect-sidebar-agent-count.test.ts": 1745, - "src/renderer/src/runtime/remote-agent-row-last-assistant-message.test.ts": 1747, - "src/renderer/src/runtime/remote-agent-session-launch.test.ts": 101, - "src/renderer/src/runtime/remote-host-file-delete-repro.test.ts": 1622, - "src/renderer/src/runtime/remote-host-file-open-repro.test.ts": 1387, - "src/renderer/src/runtime/remote-runtime-session-tabs-inflight.test.ts": 64, - "src/renderer/src/runtime/remote-runtime-snapshot-outcome.test.ts": 150, - "src/renderer/src/runtime/remote-runtime-terminal-color-push.test.ts": 89, - "src/renderer/src/runtime/remote-runtime-terminal-end-verdict.test.ts": 156, - "src/renderer/src/runtime/remote-runtime-terminal-frame-drop-resync.test.ts": 152, - "src/renderer/src/runtime/remote-runtime-terminal-parse-backpressure.test.ts": 442, - "src/renderer/src/runtime/remote-runtime-terminal-snapshot-kitty-flags.test.ts": 141, - "src/renderer/src/runtime/remote-runtime-terminal-stale-stream-frames.test.ts": 115, - "src/renderer/src/runtime/remote-runtime-terminal-stall-recovery.test.ts": 273, - "src/renderer/src/runtime/remote-server-install-failure-probe.test.ts": 49, - "src/renderer/src/runtime/remote-server-parity.test.ts": 1540, - "src/renderer/src/runtime/remote-server-restart-wait.test.ts": 64, - "src/renderer/src/runtime/remote-server-update-coordinator.test.ts": 112, - "src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts": 55, - "src/renderer/src/runtime/remote-terminal-stream-watchdog.test.ts": 54, - "src/renderer/src/runtime/restored-client-hosted-browser-host-attach.test.ts": 59, - "src/renderer/src/runtime/restored-client-hosted-browser-host-restart-attach.test.ts": 301, - "src/renderer/src/runtime/runtime-antigravity-accounts-client.test.ts": 69, - "src/renderer/src/runtime/runtime-client-events.test.ts": 61, - "src/renderer/src/runtime/runtime-client-target.test.ts": 46, - "src/renderer/src/runtime/runtime-environment-pairing-refresh.test.ts": 98, - "src/renderer/src/runtime/runtime-environment-ssh-state.test.ts": 1589, - "src/renderer/src/runtime/runtime-file-client-download.test.ts": 397, - "src/renderer/src/runtime/runtime-file-client-external-import.test.ts": 419, - "src/renderer/src/runtime/runtime-file-client-mutation-ownership.test.ts": 406, - "src/renderer/src/runtime/runtime-file-client-search-listing.test.ts": 606, - "src/renderer/src/runtime/runtime-file-client-watch.test.ts": 508, - "src/renderer/src/runtime/runtime-file-client.test.ts": 397, - "src/renderer/src/runtime/runtime-file-import-pairing-revision.test.ts": 394, - "src/renderer/src/runtime/runtime-file-range-client.test.ts": 338, - "src/renderer/src/runtime/runtime-file-search-bounds.test.ts": 49, - "src/renderer/src/runtime/runtime-git-client-branch-line-total.test.ts": 308, - "src/renderer/src/runtime/runtime-git-client-merge.test.ts": 338, - "src/renderer/src/runtime/runtime-git-client.test.ts": 421, - "src/renderer/src/runtime/runtime-git-worktree-scope-client.test.ts": 439, - "src/renderer/src/runtime/runtime-hooks-client.test.ts": 101, - "src/renderer/src/runtime/runtime-host-connection-state.test.ts": 57, - "src/renderer/src/runtime/runtime-host-contact-parity.test.ts": 58, - "src/renderer/src/runtime/runtime-host-contact-regained.test.ts": 342, - "src/renderer/src/runtime/runtime-jira-client.test.ts": 230, - "src/renderer/src/runtime/runtime-jira-connection-status.test.ts": 50, - "src/renderer/src/runtime/runtime-jira-payload-stream.test.ts": 198, + "src/renderer/src/lib/terminal-links-redos.test.ts": 49, + "src/renderer/src/lib/terminal-links.test.ts": 237, + "src/renderer/src/lib/terminal-pane-title-sanitization.test.ts": 40, + "src/renderer/src/lib/terminal-path-normalization.test.ts": 39, + "src/renderer/src/lib/terminal-quick-command-project-scope.test.ts": 54, + "src/renderer/src/lib/terminal-quick-command-score-budget.test.ts": 44, + "src/renderer/src/lib/terminal-quick-command-search.test.ts": 57, + "src/renderer/src/lib/terminal-reveal-identity.test.ts": 31, + "src/renderer/src/lib/terminal-shortcut-capture-notification.test.tsx": 1693, + "src/renderer/src/lib/terminal-tab-for-pty-id.test.ts": 38, + "src/renderer/src/lib/terminal-tab-owner-activation.test.ts": 33, + "src/renderer/src/lib/terminal-theme.test.ts": 76, + "src/renderer/src/lib/terminal-worktree-route.test.ts": 356, + "src/renderer/src/lib/text-control-paste-ownership.test.ts": 173, + "src/renderer/src/lib/text-control-paste.test.ts": 189, + "src/renderer/src/lib/titlebar-left-chrome.test.ts": 37, + "src/renderer/src/lib/titlebar-worktree-history-controls.test.ts": 34, + "src/renderer/src/lib/tui-agent-startup.test.ts": 115, + "src/renderer/src/lib/typing-latency/diagnostic-lifecycle.test.ts": 202, + "src/renderer/src/lib/typing-latency/diagnostic-summary.test.ts": 55, + "src/renderer/src/lib/typing-latency/echo-instrumentation.test.ts": 118, + "src/renderer/src/lib/typing-latency/input-events.test.ts": 161, + "src/renderer/src/lib/typing-latency/input-source.test.ts": 49, + "src/renderer/src/lib/typing-latency/sample-window.test.ts": 29, + "src/renderer/src/lib/ui-zoom.test.ts": 50, + "src/renderer/src/lib/unified-tab-anchor-insertion.test.ts": 35, + "src/renderer/src/lib/unread-badge-count-migration.test.ts": 510, + "src/renderer/src/lib/unread-badge-count-selector.test.ts": 1599, + "src/renderer/src/lib/unread-badge-count.test.ts": 393, + "src/renderer/src/lib/update-check-click-options.test.ts": 40, + "src/renderer/src/lib/updater-beforeunload.test.ts": 34, + "src/renderer/src/lib/use-tab-agent-observed-signal-dispatch.test.tsx": 1456, + "src/renderer/src/lib/use-tab-agent-opencode-native-title.test.ts": 1667, + "src/renderer/src/lib/use-tab-agent-pi-identity.test.ts": 128, + "src/renderer/src/lib/use-tab-agent-process-signals.test.ts": 1283, + "src/renderer/src/lib/use-tab-agent-remote-pty-selector.test.ts": 95, + "src/renderer/src/lib/use-tab-agent-retained-identity.test.ts": 1503, + "src/renderer/src/lib/use-tab-agent-sleeping-session.test.ts": 1597, + "src/renderer/src/lib/use-tab-agent.test.ts": 1239, + "src/renderer/src/lib/user-opened-local-path.test.ts": 1426, + "src/renderer/src/lib/visible-overlay.test.ts": 181, + "src/renderer/src/lib/wake-sleeping-agents-in-background.test.ts": 210, + "src/renderer/src/lib/wake-sleeping-agents-live-done.test.ts": 192, + "src/renderer/src/lib/web-client-location.test.ts": 32, + "src/renderer/src/lib/window-label-formatter.test.ts": 38, + "src/renderer/src/lib/window-visibility-interval.test.ts": 50, + "src/renderer/src/lib/window-visibility-timeout-poller.test.ts": 203, + "src/renderer/src/lib/windows-path-access-guards.test.ts": 271, + "src/renderer/src/lib/windows-terminal-capabilities-race.test.ts": 326, + "src/renderer/src/lib/windows-terminal-capabilities.test.ts": 448, + "src/renderer/src/lib/windows-terminal-capability-reprobe.test.ts": 197, + "src/renderer/src/lib/work-item-link-query-bounds.test.ts": 44, + "src/renderer/src/lib/work-item-lookup-text.test.ts": 34, + "src/renderer/src/lib/worker-terminal-takeover-report.test.ts": 295, + "src/renderer/src/lib/workspace-activation-path-gate.test.ts": 1725, + "src/renderer/src/lib/workspace-activation-terminal-focus.test.ts": 341, + "src/renderer/src/lib/workspace-attachment-terminal-origin.test.ts": 84, + "src/renderer/src/lib/workspace-browser-tab-open.test.ts": 583, + "src/renderer/src/lib/workspace-composer-initial-focus.test.ts": 177, + "src/renderer/src/lib/workspace-create-error-format.test.ts": 59, + "src/renderer/src/lib/workspace-directory.test.ts": 281, + "src/renderer/src/lib/workspace-doc-address-input.test.ts": 324, + "src/renderer/src/lib/workspace-emoji-shortcodes.lazy.test.ts": 58, + "src/renderer/src/lib/workspace-emoji-shortcodes.test.ts": 76, + "src/renderer/src/lib/workspace-file-drag-source-runtime.test.ts": 57, + "src/renderer/src/lib/workspace-file-drag.test.ts": 45, + "src/renderer/src/lib/workspace-port-groups.test.ts": 35, + "src/renderer/src/lib/workspace-port-host-availability.test.ts": 42, + "src/renderer/src/lib/workspace-port-host-scoped-scan.test.ts": 338, + "src/renderer/src/lib/workspace-port-scan-debounce.test.ts": 49, + "src/renderer/src/lib/workspace-port-scan-publish.test.ts": 507, + "src/renderer/src/lib/workspace-session-browser-history.test.ts": 281, + "src/renderer/src/lib/workspace-session-editor-drafts.test.ts": 292, + "src/renderer/src/lib/workspace-session-host-contention.test.ts": 334, + "src/renderer/src/lib/workspace-session-host-persistence.test.ts": 322, + "src/renderer/src/lib/workspace-session-host-split.test.ts": 314, + "src/renderer/src/lib/workspace-session-hydration-keys.test.ts": 62, + "src/renderer/src/lib/workspace-session-liveness.test.ts": 275, + "src/renderer/src/lib/workspace-session-patch.test.ts": 327, + "src/renderer/src/lib/workspace-session-persistence-gate.test.ts": 275, + "src/renderer/src/lib/workspace-session-relevant-fields.test.ts": 305, + "src/renderer/src/lib/workspace-session-ssh-partition-ownership.test.ts": 324, + "src/renderer/src/lib/workspace-session-ssh-partition-round-trip.test.ts": 322, + "src/renderer/src/lib/workspace-session-staged-browser-tabs.test.ts": 33, + "src/renderer/src/lib/workspace-session.test.ts": 335, + "src/renderer/src/lib/workspace-surface-reveal.test.ts": 217, + "src/renderer/src/lib/workspace-tab-agent-metadata.test.ts": 43, + "src/renderer/src/lib/workspace-tab-palette-activation.store.test.ts": 1570, + "src/renderer/src/lib/workspace-tab-palette-activation.test.ts": 288, + "src/renderer/src/lib/workspace-tab-palette-results.test.ts": 328, + "src/renderer/src/lib/workspace-tab-palette-search.test.ts": 412, + "src/renderer/src/lib/workspace-terminal-host-authority.test.ts": 1514, + "src/renderer/src/lib/worktree-activation-agent-startup.test.ts": 1464, + "src/renderer/src/lib/worktree-activation-automation-filter.test.ts": 1761, + "src/renderer/src/lib/worktree-activation-created-agent.test.ts": 1604, + "src/renderer/src/lib/worktree-activation-default-agent-chat.test.ts": 1652, + "src/renderer/src/lib/worktree-activation-default-tabs.test.ts": 1507, + "src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts": 1785, + "src/renderer/src/lib/worktree-activation-empty-remote.test.ts": 1541, + "src/renderer/src/lib/worktree-activation-issue-command.test.ts": 1532, + "src/renderer/src/lib/worktree-activation-launch-awaiting-host.test.ts": 1667, + "src/renderer/src/lib/worktree-activation-pty-inventory.test.ts": 336, + "src/renderer/src/lib/worktree-activation-reveal.test.ts": 1823, + "src/renderer/src/lib/worktree-activation-setup-script.test.ts": 1773, + "src/renderer/src/lib/worktree-activation-structured-chat-surface.test.ts": 1704, + "src/renderer/src/lib/worktree-activation-web-runtime.test.ts": 1702, + "src/renderer/src/lib/worktree-activity-state.test.ts": 67, + "src/renderer/src/lib/worktree-agent-activation-gate.test.ts": 1507, + "src/renderer/src/lib/worktree-agent-activation-seam.test.ts": 2047, + "src/renderer/src/lib/worktree-agent-structured-inventory.test.ts": 278, + "src/renderer/src/lib/worktree-attachment-label.test.ts": 37, + "src/renderer/src/lib/worktree-creation-agent-seeding.test.ts": 1047, + "src/renderer/src/lib/worktree-creation-agent-seeds.test.ts": 1313, + "src/renderer/src/lib/worktree-creation-backend-startup-focus.test.ts": 1458, + "src/renderer/src/lib/worktree-creation-background-mount.test.ts": 1910, + "src/renderer/src/lib/worktree-creation-chat-setup.test.ts": 1851, + "src/renderer/src/lib/worktree-creation-flow-dedupe.test.ts": 1029, + "src/renderer/src/lib/worktree-creation-flow-ready-toast.test.ts": 1742, + "src/renderer/src/lib/worktree-creation-flow-stranded-surface.test.ts": 1068, + "src/renderer/src/lib/worktree-creation-flow.test.ts": 2048, + "src/renderer/src/lib/worktree-creation-hook-flow.test.ts": 1457, + "src/renderer/src/lib/worktree-creation-hook-preparation.test.ts": 155, + "src/renderer/src/lib/worktree-creation-ready-toast.test.tsx": 415, + "src/renderer/src/lib/worktree-creation-structured-session-host-decline.test.ts": 2069, + "src/renderer/src/lib/worktree-creation-structured-session.test.ts": 1144, + "src/renderer/src/lib/worktree-creation-surface.test.ts": 37, + "src/renderer/src/lib/worktree-default-display-name.test.ts": 39, + "src/renderer/src/lib/worktree-display-name-order.test.ts": 33, + "src/renderer/src/lib/worktree-draft-startup-view-mode.test.ts": 1417, + "src/renderer/src/lib/worktree-git-identity-display.test.ts": 25, + "src/renderer/src/lib/worktree-host-connection-phase.test.ts": 1341, + "src/renderer/src/lib/worktree-initial-terminal-close-records.test.ts": 1795, + "src/renderer/src/lib/worktree-jump-navigation.test.ts": 328, + "src/renderer/src/lib/worktree-live-terminal-surface-owners.test.ts": 316, + "src/renderer/src/lib/worktree-operation-generation.test.ts": 443, + "src/renderer/src/lib/worktree-operation-route.test.ts": 358, + "src/renderer/src/lib/worktree-palette-attachment-urls.test.ts": 159, + "src/renderer/src/lib/worktree-palette-comment-snippet.test.ts": 44, + "src/renderer/src/lib/worktree-palette-create-action.test.ts": 57, + "src/renderer/src/lib/worktree-palette-multi-keyword.test.ts": 274, + "src/renderer/src/lib/worktree-palette-review-match.test.ts": 121, + "src/renderer/src/lib/worktree-palette-runtime-owner-identity.test.ts": 373, + "src/renderer/src/lib/worktree-palette-search.test.ts": 187, + "src/renderer/src/lib/worktree-palette-task-url-match.test.ts": 153, + "src/renderer/src/lib/worktree-reactivation-preserved-pane-replacement.test.ts": 1824, + "src/renderer/src/lib/worktree-reactivation-runtime-owned-resume-deferral.test.ts": 1567, + "src/renderer/src/lib/worktree-reactivation-tab-forkbomb.test.ts": 1873, + "src/renderer/src/lib/worktree-row-open-in-owner.test.ts": 340, + "src/renderer/src/lib/worktree-runtime-owner-index.detected-perf.test.ts": 284, + "src/renderer/src/lib/worktree-runtime-owner-index.detected.test.ts": 113, + "src/renderer/src/lib/worktree-runtime-owner-index.test.ts": 32, + "src/renderer/src/lib/worktree-runtime-owner.test.ts": 370, + "src/renderer/src/lib/worktree-sort-order-host-split.test.ts": 322, + "src/renderer/src/lib/worktree-sort-order-persistence.test.ts": 346, + "src/renderer/src/lib/worktree-status-spinner-launch-agent.test.ts": 91, + "src/renderer/src/lib/worktree-status-terminal-layout-roots.test.ts": 95, + "src/renderer/src/lib/worktree-status.interrupted.test.ts": 77, + "src/renderer/src/lib/worktree-status.test.ts": 117, + "src/renderer/src/lib/worktree-visit-recency.test.ts": 35, + "src/renderer/src/lib/wsl-host-account-owner.test.ts": 53, + "src/renderer/src/runtime/abortable-runtime-environment-call.test.ts": 183, + "src/renderer/src/runtime/agent-resume-host-authority-capability.test.ts": 255, + "src/renderer/src/runtime/agent-session-keyboard-capability.test.ts": 236, + "src/renderer/src/runtime/agent-session-operation-id.test.ts": 33, + "src/renderer/src/runtime/antigravity-generation-compatibility.test.ts": 292, + "src/renderer/src/runtime/browser-client-host-identity.test.ts": 36, + "src/renderer/src/runtime/browser-workspace-tab-close-plan.test.ts": 40, + "src/renderer/src/runtime/browser-workspace-tab-close.test.ts": 193, + "src/renderer/src/runtime/client-hosted-browser-close-intent-replay.test.ts": 326, + "src/renderer/src/runtime/client-hosted-browser-close-intents.test.ts": 226, + "src/renderer/src/runtime/client-hosted-browser-row-close.test.ts": 293, + "src/renderer/src/runtime/close-mirrored-editor-tab.test.ts": 199, + "src/renderer/src/runtime/file-explorer-delete-owner-provenance.test.ts": 2152, + "src/renderer/src/runtime/focus-runtime-terminal-surface-chat-view.test.ts": 168, + "src/renderer/src/runtime/focus-runtime-terminal-surface-readiness.test.ts": 173, + "src/renderer/src/runtime/github-check-details-timeout.test.ts": 42, + "src/renderer/src/runtime/gitlab-job-trace-client.test.ts": 287, + "src/renderer/src/runtime/gitlab-owner-api.test.ts": 284, + "src/renderer/src/runtime/host-model-catalog-snapshots.test.ts": 1780, + "src/renderer/src/runtime/host-route-call.test.ts": 349, + "src/renderer/src/runtime/host-session-mirror-empty-inventory-settle.test.ts": 600, + "src/renderer/src/runtime/host-session-mirror-flap-hydration.test.ts": 373, + "src/renderer/src/runtime/host-session-mirror-hydration-drain.test.ts": 354, + "src/renderer/src/runtime/host-session-mirror-hydration-frame-ordering.test.tsx": 1709, + "src/renderer/src/runtime/host-session-mirror-settle-receipt-frames.test.tsx": 1883, + "src/renderer/src/runtime/host-session-snapshot-authority-client-hosted.test.ts": 35, + "src/renderer/src/runtime/host-structured-agents.test.ts": 596, + "src/renderer/src/runtime/local-runtime-capabilities.test.ts": 169, + "src/renderer/src/runtime/local-session-tab-close-owner.test.ts": 30, + "src/renderer/src/runtime/local-structured-chats.test.ts": 280, + "src/renderer/src/runtime/local-structured-session-empty-worktree-visibility.test.ts": 1348, + "src/renderer/src/runtime/local-structured-session-retired-epoch-repair.test.ts": 1490, + "src/renderer/src/runtime/local-structured-session-reveal-visibility.test.ts": 1498, + "src/renderer/src/runtime/local-structured-session-tabs-host-isolation.test.ts": 808, + "src/renderer/src/runtime/local-structured-session-tabs-sync-hook.test.ts": 1689, + "src/renderer/src/runtime/local-structured-session-tabs-sync.test.ts": 2060, + "src/renderer/src/runtime/local-structured-session-tabs-sync/retired-epoch-repair.test.ts": 45, + "src/renderer/src/runtime/local-structured-session-unverifiable-inventory.test.ts": 1581, + "src/renderer/src/runtime/mirrored-agent-status-clock-skew.test.ts": 1538, + "src/renderer/src/runtime/mobile-markdown-bridge-save-guards.test.ts": 1399, + "src/renderer/src/runtime/mobile-markdown-bridge.test.ts": 2091, + "src/renderer/src/runtime/mounted-terminal-tab-registry.test.ts": 523, + "src/renderer/src/runtime/native-chat-launch-draft-runtime-resolution.test.ts": 29, + "src/renderer/src/runtime/paired-agent-row-turn-outcome-arms.test.ts": 1326, + "src/renderer/src/runtime/paired-host-client-capabilities.test.ts": 328, + "src/renderer/src/runtime/paired-host-structured-launch-bookkeeping.test.ts": 587, + "src/renderer/src/runtime/paired-reconnect-sidebar-agent-count.test.ts": 1639, + "src/renderer/src/runtime/remote-agent-row-last-assistant-message.test.ts": 1363, + "src/renderer/src/runtime/remote-agent-session-launch.test.ts": 255, + "src/renderer/src/runtime/remote-host-file-delete-repro.test.ts": 1568, + "src/renderer/src/runtime/remote-host-file-open-repro.test.ts": 1332, + "src/renderer/src/runtime/remote-restore-spawn-intent.test.ts": 205, + "src/renderer/src/runtime/remote-runtime-session-tabs-inflight.test.ts": 39, + "src/renderer/src/runtime/remote-runtime-snapshot-outcome.test.ts": 106, + "src/renderer/src/runtime/remote-runtime-terminal-color-push.test.ts": 191, + "src/renderer/src/runtime/remote-runtime-terminal-end-verdict.test.ts": 101, + "src/renderer/src/runtime/remote-runtime-terminal-frame-drop-resync.test.ts": 116, + "src/renderer/src/runtime/remote-runtime-terminal-parse-backpressure.test.ts": 385, + "src/renderer/src/runtime/remote-runtime-terminal-snapshot-kitty-flags.test.ts": 92, + "src/renderer/src/runtime/remote-runtime-terminal-stale-stream-frames.test.ts": 82, + "src/renderer/src/runtime/remote-runtime-terminal-stall-recovery.test.ts": 197, + "src/renderer/src/runtime/remote-server-install-failure-probe.test.ts": 31, + "src/renderer/src/runtime/remote-server-parity.test.ts": 1348, + "src/renderer/src/runtime/remote-server-restart-wait.test.ts": 40, + "src/renderer/src/runtime/remote-server-update-coordinator.test.ts": 102, + "src/renderer/src/runtime/remote-terminal-layout-resolution.test.ts": 45, + "src/renderer/src/runtime/remote-terminal-stream-watchdog.test.ts": 37, + "src/renderer/src/runtime/restored-client-hosted-browser-host-attach.test.ts": 46, + "src/renderer/src/runtime/restored-client-hosted-browser-host-restart-attach.test.ts": 418, + "src/renderer/src/runtime/resume-host-automatic-restores.test.ts": 205, + "src/renderer/src/runtime/runtime-antigravity-accounts-client.test.ts": 49, + "src/renderer/src/runtime/runtime-client-events.test.ts": 48, + "src/renderer/src/runtime/runtime-client-target.test.ts": 40, + "src/renderer/src/runtime/runtime-environment-pairing-refresh.test.ts": 94, + "src/renderer/src/runtime/runtime-environment-ssh-state.test.ts": 1370, + "src/renderer/src/runtime/runtime-file-client-download.test.ts": 324, + "src/renderer/src/runtime/runtime-file-client-external-import.test.ts": 444, + "src/renderer/src/runtime/runtime-file-client-mutation-ownership.test.ts": 419, + "src/renderer/src/runtime/runtime-file-client-search-listing.test.ts": 464, + "src/renderer/src/runtime/runtime-file-client-watch.test.ts": 322, + "src/renderer/src/runtime/runtime-file-client.test.ts": 420, + "src/renderer/src/runtime/runtime-file-import-pairing-revision.test.ts": 398, + "src/renderer/src/runtime/runtime-file-range-client.test.ts": 362, + "src/renderer/src/runtime/runtime-file-search-bounds.test.ts": 35, + "src/renderer/src/runtime/runtime-git-client-branch-line-total.test.ts": 346, + "src/renderer/src/runtime/runtime-git-client-merge.test.ts": 293, + "src/renderer/src/runtime/runtime-git-client.test.ts": 375, + "src/renderer/src/runtime/runtime-git-worktree-scope-client.test.ts": 462, + "src/renderer/src/runtime/runtime-hooks-client.test.ts": 204, + "src/renderer/src/runtime/runtime-host-connection-state.test.ts": 47, + "src/renderer/src/runtime/runtime-host-contact-parity.test.ts": 51, + "src/renderer/src/runtime/runtime-host-contact-regained.test.ts": 376, + "src/renderer/src/runtime/runtime-jira-board-filter-compatibility.test.ts": 372, + "src/renderer/src/runtime/runtime-jira-client.test.ts": 443, + "src/renderer/src/runtime/runtime-jira-connection-status.test.ts": 36, + "src/renderer/src/runtime/runtime-jira-detail-client.test.ts": 445, + "src/renderer/src/runtime/runtime-jira-detail-images.test.ts": 383, + "src/renderer/src/runtime/runtime-jira-image-cache.test.ts": 1057, + "src/renderer/src/runtime/runtime-jira-image-client.test.ts": 170, + "src/renderer/src/runtime/runtime-jira-payload-stream.test.ts": 174, + "src/renderer/src/runtime/runtime-jira-priority-client.test.ts": 410, + "src/renderer/src/runtime/runtime-jira-project-client.test.ts": 175, + "src/renderer/src/runtime/runtime-jira-saved-filter-state.test.ts": 171, + "src/renderer/src/runtime/runtime-jira-status-filters.test.ts": 325, + "src/renderer/src/runtime/runtime-jira-summary-generation-client.test.ts": 330, "src/renderer/src/runtime/runtime-layout-client.test.ts": 115, - "src/renderer/src/runtime/runtime-legacy-inventory-budget.test.ts": 58, - "src/renderer/src/runtime/runtime-legacy-quick-open-inventory-expiry.test.ts": 733, - "src/renderer/src/runtime/runtime-legacy-recent-freshness.test.ts": 343, - "src/renderer/src/runtime/runtime-linear-client.test.ts": 118, - "src/renderer/src/runtime/runtime-path-existence-batch.test.ts": 327, - "src/renderer/src/runtime/runtime-path-existence-error-parity.test.ts": 339, - "src/renderer/src/runtime/runtime-path-existence-pairing.test.ts": 307, - "src/renderer/src/runtime/runtime-path-existence-queue-pairing.test.ts": 320, - "src/renderer/src/runtime/runtime-provider-accounts-client.test.ts": 132, - "src/renderer/src/runtime/runtime-provider-search-bounds.test.ts": 50, - "src/renderer/src/runtime/runtime-quick-open-version-compatibility.test.ts": 484, - "src/renderer/src/runtime/runtime-repo-client.test.ts": 183, - "src/renderer/src/runtime/runtime-repo-search-bounds.test.ts": 53, - "src/renderer/src/runtime/runtime-rpc-client-pairing-revision.test.ts": 89, - "src/renderer/src/runtime/runtime-rpc-client.test.ts": 128, - "src/renderer/src/runtime/runtime-rpc-result.test.ts": 46, - "src/renderer/src/runtime/runtime-server-directory-browser.test.ts": 86, - "src/renderer/src/runtime/runtime-skills-client.test.ts": 90, - "src/renderer/src/runtime/runtime-skills-delete-client.test.ts": 74, - "src/renderer/src/runtime/runtime-terminal-inspection.test.ts": 1879, - "src/renderer/src/runtime/runtime-terminal-stream.test.ts": 408, - "src/renderer/src/runtime/runtime-terminal-verified-input.test.ts": 1396, - "src/renderer/src/runtime/runtime-worktree-selector.test.ts": 310, - "src/renderer/src/runtime/structured-agent-session-client.test.ts": 184, - "src/renderer/src/runtime/structured-agent-session-close.test.ts": 92, - "src/renderer/src/runtime/structured-agent-session-host-capability.test.tsx": 371, - "src/renderer/src/runtime/structured-agent-session-status-feed-lifecycle.test.ts": 95, - "src/renderer/src/runtime/structured-agent-session-status-feed.test.ts": 104, - "src/renderer/src/runtime/structured-agent-session-tab-retirement.test.ts": 1480, - "src/renderer/src/runtime/structured-agent-session-turn-completion-feed.test.ts": 102, - "src/renderer/src/runtime/structured-conversation-tab-replacement.test.ts": 1794, - "src/renderer/src/runtime/structured-session-orca-session-id.test.ts": 271, - "src/renderer/src/runtime/structured-session-tab-inventory.test.ts": 57, - "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts": 448, - "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts": 488, - "src/renderer/src/runtime/sync-runtime-graph-automation-leaf.test.ts": 471, - "src/renderer/src/runtime/sync-runtime-graph-browser.test.ts": 411, - "src/renderer/src/runtime/sync-runtime-graph-conversion-publish.test.ts": 1416, - "src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts": 425, - "src/renderer/src/runtime/sync-runtime-graph-key-reuse.test.ts": 504, - "src/renderer/src/runtime/sync-runtime-graph-parked-leaf.test.ts": 445, - "src/renderer/src/runtime/sync-runtime-graph-payload-partition.test.ts": 443, - "src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts": 513, - "src/renderer/src/runtime/sync-runtime-graph-publication-cost.test.ts": 538, - "src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts": 396, - "src/renderer/src/runtime/sync-runtime-graph-terminal-layout.test.ts": 877, - "src/renderer/src/runtime/sync-runtime-graph-terminal-registration-ownership.test.ts": 429, - "src/renderer/src/runtime/sync-runtime-graph-terminal-surface-projection.test.ts": 513, - "src/renderer/src/runtime/sync-runtime-graph-workspace-publication.test.ts": 423, - "src/renderer/src/runtime/sync-runtime-graph.test.ts": 483, - "src/renderer/src/runtime/sync-runtime-graph/mobile-terminal-theme.test.ts": 74, - "src/renderer/src/runtime/use-remote-runtime-recovery-triggers.test.ts": 277, - "src/renderer/src/runtime/use-runtime-session-mirror-environment-key.test.ts": 1954, - "src/renderer/src/runtime/use-worktree-runtime-target.test.ts": 1773, - "src/renderer/src/runtime/web-runtime-browser-capability-cleanup.test.ts": 603, - "src/renderer/src/runtime/web-runtime-browser-creation-placement.test.ts": 1772, - "src/renderer/src/runtime/web-runtime-browser-materialization.test.ts": 44, - "src/renderer/src/runtime/web-runtime-browser-tab-staging-hosting-intent.test.ts": 57, - "src/renderer/src/runtime/web-runtime-pane-layout-intent.test.ts": 561, - "src/renderer/src/runtime/web-runtime-session-browser-client-placement.test.ts": 681, - "src/renderer/src/runtime/web-runtime-session-browser-create-failure.test.ts": 610, - "src/renderer/src/runtime/web-runtime-session-browser-create-focus.test.ts": 871, - "src/renderer/src/runtime/web-runtime-session-browser-create-split-placement.test.ts": 969, - "src/renderer/src/runtime/web-runtime-session-browser-create-staged-focus.test.ts": 643, - "src/renderer/src/runtime/web-runtime-session-browser-create-staged-hosting-intent.test.ts": 480, - "src/renderer/src/runtime/web-runtime-session-browser-create-staging.test.ts": 1143, - "src/renderer/src/runtime/web-runtime-session-browser-placement-staleness.test.ts": 606, - "src/renderer/src/runtime/web-runtime-session-tab-activate-close.test.ts": 647, - "src/renderer/src/runtime/web-runtime-session-tab-move.test.ts": 580, - "src/renderer/src/runtime/web-runtime-session-tab-props.test.ts": 667, - "src/renderer/src/runtime/web-runtime-session-terminal-host-authority.test.ts": 636, - "src/renderer/src/runtime/web-runtime-session-terminal-legacy-create.test.ts": 597, - "src/renderer/src/runtime/web-runtime-session-terminal-pane-delegation.test.ts": 1145, - "src/renderer/src/runtime/web-runtime-session-terminal-workspace-routing.test.ts": 612, - "src/renderer/src/runtime/web-runtime-session.test.ts": 512, - "src/renderer/src/runtime/web-runtime-terminal-identity.test.ts": 59, - "src/renderer/src/runtime/web-runtime-wake-terminal-respawn.test.ts": 53, - "src/renderer/src/runtime/web-session-browser-placement.test.ts": 87, - "src/renderer/src/runtime/web-session-close-intent.test.ts": 51, - "src/renderer/src/runtime/web-session-existing-tab-index.test.ts": 41, - "src/renderer/src/runtime/web-session-focus-intent-visible-tab.test.ts": 1527, - "src/renderer/src/runtime/web-session-intent-owner.test.ts": 64, - "src/renderer/src/runtime/web-session-open-files-batch-equivalence.test.ts": 1429, - "src/renderer/src/runtime/web-session-restored-placeholder-identity.test.ts": 743, - "src/renderer/src/runtime/web-session-split-pane-retirement-fences.test.ts": 851, - "src/renderer/src/runtime/web-session-structured-tab-focus.test.ts": 1830, - "src/renderer/src/runtime/web-session-tabs-agent-completion-notifications.test.ts": 1651, - "src/renderer/src/runtime/web-session-tabs-late-merged-predecessor-lockout.test.ts": 620, - "src/renderer/src/runtime/web-session-tabs-publisher-identity-lineage.test.ts": 566, - "src/renderer/src/runtime/web-session-tabs-removed-frame-retires-live-publisher.test.ts": 524, - "src/renderer/src/runtime/web-session-tabs-retired-epoch-census-repair.test.tsx": 1870, - "src/renderer/src/runtime/web-session-tabs-stream-end-resubscribe.test.tsx": 1954, - "src/renderer/src/runtime/web-session-tabs-sync-agent-handoff.test.ts": 858, - "src/renderer/src/runtime/web-session-tabs-sync-agent-status.test.ts": 936, - "src/renderer/src/runtime/web-session-tabs-sync-browser-tabs.test.ts": 924, - "src/renderer/src/runtime/web-session-tabs-sync-client-owned-page-content.test.ts": 1153, - "src/renderer/src/runtime/web-session-tabs-sync-client-owned-placement.test.ts": 926, - "src/renderer/src/runtime/web-session-tabs-sync-diff-focus-steal.test.ts": 880, - "src/renderer/src/runtime/web-session-tabs-sync-editor-ownership.test.ts": 1610, - "src/renderer/src/runtime/web-session-tabs-sync-editor-tabs.test.ts": 920, - "src/renderer/src/runtime/web-session-tabs-sync-emptied-workspace-tombstone.test.ts": 907, - "src/renderer/src/runtime/web-session-tabs-sync-focus-intent.test.ts": 904, - "src/renderer/src/runtime/web-session-tabs-sync-host-restart-browser-rows.test.ts": 889, - "src/renderer/src/runtime/web-session-tabs-sync-host-tab-retraction-ghost-rows.test.ts": 1787, - "src/renderer/src/runtime/web-session-tabs-sync-html-preview-focus.test.ts": 907, - "src/renderer/src/runtime/web-session-tabs-sync-initial-terminal-relaunch.test.tsx": 1999, - "src/renderer/src/runtime/web-session-tabs-sync-layout-duplicate-groups.test.ts": 856, - "src/renderer/src/runtime/web-session-tabs-sync-layout-groups.test.ts": 849, - "src/renderer/src/runtime/web-session-tabs-sync-mirror-identity.test.ts": 1630, - "src/renderer/src/runtime/web-session-tabs-sync-mirrored-draft-close.test.ts": 1875, - "src/renderer/src/runtime/web-session-tabs-sync-reconnect-resubscribe.test.tsx": 2282, - "src/renderer/src/runtime/web-session-tabs-sync-remote-main-agent-verdict.test.ts": 1574, - "src/renderer/src/runtime/web-session-tabs-sync-remote-status-title-flap.test.ts": 1543, - "src/renderer/src/runtime/web-session-tabs-sync-restored-browser-rows.test.ts": 891, - "src/renderer/src/runtime/web-session-tabs-sync-snapshot-batch.test.ts": 1094, - "src/renderer/src/runtime/web-session-tabs-sync-staged-browser-adoption.test.ts": 847, - "src/renderer/src/runtime/web-session-tabs-sync-staged-browser-authority.test.ts": 752, - "src/renderer/src/runtime/web-session-tabs-sync-terminal-bootstrap.test.ts": 594, - "src/renderer/src/runtime/web-session-tabs-sync-terminal-mirroring.test.ts": 835, - "src/renderer/src/runtime/web-session-tabs-sync-tracking-teardown.test.ts": 764, - "src/renderer/src/runtime/web-session-tabs-sync-unverifiable-host.test.tsx": 1909, - "src/renderer/src/runtime/web-session-tabs-sync-visibility-collision.test.tsx": 1961, - "src/renderer/src/runtime/web-session-tabs-sync-window-visibility.test.tsx": 8419, - "src/renderer/src/runtime/web-session-tabs-sync.test.ts": 936, - "src/renderer/src/runtime/web-session-tabs-sync/mirrored-agent-tab-label.test.ts": 1550, - "src/renderer/src/runtime/web-session-tabs-sync/mirrored-status-tab-retractions.test.ts": 1499, - "src/renderer/src/runtime/web-session-tabs-sync/mirrored-terminal-recovery-ledger.test.ts": 1593, - "src/renderer/src/runtime/web-session-tabs-sync/retraction-pane-ownership.test.ts": 1249, - "src/renderer/src/runtime/web-session-tabs-sync/state-equality-core.test.ts": 50, - "src/renderer/src/runtime/web-session-tabs-sync/tracking-lifecycle.test.ts": 1895, - "src/renderer/src/runtime/web-session-terminal-handle-events.test.ts": 57, - "src/renderer/src/runtime/web-session-terminal-orphan-absence-across-republication.test.ts": 646, - "src/renderer/src/runtime/web-session-terminal-orphan-inventory-retry.test.ts": 1437, - "src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts": 911, - "src/renderer/src/runtime/web-session-terminal-orphan-recovery-adoption-regressions.test.ts": 983, - "src/renderer/src/runtime/web-session-terminal-orphan-recovery-host-scope-gate.test.ts": 568, - "src/renderer/src/runtime/web-session-terminal-orphan-recovery-prior-removal.test.ts": 563, - "src/renderer/src/runtime/web-session-terminal-orphan-recovery-regressions.test.ts": 1006, - "src/renderer/src/runtime/web-session-terminal-orphan-recovery-topology-fence.test.ts": 760, - "src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts": 719, - "src/renderer/src/runtime/web-session-terminal-pending-handle-recovery.test.ts": 679, - "src/renderer/src/runtime/web-session-terminal-recovery-snapshot-validation.test.ts": 750, - "src/renderer/src/runtime/web-session-terminal-retirement-proof-ledger.test.ts": 528, - "src/renderer/src/runtime/window-visibility-subscription-parking-stream-end.test.ts": 189, - "src/renderer/src/runtime/window-visibility-subscription-parking.test.ts": 218, - "src/renderer/src/runtime/worktree-create-base.test.ts": 45, - "src/renderer/src/ssh/ssh-connect-in-flight.test.ts": 77, - "src/renderer/src/ssh/ssh-connect-ui-timeout.test.ts": 58, - "src/renderer/src/ssh/ssh-connect-verb.test.ts": 57, - "src/renderer/src/ssh/ssh-connection-recoverability.test.ts": 52, - "src/renderer/src/startup/active-workspace-ssh-targets.test.ts": 70, - "src/renderer/src/startup/ssh-startup-reconnect.test.ts": 65, - "src/renderer/src/startup/startup-degraded-recovery.test.ts": 408, - "src/renderer/src/startup/startup-ssh-connection-restore.test.ts": 77, - "src/renderer/src/store/active-terminal-chrome-selector.test.ts": 52, - "src/renderer/src/store/adopt-from-endpoint.test.ts": 344, - "src/renderer/src/store/always-mounted-selector-scan-cost.test.ts": 1434, + "src/renderer/src/runtime/runtime-legacy-inventory-budget.test.ts": 52, + "src/renderer/src/runtime/runtime-legacy-quick-open-inventory-expiry.test.ts": 761, + "src/renderer/src/runtime/runtime-legacy-recent-freshness.test.ts": 327, + "src/renderer/src/runtime/runtime-linear-client.test.ts": 285, + "src/renderer/src/runtime/runtime-path-existence-batch.test.ts": 315, + "src/renderer/src/runtime/runtime-path-existence-error-parity.test.ts": 377, + "src/renderer/src/runtime/runtime-path-existence-pairing.test.ts": 287, + "src/renderer/src/runtime/runtime-path-existence-queue-pairing.test.ts": 344, + "src/renderer/src/runtime/runtime-provider-accounts-client.test.ts": 270, + "src/renderer/src/runtime/runtime-provider-search-bounds.test.ts": 31, + "src/renderer/src/runtime/runtime-quick-open-version-compatibility.test.ts": 372, + "src/renderer/src/runtime/runtime-repo-client.test.ts": 368, + "src/renderer/src/runtime/runtime-repo-search-bounds.test.ts": 38, + "src/renderer/src/runtime/runtime-rpc-client-pairing-revision.test.ts": 257, + "src/renderer/src/runtime/runtime-rpc-client.test.ts": 283, + "src/renderer/src/runtime/runtime-rpc-result.test.ts": 33, + "src/renderer/src/runtime/runtime-server-directory-browser.test.ts": 252, + "src/renderer/src/runtime/runtime-skills-client.test.ts": 242, + "src/renderer/src/runtime/runtime-skills-delete-client.test.ts": 47, + "src/renderer/src/runtime/runtime-ssh-target-management.test.ts": 61, + "src/renderer/src/runtime/runtime-terminal-artifact-read.test.ts": 311, + "src/renderer/src/runtime/runtime-terminal-inspection.test.ts": 1692, + "src/renderer/src/runtime/runtime-terminal-stream.test.ts": 436, + "src/renderer/src/runtime/runtime-terminal-verified-input.test.ts": 1343, + "src/renderer/src/runtime/runtime-worktree-selector.test.ts": 275, + "src/renderer/src/runtime/structured-agent-session-client.test.ts": 423, + "src/renderer/src/runtime/structured-agent-session-close.test.ts": 225, + "src/renderer/src/runtime/structured-agent-session-host-capability.test.tsx": 575, + "src/renderer/src/runtime/structured-agent-session-status-feed-lifecycle.test.ts": 236, + "src/renderer/src/runtime/structured-agent-session-status-feed.test.ts": 282, + "src/renderer/src/runtime/structured-agent-session-tab-retirement.test.ts": 1575, + "src/renderer/src/runtime/structured-agent-session-turn-completion-feed.test.ts": 257, + "src/renderer/src/runtime/structured-conversation-tab-replacement.test.ts": 1610, + "src/renderer/src/runtime/structured-session-orca-session-id.test.ts": 282, + "src/renderer/src/runtime/structured-session-tab-inventory.test.ts": 43, + "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection-join.test.ts": 466, + "src/renderer/src/runtime/sync-runtime-graph-agent-status-projection.test.ts": 464, + "src/renderer/src/runtime/sync-runtime-graph-automation-leaf.test.ts": 367, + "src/renderer/src/runtime/sync-runtime-graph-browser.test.ts": 399, + "src/renderer/src/runtime/sync-runtime-graph-conversion-publish.test.ts": 1455, + "src/renderer/src/runtime/sync-runtime-graph-editor-diff-tabs.test.ts": 386, + "src/renderer/src/runtime/sync-runtime-graph-key-reuse.test.ts": 459, + "src/renderer/src/runtime/sync-runtime-graph-parked-leaf.test.ts": 461, + "src/renderer/src/runtime/sync-runtime-graph-payload-partition.test.ts": 478, + "src/renderer/src/runtime/sync-runtime-graph-projection-hot-path.test.ts": 544, + "src/renderer/src/runtime/sync-runtime-graph-publication-cost.test.ts": 489, + "src/renderer/src/runtime/sync-runtime-graph-scheduling.test.ts": 384, + "src/renderer/src/runtime/sync-runtime-graph-terminal-group-grid-sizes.test.ts": 377, + "src/renderer/src/runtime/sync-runtime-graph-terminal-layout.test.ts": 1004, + "src/renderer/src/runtime/sync-runtime-graph-terminal-surface-projection.test.ts": 331, + "src/renderer/src/runtime/sync-runtime-graph-workspace-publication.test.ts": 392, + "src/renderer/src/runtime/sync-runtime-graph.test.ts": 422, + "src/renderer/src/runtime/sync-runtime-graph/mobile-terminal-theme.test.ts": 73, + "src/renderer/src/runtime/sync-runtime-graph/terminal-group-grid-sizes.test.ts": 144, + "src/renderer/src/runtime/use-remote-runtime-recovery-triggers.test.ts": 257, + "src/renderer/src/runtime/use-runtime-session-mirror-environment-key.test.ts": 1869, + "src/renderer/src/runtime/use-worktree-runtime-target.test.ts": 1567, + "src/renderer/src/runtime/web-runtime-browser-capability-cleanup.test.ts": 608, + "src/renderer/src/runtime/web-runtime-browser-creation-placement.test.ts": 1620, + "src/renderer/src/runtime/web-runtime-browser-materialization.test.ts": 26, + "src/renderer/src/runtime/web-runtime-browser-tab-staging-hosting-intent.test.ts": 44, + "src/renderer/src/runtime/web-runtime-pane-layout-intent.test.ts": 545, + "src/renderer/src/runtime/web-runtime-session-browser-client-placement.test.ts": 638, + "src/renderer/src/runtime/web-runtime-session-browser-create-failure.test.ts": 750, + "src/renderer/src/runtime/web-runtime-session-browser-create-focus.test.ts": 896, + "src/renderer/src/runtime/web-runtime-session-browser-create-split-placement.test.ts": 925, + "src/renderer/src/runtime/web-runtime-session-browser-create-staged-focus.test.ts": 626, + "src/renderer/src/runtime/web-runtime-session-browser-create-staged-hosting-intent.test.ts": 563, + "src/renderer/src/runtime/web-runtime-session-browser-create-staging.test.ts": 922, + "src/renderer/src/runtime/web-runtime-session-browser-placement-staleness.test.ts": 552, + "src/renderer/src/runtime/web-runtime-session-legacy-relay-bridged-activation.test.ts": 523, + "src/renderer/src/runtime/web-runtime-session-tab-activate-close.test.ts": 671, + "src/renderer/src/runtime/web-runtime-session-tab-move.test.ts": 321, + "src/renderer/src/runtime/web-runtime-session-tab-props.test.ts": 644, + "src/renderer/src/runtime/web-runtime-session-terminal-host-authority.test.ts": 865, + "src/renderer/src/runtime/web-runtime-session-terminal-legacy-create.test.ts": 536, + "src/renderer/src/runtime/web-runtime-session-terminal-pane-delegation.test.ts": 1173, + "src/renderer/src/runtime/web-runtime-session-terminal-workspace-routing.test.ts": 594, + "src/renderer/src/runtime/web-runtime-session.test.ts": 329, + "src/renderer/src/runtime/web-runtime-terminal-identity.test.ts": 32, + "src/renderer/src/runtime/web-runtime-wake-terminal-respawn.test.ts": 31, + "src/renderer/src/runtime/web-session-browser-placement.test.ts": 47, + "src/renderer/src/runtime/web-session-close-intent.test.ts": 40, + "src/renderer/src/runtime/web-session-existing-tab-index.test.ts": 33, + "src/renderer/src/runtime/web-session-focus-intent-visible-tab.test.ts": 1391, + "src/renderer/src/runtime/web-session-intent-owner.test.ts": 45, + "src/renderer/src/runtime/web-session-open-files-batch-equivalence.test.ts": 1115, + "src/renderer/src/runtime/web-session-restored-placeholder-identity.test.ts": 753, + "src/renderer/src/runtime/web-session-split-pane-retirement-fences.test.ts": 766, + "src/renderer/src/runtime/web-session-structured-tab-focus.test.ts": 1399, + "src/renderer/src/runtime/web-session-tabs-agent-completion-notifications.test.ts": 1491, + "src/renderer/src/runtime/web-session-tabs-late-merged-predecessor-lockout.test.ts": 585, + "src/renderer/src/runtime/web-session-tabs-publisher-identity-lineage.test.ts": 565, + "src/renderer/src/runtime/web-session-tabs-removed-frame-retires-live-publisher.test.ts": 690, + "src/renderer/src/runtime/web-session-tabs-retired-epoch-census-repair.test.tsx": 1768, + "src/renderer/src/runtime/web-session-tabs-stream-end-resubscribe.test.tsx": 2022, + "src/renderer/src/runtime/web-session-tabs-sync-agent-handoff.test.ts": 773, + "src/renderer/src/runtime/web-session-tabs-sync-agent-status.test.ts": 852, + "src/renderer/src/runtime/web-session-tabs-sync-browser-tabs.test.ts": 838, + "src/renderer/src/runtime/web-session-tabs-sync-client-owned-page-content.test.ts": 814, + "src/renderer/src/runtime/web-session-tabs-sync-client-owned-placement.test.ts": 818, + "src/renderer/src/runtime/web-session-tabs-sync-diff-focus-steal.test.ts": 820, + "src/renderer/src/runtime/web-session-tabs-sync-editor-ownership.test.ts": 1521, + "src/renderer/src/runtime/web-session-tabs-sync-editor-tabs.test.ts": 791, + "src/renderer/src/runtime/web-session-tabs-sync-emptied-workspace-tombstone.test.ts": 877, + "src/renderer/src/runtime/web-session-tabs-sync-focus-intent.test.ts": 853, + "src/renderer/src/runtime/web-session-tabs-sync-host-group-mapping.test.ts": 807, + "src/renderer/src/runtime/web-session-tabs-sync-host-restart-browser-rows.test.ts": 790, + "src/renderer/src/runtime/web-session-tabs-sync-host-tab-retraction-ghost-rows.test.ts": 1417, + "src/renderer/src/runtime/web-session-tabs-sync-html-preview-focus.test.ts": 903, + "src/renderer/src/runtime/web-session-tabs-sync-initial-terminal-relaunch.test.tsx": 1884, + "src/renderer/src/runtime/web-session-tabs-sync-layout-duplicate-groups.test.ts": 789, + "src/renderer/src/runtime/web-session-tabs-sync-layout-groups.test.ts": 743, + "src/renderer/src/runtime/web-session-tabs-sync-mirror-identity.test.ts": 1508, + "src/renderer/src/runtime/web-session-tabs-sync-mirrored-draft-close.test.ts": 1628, + "src/renderer/src/runtime/web-session-tabs-sync-reconnect-resubscribe.test.tsx": 1932, + "src/renderer/src/runtime/web-session-tabs-sync-remote-main-agent-verdict.test.ts": 1409, + "src/renderer/src/runtime/web-session-tabs-sync-remote-status-title-flap.test.ts": 1471, + "src/renderer/src/runtime/web-session-tabs-sync-restored-browser-rows.test.ts": 1077, + "src/renderer/src/runtime/web-session-tabs-sync-snapshot-batch.test.ts": 955, + "src/renderer/src/runtime/web-session-tabs-sync-staged-browser-adoption.test.ts": 747, + "src/renderer/src/runtime/web-session-tabs-sync-staged-browser-authority.test.ts": 744, + "src/renderer/src/runtime/web-session-tabs-sync-terminal-bootstrap.test.ts": 627, + "src/renderer/src/runtime/web-session-tabs-sync-terminal-mirroring.test.ts": 738, + "src/renderer/src/runtime/web-session-tabs-sync-tracking-teardown.test.ts": 706, + "src/renderer/src/runtime/web-session-tabs-sync-unverifiable-host.test.tsx": 1762, + "src/renderer/src/runtime/web-session-tabs-sync-visibility-collision.test.tsx": 1988, + "src/renderer/src/runtime/web-session-tabs-sync-window-visibility.test.tsx": 1860, + "src/renderer/src/runtime/web-session-tabs-sync.test.ts": 647, + "src/renderer/src/runtime/web-session-tabs-sync/mirrored-agent-tab-label.test.ts": 369, + "src/renderer/src/runtime/web-session-tabs-sync/mirrored-status-tab-retractions.test.ts": 1373, + "src/renderer/src/runtime/web-session-tabs-sync/mirrored-terminal-recovery-ledger.test.ts": 360, + "src/renderer/src/runtime/web-session-tabs-sync/retraction-pane-ownership.test.ts": 1290, + "src/renderer/src/runtime/web-session-tabs-sync/state-equality-core.test.ts": 39, + "src/renderer/src/runtime/web-session-tabs-sync/tracking-lifecycle.test.ts": 1416, + "src/renderer/src/runtime/web-session-terminal-handle-events.test.ts": 37, + "src/renderer/src/runtime/web-session-terminal-orphan-absence-across-republication.test.ts": 596, + "src/renderer/src/runtime/web-session-terminal-orphan-inventory-retry.test.ts": 1589, + "src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts": 718, + "src/renderer/src/runtime/web-session-terminal-orphan-recovery-adoption-regressions.test.ts": 1017, + "src/renderer/src/runtime/web-session-terminal-orphan-recovery-host-scope-gate.test.ts": 612, + "src/renderer/src/runtime/web-session-terminal-orphan-recovery-prior-removal.test.ts": 560, + "src/renderer/src/runtime/web-session-terminal-orphan-recovery-regressions.test.ts": 859, + "src/renderer/src/runtime/web-session-terminal-orphan-recovery-topology-fence.test.ts": 682, + "src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts": 840, + "src/renderer/src/runtime/web-session-terminal-pending-handle-recovery.test.ts": 644, + "src/renderer/src/runtime/web-session-terminal-recovery-snapshot-validation.test.ts": 584, + "src/renderer/src/runtime/web-session-terminal-retirement-proof-ledger.test.ts": 591, + "src/renderer/src/runtime/window-visibility-subscription-parking-stream-end.test.ts": 204, + "src/renderer/src/runtime/window-visibility-subscription-parking.test.ts": 219, + "src/renderer/src/runtime/worktree-create-base.test.ts": 35, + "src/renderer/src/ssh/ssh-connect-in-flight.test.ts": 41, + "src/renderer/src/ssh/ssh-connect-ui-timeout.test.ts": 41, + "src/renderer/src/ssh/ssh-connect-verb.test.ts": 34, + "src/renderer/src/ssh/ssh-connection-recoverability.test.ts": 41, + "src/renderer/src/ssh/ssh-host-server-status-copy.test.ts": 1309, + "src/renderer/src/startup/active-workspace-ssh-targets.test.ts": 48, + "src/renderer/src/startup/ssh-startup-reconnect.test.ts": 32, + "src/renderer/src/startup/startup-degraded-recovery.test.ts": 474, + "src/renderer/src/startup/startup-ssh-connection-restore.test.ts": 71, + "src/renderer/src/store/active-terminal-chrome-selector.test.ts": 37, + "src/renderer/src/store/adopt-from-endpoint.test.ts": 333, + "src/renderer/src/store/always-mounted-selector-scan-cost.test.ts": 1506, "src/renderer/src/store/copy-on-write-record.test.ts": 46, - "src/renderer/src/store/folder-workspaces/folder-workspace-catalog.test.ts": 378, - "src/renderer/src/store/github/cache-persistence.test.ts": 1784, - "src/renderer/src/store/github/check-actions-errors.test.ts": 192, - "src/renderer/src/store/github/project-field-mutations.test.ts": 421, - "src/renderer/src/store/github/provider-request-coalescing.test.ts": 542, - "src/renderer/src/store/github/refresh-event-noop-notification.test.ts": 1347, + "src/renderer/src/store/folder-workspaces/folder-path-status-ownership.test.ts": 1443, + "src/renderer/src/store/folder-workspaces/folder-workspace-catalog.test.ts": 359, + "src/renderer/src/store/github/cache-persistence.test.ts": 1916, + "src/renderer/src/store/github/check-actions-errors.test.ts": 370, + "src/renderer/src/store/github/project-field-mutations.test.ts": 517, + "src/renderer/src/store/github/provider-request-coalescing.test.ts": 714, + "src/renderer/src/store/github/refresh-event-noop-notification.test.ts": 1250, + "src/renderer/src/store/github/visible-detached-review-refresh.test.ts": 434, "src/renderer/src/store/github/visible-hosted-review-refresh-scheduler.test.ts": 91, - "src/renderer/src/store/github/visible-hosted-review-refresh-targets.test.ts": 490, - "src/renderer/src/store/github/work-item-routing.test.ts": 281, - "src/renderer/src/store/host-catalog-fencing.test.ts": 75, + "src/renderer/src/store/github/visible-hosted-review-refresh-targets.test.ts": 588, + "src/renderer/src/store/github/work-item-routing.test.ts": 350, + "src/renderer/src/store/host-catalog-fencing.test.ts": 48, "src/renderer/src/store/pinned-tab-close-guard.test.ts": 53, - "src/renderer/src/store/plugin-language-packs-startup.test.ts": 375, - "src/renderer/src/store/plugin-language-packs.test.ts": 206, - "src/renderer/src/store/plugin-panel-retry-timer-cleanup.test.ts": 103, - "src/renderer/src/store/plugin-panels.test.ts": 145, - "src/renderer/src/store/project-host-setup-selector.test.ts": 76, - "src/renderer/src/store/projects/project-catalog-null-field-ingest.test.ts": 101, - "src/renderer/src/store/projects/project-wsl-filesystem-boundary-advisory.test.ts": 63, - "src/renderer/src/store/react-commit-cascade-write-probe.test.ts": 80, - "src/renderer/src/store/repos/converted-ssh-browser-pages.test.ts": 349, - "src/renderer/src/store/repos/converted-ssh-host-rows.test.ts": 60, - "src/renderer/src/store/repos/repo-removal.test.ts": 324, - "src/renderer/src/store/repos/safe-auto-fork-sync.test.ts": 320, - "src/renderer/src/store/right-sidebar-route.test.ts": 55, - "src/renderer/src/store/running-terminal-close-confirm.test.ts": 73, - "src/renderer/src/store/selectors.test.ts": 1496, - "src/renderer/src/store/settled-sort-epoch.test.ts": 1678, - "src/renderer/src/store/slices/active-selection-owner-scoping.test.ts": 1368, - "src/renderer/src/store/slices/active-tab-owner-worktree.test.ts": 70, - "src/renderer/src/store/slices/activity-cleared-at.test.ts": 1529, - "src/renderer/src/store/slices/agent-generated-tab-title.test.ts": 1429, - "src/renderer/src/store/slices/agent-hibernation-live-anchor-e2e.test.ts": 1457, - "src/renderer/src/store/slices/agent-pane-authority.test.ts": 1460, - "src/renderer/src/store/slices/agent-status-ack-cleanup.test.ts": 1469, - "src/renderer/src/store/slices/agent-status-batch.test.ts": 1496, - "src/renderer/src/store/slices/agent-status-cursor-checkpoint.test.ts": 1418, - "src/renderer/src/store/slices/agent-status-drop-ipc.test.ts": 1802, - "src/renderer/src/store/slices/agent-status-freshness-cache.test.ts": 68, - "src/renderer/src/store/slices/agent-status-freshness-scheduler.test.ts": 63, - "src/renderer/src/store/slices/agent-status-live-freshness-request.test.ts": 143, - "src/renderer/src/store/slices/agent-status-live-map-leak.test.ts": 7374, - "src/renderer/src/store/slices/agent-status-main-agent-fact.test.ts": 1387, - "src/renderer/src/store/slices/agent-status-manual-sleep-capture.test.ts": 1473, - "src/renderer/src/store/slices/agent-status-pane-keyed-records.test.ts": 84, - "src/renderer/src/store/slices/agent-status-pr-refresh-handoff.test.ts": 1504, - "src/renderer/src/store/slices/agent-status-provider-session.test.ts": 1961, - "src/renderer/src/store/slices/agent-status-quit-capture-resumable-agents.test.ts": 1365, - "src/renderer/src/store/slices/agent-status-quit-capture.test.ts": 1559, - "src/renderer/src/store/slices/agent-status-reminted-pane-key.test.ts": 1580, - "src/renderer/src/store/slices/agent-status-retention-prefix-sweep.test.ts": 1850, - "src/renderer/src/store/slices/agent-status-runtime-orchestration.test.ts": 1464, - "src/renderer/src/store/slices/agent-status-session-boundary-done.test.ts": 1563, - "src/renderer/src/store/slices/agent-status-ssh-connection-clear.test.ts": 1659, - "src/renderer/src/store/slices/agent-status-tool-assistant-fields.test.ts": 1413, - "src/renderer/src/store/slices/agent-status-turn-started-at.test.ts": 1312, - "src/renderer/src/store/slices/agent-status-verdict-copies.test.ts": 431, - "src/renderer/src/store/slices/agent-status-worktree-purge-leak.test.ts": 1377, - "src/renderer/src/store/slices/agent-status.test.ts": 1842, - "src/renderer/src/store/slices/ambiguous-owner-warning-worktree-removal-leak.test.ts": 1533, - "src/renderer/src/store/slices/browser-annotation-retention.test.ts": 603, - "src/renderer/src/store/slices/browser-cleanup-close.test.ts": 1587, - "src/renderer/src/store/slices/browser-page-close-intent-recording.test.ts": 1828, - "src/renderer/src/store/slices/browser-page-conversion.test.ts": 1558, - "src/renderer/src/store/slices/browser-remote-page-lifecycle.test.ts": 671, - "src/renderer/src/store/slices/browser-remote-tab-creation.test.ts": 384, - "src/renderer/src/store/slices/browser-session-host-selection.test.ts": 672, + "src/renderer/src/store/plugin-language-packs-startup.test.ts": 386, + "src/renderer/src/store/plugin-language-packs.test.ts": 227, + "src/renderer/src/store/plugin-panel-retry-timer-cleanup.test.ts": 69, + "src/renderer/src/store/plugin-panels.test.ts": 183, + "src/renderer/src/store/project-host-setup-selector.test.ts": 52, + "src/renderer/src/store/projects/project-catalog-null-field-ingest.test.ts": 267, + "src/renderer/src/store/projects/project-wsl-filesystem-boundary-advisory.test.ts": 38, + "src/renderer/src/store/react-commit-cascade-write-probe.test.ts": 36, + "src/renderer/src/store/repos/converted-ssh-browser-pages.test.ts": 340, + "src/renderer/src/store/repos/converted-ssh-host-rows.test.ts": 53, + "src/renderer/src/store/repos/repo-removal.test.ts": 350, + "src/renderer/src/store/repos/safe-auto-fork-sync.test.ts": 302, + "src/renderer/src/store/right-sidebar-route.test.ts": 35, + "src/renderer/src/store/running-terminal-close-confirm.test.ts": 54, + "src/renderer/src/store/selectors.test.ts": 1437, + "src/renderer/src/store/settled-sort-epoch.test.ts": 1447, + "src/renderer/src/store/slices/active-selection-owner-scoping.test.ts": 1270, + "src/renderer/src/store/slices/active-tab-owner-worktree.test.ts": 54, + "src/renderer/src/store/slices/activity-cleared-at.test.ts": 1386, + "src/renderer/src/store/slices/agent-generated-tab-title.test.ts": 1238, + "src/renderer/src/store/slices/agent-hibernation-live-anchor-e2e.test.ts": 1265, + "src/renderer/src/store/slices/agent-pane-authority.test.ts": 1446, + "src/renderer/src/store/slices/agent-status-ack-cleanup.test.ts": 1309, + "src/renderer/src/store/slices/agent-status-batch.test.ts": 1403, + "src/renderer/src/store/slices/agent-status-cursor-checkpoint.test.ts": 1220, + "src/renderer/src/store/slices/agent-status-drop-ipc.test.ts": 1690, + "src/renderer/src/store/slices/agent-status-freshness-cache.test.ts": 82, + "src/renderer/src/store/slices/agent-status-freshness-scheduler.test.ts": 51, + "src/renderer/src/store/slices/agent-status-live-freshness-request.test.ts": 146, + "src/renderer/src/store/slices/agent-status-live-map-leak.test.ts": 2819, + "src/renderer/src/store/slices/agent-status-main-agent-fact.test.ts": 1531, + "src/renderer/src/store/slices/agent-status-manual-sleep-capture.test.ts": 1409, + "src/renderer/src/store/slices/agent-status-pane-keyed-records.test.ts": 56, + "src/renderer/src/store/slices/agent-status-pr-refresh-handoff.test.ts": 1240, + "src/renderer/src/store/slices/agent-status-provider-session.test.ts": 1400, + "src/renderer/src/store/slices/agent-status-quit-capture-resumable-agents.test.ts": 1480, + "src/renderer/src/store/slices/agent-status-quit-capture.test.ts": 1480, + "src/renderer/src/store/slices/agent-status-remember-sleeping-session.test.ts": 1112, + "src/renderer/src/store/slices/agent-status-reminted-pane-key.test.ts": 1368, + "src/renderer/src/store/slices/agent-status-retention-prefix-sweep.test.ts": 1328, + "src/renderer/src/store/slices/agent-status-runtime-orchestration.test.ts": 1514, + "src/renderer/src/store/slices/agent-status-session-boundary-done.test.ts": 1186, + "src/renderer/src/store/slices/agent-status-ssh-connection-clear.test.ts": 1400, + "src/renderer/src/store/slices/agent-status-tool-assistant-fields.test.ts": 1278, + "src/renderer/src/store/slices/agent-status-turn-started-at.test.ts": 1283, + "src/renderer/src/store/slices/agent-status-verdict-copies.test.ts": 473, + "src/renderer/src/store/slices/agent-status-worktree-purge-leak.test.ts": 1148, + "src/renderer/src/store/slices/agent-status.test.ts": 1659, + "src/renderer/src/store/slices/all-host-catalog-settlement.test.ts": 1126, + "src/renderer/src/store/slices/ambiguous-owner-warning-worktree-removal-leak.test.ts": 1596, + "src/renderer/src/store/slices/browser-annotation-retention.test.ts": 551, + "src/renderer/src/store/slices/browser-cleanup-close.test.ts": 1410, + "src/renderer/src/store/slices/browser-mount-admission-removal.test.ts": 1413, + "src/renderer/src/store/slices/browser-page-close-intent-recording.test.ts": 1433, + "src/renderer/src/store/slices/browser-page-conversion.test.ts": 1342, + "src/renderer/src/store/slices/browser-remote-page-lifecycle.test.ts": 651, + "src/renderer/src/store/slices/browser-remote-tab-creation.test.ts": 392, + "src/renderer/src/store/slices/browser-session-host-selection.test.ts": 579, "src/renderer/src/store/slices/browser-session-profiles.test.ts": 434, - "src/renderer/src/store/slices/browser-tab-creation-placement.test.ts": 1348, - "src/renderer/src/store/slices/browser-webview-cleanup.test.ts": 76, - "src/renderer/src/store/slices/browser-workspace-doc-location.test.ts": 1525, - "src/renderer/src/store/slices/browser-workspace-host-ownership.test.ts": 441, - "src/renderer/src/store/slices/browser.test.ts": 560, - "src/renderer/src/store/slices/bulk-worktree-purge-terminal-maps-leak.test.ts": 1419, - "src/renderer/src/store/slices/cmd-j-create-actions.test.ts": 1303, - "src/renderer/src/store/slices/codex-restart-notice-lifecycle.test.ts": 1446, - "src/renderer/src/store/slices/degraded-repo-hydration.test.ts": 1498, - "src/renderer/src/store/slices/detected-agents-environment-prune-leak.test.ts": 1505, - "src/renderer/src/store/slices/detected-agents.test.ts": 482, - "src/renderer/src/store/slices/detected-worktree-refresh-leases.test.ts": 61, - "src/renderer/src/store/slices/dictation-model-state-stabilisation.test.ts": 179, - "src/renderer/src/store/slices/diffComments.test.ts": 1533, - "src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts": 1416, - "src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts": 86, - "src/renderer/src/store/slices/editor-branch-diff-snapshots.test.ts": 765, - "src/renderer/src/store/slices/editor-branch-line-total.test.ts": 637, - "src/renderer/src/store/slices/editor-chat-visual-tabs.test.ts": 700, - "src/renderer/src/store/slices/editor-check-details-tabs.test.ts": 745, - "src/renderer/src/store/slices/editor-close-document-siblings.test.ts": 1085, - "src/renderer/src/store/slices/editor-close-file-cleanup.test.ts": 1090, - "src/renderer/src/store/slices/editor-close-file-stale-active.test.ts": 1353, - "src/renderer/src/store/slices/editor-close-workspace-selection.test.ts": 784, - "src/renderer/src/store/slices/editor-git-status-reconciliation.test.ts": 712, - "src/renderer/src/store/slices/editor-hydration-scaling.test.ts": 1448, - "src/renderer/src/store/slices/editor-markdown-link-activation.test.ts": 803, - "src/renderer/src/store/slices/editor-markdown-view-state.test.ts": 756, - "src/renderer/src/store/slices/editor-open-diff.test.ts": 831, - "src/renderer/src/store/slices/editor-preview-tab-setting.test.ts": 735, - "src/renderer/src/store/slices/editor-read-only-tabs.test.ts": 625, - "src/renderer/src/store/slices/editor-recently-closed-tabs.test.ts": 882, - "src/renderer/src/store/slices/editor-rekey-open-files.test.ts": 1416, - "src/renderer/src/store/slices/editor-remote-branch-actions.test.ts": 766, - "src/renderer/src/store/slices/editor-rich-markdown-size-override.test.ts": 649, - "src/renderer/src/store/slices/editor-right-sidebar-state.test.ts": 860, - "src/renderer/src/store/slices/editor-session-duplicate-restore.test.ts": 1413, - "src/renderer/src/store/slices/editor-state-worktree-purge-leak.test.ts": 1441, - "src/renderer/src/store/slices/editor-tab-host-ownership.test.ts": 651, - "src/renderer/src/store/slices/editor-tab-placement.test.ts": 729, - "src/renderer/src/store/slices/editor/actions/editor-cursor-line.test.ts": 667, - "src/renderer/src/store/slices/editor/actions/editor-draft-state.test.ts": 645, - "src/renderer/src/store/slices/editor/actions/markdown-preview-actions.test.ts": 294, - "src/renderer/src/store/slices/editor/file-ids/hydrated-editor-file-index.test.ts": 484, - "src/renderer/src/store/slices/editor/file-ids/hydrated-editor-projections.test.ts": 451, - "src/renderer/src/store/slices/editor/git/git-status-reconciliation.perf.test.ts": 84, - "src/renderer/src/store/slices/empty-update-notifications.test.ts": 1428, - "src/renderer/src/store/slices/floating-workspace-row.test.ts": 1455, - "src/renderer/src/store/slices/folder-workspace-activation-and-activity.test.ts": 1470, - "src/renderer/src/store/slices/folder-workspace-diff-comments.test.ts": 809, - "src/renderer/src/store/slices/folder-workspace-owner-routed-mutations.test.ts": 1733, - "src/renderer/src/store/slices/generation-records-worktree-removal-leak.test.ts": 1460, - "src/renderer/src/store/slices/github-branch-mismatched-linked-pr.test.ts": 229, - "src/renderer/src/store/slices/github-cache-eviction-and-bounds.test.ts": 330, - "src/renderer/src/store/slices/github-checks-cache.test.ts": 235, - "src/renderer/src/store/slices/github-checks.test.ts": 56, - "src/renderer/src/store/slices/github-issue-source-indicator-suppression.test.ts": 252, - "src/renderer/src/store/slices/github-issue-state-machine.test.ts": 224, - "src/renderer/src/store/slices/github-pr-branch-coordinator-events.test.ts": 247, - "src/renderer/src/store/slices/github-pr-branch-direct-refresh-scope.test.ts": 228, - "src/renderer/src/store/slices/github-pr-branch-fallback-results.test.ts": 235, - "src/renderer/src/store/slices/github-pr-branch-hosted-review-cache.test.ts": 234, - "src/renderer/src/store/slices/github-pr-branch-linked-pr-divergence.test.ts": 248, - "src/renderer/src/store/slices/github-pr-checks-fetch.test.ts": 292, - "src/renderer/src/store/slices/github-pr-comments.test.ts": 233, - "src/renderer/src/store/slices/github-pr-refresh-host-guard.test.ts": 202, - "src/renderer/src/store/slices/github-pr-refresh-hosted-review-cache-leak.test.ts": 362, - "src/renderer/src/store/slices/github-pr-refresh-owner-routing.test.ts": 364, - "src/renderer/src/store/slices/github-pr-refresh-sequences-leak.test.ts": 230, - "src/renderer/src/store/slices/github-pr-refresh-states-leak.test.ts": 903, - "src/renderer/src/store/slices/github-pr-request-lifetime.test.ts": 713, - "src/renderer/src/store/slices/github-project-request-coordination.test.ts": 233, - "src/renderer/src/store/slices/github-project-row-owner.test.ts": 53, - "src/renderer/src/store/slices/github-project-view-tables.test.ts": 237, - "src/renderer/src/store/slices/github-provider-request-concurrency.test.ts": 206, - "src/renderer/src/store/slices/github-refresh-sweep.test.ts": 348, - "src/renderer/src/store/slices/github-repo-lookup-index.test.ts": 47, - "src/renderer/src/store/slices/github-review-thread-actions.test.ts": 217, - "src/renderer/src/store/slices/github-work-item-cache-identity.test.ts": 204, - "src/renderer/src/store/slices/github-work-items-error-envelope.test.ts": 1787, - "src/renderer/src/store/slices/github-work-items-pagination.test.ts": 1619, - "src/renderer/src/store/slices/github-work-items-runtime-routing.test.ts": 1676, - "src/renderer/src/store/slices/github-worktree-refresh-if-stale.test.ts": 272, - "src/renderer/src/store/slices/hidden-ssh-host-session-round-trip.test.ts": 1423, - "src/renderer/src/store/slices/hosted-review-cache-race.test.ts": 76, - "src/renderer/src/store/slices/hosted-review-cache.test.ts": 130, + "src/renderer/src/store/slices/browser-tab-creation-placement.test.ts": 1269, + "src/renderer/src/store/slices/browser-webview-cleanup.test.ts": 44, + "src/renderer/src/store/slices/browser-workspace-doc-location.test.ts": 1517, + "src/renderer/src/store/slices/browser-workspace-host-ownership.test.ts": 361, + "src/renderer/src/store/slices/browser.test.ts": 471, + "src/renderer/src/store/slices/bulk-worktree-purge-terminal-maps-leak.test.ts": 1409, + "src/renderer/src/store/slices/cmd-j-create-actions.test.ts": 1251, + "src/renderer/src/store/slices/codex-restart-notice-lifecycle.test.ts": 1404, + "src/renderer/src/store/slices/degraded-repo-hydration.test.ts": 1346, + "src/renderer/src/store/slices/detected-agents-environment-prune-leak.test.ts": 1362, + "src/renderer/src/store/slices/detected-agents.test.ts": 499, + "src/renderer/src/store/slices/detected-worktree-refresh-leases.test.ts": 44, + "src/renderer/src/store/slices/dictation-model-state-stabilisation.test.ts": 170, + "src/renderer/src/store/slices/diffComments.test.ts": 1391, + "src/renderer/src/store/slices/direct-ssh-terminal-recovery.test.ts": 1476, + "src/renderer/src/store/slices/direct-ssh-terminal-workspace-scope.test.ts": 62, + "src/renderer/src/store/slices/editor-branch-diff-snapshots.test.ts": 649, + "src/renderer/src/store/slices/editor-branch-line-total.test.ts": 517, + "src/renderer/src/store/slices/editor-check-details-tabs.test.ts": 633, + "src/renderer/src/store/slices/editor-close-document-siblings.test.ts": 1036, + "src/renderer/src/store/slices/editor-close-file-cleanup.test.ts": 929, + "src/renderer/src/store/slices/editor-close-file-stale-active.test.ts": 1310, + "src/renderer/src/store/slices/editor-close-pinned-sibling.test.ts": 761, + "src/renderer/src/store/slices/editor-close-workspace-selection.test.ts": 665, + "src/renderer/src/store/slices/editor-git-status-reconciliation.test.ts": 611, + "src/renderer/src/store/slices/editor-hydration-scaling.test.ts": 1329, + "src/renderer/src/store/slices/editor-markdown-link-activation.test.ts": 787, + "src/renderer/src/store/slices/editor-markdown-view-state.test.ts": 672, + "src/renderer/src/store/slices/editor-open-diff.test.ts": 724, + "src/renderer/src/store/slices/editor-preview-tab-setting.test.ts": 808, + "src/renderer/src/store/slices/editor-read-only-tabs.test.ts": 798, + "src/renderer/src/store/slices/editor-recently-closed-tabs.test.ts": 774, + "src/renderer/src/store/slices/editor-rekey-open-files.test.ts": 1250, + "src/renderer/src/store/slices/editor-remote-branch-actions.test.ts": 741, + "src/renderer/src/store/slices/editor-rich-markdown-size-override.test.ts": 727, + "src/renderer/src/store/slices/editor-right-sidebar-state.test.ts": 811, + "src/renderer/src/store/slices/editor-session-duplicate-restore.test.ts": 1351, + "src/renderer/src/store/slices/editor-set-active-file-guard.test.ts": 748, + "src/renderer/src/store/slices/editor-state-worktree-purge-leak.test.ts": 1337, + "src/renderer/src/store/slices/editor-tab-host-ownership.test.ts": 806, + "src/renderer/src/store/slices/editor-tab-placement.test.ts": 775, + "src/renderer/src/store/slices/editor/actions/editor-cursor-line.test.ts": 692, + "src/renderer/src/store/slices/editor/actions/editor-draft-state.test.ts": 885, + "src/renderer/src/store/slices/editor/actions/markdown-preview-actions.test.ts": 273, + "src/renderer/src/store/slices/editor/file-ids/hydrated-editor-file-index.test.ts": 470, + "src/renderer/src/store/slices/editor/file-ids/hydrated-editor-projections.test.ts": 454, + "src/renderer/src/store/slices/editor/git/git-status-reconciliation.perf.test.ts": 65, + "src/renderer/src/store/slices/empty-update-notifications.test.ts": 1242, + "src/renderer/src/store/slices/floating-workspace-row.test.ts": 1445, + "src/renderer/src/store/slices/folder-workspace-activation-and-activity.test.ts": 1473, + "src/renderer/src/store/slices/folder-workspace-diff-comments.test.ts": 795, + "src/renderer/src/store/slices/folder-workspace-owner-routed-mutations.test.ts": 1467, + "src/renderer/src/store/slices/generation-records-worktree-removal-leak.test.ts": 1363, + "src/renderer/src/store/slices/github-branch-mismatched-linked-pr.test.ts": 346, + "src/renderer/src/store/slices/github-cache-eviction-and-bounds.test.ts": 568, + "src/renderer/src/store/slices/github-checks-cache.test.ts": 380, + "src/renderer/src/store/slices/github-checks.test.ts": 52, + "src/renderer/src/store/slices/github-issue-source-indicator-suppression.test.ts": 325, + "src/renderer/src/store/slices/github-issue-state-machine.test.ts": 399, + "src/renderer/src/store/slices/github-pr-branch-coordinator-events.test.ts": 345, + "src/renderer/src/store/slices/github-pr-branch-direct-refresh-scope.test.ts": 353, + "src/renderer/src/store/slices/github-pr-branch-fallback-results.test.ts": 398, + "src/renderer/src/store/slices/github-pr-branch-hosted-review-cache.test.ts": 385, + "src/renderer/src/store/slices/github-pr-branch-linked-pr-divergence.test.ts": 363, + "src/renderer/src/store/slices/github-pr-checks-fetch.test.ts": 453, + "src/renderer/src/store/slices/github-pr-comments.test.ts": 427, + "src/renderer/src/store/slices/github-pr-refresh-host-guard.test.ts": 285, + "src/renderer/src/store/slices/github-pr-refresh-hosted-review-cache-leak.test.ts": 519, + "src/renderer/src/store/slices/github-pr-refresh-owner-routing.test.ts": 539, + "src/renderer/src/store/slices/github-pr-refresh-sequences-leak.test.ts": 378, + "src/renderer/src/store/slices/github-pr-refresh-states-leak.test.ts": 985, + "src/renderer/src/store/slices/github-pr-rejected-cache.test.ts": 258, + "src/renderer/src/store/slices/github-pr-request-lifetime.test.ts": 904, + "src/renderer/src/store/slices/github-project-request-coordination.test.ts": 320, + "src/renderer/src/store/slices/github-project-row-owner.test.ts": 31, + "src/renderer/src/store/slices/github-project-view-tables.test.ts": 372, + "src/renderer/src/store/slices/github-provider-request-concurrency.test.ts": 334, + "src/renderer/src/store/slices/github-refresh-sweep.test.ts": 507, + "src/renderer/src/store/slices/github-repo-lookup-index.test.ts": 37, + "src/renderer/src/store/slices/github-review-thread-actions.test.ts": 407, + "src/renderer/src/store/slices/github-work-item-cache-identity.test.ts": 407, + "src/renderer/src/store/slices/github-work-items-error-envelope.test.ts": 1388, + "src/renderer/src/store/slices/github-work-items-pagination.test.ts": 1357, + "src/renderer/src/store/slices/github-work-items-runtime-routing.test.ts": 1485, + "src/renderer/src/store/slices/github-worktree-refresh-if-stale.test.ts": 431, + "src/renderer/src/store/slices/hidden-ssh-host-session-round-trip.test.ts": 1388, + "src/renderer/src/store/slices/hosted-review-cache-race.test.ts": 62, + "src/renderer/src/store/slices/hosted-review-cache.test.ts": 109, "src/renderer/src/store/slices/hosted-review-request-lifetime.test.ts": 89, - "src/renderer/src/store/slices/hosted-review.test.ts": 130, - "src/renderer/src/store/slices/jira-connection-identity.test.ts": 90, - "src/renderer/src/store/slices/jira-web-client-status.test.ts": 126, - "src/renderer/src/store/slices/jira.test.ts": 166, - "src/renderer/src/store/slices/linear-credential-error-recovery.test.ts": 189, + "src/renderer/src/store/slices/hosted-review.test.ts": 77, + "src/renderer/src/store/slices/jira-connection-identity.test.ts": 93, + "src/renderer/src/store/slices/jira-issue-patch-action.test.ts": 1116, + "src/renderer/src/store/slices/jira-sprint-filter-cache.test.ts": 100, + "src/renderer/src/store/slices/jira-status-filter-cache.test.ts": 89, + "src/renderer/src/store/slices/jira-web-client-status.test.ts": 275, + "src/renderer/src/store/slices/jira.test.ts": 178, + "src/renderer/src/store/slices/linear-credential-error-recovery.test.ts": 326, "src/renderer/src/store/slices/linear-invalidation.test.ts": 142, - "src/renderer/src/store/slices/linear-issue-cache-refresh.test.ts": 177, - "src/renderer/src/store/slices/linear-scoped-collection-cache.test.ts": 184, - "src/renderer/src/store/slices/linear-source-context-cache-scope.test.ts": 200, - "src/renderer/src/store/slices/linear.test.ts": 168, - "src/renderer/src/store/slices/local-detected-agent-state.test.ts": 357, - "src/renderer/src/store/slices/memory.test.ts": 58, - "src/renderer/src/store/slices/native-chat-launch-draft-teardown.test.ts": 1473, - "src/renderer/src/store/slices/new-issue-draft.test.ts": 50, - "src/renderer/src/store/slices/new-markdown.test.ts": 654, - "src/renderer/src/store/slices/orca-profiles-auth-actions-connect-overlap.test.ts": 1439, - "src/renderer/src/store/slices/orca-profiles-auth-actions.test.ts": 1418, - "src/renderer/src/store/slices/orca-profiles.test.ts": 1424, - "src/renderer/src/store/slices/pane-column-split-drop-no-op.test.ts": 46, - "src/renderer/src/store/slices/persisted-ui-write-baseline.test.ts": 57, - "src/renderer/src/store/slices/pinned-tab-close-confirm.test.ts": 69, - "src/renderer/src/store/slices/preflight.test.ts": 347, - "src/renderer/src/store/slices/project-group-removal-targets.test.ts": 69, - "src/renderer/src/store/slices/project-removal-owner-host.test.ts": 1320, - "src/renderer/src/store/slices/purge-stale-runtime-host-groups.test.ts": 1389, - "src/renderer/src/store/slices/purge-stale-runtime-host-ownership.test.ts": 1523, - "src/renderer/src/store/slices/purge-stale-runtime-host-state.test.ts": 1413, - "src/renderer/src/store/slices/readopted-ssh-worktree-rows.test.ts": 49, - "src/renderer/src/store/slices/recently-closed-tabs.test.ts": 1467, - "src/renderer/src/store/slices/remote-server-updates.integration.test.ts": 305, - "src/renderer/src/store/slices/repo-identity-reconcile.test.ts": 72, - "src/renderer/src/store/slices/repo-owner-cache-identity.test.ts": 53, - "src/renderer/src/store/slices/repo-reorder-host-split.test.ts": 49, - "src/renderer/src/store/slices/repos-add-races.test.ts": 1465, - "src/renderer/src/store/slices/repos-all-hosts-folder-workspaces.test.ts": 1523, - "src/renderer/src/store/slices/repos-all-hosts-generation.test.ts": 1480, - "src/renderer/src/store/slices/repos-all-hosts.test.ts": 1476, - "src/renderer/src/store/slices/repos-catalog-connection-fence.test.ts": 1552, - "src/renderer/src/store/slices/repos-catalog-merge-identity.test.ts": 1544, - "src/renderer/src/store/slices/repos-catalog-notifications.test.ts": 1367, - "src/renderer/src/store/slices/repos-cross-host-project-collisions.test.ts": 1355, - "src/renderer/src/store/slices/repos-cross-host-refresh-identity.test.ts": 1448, - "src/renderer/src/store/slices/repos-ephemeral-vm-cleanup-retention.test.ts": 2007, - "src/renderer/src/store/slices/repos-host-identity-routing.test.ts": 1747, - "src/renderer/src/store/slices/repos-manual-order-hydration.test.ts": 1398, - "src/renderer/src/store/slices/repos-module-lifetime-coordinators.test.ts": 1925, - "src/renderer/src/store/slices/repos-nested-import-refresh-failures.test.ts": 1395, - "src/renderer/src/store/slices/repos-nested-ssh-projection.test.ts": 1383, - "src/renderer/src/store/slices/repos-onboarding-folder-startup.test.ts": 1708, - "src/renderer/src/store/slices/repos-paired-runtime-add.test.ts": 1444, - "src/renderer/src/store/slices/repos-project-group-create-race.test.ts": 1513, - "src/renderer/src/store/slices/repos-project-groups-delete.test.ts": 1217, - "src/renderer/src/store/slices/repos-project-groups-owner-routing.test.ts": 1410, - "src/renderer/src/store/slices/repos-project-groups.test.ts": 1564, - "src/renderer/src/store/slices/repos-project-host-capability.test.ts": 1159, - "src/renderer/src/store/slices/repos-project-host-lifecycle.test.ts": 2194, - "src/renderer/src/store/slices/repos-project-runtime.test.ts": 1478, - "src/renderer/src/store/slices/repos-refresh-identity.test.ts": 1791, - "src/renderer/src/store/slices/repos-remove-missing-remote-project.test.ts": 1545, - "src/renderer/src/store/slices/repos-remove-project-purge-leak.test.ts": 1355, - "src/renderer/src/store/slices/repos-runtime-project-groups.test.ts": 1437, - "src/renderer/src/store/slices/repos-runtime-visibility-defaults.test.ts": 1554, - "src/renderer/src/store/slices/repos-selected-owner-routing.test.ts": 1507, - "src/renderer/src/store/slices/repos-setup-script-dismissals.test.ts": 1336, - "src/renderer/src/store/slices/repos-shared-project-badge-color.test.ts": 1400, - "src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts": 1557, - "src/renderer/src/store/slices/repos-stale-fetch.test.ts": 1405, - "src/renderer/src/store/slices/repos-update-serialization.test.ts": 1554, - "src/renderer/src/store/slices/repos.runtime-fallback.test.ts": 1456, - "src/renderer/src/store/slices/repos.test.ts": 1736, - "src/renderer/src/store/slices/restored-editor-owner-reparent.test.ts": 1695, - "src/renderer/src/store/slices/runtime-catalog-merge-removed-env-guard.test.ts": 1585, - "src/renderer/src/store/slices/runtime-detected-agents.test.ts": 1466, - "src/renderer/src/store/slices/runtime-environment-peer-replacement.test.ts": 47, - "src/renderer/src/store/slices/runtime-environment-ssh.test.ts": 1392, - "src/renderer/src/store/slices/runtime-host-purge-session-partition-split.test.ts": 1569, - "src/renderer/src/store/slices/runtime-managed-repair-keeps-workspaces.test.ts": 1477, - "src/renderer/src/store/slices/runtime-pane-title-sort-epoch.test.ts": 1397, - "src/renderer/src/store/slices/runtime-status-catalog-identity.test.ts": 1377, - "src/renderer/src/store/slices/runtime-status-first-publication.test.ts": 340, - "src/renderer/src/store/slices/runtime-status-reconnect-connection-generation.test.ts": 1417, - "src/renderer/src/store/slices/runtime-status-refresh-diagnostics.test.ts": 83, - "src/renderer/src/store/slices/runtime-status-restored-browser-host-attach.test.ts": 158, - "src/renderer/src/store/slices/runtime-status-skill-cache-eviction.test.ts": 1723, - "src/renderer/src/store/slices/runtime-status-snapshot.test.ts": 194, - "src/renderer/src/store/slices/runtime-status.test.ts": 497, - "src/renderer/src/store/slices/runtime-switch-settings-persistence.test.ts": 1350, - "src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts": 1402, - "src/renderer/src/store/slices/set-runtime-environments-purge-wiring.test.ts": 1800, - "src/renderer/src/store/slices/settings-owner-hydration-write-fence.test.ts": 1615, - "src/renderer/src/store/slices/settings-search-state.test.ts": 57, - "src/renderer/src/store/slices/settings.test.ts": 1698, - "src/renderer/src/store/slices/sparse-presets-repo-removal-purge-leak.test.ts": 1627, - "src/renderer/src/store/slices/sparse-presets.test.ts": 93, - "src/renderer/src/store/slices/ssh-target-cleanup.test.ts": 1693, - "src/renderer/src/store/slices/ssh.test.ts": 1441, - "src/renderer/src/store/slices/stale-runtime-host-rows.test.ts": 59, - "src/renderer/src/store/slices/store-active-worktree-selection.test.ts": 1522, - "src/renderer/src/store/slices/store-active-worktree-split-groups.test.ts": 1521, - "src/renderer/src/store/slices/store-active-worktree-tab-close.test.ts": 1491, - "src/renderer/src/store/slices/store-active-worktree-terminal-creation.test.ts": 1552, - "src/renderer/src/store/slices/store-create-tab-id-hint.test.ts": 1663, - "src/renderer/src/store/slices/store-session-browser-hydration.test.ts": 1483, - "src/renderer/src/store/slices/store-session-cascades.test.ts": 1360, - "src/renderer/src/store/slices/store-session-editor-hydration.test.ts": 1343, - "src/renderer/src/store/slices/store-session-terminal-activity.test.ts": 1448, - "src/renderer/src/store/slices/store-session-terminal-reconnect.test.ts": 1555, - "src/renderer/src/store/slices/store-session-workspace-hydration.test.ts": 1701, - "src/renderer/src/store/slices/store-sleep-agent-status-retention.test.ts": 2125, - "src/renderer/src/store/slices/store-sleep-exact-runtime-stop.test.ts": 1412, - "src/renderer/src/store/slices/store-sleep-pane-hibernation.test.ts": 1442, - "src/renderer/src/store/slices/store-sleep-runtime-convergence.test.ts": 1758, - "src/renderer/src/store/slices/store-sleep-shutdown-order.test.ts": 1456, - "src/renderer/src/store/slices/store-worktree-removal-cascade.test.ts": 1459, - "src/renderer/src/store/slices/superseded-ssh-repo-rows.test.ts": 54, - "src/renderer/src/store/slices/tab-group-reference-repair.test.ts": 74, - "src/renderer/src/store/slices/tab-group-state.test.ts": 69, - "src/renderer/src/store/slices/tab-view-mode.test.ts": 1235, - "src/renderer/src/store/slices/tab-worktree-orphan-map-purge-leak.test.ts": 1589, - "src/renderer/src/store/slices/tabs-close-dangling-group.test.ts": 1520, - "src/renderer/src/store/slices/tabs-hydration-generated-title.test.ts": 67, - "src/renderer/src/store/slices/tabs-hydration-group-validation.test.ts": 59, - "src/renderer/src/store/slices/tabs-hydration.test.ts": 68, - "src/renderer/src/store/slices/tabs-label-and-pin-state.test.ts": 1510, - "src/renderer/src/store/slices/tabs-model-reconciliation.test.ts": 1681, - "src/renderer/src/store/slices/tabs-open-close-lifecycle.test.ts": 1560, - "src/renderer/src/store/slices/tabs-pane-layout-operations.test.ts": 1393, - "src/renderer/src/store/slices/tabs-session-hydration.test.ts": 1813, - "src/renderer/src/store/slices/tabs-unread-and-focus.test.ts": 1566, - "src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts": 1722, - "src/renderer/src/store/slices/tabs/structured-chat-close-owner.test.ts": 1367, - "src/renderer/src/store/slices/tabs/tab-selection-contract.test.ts": 67, - "src/renderer/src/store/slices/tabs/tabs-reconciliation-batch-identity.test.ts": 1401, - "src/renderer/src/store/slices/task-creation-drafts.test.ts": 45, - "src/renderer/src/store/slices/terminal-helpers.test.ts": 91, - "src/renderer/src/store/slices/terminal-input-activity-store-write.test.ts": 1964, - "src/renderer/src/store/slices/terminal-layout-pty-ownership.test.ts": 1509, - "src/renderer/src/store/slices/terminal-orphan-helpers.test.ts": 393, - "src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts": 1381, - "src/renderer/src/store/slices/terminal-pane-detach-agent-retention.test.ts": 1758, - "src/renderer/src/store/slices/terminal-pane-retirement-ownership.test.ts": 420, - "src/renderer/src/store/slices/terminal-pty-identity-replacement.test.ts": 1511, - "src/renderer/src/store/slices/terminal-quick-command-hosts.test.ts": 1285, - "src/renderer/src/store/slices/terminal-startup-command-retention.test.ts": 2880, - "src/renderer/src/store/slices/terminal-tab-id-hydration.test.ts": 1368, - "src/renderer/src/store/slices/terminal-tab-owner-index.test.ts": 57, - "src/renderer/src/store/slices/terminal-tab-recovery-remount.test.ts": 1395, - "src/renderer/src/store/slices/terminal-tab-retirement-store.test.ts": 1489, - "src/renderer/src/store/slices/terminal-tab-retirement.test.ts": 446, - "src/renderer/src/store/slices/terminal-tab-title-batch.test.ts": 1406, - "src/renderer/src/store/slices/terminals-explicit-empty-hydration.test.ts": 1342, - "src/renderer/src/store/slices/terminals-hydration-canonical-pty-overlap.test.ts": 1470, - "src/renderer/src/store/slices/terminals-hydration-canonical-rows.test.ts": 1346, - "src/renderer/src/store/slices/terminals-hydration.test.ts": 1435, - "src/renderer/src/store/slices/ui-acknowledge-agents-clock-skew.test.ts": 411, - "src/renderer/src/store/slices/ui-agent-send-target.test.ts": 589, - "src/renderer/src/store/slices/ui-contextual-tours.test.ts": 556, - "src/renderer/src/store/slices/ui-feature-interactions.test.ts": 660, - "src/renderer/src/store/slices/ui-host-filter-sync.test.ts": 485, - "src/renderer/src/store/slices/ui-hydration-view-layout.test.ts": 618, - "src/renderer/src/store/slices/ui-hydration-workspace-cleanup-browse.test.ts": 426, - "src/renderer/src/store/slices/ui-hydration-workspace-preferences.test.ts": 520, - "src/renderer/src/store/slices/ui-modal-slot-dismissal.test.ts": 507, - "src/renderer/src/store/slices/ui-notice-dismissals.test.ts": 536, - "src/renderer/src/store/slices/ui-page-navigation.test.ts": 563, - "src/renderer/src/store/slices/ui/ui-slice-surface-actions.test.ts": 1598, - "src/renderer/src/store/slices/usage-snapshot-refresh.benchmark.test.ts": 56, - "src/renderer/src/store/slices/usage-web-client-fallback.test.ts": 44, - "src/renderer/src/store/slices/workspace-chat-draft-removal.test.ts": 1530, - "src/renderer/src/store/slices/workspace-cleanup-browse.test.ts": 69, - "src/renderer/src/store/slices/workspace-cleanup-cache-hydration.test.ts": 551, - "src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts": 671, - "src/renderer/src/store/slices/workspace-cleanup-host-qualified-list-state.test.ts": 1355, - "src/renderer/src/store/slices/workspace-cleanup-local-evidence-invariants.test.ts": 423, - "src/renderer/src/store/slices/workspace-cleanup-removal-preflight.test.ts": 423, - "src/renderer/src/store/slices/workspace-cleanup-scan-progress-boundary.test.ts": 96, - "src/renderer/src/store/slices/workspace-cleanup-scan-progress.test.ts": 1070, - "src/renderer/src/store/slices/workspace-cleanup-unverified-removal-consent.test.ts": 432, - "src/renderer/src/store/slices/workspace-cleanup-wrong-host-removal-guard.test.ts": 1619, - "src/renderer/src/store/slices/workspace-cleanup-wrong-host-removal.test.ts": 1785, - "src/renderer/src/store/slices/workspace-document-title-refresh.test.ts": 1408, - "src/renderer/src/store/slices/workspace-space.test.ts": 55, - "src/renderer/src/store/slices/worktree-by-id-index.test.ts": 42, - "src/renderer/src/store/slices/worktree-catalog-reconciliation.test.ts": 49, - "src/renderer/src/store/slices/worktree-helpers.test.ts": 88, - "src/renderer/src/store/slices/worktree-listing-branch-switch.test.ts": 50, - "src/renderer/src/store/slices/worktree-meta-update-application.test.ts": 104, - "src/renderer/src/store/slices/worktree-nav-history-view-entries.test.ts": 111, - "src/renderer/src/store/slices/worktree-nav-history.test.ts": 100, - "src/renderer/src/store/slices/worktree-removal-maps-leak.test.ts": 1546, - "src/renderer/src/store/slices/worktree-sleep-intent-lifecycle.test.ts": 1394, - "src/renderer/src/store/slices/worktree-terminal-removal-teardown.test.ts": 1404, - "src/renderer/src/store/slices/worktree-visibility-owner-settings.test.ts": 158, - "src/renderer/src/store/slices/worktree-visibility-settings-write.test.ts": 1445, - "src/renderer/src/store/slices/worktrees-activity-persistence.test.ts": 824, - "src/renderer/src/store/slices/worktrees-always-force-delete.test.ts": 786, - "src/renderer/src/store/slices/worktrees-background-removal.test.ts": 1562, - "src/renderer/src/store/slices/worktrees-create-base-status.test.ts": 887, - "src/renderer/src/store/slices/worktrees-create-captured-host.test.ts": 787, - "src/renderer/src/store/slices/worktrees-create-parent-pick.test.ts": 962, - "src/renderer/src/store/slices/worktrees-fetch-listing-merge.test.ts": 814, - "src/renderer/src/store/slices/worktrees-fetch-owner-routing.test.ts": 1908, - "src/renderer/src/store/slices/worktrees-fetch-persisted-metadata-fallback.test.ts": 886, - "src/renderer/src/store/slices/worktrees-fetch-refresh-coalescing.test.ts": 715, - "src/renderer/src/store/slices/worktrees-fetch-remote-lineage.test.ts": 883, - "src/renderer/src/store/slices/worktrees-fetch-removal-purge.test.ts": 800, - "src/renderer/src/store/slices/worktrees-fetch-stale-catalog-version.test.ts": 866, - "src/renderer/src/store/slices/worktrees-git-identity-branch-title.test.ts": 885, - "src/renderer/src/store/slices/worktrees-git-identity-review-clear-persistence.test.ts": 1395, - "src/renderer/src/store/slices/worktrees-hydration-purge.test.ts": 965, - "src/renderer/src/store/slices/worktrees-identity-migration.test.ts": 1031, - "src/renderer/src/store/slices/worktrees-lineage-state.test.ts": 863, - "src/renderer/src/store/slices/worktrees-linked-review-push-target.test.ts": 829, - "src/renderer/src/store/slices/worktrees-metadata-persistence.test.ts": 993, - "src/renderer/src/store/slices/worktrees-multiple-attachments.test.ts": 808, - "src/renderer/src/store/slices/worktrees-pending-creation-state.test.ts": 794, - "src/renderer/src/store/slices/worktrees-purge-terminal-state.test.ts": 832, - "src/renderer/src/store/slices/worktrees-remote-runtime-create.test.ts": 867, - "src/renderer/src/store/slices/worktrees-remote-runtime-removal.test.ts": 879, - "src/renderer/src/store/slices/worktrees-removal-state-cleanup.test.ts": 888, - "src/renderer/src/store/slices/worktrees-runtime-connection-generation-fence.test.ts": 1005, - "src/renderer/src/store/slices/worktrees-runtime-host-metadata-retirement.test.ts": 779, - "src/renderer/src/store/slices/worktrees-terminal-pr-exact-observation.test.ts": 805, - "src/renderer/src/store/slices/worktrees-terminal-pr-url-linking.test.ts": 1028, - "src/renderer/src/store/slices/worktrees-terminal-reference-admission.test.ts": 1524, - "src/renderer/src/store/slices/worktrees-terminal-reference-origins.test.ts": 914, - "src/renderer/src/store/slices/worktrees-terminal-reference-source-race.test.ts": 720, - "src/renderer/src/store/slices/worktrees-unread-state.test.ts": 932, - "src/renderer/src/store/slices/worktrees-workspace-selection-state.test.ts": 740, - "src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.test.ts": 338, - "src/renderer/src/store/slices/worktrees/listing/detected-worktree-unavailable-reason.test.ts": 325, - "src/renderer/src/store/slices/worktrees/listing/stale-listing-purges-created-worktree.test.ts": 1688, - "src/renderer/src/store/slices/worktrees/listing/worktree-host-ownership-unstamped.test.ts": 98, - "src/renderer/src/store/slices/worktrees/metadata/worktree-meta-persist.test.ts": 359, - "src/renderer/src/store/slices/worktrees/session/worktree-identity-rename-row-worktree-ids.test.ts": 797, - "src/renderer/src/store/slices/worktrees/session/worktree-no-op-notifications.test.ts": 883, - "src/renderer/src/store/slices/worktrees/teardown/host-qualified-removal-refusal.test.ts": 388, - "src/renderer/src/store/slices/worktrees/teardown/record-key-omission.test.ts": 50, - "src/renderer/src/store/slices/worktrees/teardown/remove-worktree-map-identity.test.ts": 299, - "src/renderer/src/store/slices/worktrees/teardown/worktree-teardown-array-identity.test.ts": 443, - "src/renderer/src/store/store-identity-churn-probe.test.ts": 71, - "src/renderer/src/store/store-listener-census.test.ts": 59, - "src/renderer/src/store/terminals/restored-relay-session-identity.test.ts": 1782, - "src/renderer/src/store/terminals/structured-chat-tab-rename.test.ts": 1376, - "src/renderer/src/store/terminals/terminal-exited-pane-liveness.test.ts": 1393, - "src/renderer/src/store/terminals/terminal-no-op-subscriber.test.ts": 1428, - "src/renderer/src/store/terminals/terminal-pane-expansion-write-bailout.test.tsx": 1787, - "src/renderer/src/store/terminals/terminal-shutdown-guards-identity.test.ts": 57, - "src/renderer/src/store/terminals/terminal-shutdown-map-identity.test.ts": 245, - "src/renderer/src/store/terminals/terminal-tab-close-intent.test.ts": 1531, - "src/renderer/src/store/terminals/terminal-workspace-routing.scale.test.ts": 347, - "src/renderer/src/store/terminals/workspace-terminal-placeholders.test.ts": 273, - "src/renderer/src/store/workspace-layout-cache.test.ts": 66, - "src/renderer/src/store/worktree-diff-comments-selector.test.ts": 101, - "src/renderer/src/store/worktree-host-collision-index.test.ts": 51, - "src/renderer/src/store/worktree-visibility-defaults-by-host.test.ts": 45, - "src/renderer/src/web/preload-api/web-agent-session-attachment-upload.test.ts": 52, - "src/renderer/src/web/preload-api/web-ai-vault-search-compat.test.ts": 267, - "src/renderer/src/web/preload-api/web-fallback-api.test.ts": 59, - "src/renderer/src/web/preload-api/web-git-stage-scope.test.ts": 528, - "src/renderer/src/web/preload-api/web-gitlab-api.test.ts": 56, - "src/renderer/src/web/preload-api/web-ui-compact-notice.test.ts": 818, - "src/renderer/src/web/preload-api/web-ui-explorer-root.test.ts": 695, - "src/renderer/src/web/preload-api/web-worktree-attachment-mutations.test.ts": 314, - "src/renderer/src/web/web-clipboard-copy-fallback.test.ts": 46, - "src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts": 140, - "src/renderer/src/web/web-clipboard-image-presence.test.ts": 367, - "src/renderer/src/web/web-file-mutation-methods.test.ts": 281, - "src/renderer/src/web/web-native-chat-appearance-settings.test.ts": 259, - "src/renderer/src/web/web-pairing.test.ts": 250, - "src/renderer/src/web/web-preload-api-agent-providers.test.ts": 795, - "src/renderer/src/web/web-preload-api-clipboard.test.ts": 1407, - "src/renderer/src/web/web-preload-api-composition.test.ts": 768, - "src/renderer/src/web/web-preload-api-filesystem.test.ts": 694, - "src/renderer/src/web/web-preload-api-git.test.ts": 605, - "src/renderer/src/web/web-preload-api-github.test.ts": 710, - "src/renderer/src/web/web-preload-api-gitlab.test.ts": 833, - "src/renderer/src/web/web-preload-api-jira.test.ts": 1051, - "src/renderer/src/web/web-preload-api-keybindings.test.ts": 597, - "src/renderer/src/web/web-preload-api-rate-limits.test.ts": 1529, - "src/renderer/src/web/web-preload-api-runtime-calls.test.ts": 1289, - "src/renderer/src/web/web-preload-api-runtime-environment.test.ts": 1527, - "src/renderer/src/web/web-preload-api-settings.test.ts": 2698, - "src/renderer/src/web/web-preload-api-ssh.test.ts": 451, - "src/renderer/src/web/web-preload-api-ui-pairing-local-fields.test.ts": 2567, - "src/renderer/src/web/web-preload-api-ui.test.ts": 2101, - "src/renderer/src/web/web-preload-api-workspace-catalog.test.ts": 2033, - "src/renderer/src/web/web-preload-updater-package-recovery.test.ts": 554, - "src/renderer/src/web/web-runtime-client-file-watch-replay.test.ts": 213, - "src/renderer/src/web/web-runtime-client-heartbeat.test.ts": 234, - "src/renderer/src/web/web-runtime-client-timeout-budget.test.ts": 264, - "src/renderer/src/web/web-runtime-client.test.ts": 603, - "src/renderer/src/web/web-runtime-connection-frame-router.test.ts": 223, - "src/renderer/src/web/web-runtime-connection-heartbeat-unsendable-probe.test.ts": 46, - "src/renderer/src/web/web-runtime-direct-proxy-live.integration.test.ts": 251, - "src/renderer/src/web/web-runtime-environment-repair.test.ts": 827, - "src/renderer/src/web/web-runtime-status-owner.test.ts": 981, - "src/renderer/src/web/web-shell-paths-exist.test.ts": 40, - "src/renderer/src/web/web-workspace-session.test.ts": 272, - "src/renderer/src/web/web-zcode-plan-settings.test.ts": 726, - "src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts": 98, - "src/shared/__tests__/process-host/windows-cmd-shim-resolution.win32.test.ts": 71, - "src/shared/__tests__/process-host/windows-command-line.win32.test.ts": 53, - "src/shared/abort-signal-reason.test.ts": 46, - "src/shared/absolute-dir-override.test.ts": 62, - "src/shared/add-repo-existing-workspaces-telemetry.test.ts": 281, - "src/shared/agent-child-row-model.test.ts": 85, - "src/shared/agent-cli-flag-detection.test.ts": 56, - "src/shared/agent-cli-install-dir-fallback.test.ts": 73, - "src/shared/agent-decorative-title-signature.test.ts": 67, - "src/shared/agent-detection.test.ts": 146, - "src/shared/agent-feature-install-commands.test.ts": 65, - "src/shared/agent-hook-endpoint-file.test.ts": 49, - "src/shared/agent-hook-endpoint-temp-cleanup.test.ts": 58, - "src/shared/agent-hook-listener-antigravity.test.ts": 202, - "src/shared/agent-hook-listener-claude-compatible-vendors.test.ts": 187, - "src/shared/agent-hook-listener-claude-reordered-task-notification.test.ts": 192, - "src/shared/agent-hook-listener-claude-subagents.test.ts": 204, - "src/shared/agent-hook-listener-claude-task-notification.test.ts": 353, - "src/shared/agent-hook-listener-claude-turn-state.test.ts": 158, - "src/shared/agent-hook-listener-codex-approval-ownership.test.ts": 211, - "src/shared/agent-hook-listener-codex-interrupt.test.ts": 164, - "src/shared/agent-hook-listener-codex-main-agent.test.ts": 190, - "src/shared/agent-hook-listener-command-code-transcript.test.ts": 255, - "src/shared/agent-hook-listener-extraction-characterization.test.ts": 207, - "src/shared/agent-hook-listener-grok-completion.test.ts": 232, - "src/shared/agent-hook-listener-grok.test.ts": 232, - "src/shared/agent-hook-listener-hermes-codex-droid.test.ts": 203, - "src/shared/agent-hook-listener-interactive-prompts.test.ts": 179, - "src/shared/agent-hook-listener-jcode.test.ts": 214, - "src/shared/agent-hook-listener-omp-approval-ownership.test.ts": 200, - "src/shared/agent-hook-listener-opencode-reattribution.test.ts": 192, - "src/shared/agent-hook-listener-opencode-session-correlation.test.ts": 71, - "src/shared/agent-hook-listener-opencode-session-registry.test.ts": 71, - "src/shared/agent-hook-listener-opencode-tui-identity.test.ts": 180, - "src/shared/agent-hook-listener-pi-compatible.test.ts": 182, - "src/shared/agent-hook-listener-roster-retention.test.ts": 266, - "src/shared/agent-hook-listener-session-replacement.test.ts": 186, - "src/shared/agent-hook-listener-startless-child-lifecycle.test.ts": 169, - "src/shared/agent-hook-listener-transport.test.ts": 239, - "src/shared/agent-hook-listener/providers/dsh-events.test.ts": 161, - "src/shared/agent-hook-listener/providers/hermes-events.test.ts": 112, - "src/shared/agent-hook-listener/providers/jcode-turn-prompt.test.ts": 115, - "src/shared/agent-hook-listener/providers/kiro-events.test.ts": 196, - "src/shared/agent-hook-listener/providers/muse-events.test.ts": 207, - "src/shared/agent-hook-listener/providers/zcode-events.test.ts": 197, - "src/shared/agent-hook-listener/reverse-file-region-scan.test.ts": 57, - "src/shared/agent-hook-listener/transcript-reader.test.ts": 94, - "src/shared/agent-hook-relay.test.ts": 54, - "src/shared/agent-hook-request-body-memory.test.ts": 92, - "src/shared/agent-hook-scrub-safe-env.test.ts": 58, - "src/shared/agent-hook-spool-read.test.ts": 62, - "src/shared/agent-hook-status-cache.test.ts": 80, - "src/shared/agent-hook-transport-interference.test.ts": 47, - "src/shared/agent-image-paste.test.ts": 57, - "src/shared/agent-kind.test.ts": 280, - "src/shared/agent-launch-operation.test.ts": 255, - "src/shared/agent-launch-preferences.test.ts": 45, - "src/shared/agent-launch-remote.test.ts": 43, - "src/shared/agent-lead-status-fold.test.ts": 55, - "src/shared/agent-main-agent-verdict.test.ts": 102, - "src/shared/agent-model-probe-spec.test.ts": 76, - "src/shared/agent-notification-id.test.ts": 48, - "src/shared/agent-process-presence-platform.test.ts": 55, - "src/shared/agent-process-presence-probe.test.ts": 69, - "src/shared/agent-process-recognition.test.ts": 204, - "src/shared/agent-prompt-injection.test.ts": 91, - "src/shared/agent-resume-argv-drop.test.ts": 134, - "src/shared/agent-resume-launch-command.test.ts": 195, - "src/shared/agent-row-conversation-name.test.ts": 70, - "src/shared/agent-scratch-worktrees.test.ts": 53, - "src/shared/agent-session-account-home.test.ts": 270, - "src/shared/agent-session-approval-blocked-path.test.ts": 50, - "src/shared/agent-session-approval-subject.test.ts": 228, - "src/shared/agent-session-attachments.test.ts": 46, - "src/shared/agent-session-availability.test.ts": 271, - "src/shared/agent-session-context-clear.test.ts": 362, - "src/shared/agent-session-conversation-name.test.ts": 64, - "src/shared/agent-session-definitive-refusal.test.ts": 277, - "src/shared/agent-session-failure-words.test.ts": 369, - "src/shared/agent-session-failure.test.ts": 269, - "src/shared/agent-session-journal-body-admission.test.ts": 303, - "src/shared/agent-session-journal-producer.test.ts": 47, - "src/shared/agent-session-journal-schemas.test.ts": 296, - "src/shared/agent-session-journal-submission-schema.test.ts": 239, - "src/shared/agent-session-lease-adjudication.test.ts": 62, - "src/shared/agent-session-legacy-handoff-lease.test.ts": 259, - "src/shared/agent-session-message-source.test.ts": 286, - "src/shared/agent-session-mutation-envelope.test.ts": 264, - "src/shared/agent-session-operation-ledger.test.ts": 284, - "src/shared/agent-session-option-catalog-grok.test.ts": 96, - "src/shared/agent-session-option-catalog-omp.test.ts": 86, - "src/shared/agent-session-option-catalog-opencode.test.ts": 79, - "src/shared/agent-session-option-catalog-removers.test.ts": 87, - "src/shared/agent-session-option-catalog.test.ts": 73, - "src/shared/agent-session-provider-context.test.ts": 267, - "src/shared/agent-session-provider-handle.test.ts": 100, - "src/shared/agent-session-question-answer.test.ts": 51, - "src/shared/agent-session-question-free-text.test.ts": 53, - "src/shared/agent-session-record-stored-form.test.ts": 264, - "src/shared/agent-session-refusal-notice.test.ts": 433, - "src/shared/agent-session-refusal-retry.test.ts": 53, - "src/shared/agent-session-registered-agents.test.ts": 233, - "src/shared/agent-session-restart-continuation.test.ts": 51, - "src/shared/agent-session-resume-cursor.test.ts": 144, - "src/shared/agent-session-resume.test.ts": 83, - "src/shared/agent-session-sign-in.test.ts": 269, - "src/shared/agent-session-status-snapshot-fold.test.ts": 62, - "src/shared/agent-session-stored-agent.test.ts": 250, - "src/shared/agent-session-thread-goal.test.ts": 56, - "src/shared/agent-session-turn-record.test.ts": 244, - "src/shared/agent-session-wire-refusals.test.ts": 250, - "src/shared/agent-session-write-failure.test.ts": 269, - "src/shared/agent-skill-sharing-contract.test.ts": 218, - "src/shared/agent-skill-sharing-gate.test.ts": 253, - "src/shared/agent-startup-plan-inputs-extra-agent-args.test.ts": 167, - "src/shared/agent-startup-plan-inputs.test.ts": 149, - "src/shared/agent-status-child-work-admission-activity.test.ts": 400, - "src/shared/agent-status-child-work-admission-collision.test.ts": 313, - "src/shared/agent-status-child-work-admission-parse.test.ts": 483, - "src/shared/agent-status-child-work-admission-sparse.test.ts": 334, - "src/shared/agent-status-child-work-admission.test.ts": 383, - "src/shared/agent-status-child-work-legacy-golden.test.ts": 77, - "src/shared/agent-status-child-work-legality.test.ts": 395, - "src/shared/agent-status-child-work-lifetime.test.ts": 464, - "src/shared/agent-status-child-work-liveness.test.ts": 47, - "src/shared/agent-status-child-work-projection.test.ts": 66, - "src/shared/agent-status-child-work-reconciliation.test.ts": 596, - "src/shared/agent-status-child-work-view-wire.test.ts": 49, - "src/shared/agent-status-child-work-view.test.ts": 339, - "src/shared/agent-status-child-work.test.ts": 311, - "src/shared/agent-status-legacy-adapter.test.ts": 65, - "src/shared/agent-status-observation.test.ts": 51, - "src/shared/agent-status-osc-pending-retention.test.ts": 126, - "src/shared/agent-status-osc-scan-budget.test.ts": 267, - "src/shared/agent-status-osc-split-frame-scan-budget.test.ts": 128, + "src/renderer/src/store/slices/linear-issue-cache-refresh.test.ts": 340, + "src/renderer/src/store/slices/linear-scoped-collection-cache.test.ts": 366, + "src/renderer/src/store/slices/linear-source-context-cache-scope.test.ts": 344, + "src/renderer/src/store/slices/linear.test.ts": 339, + "src/renderer/src/store/slices/local-detected-agent-state.test.ts": 404, + "src/renderer/src/store/slices/memory.test.ts": 48, + "src/renderer/src/store/slices/native-chat-launch-draft-teardown.test.ts": 1475, + "src/renderer/src/store/slices/new-issue-draft.test.ts": 31, + "src/renderer/src/store/slices/new-markdown.test.ts": 699, + "src/renderer/src/store/slices/orca-profiles-auth-actions-connect-overlap.test.ts": 1207, + "src/renderer/src/store/slices/orca-profiles-auth-actions.test.ts": 1291, + "src/renderer/src/store/slices/orca-profiles.test.ts": 1385, + "src/renderer/src/store/slices/pane-column-split-drop-no-op.test.ts": 34, + "src/renderer/src/store/slices/persisted-ui-write-baseline.test.ts": 50, + "src/renderer/src/store/slices/pinned-tab-close-confirm.test.ts": 85, + "src/renderer/src/store/slices/preflight.test.ts": 341, + "src/renderer/src/store/slices/project-group-removal-targets.test.ts": 38, + "src/renderer/src/store/slices/project-removal-owner-host.test.ts": 1305, + "src/renderer/src/store/slices/project-removal-runtime-terminal-order.test.ts": 1505, + "src/renderer/src/store/slices/pull-request-generation-jira-title.test.ts": 45, + "src/renderer/src/store/slices/purge-stale-runtime-host-groups.test.ts": 1231, + "src/renderer/src/store/slices/purge-stale-runtime-host-ownership.test.ts": 1471, + "src/renderer/src/store/slices/purge-stale-runtime-host-state.test.ts": 1296, + "src/renderer/src/store/slices/readopted-ssh-worktree-rows.test.ts": 40, + "src/renderer/src/store/slices/recently-closed-tabs.test.ts": 1416, + "src/renderer/src/store/slices/remote-server-updates.integration.test.ts": 284, + "src/renderer/src/store/slices/repo-identity-reconcile.test.ts": 47, + "src/renderer/src/store/slices/repo-owner-cache-identity.test.ts": 32, + "src/renderer/src/store/slices/repo-reorder-host-split.test.ts": 31, + "src/renderer/src/store/slices/repos-add-races.test.ts": 1400, + "src/renderer/src/store/slices/repos-all-hosts-folder-workspaces.test.ts": 1250, + "src/renderer/src/store/slices/repos-all-hosts-generation.test.ts": 1336, + "src/renderer/src/store/slices/repos-all-hosts.test.ts": 1295, + "src/renderer/src/store/slices/repos-catalog-connection-fence.test.ts": 1349, + "src/renderer/src/store/slices/repos-catalog-merge-identity.test.ts": 1449, + "src/renderer/src/store/slices/repos-catalog-notifications.test.ts": 1416, + "src/renderer/src/store/slices/repos-cross-host-project-collisions.test.ts": 1409, + "src/renderer/src/store/slices/repos-cross-host-refresh-identity.test.ts": 1286, + "src/renderer/src/store/slices/repos-ephemeral-vm-cleanup-retention.test.ts": 1564, + "src/renderer/src/store/slices/repos-host-identity-routing.test.ts": 1502, + "src/renderer/src/store/slices/repos-manual-order-hydration.test.ts": 1376, + "src/renderer/src/store/slices/repos-module-lifetime-coordinators.test.ts": 1733, + "src/renderer/src/store/slices/repos-nested-import-refresh-failures.test.ts": 1314, + "src/renderer/src/store/slices/repos-nested-ssh-projection.test.ts": 1387, + "src/renderer/src/store/slices/repos-onboarding-folder-startup.test.ts": 1560, + "src/renderer/src/store/slices/repos-paired-runtime-add.test.ts": 1298, + "src/renderer/src/store/slices/repos-project-group-create-race.test.ts": 1409, + "src/renderer/src/store/slices/repos-project-groups-delete.test.ts": 1163, + "src/renderer/src/store/slices/repos-project-groups-owner-routing.test.ts": 1308, + "src/renderer/src/store/slices/repos-project-groups.test.ts": 1404, + "src/renderer/src/store/slices/repos-project-host-capability.test.ts": 1473, + "src/renderer/src/store/slices/repos-project-host-lifecycle.test.ts": 1304, + "src/renderer/src/store/slices/repos-project-runtime.test.ts": 1492, + "src/renderer/src/store/slices/repos-refresh-identity.test.ts": 1464, + "src/renderer/src/store/slices/repos-remove-missing-remote-project.test.ts": 1274, + "src/renderer/src/store/slices/repos-remove-project-purge-leak.test.ts": 1266, + "src/renderer/src/store/slices/repos-runtime-project-groups.test.ts": 1224, + "src/renderer/src/store/slices/repos-runtime-visibility-defaults.test.ts": 1337, + "src/renderer/src/store/slices/repos-selected-owner-routing.test.ts": 1447, + "src/renderer/src/store/slices/repos-setup-script-dismissals.test.ts": 1304, + "src/renderer/src/store/slices/repos-shared-project-badge-color.test.ts": 1371, + "src/renderer/src/store/slices/repos-ssh-host-reconciliation.test.ts": 1383, + "src/renderer/src/store/slices/repos-stale-fetch.test.ts": 1393, + "src/renderer/src/store/slices/repos-update-serialization.test.ts": 1388, + "src/renderer/src/store/slices/repos.runtime-fallback.test.ts": 1452, + "src/renderer/src/store/slices/repos.test.ts": 1355, + "src/renderer/src/store/slices/restored-editor-owner-reparent.test.ts": 1664, + "src/renderer/src/store/slices/runtime-catalog-merge-removed-env-guard.test.ts": 1344, + "src/renderer/src/store/slices/runtime-detected-agents-pairing.test.ts": 1101, + "src/renderer/src/store/slices/runtime-detected-agents.test.ts": 1396, + "src/renderer/src/store/slices/runtime-environment-peer-replacement.test.ts": 232, + "src/renderer/src/store/slices/runtime-environment-ssh.test.ts": 1366, + "src/renderer/src/store/slices/runtime-host-purge-session-partition-split.test.ts": 1420, + "src/renderer/src/store/slices/runtime-managed-repair-keeps-workspaces.test.ts": 1285, + "src/renderer/src/store/slices/runtime-pane-title-sort-epoch.test.ts": 1322, + "src/renderer/src/store/slices/runtime-status-catalog-identity.test.ts": 1425, + "src/renderer/src/store/slices/runtime-status-first-publication.test.ts": 315, + "src/renderer/src/store/slices/runtime-status-reconnect-connection-generation.test.ts": 1411, + "src/renderer/src/store/slices/runtime-status-refresh-diagnostics.test.ts": 43, + "src/renderer/src/store/slices/runtime-status-restored-browser-host-attach.test.ts": 323, + "src/renderer/src/store/slices/runtime-status-skill-cache-eviction.test.ts": 1667, + "src/renderer/src/store/slices/runtime-status-snapshot.test.ts": 397, + "src/renderer/src/store/slices/runtime-status.test.ts": 630, + "src/renderer/src/store/slices/runtime-switch-settings-persistence.test.ts": 1392, + "src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts": 1327, + "src/renderer/src/store/slices/set-runtime-environments-purge-wiring.test.ts": 1305, + "src/renderer/src/store/slices/settings-owner-hydration-write-fence.test.ts": 1502, + "src/renderer/src/store/slices/settings-search-state.test.ts": 32, + "src/renderer/src/store/slices/settings.test.ts": 1580, + "src/renderer/src/store/slices/sparse-presets-repo-removal-purge-leak.test.ts": 1343, + "src/renderer/src/store/slices/sparse-presets.test.ts": 69, + "src/renderer/src/store/slices/ssh-target-cleanup.test.ts": 1483, + "src/renderer/src/store/slices/ssh.test.ts": 1294, + "src/renderer/src/store/slices/stale-runtime-host-rows.test.ts": 45, + "src/renderer/src/store/slices/store-active-worktree-selection.test.ts": 1470, + "src/renderer/src/store/slices/store-active-worktree-split-groups.test.ts": 1463, + "src/renderer/src/store/slices/store-active-worktree-tab-close.test.ts": 1610, + "src/renderer/src/store/slices/store-active-worktree-terminal-creation.test.ts": 1369, + "src/renderer/src/store/slices/store-create-tab-first-pane-layout.test.ts": 1422, + "src/renderer/src/store/slices/store-create-tab-id-hint.test.ts": 1351, + "src/renderer/src/store/slices/store-session-browser-hydration.test.ts": 1413, + "src/renderer/src/store/slices/store-session-cascades.test.ts": 1570, + "src/renderer/src/store/slices/store-session-editor-hydration.test.ts": 1767, + "src/renderer/src/store/slices/store-session-terminal-activity.test.ts": 1414, + "src/renderer/src/store/slices/store-session-terminal-reconnect.test.ts": 1423, + "src/renderer/src/store/slices/store-session-workspace-hydration.test.ts": 1325, + "src/renderer/src/store/slices/store-sleep-agent-status-retention.test.ts": 1830, + "src/renderer/src/store/slices/store-sleep-exact-runtime-stop.test.ts": 1381, + "src/renderer/src/store/slices/store-sleep-pane-hibernation.test.ts": 1252, + "src/renderer/src/store/slices/store-sleep-runtime-convergence.test.ts": 1696, + "src/renderer/src/store/slices/store-sleep-shutdown-order.test.ts": 1299, + "src/renderer/src/store/slices/store-worktree-removal-cascade.test.ts": 1464, + "src/renderer/src/store/slices/superseded-ssh-repo-rows.test.ts": 45, + "src/renderer/src/store/slices/tab-group-reference-repair.test.ts": 64, + "src/renderer/src/store/slices/tab-group-state.test.ts": 47, + "src/renderer/src/store/slices/tab-view-mode.test.ts": 1309, + "src/renderer/src/store/slices/tab-worktree-orphan-map-purge-leak.test.ts": 1514, + "src/renderer/src/store/slices/tabs-close-dangling-group.test.ts": 1500, + "src/renderer/src/store/slices/tabs-hydration-generated-title.test.ts": 42, + "src/renderer/src/store/slices/tabs-hydration-group-validation.test.ts": 46, + "src/renderer/src/store/slices/tabs-hydration.test.ts": 67, + "src/renderer/src/store/slices/tabs-label-and-pin-state.test.ts": 1407, + "src/renderer/src/store/slices/tabs-model-reconciliation.test.ts": 1244, + "src/renderer/src/store/slices/tabs-open-close-lifecycle.test.ts": 1492, + "src/renderer/src/store/slices/tabs-pane-layout-operations.test.ts": 1539, + "src/renderer/src/store/slices/tabs-session-hydration.test.ts": 1269, + "src/renderer/src/store/slices/tabs-unread-and-focus.test.ts": 1375, + "src/renderer/src/store/slices/tabs/hydrated-workspace-reconciliation-batch.test.ts": 1301, + "src/renderer/src/store/slices/tabs/structured-chat-close-owner.test.ts": 1337, + "src/renderer/src/store/slices/tabs/tab-selection-contract.test.ts": 48, + "src/renderer/src/store/slices/tabs/tabs-reconciliation-batch-identity.test.ts": 1451, + "src/renderer/src/store/slices/task-creation-drafts.test.ts": 38, + "src/renderer/src/store/slices/terminal-helpers.test.ts": 79, + "src/renderer/src/store/slices/terminal-input-activity-store-write.test.ts": 1172, + "src/renderer/src/store/slices/terminal-layout-pty-ownership.test.ts": 1444, + "src/renderer/src/store/slices/terminal-orphan-helpers.test.ts": 308, + "src/renderer/src/store/slices/terminal-pane-detach-agent-identity.test.ts": 1265, + "src/renderer/src/store/slices/terminal-pane-detach-agent-retention.test.ts": 1699, + "src/renderer/src/store/slices/terminal-pane-retirement-ownership.test.ts": 279, + "src/renderer/src/store/slices/terminal-pty-identity-replacement.test.ts": 1347, + "src/renderer/src/store/slices/terminal-quick-command-hosts.test.ts": 1420, + "src/renderer/src/store/slices/terminal-startup-command-retention.test.ts": 1871, + "src/renderer/src/store/slices/terminal-tab-id-hydration.test.ts": 1387, + "src/renderer/src/store/slices/terminal-tab-owner-index.test.ts": 53, + "src/renderer/src/store/slices/terminal-tab-recovery-remount.test.ts": 1449, + "src/renderer/src/store/slices/terminal-tab-retirement-store.test.ts": 1384, + "src/renderer/src/store/slices/terminal-tab-retirement.test.ts": 349, + "src/renderer/src/store/slices/terminal-tab-title-batch.test.ts": 1592, + "src/renderer/src/store/slices/terminals-explicit-empty-hydration.test.ts": 1294, + "src/renderer/src/store/slices/terminals-hydration-canonical-pty-overlap.test.ts": 1413, + "src/renderer/src/store/slices/terminals-hydration-canonical-rows.test.ts": 1304, + "src/renderer/src/store/slices/terminals-hydration.test.ts": 1408, + "src/renderer/src/store/slices/ui-acknowledge-agents-clock-skew.test.ts": 474, + "src/renderer/src/store/slices/ui-agent-send-target.test.ts": 567, + "src/renderer/src/store/slices/ui-contextual-tours.test.ts": 482, + "src/renderer/src/store/slices/ui-feature-interactions.test.ts": 509, + "src/renderer/src/store/slices/ui-host-filter-sync.test.ts": 516, + "src/renderer/src/store/slices/ui-hydration-view-layout.test.ts": 569, + "src/renderer/src/store/slices/ui-hydration-workspace-cleanup-browse.test.ts": 477, + "src/renderer/src/store/slices/ui-hydration-workspace-preferences.test.ts": 610, + "src/renderer/src/store/slices/ui-modal-slot-dismissal.test.ts": 547, + "src/renderer/src/store/slices/ui-notice-dismissals.test.ts": 611, + "src/renderer/src/store/slices/ui-page-navigation.test.ts": 668, + "src/renderer/src/store/slices/ui/ui-slice-surface-actions.test.ts": 1266, + "src/renderer/src/store/slices/usage-snapshot-refresh.benchmark.test.ts": 41, + "src/renderer/src/store/slices/usage-web-client-fallback.test.ts": 35, + "src/renderer/src/store/slices/workspace-activation-preparation.test.ts": 1480, + "src/renderer/src/store/slices/workspace-attention/workspace-live-markers.test.ts": 274, + "src/renderer/src/store/slices/workspace-attention/workspace-unread-characterization.test.ts": 1477, + "src/renderer/src/store/slices/workspace-attention/workspace-unread-on-screen-characterization.test.ts": 1264, + "src/renderer/src/store/slices/workspace-attention/workspace-unread-removal-characterization.test.ts": 1613, + "src/renderer/src/store/slices/workspace-attention/workspace-unread-screen-moves-characterization.test.ts": 1578, + "src/renderer/src/store/slices/workspace-chat-draft-removal.test.ts": 1488, + "src/renderer/src/store/slices/workspace-cleanup-browse.test.ts": 47, + "src/renderer/src/store/slices/workspace-cleanup-cache-hydration.test.ts": 548, + "src/renderer/src/store/slices/workspace-cleanup-enrichment-performance.test.ts": 658, + "src/renderer/src/store/slices/workspace-cleanup-host-qualified-list-state.test.ts": 1309, + "src/renderer/src/store/slices/workspace-cleanup-local-evidence-invariants.test.ts": 363, + "src/renderer/src/store/slices/workspace-cleanup-removal-preflight.test.ts": 466, + "src/renderer/src/store/slices/workspace-cleanup-scan-progress-boundary.test.ts": 81, + "src/renderer/src/store/slices/workspace-cleanup-scan-progress.test.ts": 1015, + "src/renderer/src/store/slices/workspace-cleanup-unverified-removal-consent.test.ts": 404, + "src/renderer/src/store/slices/workspace-cleanup-wrong-host-removal-guard.test.ts": 1185, + "src/renderer/src/store/slices/workspace-cleanup-wrong-host-removal.test.ts": 1336, + "src/renderer/src/store/slices/workspace-document-title-refresh.test.ts": 1379, + "src/renderer/src/store/slices/workspace-space.test.ts": 41, + "src/renderer/src/store/slices/worktree-by-id-index.test.ts": 37, + "src/renderer/src/store/slices/worktree-catalog-reconciliation.test.ts": 31, + "src/renderer/src/store/slices/worktree-helpers.test.ts": 79, + "src/renderer/src/store/slices/worktree-listing-branch-switch.test.ts": 44, + "src/renderer/src/store/slices/worktree-meta-update-application.test.ts": 70, + "src/renderer/src/store/slices/worktree-nav-history-view-entries.test.ts": 110, + "src/renderer/src/store/slices/worktree-nav-history.test.ts": 74, + "src/renderer/src/store/slices/worktree-removal-maps-leak.test.ts": 1410, + "src/renderer/src/store/slices/worktree-sleep-intent-lifecycle.test.ts": 1374, + "src/renderer/src/store/slices/worktree-terminal-removal-teardown.test.ts": 1408, + "src/renderer/src/store/slices/worktree-visibility-owner-settings.test.ts": 275, + "src/renderer/src/store/slices/worktree-visibility-settings-write.test.ts": 1290, + "src/renderer/src/store/slices/worktrees-activity-persistence.test.ts": 890, + "src/renderer/src/store/slices/worktrees-always-force-delete.test.ts": 831, + "src/renderer/src/store/slices/worktrees-background-removal.test.ts": 1421, + "src/renderer/src/store/slices/worktrees-create-base-status.test.ts": 802, + "src/renderer/src/store/slices/worktrees-create-captured-host.test.ts": 702, + "src/renderer/src/store/slices/worktrees-create-parent-pick.test.ts": 782, + "src/renderer/src/store/slices/worktrees-fetch-listing-merge.test.ts": 909, + "src/renderer/src/store/slices/worktrees-fetch-owner-routing.test.ts": 1433, + "src/renderer/src/store/slices/worktrees-fetch-persisted-metadata-fallback.test.ts": 800, + "src/renderer/src/store/slices/worktrees-fetch-refresh-coalescing.test.ts": 736, + "src/renderer/src/store/slices/worktrees-fetch-remote-lineage.test.ts": 821, + "src/renderer/src/store/slices/worktrees-fetch-removal-purge.test.ts": 894, + "src/renderer/src/store/slices/worktrees-fetch-stale-catalog-version.test.ts": 914, + "src/renderer/src/store/slices/worktrees-git-identity-branch-title.test.ts": 806, + "src/renderer/src/store/slices/worktrees-git-identity-review-clear-persistence.test.ts": 1290, + "src/renderer/src/store/slices/worktrees-hydration-purge.test.ts": 820, + "src/renderer/src/store/slices/worktrees-identity-migration.test.ts": 1116, + "src/renderer/src/store/slices/worktrees-lineage-state.test.ts": 859, + "src/renderer/src/store/slices/worktrees-linked-review-push-target.test.ts": 831, + "src/renderer/src/store/slices/worktrees-metadata-persistence.test.ts": 873, + "src/renderer/src/store/slices/worktrees-multiple-attachments.test.ts": 884, + "src/renderer/src/store/slices/worktrees-pending-creation-state.test.ts": 959, + "src/renderer/src/store/slices/worktrees-purge-terminal-state.test.ts": 775, + "src/renderer/src/store/slices/worktrees-remote-runtime-create.test.ts": 940, + "src/renderer/src/store/slices/worktrees-remote-runtime-removal.test.ts": 839, + "src/renderer/src/store/slices/worktrees-removal-state-cleanup.test.ts": 903, + "src/renderer/src/store/slices/worktrees-runtime-connection-generation-fence.test.ts": 871, + "src/renderer/src/store/slices/worktrees-runtime-host-metadata-retirement.test.ts": 917, + "src/renderer/src/store/slices/worktrees-terminal-pr-exact-observation.test.ts": 768, + "src/renderer/src/store/slices/worktrees-terminal-pr-url-linking.test.ts": 849, + "src/renderer/src/store/slices/worktrees-terminal-reference-admission.test.ts": 1666, + "src/renderer/src/store/slices/worktrees-terminal-reference-origins.test.ts": 708, + "src/renderer/src/store/slices/worktrees-terminal-reference-source-race.test.ts": 858, + "src/renderer/src/store/slices/worktrees-unread-state.test.ts": 1581, + "src/renderer/src/store/slices/worktrees-workspace-selection-state.test.ts": 911, + "src/renderer/src/store/slices/worktrees/create/runtime-provisioned-root-adoption.test.ts": 33, + "src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.test.ts": 276, + "src/renderer/src/store/slices/worktrees/listing/detected-worktree-unavailable-reason.test.ts": 276, + "src/renderer/src/store/slices/worktrees/listing/stale-listing-purges-created-worktree.test.ts": 1412, + "src/renderer/src/store/slices/worktrees/listing/worktree-host-ownership-unstamped.test.ts": 78, + "src/renderer/src/store/slices/worktrees/metadata/worktree-meta-persist.test.ts": 379, + "src/renderer/src/store/slices/worktrees/session/worktree-identity-rename-row-worktree-ids.test.ts": 769, + "src/renderer/src/store/slices/worktrees/session/worktree-no-op-notifications.test.ts": 703, + "src/renderer/src/store/slices/worktrees/teardown/host-qualified-removal-refusal.test.ts": 455, + "src/renderer/src/store/slices/worktrees/teardown/record-key-omission.test.ts": 40, + "src/renderer/src/store/slices/worktrees/teardown/remove-worktree-map-identity.test.ts": 394, + "src/renderer/src/store/slices/worktrees/teardown/worktree-teardown-array-identity.test.ts": 447, + "src/renderer/src/store/store-identity-churn-probe.test.ts": 44, + "src/renderer/src/store/store-listener-census.test.ts": 36, + "src/renderer/src/store/terminals/restored-relay-session-identity.test.ts": 1401, + "src/renderer/src/store/terminals/structured-chat-tab-rename.test.ts": 1235, + "src/renderer/src/store/terminals/terminal-exited-pane-liveness.test.ts": 1180, + "src/renderer/src/store/terminals/terminal-no-op-subscriber.test.ts": 1216, + "src/renderer/src/store/terminals/terminal-pane-expansion-write-bailout.test.tsx": 1564, + "src/renderer/src/store/terminals/terminal-shutdown-guards-identity.test.ts": 34, + "src/renderer/src/store/terminals/terminal-shutdown-map-identity.test.ts": 572, + "src/renderer/src/store/terminals/terminal-startup-queue-restart.test.ts": 1284, + "src/renderer/src/store/terminals/terminal-tab-close-intent.test.ts": 1411, + "src/renderer/src/store/terminals/terminal-workspace-routing.scale.test.ts": 319, + "src/renderer/src/store/terminals/workspace-terminal-placeholders.test.ts": 250, + "src/renderer/src/store/workspace-layout-cache.test.ts": 35, + "src/renderer/src/store/worktree-diff-comments-selector.test.ts": 103, + "src/renderer/src/store/worktree-host-collision-index.test.ts": 37, + "src/renderer/src/store/worktree-visibility-defaults-by-host.test.ts": 28, + "src/renderer/src/web/preload-api/web-agent-session-attachment-upload.test.ts": 37, + "src/renderer/src/web/preload-api/web-ai-vault-search-compat.test.ts": 265, + "src/renderer/src/web/preload-api/web-fallback-api.test.ts": 34, + "src/renderer/src/web/preload-api/web-git-stage-scope.test.ts": 730, + "src/renderer/src/web/preload-api/web-gitlab-api.test.ts": 229, + "src/renderer/src/web/preload-api/web-ui-compact-notice.test.ts": 916, + "src/renderer/src/web/preload-api/web-ui-explorer-root.test.ts": 629, + "src/renderer/src/web/preload-api/web-worktree-attachment-mutations.test.ts": 234, + "src/renderer/src/web/web-clipboard-copy-fallback.test.ts": 35, + "src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts": 162, + "src/renderer/src/web/web-clipboard-image-presence.test.ts": 620, + "src/renderer/src/web/web-device-key.test.ts": 92, + "src/renderer/src/web/web-file-mutation-methods.test.ts": 282, + "src/renderer/src/web/web-native-chat-appearance-settings.test.ts": 282, + "src/renderer/src/web/web-pairing.test.ts": 231, + "src/renderer/src/web/web-preload-api-agent-providers.test.ts": 949, + "src/renderer/src/web/web-preload-api-clipboard.test.ts": 2183, + "src/renderer/src/web/web-preload-api-composition.test.ts": 547, + "src/renderer/src/web/web-preload-api-filesystem.test.ts": 722, + "src/renderer/src/web/web-preload-api-git.test.ts": 535, + "src/renderer/src/web/web-preload-api-github.test.ts": 806, + "src/renderer/src/web/web-preload-api-gitlab.test.ts": 901, + "src/renderer/src/web/web-preload-api-jira.test.ts": 662, + "src/renderer/src/web/web-preload-api-keybindings.test.ts": 774, + "src/renderer/src/web/web-preload-api-rate-limits.test.ts": 2180, + "src/renderer/src/web/web-preload-api-runtime-calls.test.ts": 994, + "src/renderer/src/web/web-preload-api-runtime-environment.test.ts": 1776, + "src/renderer/src/web/web-preload-api-settings.test.ts": 2494, + "src/renderer/src/web/web-preload-api-ssh.test.ts": 454, + "src/renderer/src/web/web-preload-api-ui-pairing-local-fields.test.ts": 2126, + "src/renderer/src/web/web-preload-api-ui.test.ts": 2812, + "src/renderer/src/web/web-preload-api-workspace-catalog.test.ts": 1835, + "src/renderer/src/web/web-preload-updater-package-recovery.test.ts": 566, + "src/renderer/src/web/web-runtime-auth-verdict.test.ts": 33, + "src/renderer/src/web/web-runtime-client-file-watch-replay.test.ts": 348, + "src/renderer/src/web/web-runtime-client-heartbeat.test.ts": 375, + "src/renderer/src/web/web-runtime-client-timeout-budget.test.ts": 312, + "src/renderer/src/web/web-runtime-client.test.ts": 763, + "src/renderer/src/web/web-runtime-connection-frame-router.test.ts": 268, + "src/renderer/src/web/web-runtime-connection-heartbeat-unsendable-probe.test.ts": 33, + "src/renderer/src/web/web-runtime-credential-source.test.ts": 149, + "src/renderer/src/web/web-runtime-direct-proxy-live.integration.test.ts": 307, + "src/renderer/src/web/web-runtime-environment-repair.test.ts": 845, + "src/renderer/src/web/web-runtime-environment-token.test.ts": 191, + "src/renderer/src/web/web-runtime-status-owner.test.ts": 1031, + "src/renderer/src/web/web-shell-paths-exist.test.ts": 27, + "src/renderer/src/web/web-workspace-session.test.ts": 242, + "src/renderer/src/web/web-zcode-plan-settings.test.ts": 898, + "src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts": 77, + "src/shared/__tests__/process-host/windows-cmd-shim-resolution.win32.test.ts": 48, + "src/shared/__tests__/process-host/windows-command-line.win32.test.ts": 60, + "src/shared/abort-signal-reason.test.ts": 30, + "src/shared/absolute-dir-override.test.ts": 38, + "src/shared/add-repo-existing-workspaces-telemetry.test.ts": 266, + "src/shared/agent-chat-permission-mode.test.ts": 233, + "src/shared/agent-child-row-model.test.ts": 80, + "src/shared/agent-cli-flag-detection.test.ts": 39, + "src/shared/agent-cli-install-dir-fallback.test.ts": 69, + "src/shared/agent-command-line-intent.test.ts": 75, + "src/shared/agent-decorative-title-signature.test.ts": 46, + "src/shared/agent-detection.test.ts": 124, + "src/shared/agent-feature-install-commands.test.ts": 35, + "src/shared/agent-hook-endpoint-file.test.ts": 33, + "src/shared/agent-hook-endpoint-temp-cleanup.test.ts": 61, + "src/shared/agent-hook-listener-antigravity.test.ts": 245, + "src/shared/agent-hook-listener-claude-compatible-vendors.test.ts": 195, + "src/shared/agent-hook-listener-claude-reordered-task-notification.test.ts": 187, + "src/shared/agent-hook-listener-claude-subagents.test.ts": 260, + "src/shared/agent-hook-listener-claude-task-notification.test.ts": 344, + "src/shared/agent-hook-listener-claude-turn-state.test.ts": 218, + "src/shared/agent-hook-listener-codex-approval-ownership.test.ts": 200, + "src/shared/agent-hook-listener-codex-interrupt.test.ts": 165, + "src/shared/agent-hook-listener-codex-main-agent.test.ts": 177, + "src/shared/agent-hook-listener-command-code-transcript.test.ts": 282, + "src/shared/agent-hook-listener-extraction-characterization.test.ts": 190, + "src/shared/agent-hook-listener-grok-completion.test.ts": 188, + "src/shared/agent-hook-listener-grok.test.ts": 210, + "src/shared/agent-hook-listener-hermes-codex-droid.test.ts": 181, + "src/shared/agent-hook-listener-interactive-prompts.test.ts": 166, + "src/shared/agent-hook-listener-jcode.test.ts": 192, + "src/shared/agent-hook-listener-omp-approval-ownership.test.ts": 190, + "src/shared/agent-hook-listener-opencode-reattribution.test.ts": 227, + "src/shared/agent-hook-listener-opencode-session-correlation.test.ts": 60, + "src/shared/agent-hook-listener-opencode-session-registry.test.ts": 67, + "src/shared/agent-hook-listener-opencode-tui-identity.test.ts": 196, + "src/shared/agent-hook-listener-pi-compatible.test.ts": 206, + "src/shared/agent-hook-listener-roster-retention.test.ts": 243, + "src/shared/agent-hook-listener-session-replacement.test.ts": 180, + "src/shared/agent-hook-listener-startless-child-lifecycle.test.ts": 213, + "src/shared/agent-hook-listener-transport.test.ts": 253, + "src/shared/agent-hook-listener/providers/dsh-events.test.ts": 189, + "src/shared/agent-hook-listener/providers/hermes-events.test.ts": 91, + "src/shared/agent-hook-listener/providers/jcode-turn-prompt.test.ts": 99, + "src/shared/agent-hook-listener/providers/kiro-events.test.ts": 170, + "src/shared/agent-hook-listener/providers/muse-events.test.ts": 195, + "src/shared/agent-hook-listener/providers/zcode-events.test.ts": 164, + "src/shared/agent-hook-listener/reverse-file-region-scan.test.ts": 44, + "src/shared/agent-hook-listener/transcript-reader.test.ts": 75, + "src/shared/agent-hook-relay.test.ts": 49, + "src/shared/agent-hook-request-body-memory.test.ts": 71, + "src/shared/agent-hook-scrub-safe-env.test.ts": 37, + "src/shared/agent-hook-spool-read.test.ts": 34, + "src/shared/agent-hook-status-cache.test.ts": 70, + "src/shared/agent-hook-transport-interference.test.ts": 41, + "src/shared/agent-image-paste.test.ts": 50, + "src/shared/agent-kind.test.ts": 273, + "src/shared/agent-launch-operation.test.ts": 258, + "src/shared/agent-launch-preferences.test.ts": 29, + "src/shared/agent-launch-remote.test.ts": 40, + "src/shared/agent-lead-status-fold.test.ts": 42, + "src/shared/agent-main-agent-verdict.test.ts": 65, + "src/shared/agent-model-probe-spec.test.ts": 57, + "src/shared/agent-notification-id.test.ts": 40, + "src/shared/agent-process-presence-platform.test.ts": 35, + "src/shared/agent-process-presence-probe.test.ts": 56, + "src/shared/agent-process-recognition.test.ts": 175, + "src/shared/agent-prompt-injection.test.ts": 57, + "src/shared/agent-resume-argv-drop.test.ts": 129, + "src/shared/agent-resume-launch-command.test.ts": 202, + "src/shared/agent-row-conversation-name.test.ts": 68, + "src/shared/agent-scratch-worktrees.test.ts": 51, + "src/shared/agent-session-account-home.test.ts": 265, + "src/shared/agent-session-approval-blocked-path.test.ts": 40, + "src/shared/agent-session-approval-subject.test.ts": 233, + "src/shared/agent-session-attachments.test.ts": 39, + "src/shared/agent-session-availability.test.ts": 259, + "src/shared/agent-session-background-task-state-admission.test.ts": 224, + "src/shared/agent-session-context-clear.test.ts": 321, + "src/shared/agent-session-context-usage-schema.test.ts": 225, + "src/shared/agent-session-conversation-name.test.ts": 54, + "src/shared/agent-session-definitive-refusal.test.ts": 318, + "src/shared/agent-session-failure-words.test.ts": 390, + "src/shared/agent-session-failure.test.ts": 273, + "src/shared/agent-session-journal-body-admission.test.ts": 277, + "src/shared/agent-session-journal-producer.test.ts": 31, + "src/shared/agent-session-journal-schemas.test.ts": 285, + "src/shared/agent-session-journal-submission-schema.test.ts": 179, + "src/shared/agent-session-lease-adjudication.test.ts": 48, + "src/shared/agent-session-legacy-handoff-lease.test.ts": 276, + "src/shared/agent-session-message-source.test.ts": 229, + "src/shared/agent-session-mutation-envelope.test.ts": 239, + "src/shared/agent-session-operation-ledger.test.ts": 352, + "src/shared/agent-session-option-catalog-grok.test.ts": 107, + "src/shared/agent-session-option-catalog-omp.test.ts": 80, + "src/shared/agent-session-option-catalog-opencode.test.ts": 59, + "src/shared/agent-session-option-catalog-removers.test.ts": 67, + "src/shared/agent-session-option-catalog.test.ts": 90, + "src/shared/agent-session-provider-context.test.ts": 233, + "src/shared/agent-session-provider-handle.test.ts": 90, + "src/shared/agent-session-question-answer.test.ts": 41, + "src/shared/agent-session-question-free-text.test.ts": 41, + "src/shared/agent-session-record-stored-form.test.ts": 250, + "src/shared/agent-session-refusal-notice-stop.test.ts": 224, + "src/shared/agent-session-refusal-notice.test.ts": 401, + "src/shared/agent-session-refusal-retry.test.ts": 38, + "src/shared/agent-session-registered-agents.test.ts": 236, + "src/shared/agent-session-restart-continuation.test.ts": 31, + "src/shared/agent-session-resume-cursor.test.ts": 128, + "src/shared/agent-session-resume.test.ts": 80, + "src/shared/agent-session-sign-in.test.ts": 265, + "src/shared/agent-session-status-snapshot-fold.test.ts": 36, + "src/shared/agent-session-stopped-history.test.ts": 209, + "src/shared/agent-session-stored-agent.test.ts": 208, + "src/shared/agent-session-thread-goal.test.ts": 41, + "src/shared/agent-session-turn-record.test.ts": 220, + "src/shared/agent-session-wire-refusals.test.ts": 235, + "src/shared/agent-session-write-failure.test.ts": 253, + "src/shared/agent-skill-sharing-contract.test.ts": 230, + "src/shared/agent-skill-sharing-gate.test.ts": 252, + "src/shared/agent-startup-plan-inputs-extra-agent-args.test.ts": 101, + "src/shared/agent-startup-plan-inputs.test.ts": 123, + "src/shared/agent-status-child-work-admission-activity.test.ts": 428, + "src/shared/agent-status-child-work-admission-collision.test.ts": 309, + "src/shared/agent-status-child-work-admission-parse.test.ts": 508, + "src/shared/agent-status-child-work-admission-sparse.test.ts": 313, + "src/shared/agent-status-child-work-admission.test.ts": 415, + "src/shared/agent-status-child-work-legacy-golden.test.ts": 50, + "src/shared/agent-status-child-work-legality.test.ts": 281, + "src/shared/agent-status-child-work-lifetime.test.ts": 408, + "src/shared/agent-status-child-work-liveness.test.ts": 34, + "src/shared/agent-status-child-work-projection.test.ts": 57, + "src/shared/agent-status-child-work-reconciliation.test.ts": 687, + "src/shared/agent-status-child-work-stop-acknowledged.test.ts": 290, + "src/shared/agent-status-child-work-view-wire.test.ts": 39, + "src/shared/agent-status-child-work-view.test.ts": 347, + "src/shared/agent-status-child-work.test.ts": 294, + "src/shared/agent-status-legacy-adapter.test.ts": 40, + "src/shared/agent-status-observation.test.ts": 47, + "src/shared/agent-status-osc-pending-retention.test.ts": 107, + "src/shared/agent-status-osc-scan-budget.test.ts": 232, + "src/shared/agent-status-osc-split-frame-scan-budget.test.ts": 109, "src/shared/agent-status-osc.test.ts": 98, - "src/shared/agent-status-run-alias-index.test.ts": 327, - "src/shared/agent-status-run-capability.test.ts": 87, - "src/shared/agent-status-run.test.ts": 64, - "src/shared/agent-status-serving-readiness.test.ts": 45, - "src/shared/agent-status-store-bounds.test.ts": 1432, - "src/shared/agent-status-store-byte-budget.test.ts": 1322, - "src/shared/agent-status-store-in-place.test.ts": 3761, - "src/shared/agent-status-store-parent.test.ts": 262, - "src/shared/agent-status-store-reopen.test.ts": 301, - "src/shared/agent-status-store-scaling.test.ts": 771, + "src/shared/agent-status-run-alias-index.test.ts": 291, + "src/shared/agent-status-run-capability.test.ts": 247, + "src/shared/agent-status-run.test.ts": 42, + "src/shared/agent-status-serving-readiness.test.ts": 37, + "src/shared/agent-status-store-bounds.test.ts": 1319, + "src/shared/agent-status-store-byte-budget.test.ts": 1366, + "src/shared/agent-status-store-in-place.test.ts": 2452, + "src/shared/agent-status-store-parent.test.ts": 258, + "src/shared/agent-status-store-reopen.test.ts": 308, + "src/shared/agent-status-store-scaling.test.ts": 716, "src/shared/agent-status-store.test.ts": 408, - "src/shared/agent-status-subject.test.ts": 317, - "src/shared/agent-status-transport-envelope.test.ts": 332, - "src/shared/agent-status-types.test.ts": 163, - "src/shared/agent-stop-display-status.test.ts": 51, - "src/shared/agent-tab-title.test.ts": 53, - "src/shared/agent-terminal-status-equivalence.test.ts": 40, - "src/shared/agent-title-agy-gemini-collision.test.ts": 84, - "src/shared/agent-title-decoration.test.ts": 46, - "src/shared/agent-title-evidence.test.ts": 114, - "src/shared/agent-title-identity-characterization.test.ts": 75, - "src/shared/agent-transcript-poll-scheduler.test.ts": 165, - "src/shared/agent-tui-input-clear.test.ts": 59, - "src/shared/ai-vault-resume-command.test.ts": 65, - "src/shared/ai-vault-resume-preparation.test.ts": 48, - "src/shared/ai-vault-scan-error-message.test.ts": 61, - "src/shared/ai-vault-search-agent-history.test.ts": 291, - "src/shared/ai-vault-search-contract.test.ts": 258, - "src/shared/ai-vault-search-query-operators.test.ts": 70, - "src/shared/ai-vault-search-scope-contract.test.ts": 247, - "src/shared/ai-vault-search-settings.test.ts": 214, - "src/shared/ai-vault-search-status-fallback.test.ts": 278, - "src/shared/ai-vault-session-depth.test.ts": 66, - "src/shared/ai-vault-session-filters.test.ts": 173, - "src/shared/ai-vault-types.test.ts": 54, - "src/shared/antigravity-source-control.test.ts": 317, - "src/shared/antigravity-transcript-reference.test.ts": 59, - "src/shared/app-version.test.ts": 54, - "src/shared/artifact-sharing-gate.test.ts": 312, - "src/shared/automation-cron-dialect.test.ts": 77, - "src/shared/automation-cron-input-validation.test.ts": 66, - "src/shared/automation-execution-target.test.ts": 50, - "src/shared/automation-extra-agent-args.test.ts": 166, - "src/shared/automation-host-filter.test.ts": 68, - "src/shared/automation-legacy-list-partition.test.ts": 53, - "src/shared/automation-list-response.test.ts": 53, - "src/shared/automation-list-scope.test.ts": 96, - "src/shared/automation-owner-key.test.ts": 62, - "src/shared/automation-owner-precondition.test.ts": 80, - "src/shared/automation-run-identity.test.ts": 40, - "src/shared/automation-run-retention.test.ts": 87, - "src/shared/automation-schedule-drift.test.ts": 97, - "src/shared/automation-schedules.test.ts": 154, - "src/shared/automation-usage-summary.test.ts": 48, - "src/shared/background-task-roster.test.ts": 98, - "src/shared/base-ref-search-result.test.ts": 46, - "src/shared/binary-file-extensions.test.ts": 52, - "src/shared/bounded-map.test.ts": 73, - "src/shared/branch-name-from-work.test.ts": 59, - "src/shared/branch-prefix.test.ts": 63, - "src/shared/browser-annotation-viewport-lifecycle.test.ts": 186, - "src/shared/browser-client-automation-protocol.test.ts": 240, - "src/shared/browser-client-host-id-argument.test.ts": 52, - "src/shared/browser-client-host-protocol.test.ts": 320, + "src/shared/agent-status-subject.test.ts": 256, + "src/shared/agent-status-transport-envelope.test.ts": 345, + "src/shared/agent-status-types.test.ts": 122, + "src/shared/agent-stop-display-status.test.ts": 27, + "src/shared/agent-tab-title.test.ts": 34, + "src/shared/agent-terminal-status-equivalence.test.ts": 39, + "src/shared/agent-title-agy-gemini-collision.test.ts": 51, + "src/shared/agent-title-decoration.test.ts": 26, + "src/shared/agent-title-evidence.test.ts": 164, + "src/shared/agent-title-identity-characterization.test.ts": 57, + "src/shared/agent-transcript-poll-scheduler.test.ts": 136, + "src/shared/agent-tui-input-clear.test.ts": 47, + "src/shared/ai-vault-resume-command.test.ts": 61, + "src/shared/ai-vault-resume-preparation.test.ts": 34, + "src/shared/ai-vault-scan-error-message.test.ts": 37, + "src/shared/ai-vault-search-agent-history.test.ts": 279, + "src/shared/ai-vault-search-contract.test.ts": 214, + "src/shared/ai-vault-search-query-operators.test.ts": 60, + "src/shared/ai-vault-search-scope-contract.test.ts": 258, + "src/shared/ai-vault-search-settings.test.ts": 208, + "src/shared/ai-vault-search-status-fallback.test.ts": 290, + "src/shared/ai-vault-session-depth.test.ts": 44, + "src/shared/ai-vault-session-filters.test.ts": 178, + "src/shared/ai-vault-types.test.ts": 31, + "src/shared/ansi-escape-sequences-consumers.test.ts": 113, + "src/shared/ansi-escape-sequences.test.ts": 267, + "src/shared/antigravity-source-control.test.ts": 234, + "src/shared/antigravity-transcript-reference.test.ts": 45, + "src/shared/app-version.test.ts": 34, + "src/shared/artifact-sharing-gate.test.ts": 262, + "src/shared/automatic-work-run-marker.test.ts": 181, + "src/shared/automation-cron-dialect.test.ts": 56, + "src/shared/automation-cron-input-validation.test.ts": 58, + "src/shared/automation-execution-target.test.ts": 38, + "src/shared/automation-extra-agent-args.test.ts": 214, + "src/shared/automation-host-filter.test.ts": 50, + "src/shared/automation-legacy-list-partition.test.ts": 45, + "src/shared/automation-list-response.test.ts": 54, + "src/shared/automation-list-scope.test.ts": 265, + "src/shared/automation-owner-key.test.ts": 41, + "src/shared/automation-owner-precondition.test.ts": 51, + "src/shared/automation-run-identity.test.ts": 35, + "src/shared/automation-run-retention.test.ts": 68, + "src/shared/automation-schedule-drift.test.ts": 70, + "src/shared/automation-schedules.test.ts": 170, + "src/shared/automation-usage-summary.test.ts": 33, + "src/shared/background-task-roster.test.ts": 92, + "src/shared/base-ref-search-result.test.ts": 23, + "src/shared/binary-file-extensions.test.ts": 31, + "src/shared/bounded-map.test.ts": 50, + "src/shared/branch-name-from-work.test.ts": 51, + "src/shared/branch-prefix.test.ts": 51, + "src/shared/browser-annotation-viewport-lifecycle.test.ts": 199, + "src/shared/browser-annotation-viewport-live-target.test.ts": 187, + "src/shared/browser-client-automation-protocol.test.ts": 241, + "src/shared/browser-client-host-id-argument.test.ts": 36, + "src/shared/browser-client-host-protocol.test.ts": 356, "src/shared/browser-client-host-reconciliation-protocol.test.ts": 254, - "src/shared/browser-client-hosting-eligibility.test.ts": 67, - "src/shared/browser-cookie-import-sources.test.ts": 57, - "src/shared/browser-grab-types.test.ts": 56, - "src/shared/browser-network-tunnel-protocol.test.ts": 45, - "src/shared/browser-network-tunnel-stream-framing.test.ts": 1622, - "src/shared/browser-screencast-protocol.test.ts": 108, - "src/shared/browser-url.test.ts": 308, - "src/shared/bundled-ripgrep.test.ts": 37, - "src/shared/cheap-process-table-snapshot.test.ts": 77, - "src/shared/check-job-log-byte-cap.test.ts": 94, - "src/shared/check-job-log-retention.test.ts": 222, - "src/shared/check-job-log-tail-slice.test.ts": 71, - "src/shared/claimed-agent-pty-owner.test.ts": 71, - "src/shared/claude-agent-teams-tmux-compat.test.ts": 62, - "src/shared/claude-background-task-kind.test.ts": 42, - "src/shared/claude-background-task-status.test.ts": 260, - "src/shared/claude-model-list-probe.test.ts": 57, - "src/shared/claude-owed-notification-resource-contract.test.ts": 183, - "src/shared/claude-profile-activation-contract.test.ts": 68, - "src/shared/claude-shell-function.test.ts": 196, - "src/shared/claude-sign-in-link.test.ts": 47, - "src/shared/claude-statusline-rate-limits.test.ts": 47, - "src/shared/claude-subagent-roster.test.ts": 111, - "src/shared/claude-subagent-row-lifecycle.test.ts": 205, - "src/shared/claude-terminal-interrupt.test.ts": 54, - "src/shared/cli-argument-boundary.test.ts": 54, - "src/shared/cli-workspace-provenance.test.ts": 50, - "src/shared/client-environment-info.test.ts": 42, - "src/shared/clipboard-image.test.ts": 40, - "src/shared/clipboard-text.test.ts": 86, - "src/shared/closed-terminal-tab-tombstones.test.ts": 242, - "src/shared/cloud-service-url.test.ts": 45, - "src/shared/coalescing-keyed-runner.test.ts": 112, - "src/shared/codebuddy-agent.test.ts": 234, - "src/shared/codex-auth-errors.test.ts": 55, - "src/shared/codex-composer-ready-scanner.test.ts": 97, - "src/shared/codex-pet-sprite-defaults.test.ts": 70, - "src/shared/codex-reset-credit-attempt-ledger.test.ts": 213, - "src/shared/codex-reset-credit-scope.test.ts": 54, - "src/shared/codex-shared-server-command.test.ts": 65, - "src/shared/codex-startup-delivery.test.ts": 85, - "src/shared/codex-status-transcript-line.test.ts": 83, - "src/shared/codex-subagent-poll-scheduler.test.ts": 57, - "src/shared/codex-subagent-rollout-lifecycle.test.ts": 172, - "src/shared/codex-subagent-roster.test.ts": 60, - "src/shared/codex-subagent-transcript.test.ts": 100, - "src/shared/codex-turn-transcript.test.ts": 46, - "src/shared/combined-diff-file-tree-width.test.ts": 46, - "src/shared/command-code-output-retention.test.ts": 109, - "src/shared/command-code-output-status.test.ts": 205, - "src/shared/command-code-turn-boundary.test.ts": 42, - "src/shared/command-environment.test.ts": 86, - "src/shared/command-token-scanner.test.ts": 54, - "src/shared/commit-message-agent-spec.test.ts": 118, - "src/shared/commit-message-generation.test.ts": 62, - "src/shared/commit-message-omp.test.ts": 390, - "src/shared/commit-message-plan.test.ts": 117, - "src/shared/commit-message-prompt.test.ts": 87, - "src/shared/computer-awake-mode.test.ts": 43, - "src/shared/computer-use-error-recovery.test.ts": 59, - "src/shared/computer-use-key-spec.test.ts": 59, - "src/shared/constants.test.ts": 282, - "src/shared/contextual-tours.test.ts": 67, - "src/shared/conversation-name-action.test.ts": 263, - "src/shared/conversation-name-generation.test.ts": 50, - "src/shared/crash-reporting.test.ts": 80, - "src/shared/credit-count-format.test.ts": 58, - "src/shared/cross-platform-path-guards.test.ts": 943, - "src/shared/cross-platform-path.test.ts": 55, - "src/shared/currency-format.test.ts": 47, - "src/shared/daemon-adoption-telemetry.test.ts": 349, - "src/shared/doc-preview-file-access.test.ts": 93, - "src/shared/doc-preview-scheme.test.ts": 44, - "src/shared/draft-paste-ready-scanner-grok-trace-replay.test.ts": 75, - "src/shared/draft-paste-ready-scanner.test.ts": 92, - "src/shared/dsb-headless-command.test.ts": 55, - "src/shared/dsb-native-one-shot-recognition.test.ts": 89, - "src/shared/dsb-terminal-title.test.ts": 105, - "src/shared/dsh-launch-command.test.ts": 115, - "src/shared/dsh-real-cli.test.ts": 73, - "src/shared/dsh-startup.test.ts": 140, - "src/shared/dsh-terminal-title.test.ts": 90, - "src/shared/emoji-shortcode-catalog.lazy.test.ts": 79, - "src/shared/emulator-keyboard-frame.test.ts": 61, - "src/shared/emulator-touch-frame.test.ts": 45, - "src/shared/ephemeral-setup-terminal-worktree-id.test.ts": 55, - "src/shared/ephemeral-vm-recipe-checkout-mode.test.ts": 261, - "src/shared/ephemeral-vm-recipe-doctor.test.ts": 51, - "src/shared/ephemeral-vm-recipe-output-capture.test.ts": 260, - "src/shared/ephemeral-vm-recipe-process.test.ts": 180, - "src/shared/ephemeral-vm-recipe-repo-url.test.ts": 52, - "src/shared/ephemeral-vm-recipes.test.ts": 291, - "src/shared/ephemeral-vm-runtime-feature-sorting.test.ts": 350, - "src/shared/ephemeral-vm-runtime-store-rollback.test.ts": 548, - "src/shared/ephemeral-vm-runtime-store.test.ts": 314, - "src/shared/event-loop-yield.test.ts": 69, - "src/shared/execution-host-registry-shared-control-connect.test.ts": 82, - "src/shared/execution-host-registry.test.ts": 91, - "src/shared/execution-host.test.ts": 68, - "src/shared/export-let-function-initializer-ban.test.ts": 2472, - "src/shared/external-automation-jobs-file.test.ts": 73, - "src/shared/external-worktree-inbox.test.ts": 71, - "src/shared/feature-education-telemetry.test.ts": 47, - "src/shared/feature-interactions.test.ts": 44, - "src/shared/feature-tips.test.ts": 66, - "src/shared/feature-wall-tour-depth.test.ts": 44, - "src/shared/feedback-image-limits.test.ts": 47, - "src/shared/file-explorer-display-root.test.ts": 53, - "src/shared/file-inventory-budget.test.ts": 54, - "src/shared/file-link-location.test.ts": 56, - "src/shared/file-name-sort.test.ts": 51, - "src/shared/fish-binary-requirement.test.ts": 46, - "src/shared/folder-workspace-execution-host.test.ts": 84, - "src/shared/folder-workspace-worktree.test.ts": 89, - "src/shared/folder-workspaces.test.ts": 82, - "src/shared/foreground-process-ancestry-parity.test.ts": 259, - "src/shared/foreground-process-selection.test.ts": 85, - "src/shared/foreground-wrapper-agent.test.ts": 87, - "src/shared/freebuff-launch.test.ts": 147, - "src/shared/freebuff-startup.test.ts": 120, - "src/shared/generated-code-path.test.ts": 53, - "src/shared/git-admission-node-compatibility.test.ts": 73, - "src/shared/git-binary-compatibility.test.ts": 123, - "src/shared/git-blob-absence.test.ts": 63, - "src/shared/git-branch-cleanup.test.ts": 73, - "src/shared/git-branch-compare-head.test.ts": 47, - "src/shared/git-branch-line-total-soft-deadline.test.ts": 3606, - "src/shared/git-branch-line-total.test.ts": 102, - "src/shared/git-capability-cache.test.ts": 59, - "src/shared/git-change-list.test.ts": 115, - "src/shared/git-check-ignore-stdio.test.ts": 47, - "src/shared/git-clone-failure-message.test.ts": 55, - "src/shared/git-config-snapshot-runner.test.ts": 68, - "src/shared/git-configured-branch-target.test.ts": 55, - "src/shared/git-cquoted-path.test.ts": 45, - "src/shared/git-diff-transport-budget.test.ts": 483, - "src/shared/git-discard-path-safety.test.ts": 68, - "src/shared/git-exec-mutation.test.ts": 40, - "src/shared/git-fetch-head-capability.test.ts": 45, - "src/shared/git-fetch-head-lock-key-derivation.test.ts": 123, - "src/shared/git-fetch-head-lock.test.ts": 162, - "src/shared/git-fork-sync.test.ts": 85, - "src/shared/git-history-graph.test.ts": 70, - "src/shared/git-history-message-allocation.test.ts": 49, - "src/shared/git-history-real-repository.test.ts": 465, - "src/shared/git-history-ref-display.test.ts": 63, - "src/shared/git-history.test.ts": 69, - "src/shared/git-lock-contention.test.ts": 48, - "src/shared/git-merge-tree-capability.test.ts": 53, - "src/shared/git-metadata-path.test.ts": 70, - "src/shared/git-push-target-validation.test.ts": 50, - "src/shared/git-remote-error.test.ts": 74, - "src/shared/git-remote-identity.test.ts": 61, - "src/shared/git-remote-url-index.test.ts": 58, - "src/shared/git-resolution-config-snapshot.test.ts": 73, - "src/shared/git-rev-list-output.test.ts": 58, - "src/shared/git-ssh-policy-env.test.ts": 204, - "src/shared/git-status-branch-line-total-cache.test.ts": 83, - "src/shared/git-status-conflict-entries.test.ts": 60, - "src/shared/git-status-limit.test.ts": 46, - "src/shared/git-status-line-stat-inputs.test.ts": 51, - "src/shared/git-status-line-stats-cache.test.ts": 82, - "src/shared/git-status-read-lease-expiry.test.ts": 79, - "src/shared/git-status-upstream-ref.test.ts": 58, - "src/shared/git-tracked-pathspecs.test.ts": 163, - "src/shared/git-uncommitted-line-stats.test.ts": 94, - "src/shared/git-upstream-status.test.ts": 53, - "src/shared/git-worktree-admin.test.ts": 130, - "src/shared/git-worktree-operation-lock.test.ts": 59, - "src/shared/github/account-binding.test.ts": 65, - "src/shared/github/api-availability.test.ts": 54, - "src/shared/github/owner-slug.test.ts": 49, - "src/shared/github/project-board-columns.test.ts": 65, - "src/shared/github/project-identity.test.ts": 46, - "src/shared/github/project-ref-input.test.ts": 52, - "src/shared/github/project-roadmap-timeline.test.ts": 70, - "src/shared/github/pull-request-auto-merge-availability.test.ts": 59, - "src/shared/github/pull-request-for-branch-outcome.test.ts": 46, - "src/shared/github/pull-request-merge-methods.test.ts": 49, - "src/shared/github/repository-identity-key.test.ts": 46, - "src/shared/gitlab-job-log-excerpt.test.ts": 80, - "src/shared/gitlab-job-trace-check-details.test.ts": 67, - "src/shared/gitlab-pipeline-checks.test.ts": 53, - "src/shared/gitlab-projects.test.ts": 50, - "src/shared/grok-model-list-probe.test.ts": 69, - "src/shared/grok-session-paths.test.ts": 93, - "src/shared/handled-wire-discriminant.test.ts": 48, - "src/shared/harness-injected-user-turns.test.ts": 64, - "src/shared/hermes-run-ref-retention.test.ts": 66, - "src/shared/hermes-session-run-index.test.ts": 293, - "src/shared/hermes-startup-query.windows.test.ts": 49, - "src/shared/home-or-filesystem-root.test.ts": 50, - "src/shared/hook-command-source-policy.test.ts": 43, - "src/shared/host-authority.test.ts": 66, - "src/shared/host-balanced-listing-scaling.test.ts": 74, - "src/shared/host-display-resolution.test.ts": 59, - "src/shared/host-operation-ownership.test.ts": 758, - "src/shared/host-setting-overrides.test.ts": 53, - "src/shared/hosted-review-creation-providers.test.ts": 52, - "src/shared/hosted-review-github.test.ts": 98, - "src/shared/hosted-review-refs.test.ts": 144, - "src/shared/image-data-uri.test.ts": 57, - "src/shared/image-paste-following-text.test.ts": 47, - "src/shared/import-copy-name.test.ts": 56, - "src/shared/in-flight-promise-dedupe.test.ts": 80, - "src/shared/jcode-runtime-dir.test.ts": 54, - "src/shared/jcode-session-files.test.ts": 58, - "src/shared/jcode-terminal-title.test.ts": 50, - "src/shared/jira-issue-url.test.ts": 47, - "src/shared/jira-search-input-jql.test.ts": 71, - "src/shared/json-text-structure-limit.test.ts": 70, - "src/shared/json-token-reader.test.ts": 309, - "src/shared/keybindings-conflicts.test.ts": 97, - "src/shared/keybindings-default-bindings.test.ts": 115, - "src/shared/keybindings-digit-index.test.ts": 96, - "src/shared/keybindings-double-tap.test.ts": 83, + "src/shared/browser-client-hosting-eligibility.test.ts": 244, + "src/shared/browser-cookie-import-sources.test.ts": 41, + "src/shared/browser-grab-types.test.ts": 37, + "src/shared/browser-network-tunnel-protocol.test.ts": 39, + "src/shared/browser-network-tunnel-stream-framing.test.ts": 1577, + "src/shared/browser-screencast-protocol.test.ts": 40, + "src/shared/browser-url.test.ts": 319, + "src/shared/bundled-ripgrep.test.ts": 24, + "src/shared/cheap-process-table-snapshot.test.ts": 46, + "src/shared/check-job-log-byte-cap.test.ts": 51, + "src/shared/check-job-log-excerpt.test.ts": 116, + "src/shared/check-job-log-marker-candidates.test.ts": 135, + "src/shared/check-job-log-performance-harness.test.ts": 514, + "src/shared/check-job-log-retention.test.ts": 431, + "src/shared/check-job-log-tail-slice.test.ts": 62, + "src/shared/check-job-log-text-performance.test.ts": 93, + "src/shared/claimed-agent-pty-owner.test.ts": 55, + "src/shared/claude-agent-teams-tmux-compat.test.ts": 35, + "src/shared/claude-background-task-kind.test.ts": 34, + "src/shared/claude-background-task-status.test.ts": 224, + "src/shared/claude-model-list-probe.test.ts": 49, + "src/shared/claude-owed-notification-resource-contract.test.ts": 162, + "src/shared/claude-profile-activation-contract.test.ts": 188, + "src/shared/claude-shell-function.test.ts": 184, + "src/shared/claude-sign-in-link.test.ts": 29, + "src/shared/claude-statusline-rate-limits.test.ts": 38, + "src/shared/claude-subagent-roster.test.ts": 67, + "src/shared/claude-subagent-row-lifecycle.test.ts": 195, + "src/shared/claude-terminal-interrupt.test.ts": 36, + "src/shared/cli-argument-boundary.test.ts": 47, + "src/shared/cli-workspace-provenance.test.ts": 29, + "src/shared/client-environment-info.test.ts": 30, + "src/shared/clipboard-image.test.ts": 32, + "src/shared/clipboard-text.test.ts": 68, + "src/shared/closed-terminal-tab-tombstones.test.ts": 219, + "src/shared/cloud-service-url.test.ts": 30, + "src/shared/coalescing-keyed-runner.test.ts": 99, + "src/shared/codebuddy-agent.test.ts": 219, + "src/shared/codex-auth-errors.test.ts": 44, + "src/shared/codex-capacity-notice.test.ts": 58, + "src/shared/codex-cli-installation.test.ts": 44, + "src/shared/codex-composer-ready-scanner.test.ts": 79, + "src/shared/codex-pet-sprite-defaults.test.ts": 61, + "src/shared/codex-reset-credit-attempt-ledger.test.ts": 224, + "src/shared/codex-reset-credit-scope.test.ts": 38, + "src/shared/codex-shared-server-command.test.ts": 59, + "src/shared/codex-startup-delivery.test.ts": 58, + "src/shared/codex-status-transcript-line.test.ts": 58, + "src/shared/codex-subagent-poll-scheduler.test.ts": 35, + "src/shared/codex-subagent-rollout-lifecycle.test.ts": 152, + "src/shared/codex-subagent-roster.test.ts": 46, + "src/shared/codex-subagent-transcript.test.ts": 92, + "src/shared/codex-turn-transcript.test.ts": 31, + "src/shared/combined-diff-file-tree-width.test.ts": 34, + "src/shared/command-code-output-retention.test.ts": 81, + "src/shared/command-code-output-status.test.ts": 178, + "src/shared/command-code-turn-boundary.test.ts": 27, + "src/shared/command-environment.test.ts": 69, + "src/shared/command-token-scanner.test.ts": 38, + "src/shared/commit-message-agent-spec.test.ts": 138, + "src/shared/commit-message-generation.test.ts": 33, + "src/shared/commit-message-omp.test.ts": 286, + "src/shared/commit-message-plan.test.ts": 103, + "src/shared/commit-message-prompt.test.ts": 63, + "src/shared/computer-awake-mode.test.ts": 27, + "src/shared/computer-use-error-recovery.test.ts": 40, + "src/shared/computer-use-key-spec.test.ts": 41, + "src/shared/constants.test.ts": 286, + "src/shared/contextual-tours.test.ts": 35, + "src/shared/conversation-name-action.test.ts": 232, + "src/shared/conversation-name-generation.test.ts": 40, + "src/shared/crash-reporting.test.ts": 68, + "src/shared/credit-count-format.test.ts": 28, + "src/shared/cross-platform-path-guards.test.ts": 833, + "src/shared/cross-platform-path.test.ts": 63, + "src/shared/currency-format.test.ts": 35, + "src/shared/daemon-adoption-telemetry.test.ts": 275, + "src/shared/darwin-process-table-capture.test.ts": 52, + "src/shared/darwin-process-table-inventory-drain.test.ts": 55, + "src/shared/darwin-terminal-device-stats.test.ts": 37, + "src/shared/darwin-terminal-names-real-ps.test.ts": 29, + "src/shared/darwin-terminal-names.test.ts": 278, + "src/shared/default-agent-settings.test.ts": 32, + "src/shared/device-key-proof.test.ts": 85, + "src/shared/doc-preview-file-access.test.ts": 76, + "src/shared/doc-preview-scheme.test.ts": 34, + "src/shared/draft-paste-ready-scanner-grok-trace-replay.test.ts": 63, + "src/shared/draft-paste-ready-scanner.test.ts": 94, + "src/shared/dsb-headless-command.test.ts": 51, + "src/shared/dsb-native-one-shot-recognition.test.ts": 81, + "src/shared/dsb-terminal-title.test.ts": 108, + "src/shared/dsh-launch-command.test.ts": 78, + "src/shared/dsh-real-cli.test.ts": 57, + "src/shared/dsh-startup.test.ts": 109, + "src/shared/dsh-terminal-title.test.ts": 44, + "src/shared/electron-isolated-profile-switches.test.ts": 29, + "src/shared/emoji-shortcode-catalog.lazy.test.ts": 84, + "src/shared/emulator-keyboard-frame.test.ts": 47, + "src/shared/emulator-touch-frame.test.ts": 25, + "src/shared/ephemeral-setup-terminal-worktree-id.test.ts": 47, + "src/shared/ephemeral-vm-recipe-checkout-mode.test.ts": 225, + "src/shared/ephemeral-vm-recipe-doctor.test.ts": 37, + "src/shared/ephemeral-vm-recipe-output-capture.test.ts": 257, + "src/shared/ephemeral-vm-recipe-process.test.ts": 172, + "src/shared/ephemeral-vm-recipe-repo-url.test.ts": 32, + "src/shared/ephemeral-vm-recipes.test.ts": 251, + "src/shared/ephemeral-vm-runtime-feature-sorting.test.ts": 308, + "src/shared/ephemeral-vm-runtime-store-rollback.test.ts": 500, + "src/shared/ephemeral-vm-runtime-store.test.ts": 224, + "src/shared/event-loop-yield.test.ts": 64, + "src/shared/execution-host-registry-shared-control-connect.test.ts": 238, + "src/shared/execution-host-registry.test.ts": 224, + "src/shared/execution-host.test.ts": 40, + "src/shared/export-let-function-initializer-ban.test.ts": 2090, + "src/shared/external-automation-jobs-file.test.ts": 64, + "src/shared/external-worktree-inbox.test.ts": 45, + "src/shared/feature-education-telemetry.test.ts": 28, + "src/shared/feature-interactions.test.ts": 40, + "src/shared/feature-tips.test.ts": 45, + "src/shared/feature-wall-tour-depth.test.ts": 33, + "src/shared/feedback-image-limits.test.ts": 34, + "src/shared/file-explorer-display-root.test.ts": 49, + "src/shared/file-inventory-budget.test.ts": 24, + "src/shared/file-link-location.test.ts": 41, + "src/shared/file-name-sort.test.ts": 35, + "src/shared/fish-binary-requirement.test.ts": 37, + "src/shared/folder-workspace-execution-host.test.ts": 65, + "src/shared/folder-workspace-worktree.test.ts": 87, + "src/shared/folder-workspaces.test.ts": 67, + "src/shared/foreground-process-ancestry-parity.test.ts": 270, + "src/shared/foreground-process-selection.test.ts": 74, + "src/shared/foreground-wrapper-agent.test.ts": 78, + "src/shared/freebuff-launch.test.ts": 130, + "src/shared/freebuff-startup.test.ts": 118, + "src/shared/generated-code-path.test.ts": 35, + "src/shared/git-admission-node-compatibility.test.ts": 48, + "src/shared/git-binary-compatibility.test.ts": 132, + "src/shared/git-blob-absence.test.ts": 51, + "src/shared/git-branch-cleanup.test.ts": 55, + "src/shared/git-branch-compare-head.test.ts": 31, + "src/shared/git-branch-line-total-soft-deadline.test.ts": 63, + "src/shared/git-branch-line-total.test.ts": 105, + "src/shared/git-capability-cache.test.ts": 33, + "src/shared/git-change-list.test.ts": 129, + "src/shared/git-check-ignore-stdio.test.ts": 37, + "src/shared/git-clone-failure-message.test.ts": 38, + "src/shared/git-config-snapshot-runner.test.ts": 51, + "src/shared/git-configured-branch-target.test.ts": 45, + "src/shared/git-cquoted-path.test.ts": 39, + "src/shared/git-diff-transport-budget.test.ts": 461, + "src/shared/git-discard-path-safety.test.ts": 55, + "src/shared/git-fetch-head-capability.test.ts": 28, + "src/shared/git-fetch-head-lock-key-derivation.test.ts": 107, + "src/shared/git-fetch-head-lock.test.ts": 150, + "src/shared/git-fork-sync.test.ts": 59, + "src/shared/git-history-graph.test.ts": 58, + "src/shared/git-history-message-allocation.test.ts": 52, + "src/shared/git-history-real-repository.test.ts": 431, + "src/shared/git-history-ref-display.test.ts": 48, + "src/shared/git-history.test.ts": 67, + "src/shared/git-lock-contention.test.ts": 32, + "src/shared/git-merge-tree-capability.test.ts": 38, + "src/shared/git-metadata-path.test.ts": 66, + "src/shared/git-push-target-validation.test.ts": 32, + "src/shared/git-remote-error.test.ts": 67, + "src/shared/git-remote-identity.test.ts": 33, + "src/shared/git-remote-url-index.test.ts": 50, + "src/shared/git-resolution-config-snapshot.test.ts": 58, + "src/shared/git-rev-list-output.test.ts": 39, + "src/shared/git-ssh-policy-env.test.ts": 168, + "src/shared/git-status-branch-line-total-cache.test.ts": 54, + "src/shared/git-status-conflict-entries.test.ts": 35, + "src/shared/git-status-limit.test.ts": 43, + "src/shared/git-status-line-stat-inputs.test.ts": 36, + "src/shared/git-status-line-stats-cache.test.ts": 53, + "src/shared/git-status-read-lease-expiry.test.ts": 63, + "src/shared/git-status-upstream-ref.test.ts": 45, + "src/shared/git-tracked-pathspecs.test.ts": 119, + "src/shared/git-uncommitted-line-stats.test.ts": 75, + "src/shared/git-upstream-status.test.ts": 49, + "src/shared/git-worktree-admin.test.ts": 131, + "src/shared/git-worktree-operation-lock.test.ts": 28, + "src/shared/github/account-binding.test.ts": 26, + "src/shared/github/api-availability.test.ts": 33, + "src/shared/github/owner-slug.test.ts": 30, + "src/shared/github/project-board-columns.test.ts": 44, + "src/shared/github/project-identity.test.ts": 24, + "src/shared/github/project-ref-input.test.ts": 39, + "src/shared/github/project-roadmap-timeline.test.ts": 66, + "src/shared/github/pull-request-auto-merge-availability.test.ts": 33, + "src/shared/github/pull-request-for-branch-outcome.test.ts": 33, + "src/shared/github/pull-request-merge-methods.test.ts": 29, + "src/shared/github/repository-identity-key.test.ts": 32, + "src/shared/gitlab-job-log-excerpt.test.ts": 89, + "src/shared/gitlab-job-trace-check-details.test.ts": 57, + "src/shared/gitlab-pipeline-checks.test.ts": 40, + "src/shared/gitlab-projects.test.ts": 31, + "src/shared/grok-model-list-probe.test.ts": 63, + "src/shared/grok-session-paths.test.ts": 120, + "src/shared/handled-wire-discriminant.test.ts": 35, + "src/shared/harness-injected-user-turns.test.ts": 50, + "src/shared/hermes-startup-query.windows.test.ts": 39, + "src/shared/home-or-filesystem-root.test.ts": 36, + "src/shared/hook-command-source-policy.test.ts": 25, + "src/shared/host-authority.test.ts": 35, + "src/shared/host-automatic-work-budget.test.ts": 39, + "src/shared/host-balanced-listing-scaling.test.ts": 72, + "src/shared/host-display-resolution.test.ts": 50, + "src/shared/host-operation-ownership.test.ts": 746, + "src/shared/host-setting-overrides.test.ts": 50, + "src/shared/host-spawn-admission.test.ts": 170, + "src/shared/hosted-review-creation-providers.test.ts": 26, + "src/shared/hosted-review-github.test.ts": 28, + "src/shared/hosted-review-refs.test.ts": 114, + "src/shared/image-data-uri.test.ts": 41, + "src/shared/image-paste-following-text.test.ts": 29, + "src/shared/import-copy-name.test.ts": 39, + "src/shared/in-flight-promise-dedupe.test.ts": 62, + "src/shared/jcode-runtime-dir.test.ts": 37, + "src/shared/jcode-session-files.test.ts": 52, + "src/shared/jcode-terminal-title.test.ts": 28, + "src/shared/jira-issue-summary-generation.test.ts": 58, + "src/shared/jira-issue-url.test.ts": 36, + "src/shared/jira-jql-predicate.test.ts": 38, + "src/shared/jira-preset-jql.test.ts": 159, + "src/shared/jira-project-reference.test.ts": 278, + "src/shared/jira-review-title.test.ts": 93, + "src/shared/jira-saved-filters.test.ts": 34, + "src/shared/jira-search-input-jql.test.ts": 68, + "src/shared/jira-transition-fields.test.ts": 41, + "src/shared/json-text-structure-limit.test.ts": 63, + "src/shared/json-token-reader.test.ts": 224, + "src/shared/keybindings-conflicts.test.ts": 83, + "src/shared/keybindings-default-bindings.test.ts": 91, + "src/shared/keybindings-digit-index.test.ts": 106, + "src/shared/keybindings-double-tap.test.ts": 64, "src/shared/keybindings-keyboard-layout.test.ts": 72, - "src/shared/keybindings-parse-cache.test.ts": 70, - "src/shared/keybindings-parsing.test.ts": 91, - "src/shared/keybindings-terminal-context.test.ts": 76, - "src/shared/keybindings-unassigned-actions.test.ts": 85, - "src/shared/kiro-home.test.ts": 46, - "src/shared/linear/inline-media.test.ts": 50, - "src/shared/linear/issue-attribute-filter.test.ts": 55, - "src/shared/linear/issue-view-resume-state.test.ts": 182, - "src/shared/linear/links.test.ts": 68, - "src/shared/linear/workspace-types.test.ts": 59, - "src/shared/linux-proc-port-scan-limits.test.ts": 53, - "src/shared/local-account-runtime.test.ts": 279, - "src/shared/local-build-compatibility.test.ts": 58, - "src/shared/local-windows-terminal-runtime.test.ts": 52, - "src/shared/localhost-worktree-labels.test.ts": 58, - "src/shared/loose-ref-count.test.ts": 139, - "src/shared/machine-name.test.ts": 44, - "src/shared/macos-symbolic-hotkeys.test.ts": 75, - "src/shared/main-agent-status-parity.test.ts": 224, - "src/shared/managed-agent-command-token.test.ts": 53, - "src/shared/managed-orcad-execution-host.test.ts": 85, - "src/shared/managed-orcad-ssh-owner.test.ts": 56, - "src/shared/manual-repo-order.test.ts": 56, - "src/shared/map-settled-with-concurrency.test.ts": 54, - "src/shared/map-with-concurrency.test.ts": 106, - "src/shared/markdown-document-listing-limits.test.ts": 45, - "src/shared/markdown-toc-panel-width.test.ts": 48, - "src/shared/mcp-config.test.ts": 95, - "src/shared/mobile-e2ee-v2-contract.test.ts": 59, - "src/shared/mobile-e2ee-v2-framing.test.ts": 65, - "src/shared/mobile-file-directory-limit.test.ts": 48, - "src/shared/mobile-markdown-document.test.ts": 61, - "src/shared/mobile-pairing-connection-mode.test.ts": 59, - "src/shared/mobile-pairing-custom-address.test.ts": 62, - "src/shared/mobile-push-contract.test.ts": 48, - "src/shared/mobile-relay-close-codes.test.ts": 42, - "src/shared/mobile-relay-mint-failure.test.ts": 43, - "src/shared/mobile-relay-pairing-offer.test.ts": 246, - "src/shared/mobile-relay-phone-protocol.test.ts": 223, - "src/shared/mobile-web-bundle/bundle-range-rpc-contract.test.ts": 241, - "src/shared/mobile-web-bundle/bundle-rpc-contract.test.ts": 254, - "src/shared/mobile-web-bundle/manifest-contract.test.ts": 284, - "src/shared/model-id-label.test.ts": 46, - "src/shared/modifier-double-tap-detector.test.ts": 62, - "src/shared/muse-session-log.test.ts": 63, - "src/shared/native-chat-activity-tail-line.test.ts": 268, - "src/shared/native-chat-agent-profiles.test.ts": 58, - "src/shared/native-chat-agent-support.test.ts": 54, - "src/shared/native-chat-appearance-settings.test.ts": 56, - "src/shared/native-chat-ask-fifo.test.ts": 218, - "src/shared/native-chat-ask-row.test.ts": 62, - "src/shared/native-chat-ask.test.ts": 69, - "src/shared/native-chat-background-task-row.test.ts": 60, - "src/shared/native-chat-command-envelope.test.ts": 49, - "src/shared/native-chat-command-output.test.ts": 55, - "src/shared/native-chat-context-usage.test.ts": 52, - "src/shared/native-chat-cut-turn-notice.test.ts": 342, - "src/shared/native-chat-edit-normalize.test.ts": 326, - "src/shared/native-chat-href-routing.test.ts": 58, - "src/shared/native-chat-image-transcript-markers.test.ts": 85, - "src/shared/native-chat-live-line.test.ts": 74, - "src/shared/native-chat-merge-pairs.test.ts": 343, - "src/shared/native-chat-orca-stop-cut.test.ts": 273, - "src/shared/native-chat-provider-frame-summary.test.ts": 49, - "src/shared/native-chat-provider-retry-runs.test.ts": 340, - "src/shared/native-chat-reasoning-order.test.ts": 283, - "src/shared/native-chat-reasoning-row.test.ts": 78, - "src/shared/native-chat-scope-cache.test.ts": 47, - "src/shared/native-chat-session-option-commands.test.ts": 79, - "src/shared/native-chat-session-option-defaults.test.ts": 95, - "src/shared/native-chat-session-option-snapshot.test.ts": 126, - "src/shared/native-chat-session-option-state.test.ts": 57, - "src/shared/native-chat-shell-environment.test.ts": 53, - "src/shared/native-chat-slash-commands.omp.test.ts": 48, - "src/shared/native-chat-slash-commands.test.ts": 59, - "src/shared/native-chat-stopped-send-turn.test.ts": 336, - "src/shared/native-chat-stream-unsubscribe.test.ts": 50, - "src/shared/native-chat-streaming.test.ts": 46, - "src/shared/native-chat-subagent-group-header.test.ts": 60, - "src/shared/native-chat-subagent-summary.test.ts": 58, - "src/shared/native-chat-task-list.test.ts": 58, - "src/shared/native-chat-tool-activity.test.ts": 57, - "src/shared/native-chat-tool-attribution-allocation.test.ts": 102, - "src/shared/native-chat-tool-fold-attribution.test.ts": 52, - "src/shared/native-chat-tool-icon.test.ts": 73, - "src/shared/native-chat-tool-identity.test.ts": 66, - "src/shared/native-chat-tool-line-identity.test.ts": 42, - "src/shared/native-chat-tool-pair-limit.test.ts": 128, - "src/shared/native-chat-tool-pairing.performance.test.ts": 500, - "src/shared/native-chat-tool-pairing.test.ts": 53, - "src/shared/native-chat-tool-preview-prefix.test.ts": 52, - "src/shared/native-chat-tool-preview-scan.test.ts": 52, - "src/shared/native-chat-tool-result-orphans.test.ts": 282, - "src/shared/native-chat-tool-run-outcome.test.ts": 81, - "src/shared/native-chat-tool-run-sentence.test.ts": 64, - "src/shared/native-chat-tool-summary.test.ts": 80, - "src/shared/native-chat-transcript-projection.test.ts": 268, - "src/shared/native-chat-transcript-retention.test.ts": 57, - "src/shared/native-chat-turn-activity.test.ts": 230, - "src/shared/native-chat-turn-fold.test.ts": 57, - "src/shared/native-chat-turn-grouping.test.ts": 231, - "src/shared/native-chat-turn-membership-derived-tool-row.test.ts": 340, - "src/shared/native-chat-turn-membership.test.ts": 287, - "src/shared/native-chat-turn-status.test.ts": 75, - "src/shared/native-chat-types.test.ts": 55, - "src/shared/native-chat-unverifiable-turn-status.test.ts": 257, - "src/shared/native-chat-upgrade-tip-audience.test.ts": 45, - "src/shared/native-chat-visual-directive.test.ts": 64, - "src/shared/native-chat-visual-height-governor.test.ts": 289, - "src/shared/native-chat-visual-shell.test.ts": 274, - "src/shared/native-chat-work-run.test.ts": 310, - "src/shared/native-file-drop.test.ts": 60, - "src/shared/nested-repo-telemetry-schema.test.ts": 299, - "src/shared/nested-repo-telemetry.test.ts": 50, - "src/shared/nested-worker-depth.test.ts": 51, - "src/shared/network-proxy.test.ts": 42, + "src/shared/keybindings-parse-cache.test.ts": 68, + "src/shared/keybindings-parsing.test.ts": 69, + "src/shared/keybindings-terminal-context.test.ts": 57, + "src/shared/keybindings-unassigned-actions.test.ts": 74, + "src/shared/kiro-home.test.ts": 33, + "src/shared/linear/inline-media.test.ts": 35, + "src/shared/linear/issue-attribute-filter.test.ts": 43, + "src/shared/linear/issue-view-resume-state.test.ts": 71, + "src/shared/linear/links.test.ts": 47, + "src/shared/linear/workspace-types.test.ts": 39, + "src/shared/linux-wayland-session.test.ts": 34, + "src/shared/local-account-runtime.test.ts": 275, + "src/shared/local-build-compatibility.test.ts": 38, + "src/shared/local-windows-terminal-runtime.test.ts": 25, + "src/shared/localhost-worktree-labels.test.ts": 34, + "src/shared/loose-ref-count.test.ts": 113, + "src/shared/machine-name.test.ts": 26, + "src/shared/macos-symbolic-hotkeys.test.ts": 88, + "src/shared/main-agent-status-parity.test.ts": 308, + "src/shared/managed-agent-command-token.test.ts": 28, + "src/shared/managed-orcad-execution-host.test.ts": 269, + "src/shared/managed-orcad-ssh-owner.test.ts": 29, + "src/shared/manual-repo-order.test.ts": 32, + "src/shared/map-settled-with-concurrency.test.ts": 51, + "src/shared/map-with-concurrency.test.ts": 88, + "src/shared/markdown-document-listing-limits.test.ts": 36, + "src/shared/markdown-toc-panel-width.test.ts": 34, + "src/shared/mcp-config.test.ts": 73, + "src/shared/mobile-e2ee-v2-contract.test.ts": 42, + "src/shared/mobile-e2ee-v2-framing.test.ts": 39, + "src/shared/mobile-e2ee-v2-node-key-schedule.test.ts": 101, + "src/shared/mobile-markdown-document.test.ts": 39, + "src/shared/mobile-pairing-connection-mode.test.ts": 34, + "src/shared/mobile-pairing-custom-address.test.ts": 36, + "src/shared/mobile-push-contract.test.ts": 26, + "src/shared/mobile-relay-close-codes.test.ts": 39, + "src/shared/mobile-relay-mint-failure.test.ts": 28, + "src/shared/mobile-relay-pairing-offer.test.ts": 294, + "src/shared/mobile-relay-phone-protocol.test.ts": 241, + "src/shared/mobile-web-bundle/bundle-range-rpc-contract.test.ts": 245, + "src/shared/mobile-web-bundle/bundle-rpc-contract.test.ts": 256, + "src/shared/mobile-web-bundle/manifest-contract.test.ts": 336, + "src/shared/model-id-label.test.ts": 39, + "src/shared/modifier-double-tap-detector.test.ts": 37, + "src/shared/muse-session-log.test.ts": 60, + "src/shared/native-chat-activity-tail-line.test.ts": 256, + "src/shared/native-chat-agent-profiles.test.ts": 48, + "src/shared/native-chat-agent-support.test.ts": 33, + "src/shared/native-chat-appearance-settings.test.ts": 37, + "src/shared/native-chat-ask-fifo.test.ts": 216, + "src/shared/native-chat-ask-row.test.ts": 56, + "src/shared/native-chat-ask.test.ts": 51, + "src/shared/native-chat-background-task-row.test.ts": 45, + "src/shared/native-chat-command-envelope.test.ts": 37, + "src/shared/native-chat-command-output.test.ts": 47, + "src/shared/native-chat-context-usage.test.ts": 35, + "src/shared/native-chat-cut-turn-notice.test.ts": 364, + "src/shared/native-chat-edit-normalize.test.ts": 419, + "src/shared/native-chat-href-routing.test.ts": 64, + "src/shared/native-chat-image-transcript-markers.test.ts": 83, + "src/shared/native-chat-live-line.test.ts": 67, + "src/shared/native-chat-merge-pairs.test.ts": 339, + "src/shared/native-chat-orca-stop-cut.test.ts": 243, + "src/shared/native-chat-provider-frame-summary.test.ts": 36, + "src/shared/native-chat-provider-retry-runs.test.ts": 322, + "src/shared/native-chat-reasoning-order.test.ts": 269, + "src/shared/native-chat-reasoning-row.test.ts": 53, + "src/shared/native-chat-scope-cache.test.ts": 36, + "src/shared/native-chat-session-option-commands.test.ts": 81, + "src/shared/native-chat-session-option-defaults.test.ts": 70, + "src/shared/native-chat-session-option-snapshot.test.ts": 104, + "src/shared/native-chat-session-option-state.test.ts": 50, + "src/shared/native-chat-shell-environment.test.ts": 32, + "src/shared/native-chat-slash-commands.omp.test.ts": 38, + "src/shared/native-chat-slash-commands.test.ts": 45, + "src/shared/native-chat-stopped-send-turn.test.ts": 348, + "src/shared/native-chat-stream-unsubscribe.test.ts": 59, + "src/shared/native-chat-streaming.test.ts": 34, + "src/shared/native-chat-subagent-group-header.test.ts": 38, + "src/shared/native-chat-subagent-items.test.ts": 274, + "src/shared/native-chat-subagent-summary.test.ts": 54, + "src/shared/native-chat-task-list.test.ts": 45, + "src/shared/native-chat-tool-activity.test.ts": 49, + "src/shared/native-chat-tool-attribution-allocation.test.ts": 112, + "src/shared/native-chat-tool-fold-attribution.test.ts": 39, + "src/shared/native-chat-tool-icon.test.ts": 83, + "src/shared/native-chat-tool-identity.test.ts": 64, + "src/shared/native-chat-tool-line-identity.test.ts": 33, + "src/shared/native-chat-tool-pair-limit.test.ts": 75, + "src/shared/native-chat-tool-pairing.performance.test.ts": 463, + "src/shared/native-chat-tool-pairing.test.ts": 41, + "src/shared/native-chat-tool-preview-prefix.test.ts": 31, + "src/shared/native-chat-tool-preview-scan.test.ts": 37, + "src/shared/native-chat-tool-result-orphans.test.ts": 294, + "src/shared/native-chat-tool-run-outcome.test.ts": 58, + "src/shared/native-chat-tool-run-sentence.test.ts": 52, + "src/shared/native-chat-tool-summary.test.ts": 61, + "src/shared/native-chat-transcript-projection.test.ts": 250, + "src/shared/native-chat-transcript-retention.test.ts": 56, + "src/shared/native-chat-turn-activity.test.ts": 216, + "src/shared/native-chat-turn-fold.test.ts": 42, + "src/shared/native-chat-turn-grouping.test.ts": 212, + "src/shared/native-chat-turn-membership-derived-tool-row.test.ts": 328, + "src/shared/native-chat-turn-membership.test.ts": 253, + "src/shared/native-chat-turn-status.test.ts": 69, + "src/shared/native-chat-unverifiable-turn-status.test.ts": 245, + "src/shared/native-chat-upgrade-tip-audience.test.ts": 31, + "src/shared/native-chat-visual-directive.test.ts": 62, + "src/shared/native-chat-visual-height-governor.test.ts": 236, + "src/shared/native-chat-visual-shell.test.ts": 247, + "src/shared/native-chat-work-run.test.ts": 278, + "src/shared/native-file-drop.test.ts": 65, + "src/shared/nested-repo-telemetry-schema.test.ts": 261, + "src/shared/nested-repo-telemetry.test.ts": 40, + "src/shared/nested-worker-depth.test.ts": 35, + "src/shared/network-proxy.test.ts": 33, "src/shared/network/manual-address.test.ts": 60, - "src/shared/network/server-share-address.test.ts": 58, - "src/shared/new-workspace-dialog-repo.test.ts": 51, - "src/shared/new-workspace/smart-workspace-linear-intent.test.ts": 67, - "src/shared/new-workspace/smart-workspace-url-source-results.test.ts": 77, - "src/shared/new-workspace/work-item-lookup-text.test.ts": 59, - "src/shared/new-workspace/workspace-source.test.ts": 65, - "src/shared/new-workspace/worktree-create-retry-policy.test.ts": 49, - "src/shared/node-bounded-file-reader-sync.test.ts": 53, - "src/shared/node-bounded-file-reader.test.ts": 68, - "src/shared/node-bounded-json-stringify.test.ts": 77, - "src/shared/node-file-content-equality.test.ts": 44, - "src/shared/node-pty-spawn-helper.test.ts": 48, - "src/shared/node-readable-text.test.ts": 72, - "src/shared/node-source-copy-content-equality.test.ts": 65, - "src/shared/notebook-venv-location.test.ts": 46, - "src/shared/nul-delimited-fields.test.ts": 45, - "src/shared/nvm-default-alias.test.ts": 90, - "src/shared/omp-draft-launch-shell.test.ts": 247, - "src/shared/omp-draft-launch-windows.test.ts": 122, - "src/shared/omp-fresh-launch-shell.test.ts": 176, - "src/shared/omp-fresh-launch.test.ts": 130, - "src/shared/omp-owner-state-title.test.ts": 91, - "src/shared/omp-pi-semantic-title-preservation.test.ts": 107, - "src/shared/omp-source-control-discovery.test.ts": 287, - "src/shared/onboarding-tour-telemetry-events.test.ts": 342, - "src/shared/open-in-applications.test.ts": 54, - "src/shared/opencode-cli-version.test.ts": 64, - "src/shared/opencode-config-directory.test.ts": 44, - "src/shared/opencode-generation-command.test.ts": 82, - "src/shared/opencode-generation-output.test.ts": 230, - "src/shared/opencode-headless-command.test.ts": 63, - "src/shared/opencode-installed-plugin.test.ts": 61, - "src/shared/opencode-launch-command.test.ts": 61, - "src/shared/opencode-permission-status.test.ts": 213, - "src/shared/opencode-plugin-atomic-write.test.ts": 84, - "src/shared/opencode-plugin-permissions.test.ts": 117, - "src/shared/opencode-startup-prompt.test.ts": 178, - "src/shared/opencode-terminal-title.test.ts": 50, - "src/shared/opencode-tui-config-registration.test.ts": 85, - "src/shared/opencode-tui-plugin-install.test.ts": 59, - "src/shared/opencode2-resume-launch-mode.test.ts": 158, - "src/shared/openssh-default-shell.test.ts": 59, - "src/shared/orca-session-address.test.ts": 291, - "src/shared/orca-yaml-alias-bounds.test.ts": 398, - "src/shared/orca-yaml-bounds.test.ts": 216, - "src/shared/orcad-artifacts.test.ts": 85, - "src/shared/orcad-build-identity.test.ts": 61, - "src/shared/orcad-migration-dormant-session-validation.test.ts": 344, - "src/shared/orcad-migration-manifest-contract.test.ts": 358, - "src/shared/orcad-migration-manifest.test.ts": 432, - "src/shared/orcad-migration-scrollback.test.ts": 271, - "src/shared/orcad-migration-staged-catalog-validation.test.ts": 381, - "src/shared/orcad-profile-preflight.test.ts": 217, - "src/shared/orcad-stop-request.test.ts": 217, - "src/shared/orchestration-ask-timeout.test.ts": 49, - "src/shared/orchestration-check-output.test.ts": 66, - "src/shared/orchestration-compatibility-evidence.test.ts": 53, - "src/shared/orchestration-dispatch-refusal-contract.test.ts": 88, - "src/shared/orchestration-fleet-attention.test.ts": 46, - "src/shared/orchestration-fleet-evidence-clock.test.ts": 63, - "src/shared/orchestration-fleet-projection.test.ts": 105, - "src/shared/orchestration-rpc-contract.test.ts": 87, - "src/shared/orchestration-task-display.test.ts": 40, - "src/shared/orchestration-task-summary.test.ts": 53, - "src/shared/orchestration-timing-budgets.test.ts": 49, - "src/shared/osc-title-scan-tail-retention.test.ts": 82, - "src/shared/osc-title-scan-tail.test.ts": 51, - "src/shared/osc52-clipboard-settings.test.ts": 66, - "src/shared/own-retained-string.test.ts": 181, - "src/shared/pairing-address-auto-selection.test.ts": 54, - "src/shared/pairing-local-ui-fields.test.ts": 46, - "src/shared/pairing.test.ts": 249, - "src/shared/pane-agent-identity-adapter.test.ts": 95, - "src/shared/pane-agent-identity-resolver.test.ts": 89, - "src/shared/pane-agent-identity-title-corpus.test.ts": 73, - "src/shared/pane-agent-owner.test.ts": 52, - "src/shared/pane-key-alias.test.ts": 52, - "src/shared/physical-exit-tracker.test.ts": 51, - "src/shared/pi-agent-kind.test.ts": 68, - "src/shared/pi-overlay-ui-settings.test.ts": 50, - "src/shared/pi-state-title-marker.test.ts": 76, - "src/shared/plugins/plugin-consent-fingerprint.test.ts": 290, - "src/shared/plugins/plugin-consent-request.test.ts": 215, - "src/shared/plugins/plugin-content-pack-contributions.test.ts": 292, - "src/shared/plugins/plugin-install-lockfile.test.ts": 285, - "src/shared/plugins/plugin-kill-list.test.ts": 298, + "src/shared/network/server-share-address.test.ts": 41, + "src/shared/new-workspace-dialog-repo.test.ts": 39, + "src/shared/new-workspace/smart-workspace-linear-intent.test.ts": 44, + "src/shared/new-workspace/smart-workspace-url-source-results.test.ts": 43, + "src/shared/new-workspace/work-item-lookup-text.test.ts": 47, + "src/shared/new-workspace/workspace-source.test.ts": 57, + "src/shared/new-workspace/worktree-create-retry-policy.test.ts": 35, + "src/shared/node-bounded-file-reader-sync.test.ts": 60, + "src/shared/node-bounded-file-reader.test.ts": 39, + "src/shared/node-bounded-json-stringify.test.ts": 47, + "src/shared/node-file-content-equality.test.ts": 25, + "src/shared/node-pty-spawn-helper.test.ts": 43, + "src/shared/node-readable-text.test.ts": 45, + "src/shared/node-source-copy-content-equality.test.ts": 55, + "src/shared/notebook-venv-location.test.ts": 38, + "src/shared/nul-delimited-fields.test.ts": 24, + "src/shared/nvm-default-alias.test.ts": 56, + "src/shared/omp-draft-launch-shell.test.ts": 229, + "src/shared/omp-draft-launch-windows.test.ts": 137, + "src/shared/omp-fresh-launch-shell.test.ts": 166, + "src/shared/omp-fresh-launch.test.ts": 122, + "src/shared/omp-owner-state-title.test.ts": 92, + "src/shared/omp-pi-semantic-title-preservation.test.ts": 89, + "src/shared/omp-source-control-discovery.test.ts": 289, + "src/shared/onboarding-tour-telemetry-events.test.ts": 248, + "src/shared/open-in-applications.test.ts": 26, + "src/shared/opencode-cli-version.test.ts": 38, + "src/shared/opencode-config-directory.test.ts": 37, + "src/shared/opencode-generation-command.test.ts": 60, + "src/shared/opencode-generation-output.test.ts": 201, + "src/shared/opencode-headless-command.test.ts": 46, + "src/shared/opencode-installed-plugin.test.ts": 34, + "src/shared/opencode-launch-command.test.ts": 55, + "src/shared/opencode-permission-status.test.ts": 204, + "src/shared/opencode-plugin-atomic-write.test.ts": 65, + "src/shared/opencode-plugin-permissions.test.ts": 99, + "src/shared/opencode-startup-prompt.test.ts": 128, + "src/shared/opencode-terminal-title.test.ts": 29, + "src/shared/opencode-tui-config-registration.test.ts": 60, + "src/shared/opencode-tui-plugin-install.test.ts": 63, + "src/shared/opencode2-resume-launch-mode.test.ts": 110, + "src/shared/openssh-default-shell.test.ts": 45, + "src/shared/orca-session-address.test.ts": 250, + "src/shared/orca-yaml-alias-bounds.test.ts": 410, + "src/shared/orca-yaml-bounds.test.ts": 169, + "src/shared/orcad-artifacts.test.ts": 84, + "src/shared/orcad-build-identity.test.ts": 46, + "src/shared/orcad-migration-dormant-session-validation.test.ts": 314, + "src/shared/orcad-migration-manifest-contract.test.ts": 351, + "src/shared/orcad-migration-manifest.test.ts": 393, + "src/shared/orcad-migration-scrollback.test.ts": 240, + "src/shared/orcad-migration-source-kind.test.ts": 362, + "src/shared/orcad-migration-staged-catalog-validation.test.ts": 299, + "src/shared/orcad-profile-preflight.test.ts": 220, + "src/shared/orcad-stop-request.test.ts": 239, + "src/shared/orchestration-ask-timeout.test.ts": 30, + "src/shared/orchestration-check-output.test.ts": 216, + "src/shared/orchestration-compatibility-evidence.test.ts": 34, + "src/shared/orchestration-dispatch-refusal-contract.test.ts": 72, + "src/shared/orchestration-fleet-attention.test.ts": 39, + "src/shared/orchestration-fleet-evidence-clock.test.ts": 48, + "src/shared/orchestration-fleet-projection.test.ts": 104, + "src/shared/orchestration-rpc-contract.test.ts": 260, + "src/shared/orchestration-task-display.test.ts": 48, + "src/shared/orchestration-task-summary.test.ts": 34, + "src/shared/orchestration-timing-budgets.test.ts": 35, + "src/shared/osc-title-scan-tail-retention.test.ts": 75, + "src/shared/osc-title-scan-tail.test.ts": 26, + "src/shared/osc52-clipboard-settings.test.ts": 40, + "src/shared/own-retained-string.test.ts": 159, + "src/shared/paired-e2ee-client-handshake.test.ts": 116, + "src/shared/paired-e2ee-device-key-auth.test.ts": 92, + "src/shared/paired-runtime-connection.test.ts": 224, + "src/shared/pairing-address-auto-selection.test.ts": 38, + "src/shared/pairing-local-ui-fields.test.ts": 38, + "src/shared/pairing.test.ts": 235, + "src/shared/pane-agent-identity-adapter.test.ts": 76, + "src/shared/pane-agent-identity-resolver.test.ts": 62, + "src/shared/pane-agent-identity-title-corpus.test.ts": 70, + "src/shared/pane-agent-owner.test.ts": 45, + "src/shared/pane-key-alias.test.ts": 40, + "src/shared/physical-exit-tracker.test.ts": 32, + "src/shared/pi-agent-kind.test.ts": 69, + "src/shared/pi-state-title-marker.test.ts": 77, + "src/shared/plugins/plugin-consent-fingerprint.test.ts": 216, + "src/shared/plugins/plugin-consent-request.test.ts": 207, + "src/shared/plugins/plugin-content-pack-contributions.test.ts": 260, + "src/shared/plugins/plugin-install-lockfile.test.ts": 213, + "src/shared/plugins/plugin-kill-list.test.ts": 318, "src/shared/plugins/plugin-language-pack-artifact.test.ts": 177, - "src/shared/plugins/plugin-manifest.test.ts": 265, - "src/shared/plugins/plugin-marketplace.test.ts": 314, - "src/shared/plugins/plugin-panel-call-admission.test.ts": 246, - "src/shared/plugins/plugin-panel-message-budget-traversal.test.ts": 297, - "src/shared/plugins/plugin-panel-message-budget.test.ts": 278, - "src/shared/plugins/plugin-panel-pong-frame.test.ts": 243, - "src/shared/plugins/plugin-panel-shell.test.ts": 343, - "src/shared/plugins/plugin-path-safety.test.ts": 304, - "src/shared/plugins/plugin-vm-recipe-artifact.test.ts": 342, - "src/shared/posix-wait-status.test.ts": 50, - "src/shared/powershell-native-argument.test.ts": 33, - "src/shared/powershell-native-argument.windows.test.ts": 65, - "src/shared/pr-bot-author-overrides.test.ts": 54, - "src/shared/pr-check-severity-order.test.ts": 56, - "src/shared/pr-check-status.test.ts": 50, - "src/shared/pr-comment-audience.test.ts": 54, - "src/shared/pr-comment-groups.test.ts": 43, - "src/shared/pr-comment-time.test.ts": 45, - "src/shared/preferred-git-remote.test.ts": 53, - "src/shared/priority-semaphore.test.ts": 63, - "src/shared/process-output-field-scanner.test.ts": 55, - "src/shared/process-table-index.test.ts": 176, - "src/shared/process-table-snapshot.test.ts": 175, - "src/shared/profile-state-build-compatibility.test.ts": 52, - "src/shared/profile-state-recovery-command.test.ts": 248, - "src/shared/profile-state-telemetry-schema.test.ts": 295, - "src/shared/project-catalog-row-normalization.test.ts": 65, - "src/shared/project-execution-runtime.test.ts": 62, - "src/shared/project-groups.test.ts": 148, - "src/shared/project-host-setup-projection.test.ts": 99, - "src/shared/project-identity-succession.test.ts": 81, - "src/shared/promise-settlement-waiters.test.ts": 169, - "src/shared/protocol-compat.test.ts": 88, - "src/shared/pty-delivery-diagnostics.test.ts": 55, - "src/shared/pty-liveness-verdict.test.ts": 46, - "src/shared/pty-owner-backend.test.ts": 45, - "src/shared/pty-owner-color-query-colors.test.ts": 66, - "src/shared/pty-running-work-probe.test.ts": 61, - "src/shared/pty-slave-line-discipline-echo.test.ts": 64, - "src/shared/pty-startup-ingress-kitty.test.ts": 106, - "src/shared/pty-startup-ingress-lifetime-colors.test.ts": 90, - "src/shared/pty-startup-ingress-live-query-reply.test.ts": 119, - "src/shared/pty-startup-ingress.test.ts": 155, - "src/shared/pty-startup-reply-echo-shapes.test.ts": 59, - "src/shared/published-pane-agent-identity.test.ts": 90, - "src/shared/pull-request-generation.test.ts": 71, - "src/shared/qoder-agent.test.ts": 293, - "src/shared/qoder-cn-launch-recipe.test.ts": 147, - "src/shared/quick-open-filter.renderer-safety.test.ts": 59, - "src/shared/quick-open-filter.test.ts": 93, - "src/shared/quick-open-fuzzy-scan.test.ts": 996, - "src/shared/quick-open-git-directory-collapse.test.ts": 88, - "src/shared/quick-open-listing-limits.test.ts": 45, - "src/shared/quick-open-query-target.test.ts": 64, - "src/shared/quick-open-recent-candidates.test.ts": 63, - "src/shared/quick-open-transport-budget.test.ts": 55, - "src/shared/raster-image-base64-preview.test.ts": 391, - "src/shared/raster-image-dimensions.test.ts": 58, - "src/shared/raster-image-preview-limits.test.ts": 63, - "src/shared/rate-limit-reset-format.test.ts": 46, - "src/shared/react-update-depth-attribution.test.ts": 46, - "src/shared/reconnect-jitter.test.ts": 42, - "src/shared/relay-frame-buffer.test.ts": 275, - "src/shared/relay-optional-artifacts.test.ts": 53, - "src/shared/relay-version-marker.test.ts": 51, - "src/shared/release-channel.test.ts": 75, - "src/shared/remote-foreground-evidence.test.ts": 64, - "src/shared/remote-pairing-address.test.ts": 268, - "src/shared/remote-pairing-verification.test.ts": 64, - "src/shared/remote-rpc-content-budget.test.ts": 105, - "src/shared/remote-runtime-client-error-classification.test.ts": 50, - "src/shared/remote-runtime-client.test.ts": 5122, - "src/shared/remote-runtime-connect-bound.test.ts": 558, - "src/shared/remote-runtime-memory-limits.test.ts": 144, - "src/shared/remote-runtime-outbound-admission.test.ts": 1054, - "src/shared/remote-runtime-pty-id.test.ts": 45, - "src/shared/remote-runtime-request-connection-stale.test.ts": 312, - "src/shared/remote-runtime-request-connection.test.ts": 410, - "src/shared/remote-runtime-request-frames.test.ts": 208, - "src/shared/remote-runtime-request-response-router.test.ts": 244, - "src/shared/remote-runtime-request-websocket.test.ts": 300, - "src/shared/remote-runtime-shared-control-connection.test.ts": 3555, - "src/shared/remote-runtime-shared-control-keepalive-refresh.test.ts": 279, - "src/shared/remote-runtime-shared-control-reconnect.test.ts": 263, - "src/shared/remote-runtime-shared-control-retired-request-ids.test.ts": 42, - "src/shared/remote-runtime-shared-control-socket-generation.test.ts": 221, - "src/shared/remote-runtime-shared-control-standing-intent.test.ts": 956, - "src/shared/remote-runtime-shared-control-subscription-close.test.ts": 255, - "src/shared/remote-runtime-shared-control-subscription-start.test.ts": 267, - "src/shared/remote-runtime-shared-control-subscriptions.test.ts": 277, - "src/shared/remote-runtime-socket-liveness.test.ts": 58, - "src/shared/remote-runtime-subscription-connect-bound.test.ts": 652, - "src/shared/remote-runtime-subscription-frame-router.test.ts": 249, - "src/shared/remote-runtime-subscription-request.test.ts": 699, - "src/shared/remote-runtime-tailscale-hint.test.ts": 59, - "src/shared/remote-runtime-transport-error-agreement.test.ts": 219, - "src/shared/renderer-restart-preparation.test.ts": 169, - "src/shared/repo-badge-color.test.ts": 310, - "src/shared/repo-icon.test.ts": 80, - "src/shared/repo-pack-index-scheduling.test.ts": 86, - "src/shared/repo-ref-maintenance.test.ts": 507, - "src/shared/repo-search-limits.test.ts": 49, - "src/shared/repro-13889-claude-quarter-circle-busy-title.test.ts": 105, - "src/shared/repro-7732-gitlab-job-id-dropped.test.ts": 51, - "src/shared/repro-8478-opencode-native-title-icon.test.ts": 86, - "src/shared/require-tui-agent-config.test.ts": 60, - "src/shared/resolved-worktree-lineage.test.ts": 60, - "src/shared/retired-pty-incarnations.test.ts": 50, - "src/shared/returned-draft-text.test.ts": 45, - "src/shared/review-head-tracking-ref.test.ts": 50, - "src/shared/review-refresh-policy.test.ts": 47, - "src/shared/ripgrep-dense-match-json.test.ts": 186, - "src/shared/ripgrep-filename-decoder.test.ts": 51, - "src/shared/ripgrep-line-decoding.test.ts": 388, - "src/shared/ripgrep-match-offsets.test.ts": 132, - "src/shared/ripgrep-process-availability.test.ts": 80, - "src/shared/ripgrep-search-diagnostics.test.ts": 72, - "src/shared/rpc-contract/agent-launch-params.test.ts": 408, - "src/shared/rpc-contract/automation-params-extra-agent-args.test.ts": 256, - "src/shared/rpc-contract/structured-agent-session-agent-params.test.ts": 292, - "src/shared/rpc-contract/terminal-create-shell-param.test.ts": 219, - "src/shared/rpc-contract/worktree-review-metadata-compatibility.test.ts": 277, - "src/shared/runtime-environment-managed-orcad-store.test.ts": 332, - "src/shared/runtime-environment-sidecar.test.ts": 340, - "src/shared/runtime-environment-ssh-access-store.test.ts": 416, - "src/shared/runtime-environment-store.test.ts": 353, - "src/shared/runtime-environments-ssh-access.test.ts": 247, - "src/shared/runtime-host-pairing-identity.test.ts": 52, - "src/shared/runtime-host-status-owner.test.ts": 83, - "src/shared/runtime-listing-host-scope.test.ts": 57, - "src/shared/runtime-navigation.test.ts": 47, - "src/shared/runtime-rpc-call-queue-retention.test.ts": 279, - "src/shared/runtime-rpc-call-queue.test.ts": 87, - "src/shared/runtime-source-env.test.ts": 51, - "src/shared/runtime-workspace-file-owner.test.ts": 50, - "src/shared/runtime-workspace-window-availability.test.ts": 49, - "src/shared/search-match-count.test.ts": 43, - "src/shared/search-subprocess-lines.test.ts": 99, - "src/shared/secret-at-rest-protection.test.ts": 50, - "src/shared/secret-store.test.ts": 43, - "src/shared/secure-file-coarse-ctime.test.ts": 61, - "src/shared/secure-file-fsync-flags.test.ts": 98, - "src/shared/secure-file.test.ts": 736, - "src/shared/secure-path-hardening-cache.test.ts": 59, - "src/shared/secure-path-hardening-retry-budget.test.ts": 91, - "src/shared/secure-path-windows-acl.win32.test.ts": 68, - "src/shared/serve-option-validation.test.ts": 63, - "src/shared/session-tab-placement.test.ts": 53, - "src/shared/setup-agent-sequencing-codex-no-daemon.test.ts": 1135, - "src/shared/setup-agent-sequencing.test.ts": 5411, - "src/shared/setup-agent-sequencing.windows.test.ts": 54, - "src/shared/setup-runner-command.test.ts": 80, - "src/shared/setup-script-imports.test.ts": 118, - "src/shared/setup-script-package-manager-suggestion.test.ts": 65, - "src/shared/setup-script-shebang.test.ts": 60, - "src/shared/setup-script-telemetry-events.test.ts": 297, - "src/shared/setup-script-telemetry.test.ts": 49, - "src/shared/sha256.test.ts": 61, - "src/shared/shell-foreground-snapshot.test.ts": 173, - "src/shared/shell-process-detection.test.ts": 55, - "src/shared/shell-process-readiness.test.ts": 68, - "src/shared/skill-bundle-install-contract.test.ts": 218, - "src/shared/skill-bundle-name.test.ts": 50, - "src/shared/skill-delete-contract.test.ts": 241, - "src/shared/skill-deletion-eligibility.test.ts": 56, - "src/shared/skill-install-contract.test.ts": 220, - "src/shared/skill-install-failure.test.ts": 202, - "src/shared/skill-metadata.test.ts": 47, - "src/shared/skill-package-manifest.test.ts": 350, - "src/shared/skill-path-containment.test.ts": 53, - "src/shared/skills-cli-agent-keys.test.ts": 49, - "src/shared/source-control-ai-action-recipes.test.ts": 306, - "src/shared/source-control-ai-action-variables.test.ts": 64, - "src/shared/source-control-ai-actions.test.ts": 66, - "src/shared/source-control-ai-policy-regression.test.ts": 281, - "src/shared/source-control-ai-recipe-save.test.ts": 276, - "src/shared/source-control-ai.test.ts": 278, - "src/shared/source-control-create-review-intent.test.ts": 60, - "src/shared/source-control-group-order.test.ts": 49, + "src/shared/plugins/plugin-manifest.test.ts": 248, + "src/shared/plugins/plugin-marketplace.test.ts": 325, + "src/shared/plugins/plugin-panel-call-admission.test.ts": 238, + "src/shared/plugins/plugin-panel-message-budget-traversal.test.ts": 255, + "src/shared/plugins/plugin-panel-message-budget.test.ts": 265, + "src/shared/plugins/plugin-panel-pong-frame.test.ts": 252, + "src/shared/plugins/plugin-panel-shell.test.ts": 380, + "src/shared/plugins/plugin-path-safety.test.ts": 229, + "src/shared/plugins/plugin-vm-recipe-artifact.test.ts": 203, + "src/shared/posix-wait-status.test.ts": 31, + "src/shared/powershell-native-argument.test.ts": 24, + "src/shared/powershell-native-argument.windows.test.ts": 40, + "src/shared/pr-bot-author-overrides.test.ts": 53, + "src/shared/pr-check-severity-order.test.ts": 40, + "src/shared/pr-check-status.test.ts": 39, + "src/shared/pr-comment-audience.test.ts": 51, + "src/shared/pr-comment-groups.test.ts": 26, + "src/shared/pr-comment-time.test.ts": 41, + "src/shared/preferred-git-remote.test.ts": 39, + "src/shared/priority-semaphore.test.ts": 45, + "src/shared/process-growth-breaker.test.ts": 35, + "src/shared/process-output-field-scanner.test.ts": 41, + "src/shared/process-table-capture-parsing.test.ts": 42, + "src/shared/process-table-index.test.ts": 184, + "src/shared/process-table-snapshot.test.ts": 162, + "src/shared/profile-state-build-compatibility.test.ts": 35, + "src/shared/profile-state-recovery-command.test.ts": 222, + "src/shared/profile-state-telemetry-schema.test.ts": 307, + "src/shared/project-catalog-row-normalization.test.ts": 51, + "src/shared/project-execution-runtime.test.ts": 55, + "src/shared/project-groups.test.ts": 125, + "src/shared/project-host-setup-projection.test.ts": 86, + "src/shared/project-identity-succession.test.ts": 69, + "src/shared/promise-settlement-waiters.test.ts": 183, + "src/shared/protocol-compat.test.ts": 193, + "src/shared/pty-delivery-diagnostics.test.ts": 35, + "src/shared/pty-liveness-verdict.test.ts": 38, + "src/shared/pty-owner-backend.test.ts": 37, + "src/shared/pty-owner-color-query-colors.test.ts": 58, + "src/shared/pty-running-work-probe.test.ts": 40, + "src/shared/pty-slave-line-discipline-echo.test.ts": 38, + "src/shared/pty-startup-ingress-kitty.test.ts": 120, + "src/shared/pty-startup-ingress-lifetime-colors.test.ts": 92, + "src/shared/pty-startup-ingress-live-query-reply.test.ts": 118, + "src/shared/pty-startup-ingress.test.ts": 167, + "src/shared/pty-startup-reply-echo-shapes.test.ts": 58, + "src/shared/published-pane-agent-identity.test.ts": 87, + "src/shared/pull-request-generation.test.ts": 47, + "src/shared/qoder-agent.test.ts": 295, + "src/shared/qoder-cn-launch-recipe.test.ts": 118, + "src/shared/quick-open-filter.renderer-safety.test.ts": 49, + "src/shared/quick-open-filter.test.ts": 94, + "src/shared/quick-open-fuzzy-scan.test.ts": 1098, + "src/shared/quick-open-git-directory-collapse.test.ts": 73, + "src/shared/quick-open-listing-limits.test.ts": 29, + "src/shared/quick-open-query-target.test.ts": 63, + "src/shared/quick-open-recent-candidates.test.ts": 40, + "src/shared/quick-open-subprocess-path-accumulator.test.ts": 24, + "src/shared/quick-open-transport-budget.test.ts": 34, + "src/shared/raster-image-base64-preview.test.ts": 395, + "src/shared/raster-image-dimensions.test.ts": 39, + "src/shared/raster-image-preview-limits.test.ts": 32, + "src/shared/rate-limit-reset-format.test.ts": 36, + "src/shared/react-update-depth-attribution.test.ts": 27, + "src/shared/reconnect-jitter.test.ts": 28, + "src/shared/relay-frame-buffer.test.ts": 273, + "src/shared/relay-version-marker.test.ts": 50, + "src/shared/release-channel.test.ts": 56, + "src/shared/remote-foreground-evidence.test.ts": 44, + "src/shared/remote-pairing-address.test.ts": 239, + "src/shared/remote-pairing-verification.test.ts": 202, + "src/shared/remote-rpc-content-budget.test.ts": 95, + "src/shared/remote-runtime-client-error-classification.test.ts": 42, + "src/shared/remote-runtime-client.test.ts": 5160, + "src/shared/remote-runtime-connect-bound.test.ts": 572, + "src/shared/remote-runtime-memory-limits.test.ts": 120, + "src/shared/remote-runtime-outbound-admission.test.ts": 1044, + "src/shared/remote-runtime-pty-id.test.ts": 38, + "src/shared/remote-runtime-request-connection-stale.test.ts": 311, + "src/shared/remote-runtime-request-connection.test.ts": 393, + "src/shared/remote-runtime-request-frames.test.ts": 280, + "src/shared/remote-runtime-request-response-router.test.ts": 243, + "src/shared/remote-runtime-request-websocket.test.ts": 406, + "src/shared/remote-runtime-shared-control-connection.test.ts": 3965, + "src/shared/remote-runtime-shared-control-keepalive-refresh.test.ts": 287, + "src/shared/remote-runtime-shared-control-reconnect.test.ts": 257, + "src/shared/remote-runtime-shared-control-retired-request-ids.test.ts": 40, + "src/shared/remote-runtime-shared-control-socket-generation.test.ts": 197, + "src/shared/remote-runtime-shared-control-standing-intent.test.ts": 961, + "src/shared/remote-runtime-shared-control-subscription-close.test.ts": 261, + "src/shared/remote-runtime-shared-control-subscription-start.test.ts": 308, + "src/shared/remote-runtime-shared-control-subscriptions.test.ts": 275, + "src/shared/remote-runtime-socket-liveness.test.ts": 37, + "src/shared/remote-runtime-subscription-connect-bound.test.ts": 658, + "src/shared/remote-runtime-subscription-frame-router.test.ts": 282, + "src/shared/remote-runtime-subscription-request.test.ts": 668, + "src/shared/remote-runtime-tailscale-hint.test.ts": 47, + "src/shared/remote-runtime-transport-error-agreement.test.ts": 297, + "src/shared/renderer-restart-preparation.test.ts": 163, + "src/shared/repo-badge-color.test.ts": 233, + "src/shared/repo-icon.test.ts": 75, + "src/shared/repo-pack-index-scheduling.test.ts": 67, + "src/shared/repo-ref-maintenance.test.ts": 409, + "src/shared/repo-search-limits.test.ts": 45, + "src/shared/repro-13889-claude-quarter-circle-busy-title.test.ts": 87, + "src/shared/repro-7732-gitlab-job-id-dropped.test.ts": 31, + "src/shared/repro-8478-opencode-native-title-icon.test.ts": 69, + "src/shared/require-tui-agent-config.test.ts": 32, + "src/shared/resolved-worktree-lineage.test.ts": 55, + "src/shared/retired-pty-incarnations.test.ts": 35, + "src/shared/returned-draft-text.test.ts": 34, + "src/shared/review-head-tracking-ref.test.ts": 36, + "src/shared/review-refresh-policy.test.ts": 42, + "src/shared/ripgrep-dense-match-json.test.ts": 122, + "src/shared/ripgrep-filename-decoder.test.ts": 35, + "src/shared/ripgrep-line-decoding.test.ts": 467, + "src/shared/ripgrep-match-offsets.test.ts": 100, + "src/shared/ripgrep-process-availability.test.ts": 66, + "src/shared/ripgrep-search-diagnostics.test.ts": 68, + "src/shared/rpc-contract/agent-launch-params-account-env.test.ts": 452, + "src/shared/rpc-contract/agent-launch-params-native-chat-default.test.ts": 434, + "src/shared/rpc-contract/agent-launch-params.test.ts": 420, + "src/shared/rpc-contract/agent-session-params-account-env.test.ts": 262, + "src/shared/rpc-contract/agent-session-spawn-intent-params.test.ts": 241, + "src/shared/rpc-contract/automation-params-extra-agent-args.test.ts": 247, + "src/shared/rpc-contract/jira-transition-update.test.ts": 209, + "src/shared/rpc-contract/structured-agent-session-agent-params.test.ts": 259, + "src/shared/rpc-contract/terminal-create-shell-param.test.ts": 283, + "src/shared/rpc-contract/worktree-review-metadata-compatibility.test.ts": 236, + "src/shared/runtime-environment-managed-orcad-store.test.ts": 383, + "src/shared/runtime-environment-sidecar.test.ts": 391, + "src/shared/runtime-environment-ssh-access-store.test.ts": 493, + "src/shared/runtime-environment-store-unreadable-records-interleaving.test.ts": 1049, + "src/shared/runtime-environment-store-unreadable-records.test.ts": 334, + "src/shared/runtime-environment-store.test.ts": 323, + "src/shared/runtime-environment-wsl-host-store.test.ts": 522, + "src/shared/runtime-environments-listed-remote-server.test.ts": 199, + "src/shared/runtime-environments-ssh-access.test.ts": 273, + "src/shared/runtime-host-pairing-identity.test.ts": 33, + "src/shared/runtime-host-status-owner.test.ts": 64, + "src/shared/runtime-listing-host-scope.test.ts": 42, + "src/shared/runtime-navigation.test.ts": 34, + "src/shared/runtime-rpc-call-queue-retention.test.ts": 256, + "src/shared/runtime-rpc-call-queue.test.ts": 62, + "src/shared/runtime-source-env.test.ts": 24, + "src/shared/runtime-workspace-file-owner.test.ts": 29, + "src/shared/runtime-workspace-window-availability.test.ts": 43, + "src/shared/search-match-count.test.ts": 37, + "src/shared/search-subprocess-lines.test.ts": 90, + "src/shared/secret-at-rest-protection.test.ts": 38, + "src/shared/secret-store.test.ts": 26, + "src/shared/secure-file-coarse-ctime.test.ts": 38, + "src/shared/secure-file-fsync-flags.test.ts": 52, + "src/shared/secure-file-windows.test.ts": 183, + "src/shared/secure-file.test.ts": 66, + "src/shared/secure-path-hardening-cache.test.ts": 38, + "src/shared/secure-path-hardening-retry-budget.test.ts": 70, + "src/shared/secure-path-windows-acl.win32.test.ts": 51, + "src/shared/serve-option-validation.test.ts": 44, + "src/shared/session-tab-placement.test.ts": 45, + "src/shared/setup-agent-sequencing-codex-no-daemon.test.ts": 1123, + "src/shared/setup-agent-sequencing.test.ts": 4293, + "src/shared/setup-agent-sequencing.windows.test.ts": 42, + "src/shared/setup-runner-command.test.ts": 63, + "src/shared/setup-script-imports.test.ts": 94, + "src/shared/setup-script-package-manager-suggestion.test.ts": 44, + "src/shared/setup-script-shebang.test.ts": 52, + "src/shared/setup-script-telemetry-events.test.ts": 253, + "src/shared/setup-script-telemetry.test.ts": 34, + "src/shared/sha256.test.ts": 58, + "src/shared/shell-foreground-snapshot.test.ts": 221, + "src/shared/shell-process-detection.test.ts": 44, + "src/shared/shell-process-readiness.test.ts": 65, + "src/shared/skill-bundle-install-contract.test.ts": 225, + "src/shared/skill-bundle-name.test.ts": 30, + "src/shared/skill-delete-contract.test.ts": 240, + "src/shared/skill-deletion-eligibility.test.ts": 45, + "src/shared/skill-install-contract.test.ts": 235, + "src/shared/skill-install-failure.test.ts": 235, + "src/shared/skill-metadata.test.ts": 28, + "src/shared/skill-package-manifest.test.ts": 322, + "src/shared/skill-path-containment.test.ts": 34, + "src/shared/skills-cli-agent-keys.test.ts": 26, + "src/shared/source-control-ai-action-recipes.test.ts": 277, + "src/shared/source-control-ai-action-variables.test.ts": 45, + "src/shared/source-control-ai-actions.test.ts": 48, + "src/shared/source-control-ai-policy-regression.test.ts": 227, + "src/shared/source-control-ai-recipe-save.test.ts": 266, + "src/shared/source-control-ai.test.ts": 264, + "src/shared/source-control-create-review-intent.test.ts": 48, + "src/shared/source-control-group-order.test.ts": 33, "src/shared/source-control-launch-agent-selection.test.ts": 289, - "src/shared/source-control-primary-action-decision.test.ts": 62, - "src/shared/source-control-push-failure.test.ts": 68, - "src/shared/source-control-recovery-agent-command.test.ts": 77, - "src/shared/source-scan/source-tree-scan.test.ts": 100, - "src/shared/source-scan/source-tree-walk.test.ts": 71, - "src/shared/ssh-pending-pty-kill.test.ts": 55, - "src/shared/ssh-pty-id.test.ts": 49, - "src/shared/ssh-remote-pty-lease-predicates.test.ts": 43, - "src/shared/ssh-retained-payload-admission.test.ts": 78, - "src/shared/ssh-target-generation.test.ts": 57, - "src/shared/stable-pane-id.test.ts": 46, - "src/shared/startup-command-staging.node-pty.test.ts": 2051, - "src/shared/startup-command-staging.test.ts": 143, - "src/shared/startup-command-submission.test.ts": 42, - "src/shared/startup-line-prompt-carry.test.ts": 175, - "src/shared/status-bar-compact-change-notice.test.ts": 60, - "src/shared/status-bar-usage-mode.test.ts": 49, - "src/shared/string-chunk-compaction.test.ts": 63, - "src/shared/structural-value-equality.test.ts": 52, - "src/shared/structured-agent-session-agent-status.test.ts": 71, - "src/shared/structured-agent-session-child-work-legacy.test.ts": 65, - "src/shared/structured-agent-session-coalescer.test.ts": 215, - "src/shared/structured-agent-session-composer.test.ts": 82, - "src/shared/structured-agent-session-context-usage.test.ts": 226, - "src/shared/structured-agent-session-create.test.ts": 86, - "src/shared/structured-agent-session-dispatch-rejection.test.ts": 283, - "src/shared/structured-agent-session-draft-hand-off.test.ts": 46, - "src/shared/structured-agent-session-first-frame-picker.test.ts": 86, - "src/shared/structured-agent-session-first-prompt.test.ts": 227, - "src/shared/structured-agent-session-item-retention.test.ts": 941, - "src/shared/structured-agent-session-latest-request.test.ts": 353, - "src/shared/structured-agent-session-live-turn.test.ts": 248, - "src/shared/structured-agent-session-message-projection-stopped-before-start.test.ts": 315, - "src/shared/structured-agent-session-message-projection.test.ts": 267, - "src/shared/structured-agent-session-model-catalog-application.test.ts": 77, - "src/shared/structured-agent-session-mutation.test.ts": 260, - "src/shared/structured-agent-session-opening-send.test.ts": 233, - "src/shared/structured-agent-session-option-codec.test.ts": 40, - "src/shared/structured-agent-session-option-picks.test.ts": 80, - "src/shared/structured-agent-session-options.test.ts": 78, + "src/shared/source-control-primary-action-decision.test.ts": 45, + "src/shared/source-control-push-failure.test.ts": 37, + "src/shared/source-control-recovery-agent-command.test.ts": 48, + "src/shared/source-scan/source-tree-scan.test.ts": 78, + "src/shared/source-scan/source-tree-walk.test.ts": 48, + "src/shared/ssh-pending-pty-kill.test.ts": 30, + "src/shared/ssh-pty-id.test.ts": 42, + "src/shared/ssh-remote-pty-lease-predicates.test.ts": 28, + "src/shared/ssh-retained-payload-admission.test.ts": 62, + "src/shared/ssh-target-generation.test.ts": 47, + "src/shared/stable-pane-id.test.ts": 40, + "src/shared/startup-command-staging.node-pty.test.ts": 2025, + "src/shared/startup-command-staging.test.ts": 162, + "src/shared/startup-command-submission.test.ts": 35, + "src/shared/startup-line-prompt-carry.test.ts": 296, + "src/shared/status-bar-compact-change-notice.test.ts": 30, + "src/shared/status-bar-usage-mode.test.ts": 41, + "src/shared/structural-value-equality.test.ts": 38, + "src/shared/structured-agent-session-agent-status.test.ts": 39, + "src/shared/structured-agent-session-child-work-legacy.test.ts": 40, + "src/shared/structured-agent-session-coalescer.test.ts": 268, + "src/shared/structured-agent-session-composer.test.ts": 64, + "src/shared/structured-agent-session-context-usage.test.ts": 242, + "src/shared/structured-agent-session-create.test.ts": 207, + "src/shared/structured-agent-session-dispatch-rejection.test.ts": 280, + "src/shared/structured-agent-session-draft-hand-off.test.ts": 36, + "src/shared/structured-agent-session-first-frame-picker.test.ts": 99, + "src/shared/structured-agent-session-first-prompt.test.ts": 253, + "src/shared/structured-agent-session-item-retention.test.ts": 879, + "src/shared/structured-agent-session-latest-request.test.ts": 326, + "src/shared/structured-agent-session-live-turn.test.ts": 243, + "src/shared/structured-agent-session-message-projection-stopped-before-start.test.ts": 355, + "src/shared/structured-agent-session-message-projection.test.ts": 346, + "src/shared/structured-agent-session-model-catalog-application.test.ts": 92, + "src/shared/structured-agent-session-mutation.test.ts": 248, + "src/shared/structured-agent-session-opening-send.test.ts": 211, + "src/shared/structured-agent-session-option-codec.test.ts": 33, + "src/shared/structured-agent-session-option-picks.test.ts": 85, + "src/shared/structured-agent-session-option-view.test.ts": 95, + "src/shared/structured-agent-session-options.test.ts": 93, + "src/shared/structured-agent-session-permission-option.test.ts": 96, "src/shared/structured-agent-session-projection-sender.test.ts": 307, - "src/shared/structured-agent-session-projection.test.ts": 414, - "src/shared/structured-agent-session-reasoning-projection.test.ts": 308, - "src/shared/structured-agent-session-reducer.queued-messages.test.ts": 228, - "src/shared/structured-agent-session-reducer.test.ts": 274, - "src/shared/structured-agent-session-rejection-words.test.ts": 209, - "src/shared/structured-agent-session-send-evidence.test.ts": 225, - "src/shared/structured-agent-session-send-mutation.test.ts": 305, - "src/shared/structured-agent-session-status-started-at.test.ts": 291, - "src/shared/structured-agent-session-status-tool-line.test.ts": 324, - "src/shared/structured-agent-session-subagent-roster.test.ts": 64, - "src/shared/structured-agent-session-turn-bars.test.ts": 255, - "src/shared/structured-agent-session-turn-timing-retention.test.ts": 355, - "src/shared/structured-agent-session-turn-timing.test.ts": 301, - "src/shared/structured-agent-session-unknown-item.test.ts": 260, - "src/shared/structured-agent-turn-clock-anchor.test.ts": 290, - "src/shared/structured-chat-naming-eligibility.test.ts": 236, - "src/shared/structured-native-chat-launch-route.test.ts": 87, - "src/shared/structured-session-background-tasks-view.test.ts": 67, - "src/shared/structured-session-foreground-commands.test.ts": 230, - "src/shared/subprocess-stdin-write.test.ts": 49, - "src/shared/synthetic-agent-title.test.ts": 52, - "src/shared/tab-title-resolution.test.ts": 60, - "src/shared/tailnet-address.test.ts": 45, - "src/shared/task-providers.test.ts": 57, - "src/shared/task-query.test.ts": 61, - "src/shared/task-source-context.test.ts": 211, - "src/shared/telemetry-common-props.test.ts": 271, - "src/shared/telemetry-events-feature-education.test.ts": 298, - "src/shared/telemetry-events.test.ts": 361, - "src/shared/telemetry-feature-wall-events.test.ts": 290, - "src/shared/telemetry-orca-cli-feature-tip.test.ts": 307, - "src/shared/telemetry-workspace-create-schemas.test.ts": 242, - "src/shared/terminal-bell-detector.test.ts": 45, - "src/shared/terminal-color-scheme-protocol.test.ts": 45, - "src/shared/terminal-composer-draft.test.ts": 72, - "src/shared/terminal-custom-themes.test.ts": 54, + "src/shared/structured-agent-session-projection.test.ts": 397, + "src/shared/structured-agent-session-reasoning-projection.test.ts": 289, + "src/shared/structured-agent-session-reducer.queued-messages.test.ts": 306, + "src/shared/structured-agent-session-reducer.test.ts": 271, + "src/shared/structured-agent-session-rejection-words.test.ts": 289, + "src/shared/structured-agent-session-send-evidence.test.ts": 237, + "src/shared/structured-agent-session-send-mutation.test.ts": 265, + "src/shared/structured-agent-session-send-settlement.test.ts": 38, + "src/shared/structured-agent-session-status-started-at.test.ts": 394, + "src/shared/structured-agent-session-status-tool-line.test.ts": 317, + "src/shared/structured-agent-session-subagent-roster.test.ts": 53, + "src/shared/structured-agent-session-turn-bars.test.ts": 273, + "src/shared/structured-agent-session-turn-timing-retention.test.ts": 314, + "src/shared/structured-agent-session-turn-timing.test.ts": 279, + "src/shared/structured-agent-session-unknown-item.test.ts": 296, + "src/shared/structured-agent-turn-clock-anchor.test.ts": 273, + "src/shared/structured-chat-naming-eligibility.test.ts": 273, + "src/shared/structured-native-chat-launch-route.test.ts": 249, + "src/shared/structured-session-background-tasks-view.test.ts": 42, + "src/shared/structured-session-foreground-commands.test.ts": 213, + "src/shared/subprocess-stdin-write.test.ts": 32, + "src/shared/synthetic-agent-title.test.ts": 39, + "src/shared/tab-title-resolution.test.ts": 34, + "src/shared/tailnet-address.test.ts": 33, + "src/shared/task-providers.test.ts": 43, + "src/shared/task-query.test.ts": 55, + "src/shared/task-source-context.test.ts": 212, + "src/shared/telemetry-common-props.test.ts": 258, + "src/shared/telemetry-events-feature-education.test.ts": 256, + "src/shared/telemetry-events.test.ts": 359, + "src/shared/telemetry-feature-wall-events.test.ts": 339, + "src/shared/telemetry-orca-cli-feature-tip.test.ts": 285, + "src/shared/telemetry-workspace-create-schemas.test.ts": 293, + "src/shared/terminal-bell-detector.test.ts": 36, + "src/shared/terminal-color-scheme-protocol.test.ts": 37, + "src/shared/terminal-composer-draft.test.ts": 46, + "src/shared/terminal-custom-themes.test.ts": 35, "src/shared/terminal-escape-introducer.test.ts": 55, - "src/shared/terminal-exit-cause.test.ts": 51, - "src/shared/terminal-file-url-target.test.ts": 42, - "src/shared/terminal-fonts.test.ts": 49, - "src/shared/terminal-github-pr-link-detector.test.ts": 71, - "src/shared/terminal-input.test.ts": 90, - "src/shared/terminal-kitty-keyboard-mode-tracker.test.ts": 91, - "src/shared/terminal-kitty-keyboard-tail-retention.test.ts": 212, - "src/shared/terminal-leaf-move.test.ts": 43, - "src/shared/terminal-line-height-settings.test.ts": 55, - "src/shared/terminal-mode-2031-final-state.test.ts": 102, - "src/shared/terminal-mode-reset-profiles.test.ts": 84, - "src/shared/terminal-mouse-encoding.test.ts": 72, - "src/shared/terminal-osc-color-override-tracker.test.ts": 71, - "src/shared/terminal-osc-color-reply.test.ts": 45, - "src/shared/terminal-osc133-carry-retention.test.ts": 340, - "src/shared/terminal-output-side-effects.test.ts": 103, - "src/shared/terminal-pane-placement.test.ts": 328, - "src/shared/terminal-partial-escape-tail-ground-scan.test.ts": 61, - "src/shared/terminal-partial-escape-tail.fuzz.test.ts": 1435, - "src/shared/terminal-partial-escape-tail.test.ts": 61, - "src/shared/terminal-query-reply.test.ts": 112, - "src/shared/terminal-quick-commands.test.ts": 56, - "src/shared/terminal-reply-query-scan.test.ts": 56, - "src/shared/terminal-restore-parity-writes.test.ts": 135, - "src/shared/terminal-scrollback-policy.test.ts": 55, - "src/shared/terminal-selection-gutter.test.ts": 47, - "src/shared/terminal-session-state-save-failure.test.ts": 46, - "src/shared/terminal-startup-cwd.test.ts": 308, - "src/shared/terminal-stream-json-byte-length.test.ts": 65, - "src/shared/terminal-stream-protocol.test.ts": 63, - "src/shared/terminal-surface-close-target.test.ts": 51, - "src/shared/terminal-synchronized-output-scan.test.ts": 80, - "src/shared/terminal-theme-selection.test.ts": 53, - "src/shared/terminal-title-agent-type.test.ts": 90, - "src/shared/terminal-title-classification-corpus.test.ts": 152, + "src/shared/terminal-exit-cause.test.ts": 49, + "src/shared/terminal-file-url-target.test.ts": 37, + "src/shared/terminal-fonts.test.ts": 32, + "src/shared/terminal-github-pr-link-detector.test.ts": 60, + "src/shared/terminal-input.test.ts": 70, + "src/shared/terminal-kitty-keyboard-mode-tracker.test.ts": 73, + "src/shared/terminal-kitty-keyboard-tail-retention.test.ts": 225, + "src/shared/terminal-leaf-move.test.ts": 33, + "src/shared/terminal-line-height-settings.test.ts": 28, + "src/shared/terminal-mode-2031-final-state.test.ts": 108, + "src/shared/terminal-mode-reset-profiles.test.ts": 83, + "src/shared/terminal-mouse-encoding.test.ts": 44, + "src/shared/terminal-osc-color-override-tracker.test.ts": 53, + "src/shared/terminal-osc-color-reply.test.ts": 34, + "src/shared/terminal-osc133-carry-retention.test.ts": 392, + "src/shared/terminal-output-side-effects.test.ts": 93, + "src/shared/terminal-pane-placement.test.ts": 332, + "src/shared/terminal-pane-title-reset.test.ts": 68, + "src/shared/terminal-partial-escape-tail-ground-scan.test.ts": 45, + "src/shared/terminal-partial-escape-tail.fuzz.test.ts": 1349, + "src/shared/terminal-partial-escape-tail.test.ts": 53, + "src/shared/terminal-query-reply.test.ts": 96, + "src/shared/terminal-quick-commands.test.ts": 53, + "src/shared/terminal-reply-query-scan.test.ts": 42, + "src/shared/terminal-restore-parity-writes.test.ts": 127, + "src/shared/terminal-scrollback-policy.test.ts": 35, + "src/shared/terminal-selection-gutter.test.ts": 32, + "src/shared/terminal-session-state-save-failure.test.ts": 37, + "src/shared/terminal-startup-cwd.test.ts": 270, + "src/shared/terminal-stream-json-byte-length.test.ts": 60, + "src/shared/terminal-stream-protocol.test.ts": 40, + "src/shared/terminal-surface-close-target.test.ts": 54, + "src/shared/terminal-synchronized-output-scan.test.ts": 82, + "src/shared/terminal-theme-selection.test.ts": 45, + "src/shared/terminal-title-agent-type.test.ts": 63, + "src/shared/terminal-title-classification-corpus.test.ts": 160, "src/shared/terminal-title-classification-memo.test.ts": 95, - "src/shared/terminal-view-attributes.test.ts": 60, - "src/shared/terminal-viewport.test.ts": 189, - "src/shared/terminal-wait-blocked-reason-legacy-alias.test.ts": 48, - "src/shared/terminal-zero-dimensions-diagnostic.test.ts": 47, - "src/shared/test-code-path.test.ts": 54, - "src/shared/text-search-dense-matches.test.ts": 450, - "src/shared/text-search-glob-patterns.test.ts": 243, - "src/shared/text-search-invalid-filename.test.ts": 62, - "src/shared/text-search-paths.test.ts": 68, - "src/shared/text-search-snippet-boundaries.test.ts": 66, - "src/shared/text-search-snippet-retention.test.ts": 603, - "src/shared/text-search.test.ts": 129, - "src/shared/timer-delay.test.ts": 54, - "src/shared/tmux-agent-hook-owner.test.ts": 467, - "src/shared/tmux-client-attachment.test.ts": 64, - "src/shared/tmux-hook-refresh-retirement.test.ts": 621, - "src/shared/tmux-host-attachment-probe.test.ts": 45, - "src/shared/tui-agent-config.test.ts": 58, - "src/shared/tui-agent-fork-startup.test.ts": 112, - "src/shared/tui-agent-launch-command-argument-values.test.ts": 118, - "src/shared/tui-agent-launch-command-override.test.ts": 61, - "src/shared/tui-agent-launch-command-pick-flags.test.ts": 97, - "src/shared/tui-agent-launch-defaults.test.ts": 96, - "src/shared/tui-agent-permissions.test.ts": 42, - "src/shared/tui-agent-prompt-transport.test.ts": 147, - "src/shared/tui-agent-rest-signal.test.ts": 64, - "src/shared/tui-agent-selection.test.ts": 62, - "src/shared/tui-agent-startup-copilot-resume.test.ts": 117, - "src/shared/tui-agent-startup-hermes.test.ts": 145, - "src/shared/tui-agent-startup-kiro-resume.test.ts": 65, - "src/shared/tui-agent-startup-session-options.test.ts": 135, - "src/shared/tui-agent-startup-shell.test.ts": 129, - "src/shared/tui-agent-startup.test.ts": 191, - "src/shared/ui-language.test.ts": 41, - "src/shared/ui-locale.test.ts": 45, - "src/shared/ui-zoom-level.test.ts": 55, - "src/shared/updated-at-order.test.ts": 104, - "src/shared/updater-windows-signature-check.test.ts": 43, - "src/shared/usage-percentage-display-change-notice.test.ts": 51, - "src/shared/usage-percentage-display.test.ts": 44, - "src/shared/utf8-byte-limits.test.ts": 149, - "src/shared/uuid-v4.test.ts": 47, - "src/shared/vscode-remote-ssh-launcher.test.ts": 53, - "src/shared/window-shortcut-policy-agent-dashboard.test.ts": 72, - "src/shared/window-shortcut-policy.test.ts": 119, - "src/shared/windows-breakaway-launcher.test.ts": 82, - "src/shared/windows-breakaway-previous-stderr.test.ts": 67, - "src/shared/windows-cmd-runner-delayed-launch.test.ts": 43, - "src/shared/windows-command-line-budget.test.ts": 278, - "src/shared/windows-console-input.test.ts": 65, - "src/shared/windows-environment-expansion.test.ts": 108, - "src/shared/windows-interactive-login-spawn.test.ts": 64, - "src/shared/windows-long-path-git-args.test.ts": 49, - "src/shared/windows-security-descriptor.test.ts": 57, - "src/shared/windows-terminal-shell.test.ts": 53, - "src/shared/windows-transient-lock-removal.test.ts": 1054, - "src/shared/worker-terminal-host-scope.test.ts": 98, - "src/shared/worker-transcript-background-task-text.test.ts": 54, - "src/shared/worker-transcript-text-scaling.test.ts": 57, - "src/shared/workspace-attachment-mutation.test.ts": 112, - "src/shared/workspace-attachment-normalization.test.ts": 67, - "src/shared/workspace-attachment-origins.test.ts": 53, - "src/shared/workspace-attachments.test.ts": 291, - "src/shared/workspace-cleanup-applied-filters.test.ts": 76, - "src/shared/workspace-cleanup-browse-state-persistence.test.ts": 61, - "src/shared/workspace-cleanup-ui-state.test.ts": 50, - "src/shared/workspace-cleanup.test.ts": 49, - "src/shared/workspace-doc-history.test.ts": 70, - "src/shared/workspace-layout/tab-group-moves.test.ts": 58, - "src/shared/workspace-layout/workspace-layout-command-model.test.ts": 470, - "src/shared/workspace-layout/workspace-layout-command-persistence.test.ts": 339, - "src/shared/workspace-layout/workspace-layout-commands.test.ts": 187, - "src/shared/workspace-layout/workspace-layout-fact-locations.test.ts": 442, - "src/shared/workspace-layout/workspace-layout-legacy-persistence.test.ts": 359, - "src/shared/workspace-layout/workspace-layout-load-normalization.test.ts": 379, - "src/shared/workspace-layout/workspace-layout-minted-ids.test.ts": 41, - "src/shared/workspace-layout/workspace-layout-model-rules.test.ts": 340, - "src/shared/workspace-layout/workspace-layout-published.test.ts": 303, - "src/shared/workspace-layout/workspace-layout-round-trip.test.ts": 345, - "src/shared/workspace-layout/workspace-layout-stream-frames.test.ts": 275, - "src/shared/workspace-layout/workspace-layout-transitions.test.ts": 89, - "src/shared/workspace-linked-item-equality.test.ts": 45, - "src/shared/workspace-linked-item-source-context.test.ts": 62, - "src/shared/workspace-name.test.ts": 78, - "src/shared/workspace-reference-identity.test.ts": 96, - "src/shared/workspace-review-checks.test.ts": 86, - "src/shared/workspace-session-browser-history.test.ts": 307, - "src/shared/workspace-session-partition-owner.test.ts": 37, - "src/shared/workspace-session-salvage-equivalence.test.ts": 472, - "src/shared/workspace-session-salvage.test.ts": 951, - "src/shared/workspace-session-schema-field-coverage.test.ts": 345, - "src/shared/workspace-session-schema.sleeping-agent.test.ts": 377, - "src/shared/workspace-session-schema.test.ts": 379, - "src/shared/workspace-session-tab-focus-schema.test.ts": 360, - "src/shared/workspace-session-terminal-buffers.test.ts": 316, - "src/shared/workspace-session-terminal-schema.test.ts": 313, - "src/shared/workspace-session-terminal-tab-close.test.ts": 315, - "src/shared/workspace-session-validation-work.test.ts": 377, - "src/shared/workspace-space-compaction.test.ts": 61, - "src/shared/workspace-space-entry-traversal.test.ts": 849, - "src/shared/workspace-space-scan-budget.test.ts": 49, - "src/shared/workspace-statuses.test.ts": 63, - "src/shared/worktree-execution-host-resolution.test.ts": 106, - "src/shared/worktree-name-suggestion.test.ts": 72, - "src/shared/worktree-scan-failure.test.ts": 44, - "src/shared/worktree/archive-failure-blocking-capability.test.ts": 68, - "src/shared/worktree/base-ref.test.ts": 63, - "src/shared/worktree/card-properties.test.ts": 55, - "src/shared/worktree/catalog-version.test.ts": 45, - "src/shared/worktree/create-preparation.test.ts": 47, - "src/shared/worktree/github-pr-suppression.test.ts": 66, - "src/shared/worktree/host-context-labels.test.ts": 50, - "src/shared/worktree/host-qualified-identity.test.ts": 50, - "src/shared/worktree/id.test.ts": 71, - "src/shared/worktree/identity.test.ts": 57, - "src/shared/worktree/local-base-branch-fast-forward-real-git.test.ts": 1130, - "src/shared/worktree/local-base-branch-fast-forward.test.ts": 98, - "src/shared/worktree/ownership-configured-base-visibility.test.ts": 69, - "src/shared/worktree/ownership-worktree-base-path.test.ts": 56, - "src/shared/worktree/ownership.test.ts": 251, - "src/shared/worktree/removal-fence-error.test.ts": 54, - "src/shared/worktree/removal-force-classification.test.ts": 52, - "src/shared/worktree/retired-name-cache.test.ts": 52, - "src/shared/worktree/retired-name-registry.test.ts": 306, - "src/shared/worktree/sort-order-update.test.ts": 42, + "src/shared/terminal-view-attributes.test.ts": 58, + "src/shared/terminal-viewport.test.ts": 206, + "src/shared/terminal-wait-blocked-reason-legacy-alias.test.ts": 39, + "src/shared/terminal-zero-dimensions-diagnostic.test.ts": 25, + "src/shared/test-code-path.test.ts": 29, + "src/shared/text-search-dense-matches.test.ts": 532, + "src/shared/text-search-glob-patterns.test.ts": 277, + "src/shared/text-search-invalid-filename.test.ts": 45, + "src/shared/text-search-paths.test.ts": 51, + "src/shared/text-search-snippet-boundaries.test.ts": 53, + "src/shared/text-search-snippet-retention.test.ts": 562, + "src/shared/text-search.test.ts": 139, + "src/shared/timer-delay.test.ts": 51, + "src/shared/tmux-agent-hook-owner.test.ts": 472, + "src/shared/tmux-client-attachment.test.ts": 35, + "src/shared/tmux-hook-refresh-retirement.test.ts": 602, + "src/shared/tmux-host-attachment-probe.test.ts": 38, + "src/shared/tui-agent-config.test.ts": 47, + "src/shared/tui-agent-fork-startup.test.ts": 148, + "src/shared/tui-agent-launch-command-argument-values.test.ts": 72, + "src/shared/tui-agent-launch-command-override.test.ts": 40, + "src/shared/tui-agent-launch-command-pick-flags.test.ts": 70, + "src/shared/tui-agent-launch-defaults.test.ts": 69, + "src/shared/tui-agent-permissions.test.ts": 52, + "src/shared/tui-agent-prompt-transport.test.ts": 139, + "src/shared/tui-agent-rest-signal.test.ts": 69, + "src/shared/tui-agent-selection.test.ts": 36, + "src/shared/tui-agent-startup-copilot-resume.test.ts": 119, + "src/shared/tui-agent-startup-hermes.test.ts": 165, + "src/shared/tui-agent-startup-kiro-resume.test.ts": 55, + "src/shared/tui-agent-startup-session-options.test.ts": 119, + "src/shared/tui-agent-startup-shell.test.ts": 120, + "src/shared/tui-agent-startup.test.ts": 175, + "src/shared/ui-language.test.ts": 23, + "src/shared/ui-locale.test.ts": 32, + "src/shared/ui-zoom-level.test.ts": 31, + "src/shared/updated-at-order.test.ts": 84, + "src/shared/updater-windows-signature-check.test.ts": 31, + "src/shared/usage-percentage-display-change-notice.test.ts": 35, + "src/shared/usage-percentage-display.test.ts": 34, + "src/shared/utf8-byte-limits.test.ts": 137, + "src/shared/uuid-v4.test.ts": 34, + "src/shared/vscode-remote-ssh-launcher.test.ts": 32, + "src/shared/web-origin-security.test.ts": 29, + "src/shared/window-shortcut-policy-agent-dashboard.test.ts": 70, + "src/shared/window-shortcut-policy-ime-process.test.ts": 66, + "src/shared/window-shortcut-policy.test.ts": 96, + "src/shared/windows-breakaway-launcher.test.ts": 69, + "src/shared/windows-breakaway-previous-stderr.test.ts": 40, + "src/shared/windows-cmd-runner-delayed-launch.test.ts": 37, + "src/shared/windows-command-line-budget.test.ts": 250, + "src/shared/windows-console-input.test.ts": 29, + "src/shared/windows-environment-expansion.test.ts": 83, + "src/shared/windows-interactive-login-spawn.test.ts": 41, + "src/shared/windows-long-path-git-args.test.ts": 35, + "src/shared/windows-path-access.test.ts": 50, + "src/shared/windows-secure-dacl.test.ts": 42, + "src/shared/windows-security-addon.test.ts": 37, + "src/shared/windows-security-addon.win32.test.ts": 33, + "src/shared/windows-terminal-shell.test.ts": 41, + "src/shared/windows-transient-lock-removal.test.ts": 825, + "src/shared/worker-terminal-host-scope.test.ts": 64, + "src/shared/worker-transcript-background-task-text.test.ts": 37, + "src/shared/worker-transcript-text-scaling.test.ts": 45, + "src/shared/workspace-attachment-mutation.test.ts": 113, + "src/shared/workspace-attachment-normalization.test.ts": 87, + "src/shared/workspace-attachment-origins.test.ts": 47, + "src/shared/workspace-attachments.test.ts": 318, + "src/shared/workspace-cleanup-applied-filters.test.ts": 51, + "src/shared/workspace-cleanup-browse-state-persistence.test.ts": 46, + "src/shared/workspace-cleanup-ui-state.test.ts": 34, + "src/shared/workspace-cleanup.test.ts": 41, + "src/shared/workspace-doc-history.test.ts": 50, + "src/shared/workspace-layout/old-client/old-client-method-translation.test.ts": 463, + "src/shared/workspace-layout/old-client/old-client-pane-layout-translation.test.ts": 340, + "src/shared/workspace-layout/old-client/old-client-session-tabs-projection.test.ts": 147, + "src/shared/workspace-layout/tab-group-moves.test.ts": 48, + "src/shared/workspace-layout/terminal-row-order.test.ts": 250, + "src/shared/workspace-layout/workspace-layout-command-model.test.ts": 488, + "src/shared/workspace-layout/workspace-layout-command-persistence.test.ts": 303, + "src/shared/workspace-layout/workspace-layout-commands.test.ts": 145, + "src/shared/workspace-layout/workspace-layout-editor-rekey.test.ts": 142, + "src/shared/workspace-layout/workspace-layout-fact-locations.test.ts": 449, + "src/shared/workspace-layout/workspace-layout-initial-surface.test.ts": 81, + "src/shared/workspace-layout/workspace-layout-legacy-persistence.test.ts": 507, + "src/shared/workspace-layout/workspace-layout-load-normalization.test.ts": 378, + "src/shared/workspace-layout/workspace-layout-minted-ids.test.ts": 37, + "src/shared/workspace-layout/workspace-layout-model-rules.test.ts": 310, + "src/shared/workspace-layout/workspace-layout-pane-status.test.ts": 52, + "src/shared/workspace-layout/workspace-layout-process-rules.test.ts": 129, + "src/shared/workspace-layout/workspace-layout-published.test.ts": 307, + "src/shared/workspace-layout/workspace-layout-round-trip.test.ts": 339, + "src/shared/workspace-layout/workspace-layout-stream-frames.test.ts": 294, + "src/shared/workspace-layout/workspace-layout-transitions.test.ts": 139, + "src/shared/workspace-layout/workspace-layout-view-tab-closes.test.ts": 68, + "src/shared/workspace-layout/workspace-layout-views.test.ts": 113, + "src/shared/workspace-linked-item-equality.test.ts": 28, + "src/shared/workspace-linked-item-source-context.test.ts": 57, + "src/shared/workspace-name.test.ts": 61, + "src/shared/workspace-reference-identity.test.ts": 60, + "src/shared/workspace-review-checks.test.ts": 84, + "src/shared/workspace-session-browser-history.test.ts": 301, + "src/shared/workspace-session-partition-owner.test.ts": 23, + "src/shared/workspace-session-salvage-equivalence.test.ts": 561, + "src/shared/workspace-session-salvage.test.ts": 1048, + "src/shared/workspace-session-schema-field-coverage.test.ts": 313, + "src/shared/workspace-session-schema.sleeping-agent.test.ts": 340, + "src/shared/workspace-session-schema.test.ts": 374, + "src/shared/workspace-session-tab-focus-schema.test.ts": 337, + "src/shared/workspace-session-terminal-buffers.test.ts": 346, + "src/shared/workspace-session-terminal-schema.test.ts": 323, + "src/shared/workspace-session-terminal-tab-close.test.ts": 283, + "src/shared/workspace-session-validation-work.test.ts": 351, + "src/shared/workspace-space-compaction.test.ts": 48, + "src/shared/workspace-space-entry-traversal.test.ts": 758, + "src/shared/workspace-space-scan-budget.test.ts": 42, + "src/shared/workspace-statuses.test.ts": 62, + "src/shared/worktree-execution-host-resolution.test.ts": 59, + "src/shared/worktree-name-suggestion.test.ts": 56, + "src/shared/worktree-scan-failure.test.ts": 30, + "src/shared/worktree/archive-failure-blocking-capability.test.ts": 212, + "src/shared/worktree/base-ref.test.ts": 45, + "src/shared/worktree/card-properties.test.ts": 30, + "src/shared/worktree/catalog-version.test.ts": 35, + "src/shared/worktree/create-preparation.test.ts": 38, + "src/shared/worktree/github-pr-suppression.test.ts": 229, + "src/shared/worktree/host-context-labels.test.ts": 33, + "src/shared/worktree/host-qualified-identity.test.ts": 38, + "src/shared/worktree/id.test.ts": 62, + "src/shared/worktree/identity.test.ts": 30, + "src/shared/worktree/local-base-branch-fast-forward-real-git.test.ts": 756, + "src/shared/worktree/local-base-branch-fast-forward.test.ts": 106, + "src/shared/worktree/ownership-configured-base-visibility.test.ts": 72, + "src/shared/worktree/ownership-worktree-base-path.test.ts": 44, + "src/shared/worktree/ownership.test.ts": 200, + "src/shared/worktree/removal-fence-error.test.ts": 33, + "src/shared/worktree/removal-force-classification.test.ts": 45, + "src/shared/worktree/retired-name-cache.test.ts": 44, + "src/shared/worktree/retired-name-registry.test.ts": 412, + "src/shared/worktree/sort-order-update.test.ts": 31, "src/shared/worktree/visibility-sources.test.ts": 65, - "src/shared/ws-outbound-backpressure-queue.test.ts": 79, - "src/shared/wsl-login-shell-command.test.ts": 105, - "src/shared/wsl-paths.test.ts": 64, - "src/shared/zcode-missing-tui.test.ts": 56, - "src/shared/zcode-orchestration-contract.test.ts": 161, + "src/shared/ws-outbound-backpressure-queue.test.ts": 70, + "src/shared/wsl-host-deployment-store.test.ts": 409, + "src/shared/wsl-login-shell-command.test.ts": 84, + "src/shared/wsl-move/wsl-move-dispositions.test.ts": 529, + "src/shared/wsl-paths.test.ts": 53, + "src/shared/zcode-missing-tui.test.ts": 39, + "src/shared/zcode-orchestration-contract.test.ts": 155, "src/shared/zod-salvage-absence.test.ts": 208, - "src/wsl-guest/agent-hook-canonical-status.test.ts": 2544, - "src/wsl-guest/agent-hook-envelope-publication.test.ts": 385, + "src/wsl-guest/agent-hook-canonical-status.test.ts": 2718, + "src/wsl-guest/agent-hook-envelope-publication.test.ts": 333, "src/wsl-guest/agent-hook-integration.test.ts": 1405, - "src/wsl-guest/agent-hook-interrupt-reconciliation.test.ts": 540, - "src/wsl-guest/agent-hook-opencode-tui-identity.test.ts": 638, - "src/wsl-guest/agent-hook-server-claude-owed-notification-expiry.test.ts": 700, - "src/wsl-guest/agent-hook-server-codex-subagent-transcript.test.ts": 1475, - "src/wsl-guest/agent-hook-server-codex-turn-interruption.test.ts": 1828, - "src/wsl-guest/agent-hook-server.test.ts": 1582, - "src/wsl-guest/dispatcher-bulk-admission.test.ts": 415, - "src/wsl-guest/dispatcher-bulk-failed-admission.test.ts": 262, - "src/wsl-guest/dispatcher-bulk-retirement-admission.test.ts": 3089, - "src/wsl-guest/dispatcher-capacity-degradation.test.ts": 314, - "src/wsl-guest/dispatcher-client-close-cause.test.ts": 257, - "src/wsl-guest/dispatcher-client-writer.test.ts": 86, - "src/wsl-guest/dispatcher-frame-guard-regressions.test.ts": 280, - "src/wsl-guest/dispatcher-json-payload.test.ts": 343, - "src/wsl-guest/dispatcher-notification-ownership.test.ts": 218, - "src/wsl-guest/dispatcher-per-connection-state-baseline.test.ts": 266, - "src/wsl-guest/dispatcher-rpc-cancel-id-coercion.test.ts": 245, - "src/wsl-guest/dispatcher-silent-client-reaper.test.ts": 279, - "src/wsl-guest/dispatcher-structured-error.test.ts": 275, - "src/wsl-guest/dispatcher-timeout.test.ts": 247, - "src/wsl-guest/dispatcher-writer-admission.test.ts": 111, - "src/wsl-guest/dispatcher.test.ts": 414, - "src/wsl-guest/legacy-relay-publication-ledger.test.ts": 50, - "src/wsl-guest/omp-config-root.test.ts": 177, - "src/wsl-guest/opencode-startup-prompt-install.test.ts": 139, - "src/wsl-guest/plugin-overlay-env.test.ts": 98, - "src/wsl-guest/plugin-overlay.test.ts": 164, - "src/wsl-guest/plugin-source-limit.test.ts": 46, - "src/wsl-guest/preflight-handler.test.ts": 84, - "src/wsl-guest/protocol-backpressure.test.ts": 109, - "src/wsl-guest/protocol-handshake.test.ts": 59, - "src/wsl-guest/protocol-json-payload.test.ts": 143, - "src/wsl-guest/relay-agent-presence.test.ts": 398, - "src/wsl-guest/relay-command-env.test.ts": 77, - "src/wsl-guest/relay-hot-path-operation-counts.test.ts": 279, - "src/wsl-guest/wsl-agent-hook-relay.test.ts": 454, - "src/wsl-guest/wsl-hook-fs-bridge.test.ts": 80, - "src/wsl-guest/wsl-install-plugins-handler.test.ts": 147, - "tests/e2e/accumulated-workspace-state-builder.unit.test.ts": 120, - "tests/e2e/alternate-screen-fixture-script.unit.test.ts": 60, - "tests/e2e/codex-child-approval-activity-row.unit.test.ts": 4279, - "tests/e2e/completed-worker-retirement-resume.unit.test.ts": 14396, - "tests/e2e/cursor-quota-transport.unit.test.ts": 1102, - "tests/e2e/daemon-running-work-probe.unit.test.ts": 315, - "tests/e2e/folder-upgrade-identity-persistence.unit.test.ts": 2238, - "tests/e2e/github-work-item-search-burst.unit.test.ts": 1180, - "tests/e2e/global-setup-cli-artifact.unit.test.ts": 55, - "tests/e2e/global-teardown.unit.test.ts": 124, - "tests/e2e/helpers/alt-screen-frame.unit.test.ts": 96, - "tests/e2e/helpers/client-hosted-browser-fixture.unit.test.ts": 752, - "tests/e2e/helpers/electron-crashpad-cleanup.unit.test.ts": 49, - "tests/e2e/helpers/electron-home-isolation.unit.test.ts": 51, - "tests/e2e/helpers/electron-launch-args.unit.test.ts": 57, - "tests/e2e/helpers/electron-main-evaluate-retry.unit.test.ts": 75, - "tests/e2e/helpers/electron-process-shutdown.unit.test.ts": 69, - "tests/e2e/helpers/fake-agent-command-override.unit.test.ts": 66, - "tests/e2e/helpers/fake-agent-paste-end-scanner.unit.test.ts": 68, - "tests/e2e/helpers/git-status-retry-barrier.unit.test.ts": 59, - "tests/e2e/helpers/golden-source-control.unit.test.ts": 645, - "tests/e2e/helpers/headless-paired-runtime-serve-readiness.unit.test.ts": 56, - "tests/e2e/helpers/paired-client-runtime-environment.unit.test.ts": 59, - "tests/e2e/helpers/paired-client-window-reveal.unit.test.ts": 179, - "tests/e2e/helpers/persisted-profile-state.unit.test.ts": 470, - "tests/e2e/helpers/remote-skill-cloud-fixture.unit.test.ts": 421, - "tests/e2e/helpers/streaming-terminal-cleanup.unit.test.ts": 51, - "tests/e2e/helpers/terminal-pty-write-spy.unit.test.ts": 64, - "tests/e2e/host-cold-park-remote-subscriber.unit.test.ts": 7476, - "tests/e2e/host-guest-paint-retention-remote-viewer.unit.test.ts": 5043, - "tests/e2e/native-chat-ssh-delivery.unit.test.ts": 5842, - "tests/e2e/omp-child-session-recovery.unit.test.ts": 1983, - "tests/e2e/omp-retired-host-renderer.unit.test.ts": 1794, - "tests/e2e/omp-risk-remote-review.unit.test.ts": 965, - "tests/e2e/orca-restart-navigation.unit.test.ts": 537, - "tests/e2e/orchestration-run-pagination.unit.test.ts": 52, - "tests/e2e/paced-terminal-typing.unit.test.ts": 475, - "tests/e2e/paired-runtime-rejected-input-remount.unit.test.ts": 4522, - "tests/e2e/remote-agent-completion-authority.unit.test.ts": 1490, - "tests/e2e/remote-terminal-input-delivery-model.unit.test.ts": 18232, - "tests/e2e/remote-terminal-tab-retirement.unit.test.ts": 5251, - "tests/e2e/restored-terminal-input-readiness.unit.test.ts": 71, - "tests/e2e/runtime-graph-publication-probe.unit.test.ts": 65, - "tests/e2e/session-tabs-decorative-title-fanout.unit.test.ts": 5357, - "tests/e2e/session-tabs-empty-inventory-daemon-oracle.unit.test.ts": 1142, - "tests/e2e/session-tabs-rich-status-boundaries.unit.test.ts": 5281, - "tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts": 362, - "tests/e2e/structured-agent-session-read-owner.unit.test.ts": 2988, - "tests/e2e/structured-attention-explicit-read.unit.test.ts": 13645, - "tests/e2e/structured-attention-headless-delivery.unit.test.ts": 259, - "tests/e2e/structured-attention-host-read-retirement.unit.test.ts": 3238, - "tests/e2e/structured-attention-prompt-relay.unit.test.ts": 17392, - "tests/e2e/structured-attention-read-owner-lifecycle.unit.test.ts": 17537, - "tests/e2e/structured-attention-read-retirement.unit.test.ts": 13824, - "tests/e2e/structured-attention-relay-settlement.unit.test.ts": 19420, - "tests/e2e/structured-chat-owner-status-activation.unit.test.ts": 3376, - "tests/e2e/structured-native-chat-routing-authority.unit.test.ts": 1170, - "tests/e2e/structured-session-reopened-tab-close.unit.test.ts": 25072, - "tests/e2e/sustained-agent-typing-load-scripts.unit.test.ts": 2142, - "tests/e2e/terminal-account-restart.unit.test.ts": 2678, - "tests/e2e/terminal-binding-crash-hydration.unit.test.ts": 1477, - "tests/e2e/terminal-foreground-confirmation.unit.test.ts": 477, - "tests/e2e/terminal-layout-parity-snapshot.unit.test.ts": 91, - "tests/e2e/terminal-perf-presentation.unit.test.ts": 64, - "tests/e2e/terminal-probe-input-sequence.unit.test.ts": 42, - "tests/e2e/terminal-split-activation-latency-artifact.unit.test.ts": 64, - "tests/e2e/terminal-split-activation-latency-report.unit.test.ts": 70, - "tests/tools/omp-close-lifecycle.test.mjs": 283, - "tests/tools/omp-config-root-local-shell.test.mjs": 249, - "tests/tools/omp-config-root-relay-shell.test.mjs": 162, - "tests/tools/relay-bench/phone-e2ee-desktop-parity.test.mjs": 114, - "tests/tools/relay-bench/region-probe-replay.test.mjs": 84, - "tests/tools/relay-bench/relay-bench-invocation.test.mjs": 102, - "tests/tools/relay-bench/relay-bench-state-file.test.mjs": 65, - "tests/tools/relay-bench/relay-phone-connect-bench.test.mjs": 89, - "tests/tools/win-update-e2e/app-driver.test.mjs": 541, - "tests/tools/win-update-e2e/app-shutdown.test.mjs": 644, - "tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-oracle.test.mjs": 59, - "tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-probe.test.mjs": 253, - "tests/tools/windows-pty-native-capability-smoke/run.test.mjs": 57 + "src/wsl-guest/agent-hook-interrupt-reconciliation.test.ts": 611, + "src/wsl-guest/agent-hook-opencode-tui-identity.test.ts": 688, + "src/wsl-guest/agent-hook-server-claude-owed-notification-expiry.test.ts": 816, + "src/wsl-guest/agent-hook-server-codex-subagent-transcript.test.ts": 1462, + "src/wsl-guest/agent-hook-server-codex-turn-interruption.test.ts": 1576, + "src/wsl-guest/agent-hook-server.test.ts": 1487, + "src/wsl-guest/dispatcher-bulk-admission.test.ts": 524, + "src/wsl-guest/dispatcher-bulk-failed-admission.test.ts": 236, + "src/wsl-guest/dispatcher-bulk-retirement-admission.test.ts": 2530, + "src/wsl-guest/dispatcher-capacity-degradation.test.ts": 392, + "src/wsl-guest/dispatcher-client-close-cause.test.ts": 240, + "src/wsl-guest/dispatcher-client-writer.test.ts": 73, + "src/wsl-guest/dispatcher-frame-guard-regressions.test.ts": 204, + "src/wsl-guest/dispatcher-json-payload.test.ts": 266, + "src/wsl-guest/dispatcher-notification-ownership.test.ts": 245, + "src/wsl-guest/dispatcher-per-connection-state-baseline.test.ts": 255, + "src/wsl-guest/dispatcher-rpc-cancel-id-coercion.test.ts": 252, + "src/wsl-guest/dispatcher-silent-client-reaper.test.ts": 260, + "src/wsl-guest/dispatcher-structured-error.test.ts": 266, + "src/wsl-guest/dispatcher-timeout.test.ts": 249, + "src/wsl-guest/dispatcher-writer-admission.test.ts": 110, + "src/wsl-guest/dispatcher.test.ts": 476, + "src/wsl-guest/legacy-relay-publication-ledger.test.ts": 35, + "src/wsl-guest/omp-config-root.test.ts": 212, + "src/wsl-guest/opencode-startup-prompt-install.test.ts": 181, + "src/wsl-guest/plugin-overlay-env.test.ts": 116, + "src/wsl-guest/plugin-overlay.test.ts": 176, + "src/wsl-guest/plugin-source-limit.test.ts": 29, + "src/wsl-guest/preflight-handler.test.ts": 77, + "src/wsl-guest/protocol-backpressure.test.ts": 106, + "src/wsl-guest/protocol-handshake.test.ts": 47, + "src/wsl-guest/protocol-json-payload.test.ts": 123, + "src/wsl-guest/relay-agent-presence.test.ts": 471, + "src/wsl-guest/relay-command-env.test.ts": 63, + "src/wsl-guest/relay-hot-path-operation-counts.test.ts": 286, + "src/wsl-guest/wsl-agent-hook-relay.test.ts": 469, + "src/wsl-guest/wsl-hook-fs-bridge.test.ts": 63, + "src/wsl-guest/wsl-install-plugins-handler.test.ts": 175, + "tests/e2e/accumulated-workspace-state-builder.unit.test.ts": 97, + "tests/e2e/agent-launch-cli.unit.test.ts": 2368, + "tests/e2e/agent-launch-setup-choice.unit.test.ts": 2109, + "tests/e2e/alternate-screen-fixture-script.unit.test.ts": 41, + "tests/e2e/claude-auto-mode-host-catalog.unit.test.ts": 2987, + "tests/e2e/codex-child-approval-activity-row.unit.test.ts": 4090, + "tests/e2e/completed-worker-retirement-resume.unit.test.ts": 16736, + "tests/e2e/cursor-quota-transport.unit.test.ts": 1224, + "tests/e2e/daemon-running-work-probe.unit.test.ts": 259, + "tests/e2e/folder-upgrade-identity-persistence.unit.test.ts": 1871, + "tests/e2e/github-work-item-search-burst.unit.test.ts": 1226, + "tests/e2e/global-setup-cli-artifact.unit.test.ts": 45, + "tests/e2e/global-teardown.unit.test.ts": 111, + "tests/e2e/helpers/alt-screen-frame.unit.test.ts": 87, + "tests/e2e/helpers/client-hosted-browser-fixture.unit.test.ts": 858, + "tests/e2e/helpers/electron-crashpad-cleanup.unit.test.ts": 36, + "tests/e2e/helpers/electron-home-isolation.unit.test.ts": 229, + "tests/e2e/helpers/electron-launch-args.unit.test.ts": 44, + "tests/e2e/helpers/electron-main-evaluate-retry.unit.test.ts": 42, + "tests/e2e/helpers/electron-process-shutdown.unit.test.ts": 4342, + "tests/e2e/helpers/fake-agent-command-override.unit.test.ts": 41, + "tests/e2e/helpers/fake-agent-paste-end-scanner.unit.test.ts": 49, + "tests/e2e/helpers/git-status-retry-barrier.unit.test.ts": 40, + "tests/e2e/helpers/golden-source-control.unit.test.ts": 531, + "tests/e2e/helpers/headless-paired-runtime-serve-readiness.unit.test.ts": 47, + "tests/e2e/helpers/paired-client-runtime-environment.unit.test.ts": 44, + "tests/e2e/helpers/paired-client-window-reveal.unit.test.ts": 48, + "tests/e2e/helpers/persisted-profile-state.unit.test.ts": 407, + "tests/e2e/helpers/remote-skill-cloud-fixture.unit.test.ts": 349, + "tests/e2e/helpers/streaming-terminal-cleanup.unit.test.ts": 30, + "tests/e2e/helpers/terminal-pty-write-spy.unit.test.ts": 33, + "tests/e2e/helpers/workspace-layout-oracle-feed.unit.test.ts": 77, + "tests/e2e/host-agent-launch-settings-hook-bookkeeping.unit.test.ts": 4389, + "tests/e2e/host-cold-park-remote-subscriber.unit.test.ts": 5953, + "tests/e2e/host-guest-paint-retention-remote-viewer.unit.test.ts": 4969, + "tests/e2e/native-chat-ssh-delivery.unit.test.ts": 6131, + "tests/e2e/omp-child-session-recovery.unit.test.ts": 1804, + "tests/e2e/omp-retired-host-renderer.unit.test.ts": 1991, + "tests/e2e/omp-risk-remote-review.unit.test.ts": 876, + "tests/e2e/orca-restart-navigation.unit.test.ts": 310, + "tests/e2e/orchestration-run-pagination.unit.test.ts": 32, + "tests/e2e/paced-terminal-typing.unit.test.ts": 379, + "tests/e2e/paired-runtime-rejected-input-remount.unit.test.ts": 4236, + "tests/e2e/remote-agent-completion-authority.unit.test.ts": 1509, + "tests/e2e/remote-terminal-input-delivery-model.unit.test.ts": 32257, + "tests/e2e/remote-terminal-tab-retirement.unit.test.ts": 4857, + "tests/e2e/restored-terminal-input-readiness.unit.test.ts": 57, + "tests/e2e/runtime-graph-publication-probe.unit.test.ts": 40, + "tests/e2e/session-tabs-decorative-title-fanout.unit.test.ts": 4631, + "tests/e2e/session-tabs-empty-inventory-daemon-oracle.unit.test.ts": 828, + "tests/e2e/session-tabs-rich-status-boundaries.unit.test.ts": 5112, + "tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts": 689, + "tests/e2e/structured-agent-session-read-owner.unit.test.ts": 4296, + "tests/e2e/structured-attention-headless-delivery.unit.test.ts": 221, + "tests/e2e/structured-attention-host-read-retirement.unit.test.ts": 2723, + "tests/e2e/structured-attention-prompt-relay.unit.test.ts": 28835, + "tests/e2e/structured-attention-read-retirement.unit.test.ts": 8627, + "tests/e2e/structured-chat-owner-status-activation.unit.test.ts": 2817, + "tests/e2e/structured-native-chat-routing-authority.unit.test.ts": 1325, + "tests/e2e/structured-session-reopened-tab-close.unit.test.ts": 30040, + "tests/e2e/structured-session-tab-close-stale-frame.unit.test.ts": 14249, + "tests/e2e/sustained-agent-typing-load-scripts.unit.test.ts": 2153, + "tests/e2e/terminal-account-restart.unit.test.ts": 3437, + "tests/e2e/terminal-binding-crash-hydration.unit.test.ts": 1349, + "tests/e2e/terminal-foreground-confirmation.unit.test.ts": 483, + "tests/e2e/terminal-layout-parity-snapshot.unit.test.ts": 41, + "tests/e2e/terminal-perf-presentation.unit.test.ts": 57, + "tests/e2e/terminal-probe-input-sequence.unit.test.ts": 40, + "tests/e2e/terminal-split-activation-latency-artifact.unit.test.ts": 44, + "tests/e2e/terminal-split-activation-latency-report.unit.test.ts": 116, + "tests/e2e/worktree-status-warning.unit.test.ts": 1920, + "tests/tools/omp-close-lifecycle.test.mjs": 277, + "tests/tools/omp-config-root-local-shell.test.mjs": 216, + "tests/tools/omp-config-root-relay-shell.test.mjs": 234, + "tests/tools/relay-bench/phone-e2ee-desktop-parity.test.mjs": 131, + "tests/tools/relay-bench/region-probe-replay.test.mjs": 56, + "tests/tools/relay-bench/relay-bench-invocation.test.mjs": 67, + "tests/tools/relay-bench/relay-bench-state-file.test.mjs": 53, + "tests/tools/relay-bench/relay-phone-connect-bench.test.mjs": 77, + "tests/tools/win-update-e2e/app-driver.test.mjs": 300, + "tests/tools/win-update-e2e/app-shutdown.test.mjs": 799, + "tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-oracle.test.mjs": 44, + "tests/tools/windows-pty-native-capability-smoke/packaged-node-pty-capability-probe.test.mjs": 213, + "tests/tools/windows-pty-native-capability-smoke/run.test.mjs": 43 } }, "e2e": { diff --git a/config/scripts/ci-shard-timings.md b/config/scripts/ci-shard-timings.md index db79660a183..eee0dc34bf9 100644 --- a/config/scripts/ci-shard-timings.md +++ b/config/scripts/ci-shard-timings.md @@ -1,6 +1,6 @@ # Timing-based CI shards -The eight unit shards and fourteen general E2E shards use longest-processing-time +Unit shards and general E2E shards use longest-processing-time assignment of whole files to the currently lightest shard. Ties use file path and then shard index, independent of filesystem enumeration and locale. Unknown, zero, or invalid durations use the baseline's positive median (1 second when no @@ -46,7 +46,32 @@ age as specs change. Full CI runs on the existing runner classes are required to measure elapsed-time and occupancy improvements, including discovery overhead. No retries, assertions, coverage exclusions, runner classes or shard counts changed. -## Current unit refresh: September 27, 2026 +## Current unit refresh: October 14, 2026 + +The baseline imports all five successful ARM unit shards from +[run 38646706136, attempt 1](https://github.com/stablyai/orca/actions/runs/38646706136/attempts/1). +The tested merge was `8d7f46198b99432efd104ac7803174173b367c66`, using Node +24.21.0, Bun 1.4.3, `ubuntu-24.04-arm`, and four workers per shard. All 12,314 +files appear exactly once: 12,163 passed and 151 skipped, with zero unhandled +errors. Artifact digests, all 20 report hashes, source/runtime provenance, full +discovery, and each saved assignment were checked before importing. The E2E +weights remain unchanged. + +| Shard | Job ID | Measured worker-seconds | Job seconds | +| --- | --- | ---: | ---: | +| 1 | 116137259336 | 1,966.470 | 568 | +| 2 | 116137259460 | 2,080.624 | 604 | +| 3 | 116137259318 | 2,121.495 | 611 | +| 4 | 116137259783 | 2,094.487 | 614 | +| 5 | 116137259540 | 2,190.636 | 641 | + +Retrospectively assigning those measurements projects 2,090.726–2,090.747 +worker-seconds per shard, instead of 1,966.470–2,190.636. That is a scheduling +projection, not a hosted speedup: module times overlap across workers, and total +runner occupancy was 50.63 minutes. Runner capacity, selection, workers, and the +assignment algorithm are unchanged. Later source changes need fresh validation. + +## Historical unit refresh: September 27, 2026 The baseline imports every successful Node 24 shard from [run 36294142683, attempt 1](https://github.com/stablyai/orca/actions/runs/36294142683/attempts/1). diff --git a/config/scripts/ci-unit-files.mjs b/config/scripts/ci-unit-files.mjs index 3821d25e560..a8cc794828d 100644 --- a/config/scripts/ci-unit-files.mjs +++ b/config/scripts/ci-unit-files.mjs @@ -35,6 +35,16 @@ export const UNIT_EXCLUDE = [ 'src/shared/posix-command-path-lookup.test.ts', 'tests/e2e/relay-region-compatibility.unit.test.ts', 'tests/e2e/relay-region-correction.unit.test.ts', + // Electron probes with in-process deadlines; the package job runs them serially. + 'src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts', + 'src/main/browser/browser-route-tcp-egress.electron.test.ts', + 'src/main/browser/browser-route-webrtc-egress.electron.test.ts', + 'src/main/browser/browser-route-h3-egress.electron.test.ts', + 'src/main/browser/browser-route-dns-prefetch.electron.test.ts', + 'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts', + 'src/main/browser/browser-popup-compatibility.electron.test.ts', + 'src/main/browser/browser-popup-native-compatibility.electron.test.ts', + 'src/main/browser/browser-popup-background-compatibility.electron.test.ts', `${CROSS_VERSION_WIRE_DIR}**` ] diff --git a/config/scripts/ci-unit-module-cache-contents.mjs b/config/scripts/ci-unit-module-cache-contents.mjs new file mode 100644 index 00000000000..f0f1ffa9a75 --- /dev/null +++ b/config/scripts/ci-unit-module-cache-contents.mjs @@ -0,0 +1,217 @@ +import { createHash } from 'node:crypto' +import { constants } from 'node:fs' +import { lstat, open, readdir } from 'node:fs/promises' +import { join } from 'node:path' +import { checkModuleTrailer } from './ci-unit-module-cache-trailer.mjs' + +export const MODULE_CACHE_MANIFEST = '_orca_manifest.json' +export const MODULE_CACHE_LIMITS = Object.freeze({ + entries: 50_000, + totalBytes: 2 ** 30, + entryBytes: 32 * 2 ** 20 +}) +const METADATA = '_metadata.json' +const MODULE_NAME = /^[a-f0-9]{40}$/ +const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) +const fail = (message) => { + throw new Error(`Unit module cache: ${message}`) +} + +async function readRegularFile(path) { + const before = await lstat(path) + if (!before.isFile()) { + fail(`not a regular file: ${path}`) + } + const file = await open( + path, + constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0) + ) + try { + const info = await file.stat() + if (!info.isFile() || info.size > MODULE_CACHE_LIMITS.entryBytes) { + fail(`invalid file size or type: ${path}`) + } + const buffer = Buffer.alloc(info.size + 1) + let bytes = 0 + while (bytes < buffer.length) { + const result = await file.read(buffer, bytes, buffer.length - bytes, bytes) + if (!result.bytesRead) { + break + } + bytes += result.bytesRead + } + if (bytes !== info.size) { + fail(`file changed while reading: ${path}`) + } + return buffer.subarray(0, bytes) + } finally { + await file.close() + } +} + +function checkLockfileHash(lockfileHash) { + if (typeof lockfileHash !== 'string' || !/^[a-f0-9]{8}$/.test(lockfileHash)) { + fail('invalid expected lockfile hash') + } +} + +export async function inventoryModuleCache({ path, lockfileHash }) { + checkLockfileHash(lockfileHash) + if (!(await lstat(path)).isDirectory()) { + fail('cache root is not a regular directory') + } + const names = (await readdir(path)).sort() + if (names.length > MODULE_CACHE_LIMITS.entries) { + fail('too many cache entries') + } + if (!names.includes(METADATA)) { + fail('cache metadata is missing') + } + let totalBytes = 0 + let manifestBytes = 0 + const entries = Array.from({ length: names.length }) + let next = 0 + const workers = await Promise.allSettled( + Array.from({ length: Math.min(4, names.length) }, async () => { + while (next < names.length) { + const index = next++ + const name = names[index] + if (name !== METADATA && name !== MODULE_CACHE_MANIFEST && !MODULE_NAME.test(name)) { + fail(`invalid cache entry name: ${name}`) + } + const buffer = await readRegularFile(join(path, name)) + if (name === MODULE_CACHE_MANIFEST) { + manifestBytes = buffer.length + } else { + totalBytes += buffer.length + } + if (totalBytes + manifestBytes > MODULE_CACHE_LIMITS.totalBytes) { + fail('cache byte limit exceeded') + } + if (name === MODULE_CACHE_MANIFEST) { + continue + } + if (name === METADATA) { + const metadata = JSON.parse(buffer.toString('utf8')) + if (!isRecord(metadata) || metadata.lockfileHash !== lockfileHash) { + fail('cache lockfile hash mismatch') + } + } else { + checkModuleTrailer(buffer) + } + entries[index] = { + name, + bytes: buffer.length, + sha256: createHash('sha256').update(buffer).digest('hex') + } + } + }) + ) + const failure = workers.find((result) => result.status === 'rejected') + if (failure) { + throw failure.reason + } + return { entries: entries.filter(Boolean), totalBytes } +} + +function checkIdentity({ key, format, versions, sourceSha, lockfileHash }) { + checkLockfileHash(lockfileHash) + if (typeof key !== 'string' || !key || typeof format !== 'string' || !format) { + fail('invalid cache identity') + } + if ( + !isRecord(versions) || + !['node', 'bun', 'vitest'].every((name) => typeof versions[name] === 'string' && versions[name]) + ) { + fail('invalid cache versions') + } + if (sourceSha !== undefined && (typeof sourceSha !== 'string' || !MODULE_NAME.test(sourceSha))) { + fail('invalid source SHA') + } +} + +export async function sealModuleCache(options) { + checkIdentity(options) + if (!options.sourceSha) { + fail('source SHA is required when sealing') + } + const inventory = await inventoryModuleCache(options) + const manifest = { + version: 1, + key: options.key, + format: options.format, + versions: options.versions, + sourceSha: options.sourceSha, + lockfileHash: options.lockfileHash, + ...inventory + } + const content = `${JSON.stringify(manifest)}\n` + if ( + Buffer.byteLength(content) > MODULE_CACHE_LIMITS.entryBytes || + inventory.totalBytes + Buffer.byteLength(content) > MODULE_CACHE_LIMITS.totalBytes || + inventory.entries.length + 1 > MODULE_CACHE_LIMITS.entries + ) { + fail('sealed cache exceeds limits') + } + const file = await open( + join(options.path, MODULE_CACHE_MANIFEST), + constants.O_WRONLY | + constants.O_CREAT | + (constants.O_NOFOLLOW ?? 0) | + (constants.O_NONBLOCK ?? 0), + 0o600 + ) + try { + if (!(await file.stat()).isFile()) { + fail('manifest is not a regular file') + } + await file.truncate(0) + await file.writeFile(content) + } finally { + await file.close() + } + return manifest +} + +export async function validateModuleCache(options) { + checkIdentity(options) + const manifest = JSON.parse( + (await readRegularFile(join(options.path, MODULE_CACHE_MANIFEST))).toString('utf8') + ) + if (!isRecord(manifest)) { + fail('invalid cache manifest') + } + checkIdentity(manifest) + if ( + !manifest.sourceSha || + manifest.version !== 1 || + manifest.key !== options.key || + manifest.format !== options.format || + manifest.lockfileHash !== options.lockfileHash || + (options.sourceSha !== undefined && manifest.sourceSha !== options.sourceSha) || + !['node', 'bun', 'vitest'].every((name) => manifest.versions[name] === options.versions[name]) + ) { + fail('cache manifest identity mismatch') + } + const inventory = await inventoryModuleCache(options) + if ( + manifest.totalBytes !== inventory.totalBytes || + !Array.isArray(manifest.entries) || + manifest.entries.length !== inventory.entries.length + ) { + fail('cache inventory mismatch') + } + for (let index = 0; index < inventory.entries.length; index++) { + const expected = manifest.entries[index] + const actual = inventory.entries[index] + if ( + !isRecord(expected) || + expected.name !== actual.name || + expected.bytes !== actual.bytes || + expected.sha256 !== actual.sha256 + ) { + fail(`cache content mismatch: ${actual.name}`) + } + } + return manifest +} diff --git a/config/scripts/ci-unit-module-cache-contents.test.mjs b/config/scripts/ci-unit-module-cache-contents.test.mjs new file mode 100644 index 00000000000..195540360df --- /dev/null +++ b/config/scripts/ci-unit-module-cache-contents.test.mjs @@ -0,0 +1,313 @@ +import { + mkdtemp, + mkdir, + readFile, + rename, + rm, + symlink, + truncate, + writeFile +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + inventoryModuleCache, + MODULE_CACHE_LIMITS, + MODULE_CACHE_MANIFEST, + sealModuleCache, + validateModuleCache +} from './ci-unit-module-cache-contents.mjs' + +const roots = [] +const filename = 'a'.repeat(40) +const identity = { + key: 'unit-module-v1-Linux-ARM64-runtime-config-lock', + format: 'vitest-fs-1.0.0-beta.8', + versions: { node: '24.21.0', bun: '1.4.3', vitest: '5.0.3' }, + sourceSha: 'b'.repeat(40), + lockfileHash: '0028fbb2' +} + +function moduleTable() { + return [ + { + file: '1', + id: '1', + url: '2', + imports: '3', + mappings: false, + deps: '4', + dynamicDeps: '5', + staticMocks: '6', + moduleType: '7' + }, + '/workspace/src/main.ts', + '/src/main.ts', + { urls: '8', ids: '9' }, + ['10'], + [], + ['11'], + 'esm', + ['10'], + { '/dependency.ts': '12' }, + '/dependency.ts', + { method: '13', specifier: '10', hasFactory: true, factoryLoadsOriginal: false }, + '/workspace/dependency.ts', + 'mock' + ] +} + +const moduleCode = (table = moduleTable()) => + `export const value = 1\n//# vitestCache=${Buffer.from(JSON.stringify(table)).toString('base64')}` + +async function fixture() { + const path = await mkdtemp(join(tmpdir(), 'orca-unit-module-cache-')) + roots.push(path) + await writeFile(join(path, filename), moduleCode()) + await writeFile( + join(path, '_metadata.json'), + JSON.stringify({ lockfileHash: identity.lockfileHash }) + ) + return { path, ...identity } +} + +async function changeManifest(options, change) { + const path = join(options.path, MODULE_CACHE_MANIFEST) + const manifest = JSON.parse(await readFile(path, 'utf8')) + change(manifest) + await writeFile(path, JSON.stringify(manifest)) +} + +afterEach(async () => { + await Promise.all(roots.splice(0).map((path) => rm(path, { recursive: true, force: true }))) +}) + +describe('unit module cache contents', () => { + it('seals all transformed files and Vitest metadata with exact content hashes', async () => { + const options = await fixture() + const inventory = await inventoryModuleCache(options) + expect(inventory.entries.map((entry) => entry.name)).toEqual(['_metadata.json', filename]) + expect(inventory.entries.every((entry) => /^[a-f0-9]{64}$/.test(entry.sha256))).toBe(true) + expect(inventory.totalBytes).toBe( + inventory.entries.reduce((sum, entry) => sum + entry.bytes, 0) + ) + const manifest = await sealModuleCache(options) + expect(manifest).toMatchObject({ version: 1, ...identity, ...inventory }) + expect(await validateModuleCache(options)).toEqual(manifest) + const restoreOptions = { ...options, sourceSha: undefined } + expect(await validateModuleCache(restoreOptions)).toEqual(manifest) + }) + + it('accepts absent optional module type, dependency lists and mocks', async () => { + const options = await fixture() + await writeFile( + join(options.path, filename), + moduleCode([ + { id: '1', url: '2', file: null, imports: '3', mappings: false }, + '\u0000virtual-module', + '/@id/virtual-module', + { urls: '4', ids: '5' }, + [], + {} + ]) + ) + await sealModuleCache(options) + await expect(validateModuleCache(options)).resolves.toMatchObject({ key: identity.key }) + }) + + it.each(['missing', 'modified', 'extra', 'renamed', 'metadata'])( + 'rejects %s sealed contents', + async (change) => { + const options = await fixture() + await sealModuleCache(options) + const path = join(options.path, filename) + if (change === 'missing') { + await rm(path) + } + if (change === 'modified') { + await writeFile(path, moduleCode().replace('value = 1', 'value = 2')) + } + if (change === 'extra') { + await writeFile(join(options.path, 'c'.repeat(40)), moduleCode()) + } + if (change === 'renamed') { + await rename(path, join(options.path, 'c'.repeat(40))) + } + if (change === 'metadata') { + await writeFile( + join(options.path, '_metadata.json'), + JSON.stringify({ lockfileHash: identity.lockfileHash, changed: true }) + ) + } + await expect(validateModuleCache(options)).rejects.toThrow( + /cache (inventory|content) mismatch/ + ) + } + ) + + it.each([ + ['key', 'different-key'], + ['format', 'different-format'], + ['sourceSha', 'd'.repeat(40)], + ['lockfileHash', '12345678'], + ['version', 2], + ['versions', { ...identity.versions, bun: '1.4.4' }] + ])('rejects mismatched manifest %s', async (field, value) => { + const options = await fixture() + await sealModuleCache(options) + await changeManifest(options, (manifest) => { + manifest[field] = value + }) + await expect(validateModuleCache(options)).rejects.toThrow(/identity mismatch/) + }) + + it.each(['missing', 'malformed', 'mismatched'])( + 'rejects %s Vitest metadata before sealing', + async (kind) => { + const options = await fixture() + const path = join(options.path, '_metadata.json') + if (kind === 'missing') { + await rm(path) + } + if (kind === 'malformed') { + await writeFile(path, '{') + } + if (kind === 'mismatched') { + await writeFile(path, '{"lockfileHash":"12345678"}') + } + await expect(sealModuleCache(options)).rejects.toThrow() + } + ) + + it.each(['.tmp-partial', 'invalid-module.js', 'B'.repeat(40)])( + 'rejects invalid filename %s', + async (name) => { + const options = await fixture() + await writeFile(join(options.path, name), moduleCode()) + await expect(inventoryModuleCache(options)).rejects.toThrow('invalid cache entry name') + } + ) + + it('rejects a directory masquerading as a transformed file', async () => { + const options = await fixture() + await mkdir(join(options.path, 'c'.repeat(40))) + await expect(inventoryModuleCache(options)).rejects.toThrow('not a regular file') + }) + + it.skipIf(process.platform === 'win32')( + 'rejects symlinked modules, manifests and cache roots', + async () => { + const options = await fixture() + const outside = await fixture() + await symlink(join(outside.path, filename), join(options.path, 'c'.repeat(40))) + await expect(inventoryModuleCache(options)).rejects.toThrow('not a regular file') + await rm(join(options.path, 'c'.repeat(40))) + await symlink(join(outside.path, filename), join(options.path, MODULE_CACHE_MANIFEST)) + await expect(sealModuleCache(options)).rejects.toThrow('not a regular file') + await symlink(options.path, join(outside.path, 'linked-root')) + await expect( + inventoryModuleCache({ ...options, path: join(outside.path, 'linked-root') }) + ).rejects.toThrow('cache root') + } + ) + + it('rejects oversized entries before allocating their contents', async () => { + const options = await fixture() + await truncate(join(options.path, filename), MODULE_CACHE_LIMITS.entryBytes + 1) + await expect(inventoryModuleCache(options)).rejects.toThrow('invalid file size') + }) + + it.each([ + ['missing trailer', 'export const value = 1'], + ['invalid base64', 'x\n//# vitestCache=***'], + ['noncanonical base64', 'x\n//# vitestCache=W10'], + ['invalid JSON', 'x\n//# vitestCache=ew=='], + ['plain JSON object', `x\n//# vitestCache=${Buffer.from('{}').toString('base64')}`] + ])('rejects %s', async (_label, code) => { + const options = await fixture() + await writeFile(join(options.path, filename), code) + await expect(sealModuleCache(options)).rejects.toThrow() + }) + + it.each([ + [ + 'out-of-range reference', + (table) => { + table[0].id = '999' + } + ], + [ + 'noncanonical reference', + (table) => { + table[0].id = '01' + } + ], + [ + 'numeric id', + (table) => { + table[0].id = 1 + } + ], + [ + 'cyclic imports', + (table) => { + table[0].imports = '0' + } + ], + [ + 'invalid mappings', + (table) => { + table[0].mappings = null + } + ], + [ + 'invalid module type', + (table) => { + table[7] = 'unknown' + } + ], + [ + 'invalid import URL', + (table) => { + table[8][0] = false + } + ], + [ + 'invalid import ids', + (table) => { + table[9]['/dependency.ts'] = 4 + } + ], + [ + 'invalid dependencies', + (table) => { + table[4][0] = false + } + ], + [ + 'invalid static mocks', + (table) => { + table[11].hasFactory = 1 + } + ], + [ + 'inline object', + (table) => { + table[0].imports = {} + } + ], + [ + 'unsafe property', + (table) => { + table[0] = JSON.parse('{"__proto__":"0"}') + } + ] + ])('rejects %s in module metadata', async (_label, mutate) => { + const options = await fixture() + const table = moduleTable() + mutate(table) + await writeFile(join(options.path, filename), moduleCode(table)) + await expect(sealModuleCache(options)).rejects.toThrow() + }) +}) diff --git a/config/scripts/ci-unit-module-cache-trailer.mjs b/config/scripts/ci-unit-module-cache-trailer.mjs new file mode 100644 index 00000000000..e83212c716d --- /dev/null +++ b/config/scripts/ci-unit-module-cache-trailer.mjs @@ -0,0 +1,118 @@ +const TRAILER = '\n//# vitestCache=' +const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) +const fail = (message) => { + throw new Error(`Unit module cache: ${message}`) +} + +export function checkModuleTrailer(buffer) { + const text = buffer.toString('utf8') + if (!Buffer.from(text).equals(buffer)) { + fail('module is not valid UTF-8') + } + const marker = text.lastIndexOf(TRAILER) + if (marker === -1) { + fail('module trailer is missing') + } + const encoded = text.slice(marker + TRAILER.length) + const decoded = Buffer.from(encoded, 'base64') + if (!encoded || decoded.toString('base64') !== encoded) { + fail('module trailer is not canonical base64') + } + const json = decoded.toString('utf8') + if (!Buffer.from(json).equals(decoded)) { + fail('module metadata is not valid UTF-8') + } + const table = JSON.parse(json) + if (!Array.isArray(table) || !isRecord(table[0])) { + fail('module metadata is not a flatted object table') + } + const resolve = (value) => { + if (typeof value !== 'string') { + return value + } + if ( + !/^(0|[1-9][0-9]*)$/.test(value) || + !Number.isSafeInteger(Number(value)) || + Number(value) >= table.length + ) { + fail('invalid flatted reference') + } + return table[Number(value)] + } + // Check table references directly; the cache only needs the documented metadata fields. + for (const slot of table) { + if (typeof slot === 'string') { + continue + } + if (!slot || typeof slot !== 'object') { + fail('invalid flatted table slot') + } + if (Object.hasOwn(slot, '__proto__')) { + fail('unsafe flatted property') + } + for (const value of Object.values(slot)) { + if (value !== null && typeof value === 'object') { + fail('inline object in flatted table') + } + if (typeof value === 'number' && !Number.isFinite(value)) { + fail('invalid flatted number') + } + resolve(value) + } + } + const fields = Object.fromEntries( + Object.entries(table[0]).map(([name, value]) => [name, resolve(value)]) + ) + const strings = (value) => + Array.isArray(value) && value.every((item) => typeof resolve(item) === 'string') + if (typeof fields.id !== 'string' || typeof fields.url !== 'string') { + fail('invalid module id or url') + } + if (fields.file != null && typeof fields.file !== 'string') { + fail('invalid module file') + } + if (typeof fields.mappings !== 'boolean') { + fail('invalid module mappings') + } + if ( + fields.moduleType !== undefined && + fields.moduleType !== 'cjs' && + fields.moduleType !== 'esm' + ) { + fail('invalid module type') + } + if (!isRecord(fields.imports)) { + fail('invalid module imports') + } + const urls = resolve(fields.imports.urls) + const ids = resolve(fields.imports.ids) + if ( + !strings(urls) || + !isRecord(ids) || + !Object.values(ids).every((id) => typeof resolve(id) === 'string') + ) { + fail('invalid module import references') + } + for (const name of ['deps', 'dynamicDeps']) { + if (fields[name] !== undefined && !strings(fields[name])) { + fail(`invalid module ${name}`) + } + } + if (fields.staticMocks != null) { + if (!Array.isArray(fields.staticMocks)) { + fail('invalid module static mocks') + } + for (const reference of fields.staticMocks) { + const mock = resolve(reference) + if ( + !isRecord(mock) || + typeof resolve(mock.method) !== 'string' || + typeof resolve(mock.specifier) !== 'string' || + typeof mock.hasFactory !== 'boolean' || + typeof mock.factoryLoadsOriginal !== 'boolean' + ) { + fail('invalid module static mock') + } + } + } +} diff --git a/config/scripts/ci-unit-module-cache.mjs b/config/scripts/ci-unit-module-cache.mjs new file mode 100644 index 00000000000..f7e76882dc9 --- /dev/null +++ b/config/scripts/ci-unit-module-cache.mjs @@ -0,0 +1,239 @@ +import { createHash } from 'node:crypto' +import { + appendFileSync, + existsSync, + globSync, + readFileSync, + realpathSync, + rmSync, + statSync +} from 'node:fs' +import { createRequire } from 'node:module' +import { join, resolve } from 'node:path' +import { performance } from 'node:perf_hooks' +import { pathToFileURL } from 'node:url' +import { runProcessSync } from '@orca/process-host' +import { collectNodeServerInputs } from './node-server-change-scope.mjs' +import { workspacePackageManifests } from './workspace-source-exports.mjs' +import { writeAssignment } from './ci-shard-assignment.mjs' +import { sealModuleCache, validateModuleCache } from './ci-unit-module-cache-contents.mjs' + +const ROOT = resolve(import.meta.dirname, '../..') +const IDENTITY_REPORT = 'ci-shards/unit-module-cache-identity.json' +const CACHE_REPORT = 'ci-shards/unit-module-cache.json' +const CACHE_FORMAT = '1.0.0-beta.8' + +function digestInputs(root, files) { + const hash = createHash('sha256') + for (const file of [...new Set(files)].sort()) { + if (!existsSync(join(root, file))) { + hash.update(`${file}\0missing\0`) + continue + } + const bytes = readFileSync(join(root, file)) + hash.update(`${file}\0${bytes.length}\0`).update(bytes) + } + return hash.digest('hex') +} + +export async function moduleCachePolicyHash(root = ROOT) { + const runnerScripts = globSync('config/scripts/vitest*.{mjs,ts}', { cwd: root }).filter( + (file) => !file.includes('.test.') + ) + const configInputs = await collectNodeServerInputs({ + root, + entryPoints: [ + 'config/vitest.config.ts', + 'config/scripts/run-vitest.mjs', + 'config/scripts/ci-unit-timing-reporter.mjs', + ...runnerScripts + ] + }) + return digestInputs(root, [ + ...configInputs, + ...workspacePackageManifests(root), + ...globSync(['config/patches/**', '**/tsconfig*.json'], { + cwd: root, + exclude: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/out/**', 'mobile/vendor/**'] + }).filter((file) => statSync(join(root, file)).isFile()), + 'package.json', + 'pnpm-lock.yaml', + 'pnpm-workspace.yaml', + '.npmrc', + '.pnpmfile.cjs', + 'config/.bun-version', + 'config/scripts/ci-unit-module-cache.mjs', + 'config/scripts/ci-unit-module-cache-contents.mjs', + 'config/scripts/ci-unit-module-cache-trailer.mjs' + ]) +} + +export function moduleCacheIdentity({ + root = ROOT, + cacheRoot = process.env.RUNNER_TEMP, + policyHash, + installedLockHash, + versions, + shard, + platform = process.platform, + arch = process.arch, + nodeEnv = process.env.NODE_ENV ?? 'test', + namespace = '' +}) { + if ( + !cacheRoot || + !/^[a-f0-9]{64}$/.test(policyHash ?? '') || + !/^[a-f0-9]{64}$/.test(installedLockHash ?? '') || + ![versions?.node, versions?.bun, versions?.vitest].every((version) => + /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(version ?? '') + ) || + !Number.isInteger(shard?.index) || + !Number.isInteger(shard?.count) || + shard.index < 1 || + shard.index > shard.count + ) { + throw new Error('Unit module cache identity is incomplete') + } + const identity = { + format: CACHE_FORMAT, + root: realpathSync(root), + platform, + arch, + versions, + policyHash, + installedLockHash, + nodeEnv, + shard, + namespace + } + const digest = createHash('sha256').update(JSON.stringify(identity)).digest('hex') + return { + ...identity, + key: `unit-module-v1-${platform}-${arch}-n${versions.node}-b${versions.bun}-v${versions.vitest}-s${shard.index}of${shard.count}-${digest}`, + path: join(resolve(cacheRoot), 'orca-vitest-module-cache') + } +} + +export async function resolveModuleCacheIdentity({ + root = ROOT, + cacheRoot = process.env.RUNNER_TEMP, + shard, + namespace = '' +} = {}) { + const require = createRequire(join(root, 'package.json')) + const bun = runProcessSync({ program: 'bun', args: ['--version'], cwd: root, timeoutMs: 10_000 }) + if (bun.code !== 0) { + throw new Error('Bun version is unavailable') + } + return moduleCacheIdentity({ + root, + cacheRoot, + shard, + namespace, + policyHash: await moduleCachePolicyHash(root), + installedLockHash: createHash('sha256') + .update(readFileSync(join(root, 'node_modules', '.pnpm', 'lock.yaml'))) + .digest('hex'), + versions: { + node: process.versions.node, + bun: bun.stdout.trim(), + vitest: require('vitest/package.json').version + } + }) +} + +export async function prepareModuleCache(identity, { restoreHit = false } = {}) { + const started = performance.now() + try { + if (!restoreHit) { + throw new Error('No exact cache was restored') + } + const manifest = await validateModuleCache({ + path: identity.path, + key: identity.key, + format: identity.format, + versions: identity.versions, + lockfileHash: identity.installedLockHash.slice(0, 8) + }) + return { + available: true, + entries: manifest.entries.length, + bytes: manifest.totalBytes, + producerSourceSha: manifest.sourceSha, + validationMs: performance.now() - started + } + } catch (error) { + rmSync(identity.path, { recursive: true, force: true }) + return { + available: false, + reason: String(error), + validationMs: performance.now() - started + } + } +} + +export async function finishModuleCache(identity, { sourceSha } = {}) { + const started = performance.now() + const manifest = await sealModuleCache({ + ...identity, + sourceSha, + lockfileHash: identity.installedLockHash.slice(0, 8) + }) + return { + entries: manifest.entries.length, + bytes: manifest.totalBytes, + sealMs: performance.now() - started + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + const phase = process.argv[2] + const output = (values) => { + if (process.env.GITHUB_OUTPUT) { + for (const [key, value] of Object.entries(values)) { + appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`) + } + } + } + if (phase === 'identity') { + try { + const identity = await resolveModuleCacheIdentity({ + shard: { + index: Number(process.env.ORCA_UNIT_SHARD_INDEX), + count: Number(process.env.ORCA_UNIT_SHARD_COUNT) + }, + namespace: process.env.ORCA_UNIT_CACHE_NAMESPACE ?? '' + }) + writeAssignment(IDENTITY_REPORT, identity) + output({ key: identity.key, path: identity.path }) + if (process.env.GITHUB_ENV) { + appendFileSync(process.env.GITHUB_ENV, `ORCA_UNIT_MODULE_CACHE_PATH=${identity.path}\n`) + } + } catch (error) { + console.warn(`Unit module cache unavailable: ${String(error)}`) + } + } else { + const identity = JSON.parse(readFileSync(IDENTITY_REPORT, 'utf8')) + if (phase === 'prepare') { + const result = await prepareModuleCache(identity, { + restoreHit: process.env.ORCA_UNIT_CACHE_RESTORE_HIT === 'true' + }) + writeAssignment(CACHE_REPORT, { key: identity.key, mode: 'restore', ...result }) + console.log(JSON.stringify(result)) + } else if (phase === 'seal') { + try { + const result = await finishModuleCache(identity, { + sourceSha: process.env.ORCA_SHARD_SOURCE_SHA ?? process.env.GITHUB_SHA + }) + const previous = JSON.parse(readFileSync(CACHE_REPORT, 'utf8')) + writeAssignment(CACHE_REPORT, { ...previous, produced: result }) + output({ ready: true }) + console.log(JSON.stringify(result)) + } catch (error) { + console.warn(`Unit module cache was not saved: ${String(error)}`) + } + } else { + throw new Error(`Unknown unit module cache phase: ${phase}`) + } + } +} diff --git a/config/scripts/ci-unit-module-cache.test.mjs b/config/scripts/ci-unit-module-cache.test.mjs new file mode 100644 index 00000000000..59270e0b247 --- /dev/null +++ b/config/scripts/ci-unit-module-cache.test.mjs @@ -0,0 +1,167 @@ +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { parse } from 'yaml' +import { + finishModuleCache, + moduleCacheIdentity, + prepareModuleCache +} from './ci-unit-module-cache.mjs' + +const temporary = [] +afterEach(() => { + for (const directory of temporary.splice(0)) { + rmSync(directory, { recursive: true, force: true }) + } +}) + +function options() { + const directory = mkdtempSync(join(tmpdir(), 'orca-unit-cache-identity-')) + temporary.push(directory) + return { + root: directory, + cacheRoot: directory, + policyHash: 'a'.repeat(64), + installedLockHash: 'b'.repeat(64), + versions: { node: '24.21.0', bun: '1.4.3', vitest: '5.0.3' }, + shard: { index: 1, count: 5 }, + platform: 'linux', + arch: 'arm64', + nodeEnv: 'test', + namespace: '' + } +} + +describe('persistent unit module cache boundary', () => { + it('separates hosts, runtimes, config, lockfiles, roots, shards and experiments', () => { + const original = options() + const identity = moduleCacheIdentity(original) + const anotherRoot = options().root + for (const update of [ + { root: anotherRoot }, + { policyHash: 'c'.repeat(64) }, + { installedLockHash: 'd'.repeat(64) }, + { platform: 'win32' }, + { arch: 'x64' }, + { nodeEnv: 'production' }, + { namespace: 'comparison' }, + { shard: { index: 2, count: 5 } }, + { shard: { index: 1, count: 10 } }, + { versions: { ...original.versions, node: '26.6.0' } }, + { versions: { ...original.versions, bun: '1.4.4' } }, + { versions: { ...original.versions, vitest: '5.0.4' } } + ]) { + const next = moduleCacheIdentity({ ...original, ...update }) + expect(next.key).not.toBe(identity.key) + expect(next.path).toBe(identity.path) + } + expect(moduleCacheIdentity(original)).toEqual(identity) + }) + + it.each([ + { cacheRoot: '' }, + { policyHash: '' }, + { installedLockHash: '' }, + { versions: { node: '24', bun: '1.4.3', vitest: '5.0.3' } }, + { shard: { index: 0, count: 5 } }, + { shard: { index: 6, count: 5 } } + ])('refuses an incomplete identity %j', (update) => { + expect(() => moduleCacheIdentity({ ...options(), ...update })).toThrow('incomplete') + }) + + it.each([false, true])( + 'starts cold and discards unusable restored bytes (hit %s)', + async (hit) => { + const identity = moduleCacheIdentity(options()) + mkdirSync(identity.path) + writeFileSync(join(identity.path, 'untrusted'), 'throw Error("must not execute")') + const prepared = await prepareModuleCache(identity, { restoreHit: hit }) + expect(prepared.available).toBe(false) + expect(prepared.validationMs).toBeGreaterThanOrEqual(0) + expect(() => readFileSync(join(identity.path, 'untrusted'))).toThrow() + } + ) + it('restores a matching cache from an earlier source without copying evaluated state', async () => { + const identity = moduleCacheIdentity(options()) + mkdirSync(identity.path) + writeFileSync( + join(identity.path, '_metadata.json'), + JSON.stringify({ lockfileHash: identity.installedLockHash.slice(0, 8) }) + ) + const metadata = [ + { id: '1', url: '2', file: '1', imports: '3', mappings: false }, + join(identity.root, 'source.ts'), + '/source.ts', + { urls: '4', ids: '5' }, + [], + {} + ] + writeFileSync( + join(identity.path, 'a'.repeat(40)), + `export const value = 1\n//# vitestCache=${Buffer.from(JSON.stringify(metadata)).toString('base64')}` + ) + const produced = await finishModuleCache(identity, { sourceSha: 'a'.repeat(40) }) + expect(produced.entries).toBe(2) + const prepared = await prepareModuleCache( + { ...identity, sourceSha: 'b'.repeat(40) }, + { restoreHit: true } + ) + expect(prepared).toMatchObject({ + available: true, + entries: produced.entries, + bytes: produced.bytes, + producerSourceSha: 'a'.repeat(40) + }) + }) +}) + +it('uses an optional exact restore after install and saves successful scoped producers', () => { + const workflow = parse(readFileSync('.github/workflows/unit-tests.yml', 'utf8')) + const steps = workflow.jobs.test.steps + const dependencies = steps.findIndex( + (step) => step.uses === './.github/actions/install-node-dependencies' + ) + const identity = steps.findIndex((step) => step.id === 'module-cache') + const restore = steps.findIndex((step) => step.id === 'module-cache-restore') + const prepare = steps.findIndex((step) => step.run?.endsWith('ci-unit-module-cache.mjs prepare')) + const tests = steps.findIndex((step) => step.name === 'Test shard') + const seal = steps.find((step) => step.id === 'module-cache-seal') + const save = steps.find((step) => step.uses === 'actions/cache/save@v5') + expect(identity).toBeGreaterThan(dependencies) + expect(restore).toBeGreaterThan(identity) + expect(prepare).toBeGreaterThan(restore) + expect(tests).toBeGreaterThan(prepare) + expect(steps[restore].uses).toBe('actions/cache/restore@v5') + expect(steps[restore].with['restore-keys']).toBeUndefined() + expect(steps[restore]['continue-on-error']).toBe(true) + expect(seal.if).toContain('success()') + expect(seal.if).toContain("github.event_name == 'pull_request'") + expect(seal.if).toContain("github.ref == 'refs/heads/main'") + expect(seal.if).toContain("github.event_name != 'pull_request'") + expect(seal.if).toContain("inputs.module_cache_seed && github.event_name == 'workflow_dispatch'") + expect(save.if).toBe("steps.module-cache-seal.outputs.ready == 'true'") + expect(save.with.key).toBe(steps[restore].with.key) + expect(workflow.on.workflow_call.inputs.module_cache.default).toBe('off') + expect(workflow.on.workflow_call.inputs.run_relay_integration.default).toBe(true) +}) + +it('runs full same-source controls serially with five four-worker shards and included producer costs', () => { + const workflow = parse(readFileSync('.github/workflows/unit-module-cache-benchmark.yml', 'utf8')) + const { baseline, producer, warm } = workflow.jobs + expect(producer.needs).toBe('baseline') + expect(warm.needs).toBe('producer') + for (const job of [baseline, producer, warm]) { + expect(job.uses).toBe('./.github/workflows/unit-tests.yml') + expect(job.with.node_versions).toBe('["24.21.0"]') + expect(job.with.runner).toBe('ubuntu-24.04-arm') + expect(JSON.parse(job.with.shards)).toEqual( + Array.from({ length: 5 }, (_, index) => ({ index: index + 1, count: 5 })) + ) + expect(job.with.phase_diagnostics).toBe(true) + } + expect(producer.with.module_cache).toBe('cold') + expect(producer.with.module_cache_seed).toBe(true) + expect(warm.with.module_cache).toBe('warm') + expect(warm.with.module_cache_namespace).toBe(producer.with.module_cache_namespace) +}) diff --git a/config/scripts/ci-unit-timing-reporter.mjs b/config/scripts/ci-unit-timing-reporter.mjs index 820a101d830..e3ac8fcede4 100644 --- a/config/scripts/ci-unit-timing-reporter.mjs +++ b/config/scripts/ci-unit-timing-reporter.mjs @@ -1,8 +1,114 @@ -import { relative } from 'node:path' +import { relative, resolve } from 'node:path' import { appendFileSync, readFileSync } from 'node:fs' import { writeAssignment } from './ci-shard-assignment.mjs' import { auditUnitSelection } from './ci-unit-selection.mjs' +export const UNIT_PHASE_FILE_LIMIT = 50_000 +const phaseNames = [ + 'environmentSetupDuration', + 'prepareDuration', + 'collectDuration', + 'setupDuration', + 'duration' +] + +const nonnegativeNumber = (value) => (Number.isFinite(value) && value >= 0 ? value : null) +const emptyPhases = () => Object.fromEntries(phaseNames.map((name) => [name, 0])) +const emptyImportSample = () => ({ entries: 0, selfTimeMs: 0, totalTimeMs: 0 }) +const workerCounters = (state) => ({ + workersSpawned: Number.isInteger(state?.workersSpawned) + ? nonnegativeNumber(state.workersSpawned) + : null, + startupTimeMs: nonnegativeNumber(state?.startupTime) +}) + +export function buildUnitPhaseReport(ctx, modules, errors, reason, baseline) { + const files = [] + const projects = new Map() + const totals = emptyPhases() + const phaseSamples = emptyPhases() + for (const module of modules) { + const diagnostic = module.diagnostic() + const project = module.project?.name ?? 'unknown' + if (!projects.has(project)) { + projects.set(project, { + project, + files: 0, + phases: emptyPhases(), + phaseSamples: emptyPhases(), + sampledImports: emptyImportSample() + }) + } + const aggregate = projects.get(project) + aggregate.files += 1 + const phases = {} + for (const name of phaseNames) { + const value = nonnegativeNumber(diagnostic?.[name]) + phases[name] = value + if (value !== null) { + aggregate.phases[name] += value + aggregate.phaseSamples[name] += 1 + totals[name] += value + phaseSamples[name] += 1 + } + } + const imports = diagnostic?.importDurations + if (imports && typeof imports === 'object') { + for (const id in imports) { + if (!Object.hasOwn(imports, id)) { + continue + } + const selfTime = nonnegativeNumber(imports[id]?.selfTime) + const totalTime = nonnegativeNumber(imports[id]?.totalTime) + if (selfTime !== null && totalTime !== null) { + aggregate.sampledImports.entries += 1 + aggregate.sampledImports.selfTimeMs += selfTime + aggregate.sampledImports.totalTimeMs += totalTime + } + } + } + if (files.length < UNIT_PHASE_FILE_LIMIT) { + files.push({ + file: relative(ctx.config.root, module.moduleId).replaceAll('\\', '/'), + project, + workerId: nonnegativeNumber(diagnostic?.workerId), + concurrencyId: nonnegativeNumber(diagnostic?.concurrencyId), + phases + }) + } + } + const counters = workerCounters(ctx.state) + return { + metric: 'module-phase-diagnostics-v1', + nodeVersion: process.env.ORCA_TEST_NODE_VERSION ?? process.versions.node, + ...(process.versions.bun ? { bunVersion: process.versions.bun } : {}), + vitestVersion: ctx.version ?? null, + shard: ctx.config.shard ?? { index: 1, count: 1 }, + status: reason, + unhandledErrors: errors.length, + units: 'milliseconds', + phaseTime: 'summed-worker-time', + fileLimit: UNIT_PHASE_FILE_LIMIT, + filesSeen: modules.length, + filesOmitted: modules.length - files.length, + workerTotals: { + scope: baseline ? 'test-run' : 'vitest-session', + ...Object.fromEntries( + Object.entries(counters).map(([name, value]) => [ + name, + value === null || (baseline && baseline[name] === null) + ? null + : nonnegativeNumber(value - (baseline?.[name] ?? 0)) + ]) + ) + }, + phases: totals, + phaseSamples, + projects: [...projects.values()], + files + } +} + export function moduleDuration(diagnostic) { return Math.max( 1, @@ -21,6 +127,12 @@ export default class UnitTimingReporter { this.ctx = ctx } + onTestRunStart() { + if (process.env.ORCA_UNIT_PHASE_REPORT) { + this.workerBaseline = workerCounters(this.ctx.state) + } + } + onTestRunEnd(modules, errors, reason) { const results = Object.fromEntries( modules.map((module) => [ @@ -61,5 +173,17 @@ export default class UnitTimingReporter { } catch { // Timing evidence remains useful when selection planning was unavailable. } + const phaseReport = process.env.ORCA_UNIT_PHASE_REPORT + const timingReport = process.env.ORCA_UNIT_TIMING_REPORT ?? 'ci-shards/unit-timings.json' + if (phaseReport && resolve(phaseReport) !== resolve(timingReport)) { + try { + writeAssignment( + phaseReport, + buildUnitPhaseReport(this.ctx, modules, errors, reason, this.workerBaseline) + ) + } catch { + // Optional diagnostics must not change the test verdict. + } + } } } diff --git a/config/scripts/ci-unit-timing-reporter.test.mjs b/config/scripts/ci-unit-timing-reporter.test.mjs index b4964449a9c..4e5e5cc59ce 100644 --- a/config/scripts/ci-unit-timing-reporter.test.mjs +++ b/config/scripts/ci-unit-timing-reporter.test.mjs @@ -1,9 +1,13 @@ -import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { importUnitTimingReports } from './ci-unit-timing-import.mjs' -import UnitTimingReporter, { moduleDuration } from './ci-unit-timing-reporter.mjs' +import UnitTimingReporter, { + buildUnitPhaseReport, + moduleDuration, + UNIT_PHASE_FILE_LIMIT +} from './ci-unit-timing-reporter.mjs' const report = (index, timings) => ({ metric: 'module-duration-v1', @@ -38,6 +42,7 @@ describe('unit worker timing evidence', () => { const previous = process.env.ORCA_UNIT_TIMING_REPORT process.env.ORCA_UNIT_TIMING_REPORT = join(directory, 'unit-timings.json') vi.stubEnv('ORCA_TEST_NODE_VERSION', nodeVersion) + vi.stubEnv('ORCA_UNIT_PHASE_REPORT', undefined) try { const reporter = new UnitTimingReporter() reporter.onInit({ config: { root: process.cwd(), shard: { index: 1, count: 8 } } }) @@ -121,3 +126,217 @@ describe('unit worker timing evidence', () => { expect(() => importUnitTimingReports([first, report(2, { b: -1 })])).toThrow('timing') }) }) + +const phaseDiagnostic = (overrides = {}) => ({ + environmentSetupDuration: 1.5, + prepareDuration: 2.5, + collectDuration: 30.25, + setupDuration: 4.5, + duration: 5.25, + workerId: 7, + concurrencyId: 2, + importDurations: {}, + ...overrides +}) +const phaseModule = (file, diagnostic = phaseDiagnostic(), project = 'bun') => ({ + moduleId: resolve(file), + project: { name: project }, + state: () => 'passed', + diagnostic: () => diagnostic +}) +const phaseContext = (state) => ({ + config: { root: process.cwd(), shard: { index: 1, count: 5 } }, + version: '5.0.3', + state +}) + +function withReporterOutputs(run) { + const directory = mkdtempSync(join(tmpdir(), 'orca-unit-phases-')) + const timingPath = join(directory, 'unit-timings.json') + const phasePath = join(directory, 'unit-phase-diagnostics.json') + vi.stubEnv('ORCA_UNIT_TIMING_REPORT', timingPath) + vi.stubEnv('ORCA_UNIT_PHASE_REPORT', undefined) + vi.stubEnv('ORCA_UNIT_SELECTION_PLAN', join(directory, 'missing-selection.json')) + vi.stubEnv('ORCA_SHARD_SOURCE_SHA', 'phase-source') + vi.stubEnv('GITHUB_RUN_ID', 'phase-run') + vi.stubEnv('GITHUB_RUN_ATTEMPT', '3') + try { + run({ timingPath, phasePath }) + } finally { + vi.unstubAllEnvs() + rmSync(directory, { recursive: true, force: true }) + } +} + +describe('optional unit phase diagnostics', () => { + it('leaves the timing report identical and writes nothing extra when disabled', () => { + withReporterOutputs(({ timingPath, phasePath }) => { + const modules = [ + phaseModule( + 'src/example.test.ts', + phaseDiagnostic({ importDurations: { dependency: { selfTime: 100, totalTime: 200 } } }) + ) + ] + const reporter = new UnitTimingReporter() + reporter.onInit(phaseContext({ workersSpawned: 3, startupTime: 500 })) + reporter.onTestRunEnd(modules, [], 'passed') + const timing = readFileSync(timingPath, 'utf8') + expect(existsSync(phasePath)).toBe(false) + + vi.stubEnv('ORCA_UNIT_PHASE_REPORT', phasePath) + reporter.onTestRunEnd(modules, [], 'passed') + expect(readFileSync(timingPath, 'utf8')).toBe(timing) + expect(JSON.parse(timing).timings).toEqual({ 'src/example.test.ts': 44 }) + expect(JSON.parse(readFileSync(phasePath, 'utf8'))).toMatchObject({ + sourceSha: 'phase-source', + runId: 'phase-run', + runAttempt: '3', + metric: 'module-phase-diagnostics-v1', + vitestVersion: '5.0.3', + shard: { index: 1, count: 5 }, + status: 'passed', + unhandledErrors: 0, + workerTotals: { scope: 'vitest-session', workersSpawned: 3, startupTimeMs: 500 } + }) + }) + }) + + it('records fractions and project totals without retaining sampled import records', () => { + const report = buildUnitPhaseReport( + phaseContext(), + [ + phaseModule( + 'src/first.test.ts', + phaseDiagnostic({ + importDurations: { + first: { selfTime: 2, totalTime: 9, importer: 'src/private-account.ts' }, + second: { selfTime: 3.5, totalTime: 5.5 }, + invalid: { selfTime: Number.NaN, totalTime: 4 } + } + }) + ), + phaseModule('src/second.test.ts'), + phaseModule('src/node.test.ts', phaseDiagnostic({ workerId: 9 }), 'node-runtime') + ], + [{ message: 'test error' }], + 'failed' + ) + expect(report.files[0]).toEqual({ + file: 'src/first.test.ts', + project: 'bun', + workerId: 7, + concurrencyId: 2, + phases: { + environmentSetupDuration: 1.5, + prepareDuration: 2.5, + collectDuration: 30.25, + setupDuration: 4.5, + duration: 5.25 + } + }) + expect(report.projects).toMatchObject([ + { + project: 'bun', + files: 2, + phases: { collectDuration: 60.5 }, + phaseSamples: { collectDuration: 2 }, + sampledImports: { entries: 2, selfTimeMs: 5.5, totalTimeMs: 14.5 } + }, + { project: 'node-runtime', files: 1, sampledImports: { entries: 0 } } + ]) + expect(report.phases.collectDuration).toBe(90.75) + expect(report).toMatchObject({ + phaseTime: 'summed-worker-time', + status: 'failed', + unhandledErrors: 1 + }) + expect(JSON.stringify(report)).not.toContain('private-account') + expect(JSON.stringify(report)).not.toContain('importDurations') + }) + + it('reports successful worker startup counters for the current run separately', () => { + withReporterOutputs(({ phasePath }) => { + vi.stubEnv('ORCA_UNIT_PHASE_REPORT', phasePath) + const state = { workersSpawned: 2, startupTime: 10 } + const reporter = new UnitTimingReporter() + reporter.onInit(phaseContext(state)) + reporter.onTestRunStart() + state.workersSpawned = 5 + state.startupTime = 60.25 + reporter.onTestRunEnd([phaseModule('src/example.test.ts')], [], 'passed') + expect(JSON.parse(readFileSync(phasePath, 'utf8')).workerTotals).toEqual({ + scope: 'test-run', + workersSpawned: 3, + startupTimeMs: 50.25 + }) + }) + }) + + it('caps file detail while keeping aggregates from omitted files', () => { + const module = phaseModule('src/repeated.test.ts') + const modules = Array.from({ length: UNIT_PHASE_FILE_LIMIT }, () => module) + modules.push( + phaseModule( + 'src/omitted.test.ts', + phaseDiagnostic({ + collectDuration: 11, + importDurations: { dependency: { selfTime: 2, totalTime: 3 } } + }) + ) + ) + const report = buildUnitPhaseReport(phaseContext(), modules, [], 'passed') + expect(report.files).toHaveLength(UNIT_PHASE_FILE_LIMIT) + expect(report).toMatchObject({ + fileLimit: UNIT_PHASE_FILE_LIMIT, + filesSeen: UNIT_PHASE_FILE_LIMIT + 1, + filesOmitted: 1, + phases: { collectDuration: UNIT_PHASE_FILE_LIMIT * 30.25 + 11 }, + projects: [ + { + files: UNIT_PHASE_FILE_LIMIT + 1, + sampledImports: { entries: 1, selfTimeMs: 2, totalTimeMs: 3 } + } + ] + }) + }) + + it('marks unavailable counters, phases and worker IDs without inventing measurements', () => { + const report = buildUnitPhaseReport( + phaseContext({ workersSpawned: -1, startupTime: Number.NaN }), + [phaseModule('src/unavailable.test.ts', {})], + [], + 'interrupted' + ) + expect(report.workerTotals).toEqual({ + scope: 'vitest-session', + workersSpawned: null, + startupTimeMs: null + }) + expect(report.files[0]).toMatchObject({ + workerId: null, + concurrencyId: null, + phases: { collectDuration: null } + }) + expect(report.phaseSamples.collectDuration).toBe(0) + expect(report.status).toBe('interrupted') + }) + + it.each(['unwritable', 'same as timing'])( + 'preserves timing evidence when the optional output is %s', + (target) => { + withReporterOutputs(({ timingPath }) => { + const reporter = new UnitTimingReporter() + reporter.onInit(phaseContext()) + const modules = [phaseModule('src/example.test.ts')] + reporter.onTestRunEnd(modules, [], 'passed') + const timing = readFileSync(timingPath, 'utf8') + vi.stubEnv( + 'ORCA_UNIT_PHASE_REPORT', + target === 'unwritable' ? `${timingPath}/unwritable.json` : timingPath + ) + expect(() => reporter.onTestRunEnd(modules, [], 'passed')).not.toThrow() + expect(readFileSync(timingPath, 'utf8')).toBe(timing) + }) + } + ) +}) diff --git a/config/scripts/dev-storage-reclaim.mjs b/config/scripts/dev-storage-reclaim.mjs new file mode 100644 index 00000000000..5b2123678af --- /dev/null +++ b/config/scripts/dev-storage-reclaim.mjs @@ -0,0 +1,41 @@ +import { resolve } from 'node:path' +import { spawnProcess } from '@orca/process-host' + +export function startDevStorageReclaim({ + repoRoot, + env = process.env, + spawn = spawnProcess, + report = console.error +}) { + if (env.ORCA_SKIP_DEV_STORAGE_RECLAIM === '1') { + return + } + try { + // The worker owns its lock after dev exits and must not keep captured dev output open. + const child = spawn({ + program: process.execPath, + args: [ + resolve(import.meta.dirname, 'reclaim-worktree-space.mjs'), + '--repo', + repoRoot, + '--exclude-worktree', + repoRoot, + '--automatic', + '--apply' + ], + cwd: repoRoot, + env, + detached: true, + stdio: 'ignore' + }) + child.on('error', (error) => report(`[orca-dev] Storage cleanup skipped: ${error.message}`)) + child.on('exit', (code) => { + if (code !== 0) { + report('[orca-dev] Storage cleanup did not finish; retry with pnpm reclaim-space.') + } + }) + child.unref() + } catch (error) { + report(`[orca-dev] Storage cleanup skipped: ${error.message}`) + } +} diff --git a/config/scripts/dev-storage-reclaim.test.mjs b/config/scripts/dev-storage-reclaim.test.mjs new file mode 100644 index 00000000000..390b686d7cf --- /dev/null +++ b/config/scripts/dev-storage-reclaim.test.mjs @@ -0,0 +1,84 @@ +import { EventEmitter } from 'node:events' +import { tmpdir } from 'node:os' +import { resolve } from 'node:path' +import { pathToFileURL } from 'node:url' +import { runProcess } from '@orca/process-host' +import { expect, it, vi } from 'vitest' +import { startDevStorageReclaim } from './dev-storage-reclaim.mjs' + +function fixture() { + const child = new EventEmitter() + child.unref = vi.fn() + const spawn = vi.fn(() => child) + return { child, spawn, report: vi.fn(), repoRoot: '/dev-worktree', env: {} } +} + +it('starts one non-interactive worker without waiting for its exit', () => { + const options = fixture() + startDevStorageReclaim(options) + expect(options.spawn).toHaveBeenCalledTimes(1) + expect(options.spawn.mock.calls[0][0]).toMatchObject({ + args: expect.arrayContaining([ + '--automatic', + '--apply', + '--exclude-worktree', + options.repoRoot + ]), + detached: true, + stdio: 'ignore' + }) + expect(options.child.unref).toHaveBeenCalledTimes(1) + expect(options.report).not.toHaveBeenCalled() +}) + +it('allows tests and developers to skip the repository-wide sweep', () => { + const options = fixture() + startDevStorageReclaim({ ...options, env: { ORCA_SKIP_DEV_STORAGE_RECLAIM: '1' } }) + expect(options.spawn).not.toHaveBeenCalled() +}) + +it('reports cleanup failure without throwing into dev startup', () => { + const options = fixture() + startDevStorageReclaim(options) + options.child.emit('exit', 1) + expect(options.report).toHaveBeenCalledWith(expect.stringContaining('pnpm reclaim-space')) +}) + +it.each([true, false])('handles both sync and async spawn errors (sync: %j)', (synchronous) => { + const options = fixture() + if (synchronous) { + options.spawn.mockImplementation(() => { + throw new Error('spawn failed') + }) + } + expect(() => startDevStorageReclaim(options)).not.toThrow() + if (!synchronous) { + options.child.emit('error', new Error('spawn failed')) + } + expect(options.report).toHaveBeenCalledWith(expect.stringContaining('spawn failed')) +}) + +it('closes captured dev output before the detached worker exits', async () => { + const entry = pathToFileURL(resolve(import.meta.dirname, 'dev-storage-reclaim.mjs')).href + const result = await runProcess({ + program: process.env.ORCA_TEST_NODE_EXECUTABLE ?? process.execPath, + args: [ + '--input-type=module', + '-e', + `import { startDevStorageReclaim } from ${JSON.stringify(entry)}; + import { spawnProcess } from '@orca/process-host'; + startDevStorageReclaim({ + repoRoot: ${JSON.stringify(tmpdir())}, + env: { ...process.env, ORCA_SKIP_DEV_STORAGE_RECLAIM: '0' }, + spawn: (spec) => spawnProcess({ ...spec, args: ['-e', 'setTimeout(() => {}, 5000)'] }) + }); + process.exit(0);` + ], + cwd: resolve(import.meta.dirname, '../..'), + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, + timeoutMs: 2_000, + terminationBarrier: true + }) + expect(result.timedOut).toBe(false) + expect(result.code, result.stderr).toBe(0) +}) diff --git a/config/scripts/electron-builder-mac-channel-config.test.mjs b/config/scripts/electron-builder-mac-channel-config.test.mjs index 05572f3497a..693ac753978 100644 --- a/config/scripts/electron-builder-mac-channel-config.test.mjs +++ b/config/scripts/electron-builder-mac-channel-config.test.mjs @@ -133,20 +133,4 @@ describe('electron-builder mac channel config', () => { } ) }) - - // Why: the dev channels share every packaging decision except where they - // publish, so a future edit that collapses them must not also collapse the - // repos — a branch or daily build landing in orca-hourly would be offered to - // everyone riding main's hourlies. - it('keeps the dev channels on separate repos', () => { - withHourlyEnv((hourly) => { - withDailyEnv((daily) => { - withAdhocEnv((adhoc) => { - expect(new Set([hourly.publish.repo, daily.publish.repo, adhoc.publish.repo]).size).toBe( - 3 - ) - }) - }) - }) - }) }) diff --git a/config/scripts/generated-artifact-line-ending-pin.test.mjs b/config/scripts/generated-artifact-line-ending-pin.test.mjs index 7720cda1938..70e2ab41075 100644 --- a/config/scripts/generated-artifact-line-ending-pin.test.mjs +++ b/config/scripts/generated-artifact-line-ending-pin.test.mjs @@ -16,10 +16,14 @@ it('keeps byte-compared artifacts and source LF in an autocrlf checkout', () => ['src/renderer/src/example.tsx', lf], ['src/main/example.ts', lf], ['config/scripts/example.mjs', lf], + ['config/scripts/nested/deeper/example.mjs', lf], ...['cmd', 'bat', 'ps1', 'nsh'].map((extension) => [`config/example.${extension}`, crlf]), ['resources/skills-extra/example.json', crlf], ['vendor/resources/skills/example.json', crlf], ['scripts/example.mjs', crlf], + ['config/scripts-extra/example.mjs', crlf], + ['vendor/config/scripts/example.mjs', crlf], + ['config/scripts/example.mjsx', crlf], ['src/main/runtime/__fixtures__/example.txt', crlf], ['src/main/daemon/__fixtures__/pty-transcripts/example.txt', crlf] ]) diff --git a/config/scripts/happy-dom-offscreen-canvas.ts b/config/scripts/happy-dom-offscreen-canvas.ts index 1b06e472025..04e116a4192 100644 --- a/config/scripts/happy-dom-offscreen-canvas.ts +++ b/config/scripts/happy-dom-offscreen-canvas.ts @@ -49,7 +49,7 @@ export function installHappyDomOffscreenCanvasCompatibility(): boolean { return context } - const canvas = document.createElement('canvas') as PatchableCanvas + const canvas = document.createElement('canvas') return htmlCanvas.prototype.getContext!.call(canvas, contextType, contextAttributes) } prototype[OFFSCREEN_CANVAS_COMPATIBILITY_INSTALLED] = true diff --git a/config/scripts/locale-ja-value-overrides.mjs b/config/scripts/locale-ja-value-overrides.mjs index 1d220ba7c0f..57612fb249b 100644 --- a/config/scripts/locale-ja-value-overrides.mjs +++ b/config/scripts/locale-ja-value-overrides.mjs @@ -433,8 +433,6 @@ const JA_AUDIT_OVERRIDES = { 'ステージ済みの変更からコミットメッセージを生成します。', lan: 'LAN', deploy: 'deploy', - 'Connect this source to check for Hermes automations in the remote profile.': - 'このソースに接続すると、リモートプロファイル内の Hermes オートメーションを確認できます。', 'Notes sent to active agent.': 'メモをアクティブな Agent に送信しました。', 'Review before attaching. Captured page context may include visible site content.': '添付する前に確認してください。取得したページのコンテキストには、表示中のサイトの内容が含まれる場合があります。', diff --git a/config/scripts/locale-ko-key-overrides.test.mjs b/config/scripts/locale-ko-key-overrides.test.mjs deleted file mode 100644 index da98d401e54..00000000000 --- a/config/scripts/locale-ko-key-overrides.test.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import fs from 'node:fs' - -import { describe, expect, it } from 'vitest' - -describe('locale-ko-key-overrides', () => { - it('keeps Korean key override data scoped to Korean values', () => { - const overrides = JSON.parse( - fs.readFileSync(new URL('./locale-ko-key-overrides.json', import.meta.url), 'utf8') - ) - for (const value of Object.values(overrides)) { - expect(Object.keys(value)).toEqual(['ko']) - } - }) -}) diff --git a/config/scripts/locale-ko-value-overrides.mjs b/config/scripts/locale-ko-value-overrides.mjs index bcc5b6e9e8e..5a819b90e23 100644 --- a/config/scripts/locale-ko-value-overrides.mjs +++ b/config/scripts/locale-ko-value-overrides.mjs @@ -293,6 +293,5 @@ export const KO_VALUE_OVERRIDES = { Server: '서버', Web: '웹', Code: '코드', - Folder: '폴더', - 'Hermes automation created.': 'Hermes 자동화가 생성되었습니다.' + Folder: '폴더' } diff --git a/config/scripts/localization-extraction-call-syntax.test.mjs b/config/scripts/localization-extraction-call-syntax.test.mjs index 125fe9c4fcb..6ae1f24be04 100644 --- a/config/scripts/localization-extraction-call-syntax.test.mjs +++ b/config/scripts/localization-extraction-call-syntax.test.mjs @@ -53,4 +53,47 @@ describe.each([ rmSync(directory, { recursive: true, force: true }) } }) + + it('preserves cross-file namespaces, source locations and fresh plugin transformations', async () => { + const directory = mkdtempSync(join(tmpdir(), 'orca-extraction-passes-')) + const file = join(directory, 'a-consumer.ts') + const config = { + locales: ['en'], + extract: { + input: [join(directory, '*.ts').replaceAll('\\', '/')], + functions: ['t', '*.t'], + disablePlurals: true + } + } + try { + writeFileSync(join(directory, 'z-constants.ts'), "export const NS = 'probe' as const\n") + const source = [ + "import { NS } from './z-constants'", + '/* 🙂 */ const { t } = useTranslation(NS);', + "t('first', 'First')" + ].join('\n') + writeFileSync(file, source) + const first = await extractKeys(config) + expect([...first.allKeys.values()]).toMatchObject([ + { key: 'first', ns: 'probe', locations: [{ file, line: 3, column: 2 }] } + ]) + + writeFileSync(file, source.replace("'first'", "'updated'")) + const updated = await extractKeys(config) + expect([...updated.allKeys.values()].map((entry) => entry.key)).toEqual(['updated']) + + const transformed = await extractKeys({ + ...config, + plugins: [ + { + name: 'fixture-transform', + onLoad: (code) => code.replace("'updated'", "'transformed'") + } + ] + }) + expect([...transformed.allKeys.values()].map((entry) => entry.key)).toEqual(['transformed']) + } finally { + rmSync(directory, { recursive: true, force: true }) + } + }) }) diff --git a/config/scripts/mobile-release-check-scope.test.mjs b/config/scripts/mobile-release-check-scope.test.mjs index 7a07b19af2f..5325c1626c6 100644 --- a/config/scripts/mobile-release-check-scope.test.mjs +++ b/config/scripts/mobile-release-check-scope.test.mjs @@ -102,7 +102,8 @@ it('gates Ruby independently and retains mobile static validation', () => { for (const file of [ 'config/scripts/mobile-release-check-scope.mjs', 'config/scripts/mobile-release-check-scope.test.mjs', - 'config/scripts/pr-code-change-scope.mjs' + 'config/scripts/pr-code-change-scope.mjs', + 'config/scripts/pr-package-test-paths.mjs' ]) { expect(workflow.on.pull_request.paths.some((pattern) => matchesGlob(file, pattern))).toBe(true) } @@ -137,6 +138,7 @@ function fixture(detectorStep = detector) { for (const file of [ 'package.json', 'config/scripts/pr-code-change-scope.mjs', + 'config/scripts/pr-package-test-paths.mjs', 'config/scripts/pr-layout-oracle-route.mjs', 'config/scripts/mobile-release-check-scope.mjs', 'config/scripts/mobile-test-change-scope.mjs' diff --git a/config/scripts/mobile-test-change-scope.mjs b/config/scripts/mobile-test-change-scope.mjs index f773fb8fa02..3be5e7c5ec2 100644 --- a/config/scripts/mobile-test-change-scope.mjs +++ b/config/scripts/mobile-test-change-scope.mjs @@ -8,6 +8,7 @@ const NON_TEST_FILES = new Set([ 'mobile/README.md', '.github/workflows/mobile-ios-release.yml', 'config/scripts/pr-code-change-scope.mjs', + 'config/scripts/pr-package-test-paths.mjs', 'config/scripts/pr-code-change-scope.test.mjs', 'config/scripts/pr-layout-oracle-route.mjs', 'config/scripts/mobile-release-check-scope.mjs', diff --git a/config/scripts/mobile-test-change-scope.test.mjs b/config/scripts/mobile-test-change-scope.test.mjs index 06d5273ec74..d5cdad6abbb 100644 --- a/config/scripts/mobile-test-change-scope.test.mjs +++ b/config/scripts/mobile-test-change-scope.test.mjs @@ -12,6 +12,7 @@ it.each([ 'mobile/fastlane/Fastfile', '.github/workflows/mobile-ios-release.yml', 'config/scripts/pr-code-change-scope.mjs', + 'config/scripts/pr-package-test-paths.mjs', 'config/scripts/mobile-release-check-scope.test.mjs' ])('skips only known non-test inputs: %s', (file) => { expect(shouldRunMobileTests([file])).toBe(false) diff --git a/config/scripts/mobile-web-app-browser-pane-text-inputs.test.mjs b/config/scripts/mobile-web-app-browser-pane-text-inputs.test.mjs deleted file mode 100644 index 0482a51079f..00000000000 --- a/config/scripts/mobile-web-app-browser-pane-text-inputs.test.mjs +++ /dev/null @@ -1,108 +0,0 @@ -/** - * The browser pane's two text inputs, read the way C4.2's census reads them. - * - * The pane is not in any page route today, so no route closure reaches it and the census that - * enforces the seam is not run against it: it walks the source-control hub and the review route, - * and the pane is in neither. C6 raises both inputs anyway, because the failure is not cosmetic — - * an input under 16px makes an iOS browser zoom the page on focus, and `keyboard-occlusion.web.ts` - * reads a visual viewport scale other than 1 as "no keyboard" for the rest of the typing session. - * - * This file is the census run by hand over the pane's own modules, so the raise is checked by the - * rule that will judge it once a route lists the pane. - */ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { describe, expect, it } from 'vitest' -import { - modulesDeclaringTextInput, - textInputFontSizeOffenders, - unresolvedTextInputStyles -} from './mobile-web-app-text-input-font-size-seam.mjs' - -const mobileDir = join(fileURLToPath(new URL('../..', import.meta.url)), 'mobile') - -/** - * The pane's text-input modules, named as the bundler resolves them: a `.web.ts` where one exists, - * which is what `mobileWebAppRouteClosure` reports for a real route. - */ -const PANE_CLOSURE = { - local: [ - 'src/browser/MobileBrowserPaneView.tsx', - 'src/browser/MobileBrowserAddressField.tsx', - 'src/browser/mobile-browser-pane-styles.ts', - 'src/browser/browser-address-field-styles.web.ts', - 'src/platform/text-input-font-size.web.ts' - ] -} - -const KEY_ROW_STYLES = 'src/browser/mobile-browser-pane-styles.ts' - -/** The modules in `PANE_CLOSURE` that render an input, which is what the rule below judges. */ -const PANE_INPUT_MODULES = [ - 'src/browser/MobileBrowserAddressField.tsx', - 'src/browser/MobileBrowserPaneView.tsx' -] - -function offendingFiles() { - return textInputFontSizeOffenders(mobileDir, PANE_CLOSURE).map((entry) => - entry.slice(0, entry.lastIndexOf(':')) - ) -} - -describe('the browser pane text inputs under the C4.2 census', () => { - /** - * The precondition the rest of this file rests on. - * - * `PANE_CLOSURE` is written by hand, because the pane is in no page route and there is no - * closure to derive it from. So "no unresolved styles" says the walk read the files listed, not - * that those files are the pane's inputs — a third input module added under `src/browser/` would - * leave every assertion below green and unchecked. This scans for it instead. - */ - it('lists every module under src/browser that renders an input', () => { - expect(modulesDeclaringTextInput(mobileDir, 'src/browser')).toEqual(PANE_INPUT_MODULES) - expect(PANE_CLOSURE.local).toEqual(expect.arrayContaining(PANE_INPUT_MODULES)) - }) - - it('reads every input the pane declares, so the verdicts below are complete', () => { - // The completeness half: an empty offender list means nothing if the walk found no input. - expect(unresolvedTextInputStyles(mobileDir, PANE_CLOSURE)).toEqual([]) - expect(PANE_CLOSURE.local).toContain('src/platform/text-input-font-size.web.ts') - }) - - // A scan that happens to find two things is not a scan that would find a third. Planted in a - // scratch tree rather than in src/browser, so no other census ever walks the plant. - it('would report a third input module, and ignores tests and non-JSX mentions', () => { - const scratch = mkdtempSync(join(tmpdir(), 'orca-browser-pane-inputs-')) - try { - mkdirSync(join(scratch, 'src/browser'), { recursive: true }) - for (const [name, source] of Object.entries({ - 'Planted.tsx': 'export const P = () => ', - 'Planted.test.tsx': 'it("x", () => )', - 'mentions-only.ts': "import { TextInput } from 'react-native' // TextInput, named twice" - })) { - writeFileSync(join(scratch, 'src/browser', name), source) - } - - expect(modulesDeclaringTextInput(scratch, 'src/browser')).toEqual(['src/browser/Planted.tsx']) - } finally { - rmSync(scratch, { recursive: true, force: true }) - } - }) - - it('takes the key row input through the seam', () => { - expect(offendingFiles()).not.toContain(KEY_ROW_STYLES) - }) - - /** - * The address field is split, and the split is what the census now reads. - * - * Native keeps the 12px the toolbar has always rendered, because no phone has a page to zoom; - * the browser gets the seam. `resolveLocal` follows the `.web.ts` the builder would have - * resolved, so the size judged here is the size the page runs rather than the one it does not. - */ - it('takes the address field through the seam, through its .web sibling', () => { - expect(offendingFiles()).toEqual([]) - }) -}) diff --git a/config/scripts/mobile-web-app-module-closure.test.mjs b/config/scripts/mobile-web-app-module-closure.test.mjs deleted file mode 100644 index 4be15640f3b..00000000000 --- a/config/scripts/mobile-web-app-module-closure.test.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import { mkdir, realpath, writeFile } from 'node:fs/promises' -import { join } from 'node:path' -import { expect, it } from 'vitest' -import { mobileWebAppModuleClosure } from './build-mobile-web-app-bundle.mjs' -import { withScratch } from './mobile-web-app-bundle-test-fixture.mjs' - -it.each(['app/_layout', 'app/_layout.tsx'])( - 'resolves %s from the supplied directory without a TypeScript baseUrl', - async (entry) => { - await withScratch(async (scratch) => { - const root = await realpath(scratch) - await mkdir(join(root, 'app')) - await writeFile(join(root, 'app/_layout.tsx'), "export default 'native'") - await writeFile(join(root, 'app/_layout.web.tsx'), "export default 'web'") - const closure = await mobileWebAppModuleClosure([entry], { absWorkingDir: root }) - expect(closure.modules).toEqual(['app/_layout.web.tsx']) - expect(closure.local).toEqual(['app/_layout.web.tsx']) - }) - } -) diff --git a/config/scripts/mobile-web-app-safe-area-render.test.mjs b/config/scripts/mobile-web-app-safe-area-render.test.mjs index 5a3c5260afc..f311a45bca9 100644 --- a/config/scripts/mobile-web-app-safe-area-render.test.mjs +++ b/config/scripts/mobile-web-app-safe-area-render.test.mjs @@ -14,7 +14,7 @@ import { } from './mobile-web-app-render-harness.mjs' // The page lays out edge-to-edge under the shell's system bars and pads through its own -// SafeAreaViews, by the insets `init` carries. Measured in a real browser because what is under +// SafeAreaViews, and its root for the sides, by the insets `init` carries. Measured in a real browser because what is under // test is which providers and navigators expo-router actually mounts above a screen. const HOST_ROUTE = '/h/render-check-host' const HOST = { @@ -106,7 +106,9 @@ function readEdges(page) { ) return { titleTop: title === null ? null : range.getBoundingClientRect().top, + titleLeft: title === null ? null : range.getBoundingClientRect().left, fabBottom: fab?.getBoundingClientRect().bottom ?? null, + fabRight: fab?.getBoundingClientRect().right ?? null, envTop: (() => { const probe = document.createElement('div') probe.style.paddingTop = 'env(safe-area-inset-top)' @@ -142,6 +144,19 @@ describeRender('the page pads for the system bars once, by the shell insets', () expect(padded.titleTop - zero.titleTop).toBe(INSETS.top) }, 90_000) + // Landscape puts the iPhone sensor housing on a side. Unequal sides, so a swapped edge fails. + it('keeps the screens clear of the side insets, padding them once', async () => { + const upright = await openHostRoute(INSETS) + const base = await readEdges(upright.page) + await upright.page.close() + const sideways = await openHostRoute({ ...INSETS, left: 59, right: 44 }) + const inset = await readEdges(sideways.page) + await sideways.page.close() + expect([...upright.errors, ...sideways.errors]).toEqual([]) + expect(inset.titleLeft - base.titleLeft).toBe(59) + expect(base.fabRight - inset.fabRight).toBe(44) + }, 60_000) + it('follows a re-sent init, which is how the keyboard and rotation reach the page', async () => { const opened = await openHostRoute(INSETS) const before = await readEdges(opened.page) diff --git a/config/scripts/mobile-web-app-session-terminal-closure.test.mjs b/config/scripts/mobile-web-app-session-terminal-closure.test.mjs index 8146aa13f0c..c6ff5223013 100644 --- a/config/scripts/mobile-web-app-session-terminal-closure.test.mjs +++ b/config/scripts/mobile-web-app-session-terminal-closure.test.mjs @@ -1,6 +1,6 @@ import { join } from 'node:path' import { fileURLToPath } from 'node:url' -import { describe, expect, it } from 'vitest' +import { beforeAll, describe, expect, it } from 'vitest' import { mobileWebAppRouteClosure } from './build-mobile-web-app-bundle.mjs' import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs' import { mobileWebAppRouteChunkClosure } from './mobile-web-app-route-chunk-closure.mjs' @@ -478,8 +478,13 @@ const describeClosure = bundles ? describe : describe.skip describeClosure( "the session route's page closure with the terminal on it", () => { + let closure + beforeAll(async () => { + closure = await mobileWebAppRouteClosure(SESSION_ROUTE) + }, 300_000) + it('gains the document, xterm and the addons, and sheds the engine string', async () => { - const { local, modules } = await mobileWebAppRouteClosure(SESSION_ROUTE) + const { local, modules } = closure for (const gone of SHED) { expect(local, `${gone} is still in the closure`).not.toContain(gone) } @@ -506,7 +511,7 @@ describeClosure( }, 300_000) it('enters through the web sibling and the route body, not the switch', async () => { - const { local } = await mobileWebAppRouteClosure(SESSION_ROUTE) + const { local } = closure for (const entry of ROUTE_ENTRY) { expect(local, `${entry} is not in the closure`).toContain(entry) } @@ -518,7 +523,7 @@ describeClosure( }, 300_000) it('reaches the engine as one deferred module and never as part of the download', async () => { - const { modules } = await mobileWebAppRouteClosure(SESSION_ROUTE) + const { modules } = closure // The engine is here, as the one artifact the loader imports. expect(artifactModules(modules)).toHaveLength(1) // Package sources stay inside that artifact; unrelated module counts are not this boundary. @@ -537,16 +542,17 @@ describeClosure( }, 600_000) it('leaves the 16px seam census exactly where C7.2 left it', async () => { - const closure = await mobileWebAppRouteClosure(SESSION_ROUTE) // Two preconditions, because zero offenders is what a walk that read nothing also reports: // the seam's own web module has to be in the closure, and no style may be unresolved. expect(closure.local).toContain('src/platform/text-input-font-size.web.ts') + expect(closure.local.length).toBeGreaterThan(900) expect(unresolvedTextInputStyles(mobileDir, closure)).toEqual([]) expect(textInputFontSizeOffenders(mobileDir, closure)).toHaveLength(EXPECTED_OFFENDERS) + expect(closure.local).toContain('src/session/mobile-native-chat-input-styles.web.ts') + expect(closure.local).not.toContain('src/session/mobile-native-chat-input-styles.ts') }, 300_000) it('holds the editables the TextInput census cannot see to the same floor', async () => { - const closure = await mobileWebAppRouteClosure(SESSION_ROUTE) // The rich Markdown editor's surface is a `contenteditable` in a markup string, sized by a // rule in a stylesheet: `modulesDeclaringTextInput` matches JSX tags and never sees it, so it // shipped at 14 px and was measured at 14 px in both engines. The same floor, read by a rule diff --git a/config/scripts/mobile-web-app-session-text-inputs.test.mjs b/config/scripts/mobile-web-app-session-text-inputs.test.mjs deleted file mode 100644 index c8b1f76bb1c..00000000000 --- a/config/scripts/mobile-web-app-session-text-inputs.test.mjs +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Every text input the session screen reaches, and the size it declares. - * - * iOS zooms the page on focus of any input under 16px and does not zoom back out, so the document - * spends the rest of that typing session at a scale other than 1 — which `keyboard-occlusion.web.ts` - * reads as "not a keyboard" on purpose, because geometry cannot separate a zoom from a keyboard. - * - * This closure is the one that cannot afford it. The terminal's own input is a hidden field the - * keyboard seam's geometry is the whole basis of, and this screen reaches nine inputs that declare - * a size — a custom-key capture field, a prompt modal, the chat's ask/composer/question fields, the - * quick-command editor and its search, and the terminal command bar. One of them left off the seam - * leaves every later focus on this screen measuring a zoomed document. - * - * The rule is the closure rather than the nine sites it happens to have today: a module entering it - * later is held to it without anyone remembering to add it here. - */ -import { fileURLToPath } from 'node:url' -import { describe, expect, it } from 'vitest' -import { mobileWebAppRouteClosure } from './build-mobile-web-app-bundle.mjs' -import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs' -import { - TEXT_INPUT_FONT_SIZE_SEAM, - textInputFontSizeOffenders, - unresolvedTextInputStyles -} from './mobile-web-app-text-input-font-size-seam.mjs' - -const mobileDir = fileURLToPath(new URL('../../mobile/', import.meta.url)) -const describeClosure = mobileWebAppDependenciesPresent() ? describe : describe.skip - -const SESSION = 'app/h/[hostId]/session/[worktreeId].tsx' - -/** - * The style module this screen splits, as the page bundle resolves it. - * - * Named rather than left to the offender list because a split is the one fix that can be undone - * without reopening the offence: delete the `.web.ts` and the native sibling's size is what the - * page runs, which is a 15px chat composer and a zoomed document, and the offender list would say - * so — but only the next time someone reads it. Listed here, the closure says which file the page - * loads. - */ -const SPLIT_WEB_STYLES = ['src/session/mobile-native-chat-input-styles.web.ts'] - -describeClosure( - 'the text inputs the session screen reaches', - () => { - it('takes every input size through the seam', async () => { - const closure = await mobileWebAppRouteClosure(SESSION) - expect(textInputFontSizeOffenders(mobileDir, closure)).toEqual([]) - }) - - it('reads every input it found, so the list above is complete', async () => { - // The completeness half: an empty offender list is evidence only if every input was read. - const closure = await mobileWebAppRouteClosure(SESSION) - expect(unresolvedTextInputStyles(mobileDir, closure)).toEqual([]) - }) - - it('carries the seam, so the rule is not vacuous', async () => { - // Without this an empty offender list would also be what a closure reaching no text input at - // all produces, and the census would pass against a page that has nothing to raise. - const closure = await mobileWebAppRouteClosure(SESSION) - expect(closure.local).toContain(TEXT_INPUT_FONT_SIZE_SEAM) - expect(closure.local.length).toBeGreaterThan(900) - }) - - it('loads the web half of the split style module, not the native one', async () => { - const closure = await mobileWebAppRouteClosure(SESSION) - expect(closure.local).toEqual(expect.arrayContaining(SPLIT_WEB_STYLES)) - expect( - closure.local.filter((file) => - SPLIT_WEB_STYLES.some((web) => file === web.replace('.web.ts', '.ts')) - ) - ).toEqual([]) - }) - }, - 240_000 -) diff --git a/config/scripts/mobile-web-app-terminal-live-input-render.test.mjs b/config/scripts/mobile-web-app-terminal-live-input-render.test.mjs index b1821fd0683..20ae89f970b 100644 --- a/config/scripts/mobile-web-app-terminal-live-input-render.test.mjs +++ b/config/scripts/mobile-web-app-terminal-live-input-render.test.mjs @@ -36,7 +36,7 @@ import { LAYOUT_SOURCE } from './mobile-web-app-terminal-probe-route.mjs' * ref points at is inside that branch and never renders, `liveInputRef.current` is null, and * `liveInputRef.current?.setNativeProps(...)` is skipped by its own optional chain. The check's * error list is exactly empty for a page that never made the call. Its two siblings — - * `mobile-web-app-session-text-inputs.test.mjs` and `mobile-web-app-session-media-picker.test.mjs` + * `mobile-web-app-session-terminal-closure.test.mjs` and `mobile-web-app-session-media-picker.test.mjs` * — are source censuses over the route closure and mount nothing at all. * * So this file gives the hooks the one thing that route cannot: a mounted field. Everything else diff --git a/config/scripts/mobile-web-app-text-input-discovery.test.mjs b/config/scripts/mobile-web-app-text-input-discovery.test.mjs new file mode 100644 index 00000000000..6dc9241a898 --- /dev/null +++ b/config/scripts/mobile-web-app-text-input-discovery.test.mjs @@ -0,0 +1,22 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { expect, it } from 'vitest' +import { modulesDeclaringTextInput } from './mobile-web-app-text-input-font-size-seam.mjs' + +it('discovers input modules and ignores tests and non-JSX mentions', () => { + const scratch = mkdtempSync(join(tmpdir(), 'orca-text-input-discovery-')) + try { + mkdirSync(join(scratch, 'src/browser'), { recursive: true }) + for (const [name, source] of Object.entries({ + 'Planted.tsx': 'export const P = () => ', + 'Planted.test.tsx': 'it("x", () => )', + 'mentions-only.ts': "import { TextInput } from 'react-native' // TextInput, named twice" + })) { + writeFileSync(join(scratch, 'src/browser', name), source) + } + expect(modulesDeclaringTextInput(scratch, 'src/browser')).toEqual(['src/browser/Planted.tsx']) + } finally { + rmSync(scratch, { recursive: true, force: true }) + } +}) diff --git a/config/scripts/node-server-change-scope.mjs b/config/scripts/node-server-change-scope.mjs index 7b14c8dbbdf..9e66329e65d 100644 --- a/config/scripts/node-server-change-scope.mjs +++ b/config/scripts/node-server-change-scope.mjs @@ -18,6 +18,7 @@ const BUILD_SCRIPTS = [ 'config/scripts/build-windows-security-addon.mjs', 'config/scripts/run-node-server-tests.mjs', 'config/vitest.config.ts', + 'config/scripts/vitest-dom-setup.ts', 'config/scripts/happy-dom-offscreen-canvas.ts', 'config/scripts/happy-dom-mutation-observer-retention.ts', 'config/scripts/vitest-host-ports-setup.ts', diff --git a/config/scripts/node-server-change-scope.test.mjs b/config/scripts/node-server-change-scope.test.mjs index 156c2bcdeeb..dfaf3f2cb59 100644 --- a/config/scripts/node-server-change-scope.test.mjs +++ b/config/scripts/node-server-change-scope.test.mjs @@ -527,7 +527,7 @@ it('keeps every platform job and runs them when detection is skipped or fails', expect(workflow.on).toHaveProperty('workflow_dispatch') expect(workflow.on).toHaveProperty('workflow_call') expect(workflow.on).not.toHaveProperty('push') - expect(workflow.on).not.toHaveProperty('schedule') + expect(workflow.on.schedule).toHaveLength(1) expect(workflow.jobs.changes.if).toBe( "github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft != true)" ) diff --git a/config/scripts/orcad-e2e-routing.test.mjs b/config/scripts/orcad-e2e-routing.test.mjs index 7ccc061aab2..e1838741ccf 100644 --- a/config/scripts/orcad-e2e-routing.test.mjs +++ b/config/scripts/orcad-e2e-routing.test.mjs @@ -90,7 +90,7 @@ it('builds the e2e app when only a build-dependent orcad spec is requested', () e2e_needs_build: true }) } - expect(jobs['orcad-ssh-docker'].needs).toEqual(['build', 'prepare-native-cache']) + expect(jobs['orcad-ssh-docker'].needs).toEqual(['build']) }) it('runs every managed-SSH spec in one sharded lane, only when one is routed', () => { diff --git a/config/scripts/orcad-windows-prebuild-workflow.test.mjs b/config/scripts/orcad-windows-prebuild-workflow.test.mjs index 6c39c265e50..97f54cf3bd7 100644 --- a/config/scripts/orcad-windows-prebuild-workflow.test.mjs +++ b/config/scripts/orcad-windows-prebuild-workflow.test.mjs @@ -225,6 +225,7 @@ node() { IFS= read -r verdict < out/orcad-prebuilds/manifest.json [ "$verdict" = valid ] ;; *--print-slot) echo win32-x64 ;; + *--require-slots*|*--smoke) grep -qx valid out/orcad-prebuilds/manifest.json ;; *) return 41 ;; esac } @@ -288,8 +289,8 @@ describe.skipIf(process.platform === 'win32')('Windows prebuild Bash fallback', ).toBe(validates) expect(commands.slice(-3)).toEqual([ 'node config/scripts/build-orcad-prebuilds.mjs --print-slot', - 'pnpm build:orcad-prebuilds --require-slots win32-x64', - 'pnpm build:orcad-prebuilds --smoke' + 'node config/scripts/build-orcad-prebuilds.mjs --require-slots win32-x64', + 'node config/scripts/build-orcad-prebuilds.mjs --smoke' ]) }) diff --git a/config/scripts/playwright-focused-workflow-selection.test.mjs b/config/scripts/playwright-focused-workflow-selection.test.mjs index 36ee3045e1e..12403ab4a53 100644 --- a/config/scripts/playwright-focused-workflow-selection.test.mjs +++ b/config/scripts/playwright-focused-workflow-selection.test.mjs @@ -75,29 +75,36 @@ it('focused IME and golden commands discover only their requested files with the `const { test } = require(${testPackage}); test('selected', () => {});` ) } - for (const args of commands) { - const result = await runProcess({ - program: process.execPath, - cwd: directory, - args: [ - join(dirname(require.resolve('playwright/package.json')), 'cli.js'), - 'test', - '--config', - config, - '--list', - '--reporter=json', - ...args - ], - env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, - timeoutMs: 20_000 + const results = await Promise.allSettled( + commands.map(async (args) => { + const result = await runProcess({ + program: process.execPath, + cwd: directory, + args: [ + join(dirname(require.resolve('playwright/package.json')), 'cli.js'), + 'test', + '--config', + config, + '--list', + '--reporter=json', + ...args + ], + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' }, + timeoutMs: 20_000 + }) + expect(result.code, result.stderr).toBe(0) + const report = JSON.parse(result.stdout) + const expected = args.filter((arg) => arg.endsWith('.spec.ts')).sort() + expect( + report.suites.map((suite) => suite.file.replaceAll('\\', '/')).sort(), + JSON.stringify(args) + ).toEqual(expected) }) - expect(result.code, result.stderr).toBe(0) - const report = JSON.parse(result.stdout) - const expected = args.filter((arg) => arg.endsWith('.spec.ts')).sort() - expect( - report.suites.map((suite) => suite.file.replaceAll('\\', '/')).sort(), - JSON.stringify(args) - ).toEqual(expected) + ) + for (const result of results) { + if (result.status === 'rejected') { + throw result.reason + } } } finally { rmSync(directory, { recursive: true, force: true }) diff --git a/config/scripts/pr-auxiliary-preflight-admission.test.mjs b/config/scripts/pr-auxiliary-preflight-admission.test.mjs index d0c41ffc7c7..ff69c2c67c1 100644 --- a/config/scripts/pr-auxiliary-preflight-admission.test.mjs +++ b/config/scripts/pr-auxiliary-preflight-admission.test.mjs @@ -251,7 +251,15 @@ describe('Mobile CI tool preflight routing', () => { it('keeps root shard execution enabled for callers that do not opt out', () => { expect(unit.on.workflow_call.inputs.run_shards.default).toBe(true) expect(evaluate(unit.jobs.test.if, { inputs: { run_shards: true } })).toBe(true) - expect(unit.jobs.relay_integration.if).toBeUndefined() + const relayDefault = unit.on.workflow_call.inputs.run_relay_integration.default + expect(relayDefault).toBe(true) + expect( + evaluate(unit.jobs.relay_integration.if, { inputs: { run_relay_integration: relayDefault } }) + ).toBe(true) + expect( + evaluate(unit.jobs.relay_integration.if, { inputs: { run_relay_integration: false } }) + ).toBe(false) + expect(workflow.jobs.test.with.run_relay_integration).toBeUndefined() expect(unit.jobs.relay_integration.needs).toBeUndefined() }) @@ -266,6 +274,7 @@ describe('Mobile CI tool preflight routing', () => { [tool, 'package.json'], [tool, 'config/vitest.config.ts'], [tool, 'config/scripts/pr-code-change-scope.mjs'], + [tool, 'config/scripts/pr-package-test-paths.mjs'], [tool, 'config/scripts/new-mobile-tool.mjs'], [tool, 'README.md'], [tool, 'mobile/vitest.config.ts'] @@ -301,6 +310,7 @@ function createMobileCiScopeRepository( for (const file of [ 'package.json', 'config/scripts/pr-code-change-scope.mjs', + 'config/scripts/pr-package-test-paths.mjs', 'config/scripts/pr-layout-oracle-route.mjs', 'config/scripts/git-pull-request-diff-base.mjs', 'config/scripts/pr-e2e-source-routing.mjs', diff --git a/config/scripts/pr-code-change-scope.mjs b/config/scripts/pr-code-change-scope.mjs index e9e418643bf..29fb7490431 100644 --- a/config/scripts/pr-code-change-scope.mjs +++ b/config/scripts/pr-code-change-scope.mjs @@ -3,6 +3,7 @@ import { join } from 'node:path' import process from 'node:process' import { pathToFileURL } from 'node:url' import { isLayoutOracleSource } from './pr-layout-oracle-route.mjs' +import { LINUX_PACKAGE_TESTS, WINDOWS_PACKAGE_TESTS } from './pr-package-test-paths.mjs' const DOCS_ONLY_FILES = new Set([ 'README.md', @@ -50,7 +51,8 @@ const GLOBAL_FORCE_PREFIXES = [ '.github/actions/install-node-dependencies/', '.github/actions/restore-pnpm-verification/', '.github/actions/prepare-native-runtime/', - 'config/scripts/pr-code-change-scope' + 'config/scripts/pr-code-change-scope', + 'config/scripts/pr-package-test-paths' ] const GLOBAL_FORCE_FILES = new Set(['package.json', 'pnpm-lock.yaml']) @@ -334,6 +336,7 @@ const LINUX_PACKAGE_PREFIXES = [ 'config/scripts/process-failure-message.mjs', 'config/scripts/space-sharing-copy.mjs', '.github/actions/prepare-linux-package-fixture/', + '.github/actions/prepare-wayland-clipboard/', 'config/docker/cli-launch-contract/', 'config/docker/headless-pairing/', 'config/docker/headless-serve-shutdown/', @@ -343,6 +346,7 @@ const LINUX_PACKAGE_PREFIXES = [ 'config/scripts/run-daemon-shutdown-descendants-docker', 'config/scripts/static-appimage-package-contract', 'native/computer-use-linux/', + 'native/wayland-clipboard/', 'resources/linux/', 'config/scripts/run-headless-serve' ] @@ -361,82 +365,6 @@ const WINDOWS_PACKAGE_PREFIXES = [ 'tests/tools/windows-pty-native-capability-smoke/' ] -const LINUX_PACKAGE_TESTS = [ - 'src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts', - 'src/main/browser/browser-route-tcp-egress.electron.test.ts', - 'src/main/browser/browser-route-webrtc-egress.electron.test.ts', - 'src/main/browser/browser-route-h3-egress.electron.test.ts', - 'src/main/browser/browser-route-dns-prefetch.electron.test.ts', - 'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts' -] - -const WINDOWS_PACKAGE_TESTS = [ - 'src/main/zcode/zcode-plan-api-key-store-native-windows.test.ts', - ...LINUX_PACKAGE_TESTS, - 'config/scripts/rebuild-native-deps.test.mjs', - 'config/scripts/rebuild-native-deps-windows-process-tree.test.mjs', - 'config/scripts/rebuild-native-deps-node-pty.test.mjs', - 'config/scripts/nsis-process-check.test.mjs', - 'config/scripts/idle-cpu-process-sampling.test.mjs', - 'config/scripts/ensure-native-runtime-job-ownership.test.mjs', - 'config/scripts/verify-packaged-node-pty-job-ownership.test.mjs', - 'config/scripts/windows-pe-machine.test.mjs', - 'config/scripts/build-windows-cli-launcher.test.mjs', - 'config/scripts/verify-packaged-windows-agent-hook.test.mjs', - 'config/scripts/script-module-dependencies.test.mjs', - 'src/main/windows-registry-addon.test.ts', - 'src/shared/windows-security-addon.win32.test.ts', - 'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts', - 'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts', - 'src/shared/__tests__/process-host/windows-command-line.win32.test.ts', - 'src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts', - 'src/shared/__tests__/process-host/windows-cmd-shim-resolution.win32.test.ts', - 'src/main/agent-hooks/windows-hook-payload-delivery.test.ts', - 'src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts', - 'src/main/jcode/hook-gate-script.test.ts', - 'src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts', - 'src/main/agent-hooks/windows-native-hook-exe-hosts.test.ts', - 'src/main/claude/windows-native-hook-registration.test.ts', - 'src/main/agent-hooks/windows-cmd-hook-command-unicode.test.ts', - 'src/main/agent-hooks/windows-batch-hook-launcher.test.ts', - 'src/main/agent-hooks/windows-powershell-hook-launcher.test.ts', - 'src/main/claude/claude-status-line-windows.test.ts', - 'src/main/codex/windows-hook-command.test.ts', - 'src/main/codex/windows-hook-upgrade.test.ts', - 'src/main/codex/hook-service-managed-install.test.ts', - 'src/main/windows/windows-pty-job.win32.test.ts', - 'src/main/windows/windows-msys-job.win32.test.ts', - 'src/main/providers/agent-foreground-process-git-bash.win32.test.ts', - 'src/main/windows/windows-host-job.win32.test.ts', - 'src/main/windows/windows-process-tree-command-line-patch.test.ts', - 'src/main/windows/windows-process-table-native-addon.win32.test.ts', - 'src/main/daemon/daemon-pid-identity-native.win32.test.ts', - 'src/main/windows/windows-administrator-status.win32.test.ts', - 'src/main/windows/windows-administrator-restart.win32.test.ts', - 'src/main/persistence/profile-state/profile-state-access-windows-native.win32.test.ts', - 'src/main/windows-live-tree-kill.win32.test.ts', - 'src/main/wsl/wsl-runner.test.ts', - 'src/main/wsl/wsl-guest-environment.test.ts', - 'src/main/wsl/wsl-executable-path.win32.test.ts', - 'src/main/wsl/wsl-w1-w3-contract.test.ts', - 'src/shared/source-scan/source-tree-scan.test.ts', - 'src/main/cli/wsl-cli-powershell-boundary.test.ts', - 'src/main/computer/desktop-script-runtime-host.win32.test.ts', - 'src/main/cursor/hook-service.test.ts', - 'src/main/orca-profiles/profile-index-store.test.ts', - 'src/main/startup/windows-install-dir-acl-repair.win32.test.ts', - 'src/main/runtime/repo-worktree-admin-fingerprint.test.ts', - 'src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts', - 'src/shared/secure-file-fsync-flags.test.ts', - 'src/shared/secure-path-windows-acl.win32.test.ts', - 'src/main/runtime/unreadable-secret-store-preservation.win32.test.ts', - 'src/main/ipc/pty-codex-account-attribution.test.ts', - 'src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts', - 'src/main/ipc/preflight-provider-command-selection.test.ts', - 'src/main/ipc/preflight-runnable-local-cli.test.ts', - 'src/main/ssh/remote-node-runtime-store-windows.test.ts' -] - const DESKTOP_IRRELEVANT_PREFIXES = [ 'mobile/', 'cloud/', diff --git a/config/scripts/pr-code-change-scope.test.mjs b/config/scripts/pr-code-change-scope.test.mjs index 9a9d30852e2..6565e12977b 100644 --- a/config/scripts/pr-code-change-scope.test.mjs +++ b/config/scripts/pr-code-change-scope.test.mjs @@ -204,10 +204,9 @@ describe('per-job path classification', () => { }) it('runs native package jobs only for the platform that ships the changed native', () => { - expectClassification(['native/windows-cli-launcher/src/main.rs'], { - package_windows: true - }) - expectClassification(['native/computer-use-linux/runtime.py'], { + expectClassification(['native/windows-cli-launcher/src/main.rs'], { package_windows: true }) + expectClassification(['native/computer-use-linux/runtime.py'], { package: true }) + expectClassification(['native/wayland-clipboard/wayland-clipboard.c'], { package: true }) expectClassification(['native/computer-use-macos/Package.swift'], {}) @@ -220,6 +219,7 @@ describe('per-job path classification', () => { 'config/scripts/process-failure-message.mjs', 'config/scripts/space-sharing-copy.mjs', '.github/actions/prepare-linux-package-fixture/action.yml', + '.github/actions/prepare-wayland-clipboard/action.yml', 'config/docker/cli-launch-contract/Dockerfile', 'config/docker/cli-launch-contract/run-cli-case.sh', 'config/docker/headless-pairing/Dockerfile', diff --git a/config/scripts/pr-e2e-gate-contract.test.mjs b/config/scripts/pr-e2e-gate-contract.test.mjs index 46c5cff3629..88d70856dd8 100644 --- a/config/scripts/pr-e2e-gate-contract.test.mjs +++ b/config/scripts/pr-e2e-gate-contract.test.mjs @@ -210,8 +210,8 @@ describe('PR E2E gate contract', () => { 'src/renderer/src/lib/pane-manager/terminal-ime-anchor.ts', 'src/shared/terminal-unicode-provider.ts', // The xterm fork owns the helper textarea the IME attaches to; no file here says "ime". - 'config/patches/@xterm__xterm@6.1.0-beta.287.patch', - 'config/patches/xterm-src/browser/Terminal.ts', + 'config/patches/@xterm__xterm@6.1.0-beta.304.patch', + 'config/patches/xterm-src/@xterm__xterm@6.1.0-beta.304.src.patch', // The harness is source too: breaking the runner or a probe is how the lane goes blind. 'config/scripts/run-terminal-ibus-hangul-e2e.mjs', 'config/scripts/terminal-ime-engagement-receipt.mjs', @@ -237,6 +237,36 @@ describe('PR E2E gate contract', () => { } }) + it('routes only core xterm patches to both IME lanes across version updates', () => { + for (const file of [ + 'config/patches/@xterm__xterm@6.1.0-beta.304.patch', + 'config/patches/xterm-src/@xterm__xterm@6.1.0-beta.304.src.patch', + 'config/patches/@xterm__xterm@7.0.0.patch', + 'config/patches/xterm-src/@xterm__xterm@7.0.0.src.patch' + ]) { + const specs = selectPrE2eSpecs([file]) + expect(hasNativeImeSourceChange([file]), file).toBe(true) + expect(specs, file).toContain('tests/e2e/terminal-ime-exact-byte.spec.ts') + expect(specs, file).toContain('tests/e2e/terminal-ibus-hangul-native.spec.ts') + } + for (const file of [ + 'config/patches/i18next-cli@1.74.2.patch', + 'config/patches/node-pty@1.1.0.patch', + 'config/patches/monaco-editor@0.55.1.patch', + 'config/patches/@xterm__addon-webgl@0.20.0-beta.300.patch', + 'config/patches/xterm-src/@xterm__addon-search@0.17.0-beta.301.src.patch', + 'config/patches/xterm-upstream.json', + 'config/patches/@xterm__xterm@7.0.0.patch.bak' + ]) { + expect(hasNativeImeSourceChange([file]), file).toBe(false) + expect(selectPrE2eSpecs([file]), file).toEqual([]) + } + const corePatch = 'config/patches/@xterm__xterm@6.1.0-beta.304.patch' + const mixedChanges = ['config/patches/i18next-cli@1.74.2.patch', corePatch] + expect(hasNativeImeSourceChange(mixedChanges)).toBe(true) + expect(selectPrE2eSpecs(mixedChanges)).toEqual(selectPrE2eSpecs([corePatch])) + }) + it('keeps the native IME spec out of the lane that would silently skip it', () => { expect(DEDICATED_E2E_SPECS).toContain('tests/e2e/terminal-ibus-hangul-native.spec.ts') // Why it still has to be routed: the dedicated lane is selected by the same route, so the diff --git a/config/scripts/pr-e2e-source-routing.mjs b/config/scripts/pr-e2e-source-routing.mjs index 530abb1da06..24fed423c7a 100644 --- a/config/scripts/pr-e2e-source-routing.mjs +++ b/config/scripts/pr-e2e-source-routing.mjs @@ -9,10 +9,12 @@ import { LAYOUT_ORACLE_E2E_SPECS } from './pr-layout-oracle-route.mjs' const isProductSource = (file) => !/\.test\.tsx?$/.test(file) && !isUnitTestSupportSource(file) -// Why config/patches: the xterm fork owns the helper textarea an input method attaches to, so a -// patch edit can break composition without touching a file named "ime". +// The core xterm patch owns the helper textarea an input method attaches to. +const XTERM_CORE_PATCH = + /^config\/patches\/(?:@xterm__xterm@[^/]+\.patch|xterm-src\/@xterm__xterm@[^/]+\.src\.patch)$/ + const NATIVE_IME_PRODUCT_SOURCE = - /^(?:config\/patches\/|src\/shared\/terminal-unicode-provider\.ts$|src\/renderer\/src\/lib\/pane-manager\/terminal-ime-|src\/renderer\/src\/components\/terminal-pane\/(?:terminal-ime-|terminal-ios-hangul-|xterm-bypass-policy))/ + /^(?:src\/shared\/terminal-unicode-provider\.ts$|src\/renderer\/src\/lib\/pane-manager\/terminal-ime-|src\/renderer\/src\/components\/terminal-pane\/(?:terminal-ime-|terminal-ios-hangul-|xterm-bypass-policy))/ /** The harness itself: the session runner, the boundary probes, and the native specs. */ const NATIVE_IME_HARNESS = @@ -255,9 +257,10 @@ export const PR_E2E_SOURCE_ROUTES = [ ], matches: (file) => isProductSource(file) && - /^(?:config\/patches\/|src\/renderer\/src\/components\/terminal-pane\/(?:terminal-ime-|use-terminal-pane-lifecycle|xterm-bypass-policy|terminal-option-shortcut-policy))/.test( - file - ) + (XTERM_CORE_PATCH.test(file) || + /^src\/renderer\/src\/components\/terminal-pane\/(?:terminal-ime-|use-terminal-pane-lifecycle|xterm-bypass-policy|terminal-option-shortcut-policy)/.test( + file + )) }, { // Why a route beside terminal-input.ime-and-synthetic-forwarding rather than more specs on @@ -268,7 +271,8 @@ export const PR_E2E_SOURCE_ROUTES = [ id: 'terminal-ime.native-input-method', specs: ['tests/e2e/terminal-ibus-hangul-native.spec.ts'], matches: (file) => - (isProductSource(file) && NATIVE_IME_PRODUCT_SOURCE.test(file)) || + (isProductSource(file) && + (XTERM_CORE_PATCH.test(file) || NATIVE_IME_PRODUCT_SOURCE.test(file))) || NATIVE_IME_HARNESS.test(file) }, { diff --git a/config/scripts/pr-package-test-paths.mjs b/config/scripts/pr-package-test-paths.mjs new file mode 100644 index 00000000000..e19e1be0d51 --- /dev/null +++ b/config/scripts/pr-package-test-paths.mjs @@ -0,0 +1,78 @@ +export const LINUX_PACKAGE_TESTS = [ + 'src/main/browser/browser-client-page-renderer-lifecycle.electron.test.ts', + 'src/main/browser/browser-route-tcp-egress.electron.test.ts', + 'src/main/browser/browser-route-webrtc-egress.electron.test.ts', + 'src/main/browser/browser-route-h3-egress.electron.test.ts', + 'src/main/browser/browser-route-dns-prefetch.electron.test.ts', + 'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts', + 'src/main/browser/browser-popup-compatibility.electron.test.ts', + 'src/main/browser/browser-popup-native-compatibility.electron.test.ts', + 'src/main/browser/browser-popup-background-compatibility.electron.test.ts' +] + +export const WINDOWS_PACKAGE_TESTS = [ + 'src/main/zcode/zcode-plan-api-key-store-native-windows.test.ts', + ...LINUX_PACKAGE_TESTS, + 'config/scripts/rebuild-native-deps.test.mjs', + 'config/scripts/rebuild-native-deps-windows-process-tree.test.mjs', + 'config/scripts/rebuild-native-deps-node-pty.test.mjs', + 'config/scripts/nsis-process-check.test.mjs', + 'config/scripts/idle-cpu-process-sampling.test.mjs', + 'config/scripts/ensure-native-runtime-job-ownership.test.mjs', + 'config/scripts/verify-packaged-node-pty-job-ownership.test.mjs', + 'config/scripts/windows-pe-machine.test.mjs', + 'config/scripts/build-windows-cli-launcher.test.mjs', + 'config/scripts/verify-packaged-windows-agent-hook.test.mjs', + 'config/scripts/script-module-dependencies.test.mjs', + 'src/main/windows-registry-addon.test.ts', + 'src/shared/windows-security-addon.win32.test.ts', + 'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts', + 'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts', + 'src/shared/__tests__/process-host/windows-command-line.win32.test.ts', + 'src/shared/__tests__/process-host/windows-cmd-shim-resolution.test.ts', + 'src/shared/__tests__/process-host/windows-cmd-shim-resolution.win32.test.ts', + 'src/main/agent-hooks/windows-hook-payload-delivery.test.ts', + 'src/main/agent-hooks/managed-hook-stdin-lifecycle.test.ts', + 'src/main/jcode/hook-gate-script.test.ts', + 'src/main/agent-hooks/windows-direct-cmd-hook-command.test.ts', + 'src/main/agent-hooks/windows-native-hook-exe-hosts.test.ts', + 'src/main/claude/windows-native-hook-registration.test.ts', + 'src/main/agent-hooks/windows-cmd-hook-command-unicode.test.ts', + 'src/main/agent-hooks/windows-batch-hook-launcher.test.ts', + 'src/main/agent-hooks/windows-powershell-hook-launcher.test.ts', + 'src/main/claude/claude-status-line-windows.test.ts', + 'src/main/codex/windows-hook-command.test.ts', + 'src/main/codex/windows-hook-upgrade.test.ts', + 'src/main/codex/hook-service-managed-install.test.ts', + 'src/main/windows/windows-pty-job.win32.test.ts', + 'src/main/windows/windows-msys-job.win32.test.ts', + 'src/main/providers/agent-foreground-process-git-bash.win32.test.ts', + 'src/main/windows/windows-host-job.win32.test.ts', + 'src/main/windows/windows-process-tree-command-line-patch.test.ts', + 'src/main/windows/windows-process-table-native-addon.win32.test.ts', + 'src/main/daemon/daemon-pid-identity-native.win32.test.ts', + 'src/main/windows/windows-administrator-status.win32.test.ts', + 'src/main/windows/windows-administrator-restart.win32.test.ts', + 'src/main/persistence/profile-state/profile-state-access-windows-native.win32.test.ts', + 'src/main/windows-live-tree-kill.win32.test.ts', + 'src/main/wsl/wsl-runner.test.ts', + 'src/main/wsl/wsl-guest-environment.test.ts', + 'src/main/wsl/wsl-executable-path.win32.test.ts', + 'src/main/wsl/wsl-w1-w3-contract.test.ts', + 'src/shared/source-scan/source-tree-scan.test.ts', + 'src/main/cli/wsl-cli-powershell-boundary.test.ts', + 'src/main/computer/desktop-script-runtime-host.win32.test.ts', + 'src/main/cursor/hook-service.test.ts', + 'src/main/orca-profiles/profile-index-store.test.ts', + 'src/main/startup/windows-install-dir-acl-repair.win32.test.ts', + 'src/main/runtime/repo-worktree-admin-fingerprint.test.ts', + 'src/main/runtime/worktree-scan-admin-fingerprint-gate.test.ts', + 'src/shared/secure-file-fsync-flags.test.ts', + 'src/shared/secure-path-windows-acl.win32.test.ts', + 'src/main/runtime/unreadable-secret-store-preservation.win32.test.ts', + 'src/main/ipc/pty-codex-account-attribution.test.ts', + 'src/main/ipc/pty-spawn-env-codex-resume-provenance.test.ts', + 'src/main/ipc/preflight-provider-command-selection.test.ts', + 'src/main/ipc/preflight-runnable-local-cli.test.ts', + 'src/main/ssh/remote-node-runtime-store-windows.test.ts' +] diff --git a/config/scripts/process-host-build-dist.test.mjs b/config/scripts/process-host-build-dist.test.mjs index c79b912606d..b4b560b37d5 100644 --- a/config/scripts/process-host-build-dist.test.mjs +++ b/config/scripts/process-host-build-dist.test.mjs @@ -82,6 +82,26 @@ function runBuild(packageDir, { script = buildScript, cwd = realPackageDir } = { // Emits replacement output, then takes over or deletes the build lock as a competing build would. function installLockStealingCompiler(packageDir, { recreateLock }) { + const compromised = join(packageDir, 'lock-compromised') + const script = join(packageDir, 'observe-lock-loss.mjs') + writeFileSync( + script, + `import { writeFileSync } from 'node:fs' +import lockfile from ${JSON.stringify(pathToFileURL(packageRequire.resolve('proper-lockfile')).href)} +const lock = lockfile.lock +lockfile.lock = (file, options) => lock(file, { + ...options, + update: 1000, + onCompromised(error) { + options.onCompromised(error) + writeFileSync(${JSON.stringify(compromised)}, '') + } +}) +const { buildPackageDist } = await import(${JSON.stringify(pathToFileURL(buildScript).href)}) +try { await buildPackageDist(process.argv[2]) } +catch (error) { console.error(error.message); process.exitCode = 1 } +` + ) const compilerDir = join(packageDir, 'node_modules', 'typescript') rmSync(compilerDir, { recursive: true }) mkdirSync(join(compilerDir, 'bin'), { recursive: true }) @@ -91,7 +111,7 @@ function installLockStealingCompiler(packageDir, { recreateLock }) { ) writeFileSync( join(compilerDir, 'bin', 'tsc'), - `const { mkdirSync, rmSync, utimesSync, writeFileSync } = require('node:fs') + `const { existsSync, mkdirSync, rmSync, utimesSync, writeFileSync } = require('node:fs') const { join } = require('node:path') const outDir = process.argv[process.argv.indexOf('--outDir') + 1] mkdirSync(outDir, { recursive: true }) @@ -99,10 +119,14 @@ writeFileSync(join(outDir, 'entry.js'), 'module.exports = "from a build that los writeFileSync(join(outDir, 'added.js'), '') rmSync('.dist-build.lock', { recursive: true, force: true }) ${recreateLock ? "mkdirSync('.dist-build.lock')\nconst foreignMtime = new Date(Date.now() + 60_000)\nutimesSync('.dist-build.lock', foreignMtime, foreignMtime)" : ''} -// Hold compilation past proper-lockfile's 15-second refresh so compromise is observed. -setTimeout(() => {}, 17_000) +// Wait for the real lock monitor's verdict before compilation completes. +const wait = setInterval(() => { + if (existsSync(${JSON.stringify(compromised)})) clearInterval(wait) +}, 10) +setTimeout(() => process.exit(2), 20_000).unref() ` ) + return script } async function build(packageDir, expectResult = expect) { @@ -302,9 +326,9 @@ while (!existsSync(${JSON.stringify(finished)}) || reads === 0) { const packageDir = fixture(onTestFinished, seededDist) const before = distSnapshot(packageDir) const entry = readFileSync(join(packageDir, 'dist', 'entry.js'), 'utf8') - installLockStealingCompiler(packageDir, { recreateLock }) + const script = installLockStealingCompiler(packageDir, { recreateLock }) - const result = await runBuild(packageDir) + const result = await runBuild(packageDir, { script }) expectResult(result.code).toBe(1) expectResult(result.stderr).toContain('build lock was compromised') diff --git a/config/scripts/reclaim-electron-dists.mjs b/config/scripts/reclaim-electron-dists.mjs index 03e7b539d54..b4116c7441b 100644 --- a/config/scripts/reclaim-electron-dists.mjs +++ b/config/scripts/reclaim-electron-dists.mjs @@ -11,6 +11,7 @@ import { existsSync, readFileSync, readdirSync, + realpathSync, renameSync, rmSync, statSync, @@ -32,6 +33,24 @@ const repoRoot = process.argv.includes('--repo') ? path.resolve(process.argv[process.argv.indexOf('--repo') + 1]) : process.cwd() +const excludeIndex = process.argv.indexOf('--exclude-worktree') +const excludeArgument = excludeIndex === -1 ? undefined : process.argv[excludeIndex + 1] +if (excludeIndex !== -1 && (!excludeArgument || excludeArgument.startsWith('-'))) { + throw new Error('--exclude-worktree requires a directory') +} +const excludeWorktree = excludeArgument ? realpathSync(excludeArgument) : null + +function isExcluded(worktree) { + if (!excludeWorktree) { + return false + } + try { + return realpathSync(worktree) === excludeWorktree + } catch { + return path.resolve(worktree) === excludeWorktree + } +} + function listWorktrees(root) { const raw = execFileSync('git', ['-C', root, 'worktree', 'list', '--porcelain'], { encoding: 'utf8' @@ -88,6 +107,9 @@ function main() { let skipped = 0 for (const worktree of listWorktrees(repoRoot)) { + if (isExcluded(worktree)) { + continue + } const electronPackageDir = path.join(worktree, 'node_modules', 'electron') const distPath = path.join(electronPackageDir, 'dist') if (!existsSync(path.join(electronPackageDir, 'package.json')) || !existsSync(distPath)) { diff --git a/config/scripts/reclaim-electron-dists.test.ts b/config/scripts/reclaim-electron-dists.test.ts index b1b3e08e094..fbac962ca5e 100644 --- a/config/scripts/reclaim-electron-dists.test.ts +++ b/config/scripts/reclaim-electron-dists.test.ts @@ -1,4 +1,4 @@ -import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { chmod, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { afterEach, expect, it } from 'vitest' @@ -29,14 +29,15 @@ async function fixture() { await writeFile(join(pkg, 'package.json'), JSON.stringify({ version: '99.1.0' })) await writeFile(join(pkg, 'dist', 'version'), '99.1.0\n') await writeFile(executable, 'fake electron executable\n') - const run = (apply = false) => + const run = (apply = false, flags: string[] = []) => runProcess({ program: process.env.ORCA_TEST_NODE_EXECUTABLE ?? process.execPath, args: [ resolve(import.meta.dirname, 'reclaim-electron-dists.mjs'), '--repo', root, - ...(apply ? ['--apply'] : []) + ...(apply ? ['--apply'] : []), + ...flags ], cwd: root, env: { ...process.env, CI: '', ORCA_BACKGROUND_LAUNCH: '1' }, @@ -84,3 +85,31 @@ it('shares a writable install and preserves its executable bytes', async () => { const repeated = await run() expect(repeated.stdout).toContain('Would share 0 worktree(s)') }) + +it('protects the launching worktree, including when selected through a filesystem alias', async () => { + const { root, pkg, executable, run } = await fixture() + const alias = join(root, 'alias') + await symlink(root, alias, process.platform === 'win32' ? 'junction' : 'dir') + for (const exclusion of [root, alias]) { + for (const apply of [false, true]) { + const result = await run(apply, ['--exclude-worktree', exclusion]) + expect(result.code, result.stderr).toBe(0) + expect(result.stdout).toContain('0 worktree(s)') + expect(await readFile(executable, 'utf8')).toBe('fake electron executable\n') + await expect(readFile(join(pkg, '.orca-shared-dist'))).rejects.toMatchObject({ + code: 'ENOENT' + }) + } + } +}) + +it.each([['--exclude-worktree'], ['--exclude-worktree', '--apply']])( + 'rejects invalid exclusions before changing an install: %j', + async (...flags) => { + const { pkg, run } = await fixture() + const result = await run(true, flags) + expect(result.code).not.toBe(0) + expect(result.stderr).toContain('--exclude-worktree requires a directory') + await expect(readFile(join(pkg, '.orca-shared-dist'))).rejects.toMatchObject({ code: 'ENOENT' }) + } +) diff --git a/config/scripts/reclaim-release-test-caches.mjs b/config/scripts/reclaim-release-test-caches.mjs index 55f90a1b149..b064ba453ff 100644 --- a/config/scripts/reclaim-release-test-caches.mjs +++ b/config/scripts/reclaim-release-test-caches.mjs @@ -278,7 +278,7 @@ if (process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename const argument = process.argv[index] if (argument === '--help') { console.log( - 'Usage: pnpm run reclaim:worktree-space [--repo DIRECTORY] [--release-cache-min-age-days 30] [--apply --tests-stopped]' + 'Usage: pnpm reclaim-space [--repo DIRECTORY] [--release-cache-min-age-days 30] [--apply --tests-stopped]' ) process.exit(0) } diff --git a/config/scripts/reclaim-worktree-command.mjs b/config/scripts/reclaim-worktree-command.mjs new file mode 100644 index 00000000000..b4bac4116fa --- /dev/null +++ b/config/scripts/reclaim-worktree-command.mjs @@ -0,0 +1,83 @@ +import { createInterface } from 'node:readline/promises' +import { resolve } from 'node:path' +import { reclaimWorktreeSpace } from './reclaim-worktree-space.mjs' + +export function parseReclaimArguments(args) { + const options = { apply: false, automatic: false, testsStopped: false } + for (let index = 0; index < args.length; index += 1) { + const argument = args[index] + if (argument === '--apply') { + options.apply = true + } else if (argument === '--automatic') { + options.automatic = true + } else if (argument === '--tests-stopped') { + options.testsStopped = true + } else if (argument === '--help' || argument === '-h') { + options.help = true + } else if ( + argument === '--repo' || + argument === '--release-cache-min-age-days' || + argument === '--exclude-worktree' + ) { + const value = args[++index] + if (!value || value.startsWith('-')) { + throw new Error(`${argument} requires a value`) + } + if (argument === '--repo') { + options.repoRoot = resolve(value) + } else if (argument === '--exclude-worktree') { + options.excludeWorktree = resolve(value) + } else { + options.releaseCacheMinAgeDays = Number(value) + if ( + !Number.isFinite(options.releaseCacheMinAgeDays) || + options.releaseCacheMinAgeDays < 0 + ) { + throw new Error('--release-cache-min-age-days requires a non-negative number') + } + } + } else { + throw new Error(`Unknown argument: ${argument}`) + } + } + return options +} + +export async function runReclaimCommand( + args, + { input = process.stdin, output = process.stdout, reclaim = reclaimWorktreeSpace } = {} +) { + const options = parseReclaimArguments(args) + if (options.help) { + output.write( + 'Usage: pnpm reclaim-space [--apply] [--repo DIRECTORY] [--tests-stopped] [--release-cache-min-age-days 30]\n' + ) + output.write( + 'Shares Electron installs and removes idle dev bundles across local Git worktrees.\n' + ) + output.write('Without --apply, previews cleanup and offers [y/N] in an interactive terminal.\n') + output.write( + 'Add --tests-stopped to include old test caches after stopping cross-version tests.\n' + ) + return + } + if (!options.testsStopped && !options.automatic) { + output.write( + 'Historical test caches retained. Add --tests-stopped after stopping cross-version tests to include them.\n' + ) + } + await reclaim(options) + if (options.apply || options.automatic || !input.isTTY || !output.isTTY) { + return + } + + const prompt = createInterface({ input, output }) + try { + const answer = await prompt.question('Apply this cleanup? [y/N] ') + if (/^y(?:es)?$/i.test(answer.trim())) { + await reclaim({ ...options, apply: true }) + } + } finally { + prompt.close() + } +} diff --git a/config/scripts/reclaim-worktree-command.test.mjs b/config/scripts/reclaim-worktree-command.test.mjs new file mode 100644 index 00000000000..6b3f05eded8 --- /dev/null +++ b/config/scripts/reclaim-worktree-command.test.mjs @@ -0,0 +1,87 @@ +import { PassThrough } from 'node:stream' +import { expect, it, vi } from 'vitest' +import { parseReclaimArguments, runReclaimCommand } from './reclaim-worktree-command.mjs' + +function terminal(answer, tty = true) { + const input = new PassThrough() + const output = new PassThrough() + input.isTTY = tty + output.isTTY = tty + let text = '' + output.on('data', (chunk) => { + text += chunk.toString() + if (chunk.toString().includes('[y/N]')) { + input.write(`${answer}\n`) + } + }) + return { input, output, text: () => text } +} + +it.each(['Y', 'y', 'yes', ' YES '])( + 'applies after preview when the user answers %j', + async (answer) => { + const streams = terminal(answer) + const reclaim = vi.fn().mockResolvedValue({ completed: true }) + await runReclaimCommand(['--tests-stopped', '--release-cache-min-age-days', '7'], { + ...streams, + reclaim + }) + expect(reclaim.mock.calls.map(([options]) => options.apply)).toEqual([false, true]) + expect(reclaim.mock.calls[1][0]).toMatchObject({ + testsStopped: true, + releaseCacheMinAgeDays: 7 + }) + } +) + +it.each(['', 'n', 'no', 'sure'])('leaves files alone after preview on %j', async (answer) => { + const streams = terminal(answer) + const reclaim = vi.fn().mockResolvedValue({ completed: true }) + await runReclaimCommand([], { ...streams, reclaim }) + expect(reclaim).toHaveBeenCalledTimes(1) + expect(reclaim.mock.calls[0][0].apply).toBe(false) +}) + +it.each(['--apply', '--automatic'])('never prompts with %s', async (flag) => { + const streams = terminal('Y') + const reclaim = vi.fn().mockResolvedValue({ completed: true }) + await runReclaimCommand([flag], { ...streams, reclaim }) + expect(reclaim).toHaveBeenCalledTimes(1) + expect(streams.text()).not.toContain('[y/N]') +}) + +it('does not interpret piped input as approval', async () => { + const streams = terminal('Y', false) + streams.input.write('Y\n') + const reclaim = vi.fn().mockResolvedValue({ completed: true }) + await runReclaimCommand([], { ...streams, reclaim }) + expect(reclaim).toHaveBeenCalledTimes(1) + expect(streams.text()).not.toContain('[y/N]') +}) + +it('does not offer to apply a failed preview', async () => { + const streams = terminal('Y') + const reclaim = vi.fn().mockRejectedValue(new Error('preview failed')) + await expect(runReclaimCommand([], { ...streams, reclaim })).rejects.toThrow('preview failed') + expect(streams.text()).not.toContain('[y/N]') +}) + +it.each([ + ['--repo'], + ['--repo', '--apply'], + ['--exclude-worktree'], + ['--release-cache-min-age-days'], + ['--release-cache-min-age-days', 'NaN'], + ['--release-cache-min-age-days', '-1'], + ['--unknown'] +])('rejects invalid flags before any sweep: %j', (...args) => { + expect(() => parseReclaimArguments(args)).toThrow() +}) + +it('shows the single command without starting cleanup for help', async () => { + const streams = terminal('Y') + const reclaim = vi.fn() + await runReclaimCommand(['--help'], { ...streams, reclaim }) + expect(streams.text()).toContain('pnpm reclaim-space') + expect(reclaim).not.toHaveBeenCalled() +}) diff --git a/config/scripts/reclaim-worktree-space.mjs b/config/scripts/reclaim-worktree-space.mjs index e99b791845a..b9e8e7ea72a 100644 --- a/config/scripts/reclaim-worktree-space.mjs +++ b/config/scripts/reclaim-worktree-space.mjs @@ -12,11 +12,9 @@ export async function reclaimWorktreeSpace({ acquireLock = lock, commonDirectory, testsStopped = false, + excludeWorktree, releaseCacheMinAgeDays = 30 } = {}) { - if (apply && !automatic && !testsStopped) { - throw new Error('Stop cross-version tests, then pass --tests-stopped with --apply') - } if (!Number.isFinite(releaseCacheMinAgeDays) || releaseCacheMinAgeDays < 0) { throw new Error('--release-cache-min-age-days requires a non-negative number') } @@ -46,7 +44,7 @@ export async function reclaimWorktreeSpace({ let cleanupError try { const scripts = ['reclaim-electron-dists.mjs', 'reclaim-dev-electron-bundles.mjs'] - if (!automatic) { + if (!automatic && testsStopped) { scripts.unshift('reclaim-release-test-caches.mjs') } for (const script of scripts) { @@ -54,6 +52,9 @@ export async function reclaimWorktreeSpace({ if (apply) { args.push('--apply') } + if (excludeWorktree && script !== 'reclaim-release-test-caches.mjs') { + args.push('--exclude-worktree', excludeWorktree) + } if (script === 'reclaim-release-test-caches.mjs') { args.unshift('--import', import.meta.resolve('tsx')) args.push('--min-age-days', String(releaseCacheMinAgeDays)) @@ -94,44 +95,10 @@ export async function reclaimWorktreeSpace({ } if (process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename)) { - if (process.argv.includes('--help')) { - console.log( - 'Usage: pnpm run reclaim:worktree-space [--repo DIRECTORY] [--release-cache-min-age-days 30] [--apply --tests-stopped]' - ) - console.log( - 'Previews historical test cache cleanup, Electron sharing, and idle dev bundle removal.' - ) - process.exit(0) - } - const repoIndex = process.argv.indexOf('--repo') - const ageIndex = process.argv.indexOf('--release-cache-min-age-days') - for (let index = 2; index < process.argv.length; index += 1) { - const argument = process.argv[index] - if (argument === '--repo' || argument === '--release-cache-min-age-days') { - index += 1 - } else if (argument !== '--apply' && argument !== '--tests-stopped') { - throw new Error(`Unknown argument: ${argument}`) - } - } - if ( - repoIndex !== -1 && - (!process.argv[repoIndex + 1] || process.argv[repoIndex + 1].startsWith('--')) - ) { - throw new Error('--repo requires a directory') - } - if ( - ageIndex !== -1 && - (!process.argv[ageIndex + 1] || process.argv[ageIndex + 1].startsWith('--')) - ) { - throw new Error('--release-cache-min-age-days requires a number') - } - await reclaimWorktreeSpace({ - repoRoot: repoIndex !== -1 ? resolve(process.argv[repoIndex + 1]) : process.cwd(), - apply: process.argv.includes('--apply'), - testsStopped: process.argv.includes('--tests-stopped'), - releaseCacheMinAgeDays: ageIndex === -1 ? 30 : Number(process.argv[ageIndex + 1]) - }).catch((error) => { - console.error(error.message) - process.exitCode = 1 - }) + import('./reclaim-worktree-command.mjs') + .then(({ runReclaimCommand }) => runReclaimCommand(process.argv.slice(2))) + .catch((error) => { + console.error(error.message) + process.exitCode = 1 + }) } diff --git a/config/scripts/reclaim-worktree-space.test.mjs b/config/scripts/reclaim-worktree-space.test.mjs index 0b5967a0b41..37babcf3750 100644 --- a/config/scripts/reclaim-worktree-space.test.mjs +++ b/config/scripts/reclaim-worktree-space.test.mjs @@ -19,7 +19,7 @@ async function fixture() { } it('previews historical test caches and both Electron cleanup operations', async () => { - const options = await fixture() + const options = { ...(await fixture()), testsStopped: true } await reclaimWorktreeSpace(options) expect(options.execute).toHaveBeenCalledTimes(3) expect(options.execute.mock.calls.flatMap(([spec]) => spec.args)).not.toContain('--apply') @@ -42,22 +42,36 @@ it('forwards apply and repository selection to all cleanup operations', async () } }) -it('rejects manual apply before any operation unless cross-version tests are stopped', async () => { +it('applies Electron cleanup while retaining unacknowledged test caches', async () => { const options = { ...(await fixture()), apply: true } - await expect(reclaimWorktreeSpace(options)).rejects.toThrow('--tests-stopped') - expect(options.execute).not.toHaveBeenCalled() -}) - -it('leaves historical caches out of automatic invocations', async () => { - const options = { ...(await fixture()), automatic: true, apply: true } await reclaimWorktreeSpace(options) + expect(options.execute).toHaveBeenCalledTimes(2) expect(options.execute.mock.calls.flatMap(([spec]) => spec.args).join(' ')).not.toContain( 'reclaim-release-test-caches' ) }) +it('leaves historical caches out of automatic invocations', async () => { + const options = { + ...(await fixture()), + automatic: true, + apply: true, + testsStopped: true, + excludeWorktree: '/dev-worktree' + } + await reclaimWorktreeSpace(options) + expect(options.execute.mock.calls.flatMap(([spec]) => spec.args).join(' ')).not.toContain( + 'reclaim-release-test-caches' + ) + for (const [spec] of options.execute.mock.calls) { + expect(spec.args).toEqual( + expect.arrayContaining(['--exclude-worktree', options.excludeWorktree]) + ) + } +}) + it('forwards an age override only to historical test cache cleanup', async () => { - const options = { ...(await fixture()), releaseCacheMinAgeDays: 7 } + const options = { ...(await fixture()), releaseCacheMinAgeDays: 7, testsStopped: true } await reclaimWorktreeSpace(options) expect(options.execute.mock.calls[0][0].args).toEqual( expect.arrayContaining(['--min-age-days', '7']) @@ -73,7 +87,7 @@ it('rejects an invalid cache age before starting cleanup', async () => { expect(options.execute).not.toHaveBeenCalled() }) -it('runs the combined CLI from outside a disposable repository and requires acknowledgement', async () => { +it('runs the CLI outside the repository and retains historical caches until acknowledged', async () => { const options = await fixture() const init = await runProcess({ program: 'git', @@ -110,13 +124,13 @@ it('runs the combined CLI from outside a disposable repository and requires ackn timeoutMs: 20_000, terminationBarrier: true }) - const preview = await run() + const preview = await run(['--tests-stopped']) expect(preview.code, preview.stderr).toBe(0) expect(preview.stdout).toContain('Would remove 1 cache(s)') expect(await readFile(marker, 'utf8')).toContain(commit) - const blocked = await run(['--apply']) - expect(blocked.code).not.toBe(0) - expect(blocked.stderr).toContain('--tests-stopped') + const safe = await run(['--apply']) + expect(safe.code, safe.stderr).toBe(0) + expect(safe.stdout).toContain('Historical test caches retained') expect(await readFile(marker, 'utf8')).toContain(commit) const applied = await run(['--apply', '--tests-stopped']) expect(applied.code, applied.stderr).toBe(0) @@ -168,7 +182,7 @@ it('preserves the cleanup failure and reports a concurrent lock release failure }) it('fails if cleanup succeeds but the repository lock cannot be released', async () => { - const options = await fixture() + const options = { ...(await fixture()), testsStopped: true } const failure = new Error('lock release failure') const release = vi.fn().mockRejectedValue(failure) await expect(reclaimWorktreeSpace({ ...options, acquireLock: async () => release })).rejects.toBe( diff --git a/config/scripts/release-cut-token-permissions.test.mjs b/config/scripts/release-cut-token-permissions.test.mjs index a7dc7b8f160..e1b3292d117 100644 --- a/config/scripts/release-cut-token-permissions.test.mjs +++ b/config/scripts/release-cut-token-permissions.test.mjs @@ -14,7 +14,6 @@ const EXPECTED_MATRIX = { '.github/workflows/e2e.yml#orcad-serve-mode-switch': { contents: 'read' }, '.github/workflows/e2e.yml#orcad-serve-mode-switch-windows': { contents: 'read' }, '.github/workflows/e2e.yml#orcad-ssh-docker': { contents: 'read' }, - '.github/workflows/e2e.yml#prepare-native-cache': { contents: 'read' }, '.github/workflows/e2e.yml#ssh-browser-network-route': { contents: 'read' }, '.github/workflows/homebrew-bump.yml#bump-cask': { contents: 'read' }, '.github/workflows/node-server-tests.yml#changes': { contents: 'read' }, diff --git a/config/scripts/run-electron-vite-dev.mjs b/config/scripts/run-electron-vite-dev.mjs index c520083cb6a..66565ab0186 100644 --- a/config/scripts/run-electron-vite-dev.mjs +++ b/config/scripts/run-electron-vite-dev.mjs @@ -23,6 +23,7 @@ import { isDevBundleInUse, selectStaleDevBundleDirs } from './dev-electron-bundle-cache.mjs' +import { startDevStorageReclaim } from './dev-storage-reclaim.mjs' import { copyPrivateTree } from './space-sharing-copy.mjs' import { DEV_BUNDLE_ID, @@ -656,6 +657,12 @@ const child = spawn(process.execPath, [electronViteCli, ...forwardedArgs], { detached: process.platform !== 'win32' }) +child.once('spawn', () => { + if (!isHelpOrVersion) { + startDevStorageReclaim({ repoRoot }) + } +}) + let isShuttingDown = false let forcedKillTimer = null diff --git a/config/scripts/shebang-script-line-ending-pin.test.mjs b/config/scripts/shebang-script-line-ending-pin.test.mjs deleted file mode 100644 index 075eea6456d..00000000000 --- a/config/scripts/shebang-script-line-ending-pin.test.mjs +++ /dev/null @@ -1,70 +0,0 @@ -import { execFileSync } from 'node:child_process' -import { existsSync, readFileSync } from 'node:fs' -import { join, resolve } from 'node:path' -import { describe, expect, it } from 'vitest' - -/** - * Guard the `.gitattributes` pin that keeps `config/scripts` scripts on LF. - * - * `core.autocrlf=true` ships in the Git-for-Windows system config, so without a - * pin a Windows checkout gets CRLF. Vite's SSR transform locates the shebang - * with `/^#!.*\n/` — `\r` is a JS regex line terminator, so `.` never matches it - * and the pattern misses on CRLF. The hoisted import/export preamble then lands - * at offset 0, ahead of the shebang, which in turn defeats the `code[0] === '#'` - * guard that blanks it. A literal `#!` survives into the middle of the module and - * every suite importing the script dies at load with a SyntaxError. - * - * Scoped to `config/scripts` because that is where tests import scripts. Other - * shebanged `.mjs` in the tree are spawned, not imported, so they cannot hit this. - */ -const projectDir = resolve(import.meta.dirname, '../..') -const SCRIPT_DIRECTORY = 'config/scripts' - -function git(args) { - return execFileSync('git', args, { cwd: projectDir, encoding: 'utf8' }) -} - -/** `git check-attr -z` emits NUL-separated path/attr/value triples. */ -function eolAttributes(paths) { - const fields = git(['check-attr', '-z', 'eol', '--', ...paths]).split('\0') - const found = new Map() - for (let index = 0; index + 2 < fields.length; index += 3) { - found.set(fields[index], fields[index + 2]) - } - return found -} - -function shebangScripts() { - return git(['ls-files', '-z', '--', `${SCRIPT_DIRECTORY}/*.mjs`]) - .split('\0') - .filter((path) => path && existsSync(join(projectDir, path))) - .filter((path) => readFileSync(join(projectDir, path), 'utf8').startsWith('#!')) -} - -describe('config/scripts line-ending pin', () => { - it('pins every shebanged script to LF', () => { - const scripts = shebangScripts() - expect(scripts.length).toBeGreaterThan(0) - - const attributes = eolAttributes(scripts) - const unpinned = scripts.filter((path) => attributes.get(path) !== 'lf') - - expect( - unpinned, - 'A shebanged script left on the platform default gets CRLF on Windows, ' + - 'which makes every suite importing it fail to load. Pin it in .gitattributes.' - ).toEqual([]) - }) - - // Why: without these the assertion above still passes against a pattern so broad - // it says nothing, or so narrow it only covers the files that exist today. - it.each([ - ['config/scripts/example.mjs', 'lf'], - ['config/scripts/nested/deeper/example.mjs', 'lf'], - ['config/scripts-extra/example.mjs', 'unspecified'], - ['vendor/config/scripts/example.mjs', 'unspecified'], - ['config/scripts/example.mjsx', 'unspecified'] - ])('resolves %s to eol=%s', (path, expected) => { - expect(eolAttributes([path]).get(path)).toBe(expected) - }) -}) diff --git a/config/scripts/vitest-dom-setup.ts b/config/scripts/vitest-dom-setup.ts new file mode 100644 index 00000000000..738a876a21a --- /dev/null +++ b/config/scripts/vitest-dom-setup.ts @@ -0,0 +1,7 @@ +export {} + +// DOM compatibility patches are only needed after Vitest installs a DOM environment. +if (globalThis.document !== undefined) { + await import('./happy-dom-offscreen-canvas') + await import('./happy-dom-mutation-observer-retention') +} diff --git a/config/scripts/vitest-host-ports-setup.ts b/config/scripts/vitest-host-ports-setup.ts index 980775ec3f1..ab17cac58b4 100644 --- a/config/scripts/vitest-host-ports-setup.ts +++ b/config/scripts/vitest-host-ports-setup.ts @@ -15,14 +15,17 @@ const { mkdtempSync, rmSync } = process.getBuiltinModule('fs') * that asserts on host behaviour installs its own and wins, because this runs first. */ -// One directory per test environment, not per test: suites that write a file in one -// step and read it back in the next would otherwise lose it between them. Removed on -// teardown — vitest builds one environment per test file, so without this a full run -// leaves thousands of directories behind. -const userData = mkdtempSync(join(tmpdir(), 'orca-vitest-userdata-')) +// Most suites never read a host data path; create its per-file directory only when needed. +let userData: string | undefined + +function userDataPath(): string { + return (userData ??= mkdtempSync(join(tmpdir(), 'orca-vitest-userdata-'))) +} afterAll(() => { - rmSync(userData, { recursive: true, force: true }) + if (userData) { + rmSync(userData, { recursive: true, force: true }) + } }) // Deliberately not a plaintext passthrough: encryptString must return something a @@ -37,7 +40,7 @@ const SEAL_PREFIX = 'vitest-sealed:' */ export function fakeAppEnvironment(overrides: Partial = {}): AppEnvironment { return { - getPath: (name) => (name === 'home' ? homedir() : name === 'temp' ? tmpdir() : userData), + getPath: (name) => (name === 'home' ? homedir() : name === 'temp' ? tmpdir() : userDataPath()), getAppPath: () => process.cwd(), getVersion: () => '0.0.0-test', isPackaged: () => false, diff --git a/config/scripts/vitest-sqlite-database-runtime-files.mjs b/config/scripts/vitest-sqlite-database-runtime-files.mjs index 43701691d05..fcb249648e2 100644 --- a/config/scripts/vitest-sqlite-database-runtime-files.mjs +++ b/config/scripts/vitest-sqlite-database-runtime-files.mjs @@ -1,7 +1,10 @@ // Database opening and lifecycle fixtures exercise Node SQLite behavior. export const SQLITE_DATABASE_RUNTIME_INCLUDE = [ + 'src/main/ai-vault-search/session-search-dependent-transcripts.test.ts', 'src/main/ai-vault-search/session-search-file-write.test.ts', 'src/main/ai-vault-search/session-search-live-transcript.test.ts', + 'src/main/ai-vault-search/session-search-list-reparse.test.ts', + 'src/main/ai-vault-search/session-search-transcript-dependencies.test.ts', 'src/main/ai-vault/session-scanner-devin-contention.test.ts', 'src/main/ai-vault/session-scanner-opencode-native-worker.test.ts', 'src/main/ai-vault/session-scanner-opencode-sqlite-bounds.test.ts', diff --git a/config/scripts/vitest-sqlite-runtime-files.mjs b/config/scripts/vitest-sqlite-runtime-files.mjs index 50052f4039e..f293e0bac42 100644 --- a/config/scripts/vitest-sqlite-runtime-files.mjs +++ b/config/scripts/vitest-sqlite-runtime-files.mjs @@ -3,6 +3,12 @@ import { SQLITE_DATABASE_RUNTIME_INCLUDE } from './vitest-sqlite-database-runtim // Real SQLite fixtures keep Node publication and close semantics. export const SQLITE_RUNTIME_INCLUDE = [ ...SQLITE_DATABASE_RUNTIME_INCLUDE, + 'src/main/ipc/repos/project-host-setup-wsl-move-fence.test.ts', + 'src/main/ipc/repos/repo-ipc-wsl-move-fence.test.ts', + 'src/main/runtime/runtime-managed-worktree-metadata-wsl-move.test.ts', + 'src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.test.ts', + 'src/main/wsl-move-scan-pruning.test.ts', + 'src/main/repo-backfill-wsl-move.test.ts', 'src/main/acp/acp-structured-host-live-listing.test.ts', 'src/main/acp/acp-structured-host-configured-default.test.ts', 'src/main/pi/rpc-configured-default.test.ts', @@ -15,6 +21,7 @@ export const SQLITE_RUNTIME_INCLUDE = [ 'src/main/acp/acp-structured-prompt-failure.test.ts', 'src/main/acp/acp-timeline-turn-failures.test.ts', 'src/main/acp/acp-structured-session-adapter.test.ts', + 'src/main/acp/acp-conversation-copy.test.ts', 'src/main/pi/rpc-turn-races.test.ts', 'src/main/acp/acp-structured-child-session-end.test.ts', 'src/main/acp/acp-structured-child-stop.test.ts', @@ -28,6 +35,7 @@ export const SQLITE_RUNTIME_INCLUDE = [ 'src/main/automations/automation-skip-coalescing.test.ts', 'src/main/automations/automation-update-host-retarget.test.ts', 'src/main/automations/automation-workspace-host-attribution.test.ts', + 'src/main/automations/automation-wsl-move-refusal.test.ts', 'src/main/automations/automation-zero-grace-tick-latency.test.ts', 'src/main/automations/refused-manual-run.test.ts', 'src/main/automations/retained-run-reconciliation.test.ts', diff --git a/config/scripts/workflow-ref-mirror-case-safety.test.mjs b/config/scripts/workflow-ref-mirror-case-safety.test.mjs index a8333d76947..b9d9c752974 100644 --- a/config/scripts/workflow-ref-mirror-case-safety.test.mjs +++ b/config/scripts/workflow-ref-mirror-case-safety.test.mjs @@ -10,13 +10,6 @@ const projectDir = resolve(import.meta.dirname, '../..') const readWorkflow = (relativePath) => parse(readFileSync(join(projectDir, relativePath), 'utf8')) -// Every step that mirrors this repo's whole ref namespace onto a runner disk to -// prove a commit is reachable from a branch or tag before signing it. -const REF_MIRRORS = [ - ['.github/workflows/adhoc-mac-build.yml', 'build-adhoc-mac', 'Vet the requested ref'], - ['.github/workflows/dev-channel-win-build.yml', 'build-win', 'Vet the requested inputs'] -] - describe('ref-mirroring vet steps', () => { it.each(['daily', 'hourly', 'adhoc'])('%s builds only need the current commit', (channel) => { const job = readWorkflow(`.github/workflows/${channel}-mac-build.yml`).jobs[ @@ -98,30 +91,4 @@ describe('ref-mirroring vet steps', () => { rmSync(directory, { recursive: true, force: true }) } }) - - it('checks out only the vetted commit without remirroring refs', () => { - const steps = readWorkflow('.github/workflows/adhoc-mac-build.yml').jobs['build-adhoc-mac'] - .steps - const checkout = steps.find((step) => step.name === 'Checkout the requested ref') - expect(checkout.with.ref).toBe('${{ steps.vetted.outputs.sha }}') - expect(checkout.with['fetch-depth']).toBe(1) - expect(checkout.with['persist-credentials']).toBe(false) - }) - - // Why: macOS and Windows runner disks are case-insensitive, and this repo has - // branches that differ only in casing. The files backend cannot store both, and - // it fails the whole fetch rather than the one ref — so the vet step dies before - // any build runs. reftable keys refs in a table instead of file paths. - it.each(REF_MIRRORS)( - '%s creates its scratch repo with the reftable backend', - (path, job, step) => { - const run = readWorkflow(path).jobs[job].steps.find( - (candidate) => candidate.name === step - ).run - - expect(run).toContain('+refs/heads/*:refs/heads/*') - expect(run).toMatch(/git init\b[^\n]*--ref-format=reftable/) - expect(run).not.toMatch(/git init -q --bare "\$scratch"/) - } - ) }) diff --git a/config/scripts/workflow-ref-reachability.test.mjs b/config/scripts/workflow-ref-reachability.test.mjs index 81a8168f1d4..7c508e5b9f6 100644 --- a/config/scripts/workflow-ref-reachability.test.mjs +++ b/config/scripts/workflow-ref-reachability.test.mjs @@ -96,6 +96,8 @@ describe('release ref trust with case-twin names', () => { }) it('checks out the vetted SHA shallowly without mirroring case-twin refs again', async () => { + expect(macCheckout.with.ref).toBe('${{ steps.vetted.outputs.sha }}') + expect(macCheckout.with['persist-credentials']).toBe(false) const checkout = join(directory, 'checkout') const env = { ...identity, ...macCheckout.env } await git(['init', checkout], env) diff --git a/config/tsconfig.node.json b/config/tsconfig.node.json index 66d9b7fe541..fafa7267bc5 100644 --- a/config/tsconfig.node.json +++ b/config/tsconfig.node.json @@ -8,6 +8,9 @@ "./scripts/vitest-caller-identity-env-setup.ts", "./scripts/vitest-real-agent-home-write-guard.ts", "./scripts/vitest-bun-node-builtins.ts", + "./scripts/vitest-dom-setup.ts", + "./scripts/happy-dom-offscreen-canvas.ts", + "./scripts/happy-dom-mutation-observer-retention.ts", "./scripts/vitest-node-runtime-pool.ts", "../src/main/**/*", "../src/renderer/src/lib/skill-freshness-display-status.ts", diff --git a/config/update-translation.txt b/config/update-translation.txt index ecf51cfd151..d7f594320ff 100644 --- a/config/update-translation.txt +++ b/config/update-translation.txt @@ -200,3 +200,56 @@ Method (reproducible): blockers->阻碍者); 7 zh keys still use Latin "Agent"; zh ZCode TUI title/description left English; fr search keywords "indigène"/"procuration". +## Scan 2026-10-14 + +- HEAD scanned: 7244bf3e6f (origin/main). Base: e8cd6dcb184 — the previous scan's PR + (#28486) already merged upstream, so the branch was reset onto origin/main and this + window is e8cd6dcb184 .. HEAD. (No delta is lost: the branch's own commit 9c778181ad + was the source of e8cd6dcb184.) +- Gates before fix: catalog verify PASS, coverage --check PASS (13 allowlisted), extraction + PASS, runtime-catalog PASS. No unkeyed strings and en.json in sync; the gap was + target-locale coverage only. +- Delta since base: 154 new en keys, 33 removed, 2 changed en values. Missing per locale: + es 112, fr/ja/ko/zh 139. 14 of the 139 are the pinned English-fallback keys (11 + provider-credit-balance + 3 Chat UI), so the translatable delta is es 98 / others 125. + (es already carried the 27 `updates.installBlock.*` keys from a separate es PR.) +- Applied the 98/125 new keys x5 via apply-translation-delta.mjs (pipeline repaired ja + より新しい->より新規 and zh search keyword jira->Jira). Strictly additive: +598 keys. +- Changed-value review: the two `WorktreeJumpPalette` search placeholders gained "tasks"; + updated all five locales (ko/zh re-normalized to 워크스페이스/工作区 by the pipeline). +- Pre-existing (out-of-delta) fixes this pass, per the mandate to fix badly translated + strings: + - `auto.components.terminal.pane.zcode.missing.tui.{title,description}` were byte-English + in ALL five locales -> translated x5. + - fr settings search keywords: `procuration`->`proxy` (legal vs network sense), + `indigène`->`natif` x2. + - zh `components.workspace.cleanup.browse.*` bad MT repaired (~27 values: host->主机, + path->路径, ticket->工单/门票, blockers->阻塞项 [aligned with `chip.kind.blocker`], + blockerMode->阻塞项匹配, PR/先生->PR / MR, ahead/behind->领先/落后, review->评审, + leftover English "left" dropped, …). + - 6 zh keys moved Latin "Agent" -> 智能体 (Skills filter/detail, native-chat follow-ups, + aiVaultUnavailableForHost). + - zh `AccountsPane` credential-help cluster: leftover English "auth cookie"/"auth" and a + mangled `__Host-console_session` code token restored. +- Gates after fix: catalog verify PASS (all five 16062/16076; the 14 omissions are the + pinned keys), coverage --check PASS (13 allowlisted), extraction PASS, runtime-catalog + PASS, locale vitest suites PASS (33 files, 304 tests). +- Reviews (4 of max 4 rounds; round 0 = 5 parallel translators, then 5 reviewers/round): + R1 applied 51 (es 4: casing titles + ZCode; fr 6: nouvel elision + ZCode + 3 keywords; + ja 5: hostUpdateRequired 新しい->新規 pipeline artifact + ZCode + casing commas; ko 5: + 남은->남음, casing descriptions + ZCode; zh 31: browse MT + Agent + ZCode + + unresponsiveSummary). R2 applied 4 zh (browse sort.ahead/behind, chip.kind.review/ + reviewState). R3 applied 2 zh (preparingSummary "before restarting" fidelity, + blockerModeAny grammar); es/fr/ja/ko signed off clean. R4 applied 3 zh (AccountsPane + cluster); no other locale needed a 4th round. Post-review addressed 2 CodeRabbit + threads: ko palette "actions" -> 동작 (distinct from tasks 작업), and the zh DevTools + filter phrase clarified to `任意包含 remains 的请求` (the literal `remains` stays — it is + the request name users filter for, per `87f814af6f`). +- Remaining out-of-delta findings for a future pass (pre-existing, not from this window): + ja/zh/fr settings search-alias MT (ja トゥイ/アンシ, グーグル/クロム/リナックス/…; fr + worktree->arbre de travail, board->conseil, remote->télécommande; zh `网络`->任一 remains + mix) — the right fix is a batch addition to `SEARCH_KEYWORD_OVERRIDES`; fr + "un nouveau espace" (missing `nouvel`) in 5 pre-existing keys; ko + `newWorkspace.createDescription` 에이전트 repetition; zh `jiraColumnStoryPoints` 点数 + (story points) and `jira.transitions.chooseResolution` 解决方案 (Jira field is 解决结果). + diff --git a/config/vitest.config.ts b/config/vitest.config.ts index c4a1b27fbd5..1485f61565d 100644 --- a/config/vitest.config.ts +++ b/config/vitest.config.ts @@ -30,6 +30,9 @@ const testOptions = { environment: 'node', clearMocks: false, fsModuleCache: true, + ...(process.env.ORCA_UNIT_MODULE_CACHE_PATH + ? { fsModuleCachePath: process.env.ORCA_UNIT_MODULE_CACHE_PATH } + : {}), env: { ORCA_VITEST_RUNTIME: 'node' }, server: { deps: { inline: ['zod'] } }, // Node's storage globals and V8 retention checks require the existing child flags. @@ -37,8 +40,7 @@ const testOptions = { setupFiles: [ resolve('config/scripts/vitest-real-agent-home-write-guard.ts'), resolve('config/scripts/vitest-bun-node-builtins.ts'), - resolve('config/scripts/happy-dom-offscreen-canvas.ts'), - resolve('config/scripts/happy-dom-mutation-observer-retention.ts'), + resolve('config/scripts/vitest-dom-setup.ts'), resolve('config/scripts/vitest-host-ports-setup.ts'), resolve('config/scripts/vitest-caller-identity-env-setup.ts') ], diff --git a/docs/STYLEGUIDE.md b/docs/STYLEGUIDE.md index 733ef99402e..e236389c7d8 100644 --- a/docs/STYLEGUIDE.md +++ b/docs/STYLEGUIDE.md @@ -113,7 +113,7 @@ The chat root (`.native-chat-appearance`) redefines these steps as ratios of the - **Secondary text on the chat canvas** (transcript rows, notices, labels between messages) uses `text-chat-foreground-faint`. `text-muted-foreground` is for text on a lifted surface inside the chat: cards, menus, popovers and the docks above the composer. - **Row hover** in the transcript is `hover:bg-accent/20`. Buttons, menu items and card controls use `hover:bg-accent`. - **Focus rings:** full-width transcript rows use the inset ring (`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring`) so it isn't clipped by the scroll container; standalone controls use `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring`. Inside the chat `--ring` is `--chat-focus-ring`, which clears 3:1 on the canvas; don't lower its opacity. -- **State colours in the chat:** warnings use `text-chat-warning`; diff lines use `text-chat-diff-added` / `text-chat-diff-removed`. They are mixed toward the foreground so they pass text contrast in light and dark. Don't use raw palette colours (`text-yellow-500`) or the git decoration tokens for text on a tinted row. +- **State colours in the chat:** warnings use `text-chat-warning`; diff lines use `text-chat-diff-added` / `text-chat-diff-removed`; what the reader has not seen yet uses `text-chat-unread`. They are mixed toward the foreground so they pass text contrast in light and dark. Don't use raw palette colours (`text-yellow-500`) or the git decoration tokens for text on a tinted row. ## Radius diff --git a/docs/readme/release-source-sharing.md b/docs/readme/release-source-sharing.md index 4ccf3449ef4..f115d423ce1 100644 --- a/docs/readme/release-source-sharing.md +++ b/docs/readme/release-source-sharing.md @@ -44,17 +44,18 @@ Folder contexts without a Git repository cannot provide a historical release che ## Cleaning up older historical caches -Preview cleanup across this repository's registered local worktrees and shared source cache: +First stop cross-version tests in all this repository's worktrees, then preview historical-cache +cleanup across the registered local worktrees and shared source cache: ```sh -pnpm run reclaim:worktree-space +pnpm reclaim-space --tests-stopped ``` The default selects stamped release checkouts older than 30 days, including previous extraction -formats. To apply the preview, first stop cross-version tests in all this repository's worktrees: +formats. Answer `Y` after the preview to apply it, or run the same selection directly: ```sh -pnpm run reclaim:worktree-space --apply --tests-stopped +pnpm reclaim-space --apply --tests-stopped ``` Use `--release-cache-min-age-days 7` for a shorter retention window, or `--release-cache-min-age-days 0` to select all diff --git a/docs/readme/worktree-storage-maintenance.md b/docs/readme/worktree-storage-maintenance.md index 536a5b2a31d..636d6d65358 100644 --- a/docs/readme/worktree-storage-maintenance.md +++ b/docs/readme/worktree-storage-maintenance.md @@ -1,21 +1,27 @@ # Worktree storage maintenance -Run from the repository to preview maintenance across its locally registered Git worktrees: +Run one command from the repository to preview maintenance across its locally registered Git worktrees: ```sh -pnpm run reclaim:worktree-space +pnpm reclaim-space ``` -After stopping cross-version tests in all this repository's worktrees, apply cleanup with: +In an interactive terminal, the preview offers `Apply this cleanup? [y/N]`; enter `Y` to apply it. +Redirected input or output remains preview-only. To apply without a prompt, use `pnpm reclaim-space --apply`. + +Historical test caches are retained by default. After stopping cross-version tests in all this +repository's worktrees, preview the same selection with: ```sh -pnpm run reclaim:worktree-space --apply --tests-stopped +pnpm reclaim-space --tests-stopped ``` +Answer `Y` to apply that preview, or use `pnpm reclaim-space --apply --tests-stopped` to apply directly. + The command reuses the historical test cache, Electron distribution, and dev bundle cleanup scripts. It shares complete Electron distributions through the repository cache and removes idle -`out/electron-dev` bundles, which development rebuilds on demand. It also removes stamped historical -test checkouts and shared seeds older than 30 days; tests regenerate them when needed. It leaves source files, +`out/electron-dev` bundles, which development rebuilds on demand. With `--tests-stopped`, it also +removes stamped historical test checkouts and shared seeds older than 30 days; tests regenerate them when needed. It leaves source files, dependency installs, worktree registrations, and other build output alone. Reported sizes are logical file data; APFS clones and hardlinks mean actual disk savings can be smaller. @@ -23,7 +29,7 @@ Use `--release-cache-min-age-days 7` to shorten historical-cache retention, or ` recognised completed historical checkouts. This age option applies only to test caches. Local checkout age uses directory modification time, which ordinary reuse does not refresh. The `--tests-stopped` flag acknowledges stopped tests; older harnesses cannot prove a completed -checkout is unused. Missing acknowledgement rejects manual apply before any cleanup begins. +checkout is unused. Without that flag, historical caches are excluded from both preview and apply. Cache cleanup skips symlinks, malformed stamps, future formats, and publication/copy locks. See [release source sharing](./release-source-sharing.md) for the cache and locking details. @@ -39,8 +45,15 @@ without waiting for an Electron upgrade. The manual command migrates older insta Read-only Electron package directories are retained and reported as skipped. Cache publication or copying failures are also reported as skipped rather than silently omitted. -Historical test cache cleanup runs only in this explicit manual command. Automatic invocations -exclude it because tests may still be importing source. +`pnpm dev` and `pnpm dev-stable-name` start an automatic repository-wide Electron cleanup worker +in the background after the dev process starts. It never prompts or waits before launching the app. +The worktree being launched is excluded from both Electron sharing and dev bundle removal; active +sibling apps and recent unfinished builds remain protected by the existing bundle checks. +Background sweep output is suppressed; worker start or exit failures are reported without stopping dev. Set `ORCA_SKIP_DEV_STORAGE_RECLAIM=1` to opt out. +The worker finishes independently if the dev app exits, and each cleanup operation has a ten-minute limit. + +Historical test cache cleanup runs only in the explicit manual command with `--tests-stopped`. +Automatic invocations exclude it because tests may still be importing source. The repository lock prevents competing cleanup commands. Dev bundle removal checks active processes and recent unfinished builds, then rechecks eligibility immediately before deletion. An unreadable process table prevents removal. diff --git a/docs/site/content/docs/cli/automations.mdx b/docs/site/content/docs/cli/automations.mdx index 34f14fff180..5d4351cb020 100644 --- a/docs/site/content/docs/cli/automations.mdx +++ b/docs/site/content/docs/cli/automations.mdx @@ -131,7 +131,7 @@ orca automations show --json orca automations edit --enabled --json ``` -In the desktop automations list, use the search field to filter by **name**, **project**, or **prompt** text when you have many schedules. External automations that live on an SSH host still appear as jobs you can manage (including delete) when that host is disconnected — you do not need an active SSH session just to remove a schedule. +In the desktop automations list, use the search field to filter by **name**, **project**, or **prompt** text when you have many schedules. Use **Filters** to narrow the list by **Host**, **Enabled** or **Paused** state, last-run outcome (**Failed**, **Succeeded**, or **Never ran**), or one or more **Agents**. Search inside the agent submenu when the list is long; active filters appear as removable pills above the table. The table shows the host, last-run result, and relative time for each automation. Click the **Name** or **Last run** column to sort; name sorts alphabetically, while last run defaults to newest first. diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index 8b9a62c8391..ad7ee9ccee0 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -25,6 +25,7 @@ import { loadHostCatalog } from '../src/transport/host-store' import { extractPairingCodeFromUrl } from '../src/transport/pairing' import { recoverMobileRelayPairing } from '../src/transport/mobile-relay-pairing-recovery' import { appUpdateChecker } from '../src/app-update/app-update-runtime' +import { SideSafeArea } from '../src/layout/side-safe-area' // Why: keeps the native splash screen visible until the React tree is mounted // and ready to render. Without this the user sees a blank white/black frame @@ -200,45 +201,47 @@ export default function RootLayout() { - - + + - - - - - - - - - - - - - - - + > + + }} + /> + + + + + + + + + + + + + + + + ) diff --git a/mobile/app/_layout.web.tsx b/mobile/app/_layout.web.tsx index e33906939aa..7b20142574d 100644 --- a/mobile/app/_layout.web.tsx +++ b/mobile/app/_layout.web.tsx @@ -3,13 +3,14 @@ import { StyleSheet, useWindowDimensions, View } from 'react-native' import { Slot } from 'expo-router' import { SafeAreaFrameContext, SafeAreaInsetsContext } from 'react-native-safe-area-context' import { ZERO_SAFE_AREA_INSETS } from '../src/mobile-web-shell/bridge/bridge-safe-area-insets' +import { SideSafeArea } from '../src/layout/side-safe-area' import { usePageBridgeClient } from '../src/transport/client-context.web' import { colors } from '../src/theme/mobile-theme' /** * The page's root layout, standing where the native `_layout.tsx` does. Without one expo-router * mounts `DefaultNavigator`, an all-edges `SafeAreaView`, which would pad each screen a second time. - * Like the native root it adds no padding. It supplies the insets the shell sent in `init` below + * Like the native root it pads only the sides. It supplies the insets the shell sent in `init` below * ExpoRoot's own provider, whose web half reads `env(safe-area-inset-*)`: 0 in both WebViews. */ export default function PageRootLayout() { @@ -24,7 +25,9 @@ export default function PageRootLayout() { - + + + diff --git a/mobile/src/components/RightDrawer.tsx b/mobile/src/components/RightDrawer.tsx index 86d2709cc0a..f8de3a9dc3f 100644 --- a/mobile/src/components/RightDrawer.tsx +++ b/mobile/src/components/RightDrawer.tsx @@ -1,6 +1,6 @@ import { type ReactNode, useCallback, useEffect, useState } from 'react' -import { View, Pressable, StyleSheet, Platform, useWindowDimensions, Keyboard } from 'react-native' -import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { View, Pressable, StyleSheet, Platform, Keyboard } from 'react-native' +import { useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context' import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler' import Animated, { useSharedValue, @@ -78,7 +78,7 @@ function MountedRightDrawer({ }: MountedRightDrawerProps) { const translateX = useSharedValue(0) const progress = useSharedValue(0) - const { width: screenWidth } = useWindowDimensions() + const { width: screenWidth } = useSafeAreaFrame() const insets = useSafeAreaInsets() const { isWideLayout } = useResponsiveLayout() const panelWidth = resolveRightDrawerPanelWidth(screenWidth, isWideLayout, widthPx) diff --git a/mobile/src/components/WorktreeListRow.tsx b/mobile/src/components/WorktreeListRow.tsx index 68e1f520483..96b32835bae 100644 --- a/mobile/src/components/WorktreeListRow.tsx +++ b/mobile/src/components/WorktreeListRow.tsx @@ -285,7 +285,8 @@ const styles = StyleSheet.create({ fontSize: 11, color: colors.textMuted, fontFamily: typography.monoFamily, - flexShrink: 1 + // Why: path takes only leftover space so badges truncate last. + flex: 1 }, childBadge: { flexDirection: 'row', diff --git a/mobile/src/components/mounted-bottom-drawer.tsx b/mobile/src/components/mounted-bottom-drawer.tsx index 764a3c5437e..2d2f3e8a371 100644 --- a/mobile/src/components/mounted-bottom-drawer.tsx +++ b/mobile/src/components/mounted-bottom-drawer.tsx @@ -1,6 +1,5 @@ import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react' import { View, Pressable, useWindowDimensions, ScrollView, Keyboard, Modal } from 'react-native' -import { useSafeAreaInsets } from 'react-native-safe-area-context' import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler' import Animated, { useSharedValue, @@ -19,6 +18,7 @@ import { BOTTOM_DRAWER_HIDE_DURATION_MS } from './bottom-drawer-constants' import { bottomDrawerStyles as styles } from './bottom-drawer-styles' import { useInsideBottomDrawerModalHost } from './bottom-drawer-modal-host' import { useResponsiveLayout } from '../layout/responsive-layout' +import { useWindowSafeAreaInsets } from '../layout/side-safe-area' import { useBackClaim } from '../navigation/use-back-claim' import { currentSoftKeyboardHeight, subscribeSoftKeyboard } from '../platform/keyboard-occlusion' import { hostOs } from '../platform/host-os' @@ -68,7 +68,7 @@ export function MountedBottomDrawer({ // edge stays under the status bar. const [keyboardInset, setKeyboardInset] = useState(0) const { height: screenHeight } = useWindowDimensions() - const insets = useSafeAreaInsets() + const insets = useWindowSafeAreaInsets() // Why: on wide/tablet canvases a full-width sheet looks stretched; cap it and // center it horizontally. Vertical bottom-anchoring (and all the drag/keyboard // transforms below) is unchanged, so phone behavior stays identical. @@ -376,7 +376,14 @@ export function MountedBottomDrawer({ {interactive ? : null} - + ({ useWindowDimensions: () => ({ width: 390, height: 844 }) })) vi.mock('react-native-safe-area-context', () => ({ + useSafeAreaFrame: () => ({ x: 0, y: 0, width: 390, height: 844 }), useSafeAreaInsets: () => ({ top: 62, bottom: 34, left: 0, right: 0 }) })) vi.mock('react-native-gesture-handler', () => { diff --git a/mobile/src/diagnostics/connection-diagnostics-report.test.ts b/mobile/src/diagnostics/connection-diagnostics-report.test.ts index 991ec01f549..88794135209 100644 --- a/mobile/src/diagnostics/connection-diagnostics-report.test.ts +++ b/mobile/src/diagnostics/connection-diagnostics-report.test.ts @@ -56,6 +56,29 @@ describe('buildConnectionDiagnosticsReport', () => { expect(report).toContain('No connection events recorded.') }) + it('names the shell build and bridge version under the app line when given', () => { + const args = { + hostName: 'Host 3', + endpoint: 'ws://192.168.1.50:6768', + state: 'connected' as const, + reconnectAttempts: 0, + lastConnectedAt: null, + platform: 'ios 26', + appVersion: '0.0.60', + entries: [], + nowMs: NOW + } + const lines = buildConnectionDiagnosticsReport({ + ...args, + shell: { kind: 'ota', bridgeProtocolVersion: 1 } + }).split('\n') + expect(lines.slice(2, 4)).toEqual([ + 'App: Orca Mobile 0.0.60 · ios 26', + 'Shell: ota · bridge protocol 1' + ]) + expect(buildConnectionDiagnosticsReport(args)).not.toContain('Shell:') + }) + it('explains the most likely cause and redacts credentials before copying', () => { const report = buildConnectionDiagnosticsReport({ hostName: 'Host 3', diff --git a/mobile/src/diagnostics/connection-diagnostics-report.ts b/mobile/src/diagnostics/connection-diagnostics-report.ts index 41c22014471..25c78e224b9 100644 --- a/mobile/src/diagnostics/connection-diagnostics-report.ts +++ b/mobile/src/diagnostics/connection-diagnostics-report.ts @@ -28,6 +28,8 @@ export function buildConnectionDiagnosticsReport(args: { entries: readonly ConnectionLogEntry[] activePath?: MobileConnectionDiagnosticPath pendingPath?: MobileConnectionDiagnosticPath | null + /** Which shell this binary is and the page bridge version it speaks; omitted by older callers. */ + shell?: { kind: 'ota' | 'native'; bridgeProtocolVersion: number } nowMs?: number }): string { const now = args.nowMs ?? Date.now() @@ -43,6 +45,9 @@ export function buildConnectionDiagnosticsReport(args: { lines.push('Orca Mobile connection diagnostics') lines.push(`Generated: ${new Date(now).toISOString()}`) lines.push(`App: Orca Mobile ${args.appVersion} · ${args.platform}`) + if (args.shell) { + lines.push(`Shell: ${args.shell.kind} · bridge protocol ${args.shell.bridgeProtocolVersion}`) + } const desktopAppVersion = normalizeHostAppVersion(args.desktopAppVersion) lines.push(`Host Orca version: ${desktopAppVersion ?? 'unknown'}`) lines.push(`Host: ${redactConnectionLogText(args.hostName)}`) diff --git a/mobile/src/diagnostics/native-diagnostics-operations.ts b/mobile/src/diagnostics/native-diagnostics-operations.ts index 2b7b7fe6f0b..39e614b43ce 100644 --- a/mobile/src/diagnostics/native-diagnostics-operations.ts +++ b/mobile/src/diagnostics/native-diagnostics-operations.ts @@ -9,6 +9,8 @@ import { getReportableConnectionIncidentId } from './connection-diagnostics-anal import { buildConnectionDiagnosticsReport } from './connection-diagnostics-report' import { submitConnectionDiagnostics } from './connection-diagnostics-submission' import type { DiagnosticsDeviceOperations } from './diagnostics-device-operations' +import { mobileShellBuildKind } from '../storage/preferences' +import { BRIDGE_PROTOCOL_VERSION } from '../mobile-web-shell/bridge/bridge-frame-fields' export function createNativeDiagnosticsOperations( host: HostProfile, @@ -33,7 +35,8 @@ export function createNativeDiagnosticsOperations( desktopAppVersion, entries: snapshot.entries, activePath: snapshot.activePath, - pendingPath: snapshot.pendingPath + pendingPath: snapshot.pendingPath, + shell: { kind: mobileShellBuildKind(), bridgeProtocolVersion: BRIDGE_PROTOCOL_VERSION } }), appVersion, platform, diff --git a/mobile/src/files/MobileFilePreviewScreen.tsx b/mobile/src/files/MobileFilePreviewScreen.tsx index e71da8e2833..973751491a2 100644 --- a/mobile/src/files/MobileFilePreviewScreen.tsx +++ b/mobile/src/files/MobileFilePreviewScreen.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Pressable, Text, View, useWindowDimensions } from 'react-native' -import { SafeAreaView } from 'react-native-safe-area-context' +import { Pressable, Text, View } from 'react-native' +import { SafeAreaView, useSafeAreaFrame } from 'react-native-safe-area-context' import { ChevronLeft, Save } from 'lucide-react-native' import { useWorkspaceRouteHandoff } from '../navigation/workspace-route-handoff' import { getWorktreeLabel } from '../session/worktree-label' @@ -51,7 +51,7 @@ export function MobileFilePreviewScreen({ route }: Props) { const draftContentRef = useRef(draftContent) const savedContentRef = useRef(savedContent) const draftSourceKeyRef = useRef(null) - const { width, height } = useWindowDimensions() + const { width, height } = useSafeAreaFrame() const routePreviewSource = useMemo( () => (previewParams ? previewSourceFromRoute(previewParams) : null), [previewParams] diff --git a/mobile/src/files/mobile-file-preview-retry.test.tsx b/mobile/src/files/mobile-file-preview-retry.test.tsx index 46ed986c1bc..54d626d8283 100644 --- a/mobile/src/files/mobile-file-preview-retry.test.tsx +++ b/mobile/src/files/mobile-file-preview-retry.test.tsx @@ -26,7 +26,10 @@ vi.mock('react-native', () => ({ View: 'View', useWindowDimensions: () => ({ width: 390, height: 844 }) })) -vi.mock('react-native-safe-area-context', () => ({ SafeAreaView: 'SafeAreaView' })) +vi.mock('react-native-safe-area-context', () => ({ + SafeAreaView: 'SafeAreaView', + useSafeAreaFrame: () => ({ x: 0, y: 0, width: 390, height: 844 }) +})) vi.mock('lucide-react-native', () => ({ ChevronLeft: 'Icon', Save: 'Icon' })) vi.mock('../navigation/route-handoff', () => ({ useRouteHandoff: () => ({ back: () => {}, canGoBack: () => false }) diff --git a/mobile/src/home/MobileHomeEmptyState.tsx b/mobile/src/home/MobileHomeEmptyState.tsx index eda2496094d..e5d9c6b826e 100644 --- a/mobile/src/home/MobileHomeEmptyState.tsx +++ b/mobile/src/home/MobileHomeEmptyState.tsx @@ -1,5 +1,5 @@ import { QrCode } from 'lucide-react-native' -import { Pressable, StyleSheet, Text, View } from 'react-native' +import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native' import { colors, radii, spacing } from '../theme/mobile-theme' const ONBOARDING_STEPS = [ @@ -23,9 +23,10 @@ export function MobileHomeEmptyState(props: { isWideLayout: boolean onPairDesktop: () => void }) { + // Scrolls because a phone in landscape is shorter than the hero and the steps together. return ( - ))} - + ) } const styles = StyleSheet.create({ - emptyContainer: { flex: 1 }, + emptyContainer: { flexGrow: 1 }, emptyHero: { - flex: 1, + flexGrow: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32, diff --git a/mobile/src/host-screen/host-workspace-list-row-memo.test.tsx b/mobile/src/host-screen/host-workspace-list-row-memo.test.tsx index 4c2718dfb94..892e782420f 100644 --- a/mobile/src/host-screen/host-workspace-list-row-memo.test.tsx +++ b/mobile/src/host-screen/host-workspace-list-row-memo.test.tsx @@ -6,6 +6,7 @@ import type { Section, Worktree } from '../worktree/workspace-list-types' const { rowRenders } = vi.hoisted(() => ({ rowRenders: vi.fn() })) vi.mock('react-native', () => ({ + Platform: { OS: 'ios' }, Pressable: 'Pressable', RefreshControl: 'RefreshControl', // Renders every row through the list's own renderItem, as a mounted SectionList would. diff --git a/mobile/src/host-screen/host-workspace-list.tsx b/mobile/src/host-screen/host-workspace-list.tsx index 773b2f967f2..6f47fc6415b 100644 --- a/mobile/src/host-screen/host-workspace-list.tsx +++ b/mobile/src/host-screen/host-workspace-list.tsx @@ -1,4 +1,4 @@ -import { Pressable, RefreshControl, SectionList, Text, View } from 'react-native' +import { Platform, Pressable, RefreshControl, SectionList, Text, View } from 'react-native' import { ChevronDown, ChevronRight, Pin } from 'lucide-react-native' import { AuthFailedBanner } from '../components/AuthFailedBanner' import { HostDiagnosticsLink } from '../components/HostDiagnosticsLink' @@ -95,11 +95,11 @@ export function HostWorkspaceList({ controller }: { controller: HostScreenContro )} @@ -120,9 +120,10 @@ export function HostWorkspaceList({ controller }: { controller: HostScreenContro sections={sections} keyExtractor={(w) => w.sectionListKey ?? getWorktreeRowIdentity(w)} stickySectionHeadersEnabled={false} - // Why: keep the search IME up while tapping clear / scrolling results. + // Why: keep the search IME up while tapping clear. keyboardShouldPersistTaps="handled" - keyboardDismissMode="on-drag" + // Why: react-native-web dismisses on-drag on any scroll, and a narrowing search fires one. + keyboardDismissMode={Platform.OS === 'web' ? 'none' : 'on-drag'} onScrollToIndexFailed={activeWorktreeScroll.onScrollToIndexFailed} // Why: edge-to-edge under the system nav bar; insets.bottom keeps the last row above it. contentContainerStyle={[ diff --git a/mobile/src/layout/side-safe-area.test.tsx b/mobile/src/layout/side-safe-area.test.tsx new file mode 100644 index 00000000000..a46f4122004 --- /dev/null +++ b/mobile/src/layout/side-safe-area.test.tsx @@ -0,0 +1,71 @@ +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { describe, expect, it, vi } from 'vitest' +import type { ReactNode } from 'react' +import type { EdgeInsets, Rect } from 'react-native-safe-area-context' + +vi.mock('react-native', () => ({ + Platform: { OS: 'ios' }, + View: 'View', + StyleSheet: { create: (styles: T) => styles } +})) +// The library's hooks are plain reads of its two contexts, which is all this stands in for. +vi.mock('react-native-safe-area-context', async () => { + const { createContext, useContext } = await import('react') + const SafeAreaInsetsContext = createContext(null) + const SafeAreaFrameContext = createContext(null) + return { + SafeAreaInsetsContext, + SafeAreaFrameContext, + SafeAreaProvider: ({ children }: { children: ReactNode }) => children, + useSafeAreaInsets: () => useContext(SafeAreaInsetsContext), + useSafeAreaFrame: () => useContext(SafeAreaFrameContext) + } +}) + +const { SafeAreaFrameContext, SafeAreaInsetsContext, useSafeAreaFrame, useSafeAreaInsets } = + await import('react-native-safe-area-context') +const { SideSafeArea, useWindowSafeAreaInsets } = await import('./side-safe-area') + +// Unequal sides, so a swapped edge shows. +const WINDOW = { top: 0, right: 48, bottom: 21, left: 59 } +const FRAME = { x: 0, y: 0, width: 852, height: 393 } + +function renderInLandscape() { + const read: { screen?: EdgeInsets; frame?: Rect; modal?: EdgeInsets } = {} + function Probe() { + read.screen = useSafeAreaInsets() + read.frame = useSafeAreaFrame() + read.modal = useWindowSafeAreaInsets() + return null + } + let tree: ReactTestRenderer | undefined + act(() => { + tree = create( + + + + + + + + ) + }) + return { tree: tree!, read } +} + +describe('the app laid out clear of the side insets', () => { + it('pads the sides once, and screens below see the padded area', () => { + const { tree, read } = renderInLandscape() + const area = tree.root.find((node) => String(node.type) === 'View') + expect(Object.assign({}, ...[area.props.style].flat())).toMatchObject({ + paddingLeft: 59, + paddingRight: 48 + }) + expect(read.screen).toEqual({ top: 0, right: 0, bottom: 21, left: 0 }) + expect(read.frame).toEqual({ x: 59, y: 0, width: 745, height: 393 }) + }) + + it('gives a Modal, which opens a window of its own, the window insets', () => { + expect(renderInLandscape().read.modal).toEqual(WINDOW) + }) +}) diff --git a/mobile/src/layout/side-safe-area.tsx b/mobile/src/layout/side-safe-area.tsx new file mode 100644 index 00000000000..1bab737976a --- /dev/null +++ b/mobile/src/layout/side-safe-area.tsx @@ -0,0 +1,57 @@ +import { createContext, useContext, useMemo, type ReactNode } from 'react' +import { Platform, StyleSheet, View } from 'react-native' +import { + SafeAreaFrameContext, + SafeAreaInsetsContext, + SafeAreaProvider, + useSafeAreaFrame, + useSafeAreaInsets, + type EdgeInsets +} from 'react-native-safe-area-context' + +const WindowSafeAreaInsetsContext = createContext(null) + +/** + * Lays the app out clear of the window's side insets, which landscape brings: the iPhone sensor + * housing, an Android side navigation bar or cutout. Below it the insets and frame describe the + * padded area, so no screen pads a side twice or sizes past it. + */ +export function SideSafeArea({ children }: { children: ReactNode }) { + const insets = useSafeAreaInsets() + const frame = useSafeAreaFrame() + const areaInsets = useMemo(() => ({ ...insets, left: 0, right: 0 }), [insets]) + const areaFrame = useMemo( + () => ({ + ...frame, + x: frame.x + insets.left, + width: Math.max(0, frame.width - insets.left - insets.right) + }), + [frame, insets.left, insets.right] + ) + const area = ( + + + + {children} + + + + ) + // A native SafeAreaView measures against the nearest native provider rather than the context, so + // one goes inside the padding; on the web it reads the context. + return ( + + {Platform.OS === 'web' ? area : {area}} + + ) +} + +/** The window's insets, for a Modal: it opens a window of its own, outside `SideSafeArea`. */ +export function useWindowSafeAreaInsets(): EdgeInsets { + const insets = useSafeAreaInsets() + return useContext(WindowSafeAreaInsetsContext) ?? insets +} + +const styles = StyleSheet.create({ + area: { flex: 1 } +}) diff --git a/mobile/src/mobile-web-shell/MobileWebShellScreen.test.tsx b/mobile/src/mobile-web-shell/MobileWebShellScreen.test.tsx index f285ec7076e..69fa1bbaceb 100644 --- a/mobile/src/mobile-web-shell/MobileWebShellScreen.test.tsx +++ b/mobile/src/mobile-web-shell/MobileWebShellScreen.test.tsx @@ -47,6 +47,7 @@ import { readBridgeHostMessage } from './bridge/bridge-envelope' import { MobileWebShellScreen } from './MobileWebShellScreen' +import { connectionLogStore } from '../transport/persisted-connection-log-store' import type { MobileWebShellSessionState } from './mobile-web-shell-session-contract' import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer' @@ -244,8 +245,12 @@ describe('the hybrid shell screen', () => { // page re-posting `ready` on its backoff for as long as the screen is open. dependencies.snapshotUnreadable = true const warned = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const logged = () => connectionLogStore.get('host-1').map((entry) => entry.message) + const before = logged().length await renderScreen(readyState('session-one')) expect(dependencies.reportShellFailure.mock.calls).toEqual([['document-load-failed']]) + // Named in the exported log, not only as the generic failure it causes. + expect(logged().slice(before)).toEqual(['Web shell: host profile unreadable']) warned.mockRestore() }) diff --git a/mobile/src/mobile-web-shell/MobileWebShellScreen.tsx b/mobile/src/mobile-web-shell/MobileWebShellScreen.tsx index 34c150921ba..ea58f9876e3 100644 --- a/mobile/src/mobile-web-shell/MobileWebShellScreen.tsx +++ b/mobile/src/mobile-web-shell/MobileWebShellScreen.tsx @@ -38,6 +38,8 @@ import { storageReadParamsSchema, type BridgeNativeVerb } from './bridge/bridge- import { SHELL_OPENING_LABEL, ShellPageCover, ShellWaitingFrame } from './ShellWaitingFrame' import { ShellDownloadProgress } from './ShellDownloadProgress' import { pageSafeAreaInsets, usePublishedSafeAreaInsets } from './page-safe-area-insets' +import { hostProfileUnreadableLine, pageFaultLine } from './shell-lifecycle-log' +import { recordShellLifecycleLine } from './shell-lifecycle-line-recorder' function failureMessage(reason: MobileWebShellFailureCause): string { switch (reason) { @@ -245,6 +247,7 @@ export function MobileWebShellScreen({ // handler on the other, and neither has anywhere to put a throw. onPageFault: (error) => { console.warn('[web-shell] the page faulted', error) + recordShellLifecycleLine(hostId, pageFaultLine(error)) reportShellFailure('document-load-failed') }, // The `init` this ready is answered with is already built from the app's writes, which reach @@ -266,6 +269,7 @@ export function MobileWebShellScreen({ // `document-load-failed` because that is what happens: the document loads and the page refuses // the session, so no tree is ever built. The refetch it costs is wasted on a route this shell // produced, and the second report is terminal, which is the failure screen this deserves. + // Logged by the host's `route-refused` diagnostic, which carries the issue, in the same call. onRouteRefused: (issue) => { console.warn('[web-shell] refused to open this screen', issue) reportShellFailure('document-load-failed') @@ -341,6 +345,12 @@ export function MobileWebShellScreen({ reportShellFailure('document-load-failed') } }, [reportShellFailure, unreadable]) + // Log-only and separate, so the failure effect above keeps its own dependencies. + useEffect(() => { + if (unreadable) { + recordShellLifecycleLine(hostId, hostProfileUnreadableLine()) + } + }, [hostId, unreadable]) if (state.kind === 'native-route') { return fallback diff --git a/mobile/src/mobile-web-shell/bridge-diagnostic-log.test.ts b/mobile/src/mobile-web-shell/bridge-diagnostic-log.test.ts index c7f96afedf4..84c9c021050 100644 --- a/mobile/src/mobile-web-shell/bridge-diagnostic-log.test.ts +++ b/mobile/src/mobile-web-shell/bridge-diagnostic-log.test.ts @@ -18,6 +18,16 @@ function lines(): string[] { } describe('the bridge diagnostic log', () => { + it('hands the connection log the same once-per-key diagnostics as the console', () => { + const reported: string[] = [] + const report = createBridgeDiagnosticReporter((diagnostic) => reported.push(diagnostic.kind)) + report({ kind: 'frame-after-close' }) + report({ kind: 'frame-after-close' }) + report({ kind: 'notify-refused', name: 'fault', why: 'ungranted' }) + expect(reported).toEqual(['frame-after-close', 'notify-refused']) + expect(lines()).toHaveLength(2) + }) + it('names the notification it refused and why, for each refusal', () => { const report = createBridgeDiagnosticReporter() report({ kind: 'notify-refused', name: 'fault', why: 'before-ready' }) diff --git a/mobile/src/mobile-web-shell/bridge-diagnostic-log.ts b/mobile/src/mobile-web-shell/bridge-diagnostic-log.ts index 34ae56fa24b..6c6793008cc 100644 --- a/mobile/src/mobile-web-shell/bridge-diagnostic-log.ts +++ b/mobile/src/mobile-web-shell/bridge-diagnostic-log.ts @@ -24,7 +24,10 @@ function diagnosticKey(diagnostic: BridgeHostDiagnostic): string { * says why. The host already holds `post-failed` to one; this is the same bound for the kinds it * does not, and a new host starts the count over because a new page is new evidence. */ -export function createBridgeDiagnosticReporter(): (diagnostic: BridgeHostDiagnostic) => void { +export function createBridgeDiagnosticReporter( + // Gets the same once-per-key diagnostics as the console, for the exported connection log. + onReported?: (diagnostic: BridgeHostDiagnostic) => void +): (diagnostic: BridgeHostDiagnostic) => void { const reported = new Set() return (diagnostic) => { const key = diagnosticKey(diagnostic) @@ -32,6 +35,7 @@ export function createBridgeDiagnosticReporter(): (diagnostic: BridgeHostDiagnos return } reported.add(key) + onReported?.(diagnostic) if (diagnostic.kind === 'refused') { console.warn('[web-shell-bridge] refused a page frame', diagnostic.refusal) return diff --git a/mobile/src/mobile-web-shell/shell-bridge-log-lines.test.ts b/mobile/src/mobile-web-shell/shell-bridge-log-lines.test.ts new file mode 100644 index 00000000000..243dda5d9b5 --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-bridge-log-lines.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' +import { bridgeDiagnosticLine, frameWithoutHostLine } from './shell-bridge-log-lines' + +describe('shell bridge log lines', () => { + it('skips routine bridge kinds and names the specific failure for the rest', () => { + expect( + bridgeDiagnosticLine({ + kind: 'terminal-backlog', + id: 't', + coalescedFrames: 0, + deliveredFrames: 0, + peakPendingBytes: 0, + ended: null + }) + ).toBeNull() + expect( + bridgeDiagnosticLine({ kind: 'notify-refused', name: 'fault', why: 'ungranted' }) + ).toEqual({ + level: 'warn', + code: 'web-shell-bridge', + message: 'Web shell bridge: notify-refused', + detail: 'ungranted' + }) + const gone = new Error('view gone') + gone.name = 'BridgeViewGoneError' + expect(bridgeDiagnosticLine({ kind: 'post-failed', error: gone })?.detail).toBe( + 'error=BridgeViewGoneError' + ) + expect(bridgeDiagnosticLine({ kind: 'post-failed', error: 'nope' })?.detail).toBe( + 'error=string' + ) + expect( + bridgeDiagnosticLine({ kind: 'route-refused', issue: 'Invalid path /Users/bob/wt' })?.detail + ).toBe('issue=Invalid path [path]') + }) + + it('says which half a frame with no bridge host was waiting on', () => { + expect(frameWithoutHostLine({ clientReady: false, snapshotReady: true })).toEqual({ + level: 'warn', + code: 'web-shell-page', + message: 'Web shell: page frame with no bridge host', + detail: 'client=null snapshot=ok' + }) + }) +}) diff --git a/mobile/src/mobile-web-shell/shell-bridge-log-lines.ts b/mobile/src/mobile-web-shell/shell-bridge-log-lines.ts new file mode 100644 index 00000000000..54d950f1ccf --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-bridge-log-lines.ts @@ -0,0 +1,61 @@ +import type { BridgeHostDiagnostic } from './bridge-host-contract' +import type { ShellLifecycleLine } from './shell-lifecycle-line-recorder' +import { maskUserText } from './shell-log-path-masking' + +/** Bridge-side shell log lines: host diagnostics and frames that found no host to answer them. */ + +/** Null for routine kinds; the caller already bounds each key to one line per host. */ +export function bridgeDiagnosticLine(diagnostic: BridgeHostDiagnostic): ShellLifecycleLine | null { + // Retired terminal streams are routine and one per stream; they would crowd out the log. + if (diagnostic.kind === 'terminal-backlog') { + return null + } + const detail = bridgeDiagnosticDetail(diagnostic) + return { + level: 'warn', + code: 'web-shell-bridge', + message: `Web shell bridge: ${diagnostic.kind}`, + ...(detail === undefined ? {} : { detail }) + } +} + +/** A page frame arrived while no bridge host was built, so it was dropped unanswered. */ +export function frameWithoutHostLine(facts: { + clientReady: boolean + snapshotReady: boolean +}): ShellLifecycleLine { + return { + level: 'warn', + code: 'web-shell-page', + message: 'Web shell: page frame with no bridge host', + detail: `client=${facts.clientReady ? 'ok' : 'null'} snapshot=${facts.snapshotReady ? 'ok' : 'null'}` + } +} + +function bridgeDiagnosticDetail(diagnostic: BridgeHostDiagnostic): string | undefined { + switch (diagnostic.kind) { + case 'refused': + return diagnostic.refusal + case 'notify-refused': + case 'navigate-back-refused': + return diagnostic.why + case 'post-failed': + return `error=${maskUserText(errorName(diagnostic.error))}` + case 'route-refused': + return `issue=${maskUserText(diagnostic.issue)}` + case 'notify-failed': + case 'frame-after-dispose': + case 'frame-after-close': + case 'storage-refused': + case 'route-update-refused': + case 'binary-lane-refused': + case 'binary-frame-dropped': + case 'terminal-backlog': + // Their payloads name keys, ids or schema text that is not needed to place a stall. + return undefined + } +} + +function errorName(error: unknown): string { + return error instanceof Error ? error.name : typeof error +} diff --git a/mobile/src/mobile-web-shell/shell-lifecycle-line-recorder.ts b/mobile/src/mobile-web-shell/shell-lifecycle-line-recorder.ts new file mode 100644 index 00000000000..a2ed1b4743a --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-lifecycle-line-recorder.ts @@ -0,0 +1,39 @@ +import { connectionLogStore } from '../transport/persisted-connection-log-store' +import type { ConnectionLogEntry } from '../transport/types' + +/** + * Sparse OTA shell milestones for the per-host connection log, so an exported report says where a + * workspace open stalled. Diagnostics only: nothing here feeds back into the session. + * + * The store holds 200 entries shared with connection events and every page route mounts its own + * shell, so a healthy open writes one line (the paint); the rest are failures and one-shot stall + * warnings. Route paths, generation directories and error stacks stay out: they can name user paths. + */ +export type ShellLifecycleLine = Required> & + Pick + +let lineSequence = 0 + +// Lines a host flapping between unreachable and dialling would repeat every cycle per mounted shell. +const FLAPPING_MESSAGES: ReadonlySet = new Set([ + 'Web shell: checking host', + 'Web shell: host unreachable and no app cached' +]) +// Per host, the flapping lines already written since the last other shell line for it. +const flappingWritten = new Map>() + +export function recordShellLifecycleLine(hostId: string, line: ShellLifecycleLine): void { + if (FLAPPING_MESSAGES.has(line.message)) { + const written = flappingWritten.get(hostId) ?? new Set() + if (written.has(line.message)) { + return + } + written.add(line.message) + flappingWritten.set(hostId, written) + } else { + flappingWritten.delete(hostId) + } + const ts = Date.now() + lineSequence += 1 + connectionLogStore.append(hostId, { id: `web-shell-${ts}-${lineSequence}`, ts, ...line }) +} diff --git a/mobile/src/mobile-web-shell/shell-lifecycle-log.test.ts b/mobile/src/mobile-web-shell/shell-lifecycle-log.test.ts new file mode 100644 index 00000000000..a7c13b20039 --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-lifecycle-log.test.ts @@ -0,0 +1,383 @@ +import { describe, expect, it, vi } from 'vitest' +import { buildConnectionDiagnosticsReport } from '../diagnostics/connection-diagnostics-report' +import type { ConnectionLogEntry } from '../transport/types' +import { + CACHED, + MANIFEST, + createMobileWebShellSession, + gates, + ROUTE, + stamp +} from './mobile-web-shell-session-test-fixtures' +import { reduceMobileWebShellSession } from './mobile-web-shell-session' +import type { + MobileWebShellSession, + MobileWebShellSessionEvent +} from './mobile-web-shell-session-contract' +import { connectionLogStore } from '../transport/persisted-connection-log-store' +import { bridgeDiagnosticLine, frameWithoutHostLine } from './shell-bridge-log-lines' +import { recordShellLifecycleLine, type ShellLifecycleLine } from './shell-lifecycle-line-recorder' +import { + EMPTY_SHELL_LOG_TRACE, + pageFaultLine, + shellSessionStepLines, + shellStallLine, + traceShellStep, + type ShellLogTrace +} from './shell-lifecycle-log' + +vi.mock('@react-native-async-storage/async-storage', () => ({ + default: { getItem: vi.fn(async () => null), setItem: vi.fn(async () => undefined) } +})) + +type Pending = Parameters[1] + +/** Replays events through the real reducer, collecting what each step would log. */ +type Replayed = { + session: MobileWebShellSession + trace: ShellLogTrace + lines: ShellLifecycleLine[] +} + +function replay( + events: readonly Pending[], + from: Omit = { + session: createMobileWebShellSession(ROUTE), + trace: EMPTY_SHELL_LOG_TRACE + } +): Replayed { + let { session, trace } = from + const lines: ShellLifecycleLine[] = [] + for (const pending of events) { + const event: MobileWebShellSessionEvent = stamp(session.flow, pending) + const next = reduceMobileWebShellSession(session, event).session + trace = traceShellStep(trace, session, event, next) + lines.push(...shellSessionStepLines(session, event, next, trace)) + session = next + } + return { session, trace, lines } +} + +function stall(replayed: Omit): ShellLifecycleLine | null { + return shellStallLine(replayed.session.state, replayed.session, replayed.trace) +} + +const ACTIVATED = { + type: 'activated', + generationDirectory: '/Users/someone/Library/Caches/gen-1', + sessionId: 'session-one', + buildId: MANIFEST.buildId, + totalBytes: MANIFEST.totalBytes, + elapsedMs: 1234 +} as const + +const DOWNLOAD_OPEN: readonly Pending[] = [ + { type: 'gates-changed', gates: gates() }, + { type: 'cache-read', generation: null }, + { type: 'manifest-read', manifest: MANIFEST }, + { + type: 'fetch-progress', + completedAssets: 1, + totalAssets: MANIFEST.totalAssets, + receivedBytes: 10, + totalBytes: MANIFEST.totalBytes + }, + { + type: 'fetch-progress', + completedAssets: 2, + totalAssets: MANIFEST.totalAssets, + receivedBytes: 20, + totalBytes: MANIFEST.totalBytes + }, + { type: 'download-staged' }, + ACTIVATED +] + +function present(line: ShellLifecycleLine | null): ShellLifecycleLine[] { + expect(line).not.toBeNull() + return line === null ? [] : [line] +} + +function summary(lines: readonly ShellLifecycleLine[]): string[] { + return lines.map( + (line) => `${line.code} ${line.message}${line.detail ? ` | ${line.detail}` : ''}` + ) +} + +const PAGE_LOAD: readonly Pending[] = [ + { type: 'document-started' }, + { type: 'document-loaded' }, + { type: 'page-ready' } +] + +const BUILD = MANIFEST.buildId.slice(0, 8) + +describe('shell lifecycle log lines', () => { + it('writes exactly one line for a healthy cached open: the paint', () => { + const { lines } = replay([ + { type: 'gates-changed', gates: gates() }, + { type: 'cache-read', generation: CACHED }, + { type: 'manifest-read', manifest: MANIFEST }, + ACTIVATED, + ...PAGE_LOAD, + { type: 'page-ready' }, + { type: 'page-painted' }, + { type: 'page-painted' }, + { type: 'page-back-claim', claimed: true } + ]) + expect(summary(lines)).toEqual([ + `web-shell-page Web shell: page painted | source=cache build=${BUILD} activatedMs=1234` + ]) + }) + + it('adds only the download milestones to a downloading open, and no progress ticks', () => { + const { lines } = replay([...DOWNLOAD_OPEN, ...PAGE_LOAD, { type: 'page-painted' }]) + expect(summary(lines)).toEqual([ + `web-shell-flow Web shell: downloading app | ${MANIFEST.totalAssets} assets, ${MANIFEST.totalBytes} bytes`, + 'web-shell-flow Web shell: activating app | source=download', + `web-shell-page Web shell: page painted | source=download build=${BUILD} activatedMs=1234` + ]) + // The generation directory names a device path and never reaches the log. + expect(JSON.stringify(lines)).not.toContain('/Users/someone') + }) + + it('logs nothing for a route that stays native, the normal case for native screens', () => { + const { session, lines } = replay([ + { type: 'gates-changed', gates: gates({ hostCapabilities: [] }) } + ]) + expect(session.state.kind).toBe('native-route') + expect(lines).toEqual([]) + }) + + it('logs a missed ready deadline, the failure it causes and the recovery flow', () => { + const opened = replay(DOWNLOAD_OPEN) + const { lines } = replay([{ type: 'document-loaded' }, { type: 'page-ready-deadline' }], opened) + expect(summary(lines)).toEqual([ + 'web-shell-page Web shell: page never said ready before the deadline', + 'web-shell-flow Web shell: checking host' + ]) + }) + + it('logs a view failure but not the remount it earns', () => { + const opened = replay(DOWNLOAD_OPEN) + const { lines } = replay( + [ + { type: 'shell-failed', reason: 'render-process-gone' }, + { type: 'remounted', sessionId: 'session-two' } + ], + opened + ) + expect(summary(lines)).toEqual([ + 'web-shell-page Web shell: page load failed | reason=render-process-gone' + ]) + }) + + it('ignores results from a superseded flow', () => { + const opened = replay(DOWNLOAD_OPEN) + const { lines } = replay( + [ + { type: 'page-ready-deadline', flow: opened.session.flow - 1 }, + { ...ACTIVATED, flow: 0 } + ], + opened + ) + expect(lines).toEqual([]) + }) + + it('logs a failed download with its closed reason code', () => { + const { lines } = replay([ + { type: 'gates-changed', gates: gates() }, + { type: 'cache-read', generation: null }, + { type: 'download-failed', cause: { reason: 'connection-lost', hostCode: null } } + ]) + expect(summary(lines)).toEqual([ + 'web-shell-flow Web shell: app download failed | reason=connection-lost', + 'web-shell-flow Web shell: failed | reason=download-failed retried=no' + ]) + }) +}) + +describe('shell stall lines', () => { + it('names what a checking session waits on: the gates, then the cache', () => { + const fresh = replay([]) + expect(stall(fresh)).toMatchObject({ + code: 'web-shell-stalled', + message: 'Web shell: still checking host after 15s', + detail: 'gates=none cache=unread' + }) + expect( + stall(replay([{ type: 'gates-changed', gates: gates({ reachability: 'connecting' }) }])) + ).toMatchObject({ detail: 'gates=dialling cache=unread' }) + const opened = replay([{ type: 'gates-changed', gates: gates() }]) + expect(opened.session.state.kind).toBe('checking') + expect(stall(opened)).toMatchObject({ detail: 'gates=open cache=unread' }) + // An empty cache leaves `cached` null, so only the trace can tell the manifest wait apart. + const awaitingManifest = replay([{ type: 'cache-read', generation: null }], opened) + expect(stall(awaitingManifest)).toMatchObject({ detail: 'gates=open cache=read' }) + }) + + it('names how far a stalled download got', () => { + const fetching = replay(DOWNLOAD_OPEN.slice(0, 4)) + expect(stall(fetching)).toMatchObject({ + message: 'Web shell: download made no progress for 15s', + detail: `assets=1/${MANIFEST.totalAssets} bytes=10/${MANIFEST.totalBytes}` + }) + }) + + it('names an activation that never finished', () => { + expect(stall(replay(DOWNLOAD_OPEN.slice(0, 6)))).toMatchObject({ + message: 'Web shell: still activating after 15s', + detail: 'source=download' + }) + }) + + it('tells a page that keeps refusing init from one that took it and never drew', () => { + const opened = replay(DOWNLOAD_OPEN) + expect(stall(opened)).toMatchObject({ + message: 'Web shell: page has not drawn after 15s', + detail: `build=${BUILD} page ready=no readyAsks=0 documentLoaded=no` + }) + const refusing = replay([...PAGE_LOAD, { type: 'page-ready' }, { type: 'page-ready' }], opened) + expect(stall(refusing)).toMatchObject({ + detail: `build=${BUILD} page ready=yes readyAsks=3 documentLoaded=yes` + }) + // A replacement document starts its own count. + const reloaded = replay([{ type: 'document-started' }, { type: 'page-ready' }], refusing) + expect(stall(reloaded)).toMatchObject({ + detail: `build=${BUILD} page ready=yes readyAsks=1 documentLoaded=no` + }) + expect(stall(replay([{ type: 'page-painted' }], reloaded))).toBeNull() + }) + + it('stays quiet for states nobody waits in', () => { + expect( + stall(replay([{ type: 'gates-changed', gates: gates({ hostCapabilities: [] }) }])) + ).toBeNull() + }) +}) + +describe('shell failure details', () => { + it('keeps device paths out of a page fault', () => { + expect( + pageFaultLine({ + category: 'TypeError', + message: 'x is undefined at file:///var/mobile/app/index.js:1\n at /Users/me/secret.js', + isRpcDeliveryUnknown: false + }).detail + ).toBe('TypeError: x is undefined at [path]') + }) +}) + +describe('recording shell lines', () => { + it('writes a flapping host line once until a different shell line is written', () => { + const host = 'flapping-host' + const checking = replay([ + { type: 'gates-changed', gates: gates({ reachability: 'unreachable' }) }, + { type: 'cache-read', generation: null }, + { type: 'gates-changed', gates: gates({ reachability: 'connecting' }) } + ]).lines + expect(summary(checking)).toEqual([ + 'web-shell-flow Web shell: host unreachable and no app cached', + 'web-shell-flow Web shell: checking host' + ]) + for (let cycle = 0; cycle < 3; cycle += 1) { + for (const line of checking) { + recordShellLifecycleLine(host, line) + } + } + const messages = () => connectionLogStore.get(host).map((entry) => entry.message) + expect(messages()).toEqual([ + 'Web shell: host unreachable and no app cached', + 'Web shell: checking host' + ]) + recordShellLifecycleLine(host, frameWithoutHostLine({ clientReady: true, snapshotReady: true })) + recordShellLifecycleLine(host, checking[1]!) + recordShellLifecycleLine('other-host', checking[1]!) + expect(messages().slice(2)).toEqual([ + 'Web shell: page frame with no bridge host', + 'Web shell: checking host' + ]) + expect(connectionLogStore.get('other-host')).toHaveLength(1) + }) +}) + +describe('shell lifecycle lines in the exported report', () => { + function entries(lines: readonly ShellLifecycleLine[]): ConnectionLogEntry[] { + return lines.map((line, index) => ({ id: `e${index}`, ts: 1_000 + index * 1_000, ...line })) + } + + it('shows where a connected session stalled without changing the connection verdict', () => { + const opened = replay(DOWNLOAD_OPEN) + const page = replay(PAGE_LOAD, opened) + const report = buildConnectionDiagnosticsReport({ + hostName: 'Desk', + endpoint: 'ws://192.168.1.2:6768', + state: 'connected', + reconnectAttempts: 0, + lastConnectedAt: 500, + platform: 'ios', + appVersion: '0.0.56', + entries: entries([...opened.lines, ...page.lines, ...present(stall(page))]), + shell: { kind: 'ota', bridgeProtocolVersion: 1 }, + nowMs: 60_000 + }) + expect(report).toContain('Shell: ota · bridge protocol 1') + expect(report).toContain('Likely cause: Connection is healthy.') + expect(report).toContain('[web-shell-flow] Web shell: activating app') + expect(report).toContain( + `[warn] [web-shell-stalled] Web shell: page has not drawn after 15s — build=${BUILD} page ready=yes readyAsks=1 documentLoaded=yes` + ) + }) + + it('never reads shell lines as a connection failure', () => { + const failed = replay([ + { type: 'gates-changed', gates: gates() }, + { type: 'cache-read', generation: null }, + { type: 'download-failed', cause: { reason: 'connection-lost', hostCode: null } } + ]) + const report = buildConnectionDiagnosticsReport({ + hostName: 'Desk', + endpoint: 'ws://192.168.1.2:6768', + state: 'disconnected', + reconnectAttempts: 1, + lastConnectedAt: null, + platform: 'ios', + appVersion: '0.0.56', + entries: entries([ + ...failed.lines, + ...present(stall(replay(DOWNLOAD_OPEN.slice(0, 6)))), + ...present(stall(replay([]))), + ...present(bridgeDiagnosticLine({ kind: 'refused', refusal: 'oversized' })), + frameWithoutHostLine({ clientReady: false, snapshotReady: false }) + ]), + nowMs: 60_000 + }) + expect(report).toContain( + 'Likely cause: No single failure cause can be determined from the recorded events.' + ) + }) + + it('leaves an earlier connection failure as the likely cause', () => { + const timeout: ConnectionLogEntry = { + id: 'timeout', + ts: 500, + level: 'error', + code: 'connect-timeout', + message: 'WebSocket connect timeout' + } + const report = buildConnectionDiagnosticsReport({ + hostName: 'Desk', + endpoint: 'ws://192.168.1.2:6768', + state: 'disconnected', + reconnectAttempts: 1, + lastConnectedAt: null, + platform: 'ios', + appVersion: '0.0.56', + entries: [timeout, ...entries(replay(DOWNLOAD_OPEN).lines)], + nowMs: 60_000 + }) + expect(report).toContain( + 'Likely cause: The saved direct endpoint did not answer before the connection timeout.' + ) + }) +}) diff --git a/mobile/src/mobile-web-shell/shell-lifecycle-log.ts b/mobile/src/mobile-web-shell/shell-lifecycle-log.ts new file mode 100644 index 00000000000..0ee08fdd508 --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-lifecycle-log.ts @@ -0,0 +1,272 @@ +import type { BridgeErrorCapture } from './bridge/bridge-error-capture' +import { gateVerdict } from './mobile-web-shell-gates' +import type { + MobileWebShellSession, + MobileWebShellSessionEvent, + MobileWebShellSessionState +} from './mobile-web-shell-session-contract' +import type { ShellLifecycleLine } from './shell-lifecycle-line-recorder' +import { maskUserText } from './shell-log-path-masking' + +/** Session-side shell log lines: reducer steps, one-shot stalls and page-reported failures. */ + +const BUILD_ID_PREFIX_LENGTH = 8 + +/** + * How long a parked waiting state lasts before the log says so. Armed per state object, so for + * `ready` it starts at activation and outlasts the 10 s ready deadline, which starts at document load. + */ +export const SHELL_STALL_LOG_MS = 15_000 + +/** What the reducer does not keep but a log line needs; tracked beside the session, log-only. */ +export type ShellLogTrace = { + /** The flow whose cache read answered, so a stall can say whether it waits on the cache. */ + readonly cacheReadFlow: number | null + readonly openedFrom: 'download' | 'cache' | null + /** The page's `ready` asks for this document: more than one means it keeps refusing `init`. */ + readonly readyAsks: number + readonly documentLoaded: boolean +} + +export const EMPTY_SHELL_LOG_TRACE: ShellLogTrace = { + cacheReadFlow: null, + openedFrom: null, + readyAsks: 0, + documentLoaded: false +} + +export function traceShellStep( + trace: ShellLogTrace, + before: MobileWebShellSession, + event: MobileWebShellSessionEvent, + after: MobileWebShellSession +): ShellLogTrace { + if ('flow' in event && event.flow !== before.flow) { + return trace + } + switch (event.type) { + case 'cache-read': + return { ...trace, cacheReadFlow: event.flow } + case 'activated': + return { + ...trace, + readyAsks: 0, + documentLoaded: false, + openedFrom: before.state.kind === 'activating' ? before.state.source : null + } + case 'remounted': + case 'document-started': + return { ...trace, readyAsks: 0, documentLoaded: false } + case 'document-loaded': + return after.state.kind === 'ready' ? { ...trace, documentLoaded: true } : trace + case 'page-ready': + return after.state.kind === 'ready' ? { ...trace, readyAsks: trace.readyAsks + 1 } : trace + case 'gates-changed': + case 'manifest-read': + case 'fetch-progress': + case 'download-staged': + case 'download-failed': + case 'shell-failed': + case 'retry-pressed': + case 'page-painted': + case 'page-back-claim': + case 'page-ready-deadline': + case 'layout-changed': + return trace + } +} + +/** What one reducer step is worth logging; empty for most steps. `trace` is the post-step one. */ +export function shellSessionStepLines( + before: MobileWebShellSession, + event: MobileWebShellSessionEvent, + after: MobileWebShellSession, + trace: ShellLogTrace +): ShellLifecycleLine[] { + // A superseded flow's result changed nothing and says nothing about this one. + if ('flow' in event && event.flow !== before.flow) { + return [] + } + const lines = [...eventLines(before, event, trace)] + const transition = transitionLine(before.state, after.state) + if (transition !== null) { + lines.push(transition) + } + return lines +} + +/** The one-shot warning for a session parked in a waiting state, or null when not parked. */ +export function shellStallLine( + state: MobileWebShellSessionState, + session: Pick, + trace: ShellLogTrace +): ShellLifecycleLine | null { + const seconds = `${SHELL_STALL_LOG_MS / 1000}s` + switch (state.kind) { + case 'checking': { + const gates = session.gates === null ? 'none' : gateVerdict(session.gates).kind + const cache = trace.cacheReadFlow === session.flow ? 'read' : 'unread' + return stallLine( + `Web shell: still checking host after ${seconds}`, + `gates=${gates} cache=${cache}` + ) + } + case 'fetching': + // Each progress tick is a new state and re-arms the timer, so this means no progress. + return stallLine( + `Web shell: download made no progress for ${seconds}`, + `assets=${state.completedAssets}/${state.totalAssets} bytes=${state.receivedBytes}/${state.totalBytes}` + ) + case 'activating': + return stallLine(`Web shell: still activating after ${seconds}`, `source=${state.source}`) + case 'ready': + return session.pagePainted + ? null + : stallLine( + `Web shell: page has not drawn after ${seconds}`, + `build=${buildPrefix(state.buildId)} page ready=${yesNo(session.pageReady)} readyAsks=${trace.readyAsks} documentLoaded=${yesNo(trace.documentLoaded)}` + ) + case 'native-route': + case 'wall': + case 'failed': + case 'offline': + return null + } +} + +export function pageFaultLine(error: BridgeErrorCapture): ShellLifecycleLine { + const firstLine = error.message.split(/\r?\n/, 1)[0] ?? '' + return pageLine( + 'warn', + 'Web shell: page faulted', + `${error.category}: ${maskUserText(firstLine)}` + ) +} + +/** The profile read behind `init` rejected, so no bridge host can be built for this screen. */ +export function hostProfileUnreadableLine(): ShellLifecycleLine { + return pageLine('warn', 'Web shell: host profile unreadable') +} + +function eventLines( + before: MobileWebShellSession, + event: MobileWebShellSessionEvent, + trace: ShellLogTrace +): ShellLifecycleLine[] { + if (event.type === 'download-failed') { + const code = event.cause.hostCode === null ? '' : ` code=${event.cause.hostCode}` + return [ + flowLine('warn', 'Web shell: app download failed', `reason=${event.cause.reason}${code}`) + ] + } + // Every page signal below is only accepted while a generation is on screen. + if (before.state.kind !== 'ready') { + return [] + } + switch (event.type) { + case 'shell-failed': + return [pageLine('warn', 'Web shell: page load failed', `reason=${event.reason}`)] + case 'page-painted': + // The one line a healthy open writes; `activatedMs` is flow start to activation, not to paint. + return before.pagePainted + ? [] + : [ + pageLine( + 'success', + 'Web shell: page painted', + `source=${trace.openedFrom ?? 'unknown'} build=${buildPrefix(before.state.buildId)} activatedMs=${before.state.elapsedMs}` + ) + ] + case 'page-ready-deadline': + return before.pageReady + ? [] + : [pageLine('warn', 'Web shell: page never said ready before the deadline')] + case 'document-started': + case 'document-loaded': + case 'page-ready': + case 'gates-changed': + case 'cache-read': + case 'manifest-read': + case 'fetch-progress': + case 'download-staged': + case 'activated': + case 'remounted': + case 'page-back-claim': + case 'layout-changed': + case 'retry-pressed': + // Healthy-path signals; the paint line or a stall line's detail stands in for them. + return [] + } +} + +function transitionLine( + before: MobileWebShellSessionState, + after: MobileWebShellSessionState +): ShellLifecycleLine | null { + if (before.kind === after.kind) { + return null + } + switch (after.kind) { + case 'checking': + return flowLine('info', 'Web shell: checking host') + case 'offline': + return flowLine('warn', 'Web shell: host unreachable and no app cached') + case 'fetching': + return flowLine( + 'info', + 'Web shell: downloading app', + `${after.totalAssets} assets, ${after.totalBytes} bytes` + ) + case 'activating': + // A cache activation is immediate and shows up as the paint line's source. + return after.source === 'download' + ? flowLine('info', 'Web shell: activating app', 'source=download') + : null + case 'ready': + case 'native-route': + // `ready` is logged at paint; native is every native screen's normal outcome. + return null + case 'wall': { + const side = 'side' in after.verdict ? ` side=${after.verdict.side}` : '' + return flowLine( + 'warn', + 'Web shell: blocked by version check', + `reason=${after.verdict.reason}${side}` + ) + } + case 'failed': + return flowLine( + 'error', + 'Web shell: failed', + `reason=${after.reason} retried=${after.retriedOnce ? 'yes' : 'no'}` + ) + } +} + +function buildPrefix(buildId: string): string { + return buildId.slice(0, BUILD_ID_PREFIX_LENGTH) +} + +function yesNo(value: boolean): string { + return value ? 'yes' : 'no' +} + +function flowLine( + level: ShellLifecycleLine['level'], + message: string, + detail?: string +): ShellLifecycleLine { + return { level, code: 'web-shell-flow', message, ...(detail === undefined ? {} : { detail }) } +} + +function pageLine( + level: ShellLifecycleLine['level'], + message: string, + detail?: string +): ShellLifecycleLine { + return { level, code: 'web-shell-page', message, ...(detail === undefined ? {} : { detail }) } +} + +function stallLine(message: string, detail: string): ShellLifecycleLine { + return { level: 'warn', code: 'web-shell-stalled', message, detail } +} diff --git a/mobile/src/mobile-web-shell/shell-log-path-masking.test.ts b/mobile/src/mobile-web-shell/shell-log-path-masking.test.ts new file mode 100644 index 00000000000..2e1986f9880 --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-log-path-masking.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' +import { maskUserText } from './shell-log-path-masking' + +describe('shell log path masking', () => { + it('masks absolute paths wherever they start, including spaced and Windows ones', () => { + expect(maskUserText("open '/var/mobile/Containers/a.db' failed")).toBe("open '[path]' failed") + expect(maskUserText('at f (/Users/me/secret.js:1)')).toBe('at f ([path])') + expect(maskUserText('bad `/Users/me/x.js` here')).toBe('bad `[path]` here') + expect(maskUserText('path="/home/bob/x" [/Users/bob/y]')).toBe('path="[path]" [[path]]') + expect(maskUserText('read C:\\Users\\bob\\x.txt and \\\\srv\\share\\y')).toBe( + 'read [path] and [path]' + ) + expect(maskUserText('key:~/notes.txt')).toBe('key:[path]') + expect(maskUserText('open C:\\Users\\John Doe\\proj\\x.ts failed')).toBe('open [path] failed') + expect(maskUserText('at /Users/Jane Q/My Proj/a.js and more')).toBe('at [path] and more') + expect(maskUserText('a,/Users/x/y')).toBe('a,[path]') + expect(maskUserText('Cannot read properties of undefined')).toBe( + 'Cannot read properties of undefined' + ) + }) +}) diff --git a/mobile/src/mobile-web-shell/shell-log-path-masking.ts b/mobile/src/mobile-web-shell/shell-log-path-masking.ts new file mode 100644 index 00000000000..4d0350757b7 --- /dev/null +++ b/mobile/src/mobile-web-shell/shell-log-path-masking.ts @@ -0,0 +1,15 @@ +const MASKED_TEXT_MAX_CHARS = 160 + +/** Absolute paths and file URLs masked, then clamped: page and schema text can quote user paths. */ +export function maskUserText(text: string): string { + const masked = text + .replace(/file:\/\/[^\s)'"`\]]+/g, '[path]') + // A following word that still holds a separator is the same path (`C:\Users\John Doe\x`). + .replace( + /(^|[\s'"`([=:,])(?:~?\/|[A-Za-z]:[\\/]|\\\\)[^\s)'"`\]]+(?: [^\s)'"`\]]*[\\/][^\s)'"`\]]*)*/g, + '$1[path]' + ) + return masked.length > MASKED_TEXT_MAX_CHARS + ? `${masked.slice(0, MASKED_TEXT_MAX_CHARS)}…` + : masked +} diff --git a/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.test.ts b/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.test.ts index 67bc07fba2c..13876aa0d53 100644 --- a/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.test.ts +++ b/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.test.ts @@ -45,6 +45,7 @@ import { useMobileWebShellBridge, type MobileWebShellBridgeView } from './use-mobile-web-shell-bridge' +import { connectionLogStore } from '../transport/persisted-connection-log-store' const ID = bridgeId(1) const DIRECTORY = '/caches/mobile-web/deadbeef/generations/a1b2' @@ -425,6 +426,24 @@ describe('the bridge channel', () => { await mounted.deliver(clientFrame({ type: 'ready' })) expect(mounted.posted).toEqual([]) }) + + it('logs a frame it dropped for want of a host once per session, naming what is missing', async () => { + doubles.client = null + const dropped = () => + connectionLogStore + .get('host-1') + .filter((entry) => entry.message === 'Web shell: page frame with no bridge host') + const before = dropped().length + const mounted = await mount(readyState('session-one')) + await mounted.deliver(clientFrame({ type: 'ready' })) + await mounted.deliver(clientFrame({ type: 'ready' })) + expect(dropped().slice(before)).toEqual([ + expect.objectContaining({ detail: 'client=null snapshot=ok' }) + ]) + await mounted.update(readyState('session-two')) + await mounted.deliver(clientFrame({ type: 'ready' })) + expect(dropped().slice(before)).toHaveLength(2) + }) }) describe('teardown', () => { diff --git a/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.ts b/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.ts index 1f20a1b81b8..0db8fd8af2b 100644 --- a/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.ts +++ b/mobile/src/mobile-web-shell/use-mobile-web-shell-bridge.ts @@ -5,6 +5,8 @@ import type { } from '../../modules/orca-mobile-web-shell/src' import { useHostClient } from '../transport/client-context' import { createBridgeDiagnosticReporter } from './bridge-diagnostic-log' +import { bridgeDiagnosticLine, frameWithoutHostLine } from './shell-bridge-log-lines' +import { recordShellLifecycleLine } from './shell-lifecycle-line-recorder' import type { BridgeInitRoute } from './bridge/bridge-envelope' import type { BridgeClearableRouteParam } from './bridge/bridge-route-update' import type { BridgeSafeAreaInsets } from './bridge/bridge-safe-area-insets' @@ -169,6 +171,10 @@ export function useMobileWebShellBridge(args: MobileWebShellBridgeArgs): MobileW // Held rather than depended on, for the reason every prop above is: the identity settles with the // client, and taking it as a dependency would tear a live page session down to re-read a string. const clientIdRef = useRef(clientId) + // Read only by the dropped-frame log line, which says which of the two a host waits on. + const clientRef = useRef(client) + // The session a dropped frame was last logged for, so a page re-asking `ready` logs once. + const droppedFrameLoggedRef = useRef(null) // Commit-phase and declared above the host's effect, so the host is built against what this // render passed: a native frame can land between a commit and a passive effect. // @@ -178,6 +184,7 @@ export function useMobileWebShellBridge(args: MobileWebShellBridgeArgs): MobileW useLayoutEffect(() => { argsRef.current = args clientIdRef.current = clientId + clientRef.current = client }) const snapshot = args.snapshot @@ -214,7 +221,12 @@ export function useMobileWebShellBridge(args: MobileWebShellBridgeArgs): MobileW ? Promise.reject(new BridgeViewGoneError()) : mounted.handle.postBridgeMessage(json) }, - onDiagnostic: createBridgeDiagnosticReporter(), + onDiagnostic: createBridgeDiagnosticReporter((diagnostic) => { + const line = bridgeDiagnosticLine(diagnostic) + if (line !== null) { + recordShellLifecycleLine(latest.hostId, line) + } + }), onNavigate: (href) => argsRef.current.onNavigate(href), onNavigateBack: () => argsRef.current.onNavigateBack(), onExternalLink: (url) => argsRef.current.onExternalLink(url), @@ -256,6 +268,20 @@ export function useMobileWebShellBridge(args: MobileWebShellBridgeArgs): MobileW (event: MobileWebShellBridgeMessageEvent) => { const mounted = hostRef.current if (mounted === null || mounted.sessionId !== sessionId) { + if ( + mounted === null && + sessionId !== null && + droppedFrameLoggedRef.current !== sessionId + ) { + droppedFrameLoggedRef.current = sessionId + recordShellLifecycleLine( + argsRef.current.hostId, + frameWithoutHostLine({ + clientReady: clientRef.current !== null, + snapshotReady: argsRef.current.snapshot !== null + }) + ) + } return } mounted.host.receive(event.nativeEvent.json) diff --git a/mobile/src/mobile-web-shell/use-mobile-web-shell-session.test.ts b/mobile/src/mobile-web-shell/use-mobile-web-shell-session.test.ts index df01ca7e70d..31502505594 100644 --- a/mobile/src/mobile-web-shell/use-mobile-web-shell-session.test.ts +++ b/mobile/src/mobile-web-shell/use-mobile-web-shell-session.test.ts @@ -67,6 +67,9 @@ const doubles = vi.hoisted((): Doubles => { }) vi.mock('expo-crypto', () => ({ getRandomBytes: (length: number) => new Uint8Array(length) })) +vi.mock('@react-native-async-storage/async-storage', () => ({ + default: { getItem: async () => null, setItem: async () => undefined } +})) vi.mock('expo-file-system', () => ({ Directory: class {}, File: class {}, Paths: { cache: '' } })) vi.mock('../transport/mobile-endpoint-supervisor-support', () => ({ encodeBase64Url: () => 'session-id' @@ -95,6 +98,8 @@ vi.mock('../transport/mobile-web-bundle-fetch', () => ({ import { BRIDGE_READY_RETRY_MAX_MS } from './bridge/bridge-client-init-handshake' import { PAGE_READY_DEADLINE_MS } from './mobile-web-shell-runtime' +import { SHELL_STALL_LOG_MS } from './shell-lifecycle-log' +import { connectionLogStore } from '../transport/persisted-connection-log-store' import { useMobileWebShellSession } from './use-mobile-web-shell-session' const HOST_ID = 'host-1' @@ -214,21 +219,28 @@ type Mounted = { handshakes: () => readonly boolean[] documentLoaded: () => void pageReady: () => void + pagePainted: () => void timers: ReturnType } +function live(timers: readonly ArmedTimer[]): ArmedTimer[] { + return timers.filter((timer) => !timer.cancelled) +} + async function mount(store: GenerationStore): Promise { const timers = createTimerSeam() const handle: { retry: () => void documentLoaded: () => void pageReady: () => void + pagePainted: () => void states: MobileWebShellSessionState[] handshakes: boolean[] } = { retry: () => {}, documentLoaded: () => {}, pageReady: () => {}, + pagePainted: () => {}, states: [], handshakes: [] } @@ -246,6 +258,7 @@ async function mount(store: GenerationStore): Promise { handle.retry = session.retry handle.documentLoaded = session.reportDocumentLoaded handle.pageReady = session.reportPageReady + handle.pagePainted = session.reportPagePainted handle.states.push(session.state) handle.handshakes.push(session.pageReady) return null @@ -266,6 +279,7 @@ async function mount(store: GenerationStore): Promise { handshakes: () => handle.handshakes, documentLoaded: () => handle.documentLoaded(), pageReady: () => handle.pageReady(), + pagePainted: () => handle.pagePainted(), timers } } @@ -471,7 +485,11 @@ describe('the wait for the page to speak', () => { await act(async () => { mounted.documentLoaded() }) - expect(mounted.timers.armed.map((timer) => timer.delayMs)).toEqual([PAGE_READY_DEADLINE_MS]) + // The other live timer is the log-only stall warning; the checking one was cancelled. + expect(live(mounted.timers.armed).map((timer) => timer.delayMs)).toEqual([ + SHELL_STALL_LOG_MS, + PAGE_READY_DEADLINE_MS + ]) await act(async () => { mounted.timers.fire() }) @@ -523,7 +541,9 @@ describe('the wait for the page to speak', () => { }) // The race is real on a device: the page's first frame crosses the bridge while the WebView's // own load callback is still in the native queue. - expect(mounted.timers.armed).toHaveLength(0) + expect( + mounted.timers.armed.filter((timer) => timer.delayMs === PAGE_READY_DEADLINE_MS) + ).toEqual([]) await act(async () => { mounted.tree.unmount() }) @@ -579,6 +599,74 @@ describe('the wait for the page to speak', () => { }) }) + it('logs one stall warning for a page that never draws, without moving the session', async () => { + const logged = () => connectionLogStore.get(HOST_ID) + const before = logged().length + const mounted = await ready() + await act(async () => { + mounted.documentLoaded() + mounted.pageReady() + }) + const stallTimers = live(mounted.timers.armed).filter( + (timer) => timer.delayMs === SHELL_STALL_LOG_MS + ) + // A page signal that leaves the cover up re-arms nothing. + expect(stallTimers).toHaveLength(1) + await act(async () => { + stallTimers[0]?.run() + }) + expect(mounted.states().at(-1)?.kind).toBe('ready') + expect(logged().slice(before)).toEqual([ + expect.objectContaining({ + message: 'Web shell: page has not drawn after 15s', + detail: expect.stringContaining('page ready=yes readyAsks=1 documentLoaded=yes') + }) + ]) + }) + + it('writes one line for a healthy open, on paint', async () => { + const before = connectionLogStore.get(HOST_ID).length + const mounted = await ready() + await act(async () => { + mounted.documentLoaded() + mounted.pageReady() + mounted.pagePainted() + }) + expect( + connectionLogStore + .get(HOST_ID) + .slice(before) + .map((entry) => `${entry.message} | ${entry.detail}`) + ).toEqual([expect.stringMatching(/^Web shell: page painted \| source=cache build=\S{1,8} /)]) + expect(live(mounted.timers.armed).filter((t) => t.delayMs === SHELL_STALL_LOG_MS)).toEqual([]) + await act(async () => { + mounted.tree.unmount() + }) + }) + + it("logs a stall for the first open's checking state, which no transition line covers", async () => { + const before = connectionLogStore.get(HOST_ID).length + const fake = createFakeStore() + const mounted = await mount(fake.store) + expect(mounted.states().at(-1)?.kind).toBe('checking') + const stallTimers = live(mounted.timers.armed).filter( + (timer) => timer.delayMs === SHELL_STALL_LOG_MS + ) + expect(stallTimers).toHaveLength(1) + await act(async () => { + stallTimers[0]?.run() + }) + expect(connectionLogStore.get(HOST_ID).slice(before)).toEqual([ + expect.objectContaining({ + message: 'Web shell: still checking host after 15s', + detail: 'gates=open cache=unread' + }) + ]) + await act(async () => { + mounted.tree.unmount() + }) + }) + it('cancels the armed deadline when the session it belongs to is torn down', async () => { const mounted = await ready() await act(async () => { diff --git a/mobile/src/mobile-web-shell/use-mobile-web-shell-session.ts b/mobile/src/mobile-web-shell/use-mobile-web-shell-session.ts index dca736d415c..0d917fdd564 100644 --- a/mobile/src/mobile-web-shell/use-mobile-web-shell-session.ts +++ b/mobile/src/mobile-web-shell/use-mobile-web-shell-session.ts @@ -12,6 +12,14 @@ import { } from './mobile-web-shell-runtime' import { readMobileWebShellReachability } from './mobile-web-shell-reachability' import { shellPageBackClaimed } from './shell-page-back-claim' +import { recordShellLifecycleLine } from './shell-lifecycle-line-recorder' +import { + EMPTY_SHELL_LOG_TRACE, + SHELL_STALL_LOG_MS, + shellSessionStepLines, + shellStallLine, + traceShellStep +} from './shell-lifecycle-log' import { shellPageFrame, type ShellPageFrame } from './shell-page-frame' import { createMobileWebShellSession, @@ -102,6 +110,9 @@ export function useMobileWebShellSession(args: { // Cancelled on the same bump, for the same reason: an armed deadline belongs to the generation it // was armed under, and the epoch check alone would leave a real timer alive until it fired. const timersRef = useRef void>>(new Set()) + // The log's host for `dispatch`, which is stable; moved with the session rebuild below. + const hostIdRef = useRef(hostId) + const logTraceRef = useRef(EMPTY_SHELL_LOG_TRACE) const runEffectRef = useRef< ((epoch: number, flow: number, effect: MobileWebShellSessionEffect) => void) | null >(null) @@ -111,6 +122,11 @@ export function useMobileWebShellSession(args: { return } const stepped = reduceMobileWebShellSession(sessionRef.current, event) + const trace = traceShellStep(logTraceRef.current, sessionRef.current, event, stepped.session) + logTraceRef.current = trace + for (const line of shellSessionStepLines(sessionRef.current, event, stepped.session, trace)) { + recordShellLifecycleLine(hostIdRef.current, line) + } sessionRef.current = stepped.session setState(stepped.session.state) setPageReady(stepped.session.pageReady) @@ -218,6 +234,8 @@ export function useMobileWebShellSession(args: { useEffect(() => { // A new host is a new session: the old one's latches, cache handle and in-flight work all go. invalidate() + hostIdRef.current = hostId + logTraceRef.current = EMPTY_SHELL_LOG_TRACE sessionRef.current = createMobileWebShellSession(routePathname, wide) startedAtRef.current = runtime.now() setState(sessionRef.current.state) @@ -226,6 +244,20 @@ export function useMobileWebShellSession(args: { return invalidate }, [hostId, invalidate, routePathname, runtime]) + // Log-only: one warning per parked state object, cancelled by any state, paint, host or route change. + // `CHECKING` is one shared object, so checking-to-checking steps keep the first open's timer. + useEffect(() => { + if (shellStallLine(state, sessionRef.current, logTraceRef.current) === null) { + return + } + return runtime.setTimer(() => { + const line = shellStallLine(state, sessionRef.current, logTraceRef.current) + if (line !== null) { + recordShellLifecycleLine(hostIdRef.current, line) + } + }, SHELL_STALL_LOG_MS) + }, [hostId, pageFrame, routePathname, runtime, state]) + useEffect(() => { dispatch(epochRef.current, { type: 'layout-changed', wide }) }, [dispatch, wide]) diff --git a/mobile/src/session/MobileNativeChatMessageActionsSheet.tsx b/mobile/src/session/MobileNativeChatMessageActionsSheet.tsx index 4d9cf71c6ba..b8f1fa908de 100644 --- a/mobile/src/session/MobileNativeChatMessageActionsSheet.tsx +++ b/mobile/src/session/MobileNativeChatMessageActionsSheet.tsx @@ -1,11 +1,11 @@ import { useRef, useState } from 'react' import { Alert, Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native' -import { useSafeAreaInsets } from 'react-native-safe-area-context' import { Copy, TextSelect, X } from 'lucide-react-native' import type { NativeChatMessage } from '../../../src/shared/native-chat-types' import { ActionSheetContent } from '../components/ActionSheetModal' import { BottomDrawer } from '../components/BottomDrawer' import { useClipboardWriter } from '../platform/clipboard' +import { useWindowSafeAreaInsets } from '../layout/side-safe-area' import { colors, spacing, typography } from '../theme/mobile-theme' import { nativeChatMessagePlainText } from './mobile-native-chat-message-plain-text' @@ -75,10 +75,20 @@ function SelectTextScreen({ text: string onClose: () => void }): React.JSX.Element { - const insets = useSafeAreaInsets() + const insets = useWindowSafeAreaInsets() return ( - + Select text void onFailed: () => void }) { - const insets = useSafeAreaInsets() + const insets = useWindowSafeAreaInsets() return ( - + {title} diff --git a/mobile/src/session/mobile-native-chat-merged-snapshot-parity.test.ts b/mobile/src/session/mobile-native-chat-merged-snapshot-parity.test.ts index 89b22ac3461..985fd2da711 100644 --- a/mobile/src/session/mobile-native-chat-merged-snapshot-parity.test.ts +++ b/mobile/src/session/mobile-native-chat-merged-snapshot-parity.test.ts @@ -209,7 +209,7 @@ const range = (from: number, to: number): number[] => describe.each(['after-end', 'during-turn'] as const)( 'a short chat, the send while stopping written %s: merged frames draw as a fresh snapshot does', (sendWhileStopping) => { - const journal = stopJournal(8, sendWhileStopping) + const journal = stopJournal(2, sendWhileStopping) it('draws the send the exit took back with its stop row, from the frame that took it back', () => { const fresh = drawn( @@ -221,7 +221,7 @@ describe.each(['after-end', 'during-turn'] as const)( ]) }) - // Exhaustive frame enumeration shares CI CPUs with the typechecks. + // Both history turns revise status; one completes and the other is interrupted. it.each([1, 2, 3])( 'from every subscribe point, %i row(s) per frame', (rowsPerFrame) => { diff --git a/mobile/src/test-support/rpc-recording/golden-value-pool.ts b/mobile/src/test-support/rpc-recording/golden-value-pool.ts index 03a8fa04c23..f869e3fca8a 100644 --- a/mobile/src/test-support/rpc-recording/golden-value-pool.ts +++ b/mobile/src/test-support/rpc-recording/golden-value-pool.ts @@ -89,9 +89,12 @@ export function internRecording(recording: Recording): { } { const pool: ValuePool = {} const canonical = new Map() + // Checkpoints repeat their append-only histories; hash each distinct entry once. + const hashes = new Map() const intern = (entry: RecordedValue, at: string): string => { const json = canonicalJson(entry) - const hash = canonicalJsonHash(json) + const hash = hashes.get(json) ?? canonicalJsonHash(json) + hashes.set(json, hash) const seen = canonical.get(hash) if (seen !== undefined && seen !== json) { throw new Error(`Golden value hash collision at ${hash} (${at})`) diff --git a/mobile/src/transport/types.ts b/mobile/src/transport/types.ts index d9c9271c79d..4d83a5cb762 100644 --- a/mobile/src/transport/types.ts +++ b/mobile/src/transport/types.ts @@ -64,6 +64,11 @@ export type ConnectionDiagnosticCode = | 'direct-connected' | 'relay-credential-unavailable' | 'host-open-failed' + // OTA web shell lifecycle; diagnostics only, never read as connection failures. + | 'web-shell-flow' + | 'web-shell-page' + | 'web-shell-stalled' + | 'web-shell-bridge' export type ConnectionLogEntry = { id: string diff --git a/mobile/src/worktree/host-workspace-list-states.tsx b/mobile/src/worktree/host-workspace-list-states.tsx index a9449da2f5b..8e5d517fcf9 100644 --- a/mobile/src/worktree/host-workspace-list-states.tsx +++ b/mobile/src/worktree/host-workspace-list-states.tsx @@ -34,10 +34,10 @@ export function HostWorkspaceListStates( {props.search - ? 'No matching worktrees' + ? 'No matching workspaces' : props.activeFilterCount > 0 - ? 'No worktrees match filters' - : 'No worktrees'} + ? 'No workspaces match filters' + : 'No workspaces'} ) diff --git a/package.json b/package.json index c444b918881..b6707fa2677 100644 --- a/package.json +++ b/package.json @@ -115,9 +115,7 @@ "build:release:parallel": "pnpm run build:wsl-guest && pnpm run build:native && pnpm run verify:computer-native && pnpm run build:cli && pnpm run build:electron-vite:parallel && pnpm run verify:built-skills-cli && pnpm run build:web-from-renderer && pnpm run build:mobile-web", "postinstall": "pnpm run build:packages && node config/scripts/rebuild-native-deps.mjs", "rebuild:electron": "node config/scripts/rebuild-native-deps.mjs", - "reclaim:electron-dists": "node config/scripts/reclaim-electron-dists.mjs", - "reclaim:dev-bundles": "node config/scripts/reclaim-dev-electron-bundles.mjs", - "reclaim:worktree-space": "node config/scripts/reclaim-worktree-space.mjs", + "reclaim-space": "node config/scripts/reclaim-worktree-space.mjs", "rebuild:node": "pnpm rebuild node-pty", "build:unpack": "pnpm run build && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --dir", "build:win": "pnpm run build:desktop && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --win", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c814a86101..7bbcec05d80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,7 +173,7 @@ patchedDependencies: '@xterm/addon-serialize@0.15.0-beta.301': b35533fe252e7e45433150170348889f4e08a6c17f7017ac34ea694d831fec7f '@xterm/addon-webgl@0.20.0-beta.300': 5349718f2c19d9319a81552580bf9618ded9b00f3707a5c6cdbbae1910849cd9 '@xterm/xterm@6.1.0-beta.304': 9e1c200b2332745ef9e01e64696e9cda6f629937c37a0ee652323f25ccc8df5c - i18next-cli@1.74.2: 7955b89d3aa229f477408608d331f78c85148a85a85913729f89fac65ad8b207 + i18next-cli@1.74.2: 2ee32eef742615cd133e1a6940f027deb3723a8430bb7953bba3a6769b0aac47 lint-staged@16.4.0: 7333b3837f80a7fbd045964db6d76ba4fc118e49134bdbabb00585b6b7b60673 monaco-editor@0.55.1: 53b71f7d8a7c00b76ffdb73ce83d18e31692f193f5fafa6a390e48f5b3fdd636 node-pty@1.1.0: c2fe19cde7aef223738a7503ae429dd9b03432130b064aa64a601900b33c151f @@ -484,7 +484,7 @@ importers: version: 9.1.7 i18next-cli: specifier: 1.74.2 - version: 1.74.2(patch_hash=7955b89d3aa229f477408608d331f78c85148a85a85913729f89fac65ad8b207)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2) + version: 1.74.2(patch_hash=2ee32eef742615cd133e1a6940f027deb3723a8430bb7953bba3a6769b0aac47)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2) katex: specifier: ^0.16.47 version: 0.16.47 @@ -12939,7 +12939,7 @@ snapshots: husky@9.1.7: {} - i18next-cli@1.74.2(patch_hash=7955b89d3aa229f477408608d331f78c85148a85a85913729f89fac65ad8b207)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2): + i18next-cli@1.74.2(patch_hash=2ee32eef742615cd133e1a6940f027deb3723a8430bb7953bba3a6769b0aac47)(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2): dependencies: '@croct/json5-parser': 0.2.2 '@swc/core': 1.16.2 diff --git a/src/cli/browser-cookie-credentials-empty-value.test.ts b/src/cli/browser-cookie-credentials-empty-value.test.ts deleted file mode 100644 index 727348b9c06..00000000000 --- a/src/cli/browser-cookie-credentials-empty-value.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -const callMock = vi.fn() - -vi.mock('./runtime-client', () => { - class RuntimeClient { - call = callMock - getCliStatus = vi.fn() - openOrca = vi.fn() - } - - class RuntimeClientError extends Error { - readonly code: string - - constructor(code: string, message: string) { - super(message) - this.code = code - } - } - - class RuntimeRpcFailureError extends RuntimeClientError { - readonly response: unknown - - constructor(response: unknown) { - super('runtime_error', 'runtime_error') - this.response = response - } - } - - return { - RuntimeClient, - RuntimeClientError, - RuntimeRpcFailureError - } -}) - -import { main } from './index' -import { okFixture, queueFixtures } from './test-fixtures' - -// Why: CookieSet.value and SetCredentials.pass accept any string, empty included, -// while name and user require a non-empty one. -describe('orca cli cookie set and set credentials preserve an empty value', () => { - beforeEach(() => { - callMock.mockReset() - process.exitCode = undefined - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - it('passes an empty --value through to browser.cookie.set', async () => { - queueFixtures(callMock, okFixture('req_cookie_set', { success: true })) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - ['cookie', 'set', '--name', 'sid', '--value', '', '--worktree', 'all', '--json'], - '/tmp/not-an-orca-worktree' - ) - - expect(callMock).toHaveBeenCalledTimes(1) - expect(callMock).toHaveBeenCalledWith('browser.cookie.set', { - name: 'sid', - value: '', - worktree: undefined - }) - }) - - it('passes an empty --pass through to browser.setCredentials', async () => { - queueFixtures(callMock, okFixture('req_set_credentials', { success: true })) - vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main( - ['set', 'credentials', '--user', 'token', '--pass', '', '--worktree', 'all', '--json'], - '/tmp/not-an-orca-worktree' - ) - - expect(callMock).toHaveBeenCalledTimes(1) - expect(callMock).toHaveBeenCalledWith('browser.setCredentials', { - user: 'token', - pass: '', - worktree: undefined - }) - }) - - it('still rejects an empty --name before RPC dispatch', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const priorExitCode = process.exitCode - - await main( - ['cookie', 'set', '--name', '', '--value', 'x', '--worktree', 'all'], - '/tmp/not-an-orca-worktree' - ) - - expect(callMock).not.toHaveBeenCalled() - expect(errorSpy.mock.calls.flat().join('\n')).toContain('Missing required --name') - expect(process.exitCode).toBe(1) - - process.exitCode = priorExitCode - }) - - it('still rejects a missing --pass before RPC dispatch', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - const priorExitCode = process.exitCode - - await main( - ['set', 'credentials', '--user', 'token', '--worktree', 'all'], - '/tmp/not-an-orca-worktree' - ) - - expect(callMock).not.toHaveBeenCalled() - expect(errorSpy.mock.calls.flat().join('\n')).toContain('Missing required --pass') - expect(process.exitCode).toBe(1) - - process.exitCode = priorExitCode - }) -}) diff --git a/src/cli/browser-storage-empty-value.test.ts b/src/cli/browser-empty-values.test.ts similarity index 61% rename from src/cli/browser-storage-empty-value.test.ts rename to src/cli/browser-empty-values.test.ts index b93145e93e4..23b110d46cd 100644 --- a/src/cli/browser-storage-empty-value.test.ts +++ b/src/cli/browser-empty-values.test.ts @@ -132,3 +132,82 @@ describe('orca cli storage set preserves an empty value', () => { process.exitCode = priorExitCode }) }) + +// Why: CookieSet.value and SetCredentials.pass accept any string, empty included, +// while name and user require a non-empty one. +describe('orca cli cookie set and set credentials preserve an empty value', () => { + beforeEach(() => { + callMock.mockReset() + process.exitCode = undefined + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('passes an empty --value through to browser.cookie.set', async () => { + queueFixtures(callMock, okFixture('req_cookie_set', { success: true })) + vi.spyOn(console, 'log').mockImplementation(() => {}) + + await main( + ['cookie', 'set', '--name', 'sid', '--value', '', '--worktree', 'all', '--json'], + '/tmp/not-an-orca-worktree' + ) + + expect(callMock).toHaveBeenCalledTimes(1) + expect(callMock).toHaveBeenCalledWith('browser.cookie.set', { + name: 'sid', + value: '', + worktree: undefined + }) + }) + + it('passes an empty --pass through to browser.setCredentials', async () => { + queueFixtures(callMock, okFixture('req_set_credentials', { success: true })) + vi.spyOn(console, 'log').mockImplementation(() => {}) + + await main( + ['set', 'credentials', '--user', 'token', '--pass', '', '--worktree', 'all', '--json'], + '/tmp/not-an-orca-worktree' + ) + + expect(callMock).toHaveBeenCalledTimes(1) + expect(callMock).toHaveBeenCalledWith('browser.setCredentials', { + user: 'token', + pass: '', + worktree: undefined + }) + }) + + it('still rejects an empty --name before RPC dispatch', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const priorExitCode = process.exitCode + + await main( + ['cookie', 'set', '--name', '', '--value', 'x', '--worktree', 'all'], + '/tmp/not-an-orca-worktree' + ) + + expect(callMock).not.toHaveBeenCalled() + expect(errorSpy.mock.calls.flat().join('\n')).toContain('Missing required --name') + expect(process.exitCode).toBe(1) + + process.exitCode = priorExitCode + }) + + it('still rejects a missing --pass before RPC dispatch', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const priorExitCode = process.exitCode + + await main( + ['set', 'credentials', '--user', 'token', '--worktree', 'all'], + '/tmp/not-an-orca-worktree' + ) + + expect(callMock).not.toHaveBeenCalled() + expect(errorSpy.mock.calls.flat().join('\n')).toContain('Missing required --pass') + expect(process.exitCode).toBe(1) + + process.exitCode = priorExitCode + }) +}) diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index 2d279d38c29..1687cc215ad 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -41,7 +41,6 @@ vi.mock('child_process', async () => { }) import { COMMAND_SPECS, main } from './index' -import { formatFlagHelp } from './help' import { GLOBAL_FLAGS, specPaths } from './args' import { okFixture, queueFixtures } from './test-fixtures' @@ -460,97 +459,15 @@ describe('nested command group help', () => { ) }) -describe('orca root help', () => { - it('advertises machine-readable agent discovery', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main([], '/tmp/repo') - - expect(logSpy.mock.calls.flat().join('\n')).toContain('agent-context') - logSpy.mockRestore() - }) - - it('advertises host-local account management', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main([], '/tmp/repo') - - expect(logSpy.mock.calls.flat().join('\n')).toContain( - 'account add Add a managed agent account on this Orca host' - ) - expect(logSpy.mock.calls.flat().join('\n')).toContain( - 'account list List managed agent accounts on this Orca host' - ) - logSpy.mockRestore() - }) - - it('labels retired coordinator scheduler commands at the root', async () => { +describe('orca help', () => { + it('prints root help without contacting the runtime', async () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) await main(['--help'], '/tmp/repo') - const output = String(logSpy.mock.calls[0]?.[0]) - expect(output).toContain( - 'orchestration coordinator-start Retired: load the current orchestration skill' - ) - expect(output).toContain( - 'orchestration coordinator-stop Retired: load the current orchestration skill' - ) - expect(output).not.toContain('Start the legacy automatic coordinator loop') - expect(output).not.toContain('Stop the legacy automatic coordinator loop') - logSpy.mockRestore() - }) - - it('advertises computer-use capabilities discovery', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - - await main(['--help'], '/tmp/repo') - - expect(logSpy.mock.calls[0][0]).toContain( - 'computer capabilities Show computer-use provider capabilities' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'computer permissions Show or open computer-use permission setup' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'computer press-key Press a single key such as Return or Escape' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'project setup-existing-folder Make a project available on a host by importing an existing folder' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'project setup-create Create independent project host setup metadata' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'project setup-update Update project host setup metadata' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'project setup-delete Remove a project host setup' - ) - expect(logSpy.mock.calls[0][0]).toContain('Agent Sessions And Worktrees:') - expect(logSpy.mock.calls[0][0]).toContain( - 'orca agent launch --agent codex --model gpt-6-astra --effort high' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'orchestration worker-start Start a supervised worker locally or on a connected Orca server' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'orchestration ask Ask the coordinator a blocking question' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'orchestration worker-abandon Fence an uncertain worker without claiming it stopped' - ) - expect(logSpy.mock.calls[0][0]).toContain( - "orchestration worker-release Release a settled worker's terminal after archiving its output" - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'orchestration worker-retain Keep a worker terminal live for debugging' - ) - expect(logSpy.mock.calls[0][0]).toContain( - 'orchestration worker-list Report worker terminal resource accounting' - ) - expect(logSpy.mock.calls[0][0]).not.toContain('orchestration worker-cleanup') + expect(logSpy.mock.calls[0][0]).toContain('Usage: orca ') expect(callMock).not.toHaveBeenCalled() + logSpy.mockRestore() }) it('progressively discloses Linear commands', async () => { @@ -559,10 +476,8 @@ describe('orca root help', () => { await main(['--help'], '/tmp/repo') const rootHelp = String(logSpy.mock.calls[0][0]) - expect(rootHelp).toContain('Linear:') - expect(rootHelp).toContain('linear Read and write Linear issues for agents') + expect(rootHelp).toContain('linear') expect(rootHelp).not.toContain('linear issue') - expect(rootHelp).not.toContain('linear search') logSpy.mockClear() await main(['linear', '--help'], '/tmp/repo') @@ -570,172 +485,15 @@ describe('orca root help', () => { const groupHelp = String(logSpy.mock.calls[0][0]) expect(groupHelp).toContain('orca linear') expect(groupHelp).toContain('issue') - expect(groupHelp).toContain('search') expect(groupHelp).not.toContain('--comments') - expect(groupHelp).not.toContain('--attachments') logSpy.mockClear() await main(['linear', 'issue', '--help'], '/tmp/repo') const issueHelp = String(logSpy.mock.calls[0][0]) - expect(issueHelp).toContain('orca linear issue []') - expect(issueHelp).toContain('--comments Include threaded Linear comments') - expect(issueHelp).toContain('--attachments Include attachment metadata and URLs') - expect(issueHelp).toContain('--activity Include issue field-change history') - expect(issueHelp).toContain('--workspace Connected Linear workspace id') - expect(issueHelp).toContain('--id Linear issue key, id, or URL') - - logSpy.mockClear() - await main(['linear', 'search', '--help'], '/tmp/repo') - - const searchHelp = String(logSpy.mock.calls[0][0]) - expect(searchHelp).toContain('orca linear search ') - expect(searchHelp).toContain('--workspace Connected Linear workspace id, or all') - expect(searchHelp).toContain('--query Text to search across Linear issues') - - logSpy.mockClear() - await main(['linear', 'list-issues', '--help'], '/tmp/repo') - - const listIssuesHelp = String(logSpy.mock.calls[0][0]) - expect(listIssuesHelp).toContain( - '--cursor Opaque cursor from a previous list-issues page; issued cursors bind the workspace, raw Linear cursors need --workspace' - ) - expect(listIssuesHelp).toContain('--workspace Connected Linear workspace id, or all') - expect(listIssuesHelp).toContain('0=none, 1=urgent, 2=high, 3=medium, 4=low') - expect(listIssuesHelp).toContain( - '--limit Max issues to return; omit to return every match' - ) - expect(listIssuesHelp).not.toContain('Line cursor from a previous read') - expect(callMock).not.toHaveBeenCalled() - }) - - it('documents the machine-readable terminal topology opt-in', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['terminal', 'list', '--help'], '/tmp/repo') - - const help = String(logSpy.mock.calls[0][0]) - expect(help).toContain('[--include-visual-layouts] [--json]') - expect(help).toContain('--include-visual-layouts Include tab and pane topology in JSON output') - expect(help).toContain('JSON omits visualLayouts by default') - expect(callMock).not.toHaveBeenCalled() - }) - - it('describes worker-read cursors as opaque', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['orchestration', 'worker-read', '--help'], '/tmp/repo') - - const help = String(logSpy.mock.calls[0][0]) - expect(help).toContain( - '--cursor Opaque cursor returned by a previous worker-read page' - ) - expect(help).not.toContain('Line cursor from a previous read') - expect(callMock).not.toHaveBeenCalled() - }) - - it('describes worker-list cursors as opaque page cursors', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['orchestration', 'worker-list', '--help'], '/tmp/repo') - - const help = String(logSpy.mock.calls[0][0]) - expect(help).toContain('[--cursor ]') - expect(help).toContain('--cursor Opaque page cursor copied from page.nextCursor') - expect(help).toContain('Continue with the opaque page.nextCursor value unchanged.') - expect(help).not.toContain('--cursor ') - expect(help).not.toContain('Line cursor from a previous read') - expect(callMock).not.toHaveBeenCalled() - }) - - it('advertises Linear issue linking on worktree create and set help', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['worktree', 'create', '--help'], '/tmp/repo') - - expect(String(logSpy.mock.calls[0][0])).toContain('--linear-issue ') - - logSpy.mockClear() - await main(['worktree', 'set', '--help'], '/tmp/repo') - - const setHelp = String(logSpy.mock.calls[0][0]) - expect(setHelp).toContain('--linear-issue ') - expect(setHelp).toContain('--linear-issue Linked Linear issue identifier or URL') - expect(callMock).not.toHaveBeenCalled() - }) - - it('advertises explicit orchestration task display labels', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['orchestration', 'task-create', '--help'], '/tmp/repo') - - const help = String(logSpy.mock.calls[0][0]) - expect(help).toContain('[--task-title ] [--display-name ]') - expect(help).toContain('--task-title Concise title for the orchestration task') - expect(help).toContain('--display-name UI label shown for dispatched worker rows') - expect(callMock).not.toHaveBeenCalled() - }) - - it('hides removed parent-workspace help and scopes create parent selectors', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['--help'], '/tmp/repo') - - const rootHelp = String(logSpy.mock.calls[0][0]) - expect(rootHelp).not.toContain('--parent-workspace') - expect(rootHelp).toContain('[--parent-worktree ] [--no-parent]') - expect(rootHelp).toContain('identity:') - - logSpy.mockClear() - await main(['worktree', 'create', '--help'], '/tmp/repo') - - const createHelp = String(logSpy.mock.calls[0][0]) - expect(createHelp).not.toContain('--parent-workspace') - expect(createHelp).not.toContain('checkout/workspace') - expect(createHelp).not.toContain('caller workspace') - expect(createHelp).not.toContain('current workspace') - expect(createHelp).not.toContain('active Orca workspace') - expect(createHelp).not.toContain('folderWorkspaceId') - expect(createHelp).toContain('folder:') - expect(createHelp).toContain('folder:') - expect(createHelp).toContain('worktree:') - expect(createHelp).toContain( - '--no-parent only affects Orca lineage; omit --base-branch to use the repo default base' - ) - - logSpy.mockClear() - await main(['worktree', 'set', '--help'], '/tmp/repo') - - const setHelp = String(logSpy.mock.calls[0][0]) - expect(setHelp).not.toContain('--parent-workspace') - expect(setHelp).not.toContain('folder:') - expect(formatFlagHelp('parent-worktree')).toContain('identity:') - expect(setHelp).not.toContain('worktree:') - expect(callMock).not.toHaveBeenCalled() - }) - - it('points both create commands at the agent launch for starting agents', async () => { - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - logSpy.mockClear() - - await main(['worktree', 'create', '--help'], '/tmp/repo') - - expect(String(logSpy.mock.calls[0][0])).toContain('This creates a new checkout.') - expect(String(logSpy.mock.calls[0][0])).toContain( - 'For a fresh agent in an existing worktree, use `orca agent launch`' - ) - - logSpy.mockClear() - await main(['terminal', 'create', '--help'], '/tmp/repo') - - const terminalHelp = String(logSpy.mock.calls[0][0]) - expect(terminalHelp).toContain('To start an agent, use `orca agent launch`') + expect(issueHelp).toContain('orca linear issue') + expect(issueHelp).toContain('--comments') expect(callMock).not.toHaveBeenCalled() + logSpy.mockRestore() }) }) diff --git a/src/cli/specs/file.test.ts b/src/cli/specs/file.test.ts deleted file mode 100644 index 9ef94f20614..00000000000 --- a/src/cli/specs/file.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { findCommandSpec } from '../args' -import { formatCommandHelp } from '../help' -import { COMMAND_SPECS } from './index' - -function optionLine(path: string[], flag: string): string | undefined { - const spec = findCommandSpec(COMMAND_SPECS, path) - if (!spec) { - throw new Error(`no spec for ${path.join(' ')}`) - } - return formatCommandHelp(spec) - .split('\n') - .find((line) => line.trimStart().startsWith(`--${flag} `)) -} - -describe('orca file command help', () => { - it.each([['open'], ['diff'], ['open-changed']])( - 'file %s describes --focus as bringing the user to the file', - (command) => { - expect(optionLine(['file', command], 'focus')).toBe( - " --focus Bring the user to the file (switches Orca's window to its worktree)" - ) - } - ) - - it('leaves terminal create --focus describing its terminal session', () => { - expect(optionLine(['terminal', 'create'], 'focus')).toBe( - ' --focus Reveal the created terminal session in Orca' - ) - }) -}) diff --git a/src/cli/specs/search.test.ts b/src/cli/specs/search.test.ts deleted file mode 100644 index 007da8c9fc3..00000000000 --- a/src/cli/specs/search.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { SEARCH_COMMAND_SPECS } from './search' -import { effectiveAllowedFlags, findCommandSpec, GLOBAL_FLAGS } from '../args' -import { buildAgentContext } from '../agent-context' -import { suggestCommands } from '../command-suggestion' -import { HANDLER_COMMAND_KEYS } from '../dispatch' -import { formatCommandHelp, printHelp } from '../help' -import { ROOT_HELP_TEXT_PRIMARY } from '../root-help-text-primary' -import { ROOT_HELP_TEXT_SECONDARY } from '../root-help-text-secondary' -import { COMMAND_SPECS } from './index' -import { CLI_COMMAND_NAMES } from '../../main/startup/cli-command-names' - -const searchSpec = SEARCH_COMMAND_SPECS[0]! -const help = formatCommandHelp(searchSpec) - -describe('orca search command spec', () => { - it('is one command, not a group, because the query is a bare positional', () => { - expect(SEARCH_COMMAND_SPECS).toHaveLength(1) - expect(searchSpec.path).toEqual(['search']) - expect(searchSpec.positionalArgs).toEqual(['query']) - }) - - it('is registered in the live spec table, the dispatcher and the launch redirect', () => { - expect(COMMAND_SPECS).toContain(searchSpec) - expect(HANDLER_COMMAND_KEYS.has('search')).toBe(true) - expect(CLI_COMMAND_NAMES).toContain('search') - }) - - it('accepts exactly the flags that map onto the search contract', () => { - expect([...searchSpec.allowedFlags].sort()).toEqual([ - 'agent', - 'cursor', - 'debug', - 'environment', - 'fresh', - 'help', - 'index-status', - 'json', - 'limit', - 'pairing-code', - 'path', - 'query', - 'scope', - 'since', - 'sort' - ]) - }) - - it('keeps --agent and --path repeatability scoped to search', () => { - expect(searchSpec.repeatableFlags).toEqual(['agent', 'path']) - for (const spec of COMMAND_SPECS) { - if (spec !== searchSpec) { - expect(spec.repeatableFlags ?? []).not.toContain('agent') - expect(spec.repeatableFlags ?? []).not.toContain('path') - } - } - }) - - it('does not accept or advertise browser page targeting', () => { - expect(effectiveAllowedFlags(searchSpec)).not.toContain('page') - expect(help).not.toContain('--page') - }) - - it('describes every search flag rather than falling back to the bare name', () => { - const searchOnly = searchSpec.allowedFlags.filter((flag) => !GLOBAL_FLAGS.includes(flag)) - expect(searchOnly).toHaveLength(11) - for (const flag of searchOnly) { - expect(help).toContain(`--${flag}`) - expect(help.split('\n')).not.toContain(` --${flag}`) - } - }) - - it('describes --agent as a search filter, not a terminal agent to launch', () => { - expect(help).toContain('Restrict to one agent; repeat for several') - expect(help).not.toContain('TUI agent') - }) - - it('tells the reader to quote a multi-word query', () => { - expect(searchSpec.notes?.join('\n')).toContain('Quote a multi-word query') - }) - - it('states that it searches one host and offers no all-computers search', () => { - expect(searchSpec.notes?.join('\n')).toContain('There is no all-computers search.') - }) - - it('shows both the query and the index report in its usage', () => { - expect(searchSpec.usage).toContain('orca search ') - expect(searchSpec.usage).toContain('orca search --index-status') - }) -}) - -describe('orca search discovery surfaces', () => { - it('is listed in the root help', () => { - expect(ROOT_HELP_TEXT_PRIMARY).toContain('Agent Sessions:') - expect(ROOT_HELP_TEXT_PRIMARY).toContain( - ' search Search the full text of agent sessions on one Orca host' - ) - expect(ROOT_HELP_TEXT_SECONDARY).toContain(' orca search --index-status [--json]') - }) - - it('prints its own help for `orca search --help`', () => { - const lines: string[] = [] - const restore = console.log - console.log = (value: unknown) => void lines.push(String(value)) - try { - printHelp(COMMAND_SPECS, ['search']) - } finally { - console.log = restore - } - expect(lines.join('\n')).toContain('Usage: orca search ') - }) - - it('resolves for dispatch', () => { - expect(findCommandSpec(COMMAND_SPECS, ['search'])).toBe(searchSpec) - }) - - it('exposes the command to agent discovery with its positional and flags', () => { - const command = buildAgentContext(COMMAND_SPECS).commands.find( - (entry) => entry.command === 'search' - ) - expect(command?.positionalArgs).toEqual(['query']) - expect(command?.flags).toContain('index-status') - expect(command?.flags).not.toContain('page') - }) - - it('is offered as a suggestion for a near-miss command', () => { - expect(suggestCommands(COMMAND_SPECS, ['serch'])).toContain('search') - }) -}) diff --git a/src/cli/terminal-format-draft.test.ts b/src/cli/terminal-format-draft.test.ts deleted file mode 100644 index 3f38e48dd21..00000000000 --- a/src/cli/terminal-format-draft.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { formatTerminalRead } from './terminal-format' - -describe('formatTerminalRead draft separation', () => { - it('labels composer drafts outside terminal output', () => { - const output = formatTerminalRead({ - terminal: { - handle: 'term_1', - status: 'running', - tail: ['Build passed', '❯'], - truncated: false, - nextCursor: null, - source: 'screen', - draft: 'proceed with the release' - } - }) - - expect(output).toContain('draft: "proceed with the release"') - expect(output).toContain('\n\nBuild passed\n❯') - }) -}) diff --git a/src/cli/terminal-format.test.ts b/src/cli/terminal-format.test.ts index ee0a985441e..58904929a55 100644 --- a/src/cli/terminal-format.test.ts +++ b/src/cli/terminal-format.test.ts @@ -7,6 +7,7 @@ import type { import { formatTerminalClose, formatTerminalFocus, + formatTerminalRead, formatTerminalSend, formatTerminalShow, formatTerminalWait @@ -268,3 +269,22 @@ describe('blocked-reason rendering against a mixed-version host', () => { ) }) }) + +describe('formatTerminalRead draft separation', () => { + it('labels composer drafts outside terminal output', () => { + const output = formatTerminalRead({ + terminal: { + handle: 'term_1', + status: 'running', + tail: ['Build passed', '❯'], + truncated: false, + nextCursor: null, + source: 'screen', + draft: 'proceed with the release' + } + }) + + expect(output).toContain('draft: "proceed with the release"') + expect(output).toContain('\n\nBuild passed\n❯') + }) +}) diff --git a/src/main/acp/acp-conversation-copies.ts b/src/main/acp/acp-conversation-copies.ts new file mode 100644 index 00000000000..ed453a3f82c --- /dev/null +++ b/src/main/acp/acp-conversation-copies.ts @@ -0,0 +1,37 @@ +import { randomUUID } from 'node:crypto' +import type { StructuredAgentSessionAdapter } from '../native-chat/agent-session-wire/structured-agent-session-adapter' +import { ProviderAcquisitionStarts } from '../provider-process/provider-acquisition-starts' +import { copyAcpConversation } from './acp-conversation-copy' +import type { AcpStructuredConnection } from './acp-structured-connection' +import type { AcpStructuredSessionAdapterDeps } from './acp-structured-session-adapter-deps' + +/** Copy children have their own cleanup lifecycle; they never gate the original chat's start. */ +export class AcpConversationCopies { + private readonly children = new ProviderAcquisitionStarts() + private readonly shutdown = new AbortController() + private readonly pending = new Set>() + + constructor(private readonly deps: AcpStructuredSessionAdapterDeps) {} + + copy: NonNullable = async (input) => { + const copy = copyAcpConversation(this.deps, input, { + signal: this.shutdown.signal, + track: (connection) => this.children.retainFailed(randomUUID(), connection) + }) + this.pending.add(copy) + try { + return await copy + } finally { + this.pending.delete(copy) + } + } + + async closeAll(): Promise { + this.shutdown.abort() + await Promise.allSettled(this.pending) + const closed = await Promise.all( + this.children.failedSessionIds().map((id) => this.children.stopFailed(id)) + ) + return closed.every(Boolean) + } +} diff --git a/src/main/acp/acp-conversation-copy.test.ts b/src/main/acp/acp-conversation-copy.test.ts new file mode 100644 index 00000000000..25fc9eed346 --- /dev/null +++ b/src/main/acp/acp-conversation-copy.test.ts @@ -0,0 +1,137 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { acpLaunchSpecFor } from './acp-launch-specs' +import { copyAcpConversation } from './acp-conversation-copy' +import { + FakeAcpChild, + openAcpAdapterRig, + type AcpAdapterRig, + PROVIDER_SESSION +} from './acp-structured-adapter.test-support' + +vi.mock('../native-chat/session-file-resolver', () => ({ + resolveSessionFilePath: async () => '/account-a/sessions/copy-1.jsonl' +})) +const FORKS = { agentCapabilities: { loadSession: true, sessionCapabilities: { fork: {} } } } +const input = { + identity: { + sessionId: 'orca-chat-1', + workspaceId: 'workspace-1', + hostId: 'local', + agent: 'omp', + providerHandle: null + }, + providerSessionId: PROVIDER_SESSION, + accountHome: { variable: 'PI_CODING_AGENT_DIR', path: '/account-a' } +} +const rigs: AcpAdapterRig[] = [] +afterEach(async () => { + await Promise.all(rigs.splice(0).map((rig) => rig.adapter.closeAll())) +}) + +async function open(agent: 'omp' | 'opencode' = 'omp') { + const rig = await openAcpAdapterRig({ + spec: acpLaunchSpecFor(agent)!, + initialize: FORKS, + script: (agent) => + agent.on('session/fork', (frame) => agent.reply(frame, { sessionId: 'copy-1' })) + }) + rigs.push(rig) + return rig +} + +describe('copying a chat conversation for the CLI', () => { + it.each(['omp', 'opencode'] as const)( + 'copies a stopped and running %s chat without loading or prompting its original', + async (agent) => { + const rig = await open(agent) + const request = { + ...input, + identity: { ...input.identity, agent }, + accountHome: + agent === 'omp' + ? input.accountHome + : { kind: 'opencode' as const, locator: { kind: 'unmanaged' as const } } + } + const expected = { + sessionId: 'copy-1', + ...(agent === 'omp' ? { filePath: '/account-a/sessions/copy-1.jsonl' } : {}) + } + await expect(rig.adapter.copyConversation(request)).resolves.toEqual(expected) + expect(rig.child().agent.frames.map((frame) => frame.method)).toEqual([ + 'initialize', + 'session/fork' + ]) + expect(rig.sent('session/fork')[0]?.params).toEqual({ + sessionId: PROVIDER_SESSION, + cwd: '/workspace/project', + mcpServers: [] + }) + expect(rig.child().closes).toBe(1) + await rig.acquire() + const chat = rig.child() + const frames = [...chat.agent.frames] + await expect(rig.adapter.copyConversation(request)).resolves.toEqual(expected) + expect(chat.agent.frames).toEqual(frames) + expect(chat.closes).toBe(0) + expect(rig.child()).not.toBe(chat) + expect(rig.child().closes).toBe(1) + } + ) + + it('permits another copy after unproven cleanup and retries the retained child at shutdown', async () => { + const rig = await openAcpAdapterRig({ spec: acpLaunchSpecFor('omp')!, initialize: FORKS }) + rigs.push(rig) + const copying = rig.adapter.copyConversation(input) + await vi.waitFor(() => expect(rig.sent('session/fork')).toHaveLength(1)) + const child = rig.child() + child.proveClose = async () => false + child.agent.reply(rig.sent('session/fork')[0]!, { sessionId: 'copy-1' }) + await expect(copying).resolves.toHaveProperty('sessionId', 'copy-1') + const next = rig.adapter.copyConversation(input) + await vi.waitFor(() => { + expect(rig.child()).not.toBe(child) + expect(rig.sent('session/fork')).toHaveLength(1) + }) + rig.child().agent.reply(rig.sent('session/fork')[0]!, { sessionId: 'copy-2' }) + await expect(next).resolves.toHaveProperty('sessionId', 'copy-2') + child.proveClose = async () => { + child.exit() + return true + } + await rig.adapter.closeAll() + expect(child.closes).toBe(2) + }) + + it('bounds an unanswered fork and closes its temporary process', async () => { + const children: FakeAcpChild[] = [] + const spec = acpLaunchSpecFor('omp')! + await expect( + copyAcpConversation( + { + spec, + readLaunch: async () => ({ + spec, + command: 'fake', + args: [], + cwd: '/repo', + env: {}, + envToDelete: [], + fullAccess: false, + resume: null + }), + connect: (...args: ConstructorParameters) => { + const child = new FakeAcpChild(...args) + child.agent.on('initialize', (frame) => + child.agent.reply(frame, { protocolVersion: 1, ...FORKS }) + ) + children.push(child) + return child + } + }, + input, + { timeoutMs: 10, track: () => {} } + ) + ).rejects.toThrow('OMP could not copy this chat') + expect(children.map((child) => child.closes)).toEqual([1]) + }) +}) diff --git a/src/main/acp/acp-conversation-copy.ts b/src/main/acp/acp-conversation-copy.ts new file mode 100644 index 00000000000..afe9ddee32a --- /dev/null +++ b/src/main/acp/acp-conversation-copy.ts @@ -0,0 +1,152 @@ +import { normalizeAgentProviderSession } from '../../shared/agent-session-resume' +import { + assertOpenCodeCopyUsesLocalStore, + OpenCodeCopyServerRouteError +} from '../../shared/opencode-launch-command' +// The chat protocol saves an independent conversation; its ordinary CLI opens that copy. +import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' +import { + agentSessionAccountHomesEqual, + type AgentSessionAccountHome +} from '../../shared/agent-session-account-home' +import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason' +import type { AiVaultConversationCopy } from '../../shared/ai-vault-resume-preparation' +import { resolveSessionFilePath } from '../native-chat/session-file-resolver' +import { resolveOmpSessionsDir } from '../ai-vault/omp-session-root' +import { AcpRpcError } from './acp-errors' +import { acpAgentName } from './acp-structured-agent-definitions' +import type { AcpStructuredConnection } from './acp-structured-connection' +import type { AcpStructuredSessionAdapterDeps } from './acp-structured-session-adapter-deps' + +class AcpCopyAccountMismatchError extends Error { + constructor() { + super('Select the account this chat uses before opening its copy in a CLI.') + } +} + +export const ACP_CONVERSATION_COPY_TIMEOUT_MS = 60_000 + +export async function copyAcpConversation( + deps: Pick< + AcpStructuredSessionAdapterDeps, + 'spec' | 'readLaunch' | 'resolveLaunchArgs' | 'connect' | 'logger' + >, + input: { + identity: AgentSessionJournalIdentity + providerSessionId: string + accountHome: AgentSessionAccountHome + }, + options: { + timeoutMs?: number + signal?: AbortSignal + track: (connection: AcpStructuredConnection) => void + } +): Promise { + const name = acpAgentName(deps.spec.agent) + const timeout = AbortSignal.timeout(options.timeoutMs ?? ACP_CONVERSATION_COPY_TIMEOUT_MS) + const signal = options.signal ? AbortSignal.any([timeout, options.signal]) : timeout + let connection: AcpStructuredConnection | null = null + try { + signal.throwIfAborted() + if (deps.spec.agent === 'opencode') { + const args = await waitForPromiseWithSignal( + Promise.resolve(deps.resolveLaunchArgs?.() ?? []), + signal + ) + assertOpenCodeCopyUsesLocalStore(args) + } + signal.throwIfAborted() + const launch = await waitForPromiseWithSignal(deps.readLaunch(input.identity), signal) + signal.throwIfAborted() + await assertCopyAccountSelected(deps.spec, input.accountHome, launch.env, signal) + signal.throwIfAborted() + connection = deps.connect( + { + command: launch.command, + args: launch.args, + cwd: launch.cwd, + env: launch.env, + envToDelete: launch.envToDelete + }, + { clientInfo: { name: 'orca', version: '1' } } + ) + options.track(connection) + const sessionId = await waitForPromiseWithSignal( + forkAdvertised(connection, input.providerSessionId, launch.cwd), + signal + ) + await assertCopyAccountSelected(deps.spec, input.accountHome, launch.env, signal) + const providerSession = normalizeAgentProviderSession({ key: 'session_id', id: sessionId }) + if (!providerSession || providerSession.id !== sessionId) { + throw new Error('The agent returned an invalid copied conversation identity.') + } + if (sessionId === input.providerSessionId) { + throw new Error('The agent returned the original conversation instead of a copy.') + } + const filePath = + deps.spec.agent === 'omp' + ? await resolveSessionFilePath( + 'omp', + sessionId, + { + ompSessionsDir: resolveOmpSessionsDir({ env: launch.env }) + }, + signal + ) + : undefined + if (deps.spec.agent === 'omp' && !filePath) { + throw new Error('OMP did not save the copied conversation in its pinned session directory.') + } + return { + sessionId, + ...(filePath ? { filePath } : {}) + } + } catch (error) { + if ( + error instanceof AcpCopyAccountMismatchError || + error instanceof OpenCodeCopyServerRouteError + ) { + throw error + } + throw new Error( + error instanceof AcpRpcError && error.code === -32601 + ? `This ${name} version cannot copy a chat. Update ${name} to resume this chat in a new CLI.` + : `${name} could not copy this chat.`, + { cause: error } + ) + } finally { + if (connection && !(await connection.close().catch(() => false))) { + deps.logger?.warn('ACP conversation copy child exit is unproven; retained for cleanup', { + scope: 'acp-conversation-copy', + sessionId: input.identity.sessionId + }) + } + } +} + +async function forkAdvertised( + connection: AcpStructuredConnection, + sessionId: string, + cwd: string +): Promise { + const { agentCapabilities } = await connection.initialize() + if (agentCapabilities?.sessionCapabilities?.fork == null) { + throw new AcpRpcError(-32601, 'session/fork is not advertised') + } + return connection.forkSession(sessionId, cwd) +} + +/** Rederive selection at the fork boundaries; ordinary terminals still own their normal routing. */ +async function assertCopyAccountSelected( + spec: AcpStructuredSessionAdapterDeps['spec'], + home: AgentSessionAccountHome, + launchEnv: Record, + signal: AbortSignal +): Promise { + if ('kind' in home && home.kind === 'opencode') { + const selected = await waitForPromiseWithSignal(spec.account.resolve({ launchEnv }), signal) + if (!agentSessionAccountHomesEqual(home, selected)) { + throw new AcpCopyAccountMismatchError() + } + } +} diff --git a/src/main/acp/acp-session-runtime.ts b/src/main/acp/acp-session-runtime.ts index 5f362d0fe7d..8cfd4bbaf42 100644 --- a/src/main/acp/acp-session-runtime.ts +++ b/src/main/acp/acp-session-runtime.ts @@ -1,5 +1,5 @@ import type { Readable, Writable } from 'node:stream' -import type { z } from 'zod' +import { z } from 'zod' import { AcpAgentError, AcpAuthRequiredError, @@ -28,6 +28,7 @@ import { SetSessionModeResponseSchema, SetSessionModelResponseSchema, SetSessionConfigOptionResponseSchema, + SessionIdSchema, type InitializeRequest, type InitializeResponse, type AuthenticateResponse, @@ -42,6 +43,8 @@ import { type SetSessionModelResponse } from './generated/acp-protocol.generated' +const ForkSessionResponseSchema = z.looseObject({ sessionId: SessionIdSchema }) + type Meta = PromptRequest['_meta'] const withMeta = (meta: Meta): { _meta?: Meta } => (meta ? { _meta: meta } : {}) @@ -142,6 +145,13 @@ export class AcpSessionRuntime { return this.peer.request(method, params, { timeoutMs: null }) } + /** Copies saved session `sessionId` into a new one the agent saves (`session/fork`) and answers + * its id; this connection starts no session of its own. Ask only an agent that advertises it. */ + async forkSession(sessionId: string, cwd: string): Promise { + const params = { sessionId, cwd, mcpServers: [] } + return (await this.call('session/fork', params, ForkSessionResponseSchema)).sessionId + } + start(options: AcpSessionStartOptions): Promise { if (this.started) { return Promise.resolve(this.started) diff --git a/src/main/acp/acp-structured-connection.ts b/src/main/acp/acp-structured-connection.ts index 525189b2003..7a03b539855 100644 --- a/src/main/acp/acp-structured-connection.ts +++ b/src/main/acp/acp-structured-connection.ts @@ -9,6 +9,7 @@ import type { ProviderProcessLaunch } from '../provider-process/provider-process export type AcpStructuredConnection = Pick< AcpAgentConnection, | 'initialize' + | 'forkSession' | 'start' | 'prompt' | 'cancel' diff --git a/src/main/acp/acp-structured-session-adapter-deps.ts b/src/main/acp/acp-structured-session-adapter-deps.ts index 64f4d120791..ec2802e38f1 100644 --- a/src/main/acp/acp-structured-session-adapter-deps.ts +++ b/src/main/acp/acp-structured-session-adapter-deps.ts @@ -25,6 +25,8 @@ export type AcpStructuredSessionAdapterDeps = { }) => Promise /** The same launch for a read that starts no child (restart recovery): the agent's env, unsealed. */ readLaunch: (identity: AgentSessionJournalIdentity) => Promise + /** Current host arguments before protocol-specific filtering; copies open through the CLI. */ + resolveLaunchArgs?: () => Promise | string[] /** The chat's journal as it stands, read without opening it; null when it has none. */ readJournal?: (sessionId: string) => JournalLoad | null /** Starts the agent's process and owns its protocol: `createAcpAgentConnection` in production. */ diff --git a/src/main/acp/acp-structured-session-adapter.ts b/src/main/acp/acp-structured-session-adapter.ts index dbaec32b2e4..8b48a97b963 100644 --- a/src/main/acp/acp-structured-session-adapter.ts +++ b/src/main/acp/acp-structured-session-adapter.ts @@ -43,18 +43,25 @@ import { import { readAcpLiveOptions, writeAcpSessionOption } from './acp-structured-options' import { readAcpRecoveryHistory } from './acp-recovery-history' import { stopAcpChildren, acpChildStopCapabilities } from './acp-structured-child-stop' +import { AcpConversationCopies } from './acp-conversation-copies' export class AcpStructuredSessionAdapter implements StructuredAgentSessionAdapter { /** Live children, and ones whose exit is not yet proven; a proven exit removes its entry. */ private readonly sessions = new Map() private readonly starts = new ProviderAcquisitionStarts() + private readonly copies: AcpConversationCopies - constructor(private readonly deps: AcpStructuredSessionAdapterDeps) {} + constructor(private readonly deps: AcpStructuredSessionAdapterDeps) { + this.copies = new AcpConversationCopies(deps) + } /** Restart recovery's evidence; null for an agent whose own store Orca cannot read. */ - providerHistoryWindow: NonNullable = ({ - identity - }) => readAcpRecoveryHistory(this.deps, identity) + providerHistoryWindow: NonNullable = ( + input + ) => readAcpRecoveryHistory(this.deps, input.identity) + + copyConversation: NonNullable = (input) => + this.copies.copy(input) // The child runs on this runtime's own machine; Windows needs process start-time proof. supportsLocation = (location: AgentSessionExecutionLocation): boolean => @@ -248,10 +255,11 @@ export class AcpStructuredSessionAdapter implements StructuredAgentSessionAdapte forceCloseSession = (sessionId: string): Promise => this.stop(sessionId, false) async closeAll(): Promise { + const copiedChildren = this.copies.closeAll() const ids = new Set([...this.sessions.keys(), ...this.starts.failedSessionIds()]) // A start still under way is the host's to abort: its teardown does, before this runs. const proven = await Promise.all([...ids].map((sessionId) => this.stop(sessionId, true))) - if (proven.includes(false)) { + if (!(await copiedChildren) || proven.includes(false)) { throw new Error('an ACP agent child could not be proven stopped') } } @@ -352,11 +360,6 @@ export class AcpStructuredSessionAdapter implements StructuredAgentSessionAdapte return session } - private stopGraceMs(): number { - return this.deps.stopGraceMs ?? ACP_STOP_GRACE_MS - } - - private now(): number { - return (this.deps.now ?? Date.now)() - } + private readonly stopGraceMs = (): number => this.deps.stopGraceMs ?? ACP_STOP_GRACE_MS + private readonly now = (): number => (this.deps.now ?? Date.now)() } diff --git a/src/main/agent-hooks/first-work-branch-rename-wsl-move.test.ts b/src/main/agent-hooks/first-work-branch-rename-wsl-move.test.ts new file mode 100644 index 00000000000..36e871f3f3e --- /dev/null +++ b/src/main/agent-hooks/first-work-branch-rename-wsl-move.test.ts @@ -0,0 +1,228 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { Repo } from '../../shared/repo-types' +import { wslMoveCutoverFixture as cutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + loadWslMoveFence, + resetWslMoveFenceForTests +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' + +const { gitExecFileAsyncMock, generateBranchNameMock, resolveTextGenerationParamsMock } = + vi.hoisted(() => ({ + gitExecFileAsyncMock: vi.fn(), + generateBranchNameMock: vi.fn(), + resolveTextGenerationParamsMock: vi.fn() + })) + +vi.mock('../git/runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock })) +vi.mock('../git/git-username', () => ({ resolveLocalGitUsername: vi.fn(async () => 'you') })) +vi.mock('../text-generation/commit-message-text-generation', () => ({ + generateBranchNameFromContext: generateBranchNameMock, + resolveTextGenerationParams: resolveTextGenerationParamsMock +})) +vi.mock('../text-generation/commit-message-agent-environment', () => ({ + prepareLocalCommitMessageAgentEnv: vi.fn(async () => ({ ok: true })) +})) +vi.mock('../ipc/worktree-logic', () => ({ + computeBranchName: vi.fn((leaf: string) => `you/${leaf}`), + getConfiguredBranchPrefix: vi.fn(() => 'you') +})) + +import { + maybeAutoRenameBranchOnFirstWork, + resetFirstWorkBranchRenameState, + type FirstWorkBranchRenameDeps +} from './first-work-branch-rename' +import { + FOLDER_WORKTREE_ID, + REPO_ID, + gitResponder, + makeBranchRenameDeps, + workingEvent +} from './first-work-branch-rename-test-harness' + +const LOCAL_REPO: Repo = { + id: REPO_ID, + path: '/repo', + displayName: 'repo', + badgeColor: '', + addedAt: 0 +} + +function makeDeps(overrides: Partial = {}) { + return makeBranchRenameDeps(vi.fn, overrides) +} + +let userDataPath: string +beforeEach(() => { + userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-rename-')) + loadWslMoveFence(userDataPath) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced', {}, { repoIds: [REPO_ID] })) + resetFirstWorkBranchRenameState() + vi.clearAllMocks() + resolveTextGenerationParamsMock.mockReturnValue({ + ok: true, + params: { agentId: 'claude', model: 'm' } + }) + generateBranchNameMock.mockResolvedValue({ success: true, slug: 'fix-auth' }) + gitExecFileAsyncMock.mockImplementation( + gitResponder({ currentBranch: 'you/Nautilus', hasUpstream: false }) + ) +}) +afterEach(() => { + resetWslMoveFenceForTests() + rmSync(userDataPath, { recursive: true, force: true }) +}) + +describe('first-work rename while a WSL move takes the project', () => { + it('backs off before any git command and keeps the rename pending', async () => { + const { deps, setDisplayName, setRenameError } = makeDeps({ + getRepo: () => LOCAL_REPO + }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(gitExecFileAsyncMock).not.toHaveBeenCalled() + expect(setDisplayName).not.toHaveBeenCalled() + expect(setRenameError).not.toHaveBeenCalled() + + // A transient bail, not a settled verdict: once the move settles the next event renames. + resetWslMoveFenceForTests() + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['branch', '-m', 'you/fix-auth'], { + cwd: '/repo/wt' + }) + expect(setDisplayName).toHaveBeenCalled() + }) + + it('backs off when the move starts during generation', async () => { + resetWslMoveFenceForTests() + loadWslMoveFence(join(userDataPath, 'unused')) + generateBranchNameMock.mockImplementation(async () => { + loadWslMoveFence(userDataPath) + return { success: false, error: 'boom' } + }) + const { deps, setDisplayName, setRenameError } = makeDeps({ getRepo: () => LOCAL_REPO }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(generateBranchNameMock).toHaveBeenCalledOnce() + expect(gitExecFileAsyncMock).not.toHaveBeenCalledWith( + expect.arrayContaining(['branch', '-m']), + expect.anything() + ) + expect(setDisplayName).not.toHaveBeenCalled() + expect(setRenameError).not.toHaveBeenCalled() + }) + + it('backs off when the move starts after generation but before git branch -m', async () => { + resetWslMoveFenceForTests() + loadWslMoveFence(join(userDataPath, 'unused')) + let fenced = false + const respond = gitResponder({ + currentBranch: 'you/Nautilus', + hasUpstream: false + }) + gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { + if (args[0] === 'show-ref' && !fenced) { + fenced = true + loadWslMoveFence(userDataPath) + } + return respond(args) + }) + const { deps, setDisplayName, setRenameError, renameWorktreeFolder } = makeDeps({ + getRepo: () => LOCAL_REPO + }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(generateBranchNameMock).toHaveBeenCalledOnce() + expect(gitExecFileAsyncMock).not.toHaveBeenCalledWith( + expect.arrayContaining(['branch', '-m']), + expect.anything() + ) + expect(renameWorktreeFolder).not.toHaveBeenCalled() + expect(setDisplayName).not.toHaveBeenCalled() + expect(setRenameError).not.toHaveBeenCalled() + + // Still pending: once the move settles the next event renames. + resetWslMoveFenceForTests() + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['branch', '-m', 'you/fix-auth'], { + cwd: '/repo/wt' + }) + }) + + it('settles the rename but leaves the folder in place when the move starts during git branch -m', async () => { + resetWslMoveFenceForTests() + loadWslMoveFence(join(userDataPath, 'unused')) + const respond = gitResponder({ + currentBranch: 'you/Nautilus', + hasUpstream: false + }) + gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { + if (args[0] === 'branch' && args[1] === '-m') { + loadWslMoveFence(userDataPath) + } + return respond(args) + }) + const { deps, renameWorktreeFolder } = makeDeps({ + getRepo: () => LOCAL_REPO + }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['branch', '-m', 'you/fix-auth'], { + cwd: '/repo/wt' + }) + expect(renameWorktreeFolder).not.toHaveBeenCalled() + + // The branch already has its new name, so the rename is settled: a later event never renames again. + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + const branchRenames = gitExecFileAsyncMock.mock.calls.filter( + ([args]) => args[0] === 'branch' && args[1] === '-m' + ) + expect(branchRenames).toHaveLength(1) + }) + + it('renames a project the move is not taking', async () => { + const { deps, setDisplayName } = makeDeps({ + getRepo: () => ({ ...LOCAL_REPO, id: 'other-repo' }) + }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(setDisplayName).toHaveBeenCalled() + }) + + it('leaves the same repo id on another host to its own host', async () => { + const info = vi.spyOn(console, 'info').mockImplementation(() => {}) + const { deps } = makeDeps({ + getRepo: () => ({ ...LOCAL_REPO, executionHostId: 'runtime:env-1' }) + }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(info).toHaveBeenCalledWith(expect.stringContaining('branch is owned by runtime:env-1')) + expect(info).not.toHaveBeenCalledWith(expect.stringContaining('WSL move')) + info.mockRestore() + }) + + it('keeps a moving folder workspace title pending', async () => { + const { deps, setDisplayName, setRenameError } = makeDeps({ + resolveWorktreeIdForTab: () => FOLDER_WORKTREE_ID, + getFolderWorkspacePath: () => '/workspace/platform', + isPendingFirstAgentMessageRename: () => true, + isFolderWorkspaceWslMoveBlocked: () => true + }) + + await maybeAutoRenameBranchOnFirstWork(workingEvent(), deps) + + expect(generateBranchNameMock).not.toHaveBeenCalled() + expect(setDisplayName).not.toHaveBeenCalled() + expect(setRenameError).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/agent-hooks/first-work-branch-rename.ts b/src/main/agent-hooks/first-work-branch-rename.ts index 72ebc3f9ced..8497b1e41a3 100644 --- a/src/main/agent-hooks/first-work-branch-rename.ts +++ b/src/main/agent-hooks/first-work-branch-rename.ts @@ -30,6 +30,7 @@ import type { CommitMessageAgentEnvironmentResolvers } from '../text-generation/ import type { AgentGenerationFailureOutput } from '../text-generation/agent-failure-output' import { resolveGenerationTarget } from './first-work-generation-target' import { runFolderWorkspaceTitleAutoRename } from './first-work-workspace-title-rename' +import { isWslMoveRepoBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' export type FirstWorkBranchRenameEvent = { paneKey: string @@ -50,6 +51,8 @@ export type FirstWorkBranchRenameDeps = { getCurrentDisplayName: (worktreeId: string) => string | undefined /** Current workspace path for non-git folder workspaces. */ getFolderWorkspacePath?: (worktreeId: string) => string | undefined + /** True while a WSL move is taking the folder workspace, or the WSL host owns it. */ + isFolderWorkspaceWslMoveBlocked?: (worktreeId: string) => boolean /** True while a workspace title is waiting for the first agent message. */ isPendingFirstAgentMessageRename?: (worktreeId: string) => boolean /** True only for Orca-created worktrees whose branch Orca is allowed to rename. */ @@ -176,6 +179,10 @@ async function runAutoRename( return stop('folder project has no branch to rename', true) } const worktreePath = parsed.worktreePath + // Before any git command; retry keeps the rename pending until the move settles. + if (isWslMoveRepoBlocked(repo)) { + return retry('a WSL move is taking the project') + } // Why: a runtime row's connectionId names the server's own SSH target; that host owns the branch, never this client. const route = resolveGitRouteForHost(getRepoExecutionHostId(repo)) @@ -235,6 +242,10 @@ async function runAutoRename( params, target ) + // Generation takes seconds; a move may have started meanwhile. + if (isWslMoveRepoBlocked(repo)) { + return retry('a WSL move started during generation') + } if (!generated.success) { // Transient failure: retry on a later event; skip the badge for a user-canceled generation. if (!generated.canceled) { @@ -281,6 +292,10 @@ async function runAutoRename( return stop(`no distinct unique branch name for slug "${slug}"`, true) } + // The name lookups above await git; a move may have started since the post-generation check. + if (isWslMoveRepoBlocked(repo)) { + return retry('a WSL move started before the branch rename') + } await renameCurrentBranch(exec, newBranch) // resolveUniqueBranchName may append a collision suffix (`-2`, …), so derive names from the resolved leaf, not the slug. @@ -298,7 +313,9 @@ async function runAutoRename( // Best-effort local-only folder align, after setDisplayName so the new name rides into the migrated identity; a failure must never undo the landed rename. let folderRenamed = false - if (deps.renameWorktreeFolder) { + // The branch rename landed, but a move now taking the project must not see its folder moved. + const folderBlocked = isWslMoveRepoBlocked(repo) + if (deps.renameWorktreeFolder && !folderBlocked) { try { folderRenamed = await deps.renameWorktreeFolder(worktreeId, newBranchLeaf) } catch (error) { @@ -313,7 +330,12 @@ async function runAutoRename( const displayLog = updateDisplay ? `display "${currentDisplayName ?? ''}" -> "${newDisplayName}"` : `display kept ("${currentDisplayName}")` - const folderLog = folderRenamed ? '; folder renamed' : '' + // The branch already has its new name, so a move that skipped the folder still settles the rename. + const folderLog = folderRenamed + ? '; folder renamed' + : folderBlocked + ? '; folder kept (a WSL move started)' + : '' console.info( `[auto-branch-rename] renamed ${currentBranch} -> ${newBranch}; ${displayLog}${folderLog}` ) diff --git a/src/main/agent-hooks/first-work-rename-runtime.ts b/src/main/agent-hooks/first-work-rename-runtime.ts index ebc9484071b..2c42a623dd5 100644 --- a/src/main/agent-hooks/first-work-rename-runtime.ts +++ b/src/main/agent-hooks/first-work-rename-runtime.ts @@ -5,6 +5,7 @@ import type { FirstWorkBranchRenameDeps } from './first-work-branch-rename' import { rememberBranchRenameFailureOutput } from './branch-rename-failure-output' import { renameWorktreeFolderOnFirstWork } from './first-work-folder-rename' import { moveWorktree } from '../git/worktree' +import { isWslMoveFolderWorkspaceBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' import type { Store } from '../persistence' import type { OrcaRuntimeService } from '../runtime/orca-runtime' @@ -36,6 +37,12 @@ export function firstWorkRenameDeps( ? store.getFolderWorkspace(scope.folderWorkspaceId)?.folderPath : undefined }, + isFolderWorkspaceWslMoveBlocked: (worktreeId) => { + const scope = parseWorkspaceKey(worktreeId) + const folder = + scope?.type === 'folder' ? store.getFolderWorkspace(scope.folderWorkspaceId) : undefined + return !!folder && isWslMoveFolderWorkspaceBlocked(folder) + }, isPendingFirstAgentMessageRename: (worktreeId) => { const scope = parseWorkspaceKey(worktreeId) return scope?.type === 'folder' diff --git a/src/main/agent-hooks/first-work-workspace-title-rename.ts b/src/main/agent-hooks/first-work-workspace-title-rename.ts index 54962c62afa..da9b4ab3eb5 100644 --- a/src/main/agent-hooks/first-work-workspace-title-rename.ts +++ b/src/main/agent-hooks/first-work-workspace-title-rename.ts @@ -19,6 +19,10 @@ export async function runFolderWorkspaceTitleAutoRename( stop: (reason: string, clearError?: boolean) => true, retry: (reason: string) => false ): Promise { + // Retry keeps the title pending until the move settles. + if (deps.isFolderWorkspaceWslMoveBlocked?.(worktreeId) === true) { + return retry('a WSL move is taking the workspace') + } if (deps.isPendingFirstAgentMessageRename?.(worktreeId) !== true) { return stop('folder workspace is not pending title rename', true) } @@ -52,6 +56,9 @@ export async function runFolderWorkspaceTitleAutoRename( ) { return stop('folder workspace changed during generation', true) } + if (deps.isFolderWorkspaceWslMoveBlocked?.(worktreeId) === true) { + return retry('a WSL move started during generation') + } if (!generated.success) { if (!generated.canceled) { deps.setRenameError(worktreeId, generated.error, generated.failureOutput ?? null) diff --git a/src/main/agent-hooks/server-transport-interference.test.ts b/src/main/agent-hooks/server-transport-interference.test.ts index e8d5a8e357b..dc39cebed46 100644 --- a/src/main/agent-hooks/server-transport-interference.test.ts +++ b/src/main/agent-hooks/server-transport-interference.test.ts @@ -2,11 +2,12 @@ // short of its own Content-Length. The listener fails open on every request error, so the only // way this stays diagnosable is if the truncation is classified before it is swallowed. import { connect } from 'node:net' -import { ServerResponse } from 'node:http' +import { IncomingMessage, ServerResponse } from 'node:http' import { afterEach, describe, expect, it, vi } from 'vitest' import type { HookTransportInterferenceReport } from '../../shared/agent-hook-transport-interference' import { AgentHookServer } from './server' import { OPENCODE_STARTUP_PROMPT_CLAIM_PATH } from '../../shared/opencode-startup-prompt' +import { HOOK_REQUEST_SLOWLORIS_MS } from '../../shared/agent-hook-listener/listener-limits' async function postTruncatedHook( port: number, @@ -47,15 +48,29 @@ async function postStalledHook( token: string, pathname = '/hook/claude' ): Promise { - const socket = connect({ port, host: '127.0.0.1' }) - await new Promise((resolve) => socket.on('connect', () => resolve())) - socket.write( - `POST ${pathname} HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nX-Orca-Agent-Hook-Token: ${token}\r\nContent-Length: 100000\r\n\r\n` - ) - await new Promise((resolve) => { - socket.on('close', () => resolve()) - socket.on('error', () => resolve()) + const setRequestTimeout = IncomingMessage.prototype.setTimeout + // Keep the native timeout/destroy path while checking its full production budget. + const timeout = vi.spyOn(IncomingMessage.prototype, 'setTimeout').mockImplementation(function ( + this: IncomingMessage, + milliseconds, + callback + ) { + expect(milliseconds).toBe(HOOK_REQUEST_SLOWLORIS_MS) + return setRequestTimeout.call(this, 25, callback) }) + try { + const socket = connect({ port, host: '127.0.0.1' }) + await new Promise((resolve) => socket.on('connect', () => resolve())) + socket.write( + `POST ${pathname} HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nX-Orca-Agent-Hook-Token: ${token}\r\nContent-Length: 100000\r\n\r\n` + ) + await new Promise((resolve) => { + socket.on('close', () => resolve()) + socket.on('error', () => resolve()) + }) + } finally { + timeout.mockRestore() + } await new Promise((resolve) => setTimeout(resolve, 50)) } diff --git a/src/main/ai-vault-search/session-search-dependent-transcripts.test.ts b/src/main/ai-vault-search/session-search-dependent-transcripts.test.ts new file mode 100644 index 00000000000..daf8b38877a --- /dev/null +++ b/src/main/ai-vault-search/session-search-dependent-transcripts.test.ts @@ -0,0 +1,195 @@ +import { mkdir, stat, unlink, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { afterEach, beforeEach, expect, it } from 'vitest' +import { + parseAgentSessionFileCached, + resetSessionParseCacheForTests +} from '../ai-vault/session-scanner-parse-cache' +import { resetCodexSessionIndexTitleCacheForTests } from '../ai-vault/session-scanner-codex-title-index' +import type { SessionFileCandidate } from '../ai-vault/session-scanner-types' +import { resetTranscriptConsumersForTests } from '../ai-vault/session-transcript-consumers' +import { registerSessionSearchIndexConsumer } from './session-search-index-consumer' +import { + openSessionSearchIndexFile, + type SessionSearchIndexFile +} from './session-search-index-test-fixture' +import { SessionSearchStore } from './session-search-store' +import { + CODEX_ROLLOUT_FILE, + CODEX_SESSION_ID, + codexRolloutLines, + sessionCandidate +} from './session-search-transcript-fixtures' + +let index: SessionSearchIndexFile +let store: SessionSearchStore +let root: string +let errors: unknown[] +const SESSION_ID = '0b5e1c2a-3d4f-4a5b-8c6d-7e8f9a0b1c2d' + +beforeEach(async () => { + resetTranscriptConsumersForTests() + resetCodexSessionIndexTitleCacheForTests() + index = await openSessionSearchIndexFile('ss-dependent-transcripts') + root = dirname(index.path) + errors = [] + store = new SessionSearchStore(index.path, (error) => errors.push(error)) + registerSessionSearchIndexConsumer(store) +}) + +afterEach(async () => { + resetTranscriptConsumersForTests() + resetSessionParseCacheForTests() + resetCodexSessionIndexTitleCacheForTests() + store.close() + await index.close() +}) + +async function writeSource(path: string, content: string): Promise { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, content) +} + +async function coldRead(candidate: SessionFileCandidate) { + resetSessionParseCacheForTests() + const session = await parseAgentSessionFileCached(candidate, process.platform) + expect(errors).toEqual([]) + expect(session).not.toBeNull() + return session +} + +function matches(term: string): unknown { + return index.db + .prepare( + `SELECT count(*) AS n FROM messages_fts + JOIN messages m ON m.id = messages_fts.rowid + JOIN sessions s ON s.id = m.session_row_id + WHERE messages_fts MATCH ?` + ) + .get(term) +} + +it.each(['cline', 'kiro'] as const)( + 'removes old searchable messages when the %s sidecar is deleted', + async (agent) => { + const path = join(root, agent, SESSION_ID, `${SESSION_ID}.json`) + const sidecarPath = agent === 'cline' ? path.replace(/\.json$/, '.messages.json') : `${path}l` + await writeSource(path, JSON.stringify({ session_id: SESSION_ID, title: 'Conversation' })) + await writeSource( + sidecarPath, + agent === 'cline' + ? JSON.stringify({ messages: [{ role: 'user', content: 'oldsidecarmessage' }] }) + : `${JSON.stringify({ + kind: 'Prompt', + data: { content: [{ kind: 'text', data: 'oldsidecarmessage' }] } + })}\n` + ) + const candidate = await sessionCandidate(agent, path) + const sidecar = await stat(sidecarPath) + candidate.file.sidecar = { + path: sidecarPath, + mtimeMs: sidecar.mtimeMs, + sizeBytes: sidecar.size + } + await coldRead(candidate) + expect(matches('oldsidecarmessage')).toEqual({ n: 1 }) + + await unlink(sidecarPath) + const after = await sessionCandidate(agent, path) + expect(after.file).toEqual({ ...candidate.file, sidecar: undefined }) + after.file.sidecar = 'none' + expect(await coldRead(after)).toMatchObject({ messageCount: 0 }) + expect(matches('oldsidecarmessage')).toEqual({ n: 0 }) + expect(index.db.prepare('SELECT state FROM files').get()).toEqual({ state: 'current' }) + } +) + +it.each(['opencode', 'kimi', 'grok'] as const)( + 'repairs %s search content after a cold reread of unchanged candidate metadata', + async (agent) => { + const source = await multiFileSource(agent) + await writeSource(source.messagesPath, source.message('olddependentmessage')) + const candidate = await sessionCandidate(agent, source.path) + candidate.file.sidecar = 'none' + await coldRead(candidate) + expect(matches('olddependentmessage')).toEqual({ n: 1 }) + + await writeSource(source.messagesPath, source.message('newdependentmessage')) + expect((await sessionCandidate(agent, source.path)).file).toEqual({ + ...candidate.file, + sidecar: undefined + }) + const session = await coldRead(candidate) + expect(session?.previewMessages).toEqual( + expect.arrayContaining([expect.objectContaining({ text: 'newdependentmessage' })]) + ) + expect(matches('newdependentmessage')).toEqual({ n: 1 }) + expect(matches('olddependentmessage')).toEqual({ n: 0 }) + } +) + +async function multiFileSource(agent: 'opencode' | 'kimi' | 'grok') { + if (agent === 'opencode') { + const path = join(root, 'storage', 'session', 'project', 'ses_fixture.json') + await writeSource(path, JSON.stringify({ id: SESSION_ID, title: 'Conversation' })) + return { + path, + messagesPath: join(root, 'storage', 'message', SESSION_ID, 'msg_fixture.json'), + message: (text: string) => JSON.stringify({ role: 'user', content: text }) + } + } + if (agent === 'kimi') { + const directory = join(root, 'kimi', 'sessions', 'wd_app_abc', `session_${SESSION_ID}`) + const path = join(directory, 'state.json') + await writeSource(path, JSON.stringify({ title: 'Conversation' })) + return { + path, + messagesPath: join(directory, 'agents', 'main', 'wire.jsonl'), + message: (text: string) => + `${JSON.stringify({ + type: 'context.append_message', + message: { role: 'user', origin: { kind: 'user' }, content: text } + })}\n` + } + } + const path = join(root, 'grok', SESSION_ID, 'summary.json') + await writeSource( + path, + JSON.stringify({ info: { id: SESSION_ID }, generated_title: 'Conversation' }) + ) + return { + path, + messagesPath: join(dirname(path), 'chat_history.jsonl'), + message: (text: string) => + `${JSON.stringify({ type: 'user', content: `${text}` })}\n` + } +} + +it('updates the indexed Codex title when only session_index.jsonl changes', async () => { + const home = join(root, 'codex') + const path = join(home, 'sessions', CODEX_ROLLOUT_FILE) + await writeSource(path, `${codexRolloutLines(['pwd'], '/repo/app', 'codexmessage').join('\n')}\n`) + const titleIndex = join(home, 'session_index.jsonl') + await writeSource( + titleIndex, + `${JSON.stringify({ id: CODEX_SESSION_ID, thread_name: 'Original' })}\n` + ) + const candidate = await sessionCandidate('codex', path, home) + candidate.file.sidecar = 'none' + await coldRead(candidate) + expect(index.db.prepare('SELECT title FROM sessions').get()).toEqual({ title: 'Original' }) + + await writeSource( + titleIndex, + `${JSON.stringify({ id: CODEX_SESSION_ID, thread_name: 'Renamed conversation' })}\n` + ) + expect((await sessionCandidate('codex', path, home)).file).toEqual({ + ...candidate.file, + sidecar: undefined + }) + expect(await coldRead(candidate)).toMatchObject({ title: 'Renamed conversation' }) + expect(index.db.prepare('SELECT title FROM sessions').get()).toEqual({ + title: 'Renamed conversation' + }) + expect(matches('codexmessage')).toEqual({ n: 1 }) +}) diff --git a/src/main/ai-vault-search/session-search-file-write.test.ts b/src/main/ai-vault-search/session-search-file-write.test.ts index 2c0be88b026..56351c0f538 100644 --- a/src/main/ai-vault-search/session-search-file-write.test.ts +++ b/src/main/ai-vault-search/session-search-file-write.test.ts @@ -491,8 +491,12 @@ it('fences a first-ever read whose file was removed before it committed', () => }) it('replaces the previous generation without ever showing both', async () => { - replayTranscriptRead({ messages: userMessages('firstgeneration', 10) }) - replayTranscriptRead({ messages: userMessages('secondgeneration', 10) }) + const candidate = syntheticCandidate() + replayTranscriptRead({ candidate, messages: userMessages('firstgeneration', 10) }) + replayTranscriptRead({ + candidate: syntheticCandidate({ mtimeMs: candidate.file.mtimeMs + 1 }), + messages: userMessages('secondgeneration', 10) + }) expect(matches(index.db, 'messages_fts', 'firstgeneration')).toBe(0) expect(matches(index.db, 'messages_fts', 'secondgeneration')).toBe(10) @@ -541,8 +545,12 @@ it('replaces a generation by cutting the old one loose, not by deleting it inlin }) it('drains what a replace cut loose without being asked', async () => { - replayTranscriptRead({ messages: userMessages('firstgeneration', 200) }) - replayTranscriptRead({ messages: userMessages('secondgeneration', 3) }) + const candidate = syntheticCandidate() + replayTranscriptRead({ candidate, messages: userMessages('firstgeneration', 200) }) + replayTranscriptRead({ + candidate: syntheticCandidate({ mtimeMs: candidate.file.mtimeMs + 1 }), + messages: userMessages('secondgeneration', 3) + }) // The store schedules the reclaim the way it schedules retention's. Hiding a // generation and never reclaiming it would grow the file by every re-read. diff --git a/src/main/ai-vault-search/session-search-index-consumer.ts b/src/main/ai-vault-search/session-search-index-consumer.ts index c4ff7877453..02e39f93238 100644 --- a/src/main/ai-vault-search/session-search-index-consumer.ts +++ b/src/main/ai-vault-search/session-search-index-consumer.ts @@ -1,3 +1,4 @@ +import type { AiVaultAgent } from '../../shared/ai-vault-types' import { registerTranscriptConsumer, type TranscriptConsumer, @@ -10,6 +11,35 @@ import { fileIdentity } from './session-search-file-cursor' import type { SessionSearchFileWrite } from './session-search-index-writer' import type { SessionSearchStore } from './session-search-store' +// File-read assumptions are checked by session-search-transcript-dependencies.test.ts. +export const SELF_CONTAINED_TRANSCRIPT_AGENTS = { + claude: true, + qoder: true, + codebuddy: true, + codex: false, // session_index.jsonl supplies titles. + gemini: true, + copilot: true, + cursor: false, // Sibling chat metadata enriches the transcript. + antigravity: false, // Workspace history enriches the transcript separately. + opencode: false, // SQLite or a separate message directory. + opencode2: false, + zcode: false, + grok: false, // chat_history.jsonl supplies messages. + hermes: true, + rovo: false, // session_context.json supplies messages. + openclaw: true, + pi: true, + omp: true, + 'prime-agent': true, + droid: true, + cline: false, // A .messages.json sidecar supplies messages. + devin: false, // sessions.db supplies metadata. + kimi: false, // agents/main/wire.jsonl supplies messages. + muse: true, + jcode: true, + kiro: false // A .jsonl sidecar supplies messages. +} satisfies Record + /** * The search index as a consumer of the transcript reader. * @@ -29,6 +59,14 @@ export class SessionSearchIndexConsumer implements TranscriptConsumer { beginRead(start: TranscriptReadStart): TranscriptReadConsumer | null { const { candidate } = start + if ( + start.mode === 'replace' && + SELF_CONTAINED_TRANSCRIPT_AGENTS[candidate.agent] && + (candidate.file.sidecar === undefined || candidate.file.sidecar === 'none') && + this.store.coversTranscript(candidate) + ) { + return null + } if (start.mode === 'append') { const cursor = this.store.indexedFile(candidate.file.path, fileIdentity(candidate.file)) if (!cursor || cursor.byteOffset !== start.previousByteOffset) { diff --git a/src/main/ai-vault-search/session-search-list-reparse.test.ts b/src/main/ai-vault-search/session-search-list-reparse.test.ts new file mode 100644 index 00000000000..ade3b9a6209 --- /dev/null +++ b/src/main/ai-vault-search/session-search-list-reparse.test.ts @@ -0,0 +1,123 @@ +import { afterEach, beforeEach, expect, it } from 'vitest' +import { resetTranscriptConsumersForTests } from '../ai-vault/session-transcript-consumers' +import { registerSessionSearchIndexConsumer } from './session-search-index-consumer' +import { + openSessionSearchIndexFile, + replayTranscriptRead, + syntheticCandidate, + userMessages, + type SessionSearchIndexFile +} from './session-search-index-test-fixture' +import { SessionSearchStore } from './session-search-store' + +let index: SessionSearchIndexFile +let store: SessionSearchStore +const messages = userMessages('Searchable conversation', 2) + +beforeEach(async () => { + index = await openSessionSearchIndexFile('ss-list-reparse') + store = new SessionSearchStore(index.path) + registerSessionSearchIndexConsumer(store) +}) + +afterEach(async () => { + resetTranscriptConsumersForTests() + store.close() + await index.close() +}) + +function sessionRow(): unknown { + return index.db.prepare('SELECT id FROM sessions').get() +} + +it('keeps current search rows when a cold history list rereads the same transcript', () => { + replayTranscriptRead({ messages }) + const before = sessionRow() + replayTranscriptRead({ messages }) + expect(sessionRow()).toEqual(before) + expect(index.db.prepare('SELECT count(*) AS n FROM messages').get()).toEqual({ n: 2 }) +}) + +it('still replaces a changed transcript', () => { + const candidate = syntheticCandidate() + replayTranscriptRead({ candidate, messages }) + const before = sessionRow() + replayTranscriptRead({ + candidate: syntheticCandidate({ mtimeMs: candidate.file.mtimeMs + 1 }), + messages + }) + expect(sessionRow()).not.toEqual(before) +}) + +it('still replaces a transcript whose size changed without a timestamp change', () => { + replayTranscriptRead({ messages }) + const before = sessionRow() + replayTranscriptRead({ candidate: syntheticCandidate({ sizeBytes: 5000 }), messages }) + expect(sessionRow()).not.toEqual(before) +}) + +it('refreshes metadata supplied by a sidecar despite an unchanged transcript', () => { + replayTranscriptRead({ messages }) + const before = sessionRow() + replayTranscriptRead({ + candidate: syntheticCandidate({ + sidecar: { path: 'metadata.json', mtimeMs: 1, sizeBytes: 10 } + }), + messages + }) + expect(sessionRow()).not.toEqual(before) +}) + +it('rereads when the transcript size is unknown', () => { + replayTranscriptRead({ messages }) + const before = sessionRow() + replayTranscriptRead({ candidate: syntheticCandidate({ sizeBytes: undefined }), messages }) + expect(sessionRow()).not.toEqual(before) +}) + +it('still replaces a different file with identical size and timestamp', () => { + replayTranscriptRead({ candidate: syntheticCandidate({ dev: 1, ino: 10 }), messages }) + const before = sessionRow() + replayTranscriptRead({ candidate: syntheticCandidate({ dev: 1, ino: 11 }), messages }) + expect(sessionRow()).not.toEqual(before) +}) + +it('rereads a row indexed without a file identity once the stat supplies one', () => { + replayTranscriptRead({ messages }) + const before = sessionRow() + const identified = syntheticCandidate({ dev: 1, ino: 10 }) + replayTranscriptRead({ candidate: identified, messages }) + expect(sessionRow()).not.toEqual(before) + const repaired = sessionRow() + replayTranscriptRead({ candidate: identified, messages }) + expect(sessionRow()).toEqual(repaired) +}) + +it.each(['due', 'failed'] as const)('repairs an unchanged transcript marked %s', (state) => { + replayTranscriptRead({ messages }) + const before = sessionRow() + store.setFileState(syntheticCandidate().file.path, state) + replayTranscriptRead({ messages }) + expect(sessionRow()).not.toEqual(before) + expect(index.db.prepare('SELECT state FROM files').get()).toEqual({ state: 'current' }) +}) + +it('repairs a partial index even when the stat matches', () => { + replayTranscriptRead({ messages }) + const before = sessionRow() + index.db.exec('UPDATE files SET byte_offset = -1') + replayTranscriptRead({ messages }) + expect(sessionRow()).not.toEqual(before) +}) + +it('still indexes new appended messages', () => { + replayTranscriptRead({ messages }) + replayTranscriptRead({ + candidate: syntheticCandidate({ sizeBytes: 5000 }), + mode: 'append', + previousByteOffset: 4096, + messages, + outcome: { byteOffset: 5000 } + }) + expect(index.db.prepare('SELECT count(*) AS n FROM messages').get()).toEqual({ n: 4 }) +}) diff --git a/src/main/ai-vault-search/session-search-store.ts b/src/main/ai-vault-search/session-search-store.ts index 73da2c2db6f..93cfed0ddff 100644 --- a/src/main/ai-vault-search/session-search-store.ts +++ b/src/main/ai-vault-search/session-search-store.ts @@ -2,9 +2,10 @@ import type SyncDatabase from '../sqlite/sync-database' import { asRecord } from '../ai-vault/session-scanner-record-value' import type { SessionFileCandidate } from '../ai-vault/session-scanner-types' import type { TranscriptSessionIdentity } from '../ai-vault/session-transcript-consumers' -import type { - SessionSearchFileIdentity, - SessionSearchIndexedFile +import { + fileIdentity, + type SessionSearchFileIdentity, + type SessionSearchIndexedFile } from './session-search-file-cursor' import { SESSION_SEARCH_COMMIT_CHARS, @@ -171,6 +172,37 @@ export class SessionSearchStore { } } + /** A cold list read must not rewrite a complete, unchanged search entry. */ + coversTranscript(candidate: SessionFileCandidate): boolean { + const file = candidate.file + if (this.closed || typeof file.sizeBytes !== 'number') { + return false + } + const identity = fileIdentity(file) + try { + return ( + this.db + .prepare( + // A row without a stored identity is uncovered so the rewrite records one. + `SELECT 1 FROM files WHERE path = ? AND state = 'current' + AND mtime_ms = ? AND size_bytes = ? AND byte_offset = size_bytes + AND (? IS NULL OR (CAST(dev AS REAL) = ? AND CAST(ino AS REAL) = ?))` + ) + .get( + file.path, + file.mtimeMs, + file.sizeBytes, + identity?.dev ?? null, + identity?.dev ?? null, + identity?.ino ?? null + ) !== undefined + ) + } catch (error) { + this.onError(error) + return false + } + } + /** Null when this read cannot extend the index, or when the store refuses writes. */ beginWrite( candidate: SessionFileCandidate, diff --git a/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts b/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts new file mode 100644 index 00000000000..7544390583d --- /dev/null +++ b/src/main/ai-vault-search/session-search-transcript-dependencies.test.ts @@ -0,0 +1,132 @@ +import * as fs from 'node:fs' +import * as fsPromises from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterAll, afterEach, beforeAll, expect, it, vi } from 'vitest' +import { AI_VAULT_AGENTS } from '../../shared/ai-vault-types' +import * as transcriptFs from '../native-chat/wsl-transcript-fs-access' +import { parseAgentSessionFile } from '../ai-vault/session-scanner-agent-parser' +import { AI_VAULT_AGENT_SOURCES } from '../ai-vault/session-scanner-agent-sources' +import { discoverFiles } from '../ai-vault/session-scanner-discovery' +import { + writeEveryAgentVault, + type EveryAgentVault +} from '../ai-vault/session-scanner-every-agent-fixture' +import { geminiJsonlFixture } from '../ai-vault/session-scanner-incremental-fixtures' +import { + parseAgentSessionFileCached, + resetSessionParseCacheForTests +} from '../ai-vault/session-scanner-parse-cache' +import { + registerTranscriptConsumer, + resetTranscriptConsumersForTests, + type TranscriptMessage +} from '../ai-vault/session-transcript-consumers' +import { SELF_CONTAINED_TRANSCRIPT_AGENTS } from './session-search-index-consumer' + +vi.mock('node:fs', async (importOriginal) => { + const original = await importOriginal() + // Recursive spies alter native stream prototypes and prevent readers from finishing. + const tracked = { + readFile: vi.fn(original.readFile), + readFileSync: vi.fn(original.readFileSync), + open: vi.fn(original.open), + openSync: vi.fn(original.openSync), + createReadStream: vi.fn(original.createReadStream) + } + return { ...original, ...tracked, default: { ...original.default, ...tracked } } +}) +vi.mock('node:fs/promises', { spy: true }) +vi.mock('../native-chat/wsl-transcript-fs-access', { spy: true }) + +let root: string +let vault: EveryAgentVault + +beforeAll(async () => { + root = await fsPromises.mkdtemp(join(tmpdir(), 'ss-transcript-dependencies-')) + vault = await writeEveryAgentVault(root) + const gemini = geminiJsonlFixture() + await fsPromises.writeFile( + join(vault.roots.geminiSessionsDir, gemini.fileName), + `${gemini.seedLines.join('\n')}\n` + ) +}) + +afterEach(() => { + vi.clearAllMocks() + resetTranscriptConsumersForTests() + resetSessionParseCacheForTests() +}) + +afterAll(async () => { + await fsPromises.rm(root, { recursive: true, force: true }) +}) + +function contentReadPaths(): unknown[] { + return [ + ...vi.mocked(fsPromises.readFile).mock.calls.map(([path]) => path), + ...vi.mocked(fsPromises.open).mock.calls.map(([path]) => path), + ...vi.mocked(fs.readFile).mock.calls.map(([path]) => path), + ...vi.mocked(fs.readFileSync).mock.calls.map(([path]) => path), + ...vi.mocked(fs.open).mock.calls.map(([path]) => path), + ...vi.mocked(fs.openSync).mock.calls.map(([path]) => path), + ...vi.mocked(fs.createReadStream).mock.calls.map(([path]) => path), + ...vi.mocked(transcriptFs.wslGatedReadFile).mock.calls.map(([path]) => path), + ...vi.mocked(transcriptFs.openTranscriptFile).mock.calls.map(([path]) => path), + ...vi.mocked(transcriptFs.openTranscriptReadStream).mock.calls.map(([path]) => path), + ...vi.mocked(transcriptFs.readTranscriptSlice).mock.calls.map(([path]) => path) + ] +} + +it.each(AI_VAULT_AGENTS.filter((agent) => SELF_CONTAINED_TRANSCRIPT_AGENTS[agent]))( + '%s reads indexed content only from its candidate transcript', + async (agent) => { + const source = AI_VAULT_AGENT_SOURCES[agent] + if (!source) { + throw new Error(`${agent} needs a real transcript fixture before enabling file-stat reuse`) + } + expect(source.contentDependencyPath).toBeUndefined() + const discoveries = await Promise.all( + source.rootDirs(vault.roots, []).map((rootDir) => + discoverFiles({ + ...source, + rootDir, + agent, + limit: 10, + issues: [], + extensions: [...source.extensions] + }) + ) + ) + const files = discoveries.flatMap((discovery) => discovery.files) + expect(files.length).toBeGreaterThan(0) + + for (const file of files) { + const candidate = { agent, file, codexHome: null } + for (const read of ['direct', 'cached'] as const) { + const messages: TranscriptMessage[] = [] + const sink = { active: true, push: (message: TranscriptMessage) => messages.push(message) } + resetTranscriptConsumersForTests() + resetSessionParseCacheForTests() + registerTranscriptConsumer({ + beginRead: () => ({ message: sink.push, finish: () => undefined }) + }) + vi.clearAllMocks() + + const session = await (read === 'direct' + ? parseAgentSessionFile(candidate, process.platform, sink) + : parseAgentSessionFileCached(candidate, process.platform)) + + expect(session).not.toBeNull() + expect(messages.length).toBeGreaterThan(0) + const paths = contentReadPaths() + expect(paths).toContain(file.path) + // Include failed optional reads: an absent sidecar can appear on a later scan. + expect( + paths.filter((path) => path !== file.path), + `${read}: ${file.path}` + ).toEqual([]) + } + } + } +) diff --git a/src/main/ai-vault/runtime-session-scanner.test.ts b/src/main/ai-vault/runtime-session-scanner.test.ts index db718a4380d..9fe60aa0972 100644 --- a/src/main/ai-vault/runtime-session-scanner.test.ts +++ b/src/main/ai-vault/runtime-session-scanner.test.ts @@ -188,6 +188,29 @@ describe('runtime AI Vault session scanner', () => { }) }) + it.each([ + { agent: 'opencode' as const, sessionId: 'opencode-copy' }, + { agent: 'omp' as const, sessionId: 'omp-copy', filePath: '/pinned-omp/sessions/copy.jsonl' } + ])( + 'preserves the $agent copy identity and transcript across the paired-host response', + async (forkedSession) => { + mocks.callRuntimeEnvironment.mockResolvedValueOnce({ + ok: true, + result: { useRealCodexHome: false, forkedSession } + }) + await expect( + prepareRuntimeAiVaultSessionResume('/user-data', 'env-1', { + agent: forkedSession.agent === 'opencode' ? 'opencode2' : 'omp', + sessionId: 'source', + filePath: '/source', + codexHome: null, + executionHostId: 'runtime:env-1', + fork: true + }) + ).resolves.toEqual({ useRealCodexHome: false, forkedSession }) + } + ) + it('fails retryably when runtime preparation returns an invalid result', async () => { mocks.callRuntimeEnvironment.mockResolvedValueOnce({ ok: true, result: {} }) diff --git a/src/main/ai-vault/runtime-session-scanner.ts b/src/main/ai-vault/runtime-session-scanner.ts index e81e29e9160..b60c71ddce6 100644 --- a/src/main/ai-vault/runtime-session-scanner.ts +++ b/src/main/ai-vault/runtime-session-scanner.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { + AI_VAULT_AGENTS, AI_VAULT_SCOPE_PATHS_MAX_COUNT, type AiVaultListArgs, type AiVaultListResult, @@ -33,7 +34,14 @@ export type RuntimeAiVaultScanOptions = { // parent would silently drop it and resume under the wrong account's home. const aiVaultPrepareSessionResumeResultSchema = z.object({ useRealCodexHome: z.boolean(), - substituteCodexHome: z.string().optional() + substituteCodexHome: z.string().optional(), + forkedSession: z + .object({ + sessionId: z.string(), + agent: z.enum(AI_VAULT_AGENTS), + filePath: z.string().optional() + }) + .optional() }) export function getSavedRuntimeAiVaultHostInfos( diff --git a/src/main/ai-vault/session-list-result-validation.ts b/src/main/ai-vault/session-list-result-validation.ts index 5b005eb60bf..fcae149b68a 100644 --- a/src/main/ai-vault/session-list-result-validation.ts +++ b/src/main/ai-vault/session-list-result-validation.ts @@ -10,7 +10,7 @@ import { normalizeExecutionHostId } from '../../shared/execution-host' const aiVaultAgentSet = new Set(AI_VAULT_AGENTS) -function isAiVaultAgent(agent: string): agent is AiVaultAgent { +export function isAiVaultAgent(agent: string): agent is AiVaultAgent { return aiVaultAgentSet.has(agent) } diff --git a/src/main/ai-vault/session-scanner-agent-sources.ts b/src/main/ai-vault/session-scanner-agent-sources.ts index bce3a10ae8e..224e355538f 100644 --- a/src/main/ai-vault/session-scanner-agent-sources.ts +++ b/src/main/ai-vault/session-scanner-agent-sources.ts @@ -42,10 +42,12 @@ const CURSOR_PROJECTS_DIR = join(homedir(), '.cursor', 'projects') const CODEBUDDY_PROJECTS_DIR = join(homedir(), '.codebuddy', 'projects') const HERMES_SESSIONS_DIR = join(homedir(), '.hermes', 'sessions') const ROVO_SESSIONS_DIR = join(homedir(), '.rovodev', 'sessions') -const PI_SESSIONS_DIR = normalizeAgentSessionsDir( - process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), '.pi', 'agent', 'sessions'), - '.pi' -) +export function piSessionHistorySessionsDir(): string { + return normalizeAgentSessionsDir( + process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), '.pi', 'agent', 'sessions'), + '.pi' + ) +} // Why: Prime Agent brands Pi's env contract with its own prefix and adds a // dedicated sessions-root override, so resolution differs from Pi/OMP in shape // as well as in variable name. @@ -248,7 +250,7 @@ export const AI_VAULT_AGENT_SOURCES: AiVaultAgentSourceTable = { }, pi: { rootDirs: (options, wslHomeDirs) => - sessionRootDirs(options.piSessionsDir ?? PI_SESSIONS_DIR, wslHomeDirs, [ + sessionRootDirs(options.piSessionsDir ?? piSessionHistorySessionsDir(), wslHomeDirs, [ '.pi', 'agent', 'sessions' diff --git a/src/main/ai-vault/structured-chat-conversation-copy.test.ts b/src/main/ai-vault/structured-chat-conversation-copy.test.ts new file mode 100644 index 00000000000..d14df1807ff --- /dev/null +++ b/src/main/ai-vault/structured-chat-conversation-copy.test.ts @@ -0,0 +1,83 @@ +import { + agentSessionRecordFixture, + agentSessionLeaseFixture +} from '../../shared/agent-session-record.test-fixture' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { AgentSessionProviderHandle } from '../../shared/agent-session-provider-handle' +import type { AiVaultPrepareSessionResumeArgs } from '../../shared/ai-vault-resume-preparation' +import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' +import { copyStructuredChatConversation } from './structured-chat-conversation-copy' +import { installCopyingChat as installChat } from './structured-session-ownership.test-support' + +const OMP_ID = '019fd611-2b3c-7d4e-9f50-6b7c8d9e0f1a' +const OPENCODE_ID = 'ses_2f9aB3cD4eF5gH6iJ7kL8mN9oP' +const OMP_FILE = `/sessions/${OMP_ID}.jsonl` + +const acp = (agent: 'opencode' | 'omp', nativeId: string): AgentSessionProviderHandle => ({ + transport: 'acp', + agent, + nativeId +}) + +const fork = ( + overrides: Partial +): AiVaultPrepareSessionResumeArgs => ({ + agent: 'omp', + sessionId: OMP_ID, + filePath: OMP_FILE, + codexHome: null, + executionHostId: 'local', + fork: true, + ...overrides +}) + +afterEach(() => setStructuredAgentSessionHost(null)) + +describe('copying a chat conversation for Resume in New CLI', () => { + it.each([ + { agent: 'omp' as const, handle: acp('omp', OMP_ID), sessionId: OMP_ID }, + { agent: 'opencode2' as const, handle: acp('opencode', OPENCODE_ID), sessionId: OPENCODE_ID } + ])( + "has the chat's host copy a $agent row's conversation", + async ({ agent, handle, sessionId }) => { + const copy = vi.fn(async () => ({ + sessionId: 'copy-1', + filePath: '/sessions/copy-1.jsonl' + })) + installChat(handle, copy) + + await expect( + copyStructuredChatConversation(fork({ agent, sessionId, filePath: `/x/${sessionId}` })) + ).resolves.toEqual({ + useRealCodexHome: false, + // The copy opens with the CLI that ran the chat: `opencode` for an OpenCode 2.x row. + forkedSession: { + sessionId: 'copy-1', + filePath: '/sessions/copy-1.jsonl', + agent: handle.agent + } + }) + } + ) + + it.each(['source', 'owned'])('rejects a %s fork id on the execution host', async (returned) => { + const lease = agentSessionLeaseFixture() + const fixture = agentSessionRecordFixture(lease) + const copy = vi.fn(async () => ({ + sessionId: returned === 'source' ? OMP_ID : 'other-owned' + })) + installChat(acp('omp', OMP_ID), copy, [ + { + ...fixture, + sessionId: 'other-chat', + provider: 'omp', + providerHandleChain: [ + { ...fixture.providerHandleChain[0]!, handle: acp('omp', 'other-owned') } + ] + } + ]) + await expect(copyStructuredChatConversation(fork({}))).rejects.toThrow( + 'already owned by a chat' + ) + }) +}) diff --git a/src/main/ai-vault/structured-chat-conversation-copy.ts b/src/main/ai-vault/structured-chat-conversation-copy.ts new file mode 100644 index 00000000000..275f8f1d691 --- /dev/null +++ b/src/main/ai-vault/structured-chat-conversation-copy.ts @@ -0,0 +1,55 @@ +import { agentForksThroughChat } from '../../shared/agent-resume-argv' +import { agentSessionProviderHandleChainHead } from '../../shared/agent-session-provider-handle' +import type { + AiVaultPrepareSessionResumeArgs, + AiVaultPrepareSessionResumeResult +} from '../../shared/ai-vault-resume-preparation' +import { getStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' +import { isAiVaultAgent } from './session-list-result-validation' +import { findResumeOwnership } from './structured-session-ownership' + +/** + * A fork of a chat's conversation, for an agent whose chat makes the copy: the chat's host copies + * it and answers the copy, which the terminal resumes and no chat owns. Null for any other prepare. + * Runs on the host that runs the chat, as every resume preparation does. + */ +export async function copyStructuredChatConversation( + args: AiVaultPrepareSessionResumeArgs +): Promise { + if (!args.fork || !agentForksThroughChat(args.agent)) { + return null + } + const host = getStructuredAgentSessionHost() + const ownership = findResumeOwnership(args) + const record = ownership ? host?.deps.store.getRecord(ownership.sessionId) : undefined + const copy = host?.deps.adapter.copyConversation + if (!host || !ownership || !record || !copy) { + throw new Error('This chat is no longer in Orca, so it cannot be copied to a new CLI.') + } + const forkedSession = await copy.call(host.deps.adapter, { + identity: { + sessionId: record.sessionId, + workspaceId: record.location.workspaceId, + hostId: record.location.executionHostId, + agent: record.provider, + providerHandle: + agentSessionProviderHandleChainHead(record.providerHandleChain)?.handle ?? null + }, + providerSessionId: ownership.providerSessionId, + accountHome: record.accountHome + }) + if ( + forkedSession.sessionId === ownership.providerSessionId || + findResumeOwnership({ agent: args.agent, sessionId: forkedSession.sessionId, filePath: '' }) + ) { + throw new Error('The agent returned a conversation already owned by a chat instead of a copy.') + } + return { + useRealCodexHome: false, + forkedSession: { + ...forkedSession, + // The chat's own agent, not the row's: an OpenCode 2 row's chat runs `opencode`. + agent: isAiVaultAgent(record.provider) ? record.provider : args.agent + } + } +} diff --git a/src/main/ai-vault/structured-session-ownership-client-audience.test.ts b/src/main/ai-vault/structured-session-ownership-client-audience.test.ts index 830bc8b2520..32d91c7e2b9 100644 --- a/src/main/ai-vault/structured-session-ownership-client-audience.test.ts +++ b/src/main/ai-vault/structured-session-ownership-client-audience.test.ts @@ -6,7 +6,7 @@ import { buildAiVaultResumeShellCommand } from '../../shared/ai-vault-resume-command' import { buildAgentResumeStartupPlan } from '../../shared/tui-agent-resume-startup' -import { agentForksConversations } from '../../shared/agent-resume-argv' +import { agentForksConversations, agentForksThroughChat } from '../../shared/agent-resume-argv' import { isResumableTuiAgent } from '../../shared/agent-session-resume' import type { AgentStartupShell } from '../../shared/tui-agent-startup-shell' import { @@ -220,19 +220,21 @@ describe('resume commands Orca builds for an owned row', () => { } }) - // "Resume in New CLI" opens a copy, so it is no second writer of the chat's conversation. - it.each(everyAgent.filter(({ rowAgent }) => agentForksConversations(rowAgent)))( - 'allows each copy it opens for the $rowAgent row', - async (conversation) => { - installOwnership({ handle: conversation.handle }) - const commands = builtCommands(conversation, true) - expect(commands.length).toBe(7) - for (const command of commands) { - await expect( - assertLegacyAiVaultResumeCommandAllowed(command, async () => undefined), - command - ).resolves.toBeUndefined() - } + // "Resume in New CLI" opens a copy, so it is no second writer of the chat's conversation. A copy + // the chat makes is opened by a plain resume of the copy's own id, which no chat owns. + it.each( + everyAgent.filter( + ({ rowAgent }) => agentForksConversations(rowAgent) && !agentForksThroughChat(rowAgent) + ) + )('allows each copy it opens for the $rowAgent row', async (conversation) => { + installOwnership({ handle: conversation.handle }) + const commands = builtCommands(conversation, true) + expect(commands.length).toBe(7) + for (const command of commands) { + await expect( + assertLegacyAiVaultResumeCommandAllowed(command, async () => undefined), + command + ).resolves.toBeUndefined() } - ) + }) }) diff --git a/src/main/ai-vault/structured-session-ownership.test-support.ts b/src/main/ai-vault/structured-session-ownership.test-support.ts index f8c0c2ec6f7..f1b8048549b 100644 --- a/src/main/ai-vault/structured-session-ownership.test-support.ts +++ b/src/main/ai-vault/structured-session-ownership.test-support.ts @@ -1,9 +1,11 @@ +import type { AgentSessionRecord } from '../../shared/agent-session-record' import { agentSessionLeaseFixture, agentSessionRecordFixture } from '../../shared/agent-session-record.test-fixture' import type { AgentSessionProviderHandle } from '../../shared/agent-session-provider-handle' import type { AiVaultListResult, AiVaultSession } from '../../shared/ai-vault-types' +import type { StructuredAgentSessionAdapter } from '../native-chat/agent-session-wire/structured-agent-session-adapter' import type { StructuredProviderSessionOwnership } from '../native-chat/agent-session-wire/structured-provider-session-ownership' import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' import { @@ -59,6 +61,35 @@ export function installOwnership( } as never) } +/** A host whose one chat, `chat-1`, owns `handle`'s conversation, and whose adapter copies a + * conversation through `copy`. */ +export function installCopyingChat( + handle: AgentSessionProviderHandle, + copy: StructuredAgentSessionAdapter['copyConversation'], + otherRecords: AgentSessionRecord[] = [] +): void { + const lease = { ...agentSessionLeaseFixture(), sessionId: 'chat-1' } + const fixture = agentSessionRecordFixture(lease) + const record = { + ...fixture, + sessionId: 'chat-1', + provider: handle.agent, + location: { ...fixture.location, workspaceId: 'workspace-1' }, + providerHandleChain: [{ ...fixture.providerHandleChain[0]!, handle }], + lease + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a copy reads only `deps.store` and `deps.adapter.copyConversation`; the rest of the host is never reached. + setStructuredAgentSessionHost({ + deps: { + store: { + listRecords: () => [record, ...otherRecords], + getRecord: (sessionId: string) => (sessionId === record.sessionId ? record : null) + }, + adapter: { copyConversation: copy } + } + } as never) +} + export function listResult(): AiVaultListResult { const session: AiVaultSession = { id: `local:codex:${PROVIDER_SESSION}`, diff --git a/src/main/ai-vault/structured-session-ownership.ts b/src/main/ai-vault/structured-session-ownership.ts index 2499f9c50b2..3836b4613e8 100644 --- a/src/main/ai-vault/structured-session-ownership.ts +++ b/src/main/ai-vault/structured-session-ownership.ts @@ -175,8 +175,9 @@ export async function assertLegacyAiVaultResumeCommandAllowed( } } -function findResumeOwnership( - args: AiVaultPrepareSessionResumeArgs +/** The chat that owns the row a resume or fork names; null when none does. */ +export function findResumeOwnership( + args: Pick ): StructuredProviderSessionOwnership | null { const owner = historyRowOwner(args.agent) if (!owner) { diff --git a/src/main/automations/automation-dispatch-request.ts b/src/main/automations/automation-dispatch-request.ts index 348a1b9ecee..f6f4b0f9854 100644 --- a/src/main/automations/automation-dispatch-request.ts +++ b/src/main/automations/automation-dispatch-request.ts @@ -6,10 +6,17 @@ import type { Store } from '../persistence' import type { AutomationRunWriter } from './automation-run-writer' import type { HeadlessAutomationDispatcher } from './headless-dispatch' import type { HeadlessAutomationDispatchContext } from './headless-dispatch-runner' -import { runHeadlessAutomationDispatch } from './headless-dispatch-runner' +import { + AutomationDispatchRefusedError, + runHeadlessAutomationDispatch +} from './headless-dispatch-runner' import type { AutomationRunTargetResult } from './run-target-resolution' import { createAutomationDispatchToken } from './dispatch-tokens' -import { NO_DISPATCH_HOST, sendRendererDispatch } from './dispatch-refusal' +import { + NO_DISPATCH_HOST, + describeScheduledRefusal, + sendRendererDispatch +} from './dispatch-refusal' export type AutomationRendererChannel = Pick @@ -100,8 +107,12 @@ export async function requestAutomationDispatch( return returnDurable(run) } let target = resolveCurrentTarget() - if (!target.ok || (!ctx.getRenderer() && !ctx.headlessDispatcher)) { - return refuse(target.ok ? NO_DISPATCH_HOST : target.error) + const refusal = describeScheduledRefusal({ + target, + canDispatch: Boolean(ctx.getRenderer() || ctx.headlessDispatcher) + }) + if (refusal || !target.ok) { + return refuse(refusal ?? NO_DISPATCH_HOST) } await ctx.runs.updateRun({ runId: run.id, @@ -115,8 +126,10 @@ export async function requestAutomationDispatch( return returnDurable(run) } target = resolveCurrentTarget() - if (!target.ok) { - return refuse(target.error) + // A WSL move may have started during the claim; refuse before the worktree create would throw. + const lateRefusal = describeScheduledRefusal({ target, canDispatch: true }) + if (lateRefusal || !target.ok) { + return refuse(lateRefusal ?? NO_DISPATCH_HOST) } const renderer = ctx.getRenderer() if (renderer) { @@ -148,6 +161,14 @@ export async function requestAutomationDispatch( if (!latestTarget.ok) { throw new AutomationDispatchCancelledError(latestTarget.error) } + // The precheck can run for minutes; a WSL move starting meanwhile is a skip, not a failure. + const latestRefusal = describeScheduledRefusal({ + target: latestTarget, + canDispatch: true + }) + if (latestRefusal) { + throw new AutomationDispatchRefusedError(latestRefusal) + } return dispatcher({ ...request, target: latestTarget }) } }) diff --git a/src/main/automations/automation-wsl-move-refusal.test.ts b/src/main/automations/automation-wsl-move-refusal.test.ts new file mode 100644 index 00000000000..c659311a832 --- /dev/null +++ b/src/main/automations/automation-wsl-move-refusal.test.ts @@ -0,0 +1,189 @@ +/** + * A move in flight skips each run with a reason that promises a resume; once the WSL host owns the + * project, the retained local copy goes quiet instead of writing a skip every tick. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { mkdtempSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import type { Repo } from '../../shared/repo-types' +import type { Automation } from '../../shared/automations-types' +import { removeTreeSync } from '../../shared/windows-transient-lock-removal' +import type { Store } from '../persistence' +import { closeTestStores, createStore, testState } from '../persistence-test-harness' +import { wslMoveCutoverFixture as cutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + loadWslMoveFence, + resetWslMoveFenceForTests +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' +import { AutomationService } from './service' +import { WSL_MOVE_IN_PROGRESS, WSL_MOVE_OWNED_BY_HOST } from './dispatch-refusal' + +const runAutomationPrecheckMock = vi.hoisted(() => vi.fn()) + +vi.mock('electron', () => ({ + app: { + getPath: () => testState.dir + }, + safeStorage: { + isEncryptionAvailable: () => true, + encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'), + decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length) + } +})) + +vi.mock('./precheck-runner', () => ({ + runAutomationPrecheck: runAutomationPrecheckMock +})) + +const makeRepo = (): Repo => ({ + id: 'r1', + path: '/repo', + displayName: 'test', + badgeColor: '#fff', + addedAt: 1 +}) + +function seedAutomation(store: Store, precheck = false): Automation { + store.addRepo(makeRepo()) + return store.createAutomation({ + name: 'Nightly check', + prompt: 'Check the repo', + ...(precheck ? { precheck: { command: 'true', timeoutSeconds: 30 } } : {}), + agentId: 'claude', + projectId: 'r1', + workspaceMode: 'new_per_run', + timezone: 'UTC', + rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0', + dtstart: new Date('2026-05-12T00:00:00').getTime() + }) +} + +function attachedService(store: Store, publish = vi.fn()): AutomationService { + const service = new AutomationService(store, { + tickMs: 60_000, + onAutomationsChanged: publish + }) + service.setWebContents({ isDestroyed: () => false, send: vi.fn() }) + return service +} + +/** One scheduler pass. `nextRunAt` advancing is the signal that it finished. */ +async function evaluateAt( + store: Store, + service: AutomationService, + automationId: string, + when: string +): Promise { + const nextRunAt = (): number | undefined => + store.listAutomations().find((entry) => entry.id === automationId)?.nextRunAt + const before = nextRunAt() + vi.setSystemTime(new Date(when)) + // Re-poked each try: the previous pass may still hold the re-entrancy guard while it flushes. + await vi.waitFor(() => { + service.setRendererReady() + expect(nextRunAt()).toBeGreaterThan(before ?? 0) + }) +} + +let fenceDir: string +beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'orca-automation-wsl-move-')) + fenceDir = mkdtempSync(join(tmpdir(), 'orca-automation-wsl-fence-')) + loadWslMoveFence(fenceDir) + runAutomationPrecheckMock.mockReset() + vi.useFakeTimers() +}) +afterEach(async () => { + await closeTestStores() + vi.useRealTimers() + resetWslMoveFenceForTests() + removeTreeSync(testState.dir) + removeTreeSync(fenceDir) +}) + +describe('scheduled automations under a WSL move', () => { + it('skips a run while the move is in flight, promising a resume', async () => { + vi.setSystemTime(new Date('2026-05-13T08:00:00')) + const store = await createStore() + const automation = seedAutomation(store) + seedWslMoveCutover(fenceDir, cutover('move-1', 'source-fenced', {}, { repoIds: ['r1'] })) + const service = attachedService(store) + + await evaluateAt(store, service, automation.id, '2026-05-13T09:01:00') + + const runs = store.listAutomationRuns(automation.id) + expect(runs).toHaveLength(1) + expect(runs[0]).toMatchObject({ + status: 'skipped_unavailable', + error: WSL_MOVE_IN_PROGRESS + }) + expect(WSL_MOVE_IN_PROGRESS).toBe( + "This automation's project is moving to its WSL host, so Orca skipped this run. Runs resume when the move finishes." + ) + }) + + it('writes no skip on any tick once the WSL host owns the project', async () => { + vi.setSystemTime(new Date('2026-05-13T08:00:00')) + const store = await createStore() + const automation = seedAutomation(store) + seedWslMoveCutover( + fenceDir, + cutover('move-1', 'destination-committed', {}, { repoIds: ['r1'] }) + ) + const publish = vi.fn() + const service = attachedService(store, publish) + + await evaluateAt(store, service, automation.id, '2026-05-13T09:01:00') + await evaluateAt(store, service, automation.id, '2026-05-14T09:01:00') + await evaluateAt(store, service, automation.id, '2026-05-15T09:01:00') + + expect(store.listAutomationRuns(automation.id)).toEqual([]) + expect(publish).not.toHaveBeenCalled() + expect(store.listAutomations().find((entry) => entry.id === automation.id)?.nextRunAt).toBe( + new Date('2026-05-16T09:00:00').getTime() + ) + expect(WSL_MOVE_OWNED_BY_HOST).toBe( + "This automation's project now runs on its WSL host, which runs this automation there. Orca no longer runs this local copy." + ) + }) + + it('skips, not fails, a headless run whose move starts during the precheck', async () => { + vi.setSystemTime(new Date('2026-05-13T08:00:00')) + const store = await createStore() + const automation = seedAutomation(store, true) + runAutomationPrecheckMock.mockImplementation(async () => { + seedWslMoveCutover(fenceDir, cutover('move-1', 'source-fenced', {}, { repoIds: ['r1'] })) + return { + command: 'true', + exitCode: 0, + timedOut: false, + durationMs: 1, + stdout: '', + stderr: '', + stdoutTruncated: false, + stderrTruncated: false, + error: null, + startedAt: Date.now(), + completedAt: Date.now() + } + }) + const dispatcher = vi.fn() + const service = new AutomationService(store, { + tickMs: 60_000, + headlessDispatcher: dispatcher + }) + + vi.setSystemTime(new Date('2026-05-13T09:01:00')) + service.start() + await vi.waitFor(() => + expect(store.listAutomationRuns(automation.id)[0]?.status).toBe('skipped_unavailable') + ) + service.stop() + + expect(runAutomationPrecheckMock).toHaveBeenCalledOnce() + expect(dispatcher).not.toHaveBeenCalled() + expect(store.listAutomationRuns(automation.id)[0]?.error).toBe(WSL_MOVE_IN_PROGRESS) + }) +}) diff --git a/src/main/automations/dispatch-refusal-wsl-move.test.ts b/src/main/automations/dispatch-refusal-wsl-move.test.ts new file mode 100644 index 00000000000..eda84d15bfa --- /dev/null +++ b/src/main/automations/dispatch-refusal-wsl-move.test.ts @@ -0,0 +1,64 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import type { Repo } from '../../shared/repo-types' +import { wslMoveCutoverFixture as cutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + loadWslMoveFence, + resetWslMoveFenceForTests +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' +import { + NO_DISPATCH_HOST, + WSL_MOVE_IN_PROGRESS, + WSL_MOVE_OWNED_BY_HOST, + describeScheduledRefusal +} from './dispatch-refusal' + +function repo(id: string, overrides: Partial = {}): Repo { + return { id, path: `/ws/${id}`, displayName: id, badgeColor: '#000', addedAt: 0, ...overrides } +} + +const refusalFor = (target: Repo, canDispatch = true) => + describeScheduledRefusal({ target: { ok: true, cwd: target.path, repo: target }, canDispatch }) + +let userDataPath: string +beforeEach(() => { + userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-automation-')) + loadWslMoveFence(userDataPath) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) +}) +afterEach(() => { + resetWslMoveFenceForTests() + rmSync(userDataPath, { recursive: true, force: true }) +}) + +describe('describeScheduledRefusal under a WSL move', () => { + it('refuses a run in the moving project with one fixed sentence', () => { + expect(refusalFor(repo('repo-1'))).toBe(WSL_MOVE_IN_PROGRESS) + expect(refusalFor(repo('repo-1'), false)).toBe(WSL_MOVE_IN_PROGRESS) + expect(WSL_MOVE_IN_PROGRESS).toBe( + "This automation's project is moving to its WSL host, so Orca skipped this run. Runs resume when the move finishes." + ) + }) + + it('says the WSL host runs it once the host owns the project', () => { + seedWslMoveCutover(userDataPath, cutover('move-1', 'destination-committed')) + expect(refusalFor(repo('repo-1'))).toBe(WSL_MOVE_OWNED_BY_HOST) + expect(WSL_MOVE_OWNED_BY_HOST).toBe( + "This automation's project now runs on its WSL host, which runs this automation there. Orca no longer runs this local copy." + ) + }) + + it('runs a project the move is not taking', () => { + expect(refusalFor(repo('repo-2'))).toBeNull() + expect(refusalFor(repo('repo-2'), false)).toBe(NO_DISPATCH_HOST) + }) + + it('runs the same repo id on another host', () => { + expect( + refusalFor(repo('repo-1', { connectionId: 'target-1', executionHostId: 'ssh:target-1' })) + ).toBeNull() + }) +}) diff --git a/src/main/automations/dispatch-refusal.ts b/src/main/automations/dispatch-refusal.ts index 286abb2f252..19d1841773a 100644 --- a/src/main/automations/dispatch-refusal.ts +++ b/src/main/automations/dispatch-refusal.ts @@ -14,6 +14,7 @@ import type { } from '../../shared/automations-types' import { resolveAutomationRunTarget, type AutomationRunTargetResult } from './run-target-resolution' import type { AutomationRunWriter } from './automation-run-writer' +import { wslMoveRepoBackoff } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' export const NO_DISPATCH_HOST = 'No Orca window was available to launch the automation.' @@ -24,6 +25,14 @@ export const UNEVALUABLE_SCHEDULE = /** A record the authority refuses to execute at all, with no target diagnosis of its own. */ export const NO_RUNNABLE_HOST = 'This automation has no host to run on.' +/** A move is taking its project; the schedule keeps ticking, so runs resume once it finishes. */ +export const WSL_MOVE_IN_PROGRESS = + "This automation's project is moving to its WSL host, so Orca skipped this run. Runs resume when the move finishes." + +/** The WSL host owns its project now; this retained local copy never runs again. */ +export const WSL_MOVE_OWNED_BY_HOST = + "This automation's project now runs on its WSL host, which runs this automation there. Orca no longer runs this local copy." + /** Every reason this occurrence cannot start, decided before a run row exists so * the scheduler can fold repeats instead of writing one row each. */ export function describeScheduledRefusal(input: { @@ -33,6 +42,11 @@ export function describeScheduledRefusal(input: { if (!input.target.ok) { return input.target.error } + // Refused here, before a run row or a worktree create, so repeats fold into one skip. + const backoff = wslMoveRepoBackoff(input.target.repo) + if (backoff) { + return backoff === 'moving' ? WSL_MOVE_IN_PROGRESS : WSL_MOVE_OWNED_BY_HOST + } return input.canDispatch ? null : NO_DISPATCH_HOST } diff --git a/src/main/automations/headless-dispatch-runner.ts b/src/main/automations/headless-dispatch-runner.ts index 7315e33c390..85e63438bcd 100644 --- a/src/main/automations/headless-dispatch-runner.ts +++ b/src/main/automations/headless-dispatch-runner.ts @@ -26,6 +26,9 @@ export type HeadlessAutomationDispatchContext = { watchRun: (run: AutomationRun) => void } +/** Thrown by a dispatcher that finds the run should not start; recorded as a skip, not a failure. */ +export class AutomationDispatchRefusedError extends Error {} + function describeDispatchError(error: unknown): string { return error instanceof Error ? error.message : String(error) } @@ -51,7 +54,8 @@ export async function runHeadlessAutomationDispatch( } catch (error) { return runs.updateRun({ runId: run.id, - status: 'dispatch_failed', + status: + error instanceof AutomationDispatchRefusedError ? 'skipped_unavailable' : 'dispatch_failed', workspaceId: automation.workspaceId, error: describeDispatchError(error) }) diff --git a/src/main/automations/service.ts b/src/main/automations/service.ts index 107f01ae234..7765a3de42b 100644 --- a/src/main/automations/service.ts +++ b/src/main/automations/service.ts @@ -30,7 +30,8 @@ import { missedBeyondGrace, recordMissedRun, recordRefusedAutomationRun, - recordUnevaluableAutomation + recordUnevaluableAutomation, + WSL_MOVE_OWNED_BY_HOST } from './dispatch-refusal' import type { AutomationsChangedPayload, @@ -283,6 +284,11 @@ export class AutomationService { const target = this.resolveTarget(automation) const canDispatch = this.canDispatchToRenderer() || Boolean(this.headlessDispatcher) const refusal = describeScheduledRefusal({ target, canDispatch }) + // The WSL host runs this one now; a skip per tick would only churn the retained local copy. + if (refusal === WSL_MOVE_OWNED_BY_HOST) { + await this.runs.advanceNextRun(automation.id, now) + return + } if (refusal && (await this.runs.repeatSkip(automation.id, refusal, scheduledFor))) { await this.runs.advanceNextRun(automation.id, now) return diff --git a/src/main/browser/browser-popup-background-cdp-probe.ts b/src/main/browser/browser-popup-background-cdp-probe.ts index 171125823ce..4552730417e 100644 --- a/src/main/browser/browser-popup-background-cdp-probe.ts +++ b/src/main/browser/browser-popup-background-cdp-probe.ts @@ -98,14 +98,30 @@ export async function clickBackgroundPopupProbe( page: Page, probe: BackgroundPopupProbe ): Promise { - if (probe.context === 'opaque') { - // Electron does not forward opaque OOPIF input to the root route gesture gate. - await page.locator('#background-gesture').click() - } const locator = probe.context === 'subframe' || probe.context === 'opaque' ? page.frameLocator('#background-probe').locator('#background-open') : page.locator('#background-open') + if (probe.context === 'opaque') { + // A hidden OOPIF can finish loading before browser input reaches its new frame. + await locator.evaluate((element) => { + element.removeAttribute('data-background-input-ready') + element.addEventListener('pointermove', (event) => { + if (event.isTrusted) { + element.setAttribute('data-background-input-ready', 'true') + } + }) + }) + await page.locator('#background-gesture').click() + await expect + .poll(async () => { + await locator.hover() + return locator.getAttribute('data-background-input-ready') + }) + .toBe('true') + // Opaque frame input does not reach Electron's root route gesture gate. + await page.locator('#background-gesture').click() + } await locator.click( probe.input === 'middle' ? { button: 'middle' } : { modifiers: [backgroundModifier] } ) diff --git a/src/main/browser/browser-popup-compatibility-cdp-fixture.ts b/src/main/browser/browser-popup-compatibility-cdp-fixture.ts index e291a1677ac..96ac766eb04 100644 --- a/src/main/browser/browser-popup-compatibility-cdp-fixture.ts +++ b/src/main/browser/browser-popup-compatibility-cdp-fixture.ts @@ -6,10 +6,12 @@ import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { join } from 'node:path' import { chromium, type Browser, type Page } from 'playwright' -import { afterAll, afterEach, beforeAll, expect, vi } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, expect, vi } from 'vitest' import { build as buildVite } from 'vite' import { waitForBrowserCdpEndpoint } from './browser-session-ua-cdp-collector' +import { popupFixtureHtml } from './browser-popup-compatibility-page-fixture' import { resolveElectronProbeLaunch } from './electron-probe-display-launch' +import { startHiddenRendererFrameCapture } from './hidden-renderer-frame-capture-fixture' // Hidden renderer startup and CDP events can exceed the default one-second poll on CI. vi.setConfig({ expect: { poll: { timeout: 10_000 } } }) @@ -35,6 +37,7 @@ export let browser: Browser | null = null export let origin = '' let stderr = '' const pages = new Map() +const stopFrameCaptures = new Map Promise>() const statusPath = join(root, 'status.json') export const artifactDir = process.env.ORCA_POPUP_FIXTURE_ARTIFACT_DIR export const snapshots: ReturnType[] = [] @@ -77,25 +80,12 @@ export function status(): { return JSON.parse(readFileSync(statusPath, 'utf8')) } -function fixtureHtml(): string { - return ` - - - - - -
-
-
- Docs - OAuth link - Background - Popup link - - ` -} - +const cdpConnected = Promise.withResolvers() const server = createServer((request, response) => { + if (request.url === '/fixture-cdp-connected') { + void cdpConnected.promise.then(() => response.writeHead(204).end()) + return + } const chunks: Buffer[] = [] request.on('data', (chunk: Buffer) => chunks.push(chunk)) request.on('end', () => { @@ -127,7 +117,7 @@ const server = createServer((request, response) => { response.setHeader('Content-Type', 'text/html') response.end( ['/local', '/route', '/local-control', '/route-control'].includes(path) - ? fixtureHtml() + ? popupFixtureHtml(origin) : `Hostile synthetic title${path}` ) }) @@ -213,6 +203,7 @@ beforeAll(async () => { }) await waitForBrowserCdpEndpoint(cdpPort) browser = await chromium.connectOverCDP(`http://127.0.0.1:${cdpPort}`) + cdpConnected.resolve() await waitForFixture(() => { if (!existsSync(statusPath)) { return false @@ -246,6 +237,17 @@ beforeAll(async () => { } }, 60_000) +beforeEach(async () => { + for (const page of pages.values()) { + if (!stopFrameCaptures.has(page)) { + stopFrameCaptures.set( + page, + await startHiddenRendererFrameCapture(page, (hasFrame) => expect.poll(hasFrame).toBe(true)) + ) + } + } +}) + afterEach(async () => { const route = pages.get('route') if (route && !route.isClosed()) { @@ -258,6 +260,8 @@ afterEach(async () => { }) afterAll(async () => { + cdpConnected.resolve() + await Promise.allSettled([...stopFrameCaptures.values()].map((stop) => stop())) if (artifactDir && existsSync(statusPath)) { mkdirSync(artifactDir, { recursive: true }) writeFileSync( diff --git a/src/main/browser/browser-popup-compatibility-electron-fixture.ts b/src/main/browser/browser-popup-compatibility-electron-fixture.ts index bc48843b4f9..1196ff997e6 100644 --- a/src/main/browser/browser-popup-compatibility-electron-fixture.ts +++ b/src/main/browser/browser-popup-compatibility-electron-fixture.ts @@ -286,6 +286,8 @@ async function run(): Promise { writeStatus() } }) + // Playwright must attach before Electron changes guest targets from pages to webviews. + await fetch(`${fixtureOrigin}/fixture-cdp-connected`) await createHost('local', localPartition) phase = 'local' writeStatus() diff --git a/src/main/browser/browser-popup-compatibility-page-fixture.ts b/src/main/browser/browser-popup-compatibility-page-fixture.ts new file mode 100644 index 00000000000..66869c8fafd --- /dev/null +++ b/src/main/browser/browser-popup-compatibility-page-fixture.ts @@ -0,0 +1,17 @@ +export function popupFixtureHtml(origin: string): string { + return ` + + + + + +
+
+
+ Docs + OAuth link + Background + Popup link + + ` +} diff --git a/src/main/browser/hidden-renderer-frame-capture-fixture.ts b/src/main/browser/hidden-renderer-frame-capture-fixture.ts new file mode 100644 index 00000000000..8019bdd8e10 --- /dev/null +++ b/src/main/browser/hidden-renderer-frame-capture-fixture.ts @@ -0,0 +1,47 @@ +import type { Page } from 'playwright' + +export async function startHiddenRendererFrameCapture( + page: Page, + waitForFirstFrame: (hasFrame: () => boolean) => Promise +): Promise<() => Promise> { + const cdp = await page.context().newCDPSession(page) + let stopped = false + const handlePageClose = (): void => { + void stop().catch(() => {}) + } + const stop = async (): Promise => { + if (stopped) { + return + } + stopped = true + try { + await cdp.send('Page.stopScreencast') + } finally { + page.off('close', handlePageClose) + cdp.off('Page.screencastFrame', acknowledgeFrame) + await cdp.detach() + } + } + let hasFrame = false + const acknowledgeFrame = (frame: { sessionId: number }): void => { + hasFrame = true + void cdp.send('Page.screencastFrameAck', { sessionId: frame.sessionId }).catch(() => {}) + } + cdp.on('Page.screencastFrame', acknowledgeFrame) + page.once('close', handlePageClose) + try { + // Keep hidden renderers painting without revealing their native windows. + await cdp.send('Page.enable') + await cdp.send('Page.startScreencast', { + format: 'jpeg', + quality: 20, + maxWidth: 640, + maxHeight: 480 + }) + await waitForFirstFrame(() => hasFrame) + return stop + } catch (error) { + await stop().catch(() => {}) + throw error + } +} diff --git a/src/main/daemon/daemon-init-restart-sequence.test.ts b/src/main/daemon/daemon-init-restart-sequence.test.ts index df3b48417cb..e9cfcd704e9 100644 --- a/src/main/daemon/daemon-init-restart-sequence.test.ts +++ b/src/main/daemon/daemon-init-restart-sequence.test.ts @@ -74,7 +74,9 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => { // killedCount must be 3 — proves the count was taken *before* fanout cleared the set (a swapped-order bug reports 0). expect(result.killedCount).toBe(3) - expect(originalAdapter.fanoutSyntheticExits).toHaveBeenCalledWith(-1) + expect(originalAdapter.fanoutSyntheticExits).toHaveBeenCalledWith(-1, { + replacedByRestart: true + }) // Ordering invariant: synthetic exits must reach the renderer *before* listeners are torn down (Step 1 before 2). expect(order).toEqual(['fanout', 'unbind']) }) diff --git a/src/main/daemon/daemon-provider-restart.ts b/src/main/daemon/daemon-provider-restart.ts index f6d11d868f8..83f476c8396 100644 --- a/src/main/daemon/daemon-provider-restart.ts +++ b/src/main/daemon/daemon-provider-restart.ts @@ -83,9 +83,9 @@ async function runRestartDaemon(expectedDaemonPid?: number): Promise(listeners: readonly T[], emit: (listener: T) export function createPtyExitPayload( id: string, - { code, incarnationId, cause }: Omit + { code, incarnationId, cause, replacedByRestart }: Omit ): DaemonPtyRouterExitEvent { return { id, code, ...(incarnationId ? { incarnationId } : {}), - ...(cause ? { cause } : {}) + ...(cause ? { cause } : {}), + ...(replacedByRestart ? { replacedByRestart } : {}) } } diff --git a/src/main/daemon/daemon-pty-router-events.ts b/src/main/daemon/daemon-pty-router-events.ts index 92b626d36e9..eb12c2d7cfe 100644 --- a/src/main/daemon/daemon-pty-router-events.ts +++ b/src/main/daemon/daemon-pty-router-events.ts @@ -15,4 +15,6 @@ export type DaemonPtyRouterExitEvent = { incarnationId?: PtyIncarnationId /** Absent from a daemon predating exit causes; readers fall back to `unknown`. */ cause?: TerminalExitCause + /** Main ended the session to restart the daemon; the pane stays for its next process. */ + replacedByRestart?: true } diff --git a/src/main/daemon/daemon-pty-session-inventory.ts b/src/main/daemon/daemon-pty-session-inventory.ts index dc3c816f1ec..5f3064dbec6 100644 --- a/src/main/daemon/daemon-pty-session-inventory.ts +++ b/src/main/daemon/daemon-pty-session-inventory.ts @@ -137,7 +137,7 @@ export abstract class DaemonPtySessionInventory extends DaemonPtyProcessInspecti // Why: the daemon's kill-all-and-shutdown path suppresses onExit fanout (session.ts:246-252), so synthesize pty:exit // for every live session before teardown or renderer panes black-hole writes to a disposed adapter forever. - fanoutSyntheticExits(code: number): void { + fanoutSyntheticExits(code: number, exit: { replacedByRestart?: true } = {}): void { const ids = [...this.activeSessionIds] this.activeSessionIds.clear() this.sessionsAwaitingDaemonRecovery.clear() @@ -158,7 +158,11 @@ export abstract class DaemonPtySessionInventory extends DaemonPtyProcessInspecti // Why: don't catch listener throws — matches the natural onExit fanout so synthetic exits keep the same error semantics. emitPtyListeners(this.exitListeners, (listener) => listener( - createPtyExitPayload(id, { code, incarnationId: this.sessionIncarnations.get(id) }) + createPtyExitPayload(id, { + code, + incarnationId: this.sessionIncarnations.get(id), + ...exit + }) ) ) this.sessionIncarnations.delete(id) diff --git a/src/main/daemon/daemon-pty-startup-delivery.test.ts b/src/main/daemon/daemon-pty-startup-delivery.test.ts index 6691ca0a514..e005df68524 100644 --- a/src/main/daemon/daemon-pty-startup-delivery.test.ts +++ b/src/main/daemon/daemon-pty-startup-delivery.test.ts @@ -73,14 +73,19 @@ describe('DaemonPtyAdapter startup delivery', () => { const missingShell = join(dir, 'missing-fish') const fallbackName = basename(localPtyUtils.resolveUnixShellPath(missingShell)) context.skip(!['bash', 'zsh'].includes(fallbackName), 'Requires a Bash/zsh fallback') - await adapter.spawn({ - cols: 80, - rows: 24, - command: 'codex', - env: { SHELL: source === 'environment' ? missingShell : '/bin/sh' }, - ...(source === 'override' ? { shellOverride: missingShell } : {}) - }) - await new Promise((resolve) => setTimeout(resolve, 350)) + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) + try { + await adapter.spawn({ + cols: 80, + rows: 24, + command: 'codex', + env: { SHELL: source === 'environment' ? missingShell : '/bin/sh' }, + ...(source === 'override' ? { shellOverride: missingShell } : {}) + }) + await vi.advanceTimersByTimeAsync(350) + } finally { + vi.useRealTimers() + } expect(lastSubprocess.write).not.toHaveBeenCalled() expect(lastSpawnOpts).toEqual( expect.objectContaining({ startupCommandDelivery: 'shell-ready' }) @@ -96,9 +101,13 @@ describe('DaemonPtyAdapter startup delivery', () => { { command: 'codex', startupCommandDelivery: 'fast' as const }, { command: "codex 'linked issue context'", startupCommandDelivery: 'shell-ready' as const } ])('waits past 300ms and submits once after readiness: %j', async (startup) => { - await adapter.spawn({ cols: 80, rows: 24, ...startup, env: { SHELL: '/bin/zsh' } }) - - await new Promise((resolve) => setTimeout(resolve, 350)) + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) + try { + await adapter.spawn({ cols: 80, rows: 24, ...startup, env: { SHELL: '/bin/zsh' } }) + await vi.advanceTimersByTimeAsync(350) + } finally { + vi.useRealTimers() + } expect(lastSubprocess.write).not.toHaveBeenCalled() expect(lastSpawnOpts).toEqual( expect.objectContaining({ startupCommandDelivery: 'shell-ready' }) diff --git a/src/main/daemon/daemon-restart-state.test.ts b/src/main/daemon/daemon-restart-state.test.ts new file mode 100644 index 00000000000..f9280702050 --- /dev/null +++ b/src/main/daemon/daemon-restart-state.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { runCoalescedDaemonRestart, settledDaemonRestart } from './daemon-restart-state' + +describe('settledDaemonRestart', () => { + it('is undefined with no restart running', () => { + expect(settledDaemonRestart()).toBeUndefined() + }) + + it('holds until the running restart settles, whether it succeeds or fails', async () => { + for (const outcome of ['resolve', 'reject'] as const) { + let finish!: () => void + const restart = runCoalescedDaemonRestart( + () => + new Promise((resolve, reject) => { + finish = () => + outcome === 'resolve' ? resolve({ killedCount: 1 }) : reject(new Error('x')) + }) + ) + restart.catch(() => {}) + let settled = false + void settledDaemonRestart()?.then(() => { + settled = true + }) + await Promise.resolve() + expect(settled).toBe(false) + + finish() + await restart.catch(() => {}) + await Promise.resolve() + expect(settled).toBe(true) + expect(settledDaemonRestart()).toBeUndefined() + } + }) +}) diff --git a/src/main/daemon/daemon-restart-state.ts b/src/main/daemon/daemon-restart-state.ts index e37fad69709..70918375898 100644 --- a/src/main/daemon/daemon-restart-state.ts +++ b/src/main/daemon/daemon-restart-state.ts @@ -9,6 +9,14 @@ export function isDaemonRestartInFlight(): boolean { return restartInFlight !== null } +/** Settles once an in-flight restart has swapped in its provider (or failed); undefined when none. */ +export function settledDaemonRestart(): Promise | undefined { + return restartInFlight?.then( + () => undefined, + () => undefined + ) +} + export function runCoalescedDaemonRestart( run: () => Promise ): Promise { diff --git a/src/main/daemon/degraded-daemon-pty-provider.test.ts b/src/main/daemon/degraded-daemon-pty-provider.test.ts index eed79501220..161a676d2b8 100644 --- a/src/main/daemon/degraded-daemon-pty-provider.test.ts +++ b/src/main/daemon/degraded-daemon-pty-provider.test.ts @@ -666,10 +666,14 @@ describe('DegradedDaemonPtyProvider', () => { provider.onExit(exitSpy) await provider.discoverDaemonSessions() - provider.fanoutCurrentDaemonSyntheticExits(-1) + provider.fanoutCurrentDaemonSyntheticExits(-1, { replacedByRestart: true }) expect(exitSpy).toHaveBeenCalledOnce() - expect(exitSpy).toHaveBeenCalledWith({ id: 'current-session', code: -1 }) + expect(exitSpy).toHaveBeenCalledWith({ + id: 'current-session', + code: -1, + replacedByRestart: true + }) expect(provider.getSessionIdsRoutedTo('current-daemon')).toEqual([]) expect(provider.hasPty('legacy-session')).toBe(true) }) diff --git a/src/main/daemon/degraded-daemon-pty-provider.ts b/src/main/daemon/degraded-daemon-pty-provider.ts index f1db303ddfd..bf021bce8f8 100644 --- a/src/main/daemon/degraded-daemon-pty-provider.ts +++ b/src/main/daemon/degraded-daemon-pty-provider.ts @@ -33,7 +33,7 @@ export class DegradedDaemonPtyProvider implements IPtyProvider { private ownerRecovery: DegradedDaemonOwnerRecovery private unsubscribers: (() => void)[] = [] private dataListeners: ((payload: PtyDataEvent) => void)[] = [] - private exitListeners: ((payload: { id: string; code: number }) => void)[] = [] + private exitListeners: Parameters[0][] = [] constructor(opts: { current: DaemonPtyAdapter @@ -267,7 +267,7 @@ export class DegradedDaemonPtyProvider implements IPtyProvider { return trackedUnsubscribe } - onExit(callback: (payload: { id: string; code: number }) => void): () => void { + onExit(callback: Parameters[0]): () => void { this.exitListeners.push(callback) return () => { const idx = this.exitListeners.indexOf(callback) @@ -317,13 +317,13 @@ export class DegradedDaemonPtyProvider implements IPtyProvider { return listProviderSessionIds(this.sessionProviders, target) } - fanoutCurrentDaemonSyntheticExits(code: number): void { + fanoutCurrentDaemonSyntheticExits(code: number, exit: { replacedByRestart?: true } = {}): void { for (const id of this.getSessionIdsRoutedTo('current-daemon')) { this.sessionProviders.delete(id) // Why: restart kills listed sessions even when the adapter did not track them active. // oxlint-disable-next-line unicorn/no-useless-spread -- copy-safe: listeners may unsubscribe during iteration for (const listener of [...this.exitListeners]) { - listener({ id, code }) + listener({ id, code, ...exit }) } } } diff --git a/src/main/daemon/shell-ready.ts b/src/main/daemon/shell-ready.ts index 69f11298965..ece83ba8055 100644 --- a/src/main/daemon/shell-ready.ts +++ b/src/main/daemon/shell-ready.ts @@ -103,6 +103,18 @@ export function resolvePtyShellPath(env: Record): string { return env.SHELL || process.env.SHELL || '/bin/zsh' } +/** + * The shell a Unix pane asks for, before the executable fallback: the pane's own override, the + * host's default-shell setting, then SHELL. + */ +export function resolveUnixPaneShellPath( + env: Record, + shellOverride: string | undefined, + defaultShell: string | undefined +): string { + return shellOverride || defaultShell?.trim() || resolvePtyShellPath(env) +} + export function shellPathSupportsPtyStartupBarrier(shellPath: string): boolean { const shellName = pathWin32.basename(basename(shellPath)).toLowerCase() // Why fish: markerless, its startup command is written before fish's reader owns diff --git a/src/main/daemon/slow-daemon-session-verification.test.ts b/src/main/daemon/slow-daemon-session-verification.test.ts index 9668f1de26d..1781d423135 100644 --- a/src/main/daemon/slow-daemon-session-verification.test.ts +++ b/src/main/daemon/slow-daemon-session-verification.test.ts @@ -1,6 +1,7 @@ import './mock-descendant-sweep' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { connect, createServer, type Server, type Socket } from 'node:net' +import { once } from 'node:events' import { tmpdir } from 'node:os' import { join } from 'node:path' import { mkdirSync, mkdtempSync, rmSync } from 'node:fs' @@ -15,10 +16,7 @@ import type { SubprocessHandle } from './session-subprocess-handle' // time out the 3s startup health check against a daemon that was alive and // owning sessions, and the unhealthy path killed it. The fix re-verifies // with listSessions, which has far larger budgets (5s hello, 30s request). -// This test reproduces the production asymmetry against a REAL daemon by -// inserting a response delay that exceeds the health-check budget but fits -// the verification budgets, and asserts the guard's two inputs disagree the -// way the fix depends on. +// Real sockets preserve the handshake; virtual time crosses both production budgets. const RESPONSE_DELAY_MS = 3_500 function createMockSubprocess(): SubprocessHandle { @@ -52,6 +50,7 @@ function startDelayProxy(listenPath: string, upstreamPath: string): Server { clientSocket.write(chunk) } }, RESPONSE_DELAY_MS) + proxy.emit('response-queued') }) const teardown = (): void => { clientSocket.destroy() @@ -88,6 +87,7 @@ describe('slow daemon session verification', () => { }) afterEach(async () => { + vi.useRealTimers() for (const client of clients.splice(0)) { client.disconnect() } @@ -121,17 +121,33 @@ describe('slow daemon session verification', () => { }) proxy = startDelayProxy(proxySocketPath, daemonSocketPath) + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) // The exact pre-fix kill trigger: the daemon is alive but too slow for // the health-check budget. - await expect(healthCheckDaemon(proxySocketPath, tokenPath)).resolves.toBe(false) + const healthResponse = once(proxy, 'response-queued') + const health = healthCheckDaemon(proxySocketPath, tokenPath) + await healthResponse + await vi.advanceTimersByTimeAsync(3_000) + await expect(health).resolves.toBe(false) // The fix's re-verification against the SAME slow daemon: the larger // client budgets absorb the latency and prove the session is alive. const verificationClient = new DaemonClient({ socketPath: proxySocketPath, tokenPath }) clients.push(verificationClient) - await verificationClient.ensureConnected() - const result = await verificationClient.request('listSessions', undefined) + const helloResponse = once(proxy, 'response-queued') + const connected = verificationClient.ensureConnected() + await helloResponse + const streamResponse = once(proxy, 'response-queued') + await vi.advanceTimersByTimeAsync(RESPONSE_DELAY_MS) + await streamResponse + await vi.advanceTimersByTimeAsync(RESPONSE_DELAY_MS) + await connected + const sessionsResponse = once(proxy, 'response-queued') + const sessions = verificationClient.request('listSessions', undefined) + await sessionsResponse + await vi.advanceTimersByTimeAsync(RESPONSE_DELAY_MS) + const result = await sessions const liveSessionCount = result.sessions.filter((session) => session.isAlive).length expect(liveSessionCount).toBe(1) } diff --git a/src/main/git/repo-ref-maintenance-real-git.test.ts b/src/main/git/repo-ref-maintenance-real-git.test.ts index 7c3041e6dfe..fc74a1c2d18 100644 --- a/src/main/git/repo-ref-maintenance-real-git.test.ts +++ b/src/main/git/repo-ref-maintenance-real-git.test.ts @@ -1,8 +1,8 @@ import { execFileSync } from 'node:child_process' -import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises' +import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest' import { countLooseRefs } from '../../shared/loose-ref-count' import { RepoRefMaintenance } from '../../shared/repo-ref-maintenance' import { PACK_INDEX_MAINTENANCE_COOLDOWN_MS } from '../../shared/repo-pack-index-maintenance-policy' @@ -20,6 +20,8 @@ const roots: string[] = [] // three real `pack-refs` runs before the deferral budget is spent. const QUIET_MS = 25 const THRESHOLD = 20 +let packFixtureRoot = '' +let packFixtureBlobs: string[] = [] function git(cwd: string, args: string[], input?: string): string { return execFileSync('git', args, { @@ -45,11 +47,33 @@ function hasWriteOption(option: string): boolean { } async function createFragmentedPacks(repoPath: string): Promise { + await cp( + join(packFixtureRoot, '.git', 'objects', 'pack'), + join(repoPath, '.git', 'objects', 'pack'), + { + recursive: true + } + ) + return packFixtureBlobs +} + +beforeAll(async () => { + // Each test gets its own pack files; generating the same 64 packs only needs to happen once. + packFixtureRoot = await mkdtemp(join(tmpdir(), 'orca-fragmented-packs-')) + git(packFixtureRoot, ['init', '--quiet']) + packFixtureBlobs = await buildFragmentedPacks(packFixtureRoot) +}) + +afterAll(async () => { + await rm(packFixtureRoot, { recursive: true, force: true }) +}) + +async function buildFragmentedPacks(repoPath: string): Promise { const objects = join(repoPath, '.git', 'objects') - const blobs: string[] = [] - for (let index = 0; index < PACK_INDEX_THRESHOLD; index += 1) { - const blob = git(repoPath, ['hash-object', '-w', '--stdin'], `packed-${index}\n`) - blobs.push(blob) + const files = Array.from({ length: PACK_INDEX_THRESHOLD }, (_, index) => `packed-${index}`) + await Promise.all(files.map((file) => writeFile(join(repoPath, file), `${file}\n`))) + const blobs = git(repoPath, ['hash-object', '-w', ...files]).split('\n') + for (const blob of blobs) { git(repoPath, ['pack-objects', join(objects, 'pack', 'pack')], `${blob}\n`) await rm(join(objects, blob.slice(0, 2), blob.slice(2))) } diff --git a/src/main/git/worktree-rebase-update-refs-real-git.test.ts b/src/main/git/worktree-rebase-update-refs-real-git.test.ts index 8da9da2388b..c655836f415 100644 --- a/src/main/git/worktree-rebase-update-refs-real-git.test.ts +++ b/src/main/git/worktree-rebase-update-refs-real-git.test.ts @@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promi import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { runProcess } from '@orca/process-host' +import { GitBinaryTestFixture } from '../../shared/git-binary.test-fixture' vi.mock('./runner', () => ({ gitExecFileAsync: vi.fn(), gitExecFileSync: vi.fn() })) vi.mock('./local-repo-ref-maintenance', () => ({ @@ -11,51 +11,19 @@ vi.mock('./local-repo-ref-maintenance', () => ({ import { forceDeleteLocalBranch } from './worktree-branch-removal' -const image = process.env.ORCA_GIT_COMPAT_IMAGE -const binary = process.env.ORCA_GIT_COMPAT_BINARY ?? 'git' -const dockerUser = - typeof process.getuid === 'function' && typeof process.getgid === 'function' - ? ['--user', `${process.getuid()}:${process.getgid()}`] - : [] +let fixture: GitBinaryTestFixture let root = '' let repo = '' let checkout = '' async function git(args: string[], cwd = repo): Promise<{ stdout: string; stderr: string }> { - const result = await runProcess({ - program: image ? 'docker' : binary, - args: image - ? [ - 'run', - '--rm', - '--network=none', - ...dockerUser, - '-v', - `${root}:${root}`, - '-w', - cwd, - image, - '-c', - `safe.directory=${cwd}`, - ...args - ] - : args, - cwd, - env: { - ...process.env, - GIT_CONFIG_GLOBAL: join(root, 'empty-config'), - GIT_CONFIG_NOSYSTEM: '1' - }, - maxOutputBytes: 2 * 1024 * 1024 - }) - if (result.code !== 0) { - throw Object.assign(new Error(result.stderr), { code: result.code, stderr: result.stderr }) - } - return result + return fixture.exec(args, cwd, { GIT_CONFIG_GLOBAL: join(root, 'empty-config') }) } beforeEach(async () => { root = await realpath(await mkdtemp(join(tmpdir(), 'orca-rebase-reservation-'))) + fixture = new GitBinaryTestFixture(root) + await fixture.start() repo = join(root, 'repo') checkout = join(root, 'checkout') await mkdir(repo) @@ -77,7 +45,11 @@ beforeEach(async () => { }) afterEach(async () => { - await rm(root, { recursive: true, force: true }) + try { + await fixture.stop() + } finally { + await rm(root, { recursive: true, force: true }) + } }) describe('rebase branch reservations with real Git', () => { diff --git a/src/main/git/worktree-safety-real-git.test.ts b/src/main/git/worktree-safety-real-git.test.ts index 9a3e2fdfffa..c32f546d309 100644 --- a/src/main/git/worktree-safety-real-git.test.ts +++ b/src/main/git/worktree-safety-real-git.test.ts @@ -1,9 +1,7 @@ -import { execFile } from 'node:child_process' import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { promisify } from 'node:util' -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const { runner } = vi.hoisted(() => ({ runner: vi.fn() })) vi.mock('./runner', () => ({ @@ -15,43 +13,15 @@ vi.mock('./status', () => ({ runWithGitReadCacheInvalidation: (run: () => unknow import { getBranchConflictKind, getBranchConflictKindViaExec } from './repo-branch-conflict' import { clearGitCapabilityStateForTests } from './git-capability-state' +import { GitBinaryTestFixture } from '../../shared/git-binary.test-fixture' -const execFileAsync = promisify(execFile) -const image = process.env.ORCA_GIT_COMPAT_IMAGE -const binary = process.env.ORCA_GIT_COMPAT_BINARY ?? 'git' const expectedVersion = process.env.ORCA_GIT_COMPAT_VERSION -const dockerUser = - typeof process.getuid === 'function' && typeof process.getgid === 'function' - ? ['--user', `${process.getuid()}:${process.getgid()}`] - : [] +let fixture: GitBinaryTestFixture let root = '' let repo = '' async function git(args: string[], cwd = repo): Promise<{ stdout: string; stderr: string }> { - return image - ? execFileAsync( - 'docker', - [ - 'run', - '--rm', - '--network=none', - ...dockerUser, - '-v', - `${root}:${root}`, - '-w', - cwd, - image, - '-c', - `safe.directory=${cwd}`, - ...args - ], - { maxBuffer: 2 * 1024 * 1024 } - ) - : execFileAsync(binary, args, { - cwd, - env: { ...process.env, HOME: root, XDG_CONFIG_HOME: root, GIT_CONFIG_NOSYSTEM: '1' }, - maxBuffer: 2 * 1024 * 1024 - }) + return fixture.exec(args, cwd) } async function initializeRepo(cwd: string): Promise { @@ -68,23 +38,30 @@ async function initializeRepo(cwd: string): Promise { } beforeAll(async () => { - const { stdout } = await execFileAsync( - image ? 'docker' : binary, - image ? ['run', '--rm', '--network=none', ...dockerUser, image, '--version'] : ['--version'] - ) + root = await realpath(await mkdtemp(join(tmpdir(), 'orca-worktree-safety-'))) + fixture = new GitBinaryTestFixture(root) + await fixture.start() + const { stdout } = await fixture.exec(['--version']) expect(stdout).toContain(expectedVersion ? `git version ${expectedVersion}` : 'git version ') }) +afterAll(async () => { + try { + await fixture.stop() + } finally { + await rm(root, { recursive: true, force: true }) + } +}) + beforeEach(async () => { - root = await realpath(await mkdtemp(join(tmpdir(), 'orca-worktree-safety-'))) - repo = join(root, 'repo') + repo = await mkdtemp(join(root, 'repo-')) await initializeRepo(repo) clearGitCapabilityStateForTests() runner.mockImplementation((args: string[], options: { cwd?: string }) => git(args, options.cwd)) }) afterEach(async () => { - await rm(root, { recursive: true, force: true }) + await rm(repo, { recursive: true, force: true }) }) describe('worktree safety with the real Git binary', () => { diff --git a/src/main/hook-termination-real-process.test.ts b/src/main/hook-termination-real-process.test.ts index 65d1b0910d1..a314a2d99f4 100644 --- a/src/main/hook-termination-real-process.test.ts +++ b/src/main/hook-termination-real-process.test.ts @@ -30,14 +30,14 @@ async function survivorsAfterDeadline( try { const result = await runHook('archive', dir, REPO, dir, undefined, 400) expect(result.success).toBe(false) - // SIGTERM lands at the deadline, SIGKILL two seconds later. - await new Promise((resolve) => setTimeout(resolve, 3_500)) expect(existsSync(pidFile)).toBe(true) pids = readFileSync(pidFile, 'utf8').trim().split(/\s+/).map(Number) // Without this, a script that recorded only the shell leaves `pids[1]` undefined, `alive` // throws, and the missing descendant reads as dead — a test that passes on nothing. expect(pids).toHaveLength(2) expect(pids.every((pid) => Number.isSafeInteger(pid) && pid > 0)).toBe(true) + // SIGTERM lands at the deadline; resistant descendants need the real SIGKILL grace. + await expect.poll(() => pids.some(alive), { timeout: 5_000, interval: 25 }).toBe(false) return { shell: alive(pids[0]!), child: alive(pids[1]!), output: result.output, pids } } finally { for (const pid of pids) { diff --git a/src/main/ipc/ai-vault-resume-chat-copy.test.ts b/src/main/ipc/ai-vault-resume-chat-copy.test.ts new file mode 100644 index 00000000000..9f38fbf24e2 --- /dev/null +++ b/src/main/ipc/ai-vault-resume-chat-copy.test.ts @@ -0,0 +1,52 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' +import { installCopyingChat } from '../ai-vault/structured-session-ownership.test-support' +import type { AiVaultPrepareSessionResumeArgs } from '../../shared/ai-vault-resume-preparation' + +vi.mock('electron', () => ({ ipcMain: { handle: vi.fn() } })) + +const { prepareAiVaultSessionResume } = await import('./ai-vault-resume') + +const OMP_ID = '019fd611-2b3c-7d4e-9f50-6b7c8d9e0f1a' +const forkArgs = ( + executionHostId: AiVaultPrepareSessionResumeArgs['executionHostId'] +): AiVaultPrepareSessionResumeArgs => ({ + agent: 'omp', + sessionId: OMP_ID, + filePath: `/Users/me/.omp/agent/sessions/--repo--/x_${OMP_ID}.jsonl`, + codexHome: null, + executionHostId, + fork: true +}) + +afterEach(() => setStructuredAgentSessionHost(null)) + +describe('Resume in New CLI preparation for a chat whose host makes the copy', () => { + it('leaves the copy to the server that runs a remote chat', async () => { + const copy = vi.fn(async () => ({ + sessionId: 'local-copy' + })) + installCopyingChat({ transport: 'acp', agent: 'omp', nativeId: 'unrelated' }, copy) + const prepareRuntimeSessionResume = vi.fn(async () => ({ + useRealCodexHome: false, + forkedSession: { + sessionId: 'remote-copy', + filePath: '/sessions/remote-copy.jsonl', + agent: 'omp' as const + } + })) + + await expect( + prepareAiVaultSessionResume(forkArgs('runtime:env-1'), { prepareRuntimeSessionResume }) + ).resolves.toEqual({ + useRealCodexHome: false, + forkedSession: { + sessionId: 'remote-copy', + filePath: '/sessions/remote-copy.jsonl', + agent: 'omp' + } + }) + expect(prepareRuntimeSessionResume).toHaveBeenCalledWith('env-1', forkArgs('runtime:env-1')) + expect(copy).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/ipc/ai-vault-resume.test.ts b/src/main/ipc/ai-vault-resume.test.ts index 89187a0593b..de1952a298a 100644 --- a/src/main/ipc/ai-vault-resume.test.ts +++ b/src/main/ipc/ai-vault-resume.test.ts @@ -1,7 +1,7 @@ import { afterEach, expect, it, vi } from 'vitest' import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' import { - installOwnership, + installCopyingChat, PROVIDER_SESSION } from '../ai-vault/structured-session-ownership.test-support' import { prepareAiVaultSessionResume } from './ai-vault-resume' @@ -10,7 +10,10 @@ vi.mock('electron', () => ({ ipcMain: { handle: vi.fn() } })) afterEach(() => setStructuredAgentSessionHost(null)) it('refuses the native owner while preserving forks and other execution hosts', async () => { - installOwnership({ handle: { transport: 'acp', agent: 'omp', nativeId: PROVIDER_SESSION } }) + installCopyingChat( + { transport: 'acp', agent: 'omp', nativeId: PROVIDER_SESSION }, + vi.fn(async () => ({ sessionId: 'copy-1', filePath: '/sessions/copy-1.jsonl' })) + ) const local = { agent: 'omp' as const, sessionId: PROVIDER_SESSION, @@ -26,7 +29,8 @@ it('refuses the native owner while preserving forks and other execution hosts', ) expect(prepareSessionResume).not.toHaveBeenCalled() await expect(prepareAiVaultSessionResume({ ...local, fork: true }, options)).resolves.toEqual({ - useRealCodexHome: false + useRealCodexHome: false, + forkedSession: { agent: 'omp', sessionId: 'copy-1', filePath: '/sessions/copy-1.jsonl' } }) const paired = { ...local, executionHostId: 'runtime:paired' as const } await expect(prepareAiVaultSessionResume(paired, options)).resolves.toEqual({ diff --git a/src/main/ipc/ai-vault-resume.ts b/src/main/ipc/ai-vault-resume.ts index 25469b1d244..c46eeaecaa2 100644 --- a/src/main/ipc/ai-vault-resume.ts +++ b/src/main/ipc/ai-vault-resume.ts @@ -8,6 +8,7 @@ import { isUnresolvedOwnerHostId, parseExecutionHostId } from '../../shared/exec import { isWslUncPath } from '../../shared/wsl-paths' import { assertLegacyAiVaultResumeAllowed } from '../ai-vault/structured-session-ownership' import { tryEnsureStructuredAgentSessionHostForResume } from '../runtime/structured-agent-session-host-refusal' +import { copyStructuredChatConversation } from '../ai-vault/structured-chat-conversation-copy' export type AiVaultResumeHandlerOptions = { ensureStructuredSessionOwnership?: () => Promise @@ -47,5 +48,8 @@ export async function prepareAiVaultSessionResume( } assertLegacyAiVaultResumeAllowed(args) } - return options.prepareSessionResume?.(args) ?? { useRealCodexHome: false } + return ( + (await copyStructuredChatConversation(args)) ?? + options.prepareSessionResume?.(args) ?? { useRealCodexHome: false } + ) } diff --git a/src/main/ipc/hooks/register-worktree-hook-file-handlers.ts b/src/main/ipc/hooks/register-worktree-hook-file-handlers.ts index e5d840f73a0..9e9dd7e7e26 100644 --- a/src/main/ipc/hooks/register-worktree-hook-file-handlers.ts +++ b/src/main/ipc/hooks/register-worktree-hook-file-handlers.ts @@ -7,6 +7,7 @@ import { joinWorktreeRelativePath } from '../../runtime/runtime-relative-paths' import { readIssueCommand, writeIssueCommand } from '../../issue-command-file' import { resolveRepoForExecutionHost } from '../worktrees/repo-host-ownership' import type { WorktreeIpcContext } from '../worktrees/worktree-ipc-context' +import { assertWslMoveRepoWritable } from '../../persistence/moving-wsl-projects/wsl-move-scope-guard' /** Route private command overrides to the owning host without changing shared hook settings. */ export function registerWorktreeHookFileHandlers(context: WorktreeIpcContext): void { @@ -49,6 +50,7 @@ export function registerWorktreeHookFileHandlers(context: WorktreeIpcContext): v if (!repo || isFolderRepo(repo)) { return } + assertWslMoveRepoWritable(store, repo) if (getStoredRepoSshConnectionId(repo)) { throw new Error('Remote filesystem unavailable. Reconnect the SSH target before retrying.') } diff --git a/src/main/ipc/jira.ts b/src/main/ipc/jira.ts index 47727598a61..2ebd3b27988 100644 --- a/src/main/ipc/jira.ts +++ b/src/main/ipc/jira.ts @@ -72,13 +72,9 @@ export function registerJiraHandlers(): void { return selectSite(siteId) }) - ipcMain.handle('jira:status', async () => { - return getStatus() - }) + ipcMain.handle('jira:status', async () => getStatus()) - ipcMain.handle('jira:readStatus', async () => { - return getStatus() - }) + ipcMain.handle('jira:readStatus', async () => getStatus()) ipcMain.handle('jira:testConnection', async (_event, args?: { siteId?: string }) => { return testConnection(normalizeJiraSiteId(args?.siteId)) diff --git a/src/main/ipc/pty-launch-parity.test.ts b/src/main/ipc/pty-launch-parity.test.ts index e9d48c17974..17016cf59cf 100644 --- a/src/main/ipc/pty-launch-parity.test.ts +++ b/src/main/ipc/pty-launch-parity.test.ts @@ -34,6 +34,7 @@ import { } from './pty-launch-parity-host.test-cases' import { isHiddenRendererPty } from './pty-hidden-delivery-gate' import { ptySizes } from './pty/delivery/visibility-state' +import { agentSessionOwners } from './pty/pane/agent-session-owners' import { makePaneSpawnReservationKey, paneSpawnReservationsByOwnerKey @@ -311,7 +312,7 @@ describe('launch facts outside the tables', () => { // C5a (one execute) keeps this on both lanes; C5a-2 flips only the host's agent_error. const FAILED_START_AGENT_ERROR: Record<'window' | 'host', unknown[]> = { window: [['agent_error', { agent_kind: 'claude-code', error_class: 'binary_not_found' }]], - host: [] + host: [['agent_error', { agent_kind: 'claude-code', error_class: 'binary_not_found' }]] } it.each(['window', 'host'] as const)( '%s lane: a start the provider fails undoes everything it claimed', @@ -365,6 +366,54 @@ describe('launch facts outside the tables', () => { } ) + // C5a-2: requests that join one claimed start share its failure; only the request that started it counts it. + it('host lane: concurrent resumes of one Claude session count one failed start once', async () => { + setMainPlatform('darwin') + classifyErrorMock.mockReturnValue({ error_class: 'binary_not_found' }) + const lanes = startParityLanes(suite, { workspace: POSIX_REPO }) + lanes.provider.mockImplementation(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + throw new Error('spawn boom') + }) + const resume = { + kind: 'explicit' as const, + worktree: `id:${launchWorkspaceId(POSIX_REPO)}`, + agent: 'claude' as const, + providerSession: { key: 'session_id' as const, id: '11111111-1111-4111-8111-111111111111' } + } + + const results = await Promise.allSettled([ + lanes.runtime.ensureAgentSession(resume), + lanes.runtime.ensureAgentSession(resume) + ]) + + expect(results.map((r) => r.status)).toEqual(['rejected', 'rejected']) + expect(lanes.provider).toHaveBeenCalledOnce() + expect(trackMock.mock.calls.filter(([event]) => event === 'agent_error')).toEqual([ + ['agent_error', { agent_kind: 'claude-code', error_class: 'binary_not_found' }] + ]) + }) + + it('host lane: a Claude resume its claim refuses counts one failed start', async () => { + setMainPlatform('darwin') + classifyErrorMock.mockReturnValue({ error_class: 'unknown' }) + const lanes = startParityLanes(suite, { workspace: POSIX_REPO }) + vi.spyOn(agentSessionOwners, 'ensure').mockRejectedValueOnce( + new Error('agent_session_conflict') + ) + + await expect( + lanes.runtime.ensureAgentSession({ + kind: 'explicit', + worktree: `id:${launchWorkspaceId(POSIX_REPO)}`, + agent: 'claude', + providerSession: { key: 'session_id', id: '22222222-2222-4222-8222-222222222222' } + }) + ).rejects.toThrow('agent_session_conflict') + expect(lanes.provider).not.toHaveBeenCalled() + expect(trackMock.mock.calls.filter(([event]) => event === 'agent_error')).toHaveLength(1) + }) + // C5e (presentation merge): a worker asks the window to reveal its titled tab without activating it. it('row 10: an orchestration worker requests a titled, inactive reveal', async () => { setMainPlatform(WORKER.os) diff --git a/src/main/ipc/pty/host-env/spawn-cwd-wsl-move.test.ts b/src/main/ipc/pty/host-env/spawn-cwd-wsl-move.test.ts index 043dd5829b1..ae7fdf06a5a 100644 --- a/src/main/ipc/pty/host-env/spawn-cwd-wsl-move.test.ts +++ b/src/main/ipc/pty/host-env/spawn-cwd-wsl-move.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { wslMoveCutoverFixture as cutover } from '../../../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' -import { writeWslMoveCutover } from '../../../persistence/moving-wsl-projects/wsl-move-cutover-journal' +import { seedWslMoveCutover } from '../../../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' import { WslMoveProjectFencedError, loadWslMoveFence, @@ -24,7 +24,7 @@ const accept = (worktreeId: string, sessionId?: string) => beforeEach(() => { userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-spawn-')) loadWslMoveFence(userDataPath) - writeWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) listLiveDaemonPtyIds.mockReset() }) afterEach(() => { diff --git a/src/main/ipc/pty/ipc/spawn-execute.ts b/src/main/ipc/pty/ipc/spawn-execute.ts index 2e48c431c07..e4b7fe2bb8c 100644 --- a/src/main/ipc/pty/ipc/spawn-execute.ts +++ b/src/main/ipc/pty/ipc/spawn-execute.ts @@ -107,8 +107,7 @@ export async function executePtyIpcSpawn(ctx: PtyIpcSpawnState): Promise { throw failPtySpawnExecute(ctx, err, { sessionId: ctx.effectiveSessionId, ownsNewSession: ctx.isMintedSessionId, - telemetry: args.telemetry, - recordsAgentError: true + telemetry: args.telemetry }) } finally { if (ctx.preAllocatedHandle) { diff --git a/src/main/ipc/pty/provider/bind-listeners.ts b/src/main/ipc/pty/provider/bind-listeners.ts index ec13ad0d36e..5d455b43539 100644 --- a/src/main/ipc/pty/provider/bind-listeners.ts +++ b/src/main/ipc/pty/provider/bind-listeners.ts @@ -88,28 +88,40 @@ export function bindProviderListeners(session: PtyIpcSession): void { return } const syntheticExit = session.consumeSyntheticKillExit(payload.id, payload.incarnationId) - if (!isLocalProvider) { - clearProviderPtyState(payload.id) - ptyOwnership.delete(payload.id) - if (syntheticExit) { - session.runtime?.markPtyStopRequested(payload.id) + // Main restarted the daemon: the pane outlives its process, for the runtime and the window. + const settleRestartStop = payload.replacedByRestart + ? session.runtime?.intentionalPtyStops?.mark( + payload.id, + 'replaced', + payload.incarnationId ?? null + ) + : undefined + try { + if (!isLocalProvider) { + clearProviderPtyState(payload.id) + ptyOwnership.delete(payload.id) + if (syntheticExit) { + session.runtime?.markPtyStopRequested(payload.id) + } + session.runtime?.onPtyExit(payload.id, payload.code, payload.incarnationId, { + providerExitObserved: true, + ...(payload.cause ? { cause: payload.cause } : {}) + }) } - session.runtime?.onPtyExit(payload.id, payload.code, payload.incarnationId, { - providerExitObserved: true, - ...(payload.cause ? { cause: payload.cause } : {}) + // The control reply can overtake stream data; the physical exit must retire that late output. + if (syntheticExit) { + return + } + // Why not the whole payload: the exit cause is a main-process fact for the + // runtime's records; the renderer's pty:exit contract stays as it was. + session.sendPtyExitToRenderer({ + id: payload.id, + code: payload.code, + ...(payload.incarnationId ? { incarnationId: payload.incarnationId } : {}) }) + } finally { + settleRestartStop?.(true) } - // The control reply can overtake stream data; the physical exit must retire that late output. - if (syntheticExit) { - return - } - // Why not the whole payload: the exit cause is a main-process fact for the - // runtime's records; the renderer's pty:exit contract stays as it was. - session.sendPtyExitToRenderer({ - id: payload.id, - code: payload.code, - ...(payload.incarnationId ? { incarnationId: payload.incarnationId } : {}) - }) }) ) } diff --git a/src/main/ipc/pty/register-handlers.ts b/src/main/ipc/pty/register-handlers.ts index 4010711e28b..b40aececc21 100644 --- a/src/main/ipc/pty/register-handlers.ts +++ b/src/main/ipc/pty/register-handlers.ts @@ -1,3 +1,4 @@ +import { settledDaemonRestart } from '../../daemon/daemon-restart-state' import { getAppEnvironment } from '../../../shared/app-environment' import type { OrcaRuntimeService } from '../../runtime/orca-runtime' import type { Store } from '../../persistence' @@ -104,7 +105,12 @@ export function registerPtyHandlers( return undefined } // Why: during cold start the daemon provider swap overlaps first paint, so local spawns must wait; SSH/headless don't use the desktop daemon. - return options?.awaitLocalPtyStartup?.() + const startup = options?.awaitLocalPtyStartup?.() + // A pane replaced by a restart spawns while it runs; it must reach the new daemon, not the old. + const restart = settledDaemonRestart() + return startup && restart + ? Promise.all([startup, restart]).then(() => undefined) + : (startup ?? restart) } const getLocalPtyProviderStartupPromise = ( diff --git a/src/main/ipc/pty/runtime/spawn-execute.ts b/src/main/ipc/pty/runtime/spawn-execute.ts index 35c0ee96317..a0302e49b52 100644 --- a/src/main/ipc/pty/runtime/spawn-execute.ts +++ b/src/main/ipc/pty/runtime/spawn-execute.ts @@ -24,6 +24,7 @@ export async function executeRuntimePtySpawn(ctx: RuntimePtySpawnState): Promise refuseSleptWorktree: args.refuseSleptWorktree }) : undefined + let awaitingAnotherStart = false try { if (args.preAllocatedHandle) { ctx.deps.trustedTerminalHandleEnv.add(args.preAllocatedHandle) @@ -116,8 +117,12 @@ export async function executeRuntimePtySpawn(ctx: RuntimePtySpawnState): Promise throw new Error('execution_owner_unavailable') } return await isProviderAgentSessionOwnerLive(ownerProvider, owner) + }, + onJoin: () => { + awaitingAnotherStart = true } }) + awaitingAnotherStart = false ctx.result = providerResult ?? { id: ensured.owner.ptyId, isReattach: true, @@ -200,7 +205,7 @@ export async function executeRuntimePtySpawn(ctx: RuntimePtySpawnState): Promise sessionId: ctx.sessionId, ownsNewSession: ctx.isNewDaemonSession, telemetry: args.telemetry, - recordsAgentError: false + joinedAnotherStart: awaitingAnotherStart }) } finally { if (args.preAllocatedHandle) { diff --git a/src/main/ipc/pty/spawn-execute-failure.ts b/src/main/ipc/pty/spawn-execute-failure.ts index d4a43a0daba..04b678ff139 100644 --- a/src/main/ipc/pty/spawn-execute-failure.ts +++ b/src/main/ipc/pty/spawn-execute-failure.ts @@ -29,8 +29,8 @@ export function failPtySpawnExecute( /** This spawn minted the session id, so the state built for it must go. */ ownsNewSession: boolean telemetry: { agent_kind?: unknown } | undefined - // TEMPORARY(C5a-2): keeps host starts out of agent_error as on main; C5a-2 deletes this input. - recordsAgentError: boolean + /** This request awaited another request's claimed start, whose own catch records its failure. */ + joinedAnotherStart?: boolean } ): Error { const runtime = ctx.deps.runtime @@ -79,7 +79,7 @@ export function failPtySpawnExecute( : ctx.isClaudeLaunch ? ('claude-code' as const) : null - if (lane.recordsAgentError && agentKind) { + if (agentKind && !lane.joinedAnotherStart) { track('agent_error', { agent_kind: agentKind, error_class: classifyError(spawnError).error_class, diff --git a/src/main/ipc/repos/folder-workspace-handlers.ts b/src/main/ipc/repos/folder-workspace-handlers.ts index 638591ce227..353093a2bd6 100644 --- a/src/main/ipc/repos/folder-workspace-handlers.ts +++ b/src/main/ipc/repos/folder-workspace-handlers.ts @@ -18,6 +18,10 @@ import { parseProjectGroupIpcArgs } from './repo-ipc-arg-schemas' import { visibleFolderWorkspaces } from '../../ssh/orcad-retained-source' +import { + isLocalWslMoveRow, + shouldDropWslMoveProjectUpdate +} from '../../persistence/moving-wsl-projects/wsl-move-scope-guard' export function registerFolderWorkspaceHandlers( mainWindow: BrowserWindow, @@ -77,6 +81,15 @@ export function registerFolderWorkspaceHandlers( rawArgs, 'invalid_folder_workspace_update_args' ) + const existing = store.getFolderWorkspace(args.folderWorkspaceId) + const ref = { folderWorkspaceId: args.folderWorkspaceId } + if ( + existing && + isLocalWslMoveRow(existing) && + shouldDropWslMoveProjectUpdate(store, ref, args.updates) + ) { + return existing + } if ( typeof args.updates.folderPath === 'string' && args.updates.folderPath.trim().length > 0 diff --git a/src/main/ipc/repos/project-group-handlers.ts b/src/main/ipc/repos/project-group-handlers.ts index 25f9c4506a4..89839bfef11 100644 --- a/src/main/ipc/repos/project-group-handlers.ts +++ b/src/main/ipc/repos/project-group-handlers.ts @@ -13,6 +13,7 @@ import { ProjectGroupUpdateArgs, parseProjectGroupIpcArgs } from './repo-ipc-arg-schemas' +import { assertWslMoveProjectWritable } from '../../persistence/moving-wsl-projects/wsl-move-scope-guard' import { activeNestedRepoScans, runNestedRepoScanForIpc } from './nested-repo-scan-ipc' import { visibleProjectGroups } from '../../ssh/orcad-retained-source' @@ -68,6 +69,7 @@ export function registerProjectGroupHandlers(mainWindow: BrowserWindow, store: S rawArgs, 'invalid_project_group_move_repo_args' ) + assertWslMoveProjectWritable(store, { repoId: args.projectId }) const moved = store.moveProjectToGroup(args.projectId, args.groupId, args.order) if (moved) { notifyReposChanged(mainWindow) diff --git a/src/main/ipc/repos/project-host-setup-handlers.ts b/src/main/ipc/repos/project-host-setup-handlers.ts index 79d7976ecec..07d6119dfc8 100644 --- a/src/main/ipc/repos/project-host-setup-handlers.ts +++ b/src/main/ipc/repos/project-host-setup-handlers.ts @@ -28,6 +28,10 @@ import { } from './repo-ipc-arg-schemas' import { addLocalRepoFromPath } from './local-repo-registration' import { sshTargetNotConnectedMessage } from '../../providers/ssh-host-not-served-error' +import { + assertWslMoveHostSetupIdWritable, + assertWslMoveHostSetupWritable +} from '../../persistence/moving-wsl-projects/wsl-move-scope-guard' function buildProjectHostSetupResult(store: Store, repo: Repo): ProjectHostSetupResult { const setup = getProjectHostSetupForRepo(store.getProjectHostSetups(), repo) @@ -45,6 +49,11 @@ function alignRepoWithRequestedProject( setupMethod: ProjectHostSetupExistingFolderArgs['setupMethod'] = 'imported-existing-folder', requestedProviderIdentity?: ProjectHostSetupExistingFolderArgs['projectProviderIdentity'] ): ProjectHostSetupResult { + assertWslMoveHostSetupWritable(store, { + projectId, + hostId: getRepoExecutionHostId(repo), + repoId: repo.id + }) let setup = getProjectHostSetupForRepo(store.getProjectHostSetups(), repo) if (setup.projectId !== projectId) { const project = store.getProjects().find((entry) => entry.id === projectId) @@ -86,6 +95,7 @@ export function registerProjectHostSetupHandlers(mainWindow: BrowserWindow, stor rawArgs, 'project_host_setup_create_invalid_args' ) + assertWslMoveHostSetupWritable(store, args) const result = store.createProjectHostSetup(args) if (!result) { throw new Error(`Project not found: ${args.projectId}`) @@ -103,6 +113,7 @@ export function registerProjectHostSetupHandlers(mainWindow: BrowserWindow, stor rawArgs, 'project_host_setup_update_invalid_args' ) + assertWslMoveHostSetupIdWritable(store, args.setupId) const result = store.updateProjectHostSetup(args) if (!result) { throw new Error(`Project host setup not found: ${args.setupId}`) @@ -124,6 +135,7 @@ export function registerProjectHostSetupHandlers(mainWindow: BrowserWindow, stor rawArgs, 'project_host_setup_delete_invalid_args' ) + assertWslMoveHostSetupIdWritable(store, args.setupId) const result = store.deleteProjectHostSetup(args) if (!result) { throw new Error(`Project host setup not found: ${args.setupId}`) diff --git a/src/main/ipc/repos/project-host-setup-wsl-move-fence.test.ts b/src/main/ipc/repos/project-host-setup-wsl-move-fence.test.ts new file mode 100644 index 00000000000..c4e566178b3 --- /dev/null +++ b/src/main/ipc/repos/project-host-setup-wsl-move-fence.test.ts @@ -0,0 +1,191 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { BrowserWindow } from 'electron' +import { mkdtempSync, rmSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { closeTestStores, createStore, testState } from '../../persistence-test-harness' +import type { Store } from '../../persistence' +import type { Repo } from '../../../shared/repo-types' +import { wslMoveCutoverFixture as cutover } from '../../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + WslMoveProjectFencedError, + loadWslMoveFence, + resetWslMoveFenceForTests +} from '../../persistence/moving-wsl-projects/wsl-move-scope-guard' +import { RuntimeProjectHostSetupController } from '../../runtime/runtime-project-host-setup-controller' +import { RuntimeRepositoryIssueCommand } from '../../runtime/runtime-repository-issue-command' +import { RuntimeRepositorySettingsController } from '../../runtime/runtime-repository-settings-controller' +import * as issueCommandFile from '../../issue-command-file' +import { registerProjectHostSetupHandlers } from './project-host-setup-handlers' +import { registerRepoCatalogHandlers } from './repo-catalog-handlers' + +const { handlers } = vi.hoisted(() => ({ + handlers: new Map unknown>() +})) +vi.mock('electron', () => ({ + BrowserWindow: class {}, + app: { getPath: () => testState.dir }, + safeStorage: { isEncryptionAvailable: () => false }, + ipcMain: { + handle: (channel: string, handler: (event: unknown, args: unknown) => unknown) => { + handlers.set(channel, handler) + } + } +})) +vi.mock('../../telemetry/client', () => ({ track: vi.fn() })) +vi.mock('../../telemetry/cohort-classifier', () => ({ getCohortAtEmit: vi.fn() })) +vi.mock('./repos-changed-notification', () => ({ notifyReposChanged: vi.fn() })) + +async function invoke(channel: string, args: unknown): Promise { + const handler = handlers.get(channel) + if (!handler) { + throw new Error(`Handler not registered: ${channel}`) + } + return handler(undefined, args) +} + +function repo(id: string, extra: Partial = {}): Repo { + return { + id, + path: join(testState.dir, id), + displayName: id, + badgeColor: '#000', + addedAt: 0, + kind: 'git', + ...extra + } +} + +let store: Store +beforeEach(() => { + handlers.clear() + testState.dir = mkdtempSync(join(tmpdir(), 'wsl-move-setup-ipc-')) + store = createStore() + loadWslMoveFence(testState.dir) +}) +afterEach(async () => { + resetWslMoveFenceForTests() + await closeTestStores() + rmSync(testState.dir, { recursive: true, force: true }) + vi.restoreAllMocks() +}) + +const setupFor = (repoId: string) => { + const setup = store.getProjectHostSetups().find((entry) => entry.repoId === repoId) + if (!setup) { + throw new Error(`No setup for ${repoId}`) + } + return setup +} + +describe('project host setups under a WSL move', () => { + it('refuses IPC edits to a local setup of a moving repo and leaves others alone', async () => { + store.addRepo(repo('repo-1')) + store.addRepo(repo('repo-2')) + seedWslMoveCutover(testState.dir, cutover('move-1', 'source-fenced')) + registerProjectHostSetupHandlers(new BrowserWindow(), store) + const moving = setupFor('repo-1') + + await expect( + invoke('projectHostSetups:update', { setupId: moving.id, updates: { displayName: 'x' } }) + ).rejects.toBeInstanceOf(WslMoveProjectFencedError) + await expect(invoke('projectHostSetups:delete', { setupId: moving.id })).rejects.toBeInstanceOf( + WslMoveProjectFencedError + ) + await expect( + invoke('projectHostSetups:create', { projectId: moving.projectId, hostId: 'local' }) + ).rejects.toBeInstanceOf(WslMoveProjectFencedError) + expect(setupFor('repo-1').displayName).toBe(moving.displayName) + await expect( + invoke('projectHostSetups:update', { + setupId: setupFor('repo-2').id, + updates: { displayName: 'renamed' } + }) + ).resolves.toMatchObject({ setup: { displayName: 'renamed' } }) + }) + + it("keeps a setup's own repo frozen once the WSL host owns it", async () => { + store.addRepo(repo('repo-1')) + seedWslMoveCutover(testState.dir, cutover('move-1', 'destination-committed')) + registerProjectHostSetupHandlers(new BrowserWindow(), store) + const owned = setupFor('repo-1') + + await expect( + invoke('projectHostSetups:update', { setupId: owned.id, updates: { displayName: 'x' } }) + ).rejects.toBeInstanceOf(WslMoveProjectFencedError) + await expect(invoke('projectHostSetups:delete', { setupId: owned.id })).rejects.toBeInstanceOf( + WslMoveProjectFencedError + ) + }) + + it('refuses a runtime setup delete for a moving repo', () => { + store.addRepo(repo('repo-1')) + seedWslMoveCutover(testState.dir, cutover('move-1', 'source-fenced')) + const controller = new RuntimeProjectHostSetupController({ + getStore: () => store, + listRepos: () => store.getRepos(), + addRepo: vi.fn(), + cloneRepo: vi.fn(), + invalidateResolvedWorktrees: vi.fn(), + invalidateWorktreeScan: vi.fn(), + notifyReposChanged: vi.fn(), + countLiveTerminalsForRepo: () => 0 + }) + const setupId = setupFor('repo-1').id + + expect(() => controller.deleteSetup({ setupId })).toThrow(WslMoveProjectFencedError) + expect(() => controller.updateSetup({ setupId, updates: { displayName: 'x' } })).toThrow( + WslMoveProjectFencedError + ) + expect(store.getRepo('repo-1')).toBeDefined() + }) +}) + +describe('projects:update once the WSL host owns a repo', () => { + it('lets project-wide settings change again', async () => { + store.addRepo(repo('repo-1')) + seedWslMoveCutover(testState.dir, cutover('move-1', 'destination-committed')) + registerRepoCatalogHandlers(new BrowserWindow(), store) + const project = store.getProjects().find((entry) => entry.sourceRepoIds.includes('repo-1')) + const updates = { localWindowsRuntimePreference: { kind: 'windows-host' } } + + await expect(invoke('projects:update', { projectId: project?.id, updates })).resolves.toEqual( + expect.objectContaining({ sourceRepoIds: ['repo-1'] }) + ) + }) +}) + +describe('issue command writes under a WSL move', () => { + it('refuses a runtime write to a moving repo before touching the file', async () => { + store.addRepo(repo('repo-1')) + seedWslMoveCutover(testState.dir, cutover('move-1', 'source-fenced')) + const write = vi.spyOn(issueCommandFile, 'writeIssueCommand') + const commands = new RuntimeRepositoryIssueCommand({ + resolveRepo: async (id) => repo(id), + getLocalGitArgs: () => [], + getStore: () => store + }) + + await expect(commands.write('repo-1', 'run')).rejects.toBeInstanceOf(WslMoveProjectFencedError) + expect(write).not.toHaveBeenCalled() + }) +}) + +describe('repo settings on another host under a WSL move', () => { + it('leaves a same-id repo row on another host writable', async () => { + seedWslMoveCutover(testState.dir, cutover('move-1', 'source-fenced')) + const onHost = repo('repo-1', { executionHostId: 'runtime:wsl:ubuntu' }) + vi.spyOn(store, 'updateRepo').mockReturnValue(onHost) + const controller = new RuntimeRepositorySettingsController({ + getStore: () => store, + resolveRepo: async () => onHost, + forgetTerminalTopology: vi.fn(), + invalidateResolvedWorktrees: vi.fn(), + invalidateWorktreeScan: vi.fn(), + notifyReposChanged: vi.fn() + }) + + await expect(controller.setBaseRef('repo-1', 'origin/dev')).resolves.toBe(onHost) + }) +}) diff --git a/src/main/ipc/repos/repo-catalog-handlers.ts b/src/main/ipc/repos/repo-catalog-handlers.ts index ddb5d058056..071c8d9dfd9 100644 --- a/src/main/ipc/repos/repo-catalog-handlers.ts +++ b/src/main/ipc/repos/repo-catalog-handlers.ts @@ -9,6 +9,11 @@ import { invalidateAuthorizedRootsCache } from '../registered-worktree-roots-cac import { notifyReposChanged } from './repos-changed-notification' import { ProjectUpdateIpcArgs, parseProjectGroupIpcArgs } from './repo-ipc-arg-schemas' import { visibleRepos } from '../../ssh/orcad-retained-source' +import { + assertWslMoveProjectWritable, + assertWslMoveReposWritable, + isWslMoveSourceHost +} from '../../persistence/moving-wsl-projects/wsl-move-scope-guard' export function registerRepoCatalogHandlers(mainWindow: BrowserWindow, store: Store): void { // Why one shared reference: enrichment dedupes coalesced callers by callback identity, so a fresh @@ -33,6 +38,8 @@ export function registerRepoCatalogHandlers(mainWindow: BrowserWindow, store: St rawArgs, 'project_update_invalid_args' ) + const project = store.getProjects().find((entry) => entry.id === args.projectId) + assertWslMoveReposWritable(store, project?.sourceRepoIds ?? []) return store.updateProject(args.projectId, args.updates) }) @@ -83,6 +90,9 @@ export function registerRepoCatalogHandlers(mainWindow: BrowserWindow, store: St if (!hostId) { throw new Error(`Invalid host ID: ${args.hostId}`) } + if (isWslMoveSourceHost(hostId)) { + assertWslMoveProjectWritable(store, { repoId: args.repoId }) + } store.removeProjectForHost(args.repoId, hostId) invalidateAuthorizedRootsCache() notifyReposChanged(mainWindow) diff --git a/src/main/ipc/repos/repo-ipc-wsl-move-fence.test.ts b/src/main/ipc/repos/repo-ipc-wsl-move-fence.test.ts new file mode 100644 index 00000000000..1b8ec667fa5 --- /dev/null +++ b/src/main/ipc/repos/repo-ipc-wsl-move-fence.test.ts @@ -0,0 +1,152 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { BrowserWindow } from 'electron' +import { mkdtempSync, rmSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { closeTestStores, createStore, testState } from '../../persistence-test-harness' +import type { Store } from '../../persistence' +import type { Repo } from '../../../shared/repo-types' +import { wslMoveCutoverFixture as cutover } from '../../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + WslMoveProjectFencedError, + loadWslMoveFence, + resetWslMoveFenceForTests +} from '../../persistence/moving-wsl-projects/wsl-move-scope-guard' +import { RuntimeRepositorySettingsController } from '../../runtime/runtime-repository-settings-controller' +import { registerFolderWorkspaceHandlers } from './folder-workspace-handlers' +import { registerRepoCatalogHandlers } from './repo-catalog-handlers' + +const { handlers } = vi.hoisted(() => ({ + handlers: new Map unknown>() +})) +vi.mock('electron', () => ({ + BrowserWindow: class {}, + app: { getPath: () => testState.dir }, + safeStorage: { isEncryptionAvailable: () => false }, + ipcMain: { + handle: (channel: string, handler: (event: unknown, args: unknown) => unknown) => { + handlers.set(channel, handler) + } + } +})) +vi.mock('../../telemetry/client', () => ({ track: vi.fn() })) +vi.mock('../../telemetry/cohort-classifier', () => ({ getCohortAtEmit: vi.fn() })) +vi.mock('./repos-changed-notification', () => ({ notifyReposChanged: vi.fn() })) + +async function invoke(channel: string, args: unknown): Promise { + const handler = handlers.get(channel) + if (!handler) { + throw new Error(`Handler not registered: ${channel}`) + } + return handler(undefined, args) +} + +function repo(id: string): Repo { + return { + id, + path: join(testState.dir, id), + displayName: id, + badgeColor: '#000', + addedAt: 0, + kind: 'git' + } +} + +let store: Store +beforeEach(() => { + handlers.clear() + testState.dir = mkdtempSync(join(tmpdir(), 'wsl-move-repo-ipc-')) + store = createStore() + loadWslMoveFence(testState.dir) +}) +afterEach(async () => { + resetWslMoveFenceForTests() + await closeTestStores() + rmSync(testState.dir, { recursive: true, force: true }) +}) + +describe('folderWorkspaces:update under a WSL move', () => { + it('drops a passive update quietly and refuses a user edit', async () => { + const group = store.createProjectGroup({ + name: 'Docs', + parentPath: testState.dir, + createdFrom: 'folder-scan' + }) + const workspace = store.createFolderWorkspace({ projectGroupId: group.id, name: 'notes' }) + seedWslMoveCutover( + testState.dir, + cutover('move-1', 'source-fenced', {}, { folderWorkspaceIds: [workspace.id] }) + ) + registerFolderWorkspaceHandlers(new BrowserWindow(), store, { + deleteFolderWorkspace: vi.fn(async () => ({ deleted: true })) + }) + + await expect( + invoke('folderWorkspaces:update', { + folderWorkspaceId: workspace.id, + updates: { isUnread: true, lastActivityAt: 1 } + }) + ).resolves.toMatchObject({ id: workspace.id, isUnread: workspace.isUnread }) + await expect( + invoke('folderWorkspaces:update', { + folderWorkspaceId: workspace.id, + updates: { isUnread: true } + }) + ).rejects.toBeInstanceOf(WslMoveProjectFencedError) + await expect( + invoke('folderWorkspaces:update', { + folderWorkspaceId: workspace.id, + updates: { name: 'renamed' } + }) + ).rejects.toBeInstanceOf(WslMoveProjectFencedError) + expect(store.getFolderWorkspace(workspace.id)).toMatchObject({ + name: 'notes', + isUnread: workspace.isUnread + }) + }) +}) + +describe('projects:update under a WSL move', () => { + it('refuses a runtime preference change when a source repo is moving', async () => { + store.addRepo(repo('repo-1')) + store.addRepo(repo('repo-2')) + seedWslMoveCutover(testState.dir, cutover('move-1', 'source-fenced')) + registerRepoCatalogHandlers(new BrowserWindow(), store) + const projectFor = (repoId: string) => + store.getProjects().find((project) => project.sourceRepoIds.includes(repoId)) + const updates = { localWindowsRuntimePreference: { kind: 'windows-host' } } + + await expect( + invoke('projects:update', { projectId: projectFor('repo-1')?.id, updates }) + ).rejects.toBeInstanceOf(WslMoveProjectFencedError) + await expect( + invoke('projects:update', { projectId: projectFor('repo-2')?.id, updates }) + ).resolves.toMatchObject({ sourceRepoIds: ['repo-2'] }) + }) +}) + +describe('RuntimeRepositorySettingsController under a WSL move', () => { + it('refuses a settings edit to a moving repo and writes nothing', async () => { + store.addRepo(repo('repo-1')) + seedWslMoveCutover(testState.dir, cutover('move-1', 'source-fenced')) + const notifyReposChanged = vi.fn() + const controller = new RuntimeRepositorySettingsController({ + getStore: () => store, + resolveRepo: async (id) => repo(id), + forgetTerminalTopology: vi.fn(), + invalidateResolvedWorktrees: vi.fn(), + invalidateWorktreeScan: vi.fn(), + notifyReposChanged + }) + + await expect(controller.update('repo-1', { displayName: 'renamed' })).rejects.toBeInstanceOf( + WslMoveProjectFencedError + ) + await expect(controller.setBaseRef('repo-1', 'origin/dev')).rejects.toBeInstanceOf( + WslMoveProjectFencedError + ) + expect(store.getRepo('repo-1')?.displayName).toBe('repo-1') + expect(notifyReposChanged).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/ipc/repos/repo-update-handler.ts b/src/main/ipc/repos/repo-update-handler.ts index 5af9e097121..9ef2537aecb 100644 --- a/src/main/ipc/repos/repo-update-handler.ts +++ b/src/main/ipc/repos/repo-update-handler.ts @@ -16,6 +16,10 @@ import { import { prepareLocalWorktreeRootForRepo } from '../../worktree-root-preparation' import { invalidateAuthorizedRootsCache } from '../registered-worktree-roots-cache' import { notifyReposChanged } from './repos-changed-notification' +import { + assertWslMoveProjectWritable, + isWslMoveSourceHost +} from '../../persistence/moving-wsl-projects/wsl-move-scope-guard' export function registerRepoUpdateHandler(mainWindow: BrowserWindow, store: Store): void { ipcMain.handle( @@ -58,6 +62,9 @@ export function registerRepoUpdateHandler(mainWindow: BrowserWindow, store: Stor } } ) => { + if (isWslMoveSourceHost(args.hostId)) { + assertWslMoveProjectWritable(store, { repoId: args.repoId }) + } // Why: TS is erased at runtime, so a garbage preference would silently collapse to 'auto' in resolveIssueSource; strip it, keeping other fields. const updates = { ...args.updates } if ( diff --git a/src/main/ipc/repos/sparse-preset-handlers.ts b/src/main/ipc/repos/sparse-preset-handlers.ts index 8d426c74677..c9743c0fdf3 100644 --- a/src/main/ipc/repos/sparse-preset-handlers.ts +++ b/src/main/ipc/repos/sparse-preset-handlers.ts @@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto' import type { Store } from '../../persistence' import type { SparsePreset } from '../../../shared/worktree/create-types' import { normalizeSparseDirectories } from '../sparse-checkout-directories' +import { assertWslMoveProjectWritable } from '../../persistence/moving-wsl-projects/wsl-move-scope-guard' export function registerSparsePresetHandlers(mainWindow: BrowserWindow, store: Store): void { // ── Sparse presets ───────────────────────────────────────────── @@ -23,6 +24,7 @@ export function registerSparsePresetHandlers(mainWindow: BrowserWindow, store: S if (!repo) { throw new Error(`Repo "${args.repoId}" not found`) } + assertWslMoveProjectWritable(store, { repoId: args.repoId }) const name = normalizeSparsePresetName(args.name) const directories = normalizeSparsePresetDirectories(args.directories) const now = Date.now() @@ -48,6 +50,7 @@ export function registerSparsePresetHandlers(mainWindow: BrowserWindow, store: S if (!repo) { throw new Error(`Repo "${args.repoId}" not found`) } + assertWslMoveProjectWritable(store, { repoId: args.repoId }) store.removeSparsePreset(args.repoId, args.presetId) notifySparsePresetsChanged(mainWindow, args.repoId) }) diff --git a/src/main/ipc/ssh-passphrase.test.ts b/src/main/ipc/ssh-passphrase.test.ts index ee53cc78d30..59e2900eb1f 100644 --- a/src/main/ipc/ssh-passphrase.test.ts +++ b/src/main/ipc/ssh-passphrase.test.ts @@ -1,6 +1,6 @@ -import type { BrowserWindow } from 'electron' +import { ipcMain, type BrowserWindow } from 'electron' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { requestCredential } from './ssh-passphrase' +import { registerCredentialHandler, requestCredential } from './ssh-passphrase' import { SSH_CREDENTIAL_TIMEOUT_MS } from '../ssh/ssh-connection-utils' import { isSshCredentialDeclined, @@ -76,4 +76,52 @@ describe('SSH credential requests', () => { requestId: request.requestId }) }) + + it('closes every other open prompt for the host the user cancelled', async () => { + registerCredentialHandler() + const handler = vi.mocked(ipcMain.handle).mock.calls.at(-1)![1] + const submit = (requestId: string, value: string | null): unknown => + handler(Object.create(null), { requestId, value }) + const window = credentialWindow() + const requestIds = (): string[] => + vi + .mocked(window.webContents.send) + .mock.calls.flatMap(([channel, request]) => + channel === 'ssh:credential-request' && + request && + typeof request === 'object' && + 'requestId' in request && + typeof request.requestId === 'string' + ? [request.requestId] + : [] + ) + const first = requestCredential( + () => window, + 'target-1', + 'password', + 'host', + undefined, + new AbortController().signal + ) + const queued = requestCredential( + () => window, + 'target-1', + 'password', + 'host', + undefined, + new AbortController().signal + ) + const otherHost = requestCredential(() => window, 'target-2', 'password', 'other') + const [firstId, queuedId, otherId] = requestIds() + + await submit(firstId, null) + + await expect(first).resolves.toBeNull() + await expect(queued).resolves.toBeNull() + expect(window.webContents.send).toHaveBeenCalledWith('ssh:credential-resolved', { + requestId: queuedId + }) + await submit(otherId, 'secret') + await expect(otherHost).resolves.toBe('secret') + }) }) diff --git a/src/main/ipc/ssh-passphrase.ts b/src/main/ipc/ssh-passphrase.ts index 54de11be4c3..6c9c720b10f 100644 --- a/src/main/ipc/ssh-passphrase.ts +++ b/src/main/ipc/ssh-passphrase.ts @@ -2,7 +2,10 @@ import { ipcMain, type BrowserWindow } from 'electron' import { randomUUID } from 'node:crypto' import { SSH_CREDENTIAL_TIMEOUT_MS, type SshCredentialKind } from '../ssh/ssh-connection-utils' import { recordSshCredentialDeclined } from '../ssh/ssh-credential-decline' -const pendingRequests = new Map void }>() +const pendingRequests = new Map< + string, + { targetId: string; resolve: (value: string | null) => void; close: () => void } +>() function notifyCredentialResolved( getMainWindow: () => BrowserWindow | null, @@ -37,14 +40,23 @@ export function requestCredential( const onAbort = (): void => finish(null) // Why: an abort is the connection's own teardown; only the user's Cancel (or silence) declines. const decline = (): void => { - if (pendingRequests.has(requestId)) { - recordSshCredentialDeclined(targetId, signal) + if (!pendingRequests.has(requestId)) { + return } + recordSshCredentialDeclined(targetId, signal) finish(null) + // Why: one Cancel answers the host; a queued prompt for it would otherwise open next. + for (const [otherId, other] of pendingRequests) { + if (otherId !== requestId && other.targetId === targetId) { + other.close() + } + } } timer = setTimeout(decline, SSH_CREDENTIAL_TIMEOUT_MS) pendingRequests.set(requestId, { - resolve: (value) => (value === null ? decline() : finish(value)) + targetId, + resolve: (value) => (value === null ? decline() : finish(value)), + close: () => finish(null) }) if (signal?.aborted) { finish(null) diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index a1d42fe22b5..62852004af0 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -139,6 +139,7 @@ import { } from '../worktree-name-retirement' import { createRetiredNameLookup } from '../../shared/worktree/retired-name-registry' import { findPendingWorktreeRemovalConflict } from '../worktree-removal-table' +import { isWslMoveWorktreeBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' // Why: bound the fallback `git fetch origin` so a Windows credential-manager GUI hang (STA-1292) can't wedge worktree creation forever. const CREATE_BASE_FALLBACK_FETCH_TIMEOUT_MS = 60_000 // Why (#17828 CodeRabbit follow-up): the deferred materialize fetch runs off the main @@ -837,6 +838,10 @@ function persistMaterializedPushTargetIfCreated( if (!target.remoteCreated || !worktreeId || !store?.setWorktreeMeta) { return } + // Bookkeeping incidental to the push: a project a WSL move is taking skips it, not the push. + if (isWslMoveWorktreeBlocked(worktreeId, store.getAllWorktreeMeta()[worktreeId]?.hostId)) { + return + } store.setWorktreeMeta(worktreeId, { pushTarget: target }) } diff --git a/src/main/ipc/worktrees-wsl-move-fence.test.ts b/src/main/ipc/worktrees-wsl-move-fence.test.ts new file mode 100644 index 00000000000..1c77daca546 --- /dev/null +++ b/src/main/ipc/worktrees-wsl-move-fence.test.ts @@ -0,0 +1,142 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import type { WorktreeMeta } from '../../shared/worktree/meta-types' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { wslMoveCutoverFixture as cutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + WslMoveProjectFencedError, + loadWslMoveFence, + resetWslMoveFenceForTests +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' +import { + addWorktreeMock, + assertWorktreeCleanForRemovalMock, + forceDeleteLocalBranchMock, + gitExecFileAsyncMock, + killAllProcessesForWorktreeMock, + listWorktreesMock, + removeWorktreeMock +} from './worktrees-test-module-mocks' +import { handlers, setupWorktreeHandlers, store } from './worktrees-test-harness' +import { rememberPreservedBranchCleanupTarget } from './worktrees/removal/preserved-branch-cleanup' +import * as issueCommandFile from '../issue-command-file' + +vi.mock('electron', async () => + (await import('./worktrees-test-module-mocks')).electronModuleMock() +) +vi.mock('../git/worktree', async () => + (await import('./worktrees-test-module-mocks')).gitWorktreeModuleMock() +) +vi.mock('../git/runner', async () => + (await import('./worktrees-test-module-mocks')).gitRunnerModuleMock() +) +vi.mock('../git/repo', async () => + (await import('./worktrees-test-module-mocks')).gitRepoModuleMock() +) +vi.mock('../runtime/worktree-teardown', async () => + (await import('./worktrees-test-module-mocks')).worktreeTeardownModuleMock() +) +vi.mock('./pty/provider/registry', async () => + (await import('./worktrees-test-module-mocks')).ptyRegistryModuleMock() +) +vi.mock('./pty/provider/state-cleanup', async () => + (await import('./worktrees-test-module-mocks')).ptyStateCleanupModuleMock() +) + +const MOVING = 'repo-1::/workspace/feature-wt' +const STAYING = 'repo-2::/workspace/other-wt' + +let userDataPath: string +beforeEach(() => { + setupWorktreeHandlers() + userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-worktree-ipc-')) + loadWslMoveFence(userDataPath) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) +}) +afterEach(() => { + resetWslMoveFenceForTests() + rmSync(userDataPath, { recursive: true, force: true }) +}) + +describe('worktree IPC under a WSL move', () => { + it('refuses a create before any Git work', async () => { + await expect( + handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'new-feature' }) + ).rejects.toBeInstanceOf(WslMoveProjectFencedError) + expect(addWorktreeMock).not.toHaveBeenCalled() + expect(listWorktreesMock).not.toHaveBeenCalled() + expect(gitExecFileAsyncMock).not.toHaveBeenCalled() + }) + + it('refuses a delete before any teardown or Git work', async () => { + await expect(handlers['worktrees:remove'](null, { worktreeId: MOVING })).rejects.toBeInstanceOf( + WslMoveProjectFencedError + ) + expect(assertWorktreeCleanForRemovalMock).not.toHaveBeenCalled() + expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() + expect(removeWorktreeMock).not.toHaveBeenCalled() + expect(store.removeWorktreeMeta).not.toHaveBeenCalled() + }) + + it('refuses a forget before touching metadata', async () => { + await expect( + handlers['worktrees:forgetLocal'](null, { worktreeId: MOVING }) + ).rejects.toBeInstanceOf(WslMoveProjectFencedError) + expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() + expect(store.removeWorktreeMeta).not.toHaveBeenCalled() + }) + + it('refuses a preserved-branch delete before any Git work', async () => { + rememberPreservedBranchCleanupTarget( + MOVING, + 'local', + { preservedBranch: { branchName: 'feature', head: 'abc123' } }, + undefined, + undefined + ) + await expect( + handlers['worktrees:forceDeletePreservedBranch'](null, { + worktreeId: MOVING, + branchName: 'feature', + expectedHead: 'abc123', + hostId: 'local' + }) + ).rejects.toBeInstanceOf(WslMoveProjectFencedError) + expect(forceDeleteLocalBranchMock).not.toHaveBeenCalled() + expect(gitExecFileAsyncMock).not.toHaveBeenCalled() + }) + + it('refuses an issue-command write before touching the file', async () => { + const write = vi.spyOn(issueCommandFile, 'writeIssueCommand').mockResolvedValue(undefined) + await expect( + handlers['hooks:writeIssueCommand'](null, { repoId: 'repo-1', content: 'run' }) + ).rejects.toBeInstanceOf(WslMoveProjectFencedError) + expect(write).not.toHaveBeenCalled() + write.mockRestore() + }) + + it('drops passive metadata quietly and refuses a user edit', () => { + const updateMeta = (worktreeId: string, updates: Partial) => + handlers['worktrees:updateMeta'](null, { worktreeId, updates }) + expect(updateMeta(MOVING, { isUnread: false })).toBeNull() + expect(updateMeta(MOVING, { isUnread: true, lastActivityAt: 1 })).toBeNull() + expect(() => updateMeta(MOVING, { isUnread: true })).toThrow(WslMoveProjectFencedError) + expect(() => updateMeta(MOVING, { comment: 'x' })).toThrow(WslMoveProjectFencedError) + expect(store.setWorktreeMeta).not.toHaveBeenCalled() + updateMeta(STAYING, { comment: 'x' }) + expect(store.setWorktreeMeta).toHaveBeenCalledWith(STAYING, { comment: 'x' }) + }) + + it('drops the moving worktree from a sort-order batch', async () => { + store.getWorktreeMeta.mockImplementation((id) => + id === MOVING ? { sortOrder: 1 } : id === STAYING ? { sortOrder: 2 } : undefined + ) + await handlers['worktrees:persistSortOrder'](null, { orderedIds: [MOVING, STAYING] }) + expect(store.setWorktreeMeta).toHaveBeenCalledTimes(1) + expect(store.setWorktreeMeta).toHaveBeenCalledWith(STAYING, { + sortOrder: expect.any(Number) + }) + }) +}) diff --git a/src/main/ipc/worktrees/create/register-worktree-create-handlers.ts b/src/main/ipc/worktrees/create/register-worktree-create-handlers.ts index f1848b737c9..cf76eb8246b 100644 --- a/src/main/ipc/worktrees/create/register-worktree-create-handlers.ts +++ b/src/main/ipc/worktrees/create/register-worktree-create-handlers.ts @@ -30,6 +30,7 @@ import { findExactRepoOwner, isCapturedRepoCurrent } from '../listing/worktree-h import { requireWorktreeCreateRoute } from '../../../worktree-create-execution-host-route' import type { WorktreeIpcContext } from '../worktree-ipc-context' import { beginWorkspaceCreateTelemetry } from '../../../workspace-create-telemetry' +import { assertWslMoveRepoWritable } from '../../../persistence/moving-wsl-projects/wsl-move-scope-guard' export function registerWorktreeCreateHandlers(context: WorktreeIpcContext): void { const { mainWindow, store, runtime, options } = context @@ -44,6 +45,7 @@ export function registerWorktreeCreateHandlers(context: WorktreeIpcContext): voi if (!repo) { throw new Error(`Repo not found: ${args.repoId}`) } + assertWslMoveRepoWritable(store, repo) const automationProvenance = resolveAutomationWorkspaceProvenance({ authority: runtime, @@ -129,6 +131,7 @@ export function registerWorktreeCreateHandlers(context: WorktreeIpcContext): voi if (!repo || isFolderRepo(repo)) { throw new Error('Provisioned-root repository ownership is missing or ambiguous.') } + assertWslMoveRepoWritable(store, repo) const sourceParse = workspaceSourceSchema.safeParse(args.telemetrySource) const source: WorkspaceSource = sourceParse.success ? sourceParse.data : 'unknown' const automationProvenance = resolveAutomationWorkspaceProvenance({ diff --git a/src/main/ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning.ts b/src/main/ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning.ts index 3349a986519..5072344bc3d 100644 --- a/src/main/ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning.ts +++ b/src/main/ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning.ts @@ -16,6 +16,7 @@ import { pruneWorkspaceSpaceAnalysisSnapshots } from '../../../workspace-space-a import { isLocalWorktreeScanGenerationCurrent } from '../../../local-worktree-scan-generation' import { localWorktreePathsExistOrAreUnverifiable } from '../../../local-worktree-path-presence' import { worktreeRetentionPathComparisonKey } from '../../../worktree-retention-path-comparison' +import { isWslMoveRepoBlocked } from '../../../persistence/moving-wsl-projects/wsl-move-background-backoff' type AuthoritativeLocalMetadataPruneArgs = { store: Store @@ -65,7 +66,9 @@ export async function pruneMetadataMissingFromAuthoritativeLocalScan({ } const generationCurrent = () => isLocalWorktreeScanGenerationCurrent(repo.id, scanGeneration) const repoOwners = store.getRepos().filter((candidate) => candidate.id === repo.id) + // Mid-move a scan can miss paths that still exist; nothing is pruned until the move settles. if ( + isWslMoveRepoBlocked(repo) || !generationCurrent() || !isCallerCurrent() || gitWorktrees.length === 0 || @@ -124,7 +127,8 @@ export async function pruneMetadataMissingFromAuthoritativeLocalScan({ return result([], generationCurrent()) } - if (!generationCurrent() || !isCallerCurrent()) { + // The path probe awaits, so a move may have started since the entry check. + if (!generationCurrent() || !isCallerCurrent() || isWslMoveRepoBlocked(repo)) { return result([], generationCurrent()) } const removedIds = store.pruneSessionlessMissingLocalWorktreeMetadataForRepo( diff --git a/src/main/ipc/worktrees/listing/folder-workspace-catalog.ts b/src/main/ipc/worktrees/listing/folder-workspace-catalog.ts index abaa594bf6e..c17068b92c0 100644 --- a/src/main/ipc/worktrees/listing/folder-workspace-catalog.ts +++ b/src/main/ipc/worktrees/listing/folder-workspace-catalog.ts @@ -1,6 +1,10 @@ +import { createHash } from 'node:crypto' import type { Repo } from '../../../../shared/repo-types' import type { Worktree, DetectedWorktree } from '../../../../shared/worktree/types' +import type { WorktreeMeta } from '../../../../shared/worktree/meta-types' import type { Store } from '../../../persistence/loading-store/store' +import { mergeWorktreeMetaForWrite } from '../../../persistence/loading-store/worktree-meta-write-normalization' +import { isWslMoveRepoBlocked } from '../../../persistence/moving-wsl-projects/wsl-move-background-backoff' import { createWorktreeVisibilitySourceMatcher, resolveCustomWorktreeVisibilitySources @@ -19,7 +23,28 @@ import { mergeFolderWorkspace } from '../folder-workspace-model' +// A project a WSL move is taking still lists, with its repairs projected in memory only. +function folderMetaWriter( + store: Store, + repo: Repo +): (worktreeId: string, updates: Partial) => WorktreeMeta { + if (!isWslMoveRepoBlocked(repo)) { + return (worktreeId, updates) => store.setWorktreeMeta(worktreeId, updates) + } + return (worktreeId, updates) => { + const existing = store.getWorktreeMeta(worktreeId) + // Nothing persists, so a minted root id would change on every listing; derive it instead. + const instanceId = + existing?.instanceId ?? + (worktreeId === getFolderWorkspaceRootId(repo) + ? createHash('sha256').update(worktreeId).digest('hex').slice(0, 32) + : getFolderWorkspaceInstanceIdentity(repo, worktreeId)) + return mergeWorktreeMetaForWrite(existing, { ...updates, instanceId }) + } +} + export function listFolderWorkspaces(store: Store, repo: Repo): Worktree[] { + const writeMeta = folderMetaWriter(store, repo) const rootId = getFolderWorkspaceRootId(repo) const allMeta = store.getAllWorktreeMeta() const ids = Object.keys(allMeta).filter((worktreeId) => @@ -36,7 +61,7 @@ export function listFolderWorkspaces(store: Store, repo: Repo): Worktree[] { const meta = existing?.instanceId && Object.keys(ownershipUpdates).length === 0 ? existing - : store.setWorktreeMeta(worktreeId, { + : writeMeta(worktreeId, { instanceId: existing?.instanceId ?? getFolderWorkspaceInstanceIdentity(repo, worktreeId), ...ownershipUpdates, @@ -77,6 +102,7 @@ export function buildFolderDetectedWorktrees(store: Store, repo: Repo): Detected } export function listVisibleFolderWorkspaces(store: Store, repo: Repo): Worktree[] { + const writeMeta = folderMetaWriter(store, repo) return buildFolderDetectedWorktrees(store, repo) .filter((worktree) => worktree.visible) .map((worktree) => { @@ -85,7 +111,7 @@ export function listVisibleFolderWorkspaces(store: Store, repo: Repo): Worktree[ const repairedMeta = meta && Object.keys(ownershipUpdates).length === 0 ? meta - : store.setWorktreeMeta(worktree.id, ownershipUpdates) + : writeMeta(worktree.id, ownershipUpdates) return mergeFolderWorkspace(repo, worktree.id, repairedMeta) }) } diff --git a/src/main/ipc/worktrees/listing/register-worktree-catalog-handlers.ts b/src/main/ipc/worktrees/listing/register-worktree-catalog-handlers.ts index 2378d271cff..2c24d9c1526 100644 --- a/src/main/ipc/worktrees/listing/register-worktree-catalog-handlers.ts +++ b/src/main/ipc/worktrees/listing/register-worktree-catalog-handlers.ts @@ -1,5 +1,5 @@ import { ipcMain } from 'electron' -import { withoutWslMovedProjects } from '../../../persistence/moving-wsl-projects/wsl-move-scope-guard' +import { withoutWslMovedRepos } from '../../../persistence/moving-wsl-projects/wsl-move-scope-guard' import { isFolderRepo } from '../../../../shared/repo-kind' import { getRepoExecutionHostId, @@ -69,7 +69,7 @@ export function registerWorktreeCatalogHandlers(context: WorktreeIpcContext): vo ipcMain.handle('worktrees:listAll', async () => { // A repo the WSL host owns is listed by the host; this build must not scan its old path. - const repos = withoutWslMovedProjects(store.getRepos(), (repo) => ({ repoId: repo.id })) + const repos = withoutWslMovedRepos(store.getRepos()) const legacyMetadata = typeof store.getAllWorktreeMetaForHost === 'function' ? undefined : store.getAllWorktreeMeta() const metadataByHost = new Map>() diff --git a/src/main/ipc/worktrees/metadata/register-worktree-metadata-handlers.ts b/src/main/ipc/worktrees/metadata/register-worktree-metadata-handlers.ts index 1cb36b600b1..61f56990bb2 100644 --- a/src/main/ipc/worktrees/metadata/register-worktree-metadata-handlers.ts +++ b/src/main/ipc/worktrees/metadata/register-worktree-metadata-handlers.ts @@ -11,6 +11,12 @@ import { readBranchRenameFailureOutputForDisplay } from '../../../agent-hooks/br import { normalizeLinkedWorkItemFields } from '../ipc-context-schemas' import { isRemovedFolderWorkspaceInstance } from '../../../folder-workspace-meta-presence' import type { WorktreeIpcContext } from '../worktree-ipc-context' +import { + isWslMoveSourceHost, + isWslMoveWorkspaceBlocked, + shouldDropWslMoveProjectUpdate, + wslMoveProjectRefForWorkspace +} from '../../../persistence/moving-wsl-projects/wsl-move-scope-guard' export function registerWorktreeMetadataHandlers(context: WorktreeIpcContext): void { const { mainWindow, store, runtime } = context @@ -31,6 +37,16 @@ export function registerWorktreeMetadataHandlers(context: WorktreeIpcContext): v if (args.executionHostId !== undefined && !executionHostId) { throw new Error('Invalid execution host identity.') } + if ( + isWslMoveSourceHost(executionHostId) && + shouldDropWslMoveProjectUpdate( + store, + wslMoveProjectRefForWorkspace(args.worktreeId), + args.updates + ) + ) { + return null + } const validatedUpdates = normalizeLinkedWorkItemFields(args.updates) const updates = validatedUpdates.displayName !== undefined @@ -91,7 +107,7 @@ export function registerWorktreeMetadataHandlers(context: WorktreeIpcContext): v args.orderedIds, (worktreeId) => store.getWorktreeMeta(worktreeId), Date.now() - ) + ).filter((update) => !isWslMoveWorkspaceBlocked(store, update.worktreeId)) for (const update of updates) { store.setWorktreeMeta(update.worktreeId, { sortOrder: update.sortOrder }) } diff --git a/src/main/ipc/worktrees/removal/register-worktree-forget-handlers.ts b/src/main/ipc/worktrees/removal/register-worktree-forget-handlers.ts index 3b02e1d57eb..c330c3d217f 100644 --- a/src/main/ipc/worktrees/removal/register-worktree-forget-handlers.ts +++ b/src/main/ipc/worktrees/removal/register-worktree-forget-handlers.ts @@ -35,6 +35,11 @@ import { preservedBranchCleanupByScope } from './preserved-branch-cleanup' import type { WorktreeIpcContext } from '../worktree-ipc-context' +import { + assertWslMoveProjectWritable, + assertWslMoveRepoWritable, + isWslMoveSourceHost +} from '../../../persistence/moving-wsl-projects/wsl-move-scope-guard' export function registerWorktreeForgetHandlers(context: WorktreeIpcContext): void { const { mainWindow, store, runtime, worktreeRemovalsInFlight } = context @@ -53,6 +58,11 @@ export function registerWorktreeForgetHandlers(context: WorktreeIpcContext): voi ) } const repo = repoOwner.kind === 'resolved' ? repoOwner.repo : undefined + if (repo) { + assertWslMoveRepoWritable(store, repo) + } else if (isWslMoveSourceHost(args.hostId)) { + assertWslMoveProjectWritable(store, { repoId }) + } // Repo-first (unlike owner resolution below) so this key matches worktrees:remove's; meta only covers ownerless forgets. const inFlightKey = getWorktreeRemovalInFlightKey( args.worktreeId, @@ -166,6 +176,7 @@ export function registerWorktreeForgetHandlers(context: WorktreeIpcContext): voi if (!repo) { throw new Error(`Repo not found: ${repoId}`) } + assertWslMoveRepoWritable(store, repo) if (isFolderRepo(repo)) { throw new Error('Folder workspaces do not have local Git branches.') } diff --git a/src/main/ipc/worktrees/removal/register-worktree-removal-handlers.ts b/src/main/ipc/worktrees/removal/register-worktree-removal-handlers.ts index 661cd9111e1..7a3a43dfe27 100644 --- a/src/main/ipc/worktrees/removal/register-worktree-removal-handlers.ts +++ b/src/main/ipc/worktrees/removal/register-worktree-removal-handlers.ts @@ -21,6 +21,7 @@ import { waitForPendingWorktreeRemoval } from '../../../worktree-background-removal' import { runSerializedWorktreeRemovalAcceptance } from '../../../worktree-removal-acceptance-queue' +import { assertWslMoveRepoWritable } from '../../../persistence/moving-wsl-projects/wsl-move-scope-guard' export function registerWorktreeRemovalHandlers(context: WorktreeIpcContext): void { const { store, options, worktreeRemovalsInFlight } = context @@ -40,6 +41,7 @@ export function registerWorktreeRemovalHandlers(context: WorktreeIpcContext): vo if (!repo) { throw new Error(`Repo not found: ${repoId}`) } + assertWslMoveRepoWritable(store, repo) // The resolved repo supplies host ownership when legacy callers omit args.hostId. const removalHostId = getRepoExecutionHostId(repo) // Why: a retry or a second window asking while Git still deletes joins that removal. diff --git a/src/main/local-project-runtime-resolution.ts b/src/main/local-project-runtime-resolution.ts index 961a7148408..09dbc5754a0 100644 --- a/src/main/local-project-runtime-resolution.ts +++ b/src/main/local-project-runtime-resolution.ts @@ -21,6 +21,7 @@ import { hasCachedWslDistros } from './wsl' import { getRepoIdFromWorktreeId } from '../shared/worktree/id' +import { projectMatchesRepo } from '../shared/project-repo-match' import { resolveLocalWindowsTerminalRuntimeOptions, type LocalWindowsTerminalRuntimeOptions @@ -111,8 +112,7 @@ export function resolveLocalWorkspaceRuntime( ) { return undefined } - // TEMPORARY parity: main finds the project by its source repos only; the window also by its id. - const project = store.getProjects().find((entry) => entry.sourceRepoIds.includes(repo.id)) + const project = store.getProjects().find((entry) => projectMatchesRepo(entry, repo.id)) const preference = getWorkspaceRuntimePreference( project?.localWindowsRuntimePreference, workspacePath diff --git a/src/main/native-chat/agent-session-journal/journal-legacy-import.ts b/src/main/native-chat/agent-session-journal/journal-legacy-import.ts index b934e2a5dc7..e69abd28a67 100644 --- a/src/main/native-chat/agent-session-journal/journal-legacy-import.ts +++ b/src/main/native-chat/agent-session-journal/journal-legacy-import.ts @@ -1,8 +1,8 @@ // Hydrating a journal from a bridge-era transcript. // -// This reuses the existing per-agent transcript decoders verbatim — a second -// parser would drift from the one the live view already uses. The decoders -// return a render model with no identity, so the import wraps them: the wrapper +// Ordinary messages reuse the existing per-agent transcript decoders; Pi also +// projects its saved branch and summaries. The decoders return a render model +// with no identity, so the import wraps them: the wrapper // reads an identity anchor off the SAME raw line, then delegates the content. // // Import always opens a fresh epoch. The imported timeline is a best-effort @@ -31,6 +31,7 @@ import { decodeTranscriptStream } from '../transcript-stream-lines' import { readOpenCodeNativeChatTranscriptFull } from '../transcript-opencode' import { boundSubagentEntryId } from '../subagent-entry-id-bounds' import { createLegacyIdentityTracker } from './journal-legacy-identity' +import { createPiTranscriptCollector } from './journal-pi-active-branch' import type { JournalReplacementItem } from './journal-epoch-replacement' import { boundInlineText, @@ -89,7 +90,9 @@ export async function prepareLegacyTranscriptImport(input: { }): Promise<{ ok: true; items: JournalReplacementItem[] } | { ok: false; error: string }> { const options = input.options ?? {} const limits = options.limits ?? DEFAULT_JOURNAL_PAYLOAD_LIMITS - const transcriptAgent = resolveNativeChatTranscriptAgent(input.agent) + // OMP is a Pi fork that kept Pi's session format, so OMP's decoder reads Pi's sessions too. + const transcriptAgent = + input.agent === 'pi' ? 'omp' : resolveNativeChatTranscriptAgent(input.agent) if (!transcriptAgent) { return { ok: false, error: `Unsupported agent for journal import: ${input.agent}` } } @@ -206,6 +209,7 @@ async function decodeWithIdentities(input: { }) const decode = TRANSCRIPT_DECODERS[input.transcriptAgent] const identities: AgentJournalItemIdentity[] = [] + const pi = input.agent === 'pi' ? createPiTranscriptCollector() : null let lineIndex = 0 // Count raw bytes while reading: the source can grow after the stat check. @@ -217,6 +221,10 @@ async function decodeWithIdentities(input: { (line, fallbackId) => { const trackedIdentity = tracker.identify(line, lineIndex) lineIndex += 1 + if (pi) { + pi.record(line, fallbackId, trackedIdentity) + return null + } const message = decode(line, fallbackId) if (message) { identities.push( @@ -235,6 +243,9 @@ async function decodeWithIdentities(input: { true, MAX_LEGACY_IMPORT_SOURCE_BYTES ) + if (pi) { + return pi.project() + } return { messages, identities } } diff --git a/src/main/native-chat/agent-session-journal/journal-pi-active-branch.ts b/src/main/native-chat/agent-session-journal/journal-pi-active-branch.ts new file mode 100644 index 00000000000..7940fa70f9c --- /dev/null +++ b/src/main/native-chat/agent-session-journal/journal-pi-active-branch.ts @@ -0,0 +1,123 @@ +import type { AgentJournalItemIdentity } from '../../../shared/agent-session-journal-types' +import type { NativeChatMessage } from '../../../shared/native-chat-types' +import { asRecord, parseJsonObject, timestampMs } from '../../ai-vault/session-scanner-values' +import { decodeOmpTranscriptLine } from '../transcript-line-decoders-omp' + +type PiTranscriptBranchEntry = { + id: string + parentId: string | null + type: unknown + firstKeptEntryId: string | null + message: NativeChatMessage | null + identity: AgentJournalItemIdentity +} + +export function createPiTranscriptCollector(): { + record(line: string, fallbackId: string, identity: AgentJournalItemIdentity): void + project(): { messages: NativeChatMessage[]; identities: AgentJournalItemIdentity[] } +} { + const entries: PiTranscriptBranchEntry[] = [] + const fileEntryIds: (string | null)[] = [] + let version = 1 + return { + record(line, fallbackId, identity) { + const record = parseJsonObject(line) + if (!record) { + return + } + if (record.type === 'session') { + version = typeof record.version === 'number' ? record.version : 1 + fileEntryIds.push(null) + return + } + const id = version < 2 ? `#${fileEntryIds.length}` : record.id + const parentId = version < 2 ? (entries.at(-1)?.id ?? null) : record.parentId + if (typeof id !== 'string' || !(parentId === null || typeof parentId === 'string')) { + throw new Error('Pi session entry has no valid branch identity') + } + // Pi v1 indexes include the header and parsed metadata, but not malformed lines. + const firstKeptEntryId = + version < 2 && typeof record.firstKeptEntryIndex === 'number' + ? fileEntryIds[record.firstKeptEntryIndex] + : record.firstKeptEntryId + const message = + record.type === 'compaction' || record.type === 'branch_summary' + ? piSummaryMessage(record, fallbackId) + : asRecord(record.message)?.role === 'system' + ? null + : decodeOmpTranscriptLine(line, fallbackId) + entries.push({ + id, + parentId, + type: record.type, + firstKeptEntryId: typeof firstKeptEntryId === 'string' ? firstKeptEntryId : null, + message, + identity + }) + fileEntryIds.push(id) + }, + project: () => selectActivePiBranch(entries) + } +} + +function piSummaryMessage( + record: Record, + fallbackId: string +): NativeChatMessage | null { + const summary = typeof record.summary === 'string' ? record.summary : '' + if (record.type === 'branch_summary' && !summary) { + return null + } + const timestamp = timestampMs(record.timestamp) + return { + id: typeof record.id === 'string' ? record.id : fallbackId, + role: 'system', + blocks: [{ type: 'text', text: summary }], + timestamp: Number.isFinite(timestamp) ? timestamp : null, + source: 'transcript' + } +} + +/** Match Pi's fresh-session TUI: persisted leaf ancestry, then buildContextEntries. */ +function selectActivePiBranch(entries: readonly PiTranscriptBranchEntry[]): { + messages: NativeChatMessage[] + identities: AgentJournalItemIdentity[] +} { + const byId = new Map(entries.map((entry) => [entry.id, entry])) + if (byId.size !== entries.length) { + throw new Error('Pi session has duplicate branch identities') + } + const branch: PiTranscriptBranchEntry[] = [] + const visited = new Set() + let current = entries.at(-1) + while (current) { + if (visited.has(current.id)) { + throw new Error('Pi session branch contains a cycle') + } + visited.add(current.id) + branch.push(current) + current = current.parentId ? byId.get(current.parentId) : undefined + } + const messages: NativeChatMessage[] = [] + const identities: AgentJournalItemIdentity[] = [] + for (const entry of compactedPiBranch(branch.toReversed())) { + if (entry.message) { + messages.push(entry.message) + identities.push(entry.identity) + } + } + return { messages, identities } +} + +function compactedPiBranch(branch: PiTranscriptBranchEntry[]): PiTranscriptBranchEntry[] { + const compactionIndex = branch.findLastIndex((entry) => entry.type === 'compaction') + const compaction = branch[compactionIndex] + if (!compaction) { + return branch + } + const keptIndex = branch.findIndex((entry) => entry.id === compaction.firstKeptEntryId) + // Pi searches for firstKeptEntryId only before the latest compaction. + const kept = + keptIndex !== -1 && keptIndex < compactionIndex ? branch.slice(keptIndex, compactionIndex) : [] + return [compaction, ...kept, ...branch.slice(compactionIndex + 1)] +} diff --git a/src/main/native-chat/agent-session-journal/journal-pi-context-fixtures.ts b/src/main/native-chat/agent-session-journal/journal-pi-context-fixtures.ts new file mode 100644 index 00000000000..c738a16d6e7 --- /dev/null +++ b/src/main/native-chat/agent-session-journal/journal-pi-context-fixtures.ts @@ -0,0 +1,73 @@ +const timestamp = '2026-10-12T12:00:00.000Z' + +function turn(id: string, parentId: string | null, text = id, role = 'user') { + return { type: 'message', id, parentId, timestamp, message: { role, content: text } } +} + +function compact(id: string, parentId: string, firstKeptEntryId: string | null) { + return { + type: 'compaction', + id, + parentId, + timestamp, + firstKeptEntryId, + summary: id, + tokensBefore: 100 + } +} + +const root = turn('root', null) +const kept = turn('kept', 'root') +const checkpoint = compact('summary', 'kept', 'kept') +const later = turn('later', 'summary', 'later', 'assistant') + +// Verified against Pi 1.1.0 buildContextEntries + sessionEntryToContextMessages. +export const PI_CONTEXT_FIXTURES = [ + { + name: 'kept range before latest compaction', + entries: [root, kept, checkpoint, later], + texts: ['summary', 'kept', 'later'] + }, + { + name: 'branch summary with metadata leaf omits abandoned branch', + entries: [ + root, + turn('abandoned', 'root'), + { + type: 'branch_summary', + id: 'branch', + parentId: 'root', + fromId: 'abandoned', + summary: 'branch', + timestamp + }, + { + type: 'label', + id: 'label', + parentId: 'branch', + targetId: 'branch', + label: 'saved', + timestamp + } + ], + texts: ['root', 'branch'] + }, + { + name: 'version 1 migration uses parsed file index including header', + version: 1, + entries: [ + { type: 'message', message: { role: 'user', content: 'root' }, timestamp }, + { type: 'model_change', provider: 'test', modelId: 'test', timestamp }, + { type: 'message', message: { role: 'user', content: 'kept' }, timestamp }, + { + type: 'compaction', + firstKeptEntryIndex: 3, + summary: 'summary', + tokensBefore: 100, + timestamp + }, + { type: 'message', message: { role: 'assistant', content: 'later' }, timestamp } + ], + texts: ['summary', 'kept', 'later'] + } +] diff --git a/src/main/native-chat/agent-session-journal/journal-pi-context-import.test.ts b/src/main/native-chat/agent-session-journal/journal-pi-context-import.test.ts new file mode 100644 index 00000000000..a1450e923f3 --- /dev/null +++ b/src/main/native-chat/agent-session-journal/journal-pi-context-import.test.ts @@ -0,0 +1,98 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { + importLegacyTranscriptIntoJournal, + prepareLegacyTranscriptImport +} from './journal-legacy-import' +import { createTrackedJournalOpener } from './journal-host-database-test-support' +import { PI_CONTEXT_FIXTURES } from './journal-pi-context-fixtures' + +let root: string +const journals = createTrackedJournalOpener() +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'orca-pi-context-')) +}) +afterEach(async () => { + await journals.closeAll() + await rm(root, { recursive: true, force: true }) +}) + +async function prepare(entries: unknown[], version = 3) { + const filePath = join(root, 'session.jsonl') + const header = { type: 'session', id: 'session', version, cwd: root } + // Malformed records do not contribute to Pi's v1 entry indexes. + await writeFile( + filePath, + [JSON.stringify(header), '{bad json', ...entries.map((entry) => JSON.stringify(entry))].join( + '\n' + ) + ) + const result = await prepareLegacyTranscriptImport({ + agent: 'pi', + sessionId: 'session', + options: { filePath } + }) + if (!result.ok) { + throw new Error(result.error) + } + return result.items +} + +describe('saved Pi context import', () => { + it.each(PI_CONTEXT_FIXTURES)('$name', async (fixture) => { + const items = await prepare(fixture.entries, 'version' in fixture ? fixture.version : 3) + expect( + items.map((item) => + item.body.kind === 'message' + ? item.body.blocks.flatMap((block) => (block.type === 'text' ? [block.text] : [])) + : [] + ) + ).toEqual(fixture.texts.map((text) => [text])) + }) + + it('persists compaction and branch summaries in the adopted journal across reopen', async () => { + const fixture = PI_CONTEXT_FIXTURES[0] + if (!fixture) { + throw new Error('missing compaction fixture') + } + const expected = await prepare([ + ...fixture.entries, + { + type: 'branch_summary', + id: 'branch', + parentId: 'later', + fromId: 'discarded', + summary: 'Saved branch details.' + } + ]) + const filePath = join(root, 'session.jsonl') + const options = { + stateDirectory: join(root, 'journal'), + identity: { + sessionId: 'native-chat', + workspaceId: 'workspace', + hostId: 'host', + agent: 'pi' as const, + providerHandle: { transport: 'jsonl-rpc', agent: 'pi', nativeId: filePath } + } + } + const journal = await journals.open(options) + expect( + await importLegacyTranscriptIntoJournal({ + journal, + agent: 'pi', + sessionId: 'session', + fence: 1, + options: { filePath } + }) + ).toMatchObject({ ok: true, imported: 4, replaced: true }) + await journal.close() + const reopened = await journals.open(options) + expect(reopened.snapshot().items.map((item) => item.body)).toEqual( + expected.map((item) => item.body) + ) + expect(JSON.stringify(reopened.snapshot().items)).toContain('Saved branch details.') + }) +}) diff --git a/src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts b/src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts index f94624c26c6..7cce60a3bdd 100644 --- a/src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts +++ b/src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts @@ -119,6 +119,11 @@ describe('forward history SQL read budget', () => { Array.from({ length: count }, (_, index) => `item-${index}`) ) expect(batches.at(-1)?.batch.cursor).toEqual(journal.cursor()) + // Only the last page says the reader is caught up. + expect(batches.map((event) => event.hasNewer === true)).toEqual([ + ...Array(9).fill(true), + false + ]) expect(returnedRows).toEqual([...Array(9).fill(201), 200]) expect(parse).toHaveBeenCalledTimes(2_009) }) diff --git a/src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts b/src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts index edce937b904..7c546de2927 100644 --- a/src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts +++ b/src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts @@ -138,6 +138,7 @@ export function deliverToSubscriber( ? { actionablePromptIds: page.actionablePromptIds } : {}), ...(page.working !== undefined ? { working: page.working } : {}), + ...(page.hasNewer ? { hasNewer: true as const } : {}), ...shared }, // On a multi-page catch-up the draft list rides only the final page, or a diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts index 25e70096078..cb842d04c84 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts @@ -200,6 +200,14 @@ export class StructuredAgentSessionAdapterRouter implements StructuredAgentSessi this.requireAgent(input.identity).adapter.providerHistoryWindow?.(input) ?? Promise.resolve(null) + copyConversation: NonNullable = (input) => { + const { adapter } = this.requireAgent(input.identity) + if (!adapter.copyConversation) { + return Promise.reject(new Error(`${input.identity.agent} cannot copy a conversation`)) + } + return adapter.copyConversation(input) + } + closeSession = (sessionId: string): Promise => this.stopSession(sessionId, (adapter) => adapter.closeSession) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts index bddcd06c964..664cb423838 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts @@ -1,3 +1,4 @@ +import type { AiVaultConversationCopy } from '../../../shared/ai-vault-resume-preparation' import type { AgentSessionAccountKind, AgentSessionUnavailable @@ -439,6 +440,14 @@ export type StructuredAgentSessionAdapter = StructuredAgentSessionAdapterStop & identity: AgentSessionJournalIdentity accountHome: AgentSessionAccountHome }): Promise + /** Copies provider conversation `providerSessionId` of the chat into a new one the agent saves, + * answering the copy's id; the chat keeps its own. Runs whether or not the chat's agent is + * running. Absent where the agent copies through its CLI instead (`agentForksThroughChat`). */ + copyConversation?(input: { + identity: AgentSessionJournalIdentity + providerSessionId: string + accountHome: AgentSessionAccountHome + }): Promise /** Gracefully stops the structured owner after its event stream is drained. */ /** Returns true only after the provider child exit is proven. A root-exit or processless verdict * is thrown only once the session is finalized; read it through `stopAgentSessionProviderRoot`. */ diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts index cd9206afd5a..92eed86e5e7 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts @@ -25,8 +25,7 @@ import { NO_STRUCTURED_AGENTS, structuredAgentsDeclaringNothing } from './structured-agent-session-adapter-router-test-support' -import { acpStructuredAgentDefinition } from '../../acp/acp-structured-agent-definitions' -import { acpLaunchSpecFor } from '../../acp/acp-launch-specs' +import { structuredAgentRuntimeRegistration } from '../../runtime/structured-agent-runtime-registrations' import { writeOpenCodeSqliteDatabase } from '../../ai-vault/session-scanner-opencode-sqlite-fixture' // The OpenCode reader's worker, run in process. @@ -208,17 +207,35 @@ describe('adopting a provider conversation on create', () => { ]) return path } + }, + { + agent: 'pi', + accountHome: { variable: 'PI_CODING_AGENT_DIR', path: '/home/dev/.pi/agent' }, + record: async (dir: string, text: string) => { + const path = join(dir, `2026-10-12T00-00-00-000Z_${THREAD}.jsonl`) + const records = [ + { type: 'session', version: 3, id: THREAD, cwd: '/workspace' }, + { type: 'message', id: 'rec-1', parentId: null, message: { role: 'user', content: text } } + ] + await writeFile(path, `${records.map((line) => JSON.stringify(line)).join('\n')}\n`) + return path + } } ] as const)( "seeds a $agent chat's chain with the session it adopts and imports that CLI's history", async ({ agent, accountHome, record }) => { root = await mkdtemp(join(tmpdir(), `orca-adopt-${agent}-`)) const transcriptPath = await record(root, `ORCA-${agent}-ADOPT`) - const spec = acpLaunchSpecFor(agent) - if (!spec) { + const definition = structuredAgentRuntimeRegistration(agent)?.definition + if (!definition) { throw new Error(`this build registers no ${agent} chats`) } - const handle = { transport: 'acp', agent, nativeId: THREAD } + // A Pi chat's handle names its session file; the others name the conversation id. + const handle = { + transport: definition.handleTransport, + agent, + nativeId: agent === 'pi' ? transcriptPath : THREAD + } const sessionAdapter = adapter() vi.mocked(sessionAdapter.acquire).mockImplementation(async ({ fence, spawnToken }) => ({ process: { hostId: 'local', pid: 4242, processStartTimeMs: NOW, spawnToken }, @@ -241,7 +258,7 @@ describe('adopting a provider conversation on create', () => { accountHome, adopt: { providerHandle: handle, transcriptPath } }), - structuredAgentsDeclaringNothing([acpStructuredAgentDefinition(spec)]) + structuredAgentsDeclaringNothing([definition]) ) expect(result.ok && JSON.stringify(result.value.page.items)).toContain(`ORCA-${agent}-ADOPT`) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-attach.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-attach.ts index 54e7f61845d..c0ce0923e43 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-attach.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-attach.ts @@ -285,7 +285,10 @@ function adoptedProviderHandleLink( observedAt: number ): AgentSessionProviderHandleLink { return { - linkId: `${handle.agent}-${ADOPTED_HANDLE_FENCE}-${handle.nativeId}`.slice(0, 128), + // A link id is a bounded slug; Pi's native id is a file path. + linkId: `${handle.agent}-${ADOPTED_HANDLE_FENCE}-${handle.nativeId}` + .replace(/[^A-Za-z0-9_-]/g, '_') + .slice(0, 128), handle, origin: 'adopted', mintedAtFence: ADOPTED_HANDLE_FENCE, diff --git a/src/main/native-chat/structured-agent-cli-conversation-adoption.test.ts b/src/main/native-chat/structured-agent-cli-conversation-adoption.test.ts index 720189aa7a6..42009e6d884 100644 --- a/src/main/native-chat/structured-agent-cli-conversation-adoption.test.ts +++ b/src/main/native-chat/structured-agent-cli-conversation-adoption.test.ts @@ -39,8 +39,9 @@ const SESSION_ID = '0198c2a4-7d1e-7c3a-9a4b-1f2e3d4c5b6a' let root: string | null = null beforeEach(() => { - // Grok also looks where Session History lists it from; keep that off the real home. + // Grok and Pi also look where Session History lists them from; keep that off the real home. vi.stubEnv('GROK_HOME', join(tmpdir(), 'orca-adopt-no-grok-home')) + vi.stubEnv('PI_CODING_AGENT_DIR', join(tmpdir(), 'orca-adopt-no-pi-home')) }) afterEach(async () => { @@ -54,7 +55,7 @@ afterEach(async () => { }) /** Each agent's CLI conversation as its own CLI lays it out under an account home. */ -const RECORDED: Record<'grok' | 'omp', (home: string, text: string) => Promise> = { +const RECORDED: Record<'grok' | 'omp' | 'pi', (home: string, text: string) => Promise> = { grok: async (home, text) => { const dir = join(home, 'sessions', encodeURIComponent('/repo/orca'), SESSION_ID) await mkdir(dir, { recursive: true }) @@ -90,11 +91,46 @@ const RECORDED: Record<'grok' | 'omp', (home: string, text: string) => Promise { + const dir = join(home, 'sessions', '--repo-orca--') + await mkdir(dir, { recursive: true }) + const file = join(dir, `2026-10-12T00-00-00-000Z_${SESSION_ID}.jsonl`) + const records = [ + { type: 'session', version: 3, id: SESSION_ID, cwd: '/repo/orca' }, + { + type: 'message', + id: 'a0b1c2d3', + parentId: null, + message: { role: 'system', content: '', sections: { preamble: 'You are Pi' } } + }, + { + type: 'message', + id: 'a1b2c3d4', + parentId: 'a0b1c2d3', + message: { role: 'user', content: text, timestamp: 1_791_763_201_000 } + }, + { + type: 'message', + id: 'b2c3d4e5', + parentId: 'a1b2c3d4', + message: { role: 'assistant', content: [{ type: 'text', text: 'Hi!' }], stopReason: 'stop' } + } + ] + await writeFile(file, `${records.map((record) => JSON.stringify(record)).join('\n')}\n`) + return file } } +const ACCOUNT_VARIABLE = { + grok: 'GROK_HOME', + omp: 'PI_CODING_AGENT_DIR', + pi: 'PI_CODING_AGENT_DIR' +} + function adopt( - agent: 'grok' | 'omp' | 'opencode', + agent: 'grok' | 'omp' | 'opencode' | 'pi', selectedAccountHome: AgentSessionAccountHome, providerSessionId = SESSION_ID, launchEnv: NodeJS.ProcessEnv = process.env, @@ -287,15 +323,15 @@ it('refuses OMP history from another workspace without creating a blank chat', a }) }) -describe.each(['grok', 'omp'] as const)('adopting a %s CLI conversation', (agent) => { +describe.each(['grok', 'omp', 'pi'] as const)('adopting a %s CLI conversation', (agent) => { it('finds the conversation under the chat account and imports its history', async () => { root = await mkdtemp(join(tmpdir(), `orca-${agent}-adopt-`)) const recorded = await RECORDED[agent](root, `ORCA-${agent}-ADOPT`) - - const accountHome = { - variable: agent === 'grok' ? 'GROK_HOME' : 'PI_CODING_AGENT_DIR', - path: root + if (agent === 'pi') { + vi.stubEnv('PI_CODING_AGENT_DIR', root) } + + const accountHome = { variable: ACCOUNT_VARIABLE[agent], path: root } const adoption = await adopt(agent, accountHome) expect(adoption).toEqual({ accountHome, transcriptPath: recorded }) @@ -305,18 +341,40 @@ describe.each(['grok', 'omp'] as const)('adopting a %s CLI conversation', (agent options: { filePath: adoption.transcriptPath } }) expect(imported.ok && JSON.stringify(imported.items)).toContain(`ORCA-${agent}-ADOPT`) + if (agent === 'pi' && imported.ok) { + expect(imported.items.map((item) => item.body.kind === 'message' && item.body.role)).toEqual([ + 'user', + 'assistant' + ]) + } }) it('refuses a conversation the account does not hold, rather than opening a blank chat', async () => { root = await mkdtemp(join(tmpdir(), `orca-${agent}-adopt-missing-`)) await expect( - adopt(agent, { variable: agent === 'grok' ? 'GROK_HOME' : 'PI_CODING_AGENT_DIR', path: root }) + adopt(agent, { variable: ACCOUNT_VARIABLE[agent], path: root }) ).rejects.toMatchObject({ refusal: { code: 'agent_session_identity_required' } }) }) }) +describe('adopting a Pi CLI conversation', () => { + it('pins the Session History account when another Pi home has the same id', async () => { + root = await mkdtemp(join(tmpdir(), 'orca-pi-adopt-homes-')) + const selected = join(root, 'selected') + const history = join(root, 'history') + await RECORDED.pi(selected, 'SELECTED') + const source = await RECORDED.pi(history, 'HISTORY') + vi.stubEnv('PI_CODING_AGENT_DIR', history) + + expect(await adopt('pi', { variable: 'PI_CODING_AGENT_DIR', path: selected })).toEqual({ + accountHome: { variable: 'PI_CODING_AGENT_DIR', path: history }, + transcriptPath: source + }) + }) +}) + describe('adopting an OpenCode CLI conversation', () => { const OPENCODE_SESSION = 'ses_orca_adopt' const unmanaged: AgentSessionAccountHome = { kind: 'opencode', locator: { kind: 'unmanaged' } } diff --git a/src/main/native-chat/structured-agent-cli-conversations.ts b/src/main/native-chat/structured-agent-cli-conversations.ts index f4ed02ecda4..3e84138c0c7 100644 --- a/src/main/native-chat/structured-agent-cli-conversations.ts +++ b/src/main/native-chat/structured-agent-cli-conversations.ts @@ -30,6 +30,8 @@ export type StructuredAgentTranscriptImport = { launchEnv: NodeJS.ProcessEnv workspaceCwd: string }) => Promise + /** The adopted chat's handle names the transcript's path, not the conversation id (Pi). */ + handleNamesTranscript?: true } /** The import of an agent whose account is one directory, named by the chat's own variable. */ @@ -44,8 +46,10 @@ export function directoryTranscriptImport(directories: { launchEnv: NodeJS.ProcessEnv workspaceCwd: string }) => Promise + handleNamesTranscript?: true }): StructuredAgentTranscriptImport { return { + ...(directories.handleNamesTranscript ? { handleNamesTranscript: true } : {}), accountHomeCandidates: ({ settings, selectedAccountHome }) => isLegacyAgentSessionAccountHome(selectedAccountHome) ? directories diff --git a/src/main/native-chat/structured-agent-configured-args.test.ts b/src/main/native-chat/structured-agent-configured-args.test.ts index 58823a40608..4daf3556527 100644 --- a/src/main/native-chat/structured-agent-configured-args.test.ts +++ b/src/main/native-chat/structured-agent-configured-args.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { structuredAgentConfiguredArgs } from './structured-agent-configured-args' describe('structured chat configured Arguments', () => { - it.each(['claude', 'codex'] as const)( + it.each(['claude', 'codex', 'opencode', 'omp', 'grok'] as const)( 'reads the existing %s setting and preserves quoted values', (agent) => { expect( diff --git a/src/main/native-chat/structured-agent-configured-args.ts b/src/main/native-chat/structured-agent-configured-args.ts index 3b60c764268..49aac2e398b 100644 --- a/src/main/native-chat/structured-agent-configured-args.ts +++ b/src/main/native-chat/structured-agent-configured-args.ts @@ -1,4 +1,4 @@ -import type { AgentSessionHandleProvider } from '../../shared/agent-session-provider-handle' +import type { TuiAgent } from '../../shared/tui-agent' import type { GlobalSettings } from '../../shared/global-settings-types' import { resolveTuiAgentLaunchArgs } from '../../shared/tui-agent-launch-defaults' import { tokenizeStartupCommand } from '../../shared/tui-agent-startup-shell' @@ -6,7 +6,7 @@ import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-termi /** Use the same grouping rules as terminal launches before the provider filters its owned flags. */ export function structuredAgentConfiguredArgs( - agent: AgentSessionHandleProvider, + agent: TuiAgent, settings: Partial>, platform: NodeJS.Platform = process.platform ): string[] { diff --git a/src/main/native-chat/structured-agent-session-history-adoption.test.ts b/src/main/native-chat/structured-agent-session-history-adoption.test.ts index f2fa0348d9a..44154317b90 100644 --- a/src/main/native-chat/structured-agent-session-history-adoption.test.ts +++ b/src/main/native-chat/structured-agent-session-history-adoption.test.ts @@ -10,6 +10,7 @@ import { structuredAdoptionConflictError, type StructuredAgentSessionAdoptionOwnership } from './structured-agent-session-history-adoption' +import { PI_SESSION_HISTORY } from '../pi/pi-session-history' import { claudeProviderHandle, codexProviderHandle @@ -22,6 +23,7 @@ function committedReplay(overrides: { callerKey?: string; operationId?: string } return findCommittedStructuredAgentSessionAdoptionReplay({ agent: 'codex', providerSessionId: 'thread-1', + rowSessionId: (link) => link.handle.nativeId, selfSessionId: 'codex_adopted', callerKey: overrides.callerKey ?? 'client-1', operationId: overrides.operationId ?? OPERATION, @@ -139,6 +141,7 @@ describe('findCommittedStructuredAgentSessionAdoptionReplay', () => { findCommittedStructuredAgentSessionAdoptionReplay({ agent: 'claude', providerSessionId: 'provider-session-alpha-1', + rowSessionId: (link) => link.handle.nativeId, selfSessionId: 'claude_adopted', callerKey: 'client-1', operationId: OPERATION, @@ -161,6 +164,47 @@ describe('findCommittedStructuredAgentSessionAdoptionReplay', () => { }) }) +describe('replaying a Pi adoption, whose handle names its session file', () => { + it("matches the row id the agent's Session History shows the file as", () => { + const file = + '/home/dev/.pi/agent/sessions/--repo--/2026-10-12T00-00-00-000Z_0198c2a4-7d1e-7c3a-9a4b-1f2e3d4c5b6a.jsonl' + const lease = agentSessionLeaseFixture({ sessionId: 'pi_adopted' }) + const record = agentSessionRecordFixture(lease) + record.provider = 'pi' + record.providerHandleChain[0] = { + ...record.providerHandleChain[0]!, + origin: 'adopted', + handle: { transport: 'jsonl-rpc', agent: 'pi', nativeId: file } + } + const replay = (providerSessionId: string) => + findCommittedStructuredAgentSessionAdoptionReplay({ + agent: 'pi', + providerSessionId, + rowSessionId: PI_SESSION_HISTORY.rowSessionId, + selfSessionId: 'pi_adopted', + callerKey: 'client-1', + operationId: OPERATION, + record, + operations: [ + { + callerKey: 'client-1', + operationId: OPERATION, + fingerprint: 'fingerprint-1', + operationTimestamp: 1_800_000_000_000, + recordedAt: 1_800_000_000_000, + expiresAt: 1_900_000_000_000, + outcome: { status: 'succeeded', sessionId: 'pi_adopted' } + } + ] + }) + + expect(replay('0198c2a4-7d1e-7c3a-9a4b-1f2e3d4c5b6a')).toMatchObject({ + providerHandle: { nativeId: file } + }) + expect(replay('0198c2a4-7d1e-7c3a-9a4b-000000000000')).toBeNull() + }) +}) + describe('structuredAdoptionConflictError', () => { it('calls a conversation with an admitted writer a conflict', () => { expect(structuredAdoptionConflictError(ownership()).message).toBe('agent_session_conflict') diff --git a/src/main/native-chat/structured-agent-session-history-adoption.ts b/src/main/native-chat/structured-agent-session-history-adoption.ts index 06581ae1858..8d06a282a6e 100644 --- a/src/main/native-chat/structured-agent-session-history-adoption.ts +++ b/src/main/native-chat/structured-agent-session-history-adoption.ts @@ -9,6 +9,7 @@ import { agentSessionRefusalError } from '../../shared/agent-session-wire-refusa import type { AgentSessionOperationRow } from '../../shared/agent-session-operation-ledger' import type { AgentSessionProviderHandle, + AgentSessionProviderHandleLink, StructuredAgentId } from '../../shared/agent-session-provider-handle' import type { AgentSessionLease, AgentSessionRecord } from '../../shared/agent-session-record' @@ -41,6 +42,8 @@ export type CommittedStructuredAgentSessionAdoptionReplay = { export function findCommittedStructuredAgentSessionAdoptionReplay(input: { agent: StructuredAgentId providerSessionId: string + /** The Session History row id a link of the agent's handle chain shows as. */ + rowSessionId: (link: AgentSessionProviderHandleLink) => string selfSessionId: string callerKey: string operationId: string @@ -66,7 +69,7 @@ export function findCommittedStructuredAgentSessionAdoptionReplay(input: { ) { return null } - if (adopted.handle.nativeId !== input.providerSessionId) { + if (input.rowSessionId(adopted) !== input.providerSessionId) { return null } return { record, providerHandle: adopted.handle } diff --git a/src/main/native-chat/transcript-opencode2-sqlite.test.ts b/src/main/native-chat/transcript-opencode2-sqlite.test.ts index 7ddd653071d..df36bbc78a1 100644 --- a/src/main/native-chat/transcript-opencode2-sqlite.test.ts +++ b/src/main/native-chat/transcript-opencode2-sqlite.test.ts @@ -26,11 +26,10 @@ function fixture() { CREATE TABLE session_message (id TEXT PRIMARY KEY, session_id TEXT, type TEXT, seq INTEGER, data TEXT, time_created INTEGER, time_updated INTEGER); INSERT INTO session_v2 VALUES ('session');`) + const insertMessage = db.prepare('INSERT INTO session_message VALUES (?, ?, ?, ?, ?, ?, ?)') let sequence = 0 const insert = (id: string, type: string, data: unknown, time = 1) => - db - .prepare('INSERT INTO session_message VALUES (?, ?, ?, ?, ?, ?, ?)') - .run(id, 'session', type, ++sequence, JSON.stringify(data), time, time) + insertMessage.run(id, 'session', type, ++sequence, JSON.stringify(data), time, time) return { db, path, insert } } @@ -159,6 +158,8 @@ describe('OpenCode 2 native transcript', () => { it('refuses excessive sparse scans instead of returning a partial transcript', () => { const { db, path, insert } = fixture() + // The regression is the scan cap; avoid quadratic fixture scans between batches. + db.exec('CREATE INDEX session_message_sequence ON session_message (session_id, seq)') db.exec('BEGIN') for (let index = 0; index < 10001; index++) { insert(String(index), 'idle', {}) diff --git a/src/main/opencode/opencode-model-startup-plan.ts b/src/main/opencode/opencode-model-startup-plan.ts index ffdeb02d167..84b0599ce88 100644 --- a/src/main/opencode/opencode-model-startup-plan.ts +++ b/src/main/opencode/opencode-model-startup-plan.ts @@ -24,8 +24,6 @@ type StartupScope = { inputs: AgentStartupPlanInputs cwd: string isWsl?: boolean - /** TEMPORARY parity: the platform main checked this model on, when it differs. */ - modelCheckPlatform?: NodeJS.Platform hostIdentity?: string signal?: AbortSignal } @@ -51,20 +49,20 @@ export async function prepareOpenCodeModelStartupInputs( if (typeof model !== 'string' || model.trim().length === 0) { refuseModel() } - const platform = options.modelCheckPlatform ?? inputs.platform if ( Object.entries(inputs.sessionOptions ?? {}).some( ([key, value]) => key !== 'model' && value !== undefined ) || inputs.isRemote || options.isWsl || - platform !== process.platform || + inputs.platform !== process.platform || options.signal?.aborted ) { refuseModel() } const command = - inputs.cmdOverrides.opencode || getTuiAgentLaunchCommand(TUI_AGENT_CONFIG.opencode, platform) + inputs.cmdOverrides.opencode || + getTuiAgentLaunchCommand(TUI_AGENT_CONFIG.opencode, inputs.platform) const env: Record = {} for (const [key, value] of Object.entries({ ...process.env, ...inputs.agentEnv })) { if (value !== undefined) { diff --git a/src/main/orcad/orcad-entry.ts b/src/main/orcad/orcad-entry.ts index a56b3b42ab3..8dae0166b28 100644 --- a/src/main/orcad/orcad-entry.ts +++ b/src/main/orcad/orcad-entry.ts @@ -281,6 +281,10 @@ async function startOrcadRuntime( // Why before the RPC server binds: like `--serve`, the first client must find a ready graph. const { publishHeadlessRuntimeGraph } = await import('../runtime/headless-runtime-graph') publishHeadlessRuntimeGraph(runtime) + // Store load skipped moved shell history: only the runtime can tell which worktrees run a shell. + void runtime.installPendingOrcadMigrationShellHistory().catch((error: unknown) => { + console.error('[orcad-migration] installing pending shell history failed:', error) + }) const bindHost = resolveOrcadBindHost(options.bind) const rpc = new OrcaRuntimeRpcServer({ diff --git a/src/main/orcad/orcad-push-startup.test.ts b/src/main/orcad/orcad-push-startup.test.ts index 787d83de9f0..20f905a9a37 100644 --- a/src/main/orcad/orcad-push-startup.test.ts +++ b/src/main/orcad/orcad-push-startup.test.ts @@ -130,6 +130,7 @@ vi.mock('../runtime/orca-runtime', () => ({ restoreHostAccountLaunchEnv() {} async refreshRestoredOrchestrationAuthority() {} async reconcileLegacyWorkerTerminals() {} + async installPendingOrcadMigrationShellHistory() {} async stopLegacyWorkerTerminalRecovery() {} async closeNotificationDismissals() {} syncWindowGraph() {} diff --git a/src/main/persistence-host-partitioned-sessions.test.ts b/src/main/persistence-host-partitioned-sessions.test.ts index 9c3aad9620f..1314d8e2ff5 100644 --- a/src/main/persistence-host-partitioned-sessions.test.ts +++ b/src/main/persistence-host-partitioned-sessions.test.ts @@ -314,6 +314,20 @@ describe('Store host-partitioned workspace sessions', () => { expect(store.getWorkspaceSession('local').activeRepoId).toBe('repo-local') }) + it('names the one partition each session write changed', async () => { + const store = await createStore() + store.setWorkspaceSession(makeHostSession('repo-a'), 'runtime:env-a') + const written: (string | null)[] = [] + store.onWorkspaceSessionWritten((hostId) => written.push(hostId)) + + store.setWorkspaceSession(makeHostSession('repo-local'), 'local') + store.setWorkspaceSession(makeHostSession('repo-a2'), 'runtime:env-a') + store.patchWorkspaceSession({ activeTabId: 'tab-a' }, 'runtime:env-a') + store.removeWorkspaceSessionHost('runtime:env-a') + + expect(written).toEqual(['local', 'runtime:env-a', 'runtime:env-a', 'runtime:env-a']) + }) + it('patches a single host partition without touching the others', async () => { const store = await createStore() store.setWorkspaceSession(makeHostSession('repo-local'), 'local') diff --git a/src/main/persistence-orcad-migration-blobs.test.ts b/src/main/persistence-orcad-migration-blobs.test.ts new file mode 100644 index 00000000000..1b7ac054de3 --- /dev/null +++ b/src/main/persistence-orcad-migration-blobs.test.ts @@ -0,0 +1,116 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createHash } from 'node:crypto' +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import type { OrcadMigrationBlobDescriptor } from '../shared/orcad-migration-blob-transfer' +import { getOrcadMigrationReceivedBlobPath } from './persistence/migrating-orcad-catalog/orcad-migration-blob-staging' +import { manifest } from './persistence-orcad-migration-catalog-fixture' +import { createStore, testState } from './persistence-test-harness' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { + isEncryptionAvailable: () => true, + encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'), + decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length) + } +})) + +vi.mock('./telemetry/client', () => ({ track: vi.fn() })) + +// A kind with no installer yet, so the channel's own behaviour is all that runs. +const bytes = Buffer.from('session record one\nsession record two\n') +const blob: OrcadMigrationBlobDescriptor = { + blobKind: 'agent-session-records', + key: 'session-kept', + sha256: createHash('sha256').update(bytes).digest('hex'), + byteLength: bytes.length +} + +function chunk(input: ReturnType, offset: number, end: number) { + return { + migrationId: input.migrationId, + manifestSha256: input.manifestSha256, + blob, + offset, + bytesBase64: bytes.subarray(offset, end).toString('base64') + } +} + +function blobRoot(): string { + return join(testState.dir, 'orcad-migration-blobs') +} + +describe('orcad migration blobs through the store', () => { + beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'orca-migration-blobs-')) + }) + + afterEach(() => { + rmSync(testState.dir, { recursive: true, force: true }) + }) + + it('reports staged progress across a restart and refuses commit until complete', async () => { + const input = manifest() + const store = createStore() + store.stageOrcadMigrationCatalog(input) + await store.flushPendingOrThrowAsync() + expect(store.getOrcadMigrationCatalogState(input)).not.toHaveProperty('blobUploads') + store.stageOrcadMigrationBlobChunk(chunk(input, 0, 8)) + + const restarted = createStore() + expect(restarted.getOrcadMigrationCatalogState(input)).toMatchObject({ + state: 'staged', + blobUploads: [{ ...blob, receivedBytes: 8 }] + }) + expect(() => restarted.commitStagedOrcadMigrationCatalog(input)).toThrow( + `orcad_migration_blob_incomplete:agent-session-records:${blob.key}` + ) + expect(restarted.getOrcadMigrationCatalogState(input).state).toBe('staged') + + restarted.stageOrcadMigrationBlobChunk(chunk(input, 8, bytes.length)) + expect(restarted.commitStagedOrcadMigrationCatalog(input).state).toBe('committed') + expect(readFileSync(getOrcadMigrationReceivedBlobPath(blobRoot(), input, blob))).toEqual(bytes) + }) + + it('commits only the blob set the sender names, and checks nothing when it names none', () => { + const input = manifest() + const store = createStore() + store.stageOrcadMigrationCatalog(input) + store.stageOrcadMigrationBlobChunk(chunk(input, 0, bytes.length)) + const missing: OrcadMigrationBlobDescriptor = { + blobKind: 'agent-session-records', + key: 'session-lost', + sha256: 'e'.repeat(64), + byteLength: 4 + } + + expect(() => + store.commitStagedOrcadMigrationCatalog(input, { + expectedBlobs: [blob, missing] + }) + ).toThrow('orcad_migration_blob_set_mismatch:agent-session-records:session-lost') + expect(() => store.commitStagedOrcadMigrationCatalog(input, { expectedBlobs: [] })).toThrow( + `orcad_migration_blob_set_mismatch:agent-session-records:${blob.key}` + ) + expect(store.getOrcadMigrationCatalogState(input).state).toBe('staged') + expect(store.commitStagedOrcadMigrationCatalog(input, { expectedBlobs: [blob] }).state).toBe( + 'committed' + ) + }) + + it('removes staged blobs when the catalog is aborted', () => { + const input = manifest() + const store = createStore() + store.stageOrcadMigrationCatalog(input) + store.stageOrcadMigrationBlobChunk(chunk(input, 0, 8)) + + expect(store.abortStagedOrcadMigrationCatalog(input)).toMatchObject({ aborted: true }) + expect(() => store.stageOrcadMigrationBlobChunk(chunk(input, 8, bytes.length))).toThrow( + 'orcad_migration_blob_catalog_not_staged' + ) + store.stageOrcadMigrationCatalog(input) + expect(store.getOrcadMigrationCatalogState(input)).not.toHaveProperty('blobUploads') + }) +}) diff --git a/src/main/persistence-orcad-migration-shell-history-fixture.ts b/src/main/persistence-orcad-migration-shell-history-fixture.ts new file mode 100644 index 00000000000..404f7b245ef --- /dev/null +++ b/src/main/persistence-orcad-migration-shell-history-fixture.ts @@ -0,0 +1,98 @@ +import { createHash } from 'node:crypto' +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import type { OrcadMigrationBlobDescriptor } from '../shared/orcad-migration-blob-transfer' +import type { OrcadMigrationManifest } from '../shared/orcad-migration-manifest' +import { folderWorkspaceKey } from '../shared/workspace-scope' +import { getOrcadMigrationReceivedBlobPath } from './persistence/migrating-orcad-catalog/orcad-migration-blob-staging' +import { buildWslMoveShellHistoryBlobs } from './persistence/moving-wsl-projects/wsl-move-shell-history-blobs' +import { commitStagedOrcadMigrationCatalogDurably } from './runtime/orcad-migration-catalog-import' +import { getHistoryRoot } from './terminal-history-paths' +import { hashWorktreeId } from './terminal-history-id' +import { FOLDER_WORKSPACE } from './persistence-orcad-migration-catalog-fixture' +import { testState, type createStore } from './persistence-test-harness' + +type Store = ReturnType +export type EncodedBlob = { descriptor: OrcadMigrationBlobDescriptor; bytes: Buffer } + +export const FOLDER_KEY = folderWorkspaceKey(FOLDER_WORKSPACE.id) +export const ZSH = ': 1700000000:0;make test\n' +export const BASH = 'git status\n' + +// What the desktop sends for the moved worktree: its source ids already equal the host's here. +export async function encodedHistories( + desktopHistory: string, + files: { worktreeId: string; file: string; content: string }[] +): Promise { + for (const { worktreeId, file, content } of files) { + const directory = join(desktopHistory, hashWorktreeId(worktreeId)) + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, file), content) + } + const ids = [...new Set(files.map((entry) => entry.worktreeId))] + const blobs = await buildWslMoveShellHistoryBlobs({ + distro: 'Ubuntu', + historyRoot: desktopHistory, + worktrees: ids.map((id) => ({ sourceWorktreeId: id, destinationWorktreeId: id })) + }) + return Promise.all( + blobs.descriptors.map(async (descriptor) => ({ + descriptor, + bytes: await blobs.source.readChunk(descriptor, 0, descriptor.byteLength) + })) + ) +} + +export function rawBlob(key: string, bytes: Buffer): EncodedBlob { + return { + bytes, + descriptor: { + blobKind: 'shell-history', + key, + sha256: createHash('sha256').update(bytes).digest('hex'), + byteLength: bytes.length + } + } +} + +export function stage(store: Store, input: OrcadMigrationManifest, blobs: EncodedBlob[]): void { + store.stageOrcadMigrationCatalog(input) + for (const blob of blobs) { + store.stageOrcadMigrationBlobChunk({ + migrationId: input.migrationId, + manifestSha256: input.manifestSha256, + blob: blob.descriptor, + offset: 0, + bytesBase64: blob.bytes.toString('base64') + }) + } +} + +export function receivedPath(input: OrcadMigrationManifest, key: string): string { + return getOrcadMigrationReceivedBlobPath(join(testState.dir, 'orcad-migration-blobs'), input, { + blobKind: 'shell-history', + key + }) +} + +export function hostHistoryDirectory(worktreeId: string): string { + return join(getHistoryRoot(), hashWorktreeId(worktreeId)) +} + +export function hostHistory(worktreeId: string, file: string): string | null { + const path = join(hostHistoryDirectory(worktreeId), file) + return existsSync(path) ? readFileSync(path, 'utf8') : null +} + +/** A runtime's live-shell check on a host where no shell runs. */ +export const noLiveShells = (): boolean => false + +export async function commitDurably(store: Store, input: OrcadMigrationManifest): Promise { + const result = await commitStagedOrcadMigrationCatalogDurably({ + store, + manifest: input, + worktreeHasLiveShells: noLiveShells, + onDurableCommit: () => {} + }) + return result.state +} diff --git a/src/main/persistence-orcad-migration-shell-history-install-safety.test.ts b/src/main/persistence-orcad-migration-shell-history-install-safety.test.ts new file mode 100644 index 00000000000..afcf6c5b755 --- /dev/null +++ b/src/main/persistence-orcad-migration-shell-history-install-safety.test.ts @@ -0,0 +1,155 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + existsSync, + lstatSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync +} from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { commitStagedOrcadMigrationCatalogDurably } from './runtime/orcad-migration-catalog-import' +import { readHistoryMeta } from './terminal-history' +import { getHistoryRoot } from './terminal-history-paths' +import { DORMANT_WORKTREE_ID, manifest } from './persistence-orcad-migration-catalog-fixture' +import { + ZSH, + encodedHistories, + hostHistory, + hostHistoryDirectory, + noLiveShells, + receivedPath, + stage +} from './persistence-orcad-migration-shell-history-fixture' +import { createStore, testState } from './persistence-test-harness' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { + isEncryptionAvailable: () => true, + encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'), + decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length) + } +})) + +vi.mock('./telemetry/client', () => ({ track: vi.fn() })) + +let desktopHistory: string + +function zshHostStore(): ReturnType { + return createStore({ paneShellPath: () => '/usr/bin/zsh' }) +} + +async function stageZsh( + store: ReturnType, + content: string +): Promise<{ input: ReturnType; key: string; bytes: Buffer }> { + const input = manifest() + const [blob] = await encodedHistories(desktopHistory, [ + { worktreeId: DORMANT_WORKTREE_ID, file: 'zsh_history', content } + ]) + stage(store, input, [blob]) + return { input, key: blob.descriptor.key, bytes: blob.bytes } +} + +describe('installing moved shell history safely', () => { + beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'orca-migration-history-safety-')) + desktopHistory = mkdtempSync(join(tmpdir(), 'orca-desktop-history-')) + }) + + afterEach(() => { + rmSync(testState.dir, { recursive: true, force: true }) + rmSync(desktopHistory, { recursive: true, force: true }) + rmSync(getHistoryRoot(), { recursive: true, force: true }) + }) + + it('waits while a shell runs in the worktree and installs once the last one exits', async () => { + const store = zshHostStore() + const { input, key } = await stageZsh(store, ZSH) + let live = true + const worktreeHasLiveShells = (worktreeId: string): boolean => + live && worktreeId === DORMANT_WORKTREE_ID + + const result = await commitStagedOrcadMigrationCatalogDurably({ + store, + manifest: input, + worktreeHasLiveShells, + onDurableCommit: () => {} + }) + + expect(result.state).toBe('committed') + expect(existsSync(hostHistoryDirectory(DORMANT_WORKTREE_ID))).toBe(false) + expect(existsSync(receivedPath(input, key))).toBe(true) + + // An exit in another worktree changes nothing. + store.installDeferredOrcadMigrationShellHistory('repo-other::/srv/other', { + worktreeHasLiveShells + }) + expect(existsSync(receivedPath(input, key))).toBe(true) + + live = false + store.installDeferredOrcadMigrationShellHistory(DORMANT_WORKTREE_ID, { worktreeHasLiveShells }) + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBe(ZSH) + expect(existsSync(receivedPath(input, key))).toBe(false) + }) + + it('records the installed blob, so a retry after a shell trimmed the file never re-prepends', async () => { + const store = zshHostStore() + const { input, key, bytes } = await stageZsh(store, ZSH) + store.commitStagedOrcadMigrationCatalog(input) + await store.flushPendingOrThrowAsync() + store.installOrcadMigrationReceivedBlobs(input, { worktreeHasLiveShells: noLiveShells }) + const directory = hostHistoryDirectory(DORMANT_WORKTREE_ID) + expect(readHistoryMeta(directory)).toMatchObject({ + worktreeId: DORMANT_WORKTREE_ID, + installedMigrationBlobs: [{ shell: 'zsh' }] + }) + + // HISTSIZE trimmed the file, then a crash left the received blob behind. + writeFileSync(join(directory, 'zsh_history'), ': 9:0;newest\n') + writeFileSync(receivedPath(input, key), bytes) + // A pane shell that changed since must not install it into the other file either. + createStore({ paneShellPath: () => '/bin/bash' }).installPendingOrcadMigrationReceivedBlobs({ + worktreeHasLiveShells: noLiveShells + }) + + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBe(': 9:0;newest\n') + expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBeNull() + expect(existsSync(receivedPath(input, key))).toBe(false) + }) + + it('ends the moved history with a newline before it lands', async () => { + const store = zshHostStore() + const { input } = await stageZsh(store, ': 1:0;no newline') + store.commitStagedOrcadMigrationCatalog(input) + await store.flushPendingOrThrowAsync() + store.installOrcadMigrationReceivedBlobs(input, { worktreeHasLiveShells: noLiveShells }) + + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBe(': 1:0;no newline\n') + }) + + it.skipIf(process.platform === 'win32')( + 'writes through a symlinked history file and keeps the link', + async () => { + const store = zshHostStore() + const { input } = await stageZsh(store, ZSH) + const target = join(testState.dir, 'dotfiles-zsh_history') + writeFileSync(target, ': 2:0;host\n') + const directory = hostHistoryDirectory(DORMANT_WORKTREE_ID) + mkdirSync(directory, { recursive: true }) + const link = join(directory, 'zsh_history') + symlinkSync(target, link) + + store.commitStagedOrcadMigrationCatalog(input) + await store.flushPendingOrThrowAsync() + store.installOrcadMigrationReceivedBlobs(input, { worktreeHasLiveShells: noLiveShells }) + + expect(lstatSync(link).isSymbolicLink()).toBe(true) + expect(readFileSync(target, 'utf8')).toBe(`${ZSH}: 2:0;host\n`) + } + ) +}) diff --git a/src/main/persistence-orcad-migration-shell-history-login-shell.test.ts b/src/main/persistence-orcad-migration-shell-history-login-shell.test.ts new file mode 100644 index 00000000000..e570dd691d1 --- /dev/null +++ b/src/main/persistence-orcad-migration-shell-history-login-shell.test.ts @@ -0,0 +1,159 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type * as NodeFs from 'node:fs' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { injectHistoryEnv } from './terminal-history' +import { getHistoryRoot } from './terminal-history-paths' +import { DORMANT_WORKTREE_ID, manifest } from './persistence-orcad-migration-catalog-fixture' +import { + BASH, + ZSH, + commitDurably, + encodedHistories, + hostHistory, + hostHistoryDirectory, + stage +} from './persistence-orcad-migration-shell-history-fixture' +import { createStore, testState } from './persistence-test-harness' + +const { missingShells, presentShells } = vi.hoisted(() => ({ + missingShells: new Set(), + presentShells: new Set() +})) + +// Lets cases hide or provide shells for the real pane resolution; every other path is the real fs. +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + existsSync: (path: Parameters[0]) => + presentShells.has(String(path)) || + (!missingShells.has(String(path)) && actual.existsSync(path)), + accessSync: (...args: Parameters) => { + if (!presentShells.has(String(args[0]))) { + actual.accessSync(...args) + } + } + } +}) + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { + isEncryptionAvailable: () => true, + encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'), + decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length) + } +})) + +vi.mock('./telemetry/client', () => ({ track: vi.fn() })) + +let desktopHistory: string + +// The WSL bridge's lone `bash_history`, written by whatever the distro's login shell is. +async function moveLoneBridgeHistory( + content: string, + paneShellPath?: () => string, + terminalDefaultShell?: string +): Promise { + const input = manifest() + const store = createStore(paneShellPath ? { paneShellPath } : {}) + if (terminalDefaultShell) { + store.updateSettings({ terminalDefaultShell }) + } + stage( + store, + input, + await encodedHistories(desktopHistory, [ + { worktreeId: DORMANT_WORKTREE_ID, file: 'bash_history', content } + ]) + ) + expect(await commitDurably(store, input)).toBe('committed') +} + +describe('moved WSL history lands under the shell a host pane runs', () => { + beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'orca-migration-login-shell-')) + desktopHistory = mkdtempSync(join(tmpdir(), 'orca-desktop-history-')) + }) + + afterEach(() => { + rmSync(testState.dir, { recursive: true, force: true }) + rmSync(desktopHistory, { recursive: true, force: true }) + rmSync(getHistoryRoot(), { recursive: true, force: true }) + missingShells.clear() + presentShells.clear() + vi.unstubAllEnvs() + }) + + it('a zsh pane gets zsh_history, the HISTFILE a new zsh pane reads', async () => { + await moveLoneBridgeHistory(ZSH, () => '/usr/bin/zsh') + + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBe(ZSH) + expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBeNull() + const env: Record = {} + const injected = injectHistoryEnv(env, DORMANT_WORKTREE_ID, '/usr/bin/zsh', '/srv/x') + expect(injected.histFile).toBe(join(hostHistoryDirectory(DORMANT_WORKTREE_ID), 'zsh_history')) + }) + + it('a bash pane keeps bash_history', async () => { + await moveLoneBridgeHistory(BASH, () => '/bin/bash') + + expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBe(BASH) + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBeNull() + }) + + it.each(['/usr/bin/fish', '/usr/local/bin/nu', ''])( + 'any other pane shell (%j) keeps bash_history', + async (shell) => { + await moveLoneBridgeHistory(BASH, () => shell) + + expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBe(BASH) + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBeNull() + } + ) + + it.skipIf(process.platform === 'win32')( + 'a configured zsh that is not executable falls back as the spawn does, to bash', + async () => { + // Real pane resolution: SHELL names zsh, but no zsh exists, so the spawn falls back to bash. + vi.stubEnv('SHELL', '/usr/bin/zsh') + missingShells.add('/usr/bin/zsh') + missingShells.add('/bin/zsh') + await moveLoneBridgeHistory(BASH) + + expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBe(BASH) + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBeNull() + } + ) + + it.skipIf(process.platform === 'win32')( + "the host's default-shell setting wins over SHELL, as it does for a new pane", + async () => { + vi.stubEnv('SHELL', '/bin/bash') + presentShells.add('/usr/bin/zsh') + await moveLoneBridgeHistory(ZSH, undefined, '/usr/bin/zsh') + + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBe(ZSH) + expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBeNull() + } + ) + + it('a worktree that sent both files keeps their own names', async () => { + const input = manifest() + const store = createStore({ paneShellPath: () => '/usr/bin/zsh' }) + stage( + store, + input, + await encodedHistories(desktopHistory, [ + { worktreeId: DORMANT_WORKTREE_ID, file: 'zsh_history', content: ZSH }, + { worktreeId: DORMANT_WORKTREE_ID, file: 'bash_history', content: BASH } + ]) + ) + expect(await commitDurably(store, input)).toBe('committed') + + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBe(ZSH) + expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBe(BASH) + }) +}) diff --git a/src/main/persistence-orcad-migration-shell-history.test.ts b/src/main/persistence-orcad-migration-shell-history.test.ts new file mode 100644 index 00000000000..1bfa3846a93 --- /dev/null +++ b/src/main/persistence-orcad-migration-shell-history.test.ts @@ -0,0 +1,238 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync, existsSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { encodeOrcadMigrationShellHistoryHeader } from './persistence/migrating-orcad-catalog/orcad-migration-shell-history-format' +import { getOrcadMigrationCatalogState } from './runtime/orcad-migration-catalog-import' +import { injectHistoryEnv, readHistoryMeta } from './terminal-history' +import { getHistoryRoot } from './terminal-history-paths' +import { hashWorktreeId } from './terminal-history-id' +import { DORMANT_WORKTREE_ID, manifest } from './persistence-orcad-migration-catalog-fixture' +import { + BASH, + FOLDER_KEY, + ZSH, + commitDurably, + encodedHistories as encodeFrom, + hostHistory, + hostHistoryDirectory, + noLiveShells, + rawBlob, + receivedPath, + stage, + type EncodedBlob +} from './persistence-orcad-migration-shell-history-fixture' +import { createStore, testState } from './persistence-test-harness' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { + isEncryptionAvailable: () => true, + encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'), + decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length) + } +})) + +vi.mock('./telemetry/client', () => ({ track: vi.fn() })) + +let desktopHistory: string + +// A lone WSL bash history follows the host pane's shell; these cases pin it to bash. +function bashHostStore(): ReturnType { + return createStore({ paneShellPath: () => '/bin/bash' }) +} + +function encodedHistories( + files: { worktreeId: string; file: string; content: string }[] +): Promise { + return encodeFrom(desktopHistory, files) +} + +describe('orcad migration shell history through the store', () => { + beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'orca-migration-shell-history-')) + desktopHistory = mkdtempSync(join(tmpdir(), 'orca-desktop-history-')) + }) + + afterEach(() => { + rmSync(testState.dir, { recursive: true, force: true }) + rmSync(desktopHistory, { recursive: true, force: true }) + rmSync(getHistoryRoot(), { recursive: true, force: true }) + }) + + it('installs each history and its meta.json where a new pane in that worktree reads it', async () => { + const input = manifest() + const store = bashHostStore() + const blobs = await encodedHistories([ + { worktreeId: DORMANT_WORKTREE_ID, file: 'zsh_history', content: ZSH }, + { worktreeId: DORMANT_WORKTREE_ID, file: 'bash_history', content: BASH }, + { worktreeId: FOLDER_KEY, file: 'bash_history', content: 'ls\n' } + ]) + stage(store, input, blobs) + + expect(await commitDurably(store, input)).toBe('committed') + + for (const blob of blobs) { + expect(existsSync(receivedPath(input, blob.descriptor.key))).toBe(false) + } + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBe(ZSH) + expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBe(BASH) + expect(hostHistory(FOLDER_KEY, 'bash_history')).toBe('ls\n') + const directory = hostHistoryDirectory(DORMANT_WORKTREE_ID) + expect(readHistoryMeta(directory)?.worktreeId).toBe(DORMANT_WORKTREE_ID) + if (process.platform !== 'win32') { + expect(statSync(directory).mode & 0o777).toBe(0o700) + expect(statSync(join(directory, 'zsh_history')).mode & 0o777).toBe(0o600) + } + + // The HISTFILE a zsh and a bash pane in the moved worktree get on this host. + for (const [shell, file] of [ + ['/bin/zsh', 'zsh_history'], + ['/bin/bash', 'bash_history'] + ]) { + const env: Record = {} + const injected = injectHistoryEnv(env, DORMANT_WORKTREE_ID, shell, '/srv/repo-1-worktree') + expect(injected.histFile).toBe(join(directory, file)) + } + expect(readHistoryMeta(directory)?.worktreeId).toBe(DORMANT_WORKTREE_ID) + }) + + it('refuses before commit a history whose worktree the catalog does not move', async () => { + const input = manifest() + const store = bashHostStore() + stage( + store, + input, + await encodedHistories([ + { worktreeId: 'repo-other::/srv/other', file: 'zsh_history', content: ZSH } + ]) + ) + + expect(() => store.commitStagedOrcadMigrationCatalog(input)).toThrow( + 'orcad_migration_blob_content_invalid:shell-history:orcad_migration_shell_history_worktree_not_migrated' + ) + expect(store.getOrcadMigrationCatalogState(input).state).toBe('staged') + expect(existsSync(getHistoryRoot())).toBe(false) + }) + + it('refuses before commit a header that does not match its key, an empty history, and fish', () => { + const header = encodeOrcadMigrationShellHistoryHeader(DORMANT_WORKTREE_ID) + const hash = hashWorktreeId(DORMANT_WORKTREE_ID) + const cases: [EncodedBlob, string][] = [ + [ + rawBlob(`${hashWorktreeId(FOLDER_KEY)}-zsh`, Buffer.concat([header, Buffer.from(ZSH)])), + 'orcad_migration_shell_history_key_mismatch' + ], + [rawBlob(`${hash}-zsh`, header), 'orcad_migration_shell_history_empty'], + [ + rawBlob(`${hash}-fish`, Buffer.concat([header, Buffer.from('- cmd: ls\n')])), + 'orcad_migration_shell_history_shell_unsupported' + ], + [ + rawBlob(`${hash}-bash`, Buffer.from('no header\n')), + 'orcad_migration_shell_history_header_invalid' + ] + ] + for (const [blob, reason] of cases) { + const input = manifest({ migrationId: `migration-${blob.descriptor.key}` }) + const store = bashHostStore() + stage(store, input, [blob]) + expect(() => store.commitStagedOrcadMigrationCatalog(input)).toThrow( + `orcad_migration_blob_content_invalid:shell-history:${reason}` + ) + } + }) + + it('keeps existing host history after the moved lines, and is idempotent on a retry', async () => { + const input = manifest() + const store = bashHostStore() + const blobs = await encodedHistories([ + { worktreeId: DORMANT_WORKTREE_ID, file: 'zsh_history', content: ': 1:0;moved' } + ]) + stage(store, input, blobs) + const directory = hostHistoryDirectory(DORMANT_WORKTREE_ID) + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, 'zsh_history'), ': 2:0;host\n') + writeFileSync( + join(directory, 'meta.json'), + JSON.stringify({ worktreeId: DORMANT_WORKTREE_ID, createdAt: 'then' }) + ) + + expect(await commitDurably(store, input)).toBe('committed') + const merged = ': 1:0;moved\n: 2:0;host\n' + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBe(merged) + expect(readHistoryMeta(directory)?.createdAt).toBe('then') + + // A crash after the install but before the received blob was deleted leaves it in place. + const [blob] = blobs + writeFileSync(receivedPath(input, blob.descriptor.key), blob.bytes) + expect(await commitDurably(bashHostStore(), input)).toBe('committed') + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBe(merged) + expect(existsSync(receivedPath(input, blob.descriptor.key))).toBe(false) + }) + + it('retries a failed install on a catalog-state query, not at store load', async () => { + const input = manifest() + const store = bashHostStore() + const blobs = await encodedHistories([ + { worktreeId: DORMANT_WORKTREE_ID, file: 'zsh_history', content: ZSH }, + { worktreeId: FOLDER_KEY, file: 'bash_history', content: BASH } + ]) + stage(store, input, blobs) + // A file where the history directory belongs makes that one install fail. + mkdirSync(getHistoryRoot(), { recursive: true }) + writeFileSync(hostHistoryDirectory(DORMANT_WORKTREE_ID), '') + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + + expect(await commitDurably(store, input)).toBe('committed') + expect(error).toHaveBeenCalledWith( + '[orcad-migration] installing received blobs failed:', + expect.any(Error) + ) + // The other history still installed. + expect(hostHistory(FOLDER_KEY, 'bash_history')).toBe(BASH) + const pendingKey = `${hashWorktreeId(DORMANT_WORKTREE_ID)}-zsh` + expect(existsSync(receivedPath(input, pendingKey))).toBe(true) + + bashHostStore() + expect(existsSync(receivedPath(input, pendingKey))).toBe(true) + error.mockRestore() + + rmSync(hostHistoryDirectory(DORMANT_WORKTREE_ID)) + // Store load cannot see live shells, so history waits for a runtime-driven retry. + bashHostStore() + expect(existsSync(receivedPath(input, pendingKey))).toBe(true) + expect( + getOrcadMigrationCatalogState({ + store, + manifest: input, + worktreeHasLiveShells: noLiveShells + }).state + ).toBe('committed') + expect(existsSync(receivedPath(input, pendingKey))).toBe(false) + expect(hostHistory(DORMANT_WORKTREE_ID, 'zsh_history')).toBe(ZSH) + }) + + it('leaves history to the post-attach pass at store load, which still waits on live shells', async () => { + const input = manifest() + const store = bashHostStore() + const blobs = await encodedHistories([ + { worktreeId: DORMANT_WORKTREE_ID, file: 'bash_history', content: BASH } + ]) + stage(store, input, blobs) + store.commitStagedOrcadMigrationCatalog(input) + await store.flushPendingOrThrowAsync() + expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBeNull() + + const reloaded = bashHostStore() + expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBeNull() + expect(existsSync(receivedPath(input, blobs[0].descriptor.key))).toBe(true) + + reloaded.installPendingOrcadMigrationReceivedBlobs({ worktreeHasLiveShells: () => true }) + expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBeNull() + + reloaded.installPendingOrcadMigrationReceivedBlobs({ worktreeHasLiveShells: noLiveShells }) + expect(hostHistory(DORMANT_WORKTREE_ID, 'bash_history')).toBe(BASH) + expect(existsSync(receivedPath(input, blobs[0].descriptor.key))).toBe(false) + }) +}) diff --git a/src/main/persistence-orcad-migration-terminal-restore-hints.test.ts b/src/main/persistence-orcad-migration-terminal-restore-hints.test.ts new file mode 100644 index 00000000000..a4e6a8795b9 --- /dev/null +++ b/src/main/persistence-orcad-migration-terminal-restore-hints.test.ts @@ -0,0 +1,282 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createHash } from 'node:crypto' +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import type { OrcadMigrationBlobDescriptor } from '../shared/orcad-migration-blob-transfer' +import type { OrcadMigrationManifest } from '../shared/orcad-migration-manifest' +import { getOrcadMigrationReceivedBlobPath } from './persistence/migrating-orcad-catalog/orcad-migration-blob-staging' +import { buildWslMoveRestoreHintsBlob } from './persistence/moving-wsl-projects/wsl-move-restore-hints-blob' +import { + commitStagedOrcadMigrationCatalogDurably, + getOrcadMigrationCatalogState +} from './runtime/orcad-migration-catalog-import' +import { folderWorkspaceKey } from '../shared/workspace-scope' +import { + DORMANT_LEAF_ID, + DORMANT_WORKTREE_ID, + FOLDER_WORKSPACE, + REPOSITORY, + dormantState, + manifest as baseManifest +} from './persistence-orcad-migration-catalog-fixture' +import { createStore, testState } from './persistence-test-harness' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { + isEncryptionAvailable: () => true, + encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'), + decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length) + } +})) + +vi.mock('./telemetry/client', () => ({ track: vi.fn() })) + +const FOLDER_LEAF_ID = '22222222-2222-4222-8222-222222222222' +const PANE = `tab-dormant:${DORMANT_LEAF_ID}` +const OTHER_PANE = `tab-folder:${FOLDER_LEAF_ID}` +const ABSENT_PANE = 'tab-absent:33333333-3333-4333-8333-333333333333' +const FOLDER_KEY = folderWorkspaceKey(FOLDER_WORKSPACE.id) + +type Store = ReturnType + +// The moved session owns PANE in the dormant worktree and OTHER_PANE in the folder workspace. +function manifest(): OrcadMigrationManifest { + const dormant = dormantState() + const session = dormant.workspaceSession! + dormant.workspaceSession = { + ...session, + tabsByWorktree: { + ...session.tabsByWorktree, + [FOLDER_KEY]: [ + { + id: 'tab-folder', + ptyId: null, + worktreeId: FOLDER_KEY, + title: 'Folder terminal', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 8 + } + ] + }, + terminalLayoutsByTabId: { + ...session.terminalLayoutsByTabId, + 'tab-folder': { + root: { type: 'leaf', leafId: FOLDER_LEAF_ID }, + activeLeafId: FOLDER_LEAF_ID, + expandedLeafId: null + } + } + } + const base = baseManifest() + return baseManifest({ payload: { ...base.payload, dormantState: dormant } }) +} + +function peek(store: Store, paneKey: string, worktreeId: string): string | null { + return store.peekOrcadMigrationTerminalRestoreHint(paneKey, worktreeId) +} + +function encodedHints(): { + descriptor: OrcadMigrationBlobDescriptor + bytes: Buffer +} { + const blob = buildWslMoveRestoreHintsBlob([ + { paneKey: PANE, cwd: '/home/me/src' }, + { paneKey: OTHER_PANE, cwd: '/srv/app' }, + { paneKey: ABSENT_PANE, cwd: '/srv/gone' } + ]) + if (!blob) { + throw new Error('expected a hints blob') + } + return blob +} + +function rawBlob(bytes: Buffer): { + descriptor: OrcadMigrationBlobDescriptor + bytes: Buffer +} { + return { + bytes, + descriptor: { + blobKind: 'terminal-restore-hints', + key: 'migration', + sha256: createHash('sha256').update(bytes).digest('hex'), + byteLength: bytes.length + } + } +} + +function stageBlob( + store: Store, + input: OrcadMigrationManifest, + blob: { descriptor: OrcadMigrationBlobDescriptor; bytes: Buffer } +): void { + store.stageOrcadMigrationCatalog(input) + store.stageOrcadMigrationBlobChunk({ + migrationId: input.migrationId, + manifestSha256: input.manifestSha256, + blob: blob.descriptor, + offset: 0, + bytesBase64: blob.bytes.toString('base64') + }) +} + +function receivedPath(input: OrcadMigrationManifest): string { + return getOrcadMigrationReceivedBlobPath(join(testState.dir, 'orcad-migration-blobs'), input, { + blobKind: 'terminal-restore-hints', + key: 'migration' + }) +} + +async function commitDurably(store: Store, input: OrcadMigrationManifest): Promise { + const result = await commitStagedOrcadMigrationCatalogDurably({ + store, + manifest: input, + onDurableCommit: () => {} + }) + return result.state +} + +describe('orcad migration terminal restore hints through the store', () => { + beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'orca-migration-restore-hints-')) + }) + + afterEach(() => { + rmSync(testState.dir, { recursive: true, force: true }) + }) + + it('installs hints scoped to their worktree after the durable commit, and survives a restart', async () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, encodedHints()) + + expect(await commitDurably(store, input)).toBe('committed') + expect(existsSync(receivedPath(input))).toBe(false) + + const restarted = createStore() + expect(peek(restarted, PANE, DORMANT_WORKTREE_ID)).toBe('/home/me/src') + expect(peek(restarted, PANE, FOLDER_KEY)).toBeNull() + expect(peek(restarted, OTHER_PANE, FOLDER_KEY)).toBe('/srv/app') + // A pane the moved session does not hold has no worktree to scope it to. + expect(peek(restarted, ABSENT_PANE, DORMANT_WORKTREE_ID)).toBeNull() + restarted.consumeOrcadMigrationTerminalRestoreHint(PANE) + expect(peek(createStore(), PANE, DORMANT_WORKTREE_ID)).toBeNull() + expect(peek(createStore(), OTHER_PANE, FOLDER_KEY)).toBe('/srv/app') + }) + + it('is idempotent across commit retries and never revives a consumed hint', async () => { + const input = manifest() + const store = createStore() + const blob = encodedHints() + stageBlob(store, input, blob) + store.commitStagedOrcadMigrationCatalog(input) + await store.flushPendingOrThrowAsync() + // A crash after install but before the blob was deleted leaves it in place. + store.installOrcadMigrationReceivedBlobs(input) + expect(peek(store, PANE, DORMANT_WORKTREE_ID)).toBe('/home/me/src') + store.consumeOrcadMigrationTerminalRestoreHint(PANE) + writeFileSync(receivedPath(input), blob.bytes) + + expect(await commitDurably(createStore(), input)).toBe('committed') + expect(existsSync(receivedPath(input))).toBe(false) + const restarted = createStore() + expect(peek(restarted, PANE, DORMANT_WORKTREE_ID)).toBeNull() + expect(peek(restarted, OTHER_PANE, FOLDER_KEY)).toBe('/srv/app') + }) + + it('retries a failed install on the next store load', async () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, encodedHints()) + const installed = join(testState.dir, 'orcad-migration-blobs', 'installed') + // A file where the hint store's directory belongs makes the install's write fail. + writeFileSync(installed, '') + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + + expect(await commitDurably(store, input)).toBe('committed') + expect(error).toHaveBeenCalledWith( + '[orcad-migration] installing received blobs failed:', + expect.any(Error) + ) + expect(existsSync(receivedPath(input))).toBe(true) + error.mockRestore() + + rmSync(installed) + mkdirSync(installed) + const restarted = createStore() + expect(existsSync(receivedPath(input))).toBe(false) + expect(peek(restarted, PANE, DORMANT_WORKTREE_ID)).toBe('/home/me/src') + }) + + it('retries a failed install when the committed catalog state is queried', async () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, encodedHints()) + const installed = join(testState.dir, 'orcad-migration-blobs', 'installed') + writeFileSync(installed, '') + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + expect(await commitDurably(store, input)).toBe('committed') + error.mockRestore() + + rmSync(installed) + expect(getOrcadMigrationCatalogState({ store, manifest: input }).state).toBe('committed') + expect(existsSync(receivedPath(input))).toBe(false) + expect(peek(store, PANE, DORMANT_WORKTREE_ID)).toBe('/home/me/src') + }) + + it("drops a removed project's hints and keeps the others", async () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, encodedHints()) + expect(await commitDurably(store, input)).toBe('committed') + + store.removeProjectForHost(REPOSITORY.id, 'local') + expect(peek(store, PANE, DORMANT_WORKTREE_ID)).toBeNull() + expect(peek(store, OTHER_PANE, FOLDER_KEY)).toBe('/srv/app') + + expect(store.removeFolderWorkspace(FOLDER_WORKSPACE.id)).toBe(true) + expect(peek(createStore(), OTHER_PANE, FOLDER_KEY)).toBeNull() + }) + + it('installs nothing for a catalog that is only staged', () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, encodedHints()) + store.installOrcadMigrationReceivedBlobs(input) + expect(peek(store, PANE, DORMANT_WORKTREE_ID)).toBeNull() + }) + + it('refuses to commit a digest-exact blob that is not a hints document', () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, rawBlob(Buffer.from('{"version":1,"hints":[{"paneKey":"x"}]}'))) + + expect(() => store.commitStagedOrcadMigrationCatalog(input)).toThrow( + /^orcad_migration_blob_content_invalid:terminal-restore-hints:/ + ) + expect(store.getOrcadMigrationCatalogState(input).state).toBe('staged') + }) + + it('drops a received file damaged after commit without failing the committed catalog', async () => { + const input = manifest() + const store = createStore() + stageBlob(store, input, encodedHints()) + store.commitStagedOrcadMigrationCatalog(input) + await store.flushPendingOrThrowAsync() + writeFileSync(receivedPath(input), 'not json') + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + + expect(await commitDurably(createStore(), input)).toBe('committed') + expect(error).toHaveBeenCalledWith( + expect.stringContaining('dropped unreadable terminal restore hints'), + expect.any(Error) + ) + expect(existsSync(receivedPath(input))).toBe(false) + expect(peek(createStore(), PANE, DORMANT_WORKTREE_ID)).toBeNull() + error.mockRestore() + }) +}) diff --git a/src/main/persistence-test-harness.ts b/src/main/persistence-test-harness.ts index ca2ccaacab5..5a9d54a8348 100644 --- a/src/main/persistence-test-harness.ts +++ b/src/main/persistence-test-harness.ts @@ -86,10 +86,10 @@ export function readPersistedStateJson(path = dataFile(), profileId = 'persisten } /** Create a profile store without rebuilding its large module graph inside each test timeout. */ -export function createStore(): Store { +export function createStore(options: Pick = {}): Store { installFakeAppEnvironment({ getPath: () => testState.dir }) initDataPath() - return createSqliteTestStore(Store, { dataFile: dataFile() }) + return createSqliteTestStore(Store, { ...options, dataFile: dataFile() }) } /** Leases now arrive only from a relay-era profile, so tests seed them on disk before load. */ diff --git a/src/main/persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding.ts b/src/main/persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding.ts new file mode 100644 index 00000000000..8e45dcdb77f --- /dev/null +++ b/src/main/persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding.ts @@ -0,0 +1,23 @@ +/** + * Writes a move's record through every phase up to its own, as the journal requires, starting + * after the phase already on disk so a test can also advance an existing move. + */ +import { + WSL_MOVE_CUTOVER_PHASES, + type WslMoveCutover +} from '../../../../shared/wsl-move/wsl-move-cutover' +import { + listWslMoveCutovers, + writeWslMoveCutover +} from '../../moving-wsl-projects/wsl-move-cutover-journal' + +export function seedWslMoveCutover(userDataPath: string, cutover: WslMoveCutover): void { + const existing = listWslMoveCutovers(userDataPath).find( + (entry) => entry.migrationId === cutover.migrationId + ) + const from = existing ? WSL_MOVE_CUTOVER_PHASES.indexOf(existing.phase) + 1 : 0 + const to = WSL_MOVE_CUTOVER_PHASES.indexOf(cutover.phase) + for (const phase of WSL_MOVE_CUTOVER_PHASES.slice(from, to + 1)) { + writeWslMoveCutover(userDataPath, { ...cutover, phase }) + } +} diff --git a/src/main/persistence/loading-store/project-collection-operations.ts b/src/main/persistence/loading-store/project-collection-operations.ts index 08d6498a9c4..1ae4ec2d15e 100644 --- a/src/main/persistence/loading-store/project-collection-operations.ts +++ b/src/main/persistence/loading-store/project-collection-operations.ts @@ -29,6 +29,8 @@ import { import { scheduleSave } from './write-scheduling' import { removeWorkspaceLineageForFolderParent } from './metadata-lineage-operations' import { bumpLocalWorktreeScanGeneration } from '../../local-worktree-scan-generation' +import { dropOrcadTerminalRestoreHintsForWorktrees } from '../migrating-orcad-catalog/orcad-terminal-restore-hint-store' +import { folderWorkspaceKey } from '../../../shared/workspace-scope' type ProjectCollectionOperationsRuntime = Pick< StoreRuntimeState, @@ -37,6 +39,7 @@ type ProjectCollectionOperationsRuntime = Pick< | 'projectGroupOperations' | 'projectHostOperations' | 'state' + | 'orcadTerminalRestoreHints' > const projectCollectionOperationsContext = Symbol('ProjectCollectionOperations') @@ -56,7 +59,12 @@ export class ProjectCollectionOperations { scheduling: WriteSchedulingOperations, metadata: MetadataLineageOperations ) { - this[projectCollectionOperationsContext] = { runtime, repos, scheduling, metadata } + this[projectCollectionOperationsContext] = { + runtime, + repos, + scheduling, + metadata + } } getRepos(): Repo[] { @@ -145,7 +153,15 @@ export class ProjectCollectionOperations { } removeFolderWorkspace(id: string): boolean { - return getFolderWorkspaceOperations(this).removeFolderWorkspace(id) + const removed = getFolderWorkspaceOperations(this).removeFolderWorkspace(id) + if (removed) { + const workspaceKey = folderWorkspaceKey(id) + dropOrcadTerminalRestoreHintsForWorktrees( + this[projectCollectionOperationsContext].runtime.orcadTerminalRestoreHints, + (worktreeId) => worktreeId === workspaceKey + ) + } + return removed } moveProjectToGroup(repoId: string, groupId: string | null, order?: number): Repo | null { diff --git a/src/main/persistence/loading-store/repo-lifecycle-operations.ts b/src/main/persistence/loading-store/repo-lifecycle-operations.ts index bb22de808c4..67f28b36b13 100644 --- a/src/main/persistence/loading-store/repo-lifecycle-operations.ts +++ b/src/main/persistence/loading-store/repo-lifecycle-operations.ts @@ -1,3 +1,4 @@ +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' import type { ProjectHostSetup, ProjectHostSetupUpdateArgs } from '../../../shared/project-types' import type { Repo } from '../../../shared/repo-types' import type { GhAccountBinding } from '../../../shared/github/account-binding' @@ -27,6 +28,19 @@ import type { StoreRuntimeState } from './store-runtime-state' import type { WriteSchedulingOperations } from './write-scheduling' import { scheduleSave } from './write-scheduling' import { pruneDeregisteredRepoUiResidue } from './repo-lifecycle-ui-residue' +import { isWslMoveProjectSessionFrozen } from '../moving-wsl-projects/wsl-move-scope-guard' +import { dropOrcadTerminalRestoreHintsForWorktrees } from '../migrating-orcad-catalog/orcad-terminal-restore-hint-store' +import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' + +// A paused WSL move keeps its local rows for a downgrade. +function removeLocalRepoSession( + session: WorkspaceSessionState, + repoId: string +): WorkspaceSessionState { + return isWslMoveProjectSessionFrozen({ repoId }) + ? session + : removeRepoFromWorkspaceSession(session, repoId) +} type RepoLifecycleOperationsRuntime = Pick< StoreRuntimeState, | 'gitUsernameCache' @@ -34,6 +48,7 @@ type RepoLifecycleOperationsRuntime = Pick< | 'repoOrderOperations' | 'repoUpdateOperations' | 'state' + | 'orcadTerminalRestoreHints' > const repoLifecycleOperationsContext = Symbol('RepoLifecycleOperations') @@ -77,17 +92,17 @@ export class RepoLifecycleOperations { ) if (!idStillPresent) { retireLocalWorktreeScanGeneration(id) + dropRepoRestoreHints(this, id) delete this[repoLifecycleOperationsContext].runtime.state.sparsePresetsByRepo[id] delete this[repoLifecycleOperationsContext].runtime.state.retiredWorktreeNamesByRepo?.[id] } syncProjectHostSetupCompatibilityState(this) pruneWorktreeStateForRepo(this, id, idStillPresent ? hostId : null) if (!idStillPresent) { - this[repoLifecycleOperationsContext].runtime.state.workspaceSession = - removeRepoFromWorkspaceSession( - this[repoLifecycleOperationsContext].runtime.state.workspaceSession, - id - ) + this[repoLifecycleOperationsContext].runtime.state.workspaceSession = removeLocalRepoSession( + this[repoLifecycleOperationsContext].runtime.state.workspaceSession, + id + ) this[repoLifecycleOperationsContext].runtime.state.workspaceSessionsByHostId = removeRepoFromHostWorkspaceSessions( this[repoLifecycleOperationsContext].runtime.state.workspaceSessionsByHostId, @@ -120,8 +135,9 @@ export class RepoLifecycleOperations { } for (const repoId of orphanRepoIds) { retireLocalWorktreeScanGeneration(repoId) + dropRepoRestoreHints(this, repoId) pruneWorktreeStateForRepo(this, repoId, null) - state.workspaceSession = removeRepoFromWorkspaceSession(state.workspaceSession, repoId) + state.workspaceSession = removeLocalRepoSession(state.workspaceSession, repoId) state.workspaceSessionsByHostId = removeRepoFromHostWorkspaceSessions( state.workspaceSessionsByHostId, repoId @@ -173,6 +189,13 @@ export class RepoLifecycleOperations { } } +function dropRepoRestoreHints(owner: RepoLifecycleOperations, repoId: string): void { + dropOrcadTerminalRestoreHintsForWorktrees( + owner[repoLifecycleOperationsContext].runtime.orcadTerminalRestoreHints, + (worktreeId) => getRepoIdFromWorktreeId(worktreeId) === repoId + ) +} + export function getRepoOrderOperations( owner: RepoLifecycleOperations ): RepoOrderPersistenceOperations { diff --git a/src/main/persistence/loading-store/session-host-partitions.ts b/src/main/persistence/loading-store/session-host-partitions.ts index c5a13c505ed..12c5b7bc241 100644 --- a/src/main/persistence/loading-store/session-host-partitions.ts +++ b/src/main/persistence/loading-store/session-host-partitions.ts @@ -17,6 +17,7 @@ import { preserveRuntimeAuthoredWorkspaceSessionFields } from '../runtime-author import { dropClosedTerminalTabs } from '../closed-terminal-tab-write-fence' import { findWorktreeIdForTab } from '../restoring-sessions/pane-identity-migration' import { invalidateLocalWorktreeMetadataPruneInputs } from '../../local-worktree-metadata-prune-gate' +import { isWslMoveWorkspaceSessionFrozen } from '../moving-wsl-projects/wsl-move-scope-guard' import { removeWorkspaceSessionOwner, workspaceSessionPartitionIdsForHost @@ -92,9 +93,11 @@ export class SessionHostPartitionOperations { const { [hostId]: _removed, ...remaining } = partitions runtime.state.workspaceSessionsByHostId = remaining invalidateLocalWorktreeMetadataPruneInputs() - scheduleSave(this[sessionHostPartitionOperationsContext].scheduling, [ - 'workspaceSessionsByHostId' - ]) + scheduleSave( + this[sessionHostPartitionOperationsContext].scheduling, + ['workspaceSessionsByHostId'], + hostId + ) } readTerminalScrollbackSnapshot(ref: string): string | null { @@ -144,6 +147,10 @@ export function removeWorkspaceSessionOwnerInPartition( if (!hasPersistedWorkspaceSession(owner, resolved)) { return } + // A paused WSL move keeps its local rows for a downgrade. + if (resolved === LOCAL_EXECUTION_HOST_ID && isWslMoveWorkspaceSessionFrozen(worktreeId)) { + return + } const current = owner.getWorkspaceSession(resolved) const session = removeWorkspaceSessionOwner(current, worktreeId, { advanceTerminalTopologyRevision: options.advanceTerminalTopologyRevision ?? true @@ -165,7 +172,8 @@ export function removeWorkspaceSessionOwnerInPartition( } scheduleSave( owner[sessionHostPartitionOperationsContext].scheduling, - resolved === LOCAL_EXECUTION_HOST_ID ? ['workspaceSession'] : ['workspaceSessionsByHostId'] + resolved === LOCAL_EXECUTION_HOST_ID ? ['workspaceSession'] : ['workspaceSessionsByHostId'], + resolved ) } @@ -229,9 +237,11 @@ export function setHostWorkspaceSession( ...owner[sessionHostPartitionOperationsContext].runtime.state.workspaceSessionsByHostId, [hostId]: withRequiredWorkspaceSessionMaps(orderTerminalRowsByTabGroups(pruned)) } - scheduleSave(owner[sessionHostPartitionOperationsContext].scheduling, [ - 'workspaceSessionsByHostId' - ]) + scheduleSave( + owner[sessionHostPartitionOperationsContext].scheduling, + ['workspaceSessionsByHostId'], + hostId + ) } /** diff --git a/src/main/persistence/loading-store/session-snapshot-operations.ts b/src/main/persistence/loading-store/session-snapshot-operations.ts index d92a2cf8e14..c7320cc346e 100644 --- a/src/main/persistence/loading-store/session-snapshot-operations.ts +++ b/src/main/persistence/loading-store/session-snapshot-operations.ts @@ -19,6 +19,7 @@ import { withRequiredWorkspaceSessionMaps } from './session-host-partitions' import { dropClosedTerminalTabs } from '../closed-terminal-tab-write-fence' +import { keepWslMoveFrozenSessionsForStore } from '../moving-wsl-projects/wsl-move-session-freeze' import { scheduleSave } from './write-scheduling' import { orderTerminalRowsByTabGroups } from '../../../shared/workspace-layout/terminal-row-order' @@ -60,7 +61,12 @@ export class SessionSnapshotOperations { if (runtime.durableMutationPhase === 'rollback') { this.assertSnapshotAdmission(true) // The fieldwise rollback already preserves newer edits; renderer rebasing would undo it. - this.publishSession(session, resolved) + this.publishSession( + resolved === LOCAL_EXECUTION_HOST_ID + ? keepWslMoveFrozenSessionsForStore(session, runtime.state.workspaceSession) + : session, + resolved + ) return } if (resolved === LOCAL_EXECUTION_HOST_ID) { @@ -102,6 +108,10 @@ export class SessionSnapshotOperations { if (Object.hasOwn(patch, 'unifiedTabs')) { next = dropClosedTerminalTabs(next) } + if (resolved === LOCAL_EXECUTION_HOST_ID) { + const { state } = this[sessionSnapshotOperationsContext].runtime + next = keepWslMoveFrozenSessionsForStore(next, state.workspaceSession) + } this.publishSession(next, resolved) } @@ -118,7 +128,8 @@ export class SessionSnapshotOperations { } scheduleSave( scheduling, - hostId === LOCAL_EXECUTION_HOST_ID ? ['workspaceSession'] : ['workspaceSessionsByHostId'] + hostId === LOCAL_EXECUTION_HOST_ID ? ['workspaceSession'] : ['workspaceSessionsByHostId'], + hostId ) } diff --git a/src/main/persistence/loading-store/store-domain-composition.ts b/src/main/persistence/loading-store/store-domain-composition.ts index 5dac5df23c7..fbb1bc36dce 100644 --- a/src/main/persistence/loading-store/store-domain-composition.ts +++ b/src/main/persistence/loading-store/store-domain-composition.ts @@ -24,6 +24,7 @@ import { RetiredWorktreeNamePersistence } from './retired-worktree-name-persiste import { SshLeaseRecoveryOperations } from './ssh-lease-recovery-operations' import { OrcadSourceExportPersistence } from '../migrating-orcad-catalog/orcad-source-export' import { OrcadCatalogImportPersistence } from '../migrating-orcad-catalog/orcad-catalog-import' +import { OrcadMigrationBlobInstallPersistence } from '../migrating-orcad-catalog/orcad-migration-blob-install-persistence' export type StoreDomainOperations = WriteSchedulingOperations & PrimaryStateWriteOperations & @@ -43,6 +44,7 @@ export type StoreDomainOperations = WriteSchedulingOperations & SshLeaseRecoveryOperations & OrcadSourceExportPersistence & OrcadCatalogImportPersistence & + OrcadMigrationBlobInstallPersistence & WriteFlushBarrierOperations export type StoreDomains = { @@ -70,6 +72,7 @@ export type StoreDomains = { sshLeases: SshLeaseRecoveryOperations orcadSourceExport: OrcadSourceExportPersistence orcadCatalogImports: OrcadCatalogImportPersistence + orcadMigrationBlobInstalls: OrcadMigrationBlobInstallPersistence } export const STORE_DOMAIN_OPERATION_CLASSES = [ @@ -91,6 +94,7 @@ export const STORE_DOMAIN_OPERATION_CLASSES = [ SshLeaseRecoveryOperations, OrcadSourceExportPersistence, OrcadCatalogImportPersistence, + OrcadMigrationBlobInstallPersistence, WriteFlushBarrierOperations ] as const @@ -163,6 +167,7 @@ export function createStoreDomains(runtime: StoreRuntimeState): StoreDomains { sshLeases, // Read-only: holds the runtime state and nothing that writes. orcadSourceExport: new OrcadSourceExportPersistence(runtime), - orcadCatalogImports + orcadCatalogImports, + orcadMigrationBlobInstalls: new OrcadMigrationBlobInstallPersistence(runtime) } } diff --git a/src/main/persistence/loading-store/store-runtime-state.ts b/src/main/persistence/loading-store/store-runtime-state.ts index f72bba42ffe..e701dcd7c11 100644 --- a/src/main/persistence/loading-store/store-runtime-state.ts +++ b/src/main/persistence/loading-store/store-runtime-state.ts @@ -1,6 +1,7 @@ import { removeStaleDurableWriteTempFiles } from '../../durable-file-write' import type { GlobalSettings } from '../../../shared/global-settings-types' import type { PersistedState } from '../../../shared/persisted-state-types' +import type { WorkspaceSessionWriteListener } from './workspace-session-write-listeners' import type { ActiveViewPreference } from '../../active-view-preference' import { getProfileTerminalScrollbackSnapshotRoot, @@ -8,6 +9,7 @@ import { } from '../../terminal-scrollback-snapshots' import { ProtectedSecretPersistence } from '../../protected-secret-persistence' import { getDataFile, getGithubCacheFile } from './user-data-path' +import { dirname, join } from 'node:path' import { STALE_DURABLE_WRITE_TEMP_AGE_MS } from '../tracking-repos/worktree-metadata-normalization' import type { ProjectGroupPersistenceOperations } from '../tracking-repos/project-group-operations' import type { FolderWorkspacePersistenceOperations } from '../restoring-sessions/folder-workspace-operations' @@ -21,6 +23,8 @@ import type { } from '../scheduling-automations/automation-owner-projection' import type { ProfileStatePersistenceAuthority } from './profile-state-authority' import type { AutomationRun } from '../../../shared/automations-types' +import { resolveNewPaneShellPath } from '../migrating-orcad-catalog/orcad-shell-history-installer' +import { OrcadTerminalRestoreHintStore } from '../migrating-orcad-catalog/orcad-terminal-restore-hint-store' export type DurableProfileStateMutation = { value: T @@ -33,6 +37,8 @@ export type StoreRuntimeOptions = { dataFile?: string storageAuthority?: AutomationStorageAuthority profileStateAuthority?: ProfileStatePersistenceAuthority + /** The shell a new pane on this host runs, given its default-shell setting; tests pass a fake. */ + paneShellPath?: (defaultShell: string | undefined) => string } /** Mutable coordination state shared only with this Store's private collaborators. */ @@ -41,9 +47,16 @@ export class StoreRuntimeState { readonly dataFile: string readonly storageAuthority: AutomationStorageAuthority readonly profileStateAuthority: ProfileStatePersistenceAuthority | undefined + readonly paneShellPath: (defaultShell: string | undefined) => string + /** Worktrees whose moved shell history waits for their live shells to exit. */ + readonly deferredShellHistoryWorktreeIds = new Set() automationListProjectionCache: AutomationListProjectionCache | null = null activeViewPreference!: ActiveViewPreference readonly terminalScrollbackSnapshotStorage: TerminalScrollbackSnapshotStorage + /** Staged and received migration blobs (see orcad-migration-blob-staging.ts). */ + readonly orcadMigrationBlobRoot: string + /** Shared so project removal can drop hints for panes this host no longer owns. */ + readonly orcadTerminalRestoreHints: OrcadTerminalRestoreHintStore /** Scrollback refs each in-flight migration export still reads, keyed by migration id. */ readonly retainedScrollbackRefsByMigrationId = new Map>() writeTimer: ReturnType | null = null @@ -84,7 +97,7 @@ export class StoreRuntimeState { >() uiChangeListeners = new Set<(ui: PersistedState['ui']) => void>() /** Observers of persisted workspace-session writes; see notifyWorkspaceSessionWritten. */ - readonly workspaceSessionWriteListeners = new Set<() => void>() + readonly workspaceSessionWriteListeners = new Set() projectHostOperations: ProjectHostPersistenceOperations | null = null projectGroupOperations: ProjectGroupPersistenceOperations | null = null folderWorkspaceOperations: FolderWorkspacePersistenceOperations | null = null @@ -96,6 +109,7 @@ export class StoreRuntimeState { this.dataFile = options.dataFile ?? getDataFile() this.storageAuthority = options.storageAuthority ?? 'desktop' this.profileStateAuthority = options.profileStateAuthority + this.paneShellPath = options.paneShellPath ?? resolveNewPaneShellPath this.staleProfileStateTempCleanup = this.profileStateAuthority ? removeStaleDurableWriteTempFiles(this.dataFile, { minimumAgeMs: STALE_DURABLE_WRITE_TEMP_AGE_MS @@ -111,5 +125,7 @@ export class StoreRuntimeState { snapshotRoot: profileSnapshotRoot, fallbackSnapshotRoot: legacySnapshotRoot === profileSnapshotRoot ? null : legacySnapshotRoot } + this.orcadMigrationBlobRoot = join(dirname(this.dataFile), 'orcad-migration-blobs') + this.orcadTerminalRestoreHints = new OrcadTerminalRestoreHintStore(this.orcadMigrationBlobRoot) } } diff --git a/src/main/persistence/loading-store/store.ts b/src/main/persistence/loading-store/store.ts index 980bcfd0c46..9a231075311 100644 --- a/src/main/persistence/loading-store/store.ts +++ b/src/main/persistence/loading-store/store.ts @@ -126,6 +126,9 @@ export class Store { const sweptRepoIds = this.domains.repos.sweepDeregisteredRepoResidue() // Imported snapshots cannot own the live hook server or write their source file. if (!imported) { + // A committed migration whose blob install failed or was cut short is finished here; shell + // history waits for the runtime's post-attach pass, which can see live shells. + this.domains.orcadMigrationBlobInstalls.installPendingOrcadMigrationReceivedBlobs() for (const entry of initial?.unboundPaneAliases ?? []) { agentHookServer.registerPaneKeyAlias( entry.legacyPaneKey, diff --git a/src/main/persistence/loading-store/workspace-session-snapshot-publication.ts b/src/main/persistence/loading-store/workspace-session-snapshot-publication.ts index 4fbe44b75e5..f73fa61444c 100644 --- a/src/main/persistence/loading-store/workspace-session-snapshot-publication.ts +++ b/src/main/persistence/loading-store/workspace-session-snapshot-publication.ts @@ -12,6 +12,7 @@ import { } from '../../terminal-scrollback-snapshot-async-migration' import { preserveRuntimeAuthoredWorkspaceSessionFields } from '../runtime-authored-workspace-session-fields' import { dropClosedTerminalTabs } from '../closed-terminal-tab-write-fence' +import { keepWslMoveFrozenSessionsForStore } from '../moving-wsl-projects/wsl-move-session-freeze' import { registerPersistedPaneKeyAlias } from '../restoring-sessions/pane-alias-normalization' import { normalizeWorkspaceSessionPaneIdentities, @@ -37,6 +38,7 @@ export function setLocalWorkspaceSession( ): void { const context = getSessionSnapshotOperationsContext(owner) const prior = context.runtime.state.workspaceSession + session = keepWslMoveFrozenSessionsForStore(session, prior) // Why here and not at the callers: the before-unload stage path writes the renderer's payload // straight through, so a per-caller guard leaves the quit write erasing runtime-authored rows. session = dropClosedTerminalTabs(preserveRuntimeAuthoredWorkspaceSessionFields(session, prior)) @@ -122,11 +124,12 @@ export function setLocalWorkspaceSession( remappedManualUnread.changed ) { // UI remaps include protected fields, so keep the complete serializer boundary. - scheduleSave(context.scheduling) + scheduleSave(context.scheduling, undefined, LOCAL_EXECUTION_HOST_ID) } else { scheduleSave( context.scheduling, - remappedLeases.changed ? ['workspaceSession', 'sshRemotePtyLeases'] : ['workspaceSession'] + remappedLeases.changed ? ['workspaceSession', 'sshRemotePtyLeases'] : ['workspaceSession'], + LOCAL_EXECUTION_HOST_ID ) } } diff --git a/src/main/persistence/loading-store/workspace-session-write-listeners.ts b/src/main/persistence/loading-store/workspace-session-write-listeners.ts index 88407105e7b..9f8d4880012 100644 --- a/src/main/persistence/loading-store/workspace-session-write-listeners.ts +++ b/src/main/persistence/loading-store/workspace-session-write-listeners.ts @@ -1,19 +1,27 @@ +import type { ExecutionHostId } from '../../../shared/execution-host' import type { StoreRuntimeState } from './store-runtime-state' +/** Gets the one partition a write changed, or null when any partition may have changed. */ +export type WorkspaceSessionWriteListener = (hostId: ExecutionHostId | null) => void + const WORKSPACE_SESSION_DOMAINS: readonly string[] = [ 'workspaceSession', 'workspaceSessionsByHostId' ] -/** `dirtyDomains` undefined means a full save, which may carry an in-place session edit. */ +/** + * `dirtyDomains` undefined means a full save, which may carry an in-place session edit. + * `hostId` names the only partition the write changed; without it every partition is suspect. + */ export function notifyWorkspaceSessionWritten( runtime: Pick, - dirtyDomains?: readonly string[] + dirtyDomains?: readonly string[], + hostId?: ExecutionHostId ): void { if (dirtyDomains && !dirtyDomains.some((domain) => WORKSPACE_SESSION_DOMAINS.includes(domain))) { return } for (const listener of runtime.workspaceSessionWriteListeners) { - listener() + listener(hostId ?? null) } } diff --git a/src/main/persistence/loading-store/write-scheduling.ts b/src/main/persistence/loading-store/write-scheduling.ts index edd2754ddd9..9c622b64ee0 100644 --- a/src/main/persistence/loading-store/write-scheduling.ts +++ b/src/main/persistence/loading-store/write-scheduling.ts @@ -1,7 +1,11 @@ import type { StoreRuntimeState } from './store-runtime-state' import type { PrimaryStateWriteOperations } from './primary-state-writes' import { enqueueWrite } from './primary-state-writes' -import { notifyWorkspaceSessionWritten } from './workspace-session-write-listeners' +import type { ExecutionHostId } from '../../../shared/execution-host' +import { + notifyWorkspaceSessionWritten, + type WorkspaceSessionWriteListener +} from './workspace-session-write-listeners' const SAVE_DEBOUNCE_MS = 1_000 const SAVE_MAX_WAIT_MS = 5_000 @@ -40,7 +44,7 @@ export class WriteSchedulingOperations { } /** Called after each in-memory workspace-session write; listeners must not throw or block. */ - onWorkspaceSessionWritten(listener: () => void): () => void { + onWorkspaceSessionWritten(listener: WorkspaceSessionWriteListener): () => void { const { runtime } = this[writeSchedulingOperationsContext] runtime.workspaceSessionWriteListeners.add(listener) return () => { @@ -49,12 +53,14 @@ export class WriteSchedulingOperations { } } +/** `writtenHostId` names the one session partition this save changed, if it changed only one. */ export function scheduleSave( owner: WriteSchedulingOperations, - dirtyDomains?: readonly string[] + dirtyDomains?: readonly string[], + writtenHostId?: ExecutionHostId ): void { const { runtime, writes } = owner[writeSchedulingOperationsContext] - notifyWorkspaceSessionWritten(runtime, dirtyDomains) + notifyWorkspaceSessionWritten(runtime, dirtyDomains, writtenHostId) runtime.automationListProjectionCache = null const trackedDomains = runtime.dirtyProfileStateDomains if (dirtyDomains === undefined) { diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-catalog-import.ts b/src/main/persistence/migrating-orcad-catalog/orcad-catalog-import.ts index 817e061e755..53a77cf8dfe 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-catalog-import.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-catalog-import.ts @@ -10,6 +10,11 @@ import type { OrcadMigrationSnapshotChunkRequest, OrcadMigrationSnapshotChunkResult } from '../../../shared/orcad-migration-scrollback' +import type { + OrcadMigrationBlobChunkRequest, + OrcadMigrationBlobChunkResult, + OrcadMigrationBlobDescriptor +} from '../../../shared/orcad-migration-blob-transfer' import type { RepoLifecycleOperations } from '../loading-store/repo-lifecycle-operations' import type { StoreRuntimeState } from '../loading-store/store-runtime-state' import type { WriteSchedulingOperations } from '../loading-store/write-scheduling' @@ -35,10 +40,20 @@ import { pruneOrcadMigrationSnapshotStaging, stageOrcadMigrationSnapshotChunk } from './orcad-scrollback-snapshot-transfer' +import { + abortOrcadMigrationBlobs, + assertOrcadMigrationBlobSetMatches, + assertOrcadMigrationBlobsReady, + commitOrcadMigrationBlobs, + inspectOrcadMigrationBlobUploads, + pruneOrcadMigrationBlobStaging, + stageOrcadMigrationBlobChunk +} from './orcad-migration-blob-staging' +import { assertOrcadMigrationBlobContentsValid } from './orcad-migration-blob-installers' type OrcadCatalogImportRuntime = Pick< StoreRuntimeState, - 'state' | 'terminalScrollbackSnapshotStorage' + 'state' | 'terminalScrollbackSnapshotStorage' | 'orcadMigrationBlobRoot' > const orcadCatalogImportContext = Symbol('OrcadCatalogImportPersistence') @@ -73,11 +88,12 @@ export class OrcadCatalogImportPersistence { stagedManifests(context.runtime.state), context.runtime.terminalScrollbackSnapshotStorage ) - const current = migrationCatalogState( - context.runtime.state, - manifest, - context.runtime.terminalScrollbackSnapshotStorage + pruneOrcadMigrationBlobStaging( + stagedManifests(context.runtime.state), + committedMigrations(context.runtime.state), + context.runtime.orcadMigrationBlobRoot ) + const current = migrationCatalogState(context.runtime, manifest) if (current.state === 'committed') { return current } @@ -99,7 +115,7 @@ export class OrcadCatalogImportPersistence { } ] scheduleSave(context.scheduling) - return stagedCatalogState(manifest, stagedAt, context.runtime.terminalScrollbackSnapshotStorage) + return stagedCatalogState(manifest, stagedAt, context.runtime) } stageOrcadMigrationSnapshotChunk( @@ -116,16 +132,27 @@ export class OrcadCatalogImportPersistence { }) } + stageOrcadMigrationBlobChunk( + request: OrcadMigrationBlobChunkRequest + ): OrcadMigrationBlobChunkResult { + const context = this[orcadCatalogImportContext] + return stageOrcadMigrationBlobChunk({ + stagedManifest: + stagedManifests(context.runtime.state).find( + (manifest) => manifest.migrationId === request.migrationId + ) ?? null, + root: context.runtime.orcadMigrationBlobRoot, + request + }) + } + + /** `expectedBlobs` absent (SSH, older senders) keeps the pre-blob-set commit unchanged. */ commitStagedOrcadMigrationCatalog( manifest: OrcadMigrationManifest, - options: { now?: () => Date } = {} + options: { now?: () => Date; expectedBlobs?: readonly OrcadMigrationBlobDescriptor[] } = {} ): OrcadMigrationCatalogState { const context = this[orcadCatalogImportContext] - const current = migrationCatalogState( - context.runtime.state, - manifest, - context.runtime.terminalScrollbackSnapshotStorage - ) + const current = migrationCatalogState(context.runtime, manifest) if (current.state === 'committed') { return current } @@ -134,7 +161,17 @@ export class OrcadCatalogImportPersistence { } const prepared = prepareOrcadMigrationCatalog(manifest, context.runtime.state) assertOrcadMigrationSnapshotsReady(manifest, context.runtime.terminalScrollbackSnapshotStorage) + if (options.expectedBlobs) { + assertOrcadMigrationBlobSetMatches( + manifest, + context.runtime.orcadMigrationBlobRoot, + options.expectedBlobs + ) + } + assertOrcadMigrationBlobsReady(manifest, context.runtime.orcadMigrationBlobRoot) + assertOrcadMigrationBlobContentsValid(manifest, context.runtime.orcadMigrationBlobRoot) commitOrcadMigrationSnapshots(manifest, context.runtime.terminalScrollbackSnapshotStorage) + commitOrcadMigrationBlobs(manifest, context.runtime.orcadMigrationBlobRoot) const receipt = commitPreparedCatalog(context, manifest, prepared, options.now) scheduleSave(context.scheduling) return committedCatalogState(receipt) @@ -144,11 +181,7 @@ export class OrcadCatalogImportPersistence { manifest: OrcadMigrationManifest ): OrcadMigrationCatalogAbortResult { const context = this[orcadCatalogImportContext] - const current = migrationCatalogState( - context.runtime.state, - manifest, - context.runtime.terminalScrollbackSnapshotStorage - ) + const current = migrationCatalogState(context.runtime, manifest) if (current.state === 'committed' || current.state === 'absent') { return { ...current, aborted: false } } @@ -157,17 +190,14 @@ export class OrcadCatalogImportPersistence { context.runtime.state.orcadMigrationStagedCatalogs ?? [] ).filter((entry) => entry.manifest.migrationId !== manifest.migrationId) abortOrcadMigrationSnapshots(manifest, context.runtime.terminalScrollbackSnapshotStorage) + abortOrcadMigrationBlobs(manifest, context.runtime.orcadMigrationBlobRoot) scheduleSave(context.scheduling) return { ...absentCatalogState(manifest), aborted: true } } getOrcadMigrationCatalogState(manifest: OrcadMigrationManifest): OrcadMigrationCatalogState { const context = this[orcadCatalogImportContext] - return migrationCatalogState( - context.runtime.state, - manifest, - context.runtime.terminalScrollbackSnapshotStorage - ) + return migrationCatalogState(context.runtime, manifest) } /** A migration into this server staged recently and neither committed nor aborted. */ @@ -203,10 +233,10 @@ function commitPreparedCatalog( } function migrationCatalogState( - state: StoreRuntimeState['state'], - manifest: OrcadMigrationManifest, - storage?: StoreRuntimeState['terminalScrollbackSnapshotStorage'] + runtime: OrcadCatalogImportRuntime, + manifest: OrcadMigrationManifest ): OrcadMigrationCatalogState { + const { state } = runtime const receipt = findOrcadMigrationImportReceipt(state, manifest) if (receipt) { assertCommittedReceipt(receipt, manifest) @@ -219,13 +249,22 @@ function migrationCatalogState( return absentCatalogState(manifest) } assertSameOrcadMigrationManifest(staged.manifest, manifest) - return stagedCatalogState(manifest, staged.stagedAt, storage) + return stagedCatalogState(manifest, staged.stagedAt, runtime) } function stagedManifests(state: StoreRuntimeState['state']): OrcadMigrationManifest[] { return (state.orcadMigrationStagedCatalogs ?? []).map((entry) => entry.manifest) } +function committedMigrations( + state: StoreRuntimeState['state'] +): { migrationId: string; manifestSha256: string }[] { + return [ + ...(state.orcadMigrationImportReceipts ?? []), + ...(state.orcadMigrationEvictedReceipts ?? []) + ] +} + /** * The receipt alone proves the commit: the server is live afterwards, so its rows and snapshot * files may legitimately change, and a later read must still say "committed". @@ -251,17 +290,21 @@ function absentCatalogState(manifest: OrcadMigrationManifest): OrcadMigrationCat function stagedCatalogState( manifest: OrcadMigrationManifest, stagedAt: string, - storage?: StoreRuntimeState['terminalScrollbackSnapshotStorage'] + runtime: OrcadCatalogImportRuntime ): OrcadMigrationCatalogState { - const snapshotUploads = storage - ? inspectOrcadMigrationSnapshotUploads(manifest, storage) - : undefined + const snapshotUploads = inspectOrcadMigrationSnapshotUploads( + manifest, + runtime.terminalScrollbackSnapshotStorage + ) + // Absent, not empty, when nothing is staged: the shape an older host already answers with. + const blobUploads = inspectOrcadMigrationBlobUploads(manifest, runtime.orcadMigrationBlobRoot) return { state: 'staged', migrationId: manifest.migrationId, manifestSha256: manifest.manifestSha256, stagedAt, - ...(snapshotUploads ? { snapshotUploads } : {}) + ...(snapshotUploads ? { snapshotUploads } : {}), + ...(blobUploads ? { blobUploads } : {}) } } diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-install-persistence.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-install-persistence.ts new file mode 100644 index 00000000000..56cd41d1426 --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-install-persistence.ts @@ -0,0 +1,145 @@ +import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest' +import type { PersistedState } from '../../../shared/persisted-state-types' +import { parsePaneKey } from '../../../shared/stable-pane-id' +import { getHistoryRoot } from '../../terminal-history-paths' +import type { StoreRuntimeState } from '../loading-store/store-runtime-state' +import { findOrcadMigrationImportReceipt } from './orcad-catalog-receipt-ledger' +import { installOrcadMigrationReceivedBlobs } from './orcad-migration-blob-installers' +import type { OrcadMigrationRequestIdentity } from './orcad-migration-staged-file' + +type OrcadMigrationBlobInstallRuntime = Pick< + StoreRuntimeState, + | 'state' + | 'orcadMigrationBlobRoot' + | 'orcadTerminalRestoreHints' + | 'paneShellPath' + | 'deferredShellHistoryWorktreeIds' +> + +type ClockOptions = { now?: () => Date } +type InstallOptions = ClockOptions & { + /** The runtime's live-shell check; a moved history waits while a shell runs in its worktree. */ + worktreeHasLiveShells?: (worktreeId: string) => boolean +} + +const orcadMigrationBlobInstallContext = Symbol('OrcadMigrationBlobInstallPersistence') +type OrcadMigrationBlobInstallContext = { + runtime: OrcadMigrationBlobInstallRuntime +} + +/** The destination's committed migration blobs, installed into host-local state and consumed. */ +export class OrcadMigrationBlobInstallPersistence { + readonly [orcadMigrationBlobInstallContext]: OrcadMigrationBlobInstallContext + + constructor(runtime: OrcadMigrationBlobInstallRuntime) { + this[orcadMigrationBlobInstallContext] = { runtime } + } + + /** + * Run only once the commit is durable: installing first and then losing the commit would leave + * live hints or history for a catalog this host never received. A no-op before commit and on + * repeats. + */ + installOrcadMigrationReceivedBlobs( + manifest: OrcadMigrationManifest, + options: InstallOptions = {} + ): void { + const receipt = findOrcadMigrationImportReceipt( + this[orcadMigrationBlobInstallContext].runtime.state, + manifest + ) + if (receipt?.manifestSha256 !== manifest.manifestSha256) { + return + } + installCommitted(this[orcadMigrationBlobInstallContext].runtime, manifest, options) + } + + /** Finishes installs a crash or failed write left behind; every receipt on disk is durable. */ + installPendingOrcadMigrationReceivedBlobs(options: InstallOptions = {}): void { + const { runtime } = this[orcadMigrationBlobInstallContext] + for (const receipt of [ + ...(runtime.state.orcadMigrationImportReceipts ?? []), + ...(runtime.state.orcadMigrationEvictedReceipts ?? []) + ]) { + try { + installCommitted(runtime, receipt, options) + } catch (error) { + console.error( + `[orcad-migration] installing received blobs for ${receipt.migrationId} failed:`, + error + ) + } + } + } + + /** Retries a history install that waited on this worktree's shells; a no-op for any other. */ + installDeferredOrcadMigrationShellHistory( + worktreeId: string, + options: InstallOptions = {} + ): void { + const { runtime } = this[orcadMigrationBlobInstallContext] + if (runtime.deferredShellHistoryWorktreeIds.has(worktreeId)) { + this.installPendingOrcadMigrationReceivedBlobs(options) + } + } + + /** The moved pane's last folder while its hint is installed for this worktree; never removes it. */ + peekOrcadMigrationTerminalRestoreHint( + paneKey: string, + worktreeId: string, + options: ClockOptions = {} + ): string | null { + return this[orcadMigrationBlobInstallContext].runtime.orcadTerminalRestoreHints.peek( + paneKey, + worktreeId, + clock(options) + ) + } + + /** Called once the pane spawned: whatever cwd that create used, it was the restore. */ + consumeOrcadMigrationTerminalRestoreHint(paneKey: string, options: ClockOptions = {}): void { + this[orcadMigrationBlobInstallContext].runtime.orcadTerminalRestoreHints.consume( + paneKey, + clock(options) + ) + } +} + +function installCommitted( + runtime: OrcadMigrationBlobInstallRuntime, + migration: OrcadMigrationRequestIdentity, + options: InstallOptions +): void { + installOrcadMigrationReceivedBlobs({ + migration, + root: runtime.orcadMigrationBlobRoot, + terminalRestoreHints: runtime.orcadTerminalRestoreHints, + shellHistoryRoot: getHistoryRoot(), + paneShellPath: () => runtime.paneShellPath(runtime.state.settings?.terminalDefaultShell), + worktreeHasLiveShells: options.worktreeHasLiveShells, + deferredShellHistoryWorktreeIds: runtime.deferredShellHistoryWorktreeIds, + paneWorktreeId: (paneKey) => findPaneWorktreeId(runtime.state, paneKey), + now: clock(options) + }) +} + +function clock(options: ClockOptions): number { + return (options.now ?? (() => new Date()))().getTime() +} + +// The commit merged the moved session in with its worktree ids unchanged. +function findPaneWorktreeId(state: PersistedState, paneKey: string): string | null { + const tabId = parsePaneKey(paneKey)?.tabId + if (!tabId) { + return null + } + const sessions = [state.workspaceSession, ...Object.values(state.workspaceSessionsByHostId ?? {})] + for (const session of sessions) { + for (const [worktreeId, tabs] of Object.entries(session?.tabsByWorktree ?? {})) { + if (tabs.some((tab) => tab.id === tabId)) { + return worktreeId + } + } + } + return null +} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-installers.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-installers.ts new file mode 100644 index 00000000000..494e8ff6bc5 --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-installers.ts @@ -0,0 +1,188 @@ +/** + * Per-kind handling of migration blobs on the destination: a content check before the catalog + * commits, and an installer after the commit is durable that moves the received blob into live + * state and deletes it. Kinds without an installer are not advertised, so none arrive. + */ +import { readFileSync, rmSync } from 'node:fs' +import { dirname } from 'node:path' +import type { OrcadMigrationBlobKind } from '../../../shared/orcad-migration-blob-transfer' +import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest' +import { + ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_KEY, + parseOrcadMigrationTerminalRestoreHintsText, + type OrcadMigrationTerminalRestoreHint +} from '../../../shared/orcad-migration-terminal-restore-hints' +import { syncDirectoryDurablySync } from '../../durable-file-write' +import { + getOrcadMigrationReceivedBlobPath, + stagedOrcadMigrationBlobFiles +} from './orcad-migration-blob-staging' +import { + orcadMigrationStagingKey, + type OrcadMigrationRequestIdentity +} from './orcad-migration-staged-file' +import { + assertOrcadMigrationShellHistoryValid, + installOrcadMigrationShellHistory +} from './orcad-shell-history-installer' +import type { + OrcadTerminalRestoreHintPlacement, + OrcadTerminalRestoreHintStore +} from './orcad-terminal-restore-hint-store' + +type OrcadMigrationStagedBlobFile = { + key: string + path: string + manifest: OrcadMigrationManifest +} + +type OrcadMigrationBlobContentCheck = (file: OrcadMigrationStagedBlobFile) => void + +const UTF8 = new TextDecoder('utf-8', { fatal: true }) + +const BLOB_CONTENT_CHECKS: Partial> = + { + 'terminal-restore-hints': ({ path }) => { + parseOrcadMigrationTerminalRestoreHintsText(decodeUtf8(readFileSync(path))) + }, + 'shell-history': ({ key, path, manifest }) => { + assertOrcadMigrationShellHistoryValid(key, path, manifest.payload) + } + } + +/** + * Bytes that matched their digest but not their format mean a buggy sender. Refusing here, before + * anything lands, fails the move while the source still owns everything — nothing is lost. + */ +export function assertOrcadMigrationBlobContentsValid( + manifest: OrcadMigrationManifest, + root: string +): void { + for (const { descriptor, path } of stagedOrcadMigrationBlobFiles(manifest, root)) { + const check = BLOB_CONTENT_CHECKS[descriptor.blobKind] + if (!check) { + continue + } + try { + check({ key: descriptor.key, path, manifest }) + } catch (error) { + const reason = error instanceof Error ? error.message : String(error) + throw new Error(`orcad_migration_blob_content_invalid:${descriptor.blobKind}:${reason}`) + } + } +} + +export type OrcadMigrationBlobInstall = { + migration: OrcadMigrationRequestIdentity + root: string + terminalRestoreHints: OrcadTerminalRestoreHintStore + /** This host's `getHistoryRoot()`, where its panes' HISTFILEs live. */ + shellHistoryRoot: string + /** The shell binary a new pane on this host would actually run. */ + paneShellPath: () => string + /** + * The runtime's live-shell check. Without it (store load, before a runtime attached) shell + * history waits: a shell that outlived a restart would overwrite whatever lands under it. + */ + worktreeHasLiveShells?: (worktreeId: string) => boolean + /** Worktrees whose history install waits for their live shells to exit. */ + deferredShellHistoryWorktreeIds: Set + /** The worktree that owns the pane on this host, or null when the pane is not here. */ + paneWorktreeId: (paneKey: string) => string | null + now: number +} + +/** + * Idempotent: a blob already installed is only deleted, and the hint store remembers installed + * migrations so a retry never revives a hint a pane already took. A write failure keeps the blob + * for the next retry: a commit retry, a catalog-state query, the runtime's post-attach pass, or + * (for restore hints) the next store load. + */ +export function installOrcadMigrationReceivedBlobs(install: OrcadMigrationBlobInstall): void { + let firstError: unknown = null + // One kind's failure must not hold back the others; each keeps its own blobs for the retry. + for (const installKind of [installTerminalRestoreHints, installShellHistory]) { + try { + installKind(install) + } catch (error) { + firstError ??= error + } + } + if (firstError !== null) { + throw firstError + } +} + +function installShellHistory(install: OrcadMigrationBlobInstall): void { + if (!install.worktreeHasLiveShells) { + return + } + installOrcadMigrationShellHistory({ + migration: install.migration, + root: install.root, + historyRoot: install.shellHistoryRoot, + paneShellPath: install.paneShellPath, + worktreeHasLiveShells: install.worktreeHasLiveShells, + deferredWorktreeIds: install.deferredShellHistoryWorktreeIds, + now: install.now + }) +} + +function installTerminalRestoreHints(install: OrcadMigrationBlobInstall): void { + const path = getOrcadMigrationReceivedBlobPath(install.root, install.migration, { + blobKind: 'terminal-restore-hints', + key: ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_KEY + }) + let bytes: Buffer + try { + bytes = readFileSync(path) + } catch { + return + } + const migrationKey = orcadMigrationStagingKey(install.migration) + if (!install.terminalRestoreHints.hasInstalledMigration(migrationKey)) { + try { + const document = parseOrcadMigrationTerminalRestoreHintsText(decodeUtf8(bytes)) + install.terminalRestoreHints.install( + { migrationKey, migrationId: install.migration.migrationId }, + placeHints(document.hints, install.paneWorktreeId), + install.now + ) + } catch (error) { + if (!isHintFormatError(error)) { + throw error + } + // The pre-commit check passed these bytes, so only a damaged file lands here; the catalog + // is already durable, and its panes reopen at their default folder. + console.error( + `[orcad-migration] dropped unreadable terminal restore hints for ${install.migration.migrationId}:`, + error + ) + } + } + rmSync(path, { force: true }) + syncDirectoryDurablySync(dirname(path)) +} + +// A hint whose pane is not in this host's session has no worktree to scope it to, so it is dropped. +function placeHints( + hints: readonly OrcadMigrationTerminalRestoreHint[], + paneWorktreeId: (paneKey: string) => string | null +): OrcadTerminalRestoreHintPlacement[] { + return hints.flatMap((hint) => { + const worktreeId = paneWorktreeId(hint.paneKey) + return worktreeId ? [{ ...hint, worktreeId }] : [] + }) +} + +function decodeUtf8(bytes: Buffer): string { + try { + return UTF8.decode(bytes) + } catch { + throw new Error('orcad_migration_terminal_restore_hints_not_utf8') + } +} + +function isHintFormatError(error: unknown): boolean { + return error instanceof Error && error.message.startsWith('orcad_migration_terminal_restore_') +} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.test.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.test.ts new file mode 100644 index 00000000000..f0cdb938c12 --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.test.ts @@ -0,0 +1,264 @@ +import { createHash } from 'node:crypto' +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import type { OrcadMigrationBlobDescriptor } from '../../../shared/orcad-migration-blob-transfer' +import { + ORCAD_MIGRATION_MANIFEST_VERSION, + type OrcadMigrationManifest +} from '../../../shared/orcad-migration-manifest' +import { + abortOrcadMigrationBlobs, + assertOrcadMigrationBlobSetMatches, + assertOrcadMigrationBlobsReady, + commitOrcadMigrationBlobs, + getOrcadMigrationReceivedBlobPath, + inspectOrcadMigrationBlobUploads, + pruneOrcadMigrationBlobStaging, + stageOrcadMigrationBlobChunk +} from './orcad-migration-blob-staging' + +let root: string + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'orcad-migration-blobs-')) +}) + +afterEach(() => rmSync(root, { recursive: true, force: true })) + +describe('orcad migration blob staging', () => { + it('stages, resumes from disk after a restart, and accepts only exact retries', () => { + const bytes = Buffer.from('echo one\necho two 🐋\n', 'utf8') + const descriptor = blob('shell-history', `${'a'.repeat(16)}-zsh`, bytes) + const manifest = migrationManifest('resume') + const first = bytes.subarray(0, 9) + + expect(stage(manifest, descriptor, 0, first).receivedBytes).toBe(first.length) + // A fresh read of the directory is all a restarted host has. + expect(inspectOrcadMigrationBlobUploads(manifest, root)).toEqual([ + { ...descriptor, receivedBytes: first.length } + ]) + expect(stage(manifest, descriptor, 0, first).receivedBytes).toBe(first.length) + expect(() => stage(manifest, descriptor, 0, Buffer.from('different'))).toThrow( + 'orcad_migration_blob_retry_mismatch' + ) + expect(() => stage(manifest, descriptor, first.length + 1, Buffer.from('x'))).toThrow( + 'orcad_migration_blob_offset_invalid' + ) + expect(() => stage(manifest, descriptor, first.length, bytes)).toThrow( + 'orcad_migration_blob_offset_invalid' + ) + stage(manifest, descriptor, first.length, bytes.subarray(first.length)) + + assertOrcadMigrationBlobsReady(manifest, root) + commitOrcadMigrationBlobs(manifest, root) + expect(readFileSync(getOrcadMigrationReceivedBlobPath(root, manifest, descriptor))).toEqual( + bytes + ) + expect(inspectOrcadMigrationBlobUploads(manifest, root)).toBeUndefined() + }) + + it('refuses a chunk for a migration the importer does not hold staged', () => { + const descriptor = blob('terminal-restore-hints', 'migration', Buffer.from('{}')) + const manifest = migrationManifest('unstaged') + expect(() => + stageOrcadMigrationBlobChunk({ + stagedManifest: null, + root, + request: request(manifest, descriptor, 0, Buffer.from('{}')) + }) + ).toThrow('orcad_migration_blob_catalog_not_staged') + expect(() => + stageOrcadMigrationBlobChunk({ + stagedManifest: { ...manifest, manifestSha256: 'e'.repeat(64) }, + root, + request: request(manifest, descriptor, 0, Buffer.from('{}')) + }) + ).toThrow('orcad_migration_blob_catalog_not_staged') + }) + + it('never replaces a staged key with different bytes', () => { + const manifest = migrationManifest('conflict') + const original = blob('agent-session-records', 'session-1', Buffer.from('original')) + stage(manifest, original, 0, Buffer.from('orig')) + const recaptured = blob('agent-session-records', 'session-1', Buffer.from('recaptured')) + + expect(() => stage(manifest, recaptured, 0, Buffer.from('reca'))).toThrow( + 'orcad_migration_blob_descriptor_conflict:agent-session-records' + ) + expect(inspectOrcadMigrationBlobUploads(manifest, root)).toEqual([ + { ...original, receivedBytes: 4 } + ]) + }) + + it('refuses commit while a blob is incomplete or its bytes do not match the digest', () => { + const bytes = Buffer.from('expected bytes') + const descriptor = blob('shell-history', `${'b'.repeat(16)}-bash`, bytes) + const manifest = migrationManifest('refuse') + + stage(manifest, descriptor, 0, bytes.subarray(0, 4)) + expect(() => assertOrcadMigrationBlobsReady(manifest, root)).toThrow( + `orcad_migration_blob_incomplete:shell-history:${descriptor.key}` + ) + stage(manifest, descriptor, 4, Buffer.alloc(bytes.length - 4, 0x78)) + expect(() => assertOrcadMigrationBlobsReady(manifest, root)).toThrow( + 'orcad_migration_blob_digest_mismatch' + ) + expect(existsSync(getOrcadMigrationReceivedBlobPath(root, manifest, descriptor))).toBe(false) + }) + + it('treats a matching received file as done and a different one as a conflict', () => { + const bytes = Buffer.from('hints') + const descriptor = blob('terminal-restore-hints', 'migration', bytes) + const manifest = migrationManifest('received') + stage(manifest, descriptor, 0, bytes) + const finalPath = getOrcadMigrationReceivedBlobPath(root, manifest, descriptor) + mkdirSync(dirname(finalPath), { recursive: true }) + writeFileSync(finalPath, 'other') + + expect(() => commitOrcadMigrationBlobs(manifest, root)).toThrow( + 'orcad_migration_blob_destination_conflict:terminal-restore-hints:migration' + ) + writeFileSync(finalPath, bytes) + assertOrcadMigrationBlobsReady(manifest, root) + commitOrcadMigrationBlobs(manifest, root) + expect(readFileSync(finalPath)).toEqual(bytes) + }) + + it('aborts one migration, keeps another, and prunes migrations no longer staged', () => { + const bytes = Buffer.from('staged') + const descriptor = blob('agent-session-records', 'session-2', bytes) + const first = migrationManifest('abort-first') + const second = migrationManifest('retain-second') + stage(first, descriptor, 0, bytes) + stage(second, descriptor, 0, bytes) + + abortOrcadMigrationBlobs(first, root) + expect(inspectOrcadMigrationBlobUploads(first, root)).toBeUndefined() + expect(inspectOrcadMigrationBlobUploads(second, root)?.[0]?.receivedBytes).toBe(bytes.length) + pruneOrcadMigrationBlobStaging([second], [], root) + expect(inspectOrcadMigrationBlobUploads(second, root)?.[0]?.receivedBytes).toBe(bytes.length) + pruneOrcadMigrationBlobStaging([], [], root) + expect(inspectOrcadMigrationBlobUploads(second, root)).toBeUndefined() + }) + + it('prunes received blobs whose migration is neither staged nor committed', () => { + const bytes = Buffer.from('moved') + const descriptor = blob('shell-history', `${'e'.repeat(16)}-fish`, bytes) + const leaked = migrationManifest('failed-commit') + const staged = migrationManifest('still-staged') + const committed = migrationManifest('committed') + for (const manifest of [leaked, staged, committed]) { + stage(manifest, descriptor, 0, bytes) + commitOrcadMigrationBlobs(manifest, root) + } + const receivedPath = (manifest: OrcadMigrationManifest) => + getOrcadMigrationReceivedBlobPath(root, manifest, descriptor) + + pruneOrcadMigrationBlobStaging( + [staged], + [ + { + migrationId: committed.migrationId, + manifestSha256: committed.manifestSha256 + } + ], + root + ) + expect(existsSync(receivedPath(leaked))).toBe(false) + expect(existsSync(receivedPath(staged))).toBe(true) + expect(existsSync(receivedPath(committed))).toBe(true) + }) + + it('commits only when staged plus received blobs are exactly the expected set', () => { + const historyBytes = Buffer.from('history') + const recordBytes = Buffer.from('record') + const history = blob('shell-history', `${'f'.repeat(16)}-zsh`, historyBytes) + const record = blob('agent-session-records', 'session-3', recordBytes) + const manifest = migrationManifest('expected-set') + stage(manifest, history, 0, historyBytes) + + expect(() => assertOrcadMigrationBlobSetMatches(manifest, root, [history, record])).toThrow( + 'orcad_migration_blob_set_mismatch:agent-session-records:session-3' + ) + expect(() => assertOrcadMigrationBlobSetMatches(manifest, root, [])).toThrow( + `orcad_migration_blob_set_mismatch:shell-history:${history.key}` + ) + expect(() => + assertOrcadMigrationBlobSetMatches(manifest, root, [{ ...history, sha256: 'f'.repeat(64) }]) + ).toThrow('orcad_migration_blob_set_mismatch') + stage(manifest, record, 0, recordBytes) + assertOrcadMigrationBlobSetMatches(manifest, root, [history, record]) + + // A retry after a commit that moved the blobs still sees the whole set, from received. + commitOrcadMigrationBlobs(manifest, root) + assertOrcadMigrationBlobSetMatches(manifest, root, [history, record]) + expect(() => assertOrcadMigrationBlobSetMatches(manifest, root, [history])).toThrow( + 'orcad_migration_blob_set_mismatch' + ) + }) + + it('drops blobs an earlier commit attempt moved when the catalog is aborted', () => { + const bytes = Buffer.from('history') + const descriptor = blob('shell-history', `${'c'.repeat(16)}-zsh`, bytes) + const manifest = migrationManifest('abort-after-move') + stage(manifest, descriptor, 0, bytes) + commitOrcadMigrationBlobs(manifest, root) + + abortOrcadMigrationBlobs(manifest, root) + expect(existsSync(getOrcadMigrationReceivedBlobPath(root, manifest, descriptor))).toBe(false) + }) +}) + +function blob( + blobKind: OrcadMigrationBlobDescriptor['blobKind'], + key: string, + bytes: Buffer +): OrcadMigrationBlobDescriptor { + return { + blobKind, + key, + sha256: createHash('sha256').update(bytes).digest('hex'), + byteLength: bytes.length + } +} + +function migrationManifest(migrationId: string): OrcadMigrationManifest { + return { + version: ORCAD_MIGRATION_MANIFEST_VERSION, + migrationId, + createdAt: '2026-08-30T12:00:00.000Z', + source: { sshTargetId: 'source', sshTargetGeneration: 1, targetLabel: 'Source' }, + payload: { repositories: [], projectGroups: [], folderWorkspaces: [] }, + manifestSha256: 'a'.repeat(64) + } +} + +function request( + manifest: OrcadMigrationManifest, + descriptor: OrcadMigrationBlobDescriptor, + offset: number, + bytes: Buffer +) { + return { + migrationId: manifest.migrationId, + manifestSha256: manifest.manifestSha256, + blob: descriptor, + offset, + bytesBase64: bytes.toString('base64') + } +} + +function stage( + manifest: OrcadMigrationManifest, + descriptor: OrcadMigrationBlobDescriptor, + offset: number, + bytes: Buffer +) { + return stageOrcadMigrationBlobChunk({ + stagedManifest: manifest, + root, + request: request(manifest, descriptor, offset, bytes) + }) +} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.ts new file mode 100644 index 00000000000..f76dc4ea0a4 --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-blob-staging.ts @@ -0,0 +1,330 @@ +/** + * Destination side of the migration blob channel. Blobs are not named by the signed manifest, so + * a staged file's name carries its whole descriptor (`...part` under a + * per-kind directory) and the staged set is read back from disk after a restart. At catalog commit + * every staged blob must be complete and digest-exact, then moves to the migration's received + * directory; installing received blobs into live state is each kind's installer (D1b-6b/6c). + */ +import { readdirSync, rmSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { + ORCAD_MIGRATION_BLOB_KINDS, + assertOrcadMigrationBlobSet, + decodeOrcadMigrationBlobChunk, + orcadMigrationBlobIdentity, + parseOrcadMigrationBlobDescriptor, + sameOrcadMigrationBlob, + type OrcadMigrationBlobChunkRequest, + type OrcadMigrationBlobChunkResult, + type OrcadMigrationBlobDescriptor, + type OrcadMigrationBlobKind, + type OrcadMigrationBlobUploadState +} from '../../../shared/orcad-migration-blob-transfer' +import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest' +import { syncDirectoryDurablySync } from '../../durable-file-write' +import { + appendOrcadMigrationStagedChunk, + ensureOrcadMigrationDirectory, + inspectOrcadMigrationFinalFile, + linkOrcadMigrationStagedFile, + orcadMigrationFileMatches, + orcadMigrationStagedFileSize, + orcadMigrationStagingDirectory, + orcadMigrationStagingKey, + pruneOrcadMigrationKeyedDirectories, + pruneOrcadMigrationStagingDirectories, + removeOrcadMigrationStagingDirectory, + requireStagedOrcadMigrationManifest, + type OrcadMigrationRequestIdentity +} from './orcad-migration-staged-file' + +const RECEIVED_DIRECTORY = 'received' +const STAGED_BLOB_NAME = /^([^.]+)\.([a-f0-9]{64})\.([1-9][0-9]*)\.part$/ + +type OrcadMigrationBlobName = Pick + +type ReceivedBlobPathResolver = ( + root: string, + manifest: OrcadMigrationRequestIdentity, + descriptor: OrcadMigrationBlobName +) => string + +const receivedDirectoryPath: ReceivedBlobPathResolver = (root, manifest, descriptor) => + join(receivedBlobsDirectory(root, manifest), descriptor.blobKind, descriptor.key) + +/** Where each kind lands at commit; every kind waits in the received directory for its installer. */ +const RECEIVED_BLOB_DESTINATIONS: Record = { + 'terminal-restore-hints': receivedDirectoryPath, + 'shell-history': receivedDirectoryPath, + 'agent-session-records': receivedDirectoryPath +} + +export function getOrcadMigrationReceivedBlobPath( + root: string, + manifest: OrcadMigrationRequestIdentity, + descriptor: OrcadMigrationBlobName +): string { + return RECEIVED_BLOB_DESTINATIONS[descriptor.blobKind](root, manifest, descriptor) +} + +/** Names in a committed migration's received directory for `blobKind`; the caller checks the key. */ +export function listOrcadMigrationReceivedBlobNames( + root: string, + manifest: OrcadMigrationRequestIdentity, + blobKind: OrcadMigrationBlobKind +): string[] { + return readDirectory(join(receivedBlobsDirectory(root, manifest), blobKind)) +} + +export type OrcadMigrationBlobChunkStaging = { + /** The manifest the importer holds staged for this migration, if any. */ + stagedManifest: OrcadMigrationManifest | null + root: string + request: OrcadMigrationBlobChunkRequest +} + +export function stageOrcadMigrationBlobChunk( + args: OrcadMigrationBlobChunkStaging +): OrcadMigrationBlobChunkResult { + const manifest = requireStagedOrcadMigrationManifest( + args.stagedManifest, + args.request, + 'orcad_migration_blob_catalog_not_staged' + ) + const descriptor = parseOrcadMigrationBlobDescriptor(args.request.blob) + const bytes = decodeOrcadMigrationBlobChunk(args.request.bytesBase64) + const staged = stagedBlobDescriptors(args.root, manifest) + const existing = staged.find( + (entry) => orcadMigrationBlobIdentity(entry) === orcadMigrationBlobIdentity(descriptor) + ) + if (existing && !sameOrcadMigrationBlob(existing, descriptor)) { + // Never replace bytes already staged under this key; the sender aborts and restages instead. + throw new Error(`orcad_migration_blob_descriptor_conflict:${descriptor.blobKind}`) + } + if (!existing) { + assertOrcadMigrationBlobSet([...staged, descriptor]) + } + const path = stagedBlobPath(args.root, manifest, descriptor) + ensureOrcadMigrationDirectory(dirname(path)) + const receivedBytes = appendOrcadMigrationStagedChunk({ + channel: 'blob', + path, + offset: args.request.offset, + bytes, + byteLength: descriptor.byteLength + }) + return { + migrationId: args.request.migrationId, + manifestSha256: args.request.manifestSha256, + blobKind: descriptor.blobKind, + key: descriptor.key, + receivedBytes + } +} + +export function inspectOrcadMigrationBlobUploads( + manifest: OrcadMigrationManifest, + root: string +): OrcadMigrationBlobUploadState[] | undefined { + const uploads = stagedBlobDescriptors(root, manifest).map((descriptor) => ({ + ...descriptor, + receivedBytes: + inspectReceivedBlob(root, manifest, descriptor) === 'matching' + ? descriptor.byteLength + : orcadMigrationStagedFileSize( + stagedBlobPath(root, manifest, descriptor), + descriptor.byteLength + ) + })) + return uploads.length > 0 ? uploads : undefined +} + +export function assertOrcadMigrationBlobsReady( + manifest: OrcadMigrationManifest, + root: string +): void { + for (const upload of inspectOrcadMigrationBlobUploads(manifest, root) ?? []) { + const label = `${upload.blobKind}:${upload.key}` + const finalStatus = inspectReceivedBlob(root, manifest, upload) + if (finalStatus === 'conflict') { + throw new Error(`orcad_migration_blob_destination_conflict:${label}`) + } + if (upload.receivedBytes !== upload.byteLength) { + throw new Error(`orcad_migration_blob_incomplete:${label}`) + } + if ( + finalStatus !== 'matching' && + !orcadMigrationFileMatches(stagedBlobPath(root, manifest, upload), upload) + ) { + throw new Error(`orcad_migration_blob_digest_mismatch:${label}`) + } + } +} + +/** + * Each still-staged blob's verified file; one an earlier attempt already moved was checked then. + * Only meaningful after `assertOrcadMigrationBlobsReady` in the same synchronous turn. + */ +export function stagedOrcadMigrationBlobFiles( + manifest: OrcadMigrationManifest, + root: string +): { descriptor: OrcadMigrationBlobDescriptor; path: string }[] { + return stagedBlobDescriptors(root, manifest).map((descriptor) => ({ + descriptor, + path: stagedBlobPath(root, manifest, descriptor) + })) +} + +/** + * Refuses commit unless the staged and already-received blobs are exactly the set the sender + * moved, so a blob lost on the way (or left from another capture) fails instead of vanishing. + */ +export function assertOrcadMigrationBlobSetMatches( + manifest: OrcadMigrationManifest, + root: string, + expected: readonly OrcadMigrationBlobDescriptor[] +): void { + assertOrcadMigrationBlobSet(expected) + const wanted = new Map(expected.map((entry) => [orcadMigrationBlobIdentity(entry), entry])) + const staged = stagedBlobDescriptors(root, manifest) + const stagedIds = new Set(staged.map(orcadMigrationBlobIdentity)) + for (const descriptor of staged) { + const match = wanted.get(orcadMigrationBlobIdentity(descriptor)) + if (!match || !sameOrcadMigrationBlob(match, descriptor)) { + throw blobSetMismatch(descriptor) + } + } + for (const identity of receivedBlobIdentities(root, manifest)) { + if (!wanted.has(identity)) { + throw new Error('orcad_migration_blob_set_mismatch') + } + } + for (const descriptor of expected) { + // An earlier commit attempt may already have moved it; the received bytes must still match. + if ( + !stagedIds.has(orcadMigrationBlobIdentity(descriptor)) && + inspectReceivedBlob(root, manifest, descriptor) !== 'matching' + ) { + throw blobSetMismatch(descriptor) + } + } +} + +/** + * The caller must run `assertOrcadMigrationBlobsReady` first, in the same synchronous turn: that + * pass hashes every staged file, and no chunk can land between it and these links. + */ +export function commitOrcadMigrationBlobs(manifest: OrcadMigrationManifest, root: string): void { + const directories = new Set() + for (const descriptor of stagedBlobDescriptors(root, manifest)) { + const finalPath = getOrcadMigrationReceivedBlobPath(root, manifest, descriptor) + ensureOrcadMigrationDirectory(dirname(finalPath)) + linkOrcadMigrationStagedFile({ + stagedPath: stagedBlobPath(root, manifest, descriptor), + finalPath, + expected: descriptor, + conflictError: `orcad_migration_blob_destination_conflict:${descriptor.blobKind}:${descriptor.key}` + }) + directories.add(dirname(finalPath)) + } + for (const directory of directories) { + syncDirectoryDurablySync(directory) + } + removeOrcadMigrationStagingDirectory(root, manifest) +} + +/** Also drops blobs a failed commit already moved: the catalog they belonged to never landed. */ +export function abortOrcadMigrationBlobs(manifest: OrcadMigrationManifest, root: string): void { + removeOrcadMigrationStagingDirectory(root, manifest) + rmSync(receivedBlobsDirectory(root, manifest), { recursive: true, force: true }) +} + +/** Received blobs stay only for staged or committed migrations; a failed commit's are dropped. */ +export function pruneOrcadMigrationBlobStaging( + stagedManifests: readonly OrcadMigrationManifest[], + committedMigrations: readonly OrcadMigrationRequestIdentity[], + root: string +): void { + pruneOrcadMigrationStagingDirectories(root, stagedManifests) + pruneOrcadMigrationKeyedDirectories(join(root, RECEIVED_DIRECTORY), [ + ...stagedManifests, + ...committedMigrations + ]) +} + +function blobSetMismatch(descriptor: OrcadMigrationBlobDescriptor): Error { + return new Error(`orcad_migration_blob_set_mismatch:${descriptor.blobKind}:${descriptor.key}`) +} + +function receivedBlobIdentities(root: string, manifest: OrcadMigrationManifest): string[] { + const received = receivedBlobsDirectory(root, manifest) + return ORCAD_MIGRATION_BLOB_KINDS.flatMap((blobKind) => + readDirectory(join(received, blobKind)).map((key) => + orcadMigrationBlobIdentity({ blobKind, key }) + ) + ) +} + +function stagedBlobDescriptors( + root: string, + manifest: OrcadMigrationManifest +): OrcadMigrationBlobDescriptor[] { + const staging = orcadMigrationStagingDirectory(root, manifest) + const descriptors: OrcadMigrationBlobDescriptor[] = [] + for (const blobKind of ORCAD_MIGRATION_BLOB_KINDS) { + for (const name of readDirectory(join(staging, blobKind))) { + const match = STAGED_BLOB_NAME.exec(name) + if (!match) { + continue + } + try { + descriptors.push( + parseOrcadMigrationBlobDescriptor({ + blobKind, + key: match[1], + sha256: match[2], + byteLength: Number(match[3]) + }) + ) + } catch { + // Not a name this channel writes; it cannot belong to the staged set. + } + } + } + return descriptors +} + +function readDirectory(path: string): string[] { + try { + return readdirSync(path).sort() + } catch { + return [] + } +} + +function inspectReceivedBlob( + root: string, + manifest: OrcadMigrationManifest, + descriptor: OrcadMigrationBlobDescriptor +): 'absent' | 'matching' | 'conflict' { + return inspectOrcadMigrationFinalFile( + getOrcadMigrationReceivedBlobPath(root, manifest, descriptor), + descriptor + ) +} + +function receivedBlobsDirectory(root: string, manifest: OrcadMigrationRequestIdentity): string { + return join(root, RECEIVED_DIRECTORY, orcadMigrationStagingKey(manifest)) +} + +function stagedBlobPath( + root: string, + manifest: OrcadMigrationManifest, + descriptor: OrcadMigrationBlobDescriptor +): string { + return join( + orcadMigrationStagingDirectory(root, manifest), + descriptor.blobKind, + `${descriptor.key}.${descriptor.sha256}.${descriptor.byteLength}.part` + ) +} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-shell-history-format.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-shell-history-format.ts new file mode 100644 index 00000000000..cb8e17132ec --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-shell-history-format.ts @@ -0,0 +1,125 @@ +/** + * The `shell-history` migration blob: one worktree's history file for one shell, keyed + * `-`. The bytes are a one-line JSON header naming + * that worktree id, then the history file verbatim. The header is there because a key's hash + * cannot be reversed, the destination's meta.json needs the id (history GC prunes by it), and the + * catalog does not list every git worktree a terminal ever ran in. + */ +import { z } from 'zod' +import { ORCAD_MIGRATION_BLOB_KIND_CONTRACTS } from '../../../shared/orcad-migration-blob-transfer' +import { hashWorktreeId } from '../../terminal-history-id' + +/** fish keeps its history in the distro's own fish data dir, not Orca's store, so it never moves. */ +export const ORCAD_MIGRATION_SHELL_HISTORY_SHELLS = ['zsh', 'bash'] as const + +export type OrcadMigrationShellHistoryShell = (typeof ORCAD_MIGRATION_SHELL_HISTORY_SHELLS)[number] + +export const ORCAD_MIGRATION_SHELL_HISTORY_VERSION = 1 +/** Bounds the scan for the header's newline; a worktree id is a repo id plus one path. */ +export const ORCAD_MIGRATION_SHELL_HISTORY_HEADER_SCAN_BYTES = 16 * 1024 +const NEWLINE = 0x0a + +const HeaderSchema = z.object({ + version: z.literal(ORCAD_MIGRATION_SHELL_HISTORY_VERSION), + worktreeId: z.string().min(1), + loginShell: z.literal(true).optional() +}) + +export const MAX_ORCAD_MIGRATION_SHELL_HISTORY_BYTES = + ORCAD_MIGRATION_BLOB_KIND_CONTRACTS['shell-history'].maxBytes + +export type OrcadMigrationShellHistoryKey = { + worktreeHash: string + shell: OrcadMigrationShellHistoryShell +} + +export type OrcadMigrationShellHistoryHeader = { + worktreeId: string + /** + * The desktop's WSL bridge names every pane's history `bash_history`, but wsl.exe runs the + * distro's login shell, so the file holds that shell's history; set only on such a bash file + * that has no zsh sibling. + */ + loginShell: boolean + /** Where the history bytes start. */ + byteLength: number +} + +export function orcadMigrationShellHistoryKey( + worktreeId: string, + shell: OrcadMigrationShellHistoryShell +): string { + return `${hashWorktreeId(worktreeId)}-${shell}` +} + +/** The history file a shell keeps in a worktree's history directory (see terminal-history.ts). */ +export function shellHistoryFileName(shell: OrcadMigrationShellHistoryShell): string { + return `${shell}_history` +} + +export function parseOrcadMigrationShellHistoryKey(key: string): OrcadMigrationShellHistoryKey { + if (!ORCAD_MIGRATION_BLOB_KIND_CONTRACTS['shell-history'].key.test(key)) { + throw new Error('orcad_migration_shell_history_key_invalid') + } + const [worktreeHash, shell] = key.split('-') + const supported = ORCAD_MIGRATION_SHELL_HISTORY_SHELLS.find((entry) => entry === shell) + if (!supported) { + throw new Error('orcad_migration_shell_history_shell_unsupported') + } + return { worktreeHash, shell: supported } +} + +export function encodeOrcadMigrationShellHistoryHeader( + worktreeId: string, + options: { loginShell?: boolean } = {} +): Buffer { + const document = { + version: ORCAD_MIGRATION_SHELL_HISTORY_VERSION, + worktreeId, + ...(options.loginShell ? { loginShell: true } : {}) + } + const header = Buffer.from(`${JSON.stringify(document)}\n`, 'utf8') + if (header.length > ORCAD_MIGRATION_SHELL_HISTORY_HEADER_SCAN_BYTES) { + throw new Error('orcad_migration_shell_history_worktree_id_too_long') + } + return header +} + +/** + * Reads only the header from the first bytes of a blob and checks it names the worktree the key + * hashes. `totalBytes` is the whole blob's size, so an empty history is refused without a read. + */ +export function parseOrcadMigrationShellHistoryHeader( + key: string, + head: Buffer, + totalBytes: number +): OrcadMigrationShellHistoryHeader { + const parsedKey = parseOrcadMigrationShellHistoryKey(key) + const end = head.subarray(0, ORCAD_MIGRATION_SHELL_HISTORY_HEADER_SCAN_BYTES).indexOf(NEWLINE) + if (end === -1) { + throw new Error('orcad_migration_shell_history_header_invalid') + } + const { worktreeId, loginShell } = parseHeaderLine(head.subarray(0, end)) + if (hashWorktreeId(worktreeId) !== parsedKey.worktreeHash) { + throw new Error('orcad_migration_shell_history_key_mismatch') + } + const byteLength = end + 1 + if (totalBytes <= byteLength) { + throw new Error('orcad_migration_shell_history_empty') + } + return { worktreeId, loginShell, byteLength } +} + +function parseHeaderLine(line: Buffer): { worktreeId: string; loginShell: boolean } { + try { + const parsed = HeaderSchema.safeParse( + JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(line)) + ) + if (parsed.success) { + return { worktreeId: parsed.data.worktreeId, loginShell: parsed.data.loginShell === true } + } + } catch { + // Not UTF-8 JSON; refused below like any other malformed header. + } + throw new Error('orcad_migration_shell_history_header_invalid') +} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-migration-staged-file.ts b/src/main/persistence/migrating-orcad-catalog/orcad-migration-staged-file.ts new file mode 100644 index 00000000000..10bddc41058 --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-migration-staged-file.ts @@ -0,0 +1,214 @@ +/** + * The destination's resumable staging files, shared by scrollback snapshots and migration blobs: + * fsync'd appends whose retried ranges must match, size-as-progress, digest-checked hard-link + * commits, and per-migration staging directories that abort and prune remove whole. + */ +import { createHash } from 'node:crypto' +import { + closeSync, + existsSync, + fsyncSync, + linkSync, + mkdirSync, + openSync, + readFileSync, + readSync, + readdirSync, + rmSync, + statSync, + writeSync +} from 'node:fs' +import { join } from 'node:path' +import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest' +import { syncDirectoryDurablySync } from '../../durable-file-write' + +export const ORCAD_MIGRATION_STAGING_DIRECTORY = '.orcad-migration-staging' + +/** Names the error codes, e.g. `orcad_migration__offset_invalid`. */ +export type OrcadMigrationStagingChannel = 'snapshot' | 'blob' + +export type OrcadMigrationRequestIdentity = { migrationId: string; manifestSha256: string } + +export type OrcadMigrationStagedContent = { sha256: string; byteLength: number } + +export type OrcadMigrationStagedChunk = { + channel: OrcadMigrationStagingChannel + path: string + offset: number + bytes: Buffer + byteLength: number +} + +/** Appends one chunk, or confirms an identical retry; returns the staged size afterwards. */ +export function appendOrcadMigrationStagedChunk(chunk: OrcadMigrationStagedChunk): number { + const { channel, path, offset, bytes } = chunk + const descriptorFd = openStagedFile(path) + try { + const size = statSync(path).size + if (offset > size || offset + bytes.length > chunk.byteLength) { + throw new Error(`orcad_migration_${channel}_offset_invalid`) + } + if (offset < size) { + if (offset + bytes.length > size) { + throw new Error(`orcad_migration_${channel}_offset_invalid`) + } + const existing = Buffer.alloc(bytes.length) + const read = readSync(descriptorFd, existing, 0, existing.length, offset) + if (read !== bytes.length || !existing.equals(bytes)) { + throw new Error(`orcad_migration_${channel}_retry_mismatch`) + } + return size + } + const written = writeSync(descriptorFd, bytes, 0, bytes.length, offset) + if (written !== bytes.length) { + throw new Error(`orcad_migration_${channel}_write_incomplete`) + } + fsyncSync(descriptorFd) + return size + bytes.length + } finally { + closeSync(descriptorFd) + } +} + +export function orcadMigrationStagedFileSize(path: string, byteLength: number): number { + try { + return Math.min(statSync(path).size, byteLength) + } catch { + return 0 + } +} + +export function orcadMigrationFileMatches( + path: string, + expected: OrcadMigrationStagedContent +): boolean { + try { + const bytes = readFileSync(path) + return ( + bytes.length === expected.byteLength && + createHash('sha256').update(bytes).digest('hex') === expected.sha256 + ) + } catch { + return false + } +} + +export function inspectOrcadMigrationFinalFile( + path: string | null, + expected: OrcadMigrationStagedContent +): 'absent' | 'matching' | 'conflict' { + if (!path || !existsSync(path)) { + return 'absent' + } + return orcadMigrationFileMatches(path, expected) ? 'matching' : 'conflict' +} + +/** + * Hard-links a verified staged file into place, then drops the staged name. A final file that + * already holds the same bytes (an earlier commit attempt) counts as done; different bytes refuse. + */ +export function linkOrcadMigrationStagedFile(args: { + stagedPath: string + finalPath: string + expected: OrcadMigrationStagedContent + conflictError: string +}): void { + if (inspectOrcadMigrationFinalFile(args.finalPath, args.expected) !== 'matching') { + try { + linkSync(args.stagedPath, args.finalPath) + } catch (error) { + const finalStatus = inspectOrcadMigrationFinalFile(args.finalPath, args.expected) + if (finalStatus === 'conflict') { + throw new Error(args.conflictError) + } + if (finalStatus !== 'matching') { + throw error + } + } + } + rmSync(args.stagedPath, { force: true }) +} + +export function orcadMigrationStagingDirectory( + root: string, + manifest: OrcadMigrationManifest +): string { + return join(root, ORCAD_MIGRATION_STAGING_DIRECTORY, orcadMigrationStagingKey(manifest)) +} + +export function ensureOrcadMigrationDirectory(path: string): void { + mkdirSync(path, { recursive: true, mode: 0o700 }) +} + +export function removeOrcadMigrationStagingDirectory( + root: string, + manifest: OrcadMigrationManifest +): void { + rmSync(orcadMigrationStagingDirectory(root, manifest), { recursive: true, force: true }) + syncDirectoryDurablySync(join(root, ORCAD_MIGRATION_STAGING_DIRECTORY)) +} + +/** Removes staging for every migration the importer no longer holds staged. */ +export function pruneOrcadMigrationStagingDirectories( + root: string, + stagedManifests: readonly OrcadMigrationManifest[] +): void { + pruneOrcadMigrationKeyedDirectories( + join(root, ORCAD_MIGRATION_STAGING_DIRECTORY), + stagedManifests + ) +} + +/** Removes every per-migration directory under `parent` whose migration is not retained. */ +export function pruneOrcadMigrationKeyedDirectories( + parent: string, + retainedMigrations: readonly OrcadMigrationRequestIdentity[] +): void { + const retained = new Set(retainedMigrations.map(orcadMigrationStagingKey)) + let entries: string[] + try { + entries = readdirSync(parent) + } catch { + return + } + for (const entry of entries) { + if (/^[a-f0-9]{32}$/.test(entry) && !retained.has(entry)) { + rmSync(join(parent, entry), { recursive: true, force: true }) + } + } +} + +/** The importer's staged manifest for this request, or the channel's own refusal. */ +export function requireStagedOrcadMigrationManifest( + staged: OrcadMigrationManifest | null, + request: OrcadMigrationRequestIdentity, + notStagedError: string +): OrcadMigrationManifest { + if ( + !staged || + staged.migrationId !== request.migrationId || + staged.manifestSha256 !== request.manifestSha256 + ) { + throw new Error(notStagedError) + } + return staged +} + +export function orcadMigrationStagingKey(manifest: OrcadMigrationRequestIdentity): string { + return createHash('sha256') + .update(`${manifest.migrationId}\0${manifest.manifestSha256}`) + .digest('hex') + .slice(0, 32) +} + +function openStagedFile(path: string): number { + try { + return openSync(path, 'r+') + } catch { + try { + return openSync(path, 'wx+', 0o600) + } catch { + return openSync(path, 'r+') + } + } +} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts b/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts index 8e3efdeefe6..7f1a709acb8 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.test.ts @@ -1,5 +1,13 @@ import { createHash } from 'node:crypto' -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync +} from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' @@ -61,6 +69,30 @@ describe('orcad scrollback snapshot transfer', () => { expect(readFinal(descriptor)).toEqual(bytes) }) + it('stages through the shared helper at the pre-blob path and with the snapshot codes', () => { + const bytes = Buffer.from('pinned layout', 'utf8') + const descriptor = snapshot('8', 'tab-8', 'leaf-8', bytes) + const manifest = migrationManifest('pinned-path', [descriptor]) + const state = stagedState(manifest) + stage(state, manifest, descriptor, 0, bytes.subarray(0, 6)) + + // A host upgraded mid-move must find the staging it wrote before the shared helper existed. + const key = createHash('sha256') + .update(`${manifest.migrationId}\0${manifest.manifestSha256}`) + .digest('hex') + .slice(0, 32) + const stagingDirectory = join(storage.snapshotRoot ?? '', '.orcad-migration-staging', key) + const stagedName = `${descriptor.ref}.${descriptor.sha256}.part` + expect(readdirSync(stagingDirectory)).toEqual([stagedName]) + expect(readFileSync(join(stagingDirectory, stagedName))).toEqual(bytes.subarray(0, 6)) + expect(() => stage(state, manifest, descriptor, 0, Buffer.from('other!'))).toThrow( + /^orcad_migration_snapshot_retry_mismatch$/ + ) + expect(() => stage(state, manifest, descriptor, 7, Buffer.from('x'))).toThrow( + /^orcad_migration_snapshot_offset_invalid$/ + ) + }) + it('refuses incomplete and digest-mismatched staged bytes', () => { const bytes = Buffer.from('expected bytes', 'utf8') const descriptor = snapshot('2', 'tab-2', 'leaf-2', bytes) diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.ts b/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.ts index 80a7e566b71..cd55576a43c 100644 --- a/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.ts +++ b/src/main/persistence/migrating-orcad-catalog/orcad-scrollback-snapshot-transfer.ts @@ -1,18 +1,4 @@ -import { createHash } from 'node:crypto' -import { - closeSync, - existsSync, - fsyncSync, - linkSync, - mkdirSync, - openSync, - readFileSync, - readSync, - readdirSync, - rmSync, - statSync, - writeSync -} from 'node:fs' +import { mkdirSync } from 'node:fs' import { join } from 'node:path' import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest' import { @@ -28,8 +14,18 @@ import { getTerminalScrollbackSnapshotRoot, type TerminalScrollbackSnapshotStorage } from '../../terminal-scrollback-snapshots' - -const STAGING_DIRECTORY = '.orcad-migration-staging' +import { + appendOrcadMigrationStagedChunk, + ensureOrcadMigrationDirectory, + inspectOrcadMigrationFinalFile, + linkOrcadMigrationStagedFile, + orcadMigrationFileMatches, + orcadMigrationStagedFileSize, + orcadMigrationStagingDirectory, + pruneOrcadMigrationStagingDirectories, + removeOrcadMigrationStagingDirectory, + requireStagedOrcadMigrationManifest +} from './orcad-migration-staged-file' /** * Destination side: append one verified chunk to the staging file for a snapshot the staged @@ -41,36 +37,26 @@ export function stageOrcadMigrationSnapshotChunk(args: { storage: TerminalScrollbackSnapshotStorage request: OrcadMigrationSnapshotChunkRequest }): OrcadMigrationSnapshotChunkResult { - const manifest = requireStagedManifest(args.stagedManifest, args.request) + const manifest = requireStagedOrcadMigrationManifest( + args.stagedManifest, + args.request, + 'orcad_migration_snapshot_catalog_not_staged' + ) const descriptor = requireDescriptor(manifest, args.request.ref) const bytes = decodeOrcadMigrationSnapshotChunk(args.request.bytesBase64) - const path = stagedSnapshotPath(args.storage, manifest, descriptor) - mkdirSync(stagingDirectory(args.storage, manifest), { recursive: true, mode: 0o700 }) - const descriptorFd = openStagedFile(path) - try { - const size = statSync(path).size - if (args.request.offset > size || args.request.offset + bytes.length > descriptor.byteLength) { - throw new Error('orcad_migration_snapshot_offset_invalid') - } - if (args.request.offset < size) { - if (args.request.offset + bytes.length > size) { - throw new Error('orcad_migration_snapshot_offset_invalid') - } - const existing = Buffer.alloc(bytes.length) - const read = readSync(descriptorFd, existing, 0, existing.length, args.request.offset) - if (read !== bytes.length || !existing.equals(bytes)) { - throw new Error('orcad_migration_snapshot_retry_mismatch') - } - return chunkResult(args.request, size) - } - const written = writeSync(descriptorFd, bytes, 0, bytes.length, args.request.offset) - if (written !== bytes.length) { - throw new Error('orcad_migration_snapshot_write_incomplete') - } - fsyncSync(descriptorFd) - return chunkResult(args.request, size + bytes.length) - } finally { - closeSync(descriptorFd) + ensureOrcadMigrationDirectory(stagingDirectory(args.storage, manifest)) + const acknowledgedOffset = appendOrcadMigrationStagedChunk({ + channel: 'snapshot', + path: stagedSnapshotPath(args.storage, manifest, descriptor), + offset: args.request.offset, + bytes, + byteLength: descriptor.byteLength + }) + return { + migrationId: args.request.migrationId, + manifestSha256: args.request.manifestSha256, + ref: args.request.ref, + acknowledgedOffset } } @@ -84,9 +70,13 @@ export function inspectOrcadMigrationSnapshotUploads( } return snapshots.map((descriptor) => ({ ...descriptor, - receivedBytes: matchingFinalSnapshot(storage, descriptor) - ? descriptor.byteLength - : stagedSnapshotSize(storage, manifest, descriptor) + receivedBytes: + inspectFinalSnapshot(storage, descriptor) === 'matching' + ? descriptor.byteLength + : orcadMigrationStagedFileSize( + stagedSnapshotPath(storage, manifest, descriptor), + descriptor.byteLength + ) })) } @@ -106,7 +96,7 @@ export function assertOrcadMigrationSnapshotsReady( finalStatus === 'matching' ? getTerminalScrollbackSnapshotPath(upload.ref, storage) : stagedSnapshotPath(storage, manifest, upload) - if (!path || !fileMatches(path, upload)) { + if (!path || !orcadMigrationFileMatches(path, upload)) { throw new Error(`orcad_migration_snapshot_digest_mismatch:${upload.ref}`) } } @@ -121,37 +111,26 @@ export function commitOrcadMigrationSnapshots( const root = getTerminalScrollbackSnapshotRoot(storage) mkdirSync(root, { recursive: true, mode: 0o700 }) for (const descriptor of snapshots) { - if (matchingFinalSnapshot(storage, descriptor)) { - rmSync(stagedSnapshotPath(storage, manifest, descriptor), { force: true }) - continue - } - const stagedPath = stagedSnapshotPath(storage, manifest, descriptor) const finalPath = getTerminalScrollbackSnapshotPath(descriptor.ref, storage) if (!finalPath) { throw new Error('orcad_migration_snapshot_ref_invalid') } - try { - linkSync(stagedPath, finalPath) - } catch (error) { - const finalStatus = inspectFinalSnapshot(storage, descriptor) - if (finalStatus === 'conflict') { - throw new Error(`orcad_migration_snapshot_destination_conflict:${descriptor.ref}`) - } - if (finalStatus !== 'matching') { - throw error - } - } - rmSync(stagedPath, { force: true }) + linkOrcadMigrationStagedFile({ + stagedPath: stagedSnapshotPath(storage, manifest, descriptor), + finalPath, + expected: descriptor, + conflictError: `orcad_migration_snapshot_destination_conflict:${descriptor.ref}` + }) } syncDirectoryDurablySync(root) - removeStagingDirectory(storage, manifest) + removeOrcadMigrationStagingDirectory(root, manifest) } export function abortOrcadMigrationSnapshots( manifest: OrcadMigrationManifest, storage: TerminalScrollbackSnapshotStorage ): void { - removeStagingDirectory(storage, manifest) + removeOrcadMigrationStagingDirectory(getTerminalScrollbackSnapshotRoot(storage), manifest) } /** Removes staging for every migration the importer no longer holds staged. */ @@ -159,33 +138,7 @@ export function pruneOrcadMigrationSnapshotStaging( stagedManifests: readonly OrcadMigrationManifest[], storage: TerminalScrollbackSnapshotStorage ): void { - const root = join(getTerminalScrollbackSnapshotRoot(storage), STAGING_DIRECTORY) - const retained = new Set(stagedManifests.map(stagingKey)) - let entries: string[] - try { - entries = readdirSync(root) - } catch { - return - } - for (const entry of entries) { - if (/^[a-f0-9]{32}$/.test(entry) && !retained.has(entry)) { - rmSync(join(root, entry), { recursive: true, force: true }) - } - } -} - -function requireStagedManifest( - staged: OrcadMigrationManifest | null, - request: Pick -): OrcadMigrationManifest { - if ( - !staged || - staged.migrationId !== request.migrationId || - staged.manifestSha256 !== request.manifestSha256 - ) { - throw new Error('orcad_migration_snapshot_catalog_not_staged') - } - return staged + pruneOrcadMigrationStagingDirectories(getTerminalScrollbackSnapshotRoot(storage), stagedManifests) } function requireDescriptor( @@ -201,68 +154,21 @@ function requireDescriptor( return descriptor } -function stagedSnapshotSize( - storage: TerminalScrollbackSnapshotStorage, - manifest: OrcadMigrationManifest, - descriptor: OrcadMigrationTerminalScrollbackSnapshot -): number { - try { - return Math.min( - statSync(stagedSnapshotPath(storage, manifest, descriptor)).size, - descriptor.byteLength - ) - } catch { - return 0 - } -} - -function matchingFinalSnapshot( - storage: TerminalScrollbackSnapshotStorage, - descriptor: OrcadMigrationTerminalScrollbackSnapshot -): boolean { - return inspectFinalSnapshot(storage, descriptor) === 'matching' -} - function inspectFinalSnapshot( storage: TerminalScrollbackSnapshotStorage, descriptor: OrcadMigrationTerminalScrollbackSnapshot ): 'absent' | 'matching' | 'conflict' { - const path = getTerminalScrollbackSnapshotPath(descriptor.ref, storage) - if (!path || !existsSync(path)) { - return 'absent' - } - return fileMatches(path, descriptor) ? 'matching' : 'conflict' -} - -function fileMatches(path: string, descriptor: OrcadMigrationTerminalScrollbackSnapshot): boolean { - try { - const bytes = readFileSync(path) - return ( - bytes.length === descriptor.byteLength && - createHash('sha256').update(bytes).digest('hex') === descriptor.sha256 - ) - } catch { - return false - } -} - -function openStagedFile(path: string): number { - try { - return openSync(path, 'r+') - } catch { - try { - return openSync(path, 'wx+', 0o600) - } catch { - return openSync(path, 'r+') - } - } + return inspectOrcadMigrationFinalFile( + getTerminalScrollbackSnapshotPath(descriptor.ref, storage), + descriptor + ) } function stagingDirectory( storage: TerminalScrollbackSnapshotStorage, manifest: OrcadMigrationManifest ): string { - return join(getTerminalScrollbackSnapshotRoot(storage), STAGING_DIRECTORY, stagingKey(manifest)) + return orcadMigrationStagingDirectory(getTerminalScrollbackSnapshotRoot(storage), manifest) } function stagedSnapshotPath( @@ -272,31 +178,3 @@ function stagedSnapshotPath( ): string { return join(stagingDirectory(storage, manifest), `${descriptor.ref}.${descriptor.sha256}.part`) } - -function removeStagingDirectory( - storage: TerminalScrollbackSnapshotStorage, - manifest: OrcadMigrationManifest -): void { - const stagingRoot = join(getTerminalScrollbackSnapshotRoot(storage), STAGING_DIRECTORY) - rmSync(stagingDirectory(storage, manifest), { recursive: true, force: true }) - syncDirectoryDurablySync(stagingRoot) -} - -function stagingKey(manifest: OrcadMigrationManifest): string { - return createHash('sha256') - .update(`${manifest.migrationId}\0${manifest.manifestSha256}`) - .digest('hex') - .slice(0, 32) -} - -function chunkResult( - request: OrcadMigrationSnapshotChunkRequest, - acknowledgedOffset: number -): OrcadMigrationSnapshotChunkResult { - return { - migrationId: request.migrationId, - manifestSha256: request.manifestSha256, - ref: request.ref, - acknowledgedOffset - } -} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-shell-history-installer.ts b/src/main/persistence/migrating-orcad-catalog/orcad-shell-history-installer.ts new file mode 100644 index 00000000000..f480d50de1e --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-shell-history-installer.ts @@ -0,0 +1,283 @@ +/** + * Destination half of the `shell-history` blob: a pre-commit check that each file names a worktree + * of the migrated catalog, and the post-commit install into this host's history root at the path + * a zsh/bash pane in that worktree gets as HISTFILE (`//_history`). + */ +import { createHash } from 'node:crypto' +import { + closeSync, + lstatSync, + mkdirSync, + openSync, + readFileSync, + readlinkSync, + readSync, + realpathSync, + rmSync, + statSync +} from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { ORCAD_MIGRATION_BLOB_KIND_CONTRACTS } from '../../../shared/orcad-migration-blob-transfer' +import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest' +import { projectGroupIdFromRepoId } from '../../../shared/folder-workspace-worktree' +import { folderWorkspaceKey } from '../../../shared/workspace-scope' +import { splitWorktreeId } from '../../../shared/worktree/id' +import { + durableWriteTempPath, + syncDirectoryDurablySync, + writeFileDurableSync +} from '../../durable-file-write' +import { resolveUnixPaneShellPath } from '../../daemon/shell-ready' +import { resolveUnixShellPath } from '../../providers/local-pty-utils' +import { readHistoryMeta, resolveShellKind } from '../../terminal-history' +import { + getOrcadMigrationReceivedBlobPath, + listOrcadMigrationReceivedBlobNames +} from './orcad-migration-blob-staging' +import { + ORCAD_MIGRATION_SHELL_HISTORY_HEADER_SCAN_BYTES, + parseOrcadMigrationShellHistoryHeader, + parseOrcadMigrationShellHistoryKey, + shellHistoryFileName, + type OrcadMigrationShellHistoryShell +} from './orcad-migration-shell-history-format' +import type { OrcadMigrationRequestIdentity } from './orcad-migration-staged-file' + +type ShellHistoryCatalog = Pick< + OrcadMigrationManifest['payload'], + 'repositories' | 'projectGroups' | 'folderWorkspaces' +> + +/** Throws unless the staged file is a well-formed history for a worktree this catalog moves. */ +export function assertOrcadMigrationShellHistoryValid( + key: string, + path: string, + catalog: ShellHistoryCatalog +): void { + const { worktreeId } = parseOrcadMigrationShellHistoryHeader( + key, + readHead(path, ORCAD_MIGRATION_SHELL_HISTORY_HEADER_SCAN_BYTES), + statSync(path).size + ) + // Refused, not dropped: only a sender bug names a worktree outside the catalog, and dropping + // the file would lose that history without anyone noticing. + if (!catalogOwnsWorktree(catalog, worktreeId)) { + throw new Error('orcad_migration_shell_history_worktree_not_migrated') + } +} + +export type OrcadShellHistoryInstall = { + migration: OrcadMigrationRequestIdentity + root: string + /** This host's `getHistoryRoot()`. */ + historyRoot: string + /** The shell binary a new pane on this host would actually run; see `resolveNewPaneShellPath`. */ + paneShellPath: () => string + /** True while a shell runs in the worktree. */ + worktreeHasLiveShells: (worktreeId: string) => boolean + /** Worktrees whose install waits for their live shells to exit. */ + deferredWorktreeIds: Set + now: number +} + +/** + * Installs every received history file, then deletes it. Already installed (meta.json records the + * blob) only deletes. A worktree with a live shell keeps its blob: that shell would overwrite the + * file from its own memory on exit. A write failure keeps the blob for the next retry and is + * rethrown once the others had their turn. + */ +export function installOrcadMigrationShellHistory(install: OrcadShellHistoryInstall): void { + let firstError: unknown = null + for (const key of listOrcadMigrationReceivedBlobNames( + install.root, + install.migration, + 'shell-history' + )) { + if (!ORCAD_MIGRATION_BLOB_KIND_CONTRACTS['shell-history'].key.test(key)) { + continue + } + try { + installOne(install, key) + } catch (error) { + firstError ??= error + } + } + if (firstError !== null) { + throw firstError + } +} + +type ReceivedShellHistory = { + worktreeId: string + worktreeHash: string + shell: OrcadMigrationShellHistoryShell + sha256: string + history: Buffer +} + +function installOne(install: OrcadShellHistoryInstall, key: string): void { + const path = getOrcadMigrationReceivedBlobPath(install.root, install.migration, { + blobKind: 'shell-history', + key + }) + const bytes = readFileSync(path) + let moved: ReceivedShellHistory + try { + const header = parseOrcadMigrationShellHistoryHeader(key, bytes, bytes.length) + const { worktreeHash, shell } = parseOrcadMigrationShellHistoryKey(key) + moved = { + worktreeId: header.worktreeId, + worktreeHash, + shell: header.loginShell ? (paneHistoryShell(install.paneShellPath) ?? shell) : shell, + sha256: createHash('sha256').update(bytes).digest('hex'), + history: withFinalNewline(bytes.subarray(header.byteLength)) + } + } catch (error) { + // The pre-commit check passed these bytes, so only a damaged file lands here. + console.error(`[orcad-migration] dropped unreadable shell history ${key}:`, error) + removeReceived(path) + return + } + const directory = join(install.historyRoot, moved.worktreeHash) + const meta = readHistoryMeta(directory) + // Keyed by sha alone, so a pane shell that changed between attempts cannot install it twice. + if (meta?.installedMigrationBlobs?.some((blob) => blob.sha256 === moved.sha256)) { + removeReceived(path) + return + } + if (install.worktreeHasLiveShells(moved.worktreeId)) { + install.deferredWorktreeIds.add(moved.worktreeId) + return + } + // Owner-only, like the history directories terminal-history.ts creates. + mkdirSync(directory, { recursive: true, mode: 0o700 }) + writeHistoryFile(join(directory, shellHistoryFileName(moved.shell)), moved.history) + writeMeta(directory, moved, install.now) + removeReceived(path) + install.deferredWorktreeIds.delete(moved.worktreeId) +} + +// A history without a final newline would run its last command into the host's next one. +function withFinalNewline(history: Buffer): Buffer { + return history.at(-1) === 0x0a ? history : Buffer.concat([history, Buffer.from('\n')]) +} + +/** + * The host normally has no history for a worktree that just arrived. If it has some, nothing is + * overwritten: the moved history goes first (it is older) and the host's own lines follow, so the + * newest commands stay at the end where shells read them back from. + */ +function writeHistoryFile(path: string, moved: Buffer): void { + const existing = readExisting(path) + // A crash between this write and the meta.json marker leaves the moved bytes already in front. + if (existing?.subarray(0, moved.length).equals(moved)) { + return + } + const content = existing ? Buffer.concat([moved, existing]) : moved + const target = writeTarget(path) + writeFileDurableSync(durableWriteTempPath(target), target, content, 0o600) +} + +// A symlinked HISTFILE is written through to its target so the link the user set up survives. +function writeTarget(path: string): string { + try { + return realpathSync(path) + } catch (error) { + if (!isMissing(error)) { + throw error + } + } + try { + return lstatSync(path).isSymbolicLink() ? resolve(dirname(path), readlinkSync(path)) : path + } catch (error) { + if (isMissing(error)) { + return path + } + throw error + } +} + +// GC prunes a history directory by its meta.json worktree id, so it must name the host's id. +function writeMeta(directory: string, moved: ReceivedShellHistory, now: number): void { + const existing = readHistoryMeta(directory) + const path = join(directory, 'meta.json') + const meta = { + ...existing, + worktreeId: moved.worktreeId, + createdAt: existing?.createdAt ?? new Date(now).toISOString(), + installedMigrationBlobs: [ + ...(existing?.installedMigrationBlobs ?? []), + { sha256: moved.sha256, shell: moved.shell } + ] + } + writeFileDurableSync(durableWriteTempPath(path), path, JSON.stringify(meta), 0o600) +} + +/** + * A login-shell history from the WSL bridge (see the header's `loginShell`) lands under the file a + * new pane on this host reads: `zsh_history` or `bash_history` when that pane runs zsh or bash, + * `bash_history` for any other shell. + */ +function paneHistoryShell(paneShellPath: () => string): OrcadMigrationShellHistoryShell | null { + const kind = resolveShellKind(paneShellPath()) + return kind === 'zsh' || kind === 'bash' ? kind : null +} + +/** + * The shell a new pane spawns, resolved as the spawn does: the host's default-shell setting, then + * SHELL, then the first executable fallback. With no executable shell at all, no pane runs here. + */ +export function resolveNewPaneShellPath(defaultShell: string | undefined): string { + try { + return resolveUnixShellPath(resolveUnixPaneShellPath({}, undefined, defaultShell)) + } catch { + return '' + } +} + +function readExisting(path: string): Buffer | null { + try { + return readFileSync(path) + } catch (error) { + if (isMissing(error)) { + return null + } + throw error + } +} + +function isMissing(error: unknown): boolean { + return error instanceof Error && 'code' in error && error.code === 'ENOENT' +} + +function removeReceived(path: string): void { + rmSync(path, { force: true }) + syncDirectoryDurablySync(dirname(path)) +} + +function readHead(path: string, maxBytes: number): Buffer { + const fd = openSync(path, 'r') + try { + const buffer = Buffer.alloc(maxBytes) + const bytesRead = readSync(fd, buffer, 0, maxBytes, 0) + return buffer.subarray(0, bytesRead) + } finally { + closeSync(fd) + } +} + +function catalogOwnsWorktree(catalog: ShellHistoryCatalog, worktreeId: string): boolean { + if ( + catalog.folderWorkspaces.some((workspace) => folderWorkspaceKey(workspace.id) === worktreeId) + ) { + return true + } + const parsed = splitWorktreeId(worktreeId) + if (!parsed?.repoId || !parsed.worktreePath) { + return false + } + const projectGroupId = projectGroupIdFromRepoId(parsed.repoId) + return projectGroupId + ? catalog.projectGroups.some((group) => group.id === projectGroupId) + : catalog.repositories.some((repo) => repo.id === parsed.repoId) +} diff --git a/src/main/persistence/migrating-orcad-catalog/orcad-terminal-restore-hint-store.ts b/src/main/persistence/migrating-orcad-catalog/orcad-terminal-restore-hint-store.ts new file mode 100644 index 00000000000..11296ea2c4f --- /dev/null +++ b/src/main/persistence/migrating-orcad-catalog/orcad-terminal-restore-hint-store.ts @@ -0,0 +1,203 @@ +/** + * Installed restore hints on the destination, keyed by paneKey, until the pane's first spawn + * consumes its hint. A small fsync'd JSON file beside the received blobs rather than profile state: + * it is host-local, never synced or sent anywhere, and every change must be durable on its own. + */ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { z } from 'zod' +import { durableWriteTempPath, writeFileDurableSync } from '../../durable-file-write' +import { + MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS, + isOrcadMigrationTerminalRestoreHintCwd, + isOrcadMigrationTerminalRestoreHintPaneKey, + type OrcadMigrationTerminalRestoreHint +} from '../../../shared/orcad-migration-terminal-restore-hints' +import { ensureOrcadMigrationDirectory } from './orcad-migration-staged-file' + +const STORE_VERSION = 2 +const INSTALLED_DIRECTORY = 'installed' +const STORE_FILE = 'terminal-restore-hints.json' +// A pane not reopened within a month is not coming back through this migration. +export const ORCAD_TERMINAL_RESTORE_HINT_TTL_MS = 30 * 24 * 60 * 60 * 1000 +const MAX_INSTALLED_MIGRATIONS = 256 + +/** A received hint placed in the worktree that owns its pane on this host. */ +export type OrcadTerminalRestoreHintPlacement = OrcadMigrationTerminalRestoreHint & { + worktreeId: string +} + +export type OrcadTerminalRestoreHintMigration = { + migrationKey: string + migrationId: string +} + +type InstalledTerminalRestoreHint = OrcadTerminalRestoreHintPlacement & + OrcadTerminalRestoreHintMigration & { installedAt: number } + +type TerminalRestoreHintStoreDocument = { + version: typeof STORE_VERSION + hints: InstalledTerminalRestoreHint[] + /** Migrations already installed, so a commit retry never revives a hint a pane consumed. */ + installedMigrationKeys: string[] +} + +const MIGRATION_KEY = /^[a-f0-9]{32}$/ + +const StoreDocumentSchema = z.object({ + version: z.literal(STORE_VERSION), + hints: z.array( + z.object({ + paneKey: z.string().refine(isOrcadMigrationTerminalRestoreHintPaneKey), + cwd: z.string().refine(isOrcadMigrationTerminalRestoreHintCwd), + worktreeId: z.string().min(1), + migrationKey: z.string().regex(MIGRATION_KEY), + migrationId: z.string().min(1), + installedAt: z.number().int().nonnegative() + }) + ), + installedMigrationKeys: z.array(z.string().regex(MIGRATION_KEY)) +}) + +export class OrcadTerminalRestoreHintStore { + private readonly directory: string + private readonly path: string + private document: TerminalRestoreHintStoreDocument | null = null + + constructor(blobRoot: string) { + this.directory = join(blobRoot, INSTALLED_DIRECTORY) + this.path = join(this.directory, STORE_FILE) + } + + hasInstalledMigration(migrationKey: string): boolean { + return this.load().installedMigrationKeys.includes(migrationKey) + } + + /** Newer hints replace older ones for the same pane; the oldest drop past the count cap. */ + install( + migration: OrcadTerminalRestoreHintMigration, + hints: readonly OrcadTerminalRestoreHintPlacement[], + now: number + ): void { + const current = this.load() + if (current.installedMigrationKeys.includes(migration.migrationKey)) { + return + } + const incoming = new Set(hints.map((hint) => hint.paneKey)) + const merged = [ + ...current.hints.filter((hint) => !incoming.has(hint.paneKey)), + ...hints.map((hint) => ({ + paneKey: hint.paneKey, + cwd: hint.cwd, + worktreeId: hint.worktreeId, + migrationKey: migration.migrationKey, + migrationId: migration.migrationId, + installedAt: now + })) + ] + this.write( + { + version: STORE_VERSION, + hints: merged.slice(-MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS), + installedMigrationKeys: [...current.installedMigrationKeys, migration.migrationKey].slice( + -MAX_INSTALLED_MIGRATIONS + ) + }, + now + ) + } + + /** The pane's hinted cwd if it was moved into `worktreeId`; never removes it. */ + peek(paneKey: string, worktreeId: string, now: number): string | null { + const hint = this.load().hints.find((entry) => entry.paneKey === paneKey) + if (!hint || hint.worktreeId !== worktreeId) { + return null + } + return now - hint.installedAt > ORCAD_TERMINAL_RESTORE_HINT_TTL_MS ? null : hint.cwd + } + + /** Durably removes the pane's hint, whichever worktree it named; a no-op when it has none. */ + consume(paneKey: string, now: number): void { + this.dropWhere((hint) => hint.paneKey === paneKey, now) + } + + /** Drops hints for panes of worktrees this host no longer owns. */ + dropForWorktrees(matchesWorktreeId: (worktreeId: string) => boolean, now: number): void { + this.dropWhere((hint) => matchesWorktreeId(hint.worktreeId), now) + } + + private dropWhere(matches: (hint: InstalledTerminalRestoreHint) => boolean, now: number): void { + const current = this.load() + if (!current.hints.some(matches)) { + return + } + this.write({ ...current, hints: current.hints.filter((hint) => !matches(hint)) }, now) + } + + private write(next: TerminalRestoreHintStoreDocument, now: number): void { + const document = { + ...next, + hints: next.hints.filter( + (hint) => now - hint.installedAt <= ORCAD_TERMINAL_RESTORE_HINT_TTL_MS + ) + } + ensureOrcadMigrationDirectory(this.directory) + writeFileDurableSync( + durableWriteTempPath(this.path), + this.path, + JSON.stringify(document), + 0o600 + ) + this.document = document + } + + private load(): TerminalRestoreHintStoreDocument { + if (this.document) { + return this.document + } + this.document = readStoreDocument(this.path) + return this.document + } +} + +/** + * For project removal: a removed project's panes never come back by these keys unless a later + * migration brings them, and that migration carries its own hints. Never blocks the removal. + */ +export function dropOrcadTerminalRestoreHintsForWorktrees( + store: OrcadTerminalRestoreHintStore, + matchesWorktreeId: (worktreeId: string) => boolean +): void { + try { + store.dropForWorktrees(matchesWorktreeId, Date.now()) + } catch (error) { + console.warn('[orcad-migration] dropping terminal restore hints failed:', error) + } +} + +function emptyDocument(): TerminalRestoreHintStoreDocument { + return { version: STORE_VERSION, hints: [], installedMigrationKeys: [] } +} + +function readStoreDocument(path: string): TerminalRestoreHintStoreDocument { + let text: string + try { + text = readFileSync(path, 'utf8') + } catch { + return emptyDocument() + } + try { + const parsed = StoreDocumentSchema.safeParse(JSON.parse(text)) + if (parsed.success) { + return { + version: parsed.data.version, + hints: parsed.data.hints, + installedMigrationKeys: parsed.data.installedMigrationKeys + } + } + } catch { + // Falls through: a torn or foreign file only costs panes their old folder. + } + console.warn('[orcad-migration] ignoring an unreadable terminal restore hint store') + return emptyDocument() +} diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.test.ts new file mode 100644 index 00000000000..9cbeceee116 --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.test.ts @@ -0,0 +1,91 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { wslMoveCutoverFixture as cutover } from '../__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { wslMoveJournalDirectory } from './wsl-move-cutover-journal' +import { seedWslMoveCutover } from '../__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { loadWslMoveFence, resetWslMoveFenceForTests } from './wsl-move-scope-guard' +import { + isWslMoveFolderWorkspaceBlocked, + isWslMoveRepoBlocked, + isWslMoveWorktreeBlocked, + wslMoveRepoBackoff +} from './wsl-move-background-backoff' + +const onSsh = { connectionId: 'target-1', executionHostId: 'ssh:target-1' } +const WSL_PATH = '\\\\wsl.localhost\\Ubuntu\\home\\dev\\app' + +let userDataPath: string +beforeEach(() => { + userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-backoff-')) + loadWslMoveFence(userDataPath) +}) +afterEach(() => { + resetWslMoveFenceForTests() + rmSync(userDataPath, { recursive: true, force: true }) + vi.restoreAllMocks() +}) + +describe('WSL move background backoff', () => { + beforeEach(() => { + seedWslMoveCutover( + userDataPath, + cutover('move-1', 'source-fenced', {}, { repoIds: ['repo-1'], folderWorkspaceIds: ['notes'] }) + ) + }) + + it('backs off only the local row of a moving repo', () => { + expect(isWslMoveRepoBlocked({ id: 'repo-1', path: '/a' })).toBe(true) + expect(isWslMoveRepoBlocked({ id: 'repo-1', path: '/a', ...onSsh })).toBe(false) + expect(isWslMoveRepoBlocked({ id: 'repo-2', path: '/a' })).toBe(false) + }) + + it('backs off only the local row of a moving folder workspace', () => { + expect(isWslMoveFolderWorkspaceBlocked({ id: 'notes', folderPath: '/n' })).toBe(true) + expect(isWslMoveFolderWorkspaceBlocked({ id: 'notes', folderPath: '/n', ...onSsh })).toBe(false) + expect(isWslMoveFolderWorkspaceBlocked({ id: 'other', folderPath: '/n' })).toBe(false) + }) + + it('backs off a worktree by its id and host', () => { + expect(isWslMoveWorktreeBlocked('repo-1::/a/wt', 'local')).toBe(true) + expect(isWslMoveWorktreeBlocked('repo-1::/a/wt', undefined)).toBe(true) + expect(isWslMoveWorktreeBlocked('repo-1::/a/wt', 'ssh:target-1')).toBe(false) + expect(isWslMoveWorktreeBlocked('repo-2::/a/wt', 'local')).toBe(false) + }) +}) + +it('backs off only WSL paths when the journal is unreadable', () => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const directory = wslMoveJournalDirectory(userDataPath) + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, 'move-1.json'), '{not json') + loadWslMoveFence(userDataPath) + + expect(isWslMoveRepoBlocked({ id: 'repo-1', path: WSL_PATH })).toBe(true) + expect(isWslMoveRepoBlocked({ id: 'repo-1', path: 'C:\\src\\app' })).toBe(false) + expect(isWslMoveFolderWorkspaceBlocked({ id: 'notes', folderPath: WSL_PATH })).toBe(true) + expect(isWslMoveWorktreeBlocked(`repo-1::${WSL_PATH}\\wt`, 'local')).toBe(true) + expect(isWslMoveWorktreeBlocked('repo-1::C:\\src\\app\\wt', 'local')).toBe(false) +}) + +it('backs off a folder workspace at a WSL path when the journal is unreadable', () => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const directory = wslMoveJournalDirectory(userDataPath) + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, 'move-1.json'), '{not json') + loadWslMoveFence(userDataPath) + + expect(isWslMoveWorktreeBlocked('folder:notes', 'local', WSL_PATH)).toBe(true) + expect(isWslMoveWorktreeBlocked('folder:notes', undefined, 'C:\\notes')).toBe(false) + expect(isWslMoveWorktreeBlocked('folder:notes', 'ssh:target-1', WSL_PATH)).toBe(false) +}) + +it('tells a move in flight from a project the WSL host owns', () => { + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) + expect(wslMoveRepoBackoff({ id: 'repo-1', path: '/a' })).toBe('moving') + expect(wslMoveRepoBackoff({ id: 'repo-2', path: '/a' })).toBeNull() + seedWslMoveCutover(userDataPath, cutover('move-1', 'destination-committed')) + expect(wslMoveRepoBackoff({ id: 'repo-1', path: '/a' })).toBe('owned') + expect(wslMoveRepoBackoff({ id: 'repo-1', path: '/a', ...onSsh })).toBeNull() +}) diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.ts b/src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.ts new file mode 100644 index 00000000000..2e0db24c67b --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-background-backoff.ts @@ -0,0 +1,71 @@ +/** + * Background writers (first-work renames, scan pruning, backfills) back off quietly from a project a + * WSL move is taking, or one the WSL host now owns: no throw, no data change. They are all periodic + * or event-driven, so they retry on their own once the move settles. + * + * Each check names the row's host: a move only takes local rows, and the same id on another host is + * a different project. The row's own path stands in for the store when the journal is unreadable. + */ +import { getRepoExecutionHostId } from '../../../shared/execution-host' +import { splitWorktreeIdForFilesystem } from '../../../shared/worktree/id' +import { + isLocalWslMoveRow, + isWslMoveProjectBlocked, + isWslMoveProjectFenced, + isWslMoveSourceHost, + wslMoveProjectRefForWorkspace +} from './wsl-move-scope-guard' + +type HostedRow = { connectionId?: string | null; executionHostId?: string | null } + +export type WslMoveRepoRow = HostedRow & { id: string; path: string } + +export type WslMoveFolderWorkspaceRow = HostedRow & { id: string; folderPath: string } + +function repoStore(repo: WslMoveRepoRow) { + return { getRepo: (id: string) => (id === repo.id ? repo : undefined) } +} + +export function isWslMoveRepoBlocked(repo: WslMoveRepoRow): boolean { + if (!isWslMoveSourceHost(getRepoExecutionHostId(repo))) { + return false + } + return isWslMoveProjectBlocked(repoStore(repo), { repoId: repo.id }) +} + +/** `moving` while a move is taking the repo, `owned` once the WSL host runs it, else null. */ +export function wslMoveRepoBackoff(repo: WslMoveRepoRow): 'moving' | 'owned' | null { + if (!isWslMoveRepoBlocked(repo)) { + return null + } + return isWslMoveProjectFenced(repoStore(repo), { repoId: repo.id }) ? 'moving' : 'owned' +} + +export function isWslMoveFolderWorkspaceBlocked(folder: WslMoveFolderWorkspaceRow): boolean { + if (!isLocalWslMoveRow(folder)) { + return false + } + const store = { + getRepo: () => undefined, + getFolderWorkspace: (id: string) => (id === folder.id ? folder : undefined) + } + return isWslMoveProjectBlocked(store, { folderWorkspaceId: folder.id }) +} + +/** + * For a worktree known only by id and host, such as a resolved worktree or its metadata row. + * A folder workspace key carries no path, so callers holding one pass its folder path. + */ +export function isWslMoveWorktreeBlocked( + worktreeId: string, + hostId: string | null | undefined, + worktreePath?: string +): boolean { + if (!isLocalWslMoveRow({ executionHostId: hostId })) { + return false + } + const path = worktreePath ?? splitWorktreeIdForFilesystem(worktreeId)?.worktreePath + const row = path === undefined ? undefined : { path, folderPath: path } + const store = { getRepo: () => row, getFolderWorkspace: () => row } + return isWslMoveProjectBlocked(store, wslMoveProjectRefForWorkspace(worktreeId)) +} diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-cutover-journal.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-cutover-journal.test.ts index 028da35c276..3944b9907a7 100644 --- a/src/main/persistence/moving-wsl-projects/wsl-move-cutover-journal.test.ts +++ b/src/main/persistence/moving-wsl-projects/wsl-move-cutover-journal.test.ts @@ -2,8 +2,12 @@ import { mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' -import { wslMoveJournalKey } from '../../../shared/wsl-move/wsl-move-cutover' +import { + WSL_MOVE_CUTOVER_PHASES, + wslMoveJournalKey +} from '../../../shared/wsl-move/wsl-move-cutover' import { wslMoveCutoverFixture as cutover } from '../__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../__fixtures__/wsl-mover/wsl-move-journal-seeding' import { orcadMigrationCutoverJournalDirectory } from '../../ssh/orcad-migration-cutover-journal' import { listWslMoveCutoverChain, @@ -47,7 +51,7 @@ describe('WSL move journal', () => { }) it('refuses a second move into a distro while the first is in flight', () => { - writeWslMoveCutover(userDataPath, cutover('move-1', 'quiesced')) + seedWslMoveCutover(userDataPath, cutover('move-1', 'quiesced')) expect(() => writeWslMoveCutover( userDataPath, @@ -57,7 +61,7 @@ describe('WSL move journal', () => { }) it('chains a delta move after a finished one, and only onto the head', () => { - writeWslMoveCutover(userDataPath, cutover('move-1', 'retained')) + seedWslMoveCutover(userDataPath, cutover('move-1', 'retained')) expect(() => writeWslMoveCutover(userDataPath, cutover('move-2'))).toThrow( 'wsl_move_must_extend_its_chain' ) @@ -77,7 +81,7 @@ describe('WSL move journal', () => { }) it('never moves a record backwards, rebinds it, or advances a superseded one', () => { - writeWslMoveCutover(userDataPath, cutover('move-1', 'destination-committed')) + seedWslMoveCutover(userDataPath, cutover('move-1', 'destination-committed')) expect(() => writeWslMoveCutover(userDataPath, cutover('move-1', 'prepared'))).toThrow( 'wsl_move_phase_regressed' ) @@ -87,6 +91,7 @@ describe('WSL move journal', () => { cutover('move-1', 'retained', {}, { repoIds: ['repo-1', 'repo-2'] }) ) ).toThrow('wsl_move_binding_changed') + writeWslMoveCutover(userDataPath, cutover('move-1', 'client-rewritten')) writeWslMoveCutover(userDataPath, cutover('move-1', 'retained')) writeWslMoveCutover( userDataPath, @@ -97,7 +102,7 @@ describe('WSL move journal', () => { }) it('hands out records a caller cannot edit into the next comparison', () => { - writeWslMoveCutover(userDataPath, cutover('move-1', 'destination-committed')) + seedWslMoveCutover(userDataPath, cutover('move-1', 'destination-committed')) const [listed] = listWslMoveCutovers(userDataPath) if (listed) { listed.phase = 'prepared' @@ -122,12 +127,40 @@ describe('WSL move journal', () => { ).toThrow() }) - it('keeps the fence: an in-flight record cannot be removed', () => { - writeWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) + it('refuses a later phase whose record is missing or was lifted, or that skips a phase', () => { + expect(() => writeWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced'))).toThrow( + 'wsl_move_record_missing' + ) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) + removeWslMoveCutover(userDataPath, 'move-1') + expect(() => writeWslMoveCutover(userDataPath, cutover('move-1', 'quiesced'))).toThrow( + 'wsl_move_record_missing' + ) + expect(listWslMoveCutovers(userDataPath)).toEqual([]) + writeWslMoveCutover(userDataPath, cutover('move-1', 'prepared')) + expect(() => writeWslMoveCutover(userDataPath, cutover('move-1', 'quiesced'))).toThrow( + 'wsl_move_phase_skipped' + ) + }) + + it.each(WSL_MOVE_CUTOVER_PHASES.slice(2, -1))('refuses to remove a %s record', (phase) => { + seedWslMoveCutover(userDataPath, cutover('move-1', phase)) + expect(() => removeWslMoveCutover(userDataPath, 'move-1')).toThrow('wsl_move_in_flight') + expect(listWslMoveCutovers(userDataPath)[0]?.phase).toBe(phase) + }) + + it('keeps the fence: a record past quiesce cannot be removed', () => { + seedWslMoveCutover(userDataPath, cutover('move-1', 'quiesced')) expect(() => removeWslMoveCutover(userDataPath, 'move-1')).toThrow('wsl_move_in_flight') expect(listWslMoveCutovers(userDataPath)).toHaveLength(1) }) + it('lifts a fence whose quiesce refused: nothing reached the destination yet', () => { + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) + removeWslMoveCutover(userDataPath, 'move-1') + expect(listWslMoveCutovers(userDataPath)).toEqual([]) + }) + it('fails closed on a manifest edited without re-signing it', () => { const record = cutover() const edited = { diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-cutover-journal.ts b/src/main/persistence/moving-wsl-projects/wsl-move-cutover-journal.ts index 6032fb6cfdc..2cb6b835c86 100644 --- a/src/main/persistence/moving-wsl-projects/wsl-move-cutover-journal.ts +++ b/src/main/persistence/moving-wsl-projects/wsl-move-cutover-journal.ts @@ -81,7 +81,8 @@ export function listWslMoveCutoverChain( } /** - * Durable before it returns. A new move into a distro must extend the head of its chain, and only + * Durable before it returns. A record starts at `prepared` and then advances one phase at a time + * from the one on disk. A new move into a distro must extend the head of its chain, and only * once that head finished: one in-flight move per distro (design, section 4.3). */ export function writeWslMoveCutover(userDataPath: string, cutover: WslMoveCutover): void { @@ -97,6 +98,10 @@ export function writeWslMoveCutover(userDataPath: string, cutover: WslMoveCutove const chain = listWslMoveCutoverChain(userDataPath, parsed.journalKey) const existing = chain.find((entry) => entry.migrationId === parsed.migrationId) const head = chain.at(-1) + // Compare-and-swap: a writer whose record was lifted underneath it must not recreate the fence. + if (!existing && parsed.phase !== 'prepared') { + throw new Error('wsl_move_record_missing') + } if (existing) { assertWslMoveUpdate(existing, parsed, head) } else if (!head) { @@ -130,20 +135,34 @@ function assertWslMoveUpdate( if (order(next.phase) < order(existing.phase)) { throw new Error('wsl_move_phase_regressed') } + if (order(next.phase) > order(existing.phase) + 1) { + throw new Error('wsl_move_phase_skipped') + } if (head && head.migrationId !== existing.migrationId && next.phase !== existing.phase) { throw new Error('wsl_move_superseded') } } +// Before quiesce nothing has reached the destination, so lifting the fence loses nothing. +const REMOVABLE_IN_FLIGHT_PHASES: ReadonlySet = new Set([ + 'prepared', + 'source-fenced' +]) + /** - * Only a record that fences nothing goes: one never fenced (`prepared`) or one finished. A move in - * between keeps its fence until it resumes, or until a cancel proves the destination holds nothing. + * Only a record whose move has touched nothing beyond the source goes, or one finished. A move + * further along keeps its fence until it resumes, or until a cancel proves the destination holds + * nothing. */ export function removeWslMoveCutover(userDataPath: string, migrationId: string): void { const record = listWslMoveCutovers(userDataPath).find( (entry) => entry.migrationId === migrationId ) - if (record && record.phase !== 'prepared' && !isWslMoveCutoverFinished(record)) { + if ( + record && + !REMOVABLE_IN_FLIGHT_PHASES.has(record.phase) && + !isWslMoveCutoverFinished(record) + ) { throw new Error('wsl_move_in_flight') } journals.remove(userDataPath, migrationId) diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-fence.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-fence.test.ts index 12a8fe06eba..0215ec56fa3 100644 --- a/src/main/persistence/moving-wsl-projects/wsl-move-fence.test.ts +++ b/src/main/persistence/moving-wsl-projects/wsl-move-fence.test.ts @@ -4,16 +4,30 @@ import { wslMoveFenceFromCutovers } from './wsl-move-fence' const ids = (state: ReturnType) => ({ fenced: [...state.fenced.repoIds, ...state.fenced.folderWorkspaceIds], - hidden: [...state.hidden.repoIds, ...state.hidden.folderWorkspaceIds] + hidden: [...state.hidden.repoIds, ...state.hidden.folderWorkspaceIds], + sessionFrozen: [...state.sessionFrozen.repoIds, ...state.sessionFrozen.folderWorkspaceIds] }) describe('WSL move fence', () => { it.each([ - ['prepared', { fenced: [], hidden: [] }], - ['source-fenced', { fenced: ['repo-1', 'notes'], hidden: [] }], - ['blobs-transferred', { fenced: ['repo-1', 'notes'], hidden: [] }], - ['destination-committed', { fenced: [], hidden: ['repo-1', 'notes'] }], - ['retained', { fenced: [], hidden: ['repo-1', 'notes'] }] + ['prepared', { fenced: [], hidden: [], sessionFrozen: [] }], + // The quiesce's own Sleep still writes the session here. + ['source-fenced', { fenced: ['repo-1', 'notes'], hidden: [], sessionFrozen: [] }], + ['quiesced', { fenced: ['repo-1', 'notes'], hidden: [], sessionFrozen: ['repo-1', 'notes'] }], + [ + 'blobs-transferred', + { fenced: ['repo-1', 'notes'], hidden: [], sessionFrozen: ['repo-1', 'notes'] } + ], + // Through commit: the retained local session is what a downgrade restores. + [ + 'destination-committed', + { fenced: [], hidden: ['repo-1', 'notes'], sessionFrozen: ['repo-1', 'notes'] } + ], + [ + 'client-rewritten', + { fenced: [], hidden: ['repo-1', 'notes'], sessionFrozen: ['repo-1', 'notes'] } + ], + ['retained', { fenced: [], hidden: ['repo-1', 'notes'], sessionFrozen: ['repo-1', 'notes'] }] ] as const)('%s', (phase, expected) => { const record = cutover( 'move-1', @@ -29,6 +43,10 @@ describe('WSL move fence', () => { cutover('move-1', 'retained', {}, { repoIds: ['repo-1'] }), cutover('move-2', 'quiesced', { supersedesMigrationId: 'move-1' }, { repoIds: ['repo-2'] }) ]) - expect(ids(state)).toEqual({ fenced: ['repo-2'], hidden: ['repo-1'] }) + expect(ids(state)).toEqual({ + fenced: ['repo-2'], + hidden: ['repo-1'], + sessionFrozen: ['repo-1', 'repo-2'] + }) }) }) diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-fence.ts b/src/main/persistence/moving-wsl-projects/wsl-move-fence.ts index 0887cd9130d..e29a629a598 100644 --- a/src/main/persistence/moving-wsl-projects/wsl-move-fence.ts +++ b/src/main/persistence/moving-wsl-projects/wsl-move-fence.ts @@ -3,9 +3,10 @@ * lives outside the profile store, so an older build rewriting the profile cannot drop it, and the * scope it names is the manifest's own (design, section 4.3). * - * - Fenced (read-only): from `source-fenced` until the host owns the projects. Edits, new - * terminals and session writes for these rows are refused. + * - Fenced (read-only): from `source-fenced` until the host owns the projects. Edits and new + * terminals are refused. * - Hidden: once the destination committed. The source rows stay (retained) but are not listed. + * - Session frozen: from `quiesced` on, hidden or not, the session stays as the quiesce left it. */ import type { WslMoveCutover, WslMoveCutoverPhase } from '../../../shared/wsl-move/wsl-move-cutover' import { WSL_MOVE_CUTOVER_PHASES } from '../../../shared/wsl-move/wsl-move-cutover' @@ -21,11 +22,15 @@ export type WslMoveFenceState = { fenced: WslMoveScopeIds /** Rows the WSL host now owns: kept for a downgrade, never listed. */ hidden: WslMoveScopeIds + /** Rows already paused, through commit: their session stays as the quiesce left it. */ + sessionFrozen: WslMoveScopeIds } const phaseIndex = (phase: WslMoveCutoverPhase): number => WSL_MOVE_CUTOVER_PHASES.indexOf(phase) const FENCED_FROM = phaseIndex('source-fenced') const HIDDEN_FROM = phaseIndex('destination-committed') +// Why not from `source-fenced`: the quiesce's own Sleep writes the slept panes' session after that. +const SESSION_FROZEN_FROM = phaseIndex('quiesced') /** Throws when the journal cannot be read: callers keep every WSL project fenced then. */ export function resolveWslMoveFence(userDataPath: string): WslMoveFenceState { @@ -37,19 +42,31 @@ export function wslMoveFenceFromCutovers( ): WslMoveFenceState { const fenced = { repoIds: new Set(), folderWorkspaceIds: new Set() } const hidden = { repoIds: new Set(), folderWorkspaceIds: new Set() } + const sessionFrozen = { repoIds: new Set(), folderWorkspaceIds: new Set() } for (const cutover of cutovers) { const index = phaseIndex(cutover.phase) const target = index >= HIDDEN_FROM ? hidden : index >= FENCED_FROM ? fenced : null if (!target) { continue // `prepared` touches nothing on the source. } - const { scopeRepoIds, scopeFolderWorkspaceIds } = cutover.manifest.source - for (const id of scopeRepoIds) { - target.repoIds.add(id) - } - for (const id of scopeFolderWorkspaceIds) { - target.folderWorkspaceIds.add(id) + addScope(target, cutover) + // Through commit too: the retained local rows are what a downgrade restores. + if (index >= SESSION_FROZEN_FROM) { + addScope(sessionFrozen, cutover) } } - return { fenced, hidden } + return { fenced, hidden, sessionFrozen } +} + +function addScope( + target: { repoIds: Set; folderWorkspaceIds: Set }, + cutover: Readonly +): void { + const { scopeRepoIds, scopeFolderWorkspaceIds } = cutover.manifest.source + for (const id of scopeRepoIds) { + target.repoIds.add(id) + } + for (const id of scopeFolderWorkspaceIds) { + target.folderWorkspaceIds.add(id) + } } diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-quiesce.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-quiesce.test.ts new file mode 100644 index 00000000000..e6ad2e5d923 --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-quiesce.test.ts @@ -0,0 +1,246 @@ +import { describe, expect, it, vi } from 'vitest' +import type { WslMoveCutover } from '../../../shared/wsl-move/wsl-move-cutover' +import { wslMoveCutoverFixture as cutover } from '../__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import type { WslMoveExitProof } from './wsl-move-exit-proof' +import { + quiesceWslMove, + type WslMoveQuiesceDeps, + type WslMoveQuiesceScope +} from './wsl-move-quiesce' +import type { WslMovePaneFacts } from './wsl-move-terminal-gate' + +const WORKTREE = 'repo-1::\\\\wsl.localhost\\Ubuntu\\home\\dev\\repo-1' +const scope: WslMoveQuiesceScope = { + workspaceIds: [WORKTREE], + workspaceKeys: new Set([WORKTREE]), + panes: [{ paneKey: 'tab-1:leaf-1', sessionId: 'session-1' }] +} +const idlePane: WslMovePaneFacts = { + paneKey: 'tab-1:leaf-1', + foreground: 'shell-idle', + scrollbackBytes: 10 +} +const hints = { + hints: [{ paneKey: 'tab-1:leaf-1', cwd: '/home/dev/repo-1' }], + unreadablePaneKeys: [], + staleCwdPaneKeys: [] +} + +function harness(overrides: Partial = {}) { + const journal: WslMoveCutover['phase'][] = [] + const removed: string[] = [] + const steps: string[] = [] + const deps: WslMoveQuiesceDeps = { + gateInputs: async () => ({ + panes: [idlePane], + runningAutomationRunIds: [], + activeDispatchIds: [] + }), + sleepWorkspaces: vi.fn(async () => { + steps.push(`sleep after ${journal.at(-1)}`) + return { failedWorktreeIds: [] } + }), + livePtyIds: () => [], + captureRestoreHints: async () => hints, + proveExited: async (): Promise => ({ verdict: 'exited' }), + writeCutover: (record) => { + journal.push(record.phase) + }, + removeCutover: (migrationId) => { + removed.push(migrationId) + }, + now: () => new Date('2026-10-14T00:00:00.000Z'), + ...overrides + } + return { deps, journal, removed, steps } +} + +describe('quiesceWslMove', () => { + it('fences, sleeps, proves exit and records the proven sessions', async () => { + const { deps, journal, removed, steps } = harness() + const result = await quiesceWslMove(cutover(), scope, deps) + + expect(journal).toEqual(['source-fenced', 'quiesced']) + expect(steps).toEqual(['sleep after source-fenced']) + expect(removed).toEqual([]) + expect(result).toMatchObject({ + outcome: 'quiesced', + cutover: { phase: 'quiesced', provenExitedSessionIds: ['session-1'] }, + restoreHints: hints + }) + }) + + it('refuses with the blockers before fencing anything', async () => { + const { deps, journal } = harness({ + gateInputs: async () => ({ + panes: [{ ...idlePane, foreground: 'process-running' }], + runningAutomationRunIds: [], + activeDispatchIds: [] + }) + }) + const result = await quiesceWslMove(cutover(), scope, deps) + + expect(result).toMatchObject({ + outcome: 'refused', + reason: 'terminals-blocked', + blockers: [{ kind: 'process-running', ids: ['tab-1:leaf-1'] }], + fenceLifted: true + }) + expect(journal).toEqual([]) + expect(deps.sleepWorkspaces).not.toHaveBeenCalled() + }) + + it('gates again under the fence and refuses a pane that turned busy, without sleeping', async () => { + const phases: (WslMoveCutover['phase'] | undefined)[] = [] + let calls = 0 + const { deps, journal, removed } = harness() + deps.gateInputs = async () => { + phases.push(journal.at(-1)) + calls += 1 + const foreground = calls === 1 ? 'shell-idle' : 'process-running' + return { + panes: [{ ...idlePane, foreground }], + runningAutomationRunIds: [], + activeDispatchIds: [] + } + } + const result = await quiesceWslMove(cutover(), scope, deps) + + expect(phases).toEqual([undefined, 'source-fenced']) + expect(result).toMatchObject({ + outcome: 'refused', + reason: 'terminals-blocked', + blockers: [{ kind: 'process-running', ids: ['tab-1:leaf-1'] }], + fenceLifted: true + }) + expect(deps.sleepWorkspaces).not.toHaveBeenCalled() + expect(removed).toEqual(['move-1']) + }) + + it('refuses a second quiesce of the same distro while the first runs', async () => { + let releaseSleep: () => void = () => undefined + const first = harness({ + sleepWorkspaces: () => + new Promise((resolve) => { + releaseSleep = () => resolve({ failedWorktreeIds: [] }) + }) + }) + const running = quiesceWslMove(cutover(), scope, first.deps) + await vi.waitFor(() => expect(first.journal).toEqual(['source-fenced'])) + + const second = harness() + await expect(quiesceWslMove(cutover('move-2'), scope, second.deps)).resolves.toEqual({ + outcome: 'refused', + reason: 'quiesce-in-progress', + fenceLifted: true + }) + expect(second.journal).toEqual([]) + + releaseSleep() + await expect(running).resolves.toMatchObject({ outcome: 'quiesced' }) + await expect(quiesceWslMove(cutover('move-2'), scope, second.deps)).resolves.toMatchObject({ + outcome: 'quiesced' + }) + }) + + it.each([ + ['renderer_unavailable', 'renderer-unavailable'], + ['worktree_sleep_timeout', 'sleep-timed-out'], + ['persist failed', 'sleep-failed'] + ])('lifts the fence when Sleep rejects with %s', async (message, reason) => { + const { deps, journal, removed } = harness({ + sleepWorkspaces: async () => { + throw new Error(message) + } + }) + const result = await quiesceWslMove(cutover(), scope, deps) + + expect(result).toMatchObject({ outcome: 'refused', reason, fenceLifted: true }) + expect(journal).toEqual(['source-fenced']) + expect(removed).toEqual(['move-1']) + }) + + it('lifts the fence and names the workspaces whose Sleep failed', async () => { + const { deps, removed } = harness({ + sleepWorkspaces: async () => ({ failedWorktreeIds: [WORKTREE] }) + }) + const result = await quiesceWslMove(cutover(), scope, deps) + + expect(result).toMatchObject({ + outcome: 'refused', + reason: 'sleep-failed', + failedWorktreeIds: [WORKTREE] + }) + expect(removed).toEqual(['move-1']) + }) + + it('refuses a terminal opened in scope after Sleep, without touching the daemon proof', async () => { + const proveExited = vi.fn(async (): Promise => ({ verdict: 'exited' })) + const { deps, journal, removed } = harness({ livePtyIds: () => ['pty-new'], proveExited }) + const result = await quiesceWslMove(cutover(), scope, deps) + + expect(result).toMatchObject({ reason: 'terminals-live', liveSessionIds: ['pty-new'] }) + expect(proveExited).not.toHaveBeenCalled() + expect(journal).toEqual(['source-fenced']) + expect(removed).toEqual(['move-1']) + }) + + it.each<[WslMoveExitProof, Record]>([ + [ + { verdict: 'live', sessionIds: ['session-1'] }, + { reason: 'terminals-live', liveSessionIds: ['session-1'] } + ], + [ + { verdict: 'unverifiable', reason: 'a daemon generation did not answer' }, + { reason: 'exit-unverifiable', detail: 'a daemon generation did not answer' } + ] + ])('refuses unless the daemon proves exit: %o', async (proof, expected) => { + const { deps, journal, removed } = harness({ proveExited: async () => proof }) + const result = await quiesceWslMove(cutover(), scope, deps) + + expect(result).toMatchObject({ outcome: 'refused', ...expected, fenceLifted: true }) + expect(journal).toEqual(['source-fenced']) + expect(removed).toEqual(['move-1']) + }) + + it('lifts the fence when reading the restore hints throws', async () => { + const { deps, removed } = harness({ + captureRestoreHints: async () => { + throw new Error('disk') + } + }) + const result = await quiesceWslMove(cutover(), scope, deps) + expect(result).toMatchObject({ reason: 'quiesce-failed', detail: 'disk' }) + expect(removed).toEqual(['move-1']) + }) + + it('reports a fence it could not lift', async () => { + const { deps } = harness({ + proveExited: async () => ({ verdict: 'live', sessionIds: ['session-1'] }), + removeCutover: () => { + throw new Error('wsl_move_in_flight') + } + }) + const result = await quiesceWslMove(cutover(), scope, deps) + expect(result).toMatchObject({ outcome: 'refused', fenceLifted: false }) + }) + + it('refuses without lifting anything when the fence cannot be written', async () => { + const { deps, removed } = harness({ + writeCutover: () => { + throw new Error('wsl_move_already_in_flight') + } + }) + const result = await quiesceWslMove(cutover(), scope, deps) + expect(result).toMatchObject({ reason: 'fence-failed', fenceLifted: true }) + expect(removed).toEqual([]) + expect(deps.sleepWorkspaces).not.toHaveBeenCalled() + }) + + it('only starts from a prepared move', async () => { + const { deps } = harness() + await expect(quiesceWslMove(cutover('move-1', 'source-fenced'), scope, deps)).rejects.toThrow( + 'wsl_move_not_prepared' + ) + }) +}) diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-quiesce.ts b/src/main/persistence/moving-wsl-projects/wsl-move-quiesce.ts new file mode 100644 index 00000000000..5ad37b3e747 --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-quiesce.ts @@ -0,0 +1,212 @@ +/** + * Pausing a moving project's terminals (mover design, section 3.3): the gate must pass, the fence + * goes up, Sleep pauses every workspace in scope, and the daemon must prove each session exited + * before the journal says `quiesced`. Nothing is ever ended to make progress. + * + * A refusal after the fence lifts it again. Workspaces Sleep already paused stay asleep: that is + * their ordinary sleeping state, and waking them later restores them from it, so nothing is lost. + */ +import type { WslMoveCutover } from '../../../shared/wsl-move/wsl-move-cutover' +import type { WslMoveExitProof } from './wsl-move-exit-proof' +import type { WslMoveRestoreHintCapture } from './wsl-move-restore-hints' +import { + assessWslMoveTerminals, + type WslMoveGateBlocker, + type WslMoveGateInputs +} from './wsl-move-terminal-gate' + +export type WslMoveQuiesceScope = { + /** Every worktree and folder workspace of the moving projects; each one is slept. */ + workspaceIds: readonly string[] + /** Workspace keys as session ids mint them, for the exit proof. */ + workspaceKeys: ReadonlySet + /** The panes the move carries, with the daemon session each one recorded. */ + panes: readonly { paneKey: string; sessionId: string }[] +} + +export type WslMoveQuiesceDeps = { + gateInputs: () => Promise + /** Resolves with the workspaces whose Sleep failed; rejects when the renderer cannot answer. */ + sleepWorkspaces: (workspaceIds: readonly string[]) => Promise<{ failedWorktreeIds: string[] }> + /** PTYs this process still holds for the workspaces, daemon-backed or not. */ + livePtyIds: (workspaceIds: readonly string[]) => readonly string[] + captureRestoreHints: ( + panes: WslMoveQuiesceScope['panes'], + distro: string + ) => Promise + proveExited: (scope: { + workspaceKeys: ReadonlySet + sessionIds: ReadonlySet + }) => Promise + writeCutover: (cutover: WslMoveCutover) => void + removeCutover: (migrationId: string) => void + now: () => Date +} + +export type WslMoveQuiesceRefusalReason = + | 'terminals-blocked' + /** Another quiesce for the same distro is still running in this process. */ + | 'quiesce-in-progress' + | 'fence-failed' + | 'renderer-unavailable' + | 'sleep-timed-out' + | 'sleep-failed' + | 'terminals-live' + | 'exit-unverifiable' + | 'journal-failed' + /** Anything else that threw after the fence went up. */ + | 'quiesce-failed' + +export type WslMoveQuiesceResult = + | { + outcome: 'quiesced' + cutover: WslMoveCutover + // Not in the journal: the schema has no field for them yet, so the caller carries them on. + restoreHints: WslMoveRestoreHintCapture + } + | { + outcome: 'refused' + reason: WslMoveQuiesceRefusalReason + blockers?: WslMoveGateBlocker[] + failedWorktreeIds?: string[] + liveSessionIds?: string[] + detail?: string + /** False only when the journal record could not be removed, so the fence still stands. */ + fenceLifted: boolean + } + +type Refusal = Extract + +// Journal keys with a quiesce running in this process; the journal alone cannot order two of them. +const quiescingJournalKeys = new Set() + +export async function quiesceWslMove( + prepared: WslMoveCutover, + scope: WslMoveQuiesceScope, + deps: WslMoveQuiesceDeps +): Promise { + if (prepared.phase !== 'prepared') { + throw new Error('wsl_move_not_prepared') + } + if (quiescingJournalKeys.has(prepared.journalKey)) { + return { outcome: 'refused', reason: 'quiesce-in-progress', fenceLifted: true } + } + quiescingJournalKeys.add(prepared.journalKey) + try { + return await quiesceUnderLock(prepared, scope, deps) + } finally { + quiescingJournalKeys.delete(prepared.journalKey) + } +} + +async function quiesceUnderLock( + prepared: WslMoveCutover, + scope: WslMoveQuiesceScope, + deps: WslMoveQuiesceDeps +): Promise { + const gate = assessWslMoveTerminals(await deps.gateInputs()) + if (gate.verdict === 'blocked') { + return { + outcome: 'refused', + reason: 'terminals-blocked', + blockers: gate.blockers, + fenceLifted: true + } + } + const fenced: WslMoveCutover = { + ...prepared, + phase: 'source-fenced', + updatedAt: deps.now().toISOString() + } + try { + deps.writeCutover(fenced) + } catch (error) { + return { + outcome: 'refused', + reason: 'fence-failed', + detail: describe(error), + fenceLifted: true + } + } + const refuse = (refusal: Omit): Refusal => { + try { + deps.removeCutover(fenced.migrationId) + return { outcome: 'refused', ...refusal, fenceLifted: true } + } catch (error) { + console.warn('[wsl-move] Could not lift the fence after a refused quiesce:', error) + return { outcome: 'refused', ...refusal, fenceLifted: false } + } + } + + try { + // The fence blocks new terminals, not commands typed into open shells; a sub-second window remains. + const fencedGate = assessWslMoveTerminals(await deps.gateInputs()) + if (fencedGate.verdict === 'blocked') { + return refuse({ reason: 'terminals-blocked', blockers: fencedGate.blockers }) + } + } catch (error) { + return refuse({ reason: 'quiesce-failed', detail: describe(error) }) + } + + try { + const { failedWorktreeIds } = await deps.sleepWorkspaces(scope.workspaceIds) + if (failedWorktreeIds.length > 0) { + return refuse({ reason: 'sleep-failed', failedWorktreeIds }) + } + } catch (error) { + const detail = describe(error) + const reason = + detail === 'renderer_unavailable' + ? 'renderer-unavailable' + : detail === 'worktree_sleep_timeout' + ? 'sleep-timed-out' + : 'sleep-failed' + return refuse({ reason, detail }) + } + + try { + return await proveAndRecord(fenced, scope, deps, refuse) + } catch (error) { + return refuse({ reason: 'quiesce-failed', detail: describe(error) }) + } +} + +async function proveAndRecord( + fenced: WslMoveCutover, + scope: WslMoveQuiesceScope, + deps: WslMoveQuiesceDeps, + refuse: (refusal: Omit) => Refusal +): Promise { + // Not the gate's pane probe: a slept pane has no process to ask, so it would read unverifiable. + const live = deps.livePtyIds(scope.workspaceIds) + if (live.length > 0) { + return refuse({ reason: 'terminals-live', liveSessionIds: [...live] }) + } + const distro = fenced.manifest.source.distro + const restoreHints = await deps.captureRestoreHints(scope.panes, distro) + const sessionIds = new Set(scope.panes.map((pane) => pane.sessionId)) + // Last, so a session that came back while the hints were read still refuses. + const proof = await deps.proveExited({ workspaceKeys: scope.workspaceKeys, sessionIds }) + if (proof.verdict === 'live') { + return refuse({ reason: 'terminals-live', liveSessionIds: proof.sessionIds }) + } + if (proof.verdict === 'unverifiable') { + return refuse({ reason: 'exit-unverifiable', detail: proof.reason }) + } + const quiesced: WslMoveCutover = { + ...fenced, + phase: 'quiesced', + updatedAt: deps.now().toISOString(), + provenExitedSessionIds: [...sessionIds] + } + try { + deps.writeCutover(quiesced) + } catch (error) { + return refuse({ reason: 'journal-failed', detail: describe(error) }) + } + return { outcome: 'quiesced', cutover: quiesced, restoreHints } +} + +function describe(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.test.ts new file mode 100644 index 00000000000..0deb18ecc19 --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.test.ts @@ -0,0 +1,53 @@ +import { createHash } from 'node:crypto' +import { describe, expect, it } from 'vitest' +import { parseOrcadMigrationBlobDescriptor } from '../../../shared/orcad-migration-blob-transfer' +import { parseOrcadMigrationTerminalRestoreHintsText } from '../../../shared/orcad-migration-terminal-restore-hints' +import { buildWslMoveRestoreHintsBlob } from './wsl-move-restore-hints-blob' + +const hints = [ + { paneKey: 'tab-1:11111111-1111-4111-8111-111111111111', cwd: '/home/me/src' }, + { paneKey: 'tab-2:22222222-2222-4222-8222-222222222222', cwd: '/srv/app' } +] + +describe('WSL move restore hints blob', () => { + it('sends no blob when quiesce found no hints', () => { + expect(buildWslMoveRestoreHintsBlob([])).toBeNull() + }) + + it('encodes a descriptor the channel accepts over bytes the destination parses back', async () => { + const blob = buildWslMoveRestoreHintsBlob(hints) + expect(blob).not.toBeNull() + if (!blob) { + return + } + expect(parseOrcadMigrationBlobDescriptor(blob.descriptor)).toEqual({ + blobKind: 'terminal-restore-hints', + key: 'migration', + sha256: createHash('sha256').update(blob.bytes).digest('hex'), + byteLength: blob.bytes.length + }) + expect(parseOrcadMigrationTerminalRestoreHintsText(blob.bytes.toString('utf8'))).toEqual({ + version: 1, + hints + }) + const head = await blob.source.readChunk(blob.descriptor, 0, 5) + const rest = await blob.source.readChunk(blob.descriptor, 5, blob.bytes.length) + expect(Buffer.concat([head, rest])).toEqual(blob.bytes) + await expect( + blob.source.readChunk({ ...blob.descriptor, byteLength: 1 }, 0, 1) + ).rejects.toThrow('orcad_migration_source_blob_changed:terminal-restore-hints') + }) + + it('refuses a hint the destination would refuse, and an oversize document', () => { + expect(() => buildWslMoveRestoreHintsBlob([{ paneKey: 'tab-1:1', cwd: '/home' }])).toThrow( + 'orcad_migration_terminal_restore_hint_pane_key_invalid' + ) + const many = Array.from({ length: 1100 }, (_, index) => ({ + paneKey: `tab-${index}:11111111-1111-4111-8111-111111111111`, + cwd: `/${'a'.repeat(4000)}` + })) + expect(() => buildWslMoveRestoreHintsBlob(many)).toThrow( + 'orcad_migration_terminal_restore_hints_too_large' + ) + }) +}) diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.ts b/src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.ts new file mode 100644 index 00000000000..5ab0bed0d0f --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-restore-hints-blob.ts @@ -0,0 +1,57 @@ +/** + * The quiesce step's restore hints as the `terminal-restore-hints` migration blob: the bytes, the + * descriptor the destination checks them against, and the source the blob sender reads from. + */ +import { createHash } from 'node:crypto' +import type { OrcadMigrationBlobDescriptor } from '../../../shared/orcad-migration-blob-transfer' +import { sameOrcadMigrationBlob } from '../../../shared/orcad-migration-blob-transfer' +import { + MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_BYTES, + ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_KEY, + ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_VERSION, + parseOrcadMigrationTerminalRestoreHintsDocument, + type OrcadMigrationTerminalRestoreHint +} from '../../../shared/orcad-migration-terminal-restore-hints' +import type { OrcadMigrationBlobSource } from '../../ssh/orcad-migration-blob-coordinator' + +export type WslMoveRestoreHintsBlob = { + descriptor: OrcadMigrationBlobDescriptor + bytes: Buffer + source: OrcadMigrationBlobSource +} + +/** Null when there is nothing to carry: the channel sends empty content as no blob at all. */ +export function buildWslMoveRestoreHintsBlob( + hints: readonly OrcadMigrationTerminalRestoreHint[] +): WslMoveRestoreHintsBlob | null { + if (hints.length === 0) { + return null + } + // Validated here so a bad hint fails the move on this side, before anything is staged. + const document = parseOrcadMigrationTerminalRestoreHintsDocument({ + version: ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_VERSION, + hints: hints.map((hint) => ({ paneKey: hint.paneKey, cwd: hint.cwd })) + }) + const bytes = Buffer.from(JSON.stringify(document), 'utf8') + if (bytes.length > MAX_ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_BYTES) { + throw new Error('orcad_migration_terminal_restore_hints_too_large') + } + const descriptor: OrcadMigrationBlobDescriptor = { + blobKind: 'terminal-restore-hints', + key: ORCAD_MIGRATION_TERMINAL_RESTORE_HINTS_KEY, + sha256: createHash('sha256').update(bytes).digest('hex'), + byteLength: bytes.length + } + return { + descriptor, + bytes, + source: { + readChunk: async (requested, offset, maxBytes) => { + if (!sameOrcadMigrationBlob(requested, descriptor)) { + throw new Error(`orcad_migration_source_blob_changed:${requested.blobKind}`) + } + return bytes.subarray(offset, offset + maxBytes) + } + } + } +} diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-scope-guard.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-scope-guard.test.ts index 3889293fdac..b731ed75209 100644 --- a/src/main/persistence/moving-wsl-projects/wsl-move-scope-guard.test.ts +++ b/src/main/persistence/moving-wsl-projects/wsl-move-scope-guard.test.ts @@ -3,18 +3,24 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { wslMoveCutoverFixture as cutover } from '../__fixtures__/wsl-mover/wsl-move-cutover-fixture' -import { - removeWslMoveCutover, - writeWslMoveCutover, - wslMoveJournalDirectory -} from './wsl-move-cutover-journal' +import { seedWslMoveCutover } from '../__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { removeWslMoveCutover, wslMoveJournalDirectory } from './wsl-move-cutover-journal' import { WslMoveProjectFencedError, + assertWslMoveProjectWritable, + assertWslMoveRepoWritable, + assertWslMoveReposWritable, assertWslMoveWorkspaceWritable, + isPassiveWorkspaceUpdate, isWslMoveFenceLoaded, + isWslMoveProjectBlocked, + isWslMoveProjectFenced, + isWslMoveSourceHost, loadWslMoveFence, + shouldDropWslMoveProjectUpdate, resetWslMoveFenceForTests, - withoutWslMovedProjects, + withoutWslMovedFolderWorkspaces, + withoutWslMovedRepos, wslMoveProjectRefForWorkspace } from './wsl-move-scope-guard' @@ -28,7 +34,6 @@ afterEach(() => { rmSync(userDataPath, { recursive: true, force: true }) vi.restoreAllMocks() }) -const asRepo = (row: { id: string }) => ({ repoId: row.id }) const repoPaths = new Map([ ['repo-wsl', '\\\\wsl.localhost\\Ubuntu\\home\\dev\\app'], @@ -59,31 +64,31 @@ describe('wslMoveProjectRefForWorkspace', () => { describe('WSL move scope guard', () => { it('fences nothing before startup loads the journal', () => { resetWslMoveFenceForTests() - writeWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) expect(isWslMoveFenceLoaded()).toBe(false) expect(writable('repo-1::/x')).not.toThrow() }) it('follows a write under another spelling of the same profile folder', () => { loadWslMoveFence(`${userDataPath}/./`) - writeWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) expect(writable('repo-1::/x')).toThrow(WslMoveProjectFencedError) }) it('lifts the fence when the finished move is removed', () => { - writeWslMoveCutover(userDataPath, cutover('move-1', 'retained')) + seedWslMoveCutover(userDataPath, cutover('move-1', 'retained')) expect(writable('repo-1::/x')).toThrow(WslMoveProjectFencedError) removeWslMoveCutover(userDataPath, 'move-1') expect(writable('repo-1::/x')).not.toThrow() }) it('never matches a project on another host', () => { - writeWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) expect(writable('ssh:target-1|repo-1::/x')).not.toThrow() }) it('follows journal writes without rereading on every check', () => { - writeWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) expect(writable('repo-1::/x')).toThrow(WslMoveProjectFencedError) rmSync(wslMoveJournalDirectory(userDataPath), { recursive: true, force: true }) expect(writable('repo-1::/x')).toThrow(WslMoveProjectFencedError) @@ -91,16 +96,16 @@ describe('WSL move scope guard', () => { it('lets every project through when no move is recorded', () => { expect(writable('repo-1::/x')).not.toThrow() - expect(withoutWslMovedProjects([{ id: 'repo-1' }], asRepo)).toHaveLength(1) + expect(withoutWslMovedRepos([{ id: 'repo-1' }])).toHaveLength(1) }) it('leaves a prepared move untouched', () => { - writeWslMoveCutover(userDataPath, cutover('move-1', 'prepared')) + seedWslMoveCutover(userDataPath, cutover('move-1', 'prepared')) expect(writable('repo-1::/x')).not.toThrow() }) it('refuses work in a fenced project and keeps it listed', () => { - writeWslMoveCutover( + seedWslMoveCutover( userDataPath, cutover('move-1', 'source-fenced', {}, { repoIds: ['repo-1'], folderWorkspaceIds: ['notes'] }) ) @@ -108,20 +113,28 @@ describe('WSL move scope guard', () => { expect(writable('folder:notes')).toThrow(WslMoveProjectFencedError) expect(writable('repo-2::/x')).not.toThrow() const rows = [{ id: 'repo-1' }, { id: 'repo-2' }] - expect(withoutWslMovedProjects(rows, asRepo)).toEqual(rows) + expect(withoutWslMovedRepos(rows)).toEqual(rows) }) it('hides and refuses a project the WSL host owns', () => { - writeWslMoveCutover( + seedWslMoveCutover( userDataPath, cutover('move-1', 'destination-committed', {}, { folderWorkspaceIds: ['notes'] }) ) expect(writable('repo-1::/x')).toThrow(WslMoveProjectFencedError) - expect( - withoutWslMovedProjects([{ id: 'notes' }, { id: 'other' }], (f) => ({ - folderWorkspaceId: f.id - })) - ).toEqual([{ id: 'other' }]) + expect(withoutWslMovedFolderWorkspaces([{ id: 'notes' }, { id: 'other' }])).toEqual([ + { id: 'other' } + ]) + }) + + it("keeps the host's own row, which carries the moved project's id", () => { + seedWslMoveCutover( + userDataPath, + cutover('move-1', 'destination-committed', {}, { repoIds: ['repo-1'] }) + ) + const onHost = { id: 'repo-1', executionHostId: 'runtime:wsl:ubuntu' } + const viaSsh = { id: 'repo-1', connectionId: 'target-1' } + expect(withoutWslMovedRepos([{ id: 'repo-1' }, onHost, viaSsh])).toEqual([onHost, viaSsh]) }) it('keeps WSL projects read-only and every row listed when the journal is unreadable', () => { @@ -133,6 +146,96 @@ describe('WSL move scope guard', () => { expect(writable('repo-wsl::/x')).toThrow(WslMoveProjectFencedError) expect(writable('repo-win::/x')).not.toThrow() const rows = [{ id: 'repo-wsl' }] - expect(withoutWslMovedProjects(rows, asRepo)).toEqual(rows) + expect(withoutWslMovedRepos(rows)).toEqual(rows) + }) +}) + +describe('WSL move edit guard', () => { + const fenceRepo1AndNotes = () => + seedWslMoveCutover( + userDataPath, + cutover('move-1', 'source-fenced', {}, { repoIds: ['repo-1'], folderWorkspaceIds: ['notes'] }) + ) + + it.each([ + [{ isUnread: true, lastActivityAt: 1 }, true], + [{ lastActivityAt: 1 }, true], + [{ isUnread: false }, true], + [{ isUnread: true }, false], + [{}, false], + [{ isUnread: true, displayName: 'x' }, false], + [{ comment: 'x' }, false] + ])('treats %o as passive: %s', (updates, passive) => { + expect(isPassiveWorkspaceUpdate(updates)).toBe(passive) + }) + + it('drops passive updates to a blocked project and refuses user edits', () => { + fenceRepo1AndNotes() + const notes = { folderWorkspaceId: 'notes' } + expect(shouldDropWslMoveProjectUpdate(undefined, notes, { isUnread: false })).toBe(true) + expect(() => shouldDropWslMoveProjectUpdate(undefined, notes, { isUnread: true })).toThrow( + WslMoveProjectFencedError + ) + expect(() => shouldDropWslMoveProjectUpdate(undefined, notes, { name: 'x' })).toThrow( + WslMoveProjectFencedError + ) + expect(shouldDropWslMoveProjectUpdate(undefined, { repoId: 'repo-2' }, { name: 'x' })).toBe( + false + ) + }) + + it('refuses a blocked project, and a catalog project when any source repo is blocked', () => { + fenceRepo1AndNotes() + expect(() => assertWslMoveProjectWritable(undefined, { repoId: 'repo-1' })).toThrow( + WslMoveProjectFencedError + ) + expect(() => assertWslMoveProjectWritable(undefined, { repoId: 'repo-2' })).not.toThrow() + expect(() => assertWslMoveReposWritable(undefined, ['repo-2', 'repo-1'])).toThrow( + WslMoveProjectFencedError + ) + expect(() => assertWslMoveReposWritable(undefined, ['repo-2'])).not.toThrow() + }) + + it('freezes catalog settings only while a source repo is mid-move', () => { + seedWslMoveCutover( + userDataPath, + cutover('move-1', 'destination-committed', {}, { repoIds: ['repo-1'] }) + ) + expect(isWslMoveProjectFenced(undefined, { repoId: 'repo-1' })).toBe(false) + expect(isWslMoveProjectBlocked(undefined, { repoId: 'repo-1' })).toBe(true) + expect(() => assertWslMoveReposWritable(undefined, ['repo-1', 'repo-2'])).not.toThrow() + }) + + it('only fences a repo row that runs on this machine', () => { + fenceRepo1AndNotes() + expect(isWslMoveSourceHost(undefined)).toBe(true) + expect(isWslMoveSourceHost('local')).toBe(true) + expect(isWslMoveSourceHost('ssh:box')).toBe(false) + expect(() => assertWslMoveRepoWritable(undefined, { id: 'repo-1' })).toThrow( + WslMoveProjectFencedError + ) + expect(() => + assertWslMoveRepoWritable(undefined, { id: 'repo-1', executionHostId: 'ssh:box' }) + ).not.toThrow() + }) + + it('finds a folder path through the list when the store has no single lookup', () => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const directory = wslMoveJournalDirectory(userDataPath) + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, 'move-1.json'), '{not json') + loadWslMoveFence(userDataPath) + const listOnly = { + getRepo: () => undefined, + getFolderWorkspaces: () => [ + { id: 'wsl-notes', folderPath: '\\\\wsl.localhost\\Ubuntu\\home\\dev\\notes' }, + { id: 'win-notes', folderPath: 'C:\\notes' } + ] + } + expect(isWslMoveProjectBlocked(listOnly, { folderWorkspaceId: 'wsl-notes' })).toBe(true) + expect(isWslMoveProjectBlocked(listOnly, { folderWorkspaceId: 'win-notes' })).toBe(false) + expect(isWslMoveProjectBlocked({ getRepo: () => undefined }, { folderWorkspaceId: 'x' })).toBe( + false + ) }) }) diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-scope-guard.ts b/src/main/persistence/moving-wsl-projects/wsl-move-scope-guard.ts index 191cdc637fe..3d0a39705dc 100644 --- a/src/main/persistence/moving-wsl-projects/wsl-move-scope-guard.ts +++ b/src/main/persistence/moving-wsl-projects/wsl-move-scope-guard.ts @@ -12,6 +12,7 @@ import { resolve } from 'node:path' import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' import { parseWorkspaceKey } from '../../../shared/workspace-scope' import { isWslUncPath } from '../../../shared/wsl-paths' +import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host' import { onWslMoveJournalChange } from './wsl-move-cutover-journal' import { resolveWslMoveFence, type WslMoveFenceState } from './wsl-move-fence' @@ -28,7 +29,9 @@ export class WslMoveProjectFencedError extends Error { /** Only the project paths, read when the journal is unreadable. */ type ProjectStore = { getRepo: (repoId: string) => { path: string } | undefined - getFolderWorkspace: (folderWorkspaceId: string) => { folderPath: string } | undefined + // RuntimeStore has only the list. + getFolderWorkspace?: (folderWorkspaceId: string) => { folderPath: string } | undefined + getFolderWorkspaces?: () => readonly { id: string; folderPath: string }[] } /** An unreadable journal fences every WSL project and hides none: never edit, never lose a row. */ @@ -67,7 +70,10 @@ function readFence(userDataPath: string): FenceView { try { return { readable: true, state: resolveWslMoveFence(userDataPath) } } catch (error) { - console.warn('[wsl-move] Journal unreadable; keeping WSL projects read-only:', error) + console.warn( + '[wsl-move] Journal unreadable; keeping WSL projects read-only, sessions unfrozen:', + error + ) return { readable: false } } } @@ -90,9 +96,14 @@ function inScope(scope: WslMoveFenceState['fenced'], ref: WslMoveProjectRef): bo } function projectPath(store: ProjectStore, ref: WslMoveProjectRef): string | undefined { - return 'repoId' in ref - ? store.getRepo(ref.repoId)?.path - : store.getFolderWorkspace(ref.folderWorkspaceId)?.folderPath + if ('repoId' in ref) { + return store.getRepo(ref.repoId)?.path + } + const id = ref.folderWorkspaceId + if (store.getFolderWorkspace) { + return store.getFolderWorkspace(id)?.folderPath + } + return store.getFolderWorkspaces?.().find((folder) => folder.id === id)?.folderPath } /** @@ -102,17 +113,140 @@ function projectPath(store: ProjectStore, ref: WslMoveProjectRef): string | unde export function isWslMoveProjectBlocked( store: ProjectStore | undefined, ref: WslMoveProjectRef | null +): boolean { + return matchesFence(store, ref, true) +} + +/** Only while a move is taking the project; one the WSL host already owns is not fenced. */ +export function isWslMoveProjectFenced( + store: ProjectStore | undefined, + ref: WslMoveProjectRef | null +): boolean { + return matchesFence(store, ref, false) +} + +function matchesFence( + store: ProjectStore | undefined, + ref: WslMoveProjectRef | null, + includeHidden: boolean ): boolean { const fence = loaded?.view if (!ref || !fence) { return false } if (fence.readable) { - return inScope(fence.state.fenced, ref) || inScope(fence.state.hidden, ref) + return inScope(fence.state.fenced, ref) || (includeHidden && inScope(fence.state.hidden, ref)) } return !store || isWslUncPath(projectPath(store, ref) ?? '') } +export function assertWslMoveProjectWritable( + store: ProjectStore | undefined, + ref: WslMoveProjectRef | null +): void { + if (isWslMoveProjectBlocked(store, ref)) { + throw new WslMoveProjectFencedError() + } +} + +// Background bookkeeping a move drops quietly; anything else is a user edit and is refused. +const PASSIVE_WORKSPACE_UPDATE_KEYS: ReadonlySet = new Set(['isUnread', 'lastActivityAt']) + +/** A worktree meta or folder workspace patch, as the edit entry points receive it. */ +export type WslMoveProjectUpdate = Readonly> & { + readonly isUnread?: boolean + readonly lastActivityAt?: number +} + +// Activity carries lastActivityAt and a read clears unread; a bare `isUnread: true` is a user's mark-unread. +export function isPassiveWorkspaceUpdate(updates: WslMoveProjectUpdate): boolean { + const keys = Object.keys(updates) + if (keys.length === 0 || !keys.every((key) => PASSIVE_WORKSPACE_UPDATE_KEYS.has(key))) { + return false + } + return 'lastActivityAt' in updates || updates.isUnread === false +} + +/** A move only takes local projects; the same repo id on another host is a different project. */ +export function isWslMoveSourceHost(hostId: string | null | undefined): boolean { + return !hostId || hostId === LOCAL_EXECUTION_HOST_ID +} + +/** For entry points that already hold the repo row, which names its host. */ +export function assertWslMoveRepoWritable( + store: ProjectStore | undefined, + repo: { id: string; connectionId?: string | null; executionHostId?: string | null } +): void { + if (isWslMoveSourceHost(getRepoExecutionHostId(repo))) { + assertWslMoveProjectWritable(store, { repoId: repo.id }) + } +} + +/** + * A catalog project spans its source repos; a move taking any of them freezes the whole project. + * A repo the WSL host already owns does not, or the project's settings would freeze for good. + */ +export function assertWslMoveReposWritable( + store: ProjectStore | undefined, + repoIds: readonly string[] +): void { + if (repoIds.some((repoId) => isWslMoveProjectFenced(store, { repoId }))) { + throw new WslMoveProjectFencedError() + } +} + +type HostSetupStore = ProjectStore & { + getProjects?: () => readonly { id: string; sourceRepoIds: readonly string[] }[] + getProjectHostSetups?: () => readonly { + id: string + projectId: string + hostId: string + repoId: string + }[] +} + +/** A setup on this machine edits its project's local checkouts, so it waits out a move taking one. */ +export function assertWslMoveHostSetupWritable( + store: HostSetupStore | undefined, + setup: { projectId: string; hostId: string; repoId?: string } +): void { + if (!isWslMoveSourceHost(setup.hostId)) { + return + } + // The setup's own repo stays frozen once the host owns it; shared project repos only mid-move. + if (setup.repoId) { + assertWslMoveProjectWritable(store, { repoId: setup.repoId }) + } + const project = store?.getProjects?.().find((entry) => entry.id === setup.projectId) + assertWslMoveReposWritable(store, project?.sourceRepoIds ?? []) +} + +/** By id, for update and delete; an unknown id is left to the store's own not-found. */ +export function assertWslMoveHostSetupIdWritable( + store: HostSetupStore | undefined, + setupId: string +): void { + const setup = store?.getProjectHostSetups?.().find((entry) => entry.id === setupId) + if (setup) { + assertWslMoveHostSetupWritable(store, setup) + } +} + +/** True when the caller should drop the update quietly; throws for a user edit to a blocked project. */ +export function shouldDropWslMoveProjectUpdate( + store: ProjectStore | undefined, + ref: WslMoveProjectRef | null, + updates: WslMoveProjectUpdate +): boolean { + if (!isWslMoveProjectBlocked(store, ref)) { + return false + } + if (!isPassiveWorkspaceUpdate(updates)) { + throw new WslMoveProjectFencedError() + } + return true +} + export function isWslMoveWorkspaceBlocked( store: ProjectStore | undefined, workspaceId: string | null | undefined @@ -120,6 +254,30 @@ export function isWslMoveWorkspaceBlocked( return !!workspaceId && isWslMoveProjectBlocked(store, wslMoveProjectRefForWorkspace(workspaceId)) } +/** True once a move has paused the project: its session must stay as the quiesce left it. */ +export function isWslMoveProjectSessionFrozen(ref: WslMoveProjectRef | null): boolean { + const fence = loaded?.view + return !!ref && !!fence?.readable && inScope(fence.state.sessionFrozen, ref) +} + +export function isWslMoveWorkspaceSessionFrozen(workspaceId: string): boolean { + return isWslMoveProjectSessionFrozen(wslMoveProjectRefForWorkspace(workspaceId)) +} + +/** + * Whether any move has a frozen session; lets the hot write path skip the per-workspace scan. + * An unreadable journal freezes nothing: the edit fence already refuses visibly, and no move can + * proceed without a readable journal, so freezing would only discard sessions silently. + */ +export function hasWslMoveFrozenSessions(): boolean { + const fence = loaded?.view + if (!fence?.readable) { + return false + } + const { sessionFrozen } = fence.state + return sessionFrozen.repoIds.size > 0 || sessionFrozen.folderWorkspaceIds.size > 0 +} + export function assertWslMoveWorkspaceWritable( store: ProjectStore | undefined, workspaceId: string | null | undefined @@ -129,8 +287,23 @@ export function assertWslMoveWorkspaceWritable( } } -/** Rows the WSL host owns stay in the profile for a downgrade; this build lists the host instead. */ -export function withoutWslMovedProjects(rows: T[], toRef: (row: T) => WslMoveProjectRef): T[] { +type HostedRow = { connectionId?: string | null; executionHostId?: string | null } + +/** + * Rows the WSL host owns stay in the profile for a downgrade; this build lists the host instead. + * Only local rows: after a move the host's own row carries the same id and must stay listed. + */ +export function withoutWslMovedRepos(repos: T[]): T[] { + return withoutHidden(repos, (repo) => ({ repoId: repo.id })) +} + +export function withoutWslMovedFolderWorkspaces( + folderWorkspaces: T[] +): T[] { + return withoutHidden(folderWorkspaces, (folder) => ({ folderWorkspaceId: folder.id })) +} + +function withoutHidden(rows: T[], toRef: (row: T) => WslMoveProjectRef): T[] { const fence = loaded?.view if (rows.length === 0 || !fence?.readable) { return rows @@ -139,5 +312,10 @@ export function withoutWslMovedProjects(rows: T[], toRef: (row: T) => WslMove if (hidden.repoIds.size === 0 && hidden.folderWorkspaceIds.size === 0) { return rows } - return rows.filter((row) => !inScope(hidden, toRef(row))) + return rows.filter((row) => !isLocalWslMoveRow(row) || !inScope(hidden, toRef(row))) +} + +/** A row on this machine, the only kind a move takes. */ +export function isLocalWslMoveRow(row: HostedRow): boolean { + return !row.connectionId && isWslMoveSourceHost(row.executionHostId) } diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.test.ts new file mode 100644 index 00000000000..19e82ec6200 --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.test.ts @@ -0,0 +1,167 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../../../shared/constants' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' +import type { Store } from '../../persistence' +import { + closeTestStores, + createStore, + makeTerminalTab, + testState +} from '../../persistence-test-harness' +import { wslMoveCutoverFixture as cutover } from '../__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { ProfileStateSqliteAuthority } from '../profile-state/profile-state-sqlite-authority' +import { pruneWorktreeStateForRepo } from '../tracking-repos/repo-worktree-pruning' +import { wslMoveJournalDirectory } from './wsl-move-cutover-journal' +import { loadWslMoveFence, resetWslMoveFenceForTests } from './wsl-move-scope-guard' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { isEncryptionAvailable: () => false } +})) + +const MOVING = 'repo-1::/home/dev/repo-1' +const STAYING = 'repo-2::/home/dev/repo-2' + +function session(title: string, tabType: 'terminal' | 'editor'): WorkspaceSessionState { + return { + ...getDefaultWorkspaceSession(), + tabsByWorktree: { + [MOVING]: [makeTerminalTab({ id: 'moving-tab', ptyId: null, worktreeId: MOVING, title })], + [STAYING]: [makeTerminalTab({ id: 'staying-tab', ptyId: null, worktreeId: STAYING, title })] + }, + terminalLayoutsByTabId: { + 'moving-tab': { root: null, activeLeafId: null, expandedLeafId: null }, + 'staying-tab': { root: null, activeLeafId: null, expandedLeafId: null } + }, + activeTabTypeByWorktree: { [MOVING]: tabType, [STAYING]: tabType } + } +} + +const titles = (store: Store) => { + const { tabsByWorktree } = store.getWorkspaceSession() + return { + moving: tabsByWorktree[MOVING]?.map((tab) => tab.title), + staying: tabsByWorktree[STAYING]?.map((tab) => tab.title) + } +} + +let store: Store +beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'wsl-move-freeze-')) + store = createStore() + loadWslMoveFence(testState.dir) + store.setWorkspaceSession(session('slept', 'terminal')) +}) +afterEach(async () => { + vi.restoreAllMocks() + resetWslMoveFenceForTests() + await closeTestStores() + rmSync(testState.dir, { recursive: true, force: true }) +}) + +describe('WSL move session freeze', () => { + it("lets the quiesce's own Sleep write while the move is only fenced", () => { + seedWslMoveCutover(testState.dir, cutover('move-1', 'source-fenced')) + store.setWorkspaceSession(session('after-sleep', 'terminal')) + expect(titles(store)).toEqual({ moving: ['after-sleep'], staying: ['after-sleep'] }) + }) + + describe('once quiesced', () => { + beforeEach(() => { + seedWslMoveCutover(testState.dir, cutover('move-1', 'quiesced')) + }) + + it('keeps the moving workspace as the quiesce left it and takes the rest', () => { + store.setWorkspaceSession(session('edited', 'editor')) + expect(titles(store)).toEqual({ moving: ['slept'], staying: ['edited'] }) + expect(store.getWorkspaceSession().activeTabTypeByWorktree).toEqual({ + [MOVING]: 'terminal', + [STAYING]: 'editor' + }) + }) + + it('keeps it when a write omits the workspace entirely', () => { + const without = session('edited', 'editor') + delete without.tabsByWorktree[MOVING] + store.setWorkspaceSession(without) + expect(titles(store)).toEqual({ moving: ['slept'], staying: ['edited'] }) + }) + + it('keeps it through a patch and the before-unload stage', () => { + store.patchWorkspaceSession({ + activeTabTypeByWorktree: { [MOVING]: 'editor', [STAYING]: 'editor' } + }) + expect(store.getWorkspaceSession().activeTabTypeByWorktree).toEqual({ + [MOVING]: 'terminal', + [STAYING]: 'editor' + }) + store.stageWorkspaceSessionBeforeUnload(session('quit', 'editor')) + expect(titles(store)).toEqual({ moving: ['slept'], staying: ['quit'] }) + }) + }) + + it('keeps freezing through commit, so the retained rows survive for a downgrade', () => { + seedWslMoveCutover(testState.dir, cutover('move-1', 'destination-committed')) + store.setWorkspaceSession(session('edited', 'editor')) + expect(titles(store)).toEqual({ moving: ['slept'], staying: ['edited'] }) + }) + + it('freezes nothing, with one warning, when the journal cannot be read', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + const directory = wslMoveJournalDirectory(testState.dir) + mkdirSync(directory, { recursive: true }) + writeFileSync(join(directory, 'move-1.json'), '{not json') + loadWslMoveFence(testState.dir) + store.setWorkspaceSession(session('edited', 'editor')) + expect(titles(store)).toEqual({ moving: ['edited'], staying: ['edited'] }) + expect(warn).toHaveBeenCalledTimes(1) + }) +}) + +describe('WSL move session freeze: other session writers', () => { + beforeEach(() => { + seedWslMoveCutover(testState.dir, cutover('move-1', 'quiesced')) + }) + + it('keeps the frozen rows through a failed durable mutation that rolls the session back', async () => { + vi.spyOn(console, 'error').mockImplementation(() => undefined) + const refuse = () => { + throw new Error('disk refused') + } + const authority = ProfileStateSqliteAuthority.prototype + vi.spyOn(authority, 'writeSerializedDomains').mockImplementationOnce(refuse) + vi.spyOn(authority, 'writeCompleteSerializedDomains').mockImplementationOnce(refuse) + vi.spyOn(authority, 'writeSerializedState').mockImplementationOnce(refuse) + await expect( + store.runDurableMutation(() => { + store.setWorkspaceSession(session('mutated', 'editor')) + return { + value: undefined, + rollback: () => store.setWorkspaceSession(session('rolled-back', 'editor')) + } + }) + ).rejects.toThrow('disk refused') + expect(titles(store)).toEqual({ moving: ['slept'], staying: ['rolled-back'] }) + }) + + it('keeps the frozen rows when the repo is pruned', () => { + const state = getDefaultPersistedState(testState.dir) + state.workspaceSession = { + ...store.getWorkspaceSession(), + lastVisitedAtByWorktreeId: { [MOVING]: 1, [STAYING]: 1 } + } + pruneWorktreeStateForRepo(state, 'repo-1', null, () => undefined) + pruneWorktreeStateForRepo(state, 'repo-2', null, () => undefined) + expect(Object.keys(state.workspaceSession.tabsByWorktree)).toEqual([MOVING]) + }) + + it('keeps the frozen rows when the workspace owner is removed', () => { + store.removeWorkspaceSessionStateForWorktree(MOVING) + store.removeWorkspaceSessionStateForWorktree(STAYING) + expect(titles(store)).toEqual({ moving: ['slept'], staying: undefined }) + }) +}) diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.ts b/src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.ts new file mode 100644 index 00000000000..2b78f73d426 --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-session-freeze.ts @@ -0,0 +1,69 @@ +/** + * Once a move has paused a project, its workspaces' session stays exactly as the quiesce left it: + * that slept state is what the move carries to the host, so a later renderer write must neither + * change nor drop it. Every other workspace's rows, and the global fields, are taken as written. + */ +import { WORKSPACE_SESSION_FIELD_OWNERSHIP } from '../../../shared/workspace-session-host-field-ownership' +import { + mergeWorkspaceSessionArrayField, + mergeWorkspaceSessionRecordField +} from '../../../shared/workspace-session-host-records' +import { + filterWorkspaceRows, + withoutWorkspaces, + workspaceIdsNamedByPartition +} from '../../../shared/workspace-session-stranded-partition-adoption' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' +import { hasWslMoveFrozenSessions, isWslMoveWorkspaceSessionFrozen } from './wsl-move-scope-guard' + +export function keepWslMoveFrozenSessions( + next: WorkspaceSessionState, + prior: WorkspaceSessionState | undefined, + isFrozen: (workspaceId: string) => boolean +): WorkspaceSessionState { + const frozen = new Set() + for (const workspaceId of workspaceIdsNamedByPartition(next)) { + if (isFrozen(workspaceId)) { + frozen.add(workspaceId) + } + } + for (const workspaceId of prior ? workspaceIdsNamedByPartition(prior) : []) { + if (isFrozen(workspaceId)) { + frozen.add(workspaceId) + } + } + if (frozen.size === 0) { + return next + } + const merged = withoutWorkspaces(next, frozen) + if (!prior) { + return merged + } + const kept = filterWorkspaceRows(prior, (workspaceId) => frozen.has(workspaceId)) + for (const [field, ownership] of Object.entries(WORKSPACE_SESSION_FIELD_OWNERSHIP)) { + if (!isSessionField(field) || ownership === 'global' || ownership === 'hostPrivate') { + continue + } + if (ownership === 'worktreeArray') { + mergeWorkspaceSessionArrayField(merged, field, kept) + } else { + mergeWorkspaceSessionRecordField(merged, field, kept) + } + } + return merged +} + +function isSessionField(field: string): field is keyof WorkspaceSessionState { + return Object.hasOwn(WORKSPACE_SESSION_FIELD_OWNERSHIP, field) +} + +/** The store's write paths: a no-op unless some move has paused a project. */ +export function keepWslMoveFrozenSessionsForStore( + next: WorkspaceSessionState, + prior: WorkspaceSessionState | undefined +): WorkspaceSessionState { + if (!hasWslMoveFrozenSessions()) { + return next + } + return keepWslMoveFrozenSessions(next, prior, isWslMoveWorkspaceSessionFrozen) +} diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-shell-history-blobs.test.ts b/src/main/persistence/moving-wsl-projects/wsl-move-shell-history-blobs.test.ts new file mode 100644 index 00000000000..7c2c325c6cc --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-shell-history-blobs.test.ts @@ -0,0 +1,177 @@ +import { createHash } from 'node:crypto' +import { + appendFileSync, + mkdirSync, + mkdtempSync, + rmSync, + truncateSync, + utimesSync, + writeFileSync +} from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { + parseOrcadMigrationBlobDescriptor, + type OrcadMigrationBlobDescriptor +} from '../../../shared/orcad-migration-blob-transfer' +import { rewriteOrcadMigrationWorktreeId } from '../../ssh/orcad-migration-path-rewrite' +import { hashWorktreeId } from '../../terminal-history-id' +import { parseOrcadMigrationShellHistoryHeader } from '../migrating-orcad-catalog/orcad-migration-shell-history-format' +import { createUncToLinuxPathRewriter } from './wsl-move-path-rewriter' +import { + buildWslMoveShellHistoryBlobs, + type WslMoveShellHistoryBlobs +} from './wsl-move-shell-history-blobs' + +const SOURCE_ID = 'repo-a::\\\\wsl.localhost\\Ubuntu\\home\\dev\\app' +const DESTINATION_ID = rewriteOrcadMigrationWorktreeId( + SOURCE_ID, + createUncToLinuxPathRewriter('Ubuntu') +) +const SOURCE_FOLDER_ID = 'folder:workspace-1' + +let root: string + +function writeHistory(worktreeId: string, file: string, content: string | Buffer): string { + const directory = join(root, hashWorktreeId(worktreeId)) + mkdirSync(directory, { recursive: true }) + const path = join(directory, file) + writeFileSync(path, content) + return path +} + +function build( + worktrees = [{ sourceWorktreeId: SOURCE_ID, destinationWorktreeId: DESTINATION_ID }] +): Promise { + return buildWslMoveShellHistoryBlobs({ distro: 'Ubuntu', worktrees, historyRoot: root }) +} + +async function readAll( + blobs: WslMoveShellHistoryBlobs, + descriptor: OrcadMigrationBlobDescriptor, + chunk: number +): Promise { + const parts: Buffer[] = [] + for (let offset = 0; offset < descriptor.byteLength; offset += chunk) { + parts.push(await blobs.source.readChunk(descriptor, offset, chunk)) + } + return Buffer.concat(parts) +} + +describe('WSL move shell history blobs', () => { + beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'orca-wsl-move-history-')) + }) + + afterEach(() => { + rmSync(root, { recursive: true, force: true }) + }) + + it('keys each zsh/bash history by the host worktree id and carries that id in the header', async () => { + expect(DESTINATION_ID).toBe('repo-a::/home/dev/app') + writeHistory(SOURCE_ID, 'zsh_history', ': 1700000000:0;ls\n') + writeHistory(SOURCE_ID, 'bash_history', 'git status\n') + + const blobs = await build() + + const keys = blobs.descriptors.map((descriptor) => descriptor.key) + const hash = hashWorktreeId(DESTINATION_ID) + expect(keys).toEqual([`${hash}-zsh`, `${hash}-bash`]) + expect(keys.some((key) => key.startsWith(hashWorktreeId(SOURCE_ID)))).toBe(false) + for (const descriptor of blobs.descriptors) { + expect(parseOrcadMigrationBlobDescriptor(descriptor)).toEqual(descriptor) + // Read in pieces that straddle the header boundary. + const bytes = await readAll(blobs, descriptor, 7) + expect(bytes.length).toBe(descriptor.byteLength) + expect(createHash('sha256').update(bytes).digest('hex')).toBe(descriptor.sha256) + const header = parseOrcadMigrationShellHistoryHeader(descriptor.key, bytes, bytes.length) + expect(header.worktreeId).toBe(DESTINATION_ID) + } + const zsh = await readAll(blobs, blobs.descriptors[0], 1024) + expect(zsh.toString('utf8')).toBe( + `{"version":1,"worktreeId":"repo-a::/home/dev/app"}\n: 1700000000:0;ls\n` + ) + }) + + it('marks a bash history as the login shell only when no zsh history came with it', async () => { + writeHistory(SOURCE_ID, 'zsh_history', ': 1:0;ls\n') + writeHistory(SOURCE_ID, 'bash_history', 'ls\n') + writeHistory(SOURCE_FOLDER_ID, 'bash_history', 'make\n') + + const blobs = await build([ + { sourceWorktreeId: SOURCE_ID, destinationWorktreeId: DESTINATION_ID }, + { sourceWorktreeId: SOURCE_FOLDER_ID, destinationWorktreeId: SOURCE_FOLDER_ID } + ]) + + const loginShell = await Promise.all( + blobs.descriptors.map(async (descriptor) => { + const bytes = await readAll(blobs, descriptor, descriptor.byteLength) + return parseOrcadMigrationShellHistoryHeader(descriptor.key, bytes, bytes.length).loginShell + }) + ) + expect(loginShell).toEqual([false, false, true]) + }) + + it('skips missing and empty files and never sends fish', async () => { + writeHistory(SOURCE_ID, 'zsh_history', '') + writeHistory(SOURCE_ID, 'fish_history', '- cmd: ls\n') + writeHistory(SOURCE_FOLDER_ID, 'bash_history', 'make\n') + + const blobs = await build([ + { sourceWorktreeId: SOURCE_ID, destinationWorktreeId: DESTINATION_ID }, + { sourceWorktreeId: SOURCE_FOLDER_ID, destinationWorktreeId: SOURCE_FOLDER_ID }, + { sourceWorktreeId: 'repo-a::/never-opened', destinationWorktreeId: 'repo-a::/never-opened' } + ]) + + expect(blobs.descriptors.map((descriptor) => descriptor.key)).toEqual([ + `${hashWorktreeId(SOURCE_FOLDER_ID)}-bash` + ]) + }) + + it('blocks the move on a history over the per-blob cap instead of truncating it', async () => { + const path = writeHistory(SOURCE_ID, 'bash_history', '') + truncateSync(path, 64 * 1024 * 1024) + + await expect(build()).rejects.toThrow( + `orcad_migration_shell_history_too_large:${hashWorktreeId(DESTINATION_ID)}-bash` + ) + }) + + it('merges the \\\\wsl$ and \\\\wsl.localhost histories of one worktree oldest first', async () => { + const legacyId = SOURCE_ID.replace('wsl.localhost', 'wsl$') + const legacy = writeHistory(legacyId, 'bash_history', 'old one\nold two') + const current = writeHistory(SOURCE_ID, 'bash_history', 'new one\n') + utimesSync(legacy, 1_000, 1_000) + utimesSync(current, 2_000, 2_000) + + // The newer id is listed first; the merge still puts its commands last. + const blobs = await build([ + { sourceWorktreeId: SOURCE_ID, destinationWorktreeId: DESTINATION_ID }, + { sourceWorktreeId: legacyId, destinationWorktreeId: DESTINATION_ID } + ]) + + expect(blobs.descriptors.map((descriptor) => descriptor.key)).toEqual([ + `${hashWorktreeId(DESTINATION_ID)}-bash` + ]) + const [descriptor] = blobs.descriptors + const bytes = await readAll(blobs, descriptor, 5) + expect(createHash('sha256').update(bytes).digest('hex')).toBe(descriptor.sha256) + const header = parseOrcadMigrationShellHistoryHeader(descriptor.key, bytes, bytes.length) + expect(bytes.subarray(header.byteLength).toString('utf8')).toBe('old one\nold two\nnew one\n') + }) + + it('refuses a read once the file changed after its digest was taken', async () => { + const path = writeHistory(SOURCE_ID, 'bash_history', 'one\n') + const blobs = await build() + const [descriptor] = blobs.descriptors + appendFileSync(path, 'two\n') + + await expect(blobs.source.readChunk(descriptor, 0, 1024)).rejects.toThrow( + 'orcad_migration_source_blob_changed:shell-history' + ) + await expect( + blobs.source.readChunk({ ...descriptor, sha256: 'f'.repeat(64) }, 0, 1) + ).rejects.toThrow('orcad_migration_source_blob_changed:shell-history') + }) +}) diff --git a/src/main/persistence/moving-wsl-projects/wsl-move-shell-history-blobs.ts b/src/main/persistence/moving-wsl-projects/wsl-move-shell-history-blobs.ts new file mode 100644 index 00000000000..dd08768c5d9 --- /dev/null +++ b/src/main/persistence/moving-wsl-projects/wsl-move-shell-history-blobs.ts @@ -0,0 +1,251 @@ +/** + * The moved worktrees' zsh/bash history files (under the desktop's per-distro WSL history root) as + * `shell-history` migration blobs, keyed by the worktree id the destination host will use. + */ +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { open, stat } from 'node:fs/promises' +import { join } from 'node:path' +import { + assertOrcadMigrationBlobSet, + orcadMigrationBlobIdentity, + sameOrcadMigrationBlob, + type OrcadMigrationBlobDescriptor +} from '../../../shared/orcad-migration-blob-transfer' +import type { OrcadMigrationBlobSource } from '../../ssh/orcad-migration-blob-coordinator' +import { getHistoryRootWsl } from '../../terminal-history-paths' +import { hashWorktreeId } from '../../terminal-history-id' +import { + MAX_ORCAD_MIGRATION_SHELL_HISTORY_BYTES, + ORCAD_MIGRATION_SHELL_HISTORY_SHELLS, + encodeOrcadMigrationShellHistoryHeader, + orcadMigrationShellHistoryKey, + shellHistoryFileName +} from '../migrating-orcad-catalog/orcad-migration-shell-history-format' + +export type WslMoveShellHistoryWorktree = { + /** The id the desktop keyed history by: the UNC worktree id. */ + sourceWorktreeId: string + /** The same worktree's id on the host, after the mover's path rewrite. */ + destinationWorktreeId: string +} + +export type WslMoveShellHistoryRequest = { + distro: string + worktrees: readonly WslMoveShellHistoryWorktree[] + /** Defaults to the desktop's history root for `distro`. */ + historyRoot?: string +} + +export type WslMoveShellHistoryBlobs = { + descriptors: OrcadMigrationBlobDescriptor[] + source: OrcadMigrationBlobSource +} + +type DesktopHistoryFile = { path: string; fileBytes: number } + +/** A blob's bytes in order: the header, then one or more desktop history files. */ +type ShellHistorySegment = { bytes: Buffer } | DesktopHistoryFile + +type ShellHistoryBlob = { + descriptor: OrcadMigrationBlobDescriptor + segments: ShellHistorySegment[] +} + +const NEWLINE = 0x0a + +/** + * Missing and empty files send nothing. A file over the per-blob cap blocks the move: history is + * never truncated. The source refuses a read once a file's size moved, so a shell that wrote after + * quiesce fails the transfer instead of sending bytes the digest does not cover. + */ +export async function buildWslMoveShellHistoryBlobs( + request: WslMoveShellHistoryRequest +): Promise { + const root = request.historyRoot ?? getHistoryRootWsl(request.distro) + const blobs: ShellHistoryBlob[] = [] + for (const [destinationWorktreeId, sourceWorktreeIds] of groupByDestination(request.worktrees)) { + let zshSent = false + for (const shell of ORCAD_MIGRATION_SHELL_HISTORY_SHELLS) { + const files = await desktopHistoryFiles(root, sourceWorktreeIds, shellHistoryFileName(shell)) + if (files.length === 0) { + continue + } + const key = orcadMigrationShellHistoryKey(destinationWorktreeId, shell) + // zsh is listed first, so a bash file without a zsh sibling is known here. + const header = encodeOrcadMigrationShellHistoryHeader(destinationWorktreeId, { + loginShell: shell === 'bash' && !zshSent + }) + zshSent ||= shell === 'zsh' + const segments = await joinHistories(header, files) + const byteLength = segments.reduce((total, segment) => total + segmentBytes(segment), 0) + // A move blocker rather than a truncation: dropping old commands would silently lose them. + if (byteLength > MAX_ORCAD_MIGRATION_SHELL_HISTORY_BYTES) { + throw new Error( + `orcad_migration_shell_history_too_large:${key}: the ${shell} history of ${destinationWorktreeId} is ${byteLength} bytes, over the ${MAX_ORCAD_MIGRATION_SHELL_HISTORY_BYTES}-byte limit` + ) + } + blobs.push({ + descriptor: { + blobKind: 'shell-history', + key, + sha256: await digestSegments(segments), + byteLength + }, + segments + }) + } + } + const descriptors = blobs.map((blob) => blob.descriptor) + assertOrcadMigrationBlobSet(descriptors) + const byIdentity = new Map( + blobs.map((blob) => [orcadMigrationBlobIdentity(blob.descriptor), blob]) + ) + return { + descriptors, + source: { + readChunk: async (requested, offset, maxBytes) => { + const blob = byIdentity.get(orcadMigrationBlobIdentity(requested)) + if (!blob || !sameOrcadMigrationBlob(requested, blob.descriptor)) { + throw new Error(`orcad_migration_source_blob_changed:${requested.blobKind}`) + } + return readSegments(blob, offset, maxBytes) + } + } + } +} + +// The `\\wsl$\` and `\\wsl.localhost\` ids of one worktree rewrite to the same host id. +function groupByDestination( + worktrees: readonly WslMoveShellHistoryWorktree[] +): Map { + const groups = new Map() + for (const worktree of worktrees) { + const sources = groups.get(worktree.destinationWorktreeId) ?? [] + if (!sources.includes(worktree.sourceWorktreeId)) { + sources.push(worktree.sourceWorktreeId) + } + groups.set(worktree.destinationWorktreeId, sources) + } + return groups +} + +// Oldest first, so the newest commands end up last, where shells read them back from. +async function desktopHistoryFiles( + root: string, + sourceWorktreeIds: readonly string[], + fileName: string +): Promise { + const found: (DesktopHistoryFile & { mtimeMs: number })[] = [] + for (const sourceWorktreeId of sourceWorktreeIds) { + const path = join(root, hashWorktreeId(sourceWorktreeId), fileName) + const info = await regularFile(path) + if (info && info.size > 0) { + found.push({ path, fileBytes: info.size, mtimeMs: info.mtimeMs }) + } + } + return found + .sort((a, b) => a.mtimeMs - b.mtimeMs) + .map(({ path, fileBytes }) => ({ path, fileBytes })) +} + +// Without a newline between them, one file's last command would run into the next file's first. +async function joinHistories( + header: Buffer, + files: readonly DesktopHistoryFile[] +): Promise { + const segments: ShellHistorySegment[] = [{ bytes: header }] + for (const [index, file] of files.entries()) { + segments.push(file) + if (index < files.length - 1 && (await lastByte(file)) !== NEWLINE) { + segments.push({ bytes: Buffer.from('\n') }) + } + } + return segments +} + +async function lastByte(file: DesktopHistoryFile): Promise { + const [byte] = await readFileRange(file, file.fileBytes - 1, file.fileBytes) + return byte +} + +function segmentBytes(segment: ShellHistorySegment): number { + return 'bytes' in segment ? segment.bytes.length : segment.fileBytes +} + +// Null for a missing file or anything that is not a regular file. +async function regularFile(path: string): Promise<{ size: number; mtimeMs: number } | null> { + try { + const info = await stat(path) + return info.isFile() ? { size: info.size, mtimeMs: info.mtimeMs } : null + } catch { + return null + } +} + +async function digestSegments(segments: readonly ShellHistorySegment[]): Promise { + const hash = createHash('sha256') + for (const segment of segments) { + if ('bytes' in segment) { + hash.update(segment.bytes) + continue + } + let read = 0 + // `end` pins the digest to the size the descriptor declares even if the file grows meanwhile. + for await (const chunk of createReadStream(segment.path, { + start: 0, + end: segment.fileBytes - 1 + })) { + if (!Buffer.isBuffer(chunk)) { + throw new Error('orcad_migration_shell_history_read_failed') + } + hash.update(chunk) + read += chunk.length + } + if (read !== segment.fileBytes) { + throw new Error('orcad_migration_source_blob_changed:shell-history') + } + } + return hash.digest('hex') +} + +async function readSegments( + blob: ShellHistoryBlob, + offset: number, + maxBytes: number +): Promise { + const end = Math.min(offset + maxBytes, blob.descriptor.byteLength) + const parts: Buffer[] = [] + let segmentStart = 0 + for (const segment of blob.segments) { + const size = segmentBytes(segment) + const from = Math.max(offset - segmentStart, 0) + const to = Math.min(end - segmentStart, size) + if (to > from) { + parts.push( + 'bytes' in segment + ? segment.bytes.subarray(from, to) + : await readFileRange(segment, from, to) + ) + } + segmentStart += size + } + return Buffer.concat(parts) +} + +async function readFileRange(file: DesktopHistoryFile, from: number, to: number): Promise { + const handle = await open(file.path, 'r') + try { + if ((await handle.stat()).size !== file.fileBytes) { + throw new Error('orcad_migration_source_blob_changed:shell-history') + } + const buffer = Buffer.alloc(to - from) + const { bytesRead } = await handle.read(buffer, 0, buffer.length, from) + if (bytesRead !== buffer.length) { + throw new Error('orcad_migration_source_blob_changed:shell-history') + } + return buffer + } finally { + await handle.close() + } +} diff --git a/src/main/persistence/profile-state/profile-state-backup-worker.test.ts b/src/main/persistence/profile-state/profile-state-backup-worker.test.ts index a2d40b8328c..2f684a619cd 100644 --- a/src/main/persistence/profile-state/profile-state-backup-worker.test.ts +++ b/src/main/persistence/profile-state/profile-state-backup-worker.test.ts @@ -191,7 +191,10 @@ describe('profile state backup worker', () => { const { directory, job } = fixture() const failedWorker = script(directory, source) await expect( - runProfileStateBackupWorker(job, { workerPath: failedWorker, timeoutMs: 500 }) + runProfileStateBackupWorker(job, { + workerPath: failedWorker, + timeoutMs: error === 'timed out' ? 500 : undefined + }) ).rejects.toThrow(error) expect(existsSync(job.targetPath)).toBe(false) rmSync(directory, { recursive: true }) diff --git a/src/main/persistence/restoring-sessions/session-owner-removal.ts b/src/main/persistence/restoring-sessions/session-owner-removal.ts index 1cb7a44e004..1432a852c89 100644 --- a/src/main/persistence/restoring-sessions/session-owner-removal.ts +++ b/src/main/persistence/restoring-sessions/session-owner-removal.ts @@ -6,8 +6,15 @@ import { type ExecutionHostId } from '../../../shared/execution-host' import { workspaceSessionPartitionHostId } from '../../../shared/workspace-session-partition-owner' +import { isWslMoveWorkspaceSessionFrozen } from '../moving-wsl-projects/wsl-move-scope-guard' import { cloneWorkspaceSessionState, deleteOwnerKeyedSessionFields } from './session-owner-fields' +/** Owners whose rows may go: a paused WSL move keeps its rows for a downgrade. */ +export function withoutWslMoveFrozenOwners(ownerKeys: ReadonlySet): ReadonlySet { + const kept = [...ownerKeys].filter((ownerKey) => !isWslMoveWorkspaceSessionFrozen(ownerKey)) + return kept.length === ownerKeys.size ? ownerKeys : new Set(kept) +} + // Scans the pane-key-keyed maps, the close records and the shutdown list once, removing // every entry owned by a key matched by `isRemovedOwner` (or, for pty incarnations, whose // tab was removed). Kept separate from the O(1) deletes so a batch prune scans each @@ -81,6 +88,9 @@ export function removeWorkspaceSessionOwnerEverywhere( ownerKey: string, options: { advanceTerminalTopologyRevision?: boolean } = {} ): void { + if (isWslMoveWorkspaceSessionFrozen(ownerKey)) { + return + } state.workspaceSession = removeWorkspaceSessionOwner(state.workspaceSession, ownerKey, options)! const partitions = state.workspaceSessionsByHostId if (!partitions) { @@ -134,8 +144,9 @@ export function removeWorkspaceSessionOwners( export function removeWorkspaceSessionOwnersEverywhere( state: Pick, - ownerKeys: ReadonlySet + allOwnerKeys: ReadonlySet ): void { + const ownerKeys = withoutWslMoveFrozenOwners(allOwnerKeys) if (ownerKeys.size === 0) { return } diff --git a/src/main/persistence/terminal-topology/terminal-topology-commit.ts b/src/main/persistence/terminal-topology/terminal-topology-commit.ts index 756869d08d7..f5998264bc3 100644 --- a/src/main/persistence/terminal-topology/terminal-topology-commit.ts +++ b/src/main/persistence/terminal-topology/terminal-topology-commit.ts @@ -1,4 +1,4 @@ -import type { ExecutionHostId } from '../../../shared/execution-host' +import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../shared/execution-host' import type { PersistedState } from '../../../shared/persisted-state-types' import type { TerminalLeafMoveRequest, @@ -24,6 +24,7 @@ import { import type { DurableProfileStateMutation } from '../loading-store/store-runtime-state' import { planTerminalLeafMove, rekeyMovedLeafProfileRecords } from './terminal-leaf-move' import { assignWorkspaceSessionPartition } from './terminal-topology-membership' +import { keepWslMoveFrozenSessionsForStore } from '../moving-wsl-projects/wsl-move-session-freeze' // The commit boundary for terminal layout (tabs, panes, pane-to-PTY bindings). Wraps the close and // the pane move; the close transform still lives in runtime/ and other writers move here later. @@ -147,6 +148,17 @@ export type TerminalTopologyCommitContext = { } /** Writes `next`, returning a restore that puts the prior value back unless a later write replaced it. */ +// A paused WSL move keeps its local rows as the quiesce left them. +function keepFrozen( + hostId: ExecutionHostId, + session: WorkspaceSessionState, + context: TerminalTopologyCommitContext +): WorkspaceSessionState { + return hostId === LOCAL_EXECUTION_HOST_ID + ? keepWslMoveFrozenSessionsForStore(session, context.getSession(hostId)) + : session +} + function writeRestorable(read: () => V, write: (value: V) => void, next: V): () => void { const prior = read() write(next) @@ -173,7 +185,7 @@ function commitLeafMove( writeRestorable( () => context.getSession(hostId), (value) => context.markDirty(assignWorkspaceSessionPartition(state, hostId, value)), - session + keepFrozen(hostId, session, context) ) ) const rekeyed = rekeyMovedLeafProfileRecords(state, request) @@ -214,7 +226,7 @@ function commitPaneChange( const restore = writeRestorable( () => context.getSession(hostId), (value) => context.markDirty(assignWorkspaceSessionPartition(context.state, hostId, value)), - change.session + keepFrozen(hostId, change.session, context) ) return { value: change.value, rollback: restore } } diff --git a/src/main/persistence/tracking-repos/repo-worktree-pruning.ts b/src/main/persistence/tracking-repos/repo-worktree-pruning.ts index 2382cb6a371..6987f8b2da4 100644 --- a/src/main/persistence/tracking-repos/repo-worktree-pruning.ts +++ b/src/main/persistence/tracking-repos/repo-worktree-pruning.ts @@ -3,7 +3,10 @@ import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../shared/e import { parseWorkspaceKey } from '../../../shared/workspace-scope' import type { PersistedState } from '../../../shared/persisted-state-types' import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' -import { removeWorkspaceSessionOwners } from '../restoring-sessions/session-owner-removal' +import { + removeWorkspaceSessionOwners, + withoutWslMoveFrozenOwners +} from '../restoring-sessions/session-owner-removal' import { getExecutionHostIdFromWorktreeHostIdentity, getWorktreeIdFromHostIdentity, @@ -105,7 +108,10 @@ export function pruneWorktreeStateForRepo( const pruneLegacyLocalSession = hostId === null || hostId === LOCAL_EXECUTION_HOST_ID const pruneAllHostPartitions = hostId === null if (pruneLegacyLocalSession) { - state.workspaceSession = removeWorkspaceSessionOwners(state.workspaceSession, ownerKeysToPrune)! + state.workspaceSession = removeWorkspaceSessionOwners( + state.workspaceSession, + withoutWslMoveFrozenOwners(ownerKeysToPrune) + )! } if (state.workspaceSessionsByHostId) { for (const [partitionHostId, session] of Object.entries(state.workspaceSessionsByHostId)) { diff --git a/src/main/pi/pi-transcript-import.ts b/src/main/pi/pi-transcript-import.ts new file mode 100644 index 00000000000..d2db4dca167 --- /dev/null +++ b/src/main/pi/pi-transcript-import.ts @@ -0,0 +1,35 @@ +import { homedir } from 'node:os' +import { join } from 'node:path' +import { sessionIdFromFileName } from '../ai-vault/session-scanner-accumulator' +import { walkSessionFiles } from '../ai-vault/session-scanner-discovery' +import { piSessionHistorySessionsDir } from '../ai-vault/session-scanner-agent-sources' +import { directoryTranscriptImport } from '../native-chat/structured-agent-cli-conversations' +import { readPiSessionHeader } from './rpc-launch-resolution' + +export const PI_TRANSCRIPT_IMPORT = directoryTranscriptImport({ + // Session History's host source is authoritative, including when the chat chose another home. + accountHomeCandidates: () => [ + process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), '.pi', 'agent') + ], + // The file Session History lists under this id. + findTranscript: async ({ providerSessionId }) => { + const files = await walkSessionFiles(piSessionHistorySessionsDir(), 'pi', [], { + extensions: new Set(['.jsonl']), + filePredicate: (path) => sessionIdFromFileName(path) === providerSessionId + }) + if (files.length !== 1) { + return null + } + const file = files[0] + if (!file) { + return null + } + const header = await readPiSessionHeader(file) + if (header?.id !== providerSessionId) { + return null + } + return file + }, + // A Pi chat's handle names its session file, not the id. + handleNamesTranscript: true +}) diff --git a/src/main/pi/rpc-launch-resolution.ts b/src/main/pi/rpc-launch-resolution.ts index aa639e5315a..6ece66fa81c 100644 --- a/src/main/pi/rpc-launch-resolution.ts +++ b/src/main/pi/rpc-launch-resolution.ts @@ -17,6 +17,7 @@ import { } from '../native-chat/structured-agent-command-resolution' import type { StructuredAgentLaunchPartResolver } from '../runtime/structured-agent-launch-composition' import { isWindowsProcessStartTimeAvailable } from '../windows/windows-process-table' +import { sessionIdFromFileName } from '../ai-vault/session-scanner-accumulator' import { PI_RPC_SESSION_ENV_TO_DELETE, withoutPiCallerEnv, @@ -66,9 +67,15 @@ export function piRpcVersionSupported( return probe({ program: launch.command, cwd: launch.cwd, env: launch.env }, supportsPiRpcVersion) } -const headerSchema = z.object({ type: z.literal('session'), cwd: z.string().min(1) }) +const headerSchema = z.object({ + type: z.literal('session'), + cwd: z.string().min(1), + id: z.string().optional() +}) -async function sessionDirectory(file: string): Promise { +export async function readPiSessionHeader( + file: string +): Promise<{ cwd: string; id?: string } | null> { let descriptor try { descriptor = await open(file, 'r') @@ -86,13 +93,13 @@ async function sessionDirectory(file: string): Promise { if (lf === -1) { throw new Error('Pi session header is incomplete or oversized') } - return headerSchema.parse(JSON.parse(data.slice(0, lf))).cwd + return headerSchema.parse(JSON.parse(data.slice(0, lf))) } finally { await descriptor.close() } } -async function sameDirectory(left: string, right: string): Promise { +export async function samePiDirectory(left: string, right: string): Promise { const canonical = async (value: string): Promise => realpath(value).catch(() => normalize(value)) const [a, b] = await Promise.all([canonical(left), canonical(right)]) @@ -159,10 +166,20 @@ export function piRpcLaunchPart( throw new Error('Pi resume handle is not a host session file') } const source = previous.handle.nativeId - const directory = await sessionDirectory(source) - if (directory === null) { + const header = await readPiSessionHeader(source) + if ( + previous.origin === 'adopted' && + (!header || + header.id !== sessionIdFromFileName(source) || + !(await samePiDirectory(header.cwd, cwd))) + ) { + throw agentSessionRefusalError('agent_session_identity_required', { + reason: 'transcriptNotFound' + }) + } + if (header === null) { replacement = previous.origin === 'created' ? 'unsaved' : 'restore-failed' - } else if (await sameDirectory(directory, cwd)) { + } else if (await samePiDirectory(header.cwd, cwd)) { sessionFile = source } else { forkFile = source diff --git a/src/main/pi/rpc-runtime-registration.ts b/src/main/pi/rpc-runtime-registration.ts index 4239948c65e..ed94a89eb52 100644 --- a/src/main/pi/rpc-runtime-registration.ts +++ b/src/main/pi/rpc-runtime-registration.ts @@ -20,6 +20,7 @@ import { import { PiRpcSessionAdapter } from './rpc-session-adapter' import { createPiModelCatalogProbe } from './rpc-model-catalog-probe' import { PI_SESSION_HISTORY } from './pi-session-history' +import { PI_TRANSCRIPT_IMPORT } from './pi-transcript-import' function createPiRpcAdapter(context: StructuredAgentAdapterContext): StructuredAgentRuntimeAdapter { const { deps } = context @@ -63,7 +64,7 @@ export const PI_RPC_RUNTIME_REGISTRATION: StructuredAgentRuntimeRegistration = { : {}) }) }), - transcriptImport: null, + transcriptImport: PI_TRANSCRIPT_IMPORT, sessionHistory: PI_SESSION_HISTORY, supportsLocation: supportsSupervisedProviderChildLocation, launchCheck: { diff --git a/src/main/provider-process/provider-process-supervisor.integration.test.ts b/src/main/provider-process/provider-process-supervisor.integration.test.ts index 98e03a679d5..141935da0f6 100644 --- a/src/main/provider-process/provider-process-supervisor.integration.test.ts +++ b/src/main/provider-process/provider-process-supervisor.integration.test.ts @@ -348,15 +348,28 @@ describe.runIf(process.platform !== 'win32')('POSIX provider supervisor processe }) it('never spawns the provider when its owner is not its parent at start', async () => { - const marker = join(tempDir(), 'provider-started') + const dir = tempDir() + const marker = join(dir, 'provider-spawned') + const preload = join(dir, 'record-spawn.js') + writeFileSync( + preload, + String.raw` + const childProcess = require('node:child_process') + const spawn = childProcess.spawn + childProcess.spawn = (...args) => { + require('node:fs').writeFileSync(process.env.ORCA_TEST_PROVIDER_PID_FILE, 'spawned') + return spawn(...args) + } + ` + ) const { exit } = launchSupervisor( { ownerPid: process.pid === 1 ? 2 : 1 }, - {}, - { command: 'touch', args: [marker] } + { ORCA_TEST_PROVIDER_PID_FILE: marker }, + { command: 'touch', args: [join(dir, 'provider-started')] }, + ['--require', preload] ) await expect(exit).resolves.toEqual({ code: 1, signal: null }) - await new Promise((resolve) => setTimeout(resolve, 200)) expect(existsSync(marker)).toBe(false) }) @@ -397,10 +410,12 @@ describe.runIf(process.platform !== 'win32')('POSIX provider supervisor processe expect(alive(provider)).toBe(false) }) - it('gives a provider 1 s after stdin end, then 3 s after SIGTERM before SIGKILL', async () => { + it('gives a provider its stdin-end grace, then its SIGTERM grace before SIGKILL', async () => { const signalFile = join(tempDir(), 'provider-sigterm-at') + const stdinEndGraceMs = 200 + const sigtermGraceMs = 300 const { supervisor, exit } = launchSupervisor( - {}, + { stdinEndGraceMs, sigtermGraceMs }, { ORCA_TEST_PROVIDER_SIGNAL_FILE: signalFile }, { command: process.execPath, args: ['-e', RECORDS_SIGTERM_PROVIDER] } ) @@ -414,9 +429,9 @@ describe.runIf(process.platform !== 'win32')('POSIX provider supervisor processe expect(exited).toEqual({ code: 137, signal: null }) // Timers may fire a tick early against another process's clock. - expect(signalledAt - endedAt).toBeGreaterThanOrEqual(1_000 - 20) - expect(exitedAt - signalledAt).toBeGreaterThanOrEqual(3_000 - 20) - expect(exitedAt - endedAt).toBeLessThan(PROVIDER_SUPERVISOR_MAX_STOP_MS + 1_000) + expect(signalledAt - endedAt).toBeGreaterThanOrEqual(stdinEndGraceMs - 20) + expect(exitedAt - signalledAt).toBeGreaterThanOrEqual(sigtermGraceMs - 20) + expect(exitedAt - endedAt).toBeLessThan(stdinEndGraceMs + sigtermGraceMs + 1_000) expect(alive(provider)).toBe(false) }) @@ -513,10 +528,10 @@ describe.runIf(process.platform !== 'win32')('POSIX provider supervisor processe it('lets a one-shot answer after its stdin ends and relays its exit code', async () => { const signalFile = join(tempDir(), 'provider-signal') const { supervisor, exit } = launchSupervisor( - { lifetime: 'one-shot' }, + { lifetime: 'one-shot', stdinEndGraceMs: 100 }, { ORCA_TEST_PROVIDER_SIGNAL_FILE: signalFile, - ORCA_TEST_PROVIDER_ANSWER_DELAY_MS: String(PROVIDER_STDIN_END_GRACE_MS * 1.5) + ORCA_TEST_PROVIDER_ANSWER_DELAY_MS: '250' }, { command: process.execPath, args: ['-e', ONE_SHOT_PROVIDER] } ) @@ -536,12 +551,13 @@ describe.runIf(process.platform !== 'win32')('POSIX provider supervisor processe ] as const)( 'reaps a one-shot that ignores its stdin end when its owner %s', async (_, signal, env) => { + const stdinEndGraceMs = 100 const { owner, pids } = await launchUnderOwner( - { lifetime: 'one-shot', sigtermGraceMs: 300 }, + { lifetime: 'one-shot', stdinEndGraceMs, sigtermGraceMs: 300 }, { ORCA_TEST_OWNER_ENDS_STDIN: '1', ...env } ) // Past the session grace: a stdin end alone must not have stopped it. - await new Promise((resolve) => setTimeout(resolve, PROVIDER_STDIN_END_GRACE_MS + 300)) + await new Promise((resolve) => setTimeout(resolve, stdinEndGraceMs + 150)) expect(alive(-pids.provider)).toBe(true) owner.kill(signal) diff --git a/src/main/providers/local-pty-launch-plan.ts b/src/main/providers/local-pty-launch-plan.ts index f82dd52e869..cb03f10ef75 100644 --- a/src/main/providers/local-pty-launch-plan.ts +++ b/src/main/providers/local-pty-launch-plan.ts @@ -2,6 +2,7 @@ import { win32 as pathWin32 } from 'node:path' import { recognizeAgentProcessFromCommandLine } from '../../shared/agent-process-recognition' import { WINDOWS_GIT_BASH_SHELL } from '../../shared/windows-terminal-shell' import { resolveWindowsGitBashShellPath } from '../git-bash' +import { resolveUnixPaneShellPath } from '../daemon/shell-ready' import { getDefaultWslDistro, parseWslPath } from '../wsl' import { getDefaultCwd, @@ -234,12 +235,11 @@ export function createLocalPtyLaunchPlan( if (process.platform === 'win32') { return createWindowsLocalPtyLaunchPlan(seed, getOptions) } - const shellPath = - args.shellOverride || - getOptions().getDefaultShell?.()?.trim() || - args.env?.SHELL || - process.env.SHELL || - '/bin/zsh' + const shellPath = resolveUnixPaneShellPath( + args.env ?? {}, + args.shellOverride, + getOptions().getDefaultShell?.() + ) return finalizeLocalPtyLaunchPlan(seed, { shellPath, // Why: shellOverride here is already resolved from the setting, so it cannot diff --git a/src/main/providers/pty-provider-contract.ts b/src/main/providers/pty-provider-contract.ts index 1f17bc05a44..b16728b4423 100644 --- a/src/main/providers/pty-provider-contract.ts +++ b/src/main/providers/pty-provider-contract.ts @@ -264,6 +264,8 @@ export type IPtyProvider = { incarnationId?: PtyIncarnationId /** Absent when the provider predates exit causes; readers must not infer one from `code`. */ cause?: TerminalExitCause + /** Main ended the session to restart the daemon; the pane stays for its next process. */ + replacedByRestart?: true }) => void ): () => void } diff --git a/src/main/repo-backfill-wsl-move.test.ts b/src/main/repo-backfill-wsl-move.test.ts new file mode 100644 index 00000000000..4c18b5c3659 --- /dev/null +++ b/src/main/repo-backfill-wsl-move.test.ts @@ -0,0 +1,127 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { getRepoExecutionHostId, type ExecutionHostId } from '../shared/execution-host' +import type { Repo } from '../shared/repo-types' +import { closeTestStores, createStore, testState } from './persistence-test-harness' +import { wslMoveCutoverFixture as cutover } from './persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from './persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + loadWslMoveFence, + resetWslMoveFenceForTests +} from './persistence/moving-wsl-projects/wsl-move-scope-guard' +import { probeGitRemoteIdentity } from './repo-git-remote-identity' +import { + enrichMissingRepoGitRemoteIdentities, + flushRepoGitRemoteIdentityEnrichmentForTests, + resetRepoGitRemoteIdentityEnrichmentForTests +} from './repo-git-remote-identity-enrichment' +import { RuntimeRepositoryForkBackfill } from './runtime/runtime-repository-fork-backfill' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { isEncryptionAvailable: () => false } +})) +vi.mock('./repo-git-remote-identity', () => ({ probeGitRemoteIdentity: vi.fn() })) +vi.mock('./github/client', () => ({ + getRepoUpstream: vi.fn(async () => ({ owner: 'acme', repo: 'app' })) +})) +vi.mock('./repo-icon-autodetect', () => ({ detectGitHubAvatarIcon: vi.fn(async () => null) })) + +const MOVING = 'repo-1' +const STAYING = 'repo-2' + +function repo(id: string, overrides: Partial = {}): Repo { + return { + id, + path: `/ws/${id}`, + displayName: id, + badgeColor: '#000', + addedAt: 0, + kind: 'git', + ...overrides + } +} + +beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'wsl-move-backfill-')) + loadWslMoveFence(testState.dir) + seedWslMoveCutover(testState.dir, cutover('move-1', 'source-fenced')) + vi.mocked(probeGitRemoteIdentity).mockResolvedValue({ + status: 'resolved', + identity: { canonicalKey: 'github.com/acme/app', remoteName: 'origin', remoteUrl: 'u' } + }) +}) +afterEach(async () => { + resetWslMoveFenceForTests() + resetRepoGitRemoteIdentityEnrichmentForTests() + await closeTestStores() + rmSync(testState.dir, { recursive: true, force: true }) +}) + +describe('git remote identity enrichment under a WSL move', () => { + it('skips only the moving local repo, and retries it once the move settles', async () => { + const rows = [ + repo(MOVING), + repo(STAYING), + repo(MOVING, { connectionId: 'target-1', executionHostId: 'ssh:target-1' }) + ] + const store = { + getRepos: () => rows, + updateRepo: (id: string, updates: Partial, hostId?: ExecutionHostId) => { + const row = rows.find( + (candidate) => candidate.id === id && getRepoExecutionHostId(candidate) === hostId + ) + return row ? Object.assign(row, updates) : null + } + } + + enrichMissingRepoGitRemoteIdentities(store) + await flushRepoGitRemoteIdentityEnrichmentForTests() + + expect(rows.map((row) => row.gitRemoteIdentity?.canonicalKey)).toEqual([ + undefined, + 'github.com/acme/app', + 'github.com/acme/app' + ]) + expect(probeGitRemoteIdentity).not.toHaveBeenCalledWith( + '/ws/repo-1', + 'local', + expect.anything() + ) + + resetWslMoveFenceForTests() + enrichMissingRepoGitRemoteIdentities(store) + await flushRepoGitRemoteIdentityEnrichmentForTests() + expect(rows[0].gitRemoteIdentity?.canonicalKey).toBe('github.com/acme/app') + }) +}) + +describe('fork upstream backfill under a WSL move', () => { + it('leaves the moving repo unset for a later run and fills the rest', async () => { + const store = createStore() + store.addRepo(repo(MOVING)) + store.addRepo(repo(STAYING)) + + await new RuntimeRepositoryForkBackfill(() => store, vi.fn()).run() + + expect(store.getRepo(MOVING)?.upstream).toBeUndefined() + expect(store.getRepo(STAYING)?.upstream).toEqual({ owner: 'acme', repo: 'app' }) + }) + + it('fills the host row, not the hidden local row with the same id', async () => { + seedWslMoveCutover(testState.dir, cutover('move-1', 'destination-committed')) + const store = createStore() + store.addRepo(repo(MOVING)) + store.addRepo(repo(MOVING, { executionHostId: 'runtime:env-1' })) + + await new RuntimeRepositoryForkBackfill(() => store, vi.fn()).run() + + const rows = store.getRepos().filter((row) => row.id === MOVING) + expect(rows.map((row) => [getRepoExecutionHostId(row), row.upstream])).toEqual([ + ['local', undefined], + ['runtime:env-1', { owner: 'acme', repo: 'app' }] + ]) + }) +}) diff --git a/src/main/repo-git-remote-identity-enrichment.ts b/src/main/repo-git-remote-identity-enrichment.ts index f1cea725efc..e222c33d9a9 100644 --- a/src/main/repo-git-remote-identity-enrichment.ts +++ b/src/main/repo-git-remote-identity-enrichment.ts @@ -10,6 +10,7 @@ import { isUnresolvedSshHostAlias } from '../shared/git-remote-host-alias' import { getProjectProviderIdentity } from '../shared/project-host-setup-projection' import { getStoredRepoExecutionHostId } from './repo-execution-host' import { probeGitRemoteIdentity } from './repo-git-remote-identity' +import { isWslMoveRepoBlocked } from './persistence/moving-wsl-projects/wsl-move-background-backoff' const NO_IDENTITY_RETRY_TTL_MS = 5 * 60 * 1000 // Why 6h: a resolved identity only changes when a remote is added or the project is renamed or @@ -158,7 +159,8 @@ function writeIdentity( async function enrichRepoGitRemoteIdentity(store: RepoIdentityStore, repo: Repo): Promise { const hostId = getRepoProbeHostId(repo) - if (!hostId) { + // No probe and no deadline: the next sweep after the move picks it up. + if (!hostId || isWslMoveRepoBlocked(repo)) { return false } const locationKey = getRepoLocationKey(repo) @@ -186,6 +188,11 @@ async function enrichRepoGitRemoteIdentity(store: RepoIdentityStore, repo: Repo) if (controller.signal.aborted) { return false } + // A move started during the probe: drop the answer and its deadline so the next sweep retries. + if (isWslMoveRepoBlocked(repo)) { + probeRetryAfterByLocation.delete(locationKey) + return false + } if (result.status !== 'resolved') { // Why: repos without a parseable remote are common; cache misses briefly so // list calls stay cheap while still allowing recent remote changes to land. diff --git a/src/main/runtime/agent-launch-scope-runtime.test.ts b/src/main/runtime/agent-launch-scope-runtime.test.ts index 3070f36c460..1a9eb1495ad 100644 --- a/src/main/runtime/agent-launch-scope-runtime.test.ts +++ b/src/main/runtime/agent-launch-scope-runtime.test.ts @@ -97,15 +97,15 @@ it("answers from main's WSL cache, which the window may not have read yet", () = expect(claudeRuntime(store({}), scope(SHARE, { repo: repo(SHARE) })).platform).toBe('linux') }) -// TEMPORARY parity: the host finds a repo's project by its source repos only, as main does. -it("finds a repo's project by its source repos, not a project named after the repo", () => { +// The window's lookup: a project named after the repo counts as its project, as does one listing it. +it("finds a repo's project the way the window does, by its id too", () => { const projects = [ { id: 'r', sourceRepoIds: [], localWindowsRuntimePreference: UBUNTU }, { id: 'p', sourceRepoIds: ['r'], localWindowsRuntimePreference: { kind: 'windows-host' } } ] const fake = { getProjects: () => projects, getSettings: () => ({}), getRepo: () => repo(WIN) } // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only these members. - expect(agentLaunchPlatformForScope(fake as never, scope(WIN, { repo: repo(WIN) }))).toBe('win32') + expect(agentLaunchPlatformForScope(fake as never, scope(WIN, { repo: repo(WIN) }))).toBe('linux') }) // Relied on by C2: the pane's pty:spawn takes the fact as is, for quoting and shell alike. diff --git a/src/main/runtime/agent-launch-scope-runtime.ts b/src/main/runtime/agent-launch-scope-runtime.ts index 4edcfedfaa7..fe16ea74cb5 100644 --- a/src/main/runtime/agent-launch-scope-runtime.ts +++ b/src/main/runtime/agent-launch-scope-runtime.ts @@ -69,22 +69,6 @@ export function agentLaunchProjectRuntime( return pathDecides ? undefined : runtime } -/** TEMPORARY parity: main checked an OpenCode model on the platform it quoted for, which in a - * `\\wsl$` repo not set to WSL was the Windows host. */ -export function openCodeModelCheckPlatform( - store: LaunchScopeRuntimeStore, - scope: TerminalWorkspaceLaunchScope, - platform: NodeJS.Platform -): NodeJS.Platform { - return scope.repo && !scope.connectionId && isWslUncPath(scope.path) - ? executionRuntimePlatform( - resolveLocalProjectRuntimeForWorktreeId(store, scope.id), - undefined, - process.platform - ) - : platform -} - /** The one runtime fact for an agent launched into this workspace. Throws, under * `PROJECT_RUNTIME_REPAIR_REQUIRED_CODE` and in the settings page's words, when its runtime needs * repair. */ diff --git a/src/main/runtime/migrated-pane-restore-cwd.ts b/src/main/runtime/migrated-pane-restore-cwd.ts new file mode 100644 index 00000000000..502f6fee3b3 --- /dev/null +++ b/src/main/runtime/migrated-pane-restore-cwd.ts @@ -0,0 +1,90 @@ +import { constants } from 'node:fs' +import { access, stat } from 'node:fs/promises' +import type { RuntimeStore } from './runtime-store-contract' + +type RestoreHintSource = Pick< + RuntimeStore, + 'peekOrcadMigrationTerminalRestoreHint' | 'consumeOrcadMigrationTerminalRestoreHint' +> + +/** Non-null `connectionId` is an SSH workspace, whose folders this host's filesystem cannot see. */ +type RestoreHintWorkspace = { id: string; connectionId?: string | null } + +export type MigratedPaneRestoreHint = { + peekCwd: (requestedCwd: string | null | undefined) => Promise + consume: () => void + /** A spawn-commit callback that spends the hint first. */ + consumeThen: (onCommitted?: () => void) => () => void +} + +/** One create's view of its pane's hint: peeked to pick the cwd, consumed once the pane spawns. */ +export function migratedPaneRestoreHint( + store: RestoreHintSource | null | undefined, + workspace: RestoreHintWorkspace, + paneKey: string +): MigratedPaneRestoreHint { + const consume = (): void => consumeMigratedPaneRestoreHint(store, paneKey) + return { + peekCwd: (requestedCwd) => peekMigratedPaneRestoreCwd(store, workspace, paneKey, requestedCwd), + consume, + consumeThen: (onCommitted) => () => { + consume() + onCommitted?.() + } + } +} + +/** + * The folder a moved pane last used, for a default-cwd create. Only peeks: the hint is consumed + * when the pane spawns (see `consumeMigratedPaneRestoreHint`), so a failed create keeps it. + */ +export async function peekMigratedPaneRestoreCwd( + store: RestoreHintSource | null | undefined, + workspace: RestoreHintWorkspace, + paneKey: string, + requestedCwd: string | null | undefined, + directoryUsable: (path: string) => Promise = isSearchableDirectory +): Promise { + // Hints are Linux paths for a WSL host; a Windows process never opens one. + if (requestedCwd?.trim() || workspace.connectionId || process.platform === 'win32') { + return null + } + let cwd: string | null + try { + cwd = store?.peekOrcadMigrationTerminalRestoreHint?.(paneKey, workspace.id) ?? null + } catch (error) { + // An unreadable hint store must not block opening the terminal. + console.warn('[terminal-create] restore hint unavailable:', error) + return null + } + if (!cwd) { + return null + } + return (await directoryUsable(cwd)) ? cwd : null +} + +/** Once the pane spawned, whatever folder it opened in was its restore; the hint is spent. */ +export function consumeMigratedPaneRestoreHint( + store: RestoreHintSource | null | undefined, + paneKey: string +): void { + try { + store?.consumeOrcadMigrationTerminalRestoreHint?.(paneKey) + } catch (error) { + // The terminal already opened; a hint left behind is retried on the next spawn. + console.warn('[terminal-create] restore hint not consumed:', error) + } +} + +// A shell cannot start in a directory it cannot search, so that counts as gone. +async function isSearchableDirectory(path: string): Promise { + try { + if (!(await stat(path)).isDirectory()) { + return false + } + await access(path, constants.X_OK) + return true + } catch { + return false + } +} diff --git a/src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts b/src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts index 023d6d3ba09..fb35d9e9e94 100644 --- a/src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts +++ b/src/main/runtime/mobile-subscribe-idle-pane-hydrate.test.ts @@ -1,3 +1,4 @@ +import './rpc/unused-default-rpc-methods.test-fixture' /** * A phone subscribing to an idle reattached PTY must get its first snapshot on the phone grid. * diff --git a/src/main/runtime/opencode-wsl-folder-launch-host.test.ts b/src/main/runtime/opencode-wsl-folder-launch-host.test.ts index ad38c368385..8a94f8cf48a 100644 --- a/src/main/runtime/opencode-wsl-folder-launch-host.test.ts +++ b/src/main/runtime/opencode-wsl-folder-launch-host.test.ts @@ -1,3 +1,4 @@ +import './rpc/unused-default-rpc-methods.test-fixture' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' import { RpcDispatcher } from './rpc/dispatcher' @@ -86,12 +87,13 @@ it('refuses an agent whose WSL distro is gone, under its own code', async () => expect(response).toMatchObject({ error: { code: PROJECT_RUNTIME_REPAIR_REQUIRED_CODE, message } }) }) -// TEMPORARY parity: in a `\\wsl$` repo set to Windows host, OpenCode's model is checked on Windows, -// as on main; its command is still written for Linux, where it runs. -it('checks an OpenCode model on Windows in a WSL-share repo set to Windows host', async () => { +// OpenCode runs in WSL in a `\\wsl$` repo, where Orca can't verify a model yet, so picking one is +// refused rather than checked on Windows. +it('refuses an OpenCode model in a WSL-share repo set to Windows host', async () => { const shareScope = { ...scope, id: 's', path: share.path, repo: share, folderWorkspace: null } Object.assign(runtime, { resolveTerminalWorkspaceLaunchScope: async () => shareScope }) const request = { clientOperationId, worktree: 'id:s', agent: 'opencode' } as const - await runtime.createAgentSession({ ...request, launchPreferences: { model: 'm' } }) - expect(spawned()).toEqual(['linux']) + const picked = runtime.createAgentSession({ ...request, launchPreferences: { model: 'm' } }) + await expect(picked).rejects.toThrow('cannot verify this OpenCode') + expect(spawned()).toEqual([]) }) diff --git a/src/main/runtime/orca-runtime-create-agent-session.ts b/src/main/runtime/orca-runtime-create-agent-session.ts index 0d0e2500912..a74335fe285 100644 --- a/src/main/runtime/orca-runtime-create-agent-session.ts +++ b/src/main/runtime/orca-runtime-create-agent-session.ts @@ -14,7 +14,6 @@ import { createHash } from 'node:crypto' import { isTuiAgentEnabled } from '../../shared/tui-agent-selection' import { resolveAgentStartupPlanInputs } from '../../shared/agent-startup-plan-inputs' import { agentLaunchQuotingShell } from '../../shared/agent-launch-runtime' -import { openCodeModelCheckPlatform } from './agent-launch-scope-runtime' import { buildExecutionHostAgentStartupPlan } from '../opencode/opencode-model-startup-plan' import type { RuntimeTerminalCreate } from '../../shared/runtime-types' import type { @@ -147,11 +146,6 @@ export class OrcaRuntimeWithCreateAgentSession extends OrcaRuntimeWithGetAgentSe }) const startup = await buildExecutionHostAgentStartupPlan({ inputs: startupArgs, - modelCheckPlatform: openCodeModelCheckPlatform( - this.store!, - workspace, - launchRuntime.platform - ), cwd: startupCwd ?? workspace.path, prompt: request.prompt ?? '', promptDelivery: request.promptDelivery, diff --git a/src/main/runtime/orca-runtime-create-terminal.ts b/src/main/runtime/orca-runtime-create-terminal.ts index 3143e3e4866..434762e4149 100644 --- a/src/main/runtime/orca-runtime-create-terminal.ts +++ b/src/main/runtime/orca-runtime-create-terminal.ts @@ -3,9 +3,10 @@ import { OrcaRuntimeWithTerminalCreateDeduplication } from './orca-runtime-termi import * as dependencies from './orca-runtime-create-terminal-dependencies' import { createDesktopTerminal } from './orca-runtime-create-terminal-desktop' import { buildRuntimeAgentTeamsLaunchPlan } from './terminal/launch/agent-teams-launch-plan' -import { createPtySpawnCommitReporter } from './orca-runtime-report-pty-spawn-commit' +import { createIdempotentCallback } from './orca-runtime-report-pty-spawn-commit' import { recordPtySurface, spawnSurfaceClaimSequence } from './pty-recorded-surface-topology' import { agentLaunchTokenFor } from '../agent-launch/agent-launch-token' +import { migratedPaneRestoreHint } from './migrated-pane-restore-cwd' export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreateDeduplication { async createTerminal( @@ -31,9 +32,7 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate } const workspace = await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector, created) const launchOpts = await this.resolveAgentTerminalCreateOptions(workspace, opts) - const reportPtySpawnCommitted = createPtySpawnCommitReporter(launchOpts.onPtySpawnCommitted) - const cwd = - this.resolveWorkspaceTerminalStartupCwd(workspace, launchOpts.cwd) ?? workspace.path + let cwd = this.resolveWorkspaceTerminalStartupCwd(workspace, launchOpts.cwd) ?? workspace.path const session = this.getWorkspaceSessionForWorktree(workspace.id) // The window's latest published active group is where its reveal places the tab. const groupId = @@ -54,21 +53,21 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate let preAllocatedHandle = launchOpts.preAllocatedHandle ?? this.createPreAllocatedTerminalHandle() let { tabId, leafId, paneKey } = dependencies.allocateTerminalPaneIdentity(launchOpts) + const restoreHint = migratedPaneRestoreHint(this.store, workspace, paneKey) + const reportPtySpawnCommitted = createIdempotentCallback( + restoreHint.consumeThen(launchOpts.onPtySpawnCommitted) + ) + // Claimed before any await, so a concurrent create of this pane cannot slip in between. const claimedStablePaneCreate = this.ptyController.claimStablePaneCreate?.({ worktreeId: workspace.id, connectionId: workspace.connectionId, tabId, leafId }) - let stablePaneCreateReleased = false - const releaseStablePaneCreate = (): void => { - if (stablePaneCreateReleased) { - return - } - stablePaneCreateReleased = true - claimedStablePaneCreate?.() - } + const releaseStablePaneCreate = createIdempotentCallback(claimedStablePaneCreate) try { + // A pane moved from another host reopens where it last was, until a spawn commits. + cwd = (await restoreHint.peekCwd(launchOpts.cwd)) ?? cwd if (launchOpts.signal?.aborted) { throw new Error('client_disconnected') } @@ -117,7 +116,9 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate { ...baseEnv, ...(sequencedStartupCommand - ? { [dependencies.SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: sequencedStartupCommand } + ? { + [dependencies.SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: sequencedStartupCommand + } : {}) }, paneKey, @@ -131,7 +132,10 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate let result: Awaited>> try { const { launchAgent } = launchOpts - launchOpts.onPtySpawnDispatched?.({ launchConfig: effectiveLaunchConfig, launchAgent }) + launchOpts.onPtySpawnDispatched?.({ + launchConfig: effectiveLaunchConfig, + launchAgent + }) result = await dependencies.spawnInAdmittedPane(this.store, this.ptyController, { ...initialSize, cwd, @@ -170,7 +174,9 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate } : {}), ...(launchOpts.agentSessionCreateOperationId - ? { agentSessionCreateOperationId: launchOpts.agentSessionCreateOperationId } + ? { + agentSessionCreateOperationId: launchOpts.agentSessionCreateOperationId + } : {}), ...dependencies.callerSpawnScope(launchOpts), ...(launchOpts.onPtySpawnCommitted @@ -188,6 +194,7 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate if (!result.stablePaneOwner) { reportPtySpawnCommitted() } + restoreHint.consume() // Why here: refused before a handle, snapshot or reveal could pose the live PTY as new. const adoptedStablePane = dependencies.admitStablePaneAdoption(result, launchOpts) if (result.agentSessionEnsure) { diff --git a/src/main/runtime/orca-runtime-file-commands.ts b/src/main/runtime/orca-runtime-file-commands.ts index 97acccae806..fc9a386b3d1 100644 --- a/src/main/runtime/orca-runtime-file-commands.ts +++ b/src/main/runtime/orca-runtime-file-commands.ts @@ -28,6 +28,7 @@ import { } from './files/chat-scratch-file-root' import type { RuntimeStore } from './runtime-store-contract' +import { isWslMoveWorktreeBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' import type { ResolvedWorktree } from './runtime-worktree-path-identity' import type { Repo } from '../../shared/repo-types' @@ -151,6 +152,10 @@ export abstract class OrcaRuntimeWithFileCommands extends OrcaRuntimeWithPreserv if (!store?.setWorktreeMeta) { return } + // Bookkeeping incidental to the push: a project a WSL move is taking skips it, not the push. + if (isWslMoveWorktreeBlocked(worktreeId, store.getWorktreeMeta?.(worktreeId)?.hostId)) { + return + } store.setWorktreeMeta(worktreeId, { pushTarget }) } }) @@ -246,7 +251,8 @@ export abstract class OrcaRuntimeWithFileCommands extends OrcaRuntimeWithPreserv protected readonly repositoryIssueCommand = new RuntimeRepositoryIssueCommand({ resolveRepo: (selector) => this.resolveRepoSelector(selector), - getLocalGitArgs: (repo) => this.getLocalGitExecutionOptionArgs(repo) + getLocalGitArgs: (repo) => this.getLocalGitExecutionOptionArgs(repo), + getStore: () => this.store }) protected readonly clientHostedBrowserRows = new ClientHostedBrowserRowPublisher({ diff --git a/src/main/runtime/orca-runtime-files-artifact-facade.test.ts b/src/main/runtime/orca-runtime-files-artifact-facade.test.ts index cf4a01ac462..c2ad9bd35cc 100644 --- a/src/main/runtime/orca-runtime-files-artifact-facade.test.ts +++ b/src/main/runtime/orca-runtime-files-artifact-facade.test.ts @@ -1,6 +1,7 @@ import { afterEach, expect, it, vi } from 'vitest' import { RuntimeFileCommands } from './orca-runtime-files' import { OrcaRuntimeService } from './orca-runtime' +import { RuntimeFileAccess } from './files/file-access' import { RuntimeFileReads } from './files/file-reads' import { RuntimeTerminalArtifacts } from './terminal/artifacts/terminal-artifacts' import type { RuntimeFileCommandHost } from './files/file-command-host' @@ -25,7 +26,7 @@ const reachedArtifactOwner = (): never => { afterEach(() => vi.restoreAllMocks()) -it('preserves artifact facade arity, optional slots, and the same artifact receiver', () => { +it('preserves artifact optional slots and the same artifact receiver', () => { const read = vi .spyOn(RuntimeTerminalArtifacts.prototype, 'readTerminalArtifactFile') .mockImplementation(reachedArtifactOwner) @@ -40,13 +41,6 @@ it('preserves artifact facade arity, optional slots, and the same artifact recei .mockImplementation(reachedArtifactOwner) const commands = new RuntimeFileCommands(host) - expect([ - commands.readTerminalArtifactFile.length, - commands.readTerminalArtifactPreview.length, - commands.writeTerminalArtifactFile.length, - commands.revokeTerminalFileGrantsForClient.length - ]).toEqual([4, 5, 5, 1]) - expect(() => commands.readTerminalArtifactFile('worktree', 'grant', '/artifact')).toThrow( 'artifact_owner_reached' ) @@ -91,81 +85,37 @@ it('preserves artifact facade arity, optional slots, and the same artifact recei } }) -const originalMethodMetadata = [ - ['openMobileFile', 4, true], - ['openMobileDiff', 4, true], - ['listRuntimeMarkdownDocuments', 1, true], - ['pathsExistRuntimeFiles', 2, true], - ['statRuntimeFile', 2, true], - ['readMobileFile', 2, true], - ['readFileExplorerPreview', 3, true], - ['readDocPreviewFile', 6, true], - ['readFileExplorerChunk', 4, true], - ['writeFileExplorerFile', 6, true], - ['writeFileExplorerFileBase64', 6, true], - ['writeFileExplorerFileBase64Chunk', 7, true], - ['createFileExplorerFile', 5, true], - ['createFileExplorerDir', 5, true], - ['createFileExplorerDirNoClobber', 5, true], - ['commitFileExplorerUpload', 6, true], - ['renameFileExplorerPath', 6, true], - ['copyFileExplorerPath', 6, true], - ['deleteFileExplorerPath', 6, true], - ['readFileExplorerDir', 2, true], - ['watchFileExplorer', 2, true], - ['closeFileExplorerWatchersForPath', 2, true], - ['restoreFileExplorerWatchersAfterFailedRemoval', 2, true], - ['forgetFileExplorerWatchersAfterRemoval', 2, false], - ['resolveTerminalPath', 7, true] -] as const +it.each(['command facade', 'installed runtime'])( + 'forwards omitted file arguments and optional positions through the %s', + (surface) => { + const returned: ReturnType = Promise.resolve({ + worktree: 'worktree', + relativePath: 'file', + kind: 'text', + opened: true + }) + const open = vi.spyOn(RuntimeFileAccess.prototype, 'openMobileFile').mockReturnValue(returned) + const commands = + surface === 'command facade' ? new RuntimeFileCommands(host) : new OrcaRuntimeService(null) + const position = { line: 12, column: 5 } -it('preserves the original public command arities and native async functions', () => { - const commands = new RuntimeFileCommands(host) - for (const [name, arity, async] of originalMethodMetadata) { - expect(commands[name].length, name).toBe(arity) - expect(Object.prototype.toString.call(commands[name]), name).toBe( - async ? '[object AsyncFunction]' : '[object Function]' - ) - } -}) - -it('installs the original public file command metadata on the actual runtime', () => { - const runtime = new OrcaRuntimeService(null) - const commands = { - openMobileFile: runtime.openMobileFile, - openMobileDiff: runtime.openMobileDiff, - listRuntimeMarkdownDocuments: runtime.listRuntimeMarkdownDocuments, - pathsExistRuntimeFiles: runtime.pathsExistRuntimeFiles, - statRuntimeFile: runtime.statRuntimeFile, - readMobileFile: runtime.readMobileFile, - readFileExplorerPreview: runtime.readFileExplorerPreview, - readDocPreviewFile: runtime.readDocPreviewFile, - readFileExplorerChunk: runtime.readFileExplorerChunk, - writeFileExplorerFile: runtime.writeFileExplorerFile, - writeFileExplorerFileBase64: runtime.writeFileExplorerFileBase64, - writeFileExplorerFileBase64Chunk: runtime.writeFileExplorerFileBase64Chunk, - createFileExplorerFile: runtime.createFileExplorerFile, - createFileExplorerDir: runtime.createFileExplorerDir, - createFileExplorerDirNoClobber: runtime.createFileExplorerDirNoClobber, - commitFileExplorerUpload: runtime.commitFileExplorerUpload, - renameFileExplorerPath: runtime.renameFileExplorerPath, - copyFileExplorerPath: runtime.copyFileExplorerPath, - deleteFileExplorerPath: runtime.deleteFileExplorerPath, - readFileExplorerDir: runtime.readFileExplorerDir, - watchFileExplorer: runtime.watchFileExplorer, - resolveTerminalPath: runtime.resolveTerminalPath - } - for (const [name, arity, async] of originalMethodMetadata) { - if (!(name in commands)) { - continue + expect(commands.openMobileFile('worktree', 'file')).toBe(returned) + expect(commands.openMobileFile('worktree', 'file', 'caller')).toBe(returned) + expect(commands.openMobileFile('worktree', 'file', undefined, position)).toBe(returned) + expect(commands.openMobileFile('worktree', 'file', undefined, undefined)).toBe(returned) + expect(open.mock.calls).toEqual([ + ['worktree', 'file'], + ['worktree', 'file', 'caller'], + ['worktree', 'file', undefined, position], + ['worktree', 'file', undefined, undefined] + ]) + const owner = open.mock.contexts[0] + expect(owner).toBeInstanceOf(RuntimeFileAccess) + for (const context of open.mock.contexts) { + expect(context).toBe(owner) } - const installed = Object.getOwnPropertyDescriptor(commands, name)?.value - expect(installed.length, name).toBe(arity) - expect(Object.prototype.toString.call(installed), name).toBe( - async ? '[object AsyncFunction]' : '[object Function]' - ) } -}) +) it('preserves omitted argument slots, owner receivers, and the returned promise', () => { const returned: ReturnType = Promise.resolve({ diff --git a/src/main/runtime/orca-runtime-get-status.ts b/src/main/runtime/orca-runtime-get-status.ts index 307b5e86dd4..a88eac44678 100644 --- a/src/main/runtime/orca-runtime-get-status.ts +++ b/src/main/runtime/orca-runtime-get-status.ts @@ -8,6 +8,7 @@ import { import { isBrowserIdentityModeStoreInitialized } from '../browser/browser-identity-mode-store' import { servesLocalWslMigrationSource } from '../orcad/orcad-migration-wsl-destination' import { ORCAD_MIGRATION_LOCAL_WSL_SOURCE_RUNTIME_CAPABILITY } from '../../shared/orcad-runtime-capabilities' +import { advertisedOrcadMigrationBlobCapabilities } from '../../shared/orcad-migration-blob-transfer' import type { RuntimeCapability } from '../../shared/protocol-version' import { BROWSER_CERTIFICATE_TRUST_RUNTIME_CAPABILITY, @@ -123,6 +124,9 @@ export abstract class OrcaRuntimeWithGetStatus extends OrcaRuntimeWithGetRuntime if (servesLocalWslMigrationSource()) { capabilities.push(ORCAD_MIGRATION_LOCAL_WSL_SOURCE_RUNTIME_CAPABILITY) } + // Why not static: a blob kind is advertised only once this build installs it, since an + // unadvertised kind is refused by the sender rather than staged and silently never applied. + capabilities.push(...advertisedOrcadMigrationBlobCapabilities()) // Why: certificate proceed is owned by the browser-hosting process for both // desktop webviews and offscreen pages. Advertise whenever either backend // can host a page so remote clients can surface Proceed Anyway (Unsafe). diff --git a/src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts b/src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts index 42593b34fc8..c4ad2e6c116 100644 --- a/src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts +++ b/src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts @@ -26,6 +26,7 @@ import { import { parseWorkspaceKey } from '../../shared/workspace-scope' import type { AgentSessionAccountHome } from '../../shared/agent-session-account-home' import type { StructuredAgentId } from '../../shared/agent-session-provider-handle' +import { readPiSessionHeader, samePiDirectory } from '../pi/rpc-launch-resolution' export class OrcaRuntimeWithGetStructuredAgentSessionCreateSupport extends OrcaRuntimeWithGetWorktreePs { async getStructuredAgentSessionCreateSupport( @@ -239,6 +240,15 @@ export class OrcaRuntimeWithGetStructuredAgentSessionCreateSupport extends OrcaR workspaceCwd: (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path }) : null + if (input.agent === 'pi' && adoption) { + const header = await readPiSessionHeader(adoption.transcriptPath) + const targetDirectory = (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path + if (!header || !(await samePiDirectory(header.cwd, targetDirectory))) { + throw agentSessionRefusalError('agent_session_identity_required', { + reason: 'transcriptNotFound' + }) + } + } return { envelope: { sessionId: input.envelope.sessionId, @@ -260,7 +270,9 @@ export class OrcaRuntimeWithGetStructuredAgentSessionCreateSupport extends OrcaR providerHandle: { transport: definition.handleTransport, agent: definition.agent, - nativeId: input.resumeFrom.providerSessionId + nativeId: transcriptImport.handleNamesTranscript + ? adoption.transcriptPath + : input.resumeFrom.providerSessionId }, transcriptPath: adoption.transcriptPath } diff --git a/src/main/runtime/orca-runtime-migrated-pane-restore-cwd.test.ts b/src/main/runtime/orca-runtime-migrated-pane-restore-cwd.test.ts new file mode 100644 index 00000000000..8ab0f8b55b6 --- /dev/null +++ b/src/main/runtime/orca-runtime-migrated-pane-restore-cwd.test.ts @@ -0,0 +1,255 @@ +// A pane moved to this host reopens in the folder it last used on the source, until it spawns. +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { chmodSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { OrcaRuntimeService } from './orca-runtime' +import type { RuntimeStore } from './runtime-store-contract' +import type { RuntimePtyController } from './runtime-pty-controller-contract' +import type { TerminalWorkspaceLaunchScope } from './runtime-legacy-worker-terminal-recovery-types' +import { OrcadTerminalRestoreHintStore } from '../persistence/migrating-orcad-catalog/orcad-terminal-restore-hint-store' + +vi.mock('electron', () => ({ + BrowserWindow: { fromId: vi.fn(() => null) }, + webContents: { fromId: vi.fn(() => null) }, + ipcMain: { on: vi.fn(), removeListener: vi.fn() }, + app: { getPath: vi.fn(() => '/tmp') } +})) + +const TAB_ID = 'tab-1' +const LEAF_ID = '11111111-1111-4111-8111-111111111111' +const OTHER_LEAF_ID = '22222222-2222-4222-8222-222222222222' +const PANE = `${TAB_ID}:${LEAF_ID}` +const WORKTREE_ID = 'wt-1' +const MIGRATION = { migrationKey: 'a'.repeat(32), migrationId: 'migration-1' } + +let dir: string +let worktreePath: string +let hinted: string + +class MigratedPaneRuntime extends OrcaRuntimeService { + constructor( + store: RuntimeStore, + private readonly connectionId: string | null + ) { + super(store) + } + + protected override async resolveTerminalWorkspaceLaunchScope(): Promise { + return { + id: WORKTREE_ID, + path: worktreePath, + connectionId: this.connectionId, + repo: null, + folderWorkspace: null + } + } +} + +type Hint = { paneKey: string; cwd: string; worktreeId?: string } + +function runtimeWithHints( + hints: Hint[], + options: { + connectionId?: string | null + spawn?: RuntimePtyController['spawn'] + claimStablePaneCreate?: RuntimePtyController['claimStablePaneCreate'] + onPeek?: () => void + } = {} +): { + runtime: MigratedPaneRuntime + spawn: ReturnType + hintStore: OrcadTerminalRestoreHintStore +} { + const hintStore = new OrcadTerminalRestoreHintStore(join(dir, 'blobs')) + hintStore.install( + MIGRATION, + hints.map((hint) => ({ + ...hint, + worktreeId: hint.worktreeId ?? WORKTREE_ID + })), + Date.now() + ) + const store: RuntimeStore = { + getRepos: () => [], + getRepo: () => undefined, + addRepo: vi.fn(), + updateRepo: vi.fn(), + getAllWorktreeMeta: () => ({}), + getWorktreeMeta: () => undefined, + setWorktreeMeta: vi.fn(), + removeWorktreeMeta: vi.fn(), + getGitHubCache: vi.fn(), + getSettings: () => ({ + workspaceDir: '/workspaces', + nestWorkspaces: false, + refreshLocalBaseRefOnWorktreeCreate: false, + branchPrefix: 'none', + branchPrefixCustom: '' + }), + peekOrcadMigrationTerminalRestoreHint: (paneKey, worktreeId) => { + options.onPeek?.() + return hintStore.peek(paneKey, worktreeId, Date.now()) + }, + consumeOrcadMigrationTerminalRestoreHint: (paneKey) => hintStore.consume(paneKey, Date.now()) + } + const runtime = new MigratedPaneRuntime(store, options.connectionId ?? null) + const spawn = vi.fn(options.spawn ?? (async () => ({ id: `pty-${spawn.mock.calls.length}` }))) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + ...(options.claimStablePaneCreate + ? { claimStablePaneCreate: options.claimStablePaneCreate } + : {}) + }) + return { runtime, spawn, hintStore } +} + +function spawnedCwd(spawn: ReturnType, call: number): unknown { + const args: unknown = spawn.mock.calls[call]?.[0] + return typeof args === 'object' && args !== null && 'cwd' in args ? args.cwd : undefined +} + +function remainingHint(hintStore: OrcadTerminalRestoreHintStore, paneKey = PANE): string | null { + return hintStore.peek(paneKey, WORKTREE_ID, Date.now()) +} + +describe('migrated pane restore cwd at terminal create', () => { + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'orca-migrated-pane-cwd-')) + worktreePath = join(dir, 'repo') + hinted = join(worktreePath, 'packages', 'web') + mkdirSync(hinted, { recursive: true }) + }) + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }) + }) + + it('opens a default-cwd create in the hinted folder once, then at the worktree root', async () => { + const { runtime, spawn, hintStore } = runtimeWithHints([{ paneKey: PANE, cwd: hinted }]) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + expect(remainingHint(hintStore)).toBeNull() + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + + expect(spawnedCwd(spawn, 0)).toBe(hinted) + expect(spawnedCwd(spawn, 1)).toBe(worktreePath) + }) + + it('keeps the hint when the create fails, so the retry still opens in the hinted folder', async () => { + let attempts = 0 + const { runtime, spawn, hintStore } = runtimeWithHints([{ paneKey: PANE, cwd: hinted }], { + spawn: async () => { + attempts += 1 + if (attempts === 1) { + throw new Error('spawn_failed') + } + return { id: 'pty-retry' } + } + }) + + await expect( + runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + ).rejects.toThrow('spawn_failed') + expect(remainingHint(hintStore)).toBe(hinted) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + expect(spawnedCwd(spawn, 1)).toBe(hinted) + expect(remainingHint(hintStore)).toBeNull() + }) + + it('opens an explicit cwd and spends the hint: that create was the restore', async () => { + const { runtime, spawn, hintStore } = runtimeWithHints([{ paneKey: PANE, cwd: hinted }]) + + await runtime.createTerminal('id:wt-1', { + tabId: TAB_ID, + leafId: LEAF_ID, + cwd: dir + }) + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + + expect(spawnedCwd(spawn, 0)).toBe(dir) + expect(spawnedCwd(spawn, 1)).toBe(worktreePath) + expect(remainingHint(hintStore)).toBeNull() + }) + + it('never applies a hint installed for another worktree', async () => { + const { runtime, spawn } = runtimeWithHints([ + { paneKey: PANE, cwd: hinted, worktreeId: 'wt-other' } + ]) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + + expect(spawnedCwd(spawn, 0)).toBe(worktreePath) + }) + + it('falls back to the worktree root when the folder is gone, and spends the hint', async () => { + const { runtime, spawn, hintStore } = runtimeWithHints([ + { paneKey: PANE, cwd: join(worktreePath, 'deleted') } + ]) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + + expect(spawnedCwd(spawn, 0)).toBe(worktreePath) + expect(hintStore.peek(PANE, WORKTREE_ID, Date.now())).toBeNull() + }) + + it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)( + 'falls back to the worktree root for a folder the shell cannot search', + async () => { + chmodSync(hinted, 0o600) + try { + const { runtime, spawn } = runtimeWithHints([{ paneKey: PANE, cwd: hinted }]) + + await runtime.createTerminal('id:wt-1', { + tabId: TAB_ID, + leafId: LEAF_ID + }) + + expect(spawnedCwd(spawn, 0)).toBe(worktreePath) + } finally { + chmodSync(hinted, 0o700) + } + } + ) + + it('claims the pane before reading its hint', async () => { + const order: string[] = [] + const { runtime } = runtimeWithHints([{ paneKey: PANE, cwd: hinted }], { + claimStablePaneCreate: () => { + order.push('claim') + return () => order.push('release') + }, + onPeek: () => order.push('peek') + }) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + + expect(order.slice(0, 2)).toEqual(['claim', 'peek']) + }) + + it("never applies another pane's hint", async () => { + const otherPane = `${TAB_ID}:${OTHER_LEAF_ID}` + const { runtime, spawn, hintStore } = runtimeWithHints([{ paneKey: otherPane, cwd: hinted }]) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + await runtime.createTerminal('id:wt-1', {}) + + expect(spawnedCwd(spawn, 0)).toBe(worktreePath) + expect(spawnedCwd(spawn, 1)).toBe(worktreePath) + expect(remainingHint(hintStore, otherPane)).toBe(hinted) + }) + + it('ignores hints for an SSH workspace, whose folders this host cannot check', async () => { + const { runtime, spawn } = runtimeWithHints([{ paneKey: PANE, cwd: hinted }], { + connectionId: 'ssh-1' + }) + + await runtime.createTerminal('id:wt-1', { tabId: TAB_ID, leafId: LEAF_ID }) + + expect(spawnedCwd(spawn, 0)).toBe(worktreePath) + }) +}) diff --git a/src/main/runtime/orca-runtime-migration-catalog.ts b/src/main/runtime/orca-runtime-migration-catalog.ts index 988c4464d34..a26fe70349f 100644 --- a/src/main/runtime/orca-runtime-migration-catalog.ts +++ b/src/main/runtime/orca-runtime-migration-catalog.ts @@ -7,6 +7,11 @@ import type { OrcadMigrationSnapshotChunkRequest, OrcadMigrationSnapshotChunkResult } from '../../shared/orcad-migration-scrollback' +import type { + OrcadMigrationBlobChunkRequest, + OrcadMigrationBlobChunkResult, + OrcadMigrationBlobDescriptor +} from '../../shared/orcad-migration-blob-transfer' import { abortStagedOrcadMigrationCatalogDurably, commitStagedOrcadMigrationCatalogDurably, @@ -20,6 +25,7 @@ type MigrationStoreKey = | 'stageOrcadMigrationCatalog' | 'commitStagedOrcadMigrationCatalog' | 'stageOrcadMigrationSnapshotChunk' + | 'stageOrcadMigrationBlobChunk' | 'abortStagedOrcadMigrationCatalog' | 'getOrcadMigrationCatalogState' | 'flushPendingOrThrowAsync' @@ -39,7 +45,7 @@ export class OrcaRuntimeWithMigrationCatalog extends OrcaRuntimeWithResolveWaite async commitStagedOrcadMigrationCatalog( manifest: OrcadMigrationManifest, - options: { signal?: AbortSignal } = {} + options: { signal?: AbortSignal; expectedBlobs?: readonly OrcadMigrationBlobDescriptor[] } = {} ): Promise { return commitStagedOrcadMigrationCatalogDurably({ store: this.requireMigrationStore( @@ -48,6 +54,8 @@ export class OrcaRuntimeWithMigrationCatalog extends OrcaRuntimeWithResolveWaite ), manifest, signal: options.signal, + expectedBlobs: options.expectedBlobs, + worktreeHasLiveShells: this.migrationWorktreeHasLiveShells, onDurableCommit: () => { this.invalidateResolvedWorktreeCache() this.notifyReposChanged() @@ -63,6 +71,15 @@ export class OrcaRuntimeWithMigrationCatalog extends OrcaRuntimeWithResolveWaite .stageOrcadMigrationSnapshotChunk!(request) } + stageOrcadMigrationBlobChunk( + request: OrcadMigrationBlobChunkRequest + ): OrcadMigrationBlobChunkResult { + // SAFETY: The virtual Store may differ from the checked Store; preserve its native failure. + return this.requireMigrationStore('stageOrcadMigrationBlobChunk').stageOrcadMigrationBlobChunk!( + request + ) + } + async abortStagedOrcadMigrationCatalog( manifest: OrcadMigrationManifest, options: { signal?: AbortSignal } = {} @@ -80,10 +97,55 @@ export class OrcaRuntimeWithMigrationCatalog extends OrcaRuntimeWithResolveWaite getOrcadMigrationCatalogState(manifest: OrcadMigrationManifest): OrcadMigrationCatalogState { return getOrcadMigrationCatalogState({ store: this.requireMigrationStore('getOrcadMigrationCatalogState'), - manifest + manifest, + worktreeHasLiveShells: this.migrationWorktreeHasLiveShells }) } + /** + * The moved shell history store load left waiting, installed once this runtime can see the + * host's shells. An unanswered inventory leaves it for the next catalog-state query. + */ + async installPendingOrcadMigrationShellHistory(): Promise { + const store = this.store + if (!store?.installPendingOrcadMigrationReceivedBlobs) { + return + } + // Daemon shells that outlived a restart count as live only once the inventory recorded them. + if ( + this.ptyController?.listProcesses && + !(await this.refreshPtyWorktreeRecordsWithControllerInventory([])) + ) { + console.warn('[orcad-migration] shell history waits: the terminal inventory did not answer') + return + } + store.installPendingOrcadMigrationReceivedBlobs({ + worktreeHasLiveShells: this.migrationWorktreeHasLiveShells + }) + } + + // The last shell of a worktree exiting releases a moved history that waited on it. + override onPtyExit( + ...args: Parameters + ): ReturnType { + const worktreeId = this.ptysById.get(args[0])?.worktreeId + const result = super.onPtyExit(...args) + // The store checks its deferred set first, so an exit in any other worktree costs one lookup. + if (worktreeId) { + try { + this.store?.installDeferredOrcadMigrationShellHistory?.(worktreeId, { + worktreeHasLiveShells: this.migrationWorktreeHasLiveShells + }) + } catch (error) { + console.error('[orcad-migration] installing deferred shell history failed:', error) + } + } + return result + } + + private readonly migrationWorktreeHasLiveShells = (worktreeId: string): boolean => + this.getLivePtyIdsForWorktree(worktreeId).size > 0 + // The capability check covers the captured Store, not a different virtual return. private requireMigrationStore(...methods: MigrationStoreKey[]): RuntimeStore { const store = this.store diff --git a/src/main/runtime/orca-runtime-migration-deferred-shell-history.test.ts b/src/main/runtime/orca-runtime-migration-deferred-shell-history.test.ts new file mode 100644 index 00000000000..a73d4907bda --- /dev/null +++ b/src/main/runtime/orca-runtime-migration-deferred-shell-history.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it, vi } from 'vitest' +import { OrcaRuntimeService } from './orca-runtime-test-mocks.spec' +import { store } from './orca-runtime-test-fixtures.spec' + +type RuntimeInternals = { + recordPtyWorktree: (ptyId: string, worktreeId: string, state?: { connected?: boolean }) => unknown +} + +describe('a moved shell history that waited on live shells', () => { + it('is retried when the last shell of its worktree exits, with the live-shell check', () => { + const installDeferred = vi.fn() + const runtime = new OrcaRuntimeService({ + ...store, + installDeferredOrcadMigrationShellHistory: installDeferred + }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: recordPtyWorktree is declared on OrcaRuntimeService's inheritance chain. + const internals = runtime as unknown as RuntimeInternals + internals.recordPtyWorktree('pty-1', 'repo-1::/srv/app', { connected: true }) + internals.recordPtyWorktree('pty-2', 'repo-1::/srv/app', { connected: true }) + + runtime.onPtyExit('pty-1', 0) + const [worktreeId, options] = installDeferred.mock.calls[0] + expect(worktreeId).toBe('repo-1::/srv/app') + // pty-2 still runs there, so the store keeps waiting. + expect(options.worktreeHasLiveShells('repo-1::/srv/app')).toBe(true) + + runtime.onPtyExit('pty-2', 0) + expect(installDeferred.mock.calls[1][1].worktreeHasLiveShells('repo-1::/srv/app')).toBe(false) + }) + + it('is installed once the runtime attached, counting daemon shells that outlived a restart', async () => { + const installPending = vi.fn() + const runtime = new OrcaRuntimeService({ + ...store, + installPendingOrcadMigrationReceivedBlobs: installPending + }) + runtime.setPtyController({ + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + listProcesses: vi.fn(async () => [ + { id: 'daemon-pty', cwd: '/srv/app', title: 'zsh', worktreeId: 'repo-1::/srv/app' } + ]) + }) + + await runtime.installPendingOrcadMigrationShellHistory() + + const [options] = installPending.mock.calls[0] + expect(options.worktreeHasLiveShells('repo-1::/srv/app')).toBe(true) + expect(options.worktreeHasLiveShells('repo-1::/srv/other')).toBe(false) + }) + + it('waits when the terminal inventory does not answer', async () => { + const installPending = vi.fn() + const runtime = new OrcaRuntimeService({ + ...store, + installPendingOrcadMigrationReceivedBlobs: installPending + }) + runtime.setPtyController({ + write: () => true, + kill: () => true, + getForegroundProcess: async () => null, + listProcesses: vi.fn(async () => { + throw new Error('daemon unreachable') + }) + }) + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + await runtime.installPendingOrcadMigrationShellHistory() + + expect(installPending).not.toHaveBeenCalled() + warn.mockRestore() + }) +}) diff --git a/src/main/runtime/orca-runtime-report-pty-spawn-commit.ts b/src/main/runtime/orca-runtime-report-pty-spawn-commit.ts index e9e2963144f..6f5741047b1 100644 --- a/src/main/runtime/orca-runtime-report-pty-spawn-commit.ts +++ b/src/main/runtime/orca-runtime-report-pty-spawn-commit.ts @@ -1,5 +1,5 @@ -/** Create an idempotent callback for the provider's spawn-commit boundary. */ -export function createPtySpawnCommitReporter(callback?: () => void): () => void { +/** An idempotent callback: the spawn-commit report and the stable-pane claim release run once. */ +export function createIdempotentCallback(callback?: () => void): () => void { let reported = false return () => { if (reported) { diff --git a/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts b/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts index 27a986a51f7..8b87429e894 100644 --- a/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts +++ b/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts @@ -398,7 +398,8 @@ describe('adopting a conversation recorded outside Orca', () => { runtime.resolveStructuredAgentSessionCreateIntent({ envelope: { sessionId: 'session-1', clientOperationId: 'operation-1' }, worktree: 'id:workspace-1', - agent: 'pi', + // Every registered agent adopts; an unregistered one has no importer. + agent: 'hermes', resumeFrom: { providerSessionId: 'provider-1' } }) ).rejects.toMatchObject({ @@ -409,4 +410,67 @@ describe('adopting a conversation recorded outside Orca', () => { }) expect(resolveStructuredAgentSessionLocation).not.toHaveBeenCalled() }) + + it('adopts the original Pi file into a folder workspace and refuses another folder', async () => { + const accountHome = await mkdtemp(join(tmpdir(), 'orca-pi-adopt-intent-')) + vi.stubEnv('PI_CODING_AGENT_DIR', accountHome) + try { + const file = join( + accountHome, + 'sessions', + '--repos-workspace-1--', + '2026-10-12T00-00-00-000Z_0198c2a4-7d1e-7c3a-9a4b-1f2e3d4c5b6a.jsonl' + ) + await mkdir(dirname(file), { recursive: true }) + await writeFile( + file, + `${JSON.stringify({ type: 'session', id: '0198c2a4-7d1e-7c3a-9a4b-1f2e3d4c5b6a', cwd: '/repos/workspace-1' })}\n` + ) + const runtime = new OrcaRuntimeService( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: create intent only reads getSettings from the store. + { + getSettings: () => ({ agentDefaultEnv: { pi: { PI_CODING_AGENT_DIR: accountHome } } }) + } as never + ) + vi.spyOn(runtime, 'getStructuredAgentSessionCreateSupport').mockResolvedValue({ + supported: true + }) + let workspaceDirectory = '/repos/workspace-1' + Object.assign(runtime, { + resolveStructuredAgentSessionLocation: vi.fn(async () => ({ + executionHostId: 'local', + wslDistro: null, + workspaceId: 'workspace-1', + workspaceKind: 'folder' as const + })), + resolveRuntimeFileTarget: vi.fn(async () => ({ worktree: { path: workspaceDirectory } })) + }) + + const intent = await runtime.resolveStructuredAgentSessionCreateIntent({ + envelope: { sessionId: 'session-1', clientOperationId: 'operation-1' }, + worktree: 'id:workspace-1', + agent: 'pi', + resumeFrom: { providerSessionId: '0198c2a4-7d1e-7c3a-9a4b-1f2e3d4c5b6a' } + }) + + expect(intent.accountHome).toEqual({ variable: 'PI_CODING_AGENT_DIR', path: accountHome }) + expect(intent.adopt).toEqual({ + providerHandle: { transport: 'jsonl-rpc', agent: 'pi', nativeId: file }, + transcriptPath: file + }) + + workspaceDirectory = '/different-folder' + await expect( + runtime.resolveStructuredAgentSessionCreateIntent({ + envelope: { sessionId: 'session-2', clientOperationId: 'operation-2' }, + worktree: 'id:workspace-1', + agent: 'pi', + resumeFrom: { providerSessionId: '0198c2a4-7d1e-7c3a-9a4b-1f2e3d4c5b6a' } + }) + ).rejects.toMatchObject({ refusal: { code: 'agent_session_identity_required' } }) + } finally { + vi.unstubAllEnvs() + await rm(accountHome, { recursive: true, force: true }) + } + }) }) diff --git a/src/main/runtime/orcad-migration-catalog-import.ts b/src/main/runtime/orcad-migration-catalog-import.ts index dbccf92adae..efb34eb45d4 100644 --- a/src/main/runtime/orcad-migration-catalog-import.ts +++ b/src/main/runtime/orcad-migration-catalog-import.ts @@ -3,13 +3,17 @@ import type { OrcadMigrationCatalogState, OrcadMigrationManifest } from '../../shared/orcad-migration-manifest' +import type { OrcadMigrationBlobDescriptor } from '../../shared/orcad-migration-blob-transfer' import type { Store } from '../persistence' +/** A moved history waits while a shell runs in its worktree: that shell would overwrite it. */ +type WorktreeHasLiveShells = (worktreeId: string) => boolean + type OrcadMigrationCatalogFlushStore = Partial> type OrcadMigrationCatalogStageStore = OrcadMigrationCatalogFlushStore & Partial> type OrcadMigrationCatalogCommitStore = OrcadMigrationCatalogFlushStore & - Partial> + Partial> type OrcadMigrationCatalogAbortStore = OrcadMigrationCatalogFlushStore & Partial> @@ -28,15 +32,35 @@ export async function commitStagedOrcadMigrationCatalogDurably(args: { store: OrcadMigrationCatalogCommitStore manifest: OrcadMigrationManifest signal?: AbortSignal + expectedBlobs?: readonly OrcadMigrationBlobDescriptor[] onDurableCommit: () => void + worktreeHasLiveShells?: WorktreeHasLiveShells }): Promise { // SAFETY: Returned or mutable Store slots retain their original native missing-method failure. - const result = args.store.commitStagedOrcadMigrationCatalog!(args.manifest) + const result = args.store.commitStagedOrcadMigrationCatalog!( + args.manifest, + args.expectedBlobs ? { expectedBlobs: args.expectedBlobs } : {} + ) await flushMigrationState(args.store, args.signal) args.onDurableCommit() + installReceivedBlobs(args.store, args.manifest, args.worktreeHasLiveShells) return result } +/** After the durable commit, on its retries and state queries; the store load retries too. */ +function installReceivedBlobs( + store: Partial>, + manifest: OrcadMigrationManifest, + worktreeHasLiveShells: WorktreeHasLiveShells | undefined +): void { + try { + store.installOrcadMigrationReceivedBlobs?.(manifest, { worktreeHasLiveShells }) + } catch (error) { + // The catalog is committed; a failed install keeps its blob for the next retry. + console.error('[orcad-migration] installing received blobs failed:', error) + } +} + export async function abortStagedOrcadMigrationCatalogDurably(args: { store: OrcadMigrationCatalogAbortStore manifest: OrcadMigrationManifest @@ -53,11 +77,18 @@ export async function abortStagedOrcadMigrationCatalogDurably(args: { } export function getOrcadMigrationCatalogState(args: { - store: Partial> + store: Partial< + Pick + > manifest: OrcadMigrationManifest + worktreeHasLiveShells?: WorktreeHasLiveShells }): OrcadMigrationCatalogState { // SAFETY: Returned or mutable Store slots retain their original native missing-method failure. - return args.store.getOrcadMigrationCatalogState!(args.manifest) + const result = args.store.getOrcadMigrationCatalogState!(args.manifest) + if (result.state === 'committed') { + installReceivedBlobs(args.store, args.manifest, args.worktreeHasLiveShells) + } + return result } function flushMigrationState( @@ -65,5 +96,8 @@ function flushMigrationState( signal?: AbortSignal ): Promise { // SAFETY: Returned or mutable Store slots retain their original native missing-method failure. - return store.flushPendingOrThrowAsync!({ signal, drainToStableGeneration: false }) + return store.flushPendingOrThrowAsync!({ + signal, + drainToStableGeneration: false + }) } diff --git a/src/main/runtime/orchestration-mailbox-notification-consistency.test.ts b/src/main/runtime/orchestration-mailbox-notification-consistency.test.ts index 3475ae70eb0..ee03a149410 100644 --- a/src/main/runtime/orchestration-mailbox-notification-consistency.test.ts +++ b/src/main/runtime/orchestration-mailbox-notification-consistency.test.ts @@ -9,7 +9,6 @@ import { OrchestrationDb } from './orchestration/db' import { checkBoundMailbox, createBoundRun, - createDatabase, createRuntime, driveToLiveIdle, insertDirectRunMessage, @@ -52,7 +51,7 @@ describe('orchestration notification mailbox consistency', () => { it('never points direct Run A mail after the pane is rebound to Run B', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-consistency-') + const db = new OrchestrationDb(':memory:') const first = createRuntime(db) const runA = createBoundRun(db, 'Run A') const staleDirectMessage = insertDirectRunMessage(db, runA.id, 'Run A completion') @@ -78,7 +77,7 @@ describe('orchestration notification mailbox consistency', () => { it('does not point unbound direct mail that a later Run binding would hide', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-unbound-bind-race-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const runA = createBoundRun(db, 'Unbound Run A') const message = insertDirectRunMessage(db, runA.id, 'Run A before later binding') @@ -151,7 +150,7 @@ describe('orchestration notification mailbox consistency', () => { it('pages a large persisted mismatch and coalesces each mailbox wake', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-paged-reconciliation-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const runA = createBoundRun(db, 'Backlog Run A') const messages = Array.from({ length: 151 }, (_, index) => @@ -188,7 +187,7 @@ describe('orchestration notification mailbox consistency', () => { }) it('uses composite indexes for direct ownership reconciliation', () => { - const db = createDatabase('orca-mailbox-routing-indexes-') + const db = new OrchestrationDb(':memory:') const sqlite = sqliteFor(db) const plan = (sql: string, ...params: string[]): string => (sqlite.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...params) as { detail: string }[]) @@ -239,7 +238,7 @@ describe('orchestration notification mailbox consistency', () => { }) it('finds owned mail without scanning an older unowned direct backlog', () => { - const db = createDatabase('orca-mailbox-owned-routing-plan-') + const db = new OrchestrationDb(':memory:') const detachedHandle = 'term_backlogged_detached' const ownedRun = db.createRun({ objective: 'Owned after backlog', @@ -388,7 +387,7 @@ describe('orchestration notification mailbox consistency', () => { it('fences the pointed mailbox instead of checking a rebound empty Run', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-post-submit-rebind-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const runA = createBoundRun(db, 'Run A') insertDirectRunMessage(db, runA.id, 'Run A completion') @@ -434,7 +433,7 @@ describe('orchestration notification mailbox consistency', () => { it('releases a staged pointer when its Run moves to another live pane', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-live-rebind-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const runA = createBoundRun(db, 'Run A') const message = insertDirectRunMessage(db, runA.id, 'Run A completion') @@ -474,7 +473,7 @@ describe('orchestration notification mailbox consistency', () => { it('serializes equivalent panes while newer mail arrives during Enter delay', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-equivalent-pane-flight-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Equivalent pane Run') const first = insertDirectRunMessage(db, run.id, 'First status') @@ -520,7 +519,7 @@ describe('orchestration notification mailbox consistency', () => { 'submits a staged pointer after %s becomes working without duplicating it', async (_provider, workingTitle, idleTitle) => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-working-before-enter-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Working-before-Enter Run') const message = insertDirectRunMessage(db, run.id, 'Actionable status') @@ -546,7 +545,7 @@ describe('orchestration notification mailbox consistency', () => { 'does not submit a staged pointer after %s enters a permission state', async (_provider, permissionTitle) => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-permission-before-enter-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Permission-before-Enter Run') const message = insertDirectRunMessage(db, run.id, 'Actionable status') @@ -563,7 +562,7 @@ describe('orchestration notification mailbox consistency', () => { ) it('submits a staged pointer once when a live PTY is cold-parked before Enter', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-cold-park-submit-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Cold-park Run') const message = insertDirectRunMessage(db, run.id, 'Submit while parked') @@ -582,7 +581,7 @@ describe('orchestration notification mailbox consistency', () => { it('keeps a staged Enter deferred when a parked watcher republishes the leaf beside a decoy', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-cold-park-published-leaf-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Cold-park published leaf Run') insertDirectRunMessage(db, run.id, 'Keep Enter deferred') @@ -662,7 +661,7 @@ describe('orchestration notification mailbox consistency', () => { 'handles a cold-parked pointer after the agent becomes %s', async (state, title, idleTitle) => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-cold-park-transition-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Cold-park transition Run') const message = insertDirectRunMessage(db, run.id, 'Do not submit while unavailable') @@ -692,7 +691,7 @@ describe('orchestration notification mailbox consistency', () => { it('releases staged pointer state when an explicit check owns the batch', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-explicit-check-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Explicit-check Run') insertDirectRunMessage(db, run.id, 'Checked before Enter') @@ -712,7 +711,7 @@ describe('orchestration notification mailbox consistency', () => { it('routes same-Run direct mail through the bound check and acknowledgment path', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-current-run-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Current Run') const messages = Array.from({ length: 51 }, (_, index) => @@ -804,7 +803,7 @@ describe('orchestration notification mailbox consistency', () => { it('does not replay a visible pointer when the provider rejects Enter', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-provider-refusal-') + const db = new OrchestrationDb(':memory:') const first = createRuntime(db) const recordWrite = first.write as unknown as (id: string, payload: string) => unknown const write = vi.fn((ptyId: string, data: string) => { @@ -835,7 +834,7 @@ describe('orchestration notification mailbox consistency', () => { it('does not point newer Run mail behind an outstanding Delivery', async () => { vi.useFakeTimers() - const db = createDatabase('orca-mailbox-outstanding-') + const db = new OrchestrationDb(':memory:') const harness = createRuntime(db) const run = createBoundRun(db, 'Outstanding Delivery Run') const firstMessage = db.insertMessage({ diff --git a/src/main/runtime/repo-icon-fork-backfill.test.ts b/src/main/runtime/repo-icon-fork-backfill.test.ts index b367fed0bb3..20a4475b9d8 100644 --- a/src/main/runtime/repo-icon-fork-backfill.test.ts +++ b/src/main/runtime/repo-icon-fork-backfill.test.ts @@ -54,15 +54,19 @@ describe('startup fork-upstream backfill', () => { await (runtime as unknown as BackfillInternals).repositoryForkBackfill.run() - expect(updateRepo).toHaveBeenCalledExactlyOnceWith('repo-1', { - upstream: { owner: 'upstream-org', repo: 'rocket' }, - repoIcon: { - type: 'image', - src: 'https://github.com/acme.png?size=64', - source: 'github', - label: 'acme/rocket-pro' - } - }) + expect(updateRepo).toHaveBeenCalledExactlyOnceWith( + 'repo-1', + { + upstream: { owner: 'upstream-org', repo: 'rocket' }, + repoIcon: { + type: 'image', + src: 'https://github.com/acme.png?size=64', + source: 'github', + label: 'acme/rocket-pro' + } + }, + 'local' + ) }) it('migrates a same-name fork to the upstream owner avatar', async () => { @@ -83,15 +87,19 @@ describe('startup fork-upstream backfill', () => { await (runtime as unknown as BackfillInternals).repositoryForkBackfill.run() - expect(updateRepo).toHaveBeenCalledExactlyOnceWith('repo-1', { - upstream: { owner: 'upstream-org', repo: 'rocket' }, - repoIcon: { - type: 'image', - src: 'https://github.com/upstream-org.png?size=64', - source: 'github', - label: 'upstream-org/rocket' - } - }) + expect(updateRepo).toHaveBeenCalledExactlyOnceWith( + 'repo-1', + { + upstream: { owner: 'upstream-org', repo: 'rocket' }, + repoIcon: { + type: 'image', + src: 'https://github.com/upstream-org.png?size=64', + source: 'github', + label: 'upstream-org/rocket' + } + }, + 'local' + ) }) it('skips every row whose files sit on an SSH host', async () => { @@ -137,8 +145,12 @@ describe('startup fork-upstream backfill', () => { resolveSlug({ owner: 'acme', repo: 'rocket-pro' }) await backfill - expect(updateRepo).toHaveBeenCalledExactlyOnceWith('repo-1', { - upstream: { owner: 'upstream-org', repo: 'rocket' } - }) + expect(updateRepo).toHaveBeenCalledExactlyOnceWith( + 'repo-1', + { + upstream: { owner: 'upstream-org', repo: 'rocket' } + }, + 'local' + ) }) }) diff --git a/src/main/runtime/rpc/dispatcher-transport-parity.test.ts b/src/main/runtime/rpc/dispatcher-transport-parity.test.ts index e9eb767fd2c..b7f9e6a12a8 100644 --- a/src/main/runtime/rpc/dispatcher-transport-parity.test.ts +++ b/src/main/runtime/rpc/dispatcher-transport-parity.test.ts @@ -1,3 +1,4 @@ +import './unused-default-rpc-methods.test-fixture' import { afterEach, describe, expect, it, vi } from 'vitest' import { z } from 'zod' import { ORCHESTRATION_CONTRACT_VERSION } from '../../../shared/protocol-version' diff --git a/src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts b/src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts index 00ed70257c9..9811f3e42ad 100644 --- a/src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts +++ b/src/main/runtime/rpc/methods/agent-launch-restart-replay.test.ts @@ -1,3 +1,4 @@ +import '../unused-default-rpc-methods.test-fixture' /** * A launch's record across a host restart, against the real durable ledger. * diff --git a/src/main/runtime/rpc/methods/ai-vault.test.ts b/src/main/runtime/rpc/methods/ai-vault.test.ts index f9d76f4acdd..cea3f989ed5 100644 --- a/src/main/runtime/rpc/methods/ai-vault.test.ts +++ b/src/main/runtime/rpc/methods/ai-vault.test.ts @@ -31,6 +31,8 @@ import { } from './ai-vault' import { agentSessionRefusalError } from '../../../../shared/agent-session-wire-refusals' import { recordStructuredAgentSessionHostInstallRefusal } from '../../structured-agent-session-host-refusal' +import { setStructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-registry' +import { installCopyingChat } from '../../../ai-vault/structured-session-ownership.test-support' import { configureAiVaultSessionSources, listAiVaultSessions, @@ -252,6 +254,51 @@ describe('aiVault.prepareSessionResume', () => { }) }) +describe('aiVault.prepareSessionResume for a chat whose host makes the copy', () => { + afterEach(() => setStructuredAgentSessionHost(null)) + + it("has the server's chat copy an OMP conversation and answers the copy", async () => { + const providerSessionId = '019fd611-2b3c-7d4e-9f50-6b7c8d9e0f1a' + const copy = vi.fn(async () => ({ + sessionId: 'copy-1', + filePath: '/sessions/copy-1.jsonl' + })) + installCopyingChat({ transport: 'acp', agent: 'omp', nativeId: providerSessionId }, copy) + const prepareAiVaultSessionResume = vi.fn() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the handler reads only these runtime members. + const runtime = { + getRuntimeId: () => 'test-runtime', + ensureStructuredAgentSessionHost: vi.fn(async () => undefined), + prepareAiVaultSessionResume + } as unknown as OrcaRuntimeService + const dispatcher = new RpcDispatcher({ runtime, methods: AI_VAULT_METHODS }) + + const response = await dispatcher.dispatch( + makeRequest('aiVault.prepareSessionResume', { + agent: 'omp', + sessionId: providerSessionId, + filePath: `/home/me/.omp/agent/sessions/--repo--/x_${providerSessionId}.jsonl`, + codexHome: null, + fork: true + }) + ) + + expect(response).toMatchObject({ + ok: true, + result: { + useRealCodexHome: false, + forkedSession: { + sessionId: 'copy-1', + filePath: '/sessions/copy-1.jsonl', + agent: 'omp' + } + } + }) + expect(copy).toHaveBeenCalledWith(expect.objectContaining({ providerSessionId })) + expect(prepareAiVaultSessionResume).not.toHaveBeenCalled() + }) +}) + // Session history and terminal resume are not chats: a process whose chats are refused still // serves them, while any other host failure still fails the request. describe('aiVault methods without a structured host', () => { diff --git a/src/main/runtime/rpc/methods/ai-vault.ts b/src/main/runtime/rpc/methods/ai-vault.ts index a5e6d6a5890..effaec21648 100644 --- a/src/main/runtime/rpc/methods/ai-vault.ts +++ b/src/main/runtime/rpc/methods/ai-vault.ts @@ -10,6 +10,7 @@ import { import { defineMethod } from '../core' import { conditionalAiVaultListResult } from '../../../ai-vault/session-list-fingerprint' import { restampAiVaultListResult } from '../../../ai-vault/session-list-results' +import { copyStructuredChatConversation } from '../../../ai-vault/structured-chat-conversation-copy' import type { AiVaultPrepareSessionResumeArgs } from '../../../../shared/ai-vault-resume-preparation' import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' import { isWslUncPath } from '../../../../shared/wsl-paths' @@ -144,7 +145,9 @@ export const AI_VAULT_METHODS = [ : ensureStructuredAgentSessionHostUnlessRefused(ensureHost)) assertLegacyAiVaultResumeAllowed(args) } - return runtime.prepareAiVaultSessionResume(args) + return ( + (await copyStructuredChatConversation(args)) ?? runtime.prepareAiVaultSessionResume(args) + ) } }) ] diff --git a/src/main/runtime/rpc/methods/managed-server.test.ts b/src/main/runtime/rpc/methods/managed-server.test.ts index f25bb93eafc..1b7e97f248a 100644 --- a/src/main/runtime/rpc/methods/managed-server.test.ts +++ b/src/main/runtime/rpc/methods/managed-server.test.ts @@ -1,3 +1,4 @@ +import '../unused-default-rpc-methods.test-fixture' import { afterEach, describe, expect, it, vi } from 'vitest' import type { OrcaRuntimeService } from '../../orca-runtime' import { diff --git a/src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts b/src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts index 1e23163efe9..9e5c53c32b3 100644 --- a/src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts +++ b/src/main/runtime/rpc/methods/mobile-web-bundle-read-concurrency.test.ts @@ -1,3 +1,4 @@ +import '../unused-default-rpc-methods.test-fixture' /** * The behaviours that only exist while a read is genuinely in flight or genuinely failing: the * per-connection cap, an abort that arrives mid-read, and a verify whose open throws. All three go diff --git a/src/main/runtime/rpc/methods/mobile-web-bundle-window-refusals.test.ts b/src/main/runtime/rpc/methods/mobile-web-bundle-window-refusals.test.ts index b66507b5479..944b8584544 100644 --- a/src/main/runtime/rpc/methods/mobile-web-bundle-window-refusals.test.ts +++ b/src/main/runtime/rpc/methods/mobile-web-bundle-window-refusals.test.ts @@ -1,3 +1,4 @@ +import '../unused-default-rpc-methods.test-fixture' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' diff --git a/src/main/runtime/rpc/methods/mobile-web-bundle.test.ts b/src/main/runtime/rpc/methods/mobile-web-bundle.test.ts index c17a40f865b..377b1de04ba 100644 --- a/src/main/runtime/rpc/methods/mobile-web-bundle.test.ts +++ b/src/main/runtime/rpc/methods/mobile-web-bundle.test.ts @@ -1,3 +1,4 @@ +import '../unused-default-rpc-methods.test-fixture' import { mkdirSync, mkdtempSync, rmSync, truncateSync, unlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' diff --git a/src/main/runtime/rpc/methods/orcad-migration.test.ts b/src/main/runtime/rpc/methods/orcad-migration.test.ts index 3c40126336c..a517ecb5ca4 100644 --- a/src/main/runtime/rpc/methods/orcad-migration.test.ts +++ b/src/main/runtime/rpc/methods/orcad-migration.test.ts @@ -31,6 +31,7 @@ const MIGRATION_METHOD_NAMES = [ 'orcad.migration.stageCatalog', 'orcad.migration.commitCatalog', 'orcad.migration.stageSnapshotChunk', + 'orcad.migration.stageBlobChunk', 'orcad.migration.abortCatalog', 'orcad.migration.catalogState' ] as const @@ -123,9 +124,11 @@ describe('orcad migration RPC', () => { ? 'commitStagedOrcadMigrationCatalog' : name === 'orcad.migration.stageSnapshotChunk' ? 'stageOrcadMigrationSnapshotChunk' - : name === 'orcad.migration.abortCatalog' - ? 'abortStagedOrcadMigrationCatalog' - : 'getOrcadMigrationCatalogState' + : name === 'orcad.migration.stageBlobChunk' + ? 'stageOrcadMigrationBlobChunk' + : name === 'orcad.migration.abortCatalog' + ? 'abortStagedOrcadMigrationCatalog' + : 'getOrcadMigrationCatalogState' await expect( method.handler( @@ -137,6 +140,61 @@ describe('orcad migration RPC', () => { } }) + it('passes a named blob set to commit only when the sender sent one', async () => { + const input = manifest() + const commit = vi.fn().mockResolvedValue({ state: 'committed' }) + const caller = context(vi.fn(), {}, { commitStagedOrcadMigrationCatalog: commit }) + const method = migrationMethod('orcad.migration.commitCatalog') + const expectedBlobs = [ + { + blobKind: 'shell-history', + key: `${'a'.repeat(16)}-zsh`, + sha256: 'b'.repeat(64), + byteLength: 1 + } + ] + + await method.handler(method.params?.parse({ manifest: input, expectedBlobs }), caller) + expect(commit).toHaveBeenLastCalledWith(input, { + signal: undefined, + expectedBlobs + }) + await method.handler(method.params?.parse({ manifest: input }), caller) + expect(commit).toHaveBeenLastCalledWith(input, { signal: undefined }) + expect( + method.params?.safeParse({ + manifest: input, + expectedBlobs: [ + { + ...expectedBlobs[0], + blobKind: 'agent-session-records', + key: 'con' + } + ] + }).success + ).toBe(false) + }) + + it('stages a blob chunk through its own method and refuses malformed descriptors', async () => { + const input = manifest() + const blob = vi.fn().mockReturnValue({ receivedBytes: 1 }) + const caller = context(vi.fn(), {}, { stageOrcadMigrationBlobChunk: blob }) + const method = migrationMethod('orcad.migration.stageBlobChunk') + const request = migrationBlobRequest(input) + + await expect(method.handler(method.params?.parse(request), caller)).resolves.toEqual({ + receivedBytes: 1 + }) + expect(blob).toHaveBeenCalledWith(request) + for (const key of ['../escape', `${'A'.repeat(16)}-zsh`, `${'a'.repeat(16)}-pwsh`]) { + expect(method.params?.safeParse(migrationBlobRequest(input, key)).success, key).toBe(false) + } + expect( + method.params?.safeParse({ ...request, blob: { ...request.blob, blobKind: 'future-kind' } }) + .success + ).toBe(false) + }) + it('rejects malformed and tampered manifests before staging', async () => { const stageCatalog = vi.fn() const method = migrationMethod() @@ -205,7 +263,20 @@ function migrationSnapshotRequest(input: OrcadMigrationManifest) { function migrationMethodInput(name: (typeof MIGRATION_METHOD_NAMES)[number]) { const input = manifest() + if (name === 'orcad.migration.stageBlobChunk') { + return migrationBlobRequest(input) + } return name === 'orcad.migration.stageSnapshotChunk' ? migrationSnapshotRequest(input) : { manifest: input } } + +function migrationBlobRequest(input: OrcadMigrationManifest, key = `${'a'.repeat(16)}-zsh`) { + return { + migrationId: input.migrationId, + manifestSha256: input.manifestSha256, + blob: { blobKind: 'shell-history', key, sha256: 'b'.repeat(64), byteLength: 1 }, + offset: 0, + bytesBase64: 'YQ==' + } +} diff --git a/src/main/runtime/rpc/methods/orcad-migration.ts b/src/main/runtime/rpc/methods/orcad-migration.ts index 4aee8bcb4a5..90a79cbb7eb 100644 --- a/src/main/runtime/rpc/methods/orcad-migration.ts +++ b/src/main/runtime/rpc/methods/orcad-migration.ts @@ -1,7 +1,11 @@ import type { z } from 'zod' -import { OrcadMigrationCatalogParams } from '../../../../shared/rpc-contract/orcad-migration-params' +import { + OrcadMigrationCatalogParams, + OrcadMigrationCommitCatalogParams +} from '../../../../shared/rpc-contract/orcad-migration-params' import { parseOrcadMigrationManifest } from '../../../../shared/orcad-migration-manifest' import { OrcadMigrationSnapshotChunkRequestSchema } from '../../../../shared/orcad-migration-scrollback' +import { OrcadMigrationBlobChunkRequestSchema } from '../../../../shared/orcad-migration-blob-transfer' import { assertOrcadMigrationManifestDigest } from '../../../orcad/orcad-migration-manifest-digest' import { assertOrcadMigrationSourceServedHere } from '../../../orcad/orcad-migration-wsl-destination' import { defineMethod, type RpcContext } from '../core' @@ -21,11 +25,12 @@ export const ORCAD_MIGRATION_METHODS = [ defineMethod({ name: 'orcad.migration.commitCatalog', permission: 'host-admin', - params: OrcadMigrationCatalogParams, + params: OrcadMigrationCommitCatalogParams, handler: async (params, context) => { requireMigrationRuntimeClient(context) return context.runtime.commitStagedOrcadMigrationCatalog(migrationManifest(params), { - signal: context.signal + signal: context.signal, + ...(params.expectedBlobs ? { expectedBlobs: params.expectedBlobs } : {}) }) } }), @@ -38,6 +43,17 @@ export const ORCAD_MIGRATION_METHODS = [ return context.runtime.stageOrcadMigrationSnapshotChunk(params) } }), + // A separate method, not a field on stageSnapshotChunk: an older host would strip the field and + // stage the bytes as a snapshot. + defineMethod({ + name: 'orcad.migration.stageBlobChunk', + permission: 'host-admin', + params: OrcadMigrationBlobChunkRequestSchema, + handler: async (params, context) => { + requireMigrationRuntimeClient(context) + return context.runtime.stageOrcadMigrationBlobChunk(params) + } + }), defineMethod({ name: 'orcad.migration.abortCatalog', permission: 'host-admin', diff --git a/src/main/runtime/rpc/methods/ssh.test.ts b/src/main/runtime/rpc/methods/ssh.test.ts index b71602fadfa..3c6e6723084 100644 --- a/src/main/runtime/rpc/methods/ssh.test.ts +++ b/src/main/runtime/rpc/methods/ssh.test.ts @@ -59,10 +59,20 @@ describe('ssh RPC methods', () => { const response = await dispatcher.dispatch(makeRequest('ssh.connect', { targetId: 'ssh-1' })) - expect(connectRegisteredSshTargetMock).toHaveBeenCalledWith('ssh-1') + expect(connectRegisteredSshTargetMock).toHaveBeenCalledWith('ssh-1', { automatic: false }) expect(response).toMatchObject({ ok: true, result: { state } }) }) + it("keeps a paired client's automatic connect from lifting a cancelled sign-in", async () => { + connectRegisteredSshTargetMock.mockResolvedValueOnce(null) + const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService + const dispatcher = new RpcDispatcher({ runtime, methods: SSH_METHODS }) + + await dispatcher.dispatch(makeRequest('ssh.connect', { targetId: 'ssh-1', automatic: true })) + + expect(connectRegisteredSshTargetMock).toHaveBeenCalledWith('ssh-1', { automatic: true }) + }) + it('returns null when the target has no registered state yet', async () => { getRegisteredSshStateMock.mockReturnValueOnce(undefined) const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService diff --git a/src/main/runtime/rpc/methods/ssh.ts b/src/main/runtime/rpc/methods/ssh.ts index e2bce335a98..0f74474e3e8 100644 --- a/src/main/runtime/rpc/methods/ssh.ts +++ b/src/main/runtime/rpc/methods/ssh.ts @@ -18,6 +18,7 @@ import type { } from '../../../../shared/ssh-target-management' import { SshBrowseDir, + SshConnect, SshTarget, SshTargetAdd, SshTargetUpdate @@ -84,10 +85,13 @@ export const SSH_METHODS = [ defineMethod({ name: 'ssh.connect', permission: 'host-admin', - params: SshTarget, + params: SshConnect, handler: async (params) => { try { - return { state: getPublicSshState(await connectRegisteredSshTarget(params.targetId)) } + const state = await connectRegisteredSshTarget(params.targetId, { + automatic: params.automatic === true + }) + return { state: getPublicSshState(state) } } catch { const state = getRegisteredSshState(params.targetId) throw new Error(getPublicSshError(state?.status ?? 'error')) diff --git a/src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts index e2b33bc9bb6..f2a79711711 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-attachments.test.ts @@ -1,3 +1,4 @@ +import '../unused-default-rpc-methods.test-fixture' import { mkdtemp, readFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' diff --git a/src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher.test.ts b/src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher.test.ts index 564eaed649a..3dfe2f824bc 100644 --- a/src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher.test.ts +++ b/src/main/runtime/rpc/orchestration-legacy-compatibility-dispatcher.test.ts @@ -1,3 +1,4 @@ +import './unused-default-rpc-methods.test-fixture' import { createHash } from 'node:crypto' import { afterEach, describe, expect, it, vi } from 'vitest' import type Database from '../../sqlite/sync-database' diff --git a/src/main/runtime/runtime-agent-terminal-startup.ts b/src/main/runtime/runtime-agent-terminal-startup.ts index 8c1ae331464..7e57f9c8017 100644 --- a/src/main/runtime/runtime-agent-terminal-startup.ts +++ b/src/main/runtime/runtime-agent-terminal-startup.ts @@ -27,7 +27,6 @@ import { import { agentLaunchPlatformForScope, agentLaunchProjectRuntime, - openCodeModelCheckPlatform, resolveAgentLaunchRuntime } from './agent-launch-scope-runtime' import { resolveQoderTerminalCommand } from './qoder-terminal-command-resolution' @@ -182,9 +181,6 @@ async function buildRuntimeAgentTerminalStartupOptions( shell: agentLaunchQuotingShell(runtime), sessionOptions: host.toSessionOptions(opts.launchPreferences) }), - ...(host.store - ? { modelCheckPlatform: openCodeModelCheckPlatform(host.store, workspace, runtime.platform) } - : {}), prompt: opts.startupPrompt ?? '', cwd: resolveTerminalStartupCwd(workspace.path, opts.cwd) ?? workspace.path, hostIdentity: host.hostIdentity, diff --git a/src/main/runtime/runtime-linear-team-write-commands.ts b/src/main/runtime/runtime-linear-team-write-commands.ts index 97655e68ef2..15926c19ab8 100644 --- a/src/main/runtime/runtime-linear-team-write-commands.ts +++ b/src/main/runtime/runtime-linear-team-write-commands.ts @@ -15,6 +15,7 @@ import type { } from './runtime-linear-command-dependencies' import type { ResolvedWorktree } from './runtime-worktree-path-identity' import { RuntimeLinearDedupeCommands } from './runtime-linear-dedupe-commands' +import { isWslMoveWorktreeBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' export class RuntimeLinearTeamWriteCommands extends RuntimeLinearDedupeCommands { public parseLinearAttachmentUrl(value: string): URL { @@ -268,10 +269,13 @@ export class RuntimeLinearTeamWriteCommands extends RuntimeLinearDedupeCommands worktree.linkedLinearIssueOrganizationUrlKey ) if (backfill?.workspaceId) { - this.setWorktreeMeta(worktree.id, { - linkedLinearIssueWorkspaceId: backfill.workspaceId, - linkedLinearIssueOrganizationUrlKey: backfill.organizationUrlKey ?? null - }) + // The answer still serves this lookup; persisting it waits out a WSL move. + if (!isWslMoveWorktreeBlocked(worktree.id, worktree.hostId, worktree.path)) { + this.setWorktreeMeta(worktree.id, { + linkedLinearIssueWorkspaceId: backfill.workspaceId, + linkedLinearIssueOrganizationUrlKey: backfill.organizationUrlKey ?? null + }) + } return { ...link, workspaceId: backfill.workspaceId, diff --git a/src/main/runtime/runtime-managed-worktree-metadata-wsl-move.test.ts b/src/main/runtime/runtime-managed-worktree-metadata-wsl-move.test.ts new file mode 100644 index 00000000000..a2b11a8ef35 --- /dev/null +++ b/src/main/runtime/runtime-managed-worktree-metadata-wsl-move.test.ts @@ -0,0 +1,128 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { closeTestStores, createStore, testState } from '../persistence-test-harness' +import type { Store } from '../persistence' +import { wslMoveCutoverFixture as cutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + WslMoveProjectFencedError, + loadWslMoveFence, + resetWslMoveFenceForTests +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' +import { + persistRuntimeManagedWorktreeSortOrder, + updateRuntimeManagedWorktreeMetadata +} from './runtime-managed-worktree-metadata' +import type { ResolvedWorktree } from './runtime-worktree-path-identity' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { isEncryptionAvailable: () => false } +})) + +const MOVING = 'repo-1::/src/app' +const STAYING = 'repo-2::/src/other' + +function resolved(id: string): ResolvedWorktree { + const repoId = id.slice(0, id.indexOf('::')) + const path = id.slice(repoId.length + 2) + return { + id, + repoId, + instanceId: `${id}#instance`, + hostId: 'local', + path, + head: 'abc', + branch: 'refs/heads/main', + isBare: false, + isMainWorktree: false, + displayName: 'app', + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0, + parentWorktreeId: null, + childWorktreeIds: [], + lineage: null, + git: { path, head: 'abc', branch: 'refs/heads/main', isBare: false, isMainWorktree: false } + } +} + +function ports() { + return { + resolveWorktree: vi.fn(async (selector: string) => resolved(selector.replace(/^id:/, ''))), + validateParent: vi.fn(), + invalidateResolved: vi.fn(), + invalidateScan: vi.fn(), + notifyChanged: vi.fn(), + showWorktree: vi.fn(async (selector: string) => resolved(selector.replace(/^id:/, ''))) + } +} + +let store: Store +beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'wsl-move-meta-')) + store = createStore() + loadWslMoveFence(testState.dir) + seedWslMoveCutover(testState.dir, cutover('move-1', 'source-fenced')) +}) +afterEach(async () => { + resetWslMoveFenceForTests() + await closeTestStores() + rmSync(testState.dir, { recursive: true, force: true }) +}) + +describe('updateRuntimeManagedWorktreeMetadata under a WSL move', () => { + type Updates = Parameters[0]['updates'] + const update = (selector: string, updates: Updates, p = ports()) => + updateRuntimeManagedWorktreeMetadata({ selector: `id:${selector}`, updates, store, ports: p }) + + it('drops background bookkeeping quietly and writes nothing', async () => { + const p = ports() + const result = await update(MOVING, { isUnread: true, lastActivityAt: 5 }, p) + expect(result.id).toBe(MOVING) + expect(store.getWorktreeMeta(MOVING)).toBeUndefined() + expect(p.notifyChanged).not.toHaveBeenCalled() + }) + + it('refuses a user edit', async () => { + await expect(update(MOVING, { comment: 'mine' })).rejects.toBeInstanceOf( + WslMoveProjectFencedError + ) + expect(store.getWorktreeMeta(MOVING)).toBeUndefined() + }) + + it('refuses a lineage change even alongside passive fields', async () => { + await expect( + update(MOVING, { isUnread: true, lineage: { noParent: true } }) + ).rejects.toBeInstanceOf(WslMoveProjectFencedError) + }) + + it('leaves a project the move is not taking untouched', async () => { + await update(STAYING, { comment: 'mine' }) + expect(store.getWorktreeMeta(STAYING)?.comment).toBe('mine') + }) +}) + +describe('persistRuntimeManagedWorktreeSortOrder under a WSL move', () => { + it('drops the moving project from the batch and writes the rest', () => { + store.setWorktreeMeta(MOVING, { sortOrder: 1 }) + store.setWorktreeMeta(STAYING, { sortOrder: 2 }) + const result = persistRuntimeManagedWorktreeSortOrder({ + orderedIds: [MOVING, STAYING], + store, + invalidateResolved: vi.fn(), + notifyChanged: vi.fn() + }) + expect(result).toEqual({ updated: 1 }) + expect(store.getWorktreeMeta(MOVING)?.sortOrder).toBe(1) + expect(store.getWorktreeMeta(STAYING)?.sortOrder).toBeGreaterThan(2) + }) +}) diff --git a/src/main/runtime/runtime-managed-worktree-metadata.ts b/src/main/runtime/runtime-managed-worktree-metadata.ts index 0ee206ed7e0..ec92c43fd43 100644 --- a/src/main/runtime/runtime-managed-worktree-metadata.ts +++ b/src/main/runtime/runtime-managed-worktree-metadata.ts @@ -6,6 +6,12 @@ import { planWorktreeSortOrderUpdates } from '../../shared/worktree/sort-order-u import { stripOrcaProvenanceMetaUpdates } from '../worktree-removal-safety' import { isRemovedFolderWorkspaceInstance } from '../folder-workspace-meta-presence' import type { RuntimeStore } from './runtime-store-contract' +import { + isWslMoveSourceHost, + isWslMoveWorkspaceBlocked, + shouldDropWslMoveProjectUpdate, + wslMoveProjectRefForWorkspace +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' import { RuntimeLineageError } from './runtime-worktree-lineage-resolution' import type { ResolvedWorktree } from './runtime-worktree-path-identity' @@ -31,6 +37,17 @@ export async function updateRuntimeManagedWorktreeMetadata(args: { }): Promise { const worktree = await args.ports.resolveWorktree(args.selector) const { lineage, ...metaUpdates } = args.updates + const executionHostId = worktree.identity?.executionHostId ?? worktree.hostId + if ( + isWslMoveSourceHost(executionHostId) && + shouldDropWslMoveProjectUpdate( + args.store, + wslMoveProjectRefForWorkspace(worktree.id), + lineage ? args.updates : metaUpdates + ) + ) { + return worktree + } if (lineage?.parentWorktree) { args.ports.invalidateResolved() args.ports.invalidateScan(worktree.repoId) @@ -91,7 +108,6 @@ export async function updateRuntimeManagedWorktreeMetadata(args: { }) } const metadataUpdates = stripOrcaProvenanceMetaUpdates(persisted) - const executionHostId = worktree.identity?.executionHostId ?? worktree.hostId // Why: a resolution snapshot taken before a removal still returns the workspace (#22712). if (isRemovedFolderWorkspaceInstance(args.store, worktree.id, executionHostId)) { throw new Error('selector_not_found') @@ -113,11 +129,12 @@ export function persistRuntimeManagedWorktreeSortOrder(args: { invalidateResolved: () => void notifyChanged: (repoId: string) => void }): { updated: number } { + // A moving project keeps its old order; the rest of the batch still lands. const updates = planWorktreeSortOrderUpdates( args.orderedIds, (worktreeId) => args.store.getWorktreeMeta(worktreeId), Date.now() - ) + ).filter((update) => !isWslMoveWorkspaceBlocked(args.store, update.worktreeId)) for (const update of updates) { args.store.setWorktreeMeta(update.worktreeId, { sortOrder: update.sortOrder }) } diff --git a/src/main/runtime/runtime-preserved-branch-cleanup.ts b/src/main/runtime/runtime-preserved-branch-cleanup.ts index 99b1ccf6325..f9d34ed390b 100644 --- a/src/main/runtime/runtime-preserved-branch-cleanup.ts +++ b/src/main/runtime/runtime-preserved-branch-cleanup.ts @@ -12,6 +12,7 @@ import { cleanupUnusedWorktreePushTargetRemote } from '../ipc/worktree-remote' import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options' import { SshHostNotServedError } from '../providers/ssh-host-not-served-error' import type { Store } from '../persistence' +import { assertWslMoveRepoWritable } from '../persistence/moving-wsl-projects/wsl-move-scope-guard' import { resolveWorktreeRemovalRepoOwner } from '../worktree-removal-repo-owner' import { parseExactWorktreeIdSelector } from './runtime-worktree-selection' @@ -116,6 +117,7 @@ export class RuntimePreservedBranchCleanup { if (!repo) { throw new Error('repo_not_found') } + assertWslMoveRepoWritable(store, repo) if (isFolderRepo(repo)) { throw new Error('Folder workspaces do not have local Git branches.') } diff --git a/src/main/runtime/runtime-project-group-controller.ts b/src/main/runtime/runtime-project-group-controller.ts index 41378b45b16..491318deb43 100644 --- a/src/main/runtime/runtime-project-group-controller.ts +++ b/src/main/runtime/runtime-project-group-controller.ts @@ -19,6 +19,12 @@ import { runtimeVisibleProjectGroups } from './runtime-visible-catalog' import { folderWorkspaceKey } from '../../shared/workspace-scope' +import { + assertWslMoveProjectWritable, + assertWslMoveRepoWritable, + isLocalWslMoveRow, + shouldDropWslMoveProjectUpdate +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' type RuntimeProjectGroupDependencies = { getStore: () => RuntimeStore | null @@ -128,6 +134,7 @@ export class RuntimeProjectGroupController { throw new Error('runtime_unavailable') } const repo = await this.deps.resolveRepo(repoSelector) + assertWslMoveRepoWritable(store, repo) const moved = store.moveProjectToGroup(repo.id, groupId, order) if (!moved) { throw new Error('repo_not_found') @@ -199,6 +206,14 @@ export class RuntimeProjectGroupController { if (!store?.updateFolderWorkspace) { throw new Error('runtime_unavailable') } + const existing = store.getFolderWorkspaces?.().find((entry) => entry.id === folderWorkspaceId) + if ( + existing && + isLocalWslMoveRow(existing) && + shouldDropWslMoveProjectUpdate(store, { folderWorkspaceId }, updates) + ) { + return existing + } if (typeof updates.folderPath === 'string' && updates.folderPath.trim().length > 0) { const workspace = store .getFolderWorkspaces?.() @@ -232,6 +247,9 @@ export class RuntimeProjectGroupController { throw new Error('runtime_unavailable') } const workspace = store.getFolderWorkspaces?.().find((entry) => entry.id === folderWorkspaceId) + if (workspace && isLocalWslMoveRow(workspace)) { + assertWslMoveProjectWritable(store, { folderWorkspaceId }) + } if (workspace) { const worktreeId = folderWorkspaceKey(folderWorkspaceId) // Why: a mixed-host group has no single PTY target; forgetting the diff --git a/src/main/runtime/runtime-project-host-setup-controller.ts b/src/main/runtime/runtime-project-host-setup-controller.ts index a29e999d1a2..58b9101cf80 100644 --- a/src/main/runtime/runtime-project-host-setup-controller.ts +++ b/src/main/runtime/runtime-project-host-setup-controller.ts @@ -34,6 +34,11 @@ import { countRepoWorkspacesWithMetadata, describeProjectRemovalRefusal } from './project-removal-impact' +import { + assertWslMoveHostSetupIdWritable, + assertWslMoveHostSetupWritable, + assertWslMoveReposWritable +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' type RuntimeProjectHostSetupDependencies = { getStore: () => RuntimeStore | null @@ -73,6 +78,8 @@ export class RuntimeProjectHostSetupController { if (!store?.updateProject) { throw new Error('runtime_unavailable') } + const existing = store.getProjects?.().find((entry) => entry.id === projectId) + assertWslMoveReposWritable(store, existing?.sourceRepoIds ?? []) const project = store.updateProject(projectId, updates) if (!project) { throw new Error(`Project not found: ${projectId}`) @@ -91,6 +98,7 @@ export class RuntimeProjectHostSetupController { if (!store?.createProjectHostSetup) { throw new Error('runtime_unavailable') } + assertWslMoveHostSetupWritable(store, args) const result = store.createProjectHostSetup(args) if (!result) { throw new Error(`Project not found: ${args.projectId}`) @@ -139,6 +147,7 @@ export class RuntimeProjectHostSetupController { if (!store?.updateProjectHostSetup) { throw new Error('runtime_unavailable') } + assertWslMoveHostSetupIdWritable(store, args.setupId) const result = store.updateProjectHostSetup(args) if (!result) { throw new Error(`Project host setup not found: ${args.setupId}`) @@ -160,6 +169,7 @@ export class RuntimeProjectHostSetupController { if (!store?.deleteProjectHostSetup) { throw new Error('runtime_unavailable') } + assertWslMoveHostSetupIdWritable(store, args.setupId) if (args.force === false) { this.assertSetupRemovalIsUnused(store, args.setupId) } @@ -238,6 +248,11 @@ export class RuntimeProjectHostSetupController { if (!store) { throw new Error('runtime_unavailable') } + assertWslMoveHostSetupWritable(store, { + projectId: args.projectId, + hostId: getRepoExecutionHostId(initialRepo), + repoId: initialRepo.id + }) let repo = initialRepo let setup = getProjectHostSetupForRepo(this.listSetups(), repo) if (setup.projectId !== args.projectId) { diff --git a/src/main/runtime/runtime-provisioned-root-adoption.test.ts b/src/main/runtime/runtime-provisioned-root-adoption.test.ts index 5a6f3261a0e..364b3a90f30 100644 --- a/src/main/runtime/runtime-provisioned-root-adoption.test.ts +++ b/src/main/runtime/runtime-provisioned-root-adoption.test.ts @@ -1,4 +1,14 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' +import { wslMoveCutoverFixture as cutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from '../persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + WslMoveProjectFencedError, + loadWslMoveFence, + resetWslMoveFenceForTests +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' import type { Repo } from '../../shared/repo-types' import type { GitWorktreeInfo } from '../../shared/worktree/types' import { @@ -32,6 +42,7 @@ function ports(worktrees: GitWorktreeInfo[], sparse = false) { const value: RuntimeProvisionedRootPorts = { store: { getSettings: () => ({ workspaceDir: '/workspaces', nestWorkspaces: false }), + getRepo: () => repo, setWorktreeMeta }, resolveRepo: async () => repo, @@ -91,4 +102,29 @@ describe('adoptRuntimeProvisionedRoot', () => { adoptRuntimeProvisionedRoot('repo-vm', { name: 'fix-login', expectedPath: '/other' }, value) ).rejects.toThrow('does not match') }) + + it('refuses a project a WSL move is taking before reading its checkout', async () => { + const userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-adopt-')) + try { + loadWslMoveFence(userDataPath) + seedWslMoveCutover( + userDataPath, + cutover('move-1', 'source-fenced', {}, { repoIds: ['repo-vm'] }) + ) + const listWorktrees = vi.fn(async () => [checkout()]) + const { value, setWorktreeMeta } = ports([checkout()]) + await expect( + adoptRuntimeProvisionedRoot( + 'repo-vm', + { name: 'fix-login', expectedPath: root }, + { ...value, listWorktrees } + ) + ).rejects.toBeInstanceOf(WslMoveProjectFencedError) + expect(listWorktrees).not.toHaveBeenCalled() + expect(setWorktreeMeta).not.toHaveBeenCalled() + } finally { + resetWslMoveFenceForTests() + rmSync(userDataPath, { recursive: true, force: true }) + } + }) }) diff --git a/src/main/runtime/runtime-provisioned-root-adoption.ts b/src/main/runtime/runtime-provisioned-root-adoption.ts index 3becab13b6a..42673dd0002 100644 --- a/src/main/runtime/runtime-provisioned-root-adoption.ts +++ b/src/main/runtime/runtime-provisioned-root-adoption.ts @@ -16,11 +16,12 @@ import { } from '../provisioned-root-checkout' import { gitExecFileAsync } from '../git/runner' import type { RuntimeStore } from './runtime-store-contract' +import { assertWslMoveRepoWritable } from '../persistence/moving-wsl-projects/wsl-move-scope-guard' export type RuntimeProvisionedRootRequest = ProvisionedRootRequest & { expectedPath: string } export type RuntimeProvisionedRootPorts = { - store: ProvisionedRootMetaStore & Pick + store: ProvisionedRootMetaStore & Pick resolveRepo: (selector: string) => Promise listWorktrees: (repoPath: string) => Promise isSparseCheckoutEnabled: (checkoutPath: string) => Promise @@ -41,6 +42,7 @@ export async function adoptRuntimeProvisionedRoot( if (isFolderRepo(repo) || getRepoSshConnectionId(repo)) { throw new Error('Provisioned-root adoption requires a Git checkout on this host.') } + assertWslMoveRepoWritable(ports.store, repo) if (!provisionedRootPathsEqual(repo.path, request.expectedPath)) { throw new Error('The recipe projectRoot does not match the imported Git checkout root.') } diff --git a/src/main/runtime/runtime-repository-fork-backfill.ts b/src/main/runtime/runtime-repository-fork-backfill.ts index df9d769f167..4bbee92e640 100644 --- a/src/main/runtime/runtime-repository-fork-backfill.ts +++ b/src/main/runtime/runtime-repository-fork-backfill.ts @@ -1,9 +1,14 @@ -import { getRepoSshConnectionId, LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' +import { + getRepoExecutionHostId, + getRepoSshConnectionId, + LOCAL_EXECUTION_HOST_ID +} from '../../shared/execution-host' import type { GitHubOwnerRepo } from '../../shared/github/pull-request-types' import type { Repo } from '../../shared/repo-types' import { getRepoUpstream } from '../github/client' import { detectGitHubAvatarIcon } from '../repo-icon-autodetect' import type { RuntimeStore } from './runtime-store-contract' +import { isWslMoveRepoBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' export class RuntimeRepositoryForkBackfill { private started = false @@ -32,7 +37,13 @@ export class RuntimeRepositoryForkBackfill { // Why the resolved SSH target and not the raw `connectionId`: this backfill runs `gh`/git // in this process, so any row whose files sit on an SSH host must be skipped — including // one that carries only `executionHostId: ssh:…`, which the raw field reads as local. - if (repo.upstream !== undefined || repo.kind === 'folder' || getRepoSshConnectionId(repo)) { + // A repo a WSL move is taking keeps `upstream` unset, so the next startup retries it. + if ( + repo.upstream !== undefined || + repo.kind === 'folder' || + getRepoSshConnectionId(repo) || + isWslMoveRepoBlocked(repo) + ) { continue } let upstream: GitHubOwnerRepo | null @@ -45,8 +56,14 @@ export class RuntimeRepositoryForkBackfill { upstream && repo.repoIcon?.type === 'image' && repo.repoIcon.source === 'github' ? await detectGitHubAvatarIcon(repo.path, LOCAL_EXECUTION_HOST_ID, upstream) : null - const current = store.getRepos().find((candidate) => candidate.id === repo.id) - if (!current || current.upstream !== undefined) { + // By id and host: a WSL move leaves a hidden local row beside the host's same-id row. + const hostId = getRepoExecutionHostId(repo) + const current = store + .getRepos() + .find( + (candidate) => candidate.id === repo.id && getRepoExecutionHostId(candidate) === hostId + ) + if (!current || current.upstream !== undefined || isWslMoveRepoBlocked(current)) { continue } const updates: Partial = { upstream: upstream ?? null } @@ -57,7 +74,7 @@ export class RuntimeRepositoryForkBackfill { ) { updates.repoIcon = repoIcon } - store.updateRepo(repo.id, updates) + store.updateRepo(repo.id, updates, hostId) changed = true } if (changed) { diff --git a/src/main/runtime/runtime-repository-issue-command.ts b/src/main/runtime/runtime-repository-issue-command.ts index 9b4d5ed3794..0c3f540ac22 100644 --- a/src/main/runtime/runtime-repository-issue-command.ts +++ b/src/main/runtime/runtime-repository-issue-command.ts @@ -4,10 +4,13 @@ import type { Repo } from '../../shared/repo-types' import { readIssueCommand, writeIssueCommand } from '../issue-command-file' import { isFolderRepo } from '../../shared/repo-kind' import { joinWorktreeRelativePath } from './runtime-relative-paths' +import type { RuntimeStore } from './runtime-store-contract' +import { assertWslMoveRepoWritable } from '../persistence/moving-wsl-projects/wsl-move-scope-guard' type RuntimeRepositoryIssueCommandDeps = { resolveRepo: (selector: string) => Promise getLocalGitArgs: (repo: Repo) => [] | [GitRuntimeOptions] + getStore?: () => RuntimeStore | null } export class RuntimeRepositoryIssueCommand { @@ -41,6 +44,7 @@ export class RuntimeRepositoryIssueCommand { /** Save a private override on its execution host; blank content restores the shared command. */ async write(repoSelector: string, content: string): Promise<{ ok: true }> { const repo = await this.deps.resolveRepo(repoSelector) + assertWslMoveRepoWritable(this.deps.getStore?.() ?? undefined, repo) if (isFolderRepo(repo)) { return { ok: true } } diff --git a/src/main/runtime/runtime-repository-settings-controller.ts b/src/main/runtime/runtime-repository-settings-controller.ts index 1cc0897d566..ce7384e2297 100644 --- a/src/main/runtime/runtime-repository-settings-controller.ts +++ b/src/main/runtime/runtime-repository-settings-controller.ts @@ -5,6 +5,7 @@ import type { GhAccountBinding } from '../../shared/github/account-binding' import { invalidateAuthorizedRootsCache } from '../ipc/filesystem-auth' import { prepareLocalWorktreeRootForRepo } from '../worktree-root-preparation' import type { RuntimeStore } from './runtime-store-contract' +import { assertWslMoveRepoWritable } from '../persistence/moving-wsl-projects/wsl-move-scope-guard' type RuntimeRepositorySettingsDependencies = { getStore: () => RuntimeStore | null @@ -60,6 +61,7 @@ export class RuntimeRepositorySettingsController { async setBaseRef(repoSelector: string, baseRef: string): Promise { const store = this.requireStore() const repo = await this.deps.resolveRepo(repoSelector) + assertWslMoveRepoWritable(store, repo) if (isFolderRepo(repo)) { throw new Error('Folder mode does not support base refs.') } @@ -75,6 +77,7 @@ export class RuntimeRepositorySettingsController { async update(repoSelector: string, updates: RepositoryUpdates): Promise { const store = this.requireStore() const repo = await this.deps.resolveRepo(repoSelector) + assertWslMoveRepoWritable(store, repo) const sanitizedUpdates = omitUndefined(updates) if ('worktreeBasePath' in updates && updates.worktreeBasePath === undefined) { sanitizedUpdates.worktreeBasePath = undefined @@ -110,7 +113,9 @@ export class RuntimeRepositorySettingsController { throw new Error('runtime_unavailable') } const repo = await this.deps.resolveRepo(repoSelector) - store.removeProjectForHost(repo.id, getRepoExecutionHostId(repo)) + const hostId = getRepoExecutionHostId(repo) + assertWslMoveRepoWritable(store, repo) + store.removeProjectForHost(repo.id, hostId) this.deps.forgetTerminalTopology(repo.id) this.deps.invalidateResolvedWorktrees() this.deps.invalidateWorktreeScan(repo.id) diff --git a/src/main/runtime/runtime-repository-sparse-presets.ts b/src/main/runtime/runtime-repository-sparse-presets.ts index 664798a271d..bd5030ae12d 100644 --- a/src/main/runtime/runtime-repository-sparse-presets.ts +++ b/src/main/runtime/runtime-repository-sparse-presets.ts @@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto' import type { Repo } from '../../shared/repo-types' import { normalizeSparseDirectories } from '../ipc/sparse-checkout-directories' import type { RuntimeStore } from './runtime-store-contract' +import { assertWslMoveRepoWritable } from '../persistence/moving-wsl-projects/wsl-move-scope-guard' type RuntimeRepositorySparsePresetDependencies = { getStore: () => RuntimeStore | null @@ -56,6 +57,7 @@ export class RuntimeRepositorySparsePresets { throw new Error('runtime_unavailable') } const repo = await this.deps.resolveRepo(repoSelector) + assertWslMoveRepoWritable(store, repo) const name = normalizeName(args.name) const directories = normalizeDirectories(args.directories) const now = Date.now() diff --git a/src/main/runtime/runtime-store-contract.ts b/src/main/runtime/runtime-store-contract.ts index c6ed7797ae4..9528b78bf88 100644 --- a/src/main/runtime/runtime-store-contract.ts +++ b/src/main/runtime/runtime-store-contract.ts @@ -58,8 +58,14 @@ export type RuntimeStore = { stageOrcadMigrationCatalog?: Store['stageOrcadMigrationCatalog'] commitStagedOrcadMigrationCatalog?: Store['commitStagedOrcadMigrationCatalog'] stageOrcadMigrationSnapshotChunk?: Store['stageOrcadMigrationSnapshotChunk'] + stageOrcadMigrationBlobChunk?: Store['stageOrcadMigrationBlobChunk'] abortStagedOrcadMigrationCatalog?: Store['abortStagedOrcadMigrationCatalog'] getOrcadMigrationCatalogState?: Store['getOrcadMigrationCatalogState'] + installOrcadMigrationReceivedBlobs?: Store['installOrcadMigrationReceivedBlobs'] + installPendingOrcadMigrationReceivedBlobs?: Store['installPendingOrcadMigrationReceivedBlobs'] + installDeferredOrcadMigrationShellHistory?: Store['installDeferredOrcadMigrationShellHistory'] + peekOrcadMigrationTerminalRestoreHint?: Store['peekOrcadMigrationTerminalRestoreHint'] + consumeOrcadMigrationTerminalRestoreHint?: Store['consumeOrcadMigrationTerminalRestoreHint'] persistPtyBinding?: Store['persistPtyBinding'] admitTerminalPane?: Store['admitTerminalPane'] withdrawTerminalPane?: Store['withdrawTerminalPane'] diff --git a/src/main/runtime/runtime-worktree-filesystem.ts b/src/main/runtime/runtime-worktree-filesystem.ts index 59955e0dfe8..dfde4538a86 100644 --- a/src/main/runtime/runtime-worktree-filesystem.ts +++ b/src/main/runtime/runtime-worktree-filesystem.ts @@ -18,6 +18,7 @@ import { mergeRuntimeFolderWorkspace } from './runtime-folder-workspace' import type { RuntimeStore } from './runtime-store-contract' +import { isWslMoveRepoBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' /** Callers hold a local removal route; an `ssh:` or `runtime:` host is refused before this. */ export async function isLocalRuntimeWorktreePathMissing( @@ -44,6 +45,8 @@ export function listRuntimeFolderWorkspaces( const rootId = getRuntimeFolderWorkspaceRootId(repo) const allMeta = store.getAllWorktreeMeta() const expectedHostId = getRepoExecutionHostId(repo) + // A project a WSL move is taking still lists, without stamping identities it lacks. + const canWrite = !isWslMoveRepoBlocked(repo) const ids = Object.keys(allMeta).filter( (worktreeId) => isRuntimeFolderWorkspaceIdForRepo(repo, worktreeId) && @@ -58,12 +61,12 @@ export function listRuntimeFolderWorkspaces( const existing = getRepoOwnedWorktreeMeta(repo, worktreeId, allMeta, repoOwnerCount) const meta: Partial = existing?.instanceId ? existing - : existing || repoOwnerCount === 1 + : canWrite && (existing || repoOwnerCount === 1) ? store.setWorktreeMeta(worktreeId, { instanceId: getRuntimeFolderWorkspaceInstanceIdentity(repo, worktreeId), ...(existing ? {} : { displayName: repo.displayName, lastActivityAt: Date.now() }) }) - : {} + : (existing ?? {}) return { ...mergeRuntimeFolderWorkspace(repo, worktreeId, meta), hostId: repoOwnerCount === 1 ? (meta.hostId ?? expectedHostId) : expectedHostId diff --git a/src/main/runtime/runtime-worktree-lineage-controller.ts b/src/main/runtime/runtime-worktree-lineage-controller.ts index 93360359fba..de48270a13b 100644 --- a/src/main/runtime/runtime-worktree-lineage-controller.ts +++ b/src/main/runtime/runtime-worktree-lineage-controller.ts @@ -10,6 +10,7 @@ import { sharesResolvedWorktreeLineageBoundary } from '../../shared/resolved-wor import type { OrchestrationDb } from './orchestration/db' import type { RuntimeStore } from './runtime-store-contract' import type { ResolvedWorktree } from './runtime-worktree-path-identity' +import { isWslMoveWorktreeBlocked } from '../persistence/moving-wsl-projects/wsl-move-background-backoff' import { RuntimeLineageError, enforceCreateLineageHostBoundary, @@ -154,7 +155,11 @@ export class RuntimeWorktreeLineageController { return } for (const worktree of await this.deps.listResolvedWorktrees()) { - if (store.getWorktreeLineage(worktree.id) || !worktree.instanceId) { + if ( + store.getWorktreeLineage(worktree.id) || + !worktree.instanceId || + isWslMoveWorktreeBlocked(worktree.id, worktree.hostId, worktree.path) + ) { continue } const taskId = worktree.comment?.match(/\btask_[A-Za-z0-9]+\b/)?.[0] diff --git a/src/main/runtime/structured-agent-runtime-registrations.ts b/src/main/runtime/structured-agent-runtime-registrations.ts index a770451e54a..a062e762094 100644 --- a/src/main/runtime/structured-agent-runtime-registrations.ts +++ b/src/main/runtime/structured-agent-runtime-registrations.ts @@ -241,6 +241,7 @@ function acpRegistration(spec: AcpLaunchSpec): StructuredAgentRuntimeRegistratio readJournal, resolveLaunch: composeStructuredLaunch(definition, sources, part), readLaunch: composeStructuredLaunchRead(definition, sources, part), + resolveLaunchArgs: () => deps.resolveLaunchArgs(spec.agent), connect: (launch, options) => createAcpAgentConnection(launch, options), onChildWorkEvidence: (sessionId, evidence) => context.host()?.publishChildWorkEvidence(sessionId, evidence), diff --git a/src/main/runtime/structured-agent-session-create-adoption.ts b/src/main/runtime/structured-agent-session-create-adoption.ts index 80ed28fd402..627da4a842b 100644 --- a/src/main/runtime/structured-agent-session-create-adoption.ts +++ b/src/main/runtime/structured-agent-session-create-adoption.ts @@ -11,6 +11,7 @@ import { resolveStructuredAgentSessionAdoption, structuredAdoptionConflictError } from '../native-chat/structured-agent-session-history-adoption' +import { structuredAgentRuntimeRegistration } from './structured-agent-runtime-registrations' import type { StructuredAgentTranscriptImport, StructuredAgentTranscriptImportSettings @@ -25,11 +26,13 @@ export function resolveCommittedStructuredAgentSessionAdoptionIntent(input: { location: AgentSessionExecutionLocation options?: Readonly> }): AgentSessionAttachParams | null { + const history = structuredAgentRuntimeRegistration(input.agent)?.sessionHistory const replay = - input.resumeFrom && input.callerKey && input.host + input.resumeFrom && input.callerKey && input.host && history ? findCommittedStructuredAgentSessionAdoptionReplay({ agent: input.agent, providerSessionId: input.resumeFrom.providerSessionId, + rowSessionId: history.rowSessionId, selfSessionId: input.envelope.sessionId, callerKey: input.callerKey, operationId: input.envelope.clientOperationId, diff --git a/src/main/runtime/terminal-intentional-stop-exit.test.ts b/src/main/runtime/terminal-intentional-stop-exit.test.ts index 041d9710936..95cac3c8c5b 100644 --- a/src/main/runtime/terminal-intentional-stop-exit.test.ts +++ b/src/main/runtime/terminal-intentional-stop-exit.test.ts @@ -84,9 +84,7 @@ function createHarness(opts: { lateProviderExit?: boolean; folder?: boolean } = }) ptyOwnership.set(PTY_ID, 'local') ptyIncarnationById.set(PTY_ID, INCARNATION_ID) - let emitProviderExit: - | ((payload: { id: string; code: number; incarnationId?: string }) => void) - | undefined + let emitProviderExit: Parameters[0] | undefined const provider = { onData: () => () => {}, onExit: (listener: typeof emitProviderExit) => { @@ -124,6 +122,14 @@ function createHarness(opts: { lateProviderExit?: boolean; folder?: boolean } = deps, emitProviderExit: (incarnationId: string) => emitProviderExit?.({ id: PTY_ID, code: 0, incarnationId }), + /** The exit main synthesizes for each session when it restarts the daemon. */ + emitDaemonRestartExit: (replacedByRestart?: true) => + emitProviderExit?.({ + id: PTY_ID, + code: -1, + incarnationId: INCARNATION_ID, + ...(replacedByRestart ? { replacedByRestart } : {}) + }), boundPtyId: () => store.getWorkspaceSession().terminalLayoutsByTabId[TAB_ID]?.ptyIdsByLeafId?.[LEAF_ID] ?? null, tabIds: () => (store.getWorkspaceSession().tabsByWorktree[WORKTREE_ID] ?? []).map((t) => t.id), @@ -210,6 +216,29 @@ describe('intentional stops keep the pane through the exit', () => { ]) }) + it('keeps the pane when main restarts the daemon under it', () => { + const harness = createHarness() + + harness.emitDaemonRestartExit(true) + + expect(harness.tabIds()).toEqual([TAB_ID]) + expect(harness.boundPtyId()).toBe(PTY_ID) + expect(harness.rendererExits()).toEqual([ + { id: PTY_ID, code: -1, incarnationId: INCARNATION_ID, replacedByRestart: true } + ]) + }) + + it('retires the pane on a daemon-wide exit that no restart claims', () => { + const harness = createHarness() + + harness.emitDaemonRestartExit() + + expect(harness.boundPtyId()).toBeNull() + expect(harness.rendererExits()).toEqual([ + { id: PTY_ID, code: -1, incarnationId: INCARNATION_ID } + ]) + }) + it('keeps the pane through the synthetic exit and the provider exit that follows it', async () => { const harness = createHarness({ lateProviderExit: true }) diff --git a/src/main/runtime/terminal-intentional-stops.ts b/src/main/runtime/terminal-intentional-stops.ts index 88ac59a40d4..836743fde67 100644 --- a/src/main/runtime/terminal-intentional-stops.ts +++ b/src/main/runtime/terminal-intentional-stops.ts @@ -3,7 +3,8 @@ import { SYNTHETIC_KILL_EXIT_DUPLICATE_WINDOW_MS } from '../ipc/pty/delivery/vis /** * Why main stopped a PTY on purpose. Both kinds keep the pane's binding through the exit: * - `reversible`: sleep or hibernation; the binding is the wake hint. - * - `replaced`: a restart stops the old process, or a reattach finds it gone; a new one takes over. + * - `replaced`: a restart stops the old process, or a reattach finds it gone; a new one takes over, + * at once or (after a daemon restart) when the pane next connects. */ export type TerminalIntentionalStopKind = 'reversible' | 'replaced' diff --git a/src/main/runtime/workspace-layout-stream.test.ts b/src/main/runtime/workspace-layout-stream.test.ts index fc9bd1ac572..b85a29efebb 100644 --- a/src/main/runtime/workspace-layout-stream.test.ts +++ b/src/main/runtime/workspace-layout-stream.test.ts @@ -3,39 +3,77 @@ import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../shared/exec import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { localDesktopSession, - relaySshSession + relaySshSession, + serverRuntimeSession } from '../../shared/workspace-layout/workspace-layout-profile.test-fixture' import { emptySession, FOLDER_KEY, GIT_KEY, + SERVER_KEY, SSH_KEY } from '../../shared/workspace-layout/workspace-layout-session.test-fixture' import type { WorkspaceLayoutEvent } from '../../shared/workspace-layout/workspace-layout-stream-frames' +import { loadWorkspaceLayout } from '../../shared/workspace-layout/workspace-layout-load' +import { nameBasedLoadContext } from '../../shared/workspace-layout/workspace-layout-minted-ids' +import { + publishWorkspaceLayout, + type PublishedWorkspaceLayout +} from '../../shared/workspace-layout/workspace-layout-published' import { WorkspaceLayoutStream } from './workspace-layout-stream' const SSH_HOST: ExecutionHostId = 'ssh:target-1' +const SERVER_HOST: ExecutionHostId = 'runtime:env-1' function fakeStore(partitions: Map) { - const listeners = new Set<() => void>() - const onWorkspaceSessionWritten = vi.fn((listener: () => void) => { + const listeners = new Set<(hostId: ExecutionHostId | null) => void>() + const onWorkspaceSessionWritten = vi.fn((listener: (hostId: ExecutionHostId | null) => void) => { listeners.add(listener) return () => listeners.delete(listener) }) + const getWorkspaceSession = vi.fn((hostId: ExecutionHostId) => partitions.get(hostId)!) return { store: { - getWorkspaceSession: (hostId: ExecutionHostId) => partitions.get(hostId)!, + getWorkspaceSession, getWorkspaceSessionHostIds: () => [...partitions.keys()], onWorkspaceSessionWritten }, listeners, + /** A save that names the partition it wrote. */ write(hostId: ExecutionHostId, session: WorkspaceSessionState) { partitions.set(hostId, session) - listeners.forEach((listener) => listener()) + listeners.forEach((listener) => listener(hostId)) + }, + /** A save that names no partition, so every partition may have changed. */ + writeUnnamed(hostId: ExecutionHostId, session: WorkspaceSessionState) { + partitions.set(hostId, session) + listeners.forEach((listener) => listener(null)) + }, + /** A removed host's partition, named by the save that drops it. */ + removePartition(hostId: ExecutionHostId) { + partitions.delete(hostId) + listeners.forEach((listener) => listener(hostId)) } } } +/** Today's projection: every partition loaded with its save-and-diff report, then filtered by home. */ +function fullProjection( + partitions: Map, + homeHostId: (key: string) => ExecutionHostId | null +): [string, PublishedWorkspaceLayout][] { + const projected = new Map() + for (const [hostId, session] of partitions) { + const { layout } = loadWorkspaceLayout(hostId, session, nameBasedLoadContext()) + for (const [key, workspace] of Object.entries(layout.workspaces)) { + if (homeHostId(key) === hostId) { + projected.set(key, publishWorkspaceLayout(workspace, hostId)) + } + } + } + return [...projected] +} + function setup() { const partitions = new Map([ [LOCAL_EXECUTION_HOST_ID, localDesktopSession()], @@ -51,7 +89,7 @@ function setup() { store: () => fake.store, homeHostId: (key) => homes[key] ?? null }) - return { ...fake, stream, homes } + return { ...fake, stream, homes, partitions } } const flush = () => new Promise((resolve) => queueMicrotask(resolve)) @@ -199,4 +237,86 @@ describe('WorkspaceLayoutStream', () => { expect(JSON.stringify(later.tabs)).toBe(JSON.stringify(earlier.tabs.slice(1))) } }) + + it('re-projects only the partition a write names', async () => { + const { stream, store, write, writeUnnamed } = setup() + stream.subscribe(() => {}) + store.getWorkspaceSession.mockClear() + write(SSH_HOST, relaySshSession('target-1')) + await flush() + expect(store.getWorkspaceSession.mock.calls).toEqual([[SSH_HOST]]) + store.getWorkspaceSession.mockClear() + writeUnnamed(SSH_HOST, relaySshSession('target-1')) + await flush() + expect(store.getWorkspaceSession.mock.calls).toEqual([[LOCAL_EXECUTION_HOST_ID], [SSH_HOST]]) + }) + + it('publishes what a full re-projection with the load report would, write after write', async () => { + const { stream, write, writeUnnamed, removePartition, homes, partitions } = setup() + const homeHostId = (key: string) => homes[key] ?? null + let cache = new Map() + const { snapshot } = stream.subscribe((event) => { + if (event.type === 'removed') { + cache.delete(event.key) + } else { + cache.set(event.key, event.layout) + } + }) + cache = new Map(snapshot.map((entry) => [entry.key, entry.layout])) + const expectFullProjection = async (): Promise => { + await flush() + const expected = fullProjection(partitions, homeHostId) + expect(Object.fromEntries(cache)).toEqual(Object.fromEntries(expected)) + // A late subscriber's snapshot keeps today's order too. + const late = stream.subscribe(() => {}) + expect(late.snapshot.map((entry) => [entry.key, entry.layout])).toEqual(expected) + late.unsubscribe() + } + await expectFullProjection() + + const local = localDesktopSession() + local.tabGroups![GIT_KEY]![0]!.tabOrder.reverse() + write(LOCAL_EXECUTION_HOST_ID, local) + await expectFullProjection() + + // A named write and an unnamed one in the same task. + const ssh = relaySshSession('target-1') + delete ssh.tabsByWorktree[SSH_KEY] + delete ssh.unifiedTabs![SSH_KEY] + write(SSH_HOST, ssh) + writeUnnamed(LOCAL_EXECUTION_HOST_ID, localDesktopSession()) + await expectFullProjection() + + // A new partition, a home that moves between partitions, then the partition removed. + homes[SERVER_KEY] = SERVER_HOST + write(SERVER_HOST, serverRuntimeSession('env-1')) + await expectFullProjection() + homes[GIT_KEY] = SERVER_HOST + write(SSH_HOST, relaySshSession('target-1')) + await expectFullProjection() + homes[GIT_KEY] = LOCAL_EXECUTION_HOST_ID + removePartition(SERVER_HOST) + await expectFullProjection() + }) + + it('re-projects every partition after a failed projection', async () => { + const { stream, store, write } = setup() + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const events: WorkspaceLayoutEvent[] = [] + stream.subscribe((event) => events.push(event)) + store.getWorkspaceSession.mockImplementationOnce(() => { + throw new Error('read failed') + }) + const local = localDesktopSession() + local.tabGroups![GIT_KEY]![0]!.tabOrder.reverse() + write(LOCAL_EXECUTION_HOST_ID, local) + await flush() + expect(events).toEqual([]) + // The local edit the failed projection missed is published by a write elsewhere. + write(SSH_HOST, relaySshSession('target-1')) + await flush() + expect(events.map((event) => [event.type, event.key])).toEqual([['workspace', GIT_KEY]]) + expect(warn).toHaveBeenCalledTimes(1) + warn.mockRestore() + }) }) diff --git a/src/main/runtime/workspace-layout-stream.ts b/src/main/runtime/workspace-layout-stream.ts index 8c20e38cd18..fb3b4a0eb72 100644 --- a/src/main/runtime/workspace-layout-stream.ts +++ b/src/main/runtime/workspace-layout-stream.ts @@ -1,9 +1,10 @@ // The read-only layout stream behind `layout.subscribe`: each workspace's published layout, -// re-projected from the Store's sessions after every write while anyone subscribes. Nothing reads -// it until a client opts in; with no subscriber it does no work. +// re-projected from the Store's sessions after every write while anyone subscribes. A write that +// names its partition re-projects only that partition. Nothing reads it until a client opts in; +// with no subscriber it does no work. import type { ExecutionHostId } from '../../shared/execution-host' -import { loadWorkspaceLayout } from '../../shared/workspace-layout/workspace-layout-load' +import { loadWorkspaceLayoutWithoutReport } from '../../shared/workspace-layout/workspace-layout-load' import { stableJson } from '../../shared/workspace-layout/workspace-layout-load-report' import { publishWorkspaceLayout, @@ -26,8 +27,11 @@ export class WorkspaceLayoutStream { private readonly listeners = new Set() /** Workspace key → its published layout, with its JSON for the by-value comparison. */ private published = new Map() + /** Partition → every workspace it holds, its own copy of a workspace it does not own included. */ + private projectedPartitions = new Map>() private stopWatching: (() => void) | null = null - private dirty = false + /** Partitions to re-project at the next flush; 'all' when a write did not name its partition. */ + private dirty: Set | 'all' | null = null private failureLogged = false constructor( @@ -45,9 +49,10 @@ export class WorkspaceLayoutStream { } { if (this.listeners.size === 0) { this.stopWatching = - this.deps.store()?.onWorkspaceSessionWritten?.(() => this.markDirty()) ?? null + this.deps.store()?.onWorkspaceSessionWritten?.((hostId) => this.markDirty(hostId)) ?? null this.published = new Map() - this.dirty = true + this.projectedPartitions = new Map() + this.dirty = 'all' } // A write not yet published reaches existing listeners first, so the snapshot is current. this.flush() @@ -62,28 +67,38 @@ export class WorkspaceLayoutStream { this.stopWatching?.() this.stopWatching = null this.published.clear() + this.projectedPartitions.clear() } } } - private markDirty(): void { - if (this.dirty) { - return + private markDirty(hostId: ExecutionHostId | null): void { + const scheduled = this.dirty !== null + if (hostId === null) { + this.dirty = 'all' + } else if (this.dirty === null) { + this.dirty = new Set([hostId]) + } else if (this.dirty !== 'all') { + this.dirty.add(hostId) + } + if (!scheduled) { + // Coalesces a burst of writes in one task into one projection. + queueMicrotask(() => this.flush()) } - this.dirty = true - // Coalesces a burst of writes in one task into one projection. - queueMicrotask(() => this.flush()) } private flush(): void { - if (!this.dirty) { + const dirty = this.dirty + if (dirty === null) { return } - this.dirty = false + this.dirty = null // A failed projection is logged once; it never reaches the write that notified it. try { - this.reconcile() + this.reconcile(dirty) } catch (error) { + // The cache may be part-updated; the next write re-projects every partition. + this.projectedPartitions.clear() this.logFailure('projection', error) } } @@ -95,28 +110,43 @@ export class WorkspaceLayoutStream { } } - private project(): Map { - const store = this.deps.store() + private projectPartition(hostId: ExecutionHostId): Map { const projected = new Map() - for (const hostId of store?.getWorkspaceSessionHostIds?.() ?? []) { - const session = store?.getWorkspaceSession?.(hostId) - if (!session) { - continue - } - const { layout } = loadWorkspaceLayout(hostId, session, nameBasedLoadContext()) - for (const [key, workspace] of Object.entries(layout.workspaces)) { + const session = this.deps.store()?.getWorkspaceSession?.(hostId) + if (!session) { + return projected + } + const { layout } = loadWorkspaceLayoutWithoutReport(hostId, session, nameBasedLoadContext()) + for (const [key, workspace] of Object.entries(layout.workspaces)) { + const published = publishWorkspaceLayout(workspace, hostId) + projected.set(key, { layout: published, json: stableJson(published) }) + } + return projected + } + + private project(dirty: Set | 'all'): Map { + const hostIds = this.deps.store()?.getWorkspaceSessionHostIds?.() ?? [] + const partitions = new Map>() + for (const hostId of hostIds) { + const cached = + dirty === 'all' || dirty.has(hostId) ? undefined : this.projectedPartitions.get(hostId) + partitions.set(hostId, cached ?? this.projectPartition(hostId)) + } + this.projectedPartitions = partitions + const projected = new Map() + for (const [hostId, workspaces] of partitions) { + for (const [key, workspace] of workspaces) { // Only the owning partition's copy is published; a stray copy elsewhere is not shown. if (this.deps.homeHostId(key) === hostId) { - const layout = publishWorkspaceLayout(workspace, hostId) - projected.set(key, { layout, json: stableJson(layout) }) + projected.set(key, workspace) } } } return projected } - private reconcile(): void { - const next = this.project() + private reconcile(dirty: Set | 'all'): void { + const next = this.project(dirty) const events: WorkspaceLayoutEvent[] = [] for (const key of this.published.keys()) { if (!next.has(key)) { diff --git a/src/main/runtime/workspaces/workspace-creation.ts b/src/main/runtime/workspaces/workspace-creation.ts index f4c30418c76..4dbb9200dbc 100644 --- a/src/main/runtime/workspaces/workspace-creation.ts +++ b/src/main/runtime/workspaces/workspace-creation.ts @@ -22,6 +22,7 @@ import { trackRuntimeWorkspaceCreate } from '../../workspace-create-telemetry' import { assertOpenCodeModelLaunchPreferencesAbsent } from '../../opencode/opencode-model-startup-plan' import { resolveWorktreeCreateAgentStartup } from '../runtime-worktree-agent-startup' import type { RuntimeWorkspaceCreateEvents } from '../../workspace-create-telemetry' +import { assertWslMoveRepoWritable } from '../../persistence/moving-wsl-projects/wsl-move-scope-guard' export abstract class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWithGetWorktreeTerminalProvisioningHost { protected abstract resolveLineageForWorktreeCreate( @@ -70,6 +71,7 @@ export abstract class OrcaRuntimeWithCreateManagedWorktree extends OrcaRuntimeWi ) const repo = await this.resolveRepoSelector(args.repoSelector) + assertWslMoveRepoWritable(this.store, repo) const createSettings = this.store.getSettings() const requestedAgent = args.startupAgent ?? args.createdWithAgent const requestedAgentEnabled = diff --git a/src/main/runtime/workspaces/workspace-removal.ts b/src/main/runtime/workspaces/workspace-removal.ts index a710800b65e..8d5340b3c2a 100644 --- a/src/main/runtime/workspaces/workspace-removal.ts +++ b/src/main/runtime/workspaces/workspace-removal.ts @@ -36,6 +36,10 @@ import { removeStaleLocalWorktreeRegistration } from '../../local-worktree-remov import { cleanupUnusedWorktreePushTargetRemote } from '../../ipc/worktree-remote' import { removeRuntimeRegisteredLocalWorktree } from '../runtime-registered-local-worktree-removal' import { removeOrphanOrFolderWorktree } from './workspace-registration-removal' +import { + assertWslMoveWorkspaceWritable, + isWslMoveSourceHost +} from '../../persistence/moving-wsl-projects/wsl-move-scope-guard' export abstract class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWithCreateManagedRemoteWorktree { abstract forgetStructuredSessionMail(sessionId: string): void @@ -85,6 +89,13 @@ export abstract class OrcaRuntimeWithRemoveManagedWorktree extends OrcaRuntimeWi const store = this.store const cleanupHostId = parseExecutionHostId(hostId)?.id const removalTarget = await this.resolveWorktreeRemovalTarget(worktreeSelector, cleanupHostId) + // The repo row names the host; a caller may omit hostId for a worktree on another host. + const fenceOwner = resolveWorktreeRemovalRepoOwner(store, removalTarget.repoId, cleanupHostId) + const fenceHostId = + fenceOwner.kind === 'resolved' ? getRepoExecutionHostId(fenceOwner.repo) : cleanupHostId + if (isWslMoveSourceHost(fenceHostId)) { + assertWslMoveWorkspaceWritable(store, removalTarget.id) + } // Why: a retry or a second client asking while Git still deletes joins that removal. const pending = this.joinPendingWorktreeRemoval(removalTarget.id, options) if (pending) { diff --git a/src/main/ssh/orcad-migration-blob-coordinator.test.ts b/src/main/ssh/orcad-migration-blob-coordinator.test.ts new file mode 100644 index 00000000000..528c08fb506 --- /dev/null +++ b/src/main/ssh/orcad-migration-blob-coordinator.test.ts @@ -0,0 +1,182 @@ +import { createHash } from 'node:crypto' +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + ORCAD_MIGRATION_BLOB_CHUNK_BYTES, + requiredMigrationBlobCapabilities, + type OrcadMigrationBlobDescriptor +} from '../../shared/orcad-migration-blob-transfer' +import { + ORCAD_MIGRATION_MANIFEST_VERSION, + type OrcadMigrationCatalogState, + type OrcadMigrationManifest +} from '../../shared/orcad-migration-manifest' +import { ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY } from '../../shared/orcad-runtime-capabilities' +import { + commitOrcadMigrationBlobs, + getOrcadMigrationReceivedBlobPath, + inspectOrcadMigrationBlobUploads, + stageOrcadMigrationBlobChunk +} from '../persistence/migrating-orcad-catalog/orcad-migration-blob-staging' +import { + transferOrcadMigrationBlobs, + type OrcadMigrationBlobDestination +} from './orcad-migration-blob-coordinator' + +let root: string + +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'orcad-migration-blob-sender-')) +}) + +afterEach(() => rmSync(root, { recursive: true, force: true })) + +const manifest: OrcadMigrationManifest = { + version: ORCAD_MIGRATION_MANIFEST_VERSION, + migrationId: 'migration-1', + createdAt: '2026-08-30T12:00:00.000Z', + source: { sshTargetId: 'source', sshTargetGeneration: 1, targetLabel: 'Source' }, + payload: { repositories: [], projectGroups: [], folderWorkspaces: [] }, + manifestSha256: 'a'.repeat(64) +} + +const history = Buffer.alloc(ORCAD_MIGRATION_BLOB_CHUNK_BYTES * 2 + 17, 0x61) +const hints = Buffer.from('{"panes":[]}') +const blobs = [ + descriptor('shell-history', `${'a'.repeat(16)}-zsh`, history), + descriptor('terminal-restore-hints', 'migration', hints) +] +const contents = new Map([ + [blobs[0].key, history], + [blobs[1].key, hints] +]) + +describe('orcad migration blob sender', () => { + it('uploads every blob in bounded chunks, resuming from the staged offset', async () => { + stageOrcadMigrationBlobChunk({ + stagedManifest: manifest, + root, + request: chunkRequest(blobs[0], 0, history.subarray(0, ORCAD_MIGRATION_BLOB_CHUNK_BYTES)) + }) + const destination = realDestination(requiredMigrationBlobCapabilities(blobs)) + + await transferOrcadMigrationBlobs({ source: source(), manifest, blobs, destination }) + + expect(destination.stageChunk.mock.calls.map(([request]) => request.offset)).toEqual([ + ORCAD_MIGRATION_BLOB_CHUNK_BYTES, + ORCAD_MIGRATION_BLOB_CHUNK_BYTES * 2, + 0 + ]) + commitOrcadMigrationBlobs(manifest, root) + for (const blob of blobs) { + expect(readFileSync(getOrcadMigrationReceivedBlobPath(root, manifest, blob))).toEqual( + contents.get(blob.key) + ) + } + }) + + it.each([ + ['the blob channel', [blobs[1]], ['orcad.migration-blob.terminal-restore-hints.v1']], + ["the blob's kind", [blobs[1]], [ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY]] + ])('refuses before staging anything when the destination lacks %s', async (_, sent, caps) => { + const destination = realDestination(caps) + + await expect( + transferOrcadMigrationBlobs({ source: source(), manifest, blobs: sent, destination }) + ).rejects.toThrow(/^orcad_migration_destination_blob_kind_unsupported:terminal-restore-hints:/) + expect(destination.readState).not.toHaveBeenCalled() + expect(destination.stageChunk).not.toHaveBeenCalled() + expect(inspectOrcadMigrationBlobUploads(manifest, root)).toBeUndefined() + }) + + it('confirms a chunk whose acknowledgement was lost from the staged state', async () => { + const destination = realDestination(requiredMigrationBlobCapabilities(blobs)) + const stage = destination.stageChunk.getMockImplementation() + destination.stageChunk.mockImplementationOnce(async (request) => { + await stage?.(request) + throw new Error('connection_lost') + }) + + await transferOrcadMigrationBlobs({ source: source(), manifest, blobs, destination }) + expect(inspectOrcadMigrationBlobUploads(manifest, root)?.map((u) => u.receivedBytes)).toEqual( + expect.arrayContaining([history.length, hints.length]) + ) + }) + + it('refuses when the destination holds the same key with other bytes', async () => { + const stale = descriptor('terminal-restore-hints', 'migration', Buffer.from('stale')) + stageOrcadMigrationBlobChunk({ + stagedManifest: manifest, + root, + request: chunkRequest(stale, 0, Buffer.from('st')) + }) + const destination = realDestination(requiredMigrationBlobCapabilities(blobs)) + + await expect( + transferOrcadMigrationBlobs({ source: source(), manifest, blobs, destination }) + ).rejects.toThrow('orcad_migration_blob_descriptor_conflict:terminal-restore-hints') + expect(destination.stageChunk).not.toHaveBeenCalled() + }) + + it('sends nothing and asks nothing when there are no blobs', async () => { + const destination = realDestination([]) + await transferOrcadMigrationBlobs({ source: source(), manifest, blobs: [], destination }) + expect(destination.readCapabilities).not.toHaveBeenCalled() + }) +}) + +function descriptor( + blobKind: OrcadMigrationBlobDescriptor['blobKind'], + key: string, + bytes: Buffer +): OrcadMigrationBlobDescriptor { + return { + blobKind, + key, + sha256: createHash('sha256').update(bytes).digest('hex'), + byteLength: bytes.length + } +} + +function chunkRequest(blob: OrcadMigrationBlobDescriptor, offset: number, bytes: Buffer) { + return { + migrationId: manifest.migrationId, + manifestSha256: manifest.manifestSha256, + blob, + offset, + bytesBase64: bytes.toString('base64') + } +} + +function source() { + return { + readChunk: async (blob: OrcadMigrationBlobDescriptor, offset: number, maxBytes: number) => + (contents.get(blob.key) ?? Buffer.alloc(0)).subarray(offset, offset + maxBytes) + } +} + +/** The real staging module behind the RPC shapes, so resume reads what a host would report. */ +function realDestination(capabilities: readonly string[]) { + const readState = async (): Promise => { + const blobUploads = inspectOrcadMigrationBlobUploads(manifest, root) + return { + state: 'staged', + migrationId: manifest.migrationId, + manifestSha256: manifest.manifestSha256, + stagedAt: '2026-08-30T12:01:00.000Z', + ...(blobUploads ? { blobUploads } : {}) + } + } + const destination = { + readCapabilities: vi.fn( + async () => capabilities + ), + readState: vi.fn(readState), + stageChunk: vi.fn(async (request) => + stageOrcadMigrationBlobChunk({ stagedManifest: manifest, root, request }) + ) + } + return destination +} diff --git a/src/main/ssh/orcad-migration-blob-coordinator.ts b/src/main/ssh/orcad-migration-blob-coordinator.ts new file mode 100644 index 00000000000..c358f5e7b38 --- /dev/null +++ b/src/main/ssh/orcad-migration-blob-coordinator.ts @@ -0,0 +1,154 @@ +/** + * Sending a migration's blobs to the destination in bounded, resumable chunks, beside the + * manifest's scrollback snapshots. Every kind is checked against the destination's capabilities + * before the first byte: a kind it cannot install is refused by name, never dropped. Offsets + * resume from the destination's staged state, and a lost acknowledgement is confirmed by + * re-reading that state, never assumed. + */ +import { + ORCAD_MIGRATION_BLOB_CHUNK_BYTES, + type OrcadMigrationBlobChunkRequest, + type OrcadMigrationBlobChunkResult, + type OrcadMigrationBlobDescriptor, + assertOrcadMigrationBlobSet, + findUnsupportedMigrationBlobKind, + orcadMigrationBlobIdentity, + orcadMigrationBlobKindUnsupportedBlocker, + sameOrcadMigrationBlob +} from '../../shared/orcad-migration-blob-transfer' +import type { + OrcadMigrationCatalogState, + OrcadMigrationManifest +} from '../../shared/orcad-migration-manifest' + +export type OrcadMigrationBlobSource = { + /** Up to `maxBytes` bytes of the blob starting at `offset`, exactly as the descriptor hashed. */ + readChunk: ( + descriptor: OrcadMigrationBlobDescriptor, + offset: number, + maxBytes: number + ) => Promise +} + +export type OrcadMigrationBlobDestination = { + readCapabilities: () => Promise + readState: (manifest: OrcadMigrationManifest) => Promise + stageChunk: (request: OrcadMigrationBlobChunkRequest) => Promise +} + +export type OrcadMigrationBlobTransfer = { + source: OrcadMigrationBlobSource + manifest: OrcadMigrationManifest + blobs: readonly OrcadMigrationBlobDescriptor[] + destination: OrcadMigrationBlobDestination +} + +type StagedCatalogState = Extract + +export async function transferOrcadMigrationBlobs( + transfer: OrcadMigrationBlobTransfer +): Promise { + if (transfer.blobs.length === 0) { + return + } + assertOrcadMigrationBlobSet(transfer.blobs) + const unsupported = findUnsupportedMigrationBlobKind( + transfer.blobs, + await transfer.destination.readCapabilities() + ) + if (unsupported) { + throw new Error(orcadMigrationBlobKindUnsupportedBlocker(unsupported)) + } + const offsets = receivedOffsets(transfer, await readStagedState(transfer)) + for (const blob of transfer.blobs) { + let offset = offsets.get(orcadMigrationBlobIdentity(blob)) ?? 0 + while (offset < blob.byteLength) { + const bytes = await transfer.source.readChunk( + blob, + offset, + Math.min(ORCAD_MIGRATION_BLOB_CHUNK_BYTES, blob.byteLength - offset) + ) + if ( + bytes.length === 0 || + bytes.length > ORCAD_MIGRATION_BLOB_CHUNK_BYTES || + offset + bytes.length > blob.byteLength + ) { + throw new Error(`orcad_migration_source_blob_changed:${blob.blobKind}`) + } + const request: OrcadMigrationBlobChunkRequest = { + migrationId: transfer.manifest.migrationId, + manifestSha256: transfer.manifest.manifestSha256, + blob: { ...blob }, + offset, + bytesBase64: bytes.toString('base64') + } + offset = await stageChunkWithRecovery(transfer, request, offset + bytes.length) + } + } + assertAllReceived(transfer, await readStagedState(transfer)) +} + +async function readStagedState(transfer: OrcadMigrationBlobTransfer): Promise { + const state = await transfer.destination.readState(transfer.manifest) + if (state.state !== 'staged') { + throw new Error('orcad_migration_blob_catalog_not_staged') + } + return state +} + +function receivedOffsets( + transfer: OrcadMigrationBlobTransfer, + state: StagedCatalogState +): Map { + const wanted = new Map(transfer.blobs.map((blob) => [orcadMigrationBlobIdentity(blob), blob])) + const offsets = new Map() + for (const upload of state.blobUploads ?? []) { + const blob = wanted.get(orcadMigrationBlobIdentity(upload)) + // A staged key with other bytes is a different capture; restaging it would mix the two. + if (!blob || !sameOrcadMigrationBlob(blob, upload)) { + throw new Error(`orcad_migration_blob_descriptor_conflict:${upload.blobKind}`) + } + offsets.set(orcadMigrationBlobIdentity(upload), upload.receivedBytes) + } + return offsets +} + +function assertAllReceived(transfer: OrcadMigrationBlobTransfer, state: StagedCatalogState): void { + const received = receivedOffsets(transfer, state) + for (const blob of transfer.blobs) { + if (received.get(orcadMigrationBlobIdentity(blob)) !== blob.byteLength) { + throw new Error(`orcad_migration_blob_transfer_incomplete:${blob.blobKind}`) + } + } +} + +async function stageChunkWithRecovery( + transfer: OrcadMigrationBlobTransfer, + request: OrcadMigrationBlobChunkRequest, + expectedOffset: number +): Promise { + try { + const result = await transfer.destination.stageChunk(request) + if (result.receivedBytes !== expectedOffset) { + throw new Error('orcad_migration_blob_ack_invalid') + } + return result.receivedBytes + } catch (error) { + try { + const observed = await transfer.destination.readState(transfer.manifest) + const received = + observed.state === 'staged' + ? observed.blobUploads?.find( + (entry) => + orcadMigrationBlobIdentity(entry) === orcadMigrationBlobIdentity(request.blob) + )?.receivedBytes + : undefined + if (received === expectedOffset) { + return received + } + } catch { + // The chunk stays unverifiable; report the first failure. + } + throw error + } +} diff --git a/src/main/ssh/orcad-migration-catalog-client-blob-kind.test.ts b/src/main/ssh/orcad-migration-catalog-client-blob-kind.test.ts new file mode 100644 index 00000000000..a80a55240bc --- /dev/null +++ b/src/main/ssh/orcad-migration-catalog-client-blob-kind.test.ts @@ -0,0 +1,174 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { + OrcadMigrationBlobChunkRequest, + OrcadMigrationBlobDescriptor +} from '../../shared/orcad-migration-blob-transfer' +import { + ORCAD_MIGRATION_MANIFEST_VERSION, + type OrcadMigrationManifest +} from '../../shared/orcad-migration-manifest' +import { + ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY, + ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY +} from '../../shared/orcad-runtime-capabilities' +import { encodePairingOffer, PAIRING_OFFER_VERSION } from '../../shared/pairing' + +const sendRequest = vi.hoisted(() => vi.fn()) +const status = vi.hoisted((): { capabilities: string[] } => ({ capabilities: [] })) +vi.mock('../../shared/remote-runtime-client', () => ({ + sendRemoteRuntimeRequestWithStatusPreflight: async ( + pairing: unknown, + method: string, + params: unknown, + _timeoutMs: number, + validate: (response: unknown) => void + ) => { + validate({ + ok: true, + _meta: { runtimeId: 'runtime' }, + result: { capabilities: status.capabilities } + }) + return sendRequest(pairing, method, params) + } +})) + +const { commitRemoteOrcadMigrationCatalog, stageRemoteOrcadMigrationBlobChunk } = + await import('./orcad-migration-catalog-client') + +const pairingCode = encodePairingOffer({ + v: PAIRING_OFFER_VERSION, + endpoint: 'ws://127.0.0.1:46768/runtime', + deviceToken: 'device-token', + publicKeyB64: 'public-key', + pairedDeviceId: 'paired-desktop' +}) + +const request: OrcadMigrationBlobChunkRequest = { + migrationId: 'migration-blob', + manifestSha256: 'a'.repeat(64), + blob: { + blobKind: 'shell-history', + key: `${'b'.repeat(16)}-zsh`, + sha256: 'c'.repeat(64), + byteLength: 3 + }, + offset: 0, + bytesBase64: 'YWJj' +} + +beforeEach(() => { + vi.clearAllMocks() + sendRequest.mockResolvedValue({ + ok: true, + _meta: { runtimeId: 'runtime' }, + result: { + migrationId: request.migrationId, + manifestSha256: request.manifestSha256, + blobKind: 'shell-history', + key: request.blob.key, + receivedBytes: 3 + } + }) +}) + +describe('orcad migration blob chunk capability gate', () => { + it.each([ + ['an older destination', [ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY]], + [ + 'a destination with only the channel', + [ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY, ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY] + ] + ])('never sends a blob to %s, and names the kind', async (_, capabilities) => { + status.capabilities = capabilities + await expect(stageRemoteOrcadMigrationBlobChunk(pairingCode, request)).rejects.toThrow( + /^orcad_migration_destination_blob_kind_unsupported:shell-history:/ + ) + expect(sendRequest).not.toHaveBeenCalled() + }) + + it('sends through its own method once the destination installs the kind', async () => { + status.capabilities = [ + ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY, + ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY, + 'orcad.migration-blob.shell-history.v1' + ] + await expect(stageRemoteOrcadMigrationBlobChunk(pairingCode, request)).resolves.toMatchObject({ + receivedBytes: 3 + }) + expect(sendRequest).toHaveBeenCalledWith( + expect.anything(), + 'orcad.migration.stageBlobChunk', + request + ) + }) +}) + +describe('orcad migration commit names its blob set', () => { + const manifest: OrcadMigrationManifest = { + version: ORCAD_MIGRATION_MANIFEST_VERSION, + migrationId: request.migrationId, + createdAt: '2026-08-30T12:00:00.000Z', + source: { + sshTargetId: 'source', + sshTargetGeneration: 1, + targetLabel: 'Source' + }, + payload: { repositories: [], projectGroups: [], folderWorkspaces: [] }, + manifestSha256: request.manifestSha256 + } + const expectedBlobs: OrcadMigrationBlobDescriptor[] = [request.blob] + const installing = [ + ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY, + ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY, + 'orcad.migration-blob.shell-history.v1' + ] + + beforeEach(() => { + sendRequest.mockResolvedValue({ + ok: true, + _meta: { runtimeId: 'runtime' }, + result: { + state: 'staged', + migrationId: manifest.migrationId, + manifestSha256: manifest.manifestSha256, + stagedAt: '2026-08-30T12:00:00.000Z' + } + }) + }) + + it('sends exactly `{ manifest }` when the move carried no blobs, even to a blob host', async () => { + for (const capabilities of [[ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY], installing]) { + status.capabilities = capabilities + await commitRemoteOrcadMigrationCatalog(pairingCode, manifest) + await commitRemoteOrcadMigrationCatalog(pairingCode, manifest, { + expectedBlobs: [] + }) + expect(sendRequest).toHaveBeenLastCalledWith( + expect.anything(), + 'orcad.migration.commitCatalog', + { manifest } + ) + } + }) + + it('names the blobs to a destination with the channel, and never to one without', async () => { + status.capabilities = installing + await commitRemoteOrcadMigrationCatalog(pairingCode, manifest, { + expectedBlobs + }) + expect(sendRequest).toHaveBeenLastCalledWith( + expect.anything(), + 'orcad.migration.commitCatalog', + { manifest, expectedBlobs } + ) + + sendRequest.mockClear() + status.capabilities = [ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY] + await expect( + commitRemoteOrcadMigrationCatalog(pairingCode, manifest, { + expectedBlobs + }) + ).rejects.toThrow(/^orcad_migration_destination_blob_kind_unsupported:shell-history:/) + expect(sendRequest).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/ssh/orcad-migration-catalog-client.ts b/src/main/ssh/orcad-migration-catalog-client.ts index d23a320d4c1..03e4fbad444 100644 --- a/src/main/ssh/orcad-migration-catalog-client.ts +++ b/src/main/ssh/orcad-migration-catalog-client.ts @@ -13,6 +13,14 @@ import { type OrcadMigrationSnapshotChunkRequest, type OrcadMigrationSnapshotChunkResult } from '../../shared/orcad-migration-scrollback' +import { + findUnsupportedMigrationBlobKind, + orcadMigrationBlobKindUnsupportedBlocker, + parseOrcadMigrationBlobChunkResult, + type OrcadMigrationBlobChunkRequest, + type OrcadMigrationBlobChunkResult, + type OrcadMigrationBlobDescriptor +} from '../../shared/orcad-migration-blob-transfer' import { parsePairingCode } from '../../shared/pairing' import { ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES } from '../../shared/electron-remote-runtime-client-capabilities' import { ORCAD_MIGRATION_CATALOG_RUNTIME_CAPABILITY } from '../../shared/orcad-runtime-capabilities' @@ -58,12 +66,16 @@ export function stageRemoteOrcadMigrationCatalog( return requestCatalogState(pairingCode, 'stage', manifest, options) } -export function commitRemoteOrcadMigrationCatalog( +/** `expectedBlobs` names what was moved so the destination refuses a commit missing any. */ +export async function commitRemoteOrcadMigrationCatalog( pairingCode: string, manifest: OrcadMigrationManifest, - options: CatalogRequestOptions = {} + options: CatalogRequestOptions & { + expectedBlobs?: readonly OrcadMigrationBlobDescriptor[] + } = {} ): Promise { - return requestCatalogState(pairingCode, 'commit', manifest, options) + const result = await request(pairingCode, 'commit', manifest, options, options.expectedBlobs) + return parseOrcadMigrationCatalogState(result, manifest) } export function readRemoteOrcadMigrationCatalogState( @@ -109,6 +121,36 @@ export async function stageRemoteOrcadMigrationSnapshotChunk( return parseOrcadMigrationSnapshotChunkResult(response.result, request) } +export async function stageRemoteOrcadMigrationBlobChunk( + pairingCode: string, + request: OrcadMigrationBlobChunkRequest, + options: CatalogRequestOptions = {} +): Promise { + const pairing = parsePairingCode(pairingCode) + if (!pairing) { + throw new Error('orcad_migration_pairing_code_invalid') + } + const response = await sendSupportedMigrationRequest( + pairing, + 'orcad.migration.stageBlobChunk', + request, + options, + false, + null, + // Per request: a downgraded destination must refuse the kind, never stage what it can't install. + (capabilities) => { + const unsupported = findUnsupportedMigrationBlobKind([request.blob], capabilities) + if (unsupported) { + throw new Error(orcadMigrationBlobKindUnsupportedBlocker(unsupported)) + } + } + ) + if (!response.ok) { + throw new Error(`orcad_migration_blob_failed:${response.error.message}`) + } + return parseOrcadMigrationBlobChunkResult(response.result, request) +} + async function requestCatalogState( pairingCode: string, operation: Exclude, @@ -123,7 +165,8 @@ async function request( pairingCode: string, operation: OrcadCatalogMigrationOperation, manifest: OrcadMigrationManifest, - options: CatalogRequestOptions + options: CatalogRequestOptions, + expectedBlobs: readonly OrcadMigrationBlobDescriptor[] = [] ): Promise { const pairing = parsePairingCode(pairingCode) if (!pairing) { @@ -137,11 +180,19 @@ async function request( const response = await sendSupportedMigrationRequest( pairing, METHOD_BY_OPERATION[operation], - { manifest }, + // Only with blobs: SSH and blob-free moves keep sending exactly `{ manifest }`. + expectedBlobs.length > 0 ? { manifest, expectedBlobs } : { manifest }, options, carriesExtraAgentArgs, // Every operation: an older destination would parse a non-SSH source as malformed or as SSH. - requiredOrcadMigrationSourceCapability(manifest.source) + requiredOrcadMigrationSourceCapability(manifest.source), + // A destination without the blob channel would strip the field and commit unchecked. + (capabilities) => { + const unsupported = findUnsupportedMigrationBlobKind(expectedBlobs, capabilities) + if (unsupported) { + throw new Error(orcadMigrationBlobKindUnsupportedBlocker(unsupported)) + } + } ) if (!response.ok) { throw new Error(`orcad_migration_${operation}_failed:${response.error.message}`) @@ -155,7 +206,8 @@ async function sendSupportedMigrationRequest( params: unknown, options: CatalogRequestOptions, requiresExtraAgentArgs = false, - requiredSourceCapability: string | null = null + requiredSourceCapability: string | null = null, + assertCapabilities: (capabilities: readonly string[]) => void = () => {} ): Promise> { const response = await sendRemoteRuntimeRequestWithStatusPreflight( pairedRuntimeConnectionFromOffer(pairing), @@ -187,6 +239,7 @@ async function sendSupportedMigrationRequest( ) { throw new Error(ORCAD_MIGRATION_DESTINATION_SOURCE_KIND_UNSUPPORTED) } + assertCapabilities(status.result.capabilities ?? []) }, undefined, ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES, diff --git a/src/main/ssh/orcad-migration-path-rewrite.ts b/src/main/ssh/orcad-migration-path-rewrite.ts index 9ffe2b4fb52..e238b6de65b 100644 --- a/src/main/ssh/orcad-migration-path-rewrite.ts +++ b/src/main/ssh/orcad-migration-path-rewrite.ts @@ -122,7 +122,7 @@ function rewriteValue( ): string { switch (kind) { case 'worktree-id': - return rewriteWorktreeId(value, rewriter) + return rewriteOrcadMigrationWorktreeId(value, rewriter) case 'owner-key': return rewriteOwnerKey(value, rewriter) case 'pty-session-id': @@ -137,7 +137,11 @@ function rewriteValue( } } -function rewriteWorktreeId(value: string, rewriter: OrcadMigrationPathRewriter): string { +/** The worktree id the destination will hold for `value`, as the catalog rewrite computes it. */ +export function rewriteOrcadMigrationWorktreeId( + value: string, + rewriter: OrcadMigrationPathRewriter +): string { const parsed = splitWorktreeId(value) if (!parsed || !parsed.repoId || !parsed.worktreePath) { return value @@ -159,10 +163,10 @@ function rewriteOwnerKey(value: string, rewriter: OrcadMigrationPathRewriter): s return value } if (scope?.type === 'worktree') { - const mapped = rewriteWorktreeId(scope.worktreeId, rewriter) + const mapped = rewriteOrcadMigrationWorktreeId(scope.worktreeId, rewriter) return mapped === scope.worktreeId ? value : `worktree:${mapped}` } - return rewriteWorktreeId(value, rewriter) + return rewriteOrcadMigrationWorktreeId(value, rewriter) } function rewritePtySessionId(value: string, rewriter: OrcadMigrationPathRewriter): string { @@ -187,7 +191,7 @@ function rewriteEditorFileId(value: string, rewriter: OrcadMigrationPathRewriter return value } const [worktreeId, runtime, filePath] = decoded - const mappedWorktreeId = rewriteWorktreeId(worktreeId, rewriter) + const mappedWorktreeId = rewriteOrcadMigrationWorktreeId(worktreeId, rewriter) const mappedFilePath = rewriter.path(filePath, 'absolute') if (mappedWorktreeId === worktreeId && mappedFilePath === filePath) { return value diff --git a/src/main/ssh/orcad-retained-source.ts b/src/main/ssh/orcad-retained-source.ts index 973984c3bce..762eda19a00 100644 --- a/src/main/ssh/orcad-retained-source.ts +++ b/src/main/ssh/orcad-retained-source.ts @@ -29,7 +29,10 @@ import { repoBelongsToOrcadSource } from '../persistence/migrating-orcad-catalog/orcad-source-ownership' import { findOrcadMigrationSourceCutoverForTarget } from './orcad-migration-cutover-journal' -import { withoutWslMovedProjects } from '../persistence/moving-wsl-projects/wsl-move-scope-guard' +import { + withoutWslMovedFolderWorkspaces, + withoutWslMovedRepos +} from '../persistence/moving-wsl-projects/wsl-move-scope-guard' const appUserDataPath = (): string => getAppEnvironment().getPath('userData') @@ -92,7 +95,7 @@ export function visibleRepos( getUserDataPath = appUserDataPath ): Repo[] { const hidden = hiddenRetainedSourceTargetIds(getUserDataPath, store.getSshTargets()) - const repos = withoutWslMovedProjects(store.getRepos(), (repo) => ({ repoId: repo.id })) + const repos = withoutWslMovedRepos(store.getRepos()) if (hidden.length === 0) { return repos } @@ -121,9 +124,7 @@ export function visibleFolderWorkspaces( getUserDataPath = appUserDataPath ): FolderWorkspace[] { const hidden = hiddenRetainedSourceTargetIds(getUserDataPath, store.getSshTargets()) - const folderWorkspaces = withoutWslMovedProjects(store.getFolderWorkspaces(), (workspace) => ({ - folderWorkspaceId: workspace.id - })) + const folderWorkspaces = withoutWslMovedFolderWorkspaces(store.getFolderWorkspaces()) if (hidden.length === 0) { return folderWorkspaces } diff --git a/src/main/ssh/orcad-state-mutation-fence-heartbeat.test.ts b/src/main/ssh/orcad-state-mutation-fence-heartbeat.test.ts index 5896eda7ec0..fdc44d078a7 100644 --- a/src/main/ssh/orcad-state-mutation-fence-heartbeat.test.ts +++ b/src/main/ssh/orcad-state-mutation-fence-heartbeat.test.ts @@ -1,6 +1,6 @@ /** * A state mutation can outlast the activation fence's stale window, so it keeps the fence - * fresh while it runs. Run for real with a one-second beat and the real steal command. + * fresh while it runs. Run a short real heartbeat and the real steal command. */ import { existsSync, @@ -24,6 +24,8 @@ import { getRemoteHostPlatform } from './ssh-remote-platform' const posix = getRemoteHostPlatform('linux-x64') const STALE_SECONDS = 3 +const HEARTBEAT_SECONDS = 0.1 +const OBSERVE_HEARTBEATS_MS = 300 async function sh(command: string): Promise { return (await runProcess({ program: '/bin/sh', args: ['-c', command] })).stdout.trim() @@ -44,7 +46,10 @@ describe.skipIf(process.platform === 'win32')( }) // The mutation runs under the fence its run holds, generation `holder-1`. const mutation = (script: string): string => - serializedStateMutationCommand(base, script, 1, { lockDir: fence, token: 'holder-1' }) + serializedStateMutationCommand(base, script, HEARTBEAT_SECONDS, { + lockDir: fence, + token: 'holder-1' + }) afterEach(() => { rmSync(base, { recursive: true, force: true }) }) @@ -66,15 +71,15 @@ describe.skipIf(process.platform === 'win32')( .toBeTruthy() await backdate() - await pause(2_500) + await expect.poll(age, { timeout: 5_000, interval: 25 }).toBeLessThan(STALE_SECONDS) expect(age()).toBeLessThan(STALE_SECONDS) expect(await steal()).toBe('BUSY') // The host process dies (OOM, reboot of the session): nothing refreshes the fence now. process.kill(Number(readFileSync(pidFile, 'utf8').trim()), 'SIGKILL') - await pause(1_500) + await pause(OBSERVE_HEARTBEATS_MS) await backdate() - await pause(2_500) + await pause(OBSERVE_HEARTBEATS_MS) expect(age()).toBeGreaterThan(STALE_SECONDS) expect(await steal()).toMatch(/OK$/u) } finally { @@ -85,10 +90,14 @@ describe.skipIf(process.platform === 'win32')( it('stops refreshing a fence another run took over mid-mutation, and keeps its token', async () => { const run = spawnProcess({ program: '/bin/sh', args: ['-c', mutation('sleep 6')] }) try { - await pause(1_500) + await expect + .poll(() => existsSync(join(base, 'orcad-state-mutation.lock', 'pid'))) + .toBe(true) + await backdate() + await expect.poll(age, { timeout: 5_000, interval: 25 }).toBeLessThan(STALE_SECONDS) writeFileSync(join(fence, ORCAD_FENCE_OWNER_FILENAME), 'successor') await backdate() - await pause(2_500) + await pause(OBSERVE_HEARTBEATS_MS) expect(age()).toBeGreaterThan(STALE_SECONDS) expect(readFileSync(join(fence, ORCAD_FENCE_OWNER_FILENAME), 'utf8')).toBe('successor') } finally { @@ -105,13 +114,13 @@ describe.skipIf(process.platform === 'win32')( }, 20_000) it('stops refreshing once the mutation finishes, and never creates a missing fence', async () => { - await sh(mutation('sleep 2')) + await sh(mutation('sleep 0.3')) await backdate() - await pause(2_500) + await pause(OBSERVE_HEARTBEATS_MS) expect(age()).toBeGreaterThan(STALE_SECONDS) rmSync(fence, { recursive: true }) - await sh(serializedStateMutationCommand(base, 'sleep 2', 1, null)) + await sh(serializedStateMutationCommand(base, ':', HEARTBEAT_SECONDS, null)) expect(existsSync(fence)).toBe(false) }, 20_000) } diff --git a/src/main/ssh/ssh-connection-manager-sign-in-reuse.test.ts b/src/main/ssh/ssh-connection-manager-sign-in-reuse.test.ts index 1569833a73f..9ad909dd278 100644 --- a/src/main/ssh/ssh-connection-manager-sign-in-reuse.test.ts +++ b/src/main/ssh/ssh-connection-manager-sign-in-reuse.test.ts @@ -119,6 +119,9 @@ describe('SshConnectionManager on a password host', () => { onCredentialRequest.mockImplementationOnce(() => new Promise(() => {})) emitSshEvent('keyboard-interactive', '', '', '', passwordPrompt, vi.fn()) await vi.waitFor(() => expect(onCredentialRequest).toHaveBeenCalledTimes(2)) + // A background dial may not close the dialog the user is answering. + await expect(manager.connect(createTarget())).rejects.toThrow('already in progress') + await manager.disconnectConnection('target-1', manager.getConnection('target-1')!) ssh2Mock.connectSequence = [authFailure(), 'ready'] await manager.connect(createTarget()) diff --git a/src/main/ssh/ssh-connection-manager.test.ts b/src/main/ssh/ssh-connection-manager.test.ts index 2c3a9ed30ae..9f5f99509a1 100644 --- a/src/main/ssh/ssh-connection-manager.test.ts +++ b/src/main/ssh/ssh-connection-manager.test.ts @@ -1,18 +1,23 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { SshTarget } from '../../shared/ssh-types' -const mockState = vi.hoisted(() => ({ - connectResults: [] as Promise[], - instances: [] as { - connect: ReturnType - disconnect: ReturnType - status: 'connecting' | 'connected' | 'disconnected' - }[] -})) +type MockConnectionRecord = { + connect: ReturnType + disconnect: ReturnType + status: 'connecting' | 'connected' | 'disconnected' + awaitingCredential: boolean +} + +const mockState = vi.hoisted(() => { + const connectResults: Promise[] = [] + const instances: MockConnectionRecord[] = [] + return { connectResults, instances } +}) vi.mock('./ssh-connection', () => ({ SshConnection: class MockSshConnection { status: 'connecting' | 'connected' | 'disconnected' = 'connecting' + awaitingCredential = false connect = vi.fn(async () => { await (mockState.connectResults.shift() ?? Promise.resolve()) this.status = 'connected' @@ -35,6 +40,10 @@ vi.mock('./ssh-connection', () => ({ getReusableCredentials(): null { return null } + + isAwaitingCredential(): boolean { + return this.awaitingCredential + } } })) @@ -160,6 +169,22 @@ describe('SshConnectionManager', () => { expect(manager.getConnection(target.id)).toBe(replacement) }) + it('leaves a connection whose own reconnect is asking the user alone', async () => { + const manager = new SshConnectionManager({ onStateChange: vi.fn() }) + const conn = await manager.connect(target) + const opened = mockState.instances.at(-1)! + const openedCount = mockState.instances.length + // Its reconnect after a drop has the credential dialog open. + opened.status = 'connecting' + opened.awaitingCredential = true + + await expect(manager.connect(target)).rejects.toThrow('already in progress') + + expect(opened.disconnect).not.toHaveBeenCalled() + expect(mockState.instances).toHaveLength(openedCount) + expect(manager.getConnection(target.id)).toBe(conn) + }) + it('clears the pool entry when the closed connection is still the registered one', async () => { const manager = new SshConnectionManager({ onStateChange: vi.fn() }) const conn = await manager.connect(target) diff --git a/src/main/ssh/ssh-connection-manager.ts b/src/main/ssh/ssh-connection-manager.ts index 2b7d04c71e1..24890afd9d0 100644 --- a/src/main/ssh/ssh-connection-manager.ts +++ b/src/main/ssh/ssh-connection-manager.ts @@ -50,7 +50,9 @@ export class SshConnectionManager { throw createSshCredentialDeclinedError(target.label) } - if (this.connectingTargets.has(target.id)) { + // Why: replacing a connection whose own reconnect is asking the user would close that dialog + // and open another one. + if (this.connectingTargets.has(target.id) || existing?.isAwaitingCredential()) { throw new Error(`Connection to ${target.label} is already in progress`) } diff --git a/src/main/ssh/ssh-connection.ts b/src/main/ssh/ssh-connection.ts index 85ad2150be3..c1df60eb8e9 100644 --- a/src/main/ssh/ssh-connection.ts +++ b/src/main/ssh/ssh-connection.ts @@ -195,6 +195,7 @@ export class SshConnection { // Only a sign-in the host accepted may be handed on; a typed or re-asked one is unproven until then. private signInVerified = false private keyboardInteractiveCancelled = false + private openCredentialPrompts = 0 // A rejected cached password must reach the user on the next round. private keyboardInteractivePasswordState = { passwordAutoAnswered: false } private hostKeyFingerprint: string | undefined @@ -276,6 +277,15 @@ export class SshConnection { return this.cachedPassphrase != null || this.cachedPassword != null } + /** The user is answering a prompt, or answered one this connection's own retry has yet to use. */ + isAwaitingCredential(): boolean { + if (this.openCredentialPrompts > 0) { + return true + } + const retrying = this.state.status === 'connecting' || this.state.status === 'reconnecting' + return retrying && !this.signInVerified && this.hasCachedCredential() + } + /** Null unless the host accepted it, so a wrong password is asked for again, not replayed. */ getReusableCredentials(): SshCachedCredentials | null { if (!this.signInVerified || !this.hasCachedCredential()) { @@ -495,13 +505,18 @@ export class SshConnection { if (!this.isCurrentConnectAttempt(connectGeneration)) { return undefined } - return this.callbacks.onCredentialRequest?.( - this.target.id, - kind, - detail, - echo, - this.credentialAbortController.signal - ) + this.openCredentialPrompts += 1 + try { + return await this.callbacks.onCredentialRequest?.( + this.target.id, + kind, + detail, + echo, + this.credentialAbortController.signal + ) + } finally { + this.openCredentialPrompts -= 1 + } } private async answerKeyboardInteractive( @@ -1100,6 +1115,7 @@ export class SshConnection { let settled = false let startupTimer: ReturnType | null = null let keyboardInteractiveRounds = 0 + let keyboardInteractiveAnswer: Promise | null = null const clearStartupTimer = (): void => { if (startupTimer) { clearTimeout(startupTimer) @@ -1201,7 +1217,7 @@ export class SshConnection { return } rearmStartupTimer(SSH_KEYBOARD_INTERACTIVE_READY_TIMEOUT_MS) - void this.answerKeyboardInteractive( + const answer = this.answerKeyboardInteractive( config.host || this.target.label, name, instructions, @@ -1224,7 +1240,7 @@ export class SshConnection { ), { level: 'client-authentication' } ) - onStartupError(cancelledError) + failStartup(cancelledError) } return } @@ -1234,6 +1250,13 @@ export class SshConnection { }, () => finish([]) ) + keyboardInteractiveAnswer = answer + const forgetAnswer = (): void => { + if (keyboardInteractiveAnswer === answer) { + keyboardInteractiveAnswer = null + } + } + void answer.then(forgetAnswer, forgetAnswer) } const cleanupStartupListeners = (): void => { @@ -1276,7 +1299,19 @@ export class SshConnection { resolve() } + // Why: a host that drops an unanswered login (sshd LoginGraceTime) must not close the open + // prompt for a retry that opens another; the user's answer decides, and a typed password is + // cached for that retry. const onStartupError = (err: Error): void => { + if (keyboardInteractiveAnswer && !settled) { + const fail = (): void => failStartup(err) + void keyboardInteractiveAnswer.then(fail, fail) + return + } + failStartup(err) + } + + const failStartup = (err: Error): void => { if (settled) { return } diff --git a/src/main/ssh/ssh-credential-prompt-login-grace-wire.test.ts b/src/main/ssh/ssh-credential-prompt-login-grace-wire.test.ts new file mode 100644 index 00000000000..da63cb87c96 --- /dev/null +++ b/src/main/ssh/ssh-credential-prompt-login-grace-wire.test.ts @@ -0,0 +1,152 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { Server, utils, type Connection } from 'ssh2' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { SshConnection } from './ssh-connection' +import { createCallbacks, createResolvedConfig, createTarget } from './ssh-connection-test-fixtures' +import { initSshHostKeyStoreFile } from './ssh-host-key-store' +import { resolveWithSshG } from './ssh-config-parser' +import type * as SshConfigParser from './ssh-config-parser' + +vi.mock('./ssh-config-parser', async (importOriginal) => ({ + ...(await importOriginal()), + resolveWithSshG: vi.fn() +})) + +const PASSWORD = 'fixture-password' +// Longer than the connection's 2s retry delay, so a retry would start while the prompt is open. +const USER_ANSWER_DELAY_MS = 3_000 + +/** + * A password host whose sshd gives up on an unanswered login (LoginGraceTime) before the user has + * typed. The open password dialog must stay the one the user answers: the dropped socket may not + * silently close it and open a new one. + */ +describe('a password prompt the server gives up on', () => { + let profile: string + let server: Server + let conn: SshConnection | undefined + const sockets = new Set() + + beforeEach(() => { + profile = mkdtempSync(join(tmpdir(), 'orca-login-grace-wire-')) + initSshHostKeyStoreFile(join(profile, 'host-keys.json')) + vi.stubEnv('SSH_AUTH_SOCK', '') + vi.mocked(resolveWithSshG).mockResolvedValue( + createResolvedConfig({ + hostname: '127.0.0.1', + identitiesOnly: true, + identityAgent: 'none', + proxyUseFdpass: false, + strictHostKeyChecking: 'no' + }) + ) + }) + + afterEach(async () => { + await conn?.disconnect() + conn = undefined + for (const socket of sockets) { + socket.end() + } + sockets.clear() + if (server) { + await new Promise((resolve) => server.close(() => resolve())) + } + vi.unstubAllEnvs() + rmSync(profile, { recursive: true, force: true }) + }) + + /** Every login gets a keyboard-interactive password prompt; the first is abandoned by the host. */ + async function startHost(): Promise { + let logins = 0 + server = new Server( + { hostKeys: [utils.generateKeyPairSync('ecdsa', { bits: 256 }).private] }, + (socket) => { + sockets.add(socket) + socket.on('error', () => {}) + socket.on('close', () => sockets.delete(socket)) + const login = ++logins + socket.on('authentication', (context) => { + if (context.method !== 'keyboard-interactive') { + context.reject(['keyboard-interactive']) + return + } + context.prompt([{ prompt: 'Password: ', echo: false }], (answers) => { + if (answers[0] === PASSWORD) { + context.accept() + } else { + context.reject(['keyboard-interactive']) + } + }) + if (login === 1) { + setTimeout(() => socket.end(), 100) + } + }) + socket.on('ready', () => socket.on('session', (accept) => accept())) + } + ) + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) + const address = server.address() + if (!address || typeof address === 'string') { + throw new Error('Fixture did not bind') + } + return address.port + } + + /** Behaves like the real dialog: an abort closes it, otherwise the user answers late. */ + function slowUser(answer: string | null) { + return vi.fn( + ( + _targetId: string, + _kind: string, + _detail: string, + _echo?: boolean, + signal?: AbortSignal + ): Promise => + new Promise((resolve) => { + const timer = setTimeout(() => resolve(answer), USER_ANSWER_DELAY_MS) + signal?.addEventListener('abort', () => { + clearTimeout(timer) + resolve(null) + }) + }) + ) + } + + it('keeps the open prompt and signs in with what the user typed', async () => { + const port = await startHost() + const onCredentialRequest = slowUser(PASSWORD) + conn = new SshConnection( + createTarget({ host: '127.0.0.1', port }), + createCallbacks({ onCredentialRequest }) + ) + + const connecting = conn.connect() + await vi.waitFor(() => expect(onCredentialRequest).toHaveBeenCalled()) + const answered = onCredentialRequest.mock.results[0].value + await answered + // Answered but not yet used by the retry: a background dial must not replace it. + await vi.waitFor(() => expect(conn?.isAwaitingCredential()).toBe(true), { timeout: 1_000 }) + await connecting + + expect(conn.getState().status).toBe('connected') + expect(onCredentialRequest).toHaveBeenCalledTimes(1) + expect(conn.isAwaitingCredential()).toBe(false) + }, 15_000) + + it('stops after the user cancels the prompt the server abandoned', async () => { + const port = await startHost() + const onCredentialRequest = slowUser(null) + conn = new SshConnection( + createTarget({ host: '127.0.0.1', port }), + createCallbacks({ onCredentialRequest }) + ) + + await expect(conn.connect()).rejects.toThrow(/cancelled/i) + + expect(conn.getState().status).toBe('auth-failed') + expect(onCredentialRequest).toHaveBeenCalledTimes(1) + }, 15_000) +}) diff --git a/src/main/ssh/ssh-target-registry.ts b/src/main/ssh/ssh-target-registry.ts index 5008f3b9fc9..91878606b30 100644 --- a/src/main/ssh/ssh-target-registry.ts +++ b/src/main/ssh/ssh-target-registry.ts @@ -24,7 +24,12 @@ import type { */ let sshStore: SshConnectionStore | null = null -let registeredConnectSshTarget: ((targetId: string) => Promise) | null = null +type ConnectSshTarget = ( + targetId: string, + options?: { automatic?: boolean } +) => Promise + +let registeredConnectSshTarget: ConnectSshTarget | null = null let registeredGetSshState: ((targetId: string) => SshConnectionState | undefined) | null = null export function setSshTargetRegistryStore(store: SshConnectionStore | null): void { @@ -36,20 +41,24 @@ export function getSshTargetRegistryStore(): SshConnectionStore | null { } export function setSshTargetRegistryHandlers(handlers: { - connect: ((targetId: string) => Promise) | null + connect: ConnectSshTarget | null getState: ((targetId: string) => SshConnectionState | undefined) | null }): void { registeredConnectSshTarget = handlers.connect registeredGetSshState = handlers.getState } -export async function connectRegisteredSshTarget(targetId: string): Promise { +/** `automatic`: no user asked for this connect, so a cancelled sign-in stays cancelled. */ +export async function connectRegisteredSshTarget( + targetId: string, + options?: { automatic?: boolean } +): Promise { if (!registeredConnectSshTarget) { // Why this still throws: a headless host that never registered handlers must fail // loudly rather than report a target as unreachable, which would read as `exited`. throw new Error('ssh_handlers_not_registered') } - return registeredConnectSshTarget(targetId) + return registeredConnectSshTarget(targetId, options) } export function getRegisteredSshState(targetId: string): SshConnectionState | undefined { diff --git a/src/main/startup/codex-hook-startup-wiring.test.ts b/src/main/startup/codex-hook-startup-wiring.test.ts index f142448b98e..a62cb6fb007 100644 --- a/src/main/startup/codex-hook-startup-wiring.test.ts +++ b/src/main/startup/codex-hook-startup-wiring.test.ts @@ -32,26 +32,49 @@ vi.mock('./main-process-runtime-service', () => ({ }), configureRuntimeServices: vi.fn() })) -vi.mock('../star-nag/service') -vi.mock('../browser/agent-browser-bridge') -vi.mock('../emulator/emulator-bridge') -vi.mock('../runtime/rpc/dispatcher') +vi.mock('../star-nag/service', () => ({ + StarNagService: class { + start = vi.fn() + registerIpcHandlers = vi.fn() + } +})) +vi.mock('../browser/agent-browser-bridge', () => ({ + AgentBrowserBridge: class { + sweepOrphanedSessions = vi.fn() + } +})) +vi.mock('../emulator/emulator-bridge', () => ({ EmulatorBridge: class {} })) +vi.mock('../runtime/rpc/dispatcher', () => ({ RpcDispatcher: class {} })) vi.mock('../browser/browser-manager', () => ({ browserManager: {} })) -vi.mock('../browser/browser-client-page-automation-runtime') -vi.mock('../crash-reporting/process-gone-diagnostics') -vi.mock('./main-window-lifecycle-flags') -vi.mock('./gpu-lifecycle') +vi.mock('../browser/browser-client-page-automation-runtime', () => ({ + configureBrowserClientPageAutomationRuntime: vi.fn() +})) +vi.mock('../crash-reporting/process-gone-diagnostics', () => ({ + startPreGoneCrashSampling: vi.fn() +})) +vi.mock('./main-window-lifecycle-flags', () => ({ recordProcessGoneCrash: vi.fn() })) +vi.mock('./gpu-lifecycle', () => ({ handleGpuChildCrash: vi.fn() })) vi.mock('./configure-process', () => ({ shouldInstallManagedHooks: () => false })) -vi.mock('../agent-hooks/install-telemetry') -vi.mock('./main-process-observers') -vi.mock('./main-process-account-services') -vi.mock('./main-process-automations') -vi.mock('./main-process-plugins') -vi.mock('../worktree-trash') -vi.mock('./worktree-removal-records-load') -vi.mock('./first-window-deferral') -vi.mock('./startup-diagnostics') -vi.mock('../opencode/opencode-status-plugin-startup-refresh') +vi.mock('../agent-hooks/install-telemetry', () => ({ recordManagedHookInstallFailure: vi.fn() })) +vi.mock('./main-process-observers', () => ({ initializeMainProcessObservers: vi.fn() })) +vi.mock('./main-process-account-services', () => ({ + initializeMainProcessAccountServices: vi.fn() +})) +vi.mock('./main-process-automations', () => ({ initializeMainProcessAutomations: vi.fn() })) +vi.mock('./main-process-plugins', () => ({ initializeMainProcessPlugins: vi.fn() })) +vi.mock('../worktree-trash', () => ({ + collectWorktreeTrashSweepRoots: vi.fn(), + sweepStaleWorktreeTrash: vi.fn() +})) +vi.mock('./worktree-removal-records-load', () => ({ loadWorktreeRemovalRecordsForStore: vi.fn() })) +vi.mock('./first-window-deferral', () => ({ runAfterFirstWindowShown: vi.fn() })) +vi.mock('./startup-diagnostics', () => ({ logStartupMilestone: vi.fn() })) +vi.mock('../opencode/opencode-status-plugin-startup-refresh', () => ({ + refreshInstalledOpenCodeStatusPlugins: vi.fn() +})) +vi.mock('../orcad/orcad-artifact-cache-retention', () => ({ + pruneDesktopOrcadArtifactCache: vi.fn() +})) import { initializeReadyRuntimeServices } from './main-process-ready-runtime' diff --git a/src/main/startup/main-process-runtime-launch-activation.test.ts b/src/main/startup/main-process-runtime-launch-activation.test.ts index 80bd815606e..516342f877b 100644 --- a/src/main/startup/main-process-runtime-launch-activation.test.ts +++ b/src/main/startup/main-process-runtime-launch-activation.test.ts @@ -102,6 +102,14 @@ vi.mock('../window/foreground-activation-policy', () => ({ isWindowlessLaunch: () => false, showWindowWithoutStealingFocus })) +vi.mock('../tray/system-tray', () => ({ + createSystemTray: vi.fn(), + setMacMenuBarIconVisible: vi.fn() +})) +vi.mock('../window/attach-main-window-services', () => ({ ensureAutoUpdaterConfigured: vi.fn() })) +vi.mock('../window/dashboard-popout-window', () => ({ getDashboardPopoutWindow: () => null })) +vi.mock('../window/createMainWindow', () => ({ loadMainWindow: vi.fn() })) +vi.mock('../window/renderer-recovery-prompt', () => ({ presentRendererRecoveryPrompt: vi.fn() })) vi.mock('./main-process-ready-foundation', () => ({ initializeReadyFoundation: vi.fn(async () => {}) diff --git a/src/main/startup/run-electron-vite-dev-web.test.ts b/src/main/startup/run-electron-vite-dev-web.test.ts index ada8bb3c9fa..9b94938bcc9 100644 --- a/src/main/startup/run-electron-vite-dev-web.test.ts +++ b/src/main/startup/run-electron-vite-dev-web.test.ts @@ -156,6 +156,7 @@ describe('run-electron-vite-dev web client prepare', () => { ORCA_ELECTRON_VITE_CLI: fakeCliPath, ORCA_VITE_CLI: fakeVitePath, ORCA_SKIP_DEV_CLI_PREPARE: '1', + ORCA_SKIP_DEV_STORAGE_RECLAIM: '1', ORCA_SKIP_DEV_ELECTRON_APP_PREPARE: '1', ORCA_DEV_WRAPPER_TEST_PID_FILE: pidFile, ORCA_DEV_WRAPPER_TEST_ENV_FILE: envFile, @@ -207,6 +208,7 @@ describe('run-electron-vite-dev web client prepare', () => { ORCA_ELECTRON_VITE_CLI: fakeCliPath, ORCA_VITE_CLI: fakeVitePath, ORCA_SKIP_DEV_CLI_PREPARE: '1', + ORCA_SKIP_DEV_STORAGE_RECLAIM: '1', ORCA_SKIP_DEV_ELECTRON_APP_PREPARE: '1', ORCA_DEV_WEB_PREPARE: '1', ORCA_DEV_WRAPPER_TEST_PID_FILE: pidFile, diff --git a/src/main/startup/run-electron-vite-dev.test.ts b/src/main/startup/run-electron-vite-dev.test.ts index 73d1bb21cdc..1c0469f7664 100644 --- a/src/main/startup/run-electron-vite-dev.test.ts +++ b/src/main/startup/run-electron-vite-dev.test.ts @@ -102,7 +102,7 @@ function devWrapperTestEnv(extra: NodeJS.ProcessEnv): NodeJS.ProcessEnv { } } delete env.ELECTRON_EXEC_PATH - return { ...env, ...extra } + return { ...env, ORCA_SKIP_DEV_STORAGE_RECLAIM: '1', ...extra } } /** diff --git a/src/main/terminal-history.ts b/src/main/terminal-history.ts index 07ad8f12c93..27f8cfbac9e 100644 --- a/src/main/terminal-history.ts +++ b/src/main/terminal-history.ts @@ -107,7 +107,10 @@ function writeMetaFile( worktreeId, createdAt: existing?.createdAt ?? new Date().toISOString(), ...(fish ? { fishSession: fish.session } : {}), - ...(fish?.historyDir ? { fishHistoryDir: fish.historyDir } : {}) + ...(fish?.historyDir ? { fishHistoryDir: fish.historyDir } : {}), + ...(existing?.installedMigrationBlobs + ? { installedMigrationBlobs: existing.installedMigrationBlobs } + : {}) }), { mode: 0o600 } ) @@ -123,8 +126,12 @@ export type HistoryDirMeta = { fishSession?: string /** Directory that session's history file was written to, as the PTY saw it. */ fishHistoryDir?: string + /** Moved-history blobs (by sha256) already installed here, so a retry never prepends twice. */ + installedMigrationBlobs?: InstalledMigrationHistoryBlob[] } +export type InstalledMigrationHistoryBlob = { sha256: string; shell: string } + /** Read one history directory's meta.json, or null when it is absent or unparseable. */ export function readHistoryMeta(dir: string): HistoryDirMeta | null { try { @@ -174,13 +181,33 @@ function parseHistoryMeta(dir: string, contents: string): HistoryDirMeta | null ...(typeof record.worktreeId === 'string' ? { worktreeId: record.worktreeId } : {}), ...(typeof record.createdAt === 'string' ? { createdAt: record.createdAt } : {}), ...(fishSession ? { fishSession } : {}), - ...(fishHistoryDir ? { fishHistoryDir } : {}) + ...(fishHistoryDir ? { fishHistoryDir } : {}), + ...parseInstalledMigrationBlobs(record.installedMigrationBlobs) } } catch { return null } } +function parseInstalledMigrationBlobs( + value: unknown +): Pick { + if (!Array.isArray(value)) { + return {} + } + const blobs = value.flatMap((entry: unknown) => + entry && + typeof entry === 'object' && + 'sha256' in entry && + typeof entry.sha256 === 'string' && + 'shell' in entry && + typeof entry.shell === 'string' + ? [{ sha256: entry.sha256, shell: entry.shell }] + : [] + ) + return blobs.length > 0 ? { installedMigrationBlobs: blobs } : {} +} + // ─── Environment Injection ───────────────────────────────────────── export type HistoryInjectionResult = { diff --git a/src/main/window/worktree-sleep-request-relay.test.ts b/src/main/window/worktree-sleep-request-relay.test.ts new file mode 100644 index 00000000000..9b6a2c8f35f --- /dev/null +++ b/src/main/window/worktree-sleep-request-relay.test.ts @@ -0,0 +1,112 @@ +import { EventEmitter } from 'node:events' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { WorktreeSleepRequest } from '../../shared/worktree-sleep-request' + +const ipcEmitter = new EventEmitter() +vi.mock('electron', () => ({ + ipcMain: { + on: (channel: string, listener: (...args: unknown[]) => void) => + ipcEmitter.on(channel, listener), + removeListener: (channel: string, listener: (...args: unknown[]) => void) => + ipcEmitter.removeListener(channel, listener) + } +})) + +const { requestWorktreeSleepFromRenderer, worktreeSleepTimeoutMs } = + await import('./worktree-sleep-request-relay') + +class FakeContents extends EventEmitter { + destroyed = false + readonly sent: WorktreeSleepRequest[] = [] + isDestroyed(): boolean { + return this.destroyed + } + send(_channel: string, request: WorktreeSleepRequest): void { + this.sent.push(request) + } +} + +class FakeWindow extends EventEmitter { + readonly webContents = new FakeContents() + isDestroyed(): boolean { + return false + } +} + +const respond = (sender: unknown, response: Record): void => { + ipcEmitter.emit('ui:worktreeSleepResponse', { sender }, response) +} + +let window: FakeWindow +beforeEach(() => { + ipcEmitter.removeAllListeners() + window = new FakeWindow() +}) +afterEach(() => vi.useRealTimers()) + +describe('requestWorktreeSleepFromRenderer', () => { + it("resolves with the renderer's failed workspaces once its own window acks", async () => { + const pending = requestWorktreeSleepFromRenderer(window, ['wt-1', 'wt-2']) + const [request] = window.webContents.sent + expect(request?.worktreeIds).toEqual(['wt-1', 'wt-2']) + + respond(new FakeContents(), { requestId: request?.requestId }) + respond(window.webContents, { requestId: 'other' }) + respond(window.webContents, { requestId: request?.requestId, failedWorktreeIds: ['wt-2'] }) + + await expect(pending).resolves.toEqual({ failedWorktreeIds: ['wt-2'] }) + expect(ipcEmitter.listenerCount('ui:worktreeSleepResponse')).toBe(0) + expect(window.listenerCount('closed')).toBe(0) + }) + + it('rejects with the error the renderer reports', async () => { + const pending = requestWorktreeSleepFromRenderer(window, ['wt-1']) + respond(window.webContents, { + requestId: window.webContents.sent[0]?.requestId, + error: 'persist failed' + }) + await expect(pending).rejects.toThrow('persist failed') + }) + + it.each([ + ['the window closes', (target: FakeWindow) => target.emit('closed')], + ['the renderer dies', (target: FakeWindow) => target.webContents.emit('render-process-gone')], + ['the renderer reloads', (target: FakeWindow) => target.webContents.emit('did-start-loading')] + ])('rejects renderer_unavailable when %s', async (_name, lose) => { + const pending = requestWorktreeSleepFromRenderer(window, ['wt-1']) + lose(window) + await expect(pending).rejects.toThrow('renderer_unavailable') + expect(ipcEmitter.listenerCount('ui:worktreeSleepResponse')).toBe(0) + expect(window.webContents.listenerCount('destroyed')).toBe(0) + expect(window.webContents.listenerCount('did-start-loading')).toBe(0) + }) + + it('rejects renderer_unavailable up front when the renderer is already gone', async () => { + window.webContents.destroyed = true + await expect(requestWorktreeSleepFromRenderer(window, ['wt-1'])).rejects.toThrow( + 'renderer_unavailable' + ) + expect(window.webContents.sent).toEqual([]) + }) + + it('times out instead of waiting forever', async () => { + vi.useFakeTimers() + const pending = requestWorktreeSleepFromRenderer(window, ['wt-1']) + let done = false + const settled = expect(pending.finally(() => (done = true))).rejects.toThrow( + 'worktree_sleep_timeout' + ) + await vi.advanceTimersByTimeAsync(29_999) + expect(done).toBe(false) + await vi.advanceTimersByTimeAsync(1) + await settled + expect(ipcEmitter.listenerCount('ui:worktreeSleepResponse')).toBe(0) + }) + + it('scales the wait with the workspaces, capped at five minutes', () => { + expect(worktreeSleepTimeoutMs(0)).toBe(20_000) + expect(worktreeSleepTimeoutMs(3)).toBe(50_000) + expect(worktreeSleepTimeoutMs(28)).toBe(300_000) + expect(worktreeSleepTimeoutMs(500)).toBe(300_000) + }) +}) diff --git a/src/main/window/worktree-sleep-request-relay.ts b/src/main/window/worktree-sleep-request-relay.ts new file mode 100644 index 00000000000..2d886179d75 --- /dev/null +++ b/src/main/window/worktree-sleep-request-relay.ts @@ -0,0 +1,110 @@ +import { randomUUID } from 'node:crypto' + +import { ipcMain } from 'electron' +import type { BrowserWindow } from 'electron' +import type { + WorktreeSleepRequest, + WorktreeSleepResponse +} from '../../shared/worktree-sleep-request' + +const WORKTREE_SLEEP_BASE_TIMEOUT_MS = 20_000 +const WORKTREE_SLEEP_PER_WORKSPACE_MS = 10_000 +const WORKTREE_SLEEP_MAX_TIMEOUT_MS = 5 * 60_000 + +/** One request sleeps every workspace of a project in turn, so the wait grows with them. */ +export function worktreeSleepTimeoutMs(workspaceCount: number): number { + return Math.min( + WORKTREE_SLEEP_BASE_TIMEOUT_MS + WORKTREE_SLEEP_PER_WORKSPACE_MS * workspaceCount, + WORKTREE_SLEEP_MAX_TIMEOUT_MS + ) +} + +export type WorktreeSleepResult = { failedWorktreeIds: string[] } + +/** The parts of a BrowserWindow the relay touches. */ +export type WorktreeSleepRendererWindow = { + isDestroyed(): boolean + readonly webContents: { + isDestroyed(): boolean + send(channel: string, request: WorktreeSleepRequest): void + once(event: 'destroyed', listener: () => void): unknown + once(event: 'render-process-gone', listener: () => void): unknown + once(event: 'did-start-loading', listener: () => void): unknown + removeListener(event: 'destroyed', listener: () => void): unknown + removeListener(event: 'render-process-gone', listener: () => void): unknown + removeListener(event: 'did-start-loading', listener: () => void): unknown + } + once(event: 'closed', listener: () => void): unknown + removeListener(event: 'closed', listener: () => void): unknown +} + +// Why: no call site passes a real window yet; keep the narrow type honest against Electron's. +const browserWindowFits: BrowserWindow extends WorktreeSleepRendererWindow ? true : never = true +void browserWindowFits + +/** + * Sleep runs in the renderer, which owns the slept-pane records; this waits until it has slept + * the workspaces and persisted their session. Rejects `renderer_unavailable` or + * `worktree_sleep_timeout`; there is no headless fallback. + */ +export async function requestWorktreeSleepFromRenderer( + mainWindow: WorktreeSleepRendererWindow, + worktreeIds: readonly string[] +): Promise { + if (mainWindow.isDestroyed()) { + throw new Error('renderer_unavailable') + } + const webContents = mainWindow.webContents + if (webContents.isDestroyed()) { + throw new Error('renderer_unavailable') + } + const requestId = randomUUID() + return new Promise((resolve, reject) => { + let settled = false + const onRendererUnavailable = (): void => finish(new Error('renderer_unavailable')) + const finish = (outcome: Error | WorktreeSleepResult): void => { + if (settled) { + return + } + settled = true + clearTimeout(timeout) + ipcMain.removeListener('ui:worktreeSleepResponse', onResponse) + mainWindow.removeListener('closed', onRendererUnavailable) + webContents.removeListener('destroyed', onRendererUnavailable) + webContents.removeListener('render-process-gone', onRendererUnavailable) + webContents.removeListener('did-start-loading', onRendererUnavailable) + if (outcome instanceof Error) { + reject(outcome) + } else { + resolve(outcome) + } + } + const timeout = setTimeout( + () => finish(new Error('worktree_sleep_timeout')), + worktreeSleepTimeoutMs(worktreeIds.length) + ) + const onResponse = (event: Electron.IpcMainEvent, response: WorktreeSleepResponse): void => { + // Why: request ids are visible to renderer code; only the selected window may answer. + if (event.sender !== webContents || response.requestId !== requestId) { + return + } + finish( + response.error + ? new Error(response.error) + : { failedWorktreeIds: response.failedWorktreeIds ?? [] } + ) + } + ipcMain.on('ui:worktreeSleepResponse', onResponse) + mainWindow.once('closed', onRendererUnavailable) + webContents.once('destroyed', onRendererUnavailable) + webContents.once('render-process-gone', onRendererUnavailable) + // A reload discards the renderer's pending Sleep, so it would never answer. + webContents.once('did-start-loading', onRendererUnavailable) + const request: WorktreeSleepRequest = { requestId, worktreeIds: [...worktreeIds] } + try { + webContents.send('ui:worktreeSleepRequest', request) + } catch { + finish(new Error('renderer_unavailable')) + } + }) +} diff --git a/src/main/worktree-lineage-pruning.ts b/src/main/worktree-lineage-pruning.ts index fc0068e3779..0e27d98c550 100644 --- a/src/main/worktree-lineage-pruning.ts +++ b/src/main/worktree-lineage-pruning.ts @@ -7,6 +7,7 @@ import { isWorkspaceKey, parseWorkspaceKey, worktreeWorkspaceKey } from '../shar import { splitWorktreeId } from '../shared/worktree/id' import { worktreeRetentionPathComparisonKey } from './worktree-retention-path-comparison' import type { Store } from './persistence' +import { isWslMoveRepoBlocked } from './persistence/moving-wsl-projects/wsl-move-background-backoff' type WorktreeLineagePruningStore = Pick & Partial< @@ -57,7 +58,9 @@ export function pruneLineageForMissingRepoWorktrees( } = {} ): void { const { platform = process.platform, preservedMetadataCandidateIds } = options + // Mid-move a scan can miss paths that still exist; lineage waits for the move to settle. if ( + isWslMoveRepoBlocked(repo) || typeof store.getAllWorktreeLineage !== 'function' || typeof store.removeWorktreeLineage !== 'function' ) { diff --git a/src/main/wsl-move-incidental-writes.test.ts b/src/main/wsl-move-incidental-writes.test.ts new file mode 100644 index 00000000000..58418f1caa8 --- /dev/null +++ b/src/main/wsl-move-incidental-writes.test.ts @@ -0,0 +1,148 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { GitPushTarget } from '../shared/worktree/types' +import type { ExecutionHostId } from '../shared/execution-host' +import { mergeWorktreeMetaForWrite } from './persistence/loading-store/worktree-meta-write-normalization' +import { wslMoveCutoverFixture as cutover } from './persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from './persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + loadWslMoveFence, + resetWslMoveFenceForTests +} from './persistence/moving-wsl-projects/wsl-move-scope-guard' +import type * as LinearCommandDependencies from './runtime/runtime-linear-command-dependencies' +import type { ResolvedWorktree } from './runtime/runtime-worktree-path-identity' + +const { gitExecFileAsyncMock } = vi.hoisted(() => ({ gitExecFileAsyncMock: vi.fn() })) +vi.mock('./git/runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock })) +vi.mock('./runtime/runtime-linear-command-dependencies', async (importOriginal) => ({ + ...(await importOriginal()), + resolveLegacyLinearLinkWorkspace: () => ({ workspaceId: 'ws-1', organizationUrlKey: 'acme' }) +})) + +import { materializeWorktreePushTargetRemote } from './ipc/worktree-remote' +import { RuntimeLinearTeamWriteCommands } from './runtime/runtime-linear-team-write-commands' + +const MOVING_WT = 'repo-1::/ws/repo-1/wt' +const STAYING_WT = 'repo-2::/ws/repo-2/wt' + +let userDataPath: string +beforeEach(() => { + userDataPath = mkdtempSync(join(tmpdir(), 'wsl-move-incidental-')) + loadWslMoveFence(userDataPath) + seedWslMoveCutover(userDataPath, cutover('move-1', 'source-fenced')) + gitExecFileAsyncMock.mockReset() + gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { + if (args[0] === 'remote' && args[1] === 'get-url') { + throw new Error('No such remote') + } + return { stdout: '', stderr: '' } + }) +}) +afterEach(() => { + resetWslMoveFenceForTests() + rmSync(userDataPath, { recursive: true, force: true }) +}) + +describe('push-target bookkeeping under a WSL move', () => { + const target: GitPushTarget = { + remoteName: 'pr-contributor-orca', + branchName: 'contributor/fix', + remoteUrl: 'git@github.com:contributor/orca.git' + } + + async function push(worktreeId: string, hostId?: ExecutionHostId) { + const setWorktreeMeta = vi.fn() + const meta = hostId ? { [worktreeId]: mergeWorktreeMetaForWrite(undefined, { hostId }) } : {} + const store = { getAllWorktreeMeta: () => meta, setWorktreeMeta } + const result = await materializeWorktreePushTargetRemote( + '/ws/repo', + target, + store, + undefined, + {}, + worktreeId + ) + return { result, setWorktreeMeta } + } + + it('still pushes the moving worktree but skips its metadata write', async () => { + const { result, setWorktreeMeta } = await push(MOVING_WT) + expect(result).toEqual({ ...target, remoteCreated: true }) + expect(setWorktreeMeta).not.toHaveBeenCalled() + }) + + it.each<[string, string, ExecutionHostId | undefined]>([ + ['a project the move is not taking', STAYING_WT, undefined], + ['the same repo id on another host', MOVING_WT, 'ssh:target-1'] + ])('records the push target for %s', async (_label, worktreeId, hostId) => { + const { setWorktreeMeta } = await push(worktreeId, hostId) + expect(setWorktreeMeta).toHaveBeenCalledWith(worktreeId, { + pushTarget: { ...target, remoteCreated: true } + }) + }) +}) + +describe('Linear workspace backfill under a WSL move', () => { + function worktree(id: string, hostId: ExecutionHostId): ResolvedWorktree { + const path = id.slice(id.indexOf('::') + 2) + return { + id, + repoId: id.slice(0, id.indexOf('::')), + hostId, + path, + head: 'abc', + branch: 'refs/heads/main', + isBare: false, + isMainWorktree: false, + displayName: 'wt', + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: 'ACME-1', + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0, + parentWorktreeId: null, + childWorktreeIds: [], + lineage: null, + git: { path, head: 'abc', branch: 'refs/heads/main', isBare: false, isMainWorktree: false } + } + } + + async function resolveCurrent(row: ResolvedWorktree) { + const setWorktreeMeta = vi.fn() + const commands = new RuntimeLinearTeamWriteCommands({ + runtimeAvailable: () => true, + showTerminal: async () => { + throw new Error('unused') + }, + resolveWorktreeSelector: async () => row, + listResolvedWorktrees: async () => [row], + setWorktreeMeta, + emitClientEvent: () => {} + }) + const link = await commands.linearResolveCurrentIssue({ cwd: row.path }) + return { link, setWorktreeMeta } + } + + it('answers for the moving worktree without persisting the backfill', async () => { + const { link, setWorktreeMeta } = await resolveCurrent(worktree(MOVING_WT, 'local')) + expect(link.workspaceId).toBe('ws-1') + expect(setWorktreeMeta).not.toHaveBeenCalled() + }) + + it.each<[string, string, ExecutionHostId]>([ + ['a project the move is not taking', STAYING_WT, 'local'], + ['the same repo id on another host', MOVING_WT, 'ssh:target-1'] + ])('persists the backfill for %s', async (_label, id, hostId) => { + const { setWorktreeMeta } = await resolveCurrent(worktree(id, hostId)) + expect(setWorktreeMeta).toHaveBeenCalledWith(id, { + linkedLinearIssueWorkspaceId: 'ws-1', + linkedLinearIssueOrganizationUrlKey: 'acme' + }) + }) +}) diff --git a/src/main/wsl-move-scan-pruning.test.ts b/src/main/wsl-move-scan-pruning.test.ts new file mode 100644 index 00000000000..c1575185260 --- /dev/null +++ b/src/main/wsl-move-scan-pruning.test.ts @@ -0,0 +1,237 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { Repo } from '../shared/repo-types' +import type { ExecutionHostId } from '../shared/execution-host' +import type { WorktreeLineage } from '../shared/worktree/lineage-types' +import type { GitWorktreeInfo } from '../shared/worktree/types' +import { worktreeWorkspaceKey } from '../shared/workspace-scope' +import { closeTestStores, createStore, testState } from './persistence-test-harness' +import type { Store } from './persistence' +import { wslMoveCutoverFixture as cutover } from './persistence/__fixtures__/wsl-mover/wsl-move-cutover-fixture' +import { seedWslMoveCutover } from './persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding' +import { + loadWslMoveFence, + resetWslMoveFenceForTests +} from './persistence/moving-wsl-projects/wsl-move-scope-guard' +import { pruneLineageForMissingRepoWorktrees } from './worktree-lineage-pruning' +import { pruneMetadataMissingFromAuthoritativeLocalScan } from './ipc/worktrees/listing/authoritative-local-worktree-metadata-pruning' +import { listFolderWorkspaces } from './ipc/worktrees/listing/folder-workspace-catalog' +import { getFolderWorkspaceRootId } from './ipc/worktrees/folder-workspace-model' +import { getLocalWorktreeScanGeneration } from './local-worktree-scan-generation' +import { listRuntimeFolderWorkspaces } from './runtime/runtime-worktree-filesystem' +import { getRuntimeFolderWorkspaceRootId } from './runtime/runtime-folder-workspace' +import { RuntimeWorktreeLineageController } from './runtime/runtime-worktree-lineage-controller' +import type { ResolvedWorktree } from './runtime/runtime-worktree-path-identity' + +vi.mock('electron', () => ({ + app: { getPath: () => testState.dir }, + safeStorage: { isEncryptionAvailable: () => false } +})) + +const MOVING = 'repo-1' +const STAYING = 'repo-2' + +function repo(id: string, overrides: Partial = {}): Repo { + return { id, path: `/ws/${id}`, displayName: id, badgeColor: '#000', addedAt: 0, ...overrides } +} +const onSsh: Partial = { connectionId: 'target-1', executionHostId: 'ssh:target-1' } + +function gitRow(path: string): GitWorktreeInfo { + return { path, head: 'abc', branch: 'refs/heads/main', isBare: false, isMainWorktree: false } +} + +function lineage(childId: string, parentId: string): WorktreeLineage { + return { + worktreeId: childId, + worktreeInstanceId: `${childId}#instance`, + parentWorktreeId: parentId, + parentWorktreeInstanceId: `${parentId}#instance`, + origin: 'manual', + capture: { source: 'manual-action', confidence: 'explicit' }, + createdAt: 1 + } +} + +let store: Store +beforeEach(() => { + testState.dir = mkdtempSync(join(tmpdir(), 'wsl-move-scan-')) + store = createStore() + store.addRepo(repo(MOVING)) + store.addRepo(repo(STAYING)) + loadWslMoveFence(testState.dir) + seedWslMoveCutover(testState.dir, cutover('move-1', 'source-fenced')) +}) +afterEach(async () => { + resetWslMoveFenceForTests() + await closeTestStores() + rmSync(testState.dir, { recursive: true, force: true }) +}) + +describe('lineage pruning under a WSL move', () => { + const pruneMissingChild = (target: Repo): WorktreeLineage | undefined => { + const childId = `${target.id}::/ws/${target.id}/gone` + store.setWorktreeLineage(childId, lineage(childId, `${target.id}::${target.path}`)) + pruneLineageForMissingRepoWorktrees(store, target, [gitRow(target.path)]) + return store.getWorktreeLineage(childId) + } + + it('keeps the moving project lineage a mid-move scan cannot see', () => { + expect(pruneMissingChild(repo(MOVING))).toBeDefined() + }) + + it('prunes a project the move is not taking', () => { + expect(pruneMissingChild(repo(STAYING))).toBeUndefined() + }) + + it('prunes the same repo id on another host', () => { + expect(pruneMissingChild(repo(MOVING, onSsh))).toBeUndefined() + }) +}) + +describe('authoritative metadata pruning under a WSL move', () => { + const pruneMissingRow = async (repoId: string): Promise => { + const target = repo(repoId) + const goneId = `${repoId}::/ws/${repoId}/gone` + store.setWorktreeMeta(goneId, { displayName: 'gone' }) + const result = await pruneMetadataMissingFromAuthoritativeLocalScan({ + store, + repo: target, + gitWorktrees: [gitRow(target.path)], + scan: store.captureNativeLocalWorktreeMetadataScanExpectation(target), + scanGeneration: getLocalWorktreeScanGeneration(repoId), + platform: 'linux', + pathsExistOrAreUnverifiable: async (paths) => new Map(paths.map((path) => [path, false])) + }) + return [...result.removedWorktreeIds] + } + + it('removes nothing from the moving project', async () => { + expect(await pruneMissingRow(MOVING)).toEqual([]) + expect(store.getWorktreeMeta(`${MOVING}::/ws/${MOVING}/gone`)).toBeDefined() + }) + + it('prunes a project the move is not taking', async () => { + expect(await pruneMissingRow(STAYING)).toEqual([`${STAYING}::/ws/${STAYING}/gone`]) + }) +}) + +describe('folder catalog identity stamps under a WSL move', () => { + it('lists the moving folder project without writing its metadata', () => { + const folder = repo(MOVING, { kind: 'folder' }) + expect(listFolderWorkspaces(store, folder).map((row) => row.id)).toEqual([ + getFolderWorkspaceRootId(folder) + ]) + expect(store.getWorktreeMeta(getFolderWorkspaceRootId(folder))).toBeUndefined() + + const runtimeRows = listRuntimeFolderWorkspaces(store, folder) + expect(runtimeRows).toHaveLength(1) + expect(store.getWorktreeMeta(getRuntimeFolderWorkspaceRootId(folder))).toBeUndefined() + }) + + it('gives the moving root row the same identity on every listing', () => { + const folder = repo(MOVING, { kind: 'folder' }) + const first = listFolderWorkspaces(store, folder)[0]?.instanceId + const second = listFolderWorkspaces(store, folder)[0]?.instanceId + expect(first).toBeTruthy() + expect(second).toBe(first) + expect(store.getWorktreeMeta(getFolderWorkspaceRootId(folder))).toBeUndefined() + }) + + it.each([ + ['a project the move is not taking', repo(STAYING, { kind: 'folder' })], + ['the same repo id on another host', repo(MOVING, { kind: 'folder', ...onSsh })] + ])('stamps %s', (_label, folder) => { + listFolderWorkspaces(store, folder) + expect(store.getWorktreeMeta(getFolderWorkspaceRootId(folder))?.instanceId).toBeTruthy() + }) + + it('stamps a runtime folder project the move is not taking', () => { + const folder = repo(STAYING, { kind: 'folder' }) + listRuntimeFolderWorkspaces(store, folder) + expect(store.getWorktreeMeta(getRuntimeFolderWorkspaceRootId(folder))?.instanceId).toBeTruthy() + }) +}) + +describe('inferred lineage hydration under a WSL move', () => { + function resolved(repoId: string, name: string, hostId: ExecutionHostId, taskId?: string) { + const path = `/ws/${repoId}/${name}` + const worktree: ResolvedWorktree = { + id: `${repoId}::${path}`, + repoId, + instanceId: `${repoId}-${hostId}-${name}#instance`, + hostId, + path, + head: 'abc', + branch: `refs/heads/${name}`, + isBare: false, + isMainWorktree: false, + displayName: name, + comment: taskId ? `working on ${taskId}` : '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0, + parentWorktreeId: null, + childWorktreeIds: [], + lineage: null, + git: { path, head: 'abc', branch: `refs/heads/${name}`, isBare: false, isMainWorktree: false } + } + return worktree + } + + it('infers lineage only outside the moving local project', async () => { + const children = [ + resolved(MOVING, 'child', 'local', 'task_moving'), + resolved(STAYING, 'child', 'local', 'task_staying'), + resolved(MOVING, 'child-remote', 'ssh:target-1', 'task_remote') + ] + const parents = new Map([ + ['task_moving', resolved(MOVING, 'parent', 'local')], + ['task_staying', resolved(STAYING, 'parent', 'local')], + ['task_remote', resolved(MOVING, 'parent-remote', 'ssh:target-1')] + ]) + const controller = new RuntimeWorktreeLineageController({ + getStore: () => store, + getCachedWorktrees: () => null, + getDb: () => null, + resolveWorktree: async () => { + throw new Error('unused') + }, + listResolvedWorktrees: async () => children, + showTerminal: async () => { + throw new Error('unused') + } + }) + vi.spyOn(controller, 'resolveTaskCandidate').mockImplementation(async (taskId) => { + const parent = parents.get(taskId) + return parent + ? { + source: 'orchestration-context', + taskId, + parent: { + type: 'worktree', + workspaceKey: worktreeWorkspaceKey(parent.id), + worktree: parent, + instanceId: parent.instanceId ?? null + } + } + : null + }) + + await controller.hydrate() + + expect(store.getWorktreeLineage(children[0].id)).toBeUndefined() + expect(store.getWorktreeLineage(children[1].id)?.parentWorktreeId).toBe( + parents.get('task_staying')?.id + ) + expect(store.getWorktreeLineage(children[2].id)?.parentWorktreeId).toBe( + parents.get('task_remote')?.id + ) + }) +}) diff --git a/src/preload/api/ui-bridge-clipboard-and-window-controls.ts b/src/preload/api/ui-bridge-clipboard-and-window-controls.ts index 6baa98ad6bb..333ebd8a3bb 100644 --- a/src/preload/api/ui-bridge-clipboard-and-window-controls.ts +++ b/src/preload/api/ui-bridge-clipboard-and-window-controls.ts @@ -73,6 +73,15 @@ export const uiClipboardAndWindowControlsApi = { ipcRenderer.on('ui:sleepWorktree', listener) return () => ipcRenderer.removeListener('ui:sleepWorktree', listener) }, + onWorktreeSleepRequest: (callback) => { + const listener = (_event: Electron.IpcRendererEvent, request: Parameters[0]) => + callback(request) + ipcRenderer.on('ui:worktreeSleepRequest', listener) + return () => ipcRenderer.removeListener('ui:worktreeSleepRequest', listener) + }, + respondWorktreeSleep: (response) => { + ipcRenderer.send('ui:worktreeSleepResponse', response) + }, onResumeSleepingAgents: (callback: (data: { worktreeId: string }) => void): (() => void) => { const listener = (_event: Electron.IpcRendererEvent, data: { worktreeId: string }) => callback(data) diff --git a/src/preload/api/ui-command-event-api.ts b/src/preload/api/ui-command-event-api.ts index a00a24dbfe4..963be524031 100644 --- a/src/preload/api/ui-command-event-api.ts +++ b/src/preload/api/ui-command-event-api.ts @@ -48,6 +48,10 @@ import type { SessionTabCloseRequest, SessionTabCloseResponse } from '../../shared/session-tab-close' +import type { + WorktreeSleepRequest, + WorktreeSleepResponse +} from '../../shared/worktree-sleep-request' export type CloseActiveTabPayload = { sourceId: string } @@ -262,6 +266,8 @@ export type UiCommandEventApi = { onTerminalTabCloseRequest: (callback: (request: TerminalTabCloseRequest) => void) => () => void respondTerminalTabClose: (response: TerminalTabCloseResponse) => void onSleepWorktree: (callback: (data: { worktreeId: string }) => void) => () => void + onWorktreeSleepRequest: (callback: (request: WorktreeSleepRequest) => void) => () => void + respondWorktreeSleep: (response: WorktreeSleepResponse) => void onResumeSleepingAgents: (callback: (data: { worktreeId: string }) => void) => () => void onTerminalZoom: (callback: (direction: 'in' | 'out' | 'reset') => void) => () => void onSystemResumed: (callback: () => void) => () => void diff --git a/src/renderer/src/app-shell/startup-window-parity.test.ts b/src/renderer/src/app-shell/startup-window-parity.test.ts new file mode 100644 index 00000000000..8fd5d1b5774 --- /dev/null +++ b/src/renderer/src/app-shell/startup-window-parity.test.ts @@ -0,0 +1,141 @@ +// Window parity: for each stored profile, the window's startup hydration of the session as stored +// and of the shared Loader's rewrite of it (load then save) must show the same thing. Where they +// differ today, the difference is listed below with the Loader rule that causes it. + +import { describe, expect, it, vi } from 'vitest' +import type * as AgentStatusModule from '@/lib/agent-status' +import type { ExecutionHostId } from '../../../shared/execution-host' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' +import { LEGACY_WORKSPACE_LAYOUT_PROFILES } from '../../../shared/workspace-layout/workspace-layout-legacy-profile.test-fixture' +import { loadWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-load' +import { WORKSPACE_LAYOUT_PROFILES } from '../../../shared/workspace-layout/workspace-layout-profile.test-fixture' +import { saveWorkspaceLayout } from '../../../shared/workspace-layout/workspace-layout-save' +import { createStoreSessionMockApi } from '../store/slices/store-session-test-harness' +import { createTestStore } from '../store/slices/store-test-helpers' +import { + bootWindowFromPartitions, + diffStartupWindowViews, + projectStartupWindowView +} from './startup-window-view.test-fixture' + +vi.mock('sonner', () => ({ toast: { info: vi.fn(), success: vi.fn(), error: vi.fn() } })) +vi.mock('@/lib/agent-status', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, detectAgentStatusFromTitle: vi.fn().mockReturnValue(null) } +}) + +createStoreSessionMockApi() + +const GIT = 'repo-1::/Users/dev/orca' +const terminal = (tabId: string) => + ['title', 'panes', 'focused pane', 'reattaches'].map( + (field) => `${GIT} terminal ${tabId} ${field}` + ) + +type KnownDifference = { + fixture: string + /** The Loader rule that makes the window show something else. */ + rule: string + /** View paths that differ; see `projectStartupWindowView`. */ + paths: string[] +} + +/** Today's differences. Entries may only be removed: fix the rule, then delete its entry. */ +const KNOWN_DIFFERENCES: readonly KnownDifference[] = [ + { + fixture: 'row without a tab-bar entry or a terminal', + rule: 'ghost tab: the Loader gives every row a tab-bar entry; the window adopts only rows it can reattach', + paths: [`${GIT} group 0 tabs`, `${GIT} tab tab-b`, ...terminal('tab-b')] + }, + { + fixture: 'legacy row repeating a tab-bar tab’s terminal', + rule: 'duplicate tab: the window drops a legacy row whose terminal a tab-bar tab holds; the Loader keeps it as a tab with an unbound pane', + paths: [`${GIT} group 0 tabs`, `${GIT} tab tab-legacy`, ...terminal('tab-legacy')] + }, + { + fixture: 'row terminal differs from its focused pane’s', + rule: 'row.ptyId rewrite: the Loader sets the row terminal to its focused pane’s, which changes what the tab reattaches', + paths: [`${GIT} terminal tab-a reattaches`] + }, + { + fixture: 'row with no terminal while its pane has one', + rule: 'row.ptyId rewrite: the Loader sets the row terminal to its focused pane’s, which changes what the tab reattaches', + paths: [`${GIT} terminal tab-a reattaches`] + }, + { + fixture: 'tab-bar entry declaring a group that does not list it', + rule: 'group owner: the window keeps a tab in the group its entry declares; the Loader moves it to the first group listing it', + paths: [ + `${GIT} groups`, + `${GIT} group 0 tabs`, + `${GIT} group 1 tabs`, + `${GIT} group 1 active tab`, + `${GIT} tab tab-b` + ] + }, + { + fixture: 'session from before the tab bar, with an open file', + rule: 'old sessions: the window builds tabs from rows and open files; the Loader builds them from rows only, losing editor tabs', + paths: [ + `${GIT} group 0 tabs`, + `${GIT} group 0 active tab`, + `${GIT} tab /Users/dev/orca/src/app.ts` + ] + }, + { + fixture: 'legacy surface tombstone on a pane still showing its terminal', + rule: 'tombstones: the Loader closes a pane a legacy surface tombstone names; the window ignores the tombstone and shows the pane', + paths: [`${GIT} group 0 tabs`, `${GIT} tab tab-b`, ...terminal('tab-b')] + }, + { + fixture: 'tab-bar label differing from the row title', + rule: 'label over title: the Loader writes the tab-bar label into the row title the window shows', + paths: [`${GIT} terminal tab-a title`] + } +] + +/** The shared Loader's round trip, as main would persist it. */ +function normalize(hostId: ExecutionHostId, session: WorkspaceSessionState): WorkspaceSessionState { + let next = 0 + const loaded = loadWorkspaceLayout(hostId, structuredClone(session), { + mintId: () => `minted-${++next}`, + mintLeafId: () => `00000000-0000-4000-9000-${String(++next).padStart(12, '0')}` + }) + return saveWorkspaceLayout(loaded) +} + +const PROFILES = [...WORKSPACE_LAYOUT_PROFILES, ...LEGACY_WORKSPACE_LAYOUT_PROFILES] + +describe('the window shows the same session before and after the shared Loader rewrites it', () => { + it.each(PROFILES.map((profile) => [profile.name, profile] as const))( + '%s', + async (name, { hostId, build }) => { + const stored = build() + const storedView = projectStartupWindowView( + await bootWindowFromPartitions(createTestStore(), [{ hostId, session: stored }]) + ) + const normalizedView = projectStartupWindowView( + await bootWindowFromPartitions(createTestStore(), [ + { hostId, session: normalize(hostId, stored) } + ]) + ) + // The window must show the stored workspaces at all, or an empty view hides differences. + for (const [key, rows] of Object.entries(stored.tabsByWorktree)) { + if (rows.length > 0) { + expect(storedView, key).toHaveProperty([`${key} group 0 tabs`]) + } + } + const differences = diffStartupWindowViews(storedView, normalizedView) + const known = KNOWN_DIFFERENCES.filter((entry) => entry.fixture === name) + expect( + differences.map((difference) => difference.path).sort(), + `window differences (stored vs normalized):\n${JSON.stringify(differences, null, 2)}` + ).toEqual(known.flatMap((entry) => entry.paths).sort()) + } + ) + + it('lists differences only for fixtures that exist', () => { + const names = new Set(PROFILES.map((profile) => profile.name)) + expect(KNOWN_DIFFERENCES.filter((entry) => !names.has(entry.fixture))).toEqual([]) + }) +}) diff --git a/src/renderer/src/app-shell/startup-window-view.test-fixture.ts b/src/renderer/src/app-shell/startup-window-view.test-fixture.ts new file mode 100644 index 00000000000..ae0267af904 --- /dev/null +++ b/src/renderer/src/app-shell/startup-window-view.test-fixture.ts @@ -0,0 +1,212 @@ +// Boots a test store through the window's startup session hydration and reduces the result to +// what the window shows, so two stored sessions can be compared by what a user would see. + +import { vi } from 'vitest' +import { getDefaultWorkspaceSession } from '../../../shared/constants' +import { + LOCAL_EXECUTION_HOST_ID, + parseExecutionHostId, + type ExecutionHostId +} from '../../../shared/execution-host' +import type { TabGroupLayoutNode } from '../../../shared/tab-types' +import type { + TerminalLayoutSnapshot, + TerminalPaneLayoutNode +} from '../../../shared/terminal-tab-types' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' +import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id' +import { fetchWorkspaceSessionWithRuntimeHostOwners } from '@/lib/workspace-session-host-hydration' +import { collectFolderWorkspaceKeysFromSession } from '@/lib/workspace-session-hydration-keys' +import type { AppState } from '../store/types' +import type { createTestStore } from '../store/slices/store-test-helpers' +import { reconcileHydratedWorkspaceTabModels } from './reconcile-hydrated-workspace-tab-models' + +/** One stored session partition and the host it is stored under. */ +export type StoredPartition = { hostId: ExecutionHostId; session: WorkspaceSessionState } + +/** + * The local catalog at first paint: a repo for each git workspace the partitions name, with no + * worktree listing yet. Remote-server repos are absent, as startup skips remote catalogs. + */ +function firstPaintRepos(partitions: readonly StoredPartition[]): AppState['repos'] { + const repos = new Map() + for (const { hostId, session } of partitions) { + const host = parseExecutionHostId(hostId) + if (host?.kind === 'runtime') { + continue + } + for (const key of Object.keys(session.tabsByWorktree)) { + const id = getRepoIdFromWorktreeId(key) + if (!key.startsWith(`${id}::`) || repos.has(id)) { + continue + } + repos.set(id, { + id, + path: key.slice(id.length + 2), + displayName: id, + badgeColor: '#000', + addedAt: 0, + connectionId: host?.kind === 'ssh' ? host.targetId : null, + executionHostId: hostId + }) + } + } + return [...repos.values()] +} + +/** + * Runs the same hydration calls, in the same order, as `use-app-startup-hydration.ts`, on a fresh + * test store the caller creates. + */ +export async function bootWindowFromPartitions( + store: ReturnType, + partitions: readonly StoredPartition[] +): Promise { + let minted = 0 + // Group and tab ids the window mints must match across the two boots being compared. + const randomUUID = vi + .spyOn(globalThis.crypto, 'randomUUID') + .mockImplementation(() => `00000000-0000-4000-8000-${String(++minted).padStart(12, '0')}`) + try { + store.setState({ repos: firstPaintRepos(partitions) }) + const read = async (hostId: ExecutionHostId = LOCAL_EXECUTION_HOST_ID) => + structuredClone( + partitions.find((partition) => partition.hostId === hostId)?.session ?? + getDefaultWorkspaceSession() + ) + const hostIdsOfKind = (kind: 'ssh' | 'runtime') => + partitions + .map((partition) => partition.hostId) + .filter((hostId) => parseExecutionHostId(hostId)?.kind === kind) + const sessionRead = await fetchWorkspaceSessionWithRuntimeHostOwners( + { get: read, listHostIds: async () => hostIdsOfKind('ssh') }, + store.getState().repos, + hostIdsOfKind('runtime') + ) + const actions = store.getState() + const options = { + additionalValidWorkspaceKeys: collectFolderWorkspaceKeysFromSession(sessionRead.session) + } + actions.hydrateWorkspaceSession(sessionRead.session, { + ...options, + runtimeHostIdByWorkspaceSessionKey: sessionRead.runtimeHostIdByWorkspaceSessionKey, + contestedHostWorkspaceSessions: sessionRead.contestedHostWorkspaceSessions, + contestedPrimaryHostBySessionKey: sessionRead.contestedPrimaryHostBySessionKey + }) + actions.hydrateTabsSession(sessionRead.session, options) + actions.hydrateEditorSession(sessionRead.session, options) + actions.hydrateBrowserSession(sessionRead.session, options) + reconcileHydratedWorkspaceTabModels( + sessionRead.session, + store.getState().reconcileWorktreeTabModels + ) + return store.getState() + } finally { + randomUUID.mockRestore() + } +} + +type PaneView = { pane: string | null } | { split: string; first: PaneView; second: PaneView } + +/** Panes by position, each naming the terminal it binds; leaf ids are not shown. */ +function paneView(node: TerminalPaneLayoutNode, layout: TerminalLayoutSnapshot): PaneView { + if (node.type === 'leaf') { + return { pane: layout.ptyIdsByLeafId?.[node.leafId] ?? null } + } + return { + split: node.direction, + first: paneView(node.first, layout), + second: paneView(node.second, layout) + } +} + +function leafPosition( + node: TerminalPaneLayoutNode, + leafId: string | null, + path = '' +): string | null { + if (node.type === 'leaf') { + return node.leafId === leafId ? path || 'root' : null + } + return ( + leafPosition(node.first, leafId, `${path}1`) ?? leafPosition(node.second, leafId, `${path}2`) + ) +} + +type GroupLayoutView = number | { split: string; first: GroupLayoutView; second: GroupLayoutView } + +function groupLayoutView(node: TabGroupLayoutNode, groupIds: string[]): GroupLayoutView { + if (node.type === 'leaf') { + return groupIds.indexOf(node.groupId) + } + return { + split: node.direction, + first: groupLayoutView(node.first, groupIds), + second: groupLayoutView(node.second, groupIds) + } +} + +/** + * What the window shows after startup, as `path -> value`. Groups are named by position, since + * a group id is never shown; tab ids stay, since a tab keeps its id across restarts. + */ +export function projectStartupWindowView(state: AppState): Record { + const view: Record = { + 'active.workspace': state.activeWorktreeId, + 'active.terminal': state.activeTabId + } + const workspaceKeys = new Set([ + ...Object.keys(state.tabsByWorktree), + ...Object.keys(state.unifiedTabsByWorktree), + ...state.openFiles.map((file) => file.worktreeId), + ...Object.keys(state.browserTabsByWorktree) + ]) + for (const key of [...workspaceKeys].sort()) { + const groups = state.groupsByWorktree[key] ?? [] + const groupIds = groups.map((group) => group.id) + const layout = state.layoutByWorktree[key] + view[`${key} groups`] = layout ? groupLayoutView(layout, groupIds) : groupIds.length + groups.forEach((group, index) => { + view[`${key} group ${index} tabs`] = group.tabOrder + view[`${key} group ${index} active tab`] = group.activeTabId + }) + view[`${key} active group`] = groupIds.indexOf(state.activeGroupIdByWorktree[key] ?? '') + view[`${key} active terminal`] = state.activeTabIdByWorktree[key] ?? null + view[`${key} active file`] = state.activeFileIdByWorktree[key] ?? null + view[`${key} active kind`] = state.activeTabTypeByWorktree[key] ?? null + for (const tab of state.unifiedTabsByWorktree[key] ?? []) { + view[`${key} tab ${tab.id}`] = { + kind: tab.contentType, + shows: tab.entityId, + group: groupIds.indexOf(tab.groupId), + label: tab.customLabel ?? tab.label + } + } + for (const row of state.tabsByWorktree[key] ?? []) { + const panes = state.terminalLayoutsByTabId[row.id] + const terminal = `${key} terminal ${row.id}` + view[`${terminal} title`] = row.customTitle ?? row.title + view[`${terminal} panes`] = panes?.root ? paneView(panes.root, panes) : null + view[`${terminal} focused pane`] = panes?.root + ? leafPosition(panes.root, panes.activeLeafId) + : null + view[`${terminal} reattaches`] = state.pendingReconnectPtyIdByTabId[row.id] ?? null + } + view[`${key} open files`] = state.openFiles + .filter((file) => file.worktreeId === key) + .map((file) => file.filePath) + view[`${key} browser tabs`] = (state.browserTabsByWorktree[key] ?? []).map((tab) => tab.url) + } + return view +} + +/** Each path whose value differs between two views, with both values. */ +export function diffStartupWindowViews( + stored: Record, + normalized: Record +): { path: string; stored: unknown; normalized: unknown }[] { + const paths = [...new Set([...Object.keys(stored), ...Object.keys(normalized)])].sort() + return paths + .filter((path) => JSON.stringify(stored[path]) !== JSON.stringify(normalized[path])) + .map((path) => ({ path, stored: stored[path], normalized: normalized[path] })) +} diff --git a/src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx b/src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx index 471dfaf3ed5..44242495404 100644 --- a/src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx +++ b/src/renderer/src/app-shell/use-profile-state-save-delay-notice.sonner.test.tsx @@ -11,9 +11,9 @@ const title = 'Profile storage is taking longer than usual' const frames: FrameRequestCallback[] = [] const listeners = new Set<(delayed: boolean) => void>() -function Notice() { +function Notice({ closeButton = false }: { closeButton?: boolean }) { useProfileStateSaveDelayNotice() - return + return } beforeEach(() => { @@ -97,6 +97,16 @@ it('does not let StrictMode cleanup dismiss the initial delayed snapshot', async expect(view.queryAllByText(title)).toHaveLength(1) }) +it('lets the user close the warning with the app toaster close button', async () => { + const view = render() + await advanceTimers() + expect(view.queryByText(title)).not.toBeNull() + + act(() => view.getByRole('button', { name: 'Close toast' }).click()) + await finishDismissal() + expect(view.queryByText(title)).toBeNull() +}) + it('keeps a new warning if recovery and another delay arrive before a dismissal frame', async () => { const view = render() await advanceTimers() diff --git a/src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx b/src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx index d673aba02c1..05018d5aef7 100644 --- a/src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx +++ b/src/renderer/src/app-shell/use-profile-state-save-delay-notice.test.tsx @@ -62,9 +62,7 @@ it('restores a persistent warning from main and clears it on completion', async { id: undefined, description: 'Further saves may be delayed while this operation finishes.', - duration: Infinity, - dismissible: false, - closeButton: false + duration: Infinity } ) publish(false) diff --git a/src/renderer/src/app-shell/use-profile-state-save-delay-notice.ts b/src/renderer/src/app-shell/use-profile-state-save-delay-notice.ts index 99815c1c3f4..7b35bba58eb 100644 --- a/src/renderer/src/app-shell/use-profile-state-save-delay-notice.ts +++ b/src/renderer/src/app-shell/use-profile-state-save-delay-notice.ts @@ -34,9 +34,7 @@ export function useProfileStateSaveDelayNotice(): void { 'app.saving.delayedDescription', 'Further saves may be delayed while this operation finishes.' ), - duration: Infinity, - dismissible: false, - closeButton: false + duration: Infinity } ) } diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 840733db0e4..9a539b555fe 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -58,6 +58,7 @@ --color-chat-foreground: var(--chat-foreground); --color-chat-foreground-strong: var(--chat-foreground-strong); --color-chat-foreground-faint: var(--chat-foreground-faint); + --color-chat-unread: var(--chat-unread); --color-chat-user-surface: var(--chat-user-surface); --color-chat-user-border: var(--chat-user-border); --color-chat-code-foreground: var(--chat-code-foreground); @@ -637,6 +638,8 @@ body:has(> [data-slot='dialog-overlay'], > [data-slot='sheet-overlay']) { /* Mixed toward the foreground so each clears text contrast on the canvas in either scheme. */ --chat-focus-ring: color-mix(in srgb, var(--chat-source-foreground) 50%, var(--chat-canvas)); --chat-warning: color-mix(in srgb, var(--status-warning) 70%, var(--chat-source-foreground)); + /* What the reader has not seen yet: the transcript's "New" marker, in the note hue. */ + --chat-unread: color-mix(in srgb, var(--callout-note) 70%, var(--chat-source-foreground)); --chat-diff-added: color-mix( in srgb, var(--git-decoration-added) 70%, diff --git a/src/renderer/src/components/artifacts/ArtifactPublishButton.tsx b/src/renderer/src/components/artifacts/ArtifactPublishButton.tsx index a90c1c037a9..8bb2a49218a 100644 --- a/src/renderer/src/components/artifacts/ArtifactPublishButton.tsx +++ b/src/renderer/src/components/artifacts/ArtifactPublishButton.tsx @@ -21,6 +21,8 @@ export function ArtifactPublishButton({ sourceKey, createRequest, className, + variant = 'ghost', + size = 'icon-sm', disabled, anchorRef, open: controlledOpen, @@ -29,6 +31,9 @@ export function ArtifactPublishButton({ sourceKey: string createRequest: () => Promise className?: string + /** For a toolbar whose other buttons use a different style. */ + variant?: React.ComponentProps['variant'] + size?: React.ComponentProps['size'] disabled?: boolean } & ( | { anchorRef?: never; open?: never; onOpenChange?: never } @@ -134,8 +139,8 @@ export function ArtifactPublishButton({ @@ -91,6 +132,14 @@ export function NativeChatInlineVisual({ {openLabel} + readNativeChatVisualShareRequest(owner, file)} + open={shareOpen} + onOpenChange={setShareOpen} + /> ) : ( diff --git a/src/renderer/src/components/native-chat/NativeChatMarkdown.visual.test.tsx b/src/renderer/src/components/native-chat/NativeChatMarkdown.visual.test.tsx index 73a233cc7d3..1e71b1963c7 100644 --- a/src/renderer/src/components/native-chat/NativeChatMarkdown.visual.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMarkdown.visual.test.tsx @@ -2,7 +2,7 @@ import '@testing-library/jest-dom/vitest' -import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { cleanup, render, waitFor } from '@testing-library/react' import type { ReactNode } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -53,7 +53,7 @@ beforeEach(() => { vi.stubGlobal('IntersectionObserver', InViewObserver) clearNativeChatVisualCacheForTests() callRuntimeRpc.mockReset() - callRuntimeRpc.mockResolvedValue({ ok: true, revision: 'r1', sizeBytes: 9, html: '

v

' }) + callRuntimeRpc.mockResolvedValue({ ok: true, revision: 'r1', html: '

v

', path: '/v' }) }) afterEach(() => { @@ -78,27 +78,6 @@ describe('NativeChatMarkdown visuals', () => { } }) - it('opens the visual in its own workspace tab', async () => { - const openChatVisualTab = vi.fn() - const original = useAppStore.getState().openChatVisualTab - useAppStore.setState({ openChatVisualTab }) - try { - const { container } = render( - withOwner() - ) - await waitFor(() => expect(container.querySelector('iframe')).not.toBeNull()) - fireEvent.click(screen.getByRole('button', { name: 'Open in new tab' })) - expect(openChatVisualTab).toHaveBeenCalledWith('wt-1', { - target: { kind: 'local' }, - sessionId: 'session-alpha', - file: 'usage.html', - title: 'Usage' - }) - } finally { - useAppStore.setState({ openChatVisualTab: original }) - } - }) - it('holds back an unfinished directive at the end of a streaming reply', () => { const { container } = render( withOwner( diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.new-rows-marker.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.new-rows-marker.test.tsx new file mode 100644 index 00000000000..536d4a35e8f --- /dev/null +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.new-rows-marker.test.tsx @@ -0,0 +1,151 @@ +// @vitest-environment happy-dom + +import '@testing-library/jest-dom/vitest' + +import { act, cleanup, render, screen } from '@testing-library/react' +import { createRef } from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { NativeChatMessage } from '../../../../shared/native-chat-types' +import { NativeChatMessageList } from './NativeChatMessageList' +import type { NativeChatMessageListHandle } from './use-native-chat-reveal-latest' +import { + deliverResizes, + layout, + marker, + ROW_PITCH_PX, + scrollTranscript, + session, + stubLayout, + stubResizeObserver, + TRANSCRIPT_LENGTH +} from './native-chat-windowing-test-harness' + +// Pacing is not this test's subject. +vi.mock('./use-native-chat-paced-text', async (importOriginal) => + (await import('./native-chat-unpaced-text-fixture')).unpacedTextModule(importOriginal) +) + +afterEach(cleanup) + +/** A transcript of rows the journal placed, as a structured chat's are. */ +function chat( + length: number, + isVisible: boolean, + { + ref, + readCurrent = true + }: { ref?: React.Ref; readCurrent?: boolean } = {} +): React.JSX.Element { + const messages: NativeChatMessage[] = Array.from({ length }, (_, index) => ({ + ...marker(index), + journalPosition: { sequence: index + 1, index: 0 } + })) + return ( + + ) +} + +describe('the "New" marker in the transcript', () => { + let paneShown = true + let restore: (() => void)[] = [] + beforeEach(() => { + paneShown = true + vi.spyOn(document, 'hasFocus').mockReturnValue(true) + restore = [ + stubLayout({ scrollGeometry: true, offsetChain: true, isVisible: () => paneShown }), + stubResizeObserver() + ] + }) + afterEach(() => { + for (const undo of restore.toReversed()) { + undo() + } + layout.measuredRowHeights = [] + vi.restoreAllMocks() + }) + + // The window mounts only rows near the view; the marker's row is far from the end it opens at. + it('is drawn above the first row that came while the chat was hidden, however far back', () => { + const { rerender } = render(chat(TRANSCRIPT_LENGTH, true)) + rerender(chat(TRANSCRIPT_LENGTH, false)) + rerender(chat(TRANSCRIPT_LENGTH + 60, true)) + + const row = screen.getByRole('separator', { name: 'New' }).closest('[data-index]') + expect(row?.dataset.index).toBe(String(TRANSCRIPT_LENGTH)) + }) + + it('tells a screen reader that new messages came, in a region that was already there', () => { + const { container, rerender } = render(chat(TRANSCRIPT_LENGTH, true)) + const region = container.querySelector('[data-native-chat-scroll] ~ [aria-live="polite"]') + expect(region).toBeEmptyDOMElement() + + rerender(chat(TRANSCRIPT_LENGTH, false)) + rerender(chat(TRANSCRIPT_LENGTH + 60, true)) + expect(region).toHaveTextContent('New messages arrived while you were away') + }) + + it('goes once the reader sends something', () => { + const ref = createRef() + const { rerender } = render(chat(TRANSCRIPT_LENGTH, true, { ref })) + rerender(chat(TRANSCRIPT_LENGTH, false, { ref })) + rerender(chat(TRANSCRIPT_LENGTH + 60, true, { ref })) + expect(screen.getByRole('separator', { name: 'New' })).toBeInTheDocument() + + act(() => ref.current?.revealLatest()) + + expect(screen.queryByRole('separator', { name: 'New' })).toBeNull() + }) + + describe('when the reader comes back to new rows', () => { + /** The pane is hidden and shown again; the read catches up after the reveal has put the + * reader's place back. */ + function returnToNewRows(rerender: (ui: React.JSX.Element) => void, count = 60): void { + paneShown = false + rerender(chat(TRANSCRIPT_LENGTH, false, { readCurrent: false })) + paneShown = true + rerender(chat(TRANSCRIPT_LENGTH, true, { readCurrent: false })) + rerender(chat(TRANSCRIPT_LENGTH + count, true)) + } + const scrollTop = (container: HTMLElement): number | undefined => + container.querySelector('[data-native-chat-scroll]')?.scrollTop + + it('brings a reader who was at the end to where the new rows begin', () => { + const { container, rerender } = render(chat(TRANSCRIPT_LENGTH, true)) + returnToNewRows(rerender) + + expect(scrollTop(container)).toBe(TRANSCRIPT_LENGTH * ROW_PITCH_PX) + }) + + // A reply is drawn short and revealed over the frames after it arrives. + it('keeps the reader where the new rows begin while a reply is still being drawn', async () => { + const { container, rerender } = render(chat(TRANSCRIPT_LENGTH, true)) + returnToNewRows(rerender, 1) + + const drawn: number[] = [] + drawn[TRANSCRIPT_LENGTH] = 2_000 + layout.measuredRowHeights = drawn + act(() => void deliverResizes()) + await act(async () => { + await new Promise((resolve) => requestAnimationFrame(() => resolve())) + }) + + expect(scrollTop(container)).toBe(TRANSCRIPT_LENGTH * ROW_PITCH_PX) + }) + + it('leaves a reader who was parked further up where they were', () => { + const { container, rerender } = render(chat(TRANSCRIPT_LENGTH, true)) + const readingAt = 2_000 + scrollTranscript(container, readingAt) + returnToNewRows(rerender) + + expect(screen.getByRole('separator', { name: 'New' })).toBeInTheDocument() + expect(scrollTop(container)).toBe(readingAt) + }) + }) +}) diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx index d2a19807321..f4d141ea388 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx @@ -113,6 +113,16 @@ describe('jumping from the rail while following the end', () => { let lastEventScrollTop = 0 beforeEach(() => { + vi.useFakeTimers({ + toFake: [ + 'Date', + 'performance', + 'setTimeout', + 'clearTimeout', + 'requestAnimationFrame', + 'cancelAnimationFrame' + ] + }) const undoLayout = stubLayout({ scrollGeometry: true, offsetChain: true }) const stubbedHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight') restore = [ @@ -169,6 +179,7 @@ describe('jumping from the rail while following the end', () => { }) afterEach(() => { + vi.useRealTimers() vi.restoreAllMocks() Object.defineProperty(layout, 'aboveTranscriptPx', { configurable: true, @@ -202,8 +213,7 @@ describe('jumping from the rail while following the end', () => { fireEvent.scroll(element) } await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)) - await new Promise((resolve) => requestAnimationFrame(() => resolve())) + vi.advanceTimersByTime(16) }) } diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx index b537a0195e3..af3a96b2c4d 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.tsx @@ -3,6 +3,7 @@ import { NATIVE_CHAT_TRANSCRIPT_COLUMN_CLASS } from './native-chat-appearance-style' import { NativeChatJumpToLatest } from './NativeChatJumpToLatest' +import { NativeChatNewRowsAnnouncement } from './NativeChatNewRowsMarker' import { useNativeChatRowTypography } from './use-native-chat-row-typography' import { useCallback, useMemo, useRef, useState } from 'react' import type { CommentMarkdownLinkClickHandler } from '@/components/sidebar/CommentMarkdown' @@ -10,9 +11,8 @@ import { translate } from '@/i18n/i18n' import type { NativeChatLiveSession } from './use-native-chat-live-session' import { useNativeChatTranscriptProjection } from './use-native-chat-transcript-projection' import { structuredQuestionTranscript } from './structured-agent-question-projection' -import { nativeChatTaskListState } from './native-chat-task-list-state' import { nativeChatTaskListPredecessors } from './native-chat-task-list-history' -import { NativeChatTaskList } from './NativeChatTaskList' +import { NativeChatDockedTaskList } from './NativeChatDockedTaskList' import { useNativeChatTurnStatus } from './use-native-chat-turn-status' import { NativeChatAwaitingInputRow } from './NativeChatAwaitingInputRow' import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client-types' @@ -38,6 +38,7 @@ import { useNativeChatSubagentSections } from './use-native-chat-subagent-sectio import { toggleNativeChatExpandedKey } from './native-chat-expanded-keys' import { useNativeChatTurnMembership } from './use-native-chat-turn-membership' import { useNativeChatTranscriptScroll } from './use-native-chat-transcript-scroll' +import { useNativeChatNewRowsMarker } from './native-chat-new-rows-marker' import { useNativeChatOlderHistoryAutoload } from './use-native-chat-older-history-autoload' import { NativeChatOlderHistoryRow } from './NativeChatOlderHistoryRow' import { useNativeChatMessageRail } from './use-native-chat-message-rail' @@ -163,7 +164,6 @@ export function NativeChatMessageList({ [messages, subagentSections] ) const taskListPredecessors = useMemo(() => nativeChatTaskListPredecessors(messages), [messages]) - const taskListState = useMemo(() => nativeChatTaskListState(messages), [messages]) // Each row's turn, which turn is live, and the order the rows draw in, resolved once. const turnRows = useNativeChatTurnMembership( messages, @@ -227,6 +227,11 @@ export function NativeChatMessageList({ ), [allSlots, journalItems, stopping, journalSubmissions] ) + const { marker: newRowsMarker, dismiss } = useNativeChatNewRowsMarker( + slots, + waitingSlots, + isVisible && session.readCurrent === true + ) const transcriptWindow = useNativeChatTranscriptWindow({ scrollRef, slots, @@ -234,6 +239,7 @@ export function NativeChatMessageList({ // One pin serves both: revealing a diff and jumping from the rail are // mutually exclusive things to be doing. revealIndex: nativeChatSlotIndexOf(slots, railJump?.messageId ?? revealedDiff?.messageId), + newRowsMarkerIndex: nativeChatSlotIndexOf(slots, newRowsMarker?.key), typography }) const { showJump, onScroll, scrollToBottom, scrollMessageToTop, readerLeavesEnd } = @@ -321,7 +327,7 @@ export function NativeChatMessageList({ onTakeScroll: transcriptWindow.cancelAlign, onLeaveEnd: readerLeavesEnd }) - useNativeChatMessageListHandle(ref, jumpToLatest, readerScrollInput.revealFindMatch) + useNativeChatMessageListHandle(ref, jumpToLatest, readerScrollInput.revealFindMatch, dismiss) useNativeChatRailJumpLanding({ railJump, slots, @@ -398,6 +404,7 @@ export function NativeChatMessageList({ loadedMessageIds={loadedMessageIds} context={rowContext} window={transcriptWindow} + newRowsMarker={newRowsMarker} /> {liveLine ? ( + - {taskListState.list && taskListState.list.tasks.length > 0 ? ( -
-
- -
-
- ) : null} + ) diff --git a/src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx b/src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx new file mode 100644 index 00000000000..772c5a973c4 --- /dev/null +++ b/src/renderer/src/components/native-chat/NativeChatNewRowsMarker.test.tsx @@ -0,0 +1,109 @@ +// @vitest-environment happy-dom + +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { NativeChatNewRowsMarker } from './NativeChatNewRowsMarker' +import type { NativeChatNewRowsMarkerState as Marker } from './native-chat-new-rows-marker' +import { overrideLayoutProperty } from './native-chat-windowing-test-harness' + +afterEach(cleanup) + +function marker(overrides: Partial = {}): Marker { + return { + key: 'answer', + place: 'above-row', + arriving: true, + onArrived: vi.fn(), + onDismiss: vi.fn(), + ...overrides + } +} + +const VIEWPORT_PX = 600 +const ROW_TOP_PX = 1_000 + +let restoreLayout: (() => void)[] = [] + +/** A chat pane whose marker sits `intoRowPx` down its row. The view's bottom edge rests + * `aboveMarkerPx` short of the marker: 0 for a reader who was at the end of what there was. */ +function pane( + value: Marker, + { intoRowPx = 0, aboveMarkerPx = 0 }: { intoRowPx?: number; aboveMarkerPx?: number } = {} +) { + const isRow = (element: HTMLElement): boolean => element.hasAttribute('data-index') + const isMarker = (element: HTMLElement): boolean => element.getAttribute('role') === 'separator' + const layout = (name: string, get: (this: HTMLElement) => unknown) => + overrideLayoutProperty(name, { get }) + restoreLayout = [ + layout('clientHeight', () => VIEWPORT_PX), + layout('scrollTop', () => ROW_TOP_PX + intoRowPx - VIEWPORT_PX - aboveMarkerPx), + layout('offsetTop', function () { + return isRow(this) ? ROW_TOP_PX : isMarker(this) ? intoRowPx : 0 + }), + layout('offsetParent', function () { + return isRow(this) || isMarker(this) ? this.parentElement : null + }) + ] + const onReveal = vi.fn() + const view = render( +
+
+ +
+
+
+ +
+
+