diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9267e29dfb55..3e33da6cf1b4 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 @@ -594,24 +258,11 @@ 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 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 +285,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 +301,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 +310,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 +366,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 +374,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 +390,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 +431,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" diff --git a/config/fork-ownership.json b/config/fork-ownership.json index 8ad8d8cc219e..b3e80d2c07f5 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,71 @@ "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/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": "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 +8542,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 +8550,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-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/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/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/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') - }) -}) 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) {