From cb44ca01245aa8bdb8b4144fc9dc6e7df5286cbf Mon Sep 17 00:00:00 2001 From: zpyoung Date: Mon, 19 Oct 2026 15:10:35 -0500 Subject: [PATCH 1/3] ci: trim PR checks to the essential gates Keep code_paths, the fork ownership guard, preflight (lint, typecheck, skill guide and localization catalog verification, unit planning), unit tests and cross-version wire. Drop the remaining jobs and preflight ratchets, and the verify entries and code_paths outputs only they read. --- .github/workflows/pr.yml | 1088 +------------------------------------- 1 file changed, 1 insertion(+), 1087 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9267e29dfb55..d25f19083987 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -35,29 +35,14 @@ jobs: actions: read outputs: should_run: ${{ steps.filter.outputs.should_run }} - changed_count: ${{ steps.filter.outputs.changed_count }} reused_run_id: ${{ steps.readiness.outputs.run_id }} # A proven success masks required work only; advisory routing still uses the full diff. mobile_dependencies: ${{ steps.filter.outputs.mobile_dependencies }} - mobile_web_app: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.mobile_web_app }} static_analysis: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.static_analysis }} typecheck: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.typecheck }} mobile_ci_tools: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.mobile_ci_tools }} - git_compatibility: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.git_compatibility }} - codex_index_heal_contract: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.codex_index_heal_contract }} - xterm_patch_sync: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.xterm_patch_sync }} - shell_contracts: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.shell_contracts }} test: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.test }} - orcad_browser: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.orcad_browser }} cross-version-wire: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.cross-version-wire }} - managed_hook_node18: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.managed_hook_node18 }} - package: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.package }} - layout_oracle: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.layout_oracle }} - e2e_should_run: ${{ steps.e2e_filter.outputs.should_run }} - test_files: ${{ steps.e2e_filter.outputs.test_files }} - e2e_run_changed: ${{ steps.e2e_filter.outputs.e2e_run_changed }} - e2e_needs_build: ${{ steps.e2e_filter.outputs.e2e_needs_build }} - native_ime_source_changed: ${{ steps.e2e_filter.outputs.native_ime_source_changed }} steps: - name: Checkout uses: actions/checkout@v6 @@ -144,44 +129,6 @@ jobs: node config/scripts/pr-code-change-scope.mjs "${MOBILE_CI_ARGS[@]}" \ < "$RUNNER_TEMP/changed-paths.txt" | tee -a "$GITHUB_OUTPUT" - # Reuse the path-detector checkout instead of queuing another runner. - - name: Filter changed E2E specs - id: e2e_filter - if: github.event.pull_request.draft != true && steps.filter.outputs.should_run == 'true' - run: | - set -euo pipefail - BASE="${{ github.event.pull_request.base.sha }}" - DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE")" - # Use a file here too so sync-sized diffs cannot exhaust Node's stdin pipe buffer. - git diff --name-only --diff-filter=AMCR "$DIFF_BASE" HEAD \ - > "$RUNNER_TEMP/e2e-changed-paths.txt" - E2E_SCOPE_ARGS=() - if git diff --quiet --no-renames --diff-filter=ACDRTUXB "$DIFF_BASE" HEAD; then - E2E_SCOPE_ARGS=(--daemon-init-test-support) - fi - # Source routes are executable contracts so a test can prove exact - # authorities, exclusions, and sentinels without evaluating workflow shell. - TEST_FILES_JSON="$(node config/scripts/pr-e2e-source-routing.mjs "${E2E_SCOPE_ARGS[@]}" < "$RUNNER_TEMP/e2e-changed-paths.txt")" - echo "test_files=$TEST_FILES_JSON" >> "$GITHUB_OUTPUT" - printf '%s\n' "$TEST_FILES_JSON" | node config/scripts/ci-e2e-job-selection.mjs --job-outputs >> "$GITHUB_OUTPUT" - # Why its own signal: the real-IME lane is a whole ibus session, not a spec, so it must - # trigger on IME source rather than on a spec name in some route's list. - NATIVE_IME_SOURCE_CHANGED="$(node config/scripts/pr-e2e-source-routing.mjs --native-ime-source < "$RUNNER_TEMP/e2e-changed-paths.txt")" - echo "native_ime_source_changed=$NATIVE_IME_SOURCE_CHANGED" >> "$GITHUB_OUTPUT" - echo "Native IME source changed: $NATIVE_IME_SOURCE_CHANGED" - SHOULD_RUN="$(node config/scripts/pr-e2e-source-routing.mjs --reusable-workflow "${E2E_SCOPE_ARGS[@]}" < "$RUNNER_TEMP/e2e-changed-paths.txt")" - TEST_FILE_COUNT="$(jq 'length' <<< "$TEST_FILES_JSON")" - if [ "$TEST_FILE_COUNT" -gt 25 ]; then - echo "should_run=false" >> "$GITHUB_OUTPUT" - echo "Skipping reusable E2E workflow: $TEST_FILE_COUNT selected specs exceeds the 25-spec limit; this sync-sized selection is covered by the scheduled full suite." - elif [ "$SHOULD_RUN" = true ]; then - echo "should_run=true" >> "$GITHUB_OUTPUT" - echo "Changed E2E specs: $TEST_FILES_JSON" - else - echo "should_run=false" >> "$GITHUB_OUTPUT" - echo "No specs requiring the reusable E2E workflow" - fi - preflight: name: static analysis and typecheck needs: [code_paths] @@ -190,7 +137,6 @@ jobs: runs-on: ubuntu-24.04-arm outputs: shards: ${{ steps.unit-plan.outputs.shards }} - managed_hook_node18: ${{ steps.managed_hook_node18.outcome }} steps: - name: Checkout @@ -204,22 +150,6 @@ jobs: fetch-depth: 50 persist-credentials: false - # The dependency action below restores Node 24 before installing or building packages. - - name: Prepare Node 18 smoke runtime - id: node18_runtime - if: needs.code_paths.outputs.managed_hook_node18 == 'true' - continue-on-error: true - uses: actions/setup-node@v6 - with: - node-version: '18' - package-manager-cache: false - - - name: Capture Node 18 executable - id: node18_executable - if: needs.code_paths.outputs.managed_hook_node18 == 'true' && steps.node18_runtime.outcome == 'success' - continue-on-error: true - run: node -p '"executable=" + process.execPath' >> "$GITHUB_OUTPUT" - # Why two guarded installs: the mixed root+mobile store entry is 537 MB against # 321 MB for root alone, and restoring it costs 8.6s against 4.6s. Most PRs skip the # mobile install below, so they were paying 216 MB for packages they never link. The @@ -245,29 +175,6 @@ 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 - background: true - env: - PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} - PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} - run: | - if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - bun config/scripts/check-import-boundaries.mjs - - - name: Check main-process sync launches - if: '!cancelled()' - id: main-sync-spawn-ratchet - background: true - env: - PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} - PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} - run: | - if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - bun config/scripts/check-main-sync-spawn-ratchet.mjs - # Keep each check in its own log while sharing this runner. - name: Lint if: '!cancelled()' @@ -280,74 +187,8 @@ jobs: if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi node node_modules/oxlint/bin/oxlint --format github - - name: Reject low-evidence patterns - if: needs.code_paths.outputs.static_analysis == 'true' - run: | - node config/scripts/sync-anti-slop-plugin.mjs - bun --smol node_modules/oxlint/bin/oxlint --config config/oxlint-anti-slop.json src config tests mobile --deny-warnings - - wait: root-lint - - name: Enforce focused code-quality plugins - if: '!cancelled()' - id: native-code-quality - background: true - env: - PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} - PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} - run: | - if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - 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: 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 - - # Why here: the changed-code gate lints mobile files too, and its type-aware pass - # resolves types from mobile/node_modules. Without the install every mobile type - # degrades to an `error` type — reported as phantom findings against the changed lines. - - uses: ./.github/actions/install-mobile-dependencies - if: needs.code_paths.outputs.static_analysis == 'true' && needs.code_paths.outputs.mobile_dependencies == 'true' - - - name: Enforce changed-code quality - if: '!cancelled()' - id: changed-code-quality - background: true - env: - PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} - PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} - CHANGED_COUNT: ${{ needs.code_paths.outputs.changed_count }} - run: | - if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - # Strict changed-line scans cannot finish on a sync-sized diff; baseline checks still run. - if [ "$CHANGED_COUNT" -gt 500 ]; then - echo "Skipping changed-code quality: $CHANGED_COUNT changed files exceeds the 500-file limit for strict changed-line scans. Baseline checks remain enabled." - exit 0 - fi - node config/scripts/check-changed-code-quality.mjs "${{ github.event.pull_request.base.sha }}" - - - name: Verify locked audit tools - if: needs.code_paths.outputs.static_analysis == 'true' - env: - ORCA_BACKGROUND_LAUNCH: '1' - run: node config/scripts/verify-locked-audit-tools.mjs - - - name: Enforce React Doctor on changed lines - if: needs.code_paths.outputs.static_analysis == 'true' - env: - CHANGED_COUNT: ${{ needs.code_paths.outputs.changed_count }} - run: | - if [ "$CHANGED_COUNT" -gt 500 ]; then - echo "Skipping React Doctor: $CHANGED_COUNT changed files exceeds the 500-file limit for strict changed-line scans. Baseline checks remain enabled." - exit 0 - fi - node config/scripts/check-react-doctor-changed.mjs "${{ github.event.pull_request.base.sha }}" - - - wait: changed-code-quality - # The compiler overlaps the remaining checks after the type-aware passes release memory. - name: Typecheck if: '!cancelled()' @@ -360,149 +201,6 @@ jobs: if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi pnpm run typecheck:ci --bun-concurrency=2 --bun-threads=2 - - name: Check Zustand selector fan-out budget - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/zustand-selector-fanout-benchmark.mjs --check - - - name: Check reliability gate manifest - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/check-reliability-gates.mjs - - - name: Enforce dead design-system classes - if: needs.code_paths.outputs.static_analysis == 'true' - 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' - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - run: | - DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")" - if git diff --quiet "$DIFF_BASE" HEAD -- \ - src/shared/ephemeral-vm-runtime-store.ts \ - src/shared/ephemeral-vm-runtime-feature-store.ts \ - src/shared/ephemeral-vm-runtime-rollback-projection.ts \ - src/shared/ephemeral-vm-runtimes.ts \ - src/shared/ephemeral-vm-recipes.ts \ - src/shared/orca-yaml-hook-types.ts \ - src/main/ephemeral-vm-runtime-service.ts \ - src/main/ephemeral-vm-runtime-provisioning-persistence.ts \ - src/main/ephemeral-vm-failed-start-cleanup.ts; then - echo "VM runtime persistence is unchanged." - exit 0 - fi - # The repro pins upstream commits that live only on upstream branches it - # never merged, so a fork clone has no such object and git archive aborts. - # Public repo, so the credential-less checkout can still fetch them by sha. - repro=config/scripts/run-ephemeral-vm-runtime-store-rollback-repro.mjs - for sha in $(grep -oE "_COMMIT = '[0-9a-f]{40}'" "$repro" | grep -oE '[0-9a-f]{40}'); do - git cat-file -e "$sha^{commit}" 2>/dev/null || git fetch --no-tags origin "$sha" - done - node "$repro" config/scripts/ephemeral-vm-runtime-store-cross-version.test.ts - - - name: Enforce max-lines ratchet - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/check-max-lines-ratchet.mjs - - - name: Enforce ts-nocheck ratchet - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/check-ts-nocheck-ratchet.mjs - - - name: Enforce runtime Electron-import ratchet - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/check-runtime-electron-ratchet.mjs - - - name: Enforce focus-read ratchet - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/check-focus-read-ratchet.mjs - - - name: Enforce layout-writer ratchets - if: needs.code_paths.outputs.static_analysis == 'true' - run: bun config/scripts/check-layout-writer-ratchets.mjs - - - name: Enforce deprecated runtime-owner importer ratchet - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/check-deprecated-runtime-owner-ratchet.mjs - - - name: Enforce path-literal ratchet - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/check-path-literal-ratchet.mjs - - - name: Enforce worktree-id composer ratchet - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/check-worktree-id-composer-ratchet.mjs - - - name: Enforce e2e typecheck ratchet - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/check-e2e-typecheck-ratchet.mjs - - - name: Enforce legacy relay fence ratchet - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/check-legacy-relay-fence-ratchet.mjs - - - name: Check Node runtime pin - if: needs.code_paths.outputs.static_analysis == 'true' - 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. - # Use preflight's pinned Bun even when the optional local pin differs. - - name: Verify localization extraction - if: '!cancelled()' - id: localization-extraction - background: true - env: - PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }} - PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }} - BASE_SHA: ${{ github.event.pull_request.base.sha }} - run: | - if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi - # Detection failures run the full check; renames retain the removed input path. - DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")" - if git diff --name-only --no-renames -z "$DIFF_BASE" HEAD > "$RUNNER_TEMP/localization-changes" && - scope="$(node config/scripts/localization-extraction-change-scope.mjs "$RUNNER_TEMP/localization-changes")" && [ "$scope" = false ]; then - echo "Localization extraction inputs are unchanged." - else - bun config/scripts/fork-localization-catalog-check.mjs --verify-extraction - fi - - - name: Decide whether the orcad terminal smoke runs - id: orcad-smoke-scope - if: needs.code_paths.outputs.static_analysis == 'true' - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - run: | - # Detection failures run the smoke; renames retain the removed input path. - DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")" - if git diff --name-only --no-renames -z "$DIFF_BASE" HEAD > "$RUNNER_TEMP/orcad-smoke-changes" && - scope="$(node config/scripts/orcad-terminal-smoke-change-scope.mjs "$RUNNER_TEMP/orcad-smoke-changes")" && [ "$scope" = false ]; then - echo "Orcad terminal smoke inputs are unchanged." - echo 'run=false' >> "$GITHUB_OUTPUT" - else - echo 'run=true' >> "$GITHUB_OUTPUT" - fi - - # orcad refuses to build without the page phones load. - - uses: ./.github/actions/build-mobile-web-bundle - if: steps.orcad-smoke-scope.outputs.run == 'true' - - # Why both: the ratchet proves nothing reachable from the runtime imports electron, - # which is a property of the import graph. This proves the Node artifact it enables - # actually boots, pairs, creates a worktree and round-trips a real PTY. - - name: Boot orcad and round-trip a terminal - if: steps.orcad-smoke-scope.outputs.run == 'true' - env: - ORCA_BACKGROUND_LAUNCH: '1' - run: pnpm run smoke:orcad-terminal - - - name: Verify the generated RPC params catalog - if: needs.code_paths.outputs.static_analysis == 'true' - 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: node config/scripts/acp/generate-protocol.mjs --check - - name: Verify bundled skill guides if: needs.code_paths.outputs.static_analysis == 'true' run: node config/scripts/generate-bundled-skill-guides.mjs --check @@ -522,37 +220,7 @@ jobs: if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi bun config/scripts/fork-localization-catalog-check.mjs && bun config/scripts/generate-runtime-required-english-catalog.mjs - - name: Verify localization coverage - if: needs.code_paths.outputs.static_analysis == 'true' - run: bun config/scripts/audit-localization-coverage.mjs --check - - - wait: [localization-catalogs, localization-extraction] - - # Why: project-owned type declarations must live in .ts so tsc - # actually checks them. TypeScript's skipLibCheck: true (inherited - # from @electron-toolkit/tsconfig) silently widens unresolved names - # in .d.ts to `any`, which is how #1186 shipped a broken IPC signature - # past typecheck. See .github/CONTRIBUTING.md#type-declarations-prefer-ts-over-dts. - - name: Guard against project-owned .d.ts in preload/shared - if: needs.code_paths.outputs.static_analysis == 'true' - run: | - matches=$(find src/preload src/shared -name '*.d.ts' 2>/dev/null || true) - if [ -n "$matches" ]; then - echo "::error::Project-owned .d.ts files are not allowed under src/preload or src/shared." - echo "Move type declarations into a .ts file so skipLibCheck does not hide errors." - echo "See .github/CONTRIBUTING.md#type-declarations-prefer-ts-over-dts." - echo "Found:" - echo "$matches" - exit 1 - fi - - - name: Check feature wall asset budget - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/check-feature-wall-assets.mjs - - - name: Verify macOS entitlements - if: needs.code_paths.outputs.static_analysis == 'true' - run: node config/scripts/verify-macos-entitlements.mjs + - wait: localization-catalogs # Planning shares setup and stays off the compiler's critical path. - name: Plan unit selection @@ -571,10 +239,6 @@ jobs: - wait: typecheck - - wait: process-host-imports - - - wait: main-sync-spawn-ratchet - - wait: unit-plan - uses: actions/upload-artifact@v7 @@ -600,18 +264,6 @@ jobs: config/scripts/vitest-node-runtime-boundary.test.ts config/scripts/vitest-sqlite-runtime-boundary.test.ts - # Report failure through the separate required check without blocking unrelated consumers. - - name: Smoke process-host package on Node 18 - id: managed_hook_node18 - if: success() && needs.code_paths.outputs.managed_hook_node18 == 'true' - continue-on-error: true - env: - NODE18_EXECUTABLE: ${{ steps.node18_executable.outputs.executable }} - ORCA_BACKGROUND_LAUNCH: '1' - run: | - test -n "$NODE18_EXECUTABLE" - "$NODE18_EXECUTABLE" config/scripts/smoke-process-host-node18.mjs - fork_ownership_guard: if: github.event_name != 'pull_request' || github.event.pull_request.draft != true name: fork ownership guard @@ -634,342 +286,6 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: node .github/scripts/check-fork-ownership.mjs "$BASE_SHA" "$HEAD_SHA" - git_compatibility: - name: Git compatibility - needs: [code_paths, preflight] - if: >- - !cancelled() && - needs.code_paths.result == 'success' && - needs.code_paths.outputs.git_compatibility == 'true' && - needs.preflight.result == 'success' - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: false - - - uses: ./.github/actions/install-node-dependencies - - - uses: ./.github/actions/prepare-git-compatibility - - - name: Verify Git binary compatibility matrix - env: - ORCA_BACKGROUND_LAUNCH: '1' - run: | - specs=( - "alpine/git:edge-2.38.1|2.38.1" - "alpine/git:v2.49.1|2.49.1" - ) - # Why pull up front: a lane's first `docker run` otherwise pulls its image - # while the sibling lane is mid-test, and that stall is charged to the test. - for spec in "${specs[@]}"; do - docker pull --quiet "${spec%%|*}" - done - - pids=() - ( - ORCA_GIT_COMPAT_BINARY="$HOME/.cache/orca-git-compat/git-2.25.5/git" \ - GIT_EXEC_PATH="$HOME/.cache/orca-git-compat/git-2.25.5" \ - ORCA_GIT_COMPAT_VERSION="2.25.5" \ - pnpm exec vitest run --config config/vitest.config.ts \ - src/shared/git-binary-compatibility.test.ts \ - src/main/git/worktree-safety-real-git.test.ts \ - src/main/git/worktree-rebase-update-refs-real-git.test.ts - ) & - pids+=("$!") - - for spec in "${specs[@]}"; do - ( - image="${spec%%|*}" - version="${spec#*|}" - ORCA_GIT_COMPAT_IMAGE="$image" ORCA_GIT_COMPAT_VERSION="$version" \ - pnpm exec vitest run --config config/vitest.config.ts \ - src/shared/git-binary-compatibility.test.ts \ - src/main/git/worktree-safety-real-git.test.ts \ - src/main/git/worktree-rebase-update-refs-real-git.test.ts - ) & - pids+=("$!") - done - - status=0 - for pid in "${pids[@]}"; do - wait "$pid" || status=1 - done - exit "$status" - - # Why this job: Orca's session index-heal depends on a Codex behavior — a - # `thread/read` of an unindexed rollout performs a read-repair that inserts the - # `threads` row. Every unit test drives a stub app-server and asserts only that the - # call did not error, so if Codex dropped the repair they would all stay green while - # the subsystem went inert. This runs the pinned real binary and fails when the - # repair stops happening. Pinned because the binary is the thing expected to drift. - codex_index_heal_contract: - name: Codex index-heal contract - needs: [code_paths, preflight] - if: >- - !cancelled() && - needs.code_paths.result == 'success' && - needs.code_paths.outputs.codex_index_heal_contract == 'true' && - needs.preflight.result == 'success' - # Why ARM: @openai/codex ships @openai/codex-linux-arm64. - runs-on: ubuntu-24.04-arm - env: - CODEX_CLI_VERSION: '0.150.1' - # Why a second pin: --no-daemon only exists from 0.156, and Orca's codex wrapper relies on it. - CODEX_NO_DAEMON_CLI_VERSION: '0.158.0' - - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: false - - - uses: ./.github/actions/install-node-dependencies - - - name: Install pinned Codex CLI - run: | - set -euo pipefail - npm install --no-audit --no-fund --prefix "$RUNNER_TEMP/codex-cli" \ - "@openai/codex@$CODEX_CLI_VERSION" - - - name: Verify Codex index-heal contract - env: - # Why REQUIRED: without a binary the suite skips, and a job that skips - # reports success. This turns a failed or missing install into a red test - # instead of a green no-op. - ORCA_CODEX_CONTRACT_REQUIRED: '1' - ORCA_CODEX_CONTRACT_VERSION: ${{ env.CODEX_CLI_VERSION }} - run: | - set -euo pipefail - ORCA_CODEX_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli/node_modules/.bin/codex" \ - pnpm exec vitest run --config config/vitest.config.ts \ - src/main/codex/codex-index-heal-binary-contract.test.ts - - - name: Install pinned no-daemon Codex CLI - run: | - set -euo pipefail - npm install --no-audit --no-fund --prefix "$RUNNER_TEMP/codex-cli-no-daemon" \ - "@openai/codex@$CODEX_NO_DAEMON_CLI_VERSION" - - - name: Verify Codex --no-daemon contract - env: - ORCA_CODEX_NO_DAEMON_CONTRACT_REQUIRED: '1' - ORCA_CODEX_NO_DAEMON_CONTRACT_VERSION: ${{ env.CODEX_NO_DAEMON_CLI_VERSION }} - run: | - set -euo pipefail - ORCA_CODEX_NO_DAEMON_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli-no-daemon/node_modules/.bin/codex" \ - pnpm exec vitest run --config config/vitest.config.ts \ - src/main/pty/codex-no-daemon-binary-contract.test.ts - - # Why both pins and latest: Orca approves its managed-home hook entry with a hash - # Codex computes, so a release that changes it must redden here before users - # meet a review screen. - - name: Verify Codex hook file-entry contract (pinned) - env: - ORCA_CODEX_HOOK_CONTRACT_REQUIRED: '1' - run: | - set -euo pipefail - for install in codex-cli:"$CODEX_CLI_VERSION" codex-cli-no-daemon:"$CODEX_NO_DAEMON_CLI_VERSION"; do - ORCA_CODEX_HOOK_CONTRACT_BINARY="$RUNNER_TEMP/${install%%:*}/node_modules/.bin/codex" \ - ORCA_CODEX_HOOK_CONTRACT_VERSION="${install#*:}" \ - pnpm exec vitest run --config config/vitest.config.ts \ - src/main/codex/codex-hook-file-entry-binary-contract.test.ts - done - - - name: Install latest Codex CLI - run: | - set -euo pipefail - npm install --no-audit --no-fund --prefix "$RUNNER_TEMP/codex-cli-latest" "@openai/codex@latest" - - - name: Verify Codex hook file-entry contract (latest) - env: - ORCA_CODEX_HOOK_CONTRACT_REQUIRED: '1' - run: | - set -euo pipefail - ORCA_CODEX_HOOK_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli-latest/node_modules/.bin/codex" \ - pnpm exec vitest run --config config/vitest.config.ts \ - src/main/codex/codex-hook-file-entry-binary-contract.test.ts - - # Why the no-daemon install: it is the newest pinned Codex, and its project - # lookup decides whether a worktree launch stops at the trust prompt. - - name: Verify Codex project-trust contract - env: - ORCA_CODEX_TRUST_CONTRACT_REQUIRED: '1' - ORCA_CODEX_TRUST_CONTRACT_VERSION: ${{ env.CODEX_NO_DAEMON_CLI_VERSION }} - run: | - set -euo pipefail - ORCA_CODEX_TRUST_CONTRACT_BINARY="$RUNNER_TEMP/codex-cli-no-daemon/node_modules/.bin/codex" \ - pnpm exec vitest run --config config/vitest.config.ts \ - src/main/agent-trust-presets.test.ts - - xterm_patch_sync: - name: xterm patch sync - needs: [code_paths, preflight] - if: >- - !cancelled() && - needs.code_paths.result == 'success' && - needs.code_paths.outputs.xterm_patch_sync == 'true' && - needs.preflight.result == 'success' - # Why ARM: the patch check rebuilds 4 packages x 2 builds and byte-compares against the - # checked-in bundles. Those were generated on Linux x64 and reproduce byte-for-byte on - # darwin-arm64, so the output is neither host-arch nor host-OS dependent. - runs-on: ubuntu-24.04-arm - - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: false - - # The generator uses Node built-ins and installs its own upstream toolchain. - - uses: actions/setup-node@v6 - with: - node-version-file: package.json - package-manager-cache: false - - # Rebuild both pristine and patched bundles; reuse only the pinned toolchain. - - uses: ./.github/actions/prepare-xterm-dependencies - - - name: Verify xterm patches match the pinned upstream build - env: - WORK_DIR: ${{ runner.temp }}/xterm-patch-build - run: node config/scripts/regenerate-xterm-patches.mjs --check --work-dir="$WORK_DIR" - - shell_contracts: - name: shell contracts - needs: [code_paths, preflight] - if: >- - !cancelled() && - needs.code_paths.result == 'success' && - needs.code_paths.outputs.shell_contracts == 'true' && - needs.preflight.result == 'success' - # Why ARM: fish 4.x is published for noble/arm64 and zsh is in the arm64 archive. - runs-on: ubuntu-24.04-arm - # Why: this job's cost is almost entirely package download, and a stalled mirror has - # no wall-clock bound of its own. A successful run finishes in ~4.5 minutes, so this - # is generous; it exists so a wedge fails the job instead of holding the whole run - # open for the 6h GitHub default — which also blocks `gh run rerun --failed`. - timeout-minutes: 15 - env: - # Why: the suites below gate their live fish tests on the binary, which is - # right on a developer machine and wrong here — this job is a required check - # and its fish lane is the only end-to-end guard for #9993, so a skip would - # report green with nothing exercised. Turns those skips into failures. - ORCA_REQUIRE_FISH: '1' - # Why: the runner image ships pwsh, and the codex wrapper's PowerShell lane must not skip. - ORCA_REQUIRE_PWSH: '1' - - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: false - - # Why fish: shell-ready.test.ts gates its live fish test on the binary being - # present, so without this the fish barrier is only covered by config-shape - # assertions and never actually exercised. - # Why release-4: DECSET 2031 arming lives in the fish 4.0 Rust tty_handoff, and - # fish-color-scheme-child-stdin.node-pty.test.ts (#9993) needs it. Noble ships - # 3.7, so the PPA is what makes that lane real. - - name: Install zsh and fish - id: shells - background: true - run: | - # Why the update/PPA/fish steps are tolerant: a repo the runner image already - # ships can lack a Release file for this suite, and a failed add-apt-repository - # still leaves its list entry behind — either makes `apt-get update` exit - # non-zero and would red this required check over something unrelated to the - # PR. Every fish outcome is judged by the version gate below instead, so only - # the zsh install (which has no such gate) stays fatal here. - # Why retry only here: adding the PPA is the network-flaky step, and the - # version gate below is fatal, so a transient Launchpad blip would - # otherwise red a required check on PRs unrelated to shells. - # Why -n: add-apt-repository refreshes every configured repo on its own. With - # an update on each side of it this step refreshed them three times over, and - # the Azure archive mirror alone costs ~15-30s a pass. The PPA index is the - # only thing the repo list gains here, and the single update below fetches it. - # Why bound acquisition: measured on a *passing* run, this step spent 40s - # fetching 11.4 MB of index and then 2m17s fetching 8.9 MB of packages at - # 65 kB/s — it is dominated by download throughput, not by work. apt applies - # no wall-clock bound to a stalled mirror, so a slow Launchpad or archive - # host wedges the step for tens of minutes. This job is a required check, so - # a wedge holds the entire run open and blocks `gh run rerun --failed`. - # Bounded timeouts plus retries turn an unbounded hang into a fast, legible - # failure. Set in apt.conf.d rather than on each command line so the two - # invocations below stay exactly as pr-workflow-parallelism.test.mjs parses - # them. Retries are 1, not 3: a first attempt at these bounds already multiplied - # 30s x 3 retries across every index file into a ~15 minute stall on a dead - # mirror, which is worse than failing once and moving on. - sudo tee /etc/apt/apt.conf.d/99-orca-shell-contracts >/dev/null <<'APTCONF' - Acquire::http::Timeout "15"; - Acquire::https::Timeout "15"; - Acquire::Retries "1"; - APTCONF - for attempt in 1 2 3; do - sudo add-apt-repository -y -n ppa:fish-shell/release-4 && break - echo "add-apt-repository attempt ${attempt} failed; retrying" >&2 - sudo add-apt-repository -y -n -r ppa:fish-shell/release-4 || true - sleep 5 - done - # Why a wall-clock bound on each command: apt's Acquire timeouts are per-connection, - # so a dead mirror costs timeout x retries x every index file. Measured: the archive - # mirror stalled with zero bytes and the step burned 14m26s before the job bound - # killed it. `timeout` is the only thing that bounds the command as a whole. - # The update is already tolerant by design (see above), so bounding it just caps - # what a dead mirror can cost before the install runs against whatever index exists. - timeout 120 sudo apt-get update || true - # Why both shells on one line: pr-workflow-parallelism.test.mjs parses only the - # first install command in this step to prove the lane really installs them. - timeout 300 sudo apt-get install -y zsh fish - - - uses: ./.github/actions/install-node-dependencies - with: - native-runtime: node - - - wait: shells - - # Separate from the install so the failure names the contract, not an apt error. - # ORCA_REQUIRE_FISH re-checks this at test time; this step just fails in seconds - # instead of after a full dependency install. - - name: Require fish 4+ - run: | - version="$(fish --version 2>/dev/null || true)" - major="${version##*version }" - major="${major%%.*}" - case "$major" in '' | *[!0-9]*) major=0 ;; esac - echo "${version:-}" - if [ "$major" -lt 4 ]; then - echo "::error::shell contracts needs fish 4+ (DECSET 2031 arming, #9993) but got '${version:-none}'. Fix the ppa:fish-shell/release-4 install rather than letting the fish lane skip." >&2 - exit 1 - fi - - - name: Test real shell contracts - run: | - pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ - src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts \ - src/main/daemon/shell-ready.test.ts \ - src/main/daemon/node-pty-fd-leak.test.ts \ - src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts \ - src/main/providers/__tests__/shell-ready-framework-example.test.ts \ - src/main/pty/codex-shell-launch-preflight.test.ts \ - src/main/pty/codex-shell-no-daemon.test.ts \ - src/main/pty/omp-shell-wrapper-alias-safety.test.ts \ - src/main/pty/omp-shell-wrapper.node-pty.test.ts \ - src/main/fish-xdg-data-dirs-handoff.test.ts \ - src/main/shell-startup-feature-channel.test.ts \ - src/main/zsh-deferred-startup-line-init.live-shell.test.ts \ - src/main/terminal-history-fish-session.node-pty.test.ts \ - src/main/zsh-scoped-histfile.live-shell.test.ts \ - src/main/zsh-startup-hook-user-config-equivalence.live-shell.test.ts \ - src/main/runtime/structured-session-cli-login-shell.live-shell.test.ts \ - src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \ - src/shared/fish-query-reply-child-stdin.node-pty.test.ts \ - src/shared/pty-reply-echo-shapes.node-pty.test.ts \ - src/shared/startup-shell-portability.live-shell.test.ts \ - src/shared/posix-command-path-lookup.test.ts - # Preflight saves the Node cache and publishes the plan before tests fan out. test: needs: [code_paths, preflight] @@ -986,7 +302,6 @@ jobs: phase_diagnostics: true shards: ${{ needs.preflight.outputs.shards }} run_shards: ${{ needs.code_paths.outputs.mobile_ci_tools != 'true' }} - browser_contract: ${{ needs.code_paths.outputs.orcad_browser == 'true' }} # Why a sibling and not part of the test workflow: it is advisory, so it must not delay the # gate. Inside unit-tests.yml a caller's `needs: test` waited for it, holding verify ~36s @@ -996,84 +311,6 @@ jobs: if: ${{ !cancelled() && (needs.test.result == 'success' || needs.test.result == 'failure') }} uses: ./.github/workflows/unit-selection-evidence.yml - # Why its own job: it needs mobile/node_modules and a real browser, and the sharded `test` - # matrix would pay for both on every shard to run two files. It builds the same bundle the - # package job ships, and adds the render and census checks packaging does not run. - mobile_web_app: - name: mobile web app bundle - needs: [code_paths] - if: needs.code_paths.outputs.mobile_web_app == 'true' - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: false - - # Why no native-runtime: the builder is esbuild and the render check is a browser. Nothing - # in this job loads node-pty. - - uses: ./.github/actions/install-node-dependencies - with: - native-runtime: none - cache-dependency-path: | - pnpm-lock.yaml - mobile/pnpm-lock.yaml - - # The drawer check runs on WebKit as well as Chrome, because the shell's iOS WebView is - # WebKit and the Chrome above cannot stand in for it. Downloaded rather than resolved from - # the runner: Ubuntu ships no WebKit build to point at. - - name: Install WebKit for the drawer check - id: webkit - background: true - run: pnpm exec playwright-core install --with-deps webkit - - # The entry lives in mobile/ so one React resolves; without this every RN import is nothing. - - uses: ./.github/actions/install-mobile-dependencies - - - name: Prepare mobile route snapshot - id: mobile-routes - background: true - run: node config/scripts/run-mobile-web-app-checks.mjs --prepare-route-snapshot "$RUNNER_TEMP/mobile-routes.json" - - # Why the runner's Google Chrome and not a downloaded chromium: same reason as the orcad - # browser job -- Ubuntu 24.04 only ships an AppArmor userns profile for the Chrome .deb. - # Why fail instead of skip: a silently skipped render check is the failure this job exists - # to prevent. - - name: Resolve Chrome for the render check - run: | - set -euo pipefail - chrome="$(command -v google-chrome || command -v google-chrome-stable || true)" - if [ -z "$chrome" ]; then - echo "::error::No Google Chrome on the runner; the render check would silently skip." - exit 1 - fi - "$chrome" --version - echo "ORCA_MOBILE_WEB_RENDER_BROWSER=$chrome" >> "$GITHUB_ENV" - - - name: Build and verify the app bundle - run: pnpm run build:mobile-web - - # Browser checks must observe both a finished install and the built bundle. - - wait: [webkit, mobile-routes] - - # The bundling tests skip themselves where mobile dependencies are absent, which is how they - # stay green in the sharded `test` job. This is the job that installs them, so here a missing - # install has to fail rather than skip everything the job exists to run. - # Why a prefix and not a file list: the list this replaces had gone stale twice without - # anyone noticing, because a census whose closure block skips without the env flag below is - # green in the sharded `test` job whether or not it ever runs here. The prefix is the same - # one `pr-code-change-scope.mjs` fires this job on, so naming a test into the family is all - # it takes to have it run. Quoted because these are vitest filename filters, matched as - # substrings against the discovered files, and the shell must not touch them. - # Route censuses share fresh dependency lists for this invocation; scratch builds stay independent. - - name: Builder, override census and render checks - env: - ORCA_MOBILE_WEB_APP_DEPS_REQUIRED: '1' - ORCA_MOBILE_WEB_PREPARED_ROUTE_SNAPSHOT: ${{ runner.temp }}/mobile-routes.json - run: | - node config/scripts/run-mobile-web-app-checks.mjs - cross-version-wire: name: cross-version wire compatibility needs: [code_paths, preflight] @@ -1130,289 +367,6 @@ jobs: - name: Old/new client and server compatibility journeys run: pnpm exec vitest run --config config/vitest.config.ts tests/e2e/cross-version-wire/ - managed_hook_node18: - name: managed hooks on Node 18 - needs: [code_paths, preflight] - if: >- - !cancelled() && - needs.code_paths.result == 'success' && - needs.code_paths.outputs.managed_hook_node18 == 'true' && - needs.preflight.result == 'success' - # Retain the required check while sharing preflight's checkout and built packages. - runs-on: ubuntu-slim - timeout-minutes: 5 - - steps: - - name: Require process-host package smoke on Node 18 - env: - MANAGED_HOOK_NODE18: ${{ needs.preflight.outputs.managed_hook_node18 }} - run: | - if [ "$MANAGED_HOOK_NODE18" != success ]; then - echo "::error::Node 18 process-host smoke: expected success, got $MANAGED_HOOK_NODE18" - exit 1 - fi - - package: - name: package - needs: [code_paths, preflight] - if: needs.code_paths.outputs.package == 'true' - runs-on: ubuntu-latest - # Let the serial Docker gates reach their own deadlines and report cleanup failures. - timeout-minutes: 90 - - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - persist-credentials: false - - - name: Cache electron-builder downloads - uses: actions/cache/restore@v5 - with: - path: ~/.cache/electron-builder - key: electron-builder-linux-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - electron-builder-linux- - - - uses: ./.github/actions/prepare-linux-package-fixture - id: daemon-fixture-cache - background: true - with: - fixture: daemon-shutdown-descendants - - - uses: ./.github/actions/install-node-dependencies - with: - native-runtime: electron - cache-dependency-path: | - pnpm-lock.yaml - mobile/pnpm-lock.yaml - - # Why here: electron-builder's beforePack requires out/mobile-web, and the bundle - # build resolves React Native and Expo from mobile/node_modules. - - uses: ./.github/actions/install-mobile-dependencies - - # The checkout must pass without depending on a historical baseline that leaks. - - wait: daemon-fixture-cache - - - name: Verify Linux daemon shutdown descendant cleanup - env: - ORCA_BACKGROUND_LAUNCH: '1' - ORCA_DAEMON_SHUTDOWN_FIXTURE_CACHE_IMAGE: ${{ steps.daemon-fixture-cache.outputs.image }} - run: node config/scripts/run-daemon-shutdown-descendants-docker.mjs - - # Why --no-file-parallelism: every file here launches a full Electron stack twice, and each - # probe carries its own in-process deadline. Four at once on a 4-vCPU runner starve each other - # past those deadlines; serial, every probe owns the runner. - - name: Test Linux Electron lifecycle boundary - run: >- - xvfb-run --auto-servernum pnpm exec vitest run --config config/vitest.config.ts - --no-file-parallelism - 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 - - # 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 - background: true - with: - fixture: headless-serve-shutdown - - - uses: ./.github/actions/prepare-linux-package-fixture - id: cli-fixture-cache - background: true - with: - fixture: cli-launch-contract - - - name: Install Linux package tooling - id: linux-package-tools - background: true - run: | - # Use the shell lane's bounded refresh; required tooling installation stays fatal. - sudo tee /etc/apt/apt.conf.d/99-orca-package-tools >/dev/null <<'APTCONF' - Acquire::http::Timeout "15"; - Acquire::https::Timeout "15"; - Acquire::Retries "1"; - APTCONF - timeout 120 sudo apt-get update || true - timeout 300 sudo apt-get install -y cpio rpm - - - name: Build package inputs - env: - ORCA_CI_MAIN_SOURCEMAPS: '0' - run: | - status=0 - pnpm run build:cli || status=1 - - scripts=(build:wsl-guest build:electron-vite:parallel) - pids=() - for script in "${scripts[@]}"; do - pnpm run "$script" & - pids+=("$!") - done - - for pid in "${pids[@]}"; do - wait "$pid" || status=1 - done - exit "$status" - - - name: Project web client from renderer build - id: web-client - background: true - run: pnpm run build:web-from-renderer - - # Why here and not inside "Build package inputs": this job assembles packaging inputs step by - # step instead of calling build:release, and electron-builder's beforePack guard hard-fails - # without out/mobile-web. This is the real page, ~8 MB, not a bootstrap document. - - name: Build mobile web bundle - run: pnpm run build:mobile-web - - - wait: [linux-native, linux-package-tools, web-client] - - - name: Package unpacked app - env: - ORCA_BACKGROUND_LAUNCH: '1' - ORCA_REUSE_PREPARED_NATIVE_RUNTIME: '1' - # Prepare once with every hook, then isolate the format-specific mutations. - run: | - pnpm exec electron-builder --config config/electron-builder.config.cjs --linux dir --x64 --publish never - node config/scripts/package-linux-formats.mjs - - - name: Verify root-package marker payloads - run: | - set -euo pipefail - version="$(node -p "require('./package.json').version")" - deb="dist/orca-ide_${version}_amd64.deb" - rpm="dist/orca-ide-${version}.x86_64.rpm" - test -s "$deb" - test -s "$rpm" - # Globbed, not '/opt/Orca': the install prefix is productName, and this fork ships 'Orca Dev'. - deb_marker="$(dpkg-deb --fsys-tarfile "$deb" | tar -xOf - --wildcards './opt/*/resources/package-type')" - rpm_marker="$(rpm2cpio "$rpm" | cpio --quiet --extract --to-stdout './opt/*/resources/package-type')" - [[ "$deb_marker" == deb ]] || { echo "Expected deb marker, got: $deb_marker"; exit 1; } - [[ "$rpm_marker" == rpm ]] || { echo "Expected rpm marker, got: $rpm_marker"; exit 1; } - # Only the AppImage carries the pinned Node (config/scripts/appimage-node-runtime.cjs). - if dpkg-deb --fsys-tarfile "$deb" | tar -t | grep '/resources/node-runtime' >/dev/null; then - echo "The deb must not carry the AppImage's Node"; exit 1 - fi - if rpm2cpio "$rpm" | cpio --quiet -t | grep '/resources/node-runtime' >/dev/null; then - echo "The rpm must not carry the AppImage's Node"; exit 1 - fi - appimage_root="$(mktemp -d)" - (cd "$appimage_root" && "$GITHUB_WORKSPACE/dist/orca-linux.AppImage" --appimage-extract 'resources/node-runtime/bin/node' >/dev/null) - test -x "$appimage_root/squashfs-root/resources/node-runtime/bin/node" - "$appimage_root/squashfs-root/resources/node-runtime/bin/node" --version - rm -rf "$appimage_root" - ls -l dist/orca-linux.AppImage "$deb" "$rpm" - - # Kept briefly so a reviewer can run this exact AppImage on a real Linux desktop. - - uses: actions/upload-artifact@v7 - with: - name: orca-linux-appimage-${{ github.event.pull_request.head.sha }} - path: dist/orca-linux.AppImage - retention-days: 3 - compression-level: 0 - - - wait: shutdown-fixture-cache - - - name: Verify headless serve signal shutdown - env: - ORCA_SHUTDOWN_FIXTURE_CACHE_IMAGE: ${{ steps.shutdown-fixture-cache.outputs.image }} - run: >- - node config/scripts/run-headless-serve-shutdown-docker.mjs - --appimage dist/orca-linux.AppImage --all-entrypoints - - # A default container reproduces the hostile AppImage launch environment. - - wait: cli-fixture-cache - - - name: Verify Linux CLI launch contract - env: - ORCA_CLI_FIXTURE_CACHE_IMAGE: ${{ steps.cli-fixture-cache.outputs.image }} - run: node config/scripts/run-linux-cli-launch-contract-docker.mjs --appimage dist/orca-linux.AppImage - - - name: Smoke packaged CLI - run: node config/scripts/smoke-packaged-cli.mjs --app-dir=dist/linux-unpacked - - - name: Smoke packaged hang watchdog worker - run: xvfb-run --auto-servernum node config/scripts/smoke-packaged-hang-watchdog-worker.mjs --app-dir=dist/linux-unpacked - - e2e: - name: e2e - needs: [code_paths, preflight] - if: >- - !cancelled() && - needs.code_paths.result == 'success' && - needs.code_paths.outputs.e2e_should_run == 'true' && - ( - needs.preflight.result == 'success' || - ( - needs.preflight.result == 'skipped' && - needs.code_paths.outputs.static_analysis == 'false' && - needs.code_paths.outputs.typecheck == 'false' - ) - ) - # Why: reusable e2e.yml only checkouts, builds, and uploads artifacts. - permissions: - contents: read - uses: ./.github/workflows/e2e.yml - with: - # The synthetic pull-request merge ref can disappear while this reusable - # workflow is queued. The head SHA is immutable and works for every PR. - ref: ${{ github.event.pull_request.head.sha }} - test_files: ${{ needs.code_paths.outputs.test_files }} - run_changed_e2e: ${{ needs.code_paths.outputs.e2e_run_changed != 'false' }} - needs_build: ${{ needs.code_paths.outputs.e2e_needs_build != 'false' }} - - # Why its own required job, apart from `e2e`: the rest of the suite is red on main, but the - # layout oracle is green there, and its known-on-main list absorbs today's findings. Only the two - # oracle specs run, through the same reusable workflow: the window spec in the changed-spec lane, - # the headless spec in the orcad lane on both serve hosts. - layout_oracle: - name: layout oracle - needs: [code_paths, preflight] - if: >- - !cancelled() && - needs.code_paths.result == 'success' && - needs.code_paths.outputs.layout_oracle == 'true' && - needs.preflight.result == 'success' - permissions: - contents: read - uses: ./.github/workflows/e2e.yml - with: - # Why the merge commit, unlike `e2e`: a gate must test what lands on main, not a stale head. - ref: ${{ github.sha }} - test_files: '["tests/e2e/workspace-layout-oracle-headless.spec.ts","tests/e2e/workspace-layout-oracle.spec.ts"]' - run_changed_e2e: true - needs_build: true - artifact_suffix: -layout-oracle - - # Why this is not in verify's needs: it is the first PR-gate run of a harness whose reliability - # is only known from nightly main runs (20/20 green, 2026-08-09..2026-08-29, p50 3m25s). It - # reports a red X on the PR without blocking, exactly like `e2e` above. Deliberately no - # continue-on-error: that renders the check green and hides the signal it exists to give. To - # make it blocking, add it to verify.needs, add TERMINAL_IME_NATIVE to the env below, and - # require `success || skipped` outside the strict loop — see the note on `e2e`. - terminal_ime_native: - name: real IME - needs: code_paths - if: needs.code_paths.outputs.native_ime_source_changed == 'true' - # Why: the reusable workflow only checks out, builds, and uploads artifacts. - permissions: - contents: read - uses: ./.github/workflows/terminal-ime-e2e.yml - verify: name: PR checks passed # Publish an explicit result after every required dependency settles. @@ -1421,29 +375,13 @@ jobs: - code_paths - preflight - fork_ownership_guard - - git_compatibility - - codex_index_heal_contract - - xterm_patch_sync - - shell_contracts - test - - mobile_web_app - cross-version-wire - - managed_hook_node18 - - package - - layout_oracle # Evaluating job results only needs the lightweight container runner. runs-on: ubuntu-slim timeout-minutes: 5 steps: - # Why: e2e is deliberately absent from needs. The suite is currently red on - # main (every scheduled run), so gating merges on it would block any PR that - # touches tests/e2e/** — including the ones fixing the suite. Until it is - # green the job runs and reports for E2E-path PRs without blocking. To flip - # it on: add `e2e` to needs, add E2E to the env below, and require - # `"$E2E" = success || skipped` after the loop — skipped is the normal - # result for a path-filtered job and must keep passing, so it has to be - # checked outside the loop or it would excuse the jobs above. - name: Require successful checks env: CODE_PATHS: ${{ needs.code_paths.result }} @@ -1453,26 +391,10 @@ jobs: TYPECHECK_SHOULD_RUN: ${{ needs.code_paths.outputs.typecheck }} PREFLIGHT_SHOULD_RUN: ${{ needs.code_paths.outputs.static_analysis == 'true' || needs.code_paths.outputs.typecheck == 'true' }} FORK_OWNERSHIP_GUARD: ${{ needs.fork_ownership_guard.result }} - GIT_COMPATIBILITY: ${{ needs.git_compatibility.result }} - GIT_COMPATIBILITY_SHOULD_RUN: ${{ needs.code_paths.outputs.git_compatibility }} - CODEX_INDEX_HEAL_CONTRACT: ${{ needs.codex_index_heal_contract.result }} - CODEX_INDEX_HEAL_CONTRACT_SHOULD_RUN: ${{ needs.code_paths.outputs.codex_index_heal_contract }} - XTERM_PATCH_SYNC: ${{ needs.xterm_patch_sync.result }} - XTERM_PATCH_SYNC_SHOULD_RUN: ${{ needs.code_paths.outputs.xterm_patch_sync }} - SHELL_CONTRACTS: ${{ needs.shell_contracts.result }} - SHELL_CONTRACTS_SHOULD_RUN: ${{ needs.code_paths.outputs.shell_contracts }} TEST: ${{ needs.test.result }} TEST_SHOULD_RUN: ${{ needs.code_paths.outputs.test }} - MOBILE_WEB_APP: ${{ needs.mobile_web_app.result }} - MOBILE_WEB_APP_SHOULD_RUN: ${{ needs.code_paths.outputs.mobile_web_app }} CROSS_VERSION_WIRE: ${{ needs.cross-version-wire.result }} CROSS_VERSION_WIRE_SHOULD_RUN: ${{ needs.code_paths.outputs.cross-version-wire }} - MANAGED_HOOK_NODE18: ${{ needs.managed_hook_node18.result }} - MANAGED_HOOK_NODE18_SHOULD_RUN: ${{ needs.code_paths.outputs.managed_hook_node18 }} - PACKAGE: ${{ needs.package.result }} - PACKAGE_SHOULD_RUN: ${{ needs.code_paths.outputs.package }} - LAYOUT_ORACLE: ${{ needs.layout_oracle.result }} - LAYOUT_ORACLE_SHOULD_RUN: ${{ needs.code_paths.outputs.layout_oracle }} run: | if [ "$CODE_PATHS" != "success" ]; then exit 1 @@ -1510,14 +432,6 @@ jobs: } # Require success when the PR has code-relevant changes check_job preflight "$PREFLIGHT" "$PREFLIGHT_SHOULD_RUN" - check_job git_compatibility "$GIT_COMPATIBILITY" "$GIT_COMPATIBILITY_SHOULD_RUN" - check_job codex_index_heal_contract "$CODEX_INDEX_HEAL_CONTRACT" "$CODEX_INDEX_HEAL_CONTRACT_SHOULD_RUN" - check_job xterm_patch_sync "$XTERM_PATCH_SYNC" "$XTERM_PATCH_SYNC_SHOULD_RUN" - check_job shell_contracts "$SHELL_CONTRACTS" "$SHELL_CONTRACTS_SHOULD_RUN" check_job test "$TEST" "$TEST_SHOULD_RUN" - check_job mobile_web_app "$MOBILE_WEB_APP" "$MOBILE_WEB_APP_SHOULD_RUN" check_job cross-version-wire "$CROSS_VERSION_WIRE" "$CROSS_VERSION_WIRE_SHOULD_RUN" - check_job managed_hook_node18 "$MANAGED_HOOK_NODE18" "$MANAGED_HOOK_NODE18_SHOULD_RUN" - check_job package "$PACKAGE" "$PACKAGE_SHOULD_RUN" - check_job layout_oracle "$LAYOUT_ORACLE" "$LAYOUT_ORACLE_SHOULD_RUN" exit "$failed" From 14aea115d848462c89368fd41a9956dec41241aa Mon Sep 17 00:00:00 2001 From: zpyoung Date: Mon, 19 Oct 2026 15:20:52 -0500 Subject: [PATCH 2/3] test(ci): drop upstream contract tests for the removed PR jobs They assert jobs and preflight steps the trimmed pr.yml no longer runs. Declared as deleted permanent exceptions so syncs keep them out. --- .github/workflows/pr.yml | 1 - config/fork-ownership.json | 95 ++- .../scripts/check-node-runtime-pin.test.mjs | 196 ------ config/scripts/ci-e2e-job-selection.test.mjs | 293 --------- .../ci-unit-exclusion-coverage.test.mjs | 89 --- .../pr-workflow-contract.mjs | 24 - ...alization-extraction-change-scope.test.mjs | 119 ---- ...orcad-terminal-smoke-change-scope.test.mjs | 116 ---- .../pr-auxiliary-preflight-admission.test.mjs | 492 -------------- config/scripts/pr-code-change-scope.test.mjs | 622 ------------------ config/scripts/pr-e2e-gate-contract.test.mjs | 300 --------- .../scripts/pr-e2e-route-contracts.test.mjs | 534 --------------- config/scripts/pr-preflight-gates.test.mjs | 437 ------------ config/scripts/pr-ready-check-reuse.test.mjs | 177 ----- .../scripts/pr-workflow-parallelism.test.mjs | 80 --- 15 files changed, 63 insertions(+), 3512 deletions(-) delete mode 100644 config/scripts/check-node-runtime-pin.test.mjs delete mode 100644 config/scripts/ci-e2e-job-selection.test.mjs delete mode 100644 config/scripts/ci-unit-exclusion-coverage.test.mjs delete mode 100644 config/scripts/fork-ownership-guard/pr-workflow-contract.mjs delete mode 100644 config/scripts/localization-extraction-change-scope.test.mjs delete mode 100644 config/scripts/orcad-terminal-smoke-change-scope.test.mjs delete mode 100644 config/scripts/pr-auxiliary-preflight-admission.test.mjs delete mode 100644 config/scripts/pr-code-change-scope.test.mjs delete mode 100644 config/scripts/pr-e2e-gate-contract.test.mjs delete mode 100644 config/scripts/pr-e2e-route-contracts.test.mjs delete mode 100644 config/scripts/pr-preflight-gates.test.mjs delete mode 100644 config/scripts/pr-ready-check-reuse.test.mjs delete mode 100644 config/scripts/pr-workflow-parallelism.test.mjs diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d25f19083987..3e33da6cf1b4 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -258,7 +258,6 @@ jobs: --maxWorkers=4 --isolate --retry=0 --bail=0 config/scripts/mobile-release-check-scope.test.mjs config/scripts/mobile-test-change-scope.test.mjs - config/scripts/pr-ready-check-reuse.test.mjs config/scripts/ci-dependency-download-cache.test.mjs config/scripts/win32-test-lane-registration.test.mjs config/scripts/vitest-node-runtime-boundary.test.ts diff --git a/config/fork-ownership.json b/config/fork-ownership.json index 8ad8d8cc219e..12c05f77ac6f 100644 --- a/config/fork-ownership.json +++ b/config/fork-ownership.json @@ -854,16 +854,6 @@ " ...forkGuardNeeds()," ] }, - { - "path": "config/scripts/pr-ready-check-reuse.test.mjs", - "feature": "fork-ownership-guard", - "kind": "import-swap", - "lines": [ - "} from './fork-ownership-guard/pr-workflow-contract.mjs'", - "const workflow = workflowWithoutForkGuard(parse(readFileSync('.github/workflows/pr.yml', 'utf8')))", - " for (const name of ['test_files', 'native_ime_source_changed']) {" - ] - }, { "path": "src/main/ipc/native-chat-subscribe-lifecycle.test.ts", "feature": "native-chat-relay", @@ -5117,14 +5107,6 @@ " if (isTerminalUserInputFromKeyboard(session.pane.terminal)) {" ] }, - { - "path": "config/scripts/localization-extraction-change-scope.test.mjs", - "feature": "fork-infra", - "kind": "passthrough", - "lines": [ - " /else\\s+bun config\\/scripts\\/fork-localization-catalog-check\\.mjs --verify-extraction\\s+fi/" - ] - }, { "path": "config/scripts/benchmark-ci-preflight-critical-path.mjs", "feature": "fork-benchmark-baseline", @@ -5922,13 +5904,15 @@ }, { "path": "config/scripts/pr-code-change-scope.test.mjs", - "reason": "Retains upstream classifier behavior while dropping source-pinning assertions for the Windows PR job absent from the fork-owned workflow, and asserts the draft guard on code_paths (the tag's version asserts none and fails against the tag's own pr.yml).", - "status": "permanent" + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true }, { "path": "config/scripts/pr-workflow-parallelism.test.mjs", - "reason": "Contracts for the fork-owned PR job graph, which omits Windows packaging and must not restore upstream's exact aggregate dependency list.", - "status": "permanent" + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true }, { "path": "config/scripts/skills-cli-package-workflow.test.mjs", @@ -6317,10 +6301,65 @@ "reason": "Contracts for the fork-owned PR job graph: this fork removed the Windows PR packaging lane, so the case asserts its removal instead of the lane's Node-ABI save ordering.", "status": "permanent" }, + { + "path": "config/scripts/check-node-runtime-pin.test.mjs", + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true + }, + { + "path": "config/scripts/ci-e2e-job-selection.test.mjs", + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true + }, + { + "path": "config/scripts/ci-unit-exclusion-coverage.test.mjs", + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true + }, + { + "path": "config/scripts/localization-extraction-change-scope.test.mjs", + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true + }, + { + "path": "config/scripts/orcad-terminal-smoke-change-scope.test.mjs", + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true + }, + { + "path": "config/scripts/pr-auxiliary-preflight-admission.test.mjs", + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true + }, + { + "path": "config/scripts/pr-e2e-gate-contract.test.mjs", + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true + }, + { + "path": "config/scripts/pr-e2e-route-contracts.test.mjs", + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true + }, + { + "path": "config/scripts/pr-ready-check-reuse.test.mjs", + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true + }, { "path": "config/scripts/pr-preflight-gates.test.mjs", - "reason": "Contracts for the fork-owned PR job graph: this fork removed the Windows PR packaging lane, so its preflight gating is asserted absent.", - "status": "permanent" + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true }, { "path": "src/shared/rpc-contract/rpc-params-catalog.generated.ts", @@ -8497,10 +8536,6 @@ "added": 12, "removed": 2 }, - "config/scripts/pr-ready-check-reuse.test.mjs": { - "added": 6, - "removed": 3 - }, "config/scripts/check-path-literal-ratchet.mjs": { "added": 5, "removed": 1 @@ -8509,10 +8544,6 @@ "added": 2, "removed": 1 }, - "config/scripts/localization-extraction-change-scope.test.mjs": { - "added": 3, - "removed": 1 - }, "config/scripts/benchmark-ci-preflight-critical-path.mjs": { "added": 27, "removed": 3 diff --git a/config/scripts/check-node-runtime-pin.test.mjs b/config/scripts/check-node-runtime-pin.test.mjs deleted file mode 100644 index 885bbc397768..000000000000 --- a/config/scripts/check-node-runtime-pin.test.mjs +++ /dev/null @@ -1,196 +0,0 @@ -import { readFileSync } from 'node:fs' -import path from 'node:path' -import { describe, expect, it } from 'vitest' -import { parse } from 'yaml' -import { - NODE_RUNTIME_ASSETS, - NODE_RUNTIME_PIN, - SERVER_TARGETS -} from '../../src/shared/node-runtime-pin.ts' -import { - findNodeRuntimePinProblems, - lockfileRootImporter, - main -} from './check-node-runtime-pin.mjs' - -const projectDir = path.resolve(import.meta.dirname, '../..') -const HASH = 'a'.repeat(64) - -function validInput() { - const pin = { - version: '24.21.0', - electron: '43.7.5', - napi: 10, - headers: { file: 'node-v24.21.0-headers.tar.gz', sha256: HASH }, - windowsImportLibs: { 'win32-x64': { file: 'win-x64/node.lib', sha256: HASH } } - } - const targets = ['linux-x64-glibc', 'win32-x64'] - const assets = { - 'linux-x64-glibc': { - source: 'official', - archive: 'node-v24.21.0-linux-x64.tar.gz', - archiveSha256: HASH, - executableSha256: HASH, - executableSize: 1 - }, - 'win32-x64': { - source: 'official', - archive: 'node-v24.21.0-win-x64.zip', - archiveSha256: HASH, - executableSha256: HASH, - executableSize: 1 - } - } - return { - pin, - assets, - targets, - packageJson: { devDependencies: { electron: '43.7.5' }, engines: { node: '24' } }, - rootImporter: { - devDependencies: { - electron: { specifier: '43.7.5', version: '43.7.5(supports-color@7.2.0)' } - } - } - } -} - -describe('findNodeRuntimePinProblems', () => { - it('accepts a consistent pin', () => { - expect(findNodeRuntimePinProblems(validInput())).toEqual([]) - }) - - it('rejects an Electron bump that the pin did not follow', () => { - const input = validInput() - input.packageJson.devDependencies.electron = '43.8.0' - input.rootImporter.devDependencies.electron.version = '43.8.0' - expect(findNodeRuntimePinProblems(input)).toEqual([ - 'package.json electron is 43.8.0, but NODE_RUNTIME_PIN.electron is 43.7.5', - 'pnpm-lock.yaml resolves electron 43.8.0, but NODE_RUNTIME_PIN.electron is 43.7.5' - ]) - }) - - it('rejects a lockfile that resolves a different Electron than package.json', () => { - const input = validInput() - input.rootImporter.devDependencies.electron.version = '43.7.6' - expect(findNodeRuntimePinProblems(input)).toEqual([ - 'pnpm-lock.yaml resolves electron 43.7.6, but NODE_RUNTIME_PIN.electron is 43.7.5' - ]) - }) - - it('rejects a pin major that differs from engines.node', () => { - const input = validInput() - input.packageJson.engines.node = '>=26' - expect(findNodeRuntimePinProblems(input)).toEqual([ - 'NODE_RUNTIME_PIN.version 24.21.0 is not package.json engines.node major 26' - ]) - }) - - it('requires exactly one asset per server target', () => { - const input = validInput() - delete input.assets['win32-x64'] - input.assets['freebsd-x64'] = input.assets['linux-x64-glibc'] - expect(findNodeRuntimePinProblems(input)).toEqual([ - 'NODE_RUNTIME_ASSETS has no entry for win32-x64', - 'NODE_RUNTIME_ASSETS has freebsd-x64, which is not in SERVER_TARGETS' - ]) - }) - - it('rejects malformed hashes, sizes, sources and stale archive names', () => { - const input = validInput() - input.pin.headers.sha256 = 'ABC' - input.assets['linux-x64-glibc'] = { - source: 'mirror', - archive: 'node-v24.20.0-linux-x64.tar.gz', - archiveSha256: HASH.toUpperCase(), - executableSha256: `${HASH}0`, - executableSize: 0 - } - expect(findNodeRuntimePinProblems(input)).toEqual([ - 'NODE_RUNTIME_PIN.headers.sha256 is not a 64-character hex SHA-256', - 'linux-x64-glibc: source must be official or unofficial, got mirror', - 'linux-x64-glibc: archive node-v24.20.0-linux-x64.tar.gz is not node-v24.21.0-linux-x64.tar.gz', - 'linux-x64-glibc: archiveSha256 is not a 64-character hex SHA-256', - 'linux-x64-glibc: executableSha256 is not a 64-character hex SHA-256', - 'linux-x64-glibc: executableSize must be a positive integer' - ]) - }) - it('rejects a missing or mistargeted Windows node.lib', () => { - const input = validInput() - input.pin.windowsImportLibs['win32-x64'] = { file: 'win-arm64/node.lib', sha256: 'x' } - expect(findNodeRuntimePinProblems(input)).toEqual([ - 'NODE_RUNTIME_PIN.windowsImportLibs.win32-x64.file is not win-x64/node.lib', - 'NODE_RUNTIME_PIN.windowsImportLibs.win32-x64.sha256 is not a 64-character hex SHA-256' - ]) - }) - it('checks the compat table against its own target list', () => { - const input = validInput() - input.compatTargets = ['linux-x64-glibc217'] - input.compatAssets = { - 'linux-x64-glibc217': { - source: 'unofficial', - archive: 'node-v24.21.0-linux-x64.tar.gz', - archiveSha256: HASH, - executableSha256: HASH, - executableSize: 1 - }, - 'linux-x64-glibc': input.assets['linux-x64-glibc'] - } - expect(findNodeRuntimePinProblems(input)).toEqual([ - 'linux-x64-glibc217: archive node-v24.21.0-linux-x64.tar.gz is not node-v24.21.0-linux-x64-glibc-217.tar.gz', - 'NODE_RUNTIME_COMPAT_ASSETS has linux-x64-glibc, which is not in COMPAT_SERVER_TARGETS' - ]) - delete input.compatAssets['linux-x64-glibc217'] - expect(findNodeRuntimePinProblems(input)).toContain( - 'NODE_RUNTIME_COMPAT_ASSETS has no entry for linux-x64-glibc217' - ) - }) - it("rejects another target's archive", () => { - const input = validInput() - input.assets['win32-x64'].archive = 'node-v24.21.0-win-arm64.zip' - expect(findNodeRuntimePinProblems(input)).toEqual([ - 'win32-x64: archive node-v24.21.0-win-arm64.zip is not node-v24.21.0-win-x64.zip' - ]) - }) -}) - -describe('lockfileRootImporter', () => { - it('merges the root importer across pnpm 12 lockfile documents', () => { - const contents = [ - '---', - "lockfileVersion: '9.0'", - 'importers:', - ' .:', - ' packageManagerDependencies: {}', - '---', - "lockfileVersion: '9.0'", - 'importers:', - ' .:', - ' devDependencies:', - ' electron:', - ' specifier: 43.7.5', - ' version: 43.7.5(supports-color@7.2.0)', - '' - ].join('\n') - expect(lockfileRootImporter(contents).devDependencies.electron.version).toBe( - '43.7.5(supports-color@7.2.0)' - ) - }) -}) - -describe('committed pin', () => { - it('passes the repository check', () => { - expect(main(projectDir)).toBe(0) - }) - - it('covers every server target', () => { - expect(Object.keys(NODE_RUNTIME_ASSETS).sort()).toEqual([...SERVER_TARGETS].sort()) - expect(NODE_RUNTIME_PIN.headers.file).toBe(`node-v${NODE_RUNTIME_PIN.version}-headers.tar.gz`) - }) - - 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(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 deleted file mode 100644 index 9a217768bca2..000000000000 --- a/config/scripts/ci-e2e-job-selection.test.mjs +++ /dev/null @@ -1,293 +0,0 @@ -import { readFileSync } from 'node:fs' -import { expect, it } from 'vitest' -import { parse } from 'yaml' -import { runProcess } from '@orca/process-host' -import { - classifyE2eJobs, - DEDICATED_E2E_SPECS, - MANAGED_SSH_E2E_SPEC_PREFIX, - NATIVE_IME_E2E_SPEC, - NODE_NETWORK_E2E_SPEC, - selectGeneralE2eSpecs -} from './ci-e2e-job-selection.mjs' -import { selectPrE2eSpecs } from './pr-e2e-source-routing.mjs' - -it.each([ - 'src/main/runtime/runtime-browser-commands-factory.ts', - 'src/main/runtime/orca-runtime-get-status.ts', - 'src/main/orcad/orcad-browser-startup.ts' -])('routes the managed browser capability oracle from %s', (path) => { - expect(selectPrE2eSpecs([path])).toContain('tests/e2e/ssh-orcad-browser-capabilities.spec.ts') -}) - -it.each(['errors', 'status', 'lifecycle', 'restart-attempt'])( - 'routes the real browser service status check from %s changes', - (name) => { - expect( - selectPrE2eSpecs([ - `src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-${name}.ts` - ]) - ).toContain('tests/e2e/ssh-orcad-browser-service-status.spec.ts') - } -) - -it.each([ - 'src/renderer/src/lib/ssh-workspace-browser-route-eligibility.ts', - 'src/renderer/src/components/browser-pane/use-ssh-workspace-browser-route.ts', - 'src/main/browser/local-ssh-browser-route.ts', - 'src/renderer/src/store/repos/converted-ssh-browser-pages.ts', - 'src/renderer/src/store/slices/browser/browser-tab-actions.ts', - 'src/renderer/src/hooks/ipc-events/ssh-managed-server-state-effects.ts' -])('routes the managed browser routing oracle from %s', (path) => { - expect(selectPrE2eSpecs([path])).toContain('tests/e2e/ssh-orcad-browser-routing.spec.ts') -}) - -it.each([ - 'src/renderer/src/lib/workspace-browser-tab-open.ts', - 'src/renderer/src/lib/http-link-destinations.ts', - 'src/renderer/src/components/terminal-pane/terminal-http-link-source-owner.ts' -])('routes the managed terminal link oracle from %s', (path) => { - expect(selectPrE2eSpecs([path])).toContain('tests/e2e/ssh-orcad-terminal-browser-link.spec.ts') -}) - -it.each([ - 'src/shared/desktop-browser-bridge-protocol.ts', - 'src/main/runtime/desktop-browser-bridge/desktop-browser-bridge-route.ts', - 'src/main/browser/desktop-browser-bridge/desktop-browser-bridge-executor.ts', - 'src/main/ipc/desktop-browser-bridge-wiring.ts', - 'src/main/runtime/rpc/methods/desktop-browser-bridge.ts' -])('routes the managed desktop browser bridge oracle from %s', (path) => { - expect(selectPrE2eSpecs([path])).toContain('tests/e2e/ssh-orcad-desktop-browser-bridge.spec.ts') -}) - -const workflow = parse(readFileSync('.github/workflows/e2e.yml', 'utf8')) -const prWorkflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) -const classify = (specs) => classifyE2eJobs(JSON.stringify(specs)) - -function expectManagedSshLane(spec) { - expect(classify([spec])).toEqual({ e2e_run_changed: false, e2e_needs_build: true }) - expect(spec.startsWith(MANAGED_SSH_E2E_SPEC_PREFIX)).toBe(true) - const lane = workflow.jobs['orcad-ssh-docker'] - expect(lane.if).toContain(`contains(inputs.test_files, '${MANAGED_SSH_E2E_SPEC_PREFIX}')`) - expect( - lane.steps.some( - (step) => - step.run?.includes(` ${MANAGED_SSH_E2E_SPEC_PREFIX} `) && - step.env?.ORCA_E2E_SSH_DOCKER === '1' - ) - ).toBe(true) -} - -it('runs real browser file-drop ownership in the template-building Docker job', () => { - const spec = 'tests/e2e/ssh-orcad-browser-drop-owner.spec.ts' - expectManagedSshLane(spec) - for (const source of [ - 'src/renderer/src/lib/file-preview.ts', - 'src/renderer/src/lib/workspace-file-drag.ts', - 'src/renderer/src/components/browser-pane/navigate/use-browser-page-navigation-downloads.ts', - 'src/renderer/src/components/right-sidebar/FileExplorerRow.tsx', - 'tests/e2e/helpers/browser-split-guest-probes.ts' - ]) { - expect(selectPrE2eSpecs([source]), source).toContain(spec) - } -}) - -it('skips the general consumer only when every requested spec has a dedicated owner', () => { - for (const spec of DEDICATED_E2E_SPECS) { - expect(classify([spec]).e2e_run_changed, spec).toBe(false) - } - expect(classify(DEDICATED_E2E_SPECS).e2e_run_changed).toBe(false) - const future = 'tests/e2e/future-unclassified.spec.ts' - expect(classify([future])).toEqual({ - e2e_run_changed: true, - e2e_needs_build: true - }) - expect(selectGeneralE2eSpecs([...DEDICATED_E2E_SPECS, future])).toEqual([future]) - expect(classify([...DEDICATED_E2E_SPECS, future]).e2e_run_changed).toBe(true) -}) - -it('keeps Electron build and native prerequisites for every consumer requiring them', () => { - for (const spec of [NODE_NETWORK_E2E_SPEC, NATIVE_IME_E2E_SPEC]) { - expect(classify([spec]), spec).toEqual({ - e2e_run_changed: false, - e2e_needs_build: false - }) - } - expect(workflow.jobs['ssh-browser-network-route'].needs).toBeUndefined() - for (const name of ['e2e', 'changed-e2e']) { - expect(workflow.jobs[name].needs, name).toEqual(['build']) - } -}) - -it('retains allocations when selection evidence is incomplete', () => { - for (const input of ['', '[]', 'null', '{}', '[null]', '[""]', 'malformed']) { - expect(classifyE2eJobs(input), input).toEqual({ - e2e_run_changed: true, - e2e_needs_build: true - }) - } -}) - -it('preserves the requested specs across source-routed and mixed selections', () => { - for (const files of [ - ['src/main/browser/ssh-browser-network-execution-route.ts'], - ['src/shared/terminal-unicode-provider.ts'], - [NODE_NETWORK_E2E_SPEC, 'tests/e2e/future-unclassified.spec.ts'] - ]) { - const specs = selectPrE2eSpecs(files) - const general = selectGeneralE2eSpecs(specs) - const dedicated = specs.filter((spec) => DEDICATED_E2E_SPECS.includes(spec)) - expect([...general, ...dedicated].sort(), files.join(', ')).toEqual(specs) - expect(new Set([...general, ...dedicated]).size).toBe(specs.length) - } -}) - -it('applies allocation hints only to PRs and retains other callers and full references', () => { - for (const name of ['run_changed_e2e', 'needs_build']) { - expect(workflow.on.workflow_call.inputs[name]).toMatchObject({ - type: 'boolean', - required: false, - default: true - }) - } - 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") - const command = changed.steps.find((step) => step.name === 'Run changed E2E specs').run - expect(command).toContain('node config/scripts/ci-e2e-job-selection.mjs >') - expect(command).not.toContain('mapfile -t TEST_FILES < <(') - const fallback = [...command.matchAll(/\. != "([^"]+)"/g)].map((match) => match[1]) - expect(fallback).toEqual(DEDICATED_E2E_SPECS) - expect(command).toContain(`(startswith("${MANAGED_SSH_E2E_SPEC_PREFIX}") | not)`) -}) - -it('publishes conservative hints for malformed evidence and refuses a malformed consumer list', async () => { - const program = 'config/scripts/ci-e2e-job-selection.mjs' - const consumer = await runProcess({ - program: process.execPath, - args: [program], - input: 'malformed', - timeoutMs: 10000 - }) - expect(consumer.code).not.toBe(0) - const hints = await runProcess({ - program: process.execPath, - args: [program, '--job-outputs'], - input: 'malformed', - timeoutMs: 10000 - }) - expect(hints.code, hints.stderr).toBe(0) - expect(hints.stdout).toBe('e2e_run_changed=true\ne2e_needs_build=true\n') -}) - -it('classifies PR consumers in the existing detector and passes conservative allocation hints', () => { - const detector = prWorkflow.jobs.code_paths - expect(detector.steps[0].with['sparse-checkout']).toContain( - '/config/scripts/ci-e2e-job-selection.mjs' - ) - for (const name of ['e2e_run_changed', 'e2e_needs_build']) { - expect(detector.outputs[name]).toBe(`\${{ steps.e2e_filter.outputs.${name} }}`) - } - const command = detector.steps.find((step) => step.id === 'e2e_filter').run - expect(command).toContain('ci-e2e-job-selection.mjs --job-outputs >> "$GITHUB_OUTPUT"') - expect(prWorkflow.jobs.e2e.with.run_changed_e2e).toBe( - "${{ needs.code_paths.outputs.e2e_run_changed != 'false' }}" - ) - expect(prWorkflow.jobs.e2e.with.needs_build).toBe( - "${{ needs.code_paths.outputs.e2e_needs_build != 'false' }}" - ) -}) - -it.each([ - 'src/renderer/src/runtime/web-session-tabs-sync/mirrored-editor-file-identity.ts', - 'src/renderer/src/runtime/web-session-tabs-sync/tab-builders.ts', - 'src/renderer/src/runtime/web-session-tabs-sync/apply-preparation-browser.ts', - 'src/renderer/src/runtime/web-session-existing-tab-index.ts' -])('routes %s to the template-building editor ownership lane', (file) => { - const spec = 'tests/e2e/ssh-orcad-editor-ownership.spec.ts' - expect(selectPrE2eSpecs([file])).toContain(spec) - expectManagedSshLane(spec) -}) - -const MARKDOWN_CONVERSION_SPEC = 'tests/e2e/ssh-orcad-markdown-conversion.spec.ts' -const MARKDOWN_LINK_REFRESH_SPEC = 'tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts' -const MARKDOWN_LIVE_DOCUMENTS_SPEC = 'tests/e2e/ssh-orcad-markdown-live-documents.spec.ts' - -it.each([ - ['src/renderer/src/components/editor/useMarkdownDocuments.ts', MARKDOWN_CONVERSION_SPEC], - [ - 'src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.ts', - MARKDOWN_CONVERSION_SPEC - ], - [ - 'src/renderer/src/components/editor/migrate-restored-editor-file-owner.ts', - MARKDOWN_CONVERSION_SPEC - ], - ['src/renderer/src/components/editor/rich-markdown-doc-link.ts', MARKDOWN_LINK_REFRESH_SPEC], - [ - 'src/renderer/src/components/editor/useRichMarkdownProgrammaticSync.ts', - MARKDOWN_LINK_REFRESH_SPEC - ], - ['src/renderer/src/components/editor/useMarkdownDocuments.ts', MARKDOWN_LIVE_DOCUMENTS_SPEC], - [ - 'src/renderer/src/components/editor/use-markdown-document-watch-refresh.ts', - MARKDOWN_LIVE_DOCUMENTS_SPEC - ], - [ - 'src/renderer/src/components/editor/markdown-document-list-request.ts', - MARKDOWN_LIVE_DOCUMENTS_SPEC - ] -])('routes %s to the template-building Markdown lane %s', (file, spec) => { - expect(selectPrE2eSpecs([file])).toContain(spec) - expectManagedSshLane(spec) -}) - -it.each([ - 'src/renderer/src/hooks/useEditorExternalWatch.ts', - 'src/renderer/src/hooks/editor-runtime-file-watch.ts' -])('routes %s to the template-building editor watch recovery lane', (file) => { - const spec = 'tests/e2e/ssh-orcad-editor-watch-recovery.spec.ts' - expect(selectPrE2eSpecs([file])).toContain(spec) - expectManagedSshLane(spec) -}) - -it.each([ - 'src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts', - 'src/renderer/src/hooks/worktree-file-change-event.ts' -])('routes %s to the template-building explorer watch recovery lane', (file) => { - const spec = 'tests/e2e/ssh-orcad-explorer-watch-recovery.spec.ts' - expect(selectPrE2eSpecs([file])).toContain(spec) - expectManagedSshLane(spec) -}) - -it.each([ - 'src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts', - 'src/renderer/src/components/right-sidebar/file-explorer-operation-owner.ts', - 'src/renderer/src/components/right-sidebar/FileExplorer.tsx', - 'src/renderer/src/hooks/editor-external-watch-targets.ts', - 'src/renderer/src/components/right-sidebar/useFileExplorerTree.ts', - 'src/renderer/src/components/right-sidebar/use-file-explorer-tree-load-effects.ts' -])('routes %s to the template-building selected-host explorer lane', (file) => { - const spec = 'tests/e2e/ssh-orcad-explorer-selected-host.spec.ts' - expect(selectPrE2eSpecs([file])).toContain(spec) - expectManagedSshLane(spec) -}) - -it('runs managed terminal root ownership in the Docker conversion lane', () => { - const spec = 'tests/e2e/ssh-orcad-terminal-root-owner.spec.ts' - expectManagedSshLane(spec) - for (const source of [ - 'src/renderer/src/components/terminal-pane/terminal-worktree-path-link.ts', - 'src/renderer/src/components/terminal-pane/terminal-file-open-routing.ts', - 'src/renderer/src/components/terminal-pane/terminal-file-link-actions.ts', - 'src/renderer/src/components/terminal-pane/terminal-file-link-hit-testing.ts', - 'src/renderer/src/components/terminal-pane/terminal-link-handlers.ts', - 'src/renderer/src/lib/worktree-owner-route.ts', - 'tests/e2e/helpers/terminal-workspace-root-link.ts' - ]) { - expect(selectPrE2eSpecs([source]), source).toContain(spec) - } -}) diff --git a/config/scripts/ci-unit-exclusion-coverage.test.mjs b/config/scripts/ci-unit-exclusion-coverage.test.mjs deleted file mode 100644 index 9061b5723a6c..000000000000 --- a/config/scripts/ci-unit-exclusion-coverage.test.mjs +++ /dev/null @@ -1,89 +0,0 @@ -import { globSync, readFileSync } from 'node:fs' -import { parse } from 'yaml' -import { defaultExclude } from 'vitest/config' -import { describe, expect, it } from 'vitest' -import { CROSS_VERSION_WIRE_DIR, UNIT_INCLUDE, discoverUnitFiles } from './ci-unit-files.mjs' -import { classifyPrJobs } from './pr-code-change-scope.mjs' - -function posixPaths(files) { - return files.map((file) => file.replaceAll('\\', '/')) -} - -function workflowJobs(path) { - return parse(readFileSync(path, 'utf8')).jobs -} - -// Why verify.needs: a job left out of it (e2e, ime, wsl) can be red or skipped without blocking a merge. -// Each gating job carries the code_paths output that decides whether it runs; a called workflow's jobs inherit the caller's. -function gatingJobs() { - const jobs = workflowJobs('.github/workflows/pr.yml') - return jobs.verify.needs.flatMap((name) => { - const { if: condition = '', uses } = jobs[name] - const gate = /needs\.code_paths\.outputs\.([\w-]+) == 'true'/.exec(condition)?.[1] - const called = uses?.startsWith('./.github/workflows/') - ? Object.values(workflowJobs(uses.slice(2))) - : [jobs[name]] - return called.map((job) => ({ gate, pathArguments: vitestPathArguments(job) })) - }) -} - -// Path arguments of every step in the job that runs vitest; a trailing slash means a directory. -function vitestPathArguments(job) { - return (job.steps ?? []) - .map((step) => step.run) - .filter((run) => typeof run === 'string' && /\bvitest\b/.test(run)) - .flatMap((run) => run.replaceAll('\\\n', ' ').split(/\s+/)) - .map((token) => token.replace(/^['"]|['"]$/g, '')) - .filter((token) => /^(?:src|config|tests|mobile)\//.test(token)) -} - -function runByVitestStep(file, pathArguments) { - return pathArguments.some( - (path) => path === file || (path.endsWith('/') && file.startsWith(path)) - ) -} - -// Why module scope: globbing the unit tree costs seconds, and every test reads the same result. -const UNIT_FILES = posixPaths(globSync(UNIT_INCLUDE, { exclude: defaultExclude })) -const SHARDED_UNIT_FILES = new Set(discoverUnitFiles()) -const EXCLUDED_UNIT_FILES = UNIT_FILES.filter((file) => !SHARDED_UNIT_FILES.has(file)) -const GATING_JOBS = gatingJobs() - -describe('unit files kept out of the sharded test job', () => { - it('each still runs in a job that gates the PR', () => { - expect(EXCLUDED_UNIT_FILES.length).toBeGreaterThan(0) - // Why: an excluded file no gating step names guards nothing, and nothing else reports it. - expect( - EXCLUDED_UNIT_FILES.filter( - (file) => !GATING_JOBS.some((job) => runByVitestStep(file, job.pathArguments)) - ) - ).toEqual([]) - }) - - it('a change to each runs a gating job that names it', () => { - // Why: otherwise a PR editing only the file skips the one job that runs it. - expect( - EXCLUDED_UNIT_FILES.filter( - (file) => - !GATING_JOBS.some( - ({ gate, pathArguments }) => - runByVitestStep(file, pathArguments) && (!gate || classifyPrJobs([file])[gate]) - ) - ) - ).toEqual([]) - }) - - it('runs the whole cross-version-wire directory, not a list of its files', () => { - expect(GATING_JOBS.flatMap((job) => job.pathArguments)).toContain(CROSS_VERSION_WIRE_DIR) - }) - - it('names every cross-version-wire test so the unit include picks it up', () => { - // Why: a directory argument only reaches files the vitest include matches; any other name is silently skipped. - const tests = posixPaths( - globSync(`${CROSS_VERSION_WIRE_DIR}**/*.{test,spec}.{js,cjs,mjs,ts,tsx}`) - ) - const included = new Set(UNIT_FILES) - expect(tests.length).toBeGreaterThan(0) - expect(tests.filter((file) => !included.has(file))).toEqual([]) - }) -}) diff --git a/config/scripts/fork-ownership-guard/pr-workflow-contract.mjs b/config/scripts/fork-ownership-guard/pr-workflow-contract.mjs deleted file mode 100644 index 32296064d3d5..000000000000 --- a/config/scripts/fork-ownership-guard/pr-workflow-contract.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import { readFileSync } from 'node:fs' -import { parse } from 'yaml' -import { PR_CHECK_JOBS as CLASSIFIED_PR_CHECK_JOBS } from '../pr-code-change-scope.mjs' -import { aggregateJobWithoutForkGuard, checkJobsForWorkflow } from './aggregate-gate-jobs.mjs' - -const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) -const aggregateShell = workflow.jobs.verify.steps.find( - (step) => step.name === 'Require successful checks' -) - -/** The classifier's jobs as the fork's PR workflow reports them. */ -export const PR_CHECK_JOBS = checkJobsForWorkflow( - CLASSIFIED_PR_CHECK_JOBS, - workflow, - aggregateShell -) - -/** The workflow as upstream's contract tests expect it: `verify` needs only the gated jobs. */ -export function workflowWithoutForkGuard(source) { - return { - ...source, - jobs: { ...source.jobs, verify: aggregateJobWithoutForkGuard(source.jobs.verify) } - } -} diff --git a/config/scripts/localization-extraction-change-scope.test.mjs b/config/scripts/localization-extraction-change-scope.test.mjs deleted file mode 100644 index 67dd7606a01f..000000000000 --- a/config/scripts/localization-extraction-change-scope.test.mjs +++ /dev/null @@ -1,119 +0,0 @@ -import { readFileSync } from 'node:fs' -import { expect, it } from 'vitest' -import { parse } from 'yaml' -import localizationConfig from '../i18next.config' -import { affectsLocalizationExtraction } from './localization-extraction-change-scope.mjs' - -it('pins the extractor inputs and exclusions used by the routing decision', () => { - expect(localizationConfig.extract.input).toEqual(['src/**/*.{js,jsx,ts,tsx,mts,cts}']) - expect(localizationConfig.extract.ignore).toEqual([ - '**/*.test.*', - '**/*.spec.*', - '**/__tests__/**', - '**/__snapshots__/**', - '**/assets/**' - ]) -}) - -it.each([ - 'src/renderer/src/components/Example.tsx', - 'src/main/notifications.ts', - 'src/renderer/src/i18n/locales/en.json', - 'config/i18next.config.ts', - 'config/scripts/verify-localization-extraction.mjs', - 'config/scripts/localization-extraction-change-scope.mjs', - 'config/patches/i18next-cli.patch', - 'package.json', - 'pnpm-lock.yaml', - 'pnpm-workspace.yaml', - 'tsconfig.json', - 'config/tsconfig.web.json', - '.npmrc', - '.pnpmfile.cjs', - '.github/workflows/pr.yml', - '.github/actions/install-node-dependencies/action.yml' -])('retains extraction when an input changes: %s', (path) => { - expect(affectsLocalizationExtraction([path])).toBe(true) -}) - -it.each([ - 'src/main/notifications.test.ts', - 'src/main/notifications.spec.ts', - 'src/renderer/src/components/Example.test.tsx', - 'src/main/example.test.js', - 'src/main/example.spec.jsx', - 'src/main/example.test.mts', - 'src/main/example.spec.cts', - 'src/.test.ts', - 'src/.spec.ts', - 'src/__tests__/notifications.ts', - 'src/main/__tests__/nested/notifications.ts', - 'src/main/__snapshots__/notifications.ts' -])('avoids extraction for an explicitly ignored test source: %s', (path) => { - expect(affectsLocalizationExtraction([path])).toBe(false) -}) - -it.each([ - 'src/main/notifications-test.ts', - 'src/main/notifications.test-support.ts', - 'src/main/notifications.specification.ts', - 'src/main/notifications.TEST.ts', - 'src/main/.test./notifications.ts', - 'src/main/notifications.test/notifications.ts', - 'src/main/__tests__-support/notifications.ts', - 'src/main/__snapshots__-support/notifications.ts', - 'src/main/__tests__.ts', - 'src/main/assets/notifications.ts', - 'src/main/__tests__/en.json', - 'src/renderer/src/i18n/locales/en.test.json', - 'src/main/notifications.test.unknown' -])('retains extraction for catalogs, assets and near-match source paths: %s', (path) => { - expect(affectsLocalizationExtraction([path])).toBe(true) -}) - -it('retains mixed changes and both sides of source-to-test renames', () => { - const ignored = 'src/main/notifications.test.ts' - expect(affectsLocalizationExtraction([ignored, 'src/main/notifications.ts'])).toBe(true) - expect(affectsLocalizationExtraction(['src/main/notifications.ts', ignored])).toBe(true) - expect(affectsLocalizationExtraction([ignored, 'config/i18next.config.ts'])).toBe(true) - expect(affectsLocalizationExtraction([ignored, 'src/renderer/src/i18n/locales/en.json'])).toBe( - true - ) - expect(affectsLocalizationExtraction([ignored, 'src/main/notifications.spec.ts'])).toBe(false) -}) - -it('avoids extraction for unrelated CI, documentation, and native changes', () => { - expect( - affectsLocalizationExtraction([ - '.github/workflows/e2e.yml', - 'config/scripts/run-terminal-scale-perf-e2e.mjs', - 'docs/reference/ci-runner-efficiency.md', - 'native/windows-registry/src/addon.cc' - ]) - ).toBe(false) -}) - -it('preserves deleted and renamed inputs and falls back to extraction on detection failure', () => { - const workflow = parse( - readFileSync(new URL('../../.github/workflows/pr.yml', import.meta.url), 'utf8') - ) - const step = workflow.jobs.preflight.steps.find( - (candidate) => candidate.name === 'Verify localization extraction' - ) - expect(step.env).toMatchObject({ - BASE_SHA: '${{ github.event.pull_request.base.sha }}' - }) - // The base side comes from the merge ref's first parent, so the gate needs no merge base and - // works on a shallow checkout. The payload head SHA is no longer read: HEAD is the merged tree - // the gate is actually deciding about. - expect(step.run).toContain('node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA"') - expect(step.run).toContain('git diff --name-only --no-renames -z "$DIFF_BASE" HEAD') - expect(step.run).not.toContain('--diff-filter') - expect(step.run).toContain('&& [ "$scope" = false ]; then') - expect(step.run).toMatch( - /else\s+bun config\/scripts\/fork-localization-catalog-check\.mjs --verify-extraction\s+fi/ - ) - expect( - affectsLocalizationExtraction(['src/old-translations.ts', 'docs/moved-translations.ts']) - ).toBe(true) -}) diff --git a/config/scripts/orcad-terminal-smoke-change-scope.test.mjs b/config/scripts/orcad-terminal-smoke-change-scope.test.mjs deleted file mode 100644 index ffd9402c3e85..000000000000 --- a/config/scripts/orcad-terminal-smoke-change-scope.test.mjs +++ /dev/null @@ -1,116 +0,0 @@ -import { readFileSync } from 'node:fs' -import { beforeAll, describe, expect, it } from 'vitest' -import { parse } from 'yaml' -import { - classifyOrcadTerminalSmokeChanges, - collectOrcadTerminalSmokeInputs -} from './orcad-terminal-smoke-change-scope.mjs' -import { ORCAD_CHILD_ENTRY_POINTS } from './orcad-entry-build.mjs' - -it.each([ - 'package.json', - 'pnpm-lock.yaml', - 'tsconfig.json', - 'config/tsconfig.cli.json', - 'config/patches/node-pty@1.1.0.patch', - 'native/windows-registry/src/addon.cc', - 'config/scripts/node-pty-job-ownership.cjs', - '.github/actions/install-node-dependencies/action.yml', - '.github/workflows/pr.yml', - 'src/cli/deleted-command.ts', - 'src/shared/deleted-contract.ts', - 'src/main/persistence/profile-state/new-worker.ts', - 'resources/licenses/ripgrep/COPYING', - 'config/scripts/runtime-serve-terminal-smoke.test.mjs', - 'config/scripts/orcad-terminal-smoke-change-scope.test.mjs' -])('retains the smoke for build and dynamically opened input changes: %s', async (file) => { - expect((await classifyOrcadTerminalSmokeChanges([file], async () => new Set())).shouldRun).toBe( - true - ) -}) - -it('retains smoke when changed-file evidence or dependency analysis is unavailable', async () => { - expect((await classifyOrcadTerminalSmokeChanges([])).shouldRun).toBe(true) - const result = await classifyOrcadTerminalSmokeChanges(['src/main/deleted.ts'], async () => { - throw new Error('Could not resolve deleted dependency') - }) - expect(result.shouldRun).toBe(true) - expect(result.reason).toContain('Dependency graph unavailable') -}) - -describe('the actual terminal smoke dependency graph', () => { - let inputs - beforeAll(async () => { - inputs = await collectOrcadTerminalSmokeInputs() - }, 60_000) - - it.each([ - ...Object.values(ORCAD_CHILD_ENTRY_POINTS), - 'src/main/runtime/orca-runtime.ts', - 'src/main/windows/windows-process-table.ts', - 'src/main/worker-thread-entry-path.ts', - 'src/cli/index.ts', - 'config/scripts/runtime-serve-terminal-smoke.mjs', - 'config/scripts/build-orcad-node.mjs', - 'config/scripts/build-orcad.mjs', - 'config/scripts/profile-state-worker-smoke.mjs', - 'config/scripts/install-dev-cli.mjs', - 'config/scripts/verify-cli-bin.mjs', - 'config/scripts/node-server-change-scope.mjs' - ])('retains the runtime, CLI, build or smoke dependency: %s', async (file) => { - expect(inputs.has(file)).toBe(true) - expect((await classifyOrcadTerminalSmokeChanges([file], async () => inputs)).shouldRun).toBe( - true - ) - }) - - it.each([ - 'src/renderer/src/components/Example.tsx', - 'src/main/menu/register-app-menu.ts', - 'config/scripts/ci-shard-timings.json', - '.github/workflows/e2e.yml', - 'docs/reference/ci-runner-efficiency.md' - ])('skips unrelated inputs: %s', async (file) => { - expect((await classifyOrcadTerminalSmokeChanges([file], async () => inputs)).shouldRun).toBe( - false - ) - }) - - it('retains a removed source path when a file is renamed outside the CLI tree', async () => { - expect( - ( - await classifyOrcadTerminalSmokeChanges( - ['src/cli/deleted-command.ts', 'docs/moved-command.ts'], - async () => inputs - ) - ).shouldRun - ).toBe(true) - }) -}) - -it('only skips the unchanged smoke after a successful diff and dependency analysis', () => { - const workflow = parse( - readFileSync(new URL('../../.github/workflows/pr.yml', import.meta.url), 'utf8') - ) - const steps = workflow.jobs.preflight.steps - const scope = steps.find((candidate) => candidate.id === 'orcad-smoke-scope') - expect(scope.env).toEqual({ BASE_SHA: '${{ github.event.pull_request.base.sha }}' }) - // See the localization gate: HEAD^1 removes the merge-base computation, so a shallow checkout - // is enough and the payload head SHA is unused. - expect(scope.run).toContain('node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA"') - expect(scope.run).toContain('git diff --name-only --no-renames -z "$DIFF_BASE" HEAD') - expect(scope.run).not.toContain('--diff-filter') - expect(scope.run).toMatch( - /&& \[ "\$scope" = false \]; then[\s\S]*run=false[\s\S]*else\s+echo 'run=true'/ - ) - // orcad refuses to build without the page, so the page builds exactly when the smoke runs. - const gated = "steps.orcad-smoke-scope.outputs.run == 'true'" - const page = steps.find( - (candidate) => candidate.uses === './.github/actions/build-mobile-web-bundle' - ) - const smoke = steps.find((candidate) => candidate.name === 'Boot orcad and round-trip a terminal') - expect(page.if).toBe(gated) - expect(smoke.if).toBe(gated) - expect(smoke.run).toBe('pnpm run smoke:orcad-terminal') - expect(steps.indexOf(page)).toBeLessThan(steps.indexOf(smoke)) -}) diff --git a/config/scripts/pr-auxiliary-preflight-admission.test.mjs b/config/scripts/pr-auxiliary-preflight-admission.test.mjs deleted file mode 100644 index ff69c2c67c11..000000000000 --- a/config/scripts/pr-auxiliary-preflight-admission.test.mjs +++ /dev/null @@ -1,492 +0,0 @@ -import { - copyFileSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - symlinkSync, - writeFileSync -} from 'node:fs' -import { tmpdir } from 'node:os' -import { dirname, join, resolve } from 'node:path' -import { runInNewContext } from 'node:vm' -import { afterEach, describe, expect, it } from 'vitest' -import { runProcessSync } from '@orca/process-host' -import { parse } from 'yaml' -import { classifyPrJobs } from './pr-code-change-scope.mjs' - -const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) -const unit = parse(readFileSync('.github/workflows/unit-tests.yml', 'utf8')) -const relaySteps = unit.jobs.relay_integration.steps -const chrome = relaySteps.find((step) => step.name === 'Resolve Chrome for the browser provider') -const browser = relaySteps.find((step) => step.name === 'Test external Chromium browser provider') -const routes = [ - ['git_compatibility', 'src/shared/git-capability-cache.ts'], - ['codex_index_heal_contract', 'src/main/codex/codex-session-index-heal.ts'], - ['xterm_patch_sync', 'config/patches/xterm-upstream.json'], - ['shell_contracts', 'src/main/pty/pty-manager.ts'], - ['orcad_browser', 'src/main/orcad/orcad-browser-provider.ts'], - ['cross-version-wire', 'src/shared/orchestration-rpc-contract.ts'], - ['managed_hook_node18', 'src/shared/agent-hook-status.ts'] -] - -function evaluate(expression, context) { - return runInNewContext( - expression - .replace(/^\$\{\{\s*|\s*\}\}$/g, '') - .replaceAll('.cross-version-wire', '["cross-version-wire"]'), - context - ) -} - -function routedOutputs(files, reused, options) { - const scope = classifyPrJobs(files, options) - const context = { - steps: { - readiness: { outputs: { reused } }, - filter: { - outputs: Object.fromEntries( - Object.entries(scope).map(([key, value]) => [key, String(value)]) - ) - } - } - } - return Object.fromEntries( - ['test', 'static_analysis', 'typecheck', 'mobile_ci_tools', ...routes.map(([job]) => job)].map( - (name) => [name, String(evaluate(workflow.jobs.code_paths.outputs[name], context))] - ) - ) -} - -describe.each(routes)('%s preflight admission', (jobName, changedFile) => { - const job = workflow.jobs[jobName === 'orcad_browser' ? 'test' : jobName] - const condition = - jobName === 'orcad_browser' ? `(${job.if}) && (${chrome.if}) && (${browser.if})` : job.if - - it('waits for the detector and physical preflight job', () => { - expect(job.needs).toEqual(['code_paths', 'preflight']) - expect(job.if).toContain('!cancelled()') - expect(job['continue-on-error']).toBeUndefined() - }) - - it.each([ - { name: 'all prerequisites pass', admitted: true }, - { name: 'preflight fails', preflight: 'failure', admitted: false }, - { name: 'preflight is cancelled', preflight: 'cancelled', admitted: false }, - { name: 'preflight skips', preflight: 'skipped', admitted: false }, - { name: 'preflight result is missing', preflight: '', admitted: false }, - { name: 'preflight is unfinished', preflight: null, admitted: false }, - { name: 'detector fails', detector: 'failure', admitted: false }, - { name: 'detector is cancelled', detector: 'cancelled', admitted: false }, - { name: 'detector skips', detector: 'skipped', admitted: false }, - { name: 'detector result is missing', detector: '', admitted: false }, - { name: 'detector is unfinished', detector: null, admitted: false }, - { name: 'route is unselected', selected: 'false', admitted: false }, - { name: 'route is missing', selected: '', admitted: false }, - { name: 'route is absent', selected: null, admitted: false }, - { name: 'route is unknown', selected: 'unknown', admitted: false }, - { name: 'workflow is cancelled', cancelled: true, admitted: false } - ])('evaluates the actual workflow condition: $name', (scenario) => { - const context = { - cancelled: () => scenario.cancelled ?? false, - success: () => true, - inputs: {}, - needs: { - code_paths: { - result: scenario.detector === undefined ? 'success' : (scenario.detector ?? undefined), - outputs: { - test: 'true', - [jobName]: scenario.selected === undefined ? 'true' : (scenario.selected ?? undefined) - } - }, - preflight: { - result: scenario.preflight === undefined ? 'success' : (scenario.preflight ?? undefined) - } - } - } - if (jobName === 'orcad_browser') { - context.inputs.browser_contract = evaluate(job.with.browser_contract, context) - } - expect(evaluate(condition, context)).toBe(scenario.admitted) - }) - - it.each([ - { files: [] }, - { files: ['package.json'] }, - { files: ['.github/workflows/pr.yml'] }, - { files: [changedFile] } - ])( - 'requires unit work and preflight whenever the real classifier selects $files', - ({ files }) => { - const selected = routedOutputs(files, 'false') - expect(selected[jobName]).toBe('true') - expect(selected.test).toBe('true') - expect(selected.static_analysis).toBe('true') - expect(selected.typecheck).toBe('true') - } - ) - - it.each(['true', 'false', undefined])( - 'honors the actual readiness-masked output when reuse is %s', - (reused) => { - const outputs = routedOutputs([changedFile], reused) - expect(outputs[jobName]).toBe(String(reused !== 'true')) - expect(outputs.test).toBe(String(reused !== 'true')) - const context = { - cancelled: () => false, - success: () => true, - inputs: {}, - needs: { code_paths: { result: 'success', outputs }, preflight: { result: 'success' } } - } - if (jobName === 'orcad_browser') { - context.inputs.browser_contract = evaluate(job.with.browser_contract, context) - } - expect(evaluate(condition, context)).toBe(reused !== 'true') - } - ) -}) - -it.each([ - { files: ['README.md'] }, - { files: ['mobile/src/App.tsx'] }, - { files: ['mobile/package.json'] }, - { files: ['cloud/apps/relay/src/index.ts'] } -])('does not add preflight consumers to a no-unit diff: $files', ({ files }) => { - const outputs = routedOutputs(files, 'false') - expect(outputs.test).toBe('false') - for (const [name] of routes) { - expect(outputs[name], name).toBe('false') - } -}) - -it('keeps the mobile-only bundle job in the first wave', () => { - const mobile = workflow.jobs.mobile_web_app - expect(mobile.needs).toEqual(['code_paths']) - expect(mobile.if).toBe("needs.code_paths.outputs.mobile_web_app == 'true'") - expect(classifyPrJobs(['mobile/src/App.tsx'])).toMatchObject({ - test: false, - mobile_web_app: true - }) -}) - -it('preserves relay continuation, setup guards and cancellation', () => { - const steps = relaySteps - const install = steps.find((step) => step.id === 'relay_dependencies') - const test = steps.find((step) => step.name === 'Test relay integration contracts') - const context = { - cancelled: () => false, - success: () => false, - steps: { - root_dependencies: { outcome: 'success' }, - relay_dependencies: { outcome: 'success' } - } - } - expect(evaluate(install.if, context)).toBe(true) - expect(evaluate(test.if, context)).toBe(true) - context.steps.root_dependencies.outcome = 'failure' - expect(evaluate(install.if, context)).toBe(false) - context.steps.relay_dependencies.outcome = 'failure' - expect(evaluate(test.if, context)).toBe(false) - context.steps.root_dependencies.outcome = 'success' - context.steps.relay_dependencies.outcome = 'success' - context.cancelled = () => true - expect(evaluate(install.if, context)).toBe(false) - expect(evaluate(test.if, context)).toBe(false) - expect( - steps.findIndex((step) => step.name === 'Test external Chromium browser provider') - ).toBeLessThan(steps.indexOf(install)) - expect(test['continue-on-error']).toBeUndefined() - expect(unit.jobs.relay_integration['continue-on-error']).toBeUndefined() - expect(chrome['continue-on-error']).toBeUndefined() - expect(browser['continue-on-error']).toBeUndefined() - expect(workflow.jobs.verify.needs).toContain('test') -}) - -it('guards both browser steps after setup or discovery failure and for default callers', () => { - const context = { - inputs: { browser_contract: true }, - success: () => true - } - expect(evaluate(chrome.if, context)).toBe(true) - expect(evaluate(browser.if, context)).toBe(true) - context.success = () => false - expect(evaluate(chrome.if, context)).toBe(false) - expect(evaluate(browser.if, context)).toBe(false) - context.success = () => true - context.inputs.browser_contract = unit.on.workflow_call.inputs.browser_contract.default - expect(evaluate(chrome.if, context)).toBe(false) - expect(evaluate(browser.if, context)).toBe(false) -}) - -describe('Mobile CI tool preflight routing', () => { - const verified = { allowMobileCiTools: true } - - it.each([ - { files: ['config/scripts/run-mobile-ci-tests.mjs', 'config/mobile-ci.vitest.config.mjs'] }, - { files: ['config/scripts/mobile-release-check-scope.test.mjs'] }, - { files: ['config/scripts/mobile-test-change-scope.test.mjs'] } - ])('keeps affected controls required without starting root shards for $files', ({ files }) => { - const outputs = routedOutputs(files, 'false', verified) - expect(outputs.mobile_ci_tools).toBe('true') - expect(outputs.test).toBe('true') - const context = { needs: { code_paths: { outputs } } } - expect(evaluate(workflow.jobs.preflight.if, context)).toBe(true) - expect(evaluate(workflow.jobs.test.with.run_shards, context)).toBe(false) - expect(evaluate(unit.jobs.test.if, { inputs: { run_shards: false } })).toBe(false) - const required = workflow.jobs.verify.steps.find( - (step) => step.name === 'Require successful checks' - ) - expect(evaluate(required.env.PREFLIGHT_SHOULD_RUN, context)).toBe(true) - }) - - it('keeps workflow-only changes on their original skipped root route', () => { - const outputs = routedOutputs(['.github/workflows/mobile.yml'], 'false', verified) - expect(outputs.mobile_ci_tools).toBe('false') - expect(outputs.test).toBe('false') - expect(outputs.static_analysis).toBe('false') - expect(outputs.typecheck).toBe('false') - expect(evaluate(workflow.jobs.preflight.if, { needs: { code_paths: { outputs } } })).toBe(false) - }) - - 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) - 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() - }) - - it('requires complete modification evidence and an exact path scope', () => { - const tool = 'config/scripts/run-mobile-ci-tests.mjs' - expect(classifyPrJobs([tool]).test).toBe(true) - expect(classifyPrJobs([tool]).mobile_ci_tools).toBe(false) - for (const files of [ - [], - [tool, 'src/main/runtime/orca-runtime.ts'], - [tool, 'pnpm-lock.yaml'], - [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'] - ]) { - const selected = classifyPrJobs(files, verified) - expect(selected.mobile_ci_tools).toBe(false) - expect(selected.test).toBe(true) - } - }) - - it('reuses only identical successful checks for the focused preflight gate', () => { - const files = ['config/scripts/run-mobile-ci-tests.mjs'] - expect(routedOutputs(files, 'true', verified).mobile_ci_tools).toBe('false') - expect(routedOutputs(files, 'false', verified).mobile_ci_tools).toBe('true') - }) -}) - -const mobileCiScopeDirectories = [] -afterEach(() => { - for (const directory of mobileCiScopeDirectories.splice(0)) { - rmSync(directory, { recursive: true, force: true }) - } -}) - -function createMobileCiScopeRepository( - withTool = true, - toolPath = 'config/scripts/run-mobile-ci-tests.mjs', - companionPaths = [] -) { - const directory = mkdtempSync(join(tmpdir(), 'orca-mobile-ci-scope-')) - mobileCiScopeDirectories.push(directory) - const root = resolve(import.meta.dirname, '../..') - 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', - 'config/scripts/ci-e2e-job-selection.mjs' - ]) { - mkdirSync(dirname(join(directory, file)), { recursive: true }) - copyFileSync(join(root, file), join(directory, file)) - } - const git = (...args) => { - const result = runProcessSync({ program: 'git', args, cwd: directory }) - expect(result.code, result.stderr).toBe(0) - return result.stdout.trim() - } - const commit = () => { - git('add', '-A') - git( - '-c', - 'commit.gpgsign=false', - '-c', - 'core.hooksPath=/dev/null', - 'commit', - '-qm', - 'scope input' - ) - return git('rev-parse', 'HEAD') - } - git('init', '--quiet') - git('config', 'user.name', 'Scope control') - git('config', 'user.email', 'scope@example.invalid') - const tool = join(directory, toolPath) - mkdirSync(dirname(tool), { recursive: true }) - if (withTool) { - writeFileSync(tool, 'export const revision = 1\n') - } - for (const file of companionPaths) { - mkdirSync(dirname(join(directory, file)), { recursive: true }) - writeFileSync(join(directory, file), 'export const revision = 1\n') - } - const base = commit() - const detect = (stepId = 'filter') => { - const output = join( - directory, - stepId === 'filter' ? 'github-output' : `github-output-${stepId}` - ) - const step = workflow.jobs.code_paths.steps.find((candidate) => candidate.id === stepId) - const result = runProcessSync({ - program: 'bash', - args: [ - '-e', - '-c', - step.run.replace('${{ github.event.pull_request.base.sha }}', '$BASE_SHA') - ], - cwd: directory, - env: { - ...process.env, - BASE_SHA: base, - GITHUB_EVENT_NAME: 'pull_request', - GITHUB_OUTPUT: output, - ORCA_BACKGROUND_LAUNCH: '1' - } - }) - return { result, output } - } - return { directory, tool, git, commit, detect } -} - -describe.skipIf(process.platform === 'win32')('Mobile CI complete Git change evidence', () => { - it('admits an actual modification through the workflow detector', () => { - const repo = createMobileCiScopeRepository() - writeFileSync(repo.tool, 'export const revision = 2\n') - repo.commit() - const { result, output } = repo.detect() - expect(result.code, result.stderr).toBe(0) - expect(readFileSync(output, 'utf8')).toContain('mobile_ci_tools=true\n') - expect(readFileSync(output, 'utf8')).toContain('test=true\n') - }) - - it.each(['addition', 'deletion', 'rename', 'type change'])( - 'retains full root units for an actual %s', - (change) => { - const repo = createMobileCiScopeRepository(change !== 'addition') - if (change === 'addition') { - writeFileSync(repo.tool, 'export const revision = 1\n') - } else if (change === 'deletion') { - rmSync(repo.tool) - } else if (change === 'rename') { - repo.git( - 'mv', - 'config/scripts/run-mobile-ci-tests.mjs', - 'config/mobile-ci.vitest.config.mjs' - ) - } else { - rmSync(repo.tool) - symlinkSync('pr-code-change-scope.mjs', repo.tool) - } - repo.commit() - const { result, output } = repo.detect() - expect(result.code, result.stderr).toBe(0) - expect(readFileSync(output, 'utf8')).toContain('mobile_ci_tools=false\n') - expect(readFileSync(output, 'utf8')).toContain('test=true\n') - } - ) - - it('does not emit a passing classification when the Git evidence is unavailable', () => { - const repo = createMobileCiScopeRepository() - writeFileSync(repo.tool, 'export const revision = 2\n') - repo.commit() - expect(repo.detect().result.code).toBe(0) - rmSync(join(repo.directory, 'github-output')) - rmSync(join(repo.directory, '.git'), { recursive: true, force: true }) - const { result } = repo.detect() - expect(result.code).not.toBe(0) - expect(result.stderr).toContain('not a git repository') - }) -}) - -describe.skipIf(process.platform === 'win32')('Daemon mock complete Git change evidence', () => { - const helper = 'src/main/daemon/daemon-init-dependency-mocks.ts' - it('keeps real unit and type contracts while omitting unrelated packages for a modification', () => { - const repo = createMobileCiScopeRepository(true, helper) - writeFileSync(repo.tool, 'export const revision = 2\n') - repo.commit() - const { result, output } = repo.detect() - expect(result.code, result.stderr).toBe(0) - const flags = readFileSync(output, 'utf8') - expect(flags).toContain('package=false\n') - expect(flags).toContain('package_windows=false\n') - expect(flags).toContain('test=true\n') - expect(flags).toContain('typecheck=true\n') - }) - it('routes the complete four-file modification through the real detector', () => { - const companions = [ - 'src/main/native-chat/agent-session-wire/structured-agent-session-claude-queued-stop.test.ts', - 'config/scripts/process-host-build-dist.test.mjs', - 'src/main/codex/codex-app-server-connection.test.ts' - ] - const repo = createMobileCiScopeRepository(true, helper, companions) - for (const file of [helper, ...companions]) { - writeFileSync(join(repo.directory, file), 'export const revision = 2\n') - } - repo.commit() - const { result, output } = repo.detect() - expect(result.code, result.stderr).toBe(0) - const flags = readFileSync(output, 'utf8') - for (const name of ['package', 'package_windows']) { - expect(classifyPrJobs([helper, ...companions])[name]).toBe(true) - expect(flags).toContain(`${name}=false\n`) - } - for (const name of ['test', 'static_analysis', 'typecheck', 'cross-version-wire']) { - expect(flags).toContain(`${name}=true\n`) - } - const e2e = repo.detect('e2e_filter') - expect(e2e.result.code, e2e.result.stderr).toBe(0) - const routes = readFileSync(e2e.output, 'utf8') - expect(routes).toContain('test_files=[]\n') - expect(routes).toContain('should_run=false\n') - }) - it.each(['addition', 'deletion', 'rename', 'type change'])( - 'keeps platform packages for an actual %s', - (change) => { - const repo = createMobileCiScopeRepository(change !== 'addition', helper) - if (change === 'addition') { - writeFileSync(repo.tool, 'export const revision = 1\n') - } else if (change === 'deletion') { - rmSync(repo.tool) - } else if (change === 'rename') { - repo.git('mv', helper, helper.replace('.ts', '-renamed.ts')) - } else { - rmSync(repo.tool) - symlinkSync('../../../config/scripts/pr-code-change-scope.mjs', repo.tool) - } - repo.commit() - const { result, output } = repo.detect() - expect(result.code, result.stderr).toBe(0) - expect(readFileSync(output, 'utf8')).toContain('package=true\n') - expect(readFileSync(output, 'utf8')).toContain('package_windows=true\n') - } - ) -}) diff --git a/config/scripts/pr-code-change-scope.test.mjs b/config/scripts/pr-code-change-scope.test.mjs deleted file mode 100644 index 05cdf48fb100..000000000000 --- a/config/scripts/pr-code-change-scope.test.mjs +++ /dev/null @@ -1,622 +0,0 @@ -import { spawn, spawnSync } from 'node:child_process' -import { readFileSync } from 'node:fs' -import { join, resolve } from 'node:path' -import { describe, expect, it } from 'vitest' -import { parse } from 'yaml' -import { - classifyPrJobs, - isDocsOnlyPath, - PR_CHECK_JOBS, - shouldRunPrChecks, - STATIC_ANALYSIS_SCAN_ROOTS -} from './pr-code-change-scope.mjs' - -const projectDir = resolve(import.meta.dirname, '../..') -const prWorkflow = parse(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8')) - -const ALWAYS_ON = ['static_analysis', 'typecheck', 'test'] - -function expectedJobs(overrides, { alwaysOn = true } = {}) { - return Object.fromEntries( - PR_CHECK_JOBS.map((job) => [ - job, - (alwaysOn && ALWAYS_ON.includes(job)) || Boolean(overrides[job]) - ]) - ) -} - -function expectClassification(files, overrides) { - const result = classifyPrJobs(files) - const shouldRun = shouldRunPrChecks(files) - expect(result.should_run).toBe(shouldRun) - expect(result).toMatchObject({ - should_run: shouldRun, - ...expectedJobs(overrides, { alwaysOn: shouldRun }) - }) -} - -describe('docs-only path classification', () => { - it('treats the WeChat README PR files as docs-only', () => { - expect( - shouldRunPrChecks([ - 'README.md', - 'docs/assets/wechat-qr-group8.jpg', - 'docs/readme/README.zh-CN.md' - ]) - ).toBe(false) - }) - - it('skips root instruction files and GitHub markdown templates', () => { - expect(isDocsOnlyPath('AGENTS.md')).toBe(true) - expect(isDocsOnlyPath('CLAUDE.md')).toBe(true) - expect(isDocsOnlyPath('LICENSE')).toBe(true) - expect(isDocsOnlyPath('.github/CONTRIBUTING.md')).toBe(true) - expect(isDocsOnlyPath('.github/pull_request_template.md')).toBe(true) - expect(isDocsOnlyPath('.github/ISSUE_TEMPLATE/bug_report.yml')).toBe(true) - expect(isDocsOnlyPath('.github/CODEOWNERS')).toBe(true) - }) - - it('still runs PR Checks for product markdown and CI', () => { - expect(isDocsOnlyPath('skills/computer-use/SKILL.md')).toBe(false) - expect(isDocsOnlyPath('skill-guides/orca-cli.md')).toBe(false) - expect(isDocsOnlyPath('.github/workflows/pr.yml')).toBe(false) - expect(isDocsOnlyPath('src/main/index.ts')).toBe(false) - expect(isDocsOnlyPath('config/scripts/pr-code-change-scope.mjs')).toBe(false) - expect(shouldRunPrChecks(['README.md', 'src/main/index.ts'])).toBe(true) - }) - - it('runs PR Checks when the diff is empty rather than skipping by accident', () => { - expect(shouldRunPrChecks([])).toBe(true) - }) - - it('does not start desktop PR Checks for mobile-only diffs', () => { - expect(shouldRunPrChecks(['mobile/src/App.tsx', 'mobile/package.json'])).toBe(false) - }) - - it('does not start desktop PR Checks for cloud-only diffs', () => { - expect( - shouldRunPrChecks([ - 'cloud/apps/relay/src/index.ts', - 'cloud/package.json', - 'cloud/.gitleaks.toml', - '.github/workflows/cloud-verify.yml' - ]) - ).toBe(false) - }) -}) - -describe('per-job path classification', () => { - it('runs every expensive job on an empty diff rather than skipping by accident', () => { - const result = classifyPrJobs([]) - expect(result.should_run).toBe(true) - for (const job of PR_CHECK_JOBS) { - expect(result[job], job).toBe(true) - } - }) - - it('skips every expensive job for docs-only diffs', () => { - expectClassification(['README.md', 'docs/readme/README.zh-CN.md'], {}) - }) - - it('runs packaging and always-on jobs for product source, not git/xterm/shell lanes', () => { - expectClassification(['src/renderer/src/components/tab-bar/TabBar.tsx'], { - package: true, - package_windows: true - }) - }) - - it('runs Git compatibility only when git capability inputs change', () => { - expectClassification(['src/shared/git-capability-cache.ts'], { - git_compatibility: true, - package: true, - package_windows: true - }) - expectClassification(['src/shared/git-binary-compatibility.test.ts'], { - git_compatibility: true - }) - expectClassification(['.github/actions/prepare-git-compatibility/action.yml'], { - git_compatibility: true - }) - // The contract pins the local-main fast-forward's exact arguments. - expectClassification(['src/shared/worktree/local-base-branch-fast-forward.ts'], { - git_compatibility: true, - package: true, - package_windows: true - }) - }) - - it('runs the Codex index-heal contract only when the heal or its transport changes', () => { - expectClassification(['src/main/codex/codex-app-server-session.test.ts'], {}) - expectClassification(['src/main/codex/codex-session-index-heal.ts'], { - codex_index_heal_contract: true, - package: true, - package_windows: true - }) - expectClassification(['src/main/sqlite/sync-database.ts'], { - codex_index_heal_contract: true, - package: true, - package_windows: true - }) - expectClassification(['src/main/codex/codex-app-server-session.ts'], { - codex_index_heal_contract: true, - package: true, - package_windows: true - }) - expectClassification(['src/main/codex/codex-index-heal-binary-contract.test.ts'], { - codex_index_heal_contract: true - }) - // Keep the real-binary gate live when a trust, transport, launch or hook-approval dependency changes. - for (const file of [ - 'src/main/agent-trust-presets.ts', - 'src/main/codex/config-toml-trust.ts', - 'src/main/codex/codex-hook-trust-derivation.ts', - 'src/main/codex/codex-hook-local-install.ts', - 'src/main/codex/codex-hook-orca-approvals.ts', - 'src/main/codex/codex-hook-reconcile.ts', - 'src/main/codex/config-toml-hook-trust-edit.ts', - 'src/main/codex-cli/codex-read-only-app-server-args.ts', - 'src/main/codex/codex-app-server-capability-signal.ts', - 'src/main/provider-process/provider-process-exit-deadline.ts', - 'src/main/provider-process/provider-process-launch.ts', - 'src/main/provider-process/provider-record-reader.ts', - 'src/main/codex/codex-session-backfill.ts', - 'src/main/codex/codex-session-index-heal-state.ts', - 'src/main/codex-cli/command.ts', - 'src/main/win32-utils.ts', - 'src/shared/node-cli-command-resolution.ts', - 'src/shared/windows-batch-spawn.ts' - ]) { - expectClassification([file], { - codex_index_heal_contract: true, - package: true, - package_windows: true - }) - } - // A neighbouring Codex module must not drag the real-binary job in. - expectClassification(['src/main/codex/codex-home-paths.ts'], { - package: true, - package_windows: true - }) - }) - - it('runs xterm patch sync only when xterm inputs change', () => { - expectClassification(['config/patches/xterm-upstream.json'], { - xterm_patch_sync: true - }) - expectClassification(['config/patches/@xterm__xterm@6.1.0-beta.287.patch'], { - xterm_patch_sync: true - }) - }) - - 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'], { package: true }) - expectClassification(['native/wayland-clipboard/wayland-clipboard.c'], { - package: true - }) - expectClassification(['native/computer-use-macos/Package.swift'], {}) - }) - - it('runs Linux packaging when an artifact contract changes', () => { - for (const file of [ - 'config/scripts/package-linux-formats.mjs', - 'config/scripts/package-linux-formats-appimage.mjs', - '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', - 'config/docker/headless-pairing/run-appimage-case.sh', - 'config/docker/headless-serve-shutdown/Dockerfile', - 'config/scripts/run-linux-cli-launch-contract-docker.mjs', - 'config/scripts/run-headless-linux-pairing-docker.mjs', - 'config/scripts/static-appimage-package-contract.cjs' - ]) { - expectClassification([file], { - package: true - }) - } - }) - - it('runs Linux packaging for the daemon shutdown descendant oracle and its production paths', () => { - for (const file of [ - 'config/docker/daemon-shutdown-descendants/Dockerfile', - 'config/docker/daemon-shutdown-descendants/bundle-entry.ts', - 'config/docker/daemon-shutdown-descendants/fixture.cjs', - 'config/docker/daemon-shutdown-descendants/run-case.sh', - 'config/scripts/run-daemon-shutdown-descendants-docker.mjs' - ]) { - expectClassification([file], { - package: true - }) - } - for (const file of [ - 'src/main/daemon/terminal-host.ts', - 'src/main/daemon/terminal-session-teardown.ts', - 'src/main/daemon/terminal-host-session-shutdown.ts', - 'src/main/daemon/terminal-descendant-shutdown.ts', - 'src/main/pty-descendant-termination.ts', - 'src/main/pty-descendant-exit-verification.ts', - 'src/main/pty-process-table-parser.ts' - ]) { - expectClassification([file], { package: true, package_windows: true }) - } - }) - - it('runs both package jobs when the shared skills runtime verifier changes', () => { - expectClassification(['config/scripts/verify-skills-cli-runtime.cjs'], { - package: true, - package_windows: true - }) - }) - - it('runs shell contracts when live-shell inputs change', () => { - expectClassification(['src/main/daemon/shell-ready.ts'], { - shell_contracts: true, - package: true, - package_windows: true - }) - }) - - it('runs shell contracts when wrapper templates or live-shell fixtures change', () => { - expectClassification(['src/main/shell-templates.ts'], { - shell_contracts: true, - package: true, - package_windows: true - }) - expectClassification(['src/main/shell-startup-launch-intent-fixtures.ts'], { - shell_contracts: true, - package: true, - package_windows: true - }) - }) - - it('runs shell contracts for the structured-session login-shell test, its harness and its subject', () => { - expectClassification( - ['src/main/runtime/structured-session-cli-login-shell.live-shell.test.ts'], - { shell_contracts: true } - ) - for (const file of [ - 'src/main/runtime/structured-session-login-shell-test-harness.ts', - 'src/main/runtime/structured-session-child-env.ts', - 'src/main/runtime/structured-session-child-env.test-support.ts' - ]) { - expectClassification([file], { - shell_contracts: true, - package: true, - package_windows: true - }) - } - }) - - it('runs orcad browser when Chrome launch, session, or tab modules change', () => { - for (const file of [ - 'src/main/orcad/external-chromium-browser-session.ts', - 'src/main/orcad/external-chromium-command-arguments.ts', - 'src/main/orcad/external-chromium-tab-registry.ts', - 'src/main/orcad/external-chromium-tab-projection.ts' - ]) { - expectClassification([file], { - orcad_browser: true, - package: true, - package_windows: true - }) - } - expectClassification(['src/main/orcad/orcad-native-preflight.ts'], { - package: true, - package_windows: true - }) - }) - - it('runs the mobile web app job for the builder, the page source and the shell policy', () => { - for (const file of [ - 'config/scripts/build-mobile-web-app-bundle.mjs', - 'config/scripts/run-mobile-web-app-checks.mjs', - 'src/packages/process-host/src/run-process.ts', - 'config/scripts/mobile-web-app-route-manifest.mjs', - 'mobile/web-entry/index.tsx', - 'mobile/app/h/[hostId]/index.tsx', - 'mobile/src/transport/client-context.web.tsx', - 'mobile/modules/orca-mobile-web-shell/ios/MobileWebShellCsp.swift', - 'src/shared/native-chat-visual-shell.ts', - // The vendored Expo module the page resolves a .web.ts out of. - 'mobile/packages/expo-two-way-audio/src/ExpoTwoWayAudioModule.web.ts' - ]) { - expect(classifyPrJobs([file]).mobile_web_app, file).toBe(true) - } - }) - - it('runs it on a mobile-only diff, which should_run alone would skip', () => { - const classified = classifyPrJobs(['mobile/app/h/[hostId]/tasks.tsx']) - expect([classified.should_run, classified.mobile_web_app]).toEqual([false, true]) - }) - - it('needs no package.json prefix, because package.json already forces every job', () => { - // build:mobile-web is defined there, so the job has to run on an edit to it. A prefix - // that broad is not how: GLOBAL_FORCE_FILES already covers the file. - expect(classifyPrJobs(['package.json']).mobile_web_app).toBe(true) - }) - - it('leaves it off for changes that cannot reach the page', () => { - for (const file of ['docs/reference/x.md', 'src/main/orcad/orcad-native-preflight.ts']) { - expect(classifyPrJobs([file]).mobile_web_app, file).toBe(false) - } - }) - - it('runs cross-version wire checks for every working-tree wire module', () => { - for (const file of [ - 'src/shared/protocol-version.ts', - 'src/shared/terminal-stream-protocol.ts', - 'src/shared/agent-session-wire.ts', - 'src/shared/agent-session-provider-handle.ts', - 'src/shared/agent-session-mutation-envelope.ts', - 'src/shared/agent-session-journal-item-key.ts', - 'src/shared/agent-session-journal-types.ts', - 'src/main/ai-vault/structured-session-ownership.ts', - 'src/main/native-chat/agent-session-journal/journal-cursor.ts', - 'src/main/native-chat/agent-session-journal/journal-reducer.ts', - 'src/main/native-chat/agent-session-journal/journal-row-schema.ts', - 'src/main/native-chat/agent-session-wire/structured-agent-session-host.ts', - 'src/main/runtime/agent-session-record-store.ts', - 'src/main/runtime/rpc/dispatcher.ts', - 'src/main/runtime/rpc/methods/ai-vault.ts', - 'src/shared/rpc-contract/browser-tab-create-params.ts', - 'src/main/runtime/rpc/methods/session-tabs.ts', - 'src/main/runtime/rpc/methods/structured-agent-session.ts', - 'src/main/runtime/rpc/methods/structured-agent-session-gate.ts', - 'src/main/runtime/rpc/methods/structured-agent-session-hold.ts', - 'src/main/runtime/rpc/methods/structured-agent-session-schemas.ts', - 'src/main/runtime/rpc/methods/terminal.ts', - 'src/main/runtime/runtime-worktree-agent-rows.ts', - 'src/main/runtime/runtime-worktree-pty-agent-sources.ts', - 'src/shared/runtime-worktree-contracts.ts', - 'src/renderer/src/runtime/remote-runtime-terminal-multiplexer.ts', - 'src/shared/structured-agent-session-projection.ts', - 'src/shared/agent-turn-outcome.ts', - 'src/main/runtime/orchestration/db.ts', - 'src/main/runtime/orchestration/orchestration-schema-version-skew.ts' - ]) { - expectClassification([file], { - 'cross-version-wire': true, - package: true, - package_windows: true, - // The session-tabs RPC publishes layout, so the layout oracle gates it too. - layout_oracle: file === 'src/main/runtime/rpc/methods/session-tabs.ts' - }) - } - expectClassification( - ['tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts'], - { 'cross-version-wire': true } - ) - }) - - it('runs workflow-self-change and lockfile diffs as force-all', () => { - const result = classifyPrJobs(['.github/workflows/pr.yml']) - expect(result.should_run).toBe(true) - for (const job of PR_CHECK_JOBS) { - expect(result[job], job).toBe(true) - } - expect(classifyPrJobs(['pnpm-lock.yaml']).git_compatibility).toBe(true) - }) - - it('primes native caches only when their immutable inputs change', () => { - expect(classifyPrJobs([]).native_cache_changed).toBe(true) - expect(classifyPrJobs(['README.md']).native_cache_changed).toBe(false) - expect(classifyPrJobs(['src/main/index.ts']).native_cache_changed).toBe(false) - for (const file of [ - 'package.json', - 'pnpm-lock.yaml', - '.github/actions/install-node-dependencies/action.yml', - '.github/actions/prepare-native-runtime/action.yml', - 'config/scripts/ensure-native-runtime.mjs', - 'config/scripts/rebuild-native-deps.mjs', - 'config/patches/node-pty@1.1.0.patch' - ]) { - expect(classifyPrJobs([file]).native_cache_changed, file).toBe(true) - } - }) - - // Why: static analysis lints changed mobile files with a type-aware pass, and - // mobile is a separate pnpm project. Without its node_modules every mobile type - // resolves to an `error` type and the changed-code gate fails on phantom - // findings, which is exactly how a react-test-renderer union broke a PR. - it('installs mobile dependencies exactly when mobile files change', () => { - expect(classifyPrJobs([]).mobile_dependencies).toBe(true) - expect(classifyPrJobs(['README.md']).mobile_dependencies).toBe(false) - expect(classifyPrJobs(['src/main/index.ts']).mobile_dependencies).toBe(false) - expect( - classifyPrJobs(['src/main/index.ts', 'mobile/src/session/a.test.ts']).mobile_dependencies - ).toBe(true) - // Why true: a mobile-only diff still skips the desktop suite, but the repo-wide audits lint - // mobile/, so static analysis runs and its changed-code pass needs the mobile types. - expect(classifyPrJobs(['mobile/package.json']).mobile_dependencies).toBe(true) - expect(classifyPrJobs(['mobile/package.json']).should_run).toBe(false) - expect(classifyPrJobs(['README.md', 'mobile/src/a.ts']).mobile_dependencies).toBe(true) - }) - - // Why: `mobile/` is desktop-irrelevant for every other job, so a mobile-only diff used to skip - // the audits that do lint it. That is how #20702 landed two duplicate imports which then failed - // this gate on every later PR's merge ref until #20895 swept them. - it('runs static analysis for a mobile-only diff without dragging in the desktop suite', () => { - const result = classifyPrJobs([ - 'mobile/src/test-support/rpc-recording/adapters/push-registration-mount-adapters.ts' - ]) - expect(result.static_analysis).toBe(true) - expect(result.mobile_dependencies).toBe(true) - expect(result.should_run).toBe(false) - for (const job of ['typecheck', 'test', 'package', 'package_windows', 'git_compatibility']) { - expect(result[job], job).toBe(false) - } - }) - - // The ratchet: adding a tree to an audit command has to widen this trigger on its own. - it('runs static analysis for every tree the audit commands scan', () => { - expect(STATIC_ANALYSIS_SCAN_ROOTS).toEqual( - expect.arrayContaining(['src', 'config', 'tests', 'mobile']) - ) - for (const root of STATIC_ANALYSIS_SCAN_ROOTS) { - expect(classifyPrJobs([`${root}/changed-file.ts`]).static_analysis, root).toBe(true) - } - }) - - it('leaves diffs the audits never read out of static analysis', () => { - expect(classifyPrJobs(['README.md']).static_analysis).toBe(false) - expect(classifyPrJobs(['cloud/apps/relay/src/index.ts']).static_analysis).toBe(false) - }) - - it('keeps unit-test-only diffs out of packaging', () => { - expectClassification(['src/main/git/git-status.test.ts'], { - git_compatibility: true - }) - }) - - it('emits GitHub output pairs from the shipped CLI', () => { - const result = spawnSync(process.execPath, ['config/scripts/pr-code-change-scope.mjs'], { - cwd: projectDir, - encoding: 'utf8', - input: 'config/patches/xterm-upstream.json\n' - }) - expect(result.status, result.stderr).toBe(0) - expect(result.stdout).toContain('should_run=true\n') - expect(result.stdout).toContain('xterm_patch_sync=true\n') - expect(result.stdout).toContain('git_compatibility=false\n') - expect(result.stdout).toContain('package=false\n') - expect(result.stdout).toContain('test=true\n') - }) - - // A long-lived PR whose base.sha has gone stale diffs thousands of files, so the writer - // outruns one pipe buffer. A single fd-0 read then returns early, breaks the writer's pipe, - // and still exits 0 -- emitting no pairs at all, which silently skips every lane. - it('classifies a path that arrives after the first pipe buffer', async () => { - const filler = Array.from( - { length: 12_000 }, - (_, index) => `docs/reference/generated-placeholder-${index}.md` - ) - const input = `${[...filler, 'config/patches/xterm-upstream.json'].join('\n')}\n` - expect(input.length).toBeGreaterThan(64 * 1024) - - const child = spawn(process.execPath, ['config/scripts/pr-code-change-scope.mjs'], { - cwd: projectDir, - stdio: ['pipe', 'pipe', 'pipe'] - }) - let stdout = '' - let stderr = '' - let brokePipe = false - child.stdout.setEncoding('utf8') - child.stderr.setEncoding('utf8') - child.stdout.on('data', (chunk) => (stdout += chunk)) - child.stderr.on('data', (chunk) => (stderr += chunk)) - child.stdin.on('error', (error) => { - brokePipe ||= error.code === 'EPIPE' - }) - - const exitCode = await new Promise((resolvePromise) => { - child.on('close', resolvePromise) - let offset = 0 - const step = () => { - if (offset >= input.length) { - child.stdin.end() - return - } - child.stdin.write(input.slice(offset, offset + 64 * 1024)) - offset += 64 * 1024 - setTimeout(step, 20) - } - step() - }) - - expect(stderr).not.toContain('EAGAIN') - expect(brokePipe).toBe(false) - expect(exitCode, stderr).toBe(0) - expect(stdout).toContain('should_run=true\n') - expect(stdout).toContain('xterm_patch_sync=true\n') - }) -}) - -describe('PR Checks skip wiring', () => { - it('runs the candidate daemon shutdown Docker oracle in the existing Linux package job', () => { - const steps = prWorkflow.jobs.package.steps - const install = steps.findIndex( - (step) => step.uses === './.github/actions/install-node-dependencies' - ) - const oracle = steps.findIndex( - (step) => step.name === 'Verify Linux daemon shutdown descendant cleanup' - ) - expect(install).toBeGreaterThan(-1) - expect(oracle).toBeGreaterThan(install) - expect(steps[oracle].run).toBe('node config/scripts/run-daemon-shutdown-descendants-docker.mjs') - expect(steps[oracle].env.ORCA_BACKGROUND_LAUNCH).toBe('1') - }) - it('gives static analysis the mobile types its type-aware pass resolves', () => { - expect(prWorkflow.jobs.code_paths.outputs.mobile_dependencies).toBe( - '${{ steps.filter.outputs.mobile_dependencies }}' - ) - const steps = prWorkflow.jobs.preflight.steps - const install = steps.findIndex( - (step) => step.uses === './.github/actions/install-mobile-dependencies' - ) - const gate = steps.findIndex((step) => step.name === 'Enforce changed-code quality') - expect(install).toBeGreaterThan(-1) - expect(install).toBeLessThan(gate) - expect(steps[install].if).toBe( - "needs.code_paths.outputs.static_analysis == 'true' && needs.code_paths.outputs.mobile_dependencies == 'true'" - ) - // The install itself moved into the action the packaging jobs share; assert it there so - // this job cannot keep the step while the action stops installing anything. - const action = parse( - readFileSync( - join(projectDir, '.github/actions/install-mobile-dependencies/action.yml'), - 'utf8' - ) - ) - const [installStep] = action.runs.steps - expect(installStep['working-directory']).toBe('mobile') - expect(installStep.run).toContain('--frozen-lockfile') - }) - - it('keeps the root and README guards on docs-only PRs without another runner', () => { - const detector = prWorkflow.jobs.code_paths - expect(detector.if).toBe( - "github.event_name != 'pull_request' || github.event.pull_request.draft != true" - ) - expect(detector.needs).toBeUndefined() - for (const name of ['Reject new root-level files and folders', 'Check README local links']) { - const step = detector.steps.find((candidate) => candidate.name === name) - expect(step).toBeDefined() - expect(step.if).toBeUndefined() - } - expect(prWorkflow.jobs.root_directory_guard).toBeUndefined() - }) - - it('skips e2e detection on docs-only PRs without dropping the draft gate', () => { - const filter = prWorkflow.jobs.code_paths.steps.find((step) => step.id === 'e2e_filter') - expect(filter.if).toBe( - "github.event.pull_request.draft != true && steps.filter.outputs.should_run == 'true'" - ) - expect(prWorkflow.jobs['e2e-paths']).toBeUndefined() - }) - - it('lets verify pass skipped jobs the classifier turned off', () => { - const verifyStep = prWorkflow.jobs.verify.steps.find( - (step) => step.name === 'Require successful checks' - ) - expect(prWorkflow.jobs.verify.needs[0]).toBe('code_paths') - expect(verifyStep.env.SHOULD_RUN).toBe('${{ needs.code_paths.outputs.should_run }}') - expect(verifyStep.run).toContain('"$CODE_PATHS" != "success"') - expect(verifyStep.run).toContain('# Require success when the PR has code-relevant changes') - expect(verifyStep.run).toContain('expected skipped') - expect(verifyStep.run).toContain('expected success') - for (const job of prWorkflow.jobs.verify.needs) { - // fork_ownership_guard is ungated: it runs on every PR, so it is checked - // unconditionally rather than through the SHOULD_RUN loop. - if (job === 'code_paths' || job === 'preflight' || job === 'fork_ownership_guard') { - continue - } - const envVar = `${job.replaceAll('-', '_').toUpperCase()}_SHOULD_RUN` - expect(verifyStep.env[envVar]).toBe(`\${{ needs.code_paths.outputs.${job} }}`) - expect(verifyStep.run).toContain(`"$${envVar}"`) - } - }) -}) diff --git a/config/scripts/pr-e2e-gate-contract.test.mjs b/config/scripts/pr-e2e-gate-contract.test.mjs deleted file mode 100644 index 14638aafb29b..000000000000 --- a/config/scripts/pr-e2e-gate-contract.test.mjs +++ /dev/null @@ -1,300 +0,0 @@ -import { DEDICATED_E2E_SPECS } from './ci-e2e-job-selection.mjs' -import { existsSync, readFileSync } from 'node:fs' -import { join, resolve } from 'node:path' -import { describe, expect, it } from 'vitest' -import { parse as parseYaml } from 'yaml' -import { - hasNativeImeSourceChange, - PR_E2E_SOURCE_ROUTES, - selectPrE2eSpecs -} from './pr-e2e-source-routing.mjs' - -const projectDir = resolve(import.meta.dirname, '../..') -const prWorkflow = parseYaml(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8')) - -const filterStep = prWorkflow.jobs.code_paths.steps.find( - (step) => step.name === 'Filter changed E2E specs' -) -const rollbackStep = prWorkflow.jobs.preflight.steps.find( - (step) => step.name === 'Check VM runtime rollback compatibility' -) -/** The route that sends a change to the two-Electron restart-survival spec. */ -const restartSurvivalRoute = PR_E2E_SOURCE_ROUTES.find( - (route) => route.id === 'client-hosted-browser.restart-survival' -) - -describe('restart-survival E2E routing', () => { - // Every file below carries behavior the restart spec is the only test that exercises end to end. - it.each([ - 'src/main/runtime/orca-runtime.ts', - 'src/main/runtime/browser/browser-commands.ts', - 'src/main/runtime/client-hosted-page-reconciliation-window.ts', - 'src/main/runtime/runtime-browser-client-page-adoption.ts', - 'src/main/runtime/runtime-browser-client-page-recovery.ts', - 'src/main/runtime/browser-host/browser-host-client-page-adoption.ts', - 'src/main/runtime/browser-host/browser-host-page-reconciliation-orchestration.ts', - 'src/main/runtime/rpc/methods/browser-client-host.ts', - 'src/main/browser/browser-client-host-authority-replacement-wait.ts', - 'src/main/browser/paired-runtime-browser-client-host-composition.ts', - 'src/renderer/src/runtime/web-session-tabs-sync/snapshot-api.ts', - 'src/renderer/src/runtime/host-session-snapshot-authority.ts', - 'src/renderer/src/runtime/restored-client-hosted-browser-host-attach.ts', - 'src/renderer/src/store/slices/runtime-status.ts', - 'src/shared/runtime-types.ts', - 'src/shared/browser-client-host-protocol.ts' - ])('routes %s', (path) => { - expect(restartSurvivalRoute.matches(path)).toBe(true) - if (path === 'src/main/runtime/browser/browser-commands.ts') { - expect(existsSync(join(projectDir, path))).toBe(true) - } - }) - - // The pattern is deliberately not "anything under src": routing every PR at a two-Electron spec - // is the cost the filter exists to avoid. - it.each([ - 'src/main/git/git-status.ts', - 'src/renderer/src/components/tab-bar/BrowserTab.tsx', - 'src/main/terminal/pty-manager.ts', - // The status/types entries name whole files, not a suffix any longer name may end with. - 'src/shared/computer-use-runtime-types.ts', - 'src/main/runtime/browser/browser-commands.test.ts', - 'src/main/runtime/browser/browser-commands.test-fixture.ts', - 'src/main/runtime/browser/browser-commands.fixture.ts', - 'src/main/runtime/browser/browser-commands-extra.ts', - 'src/main/runtime/browser/browser-navigation.ts' - ])('does not route %s', (path) => { - expect(restartSurvivalRoute.matches(path)).toBe(false) - }) -}) - -describe('PR E2E gate contract', () => { - it('selects modified Playwright specs without running deleted tests', () => { - expect(filterStep.run).toContain('--diff-filter=AMCR') - expect(filterStep.run).toContain('config/scripts/pr-e2e-source-routing.mjs') - expect(filterStep.run).not.toContain('tests/playwright\\.') - expect( - selectPrE2eSpecs([ - 'tests/e2e/active-view-restart-restore.spec.ts', - 'tests/e2e/deleted.spec.ts.bak', - 'tests/e2e/global-teardown.unit.test.ts' - ]) - ).toEqual(['tests/e2e/active-view-restart-restore.spec.ts']) - }) - - it('scopes the VM rollback oracle to the PR range and recipe schema authorities', () => { - expect(rollbackStep.run).toMatch(/diff-base\.mjs "\$BASE_SHA"[\s\S]*"\$DIFF_BASE" HEAD --/) - expect(rollbackStep.run).toContain('src/shared/ephemeral-vm-recipes.ts') - expect(rollbackStep.run).toContain('src/shared/orca-yaml-hook-types.ts') - }) - - it('routes P0 sentinels from their causal sources', () => { - const cases = [ - [ - 'src/renderer/src/components/tab-bar/TabBarQuickCommandsMenu.tsx', - 'tests/e2e/terminal-quick-command-pre-bind-recovery.spec.ts' - ], - ['src/main/runtime/orca-runtime-files.ts', 'tests/e2e/paired-quick-open-large-tree.spec.ts'], - [ - 'src/renderer/src/runtime/sync-runtime-graph.ts', - 'tests/e2e/host-parked-pane-remote-viewer.spec.ts' - ], - [ - 'src/renderer/src/runtime/remote-runtime-terminal-multiplexer.ts', - 'tests/e2e/paired-remote-terminal-materialization-reconnect.spec.ts' - ], - [ - 'src/renderer/src/components/terminal-pane/remote-pane-layout-push.ts', - 'tests/e2e/paired-remote-pane-layout-retry.spec.ts' - ] - ] - for (const [source, spec] of cases) { - expect(selectPrE2eSpecs([source]), source).toEqual([spec]) - expect(selectPrE2eSpecs([source.replace(/\.tsx?$/, '.test.ts')]), source).toEqual([]) - expect(existsSync(join(projectDir, spec)), spec).toBe(true) - } - const parkedSplitSpec = 'tests/e2e/terminal-parked-cli-split.spec.ts' - for (const source of [ - 'src/main/window/attach-main-window-services.ts', - 'src/preload/api/ui-command-event-api.ts', - 'src/preload/index.ts', - 'src/renderer/src/components/terminal-pane/terminal-pane-split-request-routing.ts', - 'src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts', - 'src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.ts', - 'src/renderer/src/hooks/ipc-events/terminal-ui-routing-ipc-bridge.ts' - ]) { - expect(selectPrE2eSpecs([source]), source).toContain(parkedSplitSpec) - expect(selectPrE2eSpecs([source.replace(/\.ts$/, '.test.ts')]), source).not.toContain( - parkedSplitSpec - ) - } - expect(existsSync(join(projectDir, parkedSplitSpec)), parkedSplitSpec).toBe(true) - - const restartContinuitySpec = 'tests/e2e/paired-remote-terminal-serve-restart-binding.spec.ts' - for (const source of [ - 'src/main/daemon/daemon-attach-only-retirement.ts', - 'src/main/daemon/daemon-pty-applied-size.ts', - 'src/main/daemon/daemon-pty-session-control.ts', - 'src/main/daemon/daemon-pty-spawn-result.ts', - 'src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts', - 'src/renderer/src/components/terminal-pane/terminal-error-accumulation.ts', - 'src/renderer/src/runtime/web-runtime-terminal-creation.ts', - 'src/renderer/src/runtime/web-runtime-session-tab-lifecycle.ts', - 'src/renderer/src/runtime/web-session-tabs-sync/use-web-session-tabs-sync.ts', - 'src/renderer/src/runtime/web-session-terminal-orphan-recovery.ts', - 'src/renderer/src/runtime/web-session-terminal-orphan-recovery-adoption.ts', - 'src/renderer/src/runtime/web-session-terminal-orphan-recovery-surface.ts', - 'src/renderer/src/runtime/web-session-terminal-orphan-recovery-inventory.ts', - 'src/renderer/src/runtime/web-session-terminal-orphan-recovery-inventory-validation.ts', - 'src/renderer/src/runtime/web-session-terminal-orphan-recovery-cache.ts', - 'src/renderer/src/runtime/web-session-terminal-orphan-recovery-pane.ts', - 'src/renderer/src/runtime/web-session-terminal-orphan-recovery-queue.ts', - 'src/renderer/src/runtime/web-session-terminal-orphan-recovery-rpc-lane.ts', - 'src/renderer/src/runtime/web-session-terminal-orphan-topology.ts' - ]) { - expect(selectPrE2eSpecs([source]), source).toContain(restartContinuitySpec) - expect(selectPrE2eSpecs([source.replace(/\.ts$/, '.test.ts')]), source).not.toContain( - restartContinuitySpec - ) - } - expect(existsSync(join(projectDir, restartContinuitySpec)), restartContinuitySpec).toBe(true) - const quickCommandSpec = 'tests/e2e/terminal-quick-command-pre-bind-recovery.spec.ts' - for (const source of [ - 'src/renderer/src/components/terminal-pane/pty-connection.ts', - 'src/renderer/src/components/terminal-pane/pty-connection/connect-pane-pty.ts', - 'src/renderer/src/components/terminal-pane/pty-connection/fresh-spawn-start.ts', - 'src/renderer/src/components/terminal-pane/pty-connection/pane-pty-visibility-bind.ts', - 'src/renderer/src/components/terminal-pane/pty-connection/pty-input-recovery.ts' - ]) { - expect(selectPrE2eSpecs([source]), source).toContain(quickCommandSpec) - expect(selectPrE2eSpecs([source.replace(/\.ts$/, '.test.ts')]), source).not.toContain( - quickCommandSpec - ) - } - for (const source of [ - 'src/main/ripgrep/bundled-ripgrep-path.ts', - 'src/shared/bundled-ripgrep.ts', - 'src/shared/ripgrep-process-availability.ts' - ]) { - expect(selectPrE2eSpecs([source]), source).toEqual([ - 'tests/e2e/paired-quick-open-large-tree.spec.ts' - ]) - } - expect( - selectPrE2eSpecs([ - 'src/main/runtime/orca-runtime-files.ts', - 'tests/e2e/paired-quick-open-large-tree.spec.ts' - ]) - ).toEqual(['tests/e2e/paired-quick-open-large-tree.spec.ts']) - expect(selectPrE2eSpecs(['src/renderer/src/components/FileExplorer.tsx'])).toEqual([]) - expect( - selectPrE2eSpecs([ - 'src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts' - ]) - ).toContain('tests/e2e/paired-remote-terminal-materialization-reconnect.spec.ts') - expect( - selectPrE2eSpecs([ - 'src/renderer/src/components/terminal-pane/remote-runtime-pty-transport-test-harness.ts' - ]) - ).not.toContain('tests/e2e/paired-remote-terminal-materialization-reconnect.spec.ts') - expect(selectPrE2eSpecs(['src/main/ipc/pty/register-handlers.ts'])).not.toContain( - 'tests/e2e/paired-remote-terminal-materialization-reconnect.spec.ts' - ) - }) - - it('triggers the real-IME lane from every surface an input method can judge', () => { - for (const file of [ - 'src/renderer/src/components/terminal-pane/terminal-ime-composition-route.ts', - 'src/renderer/src/components/terminal-pane/terminal-ime-native-text-forwarder.ts', - 'src/renderer/src/components/terminal-pane/terminal-ios-hangul-preedit.ts', - 'src/renderer/src/components/terminal-pane/xterm-bypass-policy.ts', - '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.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', - 'tests/e2e/terminal-ime-boundary-probe.ts', - 'tests/e2e/terminal-ime-byte-reader.ts', - 'tests/e2e/terminal-ime-engagement-receipt.ts', - 'tests/e2e/terminal-ibus-hangul-native.spec.ts' - ]) { - expect(hasNativeImeSourceChange([file]), file).toBe(true) - } - - // Why: a real ibus session on a Git or tab-bar edit is the cost the filter exists to avoid, - // and a unit test beside the source must not summon a three-and-a-half-minute lane. - for (const file of [ - 'src/main/git/git-status.ts', - 'src/renderer/src/components/tab-bar/BrowserTab.tsx', - 'src/main/terminal/pty-manager.ts', - 'docs/STYLEGUIDE.md', - 'src/renderer/src/components/terminal-pane/terminal-ime-composition-route.test.ts', - 'src/renderer/src/lib/pane-manager/terminal-ime-anchor.test.ts' - ]) { - expect(hasNativeImeSourceChange([file]), file).toBe(false) - } - }) - - 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 - // spec appearing in test_files is how a spec-only edit reaches the real-IME lane at all. - expect(selectPrE2eSpecs(['src/shared/terminal-unicode-provider.ts'])).toContain( - 'tests/e2e/terminal-ibus-hangul-native.spec.ts' - ) - }) -}) - -describe('Daemon mock modified-only source routing', () => { - const helper = 'src/main/daemon/daemon-init-dependency-mocks.ts' - const verified = { allowDaemonInitTestSupport: true } - it('omits the real D7 route only for the proven unit-support scope', () => { - expect(selectPrE2eSpecs([helper])).toContain('tests/e2e/orcad-serve-mode-switch.spec.ts') - expect(selectPrE2eSpecs([helper], undefined, verified)).toEqual([]) - const companion = 'src/main/codex/codex-app-server-connection.test.ts' - expect(selectPrE2eSpecs([helper, companion], undefined, verified)).toEqual( - selectPrE2eSpecs([companion]) - ) - }) - it.each([ - { files: [] }, - { files: [helper, 'src/main/daemon/daemon-init.ts'] }, - { files: [helper, 'package.json'] }, - { files: [helper, 'src/main/daemon/unknown-support.ts'] }, - { files: [helper, helper] } - ])('retains existing routes for an uncertain or mixed vector $files', ({ files }) => { - expect(selectPrE2eSpecs(files, undefined, verified)).toEqual(selectPrE2eSpecs(files)) - }) -}) diff --git a/config/scripts/pr-e2e-route-contracts.test.mjs b/config/scripts/pr-e2e-route-contracts.test.mjs deleted file mode 100644 index d4841f1de49f..000000000000 --- a/config/scripts/pr-e2e-route-contracts.test.mjs +++ /dev/null @@ -1,534 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { existsSync, readdirSync, readFileSync } from 'node:fs' -import { join, resolve } from 'node:path' -import { parse } from 'yaml' -import { - classifyE2eJobs, - DEDICATED_E2E_SPECS, - MANAGED_SSH_E2E_SPEC_PREFIX, - ORCAD_SERVE_MODE_SWITCH_E2E_SPEC, - WINDOWS_COPY_FILE_E2E_SPEC, - WINDOWS_MISSING_APPDATA_E2E_SPEC, - selectGeneralE2eSpecs -} from './ci-e2e-job-selection.mjs' -import { classifyPrJobs } from './pr-code-change-scope.mjs' -import { testImportClosure } from './e2e-test-import-closure.mjs' -import { isLayoutOracleSource, LAYOUT_ORACLE_E2E_SPECS } from './pr-layout-oracle-route.mjs' -import { - hasNativeImeSourceChange, - PR_E2E_SOURCE_ROUTES, - selectPrE2eSpecs, - shouldRunReusablePrE2e -} from './pr-e2e-source-routing.mjs' - -const projectDir = resolve(import.meta.dirname, '../..') -const prWorkflow = parse(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8')) -const e2eWorkflow = parse(readFileSync(join(projectDir, '.github/workflows/e2e.yml'), 'utf8')) - -describe('pr-e2e-native-only-routing', () => { - const workflow = prWorkflow - const filterStep = workflow.jobs.code_paths.steps.find((step) => step.id === 'e2e_filter') - - describe('native-only PR E2E routing', () => { - it('avoids generic E2E allocation for native-only changes while preserving its IME lane', () => { - for (const file of [ - 'tests/e2e/terminal-ibus-hangul-native.spec.ts', - 'config/scripts/run-terminal-ibus-hangul-e2e.mjs' - ]) { - expect(hasNativeImeSourceChange([file])).toBe(true) - expect(shouldRunReusablePrE2e([file])).toBe(false) - } - expect(shouldRunReusablePrE2e([])).toBe(false) - for (const spec of [ - 'tests/e2e/paired-startup-exec-readiness.spec.ts', - 'tests/e2e/terminal-ime-exact-byte.spec.ts', - 'tests/e2e/future.spec.ts' - ]) { - expect(shouldRunReusablePrE2e([spec])).toBe(true) - expect( - shouldRunReusablePrE2e(['tests/e2e/terminal-ibus-hangul-native.spec.ts', spec]) - ).toBe(true) - } - expect(filterStep.run).toContain('pr-e2e-source-routing.mjs --reusable-workflow') - expect(filterStep.run).toContain('if [ "$SHOULD_RUN" = true ]; then') - }) - }) -}) - -describe('pr-e2e-pane-close-routing', () => { - /** - * The product paths of #21005, "retire captured remote handles when pending panes close". It - * changed the pane close and retirement lifecycle across three transports and matched no route - * at all, so it merged with E2E skipped outright. The routes named every module that BINDS a - * pane and none that unbinds one. - */ - const PENDING_PANE_CLOSE_CHANGE = [ - 'src/renderer/src/components/terminal-pane/retire-unbound-ipc-terminal-pane.ts', - 'src/renderer/src/components/terminal-pane/retire-unbound-runtime-terminal-pane.ts', - 'src/renderer/src/components/terminal-pane/terminal-pane-close-admission.ts', - 'src/renderer/src/components/terminal-pane/terminal-pane-retirement-ownership.ts', - 'src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts', - 'src/renderer/src/store/terminals/terminal-tab-close-providers.ts' - ] - - const CLOSE_ROUTE = PR_E2E_SOURCE_ROUTES.find( - (route) => route.id === 'terminal-pane.close-and-retirement' - ) - - describe('pane close and retirement PR E2E routing', () => { - it('selects the close specs for a pane close lifecycle change', () => { - expect(selectPrE2eSpecs(PENDING_PANE_CLOSE_CHANGE)).toEqual([ - 'tests/e2e/paired-remote-split-pane-host-retired-ghost.spec.ts', - 'tests/e2e/terminal-pane-close-layout-consistency.spec.ts', - 'tests/e2e/terminal-parked-close-retirement.spec.ts' - ]) - expect(shouldRunReusablePrE2e(PENDING_PANE_CLOSE_CHANGE)).toBe(true) - }) - - it('routes each unbind authority on its own, not just as a group', () => { - expect(CLOSE_ROUTE, 'the close route was renamed or removed').toBeDefined() - // A single-file change is the shape that slipped through; assert per path rather than only - // on the union, where one matching path would hide five that do not. - for (const file of PENDING_PANE_CLOSE_CHANGE) { - expect(selectPrE2eSpecs([file]), file).toEqual(CLOSE_ROUTE.specs.toSorted()) - } - }) - - it('keeps every routed spec and authority a real file', () => { - for (const spec of CLOSE_ROUTE.specs) { - expect(existsSync(join(projectDir, spec)), spec).toBe(true) - } - for (const file of PENDING_PANE_CLOSE_CHANGE) { - expect(existsSync(join(projectDir, file)), file).toBe(true) - } - }) - - it('leaves unrelated changes alone', () => { - // Neighbours in the same directories that this route must not claim. - for (const file of [ - 'src/renderer/src/components/terminal-pane/CloseTerminalDialog.tsx', - 'src/renderer/src/components/terminal-pane/terminal-hidden-view-parking.ts', - 'src/renderer/src/runtime/runtime-rpc-client.ts', - 'src/renderer/src/store/terminals/terminal-tab-close-providers.test.ts' - ]) { - expect(CLOSE_ROUTE.matches(file), file).toBe(false) - } - }) - }) -}) - -describe('pr-e2e-layout-oracle-routing', () => { - /** One product file per layout writer family; each alone must run the oracle. */ - const LAYOUT_AUTHORITIES = [ - 'src/shared/workspace-layout/workspace-layout-pane-commands.ts', - 'src/shared/workspace-layout/old-client/old-client-session-tab-rows.ts', - 'src/main/persistence/terminal-topology/terminal-topology-commit.ts', - 'src/main/persistence/restoring-sessions/session-owner-fields.ts', - 'src/main/persistence/loading-store/pty-binding-persistence.ts', - 'src/main/remote-layout-snapshot/remote-layout-snapshot-recorder.ts', - 'src/main/runtime/terminal/launch/terminal-split-surface.ts', - 'src/main/runtime/terminal/adoption/adopt-terminal-orphans-from-inventory.ts', - 'src/main/runtime/runtime-terminal-orphan-adoption.ts', - 'src/main/runtime/session-tabs/headless-tab-group-moves.ts', - 'src/main/runtime/terminal/launch/terminal-create.ts', - 'src/main/runtime/terminal/stop/worktree-terminal-stop.ts', - 'src/renderer/src/runtime/mobile-session-tab-close.ts', - 'src/renderer/src/runtime/runtime-layout-client.ts', - 'src/renderer/src/store/slices/tabs.ts', - 'src/renderer/src/store/slices/tabs/tabs-move-actions.ts', - 'tests/e2e/workspace-layout-oracle-known-on-main.ts', - 'tests/e2e/helpers/workspace-layout-oracle-compare.ts', - ...LAYOUT_ORACLE_E2E_SPECS - ] - - /** Widened while the layout-holder stack is open: these still write layout. */ - const STACK_WRITERS = [ - 'src/renderer/src/store/slices/terminals.ts', - 'src/renderer/src/components/terminal-pane/TerminalPane.tsx', - 'src/renderer/src/runtime/sync-runtime-graph.ts', - 'src/renderer/src/runtime/web-session-tabs-sync/store-patch.ts', - 'src/main/runtime/terminal/launch/terminal-split.ts', - 'src/main/runtime/workspaces/workspace-file-session-targets.ts', - 'src/main/runtime/session-graph/session-window-graph.ts', - 'src/main/runtime/session-tabs/mobile-session-hydration.ts', - 'src/main/runtime/session-tabs/mobile-session-reconciliation.ts', - 'src/main/runtime/session-tabs/mobile-session-preservation.ts', - 'src/main/runtime/workspaces/session-hosts/get-runtime-id.ts', - 'src/main/runtime/mobile-session-layout-projection.ts', - 'src/main/runtime/headless-runtime-graph.ts', - 'src/main/persistence/loading-store/session-host-partitions.ts', - 'src/main/persistence/loading-store/workspace-session-snapshot-publication.ts', - 'src/renderer/src/store/slices/tab-group-reference-repair.ts', - 'src/renderer/src/store/slices/terminal-session-row-hydration.ts', - 'src/renderer/src/store/workspace-layout-cache.ts', - 'src/renderer/src/app-shell/use-runtime-graph-sync.ts', - 'src/renderer/src/components/tab-group/tab-drag-drop-commit.ts', - 'src/shared/remote-layout-snapshot.ts', - 'src/shared/terminal-leaf-move.ts', - 'src/main/ipc/pty/ipc/leaf-move.ts', - 'src/main/persistence/closed-terminal-tab-write-fence.ts', - 'src/main/runtime/runtime-workspace-session-controller.ts', - 'src/main/runtime/workspaces/persisted-surfaces/has-exact-persisted-terminal-surface-identity.ts', - 'src/main/runtime/terminal-surface-close.ts', - 'src/main/runtime/terminal/output/terminal-derived-agent-facts.ts', - 'src/main/runtime/terminal/output/terminal-output-generation.ts', - 'src/main/runtime/session-tabs/mobile-session-membership.ts', - 'src/main/runtime/session-tabs/pty-backed-session-publication.ts', - 'src/renderer/src/components/terminal/terminal-tab-actions.ts', - 'src/renderer/src/components/tab-bar/web-runtime-tab-move-mirror.ts', - 'src/shared/rpc-contract/workspace-layout-params.ts', - 'src/renderer/src/store/slices/recently-closed-tabs.ts' - ] - - /** Every test file the oracle specs import, transitively: a change there can break the gate. */ - const oracleHarnessFiles = () => testImportClosure(projectDir, LAYOUT_ORACLE_E2E_SPECS) - - describe('workspace layout oracle merge gate', () => { - it('routes each layout authority and stack writer on its own', () => { - for (const file of [...LAYOUT_AUTHORITIES, ...STACK_WRITERS]) { - expect(isLayoutOracleSource(file), file).toBe(true) - expect(classifyPrJobs([file]).layout_oracle, file).toBe(true) - expect(existsSync(join(projectDir, file)), file).toBe(true) - } - }) - - it('routes every test file the oracle specs import', () => { - const harness = oracleHarnessFiles() - expect(harness.length).toBeGreaterThan(LAYOUT_ORACLE_E2E_SPECS.length) - expect(harness.filter((file) => !isLayoutOracleSource(file))).toEqual([]) - }) - - it('leaves tests, fixtures, harnesses and unrelated runtime modules alone', () => { - for (const file of [ - 'src/main/persistence/terminal-topology/terminal-topology-commit.test.ts', - 'src/main/persistence/terminal-topology/terminal-leaf-move-fixture.ts', - 'src/main/runtime/orca-runtime-tests/mobile-session-tabs-part-03.spec.ts', - 'src/main/runtime/session-tabs/headless-tab-group-moves.test.ts', - 'src/main/runtime/session-tabs/headless-tab-group-moves-fixture.ts', - 'src/main/runtime/session-tabs/session-tabs-inventory-test-fixture.ts', - 'src/main/runtime/terminal/stop/worktree-terminal-stop.test.ts', - 'src/main/runtime/terminal/stop/worktree-terminal-stop-fixture.ts', - 'src/main/runtime/terminal/stop/worktree-terminal-stop.ts.backup', - 'src/main/runtime/terminal/stop/worktree-terminal-stop-extra.ts', - 'src/main/runtime/runtime-durable-store-fixture.ts', - 'src/main/runtime/orchestration/db/worker-terminal/worker-terminal-store.ts', - 'src/main/runtime/rpc/methods/terminal/terminal-create.ts', - 'src/main/runtime/runtime-rpc-database.ts', - 'src/main/runtime/orca-runtime-files.ts', - 'src/main/runtime/workspaces/persisted-surfaces/automation-operations.ts', - 'src/main/runtime/workspaces/persisted-surfaces/has-exact-persisted-terminal-surface-identity.test.ts', - 'src/renderer/src/runtime/remote-runtime-terminal-multiplexer.ts', - 'src/renderer/src/runtime/web-session-tabs-sync-test-harness.ts', - 'src/renderer/src/runtime/sync-runtime-graph-test-harness.ts', - 'src/renderer/src/runtime/sync-runtime-graph.test.ts', - 'tests/e2e/workspace-layout-oracle-latency.spec.ts' - ]) { - expect(isLayoutOracleSource(file), file).toBe(false) - } - expect(classifyPrJobs(['src/main/runtime/orca-runtime-files.ts']).layout_oracle).toBe(false) - }) - - it('keeps the oracle specs out of the advisory e2e job', () => { - expect(selectPrE2eSpecs(LAYOUT_AUTHORITIES)).toEqual([]) - expect(shouldRunReusablePrE2e(LAYOUT_AUTHORITIES)).toBe(false) - }) - - it('runs exactly the oracle specs, each in the lane that can run it', () => { - const job = prWorkflow.jobs.layout_oracle - expect(job.uses).toBe('./.github/workflows/e2e.yml') - // The merge commit, so a stale-base PR cannot pass on an old main. - expect(job.with.ref).toBe('${{ github.sha }}') - const specs = JSON.parse(job.with.test_files) - expect(specs.toSorted()).toEqual(LAYOUT_ORACLE_E2E_SPECS.toSorted()) - // Window spec runs in the changed-spec lane; headless runs in the orcad lane. - expect(selectGeneralE2eSpecs(specs)).toEqual(['tests/e2e/workspace-layout-oracle.spec.ts']) - expect(classifyE2eJobs(job.with.test_files)).toEqual({ - e2e_run_changed: true, - e2e_needs_build: true - }) - }) - - it('blocks the merge on the oracle and passes when it is skipped', () => { - const verify = prWorkflow.jobs.verify - const gate = verify.steps.find((step) => step.name === 'Require successful checks') - expect(verify.needs).toContain('layout_oracle') - expect(gate.run).toContain( - 'check_job layout_oracle "$LAYOUT_ORACLE" "$LAYOUT_ORACLE_SHOULD_RUN"' - ) - // Two calls of the reusable workflow in one run must not share artifact names. - expect(prWorkflow.jobs.layout_oracle.with.artifact_suffix).toBeTruthy() - expect(prWorkflow.jobs.e2e.with.artifact_suffix).toBeUndefined() - }) - }) -}) - -describe('ssh-browser-e2e-routing', () => { - const workflow = e2eWorkflow - - it('executes both Docker network routes in a Node job with their opt-in enabled', () => { - const spec = 'tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts' - const job = workflow.jobs['ssh-browser-network-route'] - const install = job.steps.find( - (step) => step.uses === './.github/actions/install-node-dependencies' - ) - const run = job.steps.find( - (step) => step.name === 'Run Docker SSH browser network route journeys' - ) - expect(job['runs-on']).toBe('ubuntu-latest') - expect(job.if).toContain("inputs.test_files == ''") - expect(job.if).toContain(spec) - expect(install.with['native-runtime']).toBe('node') - expect(run.env.ORCA_RUN_DOCKER_SSH_BROWSER_E2E).toBe('1') - expect(run.run).toContain(`vitest run --config config/vitest.config.ts ${spec}`) - expect(run['continue-on-error']).toBeUndefined() - expect(DEDICATED_E2E_SPECS).toContain(spec) - for (const changed of [ - spec, - 'src/main/browser/ssh-browser-network-execution-route.ts', - 'src/main/browser/browser-network-deferred-socket.ts', - 'src/main/browser/browser-network-execution-route.ts', - 'src/main/browser/system-ssh-socks-client-socket.ts', - 'src/main/ssh/system-transport/system-ssh-dynamic-forward-process.ts', - 'tests/e2e/helpers/docker-ssh-host-target.ts', - 'tests/e2e/helpers/docker-ssh-host-image.ts' - ]) { - expect(selectPrE2eSpecs([changed])).toContain(spec) - } - expect(selectPrE2eSpecs(['src/renderer/src/components/Unrelated.tsx'])).not.toContain(spec) - expect(selectPrE2eSpecs(['tests/e2e/helpers/docker-ssh-host-terminal-tabs.ts'])).not.toContain( - spec - ) - }) -}) - -describe('orcad-e2e-routing', () => { - const root = projectDir - const jobs = e2eWorkflow.jobs - const ORCAD_AUTO_CONVERT_E2E_SPEC = 'tests/e2e/ssh-orcad-auto-convert.spec.ts' - const ORCAD_IDLE_EXIT_E2E_SPEC = 'tests/e2e/ssh-orcad-idle-exit.spec.ts' - const ORCAD_OPEN_IN_OWNER_E2E_SPEC = 'tests/e2e/ssh-orcad-open-in-owner.spec.ts' - const ORCAD_JOURNEY_E2E_SPECS = [ - 'tests/e2e/ssh-orcad-terminal-journeys.spec.ts', - 'tests/e2e/ssh-orcad-workspace-tools.spec.ts', - 'tests/e2e/ssh-orcad-vm-provisioned-root.spec.ts' - ] - - function expectRouted(files, spec) { - for (const file of files) { - expect(existsSync(resolve(root, file)), file).toBe(true) - expect(selectPrE2eSpecs([file]), file).toContain(spec) - expect(shouldRunReusablePrE2e([file]), file).toBe(true) - } - } - - it('routes the mode-switch spec from its serve sources and harness', () => { - expectRouted( - [ - 'src/main/orcad/orcad-lifecycle.ts', - 'src/main/daemon/daemon-spawner.ts', - 'tests/e2e/helpers/orca-serve-cli-host.ts', - 'tests/e2e/helpers/headless-paired-runtime-host.ts' - ], - ORCAD_SERVE_MODE_SWITCH_E2E_SPEC - ) - }) - - it('routes the missing-AppData spec from its startup sources and harness', () => { - expectRouted( - ['src/main/startup/windows-app-data-path.ts', 'tests/e2e/helpers/orca-serve-cli-host.ts'], - WINDOWS_MISSING_APPDATA_E2E_SPEC - ) - }) - - it('routes the Windows file-copy spec from its clipboard sources', () => { - expectRouted( - [ - 'src/main/window/clipboard-file-copy.ts', - 'src/main/window/clipboard-ipc-handlers.ts', - 'src/renderer/src/components/right-sidebar/file-explorer-row-file-transfer.ts' - ], - WINDOWS_COPY_FILE_E2E_SPEC - ) - }) - - it('routes the auto-convert spec from its conversion sources and harness', () => { - expectRouted( - [ - 'src/main/managed-server/lifecycle/orcad-runtime-conversion.ts', - 'tests/e2e/helpers/orcad-convert-flow.ts', - 'tests/e2e/helpers/orcad-convert-host.ts', - 'tests/e2e/helpers/orcad-template-variant.ts', - 'tests/e2e/helpers/orcad-upgrade-profile.ts' - ], - ORCAD_AUTO_CONVERT_E2E_SPEC - ) - expect( - selectPrE2eSpecs(['src/main/managed-server/lifecycle/orcad-runtime-conversion.test.ts']) - ).not.toContain(ORCAD_AUTO_CONVERT_E2E_SPEC) - }) - - it('routes the idle-exit spec from its idle sources and the shared convert harness', () => { - expectRouted( - [ - 'src/shared/orcad-idle-exit.ts', - 'src/main/managed-server/install/orcad-remote-runtime-control.ts', - 'src/main/managed-server/lifecycle/orcad-managed-serving.ts', - 'tests/e2e/helpers/orcad-convert-flow.ts', - 'tests/e2e/helpers/orcad-convert-host.ts' - ], - ORCAD_IDLE_EXIT_E2E_SPEC, - ORCAD_OPEN_IN_OWNER_E2E_SPEC - ) - }) - - it('builds the e2e app when only a build-dependent orcad spec is requested', () => { - for (const spec of [ - ORCAD_SERVE_MODE_SWITCH_E2E_SPEC, - ORCAD_AUTO_CONVERT_E2E_SPEC, - ORCAD_IDLE_EXIT_E2E_SPEC, - ORCAD_OPEN_IN_OWNER_E2E_SPEC - ]) { - expect(classifyE2eJobs(JSON.stringify([spec])), spec).toEqual({ - e2e_run_changed: false, - e2e_needs_build: true - }) - } - expect(jobs['orcad-ssh-docker'].needs).toEqual(['build']) - }) - - it('runs every managed-SSH spec in one sharded lane, only when one is routed', () => { - const lane = jobs['orcad-ssh-docker'] - expect(lane.if).toBe( - `inputs.test_files == '' || contains(inputs.test_files, '${MANAGED_SSH_E2E_SPEC_PREFIX}')` - ) - expect(lane.strategy.matrix.shard).toEqual(['1/2', '2/2']) - const run = lane.steps.find((step) => step.env?.ORCA_E2E_SSH_DOCKER === '1').run - expect(run).toContain(` ${MANAGED_SSH_E2E_SPEC_PREFIX} `) - expect(run).toContain('--shard="$SHARD"') - for (const spec of [ - ORCAD_AUTO_CONVERT_E2E_SPEC, - ORCAD_IDLE_EXIT_E2E_SPEC, - ORCAD_OPEN_IN_OWNER_E2E_SPEC, - ...ORCAD_JOURNEY_E2E_SPECS - ]) { - expect(existsSync(resolve(root, spec)), spec).toBe(true) - expect(spec.startsWith(MANAGED_SSH_E2E_SPEC_PREFIX), spec).toBe(true) - } - }) - - it('runs the Windows-only specs on one runner, each only when requested', () => { - expect(jobs['windows-missing-appdata-startup']).toBeUndefined() - const job = jobs['orcad-serve-mode-switch-windows'] - for (const spec of [ - ORCAD_SERVE_MODE_SWITCH_E2E_SPEC, - WINDOWS_MISSING_APPDATA_E2E_SPEC, - WINDOWS_COPY_FILE_E2E_SPEC - ]) { - expect(job.if, spec).toContain(`contains(inputs.test_files, '${spec}')`) - const step = job.steps.find((candidate) => candidate.run?.includes(spec)) - expect(step.if, spec).toContain(`contains(inputs.test_files, '${spec}')`) - expect(step.env.ORCA_STARTUP_DIAGNOSTICS, spec).toBeUndefined() - } - }) - - it('routes managed target-owner guards and menus to the real launch regression', () => { - expectRouted( - [ - 'src/renderer/src/lib/local-path-open-guard.ts', - 'src/renderer/src/lib/external-editor-open-capability.ts', - 'src/renderer/src/lib/worktree-runtime-owner.ts', - 'src/renderer/src/components/sidebar/WorktreeOpenInMenu.tsx', - 'src/renderer/src/components/sidebar/WorktreeContextMenuView.tsx', - 'src/renderer/src/components/right-sidebar/FileExplorer.tsx', - 'src/renderer/src/components/right-sidebar/FileExplorerToolbar.tsx', - 'src/renderer/src/components/right-sidebar/source-control/listing/entry-context-menu.tsx', - 'tests/e2e/helpers/orcad-convert-flow.ts', - 'tests/e2e/helpers/orcad-convert-host.ts', - 'tests/e2e/helpers/orcad-upgrade-profile.ts', - 'tests/e2e/helpers/docker-ssh-host-target.ts' - ], - ORCAD_OPEN_IN_OWNER_E2E_SPEC - ) - expect(selectPrE2eSpecs(['src/renderer/src/lib/local-path-open-guard.test.ts'])).not.toContain( - ORCAD_OPEN_IN_OWNER_E2E_SPEC - ) - }) - - it('routes the managed-host journey specs from their harness and sources', () => { - for (const spec of ORCAD_JOURNEY_E2E_SPECS) { - expectRouted( - [ - 'tests/e2e/helpers/orcad-managed-workspace.ts', - 'src/renderer/src/components/terminal-pane/terminal-host-workspace-file.ts', - 'src/renderer/src/components/terminal-pane/terminal-host-file-link-existence.ts', - 'src/renderer/src/runtime/runtime-terminal-artifact-read.ts', - // #30130's Disconnect sources merged with this lane skipped. - 'src/main/ipc/ssh/ssh-host-user-disconnect.ts', - 'src/main/ipc/ssh/ssh-connect-flow.ts', - 'src/main/ipc/ssh/ssh-connection-handlers.ts', - 'src/main/ipc/runtime-environments/runtime-environment-connectivity-handlers.ts', - 'src/main/ipc/runtime-environments/runtime-environment-manual-disconnect.ts', - 'src/main/ssh/ssh-connection-manager.ts', - 'src/main/ssh/ssh-user-disconnect.ts', - 'src/renderer/src/runtime/web-session-close-intent.ts', - 'src/renderer/src/runtime/web-session-close-replay.ts' - ], - spec - ) - expect(classifyE2eJobs(JSON.stringify([spec])), spec).toEqual({ - e2e_run_changed: false, - e2e_needs_build: true - }) - } - }) -}) - -function selectsManagedSshLane(file) { - return selectPrE2eSpecs([file]).some((spec) => spec.startsWith(MANAGED_SSH_E2E_SPEC_PREFIX)) -} - -describe('managed-SSH remote sources routing', () => { - it('gives every managed-SSH spec a source route', () => { - const routed = new Set(PR_E2E_SOURCE_ROUTES.flatMap((route) => route.specs)) - const specs = readdirSync(join(projectDir, 'tests/e2e')) - .filter((name) => /^ssh-orcad-.*\.spec\.ts$/.test(name)) - .map((name) => `tests/e2e/${name}`) - expect(specs.length).toBeGreaterThan(0) - expect(specs.filter((spec) => !routed.has(spec))).toEqual([]) - }) - - it('starts the lane for remote product sources no hand-kept route names', () => { - for (const file of [ - // #30130 and #30120 merged with the lane skipped. - 'src/main/ipc/ssh/ssh.ts', - 'src/cli/runtime/serve-orcad-launch.ts', - 'src/main/orcad/orcad-entry.ts', - 'src/main/ssh/ssh-connection.ts', - 'src/main/ipc/runtime-environments/runtime-environment-status-probe.ts', - 'src/main/ipc/runtime-environments/orcad-runtime-lifecycle-handlers.ts', - 'src/shared/protocol-version.ts', - 'src/shared/remote-runtime-client-error-classification.ts', - // #30047 and #30060: sidebar sleep and project removal. - 'src/renderer/src/components/sidebar/sleep-worktree-flow.ts', - 'src/renderer/src/components/sidebar/ssh-host-remove-resolution.ts', - 'src/renderer/src/store/repos/repo-removal.ts' - ]) { - expect(existsSync(join(projectDir, file)), file).toBe(true) - expect(selectsManagedSshLane(file), file).toBe(true) - } - }) - - it('leaves unit tests and local-only sources alone', () => { - for (const file of [ - 'src/main/ssh/ssh-connection.test.ts', - 'src/main/ipc/ssh/ssh.test.ts', - 'src/main/daemon/daemon-server.ts', - 'src/main/runtime/rpc/methods/accounts.ts', - 'src/renderer/src/components/sidebar/WorktreeCard.tsx', - 'src/renderer/src/store/repos/repo-ordering.ts', - 'docs/STYLEGUIDE.md' - ]) { - expect(selectsManagedSshLane(file), file).toBe(false) - } - }) -}) diff --git a/config/scripts/pr-preflight-gates.test.mjs b/config/scripts/pr-preflight-gates.test.mjs deleted file mode 100644 index 29831b14ef09..000000000000 --- a/config/scripts/pr-preflight-gates.test.mjs +++ /dev/null @@ -1,437 +0,0 @@ -import { readFileSync, readdirSync } from 'node:fs' -import { runInNewContext } from 'node:vm' -import { expect, it } from 'vitest' -import { runProcess } from '@orca/process-host' -import { parse } from 'yaml' -import { classifyPrJobs } from './pr-code-change-scope.mjs' - -const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) -const preflight = workflow.jobs.preflight -const steps = preflight.steps -const compiler = steps.find((step) => step.id === 'typecheck') -const compilerJoin = steps.findIndex((step) => step.wait === 'typecheck') -const plan = steps.find((step) => step.id === 'unit-plan') - -it('shares one setup and overlaps the compiler only after type-aware checks finish', () => { - expect(workflow.jobs.static_analysis).toBeUndefined() - expect(workflow.jobs.typecheck).toBeUndefined() - expect(preflight.needs).toEqual(['code_paths']) - expect(preflight['runs-on']).toBe('ubuntu-24.04-arm') - const installs = steps.filter( - (step) => step.uses === './.github/actions/install-node-dependencies' - ) - expect(installs).toHaveLength(2) - for (const install of installs) { - expect(install.with['native-runtime']).toBe('node') - expect(install.with['node-version']).toBe('24') - expect(install.with['persist-native-cache']).not.toBe('false') - } - expect(steps[0].with['fetch-depth']).toBeGreaterThanOrEqual(2) - expect(compiler.background).toBe(true) - expect(compiler.run).toContain('pnpm run typecheck:ci') - const bunSetup = steps.find((step) => step.name === 'Set up Bun for preflight checks') - expect(bunSetup.with['bun-version-file']).toBe('config/.bun-version') - expect(bunSetup.if).toBeUndefined() - expect(plan.background).toBe(true) - expect(plan.run.trim().split('\n')).toEqual([ - 'if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi', - 'node config/scripts/ci-unit-plan.mjs' - ]) - expect(plan.env.ORCA_UNIT_SELECTION_MODE).toContain('vars.ORCA_UNIT_SELECTION_MODE') - expect(plan.env.ORCA_UNIT_FULL_SHARD_COUNT).toBe("${{ vars.ORCA_UNIT_FULL_SHARD_COUNT || '5' }}") - expect(steps.indexOf(plan)).toBeLessThan(compilerJoin) - expect(steps.indexOf(compiler)).toBeGreaterThan( - steps.findIndex((step) => step.wait === 'changed-code-quality') - ) - expect(steps.indexOf(compiler)).toBeLessThan( - steps.findIndex((step) => step.id === 'localization-extraction') - ) - expect(compilerJoin).toBeGreaterThan( - steps.findIndex((step) => step.wait?.includes('localization-extraction')) - ) -}) - -it('uses the shared Bun pin for every current-toolchain workflow', () => { - expect(readFileSync('config/.bun-version', 'utf8').trim()).toBe('1.4.3') - for (const file of readdirSync('.github/workflows').filter((name) => /\.ya?ml$/.test(name))) { - const current = parse(readFileSync(`.github/workflows/${file}`, 'utf8')) - for (const job of Object.values(current.jobs)) { - for (const step of job.steps ?? []) { - if (!String(step.uses).startsWith('oven-sh/setup-bun@')) { - continue - } - expect(step.with['bun-version-file'], file).toBe('config/.bun-version') - expect(step.with['bun-version'], file).toBeUndefined() - } - } - } -}) - -it('keeps the Bun anti-slop scan aligned with the local audit', () => { - const audit = steps.find((step) => step.name === 'Reject low-evidence patterns') - const manifest = JSON.parse(readFileSync('package.json', 'utf8')) - const localCommand = manifest.scripts['audit:anti-slop'].split('&&').at(-1).trim() - expect(audit.run.trim().split('\n')[0]).toBe('node config/scripts/sync-anti-slop-plugin.mjs') - expect( - audit.run - .trim() - .split('\n') - .at(-1) - .replace('bun --smol node_modules/oxlint/bin/oxlint', 'oxlint') - ).toBe(localCommand) -}) - -it('requires physical preflight success before publishing shards and admitting consumers', () => { - const join = steps.findIndex((step) => step.wait === 'unit-plan') - const upload = steps.findIndex((step) => step.uses === 'actions/upload-artifact@v7') - expect(join).toBeGreaterThan(compilerJoin) - expect(upload).toBeGreaterThan(join) - expect(steps[upload]['continue-on-error']).toBeUndefined() - expect(steps[upload].with.name).toBe('unit-selection-attempt-${{ github.run_attempt }}') - expect(preflight.outputs.shards).toBe('${{ steps.unit-plan.outputs.shards }}') - expect(workflow.jobs.test.with.shards).toBe('${{ needs.preflight.outputs.shards }}') - // This fork removed the Windows PR packaging lane. - expect(workflow.jobs.package_windows).toBeUndefined() - for (const job of ['test', 'package']) { - expect(workflow.jobs[job].needs).toEqual(['code_paths', 'preflight']) - } - for (const result of ['success', 'failure', 'cancelled', 'skipped']) { - const admitted = runInNewContext(workflow.jobs.test.if, { - cancelled: () => false, - needs: { code_paths: { result: 'success', outputs: { test: 'true' } }, preflight: { result } } - }) - expect(admitted, result).toBe(result === 'success') - } - const verify = workflow.jobs.verify.steps.find( - (step) => step.name === 'Require successful checks' - ) - expect(verify.env.PREFLIGHT).toBe('${{ needs.preflight.result }}') - expect(verify.env.PREFLIGHT_SHOULD_RUN).toBe( - "${{ needs.code_paths.outputs.static_analysis == 'true' || needs.code_paths.outputs.typecheck == 'true' }}" - ) - expect(verify.run).toContain('check_job preflight "$PREFLIGHT" "$PREFLIGHT_SHOULD_RUN"') - expect(workflow.jobs.verify.needs).toContain('preflight') - expect(workflow.jobs.verify.needs).not.toContain('typecheck') -}) - -it.each([ - { name: 'both phases succeed', phases: ['true', 'true'], result: 'success', admitted: true }, - { name: 'static-only succeeds', phases: ['true', 'false'], result: 'success', admitted: true }, - { name: 'type-only succeeds', phases: ['false', 'true'], result: 'success', admitted: true }, - { name: 'static-only fails', phases: ['true', 'false'], result: 'failure', admitted: false }, - { name: 'type-only fails', phases: ['false', 'true'], result: 'failure', admitted: false }, - { name: 'both phases fail', phases: ['true', 'true'], result: 'failure', admitted: false }, - { name: 'preflight is cancelled', result: 'cancelled', admitted: false }, - { name: 'preflight is unfinished', result: undefined, admitted: false }, - { name: 'both phases unselected', phases: ['false', 'false'], result: 'skipped', admitted: true }, - { name: 'required static skips', phases: ['true', 'false'], result: 'skipped', admitted: false }, - { - name: 'required compiler skips', - phases: ['false', 'true'], - result: 'skipped', - admitted: false - }, - { name: 'both required phases skip', result: 'skipped', admitted: false }, - { - name: 'unselected preflight fails', - phases: ['false', 'false'], - result: 'failure', - admitted: false - }, - { - name: 'static selection missing', - phases: [undefined, 'false'], - result: 'skipped', - admitted: false - }, - { - name: 'compiler selection missing', - phases: ['false', undefined], - result: 'skipped', - admitted: false - }, - { name: 'selection unknown', phases: ['', 'false'], result: 'skipped', admitted: false }, - { - name: 'docs have no route', - phases: ['false', 'false'], - result: 'skipped', - route: 'false', - admitted: false - }, - { name: 'source has no route', result: 'success', route: 'false', admitted: false }, - { name: 'route missing', result: 'success', route: '', admitted: false }, - { name: 'path detection fails', result: 'success', pathsResult: 'failure', admitted: false }, - { name: 'path detection skips', result: 'skipped', pathsResult: 'skipped', admitted: false }, - { name: 'workflow cancelled after success', result: 'success', cancelled: true, admitted: false }, - { - name: 'workflow cancelled with unselected phases', - phases: ['false', 'false'], - result: 'skipped', - cancelled: true, - admitted: false - } -])('admits advisory E2E only after eligible preflight: $name', (scenario) => { - const [static_analysis, typecheck] = scenario.phases ?? ['true', 'true'] - expect(workflow.jobs.e2e.needs).toEqual(['code_paths', 'preflight']) - const admitted = runInNewContext(workflow.jobs.e2e.if, { - cancelled: () => scenario.cancelled ?? false, - needs: { - code_paths: { - result: scenario.pathsResult ?? 'success', - outputs: { static_analysis, typecheck, e2e_should_run: scenario.route ?? 'true' } - }, - preflight: { result: scenario.result } - } - }) - expect(admitted).toBe(scenario.admitted) -}) - -it.each(['true', 'false'])( - 'preserves advisory E2E on ready-for-review only with proven required-check reuse: %s', - (reused) => { - const scope = classifyPrJobs(['src/main/runtime/orca-runtime.ts']) - const context = { - steps: { - readiness: { outputs: { reused } }, - filter: { - outputs: Object.fromEntries( - Object.entries(scope).map(([key, value]) => [key, String(value)]) - ) - }, - e2e_filter: { outputs: { should_run: 'true' } } - } - } - const outputs = Object.fromEntries( - ['static_analysis', 'typecheck', 'e2e_should_run'].map((name) => [ - name, - String(runInNewContext(workflow.jobs.code_paths.outputs[name].slice(3, -2), context)) - ]) - ) - expect(outputs).toEqual({ - static_analysis: reused === 'true' ? 'false' : 'true', - typecheck: reused === 'true' ? 'false' : 'true', - e2e_should_run: 'true' - }) - const needs = { code_paths: { result: 'success', outputs }, preflight: { result: 'skipped' } } - expect(runInNewContext(preflight.if, { needs })).toBe(reused !== 'true') - expect(runInNewContext(workflow.jobs.e2e.if, { needs, cancelled: () => false })).toBe( - reused === 'true' - ) - expect(workflow.jobs.verify.needs).not.toContain('e2e') - } -) - -it('pins every foreground and background step to its selected phase', () => { - const staticPhase = "needs.code_paths.outputs.static_analysis == 'true'" - const typePhase = "needs.code_paths.outputs.typecheck == 'true'" - const foreground = steps.filter( - (step) => !step.background && /outputs\.(static_analysis|typecheck)/.test(step.if ?? '') - ) - expect(foreground.map((step) => [step.name ?? step.run ?? step.uses, step.if])).toEqual([ - ['Reject low-evidence patterns', staticPhase], - ['Enforce type-aware code-quality baseline', staticPhase], - [ - './.github/actions/install-mobile-dependencies', - `${staticPhase} && needs.code_paths.outputs.mobile_dependencies == 'true'` - ], - ['Verify locked audit tools', staticPhase], - ['Enforce React Doctor on changed lines', staticPhase], - ['Check Zustand selector fan-out budget', staticPhase], - ['Check reliability gate manifest', staticPhase], - ['Enforce dead design-system classes', staticPhase], - ['Check VM runtime rollback compatibility', staticPhase], - ['Enforce max-lines ratchet', staticPhase], - ['Enforce ts-nocheck ratchet', staticPhase], - ['Enforce runtime Electron-import ratchet', staticPhase], - ['Enforce focus-read ratchet', staticPhase], - ['Enforce layout-writer ratchets', staticPhase], - ['Enforce deprecated runtime-owner importer ratchet', staticPhase], - ['Enforce path-literal ratchet', staticPhase], - ['Enforce worktree-id composer ratchet', staticPhase], - ['Enforce e2e typecheck ratchet', staticPhase], - ['Enforce legacy relay fence ratchet', staticPhase], - ['Check Node runtime pin', staticPhase], - ['Decide whether the orcad terminal smoke runs', staticPhase], - ['Verify the generated RPC params catalog', staticPhase], - ['Verify the generated ACP protocol schema', staticPhase], - ['Verify bundled skill guides', staticPhase], - ['Verify skill freshness manifest', staticPhase], - ['Verify localization coverage', staticPhase], - ['Guard against project-owned .d.ts in preload/shared', staticPhase], - ['Check feature wall asset budget', staticPhase], - ['Verify macOS entitlements', staticPhase], - ['actions/upload-artifact@v7', typePhase] - ]) - expect( - steps - .filter((step) => step.background) - .map((step) => [step.id, step.env.PREFLIGHT_PHASE_SELECTED]) - ).toEqual([ - ['process-host-imports', `\${{ ${staticPhase} }}`], - ['main-sync-spawn-ratchet', `\${{ ${staticPhase} }}`], - ['root-lint', `\${{ ${staticPhase} }}`], - ['native-code-quality', `\${{ ${staticPhase} }}`], - ['changed-code-quality', `\${{ ${staticPhase} }}`], - ['typecheck', `\${{ ${typePhase} }}`], - ['localization-extraction', `\${{ ${staticPhase} }}`], - ['localization-catalogs', `\${{ ${staticPhase} }}`], - ['unit-plan', `\${{ ${typePhase} }}`] - ]) -}) - -it.each([ - { changed: ['README.md'], static_analysis: false, typecheck: false, mobile_dependencies: false }, - { - changed: ['mobile/src/App.tsx'], - static_analysis: true, - typecheck: false, - mobile_dependencies: true - }, - { - changed: ['cloud/package.json'], - static_analysis: false, - typecheck: false, - mobile_dependencies: false - }, - { - changed: ['src/main/index.ts'], - static_analysis: true, - typecheck: true, - mobile_dependencies: false - }, - { - changed: ['mobile/src/App.tsx', 'src/main/index.ts'], - static_analysis: true, - typecheck: true, - mobile_dependencies: true - }, - { changed: [], static_analysis: true, typecheck: true, mobile_dependencies: true } -])('preserves exact phase selection for unrelated paths: $changed', ({ changed, ...expected }) => { - const scope = classifyPrJobs(changed) - expect({ - static_analysis: scope.static_analysis, - typecheck: scope.typecheck, - mobile_dependencies: scope.mobile_dependencies - }).toEqual(expected) - const needs = { - code_paths: { - outputs: Object.fromEntries(Object.entries(scope).map(([key, value]) => [key, String(value)])) - } - } - expect(runInNewContext(preflight.if, { needs })).toBe( - expected.static_analysis || expected.typecheck - ) - expect(runInNewContext(compiler.env.PREFLIGHT_PHASE_SELECTED.slice(3, -2), { needs })).toBe( - expected.typecheck - ) - expect(scope.test).toBe(expected.typecheck) -}) - -it('registers successful no-op background work when a phase is unselected or already failed', () => { - for (const step of steps.filter((step) => step.background)) { - expect(step.if).toBe('!cancelled()') - expect(step.env.PREFLIGHT_PHASE_SELECTED).toContain('needs.code_paths.outputs.') - expect(step.env.PREFLIGHT_PRIOR_SUCCESS).toBe("${{ job.status == 'success' }}") - expect(step.run.split('\n')[0]).toBe( - 'if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi' - ) - } -}) - -it('shares import analysis without dropping either boundary gate', () => { - const imports = steps.find((step) => step.id === 'process-host-imports') - expect(imports.run).toContain('bun config/scripts/check-import-boundaries.mjs') - expect(steps.some((step) => step.name === 'Check compatibility-folder imports')).toBe(false) - expect(steps.find((step) => step.name === 'Verify localization coverage').run).toBe( - 'bun config/scripts/audit-localization-coverage.mjs --check' - ) -}) - -it('prepares an isolated Node 18 executable before the existing Node 24 dependency installs', () => { - const setup = steps.find((step) => step.id === 'node18_runtime') - const capture = steps.find((step) => step.id === 'node18_executable') - const selected = "needs.code_paths.outputs.managed_hook_node18 == 'true'" - expect(setup.uses).toBe('actions/setup-node@v6') - expect(setup.if).toBe(selected) - expect(setup.with).toEqual({ 'node-version': '18', 'package-manager-cache': false }) - expect(setup['continue-on-error']).toBe(true) - expect(capture['continue-on-error']).toBe(true) - expect(capture.run).toBe(`node -p '"executable=" + process.execPath' >> "$GITHUB_OUTPUT"`) - expect(steps.indexOf(capture)).toBeGreaterThan(steps.indexOf(setup)) - for (const install of steps.filter( - (step) => step.uses === './.github/actions/install-node-dependencies' - )) { - expect(steps.indexOf(install)).toBeGreaterThan(steps.indexOf(capture)) - expect(install.with['node-version']).toBe('24') - expect(install['continue-on-error']).toBeUndefined() - } - for (const outcome of ['success', 'failure', 'cancelled', 'skipped', '', undefined]) { - expect( - runInNewContext(capture.if, { - needs: { code_paths: { outputs: { managed_hook_node18: 'true' } } }, - steps: { node18_runtime: { outcome } } - }) - ).toBe(outcome === 'success') - } - const bun = steps.filter((step) => step.uses?.startsWith('oven-sh/setup-bun@')) - expect(bun).toHaveLength(1) - expect(bun[0].if).toBeUndefined() -}) - -it('runs the unchanged Node 18 smoke after every Node 24 step and preserves its required result', () => { - const smoke = steps.at(-1) - const receipt = workflow.jobs.managed_hook_node18 - expect(smoke.id).toBe('managed_hook_node18') - expect(smoke.if).toBe("success() && needs.code_paths.outputs.managed_hook_node18 == 'true'") - expect(smoke.env.NODE18_EXECUTABLE).toBe('${{ steps.node18_executable.outputs.executable }}') - expect(smoke.env.ORCA_BACKGROUND_LAUNCH).toBe('1') - expect(smoke.run.trim().split('\n')).toEqual([ - 'test -n "$NODE18_EXECUTABLE"', - '"$NODE18_EXECUTABLE" config/scripts/smoke-process-host-node18.mjs' - ]) - expect(smoke['continue-on-error']).toBe(true) - expect(preflight['continue-on-error']).toBeUndefined() - expect(preflight.outputs.managed_hook_node18).toBe('${{ steps.managed_hook_node18.outcome }}') - for (const background of steps.filter((step) => step.background)) { - const joined = steps.findIndex((step) => - Array.isArray(step.wait) ? step.wait.includes(background.id) : step.wait === background.id - ) - expect(joined, background.id).toBeGreaterThan(steps.indexOf(background)) - expect(joined, background.id).toBeLessThan(steps.indexOf(smoke)) - } - expect(receipt.name).toBe('managed hooks on Node 18') - expect(receipt['runs-on']).toBe('ubuntu-slim') - expect(receipt.steps).toHaveLength(1) - expect(receipt.steps[0].uses).toBeUndefined() - expect(receipt.steps[0].env.MANAGED_HOOK_NODE18).toBe( - '${{ needs.preflight.outputs.managed_hook_node18 }}' - ) - expect(receipt['continue-on-error']).toBeUndefined() - expect(receipt.steps[0]['continue-on-error']).toBeUndefined() -}) - -it.skipIf(process.platform === 'win32')( - 'fails the smoke for a missing captured executable and requires actual success in the receipt', - async () => { - const smoke = steps.at(-1) - const receipt = workflow.jobs.managed_hook_node18.steps[0] - const cases = [ - [smoke.run, 'NODE18_EXECUTABLE', '', 1], - ...['success', 'failure', 'cancelled', 'skipped', '', 'unknown'].map((outcome) => [ - receipt.run, - 'MANAGED_HOOK_NODE18', - outcome, - outcome === 'success' ? 0 : 1 - ]) - ] - for (const [script, variable, value, expected] of cases) { - const result = await runProcess({ - program: 'bash', - args: ['-e', '-c', script], - env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1', [variable]: value }, - timeoutMs: 10_000 - }) - expect(result.code, `${variable}=${value}: ${result.stderr}`).toBe(expected) - } - } -) diff --git a/config/scripts/pr-ready-check-reuse.test.mjs b/config/scripts/pr-ready-check-reuse.test.mjs deleted file mode 100644 index f21e4918f497..000000000000 --- a/config/scripts/pr-ready-check-reuse.test.mjs +++ /dev/null @@ -1,177 +0,0 @@ -import { readFileSync } from 'node:fs' -import { afterEach, describe, expect, it, vi } from 'vitest' -import { parse } from 'yaml' -import { - PR_CHECK_JOBS, - workflowWithoutForkGuard -} from './fork-ownership-guard/pr-workflow-contract.mjs' -import { - lookupReadyCheckRun, - prCheckRunTitle, - reusablePrCheckRun -} from './pr-ready-check-reuse.mjs' - -const identity = { - number: 42, - sourceSha: 'a'.repeat(40), - workflowSha: 'b'.repeat(40), - headSha: 'c'.repeat(40), - runId: '456' -} -const passed = { - id: 123, - path: '.github/workflows/pr.yml', - event: 'pull_request', - status: 'completed', - conclusion: 'success', - head_sha: identity.headSha, - display_title: prCheckRunTitle(identity) -} -const event = { - action: 'ready_for_review', - pull_request: { number: 42, draft: false, head: { sha: identity.headSha } } -} -const env = { - GITHUB_EVENT_NAME: 'pull_request', - GITHUB_SHA: identity.sourceSha, - PR_CHECK_WORKFLOW_SHA: identity.workflowSha, - GITHUB_RUN_ID: identity.runId, - GITHUB_REPOSITORY: 'stablyai/orca', - GH_TOKEN: 'read-only-test-token' -} -const response = (runs) => ({ ok: true, json: async () => ({ workflow_runs: runs }) }) -const workflow = workflowWithoutForkGuard(parse(readFileSync('.github/workflows/pr.yml', 'utf8'))) - -afterEach(() => vi.restoreAllMocks()) - -describe('ready-for-review required check reuse', () => { - it('reuses a completed success only for the identical PR, merge source and workflow', () => { - expect(reusablePrCheckRun([passed], identity)).toBe(passed) - expect(reusablePrCheckRun([], identity)).toBeUndefined() - expect( - reusablePrCheckRun( - [{ ...passed, display_title: prCheckRunTitle({ ...identity, unitMode: 'selected' }) }], - identity - ) - ).toBeUndefined() - for (const key of ['number', 'sourceSha', 'workflowSha', 'headSha', 'runId']) { - const changed = key === 'number' ? 43 : key === 'runId' ? '123' : 'd'.repeat(40) - expect(reusablePrCheckRun([passed], { ...identity, [key]: changed }), key).toBeUndefined() - } - for (const key of ['number', 'sourceSha', 'workflowSha', 'headSha']) { - expect(reusablePrCheckRun([passed], { ...identity, [key]: undefined }), key).toBeUndefined() - } - }) - - it.each([ - { status: 'in_progress' }, - { status: 'queued' }, - { conclusion: 'failure' }, - { conclusion: 'cancelled' }, - { conclusion: 'skipped' }, - { event: 'push' }, - { path: '.github/workflows/other.yml' }, - { display_title: 'Previous workflow without source provenance' }, - { id: -1 } - ])('rejects unusable evidence %j', (change) => { - expect(reusablePrCheckRun([{ ...passed, ...change }], identity)).toBeUndefined() - }) - - it('bounds the read-only lookup to this workflow and PR head', async () => { - const request = vi.fn(async () => response([passed])) - expect(await lookupReadyCheckRun(env, event, request)).toBe(passed) - expect(request).toHaveBeenCalledTimes(1) - const [url, options] = request.mock.calls[0] - expect(url.origin).toBe('https://api.github.com') - expect(url.pathname).toBe('/repos/stablyai/orca/actions/workflows/pr.yml/runs') - expect(Object.fromEntries(url.searchParams)).toEqual({ - event: 'pull_request', - head_sha: identity.headSha, - status: 'success', - per_page: '20' - }) - expect(options.method).toBeUndefined() - expect(options.signal).toBeInstanceOf(AbortSignal) - }) - - it('runs ordinary pushes and draft checks without consulting old results', async () => { - const request = vi.fn() - for (const action of ['opened', 'synchronize', 'reopened', 'converted_to_draft']) { - expect(await lookupReadyCheckRun(env, { ...event, action }, request)).toBeUndefined() - } - expect( - await lookupReadyCheckRun( - env, - { ...event, pull_request: { ...event.pull_request, draft: true } }, - request - ) - ).toBeUndefined() - expect(await lookupReadyCheckRun({ ...env, GH_TOKEN: '' }, event, request)).toBeUndefined() - expect( - await lookupReadyCheckRun({ ...env, GITHUB_EVENT_NAME: 'push' }, event, request) - ).toBeUndefined() - expect(request).not.toHaveBeenCalled() - }) - - it('falls back to full checks when the API is unavailable or evidence is malformed', async () => { - vi.spyOn(console, 'log').mockImplementation(() => {}) - for (const request of [ - async () => ({ ok: false, status: 403 }), - async () => ({ ok: false, status: 429 }), - async () => ({ ok: true, json: async () => ({}) }), - async () => { - throw new Error('Network unavailable') - } - ]) { - expect(await lookupReadyCheckRun(env, event, request)).toBeUndefined() - } - expect(await lookupReadyCheckRun(env, event, async () => response([]))).toBeUndefined() - }) - - it('keeps required skips conditional on proof and leaves advisory routing eligible', () => { - expect(workflow['run-name']).toBe( - "PR ${{ github.event.pull_request.number }} | source ${{ github.sha }} | workflow ${{ github.workflow_sha }} | unit ${{ github.event.pull_request.draft && vars.ORCA_UNIT_SELECTION_MODE == 'selected' && 'selected' || 'full' }}" - ) - expect(workflow.on.pull_request.types).toContain('ready_for_review') - const detector = workflow.jobs.code_paths - expect(detector.permissions).toEqual({ contents: 'read', actions: 'read' }) - const readiness = detector.steps.find((step) => step.id === 'readiness') - expect(readiness.if).toBe("github.event.action == 'ready_for_review'") - expect(readiness.env.PR_CHECK_WORKFLOW_SHA).toBe('${{ github.workflow_sha }}') - expect(readiness.run).toBe('node config/scripts/pr-ready-check-reuse.mjs') - expect(detector.steps[0].with['sparse-checkout']).toContain( - '/config/scripts/pr-ready-check-reuse.mjs' - ) - for (const job of PR_CHECK_JOBS) { - expect(detector.outputs[job]).toBe( - `\${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.${job} }}` - ) - } - expect(detector.outputs.should_run).toBe('${{ steps.filter.outputs.should_run }}') - for (const name of ['test_files', 'native_ime_source_changed']) { - expect(detector.outputs[name]).toBe(`\${{ steps.e2e_filter.outputs.${name} }}`) - } - expect(detector.steps.find((step) => step.id === 'e2e_filter').if).toBe( - "github.event.pull_request.draft != true && steps.filter.outputs.should_run == 'true'" - ) - expect(workflow.jobs.verify.needs).toEqual([ - 'code_paths', - 'preflight', - ...new Set( - PR_CHECK_JOBS.filter((job) => job !== 'static_analysis' && job !== 'typecheck').map( - (job) => (job === 'orcad_browser' ? 'test' : job) - ) - ) - ]) - }) - - it.each(['pr-test-loc.yml'])( - 'does not rerun draft-independent %s on readiness changes', - (file) => { - const source = readFileSync(`.github/workflows/${file}`, 'utf8') - const independent = parse(source) - expect(independent.on.pull_request.types).toEqual(['opened', 'synchronize', 'reopened']) - expect(source).not.toContain('pull_request.draft') - } - ) -}) diff --git a/config/scripts/pr-workflow-parallelism.test.mjs b/config/scripts/pr-workflow-parallelism.test.mjs deleted file mode 100644 index 8d82ceb93646..000000000000 --- a/config/scripts/pr-workflow-parallelism.test.mjs +++ /dev/null @@ -1,80 +0,0 @@ -import { readFileSync } from 'node:fs' -import { runInNewContext } from 'node:vm' -import { parse } from 'yaml' -import { describe, expect, it } from 'vitest' - -const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) -const unitTestWorkflow = parse(readFileSync('.github/workflows/unit-tests.yml', 'utf8')) -const evidenceWorkflow = parse( - readFileSync('.github/workflows/unit-selection-evidence.yml', 'utf8') -) - -describe('PR workflow parallelism', () => { - it('enforces import boundaries in the required static-analysis checks', () => { - const guard = workflow.jobs.preflight.steps.find((step) => - step.run?.includes('bun config/scripts/check-import-boundaries.mjs') - ) - expect(guard?.if).toBe('!cancelled()') - expect(guard?.env.PREFLIGHT_PHASE_SELECTED).toBe( - "${{ needs.code_paths.outputs.static_analysis == 'true' }}" - ) - expect(guard?.background).toBe(true) - const typecheckIndex = workflow.jobs.preflight.steps.findIndex( - (step) => step.wait === 'typecheck' - ) - const waitIndex = workflow.jobs.preflight.steps.findIndex((step) => step.wait === guard.id) - expect(waitIndex).toBeGreaterThan(typecheckIndex) - expect(guard?.['continue-on-error']).toBeUndefined() - }) - - it('keeps advisory unit-selection evidence off the gate', () => { - // It is continue-on-error, so it can never fail a PR. Living inside unit-tests.yml made a - // caller's `needs: test` wait for it anyway, holding verify ~36s past the last shard. Pinned - // here so it cannot drift back onto the critical path. - const evidence = workflow.jobs.unit_selection_evidence - expect(evidence.uses).toBe('./.github/workflows/unit-selection-evidence.yml') - expect(evidence.needs).toEqual(['test']) - for (const [result, cancelled, expected] of [ - ['success', false, true], - ['failure', false, true], - ['skipped', false, false], - ['cancelled', false, false], - ['success', true, false], - ['failure', true, false] - ]) { - expect( - runInNewContext(evidence.if.slice(3, -2), { - cancelled: () => cancelled, - needs: { test: { result } } - }) - ).toBe(expected) - } - expect(workflow.jobs.verify.needs).not.toContain('unit_selection_evidence') - expect(unitTestWorkflow.jobs.selection_evidence).toBeUndefined() - const job = evidenceWorkflow.jobs.selection_evidence - expect(job['continue-on-error']).toBe(true) - }) - - it('runs the advisory CLI without an action credential and preserves failures', async () => { - const steps = evidenceWorkflow.jobs.selection_evidence.steps - expect(steps.some((step) => step.uses?.startsWith('actions/setup-node@'))).toBe(false) - const comparison = steps.find((step) => step.name === 'Compare selection with full results') - expect(comparison.uses).toBe('actions/github-script@v8') - expect(comparison.with['github-token']).toBe('unused') - const env = { 'INPUT_GITHUB-TOKEN': 'test-token', GITHUB_STEP_SUMMARY: '/tmp/summary' } - const failure = new Error('comparison failed') - const result = runInNewContext(`(async () => { ${comparison.with.script} })()`, { - process: { env, execPath: '/bundled/node24' }, - exec: { - exec: async (command, args, options) => { - expect(command).toBe('/bundled/node24') - expect(args).toEqual(['config/scripts/ci-unit-selection-review.mjs', 'unit-evidence']) - expect(options.env).toEqual({ GITHUB_STEP_SUMMARY: '/tmp/summary' }) - throw failure - } - } - }) - await expect(result).rejects.toBe(failure) - expect(env['INPUT_GITHUB-TOKEN']).toBe('test-token') - }) -}) From 7746da916ddab9f90a890928a2eaabd31dd5d2d1 Mon Sep 17 00:00:00 2001 From: zpyoung Date: Mon, 19 Oct 2026 15:41:07 -0500 Subject: [PATCH 3/3] test(ci): retire the remaining checks of the removed PR jobs Drop linux-package-fixture-cache's upstream test (deleted exception) and the fork-owned assertions that read pr.yml's package and shell_contracts jobs. --- config/fork-ownership.json | 6 + .../ci-dependency-download-cache.test.mjs | 29 --- .../linux-package-fixture-cache.test.mjs | 178 ------------------ config/scripts/run-sandboxed-test-shards.mjs | 2 +- .../run-sandboxed-test-shards.test.mjs | 10 +- 5 files changed, 10 insertions(+), 215 deletions(-) delete mode 100644 config/scripts/linux-package-fixture-cache.test.mjs diff --git a/config/fork-ownership.json b/config/fork-ownership.json index 12c05f77ac6f..b3e80d2c07f5 100644 --- a/config/fork-ownership.json +++ b/config/fork-ownership.json @@ -6355,6 +6355,12 @@ "status": "permanent", "deleted": true }, + { + "path": "config/scripts/linux-package-fixture-cache.test.mjs", + "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", + "status": "permanent", + "deleted": true + }, { "path": "config/scripts/pr-preflight-gates.test.mjs", "reason": "Asserts PR jobs and preflight steps the fork's trimmed pr.yml no longer runs; the fork keeps only lint, typecheck, unit tests, cross-version wire and the ownership guard.", diff --git a/config/scripts/ci-dependency-download-cache.test.mjs b/config/scripts/ci-dependency-download-cache.test.mjs index d5041cdee0e5..e0c875093d25 100644 --- a/config/scripts/ci-dependency-download-cache.test.mjs +++ b/config/scripts/ci-dependency-download-cache.test.mjs @@ -423,35 +423,6 @@ describe('CI dependency download caches', () => { }) } }) - - it('seeds the existing Linux PR tool cache from successful main x64 release builds', () => { - const packaging = workflow('pr').jobs.package - const consumer = packaging.steps.find( - (step) => step.name === 'Cache electron-builder downloads' - ) - const release = workflow('release-cut').jobs.build - const combined = release.steps.find((step) => step.name === 'Cache electron-builder downloads') - const writer = release.steps.find( - (step) => step.name === 'Seed shared Linux packaging downloads' - ) - const linux = release.strategy.matrix.include.find((entry) => entry.platform === 'linux-x64') - - expect(packaging['runs-on']).toBe(linux.os) - expect(writer.if).toBe("matrix.platform == 'linux-x64' && github.ref == 'refs/heads/main'") - expect(writer.uses).toBe('actions/cache@v5') - expect(writer.with.path).toBe(consumer.with.path) - expect(writer.with.key).toBe(consumer.with.key) - expect(writer.with['restore-keys']).toBeUndefined() - expect(writer.with['lookup-only']).toBe(true) - expect(release.steps.indexOf(writer)).toBeGreaterThan(release.steps.indexOf(combined)) - expect(consumer.uses).toBe('actions/cache/restore@v5') - expect(consumer.with['restore-keys'].trim()).toBe('electron-builder-linux-') - expect(combined.uses).toBe('actions/cache@v5') - expect(linux.eb_cache_path.trim().split('\n')).toEqual([ - '~/.cache/electron', - '~/.cache/electron-builder' - ]) - }) }) it('shares Electron archives with PRs without uploading PR-local copies', () => { diff --git a/config/scripts/linux-package-fixture-cache.test.mjs b/config/scripts/linux-package-fixture-cache.test.mjs deleted file mode 100644 index 81465a38a29e..000000000000 --- a/config/scripts/linux-package-fixture-cache.test.mjs +++ /dev/null @@ -1,178 +0,0 @@ -import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { runInNewContext } from 'node:vm' -import { delimiter, join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' -import { parse } from 'yaml' -import { runProcessSync } from '@orca/process-host' -import { classifyPrJobs } from './pr-code-change-scope.mjs' - -const action = parse( - readFileSync('.github/actions/prepare-linux-package-fixture/action.yml', 'utf8') -) -const load = action.runs.steps.find((step) => step.id === 'image') -const workflow = (name) => parse(readFileSync(`.github/workflows/${name}.yml`, 'utf8')) -let directory - -afterEach(() => { - if (directory) { - rmSync(directory, { recursive: true, force: true }) - } -}) - -function exercise({ - hit = false, - save = false, - loadFails = false, - inspectFails = false, - fixture = 'cli-launch-contract' -} = {}) { - directory = mkdtempSync(join(tmpdir(), 'orca-package-cache-')) - const bin = join(directory, 'bin') - mkdirSync(bin) - const commandLog = join(directory, 'commands') - const output = join(directory, 'output') - writeFileSync(commandLog, '') - writeFileSync(output, '') - const docker = join(bin, 'docker') - writeFileSync( - docker, - '#!/bin/bash\n' + - 'echo "$*" >> "$COMMAND_LOG"\n' + - 'if [[ "$1" == load && "$LOAD_FAILS" == true ]]; then exit 1; fi\n' + - 'if [[ "$1 $2" == "image inspect" ]]; then\n' + - ' if [[ "$INSPECT_FAILS" == true ]]; then exit 1; fi\n' + - ' echo sha256:fixture-image\n' + - 'fi\n' - ) - chmodSync(docker, 0o755) - const result = runProcessSync({ - program: 'bash', - args: ['-c', load.run], - env: { - ...process.env, - ORCA_BACKGROUND_LAUNCH: '1', - PATH: `${bin}${delimiter}${process.env.PATH ?? ''}`, - RUNNER_TEMP: directory, - GITHUB_OUTPUT: output, - COMMAND_LOG: commandLog, - FIXTURE: fixture, - CACHE_HIT: String(hit), - SAVE_CACHE: String(save), - LOAD_FAILS: String(loadFails), - INSPECT_FAILS: String(inspectFails) - }, - timeoutMs: 10_000 - }) - expect(result.code, result.stderr).toBe(0) - return { commands: readFileSync(commandLog, 'utf8'), output: readFileSync(output, 'utf8') } -} - -describe.runIf(process.platform !== 'win32')('fixture cache fallbacks', () => { - it('leaves a PR cache miss to the existing runner without building or exporting', () => { - expect(exercise()).toEqual({ commands: '', output: '' }) - }) - - it.each([{ loadFails: true }, { inspectFails: true }])( - 'discards an unusable restored cache: %j', - (failure) => { - const result = exercise({ hit: true, ...failure }) - expect(result.output).toBe('') - expect(result.commands).toContain('load --input') - expect(result.commands).not.toMatch(/(?:build|save) /) - } - ) - - it('returns the local image tag after a successful load', () => { - const result = exercise({ hit: true }) - expect(result.output).toBe('image=orca-package-fixture-cli-launch-contract:cache\n') - expect(result.commands).not.toMatch(/(?:build|save) /) - }) - - it('loads the daemon shutdown fixture without running package or source tests in the cache action', () => { - const result = exercise({ hit: true, fixture: 'daemon-shutdown-descendants' }) - expect(result.output).toBe('image=orca-package-fixture-daemon-shutdown-descendants:cache\n') - expect(result.commands).not.toMatch(/(?:build|save|run) /) - }) - - it('builds and exports inline cache only when the warmer has no usable image', () => { - const result = exercise({ save: true }) - expect(result.commands).toContain( - 'build --platform linux/amd64 --build-arg BUILDKIT_INLINE_CACHE=1' - ) - expect(result.commands).toContain( - '--file config/docker/cli-launch-contract/Dockerfile config/docker/cli-launch-contract' - ) - expect(result.commands).toContain('save --output') - expect(result.output).toBe('image=orca-package-fixture-cli-launch-contract:cache\nbuilt=true\n') - }) -}) - -it('uses exact context/platform keys and the same restore-only action in package checks', () => { - const cache = action.runs.steps.find((step) => step.id === 'cache') - expect(cache.with.key).toContain('linux-amd64') - expect(cache.with.key).toContain("format('config/docker/{0}/**', inputs.fixture)") - expect(cache.with.key).toContain('.github/actions/prepare-linux-package-fixture/action.yml') - expect(cache.with['restore-keys']).toBeUndefined() - expect(action.inputs['save-cache'].default).toBe('false') - const prepared = workflow('pr').jobs.package.steps.filter((step) => - step.uses?.endsWith('/prepare-linux-package-fixture') - ) - expect(prepared.map((step) => step.with)).toEqual([ - { fixture: 'daemon-shutdown-descendants' }, - { fixture: 'headless-serve-shutdown' }, - { fixture: 'cli-launch-contract' } - ]) - const platformSource = workflow('ci-cache-warmup-platforms').jobs['warm-platforms'].uses - const warm = parse(readFileSync(platformSource, 'utf8')).jobs['warm-linux-package-fixtures'] - expect(warm.steps.slice(1).map((step) => step.with.fixture)).toEqual( - prepared.map((step) => step.with.fixture) - ) - expect( - warm.steps - .slice(1) - .every((step) => step.with['save-cache'] === "${{ github.event_name != 'pull_request' }}") - ).toBe(true) -}) - -it.each([ - ['pull_request', false], - ['push', true], - ['schedule', true], - ['workflow_dispatch', true] -])('keeps fixture seeding available without a redundant PR runner on %s', (event, runs) => { - const source = workflow('ci-cache-warmup-platforms').jobs['warm-platforms'].uses - const warm = parse(readFileSync(source, 'utf8')).jobs['warm-linux-package-fixtures'] - const context = { github: { event_name: event } } - expect(warm['runs-on']).toBe('ubuntu-latest') - expect(warm.steps.map((step) => step.uses)).toEqual([ - 'actions/checkout@v6', - './.github/actions/prepare-linux-package-fixture', - './.github/actions/prepare-linux-package-fixture', - './.github/actions/prepare-linux-package-fixture' - ]) - expect(runInNewContext(warm.if, context)).toBe(runs) - for (const step of warm.steps.slice(1)) { - expect(runInNewContext(step.with['save-cache'].slice(3, -2), context)).toBe(runs) - } - const consumers = workflow('pr').jobs.package.steps.filter((step) => - step.uses?.endsWith('/prepare-linux-package-fixture') - ) - expect(consumers.every((step) => step.with['save-cache'] === undefined)).toBe(true) - expect(consumers.map((step) => step.with.fixture)).toEqual( - warm.steps.slice(1).map((step) => step.with.fixture) - ) -}) - -it.each([ - '.github/actions/prepare-linux-package-fixture/action.yml', - 'config/docker/daemon-shutdown-descendants/Dockerfile', - 'config/docker/headless-serve-shutdown/Dockerfile', - 'config/docker/cli-launch-contract/Dockerfile' -])('validates a changed fixture in Linux packaging: %s', (file) => { - expect(classifyPrJobs([file])).toMatchObject({ - test: true, - package: true, - package_windows: false - }) -}) diff --git a/config/scripts/run-sandboxed-test-shards.mjs b/config/scripts/run-sandboxed-test-shards.mjs index a921738fc531..f7cc387faedf 100644 --- a/config/scripts/run-sandboxed-test-shards.mjs +++ b/config/scripts/run-sandboxed-test-shards.mjs @@ -61,7 +61,7 @@ const IMAGE_CONTEXT_ROOTS = [ 'native/windows-security/package.json' ] -/** Matches the serialized shell-contract selection in pr.yml. */ +/** The real-shell contracts the unit shards exclude, run serially. */ const SHELL_CONTRACT_SPECS = [ 'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts', 'src/main/daemon/shell-ready.test.ts', diff --git a/config/scripts/run-sandboxed-test-shards.test.mjs b/config/scripts/run-sandboxed-test-shards.test.mjs index 138ce1650094..bfa59403e5de 100644 --- a/config/scripts/run-sandboxed-test-shards.test.mjs +++ b/config/scripts/run-sandboxed-test-shards.test.mjs @@ -18,20 +18,16 @@ const projectDir = resolve(import.meta.dirname, '../..') const workflow = (name) => parse(readFileSync(resolve(projectDir, `.github/workflows/${name}.yml`), 'utf8')) -const shellRun = workflow('pr').jobs.shell_contracts.steps.find( - (step) => step.name === 'Test real shell contracts' -).run const unitShardEnv = workflow('unit-tests').jobs.test.steps.find( (step) => step.name === 'Test shard' ).env -const shellSpecs = shellRun.match(/src\/\S+\.test\.ts/g) - describe('sandbox lane selection', () => { - it('runs the CI shell contracts serially without stale spec paths', () => { + it('runs the shell contracts serially without stale spec paths', () => { const command = laneCommand({ lane: 'shell', extraArgs: [] }, 1) + const shellSpecs = command.filter((arg) => arg.endsWith('.test.ts')) - expect(command.filter((arg) => arg.endsWith('.test.ts')).sort()).toEqual(shellSpecs.sort()) + expect(shellSpecs.length).toBeGreaterThan(0) expect(command).toContain('--maxWorkers=1') expect(command.some((arg) => arg.startsWith('--shard='))).toBe(false) for (const spec of shellSpecs) {