diff --git a/.github/actions/prepare-native-runtime/action.yml b/.github/actions/prepare-native-runtime/action.yml index 64712b2aae7..76f9954b7e2 100644 --- a/.github/actions/prepare-native-runtime/action.yml +++ b/.github/actions/prepare-native-runtime/action.yml @@ -54,7 +54,7 @@ runs: env: NATIVE_RUNTIME: ${{ inputs.native-runtime }} NODE_VERSION: ${{ inputs.node-version }} - NATIVE_SOURCE_HASH: ${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc', '.pnpmfile.cjs', '.github/actions/prepare-native-runtime/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/rebuild-native-deps.mjs', 'config/scripts/node-pty-job-ownership.cjs', 'config/scripts/windows-pe-machine.cjs', 'config/scripts/windows-process-tree-gyp-rebuild.mjs', 'config/scripts/windows-process-tree-creation-time.cjs', 'config/scripts/install-electron-package-binary.mjs', 'config/scripts/electron-platform-path.mjs', 'config/scripts/zip-extractor-command.mjs', 'src/shared/zip-extractor-command.ts', 'config/scripts/shared-electron-dist-cache.mjs', 'config/scripts/space-sharing-copy.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json', 'native/windows-registry/index.js', 'native/windows-security/src/addon.cc', 'native/windows-security/binding.gyp', 'native/windows-security/package.json', 'native/windows-security/index.js') }} + NATIVE_SOURCE_HASH: ${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', '.npmrc', '.pnpmfile.cjs', '.github/actions/prepare-native-runtime/action.yml', 'config/scripts/ensure-native-runtime.mjs', 'config/scripts/initialize-windows-native-toolchain.mjs', 'config/scripts/capture-windows-native-environment.cmd', 'config/scripts/rebuild-native-deps.mjs', 'config/scripts/node-pty-job-ownership.cjs', 'config/scripts/windows-pe-machine.cjs', 'config/scripts/windows-process-tree-gyp-rebuild.mjs', 'config/scripts/windows-process-tree-creation-time.cjs', 'config/scripts/install-electron-package-binary.mjs', 'config/scripts/electron-platform-path.mjs', 'config/scripts/zip-extractor-command.mjs', 'src/shared/zip-extractor-command.ts', 'config/scripts/shared-electron-dist-cache.mjs', 'config/scripts/space-sharing-copy.mjs', 'config/patches/node-pty@1.1.0.patch', 'config/patches/@vscode__windows-process-tree@0.8.0.patch', 'native/windows-registry/src/addon.cc', 'native/windows-registry/binding.gyp', 'native/windows-registry/package.json', 'native/windows-registry/index.js', 'native/windows-security/src/addon.cc', 'native/windows-security/binding.gyp', 'native/windows-security/package.json', 'native/windows-security/index.js') }} run: | scope="${ImageOS:-$RUNNER_OS}" if [ -r /etc/os-release ]; then @@ -120,4 +120,9 @@ runs: shell: bash env: NATIVE_RUNTIME: ${{ inputs.native-runtime }} - run: node config/scripts/ensure-native-runtime.mjs --runtime="$NATIVE_RUNTIME" + run: | + if [ "$RUNNER_OS" = Windows ] && [ "$NATIVE_RUNTIME" = node ]; then + node config/scripts/initialize-windows-native-toolchain.mjs + else + node config/scripts/ensure-native-runtime.mjs --runtime="$NATIVE_RUNTIME" + fi 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..07fb93918b8 100644 --- a/.github/workflows/ci-cache-warmup-platform-jobs.yml +++ b/.github/workflows/ci-cache-warmup-platform-jobs.yml @@ -53,6 +53,37 @@ jobs: - name: Verify native cache is usable run: node config/scripts/ensure-native-runtime.mjs --check-only + - name: Cache Windows CLI launcher + if: runner.arch == 'X64' + 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 + if: runner.arch == 'X64' + 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 } + + - name: Warm the Windows ARM server launcher + if: runner.arch == 'ARM64' + uses: ./.github/actions/prepare-windows-cli-launcher + with: + arches: arm64 + 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..e8f37a09c78 100644 --- a/.github/workflows/ci-cache-warmup-platforms.yml +++ b/.github/workflows/ci-cache-warmup-platforms.yml @@ -10,6 +10,7 @@ on: - '.github/actions/install-node-dependencies/**' - '.github/actions/restore-pnpm-verification/**' - '.github/actions/prepare-native-runtime/**' + - '.github/actions/prepare-windows-cli-launcher/**' - '.github/actions/prepare-git-compatibility/**' - '.github/actions/prepare-linux-package-fixture/**' - 'config/docker/headless-serve-shutdown/**' @@ -21,6 +22,8 @@ on: - '.npmrc' - '.pnpmfile.cjs' - 'config/scripts/ensure-native-runtime.mjs' + - 'config/scripts/initialize-windows-native-toolchain.mjs' + - 'config/scripts/capture-windows-native-environment.cmd' - 'config/scripts/rebuild-native-deps.mjs' - 'config/scripts/node-pty-job-ownership.cjs' - 'config/scripts/windows-pe-machine.cjs' @@ -34,6 +37,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..8487d95ef34 100644 --- a/.github/workflows/ci-cache-warmup.yml +++ b/.github/workflows/ci-cache-warmup.yml @@ -14,10 +14,14 @@ on: - '.github/actions/install-node-dependencies/**' - '.github/actions/restore-pnpm-verification/**' - '.github/actions/prepare-native-runtime/**' + - '.github/actions/prepare-windows-cli-launcher/**' - '.github/actions/prepare-git-compatibility/**' - '.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/**' @@ -27,6 +31,8 @@ on: - '.npmrc' - '.pnpmfile.cjs' - 'config/scripts/ensure-native-runtime.mjs' + - 'config/scripts/initialize-windows-native-toolchain.mjs' + - 'config/scripts/capture-windows-native-environment.cmd' - 'config/scripts/rebuild-native-deps.mjs' - 'config/scripts/node-pty-job-ownership.cjs' - 'config/scripts/windows-pe-machine.cjs' @@ -48,6 +54,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 +90,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-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-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 ca5c9079cce..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 @@ -1103,6 +1105,12 @@ jobs: 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 id: shutdown-fixture-cache @@ -1157,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/ssh-windows-hosts.yml b/.github/workflows/ssh-windows-hosts.yml index 4a3b910c620..4b5bb1ce5f2 100644 --- a/.github/workflows/ssh-windows-hosts.yml +++ b/.github/workflows/ssh-windows-hosts.yml @@ -62,12 +62,23 @@ on: description: Comma-separated cell ids from src/main/ssh/ssh-windows-host-cells.ts; empty runs the default set (orcad-cli-* cells run only when named). required: false default: '' + preparation_mode: + description: Compare inbox capability preparation overlapping builds or running after them. + type: choice + options: [overlap, serial] + required: false + default: overlap + arm_inbox_only: + description: Run only the native ARM inbox lane for a preparation timing comparison. + type: boolean + required: false + default: false permissions: contents: read concurrency: - group: ssh-windows-hosts-${{ github.event.pull_request.number || github.ref }} + group: ssh-windows-hosts-${{ github.event.pull_request.number || github.ref }}${{ github.event_name == 'workflow_dispatch' && inputs.arm_inbox_only && format('-{0}', inputs.preparation_mode || 'overlap') || '' }} cancel-in-progress: true jobs: @@ -77,21 +88,10 @@ jobs: strategy: fail-fast: false matrix: - include: - - arch: x64 - runner: windows-2022 - server: inbox - - arch: x64 - runner: windows-2022 - server: preview - archive: OpenSSH-Win64.zip - - arch: arm64 - runner: windows-11-arm - server: inbox - - arch: arm64 - runner: windows-11-arm - server: preview - archive: OpenSSH-ARM64.zip + include: >- + ${{ fromJSON(github.event_name == 'workflow_dispatch' && inputs.arm_inbox_only && + '[{"arch":"arm64","runner":"windows-11-arm","server":"inbox"}]' || + '[{"arch":"x64","runner":"windows-2022","server":"inbox"},{"arch":"x64","runner":"windows-2022","server":"preview","archive":"OpenSSH-Win64.zip"},{"arch":"arm64","runner":"windows-11-arm","server":"inbox"},{"arch":"arm64","runner":"windows-11-arm","server":"preview","archive":"OpenSSH-ARM64.zip"}]') }} runs-on: ${{ matrix.runner }} # Installing the inbox capability alone can take several minutes on a fresh image; the CLI matrix # cells launch the e2e app several times each. @@ -99,6 +99,7 @@ jobs: env: ORCA_BACKGROUND_LAUNCH: '1' ORCA_ISOLATED_SSH_CI: '1' + INBOX_PREPARATION_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.preparation_mode == 'serial' && 'serial' || 'overlap' }} steps: - uses: actions/checkout@v6 with: @@ -127,11 +128,11 @@ jobs: background: true shell: pwsh run: | - if('${{ matrix.server }}' -eq 'inbox' -and '${{ matrix.arch }}' -eq 'arm64'){ + if('${{ matrix.server }}' -eq 'inbox' -and '${{ matrix.arch }}' -eq 'arm64' -and $env:INBOX_PREPARATION_MODE -eq 'overlap'){ $receipts=Join-Path $pwd '.build/ssh-windows-host-receipts' New-Item -ItemType Directory -Force -Path $receipts | Out-Null . config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1 - Initialize-WindowsInboxSshCapability -Arch '${{ matrix.arch }}' -Receipt (Join-Path $receipts 'inbox-capability-preparation.json') + Initialize-WindowsInboxSshCapability -Arch '${{ matrix.arch }}' -Receipt (Join-Path $receipts 'inbox-capability-preparation.json') -PreparationMode $env:INBOX_PREPARATION_MODE } - uses: ./.github/actions/install-node-dependencies id: dependencies @@ -164,6 +165,14 @@ jobs: pnpm build:orcad-prebuilds --require-slots "win32-${{ matrix.arch }}" node config/scripts/build-orcad-template.mjs --targets "win32-${{ matrix.arch }}" - wait: inbox-capability + - name: Prepare the Windows inbox SSH capability after the builds + if: ${{ matrix.server == 'inbox' && matrix.arch == 'arm64' && env.INBOX_PREPARATION_MODE == 'serial' }} + shell: pwsh + run: | + $receipts=Join-Path $pwd '.build/ssh-windows-host-receipts' + New-Item -ItemType Directory -Force -Path $receipts | Out-Null + . config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1 + Initialize-WindowsInboxSshCapability -Arch '${{ matrix.arch }}' -Receipt (Join-Path $receipts 'inbox-capability-preparation.json') -PreparationMode $env:INBOX_PREPARATION_MODE - name: Run the Windows host cells against a private ${{ matrix.server }} sshd shell: pwsh timeout-minutes: ${{ contains(github.event.inputs.cells || '', 'orcad-cli') && 130 || 50 }} 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/.github/workflows/windows-native-preparation-benchmark.yml b/.github/workflows/windows-native-preparation-benchmark.yml new file mode 100644 index 00000000000..477fff42c56 --- /dev/null +++ b/.github/workflows/windows-native-preparation-benchmark.yml @@ -0,0 +1,64 @@ +name: Windows native preparation comparison + +on: + workflow_dispatch: + inputs: + source_sha: + description: Exact commit containing this benchmark and the Windows compiler candidate. + required: true + type: string + +permissions: + contents: read + +concurrency: + group: windows-native-preparation-comparison-${{ github.ref }} + cancel-in-progress: false + +env: + ORCA_BACKGROUND_LAUNCH: '1' + +jobs: + compare: + name: Native preparation comparison (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + strategy: + fail-fast: false + max-parallel: 2 + matrix: + os: [windows-2022, windows-11-arm] + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.source_sha }} + persist-credentials: false + - name: Record measurement start and validate source + shell: bash + env: + SOURCE_SHA: ${{ inputs.source_sha }} + run: | + [[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]] + test "$(git rev-parse HEAD)" = "$SOURCE_SHA" + mkdir -p .build/windows-native-benchmark + node -e 'require("node:fs").writeFileSync(".build/windows-native-benchmark/job-start.json", JSON.stringify({startedAt:new Date().toISOString()}))' + - uses: ./.github/actions/install-node-dependencies + with: + node-version: '24.21.0' + native-runtime: none + cache-pnpm-store: 'false' + cache-pnpm-verification: 'false' + - name: Compare isolated cold and same-build warm preparation + shell: bash + env: + SOURCE_SHA: ${{ inputs.source_sha }} + BENCHMARK_RUNNER: ${{ matrix.os }} + run: node config/scripts/benchmark-windows-native-preparation.mjs + - uses: actions/upload-artifact@v7 + if: always() + with: + name: windows-native-preparation-${{ matrix.os }}-${{ github.run_id }}-${{ github.run_attempt }} + path: .build/windows-native-benchmark/ + include-hidden-files: true + if-no-files-found: error + retention-days: 14 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-registry.ts b/cloud/apps/relay/src/host-session-registry.ts index 1df66ae5edb..4bbbf896830 100644 --- a/cloud/apps/relay/src/host-session-registry.ts +++ b/cloud/apps/relay/src/host-session-registry.ts @@ -719,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) @@ -735,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) 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 a1ec6b5030f..8e0e209cdf8 100644 --- a/cloud/apps/relay/src/relay-observability.test.ts +++ b/cloud/apps/relay/src/relay-observability.test.ts @@ -8,6 +8,7 @@ import { PRE_AUTH_RESERVOIR_LIMIT, percentile, RelayObservability, + STICKY_CONNECTION_LIMIT, type RelayProcessCounts } from './relay-observability.js' import { RELAY_FIX_LEVEL } from './relay-fix-level.js' @@ -284,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( diff --git a/cloud/apps/relay/src/relay-observability.ts b/cloud/apps/relay/src/relay-observability.ts index d56b6ae84a1..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 @@ -184,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 @@ -209,6 +217,18 @@ type RelayMetricDeltas = { 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 @@ -254,6 +274,12 @@ const emptyDeltas = (): RelayMetricDeltas => ({ drainReturnRetryAfterSecondsSum: 0, drainReturnServiceMs: [], stickyServiceMs: [], + stickySlotQueueWaitMs: [], + stickySlotPoolWaitMs: [], + stickySlotSqlMs: [], + stickySlotStatements: [], + stickySlotByConnection: new Map(), + stickySlotConnectionSamplesDropped: 0, assign503sByCause: {}, dbLockWaitSamples: 0, dbLockWaitersByKey: {}, @@ -392,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) } @@ -596,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') @@ -718,6 +775,42 @@ function preAuthLatencyFields( } } +// 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-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 cacfd7b7bda..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"' 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/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 28ea41dc79e..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. @@ -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/ci/windows-ssh-provider/preview-ssh/prove-preview-openssh.ps1 b/config/ci/windows-ssh-provider/preview-ssh/prove-preview-openssh.ps1 index 5b86447c109..56961bbd78c 100644 --- a/config/ci/windows-ssh-provider/preview-ssh/prove-preview-openssh.ps1 +++ b/config/ci/windows-ssh-provider/preview-ssh/prove-preview-openssh.ps1 @@ -168,7 +168,7 @@ try { $report.nativeInputs=$verified Write-Stage 'preview-native-input-verification-complete' } else { - Install-WindowsInboxSshCapability $Arch $report {param($stage) Write-Stage $stage} + Install-WindowsInboxSshCapability $Arch $report {param($stage) Write-Stage $stage} ([IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($Receipt))) $verified=@() foreach($binary in @('sshd.exe','ssh.exe','ssh-keygen.exe','sftp.exe','sftp-server.exe')){ $path=Join-Path $sshDir $binary diff --git a/config/ci/windows-ssh-provider/preview-ssh/test-preview-diagnostics.ps1 b/config/ci/windows-ssh-provider/preview-ssh/test-preview-diagnostics.ps1 index 088534f7be5..9de1a4765d0 100644 --- a/config/ci/windows-ssh-provider/preview-ssh/test-preview-diagnostics.ps1 +++ b/config/ci/windows-ssh-provider/preview-ssh/test-preview-diagnostics.ps1 @@ -38,22 +38,52 @@ try { function Assert-IsolatedWindowsSshCi([string]$Arch){if($script:refuseCapabilityHost){throw 'Injected host refusal'}} function Assert-WindowsSshGlobalServerDormant([string]$Server){$script:globalChecks++;if($script:globalChecks -eq $script:refuseGlobalCheck){throw 'Injected global server refusal'}} function Assert-WindowsSshStockShell {$script:shellChecks++;if($script:shellChecks -eq $script:refuseShellCheck){throw 'Injected shell refusal'}} -function Get-WindowsCapability([switch]$Online,[string]$Name){$script:capabilityQueries++;return @{State=$script:capabilityState}} -function Add-WindowsCapability([switch]$Online,[string]$Name){$script:capabilityAdds++;if($script:failCapabilityInstall){throw 'Injected capability install failure'}} +function Assert-CapabilityCommand([switch]$Online,[string]$Name,[string]$LogPath,[object]$ErrorAction,[string]$Operation){ + if(-not $Online -or $Name -ne 'OpenSSH.Server~~~~0.0.1.0' -or $ErrorAction -ne 'Stop' -or [IO.Path]::GetDirectoryName($LogPath) -ne $script:capabilityLogs -or [IO.Path]::GetFileName($LogPath) -notlike "inbox-capability-$Operation-*.log"){throw 'Capability command changed its online identity, error handling or receipt log boundary'} + $script:capabilityLogPaths.Add($LogPath) + [IO.File]::WriteAllText($LogPath,"$Operation fixture") +} +function Get-WindowsCapability([switch]$Online,[string]$Name,[string]$LogPath,[object]$ErrorAction){ + $script:capabilityQueries++;Assert-CapabilityCommand -Online:$Online -Name $Name -LogPath $LogPath -ErrorAction $ErrorAction -Operation 'query' + if($script:failCapabilityQuery){Write-Error 'Injected capability query failure' -ErrorAction $ErrorAction} + return @{State=$script:capabilityState} +} +function Add-WindowsCapability([switch]$Online,[string]$Name,[string]$LogPath,[object]$ErrorAction){ + $script:capabilityAdds++;Assert-CapabilityCommand -Online:$Online -Name $Name -LogPath $LogPath -ErrorAction $ErrorAction -Operation 'install' + if($script:activeCapabilityReceipt){ + $script:installObservedReceipt=Get-Content -LiteralPath $script:activeCapabilityReceipt -Raw | ConvertFrom-Json + if($script:installObservedReceipt.inboxCapabilityInitialState -ne 'NotPresent' -or $script:installObservedReceipt.inboxCapabilityOperations[0].status -ne 'passed' -or $script:installObservedReceipt.inboxCapabilityOperations[1].status -ne 'running'){throw 'Initial capability state was not durably recorded before installation'} + } + if($script:failCapabilityInstall){Write-Error 'Injected capability install failure' -ErrorAction $ErrorAction} + return @{Path='C:\';Online=$true;RestartNeeded=$script:capabilityRestartNeeded} +} function Reset-CapabilityControl([string]$State){ $script:capabilityState=$State;$script:capabilityQueries=0;$script:capabilityAdds=0 $script:globalChecks=0;$script:shellChecks=0;$script:refuseGlobalCheck=0;$script:refuseShellCheck=0 - $script:refuseCapabilityHost=$false;$script:failCapabilityInstall=$false + $script:refuseCapabilityHost=$false;$script:failCapabilityInstall=$false;$script:failCapabilityQuery=$false + $script:activeCapabilityReceipt=$null;$script:installObservedReceipt=$null;$script:capabilityRestartNeeded=$false $script:capabilityStages=[Collections.Generic.List[string]]::new() + $script:capabilityLogPaths=[Collections.Generic.List[string]]::new() } +$script:capabilityLogs=Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString('N')) +try { foreach($state in @('Installed','NotPresent')){ Reset-CapabilityControl $state $capabilityReport=@{} - Install-WindowsInboxSshCapability 'x64' $capabilityReport {param($name) $script:capabilityStages.Add($name)} + Install-WindowsInboxSshCapability 'x64' $capabilityReport {param($name) $script:capabilityStages.Add($name)} $script:capabilityLogs $expectedAdds=if($state -eq 'Installed'){0}else{1} - if($capabilityReport.inboxCapabilityInitialState -ne $state -or $script:capabilityAdds -ne $expectedAdds -or $script:globalChecks -ne 2 -or $script:shellChecks -ne 2 -or ($script:capabilityStages -join ',') -ne 'inbox-capability-start,inbox-capability-complete'){throw 'Shared capability preparation did not preserve the install and guard boundaries'} + $expectedStages=@('inbox-capability-start','inbox-capability-query-start','inbox-capability-query-finished') + if($expectedAdds){$expectedStages+=@('inbox-capability-install-start','inbox-capability-install-finished')} + $expectedStages+=@('inbox-capability-guards-start','inbox-capability-complete') + if($capabilityReport.inboxCapabilityInitialState -ne $state -or $script:capabilityQueries -ne 1 -or $script:capabilityAdds -ne $expectedAdds -or $script:globalChecks -ne 2 -or $script:shellChecks -ne 2 -or ($script:capabilityStages -join ',') -ne ($expectedStages -join ',')){throw 'Shared capability preparation did not preserve the install and guard boundaries'} + if($capabilityReport.inboxCapabilityOperations.Count -ne 1+$expectedAdds){throw 'Capability operations missing'} + foreach($operation in $capabilityReport.inboxCapabilityOperations){ + if($operation.status -ne 'passed' -or $operation.elapsedMs -isnot [long] -or $operation.elapsedMs -lt 0 -or -not [IO.File]::Exists((Join-Path $script:capabilityLogs $operation.log))){throw 'Capability operation did not retain its timing, verdict or diagnostic log'} + } + if($expectedAdds -and ($capabilityReport.inboxCapabilityInstallResult.online -ne $true -or $capabilityReport.inboxCapabilityInstallResult.path -ne 'C:\' -or $capabilityReport.inboxCapabilityInstallResult.restartNeeded -ne $false)){throw 'Capability install result omitted'} + if(-not $expectedAdds -and $capabilityReport.ContainsKey('inboxCapabilityInstallResult')){throw 'Installed capability invented an install result'} } -foreach($fault in @('host','global-before','shell-before','global-after','shell-after','install')){ +foreach($fault in @('host','global-before','shell-before','global-after','shell-after','query','install')){ Reset-CapabilityControl 'NotPresent' switch($fault){ 'host' {$script:refuseCapabilityHost=$true} @@ -61,26 +91,45 @@ foreach($fault in @('host','global-before','shell-before','global-after','shell- 'shell-before' {$script:refuseShellCheck=1} 'global-after' {$script:refuseGlobalCheck=2} 'shell-after' {$script:refuseShellCheck=2} + 'query' {$script:failCapabilityQuery=$true} 'install' {$script:failCapabilityInstall=$true} } $rejected=$false - try {Install-WindowsInboxSshCapability 'x64' @{} {param($name) $script:capabilityStages.Add($name)}} catch {$rejected=$true} + $faultReport=@{} + try {Install-WindowsInboxSshCapability 'x64' $faultReport {param($name) $script:capabilityStages.Add($name)} $script:capabilityLogs} catch {$rejected=$true} if(-not $rejected -or $script:capabilityStages.Contains('inbox-capability-complete')){throw "Capability fault did not fail closed: $fault"} if($fault -in @('host','global-before','shell-before') -and $script:capabilityAdds){throw 'Capability mutation preceded its host guards'} + if($fault -eq 'query' -and $script:capabilityAdds){throw 'Failed query proceeded to installation'} + if($fault -in @('query','install') -and ($faultReport.inboxCapabilityOperations[-1].status -ne 'failed' -or $faultReport.inboxCapabilityOperations[-1].elapsedMs -isnot [long])){throw 'Failed servicing operation lost its verdict or duration'} } +Reset-CapabilityControl 'NotPresent' +foreach($invocation in 1..2){Install-WindowsInboxSshCapability 'x64' @{} {param($name) $script:capabilityStages.Add($name)} $script:capabilityLogs} +if($script:capabilityLogPaths.Count -ne 4 -or ($script:capabilityLogPaths | Select-Object -Unique).Count -ne 4){throw 'Separate servicing operations or invocations reused a diagnostic log'} +} finally {Remove-Item -LiteralPath $script:capabilityLogs -Recurse -Force -ErrorAction SilentlyContinue} $capabilityRoot=Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString('N')) try { New-Item -ItemType Directory -Path $capabilityRoot | Out-Null $capabilityReceipt=Join-Path $capabilityRoot 'capability.json' + $script:capabilityLogs=$capabilityRoot Reset-CapabilityControl 'Installed' Initialize-WindowsInboxSshCapability 'x64' $capabilityReceipt $completed=Get-Content -LiteralPath $capabilityReceipt -Raw | ConvertFrom-Json - if($completed.status -ne 'passed' -or $completed.inboxCapabilityInitialState -ne 'Installed'){throw 'Capability success receipt missing its observed initial state'} - Reset-CapabilityControl 'NotPresent';$script:failCapabilityInstall=$true + if($completed.status -ne 'passed' -or $completed.inboxCapabilityInitialState -ne 'Installed' -or $completed.preparationMode -ne 'overlap'){throw 'Capability success receipt missing its observed initial state or default mode'} + Reset-CapabilityControl 'NotPresent';$script:capabilityLogs=$capabilityRoot;$script:activeCapabilityReceipt=$capabilityReceipt;$script:capabilityRestartNeeded=$true + Initialize-WindowsInboxSshCapability 'x64' $capabilityReceipt 'serial' + $installed=Get-Content -LiteralPath $capabilityReceipt -Raw | ConvertFrom-Json + if($installed.status -ne 'passed' -or $installed.preparationMode -ne 'serial' -or $installed.inboxCapabilityInstallResult.restartNeeded -ne $true -or -not $script:installObservedReceipt){throw 'Serial preparation lost its mode, restart result or pre-install receipt'} + Reset-CapabilityControl 'NotPresent';$script:activeCapabilityReceipt=$capabilityReceipt;$script:failCapabilityInstall=$true $rejected=$false try {Initialize-WindowsInboxSshCapability 'x64' $capabilityReceipt} catch {$rejected=$true} $failed=Get-Content -LiteralPath $capabilityReceipt -Raw | ConvertFrom-Json if(-not $rejected -or $failed.status -ne 'failed' -or $failed.inboxCapabilityInitialState -ne 'NotPresent' -or $failed.error -ne 'Injected capability install failure'){throw 'Failed capability preparation masqueraded as a passing receipt'} + if($failed.inboxCapabilityOperations[-1].status -ne 'failed' -or -not $failed.stages.stage.Contains('inbox-capability-install-finished')){throw 'Failed preparation omitted its servicing result'} + Reset-CapabilityControl 'NotPresent';$script:failCapabilityQuery=$true + $rejected=$false + try {Initialize-WindowsInboxSshCapability 'x64' $capabilityReceipt} catch {$rejected=$true} + $failedQuery=Get-Content -LiteralPath $capabilityReceipt -Raw | ConvertFrom-Json + if(-not $rejected -or $failedQuery.status -ne 'failed' -or $failedQuery.inboxCapabilityOperations[0].status -ne 'failed' -or $failedQuery.inboxCapabilityOperations.Count -ne 1 -or $script:capabilityAdds -or $failedQuery.inboxCapabilityInitialState){throw 'Failed query receipt invented a capability state or proceeded to installation'} } finally {Remove-Item -LiteralPath $capabilityRoot -Recurse -Force -ErrorAction SilentlyContinue} & { param($ProofAst) diff --git a/config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1 b/config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1 index d2bdc9096a3..df0bbaa707e 100644 --- a/config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1 +++ b/config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1 @@ -33,26 +33,48 @@ function Write-WindowsSshReceiptStage([hashtable]$Report,[string]$Receipt,[strin return $written } -function Install-WindowsInboxSshCapability([ValidateSet('arm64','x64')][string]$Arch,[hashtable]$Report,[scriptblock]$Stage) { +function Install-WindowsInboxSshCapability([ValidateSet('arm64','x64')][string]$Arch,[hashtable]$Report,[scriptblock]$Stage,[Parameter(Mandatory=$true)][string]$LogDirectory) { Assert-IsolatedWindowsSshCi $Arch Assert-WindowsSshGlobalServerDormant 'inbox' Assert-WindowsSshStockShell & $Stage 'inbox-capability-start' - $capability=Get-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0' - $Report.inboxCapabilityInitialState=[string]$capability.State - if($capability.State -ne 'Installed'){Add-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0' | Out-Null} + New-Item -ItemType Directory -Force -Path $LogDirectory | Out-Null + $invocation=[Guid]::NewGuid().ToString('N') + $query=@{operation='query';status='running';log="inbox-capability-query-$invocation.log"} + $Report.inboxCapabilityOperations=@($query) + & $Stage 'inbox-capability-query-start' + $watch=[Diagnostics.Stopwatch]::StartNew() + try { + $capability=Get-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0' -LogPath (Join-Path $LogDirectory $query.log) -ErrorAction Stop + $Report.inboxCapabilityInitialState=[string]$capability.State + $query.status='passed' + } catch {$query.status='failed';$query.error=$_.Exception.Message;throw} + finally {$watch.Stop();$query.elapsedMs=$watch.ElapsedMilliseconds;& $Stage 'inbox-capability-query-finished'} + if($capability.State -ne 'Installed'){ + $install=@{operation='install';status='running';log="inbox-capability-install-$invocation.log"} + $Report.inboxCapabilityOperations+=@($install) + & $Stage 'inbox-capability-install-start' + $watch=[Diagnostics.Stopwatch]::StartNew() + try { + $result=Add-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0' -LogPath (Join-Path $LogDirectory $install.log) -ErrorAction Stop + $Report.inboxCapabilityInstallResult=@{path=$result.Path;online=$result.Online;restartNeeded=$result.RestartNeeded} + $install.status='passed' + } catch {$install.status='failed';$install.error=$_.Exception.Message;throw} + finally {$watch.Stop();$install.elapsedMs=$watch.ElapsedMilliseconds;& $Stage 'inbox-capability-install-finished'} + } + & $Stage 'inbox-capability-guards-start' Assert-WindowsSshGlobalServerDormant 'inbox' Assert-WindowsSshStockShell & $Stage 'inbox-capability-complete' } -function Initialize-WindowsInboxSshCapability([ValidateSet('arm64','x64')][string]$Arch,[string]$Receipt) { - $report=@{scope='Windows inbox OpenSSH.Server capability preparation only';arch=$Arch;sourceSha=$env:GITHUB_SHA;runId=$env:GITHUB_RUN_ID;runAttempt=$env:GITHUB_RUN_ATTEMPT;runnerName=$env:RUNNER_NAME;imageVersion=$env:ImageVersion;status='running';stages=@();globalBootstrapCleanup='Disposable CI VM destruction is the boundary; no global sshd is started'} +function Initialize-WindowsInboxSshCapability([ValidateSet('arm64','x64')][string]$Arch,[string]$Receipt,[ValidateSet('overlap','serial')][string]$PreparationMode='overlap') { + $report=@{scope='Windows inbox OpenSSH.Server capability preparation only';arch=$Arch;sourceSha=$env:GITHUB_SHA;runId=$env:GITHUB_RUN_ID;runAttempt=$env:GITHUB_RUN_ATTEMPT;runnerName=$env:RUNNER_NAME;imageVersion=$env:ImageVersion;preparationMode=$PreparationMode;status='running';stages=@();globalBootstrapCleanup='Disposable CI VM destruction is the boundary; no global sshd is started'} try { if(-not (Write-WindowsSshReceiptStage $report $Receipt 'preparation-start')){throw 'Initial progress receipt unavailable; refuse provisioning'} Install-WindowsInboxSshCapability $Arch $report {param($name) if(-not (Write-WindowsSshReceiptStage $report $Receipt $name)){throw 'Capability preparation progress receipt unavailable'} - } + } ([IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($Receipt))) $report.status='passed' } catch {$report.status='failed';$report.error=$_.Exception.Message;throw} finally { 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 225a2c2f0ab..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 @@ -81,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 @@ -95,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/benchmark-windows-native-preparation.mjs b/config/scripts/benchmark-windows-native-preparation.mjs new file mode 100644 index 00000000000..336e6fbbd26 --- /dev/null +++ b/config/scripts/benchmark-windows-native-preparation.mjs @@ -0,0 +1,518 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + realpathSync, + rmSync, + statSync, + writeFileSync +} from 'node:fs' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { dirname, join, relative, resolve, sep } from 'node:path' +import { performance } from 'node:perf_hooks' +import { pathToFileURL } from 'node:url' +import { runProcess } from '@orca/process-host' +import { buildCounterbalancedSchedule } from './counterbalanced-benchmark-schedule.mjs' +import { summarizeBenchmarkSamples } from './benchmark-sample-summary.mjs' +import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs' +import { hasWindowsCompilerSelection } from './initialize-windows-native-toolchain.mjs' +import { + compilerEnvironmentKey, + environmentReceipt, + hash +} from './windows-native-benchmark-observer.mjs' + +const addonNames = [ + 'node-pty', + '@orca/windows-registry', + '@orca/windows-security', + '@vscode/windows-process-tree' +] +const candidate = 'initialize-windows-native-toolchain.mjs' +const control = 'ensure-native-runtime.mjs' +const unchangedFiles = [ + `.github/actions/prepare-native-runtime/action.yml`, + `config/scripts/${control}`, + `config/scripts/${candidate}`, + 'config/scripts/capture-windows-native-environment.cmd', + 'config/scripts/windows-native-benchmark-observer.mjs', + 'config/scripts/benchmark-windows-native-preparation.mjs' +] + +export function validateObservedPreparation(receipt, variant, warm = false) { + assert.equal( + receipt.parentBefore.hash, + receipt.parentAfter.hash, + 'Outer compiler environment changed' + ) + const calls = (phase) => receipt.calls.filter((call) => call.phase === phase) + const captured = variant !== 'control' && !warm + assert.equal(calls('discovery').length, captured ? 1 : 0, 'Unexpected compiler discovery count') + assert.equal(calls('capture').length, captured ? 1 : 0, 'Unexpected developer command count') + assert.equal( + calls('probe').length, + variant === 'control' ? 0 : 1, + 'Missing helper preflight observation' + ) + for (const call of receipt.calls) { + assert( + !call.error && !call.timedOut && !call.outputTruncated, + 'Incomplete compiler observation' + ) + } + if (variant === 'control') { + assert.equal(calls('addon-build').length, warm ? 0 : 4, 'Expected all four cold addon builds') + assert( + receipt.calls.every((call) => call.code === 0), + 'Control preparation failed' + ) + return + } + const [prepare] = calls('preparation') + assert.equal(calls('preparation').length, 1, 'Missing unchanged preparation child') + assert.equal(prepare.code, 0, 'Native preparation failed') + assert.equal(calls('probe')[0].code, warm ? 0 : 1, 'Unexpected helper cold/warm probe') + if (warm || variant === 'fallback') { + assert.equal( + prepare.environment.hash, + receipt.parentBefore.hash, + 'Fallback/warm changed compiler selection' + ) + assert.deepEqual(prepare.changedNames, [], 'Fallback/warm altered child environment') + } else { + assert.equal(calls('discovery')[0].code, 0) + assert.equal(calls('capture')[0].code, 0) + assert(prepare.changedNames.length > 0, 'Captured environment was not applied') + assert( + prepare.changedNames.every((name) => compilerEnvironmentKey.test(name)), + 'Unrelated environment changed' + ) + assert(prepare.environment.compiler.vcinstalldir, 'Prepared child lacks compiler selection') + } + if (variant === 'fallback' && !warm) { + assert.equal(calls('discovery')[0].code, 0) + assert.equal(calls('capture')[0].code, 7, 'Fixed failure fixture did not run') + } +} + +export function assertNativeColdProbe(result) { + assert.equal(result.code, 1, 'Cold fixture unexpectedly passed') + for (const name of addonNames) { + assert(result.stderr.includes(`${name}:`), `Cold fixture did not fail ${name}`) + } +} + +export function assertNoHeaderDownloads(result) { + assert( + !/gyp http (GET|200|download)/i.test(`${result.stdout}\n${result.stderr}`), + 'Timed preparation downloaded headers/libraries' + ) +} + +function digestFiles(root, paths) { + return Object.fromEntries( + paths + .filter((path) => existsSync(join(root, path))) + .map((path) => [path, hash(readFileSync(join(root, path)))]) + ) +} + +function treeDigest(root) { + const digest = createHash('sha256') + const files = [] + const visit = (path) => { + for (const name of readdirSync(path).sort()) { + const child = join(path, name) + if (statSync(child).isDirectory()) { + visit(child) + } else { + files.push(child) + } + } + } + visit(root) + for (const file of files) { + digest.update(relative(root, file)).update('\0').update(readFileSync(file)) + } + return { sha256: digest.digest('hex'), files: files.length } +} + +function nativeDirectories(snapshot) { + const require = createRequire(join(snapshot, 'package.json')) + const root = realpathSync(snapshot) + return addonNames.map((name) => { + const directory = realpathSync(dirname(require.resolve(`${name}/package.json`))) + assert(directory.startsWith(`${root}${sep}`), `Addon escapes its isolated copy: ${name}`) + return { name, directory } + }) +} + +function binaryReceipt(snapshot) { + const require = createRequire(join(snapshot, 'package.json')) + const paths = [ + join(dirname(require.resolve('node-pty/package.json')), 'build/Release/conpty.node'), + join(dirname(require.resolve('node-pty/package.json')), 'build/Release/conpty/conpty.dll'), + join(dirname(require.resolve('node-pty/package.json')), 'build/Release/conpty/OpenConsole.exe'), + join(snapshot, 'native/windows-registry/build/Release/orca_windows_registry.node'), + join(snapshot, 'native/windows-security/build/Release/orca_windows_security.node'), + join( + dirname(require.resolve('@vscode/windows-process-tree/package.json')), + 'build/Release/windows_process_tree.node' + ) + ] + return paths.map((path) => { + assert(existsSync(path), `Missing prepared artifact: ${path}`) + return { + path: relative(snapshot, path), + bytes: statSync(path).size, + sha256: hash(readFileSync(path)) + } + }) +} + +export async function benchmarkWindowsNativePreparation( + root = resolve(import.meta.dirname, '../..') +) { + assert.equal(process.platform, 'win32', 'This hosted comparison requires Windows') + assert(['x64', 'arm64'].includes(process.arch), 'Unsupported Windows runner') + assert.equal(process.versions.node, '24.21.0', 'Comparison requires pinned Node') + assert( + !hasWindowsCompilerSelection(process.env), + 'Explicit compiler selection prevents a qualified automatic-capture comparison' + ) + assert(/^[0-9a-f]{40}$/.test(process.env.SOURCE_SHA ?? ''), 'SOURCE_SHA must be an exact commit') + const output = join(root, '.build/windows-native-benchmark') + mkdirSync(output, { recursive: true }) + const temporary = mkdtempSync( + join(process.env.RUNNER_TEMP || tmpdir(), 'orca-native-comparison-') + ) + const archive = join(temporary, 'source.tar') + const headers = join(temporary, 'headers') + const pnpm = resolvePnpmCliInvocation() + const environment = { + ...process.env, + ORCA_BACKGROUND_LAUNCH: '1', + npm_config_devdir: headers, + npm_package_config_node_gyp_devdir: headers + } + const parentBefore = environmentReceipt(process.env) + const githubEnvironmentBefore = existsSync(process.env.GITHUB_ENV ?? '') + ? hash(readFileSync(process.env.GITHUB_ENV)) + : null + const report = { + schemaVersion: 1, + startedAt: new Date().toISOString(), + qualification: 'incomplete', + firstRecordedJobStep: JSON.parse(readFileSync(join(output, 'job-start.json'), 'utf8')), + phases: [], + samples: [], + scope: { + warm: 'Same-build warm preparation; no Actions native-cache restore/save is performed.', + metric: + 'Cold addon preparation with identical diagnostic preload; installs, header downloads, standalone qualification probes and artifact upload are recorded separately; the helper preflight remains included.', + occupancy: + 'Use complete hosted job started_at/completed_at, including setup and upload/post steps, for actual runner occupancy. This artifact only measures its explicit window.', + excluded: + 'No Node-to-Electron, cross-target template, exact-key restore, or CI workload gain is claimed.' + } + } + const writeReport = () => + writeFileSync(join(output, 'receipt.json'), `${JSON.stringify(report, null, 2)}\n`) + const command = async (label, program, args, cwd = root, env = environment, accepted = [0]) => { + const startedAt = new Date().toISOString() + const start = performance.now() + const result = await runProcess({ + program, + args, + cwd, + env, + timeoutMs: 1_200_000, + maxOutputBytes: 64 * 1024 * 1024, + terminationBarrier: true + }) + const elapsedMs = performance.now() - start + const stdoutFile = `${label}.stdout.log` + const stderrFile = `${label}.stderr.log` + writeFileSync(join(output, stdoutFile), result.stdout) + writeFileSync(join(output, stderrFile), result.stderr) + report.phases.push({ + label, + program, + args, + startedAt, + finishedAt: new Date().toISOString(), + durationMs: elapsedMs, + code: result.code, + signal: result.signal, + timedOut: result.timedOut, + outputTruncated: result.outputTruncated, + stdoutFile, + stderrFile, + stdoutSha256: hash(result.stdout), + stderrSha256: hash(result.stderr) + }) + writeReport() + assert( + !result.timedOut && + !result.signal && + !result.outputTruncated && + accepted.includes(result.code), + `${label} failed: ${result.stderr.slice(-4000)}` + ) + return { ...result, durationMs: elapsedMs } + } + const pnpmCommand = (label, args, cwd) => + command(label, pnpm.command, [...pnpm.prefixArgs, ...args], cwd) + try { + const head = (await command('source-head', 'git', ['rev-parse', 'HEAD'])).stdout.trim() + assert.equal(head, process.env.SOURCE_SHA, 'Checkout differs from frozen source') + await command('source-clean', 'git', ['diff', '--exit-code', 'HEAD', '--']) + const tree = (await command('source-tree', 'git', ['rev-parse', 'HEAD^{tree}'])).stdout.trim() + const pnpmVersion = (await pnpmCommand('pnpm-version', ['--version'], root)).stdout.trim() + assert.equal(pnpmVersion, '12.8.1', 'Comparison requires pinned pnpm') + const action = readFileSync( + join(root, '.github/actions/prepare-native-runtime/action.yml'), + 'utf8' + ) + const hashExpression = action.match(/NATIVE_SOURCE_HASH: (.+)/)?.[1] + assert(hashExpression, 'Missing production source identity') + const inputs = [...hashExpression.matchAll(/'([^']+)'/g)].map((match) => match[1]) + assert( + inputs.includes(`config/scripts/${candidate}`), + 'Frozen source lacks the candidate native-cache fingerprint' + ) + const requiredScriptHashes = digestFiles(root, unchangedFiles) + assert.equal( + Object.keys(requiredScriptHashes).length, + unchangedFiles.length, + 'Frozen source lacks required scripts' + ) + const sourceInputs = digestFiles(root, inputs) + report.identity = { + sourceSha: head, + tree, + runtime: 'node', + node: process.versions.node, + nodeAbi: process.versions.modules, + arch: process.arch, + pnpm: pnpmVersion, + runner: process.env.BENCHMARK_RUNNER, + imageOS: process.env.ImageOS, + imageVersion: process.env.ImageVersion, + workflowRef: process.env.GITHUB_WORKFLOW_REF, + workflowSha: process.env.GITHUB_WORKFLOW_SHA, + runId: process.env.GITHUB_RUN_ID, + runAttempt: process.env.GITHUB_RUN_ATTEMPT, + sourceInputs, + requiredScriptHashes, + sourceInputDigest: hash(JSON.stringify(sourceInputs)), + productionHashExpression: hashExpression, + parentCompilerEnvironment: parentBefore.compiler + } + await command('archive-source', 'git', ['archive', '--format=tar', '--output', archive, 'HEAD']) + report.identity.archiveSha256 = hash(readFileSync(archive)) + const require = createRequire(join(root, 'package.json')) + const nodeGyp = process.env.npm_config_node_gyp || require.resolve('node-gyp/bin/node-gyp.js') + report.identity.nodeGyp = JSON.parse( + readFileSync(resolve(dirname(nodeGyp), '..', 'package.json'), 'utf8') + ).version + report.identity.nodeGypEntrySha256 = hash(readFileSync(nodeGyp)) + await command('download-headers-before-measurement', process.execPath, [ + nodeGyp, + 'install', + `--target=${process.versions.node}`, + `--arch=${process.arch}`, + `--devdir=${headers}` + ]) + const headersBefore = treeDigest(headers) + report.identity.headers = headersBefore + const physicalDirectories = new Set() + const schedule = buildCounterbalancedSchedule(2, 'control', 'candidate').flat() + report.schedule = schedule + for (const [index, variant] of [...schedule, 'fallback'].entries()) { + const label = `sample-${String(index + 1).padStart(2, '0')}` + const snapshot = join(temporary, label) + mkdirSync(snapshot) + await command(`${label}-extract`, 'tar', ['-xf', archive, '-C', snapshot]) + await pnpmCommand( + `${label}-install`, + [ + 'install', + '--offline', + '--frozen-lockfile', + '--ignore-scripts', + '--package-import-method=copy', + '--side-effects-cache=false' + ], + snapshot + ) + await pnpmCommand(`${label}-internal-packages`, ['run', 'build:packages'], snapshot) + const directories = nativeDirectories(snapshot) + for (const { directory } of directories) { + assert( + !physicalDirectories.has(directory.toLowerCase()), + 'Native copies share a build directory' + ) + physicalDirectories.add(directory.toLowerCase()) + rmSync(join(directory, 'build'), { recursive: true, force: true }) + } + const initial = await command( + `${label}-cold-probe`, + process.execPath, + [join(snapshot, 'config/scripts', control), '--check-only'], + snapshot, + environment, + [1] + ) + assertNativeColdProbe(initial) + const snapshotScriptHashes = digestFiles(snapshot, unchangedFiles) + assert.deepEqual( + snapshotScriptHashes, + requiredScriptHashes, + 'Ordinary snapshot source/config drift' + ) + const originalCapture = readFileSync( + join(snapshot, 'config/scripts/capture-windows-native-environment.cmd') + ) + if (variant === 'fallback') { + writeFileSync( + join(snapshot, 'config/scripts/capture-windows-native-environment.cmd'), + '@echo off\r\nexit /b 7\r\n' + ) + } + const sample = { + label, + variant, + sourceHashesBeforeInjection: snapshotScriptHashes, + originalCaptureSha256: hash(originalCapture), + addonDirectories: directories, + measurements: [] + } + report.samples.push(sample) + for (const warm of [false, true]) { + const stage = `${label}-${warm ? 'warm' : 'cold'}` + const traceFile = join(output, `${stage}.trace.json`) + const script = join(snapshot, 'config/scripts', variant === 'control' ? control : candidate) + const args = [ + '--import', + pathToFileURL(join(snapshot, 'config/scripts/windows-native-benchmark-observer.mjs')) + .href, + script, + ...(variant === 'control' ? ['--runtime=node'] : []) + ] + const result = await command(stage, process.execPath, args, snapshot, { + ...environment, + ORCA_NATIVE_BENCHMARK_TRACE: traceFile + }) + assertNoHeaderDownloads(result) + const receipt = JSON.parse(readFileSync(traceFile, 'utf8')) + validateObservedPreparation(receipt, variant, warm) + if (variant === 'fallback' && !warm) { + assert(result.stderr.includes('retaining node-gyp discovery'), 'Fallback warning missing') + } + const final = await command( + `${stage}-final-probe`, + process.execPath, + [join(snapshot, 'config/scripts', control), '--check-only'], + snapshot + ) + assert.equal(final.code, 0) + const calls = receipt.calls + const milliseconds = (phase) => + calls + .filter((call) => call.phase === phase) + .reduce((total, call) => total + call.durationMs, 0) + sample.measurements.push({ + warm, + totalPreparationMs: result.durationMs, + helperDiscoveryMs: milliseconds('discovery'), + helperCaptureMs: milliseconds('capture'), + helperPreprobeMs: milliseconds('probe'), + unchangedPreparationChildMs: variant === 'control' ? null : milliseconds('preparation'), + helperOutsidePreparationChildMs: + variant === 'control' ? null : result.durationMs - milliseconds('preparation'), + traceFile: relative(output, traceFile), + traceSha256: hash(readFileSync(traceFile)), + binaries: binaryReceipt(snapshot) + }) + assert.deepEqual( + treeDigest(headers), + headersBefore, + 'Timed preparation changed prefilled headers/libraries' + ) + assert.equal( + environmentReceipt(process.env).hash, + parentBefore.hash, + 'Parent environment changed' + ) + assert.equal( + existsSync(process.env.GITHUB_ENV ?? '') + ? hash(readFileSync(process.env.GITHUB_ENV)) + : null, + githubEnvironmentBefore, + 'Compiler setup wrote GITHUB_ENV' + ) + writeReport() + } + sample.isolatedCopyWindowMs = report.phases + .filter((phase) => phase.label.startsWith(`${label}-`)) + .reduce((total, phase) => total + phase.durationMs, 0) + sample.injectedCaptureSha256 = + variant === 'fallback' + ? hash( + readFileSync(join(snapshot, 'config/scripts/capture-windows-native-environment.cmd')) + ) + : null + } + report.comparison = Object.fromEntries( + [false, true].map((warm) => { + const timings = (variant) => + report.samples + .filter((sample) => sample.variant === variant) + .map( + (sample) => + sample.measurements.find((measurement) => measurement.warm === warm) + .totalPreparationMs + ) + const baseline = timings('control') + const proposed = timings('candidate') + return [ + warm ? 'sameBuildWarm' : 'cold', + { + control: summarizeBenchmarkSamples(baseline), + candidate: summarizeBenchmarkSamples(proposed), + pairDeltaMs: baseline.map((value, index) => proposed[index] - value), + role: 'Two counterbalanced pairs are descriptive; no significance or ordinary CI hit-rate estimate.' + } + ] + }) + ) + report.parentEnvironment = { + before: parentBefore, + after: environmentReceipt(process.env), + githubEnvironmentBefore, + githubEnvironmentAfter: githubEnvironmentBefore + } + report.qualification = 'passed' + } catch (error) { + report.failure = error.message + report.qualification = 'failed' + throw error + } finally { + rmSync(temporary, { recursive: true, force: true }) + report.finishedAt = new Date().toISOString() + report.explicitWindowMs = Date.parse(report.finishedAt) - Date.parse(report.startedAt) + report.explicitJobWindowMs = + Date.parse(report.finishedAt) - Date.parse(report.firstRecordedJobStep.startedAt) + writeReport() + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + await benchmarkWindowsNativePreparation() +} 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/build-windows-cli-launcher.test.mjs b/config/scripts/build-windows-cli-launcher.test.mjs index 5dc3569f00b..abdff17ec7d 100644 --- a/config/scripts/build-windows-cli-launcher.test.mjs +++ b/config/scripts/build-windows-cli-launcher.test.mjs @@ -11,7 +11,8 @@ import { import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' import { spawnSync } from 'node:child_process' -import { describe, expect, it } from 'vitest' +import { runProcessSync } from '@orca/process-host' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { shouldReuseCompiledWindowsRustExecutable, windowsRustExecutableFileVersion, @@ -45,6 +46,43 @@ function itWindows(name, test) { runner(name, { timeout: 300_000 }, test) } +let sharedLauncherRoot = null +let sharedLauncherPath = null +let sharedLauncherBuild = null + +// Compile once; each launch case gets a private copy so it can own its fixture tree. +beforeAll(() => { + if (process.platform !== 'win32') { + return + } + sharedLauncherRoot = mkdtempSync(join(tmpdir(), 'orca cli launcher build ')) + sharedLauncherPath = join(sharedLauncherRoot, 'orca.exe') + const result = runProcessSync({ + program: process.execPath, + args: ['config/scripts/build-windows-cli-launcher.mjs', '--output', sharedLauncherPath], + cwd: projectRoot, + timeoutMs: 300_000 + }) + sharedLauncherBuild = { status: result.code, stdout: result.stdout, stderr: result.stderr } + if (result.code !== 0) { + throw new Error(`${result.stdout}\n${result.stderr}`) + } +}, 300_000) + +afterAll(() => { + if (sharedLauncherRoot) { + removeFixtureTree(sharedLauncherRoot) + } +}) + +function copySharedLauncher(target) { + if (!sharedLauncherPath) { + throw new Error('shared Windows CLI launcher was not built') + } + mkdirSync(dirname(target), { recursive: true }) + copyFileSync(sharedLauncherPath, target) +} + describe('Windows CLI launcher', () => { it('reuses restored builds only while all embedded inputs and the release version match', () => { const root = mkdtempSync(join(tmpdir(), 'orca-cli-launcher-reuse-')) @@ -107,12 +145,12 @@ describe('Windows CLI launcher', () => { const root = mkdtempSync(join(tmpdir(), 'orca launcher metadata ')) try { const launcherPath = join(root, 'orca.exe') - const build = spawnSync( - process.execPath, - ['config/scripts/build-windows-cli-launcher.mjs', '--output', launcherPath], - { cwd: projectRoot, encoding: 'utf8' } - ) - expect(build.status, `${build.stdout}\n${build.stderr}`).toBe(0) + expect(sharedLauncherBuild).not.toBeNull() + expect( + sharedLauncherBuild.status, + `${sharedLauncherBuild.stdout}\n${sharedLauncherBuild.stderr}` + ).toBe(0) + copySharedLauncher(launcherPath) const inspect = spawnSync( 'powershell.exe', [ @@ -183,12 +221,7 @@ describe('Windows CLI launcher', () => { 'utf8' ) - const build = spawnSync( - process.execPath, - ['config/scripts/build-windows-cli-launcher.mjs', '--output', launcherPath], - { cwd: projectRoot, encoding: 'utf8' } - ) - expect(build.status, `${build.stdout}\n${build.stderr}`).toBe(0) + copySharedLauncher(launcherPath) const body = 'paragraph one line one\nparagraph one line two\n\nparagraph two' const powershell = spawnSync( @@ -247,12 +280,7 @@ describe('Windows CLI launcher', () => { }))\n`, 'utf8' ) - const build = spawnSync( - process.execPath, - ['config/scripts/build-windows-cli-launcher.mjs', '--output', launcherPath], - { cwd: projectRoot, encoding: 'utf8' } - ) - expect(build.status, `${build.stdout}\n${build.stderr}`).toBe(0) + copySharedLauncher(launcherPath) writeFileSync( join(dirname(launcherPath), 'orca-launcher.cfg'), `node=${process.execPath}\ncli=${cliPath}\nuser-data=${profile}\n` @@ -326,12 +354,7 @@ describe('Windows CLI launcher', () => { }))\n`, 'utf8' ) - const build = spawnSync( - process.execPath, - ['config/scripts/build-windows-cli-launcher.mjs', '--output', launcherPath], - { cwd: projectRoot, encoding: 'utf8' } - ) - expect(build.status, `${build.stdout}\n${build.stderr}`).toBe(0) + copySharedLauncher(launcherPath) const compiler = findFrameworkCompiler() expect(compiler).not.toBeNull() diff --git a/config/scripts/capture-windows-native-environment.cmd b/config/scripts/capture-windows-native-environment.cmd new file mode 100644 index 00000000000..52dab775b3f --- /dev/null +++ b/config/scripts/capture-windows-native-environment.cmd @@ -0,0 +1,6 @@ +@echo off +chcp 65001 >nul +call "%ORCA_WINDOWS_VSDEVCMD%" -no_logo -arch=%ORCA_WINDOWS_NATIVE_ARCH% -host_arch=%ORCA_WINDOWS_NATIVE_ARCH% +if errorlevel 1 exit /b 1 +echo ORCA_WINDOWS_NATIVE_ENVIRONMENT +set 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..19d51b803bb 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -1,11724 +1,12326 @@ { "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/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/shared/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-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/electron-builder-runtime-resources.test.mjs b/config/scripts/electron-builder-runtime-resources.test.mjs index 7c774e4cd7d..2d0c60099ca 100644 --- a/config/scripts/electron-builder-runtime-resources.test.mjs +++ b/config/scripts/electron-builder-runtime-resources.test.mjs @@ -477,7 +477,12 @@ describe('packaged runtime resources', () => { { recursive: true } ) await seedBundledRipgrep(resourcesDir) - await seedClipboardPublisher(resourcesDir, 0x3e) + // Layout fixtures use a cross-arch slice; native PTY probes need real packaged binaries. + const target = + process.arch === 'x64' + ? { electronArch: 3, machine: 0xb7 } + : { electronArch: 1, machine: 0x3e } + await seedClipboardPublisher(resourcesDir, target.machine) await stubObjdump(root) await mkdir(join(resourcesDir, 'node_modules', 'zod', 'src'), { recursive: true }) // Why: afterPack now fails hard when the unpacked daemon entry is @@ -512,7 +517,7 @@ describe('packaged runtime resources', () => { await electronBuilderConfig.afterPack({ appOutDir: join(root, 'linux-unpacked'), electronPlatformName: 'linux', - arch: 1, + arch: target.electronArch, packager: { appInfo: { version: '9.9.9' } } }) 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/initialize-windows-native-toolchain.mjs b/config/scripts/initialize-windows-native-toolchain.mjs new file mode 100644 index 00000000000..86be338d549 --- /dev/null +++ b/config/scripts/initialize-windows-native-toolchain.mjs @@ -0,0 +1,159 @@ +import { existsSync } from 'node:fs' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { runProcessSync } from '@orca/process-host' + +const ENVIRONMENT_MARKER = 'ORCA_WINDOWS_NATIVE_ENVIRONMENT' +const compilerEnvironmentKey = + /^(PATH|INCLUDE|LIB|LIBPATH|VCINSTALLDIR|VSINSTALLDIR|VisualStudioVersion|VSCMD_.+|VCTools.+|WindowsSDK.+|UniversalCRT.+|UCRTVersion)$/i + +export function parseWindowsCompilerEnvironment(output, arch) { + const lines = output.split(/\r?\n/) + const marker = lines.indexOf(ENVIRONMENT_MARKER) + if (marker === -1) { + throw new Error('Developer environment receipt is missing') + } + const environment = {} + for (const line of lines.slice(marker + 1)) { + const separator = line.indexOf('=') + if (separator <= 0) { + continue + } + const name = line.slice(0, separator) + if (compilerEnvironmentKey.test(name)) { + environment[name] = line.slice(separator + 1) + } + } + const value = (name) => { + const key = Object.keys(environment).find((key) => key.toLowerCase() === name.toLowerCase()) + return key ? environment[key] : undefined + } + if (!value('VCINSTALLDIR') || !value('VSCMD_VER') || !value('WindowsSDKVersion')) { + throw new Error('Developer environment lacks the compiler or Windows SDK') + } + if ( + !/^\d+\.\d+\./.test(value('VSCMD_VER')) || + !/^\d+\.\d+\.\d+\.\d+\\?$/.test(value('WindowsSDKVersion')) + ) { + throw new Error('Developer environment has an invalid compiler or Windows SDK version') + } + const target = value('VSCMD_ARG_TGT_ARCH')?.toLowerCase() + if (!(target === arch || (arch === 'x64' && target === 'amd64'))) { + throw new Error('Developer environment targets a different architecture') + } + return environment +} + +export function hasWindowsCompilerSelection(environment) { + return Object.entries(environment).some( + ([name, value]) => + /^(VCINSTALLDIR|GYP_MSVS_VERSION|npm_config_msvs_version|npm_package_config_node_gyp_msvs_version)$/i.test( + name + ) && value + ) +} + +export function mergeWindowsCompilerEnvironment(environment, compilerEnvironment) { + const compilerKeys = new Set(Object.keys(compilerEnvironment).map((key) => key.toLowerCase())) + return { + ...Object.fromEntries( + Object.entries(environment).filter(([key]) => !compilerKeys.has(key.toLowerCase())) + ), + ...compilerEnvironment + } +} + +export function initializeWindowsNativeToolchain() { + if (process.platform !== 'win32') { + return + } + if (hasWindowsCompilerSelection(process.env)) { + return + } + try { + if (!['x64', 'arm64'].includes(process.arch)) { + throw new Error('Windows compiler setup requires a supported runner architecture') + } + const programFiles = Object.entries(process.env) + .filter(([key]) => /^(ProgramFiles|ProgramFiles\(x86\))$/i.test(key)) + .map(([, value]) => value) + const vswhere = programFiles + .map((directory) => join(directory, 'Microsoft Visual Studio', 'Installer', 'vswhere.exe')) + .find(existsSync) + if (!vswhere) { + throw new Error('Visual Studio discovery tool is unavailable') + } + const discovery = runProcessSync({ + program: vswhere, + args: [ + '-latest', + '-products', + '*', + '-utf8', + '-requires', + process.arch === 'arm64' + ? 'Microsoft.VisualStudio.Component.VC.Tools.ARM64' + : 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', + '-property', + 'installationPath' + ], + stdio: 'pipe', + timeoutMs: 30_000 + }) + const installations = discovery.stdout.trim().split(/\r?\n/).filter(Boolean) + if (discovery.code !== 0 || installations.length !== 1) { + throw new Error('Visual Studio discovery did not select one compiler') + } + const devcmd = join(installations[0], 'Common7', 'Tools', 'VsDevCmd.bat') + if (!existsSync(devcmd)) { + throw new Error('Visual Studio developer command is unavailable') + } + // process-host owns the fixed batch runner's quoting and hidden console. + const result = runProcessSync({ + program: join(import.meta.dirname, 'capture-windows-native-environment.cmd'), + args: [], + env: { + ...process.env, + ORCA_WINDOWS_VSDEVCMD: devcmd, + ORCA_WINDOWS_NATIVE_ARCH: process.arch + }, + stdio: 'pipe', + timeoutMs: 60_000 + }) + if (result.code !== 0) { + throw new Error('Visual Studio developer command failed') + } + const environment = parseWindowsCompilerEnvironment(result.stdout, process.arch) + console.log(`[native-runtime] Initialized Windows ${process.arch} compiler environment`) + return environment + } catch { + console.warn( + '[native-runtime] Compiler environment setup unavailable; retaining node-gyp discovery' + ) + } +} + +export function prepareWindowsNodeRuntime(runProcess = runProcessSync) { + const script = join(import.meta.dirname, 'ensure-native-runtime.mjs') + const check = runProcess({ + program: process.execPath, + args: [script, '--check-only'], + stdio: 'pipe', + timeoutMs: null + }) + const compilerEnvironment = check.code === 0 ? undefined : initializeWindowsNativeToolchain() + const result = runProcess({ + program: process.execPath, + args: [script, '--runtime=node'], + env: compilerEnvironment + ? mergeWindowsCompilerEnvironment(process.env, compilerEnvironment) + : process.env, + stdio: 'inherit', + timeoutMs: null + }) + return result.code ?? 1 +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exitCode = prepareWindowsNodeRuntime() +} diff --git a/config/scripts/initialize-windows-native-toolchain.test.mjs b/config/scripts/initialize-windows-native-toolchain.test.mjs new file mode 100644 index 00000000000..d9765d6523d --- /dev/null +++ b/config/scripts/initialize-windows-native-toolchain.test.mjs @@ -0,0 +1,85 @@ +import { describe, expect, it, vi } from 'vitest' +import { + hasWindowsCompilerSelection, + mergeWindowsCompilerEnvironment, + parseWindowsCompilerEnvironment, + prepareWindowsNodeRuntime +} from './initialize-windows-native-toolchain.mjs' + +const receipt = [ + 'Developer Command Prompt banner, not an environment variable', + 'TOKEN=should-not-be-exported', + 'ORCA_WINDOWS_NATIVE_ENVIRONMENT', + 'VCINSTALLDIR=C:\\Visual Studio\\VC\\', + 'VSCMD_VER=18.10.12224.181', + 'WindowsSDKVersion=10.0.28000.0\\', + 'VSCMD_ARG_TGT_ARCH=arm64', + 'Path=C:\\工具;C:\\Program Files\\Visual Studio', + 'INCLUDE=C:\\SDK=Custom\\Include', + 'TOKEN=should-not-be-exported', + '=C:=C:\\', + '' +].join('\r\n') + +describe('Windows compiler environment receipt', () => { + it('keeps compiler paths and equals signs while excluding unrelated variables and banners', () => { + expect(parseWindowsCompilerEnvironment(receipt, 'arm64')).toEqual({ + VCINSTALLDIR: 'C:\\Visual Studio\\VC\\', + VSCMD_VER: '18.10.12224.181', + WindowsSDKVersion: '10.0.28000.0\\', + VSCMD_ARG_TGT_ARCH: 'arm64', + Path: 'C:\\工具;C:\\Program Files\\Visual Studio', + INCLUDE: 'C:\\SDK=Custom\\Include' + }) + }) + + it.each([ + receipt.replace('ORCA_WINDOWS_NATIVE_ENVIRONMENT', 'missing'), + receipt.replace('VCINSTALLDIR=', 'unrelated='), + receipt.replace('VSCMD_VER=', 'unrelated='), + receipt.replace('WindowsSDKVersion=', 'unrelated='), + receipt.replace('10.0.28000.0\\', 'unavailable'), + receipt.replace('18.10.12224.181', 'unavailable'), + receipt.replace('VSCMD_ARG_TGT_ARCH=arm64', 'VSCMD_ARG_TGT_ARCH=x64') + ])('rejects incomplete, invalid or wrong-architecture setup before use', (output) => { + expect(() => parseWindowsCompilerEnvironment(output, 'arm64')).toThrow() + }) +}) + +describe('Windows native compiler environment scope', () => { + it('replaces path keys case-insensitively without changing the parent environment', () => { + const original = { PATH: 'original-path', LIB: 'original-lib', TOKEN: 'unchanged' } + expect( + mergeWindowsCompilerEnvironment(original, { Path: 'compiler-path', Lib: 'compiler-lib' }) + ).toEqual({ Path: 'compiler-path', Lib: 'compiler-lib', TOKEN: 'unchanged' }) + expect(original).toEqual({ PATH: 'original-path', LIB: 'original-lib', TOKEN: 'unchanged' }) + }) + + it.each([ + 'VCINSTALLDIR', + 'GYP_MSVS_VERSION', + 'npm_config_msvs_version', + 'npm_package_config_node_gyp_msvs_version' + ])('retains an existing compiler selection through %s', (name) => { + expect(hasWindowsCompilerSelection({ [name.toLowerCase()]: 'requested' })).toBe(true) + expect(hasWindowsCompilerSelection({ [name]: '' })).toBe(false) + }) + + it('uses the original preparation and environment after a passing warm probe', () => { + const runProcess = vi.fn(() => ({ code: 0 })) + expect(prepareWindowsNodeRuntime(runProcess)).toBe(0) + expect(runProcess).toHaveBeenCalledTimes(2) + expect(runProcess).toHaveBeenLastCalledWith( + expect.objectContaining({ + args: [expect.stringContaining('ensure-native-runtime.mjs'), '--runtime=node'], + env: process.env, + stdio: 'inherit' + }) + ) + }) + + it('propagates native preparation failure after a passing warm probe', () => { + const runProcess = vi.fn().mockReturnValueOnce({ code: 0 }).mockReturnValueOnce({ code: 9 }) + expect(prepareWindowsNodeRuntime(runProcess)).toBe(9) + }) +}) 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.json b/config/scripts/locale-ko-key-overrides.json index 091b094c347..6a7956efc64 100644 --- a/config/scripts/locale-ko-key-overrides.json +++ b/config/scripts/locale-ko-key-overrides.json @@ -3734,9 +3734,6 @@ "auto.components.settings.terminal.clipboard.search.5ffcd13c90": { "ko": "tmux" }, - "auto.components.settings.terminal.clipboard.search.603818e8d8": { - "ko": "터미널 선택 영역이 만들어지는 즉시 자동으로 클립보드에 복사합니다." - }, "auto.components.settings.terminal.clipboard.search.e87c6d776d": { "ko": "자동" }, @@ -5246,9 +5243,6 @@ "auto.components.settings.TerminalPane.a9d47451d1": { "ko": "\"새 탭\"은 현재 포커스를 가져가지 않고 \"Setup\"이라는 제목의 백그라운드 탭에서 설정 명령을 엽니다." }, - "auto.components.settings.TerminalPane.4729c645fc": { - "ko": "터미널에서 선택한 내용을 클립보드에 자동으로 복사합니다." - }, "auto.components.settings.TerminalPane.9129b7e805": { "ko": "터미널 패널 위에 마우스를 올리면 클릭하지 않고도 활성화됩니다." }, 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..cd06384f996 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', @@ -40,6 +41,8 @@ const ALWAYS_FILES = new Set([ 'config/scripts/workspace-source-exports.mjs', 'config/scripts/workspace-source-exports.test.mjs', 'config/scripts/headless-detector-compiler-cache.mjs', + 'config/scripts/initialize-windows-native-toolchain.mjs', + 'config/scripts/capture-windows-native-environment.cmd', 'config/scripts/node-server-qualification.mjs', // The source graph sees process-host source, not the build that emits the dist servers load. 'src/packages/process-host/.gitignore', 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..26cb30088fc 100644 --- a/config/scripts/orcad-windows-prebuild-workflow.test.mjs +++ b/config/scripts/orcad-windows-prebuild-workflow.test.mjs @@ -59,6 +59,8 @@ function context(os, arch, event, ref, template = false) { }, success: () => true, fromJSON: JSON.parse, + format: (template, ...values) => + template.replace(/\{(\d+)\}/g, (_match, index) => String(values[Number(index)])), contains: (values, value) => values.includes(value) } } @@ -173,6 +175,38 @@ describe('SSH Windows consumers of qualified server slots', () => { const sshWorkflow = parse(readFileSync('.github/workflows/ssh-windows-hosts.yml', 'utf8')) const sshSteps = sshWorkflow.jobs.hosts.steps const sshPrepare = sshSteps.find((step) => step.uses === prepare.uses) + const inboxBackground = sshSteps.find((step) => step.id === 'inbox-capability') + const inboxSerial = sshSteps.find((step) => step.name?.endsWith('after the builds')) + const allLanes = [ + { arch: 'x64', runner: 'windows-2022', server: 'inbox' }, + { arch: 'x64', runner: 'windows-2022', server: 'preview', archive: 'OpenSSH-Win64.zip' }, + { arch: 'arm64', runner: 'windows-11-arm', server: 'inbox' }, + { arch: 'arm64', runner: 'windows-11-arm', server: 'preview', archive: 'OpenSSH-ARM64.zip' } + ] + + function sshContext(event, inputs = {}) { + const ctx = context('Windows', 'ARM64', event, 'refs/heads/main') + ctx.github.event = { inputs, pull_request: { number: undefined } } + ctx.inputs = inputs + ctx.env = { + INBOX_PREPARATION_MODE: sshValue(sshWorkflow.jobs.hosts.env.INBOX_PREPARATION_MODE, ctx) + } + return ctx + } + + function sshValue(value, ctx) { + return evaluate(value.slice(3, -2).trim(), ctx) + } + + function backgroundPreparationRuns(ctx) { + const condition = inboxBackground.run + .match(/^\s*if\((.*)\)\{\s*$/m)[1] + .replace(/'\$\{\{ matrix\.(arch|server) \}\}'/g, (_match, name) => `matrix.${name}`) + .replaceAll('$env:INBOX_PREPARATION_MODE', 'env.INBOX_PREPARATION_MODE') + .replaceAll(' -eq ', ' === ') + .replaceAll(' -and ', ' && ') + return evaluate(condition, ctx) + } it.each(['pull_request', 'workflow_dispatch'])( 'restores only PR slots and keeps manual %s qualification fresh', @@ -188,13 +222,8 @@ describe('SSH Windows consumers of qualified server slots', () => { it('keeps both architectures, both sshd versions and the existing build order', () => { expect( - sshWorkflow.jobs.hosts.strategy.matrix.include.map(({ arch, server }) => [arch, server]) - ).toEqual([ - ['x64', 'inbox'], - ['x64', 'preview'], - ['arm64', 'inbox'], - ['arm64', 'preview'] - ]) + sshValue(sshWorkflow.jobs.hosts.strategy.matrix.include, sshContext('pull_request')) + ).toEqual(allLanes) const addon = sshSteps.findIndex((step) => step.run?.includes('build-windows-process-tree-addon.mjs') ) @@ -215,6 +244,92 @@ describe('SSH Windows consumers of qualified server slots', () => { 'config/scripts/orcad-windows-prebuild-cache.mjs' ) }) + + it.each([ + ['pull_request', {}, 'overlap', false], + ['pull_request', { preparation_mode: 'serial', arm_inbox_only: true }, 'overlap', false], + ['push', { preparation_mode: 'serial', arm_inbox_only: true }, 'overlap', false], + ['workflow_dispatch', {}, 'overlap', false], + ['workflow_dispatch', { preparation_mode: 'overlap', arm_inbox_only: false }, 'overlap', false], + ['workflow_dispatch', { preparation_mode: 'serial', arm_inbox_only: false }, 'serial', false], + ['workflow_dispatch', { preparation_mode: 'overlap', arm_inbox_only: true }, 'overlap', true], + ['workflow_dispatch', { preparation_mode: 'serial', arm_inbox_only: true }, 'serial', true] + ])('%s inputs=%j mode=%s ARM-only=%s', (event, inputs, mode, armOnly) => { + const ctx = sshContext(event, inputs) + const lanes = sshValue(sshWorkflow.jobs.hosts.strategy.matrix.include, ctx) + expect(lanes).toEqual(armOnly ? [allLanes[2]] : allLanes) + expect(ctx.env.INBOX_PREPARATION_MODE).toBe(mode) + for (const matrix of lanes) { + ctx.matrix = matrix + const inboxArm = matrix.server === 'inbox' && matrix.arch === 'arm64' + expect(backgroundPreparationRuns(ctx)).toBe(inboxArm && mode === 'overlap') + expect(sshValue(inboxSerial.if, ctx)).toBe(inboxArm && mode === 'serial') + } + }) + + it('defaults to the existing overlap schedule and keeps serial after the successful wait', () => { + expect(sshWorkflow.on.workflow_dispatch.inputs.preparation_mode).toMatchObject({ + type: 'choice', + default: 'overlap', + options: ['overlap', 'serial'] + }) + expect(sshWorkflow.on.workflow_dispatch.inputs.arm_inbox_only).toMatchObject({ + type: 'boolean', + default: false + }) + const dependencies = sshSteps.findIndex((step) => step.id === 'dependencies') + const template = sshSteps.findIndex((step) => step.run?.includes('build-orcad-template.mjs')) + const wait = sshSteps.findIndex((step) => step.wait === 'inbox-capability') + const serial = sshSteps.indexOf(inboxSerial) + const hosts = sshSteps.findIndex((step) => step.name?.startsWith('Run the Windows host cells')) + expect(inboxBackground.background).toBe(true) + expect(inboxBackground.if).toBeUndefined() + expect(sshSteps.indexOf(inboxBackground)).toBeLessThan(dependencies) + expect(template).toBeLessThan(wait) + expect(serial).toBe(wait + 1) + expect(hosts).toBe(serial + 1) + expect(inboxSerial.background).toBeUndefined() + for (const preparation of [inboxBackground, inboxSerial]) { + expect(preparation['continue-on-error']).toBeUndefined() + expect(preparation.run).toContain( + '. config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1' + ) + expect(preparation.run).toContain( + "-Receipt (Join-Path $receipts 'inbox-capability-preparation.json') -PreparationMode $env:INBOX_PREPARATION_MODE" + ) + } + expect(sshSteps[hosts].run).toContain('-InboxPreparationReceipt $preparation') + expect(sshWorkflow.jobs.hosts.env.ORCA_BACKGROUND_LAUNCH).toBe('1') + expect(sshWorkflow.jobs.hosts.env.ORCA_ISOLATED_SSH_CI).toBe('1') + for (const event of ['pull_request', 'workflow_dispatch']) { + for (const preparation_mode of ['overlap', 'serial']) { + for (const [cells, jobMinutes, hostMinutes] of [ + ['', 75, 50], + ['orcad-cli-managed', 160, 130] + ]) { + const ctx = sshContext(event, { preparation_mode, arm_inbox_only: true, cells }) + expect(sshValue(sshWorkflow.jobs.hosts['timeout-minutes'], ctx)).toBe(jobMinutes) + expect(sshValue(sshSteps[hosts]['timeout-minutes'], ctx)).toBe(hostMinutes) + } + } + } + }) + + it('isolates manual comparison modes while preserving ordinary cancellation groups', () => { + const group = (ctx) => + sshWorkflow.concurrency.group.replace(/\$\{\{(.*?)\}\}/g, (_match, expression) => + String(evaluate(expression.trim(), ctx)) + ) + const regular = sshContext('pull_request') + regular.github.event.pull_request = { number: 28669 } + expect(group(regular)).toBe('ssh-windows-hosts-28669') + expect(group(sshContext('workflow_dispatch'))).toBe('ssh-windows-hosts-refs/heads/main') + for (const mode of ['overlap', 'serial']) { + expect( + group(sshContext('workflow_dispatch', { arm_inbox_only: true, preparation_mode: mode })) + ).toBe(`ssh-windows-hosts-refs/heads/main-${mode}`) + } + }) }) const commandMocks = ` @@ -225,6 +340,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 +404,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 d006e10a6cb..ff383bdaa7b 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']) @@ -265,6 +267,8 @@ const MANAGED_HOOK_PREFIXES = [ const NATIVE_RUNTIME_PREFIXES = [ 'config/scripts/ensure-native-runtime', + 'config/scripts/initialize-windows-native-toolchain', + 'config/scripts/capture-windows-native-environment', 'config/scripts/rebuild-native-deps', 'config/scripts/node-pty-job-ownership', 'config/scripts/windows-pe-machine', @@ -286,6 +290,8 @@ const NATIVE_CACHE_FILES = new Set([ '.npmrc', '.pnpmfile.cjs', 'config/scripts/ensure-native-runtime.mjs', + 'config/scripts/initialize-windows-native-toolchain.mjs', + 'config/scripts/capture-windows-native-environment.cmd', 'config/scripts/rebuild-native-deps.mjs', 'config/scripts/node-pty-job-ownership.cjs', 'config/scripts/windows-pe-machine.cjs', @@ -334,6 +340,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 +350,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,85 +369,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', - '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' -] - -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..7386699fc1a 100644 --- a/config/scripts/process-host-build-dist.test.mjs +++ b/config/scripts/process-host-build-dist.test.mjs @@ -54,6 +54,7 @@ function fixture(onTestFinished, distSeed) { JSON.stringify({ compilerOptions: { target: 'ES2022', + lib: ['ES2022'], module: 'Node16', moduleResolution: 'Node16', rootDir: 'src', @@ -82,6 +83,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 +112,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 +120,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 +327,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/process-host-dev-rebuild.test.ts b/config/scripts/process-host-dev-rebuild.test.ts index fa27981184b..f732451beef 100644 --- a/config/scripts/process-host-dev-rebuild.test.ts +++ b/config/scripts/process-host-dev-rebuild.test.ts @@ -69,6 +69,7 @@ function fixture(): { root: string; packageDir: string; main: string } { extends: '../../../tsconfig.base.json', compilerOptions: { target: 'ES2022', + lib: ['ES2022'], module: 'Node16', moduleResolution: 'Node16', rootDir: 'src', 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 a96647dfe09..540856c7122 100644 --- a/config/scripts/vitest-sqlite-runtime-files.mjs +++ b/config/scripts/vitest-sqlite-runtime-files.mjs @@ -7,6 +7,8 @@ export const SQLITE_RUNTIME_INCLUDE = [ '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', @@ -19,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', @@ -32,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', @@ -148,6 +152,7 @@ export const SQLITE_RUNTIME_INCLUDE = [ 'src/main/native-chat/agent-session-wire/structured-agent-session-refusal-retry.test.ts', 'src/main/native-chat/agent-session-wire/structured-agent-session-repeat-press.test.ts', 'src/main/native-chat/agent-session-wire/structured-agent-session-repeated-stop.test.ts', + 'src/main/native-chat/native-chat-visuals-sweep.test.ts', 'src/main/native-chat/agent-session-wire/structured-agent-session-resend-answer.test.ts', 'src/main/native-chat/agent-session-wire/structured-agent-session-replaced-server-turn.test.ts', 'src/main/native-chat/agent-session-wire/structured-agent-session-resend-ledger-refusal.test.ts', diff --git a/config/scripts/windows-cmd-shim-spawn-boundary.test.mjs b/config/scripts/windows-cmd-shim-spawn-boundary.test.mjs index afd6af518d1..3d6958389e0 100644 --- a/config/scripts/windows-cmd-shim-spawn-boundary.test.mjs +++ b/config/scripts/windows-cmd-shim-spawn-boundary.test.mjs @@ -2,10 +2,11 @@ import { readdirSync, readFileSync } from 'node:fs' import path from 'node:path' import { describe, expect, it } from 'vitest' import { hasNodeModulesBinSpawn } from './windows-bin-spawn-predicate.mjs' +import { hasWindowsNativeBatchReferenceViolation } from './windows-native-batch-reference-policy' /** - * Bare JS scripts cannot use the TS runProcess chokepoint; resolve package bins - * under process.execPath instead (see oxc-cli-invocation.mjs). The list only shrinks. + * Resolve package bins under process.execPath (see oxc-cli-invocation.mjs). + * The legacy list only shrinks; reviewed native compiler references use process-host. * The literal check catches quoted .cmd/.bat paths, excluding drive letters and * templates; comment-only lines are dropped, but trailing comments fail closed. * The AST check catches spawn/spawnSync/execFile/execFileSync first arguments @@ -13,10 +14,10 @@ import { hasNodeModulesBinSpawn } from './windows-bin-spawn-predicate.mjs' * local variable initializers and ternaries. It does not follow imports, function * returns, assignments, destructuring, aliased spawn names or computed members. * Names are matched without scope/import resolution; shadowed names and non-path - * join/resolve calls can fail closed. Paths are joined textually, without reducing - * dot segments. Neither check proves a Windows branch or an actual unsafe spawn. + * join/resolve calls can fail closed. Package paths fold dot segments. + * Neither check proves a Windows branch or an actual unsafe spawn. */ -const WINDOWS_SHIM_LITERAL = /['"][\w./\\-]*\.(?:cmd|bat)['"]/i +const WINDOWS_SHIM_LITERAL = /['"][\w./\\-]*\.(?:cmd|bat)['"]/gi const SCANNED_ROOTS = ['config/scripts', 'tests/tools'] @@ -83,7 +84,13 @@ describe('windows batch shim spawn boundary', () => { ) const offenders = scripts.filter((relativePath) => { const contents = readFileSync(path.join(repoRoot, relativePath), 'utf8') - return WINDOWS_SHIM_LITERAL.test(codeText(contents)) || hasNodeModulesBinSpawn(contents) + const literals = [...codeText(contents).matchAll(WINDOWS_SHIM_LITERAL)].map((match) => + match[0].slice(1, -1) + ) + return ( + hasWindowsNativeBatchReferenceViolation(relativePath, contents, literals) || + hasNodeModulesBinSpawn(contents) + ) }) it('scans a plausible number of scripts', () => { @@ -95,7 +102,7 @@ describe('windows batch shim spawn boundary', () => { const unlisted = offenders.filter((name) => !WINDOWS_SHIM_SPAWN_ALLOWLIST.includes(name)) expect( unlisted, - 'Node cannot spawn a Windows batch shim without a shell. Resolve the real executable — see oxc-cli-invocation.mjs.' + 'Resolve package shims to their real executable. Fixed native compiler batch runners must pass the scoped process-host policy.' ).toEqual([]) }) diff --git a/config/scripts/windows-native-batch-reference-policy.test.ts b/config/scripts/windows-native-batch-reference-policy.test.ts new file mode 100644 index 00000000000..72bcb34079b --- /dev/null +++ b/config/scripts/windows-native-batch-reference-policy.test.ts @@ -0,0 +1,166 @@ +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { describe, expect, it } from 'vitest' +import { hasWindowsNativeBatchReferenceViolation } from './windows-native-batch-reference-policy' + +const initializer = 'config/scripts/initialize-windows-native-toolchain.mjs' +const benchmark = 'config/scripts/benchmark-windows-native-preparation.mjs' +const observer = 'config/scripts/windows-native-benchmark-observer.mjs' +const metadataCallers = [ + 'config/scripts/node-server-change-scope.mjs', + 'config/scripts/pr-code-change-scope.mjs', + 'config/scripts/windows-native-toolchain-routing.test.mjs' +] +const capture = 'capture-windows-native-environment.cmd' +const initializerSource = ` + import { runProcessSync as captureEnvironment } from '@orca/process-host' + const devcmd = join(installation, 'Common7', 'Tools', 'VsDevCmd.bat') + existsSync(devcmd) + captureEnvironment({ + program: join(import.meta.dirname, '${capture}'), + env: { ORCA_WINDOWS_VSDEVCMD: devcmd } + }) +` +const benchmarkSource = ` + import { runProcess } from '@orca/process-host' + const source = 'config/scripts/${capture}' + await runProcess({ program: process.execPath, args: [source] }) +` + +function hasViolation(relativePath: string, contents: string): boolean { + const literals = [...contents.matchAll(/['"][\w./\\-]*\.(?:cmd|bat)['"]/gi)].map((match) => + match[0].slice(1, -1) + ) + return hasWindowsNativeBatchReferenceViolation(relativePath, contents, literals) +} + +describe('reviewed Windows native batch references', () => { + it.each([...metadataCallers, initializer, benchmark, observer])( + 'accepts the current reviewed source in %s', + (relativePath) => { + const root = path.resolve(import.meta.dirname, '..', '..') + expect(hasViolation(relativePath, readFileSync(path.join(root, relativePath), 'utf8'))).toBe( + false + ) + } + ) + + it.each(metadataCallers)('allows the fixed source path as metadata in %s', (relativePath) => { + expect(hasViolation(relativePath, `const source = 'config/scripts/${capture}'`)).toBe(false) + }) + + it('accepts the process-host executor through its imported alias', () => { + expect(hasViolation(initializer, initializerSource)).toBe(false) + }) + + it('accepts the benchmark executor', () => { + expect(hasViolation(benchmark, benchmarkSource)).toBe(false) + }) + + it.each([ + 'config/scripts/other-native-preparation.mjs', + 'tests/tools/initialize-windows-native-toolchain.mjs', + `config/scripts/nested/${path.basename(initializer)}` + ])('rejects the fixed runner in another caller: %s', (relativePath) => { + expect(hasViolation(relativePath, initializerSource)).toBe(true) + }) + + it.each([ + 'elsewhere/capture-windows-native-environment.cmd', + './capture-windows-native-environment.cmd', + 'CAPTURE-WINDOWS-NATIVE-ENVIRONMENT.cmd', + 'other.cmd', + 'other.bat' + ])('rejects another batch path in a reviewed executor: %s', (literal) => { + expect(hasViolation(initializer, `${initializerSource}\nconst other = '${literal}'`)).toBe(true) + }) + + it.each(metadataCallers)('rejects other batch metadata in %s', (relativePath) => { + expect( + hasViolation(relativePath, `const sources = ['config/scripts/${capture}', 'other.cmd']`) + ).toBe(true) + expect(hasViolation(relativePath, `const command = 'VsDevCmd.bat'`)).toBe(true) + }) + + it.each([ + [initializer, initializerSource], + [benchmark, benchmarkSource], + [observer, `require('@orca/process-host'); const capture = '${capture}'`] + ])('rejects a missing process-host load in %s', (relativePath, contents) => { + expect(hasViolation(relativePath, contents.replace('@orca/process-host', 'other-runner'))).toBe( + true + ) + }) + + it('rejects an unused process-host import beside an unverified executor', () => { + expect( + hasViolation(initializer, initializerSource.replace('captureEnvironment({', 'otherRunner({')) + ).toBe(true) + }) + + it('rejects another executor for the fixed capture script beside a valid call', () => { + expect( + hasViolation( + initializer, + `${initializerSource}\notherRunner({ program: join(import.meta.dirname, '${capture}') })` + ) + ).toBe(true) + }) + + it.each([ + "import { spawnSync as execute } from 'node:child_process'", + "import * as cp from 'child_process'", + "await import('node:child_process')", + "const builtins = 'node:child_process'; process.getBuiltinModule(builtins)", + "const require = createRequire(import.meta.url); const cp = require('child_process')" + ])('rejects raw child-process module loads: %s', (load) => { + expect(hasViolation(initializer, `${initializerSource}\n${load}`)).toBe(true) + }) + + it.each([ + "spawn('program', [])", + "cp.spawnSync('program', [])", + "cp['execFile']('program', [])", + "execFileSync('program', [])", + "exec('program')", + "execSync('program')", + "fork('program')" + ])('rejects raw process calls: %s', (call) => { + expect(hasViolation(initializer, `${initializerSource}\n${call}`)).toBe(true) + }) + + it.each([ + "runProcess({ program: 'node_modules/.bin/pnpm' })", + "const bin = join(root, 'node_modules', '.bin', 'pnpm'); runProcess({ program: bin })", + "runProcess({ program: join(root, 'node_modules', 'tools', '..', '.bin', 'pnpm') })", + "const spec = { 'program': 'root\\\\node_modules\\\\.bin\\\\pnpm' }; runProcess(spec)", + "const program = join(root, 'node_modules', '.bin', 'pnpm'); runProcess({ program })", + "runProcess({ ['program']: join(root, 'node_modules', '.bin', 'pnpm') })" + ])('rejects package-bin programs through process-host: %s', (call) => { + expect(hasViolation(benchmark, `${benchmarkSource}\n${call}`)).toBe(true) + }) + + it.each(metadataCallers)('rejects process execution added to metadata in %s', (relativePath) => { + expect( + hasViolation( + relativePath, + `const capture = 'config/scripts/${capture}'; runProcess({ program: capture })` + ) + ).toBe(true) + expect( + hasViolation( + relativePath, + `import { runProcess as execute } from '@orca/process-host'; const capture = 'config/scripts/${capture}'; execute({ program: capture })` + ) + ).toBe(true) + }) + + it.each([ + "captureEnvironment({ program: 'VsDevCmd.bat' })", + 'captureEnvironment({ program: devcmd })', + 'const program = devcmd; captureEnvironment({ program })', + "const devcmd = join(other, 'VsDevCmd.bat')" + ])('keeps the discovered developer command as metadata: %s', (extra) => { + expect(hasViolation(initializer, `${initializerSource}\n${extra}`)).toBe(true) + }) +}) diff --git a/config/scripts/windows-native-batch-reference-policy.ts b/config/scripts/windows-native-batch-reference-policy.ts new file mode 100644 index 00000000000..60dcc4b8ca6 --- /dev/null +++ b/config/scripts/windows-native-batch-reference-policy.ts @@ -0,0 +1,229 @@ +import ts from 'typescript-api' +import { + collectModuleSpecifiers, + literalString, + propertyName +} from './static-module-specifiers.mjs' +import { hasNodeModulesBinSpawn } from './windows-bin-spawn-predicate.mjs' + +const references = new Map([ + [ + 'config/scripts/node-server-change-scope.mjs', + 'config/scripts/capture-windows-native-environment.cmd' + ], + [ + 'config/scripts/pr-code-change-scope.mjs', + 'config/scripts/capture-windows-native-environment.cmd' + ], + [ + 'config/scripts/windows-native-toolchain-routing.test.mjs', + 'config/scripts/capture-windows-native-environment.cmd' + ], + [ + 'config/scripts/windows-native-benchmark-observer.mjs', + 'capture-windows-native-environment.cmd' + ], + [ + 'config/scripts/initialize-windows-native-toolchain.mjs', + 'capture-windows-native-environment.cmd' + ], + [ + 'config/scripts/benchmark-windows-native-preparation.mjs', + 'config/scripts/capture-windows-native-environment.cmd' + ] +]) +const executors = new Map([ + ['config/scripts/initialize-windows-native-toolchain.mjs', 'runProcessSync'], + ['config/scripts/benchmark-windows-native-preparation.mjs', 'runProcess'] +]) +const initializer = 'config/scripts/initialize-windows-native-toolchain.mjs' +const observer = 'config/scripts/windows-native-benchmark-observer.mjs' +const rawProcessMethods = new Set([ + 'spawn', + 'spawnSync', + 'exec', + 'execSync', + 'execFile', + 'execFileSync', + 'fork' +]) +const processHostMethods = new Set(['runProcess', 'runProcessSync', 'spawnProcess']) + +function callName(node: ts.Expression): string { + return ts.isIdentifier(node) ? node.text : (propertyName(node) ?? '') +} + +function isDeveloperCommandMetadata(node: ts.StringLiteral): boolean { + const call = node.parent + const declaration = call.parent + return ( + ts.isCallExpression(call) && + callName(call.expression) === 'join' && + call.arguments.at(-1) === node && + ts.isVariableDeclaration(declaration) && + ts.isIdentifier(declaration.name) && + declaration.name.text === 'devcmd' + ) +} + +function isDeveloperCommandUse(node: ts.Identifier): boolean { + const parent = node.parent + return ( + (ts.isVariableDeclaration(parent) && parent.name === node) || + (ts.isCallExpression(parent) && + callName(parent.expression) === 'existsSync' && + parent.arguments[0] === node) || + (ts.isPropertyAssignment(parent) && + parent.initializer === node && + ts.isIdentifier(parent.name) && + parent.name.text === 'ORCA_WINDOWS_VSDEVCMD') + ) +} + +function isProcessHostCapture(node: ts.StringLiteral, importedMethods: Set): boolean { + const path = node.parent + if ( + !ts.isCallExpression(path) || + callName(path.expression) !== 'join' || + path.arguments.length !== 2 || + path.arguments[0].getText() !== 'import.meta.dirname' || + path.arguments[1] !== node + ) { + return false + } + const program = path.parent + if ( + !ts.isPropertyAssignment(program) || + !ts.isIdentifier(program.name) || + program.name.text !== 'program' + ) { + return false + } + const spec = program.parent + const call = spec.parent + return ( + ts.isObjectLiteralExpression(spec) && + ts.isCallExpression(call) && + call.arguments[0] === spec && + ts.isIdentifier(call.expression) && + importedMethods.has(call.expression.text) + ) +} + +export function hasWindowsNativeBatchReferenceViolation( + relativePath: string, + contents: string, + literals: readonly string[] +): boolean { + const capture = references.get(relativePath) + if (!capture) { + return literals.length > 0 + } + if ( + literals.some( + (literal) => + literal !== capture && !(relativePath === initializer && literal === 'VsDevCmd.bat') + ) + ) { + return true + } + const modules = collectModuleSpecifiers(relativePath, contents) + if (modules.some((module) => module === 'child_process' || module === 'node:child_process')) { + return true + } + const expectedMethod = executors.get(relativePath) + if ((expectedMethod || relativePath === observer) && !modules.includes('@orca/process-host')) { + return true + } + if (!expectedMethod && relativePath !== observer && modules.includes('@orca/process-host')) { + return true + } + const source = ts.createSourceFile(relativePath, contents, ts.ScriptTarget.Latest, true) + const importedMethods = new Set() + const calls: ts.CallExpression[] = [] + const programs: ts.Expression[] = [] + const captureReferences: ts.StringLiteral[] = [] + let violation = false + let developerCommandDeclarations = 0 + function visit(node: ts.Node): void { + if ( + ts.isImportDeclaration(node) && + ts.isStringLiteral(node.moduleSpecifier) && + node.moduleSpecifier.text === '@orca/process-host' + ) { + const bindings = node.importClause?.namedBindings + if (bindings && ts.isNamedImports(bindings)) { + for (const binding of bindings.elements) { + if ((binding.propertyName ?? binding.name).text === expectedMethod) { + importedMethods.add(binding.name.text) + } + } + } + } + if (ts.isCallExpression(node)) { + calls.push(node) + const method = callName(node.expression) + if (rawProcessMethods.has(method)) { + violation = true + } + if (!expectedMethod && relativePath !== observer && processHostMethods.has(method)) { + violation = true + } + } + if ( + ts.isPropertyAssignment(node) && + ((ts.isIdentifier(node.name) && node.name.text === 'program') || + (ts.isStringLiteral(node.name) && node.name.text === 'program') || + (ts.isComputedPropertyName(node.name) && literalString(node.name.expression) === 'program')) + ) { + programs.push(node.initializer) + } + if (ts.isShorthandPropertyAssignment(node) && node.name.text === 'program') { + programs.push(node.name) + } + if (relativePath === initializer) { + if (ts.isStringLiteral(node) && node.text === capture) { + captureReferences.push(node) + } + if ( + ts.isStringLiteral(node) && + node.text === 'VsDevCmd.bat' && + !isDeveloperCommandMetadata(node) + ) { + violation = true + } + if (ts.isIdentifier(node) && node.text === 'devcmd') { + if (!isDeveloperCommandUse(node)) { + violation = true + } + if (ts.isVariableDeclaration(node.parent) && node.parent.name === node) { + developerCommandDeclarations += 1 + } + } + } + ts.forEachChild(node, visit) + } + visit(source) + if ( + expectedMethod && + !calls.some( + (call) => ts.isIdentifier(call.expression) && importedMethods.has(call.expression.text) + ) + ) { + violation = true + } + if (relativePath === initializer && developerCommandDeclarations !== 1) { + violation = true + } + if (captureReferences.some((node) => !isProcessHostCapture(node, importedMethods))) { + violation = true + } + // Reuse package-path folding for process-host's named program option. + return ( + violation || + hasNodeModulesBinSpawn(contents) || + programs.some((program) => + hasNodeModulesBinSpawn(`${contents}\nspawnSync(${program.getText(source)})`) + ) + ) +} diff --git a/config/scripts/windows-native-benchmark-observer.mjs b/config/scripts/windows-native-benchmark-observer.mjs new file mode 100644 index 00000000000..8694f5b7706 --- /dev/null +++ b/config/scripts/windows-native-benchmark-observer.mjs @@ -0,0 +1,118 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { writeFileSync } from 'node:fs' +import { createRequire } from 'node:module' +import { basename } from 'node:path' +import { performance } from 'node:perf_hooks' + +export const compilerEnvironmentKey = + /^(PATH|INCLUDE|LIB|LIBPATH|VCINSTALLDIR|VSINSTALLDIR|VisualStudioVersion|VSCMD_.+|VCTools.+|WindowsSDK.+|UniversalCRT.+|UCRTVersion)$/i +export const hash = (value) => createHash('sha256').update(value).digest('hex') + +export function environmentReceipt(environment) { + const entries = Object.entries(environment) + .filter(([, value]) => value !== undefined) + .map(([name, value]) => [name.toLowerCase(), String(value)]) + .sort(([left], [right]) => left.localeCompare(right)) + assert.equal( + new Set(entries.map(([name]) => name)).size, + entries.length, + 'Duplicate environment keys' + ) + return { + hash: hash(JSON.stringify(entries)), + versions: Object.fromEntries( + entries.filter( + ([name, value]) => + /^(visualstudioversion|vscmd_ver|windowssdkversion|vctoolsversion|vscmd_arg_tgt_arch)$/.test( + name + ) && /^(?:[\d.]+\\?|x64|amd64|arm64)$/.test(value) + ) + ), + compiler: Object.fromEntries( + entries + .filter(([name]) => compilerEnvironmentKey.test(name)) + .map(([name, value]) => [name, hash(value)]) + ) + } +} + +export function changedEnvironmentNames(before, after) { + const normalize = (environment) => + new Map(Object.entries(environment).map(([name, value]) => [name.toLowerCase(), value])) + const original = normalize(before) + const prepared = normalize(after) + return [...new Set([...original.keys(), ...prepared.keys()])] + .filter((name) => original.get(name) !== prepared.get(name)) + .sort() +} + +export function processPhase(spec) { + const name = basename(spec.program).toLowerCase() + if (name === 'vswhere.exe') { + return 'discovery' + } + if (name === 'capture-windows-native-environment.cmd') { + return 'capture' + } + if (spec.args?.some((arg) => arg.endsWith('ensure-native-runtime.mjs'))) { + return spec.args.includes('--check-only') ? 'probe' : 'preparation' + } + return spec.args?.includes('rebuild') ? 'addon-build' : 'other' +} + +export function installNativeBenchmarkObserver(api, output, environment = process.env) { + const original = api.runProcessSync + const parent = environmentReceipt(environment) + const receipt = { parentBefore: parent, parentAfter: parent, calls: [] } + writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`) + api.runProcessSync = (spec) => { + const startedAt = new Date().toISOString() + const start = performance.now() + const childEnvironment = spec.env ?? environment + const call = { + phase: processPhase(spec), + program: basename(spec.program), + args: spec.args, + startedAt, + environment: environmentReceipt(childEnvironment), + changedNames: changedEnvironmentNames(environment, childEnvironment) + } + try { + const result = original(spec) + Object.assign(call, { + code: result.code, + timedOut: result.timedOut, + outputTruncated: result.outputTruncated, + stdoutSha256: hash(result.stdout ?? ''), + stderrSha256: hash(result.stderr ?? ''), + stdoutBytes: Buffer.byteLength(result.stdout ?? ''), + stderrBytes: Buffer.byteLength(result.stderr ?? '') + }) + return result + } catch (error) { + call.error = error.message + throw error + } finally { + call.durationMs = performance.now() - start + call.finishedAt = new Date().toISOString() + receipt.calls.push(call) + receipt.parentAfter = environmentReceipt(environment) + assert.equal( + receipt.parentAfter.hash, + parent.hash, + 'Compiler setup changed its parent environment' + ) + writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`) + } + } + return receipt +} + +if (process.env.ORCA_NATIVE_BENCHMARK_TRACE) { + const require = createRequire(import.meta.url) + installNativeBenchmarkObserver( + require('@orca/process-host'), + process.env.ORCA_NATIVE_BENCHMARK_TRACE + ) +} diff --git a/config/scripts/windows-native-preparation-benchmark.test.mjs b/config/scripts/windows-native-preparation-benchmark.test.mjs new file mode 100644 index 00000000000..fa5ea5253aa --- /dev/null +++ b/config/scripts/windows-native-preparation-benchmark.test.mjs @@ -0,0 +1,212 @@ +import assert from 'node:assert/strict' +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { test } from 'vitest' +import { parse } from 'yaml' +import { runProcess } from '@orca/process-host' +import { + assertNativeColdProbe, + assertNoHeaderDownloads, + validateObservedPreparation +} from './benchmark-windows-native-preparation.mjs' +import { + changedEnvironmentNames, + environmentReceipt, + installNativeBenchmarkObserver +} from './windows-native-benchmark-observer.mjs' + +const original = { Path: 'unchanged', TOKEN: 'private' } +const parent = environmentReceipt(original) +const captureEnvironment = { ...original, VCINSTALLDIR: 'compiler' } +const call = (phase, code = 0, prepared = false) => ({ + phase, + code, + environment: prepared ? environmentReceipt(captureEnvironment) : parent, + changedNames: prepared ? ['vcinstalldir'] : [] +}) +const receipt = (calls) => ({ parentBefore: parent, parentAfter: parent, calls }) +const candidate = () => + receipt([call('probe', 1), call('discovery'), call('capture'), call('preparation', 0, true)]) + +for (const [variant, warm, calls] of [ + [ + 'control', + false, + [call('addon-build'), call('addon-build'), call('addon-build'), call('addon-build')] + ], + ['control', true, []], + ['candidate', false, candidate().calls], + ['candidate', true, [call('probe'), call('preparation')]], + [ + 'fallback', + false, + [call('probe', 1), call('discovery'), call('capture', 7), call('preparation')] + ], + ['fallback', true, [call('probe'), call('preparation')]] +]) { + test(`qualifies ${variant} ${warm ? 'warm' : 'cold'} receipts`, () => + validateObservedPreparation(receipt(calls), variant, warm)) +} + +test('fails closed on an unobserved or unchanged candidate', () => { + assert.throws(() => validateObservedPreparation(receipt([]), 'candidate'), /discovery count/) + const missingCapture = candidate() + missingCapture.calls.splice(2, 1) + assert.throws( + () => validateObservedPreparation(missingCapture, 'candidate'), + /developer command count/ + ) + const unchanged = candidate() + unchanged.calls[3] = call('preparation') + assert.throws(() => validateObservedPreparation(unchanged, 'candidate'), /not applied/) +}) + +test('rejects failed preparation and unrelated or persistent environment changes', () => { + for (const mutate of [ + (value) => { + value.calls[3].code = 2 + }, + (value) => { + value.calls[3].changedNames = ['token'] + }, + (value) => { + value.parentAfter = environmentReceipt(captureEnvironment) + }, + (value) => { + value.calls[2].timedOut = true + } + ]) { + const value = candidate() + mutate(value) + assert.throws(() => validateObservedPreparation(value, 'candidate')) + } + assert.throws( + () => + validateObservedPreparation( + receipt([call('probe'), call('preparation', 0, true)]), + 'candidate', + true + ), + /changed compiler/ + ) +}) + +test('requires all four initial addon failures and excludes timed header downloads', () => { + const result = { + code: 1, + stderr: [ + 'node-pty', + '@orca/windows-registry', + '@orca/windows-security', + '@vscode/windows-process-tree' + ] + .map((name) => `${name}: unavailable`) + .join('\n') + } + assertNativeColdProbe(result) + assert.throws(() => + assertNativeColdProbe({ ...result, stderr: result.stderr.replace('node-pty:', 'missing:') }) + ) + assertNoHeaderDownloads({ stdout: '', stderr: 'gyp info find VS using VS2026' }) + assert.throws( + () => + assertNoHeaderDownloads({ + stdout: '', + stderr: 'gyp http GET https://nodejs.org/header.tar.gz' + }), + /downloaded/ + ) +}) + +test('uses case-insensitive environment comparisons and never publishes values', () => { + assert.equal( + environmentReceipt(original).hash, + environmentReceipt({ PATH: 'unchanged', token: 'private' }).hash + ) + assert.deepEqual( + changedEnvironmentNames(original, { + PATH: 'unchanged', + TOKEN: 'private', + VCINSTALLDIR: 'compiler' + }), + ['vcinstalldir'] + ) + assert(!JSON.stringify(parent).includes('private')) + assert.throws(() => environmentReceipt({ PATH: 'first', Path: 'second' }), /Duplicate/) +}) + +test('writes an empty warm trace and observes calls without changing execution', () => { + const directory = mkdtempSync(join(tmpdir(), 'orca-native-observer-test-')) + try { + const output = join(directory, 'trace.json') + const result = { code: 0, stdout: 'original stdout', stderr: '', timedOut: false } + const seen = [] + const api = { + runProcessSync: (spec) => { + seen.push(spec) + return result + } + } + installNativeBenchmarkObserver(api, output, original) + assert.deepEqual(JSON.parse(readFileSync(output, 'utf8')).calls, []) + const spec = { program: 'vswhere.exe', args: ['-latest'], stdio: 'pipe' } + assert.equal(api.runProcessSync(spec), result) + assert.deepEqual(seen, [spec]) + const value = JSON.parse(readFileSync(output, 'utf8')) + assert.equal(value.calls[0].phase, 'discovery') + assert.equal(value.calls[0].stdoutBytes, result.stdout.length) + assert.equal(value.parentBefore.hash, value.parentAfter.hash) + assert(!readFileSync(output, 'utf8').includes('original stdout')) + } finally { + rmSync(directory, { recursive: true, force: true }) + } +}) + +test('preload observes the CJS process-host export imported by production ESM code', async () => { + const directory = mkdtempSync(join(tmpdir(), 'orca-native-observer-esm-test-')) + try { + const trace = join(directory, 'trace.json') + const program = `import { runProcessSync } from '@orca/process-host'; const value = runProcessSync({program:process.execPath,args:['-e','process.stdout.write("proof")']}); if(value.stdout !== 'proof') process.exit(2);` + const result = await runProcess({ + program: process.execPath, + args: [ + '--import', + pathToFileURL(join(import.meta.dirname, 'windows-native-benchmark-observer.mjs')).href, + '--input-type=module', + '-e', + program + ], + cwd: join(import.meta.dirname, '../..'), + env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1', ORCA_NATIVE_BENCHMARK_TRACE: trace } + }) + assert.equal(result.code, 0, result.stderr) + const value = JSON.parse(readFileSync(trace, 'utf8')) + assert.equal(value.calls.length, 1) + assert.equal(value.calls[0].code, 0) + } finally { + rmSync(directory, { recursive: true, force: true }) + } +}) + +test('manual workflow freezes source and limits Windows occupancy without changing a PR gate', () => { + const workflow = parse( + readFileSync( + join(import.meta.dirname, '../../.github/workflows/windows-native-preparation-benchmark.yml'), + 'utf8' + ) + ) + assert.deepEqual(Object.keys(workflow.on), ['workflow_dispatch']) + assert.equal(workflow.jobs.compare.strategy['max-parallel'], 2) + assert.deepEqual(workflow.jobs.compare.strategy.matrix.os, ['windows-2022', 'windows-11-arm']) + assert.equal(workflow.jobs.compare.steps[0].with.ref, '${{ inputs.source_sha }}') + const install = workflow.jobs.compare.steps.find( + (step) => step.uses === './.github/actions/install-node-dependencies' + ) + assert.equal(install.with['native-runtime'], 'none') + assert.equal(install.with['node-version'], '24.21.0') + assert.equal(install.with['cache-pnpm-store'], 'false') + assert.equal(workflow.jobs.compare.steps.at(-1).if, 'always()') + assert(!Object.hasOwn(workflow.jobs, 'verify')) +}) diff --git a/config/scripts/windows-native-toolchain-routing.test.mjs b/config/scripts/windows-native-toolchain-routing.test.mjs new file mode 100644 index 00000000000..a8e066b1610 --- /dev/null +++ b/config/scripts/windows-native-toolchain-routing.test.mjs @@ -0,0 +1,55 @@ +import { describe, expect, it, vi } from 'vitest' +import { classifyPrJobs } from './pr-code-change-scope.mjs' +import { classifyNodeServerChanges } from './node-server-change-scope.mjs' +import { nodeServerQualification } from './node-server-qualification.mjs' + +const toolchainSources = [ + 'config/scripts/initialize-windows-native-toolchain.mjs', + 'config/scripts/capture-windows-native-environment.cmd' +] + +describe('Windows native toolchain CI routing', () => { + it.each(toolchainSources)( + 'prepares native caches and packages for a change to %s alone', + (file) => { + expect(classifyPrJobs([file])).toMatchObject({ + should_run: true, + native_cache_changed: true, + package: true, + package_windows: true + }) + } + ) + + it.each(toolchainSources)( + 'selects both Windows server runners for a change to %s alone', + async (file) => { + const collect = vi.fn(async () => new Set()) + for (const options of [ + {}, + { deferGraph: true }, + { modifiedFiles: [file] }, + { deferGraph: true, modifiedFiles: [file] } + ]) { + const scope = await classifyNodeServerChanges([file], collect, options) + expect(scope).toEqual({ shouldRun: true, reason: `Build or CI input changed: ${file}` }) + expect(nodeServerQualification([file], scope)).toEqual({ + qualification: false, + runners: ['ubuntu-22.04', 'windows-2022', 'windows-11-arm'] + }) + } + expect(collect).not.toHaveBeenCalled() + } + ) + + it('preserves unrelated build-script routing', async () => { + const file = 'config/scripts/unrelated-toolchain.mjs' + expect(classifyPrJobs([file])).toMatchObject({ + test: true, + native_cache_changed: false, + package: false, + package_windows: false + }) + expect((await classifyNodeServerChanges([file], async () => new Set())).shouldRun).toBe(false) + }) +}) 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/package.json b/mobile/package.json index bd704db5504..565b35af925 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -54,6 +54,8 @@ "fflate": "0.8.3", "lowlight": "^3.3.0", "lucide-react-native": "^1.14.0", + "markdown-it": "^14.3.2", + "markdown-it-footnote": "^4.0.0", "mermaid": "11.17.2", "react": "19.2.8", "react-dom": "19.2.8", @@ -67,6 +69,7 @@ "react-native-web": "^0.21.3", "react-native-webview": "13.16.2", "react-native-worklets": "^0.8.3", + "remend": "1.4.0", "tweetnacl": "^1.0.3", "ws": "^8.22.0", "zod": "~4.6.5", @@ -74,6 +77,9 @@ }, "devDependencies": { "@orca/process-host": "link:../src/packages/process-host", + "@types/markdown-it": "14.1.2", + "@types/markdown-it-footnote": "^3.0.4", + "@types/mdast": "^4.0.4", "@types/react": "~19.2.18", "@types/react-test-renderer": "19.1.0", "@types/ws": "^8.18.1", diff --git a/mobile/pnpm-lock.yaml b/mobile/pnpm-lock.yaml index ce6b6c4e37d..1602c4ea75f 100644 --- a/mobile/pnpm-lock.yaml +++ b/mobile/pnpm-lock.yaml @@ -275,6 +275,12 @@ importers: lucide-react-native: specifier: ^1.14.0 version: 1.14.0(react-native-svg@15.15.5(react-native@0.83.10(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.8))(react@19.2.8))(react-native@0.83.10(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.8))(react@19.2.8) + markdown-it: + specifier: ^14.3.2 + version: 14.3.2 + markdown-it-footnote: + specifier: ^4.0.0 + version: 4.0.0 mermaid: specifier: 11.17.2 version: 11.17.2 @@ -314,6 +320,9 @@ importers: react-native-worklets: specifier: ^0.8.3 version: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(react-native@0.83.10(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.18)(react@19.2.8))(react@19.2.8) + remend: + specifier: 1.4.0 + version: 1.4.0 tweetnacl: specifier: ^1.0.3 version: 1.0.3 @@ -330,6 +339,15 @@ importers: '@orca/process-host': specifier: link:../src/packages/process-host version: link:../src/packages/process-host + '@types/markdown-it': + specifier: 14.1.2 + version: 14.1.2 + '@types/markdown-it-footnote': + specifier: ^3.0.4 + version: 3.0.4 + '@types/mdast': + specifier: ^4.0.4 + version: 4.0.4 '@types/react': specifier: ~19.2.18 version: 19.2.18 @@ -2883,6 +2901,24 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it-footnote@3.0.4': + resolution: {integrity: sha512-XJ6n+v+2u+2gmYLSHcxyoNT/YrgrKvHuDJQlykFjuxCQCr86P2/fx1V6/0lcKxv5cSIlCaJ6sUcNS3zDI7I+LA==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/markdown-it@14.2.0': + resolution: {integrity: sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/node@25.6.0': resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} @@ -5443,6 +5479,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.2: + resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -5514,6 +5553,13 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + markdown-it-footnote@4.0.0: + resolution: {integrity: sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ==} + + markdown-it@14.3.2: + resolution: {integrity: sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==} + hasBin: true + marked@16.4.2: resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} engines: {node: '>= 20'} @@ -5529,6 +5575,9 @@ packages: mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + mdurl@2.1.0: + resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==} + memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} @@ -6071,6 +6120,10 @@ packages: psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -6281,6 +6334,9 @@ packages: resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} hasBin: true + remend@1.4.0: + resolution: {integrity: sha512-mHiYhhwphs1IgIZmkToKLwJn0U99IjPXuutkA2acrNcG0vlEq2G4TGnd2eT0saIHwh2KjaCpvleHcsd5SAz96A==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -6840,6 +6896,9 @@ packages: resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -10239,6 +10298,28 @@ snapshots: '@types/json5@0.0.29': {} + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it-footnote@3.0.4': + dependencies: + '@types/markdown-it': 14.2.0 + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/markdown-it@14.2.0': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + '@types/node@25.6.0': dependencies: undici-types: 7.19.2 @@ -13388,6 +13469,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.2: + dependencies: + uc.micro: 2.1.0 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -13455,6 +13540,17 @@ snapshots: dependencies: tmpl: 1.0.5 + markdown-it-footnote@4.0.0: {} + + markdown-it@14.3.2: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.2 + mdurl: 2.1.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + marked@16.4.2: {} marky@1.3.0: {} @@ -13463,6 +13559,8 @@ snapshots: mdn-data@2.0.14: {} + mdurl@2.1.0: {} + memoize-one@5.2.1: {} memoize-one@6.0.0: {} @@ -14244,6 +14342,8 @@ snapshots: dependencies: punycode: 2.3.1 + punycode.js@2.3.1: {} + punycode@2.3.1: {} pure-rand@6.1.0: {} @@ -14520,6 +14620,8 @@ snapshots: dependencies: jsesc: 3.1.0 + remend@1.4.0: {} + require-directory@2.1.1: {} requires-port@1.0.0: {} @@ -15102,6 +15204,8 @@ snapshots: ua-parser-js@1.0.41: {} + uc.micro@2.1.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -15408,3 +15512,4 @@ snapshots: dependencies: '@types/emscripten': 1.41.5 type-fest: 5.6.0 + diff --git a/mobile/src/components/MobileMarkdown.file-links.test.ts b/mobile/src/components/MobileMarkdown.file-links.test.ts index c2243cd6cc3..f6ac87ace55 100644 --- a/mobile/src/components/MobileMarkdown.file-links.test.ts +++ b/mobile/src/components/MobileMarkdown.file-links.test.ts @@ -62,9 +62,10 @@ describe('MobileMarkdown file links', () => { expect(pressables(tree)).toEqual([]) }) - it('preserves nested labels and empty image labels', () => { + it('links the inner label of a nested bracket, and names an empty image label', () => { const tree = render('[[nested](https://example.com) ![](https://example.com/image)') - pressByText(tree, '[nested') + expect(flattenText(tree.root)).toBe('[nested image') + pressByText(tree, 'nested') expect(openURL).toHaveBeenLastCalledWith('https://example.com') pressByText(tree, 'image') expect(openURL).toHaveBeenLastCalledWith('https://example.com/image') diff --git a/mobile/src/components/MobileMarkdown.rendering.test.tsx b/mobile/src/components/MobileMarkdown.rendering.test.tsx new file mode 100644 index 00000000000..abbcfd39204 --- /dev/null +++ b/mobile/src/components/MobileMarkdown.rendering.test.tsx @@ -0,0 +1,164 @@ +import { createElement } from 'react' +import { act, create, type ReactTestInstance, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { MobileMarkdown } from './MobileMarkdown' + +const openURL = vi.fn((_url: string) => Promise.resolve()) + +vi.mock('react-native', () => ({ + Linking: { openURL: (url: string) => openURL(url) }, + Platform: { OS: 'ios' }, + Pressable: 'Pressable', + ScrollView: 'ScrollView', + StyleSheet: { create: (styles: unknown) => styles, hairlineWidth: 1 }, + Text: 'Text', + View: 'View' +})) +vi.mock('./pr-sidebar/MermaidDiagram', () => ({ MermaidDiagram: 'MermaidDiagram' })) + +let renderer: ReactTestRenderer | null = null + +afterEach(() => { + act(() => renderer?.unmount()) + renderer = null + openURL.mockClear() +}) + +function render(content: string, growing = false): ReactTestRenderer { + act(() => { + renderer = create(createElement(MobileMarkdown, { content, growing })) + }) + return renderer! +} + +function flatten(node: ReactTestInstance | string): string { + return typeof node === 'string' ? node : node.children.map(flatten).join('') +} + +const isText = (node: ReactTestInstance): boolean => String(node.type) === 'Text' + +/** Every Text not inside another Text, in reading order: what the reader sees, block by block. */ +function texts(tree: ReactTestRenderer): string[] { + return tree.root + .findAll((node) => isText(node) && !(node.parent && hasTextAncestor(node.parent))) + .map(flatten) +} + +function hasTextAncestor(node: ReactTestInstance): boolean { + return isText(node) || (node.parent !== null && hasTextAncestor(node.parent)) +} + +function press(tree: ReactTestRenderer, label: string): void { + const target = tree.root.find( + (node) => isText(node) && typeof node.props.onPress === 'function' && flatten(node) === label + ) + act(() => target.props.onPress()) +} + +describe('MobileMarkdown rendering', () => { + it.each([ + [ + 'C1: nested lists keep their own markers and numbers', + '1. **Frontend**\n - React\n - Vite\n2. Backend', + ['1.', 'Frontend', '-', 'React', '-', 'Vite', '2.', 'Backend'] + ], + [ + 'C1: blank lines keep one list, and a list cut short keeps its numbers', + '1. Install\n\n Pulls in everything.\n\n2. Build\n\nDone.\n\n3. Ship', + ['1.', 'Install', 'Pulls in everything.', '2.', 'Build', 'Done.', '3.', 'Ship'] + ], + [ + 'C1: a wrapped item stays one item', + '- a long item\nwrapped at sixty\n- next', + ['-', 'a long item\nwrapped at sixty', '-', 'next'] + ], + [ + 'C1: a number starting a wrapped line is not a list', + 'We counted to\n42. Then we stop.', + ['We counted to\n42. Then we stop.'] + ], + [ + 'C2: code, links and emphasis inside bold', + 'Edit **`file.ts`** per **[docs](https://x.com)**, ***both***, *a **b** c*, **bold _ital_**', + ['Edit file.ts per docs, both, a b c, bold ital'] + ], + [ + 'C4: a fence with an unusual language tag', + '```c++\nint x;\n```\nAfter.\n\n```ts title="a.ts"\nconst a = 1\n```', + ['c++', 'int x;', 'After.', 'ts', 'const a = 1'] + ], + [ + 'C5: tilde and four-backtick fences, with HTML inside left alone', + '~~~html\n
Hi
\n~~~\n\n````md\n```js\nx\n```\n````', + ['html', '
Hi
', 'md', '```js\nx\n```'] + ], + [ + 'C6: a fence under a list step', + '1. Install:\n ```bash\n pnpm i\n ```\n2. Run', + ['1.', 'Install:', 'bash', 'pnpm i', '2.', 'Run'] + ], + [ + 'C7: a table right under a sentence', + 'Here:\n| a | b |\n| --- | --- |\n| 1 | 2 |', + ['Here:', 'a', 'b', '1', '2'] + ], + ['C8: short separator rows', '| a | b |\n|:-|-:|\n| 1 | 2 |', ['a', 'b', '1', '2']], + [ + 'C9: a line break inside a table cell', + '| Flag | Note |\n| --- | --- |\n| -f | Skips checks
Use with care |', + ['Flag', 'Note', '-f', 'Skips checks\nUse with care'] + ], + ['C14: backslash escapes', '\\*not italic\\* a\\_b a \\| b \\#', ['*not italic* a_b a | b #']], + ['C15: lone asterisks', '2 * 3 * 4 and *.ts and *.tsx', ['2 * 3 * 4 and *.ts and *.tsx']], + ['C16: inline code holding a backtick', '``a`b``', ['a`b']], + [ + 'C18: blocks inside a quote', + '> - a\n>\n> ```sh\n> ls\n> ```\n> > inner', + ['-', 'a', 'sh', 'ls', 'inner'] + ], + ['C19: underlined headings and closing hashes', 'Title\n=====\n\n## Sub ##', ['Title', 'Sub']], + [ + 'a README header block reads as its heading', + '

Orca

\n\nText', + ['Orca', 'Text'] + ] + ])('%s', (_name, content, expected) => { + expect(texts(render(content))).toEqual(expected) + }) + + it('C12: opens a link with brackets in it, and one with a title, at their own address', () => { + const tree = render( + '[wiki](https://en.wikipedia.org/wiki/Foo_(bar)) and [docs](https://x.com/a "The docs")' + ) + press(tree, 'wiki') + expect(openURL).toHaveBeenLastCalledWith('https://en.wikipedia.org/wiki/Foo_(bar)') + press(tree, 'docs') + expect(openURL).toHaveBeenLastCalledWith('https://x.com/a') + }) + + it('C13: opens an angle-bracket link without its brackets', () => { + const tree = render('Open .') + expect(texts(tree)).toEqual(['Open http://localhost:3000.']) + press(tree, 'http://localhost:3000') + expect(openURL).toHaveBeenLastCalledWith('http://localhost:3000') + }) + + it('C17: resolves a reference link and draws a footnote', () => { + const tree = render( + 'See [the docs][1] and a note[^1].\n\n[1]: https://example.com/docs\n\n[^1]: The note.' + ) + expect(texts(tree)).toEqual(['See the docs and a note[1].', '[1]', 'The note.']) + press(tree, 'the docs') + expect(openURL).toHaveBeenLastCalledWith('https://example.com/docs') + }) + + it('C21: closes open markup while a reply grows, and never links a half-written address', () => { + expect(texts(render('2 * 3 is six.\n\nRead **src/main/ses', true))).toEqual([ + '2 * 3 is six.', + 'Read src/main/ses' + ]) + const tree = render('See [config loader](https://exa', true) + expect(texts(tree)).toEqual(['See config loader']) + expect(tree.root.findAll((node) => typeof node.props.onPress === 'function')).toEqual([]) + }) +}) diff --git a/mobile/src/components/MobileMarkdown.test.ts b/mobile/src/components/MobileMarkdown.test.ts index e85eb1ab197..455c05f153d 100644 --- a/mobile/src/components/MobileMarkdown.test.ts +++ b/mobile/src/components/MobileMarkdown.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest' import { isMobileMermaidLanguage } from './mobile-mermaid-language' import { normalizeMobileMarkdownPreviewHtml } from './mobile-markdown-preview-html' -import { parseMobileMarkdown } from './mobile-markdown-parser' describe('isMobileMermaidLanguage', () => { it('matches mermaid case-insensitively after trim', () => { @@ -18,63 +17,7 @@ describe('isMobileMermaidLanguage', () => { }) }) -describe('parseMobileMarkdown', () => { - it('parses mermaid fences as code blocks with language mermaid', () => { - expect(parseMobileMarkdown('```mermaid\ngraph TD; A-->B\n```')).toEqual([ - { type: 'code', text: 'graph TD; A-->B', language: 'mermaid', closed: true } - ]) - expect(isMobileMermaidLanguage('mermaid')).toBe(true) - }) - - it('marks an unterminated fence as not closed while it streams', () => { - expect(parseMobileMarkdown('```mermaid\ngraph TD; A-->B')).toEqual([ - { type: 'code', text: 'graph TD; A-->B', language: 'mermaid', closed: false } - ]) - expect(parseMobileMarkdown('```mermaid\ngraph TD; A-->B\n```')[0]).toMatchObject({ - closed: true - }) - }) - - it('parses GFM tables into table blocks', () => { - expect(parseMobileMarkdown('| Name | State |\n| --- | --- |\n| Orca | Open |')).toEqual([ - { - type: 'table', - headers: ['Name', 'State'], - rows: [['Orca', 'Open']] - } - ]) - }) - - it('keeps an escaped pipe inside the cell that escaped it', () => { - expect(parseMobileMarkdown('| Cmd | Note |\n| --- | --- |\n| a \\| b | c |')).toEqual([ - { - type: 'table', - headers: ['Cmd', 'Note'], - rows: [['a | b', 'c']] - } - ]) - }) - - it('ends a cell at the pipe following an escaped backslash', () => { - expect(parseMobileMarkdown('| A | B |\n| --- | --- |\n| x\\\\|y |')).toEqual([ - { - type: 'table', - headers: ['A', 'B'], - rows: [['x\\', 'y']] - } - ]) - }) - - it('parses standalone HTTPS images without folding them into paragraphs', () => { - expect(parseMobileMarkdown('![Screenshot](https://example.com/screen.png)')).toEqual([ - { - type: 'image', - alt: 'Screenshot', - url: 'https://example.com/screen.png' - } - ]) - }) - +describe('normalizeMobileMarkdownPreviewHtml', () => { it('normalizes common README HTML into readable Markdown preview text', () => { const normalized = normalizeMobileMarkdownPreviewHtml(`

diff --git a/mobile/src/components/MobileMarkdown.tsx b/mobile/src/components/MobileMarkdown.tsx index 31c92d8c9eb..e6db4813e98 100644 --- a/mobile/src/components/MobileMarkdown.tsx +++ b/mobile/src/components/MobileMarkdown.tsx @@ -1,33 +1,13 @@ -import { openExternalLink } from '../platform/external-link' -import { createMarkdownInlineMatcher, type MarkdownInlineMatch } from './markdown-inline-matcher' import { INLINE_TEXT_SELECTION } from './inline-text-selection' import { MobileSelectableText } from './MobileSelectableText' -import { - Fragment, - createElement, - createContext, - memo, - useContext, - useMemo, - type ComponentType, - type ReactNode -} from 'react' -import { Pressable, ScrollView, Text as NativeText, View, type TextProps } from 'react-native' +import { memo, useContext, useMemo, type ReactNode } from 'react' +import { Text as NativeText, View } from 'react-native' import { styles } from './mobile-markdown-styles' -import { - detectFilePathSegments, - isFilePathCodeSpan, - normalizeFilePath -} from './markdown-file-path-detection' -import { routeMarkdownHref } from './markdown-href-routing' -import { - isIntrawordUnderscoreToken, - trimAutolinkTrailingPunctuation -} from './markdown-inline-token-rules' -import { isMobileMermaidLanguage } from './mobile-mermaid-language' -import { useMobileMarkdownBlocks } from './use-mobile-markdown-blocks' +import { renderBlocks, type MarkdownBlockContext } from './mobile-markdown-blocks' +import { MarkdownText } from './mobile-markdown-text' +import { MarkdownTextContext, type MarkdownTextSetup } from './mobile-markdown-text-setup' +import { useMobileMarkdownTree } from './use-mobile-markdown-tree' import type { NativeChatVisualDirective } from '../../../src/shared/native-chat-visual-directive' -import { MermaidDiagram } from './pr-sidebar/MermaidDiagram' type Props = { content?: string @@ -47,188 +27,9 @@ type Props = { /** Native-chat assistant prose only: renders `::orca-visual{...}` directive lines. Without it, * a directive line is ordinary text. Must be referentially stable (this component is memoized). */ renderVisual?: (directive: NativeChatVisualDirective, index: number) => ReactNode -} - -const MAX_TABLE_ROWS = 40 -const MAX_TABLE_COLUMNS = 8 -/** Prose base size — passed to MermaidDiagram fallback mono text. */ -const MERMAID_BASE = 13 -type MarkdownTextSetup = { - TextComponent: ComponentType - /** Disable native selection only within the Android transcript. */ - androidTranscript: boolean - onLongPress?: () => void -} -const MarkdownTextContext = createContext({ - TextComponent: NativeText, - androidTranscript: false -}) - -function MarkdownText(props: TextProps): React.JSX.Element { - const { TextComponent, androidTranscript, onLongPress } = useContext(MarkdownTextContext) - if (!androidTranscript) { - return createElement(TextComponent, props) - } - // Override selection without changing the nested spans' inherited behavior. - return createElement(TextComponent, { - ...props, - ...(props.selectable === true ? { selectable: false } : {}), - ...(onLongPress && props.onPress ? { onLongPress } : {}) - }) -} - -// Web/mail hrefs open the system handler; file-target hrefs (file: URIs and -// scheme-less paths — the entire desktop file-link contract) go to onOpenFile. -function openMarkdownHref(href: string, onOpenFile?: (pathText: string) => void): void { - const route = routeMarkdownHref(href) - if (route.kind === 'web') { - // The seam, not react-native's `Linking`: this module is in the tasks page closure, and inside - // the shell's WebView `openURL` resolves without opening anything. - openExternalLink(route.url) - return - } - if (route.kind === 'file' && onOpenFile) { - onOpenFile(route.pathText) - } -} - -// Render a plain (non-token) text run, splitting out tappable file paths when -// onOpenFile is provided. Without it, paths stay plain text. -function renderTextRun( - text: string, - keyPrefix: string, - onOpenFile?: (pathText: string) => void -): ReactNode { - if (!onOpenFile) { - return text - } - const segments = detectFilePathSegments(text) - if (segments.length === 1 && segments[0]!.type === 'text') { - return text - } - return segments.map((segment, segmentIndex) => { - if (segment.type === 'file') { - return ( - onOpenFile(segment.path)} - > - {segment.value} - - ) - } - return {segment.value} - }) -} - -function renderInline(text: string, onOpenFile?: (pathText: string) => void): ReactNode[] { - const parts: ReactNode[] = [] - const pattern = createMarkdownInlineMatcher( - text, - /(`[^`]+`|~~[^~]+~~|\*\*[^*]+\*\*|__[^_]+__|\*[^*\n]+\*|_[^_\n]+_|https?:\/\/[^\s<]+)/g, - true - ) - let pendingStart = 0 - let match: MarkdownInlineMatch | null - - while ((match = pattern.exec())) { - const token = match[0] - // Intraword `_` runs (snake_case, dunder tails) are literal text per - // CommonMark; leaving them unflushed keeps surrounding file paths whole - // for detection in the eventual text run. - if (token.startsWith('_') && isIntrawordUnderscoreToken(text, match.index, token)) { - // Resume after the opener so real tokens inside the rejected span are still scanned. - pattern.lastIndex = match.index + 1 - continue - } - if (match.index > pendingStart) { - parts.push( - renderTextRun(text.slice(pendingStart, match.index), `t${pendingStart}`, onOpenFile) - ) - } - pendingStart = pattern.lastIndex - const key = `${match.index}:${token}` - const image = token.match(/^!\[([^\]]*)\]\(([^)]+)\)$/) - const link = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/) - if (image) { - parts.push( - openMarkdownHref(image[2]!, onOpenFile)} - > - {image[1] || 'image'} - - ) - } else if (link) { - parts.push( - openMarkdownHref(link[2]!, onOpenFile)} - > - {link[1]} - - ) - } else if (/^https?:\/\//i.test(token)) { - const { url, trailing } = trimAutolinkTrailingPunctuation(token) - parts.push( - openMarkdownHref(url, onOpenFile)} - > - {url} - - ) - if (trailing) { - parts.push({trailing}) - } - } else if (token.startsWith('`')) { - const code = token.slice(1, -1) - if (onOpenFile && isFilePathCodeSpan(code)) { - parts.push( - onOpenFile(normalizeFilePath(code.trim()))} - > - {code} - - ) - } else { - parts.push( - - {code} - - ) - } - } else if (token.startsWith('~~')) { - parts.push( - - {renderTextRun(token.slice(2, -2), `${key}i`, onOpenFile)} - - ) - } else if (token.startsWith('**') || token.startsWith('__')) { - parts.push( - - {renderTextRun(token.slice(2, -2), `${key}i`, onOpenFile)} - - ) - } else { - parts.push( - - {renderTextRun(token.slice(1, -1), `${key}i`, onOpenFile)} - - ) - } - } - - if (pendingStart < text.length) { - parts.push(renderTextRun(text.slice(pendingStart), `t${pendingStart}`, onOpenFile)) - } - return parts + /** The text is still being appended to: markup its end leaves open is closed, as on desktop, + * and a diagram stays code until the text stops. */ + growing?: boolean } function MobileMarkdownContent({ @@ -237,18 +38,13 @@ function MobileMarkdownContent({ rangeSelectable = false, textScale = 1, onOpenFile, - renderVisual + renderVisual, + growing = false }: Props) { // Interactive children own their touches and must forward the row action. const setup = useContext(MarkdownTextContext) - const rowLongPress = setup.androidTranscript ? setup.onLongPress : undefined const text = content?.trim() ?? '' - const { blocks, directives } = useMobileMarkdownBlocks(text, renderVisual !== undefined) - // Scale prose sizes; inline spans inherit fontSize from the wrapping Text. - const scaled = (size: number): { fontSize: number; lineHeight: number } | null => - textScale !== 1 ? { fontSize: size * textScale, lineHeight: (size + 6) * textScale } : null - const proseScale = scaled(13) - const listScale = scaled(14) + const { root, directives } = useMobileMarkdownTree(text, renderVisual !== undefined, growing) if (!text) { return fallback ? ( @@ -256,159 +52,24 @@ function MobileMarkdownContent({ ) : null } - const mermaidSourceOccurrences = new Map() + // Scale prose sizes; inline spans inherit fontSize from the wrapping Text. + const scaled = (size: number): { fontSize: number; lineHeight: number } | null => + textScale !== 1 ? { fontSize: size * textScale, lineHeight: (size + 6) * textScale } : null + const ctx: MarkdownBlockContext = { + growing, + onOpenFile, + listText: [styles.listText, scaled(14)], + rowLongPress: setup.androidTranscript ? setup.onLongPress : undefined, + directives, + renderVisual, + mermaidOccurrences: new Map() + } // Native-chat range selection is set on each block; nested inline spans inherit it. - return ( - {blocks.map((block, index) => { - if (block.type === 'visual') { - const directive = directives[block.index] - return directive && renderVisual ? ( - - {renderVisual(directive, block.index)} - - ) : null - } - if (block.type === 'heading') { - return ( - - {renderInline(block.text, onOpenFile)} - - ) - } - if (block.type === 'quote') { - return ( - - - {renderInline(block.text, onOpenFile)} - - - ) - } - if (block.type === 'code') { - // Mermaid fences render as diagrams (WebView), not as raw code — same as PR sidebar. - // Unclosed fences are still streaming: mounting the WebView per tick would - // reload its document up to 20x/sec, so they stay raw code until terminated. - if (isMobileMermaidLanguage(block.language) && block.closed) { - const occurrence = mermaidSourceOccurrences.get(block.text) ?? 0 - mermaidSourceOccurrences.set(block.text, occurrence + 1) - return ( - - ) - } - return ( - - {block.language ? ( - {block.language} - ) : null} - - {block.text} - - - ) - } - if (block.type === 'image') { - return ( - openMarkdownHref(block.url, onOpenFile)} - onLongPress={rowLongPress} - > - {block.alt || 'Open image'} - - {block.url} - - - ) - } - if (block.type === 'table') { - const visibleHeaders = block.headers.slice(0, MAX_TABLE_COLUMNS) - const visibleRows = block.rows.slice(0, MAX_TABLE_ROWS) - const hiddenRows = Math.max(0, block.rows.length - visibleRows.length) - const hiddenColumns = Math.max(0, block.headers.length - visibleHeaders.length) - return ( - - - - {visibleHeaders.map((header, cellIndex) => ( - - {renderInline(header, onOpenFile)} - - ))} - - {visibleRows.map((row, rowIndex) => ( - - {visibleHeaders.map((_, cellIndex) => ( - - {renderInline(row[cellIndex] ?? '', onOpenFile)} - - ))} - - ))} - {hiddenRows > 0 || hiddenColumns > 0 ? ( - - {hiddenRows > 0 ? `${hiddenRows} more rows` : ''} - {hiddenRows > 0 && hiddenColumns > 0 ? ' · ' : ''} - {hiddenColumns > 0 ? `${hiddenColumns} more columns` : ''} - - ) : null} - - - ) - } - if (block.type === 'list') { - return ( - - {block.items.map((item, itemIndex) => ( - - - {item.checked == null - ? block.ordered - ? `${itemIndex + 1}.` - : '-' - : item.checked - ? '[x]' - : '[ ]'} - - - {renderInline(item.text, onOpenFile)} - - - ))} - - ) - } - if (block.type === 'rule') { - return - } - return ( - - {block.text.split('\n').map((line, lineIndex) => ( - - {lineIndex > 0 ? '\n' : null} - {renderInline(line, onOpenFile)} - - ))} - - ) + {renderBlocks(root.children, ctx, { + style: [styles.paragraph, scaled(13)], + selectable: rangeSelectable })} ) 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/bottom-drawer-modal-host-scope.ts b/mobile/src/components/bottom-drawer-modal-host-scope.ts new file mode 100644 index 00000000000..2ec08b4e34d --- /dev/null +++ b/mobile/src/components/bottom-drawer-modal-host-scope.ts @@ -0,0 +1,33 @@ +import { createContext, useContext, useEffect, useRef } from 'react' + +type BottomDrawerModalHostScope = { + /** False only while a self-showing host is down; its drawers render nothing until it is up. */ + shown: boolean + /** Keeps a self-showing host up; null when the caller owns `visible`. */ + hold: (() => () => void) | null + claimBack: (dismiss: () => void) => () => void +} + +export const BottomDrawerModalHostContext = createContext(null) + +/** Non-null when a BottomDrawer is rendered inside a shared BottomDrawerModalHost and + * must therefore skip its own native Modal (the host owns the single Modal). */ +export function useBottomDrawerModalHost(): BottomDrawerModalHostScope | null { + return useContext(BottomDrawerModalHostContext) +} + +/** Back on the host's Modal reaches the host, not BackHandler; the newest claim takes it. */ +export function useBottomDrawerModalHostBackClaim(dismiss: (() => void) | null): void { + const claimBack = useContext(BottomDrawerModalHostContext)?.claimBack + const latest = useRef(dismiss) + useEffect(() => { + latest.current = dismiss + }) + const claimed = dismiss !== null + useEffect(() => { + if (!claimBack || !claimed) { + return + } + return claimBack(() => latest.current?.()) + }, [claimBack, claimed]) +} diff --git a/mobile/src/components/bottom-drawer-modal-host.test.tsx b/mobile/src/components/bottom-drawer-modal-host.test.tsx new file mode 100644 index 00000000000..360addfa7f6 --- /dev/null +++ b/mobile/src/components/bottom-drawer-modal-host.test.tsx @@ -0,0 +1,168 @@ +import { createElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, describe, expect, it, vi } from 'vitest' + +type Timing = { to: number; callback?: (finished: boolean) => void } + +const animations = vi.hoisted((): { running: Timing[] } => ({ running: [] })) + +vi.mock('../navigation/use-back-claim', () => ({ useBackClaim: () => {} })) +vi.mock('../platform/keyboard-occlusion', () => ({ + currentSoftKeyboardHeight: () => 0, + subscribeSoftKeyboard: () => () => {} +})) +vi.mock('react-native', () => ({ + Keyboard: { dismiss: () => {} }, + Modal: 'Modal', + Platform: { OS: 'android', select: (options: { android?: unknown }) => options.android }, + Pressable: 'Pressable', + ScrollView: 'ScrollView', + StyleSheet: { create: (styles: T) => styles, absoluteFillObject: {} }, + View: 'View', + useWindowDimensions: () => ({ width: 412, height: 900 }) +})) +vi.mock('react-native-safe-area-context', () => ({ + useSafeAreaInsets: () => ({ top: 24, bottom: 0, left: 0, right: 0 }) +})) +vi.mock('react-native-gesture-handler', () => { + const chain: Record = {} + for (const method of [ + 'activeOffsetY', + 'simultaneousWithExternalGesture', + 'onBegin', + 'onUpdate', + 'onEnd' + ]) { + chain[method] = () => chain + } + return { + Gesture: { Pan: () => chain, Native: () => chain }, + GestureDetector: 'GestureDetector', + GestureHandlerRootView: 'GestureHandlerRootView' + } +}) +vi.mock('react-native-reanimated', () => ({ + default: { View: 'AnimatedView', ScrollView: 'AnimatedScrollView' }, + useSharedValue: (initial: number) => ({ value: initial }), + useAnimatedStyle: () => ({}), + useAnimatedScrollHandler: () => () => {}, + withSpring: (to: number) => to, + withTiming: (to: number, _config?: unknown, callback?: (finished: boolean) => void) => { + animations.running.push({ to, callback }) + return to + }, + runOnJS: (fn: () => void) => fn, + interpolate: () => 0, + Extrapolation: { CLAMP: 'clamp' } +})) + +import { BottomDrawer } from './BottomDrawer' +import { BottomDrawerModalHost } from './bottom-drawer-modal-host' + +function finishAnimations(): void { + const finishing = animations.running + animations.running = [] + act(() => { + for (const animation of finishing) { + animation.callback?.(true) + } + }) +} + +type Layers = { detail: boolean; form: boolean } + +const closeDetail = vi.fn() +const closeForm = vi.fn() + +// Mirrors the Tasks surface: the Create Workspace form renders before the issue detail and is +// raised over it by zIndex, both inside one host. +function tasksSheets({ detail, form }: Layers) { + return ( + + + {createElement('Sheet', { name: 'form' })} + + + {createElement('Sheet', { name: 'detail' })} + + + ) +} + +let rendered: ReactTestRenderer | null = null + +function show(layers: Layers): void { + act(() => { + if (rendered) { + rendered.update(tasksSheets(layers)) + } else { + rendered = create(tasksSheets(layers)) + } + }) +} + +function nativeModals() { + return rendered?.root.findAll((node) => String(node.type) === 'Modal') ?? [] +} + +function sheetsInModal(): string[] { + return nativeModals().flatMap((modal) => + modal.findAll((node) => String(node.type) === 'Sheet').map((sheet) => String(sheet.props.name)) + ) +} + +function pressAndroidBack(): void { + act(() => nativeModals()[0]?.props.onRequestClose()) + finishAnimations() +} + +describe('BottomDrawerModalHost without a caller-owned visible', () => { + afterEach(() => { + act(() => rendered?.unmount()) + rendered = null + animations.running = [] + closeDetail.mockReset() + closeForm.mockReset() + }) + + // #21003: iOS refuses a native modal presented over a sibling one, so the form opened from an + // issue detail never appeared. Both sheets must live in the single native Modal. + it('opens a sheet over another inside the one native Modal', () => { + show({ detail: true, form: false }) + finishAnimations() + show({ detail: true, form: true }) + + expect(nativeModals()).toHaveLength(1) + expect(sheetsInModal().sort()).toEqual(['detail', 'form']) + }) + + it('keeps the Modal through the last close animation, then leaves none to swallow taps', () => { + show({ detail: true, form: true }) + finishAnimations() + + show({ detail: true, form: false }) + finishAnimations() + expect(sheetsInModal()).toEqual(['detail']) + + show({ detail: false, form: false }) + expect(sheetsInModal()).toEqual(['detail']) + finishAnimations() + expect(nativeModals()).toHaveLength(0) + }) + + it('hands Back to the sheet on top, then to the one under it', () => { + show({ detail: true, form: false }) + finishAnimations() + show({ detail: true, form: true }) + finishAnimations() + + pressAndroidBack() + expect(closeForm).toHaveBeenCalledTimes(1) + expect(closeDetail).not.toHaveBeenCalled() + + show({ detail: true, form: false }) + finishAnimations() + pressAndroidBack() + expect(closeDetail).toHaveBeenCalledTimes(1) + }) +}) diff --git a/mobile/src/components/bottom-drawer-modal-host.tsx b/mobile/src/components/bottom-drawer-modal-host.tsx index b31a31136e0..29e294739fe 100644 --- a/mobile/src/components/bottom-drawer-modal-host.tsx +++ b/mobile/src/components/bottom-drawer-modal-host.tsx @@ -1,28 +1,57 @@ -import { createContext, useContext, type ReactNode } from 'react' +import { useCallback, useMemo, useRef, useState, type ReactNode } from 'react' import { Modal } from 'react-native' - -const BottomDrawerModalHostContext = createContext(false) - -/** True when a BottomDrawer is rendered inside a shared BottomDrawerModalHost and - * must therefore skip its own native Modal (the host owns the single Modal). */ -export function useInsideBottomDrawerModalHost(): boolean { - return useContext(BottomDrawerModalHostContext) -} +import { BottomDrawerModalHostContext } from './bottom-drawer-modal-host-scope' type Props = { - visible: boolean - onRequestClose: () => void + /** Omit to show the host while any drawer inside is requested, shown or closing. */ + visible?: boolean + /** Omit to hand Back to the drawer that claimed it last. */ + onRequestClose?: () => void children: ReactNode } // Why: iOS cannot reliably dismiss one native modal and present another in the same -// beat. Flows that swap between sibling drawer modals (e.g. the Create Workspace form -// → its repository/agent pickers) dropped the incoming modal, leaving the sheet dead -// to taps. Hosting every drawer in ONE persistent native Modal makes those swaps -// in-window view changes instead, so no present/dismiss race can eat the transition. +// beat, and refuses outright to present one while a sibling is presented. Flows that +// swap or stack sibling drawer modals (e.g. the Create Workspace form → its pickers, or +// an issue detail → Create Workspace) dropped the incoming modal, leaving the sheet +// dead to taps. Hosting every drawer in ONE native Modal makes those swaps and stacks +// in-window view changes instead, ordered by zIndex. export function BottomDrawerModalHost({ visible, onRequestClose, children }: Props) { - if (!visible) { - return null + const [holds, setHolds] = useState(0) + const backClaims = useRef<(() => void)[]>([]) + const hold = useCallback(() => { + let held = true + setHolds((count) => count + 1) + return () => { + if (held) { + held = false + setHolds((count) => count - 1) + } + } + }, []) + const claimBack = useCallback((dismiss: () => void) => { + const claim = () => dismiss() + backClaims.current.push(claim) + return () => { + backClaims.current = backClaims.current.filter((entry) => entry !== claim) + } + }, []) + const selfShowing = visible === undefined + const shown = selfShowing ? holds > 0 : visible + const scope = useMemo( + () => ({ shown, hold: selfShowing ? hold : null, claimBack }), + [shown, selfShowing, hold, claimBack] + ) + const content = ( + + {children} + + ) + + if (!shown) { + // Why: a self-showing host keeps its drawers mounted while down so a request can raise it. + // They remount inside the Modal, which is safe because none of them is showing anything yet. + return selfShowing ? content : null } return ( backClaims.current.at(-1)?.())} > - - {children} - + {content} ) } diff --git a/mobile/src/components/keyed-bottom-drawer.tsx b/mobile/src/components/keyed-bottom-drawer.tsx index 83265cc079b..db7c6a82c6e 100644 --- a/mobile/src/components/keyed-bottom-drawer.tsx +++ b/mobile/src/components/keyed-bottom-drawer.tsx @@ -1,5 +1,6 @@ -import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react' +import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import { MountedBottomDrawer, type MountedBottomDrawerProps } from './mounted-bottom-drawer' +import { useBottomDrawerModalHost } from './bottom-drawer-modal-host-scope' // iOS cannot present a native Modal while another is still presented, even mid-close, so one // drawer owns what is on screen and swaps sheets only after the previous Modal has unmounted. @@ -70,8 +71,14 @@ export function KeyedBottomDrawer({ ) }, [released]) + const host = useBottomDrawerModalHost() + const holdHost = host?.hold + // Why: a self-showing host stays up from the request until this sheet's close has landed. + const wantsHost = sheet !== null || next.presented !== null || next.released !== null + useLayoutEffect(() => (holdHost && wantsHost ? holdHost() : undefined), [holdHost, wantsHost]) + const presented = next.presented - if (presented === null) { + if (presented === null || host?.shown === false) { return null } return ( diff --git a/mobile/src/components/markdown-inline-matcher.test.ts b/mobile/src/components/markdown-inline-matcher.test.ts index b252a434b0c..b1a6da3e66e 100644 --- a/mobile/src/components/markdown-inline-matcher.test.ts +++ b/mobile/src/components/markdown-inline-matcher.test.ts @@ -1,19 +1,14 @@ import { runInNewContext } from 'node:vm' import { describe, expect, it } from 'vitest' import { createMarkdownInlineMatcher } from './markdown-inline-matcher' -import { isIntrawordUnderscoreToken } from './markdown-inline-token-rules' import { parseInline } from './pr-sidebar/markdown-blocks' -const ORIGINAL_CHAT = - /(!\[[^\]]*\]\([^)]+\)|`[^`]+`|~~[^~]+~~|\*\*[^*]+\*\*|__[^_]+__|\*[^*\n]+\*|_[^_\n]+_|\[[^\]]+\]\([^)]+\)|https?:\/\/[^\s<]+)/g -const CHAT_OTHER = - /(`[^`]+`|~~[^~]+~~|\*\*[^*]+\*\*|__[^_]+__|\*[^*\n]+\*|_[^_\n]+_|https?:\/\/[^\s<]+)/g const ORIGINAL_REVIEW = /(`[^`]+`)|(\*\*[^*]+\*\*)|(__[^_]+__)|(\*[^*]+\*)|(_[^_]+_)|(\[[^\]]+\]\([^)]+\))/g const REVIEW_OTHER = /(`[^`]+`)|(\*\*[^*]+\*\*)|(__[^_]+__)|(\*[^*]+\*)|(_[^_]+_)/g -function tokens(text: string, chat: boolean, original: boolean) { - const pattern = new RegExp(chat ? ORIGINAL_CHAT : ORIGINAL_REVIEW) +function tokens(text: string, original: boolean) { + const pattern = new RegExp(ORIGINAL_REVIEW) const matcher = original ? { get lastIndex() { @@ -24,21 +19,17 @@ function tokens(text: string, chat: boolean, original: boolean) { }, exec: () => pattern.exec(text) } - : createMarkdownInlineMatcher(text, new RegExp(chat ? CHAT_OTHER : REVIEW_OTHER), chat) + : createMarkdownInlineMatcher(text, new RegExp(REVIEW_OTHER)) const result: Array<{ text: string; index: number; end: number }> = [] let match while ((match = matcher.exec())) { - if (chat && isIntrawordUnderscoreToken(text, match.index, match[0])) { - matcher.lastIndex = match.index + 1 - continue - } result.push({ text: match[0], index: match.index, end: matcher.lastIndex }) } return result } describe('mobile inline link scanning', () => { - it.each([false, true])('preserves original token order (chat=%s)', (chat) => { + it('preserves original token order', () => { const fragments = [ '[', ']', @@ -71,7 +62,7 @@ describe('mobile inline link scanning', () => { { length: 1 + (random() % 40) }, () => fragments[random() % fragments.length] ).join('') - expect(tokens(text, chat, false), text).toEqual(tokens(text, chat, true)) + expect(tokens(text, false), text).toEqual(tokens(text, true)) } }) diff --git a/mobile/src/components/markdown-inline-matcher.ts b/mobile/src/components/markdown-inline-matcher.ts index ba86597f32a..02e8e84994f 100644 --- a/mobile/src/components/markdown-inline-matcher.ts +++ b/mobile/src/components/markdown-inline-matcher.ts @@ -3,8 +3,7 @@ export type MarkdownInlineMatch = { 0: string; index: number; end: number } /** Merge a global non-link regex with links; search starts must advance between calls. */ export function createMarkdownInlineMatcher( text: string, - nonLinkPattern: RegExp, - images = false + nonLinkPattern: RegExp ): { lastIndex: number; exec: () => MarkdownInlineMatch | null } { let nextOther: MarkdownInlineMatch | null | undefined let nextLink: MarkdownInlineMatch | null | undefined @@ -26,8 +25,7 @@ export function createMarkdownInlineMatcher( noMoreLabels = true return null } - const image = images && open > from && text[open - 1] === '!' - if ((image || labelEnd > open + 1) && text[labelEnd + 1] === '(') { + if (labelEnd > open + 1 && text[labelEnd + 1] === '(') { if (destinationEnd < labelEnd + 2) { destinationEnd = text.indexOf(')', labelEnd + 2) } @@ -36,8 +34,7 @@ export function createMarkdownInlineMatcher( return null } if (destinationEnd > labelEnd + 2) { - const index = image ? open - 1 : open - return { 0: text.slice(index, destinationEnd + 1), index, end: destinationEnd + 1 } + return { 0: text.slice(open, destinationEnd + 1), index: open, end: destinationEnd + 1 } } } // Every opener before this closing bracket shares the same invalid suffix. diff --git a/mobile/src/components/markdown-inline-token-rules.test.ts b/mobile/src/components/markdown-inline-token-rules.test.ts deleted file mode 100644 index 27d44a36d9c..00000000000 --- a/mobile/src/components/markdown-inline-token-rules.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - isIntrawordUnderscoreToken, - trimAutolinkTrailingPunctuation -} from './markdown-inline-token-rules' - -describe('isIntrawordUnderscoreToken', () => { - it('rejects snake_case emphasis spans', () => { - const text = 'src/foo_bar.ts and src/baz_qux.ts' - const index = text.indexOf('_') - const token = text.slice(index, text.lastIndexOf('_') + 1) - expect(isIntrawordUnderscoreToken(text, index, token)).toBe(true) - }) - - it('keeps standalone emphasis', () => { - expect(isIntrawordUnderscoreToken('say _hello_ now', 4, '_hello_')).toBe(false) - expect(isIntrawordUnderscoreToken('_hello_.', 0, '_hello_')).toBe(false) - }) - - it('rejects emphasis closed against a following word', () => { - expect(isIntrawordUnderscoreToken('_foo_s bar', 0, '_foo_')).toBe(true) - }) - - it('rejects dunder emphasis inside a path', () => { - expect(isIntrawordUnderscoreToken('src/__init__.py', 4, '__init__')).toBe(true) - expect(isIntrawordUnderscoreToken(String.raw`src\__init__.py`, 4, '__init__')).toBe(true) - }) - - it('ignores non-underscore tokens', () => { - expect(isIntrawordUnderscoreToken('a*b*c', 1, '*b*')).toBe(false) - }) -}) - -describe('trimAutolinkTrailingPunctuation', () => { - it('splits sentence punctuation off the URL', () => { - expect(trimAutolinkTrailingPunctuation('https://x.com/a.')).toEqual({ - url: 'https://x.com/a', - trailing: '.' - }) - expect(trimAutolinkTrailingPunctuation('https://x.com/a,')).toEqual({ - url: 'https://x.com/a', - trailing: ',' - }) - expect(trimAutolinkTrailingPunctuation('https://x.com/a?!')).toEqual({ - url: 'https://x.com/a', - trailing: '?!' - }) - }) - - it('keeps balanced parens and strips unbalanced ones', () => { - expect(trimAutolinkTrailingPunctuation('https://x.com/a_(b)')).toEqual({ - url: 'https://x.com/a_(b)', - trailing: '' - }) - expect(trimAutolinkTrailingPunctuation('https://x.com/a).')).toEqual({ - url: 'https://x.com/a', - trailing: ').' - }) - }) - - it('handles long unmatched closing-parenthesis tails', () => { - const url = 'https://x.com/a_(b)' - const trailing = ')'.repeat(4096) - expect(trimAutolinkTrailingPunctuation(`${url}${trailing}`)).toEqual({ url, trailing }) - }) - - it('leaves clean URLs untouched', () => { - expect(trimAutolinkTrailingPunctuation('https://x.com/a')).toEqual({ - url: 'https://x.com/a', - trailing: '' - }) - }) -}) diff --git a/mobile/src/components/markdown-inline-token-rules.ts b/mobile/src/components/markdown-inline-token-rules.ts deleted file mode 100644 index 1afa8477fbb..00000000000 --- a/mobile/src/components/markdown-inline-token-rules.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Post-checks for inline markdown tokens that a single-pass tokenizer regex -// cannot express on its own. - -const INTRAWORD_FLANK_PATTERN = /[\w\\/]/ - -/** - * True when a `_…_` / `__…__` token sits inside a word (snake_case, dunder - * tails). CommonMark treats intraword underscores as literal text; path - * separators count as flanks so dunder path segments also stay whole. - */ -export function isIntrawordUnderscoreToken(text: string, index: number, token: string): boolean { - if (!token.startsWith('_')) { - return false - } - const prev = index > 0 ? text[index - 1]! : '' - const next = text[index + token.length] ?? '' - return INTRAWORD_FLANK_PATTERN.test(prev) || INTRAWORD_FLANK_PATTERN.test(next) -} - -/** - * Split sentence punctuation off an autolinked URL tail ("see https://x.com/a."), - * keeping a trailing ')' only when the URL itself opened a paren. - */ -export function trimAutolinkTrailingPunctuation(url: string): { url: string; trailing: string } { - let end = url.length - let parenthesisCountsReady = false - let openParentheses = 0 - let closeParentheses = 0 - while (end > 0) { - const char = url[end - 1]! - if ('.,;:!?'.includes(char)) { - end-- - continue - } - if (char === ')') { - if (!parenthesisCountsReady) { - for (let index = 0; index < end; index++) { - if (url[index] === '(') { - openParentheses++ - } else if (url[index] === ')') { - closeParentheses++ - } - } - parenthesisCountsReady = true - } - if (closeParentheses > openParentheses) { - end-- - closeParentheses-- - continue - } - } - break - } - return { url: url.slice(0, end), trailing: url.slice(end) } -} diff --git a/mobile/src/components/mobile-markdown-blocks.tsx b/mobile/src/components/mobile-markdown-blocks.tsx new file mode 100644 index 00000000000..eeef850dbe0 --- /dev/null +++ b/mobile/src/components/mobile-markdown-blocks.tsx @@ -0,0 +1,249 @@ +import type { Code, List, RootContent, Table } from 'mdast' +import { Fragment, type ReactNode } from 'react' +import { + Pressable, + ScrollView, + Text as NativeText, + View, + type StyleProp, + type TextStyle +} from 'react-native' +import type { NativeChatVisualDirective } from '../../../src/shared/native-chat-visual-directive' +import { isMobileMermaidLanguage } from './mobile-mermaid-language' +import { mobileMarkdownTableColumnWidths } from './mobile-markdown-table-columns' +import { mobileMarkdownVisualPlaceholderIndex } from './mobile-markdown-visual-lines' +import { + openMarkdownHref, + renderInline, + type MarkdownInlineContext +} from './mobile-markdown-inline' +import { styles } from './mobile-markdown-styles' +import { MarkdownText } from './mobile-markdown-text' +import { codeFenceClosed, plainText } from './mobile-markdown-tree' +import { MermaidDiagram } from './pr-sidebar/MermaidDiagram' + +/** Prose base size — passed to MermaidDiagram fallback mono text. */ +const MERMAID_BASE = 13 +/** Containers nested deeper than this draw as plain text, so a pathological nesting cannot + * exhaust native layout. */ +const MAX_NESTING = 8 + +export type MarkdownBlockContext = MarkdownInlineContext & { + /** The text is still arriving: a diagram stays code until it stops. */ + growing: boolean + listText: StyleProp + /** Interactive children own their touches and must forward the row action. */ + rowLongPress?: () => void + directives: readonly NativeChatVisualDirective[] + renderVisual?: (directive: NativeChatVisualDirective, index: number) => ReactNode + mermaidOccurrences: Map +} + +/** How a paragraph draws in the container holding it. */ +export type ParagraphText = { style: StyleProp; selectable: boolean } + +function renderCode(node: Code, key: number, ctx: MarkdownBlockContext): ReactNode { + // Mermaid fences render as diagrams (WebView), not as raw code — same as PR sidebar. + // Growing or unclosed fences are still streaming: mounting the WebView per tick would + // reload its document up to 20x/sec, so they stay raw code until done. + if (isMobileMermaidLanguage(node.lang ?? undefined) && !ctx.growing && codeFenceClosed(node)) { + const occurrence = ctx.mermaidOccurrences.get(node.value) ?? 0 + ctx.mermaidOccurrences.set(node.value, occurrence + 1) + return ( + + ) + } + return ( + + {node.lang ? {node.lang} : null} + + {node.value} + + + ) +} + +function renderList(node: List, key: number, ctx: MarkdownBlockContext, depth: number): ReactNode { + const itemText: ParagraphText = { style: ctx.listText, selectable: true } + return ( + + {node.children.map((item, itemIndex) => ( + + + {item.checked == null + ? node.ordered + ? `${(node.start ?? 1) + itemIndex}.` + : '-' + : item.checked + ? '[x]' + : '[ ]'} + + + {renderBlocks(item.children, ctx, itemText, depth + 1)} + + + ))} + + ) +} + +function renderTable(node: Table, key: number, ctx: MarkdownBlockContext): ReactNode { + const rows = node.children.map((row) => row.children) + // Rows lay out independently, so a shared width per column is what lines them up. + const columnWidths = mobileMarkdownTableColumnWidths( + rows.map((cells) => cells.map((cell) => plainText(cell))) + ) + return ( + + + {rows.map((cells, rowIndex) => ( + + {columnWidths.map((width, cellIndex) => ( + + {renderInline(cells[cellIndex]?.children ?? [], ctx)} + + ))} + + ))} + + + ) +} + +function renderBlock( + node: RootContent, + key: number, + ctx: MarkdownBlockContext, + text: ParagraphText, + depth: number +): ReactNode { + if (depth > MAX_NESTING && 'children' in node) { + return ( + + {plainText(node)} + + ) + } + switch (node.type) { + case 'paragraph': { + const [only] = node.children + const visual = + node.children.length === 1 && only?.type === 'text' + ? mobileMarkdownVisualPlaceholderIndex(only.value, ctx.directives.length) + : null + if (visual !== null) { + const directive = ctx.directives[visual] + return directive && ctx.renderVisual ? ( + + {ctx.renderVisual(directive, visual)} + + ) : null + } + if (node.children.length === 1 && only?.type === 'image' && /^https?:\/\//i.test(only.url)) { + return ( + openMarkdownHref(only.url, ctx.onOpenFile)} + onLongPress={ctx.rowLongPress} + > + {only.alt || 'Open image'} + + {only.url} + + + ) + } + return ( + + {renderInline(node.children, ctx)} + + ) + } + case 'heading': + return ( + + {renderInline(node.children, ctx)} + + ) + case 'blockquote': + return ( + + {renderBlocks( + node.children, + ctx, + { style: styles.quoteText, selectable: true }, + depth + 1 + )} + + ) + case 'code': + return renderCode(node, key, ctx) + case 'list': + return renderList(node, key, ctx, depth) + case 'table': + return renderTable(node, key, ctx) + case 'thematicBreak': + return + case 'footnoteDefinition': + return ( + + {`[${node.label ?? node.identifier}]`} + + {renderBlocks(node.children, ctx, { style: ctx.listText, selectable: true }, depth + 1)} + + + ) + case 'html': + return ( + + {node.value} + + ) + // Inline content never sits at block level in a parsed tree; drawn as text if it does. + case 'break': + case 'delete': + case 'emphasis': + case 'footnoteReference': + case 'image': + case 'imageReference': + case 'inlineCode': + case 'link': + case 'linkReference': + case 'strong': + case 'text': + return ( + + {renderInline([node], ctx)} + + ) + case 'definition': + case 'listItem': + case 'tableCell': + case 'tableRow': + case 'yaml': + return null + } +} + +/** Block content as views; `text` is how paragraphs draw in the container holding them. */ +export function renderBlocks( + nodes: readonly RootContent[], + ctx: MarkdownBlockContext, + text: ParagraphText, + depth = 0 +): ReactNode[] { + return nodes.map((node, index) => renderBlock(node, index, ctx, text, depth)) +} diff --git a/mobile/src/components/mobile-markdown-inline.tsx b/mobile/src/components/mobile-markdown-inline.tsx new file mode 100644 index 00000000000..79d1dc3454c --- /dev/null +++ b/mobile/src/components/mobile-markdown-inline.tsx @@ -0,0 +1,154 @@ +import type { PhrasingContent } from 'mdast' +import { Fragment, type ReactNode } from 'react' +import type { TextProps } from 'react-native' +import { openExternalLink } from '../platform/external-link' +import { + detectFilePathSegments, + isFilePathCodeSpan, + normalizeFilePath +} from './markdown-file-path-detection' +import { routeMarkdownHref } from './markdown-href-routing' +import { styles } from './mobile-markdown-styles' +import { MarkdownText } from './mobile-markdown-text' +import { plainText } from './mobile-markdown-tree' + +export type MarkdownInlineContext = { + /** When provided, detected file paths and file-target hrefs render as tappable and invoke + * this with the path text; without it, paths render as plain text. */ + onOpenFile?: (pathText: string) => void +} + +type SpanStyle = TextProps['style'] + +// Web/mail hrefs open the system handler; file-target hrefs (file: URIs and +// scheme-less paths — the entire desktop file-link contract) go to onOpenFile. +export function openMarkdownHref(href: string, onOpenFile?: (pathText: string) => void): void { + const route = routeMarkdownHref(href) + if (route.kind === 'web') { + // The seam, not react-native's `Linking`: this module is in the tasks page closure, and inside + // the shell's WebView `openURL` resolves without opening anything. + openExternalLink(route.url) + return + } + if (route.kind === 'file' && onOpenFile) { + onOpenFile(route.pathText) + } +} + +// A plain text run, with tappable file paths split out when onOpenFile is provided. +function renderTextRun( + text: string, + key: string, + onOpenFile: ((pathText: string) => void) | undefined, + outer: SpanStyle +): ReactNode { + if (!onOpenFile) { + return text + } + const segments = detectFilePathSegments(text) + if (segments.length === 1 && segments[0]!.type === 'text') { + return text + } + return segments.map((segment, segmentIndex) => + segment.type === 'file' ? ( + onOpenFile(segment.path)} + > + {segment.value} + + ) : ( + {segment.value} + ) + ) +} + +function renderLink( + key: string, + label: string, + url: string, + ctx: MarkdownInlineContext, + outer: SpanStyle +): ReactNode { + return ( + openMarkdownHref(url, ctx.onOpenFile)} + > + {label} + + ) +} + +function renderPhrasing( + node: PhrasingContent, + key: string, + ctx: MarkdownInlineContext, + outer: SpanStyle +): ReactNode { + switch (node.type) { + case 'text': + return {renderTextRun(node.value, key, ctx.onOpenFile, outer)} + case 'break': + return {'\n'} + case 'emphasis': + case 'strong': + case 'delete': { + const emphasis = + node.type === 'strong' + ? styles.bold + : node.type === 'emphasis' + ? styles.italic + : styles.strike + const spanStyle = [outer, emphasis] + return ( + + {renderInline(node.children, ctx, spanStyle, `${key}.`)} + + ) + } + case 'inlineCode': + return ctx.onOpenFile && isFilePathCodeSpan(node.value) ? ( + ctx.onOpenFile?.(normalizeFilePath(node.value.trim()))} + > + {node.value} + + ) : ( + + {node.value} + + ) + case 'link': + return renderLink(key, plainText(node), node.url, ctx, outer) + case 'image': + return renderLink(key, node.alt || 'image', node.url, ctx, outer) + case 'footnoteReference': + return ( + + {`[${node.label ?? node.identifier}]`} + + ) + case 'html': + return {node.value} + // markdown-it resolves references itself, so these only carry their text. + case 'linkReference': + case 'imageReference': + return {plainText(node)} + } +} + +/** Inline content as spans. iOS range-selectable spans are styled from the root alone, so `outer` + * restates the enclosing emphasis on every span inside it. */ +export function renderInline( + nodes: readonly PhrasingContent[], + ctx: MarkdownInlineContext, + outer?: SpanStyle, + keyPrefix = '' +): ReactNode[] { + return nodes.map((node, index) => renderPhrasing(node, `${keyPrefix}${index}`, ctx, outer)) +} diff --git a/mobile/src/components/mobile-markdown-parser-progress.test.ts b/mobile/src/components/mobile-markdown-parser-progress.test.ts deleted file mode 100644 index e5cf3181d0c..00000000000 --- a/mobile/src/components/mobile-markdown-parser-progress.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { runInNewContext } from 'node:vm' -import { describe, expect, it } from 'vitest' -import { normalizeMobileMarkdownPreviewHtml } from './mobile-markdown-preview-html' -import { parseMobileMarkdown } from './mobile-markdown-parser' - -function parseWithDeadline(input: string) { - // A synchronous parser loop must fail without hanging the test worker. - return runInNewContext('parse(input)', { parse: parseMobileMarkdown, input }, { timeout: 250 }) -} - -describe('mobile Markdown parser progress', () => { - it.each(['```c++', '```c#', '``` ts', '```ts title="file.ts"', '````', '```!'])( - 'retains unsupported fence %s as paragraph text', - (fence) => { - expect(parseWithDeadline(fence)).toEqual([{ type: 'paragraph', text: fence }]) - expect(parseWithDeadline(`before\n${fence}\nafter`)).toEqual([ - { type: 'paragraph', text: `before\n${fence}\nafter` } - ]) - } - ) - - it.each(['# ', '## ', '###### ', '#\t'])('consumes incomplete heading %s', (heading) => { - expect(parseWithDeadline(`${heading}\nnext`)).toEqual([ - { type: 'paragraph', text: `${heading}\nnext` } - ]) - }) - - it('handles unsupported fences through the production preview normalization', () => { - const input = normalizeMobileMarkdownPreviewHtml('```c++\nint x = 1;') - expect(parseWithDeadline(input)).toEqual([{ type: 'paragraph', text: input }]) - }) - - it('recognizes a supported fence after unsupported fence text', () => { - expect(parseWithDeadline('```c++\n```ts\nconst x = 1\n```')).toEqual([ - { type: 'paragraph', text: '```c++' }, - { type: 'code', text: 'const x = 1', language: 'ts', closed: true } - ]) - }) - - it('preserves supported fences and their streaming state', () => { - expect(parseWithDeadline('before\n```ts\nconst x = 1')).toEqual([ - { type: 'paragraph', text: 'before' }, - { type: 'code', text: 'const x = 1', language: 'ts', closed: false } - ]) - }) -}) diff --git a/mobile/src/components/mobile-markdown-parser.ts b/mobile/src/components/mobile-markdown-parser.ts deleted file mode 100644 index 8c56bd7516d..00000000000 --- a/mobile/src/components/mobile-markdown-parser.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { isTableSeparator, splitTableRow } from './rich-markdown/markdown-table-rows' -import { mobileMarkdownVisualPlaceholderIndex } from './mobile-markdown-visual-lines' - -export type MobileMarkdownBlock = - | { type: 'paragraph'; text: string } - | { type: 'heading'; level: number; text: string } - | { type: 'quote'; text: string } - | { type: 'code'; text: string; language?: string; closed: boolean } - | { type: 'list'; ordered: boolean; items: Array<{ text: string; checked?: boolean }> } - | { type: 'image'; alt: string; url: string } - | { type: 'table'; headers: string[]; rows: string[][] } - | { type: 'rule' } - /** A native-chat visual directive; `index` is its position in the protected directive list. */ - | { type: 'visual'; index: number } - -const HEADING = /^(#{1,6})\s+(.+)$/ -const CODE_FENCE = /^```([A-Za-z0-9_-]+)?\s*$/ - -/** `visualCount`: how many directive placeholders `protectMobileMarkdownVisualLines` left in - * `content`; zero (the default) leaves every line to the ordinary rules. */ -export function parseMobileMarkdown(content: string, visualCount = 0): MobileMarkdownBlock[] { - const visualIndex = (line: string): number | null => - visualCount > 0 ? mobileMarkdownVisualPlaceholderIndex(line, visualCount) : null - const lines = content.replace(/\r\n?/g, '\n').split('\n') - const blocks: MobileMarkdownBlock[] = [] - let index = 0 - - while (index < lines.length) { - const line = lines[index] ?? '' - if (!line.trim()) { - index += 1 - continue - } - - const fence = line.match(CODE_FENCE) - if (fence) { - index += 1 - const code: string[] = [] - while (index < lines.length && !/^```\s*$/.test(lines[index] ?? '')) { - code.push(lines[index] ?? '') - index += 1 - } - // closed=false means the fence is still streaming in (no terminator yet). - const closed = index < lines.length - if (closed) { - index += 1 - } - blocks.push({ type: 'code', text: code.join('\n'), language: fence[1], closed }) - continue - } - - const visual = visualIndex(line) - if (visual !== null) { - blocks.push({ type: 'visual', index: visual }) - index += 1 - continue - } - - if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) { - blocks.push({ type: 'rule' }) - index += 1 - continue - } - - const standaloneImage = line.match(/^!\[([^\]]*)\]\((https?:\/\/[^)\s]+)(?:\s+"[^"]*")?\)\s*$/i) - if (standaloneImage) { - blocks.push({ type: 'image', alt: standaloneImage[1] ?? '', url: standaloneImage[2]! }) - index += 1 - continue - } - - if ( - line.includes('|') && - index + 1 < lines.length && - isTableSeparator(lines[index + 1] ?? '') - ) { - const headers = splitTableRow(line) - index += 2 - const rows: string[][] = [] - while (index < lines.length && (lines[index] ?? '').includes('|') && lines[index]?.trim()) { - rows.push(splitTableRow(lines[index] ?? '')) - index += 1 - } - blocks.push({ type: 'table', headers, rows }) - continue - } - - const heading = line.match(HEADING) - if (heading) { - blocks.push({ type: 'heading', level: heading[1]!.length, text: heading[2]!.trim() }) - index += 1 - continue - } - - if (/^>\s?/.test(line)) { - const quote: string[] = [] - while (index < lines.length && /^>\s?/.test(lines[index] ?? '')) { - quote.push((lines[index] ?? '').replace(/^>\s?/, '')) - index += 1 - } - blocks.push({ type: 'quote', text: quote.join('\n').trim() }) - continue - } - - if (/^\s*(?:[-*+]|\d+[.)])\s+/.test(line)) { - const items: Array<{ text: string; checked?: boolean }> = [] - let ordered = false - while (index < lines.length && /^\s*(?:[-*+]|\d+[.)])\s+/.test(lines[index] ?? '')) { - const current = lines[index] ?? '' - const orderedMatch = current.match(/^\s*\d+[.)]\s+(.+)$/) - const unorderedMatch = current.match(/^\s*[-*+]\s+(.+)$/) - ordered ||= Boolean(orderedMatch) - const rawText = (orderedMatch?.[1] ?? unorderedMatch?.[1] ?? '').trim() - const task = rawText.match(/^\[([ xX])\]\s+(.+)$/) - items.push({ - text: task?.[2] ?? rawText, - checked: task ? task[1]?.toLowerCase() === 'x' : undefined - }) - index += 1 - } - blocks.push({ type: 'list', ordered, items }) - continue - } - - const paragraph: string[] = [] - while ( - index < lines.length && - lines[index]?.trim() && - !CODE_FENCE.test(lines[index] ?? '') && - !HEADING.test(lines[index] ?? '') && - !/^>\s?/.test(lines[index] ?? '') && - !/^\s*(?:[-*+]|\d+[.)])\s+/.test(lines[index] ?? '') && - !/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(lines[index] ?? '') && - visualIndex(lines[index] ?? '') === null - ) { - paragraph.push(lines[index] ?? '') - index += 1 - } - blocks.push({ type: 'paragraph', text: paragraph.join('\n').trim() }) - } - - return blocks -} diff --git a/mobile/src/components/mobile-markdown-styles.ts b/mobile/src/components/mobile-markdown-styles.ts index 414a8daf57b..cb61a56d70f 100644 --- a/mobile/src/components/mobile-markdown-styles.ts +++ b/mobile/src/components/mobile-markdown-styles.ts @@ -49,7 +49,8 @@ export const styles = StyleSheet.create({ quote: { borderLeftWidth: 2, borderLeftColor: colors.borderSubtle, - paddingLeft: spacing.sm + paddingLeft: spacing.sm, + gap: spacing.xs }, quoteText: { fontSize: 13, @@ -101,8 +102,6 @@ export const styles = StyleSheet.create({ flexDirection: 'row' }, tableCell: { - minWidth: 112, - maxWidth: 220, borderRightWidth: 1, borderBottomWidth: 1, borderColor: colors.borderSubtle, @@ -116,11 +115,6 @@ export const styles = StyleSheet.create({ fontWeight: '700', backgroundColor: colors.bgRaised }, - tableTruncated: { - padding: spacing.sm, - fontSize: 12, - color: colors.textMuted - }, list: { gap: spacing.xs }, @@ -130,19 +124,26 @@ export const styles = StyleSheet.create({ gap: spacing.sm }, listMarker: { - width: 22, + // A floor, not a width: "10." would wrap inside a fixed 22. + minWidth: 22, fontSize: 13, lineHeight: 19, color: colors.textSecondary, fontFamily: typography.monoFamily }, - listText: { + listItemBody: { flex: 1, minWidth: 0, + gap: spacing.xs + }, + listText: { fontSize: 13, lineHeight: 19, color: colors.textPrimary }, + footnoteReference: { + color: colors.textSecondary + }, rule: { height: StyleSheet.hairlineWidth, backgroundColor: colors.borderSubtle diff --git a/mobile/src/components/mobile-markdown-table-columns.ts b/mobile/src/components/mobile-markdown-table-columns.ts new file mode 100644 index 00000000000..1feae1a02a9 --- /dev/null +++ b/mobile/src/components/mobile-markdown-table-columns.ts @@ -0,0 +1,38 @@ +import { spacing } from '../theme/mobile-theme' + +// Per-glyph advances at the 12px table font, measured on agent tables: 6.4 regular and 6.9 bold +// on average, and up to 8.7 for a bold word. A wrong guess wraps text; it cannot misalign columns. +const AVERAGE_GLYPH_WIDTH = 7 +const WIDEST_GLYPH_WIDTH = 9 +// Horizontal padding plus the right border of `styles.tableCell`. +const CELL_CHROME = spacing.sm * 2 + 1 +const MIN_COLUMN_WIDTH = 48 +const MAX_COLUMN_WIDTH = 220 +const WHITESPACE = /\s/ + +function cellContentWidth(text: string): number { + let columns = 0 + let word = 0 + let longestWord = 0 + for (const char of text) { + // CJK, kana, Hangul, fullwidth forms and emoji draw about two Latin glyphs wide. + const wide = char.codePointAt(0)! >= 0x2e80 + columns += wide ? 2 : 1 + // Lines break at spaces and around wide glyphs; only a longer unbroken run splits mid-word. + word = wide || WHITESPACE.test(char) ? 0 : word + 1 + longestWord = Math.max(longestWord, word) + } + return Math.max(columns * AVERAGE_GLYPH_WIDTH, longestWord * WIDEST_GLYPH_WIDTH) +} + +/** One width per header column, sized to the column's widest cell. `rows` holds each cell's + * drawn text, header row first. */ +export function mobileMarkdownTableColumnWidths(rows: readonly string[][]): number[] { + return (rows[0] ?? []).map((_, column) => { + let widest = 0 + for (const row of rows) { + widest = Math.max(widest, cellContentWidth(row[column] ?? '')) + } + return Math.min(MAX_COLUMN_WIDTH, Math.max(MIN_COLUMN_WIDTH, widest + CELL_CHROME)) + }) +} diff --git a/mobile/src/components/mobile-markdown-table.test.tsx b/mobile/src/components/mobile-markdown-table.test.tsx new file mode 100644 index 00000000000..8a80c8d92ef --- /dev/null +++ b/mobile/src/components/mobile-markdown-table.test.tsx @@ -0,0 +1,75 @@ +import { createElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { describe, expect, it, vi } from 'vitest' +import { MobileMarkdown } from './MobileMarkdown' +import { styles } from './mobile-markdown-styles' + +vi.mock('react-native', () => ({ + Linking: { openURL: () => Promise.resolve() }, + Platform: { OS: 'ios' }, + Pressable: 'Pressable', + ScrollView: 'ScrollView', + StyleSheet: { create: (styles: unknown) => styles, hairlineWidth: 1 }, + Text: 'Text', + View: 'View' +})) + +vi.mock('./pr-sidebar/MermaidDiagram', () => ({ MermaidDiagram: 'MermaidDiagram' })) + +/** Each rendered table row's cell styles, header row first. */ +function renderTableCellStyles(content: string): unknown[][] { + let renderer: ReactTestRenderer | null = null + act(() => { + renderer = create(createElement(MobileMarkdown, { content })) + }) + const rows = renderer!.root.findAll((node) => node.props.style === styles.tableRow) + const cellStyles = rows.map((row) => + row.children.map((cell) => (typeof cell === 'string' ? undefined : cell.props.style)) + ) + act(() => renderer!.unmount()) + return cellStyles +} + +function cellWidth(style: unknown): number { + for (const entry of [style].flat()) { + if (entry && typeof entry === 'object' && 'width' in entry && typeof entry.width === 'number') { + return entry.width + } + } + throw new Error('table cell has no width') +} + +function markdownTable(headers: string[], rows: string[][]): string { + return [headers, headers.map(() => '---'), ...rows] + .map((row) => `| ${row.join(' | ')} |`) + .join('\n') +} + +describe('MobileMarkdown tables', () => { + it('renders every row and column with one width per column', () => { + const headers = Array.from({ length: 10 }, (_, column) => `Column ${column}`) + const rows = Array.from({ length: 50 }, (_, row) => + headers.map((_, column) => (row === 7 && column === 3 ? 'a much longer cell value' : 'x')) + ) + const cellStyles = renderTableCellStyles(markdownTable(headers, rows)) + + expect(cellStyles.map((row) => row.length)).toEqual(Array.from({ length: 51 }, () => 10)) + const widths = cellStyles.map((row) => row.map(cellWidth)) + for (const row of widths) { + expect(row).toEqual(widths[0]) + } + expect(widths[0]![3]).toBeGreaterThan(widths[0]![2]!) + }) + + it('sizes a column by the text it draws, not its markdown source', () => { + const [widths] = renderTableCellStyles( + markdownTable( + ['A', 'B', 'C'], + [['docs', '[docs](https://example.com/a/very/long/path)', '文档文档']] + ) + ).map((row) => row.map(cellWidth)) + + expect(widths![1]).toBe(widths![0]) + expect(widths![2]).toBeGreaterThan(widths![0]!) + }) +}) diff --git a/mobile/src/components/mobile-markdown-text-setup.ts b/mobile/src/components/mobile-markdown-text-setup.ts new file mode 100644 index 00000000000..87d8fd4e17f --- /dev/null +++ b/mobile/src/components/mobile-markdown-text-setup.ts @@ -0,0 +1,14 @@ +import { createContext, type ComponentType } from 'react' +import { Text as NativeText, type TextProps } from 'react-native' + +export type MarkdownTextSetup = { + TextComponent: ComponentType + /** Disable native selection only within the Android transcript. */ + androidTranscript: boolean + onLongPress?: () => void +} + +export const MarkdownTextContext = createContext({ + TextComponent: NativeText, + androidTranscript: false +}) diff --git a/mobile/src/components/mobile-markdown-text.tsx b/mobile/src/components/mobile-markdown-text.tsx new file mode 100644 index 00000000000..bd818852d64 --- /dev/null +++ b/mobile/src/components/mobile-markdown-text.tsx @@ -0,0 +1,16 @@ +import { createElement, useContext } from 'react' +import type { TextProps } from 'react-native' +import { MarkdownTextContext } from './mobile-markdown-text-setup' + +export function MarkdownText(props: TextProps): React.JSX.Element { + const { TextComponent, androidTranscript, onLongPress } = useContext(MarkdownTextContext) + if (!androidTranscript) { + return createElement(TextComponent, props) + } + // Override selection without changing the nested spans' inherited behavior. + return createElement(TextComponent, { + ...props, + ...(props.selectable === true ? { selectable: false } : {}), + ...(onLongPress && props.onPress ? { onLongPress } : {}) + }) +} diff --git a/mobile/src/components/mobile-markdown-tree.ts b/mobile/src/components/mobile-markdown-tree.ts new file mode 100644 index 00000000000..4b4cb3494fe --- /dev/null +++ b/mobile/src/components/mobile-markdown-tree.ts @@ -0,0 +1,309 @@ +import MarkdownIt from 'markdown-it' +import footnote from 'markdown-it-footnote' +import type { + AlignType, + Code, + Nodes, + Parent, + PhrasingContent, + Root, + RootContent, + Table +} from 'mdast' +import remend from 'remend' +import { normalizeMobileMarkdownPreviewHtml } from './mobile-markdown-preview-html' + +// CommonMark with GitHub's tables, strikethrough, autolinks and footnotes: the rules desktop's +// renderer follows, read by a parser fast enough for the phone's JavaScript engine. +const markdown = new MarkdownIt({ html: true, linkify: true }).use(footnote) +// Every href parses as a link, file: included; routeMarkdownHref decides what a tap opens. +markdown.validateLink = () => true + +type Token = ReturnType[number] + +const PATH_FLANK = /[\w\\/]/ +const TASK_MARKER = /^\[([ xX])\](?:\s+|$)/ +const LINE_BREAK_TAG = /^$/i +const HEADING_DEPTHS = [1, 2, 3, 4, 5, 6] as const +/** Parents that hold blocks; any other parent holds inline content. */ +const BLOCK_PARENTS = new Set(['root', 'blockquote', 'listItem', 'footnoteDefinition']) +const CLOSING_TOKENS = new Set([ + 'paragraph_close', + 'heading_close', + 'blockquote_close', + 'bullet_list_close', + 'ordered_list_close', + 'list_item_close', + 'table_close', + 'tr_close', + 'th_close', + 'td_close', + 'footnote_close' +]) + +/** The text a node draws, as mdast-util-to-string reads it. */ +export function plainText(node: Nodes): string { + if ('value' in node) { + return node.value + } + if (node.type === 'image') { + return node.alt ?? '' + } + return 'children' in node ? node.children.map(plainText).join('') : '' +} + +function pushText(nodes: PhrasingContent[], value: string): void { + const previous = nodes.at(-1) + if (previous?.type === 'text') { + previous.value += value + } else { + nodes.push({ type: 'text', value }) + } +} + +/** Inline tokens up to `close`, and the index of that closing token. */ +function phrasing( + tokens: readonly Token[], + start: number, + close?: string +): { nodes: PhrasingContent[]; next: number } { + const nodes: PhrasingContent[] = [] + let index = start + while (index < tokens.length && tokens[index]!.type !== close) { + const token = tokens[index]! + index += 1 + if (token.type === 'text') { + pushText(nodes, token.content) + } else if (token.type === 'softbreak') { + pushText(nodes, '\n') + } else if (token.type === 'hardbreak') { + nodes.push({ type: 'break' }) + } else if (token.type === 'code_inline') { + nodes.push({ type: 'inlineCode', value: token.content }) + } else if (token.type === 'html_inline') { + nodes.push({ type: 'html', value: token.content }) + } else if (token.type === 'image') { + nodes.push({ type: 'image', url: token.attrGet('src') ?? '', alt: token.content || null }) + } else if (token.type === 'footnote_ref') { + const label = String(token.meta?.label ?? '') + nodes.push({ type: 'footnoteReference', identifier: label, label }) + } else if (token.nesting === 1) { + const inner = phrasing(tokens, index, token.type.replace(/_open$/, '_close')) + index = inner.next + 1 + const previous = nodes.at(-1) + const before = previous?.type === 'text' ? previous.value.slice(-1) : '' + const after = tokens[index]?.type === 'text' ? (tokens[index]?.content[0] ?? '') : '' + if (token.type === 'link_open') { + nodes.push({ type: 'link', url: token.attrGet('href') ?? '', children: inner.nodes }) + } else if ( + // `_` emphasis against a path separator stays literal, so a dunder segment + // (`a/__tests__/x.ts`) keeps its path whole and tappable. + token.markup.startsWith('_') && + (PATH_FLANK.test(before) || PATH_FLANK.test(after)) + ) { + const literal = inner.nodes.map(plainText).join('') + pushText(nodes, `${token.markup}${literal}${token.markup}`) + } else if (token.type === 'strong_open') { + nodes.push({ type: 'strong', children: inner.nodes }) + } else if (token.type === 'em_open') { + nodes.push({ type: 'emphasis', children: inner.nodes }) + } else if (token.type === 's_open') { + nodes.push({ type: 'delete', children: inner.nodes }) + } else { + nodes.push(...inner.nodes) + } + } + } + return { nodes, next: index } +} + +/** Whether a code block's closing fence has arrived; an unclosed one is still streaming in. */ +export function codeFenceClosed(node: Code): boolean { + return !('closed' in node) || node.closed !== false +} + +function codeNode(token: Token, lines: readonly string[]): Code & { closed: boolean } { + if (token.type !== 'fence') { + return { type: 'code', lang: null, value: token.content.replace(/\n$/, ''), closed: true } + } + const [start, end] = token.map ?? [0, 0] + // A fence inside a quote carries the quote's marker on its closing line. + const closing = end - start > 1 ? (lines[end - 1] ?? '').replace(/^[\s>]*/, '').trim() : '' + return { + type: 'code', + lang: token.info.trim().split(/\s+/)[0] || null, + value: token.content.replace(/\n$/, ''), + closed: + closing.length >= token.markup.length && + [...closing].every((char) => char === token.markup[0]) + } +} + +function alignOf(token: Token): AlignType { + const align = /text-align:\s*(left|right|center)/.exec(token.attrGet('style') ?? '')?.[1] + return align === 'left' || align === 'right' || align === 'center' ? align : null +} + +/** GitHub task list items: `[ ]` or `[x]` opening the item's first paragraph. */ +function readTaskMarker(item: Parent & { checked?: boolean | null }): void { + const paragraph = item.children[0] + const text = paragraph?.type === 'paragraph' ? paragraph.children[0] : undefined + const marker = text?.type === 'text' ? TASK_MARKER.exec(text.value) : null + if (text?.type === 'text' && marker) { + item.checked = marker[1] !== ' ' + text.value = text.value.slice(marker[0].length) + } +} + +/** markdown-it's flat token stream as the mdast tree the renderer draws. */ +function toTree(tokens: readonly Token[], source: string): Root { + const root: Root = { type: 'root', children: [] } + const lines = source.split('\n') + const stack: Parent[] = [root] + let table: Table | undefined + const open = (node: Parent & RootContent): void => { + stack.at(-1)!.children.push(node) + stack.push(node) + } + for (const token of tokens) { + const parent = stack.at(-1)! + switch (token.type) { + case 'paragraph_open': + open({ type: 'paragraph', children: [] }) + break + case 'heading_open': + open({ + type: 'heading', + depth: HEADING_DEPTHS.find((depth) => token.tag === `h${depth}`) ?? 6, + children: [] + }) + break + case 'blockquote_open': + open({ type: 'blockquote', children: [] }) + break + case 'bullet_list_open': + case 'ordered_list_open': + open({ + type: 'list', + ordered: token.type === 'ordered_list_open', + start: Number(token.attrGet('start') ?? 1), + children: [] + }) + break + case 'list_item_open': + open({ type: 'listItem', checked: null, children: [] }) + break + case 'table_open': + table = { type: 'table', align: [], children: [] } + open(table) + break + case 'tr_open': + open({ type: 'tableRow', children: [] }) + break + case 'th_open': + case 'td_open': + if (token.type === 'th_open') { + table?.align?.push(alignOf(token)) + } + open({ type: 'tableCell', children: [] }) + break + case 'footnote_open': { + const label = String(token.meta?.label ?? '') + open({ type: 'footnoteDefinition', identifier: label, label, children: [] }) + break + } + case 'inline': + parent.children.push(...phrasing(token.children ?? [], 0).nodes) + break + case 'fence': + case 'code_block': + parent.children.push(codeNode(token, lines)) + break + case 'hr': + parent.children.push({ type: 'thematicBreak' }) + break + case 'html_block': + parent.children.push({ type: 'html', value: token.content }) + break + default: + if (CLOSING_TOKENS.has(token.type)) { + const closed = stack.pop() + if (closed?.type === 'listItem') { + readTaskMarker(closed) + } + } + } + } + return root +} + +/** + * Raw HTML the parser found, and only that: code is never an `html` node, so nothing inside a + * code block or span is touched. A block is read as the Markdown the preview cleanup makes of + * it (README headers and badges); an inline tag becomes the text the cleanup leaves, if any. + * `nested`: a block the cleanup produced, where leftover block HTML stays text. + */ +function withoutHtml(parent: Parent, nested: boolean): void { + const inline = !BLOCK_PARENTS.has(parent.type) + parent.children = parent.children.flatMap((child): RootContent[] => { + if (child.type !== 'html') { + if ('children' in child) { + withoutHtml(child, nested) + } + return [child] + } + if (inline && LINE_BREAK_TAG.test(child.value.trim())) { + return [{ type: 'break' }] + } + const text = normalizeMobileMarkdownPreviewHtml(child.value) + if (!text) { + return [] + } + if (inline) { + return [{ type: 'text', value: text }] + } + if (nested) { + return [{ type: 'paragraph', children: [{ type: 'text', value: text }] }] + } + const blocks = toTree(markdown.parse(text, {}), text) + withoutHtml(blocks, true) + return blocks.children + }) +} + +/** The text with the markup its last top-level block leaves open closed, as desktop closes it. + * Only that block: a lone `*` earlier in the reply is no opener, and remend would close it. */ +function withLastBlockClosed(source: string, tokens: readonly Token[]): string { + const lastBlock = tokens.findLast((token) => token.level === 0 && token.map)?.map?.[0] ?? 0 + const lines = source.split('\n') + const tail = lines.slice(lastBlock).join('\n') + const closed = remend(tail, { linkMode: 'text-only' }) + return closed === tail ? source : [...lines.slice(0, lastBlock), closed].join('\n') +} + +/** `growing`: the text is still arriving, so markup its end leaves open is closed first, instead + * of flashing raw markers and half-written links. */ +export function parseMobileMarkdownTree(source: string, growing: boolean): Root { + let tokens = markdown.parse(source, {}) + let text = source + if (growing) { + text = withLastBlockClosed(source, tokens) + tokens = text === source ? tokens : markdown.parse(text, {}) + } + const root = toTree(tokens, text) + withoutHtml(root, false) + return root +} + +/** The 0-based lines markdown-it reads as code, at any depth. */ +export function mobileMarkdownCodeLines(source: string): Set { + const lines = new Set() + for (const token of markdown.parse(source, {})) { + if ((token.type === 'fence' || token.type === 'code_block') && token.map) { + for (let line = token.map[0]; line < token.map[1]; line += 1) { + lines.add(line) + } + } + } + return lines +} diff --git a/mobile/src/components/mobile-markdown-visual-lines.test.ts b/mobile/src/components/mobile-markdown-visual-lines.test.ts index 61fd60ad45e..e4c33f21a40 100644 --- a/mobile/src/components/mobile-markdown-visual-lines.test.ts +++ b/mobile/src/components/mobile-markdown-visual-lines.test.ts @@ -1,16 +1,31 @@ import { describe, expect, it } from 'vitest' import { NATIVE_CHAT_VISUAL_MAX_PER_MESSAGE } from '../../../src/shared/native-chat-visual-directive' -import { normalizeMobileMarkdownPreviewHtml } from './mobile-markdown-preview-html' -import { parseMobileMarkdown } from './mobile-markdown-parser' -import { protectMobileMarkdownVisualLines } from './mobile-markdown-visual-lines' +import { parseMobileMarkdownTree, plainText } from './mobile-markdown-tree' +import { + mobileMarkdownVisualPlaceholderIndex, + protectMobileMarkdownVisualLines +} from './mobile-markdown-visual-lines' + +/** The top-level blocks drawn, with a lone placeholder paragraph read as its visual. */ +function topLevelBlocks(text: string, visualCount: number) { + return parseMobileMarkdownTree(text, false).children.map((node) => { + const visual = + node.type === 'paragraph' + ? mobileMarkdownVisualPlaceholderIndex(plainText(node), visualCount) + : null + if (visual !== null) { + return { type: 'visual', index: visual } + } + return node.type === 'code' + ? { type: 'code', text: node.value, language: node.lang } + : { type: node.type, text: plainText(node) } + }) +} /** The same steps MobileMarkdown runs when a transcript renders visuals. */ function blocksOf(content: string) { const { text, directives } = protectMobileMarkdownVisualLines(content) - return { - directives, - blocks: parseMobileMarkdown(normalizeMobileMarkdownPreviewHtml(text), directives.length) - } + return { directives, blocks: topLevelBlocks(text, directives.length) } } describe('native-chat visual lines in mobile markdown', () => { @@ -40,7 +55,7 @@ describe('native-chat visual lines in mobile markdown', () => { const fenced = blocksOf('```md\n::orca-visual{file="a.html"}\n```') expect(fenced.directives).toEqual([]) expect(fenced.blocks).toEqual([ - { type: 'code', text: '::orca-visual{file="a.html"}', language: 'md', closed: true } + { type: 'code', text: '::orca-visual{file="a.html"}', language: 'md' } ]) expect(blocksOf('> ::orca-visual{file="a.html"}').directives).toEqual([]) expect(blocksOf('- ::orca-visual{file="a.html"}').directives).toEqual([]) @@ -83,7 +98,7 @@ describe('native-chat visual lines in mobile markdown', () => { }) it('never treats a placeholder-looking line as a visual when none were protected', () => { - expect(parseMobileMarkdown('\uE000ORCA_VISUAL_0\uE000')).toEqual([ + expect(topLevelBlocks('\uE000ORCA_VISUAL_0\uE000', 0)).toEqual([ { type: 'paragraph', text: '\uE000ORCA_VISUAL_0\uE000' } ]) }) diff --git a/mobile/src/components/mobile-markdown-visual-lines.ts b/mobile/src/components/mobile-markdown-visual-lines.ts index 56ab8c14d90..2c04ea5a67e 100644 --- a/mobile/src/components/mobile-markdown-visual-lines.ts +++ b/mobile/src/components/mobile-markdown-visual-lines.ts @@ -1,12 +1,10 @@ import { + NATIVE_CHAT_VISUAL_DIRECTIVE_MARKER, NATIVE_CHAT_VISUAL_MAX_PER_MESSAGE, parseNativeChatVisualDirectiveLine, type NativeChatVisualDirective } from '../../../src/shared/native-chat-visual-directive' - -/** Same fence shapes the block parser opens and closes on, so a directive inside code stays code. */ -const CODE_FENCE_OPEN = /^```([A-Za-z0-9_-]+)?\s*$/ -const CODE_FENCE_CLOSE = /^```\s*$/ +import { mobileMarkdownCodeLines } from './mobile-markdown-tree' // Private-use delimiters, as the preview normalizer's own code placeholders use. const PLACEHOLDER_PREFIX = '\uE000ORCA_VISUAL_' const PLACEHOLDER = /^\uE000ORCA_VISUAL_(\d+)\uE000$/ @@ -22,29 +20,24 @@ function placeholderFor(index: number): string { } /** - * Lifts native-chat visual directive lines out of assistant prose before preview normalization, - * which decodes entities and strips tags and would otherwise rewrite a title. Only top-level lines - * outside fenced code count; quotes and list items never start with the marker. Past the per-message - * cap a directive stays literal text. + * Lifts native-chat visual directive lines out of assistant prose before it is parsed, which + * decodes entities and reads tags and would otherwise rewrite a title. Only lines outside code + * count, as the parser reads it; quotes and list items never start with the marker. Past the + * per-message cap a directive stays literal text. */ export function protectMobileMarkdownVisualLines(content: string): MobileMarkdownVisualLines { // Text that already spells a placeholder would alias a real one; it renders no visuals at all. - if (content.includes(PLACEHOLDER_PREFIX)) { + if ( + content.includes(PLACEHOLDER_PREFIX) || + !content.includes(NATIVE_CHAT_VISUAL_DIRECTIVE_MARKER) + ) { return { text: content, directives: [] } } - const lines = content.replace(/\r\n?/g, '\n').split('\n') + const normalized = content.replace(/\r\n?/g, '\n') + const code = mobileMarkdownCodeLines(normalized) const directives: NativeChatVisualDirective[] = [] - let inFence = false - const next = lines.map((line) => { - if (inFence) { - inFence = !CODE_FENCE_CLOSE.test(line) - return line - } - if (CODE_FENCE_OPEN.test(line)) { - inFence = true - return line - } - if (directives.length >= NATIVE_CHAT_VISUAL_MAX_PER_MESSAGE) { + const next = normalized.split('\n').map((line, index) => { + if (code.has(index) || directives.length >= NATIVE_CHAT_VISUAL_MAX_PER_MESSAGE) { return line } const directive = parseNativeChatVisualDirectiveLine(line) diff --git a/mobile/src/components/mounted-bottom-drawer.tsx b/mobile/src/components/mounted-bottom-drawer.tsx index 764a3c5437e..96fb131fe4d 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, @@ -17,8 +16,12 @@ import { resolveBottomDrawerFillHeight } from './bottom-drawer-fill-height' import { resolveBottomDrawerKeyboardInset } from './bottom-drawer-keyboard-inset' 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 { + useBottomDrawerModalHost, + useBottomDrawerModalHostBackClaim +} from './bottom-drawer-modal-host-scope' 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,12 +71,12 @@ 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. const { isWideLayout, modalMaxWidth } = useResponsiveLayout() - const insideModalHost = useInsideBottomDrawerModalHost() + const insideModalHost = useBottomDrawerModalHost() !== null const fillHeight = fillAvailable ? resolveBottomDrawerFillHeight({ screenHeight, @@ -202,6 +205,7 @@ export function MountedBottomDrawer({ } : null ) + useBottomDrawerModalHostBackClaim(visible && interactive ? dismiss : null) const scrollHandler = useAnimatedScrollHandler((event) => { scrollOffsetY.value = Math.max(event.contentOffset.y, 0) @@ -376,7 +380,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/components/use-mobile-markdown-blocks.ts b/mobile/src/components/use-mobile-markdown-blocks.ts deleted file mode 100644 index 05074eefcac..00000000000 --- a/mobile/src/components/use-mobile-markdown-blocks.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useMemo } from 'react' -import type { NativeChatVisualDirective } from '../../../src/shared/native-chat-visual-directive' -import { normalizeMobileMarkdownPreviewHtml } from './mobile-markdown-preview-html' -import { parseMobileMarkdown, type MobileMarkdownBlock } from './mobile-markdown-parser' -import { protectMobileMarkdownVisualLines } from './mobile-markdown-visual-lines' - -const NO_DIRECTIVES: NativeChatVisualDirective[] = [] - -/** The blocks MobileMarkdown draws; with `visuals`, directive lines become `visual` blocks. */ -export function useMobileMarkdownBlocks( - text: string, - visuals: boolean -): { blocks: MobileMarkdownBlock[]; directives: NativeChatVisualDirective[] } { - const visualLines = useMemo( - () => (visuals ? protectMobileMarkdownVisualLines(text) : null), - [text, visuals] - ) - const previewText = useMemo( - () => normalizeMobileMarkdownPreviewHtml(visualLines?.text ?? text), - [visualLines, text] - ) - const directives = visualLines?.directives ?? NO_DIRECTIVES - const blocks = useMemo( - () => parseMobileMarkdown(previewText, directives.length), - [previewText, directives.length] - ) - return { blocks, directives } -} diff --git a/mobile/src/components/use-mobile-markdown-tree.ts b/mobile/src/components/use-mobile-markdown-tree.ts new file mode 100644 index 00000000000..67f7928beb7 --- /dev/null +++ b/mobile/src/components/use-mobile-markdown-tree.ts @@ -0,0 +1,22 @@ +import type { Root } from 'mdast' +import { useMemo } from 'react' +import type { NativeChatVisualDirective } from '../../../src/shared/native-chat-visual-directive' +import { parseMobileMarkdownTree } from './mobile-markdown-tree' +import { protectMobileMarkdownVisualLines } from './mobile-markdown-visual-lines' + +const NO_DIRECTIVES: NativeChatVisualDirective[] = [] + +/** The tree MobileMarkdown draws; with `visuals`, directive lines become placeholders. */ +export function useMobileMarkdownTree( + text: string, + visuals: boolean, + growing: boolean +): { root: Root; directives: NativeChatVisualDirective[] } { + const visualLines = useMemo( + () => (visuals ? protectMobileMarkdownVisualLines(text) : null), + [text, visuals] + ) + const source = visualLines?.text ?? text + const root = useMemo(() => parseMobileMarkdownTree(source, growing), [source, growing]) + return { root, directives: visualLines?.directives ?? NO_DIRECTIVES } +} 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/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/MobileNativeChatLongPressContent.tsx b/mobile/src/session/MobileNativeChatLongPressContent.tsx index ca6e1b149b3..1481dc468d1 100644 --- a/mobile/src/session/MobileNativeChatLongPressContent.tsx +++ b/mobile/src/session/MobileNativeChatLongPressContent.tsx @@ -1,30 +1,20 @@ import type { ComponentProps, ReactNode } from 'react' import { Pressable, View } from 'react-native' -// A held press selects text natively; claiming it keeps that release from counting as a tap. -function ignoreLongPress(): void {} - /** A message body that opens the actions sheet on long press (Android, which has no inline - * selection) or reveals its copy button on tap (platforms with inline selection). */ + * selection); elsewhere a plain view, so a held press selects text natively. */ export function MobileNativeChatLongPressContent({ onLongPress, - onPress, style, children }: { onLongPress?: () => void - onPress?: () => void style: ComponentProps['style'] children: ReactNode }): React.JSX.Element { - return onLongPress || onPress ? ( + return onLongPress ? ( // Not one accessible element: screen readers keep reaching the text and controls inside. - + {children} ) : ( diff --git a/mobile/src/session/MobileNativeChatMessage.android.test.ts b/mobile/src/session/MobileNativeChatMessage.android.test.ts index 52032ebae90..24ef4bfb698 100644 --- a/mobile/src/session/MobileNativeChatMessage.android.test.ts +++ b/mobile/src/session/MobileNativeChatMessage.android.test.ts @@ -76,7 +76,7 @@ describe('MobileNativeChatMessage on Android', () => { expect(byType('MessageActionsSheet')).toHaveLength(0) expect( byType('Pressable').filter((node) => node.props.accessibilityLabel === 'Copy message') - ).toHaveLength(0) + ).toHaveLength(1) const [bubble] = byType('Pressable') expect(typeof bubble!.props.onLongPress).toBe('function') @@ -155,7 +155,7 @@ describe('MobileNativeChatMessage on Android', () => { } expect( byType('Pressable').filter((node) => node.props.accessibilityLabel === 'Copy message') - ).toHaveLength(0) + ).toHaveLength(1) act(() => byType('Pressable')[0]!.props.onLongPress()) expect(byType('MessageActionsSheet')).toHaveLength(1) }) diff --git a/mobile/src/session/MobileNativeChatMessage.copy.test.tsx b/mobile/src/session/MobileNativeChatMessage.copy.test.tsx index 090de93dc7a..f0793188d19 100644 --- a/mobile/src/session/MobileNativeChatMessage.copy.test.tsx +++ b/mobile/src/session/MobileNativeChatMessage.copy.test.tsx @@ -69,18 +69,10 @@ describe('iOS whole-message copy', () => { } return node } - function tapMessage() { - const body = renderer!.root - .findByType(MobileNativeChatLongPressContent) - .findAll((node) => String(node.type) === 'Pressable')[0] - act(() => body?.props.onPress?.()) - } - // Every copy case starts from a tapped message, the only state that shows the button. function render(value = message) { act(() => { renderer = create(createElement(MobileNativeChatMessage, { message: value })) }) - tapMessage() } beforeEach(() => { vi.clearAllMocks() @@ -122,11 +114,8 @@ describe('iOS whole-message copy', () => { for (const node of nodes('MobileMarkdown')) { expect(node.props.onLongPress).toBeUndefined() } - // A held press selects text natively: no actions sheet, and no copy toggle. - const press = body.findAll((node) => String(node.type) === 'Pressable')[0] - act(() => press?.props.onLongPress()) - expect(nodes('MessageActionsSheet')).toHaveLength(0) - expect(copyButton()).toBeDefined() + // A held press selects text natively: the body claims no press of its own. + expect(body.findAll((node) => String(node.type) === 'Pressable')).toHaveLength(0) await act(async () => copy.props.onPress()) expect(writeText.mock.calls).toEqual([[first + '\n\n' + last]]) expect(nodes('MessageActionsSheet')).toHaveLength(0) @@ -141,25 +130,13 @@ describe('iOS whole-message copy', () => { } ) - it('shows the copy button only after a tap on the message, and hides it on the next', () => { - act(() => { - renderer = create(createElement(MobileNativeChatMessage, { message })) - }) - const hasCopy = () => - nodes('Pressable').some((node) => node.props.accessibilityLabel === 'Copy message') - const body = renderer!.root - .findByType(MobileNativeChatLongPressContent) - .findAll((node) => String(node.type) === 'Pressable')[0] - expect(hasCopy()).toBe(false) - expect(body?.props).toMatchObject({ accessible: false }) - act(() => body?.props.onLongPress()) - expect(hasCopy()).toBe(false) - - tapMessage() - expect(hasCopy()).toBe(true) - tapMessage() - expect(hasCopy()).toBe(false) - }) + it.each(['user', 'assistant'] as const)( + 'shows the %s message copy button without a tap', + (role) => { + render({ ...message, role }) + expect(copyButton()).toBeDefined() + } + ) it.each([ 'Please repair this chart:\n::orca-visual{file="usage.html"}\nKeep this text.', diff --git a/mobile/src/session/MobileNativeChatMessage.tsx b/mobile/src/session/MobileNativeChatMessage.tsx index e539f506222..3dac76c5db2 100644 --- a/mobile/src/session/MobileNativeChatMessage.tsx +++ b/mobile/src/session/MobileNativeChatMessage.tsx @@ -211,9 +211,6 @@ function MobileNativeChatMessageImpl({ // Keep the memoized Markdown context stable as the message streams. const openActions = useCallback(() => setActionsOpen(true), []) const onLongPress = INLINE_TEXT_SELECTION ? undefined : openActions - // Like desktop's hover-only copy: a tap on the message shows it, another tap hides it. - const [copyShown, setCopyShown] = useState(false) - const toggleCopy = useCallback(() => setCopyShown((shown) => !shown), []) const renderVisual = useContext(MobileNativeChatVisualContext) ?? undefined // Structured replies grow in place: only the last block of the newest row may still be typing. const growingBlock = @@ -262,8 +259,9 @@ function MobileNativeChatMessageImpl({ ) } + // Always shown, so the row never shifts when copy appears. const copyText = - INLINE_TEXT_SELECTION && (message.role === 'user' || message.role === 'assistant') + message.role === 'user' || message.role === 'assistant' ? nativeChatMessagePlainText(message) : '' return ( @@ -273,7 +271,6 @@ function MobileNativeChatMessageImpl({ 0 ? toggleCopy : undefined} style={[ styles.content, isUser && styles.userBubble, @@ -322,7 +319,7 @@ function MobileNativeChatMessageImpl({ ) : null} {unsentNotice ? {unsentNotice} : null} - {copyShown && copyText.length > 0 ? ( + {copyText.length > 0 ? ( 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/tasks/MobileTasksLegacySurface.tsx b/mobile/src/tasks/MobileTasksLegacySurface.tsx index 6bdef8a3c75..382c7cc186f 100644 --- a/mobile/src/tasks/MobileTasksLegacySurface.tsx +++ b/mobile/src/tasks/MobileTasksLegacySurface.tsx @@ -7,6 +7,7 @@ import { AlertTriangle, colors, ActionSheetModal, + BottomDrawerModalHost, GitBranch, ConfirmModal } from './mobile-tasks-dependencies' @@ -180,202 +181,210 @@ export function MobileTasksLegacySurface({ model }: { model: ConnectionPresentat {renderMobileTasksListSurface(model)} - {renderMobileTasksProviderPicker(model)} + {/* Why: Tasks stacks sheets (an issue detail under Create Workspace or a confirm), and iOS + refuses a native modal over a presented sibling, so they share one Modal, ordered by zIndex. */} + + {renderMobileTasksProviderPicker(model)} - {renderMobileTasksRepoPicker(model)} + {renderMobileTasksRepoPicker(model)} - {renderMobileTasksGitHubIssueSourcePicker(model)} + {renderMobileTasksGitHubIssueSourcePicker(model)} - {renderMobileTasksGitHubViewPicker(model)} + {renderMobileTasksGitHubViewPicker(model)} - {renderMobileTasksGitHubPresetPicker(model)} + {renderMobileTasksGitHubPresetPicker(model)} - {renderMobileTasksPagePicker(model)} + {renderMobileTasksPagePicker(model)} - {renderMobileTasksGitHubProjectPicker(model)} + {renderMobileTasksGitHubProjectPicker(model)} - {renderMobileTasksGitHubProjectViewPicker(model)} + {renderMobileTasksGitHubProjectViewPicker(model)} - {renderMobileTasksGitHubProjectSortPicker(model)} + {renderMobileTasksGitHubProjectSortPicker(model)} - {renderMobileTasksGitHubProjectFieldsPicker(model)} + {renderMobileTasksGitHubProjectFieldsPicker(model)} - {renderMobileTasksGitLabViewPicker(model)} + {renderMobileTasksGitLabViewPicker(model)} - {renderMobileTasksGitLabFilterPicker(model)} + {renderMobileTasksGitLabFilterPicker(model)} - {renderMobileTasksLinearFilterPicker(model)} - {renderMobileTasksJiraFilterPicker(model)} - {renderMobileTasksJiraSitePicker(model)} + {renderMobileTasksLinearFilterPicker(model)} + {renderMobileTasksJiraFilterPicker(model)} + {renderMobileTasksJiraSitePicker(model)} - {renderMobileTasksLinearWorkspacePicker(model)} + {renderMobileTasksLinearWorkspacePicker(model)} - {renderMobileTasksLinearTeamPicker(model)} + {renderMobileTasksLinearTeamPicker(model)} - {renderMobileTasksLinearStatusPicker(model)} + {renderMobileTasksLinearStatusPicker(model)} - {renderMobileTasksLinearViewPicker(model)} + {renderMobileTasksLinearViewPicker(model)} - {renderMobileTasksLinearGroupPicker(model)} + {renderMobileTasksLinearGroupPicker(model)} - {renderMobileTasksLinearOrderPicker(model)} + {renderMobileTasksLinearOrderPicker(model)} - {renderMobileTasksLinearDisplayPicker(model)} + {renderMobileTasksLinearDisplayPicker(model)} - {renderMobileTasksSortPicker(model)} + {renderMobileTasksSortPicker(model)} - {renderMobileTasksCreateDrawer(model)} + {renderMobileTasksCreateDrawer(model)} - {renderMobileTasksCreateTargetPicker(model)} + {renderMobileTasksCreateTargetPicker(model)} - {renderMobileTasksLinearConnectDrawer(model)} + {renderMobileTasksLinearConnectDrawer(model)} - {renderMobileTasksWorkspaceCreateTargetPicker(model)} + {renderMobileTasksWorkspaceCreateTargetPicker(model)} - {renderMobileTasksWorkspaceCreateDrawer(model)} + {renderMobileTasksWorkspaceCreateDrawer(model)} - {renderMobileTasksWorkspaceCreateRepoPicker(model)} + {renderMobileTasksWorkspaceCreateRepoPicker(model)} - {renderMobileTasksWorkspaceAgentPicker(model)} + {renderMobileTasksWorkspaceAgentPicker(model)} - {renderMobileTasksWorkspaceBaseBranchPicker(model)} + {renderMobileTasksWorkspaceBaseBranchPicker(model)} - {renderMobileTasksWorkspaceSparsePicker(model)} + {renderMobileTasksWorkspaceSparsePicker(model)} - {renderMobileTasksWorkspaceSparseDrawer(model)} + {renderMobileTasksWorkspaceSparseDrawer(model)} - {renderMobileTasksSetupTrustDrawer(model)} + {renderMobileTasksSetupTrustDrawer(model)} - {renderMobileTasksOrcaYamlTrustDrawer(model)} + {renderMobileTasksOrcaYamlTrustDrawer(model)} - {renderMobileTasksProjectMissingRepoDrawer(model)} + {renderMobileTasksProjectMissingRepoDrawer(model)} - {renderMobileTasksProjectDetailDrawer(model)} + {renderMobileTasksProjectDetailDrawer(model)} - {renderMobileTasksItemDetailDrawer(model)} + {renderMobileTasksItemDetailDrawer(model)} - ({ - label: getHostedReviewMergeMethodLabel(method), - icon: GitBranch, - onPress: () => { - setPendingProjectGitHubMerge({ row: mergeMethodProjectRow, method }) - } - })) - : [] - } - onClose={() => setMergeMethodProjectRow(null)} - /> - ({ - label: - mergeMethodTaskItem.provider === 'gitlab' && method === 'merge' - ? 'Merge' - : getHostedReviewMergeMethodLabel(method), - icon: GitBranch, - onPress: () => { - const item = mergeMethodTaskItem - setPendingHostedMerge({ item, method }) - } - })) - : [] - } - onClose={() => setMergeMethodTaskItem(null)} - /> - { - if (!taskUiReady || !pendingHostedMerge) { - return + ({ + label: getHostedReviewMergeMethodLabel(method), + icon: GitBranch, + onPress: () => { + setPendingProjectGitHubMerge({ row: mergeMethodProjectRow, method }) + } + })) + : [] } - void mergeHostedReview(pendingHostedMerge.item, pendingHostedMerge.method) - }} - onCancel={() => setPendingHostedMerge(null)} - /> - { - if (!taskUiReady || !pendingProjectGitHubMerge) { - return + onClose={() => setMergeMethodProjectRow(null)} + /> + setPendingProjectGitHubMerge(null)} - /> - { - if (!taskUiReady || !pendingHostedStateChange) { - return + actions={ + mergeMethodTaskItem + ? (mergeMethodTaskItem.provider === 'gitlab' + ? (['merge', 'squash', 'rebase'] as const) + : (['squash', 'merge', 'rebase'] as const) + ).map((method) => ({ + label: + mergeMethodTaskItem.provider === 'gitlab' && method === 'merge' + ? 'Merge' + : getHostedReviewMergeMethodLabel(method), + icon: GitBranch, + onPress: () => { + const item = mergeMethodTaskItem + setPendingHostedMerge({ item, method }) + } + })) + : [] } - if (pendingHostedStateChange.source === 'task') { - if (pendingHostedStateChange.item.provider === 'gitlab') { - void toggleGitLabStatus(pendingHostedStateChange.item) + onClose={() => setMergeMethodTaskItem(null)} + /> + { + if (!taskUiReady || !pendingHostedMerge) { return } - void toggleGitHubStatus(pendingHostedStateChange.item) - return + void mergeHostedReview(pendingHostedMerge.item, pendingHostedMerge.method) + }} + onCancel={() => setPendingHostedMerge(null)} + /> + setPendingHostedStateChange(null)} - /> + confirmLabel={ + pendingProjectGitHubMerge + ? getHostedReviewMergeMethodLabel(pendingProjectGitHubMerge.method) + : 'Merge' + } + onConfirm={() => { + if (!taskUiReady || !pendingProjectGitHubMerge) { + return + } + void mergeProjectGitHubPullRequest( + pendingProjectGitHubMerge.row, + pendingProjectGitHubMerge.method + ) + }} + onCancel={() => setPendingProjectGitHubMerge(null)} + /> + { + if (!taskUiReady || !pendingHostedStateChange) { + return + } + if (pendingHostedStateChange.source === 'task') { + if (pendingHostedStateChange.item.provider === 'gitlab') { + void toggleGitLabStatus(pendingHostedStateChange.item) + return + } + void toggleGitHubStatus(pendingHostedStateChange.item) + return + } + void mutateProjectRowIssueOrPr(pendingHostedStateChange.row, { + state: pendingHostedStateChange.nextState + }) + }} + onCancel={() => setPendingHostedStateChange(null)} + /> + ) } diff --git a/mobile/src/tasks/mobile-tasks-dependencies.ts b/mobile/src/tasks/mobile-tasks-dependencies.ts index aa473eb04af..2be9049c975 100644 --- a/mobile/src/tasks/mobile-tasks-dependencies.ts +++ b/mobile/src/tasks/mobile-tasks-dependencies.ts @@ -65,6 +65,7 @@ export { classifyConnection } from '../transport/connection-health' export { StatusDot } from '../components/StatusDot' export { ActionSheetModal } from '../components/ActionSheetModal' export { BottomDrawer } from '../components/BottomDrawer' +export { BottomDrawerModalHost } from '../components/bottom-drawer-modal-host' export { ConfirmModal } from '../components/ConfirmModal' export { MobileMarkdown } from '../components/MobileMarkdown' export { MobileAgentIcon } from '../components/MobileAgentIcon' diff --git a/mobile/src/tasks/mobile-tasks-project-detail-drawer.tsx b/mobile/src/tasks/mobile-tasks-project-detail-drawer.tsx index 47a071d75f4..31bf98791cc 100644 --- a/mobile/src/tasks/mobile-tasks-project-detail-drawer.tsx +++ b/mobile/src/tasks/mobile-tasks-project-detail-drawer.tsx @@ -22,7 +22,8 @@ import { SHOW_MOBILE_PROJECT_METADATA_EDITORS, githubProjectOptionColor, canCreateWorkspaceFromProjectRow, - projectRowType + projectRowType, + TASK_SECONDARY_DRAWER_Z_INDEX } from './mobile-tasks-legacy-foundation' import { renderMobileTasksProjectFieldEditors } from './mobile-tasks-project-field-editors' import { @@ -45,6 +46,8 @@ export function renderMobileTasksProjectMissingRepoDrawer(model: ConnectionPrese onClose={() => { setProjectRepoNotInOrca(null) }} + // Why: opens over the project detail, which renders after it in the shared host. + zIndex={TASK_SECONDARY_DRAWER_Z_INDEX} > {projectRepoNotInOrca ? ( 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/workspace-list-sections.test.ts b/mobile/src/worktree/workspace-list-sections.test.ts index 0e82229f0ec..f5dfe3f52c6 100644 --- a/mobile/src/worktree/workspace-list-sections.test.ts +++ b/mobile/src/worktree/workspace-list-sections.test.ts @@ -211,6 +211,19 @@ describe('filterWorktrees', () => { ) ).toEqual([folder]) }) + + it("drops rows the desktop's Hide other-client workspaces hides", () => { + const mine = worktree({ worktreeId: 'mine' }) + const other = worktree({ worktreeId: 'other', desktopOtherClient: true }) + + expect( + filterWorktrees( + [mine, other], + { filterRepoIds: new Set(), hideSleeping: false, hideDefaultBranch: false }, + '' + ) + ).toEqual([mine]) + }) }) describe('getWorktreeStatus', () => { diff --git a/mobile/src/worktree/workspace-list-sections.ts b/mobile/src/worktree/workspace-list-sections.ts index d231d81ba54..9f3b480d4b3 100644 --- a/mobile/src/worktree/workspace-list-sections.ts +++ b/mobile/src/worktree/workspace-list-sections.ts @@ -95,7 +95,7 @@ export function filterWorktrees( filters: FilterState, search: string ): Worktree[] { - let result = worktrees.filter((w) => !w.isArchived) + let result = worktrees.filter((w) => !w.isArchived && w.desktopOtherClient !== true) if (filters.hideSleeping) { result = result.filter( (w) => isSleepingSweepExempt(w, filters.alwaysShowDefaultBranch) || isWorktreeActive(w) diff --git a/mobile/src/worktree/workspace-list-types.ts b/mobile/src/worktree/workspace-list-types.ts index b2d592431b1..7d9061ec2ca 100644 --- a/mobile/src/worktree/workspace-list-types.ts +++ b/mobile/src/worktree/workspace-list-types.ts @@ -29,6 +29,8 @@ export type Worktree = { isArchived?: boolean isMainWorktree?: boolean hasHostSidebarActivity?: boolean + /** The paired desktop's "Hide other-client workspaces" hides this row. */ + desktopOtherClient?: true worktreeInstanceId?: string lineageWorktreeInstanceId?: string parentWorktreeInstanceId?: string 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/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-idle-shutdown.test.ts b/src/main/daemon/daemon-idle-shutdown.test.ts index 43a82df72bb..051bd1ff1ec 100644 --- a/src/main/daemon/daemon-idle-shutdown.test.ts +++ b/src/main/daemon/daemon-idle-shutdown.test.ts @@ -464,10 +464,14 @@ describe('current daemon lifecycle retirement', () => { liveSessions: 1, admissionReopened: true }) + const alreadyLiveSubprocess = subprocess + subprocess = createMockSubprocess() await expect( adapter.spawn({ sessionId: 'allowed-after-refusal', cols: 80, rows: 24 }) ).resolves.toMatchObject({ id: 'allowed-after-refusal' }) expect(onIdleShutdown).not.toHaveBeenCalled() + alreadyLiveSubprocess.exit(0) + subprocess.exit(0) adapter.dispose() }) 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/filesystem/filesystem-write-handlers.ts b/src/main/ipc/filesystem/filesystem-write-handlers.ts index ee9646ac3e6..6bf20868f28 100644 --- a/src/main/ipc/filesystem/filesystem-write-handlers.ts +++ b/src/main/ipc/filesystem/filesystem-write-handlers.ts @@ -14,6 +14,8 @@ import { registerFilesystemMutationHandlers } from '../filesystem-mutations' import { registerAgentSessionAttachmentUploadHandlers } from '../agent-session-attachment-upload-ipc' import type { FilesystemHandlerContext } from './filesystem-handler-context' import { assertLocalWriteTargetIsRegularFile } from './local-regular-file-read' +import { runNativeChatScratchSave } from '../../native-chat/native-chat-scratch-folder-lock' +import { getProfileUserDataPath } from '../../orca-profiles/profile-storage-paths' export function registerFilesystemWriteHandlers(context: FilesystemHandlerContext): void { const { store } = context @@ -50,7 +52,9 @@ export function registerFilesystemWriteHandlers(context: FilesystemHandlerContex } } await assertLocalWriteTargetIsRegularFile(filePath) - await writeFile(filePath, args.content, 'utf-8') + await runNativeChatScratchSave(getProfileUserDataPath(), filePath, () => + writeFile(filePath, args.content, 'utf-8') + ) } ) 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/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/runtime-environment-mobile-relay-hosts.ts b/src/main/ipc/runtime-environment-mobile-relay-hosts.ts index 595ed8fae40..b04b630fcbe 100644 --- a/src/main/ipc/runtime-environment-mobile-relay-hosts.ts +++ b/src/main/ipc/runtime-environment-mobile-relay-hosts.ts @@ -68,7 +68,8 @@ export function createRuntimeEnvironmentMobileRelayHosts(): MobileDesktopRelayHo source: environment.source, orcadDeployment: environment.orcadDeployment, pairingRevision: pairingRevision(environment), - runtimeId: environment.runtimeId + runtimeId: environment.runtimeId, + ...(environment.pairedDeviceId ? { pairedDeviceId: environment.pairedDeviceId } : {}) })), statusByEnvironmentId } 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/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/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-row-table.ts b/src/main/native-chat/agent-session-journal/journal-row-table.ts index 7ff185857cf..8a108c5f80f 100644 --- a/src/main/native-chat/agent-session-journal/journal-row-table.ts +++ b/src/main/native-chat/agent-session-journal/journal-row-table.ts @@ -21,12 +21,23 @@ const SELECT_ROWS_AFTER = `SELECT seq, ts, row_json FROM journal_rows WHERE session_id = ? AND epoch = ? AND seq > ? ORDER BY seq ASC` const SELECT_ROWS_AFTER_LIMITED = `${SELECT_ROWS_AFTER} LIMIT ?` const DELETE_EPOCH = 'DELETE FROM journal_rows WHERE session_id = ? AND epoch = ?' +const SELECT_LAST_ACTIVITY = 'SELECT MAX(ts) AS ts FROM journal_rows WHERE session_id = ?' export function readJournalSessionEpoch(db: Database.Database, sessionId: string): string | null { const epoch = db.prepare(SELECT_EPOCH).get(sessionId)?.epoch return typeof epoch === 'string' ? epoch : null } +/** When the chat's newest row was saved, across every epoch; null for a chat with no rows. One + * aggregate over the chat's key range, so no row body reaches this process. */ +export function readJournalSessionLastActivityAt( + db: Database.Database, + sessionId: string +): number | null { + const ts = db.prepare(SELECT_LAST_ACTIVITY).get(sessionId)?.ts + return typeof ts === 'number' ? ts : null +} + /** Points the chat at `epoch`. Only an epoch change writes this row. */ export function publishJournalSessionEpoch( db: Database.Database, 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 3546a7a6b8f..f54be54da13 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 @@ -115,6 +115,7 @@ export function deliverToSubscriber( }, fence: subscriber.fence, ...(page.latestTurn !== undefined ? { latestTurn: page.latestTurn } : {}), + ...(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-conversation-lifetime.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-conversation-lifetime.ts index 431ca9e761d..2574f6c3a8a 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-conversation-lifetime.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-conversation-lifetime.ts @@ -77,6 +77,13 @@ export function createStructuredAgentSessionConversationLifetime(host: { return closed } + const hasOpenDispatch = (sessionId: string): boolean => { + const record = deps().store.getRecord(sessionId) + return record !== null && deps().hasOpenDispatch?.(record) === true + } + const providerHoldsDispatch = (sessionId: string): boolean => + deps().adapter.holdsDispatch?.(sessionId) === true + const idleSweep = new StructuredAgentSessionIdleSweep({ sessions, serialize, @@ -84,11 +91,8 @@ export function createStructuredAgentSessionConversationLifetime(host: { isDisposed: () => disposed, deliveryActive: host.deliveryActive, childWork: host.readChildWork, - hasOpenDispatch: (sessionId) => { - const record = deps().store.getRecord(sessionId) - return record !== null && deps().hasOpenDispatch?.(record) === true - }, - providerHoldsDispatch: (sessionId) => deps().adapter.holdsDispatch?.(sessionId) === true, + hasOpenDispatch, + providerHoldsDispatch, // The host puts an idle agent to rest: a turn it cuts short is news, not the user's Stop. stopAgent: (sessionId) => stopAgent(sessionId, { cause: 'evict', resting: true }), // A host stop: the child's end rejects what it was handed and writes the one error row, and @@ -130,6 +134,18 @@ export function createStructuredAgentSessionConversationLifetime(host: { }) ) }, + /** Runs `task` in the chat's lane, so no start interleaves with it, unless quit has begun or + * the host holds a child, a delivery or a dispatch for the chat; null then. */ + runWhileExecutionIdle: (sessionId: string, task: () => Promise): Promise => + serialize(sessionId, async () => + disposed || + sessions.get(sessionId)?.child || + host.deliveryActive(sessionId) || + providerHoldsDispatch(sessionId) || + hasOpenDispatch(sessionId) + ? null + : await task() + ), /** Quit has begun: nothing opens a conversation or sweeps one after this. */ dispose: (): void => { disposed = true diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts index b5716d6cdfc..3e364c4a8ab 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts @@ -177,8 +177,7 @@ export class StructuredAgentSessionHost { private now = (): number => this.deps.now?.() ?? Date.now() hasSession = (sessionId: string): boolean => this.sessions.has(sessionId) - hasLoadedProviders = (): boolean => - [...this.sessions.values()].some(({ child }) => child !== null) + hasLoadedProviders = () => [...this.sessions.values()].some(({ child }) => child !== null) sessionAgent = (sessionId: string) => this.deps.store.getRecord(sessionId)?.provider ?? null handleAdapterEvent = (event: Parameters[0]) => @@ -232,6 +231,8 @@ export class StructuredAgentSessionHost { showSessionTabs = this.tabs.showSessionTabs setSessionTabVisibility = this.tabs.setSessionTabVisibility notifySessionTabHidden = this.tabs.notifySessionTabHidden + runWhileExecutionIdle = (sessionId: string, task: () => Promise): Promise => + this.lifetime.runWhileExecutionIdle(sessionId, task) /** This runtime holds a chat record, readable or not. */ holdsSessions = (): boolean => this.deps.store.holdsRecords() onSessionsHeld = (listener: () => void): (() => void) => this.deps.store.onFirstRecord(listener) diff --git a/src/main/native-chat/native-chat-scratch-folder-lock.ts b/src/main/native-chat/native-chat-scratch-folder-lock.ts new file mode 100644 index 00000000000..a270f804151 --- /dev/null +++ b/src/main/native-chat/native-chat-scratch-folder-lock.ts @@ -0,0 +1,72 @@ +// Orders an Orca save into a chat's scratch folder against the sweep expiring that folder. Expiry +// renames the folder aside under this lock, so a save either lands first (its candidate mark keeps +// the folder) or runs after and fails on the missing folder, leaving the draft unsaved. A save +// never takes the session lane, so it never waits for an agent start. + +import { relative } from 'node:path' +import { runKeyedSerializedOperation } from '../cli/keyed-promise-queue' +import { + isNativeChatScratchFolderName, + nativeChatScratchRootFor +} from './native-chat-scratch-folder' + +type ExpiryCandidate = { current: boolean } + +const folderQueues = new Map>() +// The age scan runs outside the lock, so a save that settles after it starts makes it stale. +// A set per folder: each runtime install runs its own sweep, so two can scan one folder at once. +const expiryCandidates = new Map>() + +/** The scratch folder `filePath` lies inside, or null for any other path. */ +function scratchFolderNameOf(stateDirectory: string, filePath: string): string | null { + const segments = relative(nativeChatScratchRootFor(stateDirectory), filePath).split(/[\\/]/) + return segments.length > 1 && isNativeChatScratchFolderName(segments[0]) ? segments[0] : null +} + +/** Runs an Orca save; one into a chat scratch folder holds that folder's lock. */ +export function runNativeChatScratchSave( + stateDirectory: string, + filePath: string, + save: () => Promise +): Promise { + const folderName = scratchFolderNameOf(stateDirectory, filePath) + if (!folderName) { + return save() + } + return runKeyedSerializedOperation(folderQueues, folderName, async () => { + try { + return await save() + } finally { + for (const candidate of expiryCandidates.get(folderName) ?? []) { + candidate.current = false + } + } + }) +} + +export function runUnderNativeChatScratchFolderLock( + folderName: string, + task: () => Promise +): Promise { + return runKeyedSerializedOperation(folderQueues, folderName, task) +} + +/** Registered before the sweep's age scan; the scan stays usable only while `isCurrent`. */ +export function trackNativeChatScratchExpiryCandidate(folderName: string): { + isCurrent: () => boolean + dispose: () => void +} { + const candidate: ExpiryCandidate = { current: true } + const candidates = expiryCandidates.get(folderName) ?? new Set() + candidates.add(candidate) + expiryCandidates.set(folderName, candidates) + return { + isCurrent: () => candidate.current, + dispose: () => { + candidates.delete(candidate) + if (candidates.size === 0) { + expiryCandidates.delete(folderName) + } + } + } +} diff --git a/src/main/native-chat/native-chat-scratch-folder.ts b/src/main/native-chat/native-chat-scratch-folder.ts index e52acaf1624..bd82f646b36 100644 --- a/src/main/native-chat/native-chat-scratch-folder.ts +++ b/src/main/native-chat/native-chat-scratch-folder.ts @@ -5,6 +5,7 @@ import { createHash } from 'node:crypto' import { join } from 'node:path' const NATIVE_CHAT_SCRATCH_DIR_NAME = 'chat-scratch' +const FOLDER_NAME = /^[0-9a-f]{32}$/ /** Where every chat's scratch folder on this host lives. */ export function nativeChatScratchRootFor(stateDirectory: string): string { @@ -16,6 +17,11 @@ export function nativeChatScratchFolderName(sessionId: string): string { return createHash('sha256').update(sessionId, 'utf8').digest('hex').slice(0, 32) } +/** A name this host minted for a chat's folder; nothing else under the root is a chat's. */ +export function isNativeChatScratchFolderName(name: string): boolean { + return FOLDER_NAME.test(name) +} + /** * `/chat-scratch/`. Keyed by the session id * alone, the chat record's primary key, so the folder needs no workspace lookup to find or remove; diff --git a/src/main/native-chat/native-chat-visuals-sweep.test.ts b/src/main/native-chat/native-chat-visuals-sweep.test.ts index d5daee4eaa3..6592f3d6735 100644 --- a/src/main/native-chat/native-chat-visuals-sweep.test.ts +++ b/src/main/native-chat/native-chat-visuals-sweep.test.ts @@ -1,16 +1,35 @@ -import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + statSync, + symlinkSync, + utimesSync, + writeFileSync +} from 'node:fs' +import { writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { basename, join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' -import type { AgentSessionExecutionLocation } from '../../shared/agent-session-record' -import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' +import { runKeyedSerializedOperation } from '../cli/keyed-promise-queue' import type { StructuredAgentSessionLogger } from './agent-session-wire/structured-agent-session-logger' import { nativeChatScratchFolderFor, nativeChatScratchRootFor } from './native-chat-scratch-folder' +import { + runNativeChatScratchSave, + trackNativeChatScratchExpiryCandidate +} from './native-chat-scratch-folder-lock' +import { + attach, + hostTestState +} from './agent-session-wire/structured-agent-session-host-test-harness' +import { HOST_TEST_SESSION } from './agent-session-wire/structured-agent-session-host-test-data' import { scheduleNativeChatVisualsSweep, sweepNativeChatVisualsFolders, - type NativeChatVisualsSweepDeps, - type NativeChatVisualsWorkspaceVerdict + type NativeChatVisualsSweepDeps } from './native-chat-visuals-sweep' const scratch: string[] = [] @@ -34,25 +53,36 @@ function visualsFor(state: string, sessionId: string): string { return folder } -const location = (workspaceId: string): AgentSessionExecutionLocation => ({ - executionHostId: LOCAL_EXECUTION_HOST_ID, - wslDistro: null, - workspaceId, - workspaceKind: 'git-worktree' -}) +const DAY_MS = 24 * 60 * 60_000 + +/** Sets the mtime of `path` and everything under it to `daysAgo`, children before parents. */ +function age(path: string, daysAgo = 40): void { + if (statSync(path).isDirectory()) { + for (const name of readdirSync(path)) { + age(join(path, name), daysAgo) + } + } + const at = new Date(Date.now() - daysAgo * DAY_MS) + utimesSync(path, at, at) +} function deps( state: string, held: readonly string[] | null, - verdicts: Record = {} + facts: { journal?: Record } = {} ): NativeChatVisualsSweepDeps & { logger: { warn: ReturnType> } + lanes: Map> } { + const lanes = new Map>() return { stateDirectory: state, listHeldSessionIds: () => held, - locationOf: (sessionId) => (held?.includes(sessionId) ? location(`ws-${sessionId}`) : null), - workspaceVerdicts: () => async (where) => verdicts[where.workspaceId] ?? 'present', + lastJournalActivityAt: (sessionId) => facts.journal?.[sessionId] ?? null, + // Stands in for the host's lane: one task at a time per chat. + runWhileExecutionIdle: (sessionId: string, task: () => Promise) => + runKeyedSerializedOperation(lanes, sessionId, task), + lanes, logger: { warn: vi.fn(), error: vi.fn() } } } @@ -72,32 +102,86 @@ describe('the visuals folder sweep', () => { expect(existsSync(orphan)).toBe(false) }) - it('removes a chat folder only when its local workspace is proven removed', async () => { + it('expires a chat unused for 30 days and keeps one with recent use', async () => { const state = stateDirectory() - const removed = visualsFor(state, 'chat-a') - const unverifiable = visualsFor(state, 'chat-b') - const present = visualsFor(state, 'chat-c') - await sweepNativeChatVisualsFolders( - deps(state, ['chat-a', 'chat-b', 'chat-c'], { - 'ws-chat-a': 'removed', - 'ws-chat-b': 'unverifiable' + const unused = visualsFor(state, 'chat-unused') + const talkedTo = visualsFor(state, 'chat-journal') + const edited = visualsFor(state, 'chat-nested') + mkdirSync(join(edited, 'drafts')) + writeFileSync(join(edited, 'drafts', 'v2.html'), '

v2

') + const legacy = join(state, 'native-chat-visuals', 'a'.repeat(32)) + mkdirSync(legacy, { recursive: true }) + age(nativeChatScratchRootFor(state)) + const fresh = new Date() + utimesSync(join(edited, 'drafts', 'v2.html'), fresh, fresh) + + const result = await sweepNativeChatVisualsFolders( + deps(state, ['chat-unused', 'chat-journal', 'chat-nested'], { + journal: { 'chat-unused': Date.now() - 31 * DAY_MS, 'chat-journal': Date.now() - DAY_MS } }) ) - expect(existsSync(removed)).toBe(false) - expect(existsSync(unverifiable)).toBe(true) - expect(existsSync(present)).toBe(true) + + expect(result).toEqual({ removed: 1, failed: 0 }) + expect(existsSync(unused)).toBe(false) + expect(existsSync(`${unused}.expired`)).toBe(false) + expect(existsSync(talkedTo)).toBe(true) + expect(existsSync(edited)).toBe(true) + expect(existsSync(join(state, 'native-chat-visuals'))).toBe(false) }) - it('keeps a held chat whose workspace verdict throws', async () => { + it('keeps a chat past 30 days while the host runs its agent', async () => { + await attach() + const state = stateDirectory() + const folder = visualsFor(state, HOST_TEST_SESSION) + age(folder) + const result = await sweepNativeChatVisualsFolders({ + ...deps(state, [HOST_TEST_SESSION]), + runWhileExecutionIdle: hostTestState().host.runWhileExecutionIdle + }) + expect(result).toEqual({ removed: 0, failed: 0 }) + expect(existsSync(folder)).toBe(true) + }) + + it('gives a start queued behind expiry a fresh folder', async () => { const state = stateDirectory() const folder = visualsFor(state, 'chat-a') - await sweepNativeChatVisualsFolders({ - ...deps(state, ['chat-a']), - workspaceVerdicts: () => async () => { - throw new Error('catalog unavailable') + age(folder) + const sweep = deps(state, ['chat-a']) + const result = await sweepNativeChatVisualsFolders({ + ...sweep, + runWhileExecutionIdle: (sessionId, task) => { + const expiry = sweep.runWhileExecutionIdle(sessionId, task) + // The start's own lane task, as the host's agent start runs it. + void runKeyedSerializedOperation(sweep.lanes, sessionId, async () => { + mkdirSync(folder, { recursive: true }) + writeFileSync(join(folder, 'new.html'), '

new

') + }) + return expiry } }) - expect(existsSync(folder)).toBe(true) + expect(result).toEqual({ removed: 1, failed: 0 }) + expect(readdirSync(folder)).toEqual(['new.html']) + expect(existsSync(`${folder}.expired`)).toBe(false) + }) + + it('keeps a folder saved into after its age scan', async () => { + const state = stateDirectory() + const folder = visualsFor(state, 'chat-a') + age(folder) + const sweep = deps(state, ['chat-a']) + await sweepNativeChatVisualsFolders({ + ...sweep, + runWhileExecutionIdle: async (sessionId, task) => { + // An overlapping sweep of the same folder ends first; this sweep's candidate must survive. + trackNativeChatScratchExpiryCandidate(basename(folder)).dispose() + // Settles between the scan and the locked recheck, which does not scan again. + await runNativeChatScratchSave(state, join(folder, 'chart.html'), () => + writeFile(join(folder, 'chart.html'), '

saved

') + ) + return sweep.runWhileExecutionIdle(sessionId, task) + } + }) + expect(readFileSync(join(folder, 'chart.html'), 'utf8')).toBe('

saved

') }) it('removes nothing when the held chats cannot be read', async () => { @@ -177,13 +261,4 @@ describe('the visuals folder sweep', () => { await sweepNativeChatVisualsFolders(deps(state, [])) expect(existsSync(join(elsewhere, 'a'.repeat(32)))).toBe(true) }) - - it('takes one verdict snapshot per run', async () => { - const state = stateDirectory() - visualsFor(state, 'chat-a') - visualsFor(state, 'chat-b') - const workspaceVerdicts = vi.fn(() => async () => 'present' as const) - await sweepNativeChatVisualsFolders({ ...deps(state, ['chat-a', 'chat-b']), workspaceVerdicts }) - expect(workspaceVerdicts).toHaveBeenCalledOnce() - }) }) diff --git a/src/main/native-chat/native-chat-visuals-sweep.ts b/src/main/native-chat/native-chat-visuals-sweep.ts index 8306a2aeb9b..01cfd046153 100644 --- a/src/main/native-chat/native-chat-visuals-sweep.ts +++ b/src/main/native-chat/native-chat-visuals-sweep.ts @@ -1,40 +1,49 @@ // How a chat's scratch folder dies. The host never deletes a chat record, so no deletion event -// exists to hang cleanup on; instead each folder's life is derived from the records every run: +// exists to hang cleanup on; instead each folder's life is re-derived every run: // 1. a folder no chat record (readable or not) maps to is removed, so any future record // deletion cleans up for free; -// 2. a folder whose chat ran in a local workspace this host can prove was removed is removed. -// Anything unproven is kept. A failure is logged and never blocks anything else. +// 2. a folder whose chat went unused for the retention window is removed: its newest journal row +// and everything in the folder are older than that, and the host holds no execution for it. +// Rule 2 decides again under the chat's lane and then its folder lock, so a start or a save that +// lands after the first look keeps the folder. A failure is logged and never blocks anything else. import type { Dirent } from 'node:fs' -import { lstat, readdir, rm } from 'node:fs/promises' +import { lstat, readdir, rename, rm } from 'node:fs/promises' import { join } from 'node:path' -import type { AgentSessionExecutionLocation } from '../../shared/agent-session-record' import { isDefinitiveAbsence } from '../../shared/definitive-filesystem-absence' import { yieldToEventLoop } from '../../shared/event-loop-yield' import type { StructuredAgentSessionLogger } from './agent-session-wire/structured-agent-session-logger' -import { nativeChatScratchFolderName, nativeChatScratchRootFor } from './native-chat-scratch-folder' +import { + isNativeChatScratchFolderName, + nativeChatScratchFolderName, + nativeChatScratchRootFor +} from './native-chat-scratch-folder' +import { + runUnderNativeChatScratchFolderLock, + trackNativeChatScratchExpiryCandidate +} from './native-chat-scratch-folder-lock' -/** `removed` only on positive proof from the host that owns the workspace. */ -export type NativeChatVisualsWorkspaceVerdict = 'present' | 'removed' | 'unverifiable' +/** How long a chat's scratch files outlive its last use. */ +export const NATIVE_CHAT_SCRATCH_RETENTION_MS = 30 * 24 * 60 * 60_000 export type NativeChatVisualsSweepDeps = { stateDirectory: string /** Every chat this host holds a row for, readable or not; null when that can't be trusted. */ listHeldSessionIds: () => readonly string[] | null - locationOf: (sessionId: string) => AgentSessionExecutionLocation | null - /** Called at most once per run, so one run judges every chat against one catalog snapshot. - * Absent: only rule 1 runs. */ - workspaceVerdicts?: () => ( - location: AgentSessionExecutionLocation - ) => Promise + /** When the chat's newest journal row was saved; null when it has none. */ + lastJournalActivityAt: (sessionId: string) => number | null + /** Runs `task` in the chat's lane unless the host holds execution for it; null when it does. */ + runWhileExecutionIdle: (sessionId: string, task: () => Promise) => Promise logger: StructuredAgentSessionLogger remove?: (path: string) => Promise } export type NativeChatVisualsSweepResult = { removed: number; failed: number } -// Only names this host minted: anything else in the folder is never touched. -const FOLDER_NAME = /^[0-9a-f]{32}$/ +// Renamed under the locks and deleted after them; a cut-short delete leaves it for a later run. +const EXPIRED_SUFFIX = '.expired' +// The pre-rename root: native chat had no real users, and an update restarts every old child. +const LEGACY_ROOT_NAME = 'native-chat-visuals' // Bounds one run's disk work; whatever is left goes in a later run. const MAX_REMOVALS_PER_RUN = 200 const YIELD_EVERY = 32 @@ -60,6 +69,13 @@ export async function sweepNativeChatVisualsFolders( ): Promise { const result = { removed: 0, failed: 0 } const root = nativeChatScratchRootFor(deps.stateDirectory) + await rm(join(deps.stateDirectory, LEGACY_ROOT_NAME), { recursive: true, force: true }).catch( + (error: unknown) => + deps.logger.warn('the pre-rename chat visuals root could not be removed', { + scope: 'nativeChatVisuals.sweep', + error + }) + ) // Listed before the records are read: a folder is created only after its chat's record, so // every folder seen here already has its record in the snapshot below. const entries = await listFolders(root) @@ -71,9 +87,7 @@ export async function sweepNativeChatVisualsFolders( held.map((sessionId) => [nativeChatScratchFolderName(sessionId), sessionId]) ) const remove = deps.remove ?? ((path: string) => rm(path, { recursive: true, force: true })) - let verdict: ReturnType> | undefined - const verdictFor = (location: AgentSessionExecutionLocation) => - (verdict ??= deps.workspaceVerdicts?.())?.(location) + const cutoff = Date.now() - NATIVE_CHAT_SCRATCH_RETENTION_MS let visited = 0 for (const entry of entries) { if (isStopped() || result.removed + result.failed >= MAX_REMOVALS_PER_RUN) { @@ -82,18 +96,22 @@ export async function sweepNativeChatVisualsFolders( if (++visited % YIELD_EVERY === 0) { await yieldToEventLoop() } + const expired = entry.name.endsWith(EXPIRED_SUFFIX) + const folderName = expired ? entry.name.slice(0, -EXPIRED_SUFFIX.length) : entry.name // A Dirent describes the entry itself, so a symlink is never followed or removed here. - if (!entry.isDirectory() || !FOLDER_NAME.test(entry.name)) { - continue - } - const sessionId = sessionByFolder.get(entry.name) - const reason = - sessionId === undefined ? 'no-record' : await removedWorkspace(deps, sessionId, verdictFor) - if (!reason) { + if (!entry.isDirectory() || !isNativeChatScratchFolderName(folderName)) { continue } + const sessionId = expired ? undefined : sessionByFolder.get(entry.name) + const reason = expired ? 'expired' : sessionId === undefined ? 'no-record' : 'unused' try { - await remove(join(root, entry.name)) + if ( + sessionId !== undefined && + !(await parkUnusedFolder(deps, root, folderName, sessionId, cutoff)) + ) { + continue + } + await remove(join(root, sessionId === undefined ? entry.name : folderName + EXPIRED_SUFFIX)) result.removed += 1 } catch (error) { result.failed += 1 @@ -108,26 +126,69 @@ export async function sweepNativeChatVisualsFolders( return result } -async function removedWorkspace( - deps: Pick, +/** Renames a chat's folder to `.expired` under the chat's lane and then its folder lock; + * false when the chat was used since `cutoff`, holds live work, or a save landed after the scan. */ +async function parkUnusedFolder( + deps: NativeChatVisualsSweepDeps, + root: string, + name: string, sessionId: string, - verdictFor: ( - location: AgentSessionExecutionLocation - ) => Promise | undefined -): Promise<'workspace-removed' | null> { - const location = deps.locationOf(sessionId) - if (!location) { - return null - } + cutoff: number +): Promise { + const folder = join(root, name) + const usedSince = (): boolean => (deps.lastJournalActivityAt(sessionId) ?? 0) >= cutoff + // Before the scan, so a save already running when it starts still marks it stale. + const candidate = trackNativeChatScratchExpiryCandidate(name) try { - return (await verdictFor(location)) === 'removed' ? 'workspace-removed' : null - } catch { - return null + if (usedSince() || (await changedSince(folder, cutoff))) { + return false + } + const parked = await deps.runWhileExecutionIdle(sessionId, () => + runUnderNativeChatScratchFolderLock(name, async () => { + if (!candidate.isCurrent() || usedSince()) { + return false + } + await rename(folder, folder + EXPIRED_SUFFIX) + return true + }) + ) + return parked === true + } catch (error) { + if (isDefinitiveAbsence(error)) { + return false + } + throw error + } finally { + candidate.dispose() } } -// Why minutes, not at boot: the first sweep must not compete with session restore and the -// workspace catalog loading; after that, a few runs a day is enough for files nothing reads. +/** Whether the folder or anything under it changed at or after `cutoff`. Every entry is read with + * lstat, so a link is judged as itself and never followed; the scan stops at the first change. */ +async function changedSince(folder: string, cutoff: number): Promise { + const pending = [folder] + let visited = 0 + for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) { + if ((await lstat(dir)).mtimeMs >= cutoff) { + return true + } + for (const entry of await readdir(dir, { withFileTypes: true })) { + if (++visited % YIELD_EVERY === 0) { + await yieldToEventLoop() + } + const path = join(dir, entry.name) + if (entry.isDirectory()) { + pending.push(path) + } else if ((await lstat(path)).mtimeMs >= cutoff) { + return true + } + } + } + return false +} + +// Why minutes, not at boot: the first sweep must not compete with session restore; after that, a +// few runs a day is enough for files nothing reads. const FIRST_SWEEP_DELAY_MS = 2 * 60_000 const SWEEP_INTERVAL_MS = 6 * 60 * 60_000 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/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/orca-profiles/other-profile-workspace-catalog.test.ts b/src/main/orca-profiles/other-profile-workspace-catalog.test.ts index 43fd24d919b..ae85981096a 100644 --- a/src/main/orca-profiles/other-profile-workspace-catalog.test.ts +++ b/src/main/orca-profiles/other-profile-workspace-catalog.test.ts @@ -287,29 +287,4 @@ describe('readOtherProfileWorkspaceCatalog', () => { ]) expect(readOtherProfileWorkspaceCatalog(root).unreadableProfiles).toBe(1) }) - - it('skips the running profile even when the index already names another as active', () => { - const root = userDataWithProfiles('next', [ - { id: 'running', state: { repos: [{ id: 'repo-running' }] } }, - { id: 'next', state: { repos: [{ id: 'repo-next' }] } } - ]) - const catalog = readOtherProfileWorkspaceCatalog(root, { runningProfileId: 'running' }) - expect(catalog.repoIds).toEqual(new Set(['repo-next'])) - }) - - it('counts a never-written profile as empty only when asked', () => { - const root = userDataWithProfiles('active', [{ id: 'active', state: {} }, { id: 'fresh' }]) - expect(readOtherProfileWorkspaceCatalog(root).unreadableProfiles).toBe(1) - const lenient = readOtherProfileWorkspaceCatalog(root, { neverWrittenIsEmpty: true }) - expect(lenient.unreadableProfiles).toBe(0) - expect(lenient.repoIds.size).toBe(0) - }) - - it('never counts a profile with only backups of its data file as empty', () => { - const root = userDataWithProfiles('active', [{ id: 'active', state: {} }, { id: 'lost' }]) - writeFileSync(join(root, 'profiles', 'lost', 'orca-data.json.bak.0'), '{}') - expect( - readOtherProfileWorkspaceCatalog(root, { neverWrittenIsEmpty: true }).unreadableProfiles - ).toBe(1) - }) }) diff --git a/src/main/orca-profiles/other-profile-workspace-catalog.ts b/src/main/orca-profiles/other-profile-workspace-catalog.ts index 1ba2e91a114..a52f7316e79 100644 --- a/src/main/orca-profiles/other-profile-workspace-catalog.ts +++ b/src/main/orca-profiles/other-profile-workspace-catalog.ts @@ -1,5 +1,4 @@ import { lstatSync, readFileSync } from 'node:fs' -import { isDefinitiveAbsence } from '../../shared/definitive-filesystem-absence' import { folderWorkspaceKey } from '../../shared/workspace-scope' import { getOrcaProfileDataFile, @@ -9,10 +8,6 @@ import { import { getOrcaProfileIndexPath, readProfileIndex } from './profile-index-store' import { readProfileStateDomains } from '../persistence/profile-state/profile-state-domain-reader' import { assertNoRetainedProfileStateExports } from '../persistence/profile-state/profile-state-recovery-required' -import { - PROFILE_STATE_LEGACY_BACKUP_COUNT, - profileStateLegacyBackupPath -} from '../persistence/profile-state/legacy-json/profile-state-legacy-backup-path' /** * Worktree ids owned by Orca profiles OTHER than the running one. @@ -45,20 +40,10 @@ type ProfileWorkspaceIds = { repoIds: Set } -export type OtherProfileCatalogOptions = { - /** The profile this process runs, whose catalog the caller reads live. Defaults to the index's - * active profile, which a profile switch rewrites before the running one exits. */ - runningProfileId?: string - /** A profile with no state database, sidecar or data file at all has never been written and - * holds nothing; by default it counts as unreadable like a missing file anywhere else. */ - neverWrittenIsEmpty?: boolean -} - /** The workspaces and projects every profile other than the running one holds. Any profile whose * state can't be read is counted, never skipped silently: its ids are unknown. */ export function readOtherProfileWorkspaceCatalog( - userDataPath = getProfileUserDataPath(), - options: OtherProfileCatalogOptions = {} + userDataPath = getProfileUserDataPath() ): ProfileWorkspaceIds & { unreadableProfiles: number } { const ids = new Set() const repoIds = new Set() @@ -67,12 +52,11 @@ export function readOtherProfileWorkspaceCatalog( return { ids, repoIds, unreadableProfiles: 0 } } let unreadableProfiles = 0 - const running = options.runningProfileId ?? index.activeProfileId for (const profile of index.profiles) { - if (profile.id === running) { + if (profile.id === index.activeProfileId) { continue } - const collected = readProfileWorktreeIds(profile.id, userDataPath, options) + const collected = readProfileWorktreeIds(profile.id, userDataPath) if (!collected) { unreadableProfiles += 1 continue @@ -85,8 +69,7 @@ export function readOtherProfileWorkspaceCatalog( function readProfileWorktreeIds( profileId: string, - userDataPath: string, - options: OtherProfileCatalogOptions = {} + userDataPath: string ): ProfileWorkspaceIds | null { const databaseFile = getOrcaProfileStateDatabaseFile(profileId, userDataPath) // A present database is authoritative. In particular, do not fall back to a @@ -105,31 +88,9 @@ function readProfileWorktreeIds( } catch { return null } - if (options.neverWrittenIsEmpty && neverWritten(dataFile)) { - return { ids: new Set(), repoIds: new Set() } - } return readProfileWorktreeIdsFromJson(dataFile) } -/** No data file and none of Orca's own backups of it: a lost primary file is not a fresh profile. */ -function neverWritten(dataFile: string): boolean { - for (let index = 0; index < PROFILE_STATE_LEGACY_BACKUP_COUNT; index += 1) { - if (!definitivelyAbsent(profileStateLegacyBackupPath(dataFile, index))) { - return false - } - } - return definitivelyAbsent(dataFile) -} - -function definitivelyAbsent(path: string): boolean { - try { - lstatSync(path) - return false - } catch (error) { - return isDefinitiveAbsence(error) - } -} - function profileStateDatabasePresence(path: string): 'absent' | 'present' | 'unreadable' { let mainDatabasePresent = false try { 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 index 24dcfd08e64..8e45dcdb77f 100644 --- a/src/main/persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding.ts +++ b/src/main/persistence/__fixtures__/wsl-mover/wsl-move-journal-seeding.ts @@ -1,15 +1,23 @@ -/** Writes a move's record through every phase up to its own, as the journal requires. */ +/** + * 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 { writeWslMoveCutover } from '../../moving-wsl-projects/wsl-move-cutover-journal' +import { + listWslMoveCutovers, + writeWslMoveCutover +} from '../../moving-wsl-projects/wsl-move-cutover-journal' export function seedWslMoveCutover(userDataPath: string, cutover: WslMoveCutover): void { - for (const phase of WSL_MOVE_CUTOVER_PHASES) { + 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 }) - if (phase === cutover.phase) { - return - } } } 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 47a18973e47..67f28b36b13 100644 --- a/src/main/persistence/loading-store/repo-lifecycle-operations.ts +++ b/src/main/persistence/loading-store/repo-lifecycle-operations.ts @@ -29,6 +29,8 @@ 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( @@ -46,6 +48,7 @@ type RepoLifecycleOperationsRuntime = Pick< | 'repoOrderOperations' | 'repoUpdateOperations' | 'state' + | 'orcadTerminalRestoreHints' > const repoLifecycleOperationsContext = Symbol('RepoLifecycleOperations') @@ -89,6 +92,7 @@ export class RepoLifecycleOperations { ) if (!idStillPresent) { retireLocalWorktreeScanGeneration(id) + dropRepoRestoreHints(this, id) delete this[repoLifecycleOperationsContext].runtime.state.sparsePresetsByRepo[id] delete this[repoLifecycleOperationsContext].runtime.state.retiredWorktreeNamesByRepo?.[id] } @@ -131,6 +135,7 @@ export class RepoLifecycleOperations { } for (const repoId of orphanRepoIds) { retireLocalWorktreeScanGeneration(repoId) + dropRepoRestoreHints(this, repoId) pruneWorktreeStateForRepo(this, repoId, null) state.workspaceSession = removeLocalRepoSession(state.workspaceSession, repoId) state.workspaceSessionsByHostId = removeRepoFromHostWorkspaceSessions( @@ -184,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 aace1ce4b68..12c5b7bc241 100644 --- a/src/main/persistence/loading-store/session-host-partitions.ts +++ b/src/main/persistence/loading-store/session-host-partitions.ts @@ -93,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 { @@ -170,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 ) } @@ -234,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 88c209485b3..c7320cc346e 100644 --- a/src/main/persistence/loading-store/session-snapshot-operations.ts +++ b/src/main/persistence/loading-store/session-snapshot-operations.ts @@ -128,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 d1446c701c4..f73fa61444c 100644 --- a/src/main/persistence/loading-store/workspace-session-snapshot-publication.ts +++ b/src/main/persistence/loading-store/workspace-session-snapshot-publication.ts @@ -124,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-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-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/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/automation-change-publication.test.ts b/src/main/runtime/automation-change-publication.test.ts index a63d56efb9b..60c602a87af 100644 --- a/src/main/runtime/automation-change-publication.test.ts +++ b/src/main/runtime/automation-change-publication.test.ts @@ -9,7 +9,7 @@ import { closeTestStores, createSqliteTestStore } from '../persistence-test-harn * are the single publication site for every transport (local IPC and remote RPC). */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' @@ -19,8 +19,16 @@ import type { Repo } from '../../shared/repo-types' import type { SshTarget } from '../../shared/ssh-types' import { getDefaultPersistedState } from '../../shared/constants' import type { AutomationsChangedPayload } from '../../shared/runtime-client-events' +import { Store, initDataPath } from '../persistence' +import { OrcaRuntimeService } from './orca-runtime' +import * as worktreeChangeInvalidators from '../ipc/worktree-change-invalidators' +import { _resetTerminalModelQueryAuthorityForTest } from './terminal-model-query-authority' +import { _resetTerminalViewAttributesForTest } from './terminal-view-attribute-store' const testState = { dir: '' } +let registrationSpy: + | MockInstance + | undefined vi.mock('electron', () => ({ app: { getPath: () => testState.dir }, @@ -104,11 +112,8 @@ async function makeRuntime() { }), 'utf-8' ) - vi.resetModules() - const { Store, initDataPath } = await import('../persistence') initDataPath() const store = createSqliteTestStore(Store, { dataFile: join(testState.dir, 'orca-data.json') }) - const { OrcaRuntimeService } = await import('./orca-runtime') const runtime = new OrcaRuntimeService(store as never) const published: AutomationsChangedPayload[] = [] vi.spyOn(runtime, 'notifyAutomationsChanged').mockImplementation( @@ -126,11 +131,28 @@ beforeEach(() => { installFakeAppEnvironment({ getPath: (name) => (name === 'userData' ? testState.dir : tmpdir()) }) + registrationSpy = vi.spyOn(worktreeChangeInvalidators, 'registerWorktreeChangeInvalidator') }) afterEach(async () => { - await closeTestStores() - rmSync(testState.dir, { recursive: true, force: true }) + try { + await closeTestStores() + } finally { + try { + // Runtime callbacks must end with their fixture. + for (const result of registrationSpy?.mock.results ?? []) { + if (result.type === 'return') { + result.value() + } + } + } finally { + registrationSpy?.mockRestore() + registrationSpy = undefined + _resetTerminalModelQueryAuthorityForTest() + _resetTerminalViewAttributesForTest() + rmSync(testState.dir, { recursive: true, force: true }) + } + } }) describe('scoped automationsChanged publication', () => { diff --git a/src/main/runtime/files/file-mutations.ts b/src/main/runtime/files/file-mutations.ts index d7a98d651e8..7f8f1858d58 100644 --- a/src/main/runtime/files/file-mutations.ts +++ b/src/main/runtime/files/file-mutations.ts @@ -6,6 +6,8 @@ import { resolveFileExplorerPaths } from './file-target' import type { RuntimeFileCommandHost } from './file-command-host' +import { runNativeChatScratchSave } from '../../native-chat/native-chat-scratch-folder-lock' +import { getProfileUserDataPath } from '../../orca-profiles/profile-storage-paths' import { resolveRuntimeAuthorizedPath } from './chat-scratch-file-root' import { constants, copyFile, mkdir, rm } from 'node:fs/promises' import { dirname } from 'node:path' @@ -33,7 +35,9 @@ export class RuntimeFileMutations { expectedSshTargetId, expectedSshConnectionGeneration ) - await requireRuntimeFileProvider(target, this.host).writeFile(target.path, content) + await runNativeChatScratchSave(getProfileUserDataPath(), target.path, () => + requireRuntimeFileProvider(target, this.host).writeFile(target.path, content) + ) return { ok: true } } @@ -52,7 +56,9 @@ export class RuntimeFileMutations { expectedSshTargetId, expectedSshConnectionGeneration ) - await requireRuntimeFileProvider(target, this.host).writeFileBase64(target.path, contentBase64) + await runNativeChatScratchSave(getProfileUserDataPath(), target.path, () => + requireRuntimeFileProvider(target, this.host).writeFileBase64(target.path, contentBase64) + ) return { ok: true } } @@ -72,10 +78,12 @@ export class RuntimeFileMutations { expectedSshTargetId, expectedSshConnectionGeneration ) - await requireRuntimeFileProvider(target, this.host).writeFileBase64Chunk( - target.path, - contentBase64, - append + await runNativeChatScratchSave(getProfileUserDataPath(), target.path, () => + requireRuntimeFileProvider(target, this.host).writeFileBase64Chunk( + target.path, + contentBase64, + append + ) ) return { ok: true } } 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-desktop-relay/mobile-desktop-relay-hosts.ts b/src/main/runtime/mobile-desktop-relay/mobile-desktop-relay-hosts.ts index 24cae0928c9..6f2a8b202a9 100644 --- a/src/main/runtime/mobile-desktop-relay/mobile-desktop-relay-hosts.ts +++ b/src/main/runtime/mobile-desktop-relay/mobile-desktop-relay-hosts.ts @@ -22,6 +22,8 @@ export type MobileDesktopRelayHostListing = { orcadDeployment?: ManagedOrcadDeploymentSummary | null pairingRevision: number runtimeId: string | null + /** The desktop's own device id on that server, as pairing recorded it. */ + pairedDeviceId?: string } /** Bounds a call; `expected` refuses it once the server was re-paired or its runtime changed. */ diff --git a/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.test.ts b/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.test.ts index 7b4c498aaab..87ade175b5a 100644 --- a/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.test.ts +++ b/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.test.ts @@ -8,6 +8,10 @@ import { import type { RuntimeRpcResponse } from '../../../shared/runtime-rpc-envelope' import type { RuntimeStatus } from '../../../shared/runtime-types' import type { SshConnectionState } from '../../../shared/ssh-types' +import { getDefaultWorkspaceSession } from '../../../shared/constants' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' +import type { BrowserWorkspace } from '../../../shared/browser-workspace-types' +import type { Tab } from '../../../shared/tab-types' import type { MobileDesktopRelayHosts } from './mobile-desktop-relay-hosts' import { MobileRelayHostCatalog } from './mobile-relay-host-catalog' @@ -56,6 +60,7 @@ function fakeHosts( snapshot: RuntimeHostStatusSnapshot orcadDeployment?: { sshTargetId: string } runtimeId?: string | null + pairedDeviceId?: string }[] ) { const state = { @@ -68,13 +73,16 @@ function fakeHosts( const call = vi.fn() const hosts: MobileDesktopRelayHosts = { list: () => ({ - environments: state.environments.map(({ id, name, orcadDeployment, runtimeId }) => ({ - id, - name, - orcadDeployment, - pairingRevision: state.pairingRevision, - runtimeId: runtimeId === undefined ? 'runtime-a' : runtimeId - })), + environments: state.environments.map( + ({ id, name, orcadDeployment, runtimeId, pairedDeviceId }) => ({ + id, + name, + orcadDeployment, + pairingRevision: state.pairingRevision, + runtimeId: runtimeId === undefined ? 'runtime-a' : runtimeId, + pairedDeviceId + }) + ), sshTargetLabels: state.sshTargetLabels, sshConnectionStates: state.sshConnectionStates, statusByEnvironmentId: new Map( @@ -99,8 +107,25 @@ function fakeHosts( return { hosts, call, state } } -function catalog(hosts: MobileDesktopRelayHosts, labels = new Map()) { - return { catalog: new MobileRelayHostCatalog({ hosts, hostLabelOverrides: () => labels }) } +function catalog( + hosts: MobileDesktopRelayHosts, + labels = new Map(), + desktop: { + hidesWorkspacesFromOtherDevices?: boolean + sessions?: Record> + } = {} +) { + return { + catalog: new MobileRelayHostCatalog({ + hosts, + hostLabelOverrides: () => labels, + hidesWorkspacesFromOtherDevices: () => desktop.hidesWorkspacesFromOtherDevices ?? false, + getWorkspaceSession: (hostId) => { + const session = desktop.sessions?.[hostId] + return session ? { ...getDefaultWorkspaceSession(), ...session } : null + } + }) + } } describe('mobile relay host catalog', () => { @@ -397,3 +422,99 @@ describe('mobile relay host catalog', () => { expect(call).toHaveBeenCalledTimes(1) }) }) + +describe("mobile relay host catalog: the desktop sidebar's own view of server rows", () => { + // What resolveRpcWorkspaceCreatorProvenance records: a relayed phone records the desktop relaying it. + const creators = [ + { worktreeId: 'mine', creatorProvenance: { kind: 'paired-device', deviceId: 'desktop' } }, + { worktreeId: 'other', creatorProvenance: { kind: 'paired-device', deviceId: 'laptop' } }, + { worktreeId: 'server', creatorProvenance: { kind: 'host' } }, + { worktreeId: 'unknown' } + ] + + function serverRows(desktop: Parameters[2], pairedDeviceId = 'desktop') { + const { hosts, call } = fakeHosts([ + { id: 'env', name: 'Box', snapshot: snapshot('env', 'live'), pairedDeviceId } + ]) + call.mockResolvedValue(psReply(creators)) + return catalog(hosts, new Map(), desktop).catalog.worktrees('runtime:env') + } + + function marked(rows: Awaited>, field: string): unknown[] { + return (rows.worktrees ?? []).filter((row) => row[field] === true).map((row) => row.worktreeId) + } + + it('marks rows someone else created while the desktop hides them', async () => { + const rows = await serverRows({ hidesWorkspacesFromOtherDevices: true }) + expect(marked(rows, 'desktopOtherClient')).toEqual(['other', 'server']) + // Marked, never dropped: deep links and lineage still resolve every row. + expect(rows.worktrees).toHaveLength(creators.length) + }) + + it('marks nothing while the desktop shows other-client workspaces', async () => { + expect(marked(await serverRows({}), 'desktopOtherClient')).toEqual([]) + }) + + it('marks nothing on a server whose pairing the desktop cannot name', async () => { + const { hosts, call } = fakeHosts([ + { id: 'env', name: 'Box', snapshot: snapshot('env', 'live') } + ]) + call.mockResolvedValue(psReply(creators)) + const rows = await catalog(hosts, new Map(), { + hidesWorkspacesFromOtherDevices: true + }).catalog.worktrees('runtime:env') + expect(marked(rows, 'desktopOtherClient')).toEqual([]) + }) + + function browserTab(worktreeId: string): BrowserWorkspace { + return { + id: `browser-${worktreeId}`, + worktreeId, + url: 'about:blank', + title: '', + loading: false, + faviconUrl: null, + canGoBack: false, + canGoForward: false, + loadError: null, + createdAt: 0 + } + } + + function tab( + worktreeId: string, + contentType: Tab['contentType'], + agent?: Tab['agentSessionAgent'] + ): Tab { + return { + id: `${contentType}-${worktreeId}`, + entityId: worktreeId, + groupId: 'group', + worktreeId, + contentType, + label: '', + customLabel: null, + color: null, + sortOrder: 0, + createdAt: 0, + ...(agent ? { agentSessionAgent: agent } : {}) + } + } + + it("counts the desktop's saved browser or chat tabs as the row's sidebar activity", async () => { + const rows = await serverRows({ + sessions: { + 'runtime:env': { + browserTabsByWorktree: { mine: [browserTab('mine')], other: [] }, + unifiedTabs: { + 'worktree:server': [tab('server', 'agent-session', 'claude')], + unknown: [tab('unknown', 'terminal')] + } + }, + // Another host's session says nothing about this server's rows. + local: { browserTabsByWorktree: { other: [browserTab('other')] } } + } + }) + expect(marked(rows, 'hasHostSidebarActivity')).toEqual(['mine', 'server']) + }) +}) diff --git a/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.ts b/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.ts index 90fb0a5bab3..d64cdb5185d 100644 --- a/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.ts +++ b/src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.ts @@ -16,6 +16,13 @@ import type { } from '../../../shared/mobile-relay-hosts-contract' import { lastVerifiedRuntimeStatus } from '../../../shared/runtime-host-status' import type { RuntimeStatus } from '../../../shared/runtime-types' +import { + getPairedDeviceIdsByEnvironment, + isWorkspaceOwnedBySomeoneElse +} from '../../../shared/workspace-creator-visibility' +import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types' +import { getWorkspaceSessionTabActivityIds } from '../../../shared/workspace-session-tab-activity' +import { runtimeWorktreeIdentityKey } from '../runtime-worktree-path-identity' import type { MobileDesktopRelayHostListing, MobileDesktopRelayHosts @@ -53,6 +60,10 @@ export class MobileRelayHostCatalog { private readonly options: { hosts: MobileDesktopRelayHosts hostLabelOverrides: () => ReadonlyMap + /** The desktop's own "Hide other-client workspaces" setting. */ + hidesWorkspacesFromOtherDevices: () => boolean + /** The desktop's saved session for a host, as its sidebar last wrote it. */ + getWorkspaceSession: (hostId: ExecutionHostId) => WorkspaceSessionState | null } ) {} @@ -80,15 +91,30 @@ export class MobileRelayHostCatalog { await withinFetchBound(this.refresh(host)) } const cached = this.cached.get(host.environmentId) - const current = this.options.hosts - .list() - .environments.find((environment) => environment.id === host.environmentId) + const { environments, statusByEnvironmentId } = this.options.hosts.list() + const current = environments.find((environment) => environment.id === host.environmentId) // Why the identity read after the fetch: one answered after a re-pair stored the old server's rows. if (!cached || !current || cached.fence !== fenceOf(current)) { return { worktrees: null } } + const desktopDeviceIds = this.options.hidesWorkspacesFromOtherDevices() + ? getPairedDeviceIdsByEnvironment([current], statusByEnvironmentId) + : null + const sessionActivity = new Set( + [ + ...getWorkspaceSessionTabActivityIds( + this.options.getWorkspaceSession(toRuntimeExecutionHostId(host.environmentId)) + ) + ].map(runtimeWorktreeIdentityKey) + ) // Why: the desktop asks with background-removal support; a phone asking itself never sees these rows. - return { worktrees: cached.worktrees.filter((row) => row.removing !== true) } + return { + worktrees: cached.worktrees + .filter((row) => row.removing !== true) + .map((row) => + stampDesktopSidebarView(row, host.environmentId, desktopDeviceIds, sessionActivity) + ) + } } private describe(): DescribedHost[] { @@ -199,6 +225,35 @@ function relayVerdict( return health === 'available' ? 'ready' : 'unavailable' } +/** + * What the desktop's own sidebar decides and the server cannot: its "Hide other-client workspaces" + * (marked, not dropped, so deep links still resolve), and its own browser and chat tabs as activity. + */ +function stampDesktopSidebarView( + row: MobileRelayServerWorktreeRow, + environmentId: string, + desktopDeviceIds: ReadonlyMap | null, + sessionActivity: ReadonlySet +): MobileRelayServerWorktreeRow { + const otherClient = + desktopDeviceIds !== null && + isWorkspaceOwnedBySomeoneElse( + { creatorProvenance: row.creatorProvenance, runtimeOwnerEnvironmentId: environmentId }, + desktopDeviceIds + ) + const active = + typeof row.worktreeId === 'string' && + sessionActivity.has(runtimeWorktreeIdentityKey(row.worktreeId)) + if (!otherClient && !active) { + return row + } + return { + ...row, + ...(otherClient ? { desktopOtherClient: true } : {}), + ...(active ? { hasHostSidebarActivity: true } : {}) + } +} + /** * A server's rows name hosts relative to that server (`local`, its own `ssh:` targets). Like the * desktop sidebar, the phone shows every one of them under the server, so all become its host id. 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/native-chat-visuals-workspace-verdict.test.ts b/src/main/runtime/native-chat-visuals-workspace-verdict.test.ts deleted file mode 100644 index 07ad7372d75..00000000000 --- a/src/main/runtime/native-chat-visuals-workspace-verdict.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it, vi } from 'vitest' -import type { AgentSessionExecutionLocation } from '../../shared/agent-session-record' -import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants' -import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' -import { - createNativeChatVisualsWorkspaceVerdicts, - readNativeChatVisualsWorkspaceCatalogs, - type NativeChatVisualsFilesystem, - type NativeChatVisualsWorkspaceCatalogs -} from './native-chat-visuals-workspace-verdict' - -const REPO_PATH = '/work/repo-1' -const WORKTREE_PATH = '/work/repo-1-feature' -const WORKTREE_ID = `repo-1::${WORKTREE_PATH}` - -function catalogs( - options: { - repos?: string[] - meta?: string[] - folders?: string[] - others?: Partial - } = {} -): NativeChatVisualsWorkspaceCatalogs { - return { - active: { - getRepo: (id: string) => - (options.repos ?? ['repo-1']).includes(id) ? { path: REPO_PATH } : undefined, - getAllWorktreeMeta: () => Object.fromEntries((options.meta ?? []).map((id) => [id, {}])), - getFolderWorkspaces: () => (options.folders ?? []).map((id) => ({ id })) - }, - others: { ids: new Set(), repoIds: new Set(), unreadableProfiles: 0, ...options.others } - } -} - -const at = ( - workspaceId: string, - overrides: Partial = {} -): AgentSessionExecutionLocation => ({ - executionHostId: LOCAL_EXECUTION_HOST_ID, - wslDistro: null, - workspaceId, - workspaceKind: 'git-worktree', - ...overrides -}) - -type Presence = 'present' | 'absent' | 'unknown' - -function verdictWith( - known: NativeChatVisualsWorkspaceCatalogs | null, - paths: Record = {}, - gitRecords: boolean | null = false -) { - const fs: NativeChatVisualsFilesystem = { - presence: vi.fn(async (path: string): Promise => paths[path] ?? 'present'), - gitRecordsWorktree: vi.fn(async () => gitRecords) - } - return { verdict: createNativeChatVisualsWorkspaceVerdicts(() => known, fs)(), fs } -} - -describe('whether a chat workspace is provably removed', () => { - it('is removed when its project was removed from Orca in every profile', async () => { - const { verdict, fs } = verdictWith(catalogs({ repos: [] })) - await expect(verdict(at(WORKTREE_ID))).resolves.toBe('removed') - expect(fs.presence).not.toHaveBeenCalled() - }) - - it("keeps a chat whose project or folder only another profile holds (chats are shared, catalogs aren't)", async () => { - const { verdict } = verdictWith( - catalogs({ - repos: [], - others: { repoIds: new Set(['repo-1']), ids: new Set(['folder:f-2']) } - }) - ) - await expect(verdict(at(WORKTREE_ID))).resolves.toBe('unverifiable') - await expect(verdict(at('folder:f-2', { workspaceKind: 'folder' }))).resolves.toBe('present') - }) - - it('decides nothing while any profile cannot be read', async () => { - const { verdict } = verdictWith(catalogs({ repos: [], others: { unreadableProfiles: 1 } })) - await expect(verdict(at(WORKTREE_ID))).resolves.toBe('unverifiable') - await expect(verdict(at('folder:gone', { workspaceKind: 'folder' }))).resolves.toBe( - 'unverifiable' - ) - }) - - it('is present while any profile still tracks the worktree', async () => { - for (const known of [ - catalogs({ meta: [WORKTREE_ID] }), - catalogs({ others: { ids: new Set([WORKTREE_ID]) } }) - ]) { - const { verdict, fs } = verdictWith(known, { [WORKTREE_PATH]: 'absent' }) - await expect(verdict(at(WORKTREE_ID))).resolves.toBe('present') - expect(fs.presence).not.toHaveBeenCalled() - } - }) - - it('is removed when the worktree folder is gone and git no longer records it', async () => { - const { verdict, fs } = verdictWith(catalogs(), { [WORKTREE_PATH]: 'absent' }, false) - await expect(verdict(at(WORKTREE_ID))).resolves.toBe('removed') - expect(fs.gitRecordsWorktree).toHaveBeenCalledWith(REPO_PATH, WORKTREE_PATH) - }) - - it('keeps a worktree on an unmounted drive: git still records it, or the project is unreachable', async () => { - await expect( - verdictWith(catalogs(), { [WORKTREE_PATH]: 'absent' }, true).verdict(at(WORKTREE_ID)) - ).resolves.toBe('unverifiable') - await expect( - verdictWith(catalogs(), { [WORKTREE_PATH]: 'absent' }, null).verdict(at(WORKTREE_ID)) - ).resolves.toBe('unverifiable') - await expect( - verdictWith(catalogs(), { [WORKTREE_PATH]: 'absent', [REPO_PATH]: 'absent' }).verdict( - at(WORKTREE_ID) - ) - ).resolves.toBe('unverifiable') - }) - - it("never removes the project's own checkout while the project is known", async () => { - const { verdict } = verdictWith(catalogs(), { [REPO_PATH]: 'absent' }) - await expect(verdict(at(`repo-1::${REPO_PATH}`))).resolves.toBe('unverifiable') - }) - - it('keeps a worktree whose folder is there or unreadable', async () => { - await expect(verdictWith(catalogs()).verdict(at(WORKTREE_ID))).resolves.toBe('present') - await expect( - verdictWith(catalogs(), { [WORKTREE_PATH]: 'unknown' }).verdict(at(WORKTREE_ID)) - ).resolves.toBe('unverifiable') - }) - - it('reads a folder workspace from every catalog', async () => { - const folder = (id: string) => at(`folder:${id}`, { workspaceKind: 'folder' }) - const { verdict } = verdictWith(catalogs({ folders: ['f-1'] })) - await expect(verdict(folder('f-1'))).resolves.toBe('present') - await expect(verdict(folder('f-2'))).resolves.toBe('removed') - }) - - it('never decides for another host, a WSL distro, the floating workspace, or without catalogs', async () => { - const { verdict, fs } = verdictWith(catalogs({ repos: [] })) - await expect(verdict(at(WORKTREE_ID, { executionHostId: 'ssh:box' }))).resolves.toBe( - 'unverifiable' - ) - await expect(verdict(at(WORKTREE_ID, { wslDistro: 'Ubuntu' }))).resolves.toBe('unverifiable') - await expect( - verdict(at(FLOATING_TERMINAL_WORKTREE_ID, { workspaceKind: 'folder' })) - ).resolves.toBe('unverifiable') - await expect(verdictWith(null).verdict(at(WORKTREE_ID))).resolves.toBe('unverifiable') - expect(fs.presence).not.toHaveBeenCalled() - }) - - it('reads the catalogs once per run, however many chats it judges', async () => { - const read = vi.fn(() => catalogs({ repos: [] })) - const verdict = createNativeChatVisualsWorkspaceVerdicts(read)() - await verdict(at(WORKTREE_ID)) - await verdict(at('repo-1::/work/other')) - expect(read).toHaveBeenCalledOnce() - }) -}) - -describe("git's own record of a linked worktree", () => { - const scratch: string[] = [] - afterEach(() => { - for (const dir of scratch.splice(0)) { - rmSync(dir, { recursive: true, force: true }) - } - }) - - function repoWithWorktreeRecord(recordedWorktree: string | null): string { - const repo = realpathSync(mkdtempSync(join(tmpdir(), 'orca-visuals-git-'))) - scratch.push(repo) - mkdirSync(join(repo, '.git', 'worktrees', 'feature'), { recursive: true }) - if (recordedWorktree) { - writeFileSync( - join(repo, '.git', 'worktrees', 'feature', 'gitdir'), - `${recordedWorktree}/.git\n` - ) - } - return repo - } - - it('removes only a worktree git no longer records', async () => { - const gone = join(tmpdir(), 'orca-visuals-missing-worktree-a') - const verdicts = (repo: string) => - createNativeChatVisualsWorkspaceVerdicts(() => ({ - ...catalogs(), - active: { - ...catalogs().active, - getRepo: () => ({ path: repo }) - } - }))() - const recorded = repoWithWorktreeRecord(gone) - await expect(verdicts(recorded)(at(`repo-1::${gone}`))).resolves.toBe('unverifiable') - const pruned = repoWithWorktreeRecord(null) - await expect(verdicts(pruned)(at(`repo-1::${gone}`))).resolves.toBe('removed') - }) - - it('reads a record git wrote relative to its own folder', async () => { - const repo = repoWithWorktreeRecord(null) - const worktree = join(repo, '..', 'orca-visuals-relative-worktree-b') - writeFileSync( - join(repo, '.git', 'worktrees', 'feature', 'gitdir'), - '../../../../orca-visuals-relative-worktree-b/.git\n' - ) - const verdict = createNativeChatVisualsWorkspaceVerdicts(() => ({ - ...catalogs(), - active: { ...catalogs().active, getRepo: () => ({ path: repo }) } - }))() - await expect(verdict(at(`repo-1::${worktree}`))).resolves.toBe('unverifiable') - }) - - it('resolves a relative record against the real path when the project was added through a link', async () => { - const real = realpathSync(mkdtempSync(join(tmpdir(), 'orca-visuals-real-'))) - scratch.push(real) - const nested = join(real, 'a', 'b', 'c') - mkdirSync(join(nested, 'repo', '.git', 'worktrees', 'wt'), { recursive: true }) - const linkedParent = join(real, 'deep') - symlinkSync(nested, linkedParent) - const worktree = join(real, 'wts', 'wt') - // What git writes: relative from the record folder's real path. - writeFileSync( - join(nested, 'repo', '.git', 'worktrees', 'wt', 'gitdir'), - '../../../../../../../wts/wt/.git\n' - ) - const verdict = createNativeChatVisualsWorkspaceVerdicts(() => ({ - ...catalogs(), - active: { ...catalogs().active, getRepo: () => ({ path: join(linkedParent, 'repo') }) } - }))() - await expect(verdict(at(`repo-1::${worktree}`))).resolves.toBe('unverifiable') - }) -}) - -describe('reading the catalogs for a sweep run', () => { - it('skips the running profile named by its own storage folder', () => { - const root = mkdtempSync(join(tmpdir(), 'orca-visuals-profiles-')) - try { - mkdirSync(join(root, 'profiles', 'running'), { recursive: true }) - mkdirSync(join(root, 'profiles', 'next'), { recursive: true }) - const profile = (id: string) => ({ - id, - name: id, - kind: 'local', - createdAt: 0, - updatedAt: 0, - lastOpenedAt: 0, - avatar: { kind: 'initials', initials: id.slice(0, 2), color: 'neutral' } - }) - // A switch already named the next profile active; this process still runs the old one. - writeFileSync( - join(root, 'orca-profile-index.json'), - JSON.stringify({ activeProfileId: 'next', profiles: [profile('running'), profile('next')] }) - ) - for (const id of ['running', 'next']) { - writeFileSync( - join(root, 'profiles', id, 'orca-data.json'), - JSON.stringify({ repos: [{ id: `repo-${id}` }] }) - ) - } - const read = readNativeChatVisualsWorkspaceCatalogs( - { - ...catalogs().active, - getProfileStorageDirectory: () => join(root, 'profiles', 'running') - }, - root - ) - expect([...read.others.repoIds]).toEqual(['repo-next']) - } finally { - rmSync(root, { recursive: true, force: true }) - } - }) -}) diff --git a/src/main/runtime/native-chat-visuals-workspace-verdict.ts b/src/main/runtime/native-chat-visuals-workspace-verdict.ts deleted file mode 100644 index 3dce96bb0f7..00000000000 --- a/src/main/runtime/native-chat-visuals-workspace-verdict.ts +++ /dev/null @@ -1,204 +0,0 @@ -// Whether the local workspace a chat ran in is provably gone, for the scratch folder sweep. -// Chat records and visuals live at the user-data root, shared by every Orca profile, while each -// profile keeps its own workspace catalog: so only absence from EVERY profile's catalog counts. A -// worktree still in a known project is gone only when git itself no longer records it. Never runs -// git or lists a repo's worktrees: that walk can touch protected folders. - -import { lstat, readdir, readFile, realpath, stat } from 'node:fs/promises' -import { basename, dirname, isAbsolute, join, resolve } from 'node:path' -import type { AgentSessionExecutionLocation } from '../../shared/agent-session-record' -import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' -import { isDefinitiveAbsence } from '../../shared/definitive-filesystem-absence' -import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' -import { isFloatingWorkspaceId } from '../../shared/floating-workspace-worktree' -import { splitWorktreeIdForFilesystem } from '../../shared/worktree/id' -import { folderWorkspaceKey, parseWorkspaceKey } from '../../shared/workspace-scope' -import type { Store } from '../persistence' -import { readOtherProfileWorkspaceCatalog } from '../orca-profiles/other-profile-workspace-catalog' -import { - getOrcaProfilesDirectory, - getProfileUserDataPath -} from '../orca-profiles/profile-storage-paths' -import type { NativeChatVisualsWorkspaceVerdict } from '../native-chat/native-chat-visuals-sweep' - -/** This profile's live catalog and every other profile's persisted one. The running profile is - * named by its own storage folder, not the profile index, which a switch rewrites first. */ -export function readNativeChatVisualsWorkspaceCatalogs( - store: NativeChatVisualsWorkspaceCatalogs['active'] & Pick, - userDataPath = getProfileUserDataPath() -): NativeChatVisualsWorkspaceCatalogs { - const storage = store.getProfileStorageDirectory() - const runningProfileId = samePath(dirname(storage), getOrcaProfilesDirectory(userDataPath)) - ? basename(storage) - : undefined - return { - active: store, - others: readOtherProfileWorkspaceCatalog(userDataPath, { - ...(runningProfileId ? { runningProfileId } : {}), - // Chat records are never deleted, so this rule is what cleans up; a profile created but never - // opened must not switch it off. - neverWrittenIsEmpty: true - }) - } -} - -export type NativeChatVisualsWorkspaceCatalogs = { - /** The running profile's live catalog: the Store, narrowed to what a verdict reads. */ - active: { - getRepo: (id: string) => { path: string } | undefined - getAllWorktreeMeta: () => Readonly> - getFolderWorkspaces: () => readonly { id: string }[] - } - /** Every other profile's worktree ids, folder workspace keys and project ids. */ - others: { ids: ReadonlySet; repoIds: ReadonlySet; unreadableProfiles: number } -} - -export type NativeChatVisualsFilesystem = { - /** `present`, `absent` (definitively), or `unknown` for any other answer. */ - presence: (path: string) => Promise<'present' | 'absent' | 'unknown'> - /** Whether git in the repo at `repoPath` records a linked worktree at `worktreePath`; null when - * its records can't be read. */ - gitRecordsWorktree: (repoPath: string, worktreePath: string) => Promise -} - -async function presence(path: string): Promise<'present' | 'absent' | 'unknown'> { - try { - await stat(path) - return 'present' - } catch (error) { - return isDefinitiveAbsence(error) ? 'absent' : 'unknown' - } -} - -const samePath = (left: string, right: string): boolean => - normalizeRuntimePathForComparison(left) === normalizeRuntimePathForComparison(right) - -async function gitRecordsWorktree(repoPath: string, worktreePath: string): Promise { - const gitDir = join(repoPath, '.git') - try { - // A `.git` file (the repo is itself a linked worktree or a submodule) is not read here. - if (!(await lstat(gitDir)).isDirectory()) { - return null - } - let names: string[] - try { - names = await readdir(join(gitDir, 'worktrees')) - } catch (error) { - if (isDefinitiveAbsence(error)) { - return false - } - throw error - } - for (const name of names) { - let recorded: string - try { - recorded = await readFile(join(gitDir, 'worktrees', name, 'gitdir'), 'utf8') - } catch (error) { - if (isDefinitiveAbsence(error)) { - continue - } - throw error - } - // The record names the worktree's own `.git` file; git 2.48+ may write it relative to the - // record folder's real path, which differs from the stored one when that runs through a link. - const gitFile = recorded.trim() - const absolute = isAbsolute(gitFile) - ? gitFile - : resolve(await realpath(join(gitDir, 'worktrees', name)), gitFile) - if (samePath(dirname(absolute), worktreePath)) { - return true - } - } - return false - } catch { - return null - } -} - -const NODE_FILESYSTEM: NativeChatVisualsFilesystem = { presence, gitRecordsWorktree } - -async function worktreeVerdict( - catalogs: NativeChatVisualsWorkspaceCatalogs, - workspaceId: string, - fs: NativeChatVisualsFilesystem -): Promise { - const worktree = splitWorktreeIdForFilesystem(workspaceId) - if (!worktree?.repoId || !worktree.worktreePath) { - return 'unverifiable' - } - const repo = catalogs.active.getRepo(worktree.repoId) - // The project was removed from Orca in every profile. - if (!repo && !catalogs.others.repoIds.has(worktree.repoId)) { - return 'removed' - } - if ( - Object.hasOwn(catalogs.active.getAllWorktreeMeta(), workspaceId) || - catalogs.others.ids.has(workspaceId) - ) { - return 'present' - } - // The project's own checkout, or one only another profile can say anything about. - if (!repo || samePath(repo.path, worktree.worktreePath)) { - return 'unverifiable' - } - const own = await fs.presence(worktree.worktreePath) - if (own !== 'absent') { - return own === 'present' ? 'present' : 'unverifiable' - } - // A worktree on an unmounted drive is still recorded by git, so absence alone proves nothing. - if ((await fs.presence(repo.path)) !== 'present') { - return 'unverifiable' - } - return (await fs.gitRecordsWorktree(repo.path, worktree.worktreePath)) === false - ? 'removed' - : 'unverifiable' -} - -/** - * A verdict function over one snapshot of every profile's catalog; call once per sweep run. - * Null catalogs, or any profile whose state can't be read, decide nothing. - */ -export function createNativeChatVisualsWorkspaceVerdicts( - readCatalogs: () => NativeChatVisualsWorkspaceCatalogs | null, - fs: NativeChatVisualsFilesystem = NODE_FILESYSTEM -): () => (location: AgentSessionExecutionLocation) => Promise { - return () => { - let snapshot: NativeChatVisualsWorkspaceCatalogs | null | undefined - const catalogs = (): NativeChatVisualsWorkspaceCatalogs | null => { - if (snapshot === undefined) { - try { - snapshot = readCatalogs() - } catch { - snapshot = null - } - } - return snapshot && snapshot.others.unreadableProfiles === 0 ? snapshot : null - } - return async (location) => { - // Another host or a WSL distro owns its own answer; loss of contact is never removal. - if (location.executionHostId !== LOCAL_EXECUTION_HOST_ID || location.wslDistro !== null) { - return 'unverifiable' - } - // The floating workspace names a setting, not a place the user removes. - if (isFloatingWorkspaceId(location.workspaceId)) { - return 'unverifiable' - } - const known = catalogs() - if (!known) { - return 'unverifiable' - } - if (location.workspaceKind !== 'folder') { - return worktreeVerdict(known, location.workspaceId, fs) - } - if (parseWorkspaceKey(location.workspaceId)?.type !== 'folder') { - return 'unverifiable' - } - const listed = - known.others.ids.has(location.workspaceId) || - known.active - .getFolderWorkspaces() - .some((workspace) => folderWorkspaceKey(workspace.id) === location.workspaceId) - return listed ? 'present' : 'removed' - } - } -} 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 32e8293a7d6..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' 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 b40e7f61367..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 }) } }) 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-worktree-ps.ts b/src/main/runtime/orca-runtime-get-worktree-ps.ts index 8f5c14539c0..7ae72c48b0b 100644 --- a/src/main/runtime/orca-runtime-get-worktree-ps.ts +++ b/src/main/runtime/orca-runtime-get-worktree-ps.ts @@ -57,10 +57,6 @@ import { } from './runtime-visible-catalog' import { claudeCliFlagSupport } from '../claude/claude-cli-flag-support' import { isStructuredAttentionWorkspaceHidden } from './structured-attention-hidden-workspace' -import { - createNativeChatVisualsWorkspaceVerdicts, - readNativeChatVisualsWorkspaceCatalogs -} from './native-chat-visuals-workspace-verdict' export class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithStartTuiIdleVisibleReadProbe { async getWorktreePs( @@ -242,11 +238,7 @@ export class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithStartTuiIdleVis // Wired only here, so a test runtime never runs a real `claude --version`. claudeCliFlags: claudeCliFlagSupport, nativeChatVisuals: { - isEnabled: () => this.requireStore().getSettings().nativeChatInlineVisuals !== false, - // Chats and their visuals are shared by every profile; each profile keeps its own catalog. - workspaceVerdicts: createNativeChatVisualsWorkspaceVerdicts(() => - this.store ? readNativeChatVisualsWorkspaceCatalogs(this.store) : null - ) + isEnabled: () => this.requireStore().getSettings().nativeChatInlineVisuals !== false }, resolveShellEnvironmentPolicy: () => nativeChatShellEnvironmentPolicy(this.requireStore().getSettings()), 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-pty-foreground-process-reads.ts b/src/main/runtime/orca-runtime-pty-foreground-process-reads.ts index 12eb7cddb13..a8768cd2487 100644 --- a/src/main/runtime/orca-runtime-pty-foreground-process-reads.ts +++ b/src/main/runtime/orca-runtime-pty-foreground-process-reads.ts @@ -182,6 +182,16 @@ export class OrcaRuntimeWithPtyForegroundProcessReads extends OrcaRuntimeWithSta return this.store.getUI() } + /** This client's own sidebar setting; paired clients never receive it. */ + hidesWorkspacesFromOtherDevices(): boolean { + return this.store?.getUI?.().hideWorkspacesFromOtherDevices === true + } + + // Why null rather than runtime_unavailable: a missing session only means no activity to add. + getWorkspaceSessionForHost(hostId: ExecutionHostId): WorkspaceSessionState | null { + return this.store?.getWorkspaceSession?.(hostId) ?? null + } + updateUIState(updates: Partial): PersistedUIState { if (!this.store?.getUI || !this.store.updateUI) { throw new Error('runtime_unavailable') 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.test.ts b/src/main/runtime/orca-runtime.test.ts index c32d20e71d1..bc903f2c9e3 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -1,6 +1,7 @@ // Compatibility entrypoint: reliability gates invoke this path directly; fragments stay side-effect ordered. export {} +await import('./rpc/unused-default-rpc-methods.test-fixture') await import('./orca-runtime-test-mocks.spec') await import('./orca-runtime-test-lifecycle.spec') await import('./orca-runtime-test-fixtures.spec') 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/core.ts b/src/main/runtime/rpc/core.ts index ba11c2cf4fc..4f12fe7a82b 100644 --- a/src/main/runtime/rpc/core.ts +++ b/src/main/runtime/rpc/core.ts @@ -99,6 +99,8 @@ export type RpcContext = { clientId?: string // Why: navigation is keyed by revocable device identity, never by the bearer credential or transient socket id. pairedDeviceId?: string + // Why: a delegated phone's desktop device; its workspaces are that desktop's, not this host's. + pairedDeviceParentId?: string // Why: host-assigned from the transport (rpc-caller-identity.ts); absent when the transport could not name its caller. caller?: RpcCallerIdentity // Why: lets handlers gate mobile payload truncation to phones only; undefined for in-process callers → treat as full-class (no clip). diff --git a/src/main/runtime/rpc/dispatcher-stream-options.ts b/src/main/runtime/rpc/dispatcher-stream-options.ts index 7de6511411e..df94afa7af9 100644 --- a/src/main/runtime/rpc/dispatcher-stream-options.ts +++ b/src/main/runtime/rpc/dispatcher-stream-options.ts @@ -11,6 +11,7 @@ export type RpcDispatchStreamingOptions = Pick< | 'signal' | 'clientId' | 'pairedDeviceId' + | 'pairedDeviceParentId' | 'caller' | 'clientKind' | 'clientCapabilities' @@ -45,6 +46,7 @@ export function rpcContextFromTransport( : undefined, clientId: options?.clientId, pairedDeviceId: options?.pairedDeviceId, + pairedDeviceParentId: options?.pairedDeviceParentId, caller: resolveRpcCallerIdentity(options), clientKind: options?.clientKind, clientCapabilities: options?.clientCapabilities, 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/methods/worktree-catalog-methods.ts b/src/main/runtime/rpc/methods/worktree-catalog-methods.ts index 58526529ce5..36e5ef60e78 100644 --- a/src/main/runtime/rpc/methods/worktree-catalog-methods.ts +++ b/src/main/runtime/rpc/methods/worktree-catalog-methods.ts @@ -1,6 +1,7 @@ import { defineMethod } from '../core' import { resolveWorktreeCatalogSnapshot } from '../worktree-catalog-snapshot' import { coarsenWorktreePsActivity } from '../worktree-ps-coarse-activity' +import { markWorktreePsOtherClientRows } from '../worktree-ps-other-client-marks' import { supportsWorktreeVisibilitySourceDefaults } from '../worktree-visibility-client-capability' import { projectWorktreeListRemovals, @@ -20,16 +21,19 @@ export const WORKTREE_CATALOG_METHODS = [ params: WorktreePsParams, handler: async (params, context) => { const pendingAtScan = snapshotPendingWorktreeRemovals() - const result = projectWorktreePsRemovals( - await context.runtime.getWorktreePs( - params.limit, - supportsWorktreeVisibilitySourceDefaults( - context, - params.supportsWorktreeVisibilitySourceDefaults - ) + const result = markWorktreePsOtherClientRows( + projectWorktreePsRemovals( + await context.runtime.getWorktreePs( + params.limit, + supportsWorktreeVisibilitySourceDefaults( + context, + params.supportsWorktreeVisibilitySourceDefaults + ) + ), + context, + pendingAtScan ), - context, - pendingAtScan + context ) // Why: callers that never send the field get the byte-exact legacy response. if (params.afterSnapshotId === undefined) { 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/rpc/workspace-creator-context.test.ts b/src/main/runtime/rpc/workspace-creator-context.test.ts new file mode 100644 index 00000000000..35cc80df1a3 --- /dev/null +++ b/src/main/runtime/rpc/workspace-creator-context.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' +import { resolveRpcWorkspaceCreatorProvenance } from './workspace-creator-context' + +describe('resolveRpcWorkspaceCreatorProvenance', () => { + it("records a phone paired here as this host's owner", () => { + expect( + resolveRpcWorkspaceCreatorProvenance({ pairedDeviceId: 'phone', clientKind: 'mobile' }) + ).toEqual({ kind: 'host' }) + }) + + it('records a delegated phone as the desktop that relays it', () => { + expect( + resolveRpcWorkspaceCreatorProvenance({ + pairedDeviceId: 'child', + pairedDeviceParentId: 'desktop', + clientKind: 'mobile' + }) + ).toEqual({ kind: 'paired-device', deviceId: 'desktop' }) + }) + + it('records a paired desktop as itself, and an in-process caller as the host', () => { + expect( + resolveRpcWorkspaceCreatorProvenance({ pairedDeviceId: 'laptop', clientKind: 'runtime' }) + ).toEqual({ kind: 'paired-device', deviceId: 'laptop' }) + expect(resolveRpcWorkspaceCreatorProvenance({})).toEqual({ kind: 'host' }) + }) +}) diff --git a/src/main/runtime/rpc/workspace-creator-context.ts b/src/main/runtime/rpc/workspace-creator-context.ts index 6bf1adf9949..fcbdad4d1eb 100644 --- a/src/main/runtime/rpc/workspace-creator-context.ts +++ b/src/main/runtime/rpc/workspace-creator-context.ts @@ -2,10 +2,19 @@ import type { WorkspaceCreatorProvenance } from '../../../shared/worktree/types' import type { RpcContext } from './core' export function resolveRpcWorkspaceCreatorProvenance( - context: Pick + context: Pick< + RpcContext, + 'pairedDeviceId' | 'pairedDeviceParentId' | 'clientId' | 'clientKind' | 'connectionId' + > ): WorkspaceCreatorProvenance { if (context.pairedDeviceId) { - return { kind: 'paired-device', deviceId: context.pairedDeviceId } + if (context.clientKind !== 'mobile') { + return { kind: 'paired-device', deviceId: context.pairedDeviceId } + } + // Why the owner, not the phone: a phone acts for whoever paired it, so its rows are theirs. + return context.pairedDeviceParentId + ? { kind: 'paired-device', deviceId: context.pairedDeviceParentId } + : { kind: 'host' } } if (context.clientId || context.clientKind || context.connectionId) { throw new Error('authenticated_device_identity_missing') diff --git a/src/main/runtime/rpc/worktree-ps-other-client-marks.test.ts b/src/main/runtime/rpc/worktree-ps-other-client-marks.test.ts new file mode 100644 index 00000000000..0750d99f4c2 --- /dev/null +++ b/src/main/runtime/rpc/worktree-ps-other-client-marks.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'vitest' +import type { + RuntimeWorktreePsResult, + RuntimeWorktreePsSummary +} from '../../../shared/runtime-worktree-contracts' +import type { RpcContext } from './core' +import { markWorktreePsOtherClientRows } from './worktree-ps-other-client-marks' + +function row( + worktreeId: string, + creatorProvenance?: RuntimeWorktreePsSummary['creatorProvenance'] +): RuntimeWorktreePsSummary { + return { + worktreeId, + repoId: 'repo', + repo: 'repo', + path: `/repo/${worktreeId}`, + branch: worktreeId, + isArchived: false, + isMainWorktree: false, + hasHostSidebarActivity: false, + parentWorktreeId: null, + childWorktreeIds: [], + displayName: worktreeId, + workspaceStatus: 'in-progress', + sortOrder: 0, + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + linkedGitLabMR: null, + linkedGitLabIssue: null, + comment: '', + isPinned: false, + isActive: false, + unread: false, + liveTerminalCount: 0, + hasAttachedPty: false, + lastOutputAt: null, + preview: '', + status: 'inactive', + agents: [], + ...(creatorProvenance ? { creatorProvenance } : {}) + } +} + +const rows = [ + row('desktop', { kind: 'host' }), + row('laptop', { kind: 'paired-device', deviceId: 'laptop' }), + row('unknown') +] +const result: RuntimeWorktreePsResult = { worktrees: rows, totalCount: 4, truncated: false } + +function context( + overrides: Partial>, + hides = true +): Parameters[1] { + return { runtime: { hidesWorkspacesFromOtherDevices: () => hides }, ...overrides } +} + +function marked(next: RuntimeWorktreePsResult): string[] { + return next.worktrees.filter((entry) => entry.desktopOtherClient).map((entry) => entry.worktreeId) +} + +describe('markWorktreePsOtherClientRows', () => { + it("marks, for this host's own phone, the rows its sidebar hides", () => { + expect( + marked(markWorktreePsOtherClientRows(result, context({ clientKind: 'mobile' }))) + ).toEqual(['laptop']) + }) + + it('leaves rows untouched while the setting is off, or for any other caller', () => { + expect(markWorktreePsOtherClientRows(result, context({ clientKind: 'mobile' }, false))).toBe( + result + ) + expect(markWorktreePsOtherClientRows(result, context({ clientKind: 'runtime' }))).toBe(result) + expect(markWorktreePsOtherClientRows(result, context({}))).toBe(result) + expect( + markWorktreePsOtherClientRows( + result, + context({ clientKind: 'mobile', pairedDeviceParentId: 'other-desktop' }) + ) + ).toBe(result) + }) +}) diff --git a/src/main/runtime/rpc/worktree-ps-other-client-marks.ts b/src/main/runtime/rpc/worktree-ps-other-client-marks.ts new file mode 100644 index 00000000000..adaf5988860 --- /dev/null +++ b/src/main/runtime/rpc/worktree-ps-other-client-marks.ts @@ -0,0 +1,35 @@ +import type { RuntimeWorktreePsResult } from '../../../shared/runtime-worktree-contracts' +import { + EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT, + isWorkspaceOwnedBySomeoneElse +} from '../../../shared/workspace-creator-visibility' +import type { RpcContext } from './core' + +/** + * Marks the rows this host's own sidebar hides under "Hide other-client workspaces", for a phone + * paired to it: the setting is the desktop's own and never crosses the pairing. A delegated phone + * belongs to another desktop, whose view this host cannot speak for. + */ +export function markWorktreePsOtherClientRows( + result: T, + context: Pick & { + runtime: Pick + } +): T { + if ( + context.clientKind !== 'mobile' || + context.pairedDeviceParentId !== undefined || + !context.runtime.hidesWorkspacesFromOtherDevices() + ) { + return result + } + let marked = false + const worktrees = result.worktrees.map((row) => { + if (!isWorkspaceOwnedBySomeoneElse(row, EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT)) { + return row + } + marked = true + return { ...row, desktopOtherClient: true as const } + }) + return marked ? { ...result, worktrees } : result +} 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-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-rpc/runtime-rpc-pairing.ts b/src/main/runtime/runtime-rpc/runtime-rpc-pairing.ts index 2069c67ff74..ba241e5dc43 100644 --- a/src/main/runtime/runtime-rpc/runtime-rpc-pairing.ts +++ b/src/main/runtime/runtime-rpc/runtime-rpc-pairing.ts @@ -105,7 +105,9 @@ export class RuntimeRpcPairing extends RuntimeRpcNetworkExposure { this.mobileRelayHostCatalog = hosts ? new MobileRelayHostCatalog({ hosts, - hostLabelOverrides: () => getHostDisplayLabelOverrides(this.runtime.getClientSettings()) + hostLabelOverrides: () => getHostDisplayLabelOverrides(this.runtime.getClientSettings()), + hidesWorkspacesFromOtherDevices: () => this.runtime.hidesWorkspacesFromOtherDevices(), + getWorkspaceSession: (hostId) => this.runtime.getWorkspaceSessionForHost(hostId) }) : null } diff --git a/src/main/runtime/runtime-rpc/runtime-rpc-websocket-dispatch.ts b/src/main/runtime/runtime-rpc/runtime-rpc-websocket-dispatch.ts index 22c6290675a..4e6cf61bc5c 100644 --- a/src/main/runtime/runtime-rpc/runtime-rpc-websocket-dispatch.ts +++ b/src/main/runtime/runtime-rpc/runtime-rpc-websocket-dispatch.ts @@ -237,6 +237,7 @@ export class RuntimeRpcWebSocketDispatch extends RuntimeRpcMobileDevices { connectionId, clientId: principal.clientId, pairedDeviceId: device.deviceId, + pairedDeviceParentId: device.parentDeviceId, // Why: gates the mobile-only payload diet so full-screen web/desktop clients aren't truncated. clientKind: device.scope, callerScope, 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/runtime-worktree-ps-activity.ts b/src/main/runtime/runtime-worktree-ps-activity.ts index 1b347c9080c..9232f684b63 100644 --- a/src/main/runtime/runtime-worktree-ps-activity.ts +++ b/src/main/runtime/runtime-worktree-ps-activity.ts @@ -3,6 +3,7 @@ import type { Repo } from '../../shared/repo-types' import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types' import { getRepoExecutionHostId, type ExecutionHostId } from '../../shared/execution-host' import { parsePaneKey } from '../../shared/stable-pane-id' +import { getWorkspaceSessionTabActivityIds } from '../../shared/workspace-session-tab-activity' import type { RuntimeStore } from './runtime-store-contract' import type { RuntimeLeafRecord, RuntimePtyWorktreeRecord } from './runtime-terminal-state-records' import type { RuntimeWorktreePsSummaryLookup } from './runtime-worktree-summary-paths' @@ -212,10 +213,7 @@ export function applyRuntimeWorktreePsSessionActivity(args: { summary.hasHostSidebarActivity = true } } - for (const [worktreeId, tabs] of Object.entries(session.browserTabsByWorktree ?? {})) { - if (tabs.length === 0) { - continue - } + for (const worktreeId of getWorkspaceSessionTabActivityIds(session)) { const summary = args.getSummary(worktreeId) if (summary) { summary.hasHostSidebarActivity = true 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-runtime.ts b/src/main/runtime/structured-agent-session-runtime.ts index 3ec22cc3ac0..a9e2d69dda1 100644 --- a/src/main/runtime/structured-agent-session-runtime.ts +++ b/src/main/runtime/structured-agent-session-runtime.ts @@ -67,10 +67,8 @@ import { type RuntimeAgentAccountHomeResolver } from './structured-agent-model-catalog-wiring' import type { ClaudeCliFlagSupport } from '../claude/claude-cli-flag-support' -import { - scheduleNativeChatVisualsSweep, - type NativeChatVisualsSweepDeps -} from '../native-chat/native-chat-visuals-sweep' +import { scheduleNativeChatVisualsSweep } from '../native-chat/native-chat-visuals-sweep' +import { readJournalSessionLastActivityAt } from '../native-chat/agent-session-journal/journal-row-table' /** Whether this profile holds a structured chat: a record or tab in the journal database. */ export function hasPersistedStructuredAgentSessionStore( @@ -104,11 +102,11 @@ export type StructuredAgentSessionRuntimeDeps = { /** Which version-gated flags a Claude CLI takes; absent never passes one. */ claudeCliFlags?: ClaudeCliFlagSupport /** Gives each chat a scratch folder and the visuals skill that teaches it, and sweeps folders - * whose chat is gone. Wired by the real hosts only, so a test runtime never loads the bundled skill. */ + * whose chat is gone or long unused. Wired by the real hosts only, so a test runtime never loads + * the bundled skill. */ nativeChatVisuals?: { /** Read this host's preference when a chat starts a provider process. */ isEnabled: () => boolean - workspaceVerdicts: NonNullable } /** Provider transports are overridden only to drive the runtime against scripted children. */ openCodexConnection?: CodexStructuredSessionAdapterDeps['openConnection'] @@ -368,8 +366,9 @@ async function installOnJournal( ? scheduleNativeChatVisualsSweep({ stateDirectory: deps.stateDirectory, listHeldSessionIds: () => (store.readOnly ? null : store.listHeldSessionIds()), - locationOf: (sessionId) => store.getRecord(sessionId)?.location ?? null, - workspaceVerdicts: deps.nativeChatVisuals.workspaceVerdicts, + lastJournalActivityAt: (sessionId) => + readJournalSessionLastActivityAt(journalDatabase.db, sessionId), + runWhileExecutionIdle: host.runWhileExecutionIdle, logger: deps.logger }) : undefined diff --git a/src/main/runtime/structured-attention-hidden-workspace.ts b/src/main/runtime/structured-attention-hidden-workspace.ts index bc6801f1c8f..83065e7f46e 100644 --- a/src/main/runtime/structured-attention-hidden-workspace.ts +++ b/src/main/runtime/structured-attention-hidden-workspace.ts @@ -3,8 +3,8 @@ // // WHY NOT "HIDE SLEEPING": main sees the persisted switches and its in-memory workspace records, // not the renderer's live tabs, so sleeping is left to the renderer; a local chat that raises -// attention is live anyway. Rows here are this host's own, so "Hide other-client" needs no -// paired-device map: any known creator other than the host is another client. +// attention is live anyway. Rows here are this host's own (no runtime owner environment), so the +// shared other-client rule needs no paired-device map; this host's own phones record it as creator. // // FAILS OPEN: no persisted UI, no record of the workspace, or a hidden parent with a shown lineage // child (the sidebar keeps it above that child) all notify. @@ -12,10 +12,13 @@ import type { AgentSessionExecutionLocation } from '../../shared/agent-session-r import { getRepoIdFromWorktreeId } from '../../shared/worktree/id' import type { Worktree } from '../../shared/worktree/types' import { - isCreatedByAnotherClient, isHiddenByWorkspaceKindSwitch, type WorkspaceKindSwitches } from '../../shared/worktree/workspace-kinds' +import { + EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT, + isWorkspaceOwnedBySomeoneElse +} from '../../shared/workspace-creator-visibility' import type { RuntimeStore } from './runtime-store-contract' export type HiddenWorkspaceAttentionDeps = { @@ -47,7 +50,9 @@ export function isStructuredAttentionWorkspaceHidden( // Folder rows answer only the other-client switch, as in the sidebar. const folder = deps.store.getFolderWorkspaces?.().find((item) => item.id === workspaceId) return ( - hideOtherClients && folder !== undefined && isCreatedByAnotherClient(folder.creatorProvenance) + hideOtherClients && + folder !== undefined && + isWorkspaceOwnedBySomeoneElse(folder, EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT) ) } const lineage = Object.values(deps.store.getAllWorktreeLineage?.() ?? {}) @@ -59,7 +64,10 @@ export function isStructuredAttentionWorkspaceHidden( } if ( hideOtherClients && - isCreatedByAnotherClient(listed?.creatorProvenance ?? meta?.creatorProvenance) + isWorkspaceOwnedBySomeoneElse( + { creatorProvenance: listed?.creatorProvenance ?? meta?.creatorProvenance }, + EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT + ) ) { return true } 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/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-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-tombstone-retry.test.ts b/src/main/terminal-history-tombstone-retry.test.ts index 575d81415a9..0cca210dc14 100644 --- a/src/main/terminal-history-tombstone-retry.test.ts +++ b/src/main/terminal-history-tombstone-retry.test.ts @@ -141,6 +141,7 @@ describe('tombstoned history removal retries', () => { for (let pass = 0; removeHostTreeMock.mock.calls.length < 1_000; pass++) { expect(pass).toBeLessThan(200) + await vi.waitFor(() => expect(releases.length).toBeGreaterThan(0)) releases.splice(0).forEach((release) => release()) await vi.advanceTimersByTimeAsync(0) await settleTombstoneDirectoryReads() 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/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/main/wsl/wsl-directory-upload.test.ts b/src/main/wsl/wsl-directory-upload.test.ts index a7240ac3823..5d6c78d6ff5 100644 --- a/src/main/wsl/wsl-directory-upload.test.ts +++ b/src/main/wsl/wsl-directory-upload.test.ts @@ -14,8 +14,9 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { spawnProcess } from '@orca/process-host' +import type { PipedChildProcess } from '@orca/process-host/process-spec' import { uploadDirectoryIntoWsl, wslGuestSourcePath } from './wsl-directory-upload' -import { createWslOrcadHostTransport } from './wsl-orcad-host-transport' +import { createWslOrcadHostTransport, type WslOrcadHostTransport } from './wsl-orcad-host-transport' function hasSha256sum(): boolean { try { @@ -56,22 +57,39 @@ describe.skipIf(process.platform === 'win32' || !hasSha256sum())('uploadDirector let source: string let dest: string let commands: string[] + const transports: WslOrcadHostTransport[] = [] + const children: PipedChildProcess[] = [] - const transport = createWslOrcadHostTransport({ - target: { kind: 'wsl', id: 'env-test', distro: 'test', label: 'test' }, - spawn: (spec) => { - const args = spec.args ?? [] - const command = args[args.indexOf('--exec') + 3] ?? '' - commands.push(command) - return spawnProcess({ program: 'sh', args: ['-c', command], stdio: 'pipe' }) + const getTransport = (): WslOrcadHostTransport => { + const transport = transports.at(-1) + if (!transport) { + throw new Error('WSL test transport was not initialized') } - }) + return transport + } beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'wsl-upload-')) source = join(root, 'source') dest = join(root, 'dest') commands = [] + const transport = createWslOrcadHostTransport({ + target: { kind: 'wsl', id: 'env-test', distro: 'test', label: 'test' }, + spawn: (spec) => { + const args = spec.args ?? [] + const execIndex = args.indexOf('--exec') + const command = args[execIndex + 3] ?? '' + commands.push(command) + const child = spawnProcess({ + program: 'sh', + args: args.slice(execIndex + 2), + stdio: 'pipe' + }) + children.push(child) + return child + } + }) + transports.push(transport) mkdirSync(join(source, 'bin'), { recursive: true }) mkdirSync(join(source, 'empty'), { recursive: true }) writeFileSync(join(source, 'bin', 'tool'), '#!/bin/sh\necho ran\n') @@ -81,6 +99,8 @@ describe.skipIf(process.platform === 'win32' || !hasSha256sum())('uploadDirector }) afterEach(() => { + transports.splice(0).forEach((transport) => transport.dispose()) + children.splice(0).forEach((child) => child.kill()) rmSync(root, { recursive: true, force: true }) }) @@ -94,19 +114,19 @@ describe.skipIf(process.platform === 'win32' || !hasSha256sum())('uploadDirector } it('streams a tar when the guest cannot see the source', async () => { - await uploadDirectoryIntoWsl(transport, source, null, dest) + await uploadDirectoryIntoWsl(getTransport(), source, null, dest) expectUploaded() expect(commands.some((command) => command.includes('tar -x'))).toBe(true) }) it('copies from the guest-visible path without a tar stream', async () => { - await uploadDirectoryIntoWsl(transport, source, source, dest) + await uploadDirectoryIntoWsl(getTransport(), source, source, dest) expectUploaded() expect(commands.some((command) => command.includes('tar -x'))).toBe(false) }) it('falls back to the tar stream when the mount is missing', async () => { - await uploadDirectoryIntoWsl(transport, source, join(root, 'not-mounted'), dest) + await uploadDirectoryIntoWsl(getTransport(), source, join(root, 'not-mounted'), dest) expectUploaded() expect(commands.some((command) => command.includes('tar -x'))).toBe(true) }) @@ -115,7 +135,7 @@ describe.skipIf(process.platform === 'win32' || !hasSha256sum())('uploadDirector mkdirSync(dest, { recursive: true }) writeFileSync(join(dest, 'keep.txt'), 'kept') writeFileSync(join(dest, 'large.bin'), 'stale') - await uploadDirectoryIntoWsl(transport, source, null, dest) + await uploadDirectoryIntoWsl(getTransport(), source, null, dest) expectUploaded() expect(readFileSync(join(dest, 'keep.txt'), 'utf8')).toBe('kept') }) @@ -124,7 +144,7 @@ describe.skipIf(process.platform === 'win32' || !hasSha256sum())('uploadDirector const other = join(root, 'other') execFileSync('cp', ['-R', source, other]) writeFileSync(join(other, 'large.bin'), 'tampered') - await expect(uploadDirectoryIntoWsl(transport, source, other, dest)).rejects.toThrow( + await expect(uploadDirectoryIntoWsl(getTransport(), source, other, dest)).rejects.toThrow( /digest check/ ) expect(existsSync(join(dest, 'large.bin'))).toBe(false) @@ -132,7 +152,7 @@ describe.skipIf(process.platform === 'win32' || !hasSha256sum())('uploadDirector }) it('writes a file atomically through stdin', async () => { - await transport.writeFile(join(dest, 'nested', 'record.json'), '{"ok":true}') + await getTransport().writeFile(join(dest, 'nested', 'record.json'), '{"ok":true}') expect(readFileSync(join(dest, 'nested', 'record.json'), 'utf8')).toBe('{"ok":true}') }) }) 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/attention/hidden-workspace-notification-mute.ts b/src/renderer/src/attention/hidden-workspace-notification-mute.ts index 6d368bbf6ed..1b8b553f448 100644 --- a/src/renderer/src/attention/hidden-workspace-notification-mute.ts +++ b/src/renderer/src/attention/hidden-workspace-notification-mute.ts @@ -5,7 +5,7 @@ import { parseWorkspaceKey } from '../../../shared/workspace-scope' import { sidebarHasActiveFilters } from '@/components/sidebar/sidebar-filter-actions' import { computeVisibleWorktreeIds } from '@/components/sidebar/compute-visible-worktrees' import { buildVisibleWorktreeOptionsFromState } from '@/components/sidebar/visible-worktree-options-from-state' -import { isFolderWorkspaceFromOtherDevice } from '@/components/sidebar/workspace-creator-visibility' +import { isFolderWorkspaceFromOtherDevice } from '../../../shared/workspace-creator-visibility' /** * Whether attention from this workspace must raise nothing — no unread, banner, sound or push — diff --git a/src/renderer/src/components/activity/activity-scope-filter.ts b/src/renderer/src/components/activity/activity-scope-filter.ts index 72daefced4e..069d3d842ba 100644 --- a/src/renderer/src/components/activity/activity-scope-filter.ts +++ b/src/renderer/src/components/activity/activity-scope-filter.ts @@ -5,7 +5,7 @@ import { isAutomationGeneratedWorkspace, isCliCreatedWorkspace } from '../../../../shared/worktree/workspace-kinds' -import { isWorkspaceFromOtherDevice } from '@/components/sidebar/workspace-creator-visibility' +import { isWorkspaceOwnedBySomeoneElse } from '../../../../shared/workspace-creator-visibility' import type { AgentPaneThread } from './activity-thread-types' /** Host/project/workspace-origin scope for the Agents activity surfaces. Persisted and separate @@ -105,7 +105,7 @@ export function threadMatchesActivityScope( } if ( scope.hideWorkspacesFromOtherDevices && - isWorkspaceFromOtherDevice(thread.worktree, scope.pairedDeviceIdsByEnvironment) + isWorkspaceOwnedBySomeoneElse(thread.worktree, scope.pairedDeviceIdsByEnvironment) ) { return false } diff --git a/src/renderer/src/components/activity/use-agent-pane-threads.ts b/src/renderer/src/components/activity/use-agent-pane-threads.ts index 5f3cb64d09f..5c587f8eed3 100644 --- a/src/renderer/src/components/activity/use-agent-pane-threads.ts +++ b/src/renderer/src/components/activity/use-agent-pane-threads.ts @@ -14,7 +14,7 @@ import { collectChildAgentPaneKeys } from './activity-thread-child-agent' import { EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT, getPairedDeviceIdsByEnvironment -} from '@/components/sidebar/workspace-creator-visibility' +} from '../../../../shared/workspace-creator-visibility' const EMPTY_PANE_KEYS: ReadonlySet = new Set() import { filterThreadsByActivityScope, resolveActivityScopeRepoIds } from './activity-scope-filter' diff --git a/src/renderer/src/components/automations/AutomationRunDetailsPage.test.tsx b/src/renderer/src/components/automations/AutomationRunDetailsPage.test.tsx index 785c002478e..19f515eeeac 100644 --- a/src/renderer/src/components/automations/AutomationRunDetailsPage.test.tsx +++ b/src/renderer/src/components/automations/AutomationRunDetailsPage.test.tsx @@ -2,11 +2,20 @@ import '@testing-library/jest-dom/vitest' import { cleanup, render, screen } from '@testing-library/react' -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import type { AutomationRun } from '../../../../shared/automations-types' import { AutomationRunDetailsPage } from './AutomationRunDetailsPage' import type { AutomationRunViewState } from './automation-run-view-state' +// These fixtures exercise markdown without rendering diagrams. +const boundaryMocks = vi.hoisted(() => ({ + mermaid: vi.fn(() => { + throw new Error('These fixtures must not render Mermaid diagrams') + }) +})) + +vi.mock('@/components/sidebar/CommentMermaidBlock', () => ({ default: boundaryMocks.mermaid })) + const run: AutomationRun = { id: 'run-1', automationId: 'automation-1', @@ -47,7 +56,14 @@ function renderPage(viewState: AutomationRunViewState): void { } describe('AutomationRunDetailsPage', () => { - afterEach(cleanup) + afterEach(() => { + cleanup() + try { + expect(boundaryMocks.mermaid).not.toHaveBeenCalled() + } finally { + boundaryMocks.mermaid.mockClear() + } + }) it('shows the open action when it can open something', () => { renderPage({ diff --git a/src/renderer/src/components/automations/automation-run-open-target.test.ts b/src/renderer/src/components/automations/automation-run-open-target.test.ts index 40e23c904ed..93938cc4342 100644 --- a/src/renderer/src/components/automations/automation-run-open-target.test.ts +++ b/src/renderer/src/components/automations/automation-run-open-target.test.ts @@ -114,13 +114,38 @@ describe('automation run open target', () => { ).toBeNull() }) - it('rejects a run without an exact PTY identity', () => { + it('targets the pane a successful run slept in place, before and after it wakes', () => { + // Why: hibernation drops only the run's PTY id; the pane key still names the run's own pane. + const sleptRun = run({ terminalPtyId: null }) + const asleepLayout = { ...runLeafLayout, ptyIdsByLeafId: { [leafId]: 'pty-run' } } + const asleep = resolveAutomationRunOpenTarget({ + run: sleptRun, + terminalTabExists: true, + currentLayout: asleepLayout, + livePtyIds: [] + }) + + expect(asleep).toEqual({ tabId: 'tab-1', paneKey, leafId, ptyId: null }) + if (!asleep) { + throw new Error('Expected target.') + } + expect( + buildAutomationRunOpenLayout({ target: asleep, currentLayout: asleepLayout }).ptyIdsByLeafId + ).toBe(asleepLayout.ptyIdsByLeafId) + expect( + resolveAutomationRunOpenTarget({ + run: sleptRun, + terminalTabExists: true, + currentLayout: { ...runLeafLayout, ptyIdsByLeafId: { [leafId]: 'pty-resumed' } }, + livePtyIds: ['pty-resumed'] + }) + ).toEqual({ tabId: 'tab-1', paneKey, leafId, ptyId: 'pty-resumed' }) expect( canOpen({ - run: run({ terminalPtyId: null }), - terminalTabExists: true, - currentLayout: runLeafLayout, - livePtyIds + run: sleptRun, + terminalTabExists: false, + currentLayout: asleepLayout, + livePtyIds: [] }) ).toBe(false) }) diff --git a/src/renderer/src/components/automations/automation-run-open-target.ts b/src/renderer/src/components/automations/automation-run-open-target.ts index 4a24aecbe22..f70a8ad9d94 100644 --- a/src/renderer/src/components/automations/automation-run-open-target.ts +++ b/src/renderer/src/components/automations/automation-run-open-target.ts @@ -15,7 +15,8 @@ export type AutomationRunPaneTarget = { tabId: string paneKey: string leafId: string - ptyId: string + /** Null while a hibernated run pane waits for its tab to wake it. */ + ptyId: string | null } function getAutomationRunOpenTabId(run: Pick): string | null { @@ -41,16 +42,25 @@ export function resolveAutomationRunOpenTarget({ livePtyIds: readonly string[] }): AutomationRunPaneTarget | null { const parsed = parsePaneKey(run.terminalPaneKey ?? '') - if (!terminalTabExists || !parsed || !run.terminalPtyId || !currentLayout?.root) { + if (!terminalTabExists || !parsed || !currentLayout?.root) { return null } if (!terminalLayoutContainsLeaf(currentLayout.root, parsed.leafId)) { return null } + const layoutPtyId = currentLayout.ptyIdsByLeafId?.[parsed.leafId] + if (!run.terminalPtyId) { + // Why: a slept run pane keeps its key but drops its PTY id; the pane, not a process, is the target. + return { + tabId: parsed.tabId, + paneKey: run.terminalPaneKey!, + leafId: parsed.leafId, + ptyId: layoutPtyId && livePtyIds.includes(layoutPtyId) ? layoutPtyId : null + } + } if (!livePtyIds.includes(run.terminalPtyId)) { return null } - const layoutPtyId = currentLayout.ptyIdsByLeafId?.[parsed.leafId] if (layoutPtyId !== undefined && layoutPtyId !== run.terminalPtyId) { return null } @@ -125,10 +135,9 @@ export function buildAutomationRunOpenLayout({ ...currentLayout, activeLeafId: target.leafId, expandedLeafId: currentLayout.expandedLeafId === target.leafId ? target.leafId : null, - ptyIdsByLeafId: { - ...currentLayout.ptyIdsByLeafId, - [target.leafId]: target.ptyId - } + ptyIdsByLeafId: target.ptyId + ? { ...currentLayout.ptyIdsByLeafId, [target.leafId]: target.ptyId } + : currentLayout.ptyIdsByLeafId } } diff --git a/src/renderer/src/components/automations/automation-run-view-state.test.ts b/src/renderer/src/components/automations/automation-run-view-state.test.ts index bc007f51c62..dc50ad0b319 100644 --- a/src/renderer/src/components/automations/automation-run-view-state.test.ts +++ b/src/renderer/src/components/automations/automation-run-view-state.test.ts @@ -100,7 +100,7 @@ describe('automation run view state', () => { }) ).toMatchObject({ availability: 'workspace', - actionLabel: 'Resume workspace', + actionLabel: 'Open workspace', statusLabel: 'Workspace is available.' }) }) diff --git a/src/renderer/src/components/automations/automation-run-view-state.ts b/src/renderer/src/components/automations/automation-run-view-state.ts index 4f35e3f8f37..c8066c0146b 100644 --- a/src/renderer/src/components/automations/automation-run-view-state.ts +++ b/src/renderer/src/components/automations/automation-run-view-state.ts @@ -92,7 +92,7 @@ export function getAutomationRunViewState({ if (run.workspaceId && workspaceExists && run.dispatchedAt !== null) { return { availability: 'workspace', - actionLabel: 'Resume workspace', + actionLabel: 'Open workspace', statusLabel: 'Workspace is available.', canOpen: true } diff --git a/src/renderer/src/components/editor/MonacoEditor.content-owner.test.tsx b/src/renderer/src/components/editor/MonacoEditor.content-owner.test.tsx index 68c8740eda6..387078a6971 100644 --- a/src/renderer/src/components/editor/MonacoEditor.content-owner.test.tsx +++ b/src/renderer/src/components/editor/MonacoEditor.content-owner.test.tsx @@ -38,6 +38,8 @@ vi.mock('@monaco-editor/react', () => ({ }, loader: { config: vi.fn() } })) +// Why: these wrapper contracts never mount Monaco. +vi.mock('@/lib/monaco-setup', () => ({})) vi.mock('@/store', () => ({ useAppStore: (selector: (state: Record) => unknown) => selector({ diff --git a/src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx b/src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx index 9e49a3d6ed3..736c75979bf 100644 --- a/src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx +++ b/src/renderer/src/components/editor/MonacoEditor.font-family.test.tsx @@ -19,6 +19,7 @@ vi.mock('@monaco-editor/react', () => ({ }, loader: { config: vi.fn() } })) +vi.mock('@/lib/monaco-setup', () => ({})) vi.mock('@/store', () => ({ useAppStore: (selector: (state: Record) => unknown) => selector({ diff --git a/src/renderer/src/components/editor/MonacoEditor.system-theme.test.tsx b/src/renderer/src/components/editor/MonacoEditor.system-theme.test.tsx index 9f1065591a4..d3ac9e63e13 100644 --- a/src/renderer/src/components/editor/MonacoEditor.system-theme.test.tsx +++ b/src/renderer/src/components/editor/MonacoEditor.system-theme.test.tsx @@ -17,6 +17,7 @@ vi.mock('@monaco-editor/react', () => ({ }, loader: { config: vi.fn() } })) +vi.mock('@/lib/monaco-setup', () => ({})) vi.mock('@/store', () => ({ useAppStore: (selector: (state: Record) => unknown) => selector({ diff --git a/src/renderer/src/components/editor/editor-model-lifetime-fixture.ts b/src/renderer/src/components/editor/editor-model-lifetime-fixture.ts index 5ec2466f452..a6821baf128 100644 --- a/src/renderer/src/components/editor/editor-model-lifetime-fixture.ts +++ b/src/renderer/src/components/editor/editor-model-lifetime-fixture.ts @@ -1,7 +1,8 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js' import { vi } from 'vitest' import { createEditorModelRegistry } from '@/lib/editor-model-registry' -import { createTestStore, makeWorktree, TEST_REPO } from '@/store/slices/store-test-helpers' +import { createEditorTabsStore } from '@/store/slices/editor-slice-test-harness' +import { makeWorktree, TEST_REPO } from '@/store/slices/worktrees-slice-test-fixtures' import { createStoreSessionMockApi } from '@/store/slices/store-session-test-harness' import type { OpenFile } from '@/store/slices/editor' import { attachClosedEditorTabCleanup } from './closed-editor-tab-controller' @@ -22,8 +23,16 @@ export function createModelLifetimeFixture(register = true) { const api = createStoreSessionMockApi() Reflect.set(globalThis, 'window', domWindow) Reflect.set(domWindow, 'api', api) - const store = createTestStore() + const store = createEditorTabsStore() store.setState({ + settings: null, + detectedWorktreesByRepo: {}, + activeWorkspaceExecutionHostId: null, + restoredRuntimeHostIdByWorkspaceSessionKey: {}, + runtimeEnvironments: [], + runtimeEnvironmentCatalogHydrated: false, + removedRuntimeEnvironmentIds: new Set(), + recentlyClosedTabKindsByWorktree: {}, repos: [{ ...TEST_REPO, id: 'model-fixture', path: '/fixture/workspace' }], worktreesByRepo: { 'model-fixture': [ diff --git a/src/renderer/src/components/editor/rich-markdown-large-text-paste.selection.test.ts b/src/renderer/src/components/editor/rich-markdown-large-text-paste.selection.test.ts index 839c625d56d..4c7438353a3 100644 --- a/src/renderer/src/components/editor/rich-markdown-large-text-paste.selection.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-large-text-paste.selection.test.ts @@ -12,6 +12,15 @@ import { setRichMarkdownImageResolverContext } from './rich-markdown-image-conte vi.mock('sonner', () => ({ toast: { error: vi.fn(), info: vi.fn() } })) vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback })) +// These paths use supplied file context without resolving its editor connection. +const boundaryMocks = vi.hoisted(() => ({ + getConnectionId: vi.fn(() => { + throw new Error('These fixtures must not resolve editor connections') + }) +})) + +vi.mock('@/lib/connection-context', () => ({ getConnectionId: boundaryMocks.getConnectionId })) + const editors: Editor[] = [] const largeText = 'X'.repeat(70 * 1024) @@ -65,8 +74,13 @@ function textPosition(editor: Editor, text: string): number { afterEach(() => { editors.splice(0).forEach((editor) => editor.destroy()) document.body.replaceChildren() - vi.restoreAllMocks() - vi.clearAllMocks() + try { + expect(boundaryMocks.getConnectionId).not.toHaveBeenCalled() + } finally { + boundaryMocks.getConnectionId.mockClear() + vi.restoreAllMocks() + vi.clearAllMocks() + } }) describe('large Markdown paste selection during yields', () => { diff --git a/src/renderer/src/components/editor/rich-markdown-large-text-paste.test.ts b/src/renderer/src/components/editor/rich-markdown-large-text-paste.test.ts index bf904cc1eed..6300dc706f5 100644 --- a/src/renderer/src/components/editor/rich-markdown-large-text-paste.test.ts +++ b/src/renderer/src/components/editor/rich-markdown-large-text-paste.test.ts @@ -14,6 +14,15 @@ vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback })) +// These paths use supplied file context without resolving its editor connection. +const boundaryMocks = vi.hoisted(() => ({ + getConnectionId: vi.fn(() => { + throw new Error('These fixtures must not resolve editor connections') + }) +})) + +vi.mock('@/lib/connection-context', () => ({ getConnectionId: boundaryMocks.getConnectionId })) + const editors: Editor[] = [] function makePasteEvent(text: string, html = ''): ClipboardEvent { @@ -68,8 +77,13 @@ async function flushPromises(count = 12): Promise { afterEach(() => { editors.splice(0).forEach((editor) => editor.destroy()) document.body.replaceChildren() - vi.restoreAllMocks() - vi.clearAllMocks() + try { + expect(boundaryMocks.getConnectionId).not.toHaveBeenCalled() + } finally { + boundaryMocks.getConnectionId.mockClear() + vi.restoreAllMocks() + vi.clearAllMocks() + } }) describe('rich markdown large text paste', () => { diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.shortcuts.test.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.shortcuts.test.tsx index 597a3fff646..a4d103846e3 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.shortcuts.test.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.shortcuts.test.tsx @@ -225,7 +225,8 @@ describe('FloatingTerminalPanel close behavior', () => { expect(mocks.createTab).toHaveBeenCalledWith( FLOATING_TERMINAL_WORKTREE_ID, 'floating-group', - undefined + undefined, + { id: expect.any(String) } ) expect(mocks.activateTab).not.toHaveBeenCalled() }) @@ -321,7 +322,8 @@ describe('FloatingTerminalPanel close behavior', () => { expect(mocks.createTab).toHaveBeenCalledWith( FLOATING_TERMINAL_WORKTREE_ID, 'floating-group', - undefined + undefined, + { id: expect.any(String) } ) expect(mocks.activateTab).not.toHaveBeenCalled() }) diff --git a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tab-lifecycle.test.tsx b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tab-lifecycle.test.tsx index 9337edcde1c..a8991056fed 100644 --- a/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tab-lifecycle.test.tsx +++ b/src/renderer/src/components/floating-terminal/FloatingTerminalPanel.tab-lifecycle.test.tsx @@ -202,10 +202,11 @@ describe('FloatingTerminalPanel close behavior', () => { expect(mocks.createTab).toHaveBeenCalledWith( FLOATING_TERMINAL_WORKTREE_ID, 'floating-group', - undefined + undefined, + { id: expect.any(String) } ) expect(mocks.activateTab).not.toHaveBeenCalled() - expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('created-tab') + expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith(mocks.createTab.mock.calls[0][3]?.id) }) it('keeps floating browser create and duplicate local during active web runtime sessions', async () => { @@ -488,10 +489,11 @@ describe('FloatingTerminalPanel close behavior', () => { expect(mocks.createTab).toHaveBeenCalledWith( FLOATING_TERMINAL_WORKTREE_ID, 'floating-group', - undefined + undefined, + { id: expect.any(String) } ) expect(mocks.activateTab).not.toHaveBeenCalled() - expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith('created-tab') + expect(mocks.focusTerminalTabSurface).toHaveBeenCalledWith(mocks.createTab.mock.calls[0][3]?.id) ;(tabBar.props.onClose as (tabId: string) => void)('tab-1') expect(mocks.closeWebRuntimeSessionTab).not.toHaveBeenCalled() diff --git a/src/renderer/src/components/floating-terminal/floating-terminal-panel-test-fixtures.ts b/src/renderer/src/components/floating-terminal/floating-terminal-panel-test-fixtures.ts index cd061b5ccf0..48e3044bac6 100644 --- a/src/renderer/src/components/floating-terminal/floating-terminal-panel-test-fixtures.ts +++ b/src/renderer/src/components/floating-terminal/floating-terminal-panel-test-fixtures.ts @@ -29,7 +29,12 @@ export type FloatingPanelStoreState = { worktreeId: string, groupId?: string, shellOverride?: string, - options?: { activate?: boolean; pendingActivationSpawn?: boolean; initialPtyId?: string } + options?: { + activate?: boolean + pendingActivationSpawn?: boolean + initialPtyId?: string + id?: string + } ) => TerminalTab createBrowserTab: ( worktreeId: string, diff --git a/src/renderer/src/components/floating-terminal/use-floating-terminal-create-actions.ts b/src/renderer/src/components/floating-terminal/use-floating-terminal-create-actions.ts index 23f87a7ea90..44839079112 100644 --- a/src/renderer/src/components/floating-terminal/use-floating-terminal-create-actions.ts +++ b/src/renderer/src/components/floating-terminal/use-floating-terminal-create-actions.ts @@ -7,6 +7,7 @@ import { ensureClientCreationActionAllowed } from '@/lib/client-creation-action- import { openDocumentInFloatingWorkspace } from '@/lib/open-document-in-floating-workspace' import { extractIpcErrorMessage } from '@/lib/ipc-error' import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface' +import { createBrowserUuid } from '@/lib/browser-uuid' import { translate } from '@/i18n/i18n' import { useAppStore } from '@/store' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' @@ -66,8 +67,9 @@ export function useFloatingTerminalCreateActions({ const createFloatingTerminalTab = useCallback( (shellOverride?: string) => { - const tab = createTab(FLOATING_TERMINAL_WORKTREE_ID, activeGroup?.id, shellOverride) - focusTerminalTabSurface(tab.id) + const tabId = createBrowserUuid() + createTab(FLOATING_TERMINAL_WORKTREE_ID, activeGroup?.id, shellOverride, { id: tabId }) + focusTerminalTabSurface(tabId) }, [activeGroup, createTab] ) diff --git a/src/renderer/src/components/native-chat/NativeChatDockedTaskList.tsx b/src/renderer/src/components/native-chat/NativeChatDockedTaskList.tsx new file mode 100644 index 00000000000..3ed6376e95a --- /dev/null +++ b/src/renderer/src/components/native-chat/NativeChatDockedTaskList.tsx @@ -0,0 +1,23 @@ +import { useMemo } from 'react' +import type { NativeChatMessage } from '../../../../shared/native-chat-types' +import { NativeChatTaskList } from './NativeChatTaskList' +import { nativeChatTaskListState } from './native-chat-task-list-state' + +/** The conversation's current task list, docked under the transcript. */ +export function NativeChatDockedTaskList({ + messages +}: { + messages: readonly NativeChatMessage[] +}): React.JSX.Element | null { + const { list } = useMemo(() => nativeChatTaskListState(messages), [messages]) + if (!list || list.tasks.length === 0) { + return null + } + return ( +
+
+ +
+
+ ) +} diff --git a/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.test.tsx b/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.test.tsx index f023b17f9c4..e49929fe06c 100644 --- a/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatImageAttachmentPreview.test.tsx @@ -6,7 +6,10 @@ import { NativeChatImageAttachmentPreview } from './NativeChatImageAttachmentPre import type { NativeChatComposerImageAttachment } from './NativeChatComposerField' const mocks = vi.hoisted(() => ({ - useLocalImageSrc: vi.fn() + useLocalImageSrc: vi.fn(), + getConnectionIdForFile: vi.fn(() => { + throw new Error('Image preview fixtures must not resolve editor file connections') + }) })) vi.mock('@/i18n/i18n', () => ({ @@ -14,6 +17,10 @@ vi.mock('@/i18n/i18n', () => ({ fallback.replace(/\{\{(\w+)\}\}/g, (_match, name: string) => options?.[name] ?? '') })) +vi.mock('@/lib/connection-context', () => ({ + getConnectionIdForFile: mocks.getConnectionIdForFile +})) + vi.mock('@/components/editor/useLocalImageSrc', () => ({ useLocalImageSrc: mocks.useLocalImageSrc })) @@ -22,6 +29,8 @@ afterEach(() => { cleanup() vi.unstubAllGlobals() mocks.useLocalImageSrc.mockReset() + expect(mocks.getConnectionIdForFile).not.toHaveBeenCalled() + mocks.getConnectionIdForFile.mockClear() }) function renderPreview( diff --git a/src/renderer/src/components/native-chat/NativeChatMarkdown.code-wrap.test.tsx b/src/renderer/src/components/native-chat/NativeChatMarkdown.code-wrap.test.tsx index b19f059b83b..5b753a5789f 100644 --- a/src/renderer/src/components/native-chat/NativeChatMarkdown.code-wrap.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMarkdown.code-wrap.test.tsx @@ -2,6 +2,19 @@ import '@testing-library/jest-dom/vitest' import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() +}) import { NativeChatCodeBlock } from './NativeChatCodeBlock' import { NativeChatMarkdown } from './NativeChatMarkdown' import { diff --git a/src/renderer/src/components/native-chat/NativeChatMarkdown.mermaid.test.tsx b/src/renderer/src/components/native-chat/NativeChatMarkdown.mermaid.test.tsx index 8667044424c..8f7d31f4c6c 100644 --- a/src/renderer/src/components/native-chat/NativeChatMarkdown.mermaid.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMarkdown.mermaid.test.tsx @@ -3,6 +3,19 @@ import '@testing-library/jest-dom/vitest' import { act, cleanup, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() +}) + const mermaid = vi.hoisted(() => ({ initialize: vi.fn(), render: vi.fn() })) const theme = vi.hoisted(() => ({ value: 'light', listeners: new Set<() => void>() })) vi.mock('mermaid', () => ({ default: mermaid })) diff --git a/src/renderer/src/components/native-chat/NativeChatMarkdown.test.tsx b/src/renderer/src/components/native-chat/NativeChatMarkdown.test.tsx index 8b189102ee3..509a3880dc4 100644 --- a/src/renderer/src/components/native-chat/NativeChatMarkdown.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMarkdown.test.tsx @@ -1,7 +1,20 @@ // @vitest-environment happy-dom import '@testing-library/jest-dom/vitest' import { cleanup, render, screen } from '@testing-library/react' -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() +}) import CommentMarkdown from '@/components/sidebar/CommentMarkdown' import { NativeChatMarkdown } from './NativeChatMarkdown' diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.growth-windowing.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.growth-windowing.test.tsx index 218f66e2d37..2e841c63e38 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.growth-windowing.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.growth-windowing.test.tsx @@ -4,6 +4,26 @@ import '@testing-library/jest-dom/vitest' import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type { NativeChatMessage } from '../../../../shared/native-chat-types' import { NativeChatMessageList } from './NativeChatMessageList' import { 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.older-history.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.older-history.test.tsx index 478e0c85a2f..8bf607c9644 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.older-history.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.older-history.test.tsx @@ -4,6 +4,26 @@ import '@testing-library/jest-dom/vitest' import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type { NativeChatMessage } from '../../../../shared/native-chat-types' import { NativeChatMessageList } from './NativeChatMessageList' import type { NativeChatOlderPageResult } from './native-chat-pagination' diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.opening-send.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.opening-send.test.tsx index 8fef8febbe0..1b81ce88f2e 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.opening-send.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.opening-send.test.tsx @@ -6,6 +6,26 @@ import '@testing-library/jest-dom/vitest' import { cleanup, render, screen } from '@testing-library/react' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import { agentSessionFailureFact } from '../../../../shared/agent-session-failure' import { agentSessionFailureWords } from '../../../../shared/agent-session-failure-words' import { agentJournalSubmissionKey } from '../../../../shared/agent-session-journal-item-key' diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.queued-turn-order.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.queued-turn-order.test.tsx index c50397706f6..7a36f6886c6 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.queued-turn-order.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.queued-turn-order.test.tsx @@ -4,6 +4,26 @@ import '@testing-library/jest-dom/vitest' import { cleanup, render, screen } from '@testing-library/react' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type { AgentJournalItemBody, AgentJournalRenderItem, 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..f147eff5f1a 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 @@ -5,6 +5,26 @@ import '@testing-library/jest-dom/vitest' import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import { useCallback, useMemo, useState } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type { AgentJournalItemBody, AgentJournalRenderItem @@ -113,6 +133,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 +199,7 @@ describe('jumping from the rail while following the end', () => { }) afterEach(() => { + vi.useRealTimers() vi.restoreAllMocks() Object.defineProperty(layout, 'aboveTranscriptPx', { configurable: true, @@ -202,8 +233,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.reader-navigation.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.reader-navigation.test.tsx index d777dd72d83..db958b6e19c 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.reader-navigation.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.reader-navigation.test.tsx @@ -5,6 +5,26 @@ import '@testing-library/jest-dom/vitest' import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import { createRef } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type { NativeChatMessage } from '../../../../shared/native-chat-types' import { routeNativeChatRootKeyToInput } from './native-chat-root-key-routing' import { NativeChatMessageList } from './NativeChatMessageList' diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.rewind-rows.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.rewind-rows.test.tsx index c6d90d0298f..c4ee6d37416 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.rewind-rows.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.rewind-rows.test.tsx @@ -4,6 +4,26 @@ import '@testing-library/jest-dom/vitest' import { cleanup, render, screen } from '@testing-library/react' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import { agentJournalItemKey, agentJournalSubmissionKey diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.stream-render.perf.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.stream-render.perf.test.tsx index beda7f0453c..1fb38fed9a4 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.stream-render.perf.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.stream-render.perf.test.tsx @@ -4,6 +4,26 @@ import '@testing-library/jest-dom/vitest' import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type * as NativeChatProseModule from '../../../../shared/native-chat-prose' import type { NativeChatMessage } from '../../../../shared/native-chat-types' import { projectStructuredAgentSessionMessages } from '../../../../shared/structured-agent-session-message-projection' diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.subagent-sections.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.subagent-sections.test.tsx index bc1e583c1cd..f77dacbbbe3 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.subagent-sections.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.subagent-sections.test.tsx @@ -6,6 +6,26 @@ import { cleanup, fireEvent, render, screen, within, act } from '@testing-librar import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) + import type { AgentJournalItemBody, AgentJournalProducerLinkage, diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx index a31c831b10a..e79c98c8e7a 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.test.tsx @@ -6,6 +6,26 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) + import { subagentGroupFallbackText } from '../../../../shared/native-chat-subagent-summary' import type { diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.tool-stream-cost.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.tool-stream-cost.test.tsx index bebcab390d6..ed2c0328bef 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.tool-stream-cost.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.tool-stream-cost.test.tsx @@ -1,6 +1,26 @@ // @vitest-environment happy-dom import { cleanup, render, screen } from '@testing-library/react' import { afterAll, afterEach, beforeAll, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type { AgentJournalRenderItem } from '../../../../shared/agent-session-journal-types' import type * as EditNormalization from '../../../../shared/native-chat-edit-normalize' import type { NativeChatLiveSession } from './use-native-chat-live-session' 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/NativeChatMessageList.turn-indicator.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx index 5887b9a929e..beb8be27d95 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx @@ -4,6 +4,26 @@ import '@testing-library/jest-dom/vitest' import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type { NativeChatLiveSession } from './use-native-chat-live-session' import { NativeChatMessageList } from './NativeChatMessageList' import { installNativeChatMessageListTestViewport } from './native-chat-message-list-test-viewport' diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.turn-membership.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.turn-membership.test.tsx index da01701f637..b0b325951c9 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.turn-membership.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.turn-membership.test.tsx @@ -6,6 +6,26 @@ import { cleanup, render, screen, fireEvent } from '@testing-library/react' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) + import { agentJournalItemKey, agentJournalSubmissionKey diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.turn-status.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.turn-status.test.tsx index b9d3949300d..cc7c6c86f39 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.turn-status.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.turn-status.test.tsx @@ -4,6 +4,26 @@ import '@testing-library/jest-dom/vitest' import { cleanup, render, screen } from '@testing-library/react' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import { agentSessionFailureWords } from '../../../../shared/agent-session-failure-words' import type { AgentJournalItemBody, diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.unsent-message.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.unsent-message.test.tsx index 9b2e1dc8ef9..d042db15882 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.unsent-message.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.unsent-message.test.tsx @@ -8,6 +8,26 @@ import '@testing-library/jest-dom/vitest' import { cleanup, render } from '@testing-library/react' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type { AgentJournalItemBody, AgentJournalRenderItem, diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx index 489bd992d1c..8c6c7ddd02d 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.windowing.test.tsx @@ -4,6 +4,26 @@ import '@testing-library/jest-dom/vitest' import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type { AgentJournalItemBody, AgentJournalRenderItem diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.work-run-memo.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.work-run-memo.test.tsx index f73e96dfc1a..3919fded6b0 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.work-run-memo.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.work-run-memo.test.tsx @@ -2,6 +2,26 @@ import { cleanup, render } from '@testing-library/react' import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type * as NativeChatWorkRunModule from '../../../../shared/native-chat-work-run' import type { AgentJournalRenderItem } from '../../../../shared/agent-session-journal-types' import type { NativeChatMessage } from '../../../../shared/native-chat-types' diff --git a/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx index e1cd980a791..46a064dc422 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageRow.test.tsx @@ -2,6 +2,26 @@ import '@testing-library/jest-dom/vitest' import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type { NativeChatMessage } from '../../../../shared/native-chat-types' import { MessageRow, type NativeChatDeliveryNotice } from './NativeChatMessageRow' import { TooltipProvider } from '@/components/ui/tooltip' diff --git a/src/renderer/src/components/native-chat/NativeChatMessageRow.work-run.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageRow.work-run.test.tsx index 7452b7a9ed6..4653953e172 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageRow.work-run.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageRow.work-run.test.tsx @@ -1,7 +1,27 @@ // @vitest-environment happy-dom import '@testing-library/jest-dom/vitest' import { cleanup, fireEvent, render, screen } from '@testing-library/react' -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' + +// Why: these fixtures have neither inline visual owners nor sender navigation. +const unusedChatLeaves = vi.hoisted(() => ({ + inlineVisual: vi.fn(() => { + throw new Error('Unexpected inline visual in transcript fixture') + }), + openSender: vi.fn(() => { + throw new Error('Unexpected sender navigation in transcript fixture') + }) +})) +vi.mock('./NativeChatInlineVisual', () => ({ + NativeChatInlineVisual: unusedChatLeaves.inlineVisual +})) +vi.mock('@/lib/open-agent-message-sender', () => ({ + openAgentMessageSender: unusedChatLeaves.openSender +})) +afterEach(() => { + expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled() + expect(unusedChatLeaves.openSender).not.toHaveBeenCalled() +}) import type { NativeChatMessage } from '../../../../shared/native-chat-types' import { MessageRow } from './NativeChatMessageRow' 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( +
+
+ +
+
+
+ +
+
+