diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed187baffd8..635c51b5929 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,6 +191,11 @@ jobs: packaging: ${{ steps.filter.outputs.packaging }} docs: ${{ steps.filter.outputs.docs }} structure: ${{ steps.filter.outputs.structure }} + # Narrow scopes for two paths the ci filter leaves out on purpose. Both are re-emitted by the + # validation step below, so a malformed filter output fails this job instead of silently + # skipping the check it selects. + setup_action: ${{ steps.narrow.outputs.setup_action }} + remote_helper: ${{ steps.narrow.outputs.remote_helper }} # Re-emitted by the scope step like `ci`: a missing value must fail this # job, not read as "no devlog change" and skip the only scan that covers it. privacy: ${{ steps.scope.outputs.privacy }} @@ -301,6 +306,19 @@ jobs: structure: - 'structure/**' - '.github/workflows/ci.yml' + # The composite action every Bun job runs. `ci` omits .github/actions/** for the same + # reason it omits docs-site/** and structure/**: a change that touches only the action + # would otherwise start the full matrix. Without this filter it started nothing, and the + # aggregate reported success over skips. The job it feeds runs the action on the three + # runner families and checks what it installed. Pull-request scope, like docs and + # structure; ci.yml is listed so an edit here verifies itself. + setup_action: + - '.github/actions/**' + - '.github/workflows/ci.yml' + # The Rust remote-workspace helper. Nothing else in CI builds it, and its sandbox is + # real only on macOS and Windows, so its job lints and tests the crate on all three. + remote_helper: + - 'native/remote-workspace-helper/**' # `privacy:scan` is a step of `gates`, and `gates` is gated on `ci` # above -- which does not list `devlog/**`. So the one diff class the # scan exists for, adding public devlog prose, was the one class that @@ -414,11 +432,32 @@ jobs: printf 'keyring_matrix=%s\n' "$keyring_matrix" >> "$GITHUB_OUTPUT" printf 'npm_global_matrix=%s\n' "$npm_global_matrix" >> "$GITHUB_OUTPUT" + - name: Assert the narrow scope outputs are usable + id: narrow + shell: bash + env: + SETUP_ACTION: ${{ steps.filter.outputs.setup_action }} + REMOTE_HELPER: ${{ steps.filter.outputs.remote_helper }} + run: | + set -euo pipefail + for pair in "setup_action=$SETUP_ACTION" "remote_helper=$REMOTE_HELPER"; do + case "${pair#*=}" in + true|false) + printf '%s\n' "$pair" >> "$GITHUB_OUTPUT" + ;; + *) + printf '::error::changes.outputs.%s was %q, expected true or false\n' "${pair%%=*}" "${pair#*=}" + exit 1 + ;; + esac + done + # The suite, split by file across four Linux runners. # - # `scripts/ci/run-bun-test-batches.sh` mirrors Bun's sorted round-robin shard - # assignment, then runs each shard in small batches so every batch gets a fresh - # Bun process. The helper prints the exact files before each batch and retries + # `scripts/ci/run-bun-test-batches.sh` assigns files to shards by the per-file + # durations recorded in `scripts/ci/test-durations.tsv` (sorted round-robin when + # nothing is recorded), then runs each shard in small batches so every batch gets + # a fresh Bun process. The helper prints the exact files before each batch and retries # nothing: a test failure, a process timeout and a Bun runtime crash each fail # the shard where they happen. A timeout or a crash is additionally swept one # file per process, after the shard has already failed, to attribute it. @@ -1082,6 +1121,78 @@ jobs: - name: Structure doc-map, ownership, and invariant bindings run: bun run structure:check + # The composite Bun setup, run on each runner family it serves, when a change touches only the + # action (see the setup_action filter). One step past the action proves it installed the runtime + # package.json declares; nothing else runs, so this never grows into a suite. + setup-action: + name: setup action ${{ matrix.os }} + needs: changes + if: needs.changes.outputs.setup_action == 'true' + runs-on: ${{ matrix.os }} + timeout-minutes: 5 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup project Bun + id: bun + uses: ./.github/actions/setup-project-bun + + - name: Require the runtime package.json declares + shell: bash + env: + RESOLVED: ${{ steps.bun.outputs.version }} + run: | + set -euo pipefail + declared="$(node -p "require('./package.json').dependencies.bun")" + installed="$(bun --version)" + echo "declared=$declared resolved=$RESOLVED installed=$installed" + if [ "$RESOLVED" != "$declared" ] || [ "$installed" != "$declared" ]; then + echo "::error::setup-project-bun resolved '$RESOLVED' and installed '$installed', but package.json declares '$declared'" + exit 1 + fi + + # The Rust remote-workspace helper, when a change touches it (see the remote_helper filter). + # Formatting once, then clippy and the crate's tests on each platform: the sandbox and the live + # confinement tests compile only on macOS and Windows, and Linux covers the protocol and stub. + remote-helper: + name: remote helper ${{ matrix.os }} + needs: changes + if: needs.changes.outputs.remote_helper == 'true' + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup Rust + uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # master + with: + toolchain: stable + components: rustfmt, clippy + + - name: Check Rust formatting + if: runner.os == 'Linux' + run: cargo fmt --manifest-path native/remote-workspace-helper/Cargo.toml --check + + - name: Run Rust clippy + run: cargo clippy --locked --manifest-path native/remote-workspace-helper/Cargo.toml --all-targets -- -D warnings + + - name: Run Rust tests + run: cargo test --locked --manifest-path native/remote-workspace-helper/Cargo.toml + # `gates` already runs `privacy:scan` on every event it runs for, so this job # covers exactly the pull requests `gates` skips -- its condition is the # complement of `gates`' own, restricted to a devlog change. A `ci.yml` edit @@ -1311,7 +1422,7 @@ jobs: # direct dependencies only, so a failing `select-windows-runner` would # otherwise reach this gate as nothing at all while its dependents report # `skipped`, which is the shape the step below is written to catch. - needs: [changes, select-windows-runner, test, storage-policy, api-usage, gates, platform-macos, macos-control, platform-windows, keyring-smoke, docker-smoke, docs-site-build, structure-gate, privacy-gate, npm-global-smoke, widget, desktop-shell] + needs: [changes, select-windows-runner, test, storage-policy, api-usage, gates, platform-macos, macos-control, platform-windows, keyring-smoke, docker-smoke, docs-site-build, structure-gate, privacy-gate, npm-global-smoke, widget, desktop-shell, setup-action, remote-helper] runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -1330,6 +1441,8 @@ jobs: CHANGES_PACKAGING: ${{ needs.changes.outputs.packaging }} CHANGES_DOCS: ${{ needs.changes.outputs.docs }} CHANGES_STRUCTURE: ${{ needs.changes.outputs.structure }} + CHANGES_SETUP_ACTION: ${{ needs.changes.outputs.setup_action }} + CHANGES_REMOTE_HELPER: ${{ needs.changes.outputs.remote_helper }} CHANGES_NATIVE: ${{ needs.changes.outputs.native }} CHANGES_PRIVACY: ${{ needs.changes.outputs.privacy }} GH_TOKEN: ${{ github.token }} @@ -1373,6 +1486,14 @@ jobs: if [ "$CHANGES_STRUCTURE" = "true" ]; then structure=requested fi + setup_action=not-requested + if [ "$CHANGES_SETUP_ACTION" = "true" ]; then + setup_action=requested + fi + remote_helper=not-requested + if [ "$CHANGES_REMOTE_HELPER" = "true" ]; then + remote_helper=requested + fi # privacy-gate runs exactly where `gates` (scoped) does not, for a devlog # change. Deriving it from `scoped` keeps the two scans complementary here # as they are in the jobs' own conditions. @@ -1400,6 +1521,7 @@ jobs: GATED_JOBS="$GATED_JOBS macos-control platform-windows docs-site-build" GATED_JOBS="$GATED_JOBS structure-gate widget" GATED_JOBS="$GATED_JOBS desktop-shell" + GATED_JOBS="$GATED_JOBS setup-action remote-helper" GATED_JOBS="$GATED_JOBS privacy-gate" expected_for() { @@ -1412,6 +1534,8 @@ jobs: npm-global-smoke) echo "$packaging" ;; docs-site-build) echo "$docs" ;; structure-gate) echo "$structure" ;; + setup-action) echo "$setup_action" ;; + remote-helper) echo "$remote_helper" ;; privacy-gate) echo "$privacy" ;; macos-control) echo "$dispatch" ;; platform-windows) echo "$windows" ;; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6047c14d91c..498fa9246e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,8 +74,52 @@ jobs: process.exit(1); } NODE - package-standalone: + + # Every publication precondition the dispatch can already decide, checked before any runner + # starts packaging: channel and dist-tag, every version source, the tag, the GitHub release, + # npm, the global tag ordering and the dev pre-move (scripts/ci/release-preflight.sh). + # + # Run 35783865160 packaged 2.62.0 for nineteen minutes and then failed the ordering gate in + # `publish` on v2.63.0-preview.20260923. That tag already existed when the run's first job + # started: the workflow-level `release` concurrency group above is one constant slot for every + # ref, so the stable run had waited for the preview run to finish. The runs were serialised; + # the check was in the wrong place. Because of that shared slot, this job sees whatever the + # previous release run published. + # + # It is an early answer, not the final one. Tags, releases and registry state can still move + # while a run packages (a hand-pushed tag, a first local publish), so `publish` repeats every + # one of these checks immediately before `npm publish`. + preflight: + name: release preflight needs: validate-dispatch + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + fetch-tags: true + + - name: Setup project Bun + uses: ./.github/actions/setup-project-bun + + - name: Fetch the dev line + run: git fetch --no-tags --depth=1 origin +refs/heads/dev:refs/remotes/origin/dev + + - name: Refuse a release that cannot publish + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ inputs.version }} + NPM_DIST_TAG: ${{ inputs.tag }} + DRY_RUN: ${{ inputs.dry-run }} + RESUME: ${{ inputs.resume-after-npm-publish }} + run: bash scripts/ci/release-preflight.sh + + package-standalone: + needs: [validate-dispatch, preflight] strategy: fail-fast: false matrix: @@ -181,7 +225,7 @@ jobs: retention-days: 7 package-desktop: - needs: validate-dispatch + needs: [validate-dispatch, preflight] strategy: fail-fast: false matrix: @@ -604,10 +648,47 @@ jobs: gh release edit "$release_tag" --draft=false fi + # One row per fact a release run can establish: the public GitHub release, the npm version read + # back from the registry, and the npm dist-tag. A green run used to read the same whichever of + # them were true, because the registry smoke continues to the GitHub release when its reads stay + # pending, which is the intended publishing behaviour. This job only reports; it never changes the + # run's result. + # + # A job of its own, not a step in attach-release: a failed publish skips attach-release entirely, + # and that is when the rows matter most. It reads with the job token at contents: read, so a draft + # release is invisible to it and reads as not public, which is the question the row answers. + release-outcomes: + name: release outcomes + needs: [publish, attach-release] + if: ${{ always() && inputs.dry-run != true }} + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Report release outcomes + env: + GH_TOKEN: ${{ github.token }} + RELEASE_VERSION: ${{ inputs.version }} + NPM_DIST_TAG: ${{ inputs.tag }} + NPM_VERSION_STATE: ${{ needs.publish.outputs.npm_version }} + NPM_DIST_TAG_STATE: ${{ needs.publish.outputs.npm_dist_tag }} + PUBLISH_RESULT: ${{ needs.publish.result }} + ATTACH_RESULT: ${{ needs.attach-release.result }} + run: bash scripts/ci/release-outcome-report.sh + publish: needs: [validate-dispatch, verify-release] runs-on: ubuntu-latest timeout-minutes: 15 + outputs: + npm_version: ${{ steps.registry-smoke.outputs.npm_version }} + npm_dist_tag: ${{ steps.registry-smoke.outputs.npm_dist_tag }} permissions: contents: write actions: read @@ -933,6 +1014,7 @@ jobs: if: ${{ inputs.dry-run != true && steps.publication.outputs.published == 'true' }} env: RELEASE_VERSION: ${{ inputs.version }} + NPM_DIST_TAG: ${{ inputs.tag }} PUBLISHED: ${{ steps.publication.outputs.published }} run: | set -euo pipefail @@ -949,14 +1031,35 @@ jobs: fi echo "registry version=$VERSION" echo "verification=verified" >> "$GITHUB_OUTPUT" + echo "npm_version=confirmed" >> "$GITHUB_OUTPUT" echo "Registry verified ${pkg_name}@${RELEASE_VERSION}." >> "$GITHUB_STEP_SUMMARY" - timeout --kill-after=2s 10s npm dist-tag ls "$pkg_name" --fetch-retries=0 --fetch-timeout=8000 || echo "::warning::Could not read npm dist-tags; exact version was verified" + # The dist-tag is its own outcome: a version can be on the registry while the tag + # still names the previous release. + dist_tag_state="unconfirmed" + if dist_tags="$(timeout --kill-after=2s 10s npm dist-tag ls "$pkg_name" --fetch-retries=0 --fetch-timeout=8000)"; then + printf '%s\n' "$dist_tags" + tagged="$(printf '%s\n' "$dist_tags" | awk -F': ' -v tag="$NPM_DIST_TAG" '$1 == tag { print $2; exit }')" + if [ "$tagged" = "$RELEASE_VERSION" ]; then + dist_tag_state="confirmed" + elif [ -n "$tagged" ]; then + dist_tag_state="mismatch" + echo "::warning::npm dist-tag ${NPM_DIST_TAG} points at ${tagged}, not ${RELEASE_VERSION}" + else + echo "::warning::npm dist-tag ${NPM_DIST_TAG} is not listed for ${pkg_name}" + fi + else + echo "::warning::Could not read npm dist-tags; exact version was verified" + fi + echo "npm_dist_tag=${dist_tag_state}" >> "$GITHUB_OUTPUT" + echo "npm dist-tag ${NPM_DIST_TAG}: ${dist_tag_state}." >> "$GITHUB_STEP_SUMMARY" exit 0 fi echo "::notice::Registry lookup not confirmed (attempt $attempt/6)" if [ "$attempt" -lt 6 ]; then sleep 5; fi done echo "verification=pending" >> "$GITHUB_OUTPUT" + echo "npm_version=unconfirmed" >> "$GITHUB_OUTPUT" + echo "npm_dist_tag=unconfirmed" >> "$GITHUB_OUTPUT" echo "::warning::npm publish succeeded, but registry verification remains pending; continuing GitHub release creation without republishing" echo "Publication acknowledged for ${pkg_name}@${RELEASE_VERSION}; registry verification pending after bounded reads. Inspect the registry before announcing availability. Do not republish this version." >> "$GITHUB_STEP_SUMMARY" diff --git a/devlog/_plan/260923_p5_ci_release_gaps/020_release_outcome_report.md b/devlog/_plan/260923_p5_ci_release_gaps/020_release_outcome_report.md index 46cd11cadf8..dbf0b6342c2 100644 --- a/devlog/_plan/260923_p5_ci_release_gaps/020_release_outcome_report.md +++ b/devlog/_plan/260923_p5_ci_release_gaps/020_release_outcome_report.md @@ -1,5 +1,15 @@ # 020 — Separate release outcomes (wp2) +## Amendment after audit + +The audit found that a report step inside `attach-release` can never run when `publish` fails, +because `attach-release` needs `publish`. The report is therefore its own job, +`release-outcomes`: `needs: [publish, attach-release]`, `if: always() && inputs.dry-run != true`, +`permissions: contents: read`, and it also prints both job results. Under a read token a draft +release is invisible, so the GitHub row reads `published` or `not public (draft, missing or +unreadable)`; no write permission is added to observe drafts. The sections below describe the +original step placement; the job shape above supersedes it. + ## Change map | Path | Action | diff --git a/scripts/ci/release-outcome-report.sh b/scripts/ci/release-outcome-report.sh new file mode 100755 index 00000000000..6b96629269c --- /dev/null +++ b/scripts/ci/release-outcome-report.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Report what a release run established, one fact per row. +# +# npm publication, the registry read-back, the dist-tag and the public GitHub release are four +# separate facts, and a green run used to read the same whichever of them were true: the registry +# smoke warns "Registry lookup not confirmed" and the run still continues to the GitHub release. +# This writes each outcome as its own row of the job summary and adds a warning annotation for +# every row that is not confirmed. It never changes the run's result; publishing behaviour is +# owned by the publish and attach steps. +# +# Environment: RELEASE_VERSION, NPM_DIST_TAG, GITHUB_STEP_SUMMARY (required); +# NPM_VERSION_STATE and NPM_DIST_TAG_STATE from the publish job (confirmed | mismatch | +# unconfirmed; empty when the smoke never ran); PUBLISH_RESULT and ATTACH_RESULT (job results). +# +# The GitHub row reads the release with a contents: read token. A draft release is not visible to +# that token, so anything but a published release reads as not public. +set -uo pipefail + +: "${RELEASE_VERSION:?RELEASE_VERSION is required}" +: "${NPM_DIST_TAG:?NPM_DIST_TAG is required}" +: "${GITHUB_STEP_SUMMARY:?GITHUB_STEP_SUMMARY is required}" +release_tag="v${RELEASE_VERSION}" + +github_state="not public (draft, missing or unreadable)" +if draft="$(gh release view "$release_tag" --json isDraft --jq .isDraft 2>/dev/null)"; then + case "$draft" in + false) github_state="published" ;; + true) github_state="draft (not public)" ;; + esac +fi + +describe() { + case "$1" in + confirmed) echo "confirmed" ;; + mismatch) echo "points at another version" ;; + *) echo "not confirmed" ;; + esac +} +npm_version_state="$(describe "${NPM_VERSION_STATE:-}")" +npm_tag_state="$(describe "${NPM_DIST_TAG_STATE:-}")" + +{ + echo "### Release outcomes for ${RELEASE_VERSION}" + echo "" + echo "| Outcome | State |" + echo "| --- | --- |" + echo "| GitHub release \`${release_tag}\` | ${github_state} |" + echo "| npm version \`${RELEASE_VERSION}\` read back from the registry | ${npm_version_state} |" + echo "| npm dist-tag \`${NPM_DIST_TAG}\` points at \`${RELEASE_VERSION}\` | ${npm_tag_state} |" + echo "" + echo "Publish job: ${PUBLISH_RESULT:-unknown}. Attach job: ${ATTACH_RESULT:-unknown}." + echo "" + echo "Each row is read separately. A row that is not confirmed is not a failure of this run; inspect it before announcing availability, and never republish the version." +} >> "$GITHUB_STEP_SUMMARY" + +[[ "$github_state" == "published" ]] \ + || echo "::warning::GitHub release ${release_tag} is ${github_state}" +[[ "$npm_version_state" == "confirmed" ]] \ + || echo "::warning::npm version ${RELEASE_VERSION} was not read back from the registry" +[[ "$npm_tag_state" == "confirmed" ]] \ + || echo "::warning::npm dist-tag ${NPM_DIST_TAG} ${npm_tag_state} for ${RELEASE_VERSION}" +exit 0 diff --git a/scripts/ci/release-preflight.sh b/scripts/ci/release-preflight.sh new file mode 100755 index 00000000000..7b6b7b6f67b --- /dev/null +++ b/scripts/ci/release-preflight.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Release preflight: every publication precondition the dispatch can already decide, checked +# before any runner starts packaging. +# +# The publish job repeats these checks immediately before `npm publish`, and that copy stays the +# final authority: tags, releases and registry state can still move while a run packages. This +# copy exists so a release that can never publish fails in its first minute. Run 35783865160 +# packaged 2.62.0 for nineteen minutes and then failed the ordering gate on a preview tag that +# already existed when its first job started. +# +# Environment: RELEASE_VERSION, NPM_DIST_TAG, GITHUB_REF, GITHUB_SHA (required); DRY_RUN, RESUME. +# Reads the checkout's tags and refs/remotes/origin/dev, `gh release view` and `npm view`. +# Every problem is reported before the script exits, so one run names all of them. +set -euo pipefail + +: "${RELEASE_VERSION:?RELEASE_VERSION is required}" +: "${NPM_DIST_TAG:?NPM_DIST_TAG is required}" +: "${GITHUB_REF:?GITHUB_REF is required}" +: "${GITHUB_SHA:?GITHUB_SHA is required}" +dry_run="${DRY_RUN:-false}" +resume="${RESUME:-false}" +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +release_tag="v${RELEASE_VERSION}" + +problems=0 +problem_list="" +fail() { + problems=$((problems + 1)) + problem_list="${problem_list}- $1 +" + echo "::error::$1" +} + +# Channel and dist-tag, exactly as the publish job derives them from the dispatched ref. +expected_tag="" +case "$GITHUB_REF" in + refs/heads/main) + expected_tag="latest" + [[ "$RELEASE_VERSION" != *-* ]] \ + || fail "main releases must use a stable semver version; got ${RELEASE_VERSION}" + ;; + refs/heads/preview) + expected_tag="preview" + [[ "$RELEASE_VERSION" == *-preview.* ]] \ + || fail "preview releases must use a preview prerelease version; got ${RELEASE_VERSION}" + ;; + *) + fail "Release must run from main or preview; got ${GITHUB_REF}" + ;; +esac +if [[ -n "$expected_tag" && "$NPM_DIST_TAG" != "$expected_tag" ]]; then + fail "${GITHUB_REF#refs/heads/} releases must publish with npm dist-tag '${expected_tag}', got '${NPM_DIST_TAG}'" +fi + +if [[ "$resume" == "true" && "$dry_run" == "true" ]]; then + fail "resume-after-npm-publish is a real-publication recovery path and cannot combine with dry-run" +fi + +# Every version source (package.json and the desktop manifests) must already name the release. +if ! bun "$repo_root/scripts/release-version-sources.ts" check "$RELEASE_VERSION"; then + fail "a version source does not match ${RELEASE_VERSION}; run scripts/release.ts on the release branch first" +fi + +# Git tag. A tag at another commit is always fatal; one at this commit is expected only when a +# dry run is repeated or a partial publication is resumed. +existing_tag_sha="$(git rev-parse -q --verify "refs/tags/${release_tag}^{commit}" || true)" +if [[ -n "$existing_tag_sha" && "$existing_tag_sha" != "$GITHUB_SHA" ]]; then + fail "${release_tag} already points at ${existing_tag_sha}, not ${GITHUB_SHA}" +elif [[ -n "$existing_tag_sha" && "$resume" != "true" && "$dry_run" != "true" ]]; then + fail "${release_tag} already exists. Refusing to publish a version with pre-existing Git metadata." +fi + +# GitHub release. An unreadable answer counts as absent here; the publish job reads it again. +if gh release view "$release_tag" >/dev/null 2>&1 && [[ "$resume" != "true" && "$dry_run" != "true" ]]; then + fail "GitHub Release ${release_tag} already exists. Choose the next unused version." +fi + +# npm. Only an exact version answer counts as present and only E404 counts as absent; anything +# else is a registry read failure, which warns rather than blocking a release it cannot judge. +pkg_name="$(node -p "require(process.argv[1]).name" "$repo_root/package.json")" +npm_error="$(mktemp "${TMPDIR:-/tmp}/ocx-release-preflight.XXXXXX")" +trap 'rm -f -- "$npm_error"' EXIT +npm_state="unknown" +if npm_answer="$(npm view "${pkg_name}@${RELEASE_VERSION}" version --fetch-retries=0 --fetch-timeout=8000 2>"$npm_error")"; then + if [[ "$npm_answer" == "$RELEASE_VERSION" ]]; then npm_state="present"; else npm_state="absent"; fi +elif grep -q "E404" "$npm_error"; then + npm_state="absent" +fi +case "$npm_state" in + present) + if [[ "$resume" == "true" ]]; then + echo "${pkg_name}@${RELEASE_VERSION} is on npm; the publish job verifies its source before resuming." + elif [[ "$dry_run" == "true" ]]; then + echo "::notice::${pkg_name}@${RELEASE_VERSION} already exists on npm; dry-run only" + else + fail "${pkg_name}@${RELEASE_VERSION} already exists on npm. Re-dispatch with resume-after-npm-publish: true if a previous run acknowledged it; otherwise choose the next unused version." + fi + ;; + absent) + [[ "$resume" != "true" ]] \ + || fail "resume-after-npm-publish is set, but ${pkg_name}@${RELEASE_VERSION} is not on npm" + ;; + *) + echo "::warning::Could not read npm for ${pkg_name}@${RELEASE_VERSION}; the publish job checks again before publishing." + ;; +esac + +# Cross-channel ordering against the whole tag set: the gate run 35783865160 reached too late. +allow="" +if [[ ( "$dry_run" == "true" || "$resume" == "true" ) && -n "$existing_tag_sha" && "$existing_tag_sha" == "$GITHUB_SHA" ]]; then + allow="--allow-existing-tag-at-head" +fi +if ! git tag --list 'v*' | bun "$repo_root/scripts/version-line.ts" assert-releasable "$RELEASE_VERSION" ${allow:+"$allow"}; then + fail "${RELEASE_VERSION} does not outrank the current tag set" +fi + +# dev must already carry a higher version (the pre-move). +if dev_package="$(git show refs/remotes/origin/dev:package.json 2>/dev/null)"; then + dev_version="$(printf '%s' "$dev_package" | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).version")" + bun "$repo_root/scripts/version-line.ts" assert-ahead "$dev_version" "$RELEASE_VERSION" \ + || fail "dev carries ${dev_version}, which does not outrank ${RELEASE_VERSION}; merge the dev pre-move first" +else + fail "cannot read package.json from refs/remotes/origin/dev" +fi + +if (( problems > 0 )); then + if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then + printf '### Release preflight refused %s\n\n%s' "$RELEASE_VERSION" "$problem_list" >> "$GITHUB_STEP_SUMMARY" + fi + echo "Release preflight found ${problems} blocking problem(s); nothing was packaged." + exit 1 +fi +echo "Release preflight passed for ${RELEASE_VERSION} at ${GITHUB_SHA}; the publish job repeats these checks before publishing." diff --git a/scripts/ci/run-bun-test-batches.sh b/scripts/ci/run-bun-test-batches.sh index eb3978ff9c9..023f4ba0576 100644 --- a/scripts/ci/run-bun-test-batches.sh +++ b/scripts/ci/run-bun-test-batches.sh @@ -282,29 +282,100 @@ done < <( | LC_ALL=C sort -z ) -SELECTED_FILES=() -general_index=0 +# Shard ownership by recorded duration. +# +# Sorted round-robin split the suite evenly by COUNT, while file durations differ by three orders +# of magnitude: one Linux shard carried 394 s of tests and another 248 s (run 35816902207). Each +# file now weighs the milliseconds recorded for it in scripts/ci/test-durations.tsv, and the +# heaviest file goes first to the least-loaded shard, lowest index on a tie. A file the table does +# not know weighs the table's median, so with no usable table every file weighs the same and the +# result is exactly the old sorted round-robin. Every shard computes the whole assignment and +# refuses to run unless it covers every general file exactly once, because a shard that silently +# drops files is the one failure here that stays green. Each shard still runs its files in sorted +# order. Refresh the table with scripts/ci/test-durations.ts from hosted job logs. +batch_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly DURATIONS_FILE="${BUN_TEST_DURATIONS_FILE:-$batch_script_dir/test-durations.tsv}" +durations_source="/dev/null" +if [[ -f "$DURATIONS_FILE" ]]; then + durations_source="$DURATIONS_FILE" +fi + +GENERAL_FILES=() for path in "${ALL_TEST_FILES[@]}"; do - if ! is_general_test_file "$path"; then - continue + if is_general_test_file "$path"; then + GENERAL_FILES+=("$path") fi +done +if (( ${#GENERAL_FILES[@]} == 0 )); then + echo "No tests selected for shard ${SHARD_SPEC}." >&2 + exit 1 +fi - if (( general_index % SHARD_COUNT == SHARD_INDEX - 1 )); then +# The median of the recorded milliseconds; any constant would do for an empty table. +fallback_ms="$( + awk -F '\t' '{ sub(/\r$/, "") } !/^#/ && NF == 2 && $1 ~ /^[0-9]+$/ { print $1 }' "$durations_source" \ + | LC_ALL=C sort -n \ + | awk '{ values[NR] = $1 } END { value = (NR > 0 ? values[int((NR + 1) / 2)] : 1000); print (value > 0 ? value : 1) }' +)" +readonly FALLBACK_MS="$fallback_ms" + +assignment_file="$(mktemp -t ocx-bun-test-shards.XXXXXX)" +printf '%s\n' "${GENERAL_FILES[@]}" \ + | awk -F '\t' -v table="$durations_source" -v fallback="$FALLBACK_MS" ' + BEGIN { + while ((getline line < table) > 0) { + sub(/\r$/, "", line) + if (line ~ /^#/ || split(line, field, "\t") != 2 || field[1] !~ /^[0-9]+$/) continue + weight[field[2]] = (field[1] > 0 ? field[1] : 1) + } + close(table) + } + { print (($0 in weight) ? weight[$0] : fallback) "\t" $0 } + ' \ + | LC_ALL=C sort -t $'\t' -k1,1nr -k2,2 \ + | awk -F '\t' -v shards="$SHARD_COUNT" ' + BEGIN { for (shard = 1; shard <= shards; shard += 1) load[shard] = 0 } + { + best = 1 + for (shard = 2; shard <= shards; shard += 1) if (load[shard] < load[best]) best = shard + load[best] += $1 + print best "\t" $1 "\t" $2 + } + ' > "$assignment_file" +assigned_count="$(awk 'END { print NR }' "$assignment_file")" +if (( assigned_count != ${#GENERAL_FILES[@]} )); then + rm -f -- "$assignment_file" + echo "Shard assignment covered ${assigned_count} of ${#GENERAL_FILES[@]} test files; refusing to run a partial suite." >&2 + exit 1 +fi + +SELECTED_FILES=() +SELECTED_WEIGHTS=() +predicted_ms=0 +while IFS=$'\t' read -r owner weight path; do + if [[ "$owner" == "$SHARD_INDEX" ]]; then SELECTED_FILES+=("$path") + SELECTED_WEIGHTS+=("$weight") + predicted_ms=$((predicted_ms + weight)) fi - ((general_index += 1)) -done +done < <(LC_ALL=C sort -t $'\t' -k3,3 "$assignment_file") +rm -f -- "$assignment_file" if (( ${#SELECTED_FILES[@]} == 0 )); then echo "No tests selected for shard ${SHARD_SPEC}." >&2 exit 1 fi -# Keep shard ownership and sorted execution order; split only the process boundary. +# Keep shard ownership and sorted execution order; split only the process boundary. A batch also +# closes before its predicted duration would pass half the process timeout: balancing by duration +# changes which files share a process, and without this bound one twelve-file batch was predicted +# at 89 of its 120 seconds. A file heavier than the budget still runs, alone. +readonly BATCH_BUDGET_MS=$(( BATCH_TIMEOUT_SECONDS * 1000 / 2 )) BATCH_STARTS=() BATCH_LENGTHS=() pending_start=0 pending_count=0 +pending_ms=0 for ((index = 0; index < ${#SELECTED_FILES[@]}; index += 1)); do if is_serial_test_file "${SELECTED_FILES[$index]}"; then if (( pending_count > 0 )); then @@ -313,8 +384,14 @@ for ((index = 0; index < ${#SELECTED_FILES[@]}; index += 1)); do fi BATCH_STARTS+=("$index"); BATCH_LENGTHS+=(1) else - if (( pending_count == 0 )); then pending_start=$index; fi - ((pending_count += 1)) + weight="${SELECTED_WEIGHTS[$index]}" + if (( pending_count > 0 && pending_ms + weight > BATCH_BUDGET_MS )); then + BATCH_STARTS+=("$pending_start"); BATCH_LENGTHS+=("$pending_count") + pending_count=0 + fi + if (( pending_count == 0 )); then pending_start=$index; pending_ms=0; fi + pending_count=$((pending_count + 1)) + pending_ms=$((pending_ms + weight)) if (( pending_count == BATCH_SIZE )); then BATCH_STARTS+=("$pending_start"); BATCH_LENGTHS+=("$pending_count") pending_count=0 @@ -326,6 +403,7 @@ if (( pending_count > 0 )); then fi readonly TOTAL_BATCHES=${#BATCH_STARTS[@]} echo "Shard ${SHARD_SPEC}: ${#SELECTED_FILES[@]} files in ${TOTAL_BATCHES} primary Bun processes (scope ${TEST_FILE_SCOPE}, batch size <= ${BATCH_SIZE}, timeout ${BATCH_TIMEOUT_SECONDS}s)." +echo "Predicted shard time from recorded durations: $((predicted_ms / 1000))s; a file without a record weighs ${FALLBACK_MS}ms and a batch closes before ${BATCH_BUDGET_MS}ms." echo "Nothing here is retried. A test failure, a process timeout and a Bun runtime crash each fail this shard on their first occurrence." echo "A timeout or a crash is additionally swept one file per process for attribution, after the shard has already failed; that sweep cannot turn it green." diff --git a/scripts/ci/test-durations.ts b/scripts/ci/test-durations.ts new file mode 100644 index 00000000000..b6c056bf4d3 --- /dev/null +++ b/scripts/ci/test-durations.ts @@ -0,0 +1,145 @@ +/** + * Per-file Bun test durations for shard assignment. + * + * `scripts/ci/run-bun-test-batches.sh` weighs every test file by the milliseconds recorded for it in + * `scripts/ci/test-durations.tsv` and assigns the heaviest file first to the least-loaded shard. This + * tool keeps that table honest by reading it back from hosted CI job logs, where every line carries + * a runner timestamp and Bun wraps each file's output in its own `##[group]:` ... + * `##[endgroup]` pair. A file's duration is the time from the previous file's end (or its batch + * header, for the first file of a process) to its own end, so process start and module loading are + * charged to the file that caused them. + * + * Refresh from a green run on dev (all four Linux shards): + * + * gh run view --log > .tmp/ci-run.log + * bun scripts/ci/test-durations.ts refresh --source "run " .tmp/ci-run.log + * + * Files measured in the logs replace their rows; rows for files that still exist are kept; rows for + * files that no longer exist are dropped. Attribution sweeps (a failed shard re-running files one at a + * time) are ignored because they do not measure the batch shape the table is used for. + */ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const REPO_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url)))); +export const DURATIONS_TABLE = join(REPO_ROOT, "scripts", "ci", "test-durations.tsv"); + +const LOG_LINE = /^(?:(.*?)\t[^\t]*\t)?\uFEFF?(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d)(\.\d+)?Z (.*)$/; +const BATCH_HEADER = /^##\[group\]shard \S+ batch \d+\/\d+(.*)$/; +const FILE_HEADER = /^##\[group\](tests\/.+):$/; +const ANSI = /\u001b\[[0-9;]*m/g; + +function timestampMs(seconds: string, fraction: string | undefined): number { + const milliseconds = (fraction ?? ".0").slice(1, 4).padEnd(3, "0"); + return Date.parse(`${seconds}.${milliseconds}Z`); +} + +/** Every measured duration per file, in milliseconds, from one or more concatenated job logs. */ +export function parseJobLog(text: string): Map { + type JobState = { previousEnd: number | null; file: string | null; attribution: boolean }; + const jobs = new Map(); + const samples = new Map(); + for (const rawLine of text.split(/\r?\n/)) { + const match = LOG_LINE.exec(rawLine.replace(ANSI, "")); + if (!match) continue; + const job = match[1] ?? ""; + const at = timestampMs(match[2]!, match[3]); + const body = match[4]!; + let state = jobs.get(job); + if (!state) { + state = { previousEnd: null, file: null, attribution: false }; + jobs.set(job, state); + } + const batch = BATCH_HEADER.exec(body); + if (batch) { + state.previousEnd = at; + state.file = null; + state.attribution = batch[1]!.includes("attribution"); + continue; + } + const file = FILE_HEADER.exec(body); + if (file) { + state.file = state.attribution || state.previousEnd === null ? null : file[1]!; + continue; + } + if (body.startsWith("##[endgroup]") && state.file !== null && state.previousEnd !== null) { + const list = samples.get(state.file) ?? []; + list.push(Math.max(0, at - state.previousEnd)); + samples.set(state.file, list); + state.previousEnd = at; + state.file = null; + } + } + return samples; +} + +/** The recorded table, path -> milliseconds. Comment lines and malformed rows are ignored. */ +export function parseTable(text: string): Map { + const table = new Map(); + for (const line of text.split(/\r?\n/)) { + if (line.startsWith("#")) continue; + const fields = line.split("\t"); + if (fields.length !== 2 || !/^\d+$/.test(fields[0]!)) continue; + table.set(fields[1]!, Number(fields[0])); + } + return table; +} + +/** New measurements win; kept rows must still name a file; every duration is at least 1 ms. */ +export function mergeDurations( + previous: ReadonlyMap, + measured: ReadonlyMap, + exists: (path: string) => boolean, +): Map { + const merged = new Map(); + for (const [path, value] of previous) if (exists(path)) merged.set(path, value); + for (const [path, values] of measured) { + if (!exists(path) || values.length === 0) continue; + const mean = values.reduce((sum, value) => sum + value, 0) / values.length; + merged.set(path, Math.max(1, Math.round(mean))); + } + return merged; +} + +export function renderTable(durations: ReadonlyMap, source: string): string { + const rows = [...durations.entries()] + .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) + .map(([path, value]) => `${value}\t${path}`); + return [ + "# Per-file Bun test durations in milliseconds, read from hosted CI job logs.", + "# Consumed by scripts/ci/run-bun-test-batches.sh to balance shards; a file without a row weighs the median.", + "# Regenerate with scripts/ci/test-durations.ts (usage in its header); do not edit by hand.", + `# Source: ${source}`, + ...rows, + "", + ].join("\n"); +} + +if (import.meta.main) { + const [command, ...rest] = process.argv.slice(2); + let source = "unspecified"; + const logs: string[] = []; + for (let index = 0; index < rest.length; index += 1) { + if (rest[index] === "--source") source = rest[++index] ?? source; + else logs.push(rest[index]!); + } + if (command !== "refresh" || logs.length === 0) { + console.error("usage: bun scripts/ci/test-durations.ts refresh [--source ] ..."); + process.exit(64); + } + const measured = new Map(); + for (const log of logs) { + for (const [path, values] of parseJobLog(readFileSync(log, "utf8"))) { + measured.set(path, [...(measured.get(path) ?? []), ...values]); + } + } + if (measured.size === 0) { + console.error("No per-file durations found; pass hosted logs of the Linux test shards."); + process.exit(1); + } + const previous = existsSync(DURATIONS_TABLE) ? parseTable(readFileSync(DURATIONS_TABLE, "utf8")) : new Map(); + const merged = mergeDurations(previous, measured, path => existsSync(join(REPO_ROOT, path))); + writeFileSync(DURATIONS_TABLE, renderTable(merged, source)); + console.log(`Recorded ${merged.size} files (${measured.size} measured) in ${DURATIONS_TABLE}.`); +} diff --git a/scripts/ci/test-durations.tsv b/scripts/ci/test-durations.tsv new file mode 100644 index 00000000000..b66d5022526 --- /dev/null +++ b/scripts/ci/test-durations.tsv @@ -0,0 +1,1562 @@ +# Per-file Bun test durations in milliseconds, read from hosted CI job logs. +# Consumed by scripts/ci/run-bun-test-batches.sh to balance shards; a file without a row weighs the median. +# Regenerate with scripts/ci/test-durations.ts (usage in its header); do not edit by hand. +# Source: run 35816902207, Linux test 1/4-4/4 +399 tests/adapters/abort-race.test.ts +524 tests/adapters/adapter-buffered-tool-conformance.test.ts +126 tests/adapters/adapter-error-inline.test.ts +1699 tests/adapters/adapter-inner-send-budget-wiring.test.ts +2899 tests/adapters/adapter-inner-send-budget.test.ts +153 tests/adapters/adapter-input-media-guard.test.ts +224 tests/adapters/adapter-registry-authority.test.ts +635 tests/adapters/adapter-tool-conformance.test.ts +147 tests/adapters/adapter-usage.test.ts +271 tests/adapters/anthropic/anthropic-account-pool.test.ts +86 tests/adapters/anthropic/anthropic-agentrouter-language-framing.test.ts +131 tests/adapters/anthropic/anthropic-baseurl-override.test.ts +1290 tests/adapters/anthropic/anthropic-compatible-stream.test.ts +93 tests/adapters/anthropic/anthropic-empty-content.test.ts +146 tests/adapters/anthropic/anthropic-eof-tolerance.test.ts +296 tests/adapters/anthropic/anthropic-error-body.test.ts +107 tests/adapters/anthropic/anthropic-error-stop-reason.test.ts +114 tests/adapters/anthropic/anthropic-fast-speed.test.ts +100 tests/adapters/anthropic/anthropic-hardening.test.ts +213 tests/adapters/anthropic/anthropic-image-guard.test.ts +3066 tests/adapters/anthropic/anthropic-image-normalize.test.ts +807 tests/adapters/anthropic/anthropic-image-retry-e2e.test.ts +201 tests/adapters/anthropic/anthropic-image-retry.test.ts +100 tests/adapters/anthropic/anthropic-parallel-tool-disable.test.ts +110 tests/adapters/anthropic/anthropic-pool-toggle-copy.test.ts +302 tests/adapters/anthropic/anthropic-quota-dispatch.test.ts +206 tests/adapters/anthropic/anthropic-ratelimit-headers.test.ts +153 tests/adapters/anthropic/anthropic-reasoning.test.ts +125 tests/adapters/anthropic/anthropic-reset-grants.test.ts +179 tests/adapters/anthropic/anthropic-sidecar-account-failover.test.ts +85 tests/adapters/anthropic/anthropic-stream-hardening.test.ts +88 tests/adapters/anthropic/anthropic-tail-guard.test.ts +182 tests/adapters/anthropic/anthropic-thinking-signature.test.ts +97 tests/adapters/anthropic/anthropic-tool-call-id.test.ts +175 tests/adapters/anthropic/anthropic-tool-declaration-constraints.test.ts +104 tests/adapters/anthropic/anthropic-tool-schema.test.ts +254 tests/adapters/bridge-legacy-shell-normalization.test.ts +3485 tests/adapters/bridge-lifecycle.test.ts +113 tests/adapters/bridge-nonstreaming-terminal.test.ts +281 tests/adapters/bridge-raw-reasoning-hidden.test.ts +91 tests/adapters/bridge-reasoning-replay-batch.test.ts +92 tests/adapters/bridge-terminal-singleness.test.ts +198 tests/adapters/bridge.test.ts +139 tests/adapters/buffered-response-shape-guards.test.ts +89 tests/adapters/coding-agent-tool-result-images.test.ts +270 tests/adapters/empty-tool-output-annotation.test.ts +363 tests/adapters/exec-tool-result-normalize.test.ts +123 tests/adapters/google/antigravity-baseurl-override.test.ts +152 tests/adapters/google/antigravity-static-catalog.test.ts +877 tests/adapters/google/gcp-adc.test.ts +117 tests/adapters/google/gemini-37-flash-migration.test.ts +139 tests/adapters/google/gemini-web-search.test.ts +137 tests/adapters/google/google-adapter.test.ts +4106 tests/adapters/google/google-antigravity-oauth.test.ts +2533 tests/adapters/google/google-antigravity-replay.test.ts +135 tests/adapters/google/google-antigravity-wire.test.ts +102 tests/adapters/google/google-buffered-stop-reason.test.ts +108 tests/adapters/google/google-claude-prefill-guard.test.ts +112 tests/adapters/google/google-empty-content.test.ts +85 tests/adapters/google/google-errors.test.ts +121 tests/adapters/google/google-hardening.test.ts +156 tests/adapters/google/google-models-listing.test.ts +93 tests/adapters/google/google-output-clamp.test.ts +87 tests/adapters/google/google-provider-metadata-roundtrip.test.ts +141 tests/adapters/google/google-signature-history-roundtrip.test.ts +105 tests/adapters/google/google-strict-tool-validated-mode.test.ts +99 tests/adapters/google/google-structured-output.test.ts +92 tests/adapters/google/google-tool-result-adjacency.test.ts +128 tests/adapters/google/google-tool-schema-contract.test.ts +103 tests/adapters/google/google-tool-schema.test.ts +434 tests/adapters/google/google-vertex-http.test.ts +108 tests/adapters/google/google-vertex-stream.test.ts +98 tests/adapters/google/google-vertex-thought-signature.test.ts +103 tests/adapters/google/google-wire-compiler.test.ts +133 tests/adapters/google/google-wire-shape.test.ts +143 tests/adapters/google/vertex-catalog.test.ts +236 tests/adapters/identity-neutralize.test.ts +528 tests/adapters/key-failover.test.ts +128 tests/adapters/openai/inline-think-boundaries.test.ts +1962 tests/adapters/openai/openai-api-virtual-models.test.ts +302 tests/adapters/openai/openai-chat-bounded-tool-names.test.ts +103 tests/adapters/openai/openai-chat-dangling-toolcalls.test.ts +103 tests/adapters/openai/openai-chat-developer-position.test.ts +100 tests/adapters/openai/openai-chat-eof.test.ts +360 tests/adapters/openai/openai-chat-hardening.test.ts +6406 tests/adapters/openai/openai-chat-image-normalization.test.ts +125 tests/adapters/openai/openai-chat-inline-think-tags.test.ts +124 tests/adapters/openai/openai-chat-invalid-tool-call-diagnostics.test.ts +142 tests/adapters/openai/openai-chat-model-suffix.test.ts +418 tests/adapters/openai/openai-chat-native-policy.test.ts +240 tests/adapters/openai/openai-chat-parallel-stream.test.ts +265 tests/adapters/openai/openai-chat-path-override.test.ts +111 tests/adapters/openai/openai-chat-reasoning-wire-policy.test.ts +149 tests/adapters/openai/openai-chat-system-order.test.ts +90 tests/adapters/openai/openai-chat-tool-result-images.test.ts +92 tests/adapters/openai/openai-chat-url.test.ts +98 tests/adapters/openai/openai-chat-video-part.test.ts +1062 tests/adapters/openai/openai-provider-option-e2e.test.ts +100 tests/adapters/openai/openai-provider-option-migration.test.ts +105 tests/adapters/openai/openai-provider-option-startup.test.ts +92 tests/adapters/openai/openai-provider-option-tooling.test.ts +91 tests/adapters/openai/openai-provider-option.test.ts +122 tests/adapters/physical-send.test.ts +104 tests/adapters/reasoning-replay-identity.test.ts +115 tests/adapters/reasoning-replay-robustness.test.ts +154 tests/adapters/routed-agent-messages.test.ts +115 tests/adapters/run-turn-queue.test.ts +314 tests/adapters/terminal-continuation-owner-rotation.test.ts +104 tests/adapters/tool-argument-integers.test.ts +81 tests/adapters/tool-catalog-nudge.test.ts +92 tests/adapters/tool-choice-performance.test.ts +3069 tests/adapters/translator-budget.test.ts +108 tests/adapters/upstream-http-error.test.ts +2576 tests/ci-workflows/assert-mergeable-review.test.ts +194 tests/ci-workflows/brand-favicons.test.ts +94 tests/ci-workflows/build-desktop-icon-set.test.ts +107 tests/ci-workflows/build-release-changelog.test.ts +245 tests/ci-workflows/bump-dev-version.test.ts +100 tests/ci-workflows/bun-runtime.test.ts +84 tests/ci-workflows/ci-bun-crash-classifier.test.ts +89 tests/ci-workflows/ci-concurrency-groups.test.ts +422 tests/ci-workflows/ci-crash-disposition.test.ts +3005 tests/ci-workflows/ci-review-lanes.test.ts +237 tests/ci-workflows/ci-scope-reduction.test.ts +180 tests/ci-workflows/ci-structure-gate.test.ts +766 tests/ci-workflows/ci-workflows.test.ts +88 tests/ci-workflows/cleanup-orphaned-workflows.test.ts +93 tests/ci-workflows/closed-pr-branch-cleanup.test.ts +6512 tests/ci-workflows/cold-spawn-warmup.test.ts +238 tests/ci-workflows/compatibility-version.test.ts +91 tests/ci-workflows/docs-429-failover-claims.test.ts +89 tests/ci-workflows/docs-bun-source-requirement.test.ts +111 tests/ci-workflows/docs-developer-role-policy.test.ts +79 tests/ci-workflows/docs-gui-screenshot-policy.test.ts +221 tests/ci-workflows/docs-link-targets.test.ts +84 tests/ci-workflows/docs-provider-billing-claims.test.ts +121 tests/ci-workflows/docs-provider-discovery-limits.test.ts +98 tests/ci-workflows/docs-provider-preset-counts.test.ts +112 tests/ci-workflows/docs-readme-translation-parity.test.ts +202 tests/ci-workflows/docs-remote-hub-claims.test.ts +181 tests/ci-workflows/dsh-path-contract.test.ts +352 tests/ci-workflows/dsh-rc6-compat-script.test.ts +200 tests/ci-workflows/dsh-writer-lock.test.ts +98 tests/ci-workflows/exhaustive-deps-suppression.test.ts +341 tests/ci-workflows/file-size-ratchet.test.ts +265 tests/ci-workflows/fixture-dir-uniqueness.test.ts +179 tests/ci-workflows/install-scripts.test.ts +116 tests/ci-workflows/installed-gate-drivers.test.ts +96 tests/ci-workflows/keyring-smoke.test.ts +2948 tests/ci-workflows/macos-serial-lanes.test.ts +2704 tests/ci-workflows/package-tree-integrity.test.ts +259 tests/ci-workflows/package-tree-restart-ownership.test.ts +139 tests/ci-workflows/pr-readiness-reattest.test.ts +174 tests/ci-workflows/privacy-scan-asset-names.test.ts +93 tests/ci-workflows/privacy-scan-meta-key.test.ts +94 tests/ci-workflows/privacy-scan-ssh-endpoint.test.ts +127 tests/ci-workflows/release-desktop-scripts.test.ts +12261 tests/ci-workflows/release-helper.test.ts +96 tests/ci-workflows/release-notes.test.ts +91 tests/ci-workflows/release-pipeline-contract.test.ts +179 tests/ci-workflows/release-resume-identity.test.ts +194 tests/ci-workflows/release-version-line.test.ts +221 tests/ci-workflows/release-version-sources.test.ts +276 tests/ci-workflows/repo-hygiene.test.ts +383 tests/ci-workflows/repo-import-resolution.test.ts +383 tests/ci-workflows/setup-hooks.test.ts +103 tests/ci-workflows/skill-ocx.test.ts +468 tests/ci-workflows/structure-ssot.test.ts +40405 tests/ci-workflows/test-home-guard.test.ts +17910 tests/ci-workflows/test-runner.test.ts +93 tests/ci-workflows/test-sandbox-cleanup.test.ts +183 tests/ci-workflows/version-line.test.ts +108 tests/ci-workflows/warmup-registration.test.ts +95 tests/ci-workflows/zz-ci-api-usage-isolation.test.ts +188 tests/ci-workflows/zz-ci-storage-policy-isolation.test.ts +124 tests/ci-workflows/zz-pr-coderabbit-readiness-revalidation.test.ts +2836 tests/claude-integration/claude-529-mapping.test.ts +109 tests/claude-integration/claude-agent-startup-sync.test.ts +124 tests/claude-integration/claude-agents-inject-client.test.ts +139 tests/claude-integration/claude-agents-inject.test.ts +150 tests/claude-integration/claude-alias.test.ts +83 tests/claude-integration/claude-auth-detect.test.ts +115 tests/claude-integration/claude-auth-mode.test.ts +105 tests/claude-integration/claude-authmode-migration.test.ts +142 tests/claude-integration/claude-cli.test.ts +219 tests/claude-integration/claude-code-thought-signature-scope.test.ts +96 tests/claude-integration/claude-compatibility.test.ts +131 tests/claude-integration/claude-context-windows.test.ts +117 tests/claude-integration/claude-desktop-1m.test.ts +570 tests/claude-integration/claude-desktop-cli.test.ts +330 tests/claude-integration/claude-desktop-config-path.test.ts +439 tests/claude-integration/claude-desktop-discovery.test.ts +522 tests/claude-integration/claude-desktop-first-party.test.ts +121 tests/claude-integration/claude-desktop-mode-explanation.test.ts +140 tests/claude-integration/claude-desktop-native-context.test.ts +96 tests/claude-integration/claude-desktop-policy.test.ts +5687 tests/claude-integration/claude-desktop-remote-hub.test.ts +183 tests/claude-integration/claude-dotenv-provenance-transport.test.ts +106 tests/claude-integration/claude-gateway-cache.test.ts +408 tests/claude-integration/claude-inbound-cache-stabilize.test.ts +100 tests/claude-integration/claude-inbound-debug.test.ts +157 tests/claude-integration/claude-inbound.test.ts +217 tests/claude-integration/claude-intercept-local-ca.test.ts +159 tests/claude-integration/claude-intercept-proxy.test.ts +199 tests/claude-integration/claude-intercept-settings.test.ts +1761 tests/claude-integration/claude-management-api.test.ts +4557 tests/claude-integration/claude-messages-endpoint.test.ts +122 tests/claude-integration/claude-model-info.test.ts +864 tests/claude-integration/claude-models-discovery.test.ts +2441 tests/claude-integration/claude-native-affinity.test.ts +3021 tests/claude-integration/claude-native-passthrough.test.ts +1031 tests/claude-integration/claude-outbound.test.ts +126 tests/claude-integration/claude-shell-hook.test.ts +379 tests/claude-integration/claude-sidecar-override.test.ts +108 tests/claude-integration/claude-source-envelope.test.ts +122 tests/claude-integration/claude-system-env-auto.test.ts +107 tests/cli/agent-driven.test.ts +107 tests/cli/cli-account-cancel-flow.test.ts +80 tests/cli/cli-account-orca-import.test.ts +136 tests/cli/cli-account-pin-drain.test.ts +118 tests/cli/cli-account-pool-verbs.test.ts +136 tests/cli/cli-account-threshold.test.ts +829 tests/cli/cli-account.test.ts +134 tests/cli/cli-capabilities.test.ts +100 tests/cli/cli-catalog-prewarm.test.ts +139 tests/cli/cli-codex-cli-update.test.ts +123 tests/cli/cli-codex-log-guard-compact.test.ts +126 tests/cli/cli-codex-log-guard-protection.test.ts +115 tests/cli/cli-codex-log-guard.test.ts +106 tests/cli/cli-companion.test.ts +4458 tests/cli/cli-config-command.test.ts +2354 tests/cli/cli-config-show-client.test.ts +7261 tests/cli/cli-connect-readiness.test.ts +218 tests/cli/cli-dispatch.test.ts +129 tests/cli/cli-dto-fidelity.test.ts +230 tests/cli/cli-effort.test.ts +642 tests/cli/cli-export-command.test.ts +110 tests/cli/cli-head.test.ts +371 tests/cli/cli-headless-parity.test.ts +12045 tests/cli/cli-help.test.ts +133 tests/cli/cli-json-contract.test.ts +112 tests/cli/cli-management-auth.test.ts +122 tests/cli/cli-models-free-only.test.ts +143 tests/cli/cli-models-price.test.ts +167 tests/cli/cli-models-reasoning.test.ts +115 tests/cli/cli-models-runtime-dispatch.test.ts +11305 tests/cli/cli-models.test.ts +216 tests/cli/cli-native-profile.test.ts +111 tests/cli/cli-observe-logs.test.ts +15141 tests/cli/cli-provider.test.ts +864 tests/cli/cli-ready-subprocess.test.ts +193 tests/cli/cli-ready.test.ts +116 tests/cli/cli-registry.test.ts +2786 tests/cli/cli-resolve-subprocess.test.ts +98 tests/cli/cli-resolve.test.ts +3534 tests/cli/cli-restart-health.test.ts +2957 tests/cli/cli-restore-back.test.ts +4480 tests/cli/cli-start-auxiliary-bind.test.ts +4594 tests/cli/cli-start-journal-order.test.ts +2577 tests/cli/cli-status-hub-state.test.ts +14493 tests/cli/cli-status-json.test.ts +127 tests/cli/cli-status-oauth-health.test.ts +133 tests/cli/cli-stop-json.test.ts +135 tests/cli/cli-storage-inspect.test.ts +140 tests/cli/cli-transport-honesty.test.ts +153 tests/cli/cli-usage-hub.test.ts +146 tests/cli/cli-usage-report.test.ts +100 tests/cli/cli-version-skew.test.ts +131 tests/cli/ensure-desired-integrations-race.test.ts +149 tests/cli/hub-gated-local-clients.test.ts +92 tests/cli/hub-invite.test.ts +97 tests/cli/interactive-confirm.test.ts +111 tests/cli/model-selection-guidance.test.ts +971 tests/cli/ocx-launcher-runtime.test.ts +97 tests/cli/ocx-launcher-source.test.ts +100 tests/cli/ocx-run.test.ts +1150 tests/cli/restore-completes-shared-teardown.test.ts +411 tests/cli/route-explainability.test.ts +102 tests/cli/star-deferral.test.ts +78 tests/cli/start-args.test.ts +77 tests/cli/start-ownership-publication.test.ts +98 tests/cli/system-restart-client.test.ts +153 tests/cli/uninstall.test.ts +148 tests/clients/aside-profile-identity.test.ts +281 tests/clients/aside-profile-paths.test.ts +1825 tests/clients/aside-profile-sync-owner.test.ts +574 tests/clients/aside-profiles.test.ts +76 tests/clients/client-catalog-compatibility.test.ts +19498 tests/clients/client-connect.test.ts +121 tests/clients/client-export-modality-enum.test.ts +92 tests/clients/client-fingerprint.test.ts +94 tests/clients/client-hub-relay.test.ts +111 tests/clients/client-hub-state.test.ts +114 tests/clients/client-hub-usage.test.ts +368 tests/clients/client-lifecycle-lock.test.ts +247 tests/clients/client-machine-listener.test.ts +119 tests/clients/client-runtime.test.ts +250 tests/clients/cline-client.test.ts +200 tests/clients/cline-writer.test.ts +161 tests/clients/desktop-3p-guard.test.ts +167 tests/clients/desktop-3p-removal.test.ts +187 tests/clients/desktop-3p.test.ts +102 tests/clients/desktop-app-restart-posix.test.ts +201 tests/clients/desktop-app-restart.test.ts +74 tests/clients/desktop-cli-contracts.test.ts +86 tests/clients/desktop-exit-ownership.test.ts +85 tests/clients/desktop-install-identity.test.ts +556 tests/clients/desktop-profile.test.ts +81 tests/clients/desktop-proxy-direct-transport.test.ts +341 tests/clients/desktop-remote-store.test.ts +98 tests/clients/desktop-restart-handoff.test.ts +89 tests/clients/desktop-runtime-identity.test.ts +89 tests/clients/desktop-start-at-login-default.test.ts +85 tests/clients/desktop-startup-surface.test.ts +96 tests/clients/desktop-tray-availability.test.ts +99 tests/clients/desktop-widget-entry.test.ts +297 tests/clients/integrations-current-store.test.ts +163 tests/clients/integrations-journal.test.ts +139 tests/clients/integrations-merge.test.ts +195 tests/clients/integrations-serialize.test.ts +146 tests/clients/integrations-state.test.ts +184 tests/clients/integrations-superseded-store.test.ts +154 tests/clients/integrations-writer-frozen-config.test.ts +286 tests/clients/integrations-writer.test.ts +143 tests/clients/mutation-plan.test.ts +123 tests/clients/omo-client.test.ts +261 tests/clients/omp-path-contract.test.ts +91 tests/clients/omp-yaml-source-inline-comments.test.ts +115 tests/clients/pi-path-contract.test.ts +115 tests/clients/prime-client.test.ts +163 tests/clients/raycast-client.test.ts +87 tests/clients/raycast-detect.test.ts +6922 tests/clients/remote-catalog.test.ts +99 tests/clients/remote-control-prototype.test.ts +171 tests/clients/remote-workspace-activation.test.ts +115 tests/clients/remote-workspace-agent-wire.test.ts +192 tests/clients/remote-workspace-app-server.integration.test.ts +187 tests/clients/remote-workspace-claude.integration.test.ts +124 tests/clients/remote-workspace-cli-runtimes.test.ts +145 tests/clients/remote-workspace-cli.test.ts +2469 tests/clients/remote-workspace-codex-runtime.test.ts +128 tests/clients/remote-workspace-command-runner.test.ts +98 tests/clients/remote-workspace-device.test.ts +100 tests/clients/remote-workspace-hub.test.ts +85 tests/clients/remote-workspace-linux-confinement.test.ts +172 tests/clients/remote-workspace-management.test.ts +212 tests/clients/remote-workspace-platform.test.ts +78 tests/clients/remote-workspace-protocol.test.ts +87 tests/clients/remote-workspace-rpc-framing.test.ts +79 tests/clients/remote-workspace-secret-store.test.ts +709 tests/clients/remote-workspace-server.test.ts +104 tests/clients/remote-workspace-session-binding.test.ts +129 tests/clients/remote-workspace-sessions.test.ts +198 tests/clients/remote-workspace-tool-bridge.test.ts +163 tests/clients/remote-workspace.test.ts +401 tests/clients/sync-client-integrations.test.ts +550 tests/codex-integration/active-registry-admission.test.ts +137 tests/codex-integration/app-owned-memory.test.ts +3457 tests/codex-integration/bearer-admission-routed-provider.test.ts +147 tests/codex-integration/catalog-auto-refresh-scheduler.test.ts +115 tests/codex-integration/catalog-cursor-search.test.ts +147 tests/codex-integration/catalog-duplicate-slug-dedup.test.ts +142 tests/codex-integration/catalog-free-pricing-status.test.ts +4072 tests/codex-integration/catalog-full-picker-order.test.ts +139 tests/codex-integration/catalog-gated-native-suppression-reason.test.ts +131 tests/codex-integration/catalog-go-exact-efforts.test.ts +116 tests/codex-integration/catalog-hub-context-window.test.ts +181 tests/codex-integration/catalog-input-modality-enum.test.ts +126 tests/codex-integration/catalog-llamacpp-capabilities.test.ts +775 tests/codex-integration/catalog-modelalias-unique-sync.test.ts +153 tests/codex-integration/catalog-oauth-observation.test.ts +136 tests/codex-integration/catalog-opencode-go-context-window.test.ts +918 tests/codex-integration/catalog-remote-pull.test.ts +117 tests/codex-integration/catalog-retain-models.test.ts +110 tests/codex-integration/catalog-seed-window-fill.test.ts +187 tests/codex-integration/catalog-slug-uniqueness-boundary.test.ts +126 tests/codex-integration/catalog-verbosity-default.test.ts +143 tests/codex-integration/catalog-vision-sidecar-modalities.test.ts +115 tests/codex-integration/catalog-zero-credit-picker.test.ts +4512 tests/codex-integration/client-injection-guard.test.ts +172 tests/codex-integration/codex-account-delete-atomicity.test.ts +89 tests/codex-integration/codex-account-label.test.ts +83 tests/codex-integration/codex-account-namespaces.test.ts +173 tests/codex-integration/codex-account-selection-preferences.test.ts +151 tests/codex-integration/codex-account-store-refresh-classification.test.ts +886 tests/codex-integration/codex-account-store.test.ts +473 tests/codex-integration/codex-account-threshold-api.test.ts +255 tests/codex-integration/codex-account-threshold-auth.test.ts +304 tests/codex-integration/codex-account-threshold-routing.test.ts +153 tests/codex-integration/codex-account-unusable-reason.test.ts +133 tests/codex-integration/codex-admission-primitives.test.ts +150 tests/codex-integration/codex-admission.test.ts +105 tests/codex-integration/codex-affinity-debug.test.ts +79 tests/codex-integration/codex-app-server-path-spaces.test.ts +846 tests/codex-integration/codex-app-server-processes.test.ts +99 tests/codex-integration/codex-app-server-restart-service.test.ts +1925 tests/codex-integration/codex-auth-api.test.ts +110 tests/codex-integration/codex-auth-cancel-ownership.test.ts +138 tests/codex-integration/codex-auth-collision.test.ts +674 tests/codex-integration/codex-auth-context.test.ts +114 tests/codex-integration/codex-catalog-admission.test.ts +108 tests/codex-integration/codex-catalog-exclusions.test.ts +108 tests/codex-integration/codex-catalog-golden.test.ts +132 tests/codex-integration/codex-catalog-ladders.test.ts +123 tests/codex-integration/codex-catalog-model-picker-order.test.ts +83 tests/codex-integration/codex-catalog-refresh-status.test.ts +4572 tests/codex-integration/codex-catalog-restore.test.ts +18585 tests/codex-integration/codex-catalog-sync-hardening.test.ts +107 tests/codex-integration/codex-catalog-write-serialization.test.ts +103 tests/codex-integration/codex-catalog-writer.test.ts +1857 tests/codex-integration/codex-catalog.test.ts +125 tests/codex-integration/codex-cli-install-provenance.test.ts +104 tests/codex-integration/codex-cli-installation-identity.test.ts +79 tests/codex-integration/codex-cli-installation-targets.test.ts +89 tests/codex-integration/codex-cli-update-launcher-policy.test.ts +965 tests/codex-integration/codex-cli-update-zero-effect.test.ts +86 tests/codex-integration/codex-cli-windows-installation-files.test.ts +15251 tests/codex-integration/codex-composed-acceptance.test.ts +475 tests/codex-integration/codex-config-generation.test.ts +266 tests/codex-integration/codex-context-owner.test.ts +3803 tests/codex-integration/codex-convergence-account-selectors.test.ts +538 tests/codex-integration/codex-convergence-contract.test.ts +265 tests/codex-integration/codex-cooldown-recovery.test.ts +170 tests/codex-integration/codex-coordinator-doctor.test.ts +153 tests/codex-integration/codex-desired-state.test.ts +97 tests/codex-integration/codex-entitlement-identity-read-fence.test.ts +382 tests/codex-integration/codex-envkey-admission-substitution.test.ts +119 tests/codex-integration/codex-exec-invocation.test.ts +97 tests/codex-integration/codex-features-cache.test.ts +88 tests/codex-integration/codex-features-residual.test.ts +200 tests/codex-integration/codex-filesystem-evidence.test.ts +399 tests/codex-integration/codex-gather-authority.test.ts +2798 tests/codex-integration/codex-history-job.test.ts +151 tests/codex-integration/codex-history-lock.test.ts +1661 tests/codex-integration/codex-history-provider.test.ts +303 tests/codex-integration/codex-history-reachability.test.ts +93 tests/codex-integration/codex-history-worker-boundary.test.ts +1227 tests/codex-integration/codex-history-worker.test.ts +111 tests/codex-integration/codex-history-writer.test.ts +121 tests/codex-integration/codex-home-wsl.test.ts +257 tests/codex-integration/codex-inject-history-wording.test.ts +70464 tests/codex-integration/codex-inject-integration.test.ts +1122 tests/codex-integration/codex-inject-retained-table.test.ts +6724 tests/codex-integration/codex-inject-v1-reconcile.test.ts +9100 tests/codex-integration/codex-inject-write-lock.test.ts +138 tests/codex-integration/codex-inject.test.ts +91 tests/codex-integration/codex-injected-marker.test.ts +121 tests/codex-integration/codex-integration-record.test.ts +17011 tests/codex-integration/codex-journal.test.ts +100 tests/codex-integration/codex-legacy-config-keys.test.ts +213 tests/codex-integration/codex-lineage-placement.test.ts +143 tests/codex-integration/codex-log-guard-coderabbit.test.ts +98 tests/codex-integration/codex-log-guard-doctor-coderabbit.test.ts +183 tests/codex-integration/codex-log-guard-doctor-protection.test.ts +94 tests/codex-integration/codex-log-guard-doctor.test.ts +225 tests/codex-integration/codex-log-guard-inspect.test.ts +78 tests/codex-integration/codex-log-guard-lock.test.ts +735 tests/codex-integration/codex-log-guard-maintenance-coderabbit.test.ts +1146 tests/codex-integration/codex-log-guard-maintenance.test.ts +76 tests/codex-integration/codex-log-guard-policy.test.ts +78 tests/codex-integration/codex-log-guard-processes.test.ts +212 tests/codex-integration/codex-log-guard-protection.test.ts +208 tests/codex-integration/codex-log-guard-status-zero-write.test.ts +189 tests/codex-integration/codex-main-account-refresh.test.ts +227 tests/codex-integration/codex-main-rotation.test.ts +146 tests/codex-integration/codex-management-convergence.test.ts +176 tests/codex-integration/codex-metadata-integrity.test.ts +173 tests/codex-integration/codex-model-availability-error.test.ts +201 tests/codex-integration/codex-model-denial-evidence.test.ts +92 tests/codex-integration/codex-model-entitlement-admission.test.ts +214 tests/codex-integration/codex-model-entitlements.test.ts +194 tests/codex-integration/codex-models-cache-invalidate.test.ts +345 tests/codex-integration/codex-native-residue.test.ts +137 tests/codex-integration/codex-pin-drain-projection.test.ts +145 tests/codex-integration/codex-plan.test.ts +568 tests/codex-integration/codex-plugins-doctor.test.ts +178 tests/codex-integration/codex-pool-plan-exclusion.test.ts +82 tests/codex-integration/codex-pool-refresh-backoff.test.ts +536 tests/codex-integration/codex-pool-rotation.test.ts +262 tests/codex-integration/codex-priority-failback.test.ts +119 tests/codex-integration/codex-prompt-adopt.test.ts +126 tests/codex-integration/codex-prompt-base-variants.test.ts +91 tests/codex-integration/codex-prompt-journal.test.ts +101 tests/codex-integration/codex-prompt-layers-read.test.ts +264 tests/codex-integration/codex-prompt-layers-write.test.ts +86 tests/codex-integration/codex-prompt-layers.test.ts +98 tests/codex-integration/codex-prompt-lock.test.ts +1370 tests/codex-integration/codex-prompt-route.test.ts +856 tests/codex-integration/codex-prompt-text-probe.test.ts +97 tests/codex-integration/codex-provider-table-retention.test.ts +200 tests/codex-integration/codex-quota-auto-refresh-main-admission.test.ts +316 tests/codex-integration/codex-quota-auto-refresh.test.ts +95 tests/codex-integration/codex-quota-capacity.test.ts +194 tests/codex-integration/codex-quota-history.test.ts +131 tests/codex-integration/codex-quota-parser-parity.test.ts +361 tests/codex-integration/codex-quota-prime.test.ts +181 tests/codex-integration/codex-quota-rejection.test.ts +162 tests/codex-integration/codex-refresh.test.ts +1519 tests/codex-integration/codex-reset-credit-auto-redeem.test.ts +634 tests/codex-integration/codex-reset-credit-operation-ledger.test.ts +307 tests/codex-integration/codex-reset-credit-recovery.test.ts +91 tests/codex-integration/codex-restart-contract-parity.test.ts +271 tests/codex-integration/codex-restart-route.test.ts +5829 tests/codex-integration/codex-restore-app-rewrite.test.ts +7237 tests/codex-integration/codex-retained-root-serialization.test.ts +147 tests/codex-integration/codex-routing-cache-affinity-detour.test.ts +2801 tests/codex-integration/codex-routing.test.ts +141 tests/codex-integration/codex-runtime.test.ts +507 tests/codex-integration/codex-service-manager-probe-hardening.test.ts +156 tests/codex-integration/codex-service-manager-probe.test.ts +16277 tests/codex-integration/codex-shim-autorestore.test.ts +405 tests/codex-integration/codex-shim-destroyed-probe.test.ts +93 tests/codex-integration/codex-shim-ensure-failure.test.ts +10961 tests/codex-integration/codex-shim-readiness.test.ts +25412 tests/codex-integration/codex-shim.test.ts +134 tests/codex-integration/codex-signin-lockout.test.ts +604 tests/codex-integration/codex-spark-visibility.test.ts +180 tests/codex-integration/codex-sqlite-home.test.ts +2023 tests/codex-integration/codex-sync-api.test.ts +108 tests/codex-integration/codex-sync-response.test.ts +122 tests/codex-integration/codex-tool-mode.test.ts +701 tests/codex-integration/codex-transition-state-adoption.test.ts +97 tests/codex-integration/codex-transition-state-first-use-regression.test.ts +1878 tests/codex-integration/codex-transition-state-race.test.ts +287 tests/codex-integration/codex-transition-state.test.ts +121 tests/codex-integration/codex-user-identity.test.ts +528 tests/codex-integration/codex-v2-gate.test.ts +505 tests/codex-integration/codex-warmup.test.ts +89 tests/codex-integration/codex-websocket-registry.test.ts +3148 tests/codex-integration/codex-write-lock.test.ts +192 tests/codex-integration/combo-authoritative-reset.test.ts +325 tests/codex-integration/combos.test.ts +649 tests/codex-integration/compatibility-manifest.test.ts +91 tests/codex-integration/context-compat.test.ts +84 tests/codex-integration/custom-model-catalog-migration.test.ts +1093 tests/codex-integration/doctor.test.ts +237 tests/codex-integration/effort-policy.test.ts +146 tests/codex-integration/fast-row-listing.test.ts +131 tests/codex-integration/fast-row.test.ts +176 tests/codex-integration/gather-routed-models-single-flight.test.ts +129 tests/codex-integration/gpt6-native-rows.test.ts +87 tests/codex-integration/history-migration-guardian.test.ts +104 tests/codex-integration/history-ocx-compaction-recovery.test.ts +87 tests/codex-integration/history-paginated-openai-compat.test.ts +2318 tests/codex-integration/history-paginated-transition-destinations.test.ts +215 tests/codex-integration/injection-model-api.test.ts +5248 tests/codex-integration/issue-452-empty-503.test.ts +2788 tests/codex-integration/issue-702-expired-replay-state.test.ts +623 tests/codex-integration/issue-914-transport-attribution.test.ts +17598 tests/codex-integration/main-account-hard-lock-auth.test.ts +88 tests/codex-integration/main-account-hard-lock-policy.test.ts +153 tests/codex-integration/main-account-hard-lock-recovery.test.ts +129 tests/codex-integration/main-device-reauth-api.test.ts +188 tests/codex-integration/main-device-reauth.test.ts +160 tests/codex-integration/main-quota-evidence-validation.test.ts +878 tests/codex-integration/main-quota-provenance.test.ts +269 tests/codex-integration/main-quota-window-observation.test.ts +94 tests/codex-integration/model-cache-generation-tombstone.test.ts +89 tests/codex-integration/model-cache.test.ts +156 tests/codex-integration/model-display-names-management-api.test.ts +243 tests/codex-integration/model-metadata-sync.test.ts +839 tests/codex-integration/model-pinned-effort.test.ts +564 tests/codex-integration/model-visibility-management-api.test.ts +634 tests/codex-integration/multi-agent-compat.test.ts +325 tests/codex-integration/multi-agent-keep-native-v1.test.ts +139 tests/codex-integration/native-alias-maintainer-regressions.test.ts +254 tests/codex-integration/native-claude-code-toggle.test.ts +353 tests/codex-integration/native-claude-desktop-toggle.test.ts +2112 tests/codex-integration/native-codex-toggle.test.ts +408 tests/codex-integration/native-grok-toggle.test.ts +195 tests/codex-integration/native-main-auth-temp.test.ts +90 tests/codex-integration/native-main-claim-cache.test.ts +161 tests/codex-integration/native-main-claim.test.ts +7905 tests/codex-integration/native-main-owner-lifetime.test.ts +344 tests/codex-integration/native-model-toggle.test.ts +135 tests/codex-integration/native-profile-api.test.ts +8866 tests/codex-integration/native-profile-crash-boundaries.test.ts +2226 tests/codex-integration/native-profile-drain-server.test.ts +3526 tests/codex-integration/native-profile-manager.test.ts +487 tests/codex-integration/native-profile-processes.test.ts +106 tests/codex-integration/native-profile-recovery.test.ts +342 tests/codex-integration/native-profile-route-security.test.ts +303 tests/codex-integration/native-profile-stage-lifecycle.test.ts +14260 tests/codex-integration/native-profile-startup.test.ts +564 tests/codex-integration/native-profile-store.test.ts +328 tests/codex-integration/orca-import.test.ts +148 tests/codex-integration/parallel-tool-calls-optin.test.ts +382 tests/codex-integration/project-config-warnings.test.ts +244 tests/codex-integration/reasoning-effort.test.ts +127 tests/codex-integration/reasoning-metadata.test.ts +185 tests/codex-integration/reserve-auth-context.test.ts +122 tests/codex-integration/reserve-availability.test.ts +5880 tests/codex-integration/reserve-catalog-lifecycle.test.ts +153 tests/codex-integration/reserve-catalog.test.ts +1751 tests/codex-integration/reserve-dispatch.test.ts +171 tests/codex-integration/reserve-helper-boundary.test.ts +119 tests/codex-integration/reserve-passive-revocation.test.ts +143 tests/codex-integration/reserve-quota-scope.test.ts +129 tests/codex-integration/selected-models.test.ts +146 tests/codex-integration/slug-codec.test.ts +192 tests/codex-integration/token-guardian.test.ts +109 tests/codex-integration/ultrafast-tier-honesty.test.ts +128 tests/codex-integration/upstream-reachability.test.ts +91 tests/codex-integration/warmup.test.ts +182 tests/config/client-config-export-new-clients.test.ts +166 tests/config/client-config-export.test.ts +130 tests/config/client-config-new-clients.test.ts +95 tests/config/config-account-thresholds.test.ts +92 tests/config/config-catalog-auto-refresh.test.ts +86 tests/config/config-commandcode-claude-pin.test.ts +140 tests/config/config-load-degrade.test.ts +1085 tests/config/config-mutation-lock.test.ts +98 tests/config/config-non-object-backup.test.ts +100 tests/config/config-ownership-uninstall.test.ts +91 tests/config/config-rebase-provenance-writers.test.ts +93 tests/config/config-save-boundary.test.ts +95 tests/config/config-spend-ceilings.test.ts +513 tests/config/config-user-edits.test.ts +82 tests/config/expand-user-path.test.ts +249 tests/config/model-pinned-effort-config.test.ts +141 tests/config/settings-desktop-switch-apply.test.ts +186 tests/config/settings-fast-rows.test.ts +259 tests/config/settings-main-account-hard-lock.test.ts +173 tests/config/settings-oauth-open-browser.test.ts +172 tests/config/settings-startup-health-seam.test.ts +2628 tests/config/settings-stream-mode.test.ts +78 tests/config/types-barrel-identity.test.ts +94 tests/config/url-normalization.test.ts +99 tests/config/yaml-fragment-source.test.ts +133 tests/e2e-style/phase100-native-parity.test.ts +104 tests/gui/alibaba-intl-token-plan.test.ts +106 tests/gui/claude-manual-env.test.ts +92 tests/gui/codex-account-mode-state.test.ts +90 tests/gui/codex-auth-modal-status.test.ts +105 tests/gui/combo-workspace-data.test.ts +98 tests/gui/dashboard-uptime.test.ts +188 tests/gui/gui-api-error.test.ts +229 tests/gui/gui-codex-usage-score-parity.test.ts +188 tests/gui/gui-desktop-sidecar-script.test.ts +262 tests/gui/gui-management-session.test.ts +89 tests/gui/gui-pair-capability.test.ts +91 tests/gui/gui-pair-client.test.ts +105 tests/gui/gui-static.test.ts +87 tests/gui/gui-tray-vibrancy-surface.test.ts +375 tests/gui/integrations-invariants.test.ts +89 tests/gui/logs-model-tier-confirmation.test.ts +108 tests/gui/main-device-reauth-ui.test.ts +86 tests/gui/models-feedback-callback.test.ts +94 tests/gui/models-free-filter.test.ts +84 tests/gui/models-page-groups.test.ts +193 tests/gui/models-workspace-tabs.test.ts +86 tests/gui/oauth-first-add-hint.test.ts +96 tests/gui/oauth-tos-warning.test.ts +402 tests/gui/platform-dialog-guard.test.ts +130 tests/gui/provider-payload.test.ts +106 tests/gui/provider-workspace-auth.test.ts +174 tests/gui/provider-workspace-data.test.ts +164 tests/gui/provider-workspace-rail.test.ts +94 tests/gui/provider-workspace-state.test.ts +148 tests/gui/quota-bars-rows.test.ts +96 tests/gui/qwen-cloud-endpoints.test.ts +102 tests/gui/rate-limit-reset-credits.test.ts +98 tests/gui/routing-intelligence-ui.test.ts +89 tests/gui/routing-profile-editor-data.test.ts +99 tests/gui/standalone-build-script.test.ts +97 tests/gui/startup-health-ui.test.ts +200 tests/gui/tencent-siliconflow-providers.test.ts +263 tests/gui/vision-sidecar-timeout-bounds.test.ts +261 tests/gui/volcengine-providers.test.ts +228 tests/images/artifacts-prune.test.ts +109 tests/images/artifacts-ssrf.test.ts +90 tests/images/download-cap-default.test.ts +123 tests/images/gemini-inline.test.ts +156 tests/images/loop-reasoning-replay.test.ts +3410 tests/images/loop.test.ts +108 tests/images/pinned-https-get.test.ts +123 tests/images/plan.test.ts +88 tests/images/synthetic-tool.test.ts +148 tests/images/xai-client.test.ts +87 tests/images/z-fulfill.test.ts +235 tests/images/z-handler-activation.test.ts +504 tests/lab/core-lab-boundary.test.ts +332 tests/lab/lab-activation.test.ts +372 tests/lab/lab-automation-coderabbit-regressions.test.ts +294 tests/lab/lab-automation-final-coderabbit-regressions.test.ts +264 tests/lab/lab-automation-ingwannu-regressions.test.ts +228 tests/lab/lab-automation-management-http.test.ts +78 tests/lab/lab-automation-persisted-cap-regression.test.ts +673 tests/lab/lab-automation-review-regressions.test.ts +495 tests/lab/lab-automation.test.ts +271 tests/lab/lab-community-evidence.test.ts +214 tests/lab/lab-community-filename-contract.test.ts +103 tests/lab/lab-community-mutation-lock.test.ts +190 tests/lab/lab-community-publisher-continuity.test.ts +267 tests/lab/lab-conformance-harness.test.ts +174 tests/lab/lab-conformance-runner-failures.test.ts +476 tests/lab/lab-evidence-ledger.test.ts +362 tests/lab/lab-evidence-sanitization.test.ts +152 tests/lab/lab-fabric-outcome-validation.test.ts +93 tests/lab/lab-fabric-persistence-boundary.test.ts +173 tests/lab/lab-fabric-producer-deadline.test.ts +68756 tests/lab/lab-fabric-task.test.ts +110 tests/lab/lab-installation-salt-cache.test.ts +762 tests/lab/lab-ledger-mutation-lock.test.ts +360 tests/lab/lab-live-pinned-timeouts.test.ts +214 tests/lab/lab-live-probe.test.ts +228 tests/lab/lab-live-receipt-integrity.test.ts +149 tests/lab/lab-live-review-regressions.test.ts +523 tests/lab/lab-live-sandbox.test.ts +172 tests/lab/lab-passive-production-evidence.test.ts +159 tests/lab/lab-passive-production-surfaces.test.ts +89 tests/lab/lab-paths-security.test.ts +160 tests/lab/lab-post-merge-hardening.test.ts +138 tests/lab/lab-post-merge-projection.test.ts +108 tests/lab/lab-private-file-consumer-recovery.test.ts +100 tests/lab/lab-private-file-durability.test.ts +210 tests/lab/lab-public-api-json.test.ts +175 tests/lab/lab-public-artifact-policy.test.ts +253 tests/lab/lab-public-coderabbit-regressions.test.ts +96 tests/lab/lab-public-core-contract.test.ts +170 tests/lab/lab-public-deep-review-regressions.test.ts +161 tests/lab/lab-public-evidence.test.ts +178 tests/lab/lab-public-export-transaction.test.ts +103 tests/lab/lab-public-file-safety.test.ts +230 tests/lab/lab-public-final-review-regressions.test.ts +222 tests/lab/lab-public-lifecycle-hardening.test.ts +83 tests/lab/lab-public-privacy-ipv6.test.ts +419 tests/lab/lab-public-provenance-recovery.test.ts +303 tests/lab/lab-public-review-fixes.test.ts +77 tests/lab/lab-public-route-registry.test.ts +108 tests/lab/lab-public-security-regressions.test.ts +510 tests/lab/lab-public-surfaces.test.ts +119 tests/lab/lab-public-wire-contract.test.ts +212 tests/lab/lab-read-filter-validation.test.ts +618 tests/lab/lab-read-surfaces.test.ts +758 tests/lib/abort-idle-deadline.test.ts +88 tests/lib/acl-error-classification.test.ts +313 tests/lib/ambiguous-resend-composition.test.ts +186 tests/lib/ambiguous-resend-gate.test.ts +188 tests/lib/bun-stream-caps.test.ts +120 tests/lib/clearable-deadline.test.ts +317 tests/lib/credential-redirect-guard.test.ts +100 tests/lib/debug.test.ts +95 tests/lib/execution-budget-permits.test.ts +119 tests/lib/failure-attribution.test.ts +84 tests/lib/failure-stage-model.test.ts +191 tests/lib/local-destinations.test.ts +298 tests/lib/optional-shutdown-hooks.test.ts +265 tests/lib/pinned-http-content-coding.test.ts +361 tests/lib/pinned-http.test.ts +95 tests/lib/privacy-mask-account.test.ts +198 tests/lib/process-control-graceful.test.ts +93 tests/lib/process-control.test.ts +184 tests/lib/provider-egress.test.ts +88 tests/lib/reasoning-replay-scope-source.test.ts +142 tests/lib/redact.test.ts +88 tests/lib/remove-tree-helper.test.ts +92 tests/lib/self-launch-argv.test.ts +1512 tests/lib/socks5-fetch.test.ts +134 tests/lib/socks5-upload-lifecycle.test.ts +186 tests/lib/spend-ceiling-enforcement.test.ts +101 tests/lib/spend-ledger-file-journal.test.ts +718 tests/lib/spend-ledger-owner.test.ts +107 tests/lib/spend-reservation-ledger.test.ts +100 tests/lib/stall-subprocess-exit.test.ts +82 tests/lib/stall-timeout.test.ts +88 tests/lib/standalone.test.ts +83 tests/lib/strict-semver.test.ts +76 tests/lib/system-restart-contract-security.test.ts +103 tests/lib/token-estimate.test.ts +89 tests/lib/transient-budget-scope-source.test.ts +132 tests/lib/transport-null-body.test.ts +8163 tests/lib/upstream-retry.test.ts +159 tests/lib/workflow-budget.test.ts +331 tests/oauth/adapter-event-oauth-failover.test.ts +4173 tests/oauth/chatgpt-device-auth.test.ts +98 tests/oauth/chatgpt-oauth.test.ts +94 tests/oauth/chatgpt-token-expiry.test.ts +225 tests/oauth/generic-oauth-failover.test.ts +384 tests/oauth/key-login-live-update.test.ts +118 tests/oauth/key-login-preserves-model-costs.test.ts +99 tests/oauth/local-token-detect.test.ts +367 tests/oauth/oauth-account-attribution.test.ts +94 tests/oauth/oauth-account-id-collision.test.ts +151 tests/oauth/oauth-account-quota-rank.test.ts +1023 tests/oauth/oauth-accounts-api.test.ts +72 tests/oauth/oauth-callback-binds.test.ts +113 tests/oauth/oauth-callback-server.test.ts +95 tests/oauth/oauth-device-code-contract.test.ts +141 tests/oauth/oauth-health.test.ts +95 tests/oauth/oauth-log.test.ts +147 tests/oauth/oauth-login-cli-browser-launch.test.ts +453 tests/oauth/oauth-login-cli-live-update.test.ts +115 tests/oauth/oauth-login-open-browser.test.ts +132 tests/oauth/oauth-login-summary.test.ts +534 tests/oauth/oauth-manual-code.test.ts +164 tests/oauth/oauth-open-browser-choice.test.ts +118 tests/oauth/oauth-open-url-result.test.ts +199 tests/oauth/oauth-provider-reconcile.test.ts +310 tests/oauth/oauth-public-surface.test.ts +178 tests/oauth/oauth-reauth-bind.test.ts +141 tests/oauth/oauth-refresh-generic-lock.test.ts +3207 tests/oauth/oauth-refresh-lock-multiprocess.test.ts +496 tests/oauth/oauth-refresh.test.ts +195 tests/oauth/oauth-status-privacy.test.ts +256 tests/oauth/oauth-store-multi.test.ts +244 tests/oauth/oauth-upsert-preserves-api-key.test.ts +81 tests/oauth/pool-kernel-generic-sweep.test.ts +159 tests/oauth/state-store-sweeper.test.ts +184 tests/providers/alibaba-region-backup.test.ts +162 tests/providers/alibaba-region-migration.test.ts +95 tests/providers/alibaba-region-startup.test.ts +340 tests/providers/alibaba-token-plan-responses-optin.test.ts +150 tests/providers/api-key-catalog-authority.test.ts +256 tests/providers/api-key-selection-capture.test.ts +139 tests/providers/aside-client.test.ts +87 tests/providers/auto-compact-budget.test.ts +140 tests/providers/azure-adapter.test.ts +102 tests/providers/azure-model-router-tool-schema.test.ts +296 tests/providers/baseten-provider.test.ts +169 tests/providers/chutes-provider.test.ts +145 tests/providers/cline-pass-deepseek-v4-tool-replay.test.ts +139 tests/providers/cline-pass-provider.test.ts +133 tests/providers/cline-pass-reasoning-efforts.test.ts +188 tests/providers/cline-provider.test.ts +3109 tests/providers/codebuddy-adapter.test.ts +106 tests/providers/codebuddy-live-acceptance.test.ts +2686 tests/providers/codebuddy-mcp-server.test.ts +98 tests/providers/codebuddy-protocol.test.ts +283 tests/providers/codebuddy-tool-bridge-turn.test.ts +129 tests/providers/codebuddy-tool-bridge.test.ts +100 tests/providers/command-code-error-finish.test.ts +278 tests/providers/command-code-fakeip-discovery.test.ts +676 tests/providers/command-code-provider.test.ts +146 tests/providers/command-code-quota.test.ts +432 tests/providers/command-code-tool-text.test.ts +101 tests/providers/command-code-workspace-cache.test.ts +233 tests/providers/commandcode-provider.test.ts +183 tests/providers/context-cap-unknown-window.test.ts +93 tests/providers/context-window-seed-repair.test.ts +183 tests/providers/crusoe-provider.test.ts +357 tests/providers/cursor/cursor-adapter.test.ts +76 tests/providers/cursor/cursor-arg-normalize.test.ts +85 tests/providers/cursor/cursor-blob-integrity.test.ts +506 tests/providers/cursor/cursor-blob.test.ts +94 tests/providers/cursor/cursor-call-id.test.ts +126 tests/providers/cursor/cursor-cancel-provenance.test.ts +118 tests/providers/cursor/cursor-catalog.test.ts +91 tests/providers/cursor/cursor-claude-id.test.ts +223 tests/providers/cursor/cursor-continuation-invariants.test.ts +250 tests/providers/cursor/cursor-continuity-retention.test.ts +92 tests/providers/cursor/cursor-default-catalog-suppression.test.ts +109 tests/providers/cursor/cursor-desktop-exec.test.ts +95 tests/providers/cursor/cursor-discovery.test.ts +126 tests/providers/cursor/cursor-display-names.test.ts +632 tests/providers/cursor/cursor-effort-rows.test.ts +106 tests/providers/cursor/cursor-effort-suffix.test.ts +103 tests/providers/cursor/cursor-effort-table.test.ts +190 tests/providers/cursor/cursor-envelope-echo-retry.test.ts +172 tests/providers/cursor/cursor-eof-terminal.test.ts +108 tests/providers/cursor/cursor-errors.test.ts +105 tests/providers/cursor/cursor-exec-empty-result.test.ts +119 tests/providers/cursor/cursor-fast-listing.test.ts +96 tests/providers/cursor/cursor-fast-tier.test.ts +196 tests/providers/cursor/cursor-framing.test.ts +264 tests/providers/cursor/cursor-h2-pool-shutdown.test.ts +3267 tests/providers/cursor/cursor-hardening.test.ts +290 tests/providers/cursor/cursor-http1-transport.test.ts +3705 tests/providers/cursor/cursor-images.test.ts +594 tests/providers/cursor/cursor-integration-status.test.ts +111 tests/providers/cursor/cursor-interaction-query.test.ts +77 tests/providers/cursor/cursor-kv-store.test.ts +83 tests/providers/cursor/cursor-live-smoke-gate.test.ts +296 tests/providers/cursor/cursor-live-transport.test.ts +359 tests/providers/cursor/cursor-local-models-schema.test.ts +226 tests/providers/cursor/cursor-mcp-manager.test.ts +536 tests/providers/cursor/cursor-mcp-stdio.test.ts +79 tests/providers/cursor/cursor-message-mapper.test.ts +76 tests/providers/cursor/cursor-native-exec-common.test.ts +130 tests/providers/cursor/cursor-native-exec-policy.test.ts +210 tests/providers/cursor/cursor-native-exec-shell.test.ts +593 tests/providers/cursor/cursor-native-exec.test.ts +169 tests/providers/cursor/cursor-oauth-shell.test.ts +731 tests/providers/cursor/cursor-oauth.test.ts +81 tests/providers/cursor/cursor-pool.test.ts +113 tests/providers/cursor/cursor-protobuf-events.test.ts +187 tests/providers/cursor/cursor-repetition-breaker.test.ts +208 tests/providers/cursor/cursor-request-builder.test.ts +91 tests/providers/cursor/cursor-request-compat.test.ts +126 tests/providers/cursor/cursor-roster-account-scope.test.ts +102 tests/providers/cursor/cursor-silent-redirect.test.ts +163 tests/providers/cursor/cursor-static-catalog.test.ts +3572 tests/providers/cursor/cursor-stream-health.test.ts +141 tests/providers/cursor/cursor-structured-edit.test.ts +96 tests/providers/cursor/cursor-tool-arg-decoding.test.ts +105 tests/providers/cursor/cursor-tool-choice.test.ts +133 tests/providers/cursor/cursor-tool-continuation.test.ts +102 tests/providers/cursor/cursor-tool-definitions.test.ts +4012 tests/providers/cursor/cursor-tool-finalize-race.test.ts +181 tests/providers/cursor/cursor-tool-result-image.test.ts +353 tests/providers/cursor/cursor-tool-result-invocation.test.ts +139 tests/providers/cursor/cursor-tool-suspended-checkpoint.test.ts +132 tests/providers/cursor/cursor-toolresult-normalize.test.ts +1868 tests/providers/cursor/cursor-transport-retry.test.ts +99 tests/providers/cursor/cursor-ultra-mode.test.ts +96 tests/providers/cursor/cursor-umbrella-rows.test.ts +90 tests/providers/cursor/cursor-uncallable-quarantine.test.ts +107 tests/providers/cursor/cursor-vision-wire-harness.test.ts +314 tests/providers/cyber-policy-error-fidelity.test.ts +160 tests/providers/deepinfra-provider.test.ts +512 tests/providers/deepseek-inbound-wire.test.ts +156 tests/providers/deepseek-reasoning-replay-gaps.test.ts +256 tests/providers/deepseek-reasoning-replay.test.ts +274 tests/providers/deepseek-responses-item-id-repair.test.ts +136 tests/providers/devin-adapter-reset-wait.test.ts +121 tests/providers/devin-adapter.test.ts +86 tests/providers/devin-cli-authmode-migration.test.ts +95 tests/providers/devin-effort-ladder.test.ts +138 tests/providers/devin-hardening.test.ts +132 tests/providers/devin-image-passthrough.test.ts +123 tests/providers/devin-live-models.test.ts +139 tests/providers/devin-login.test.ts +127 tests/providers/devin-output-budget.test.ts +149 tests/providers/devin-prompt-cache.test.ts +100 tests/providers/devin-provider-merge-migration.test.ts +88 tests/providers/devin-stated-reset-hardening.test.ts +103 tests/providers/devin-stated-reset-retry.test.ts +121 tests/providers/devin-stream-deadline.test.ts +198 tests/providers/digitalocean-scaleway-provider.test.ts +280 tests/providers/exa-web-search.test.ts +237 tests/providers/fast-row-ingress.test.ts +296 tests/providers/featherless-provider.test.ts +149 tests/providers/flash-route-image-modalities.test.ts +399 tests/providers/forward-admission-separation.test.ts +379 tests/providers/github-copilot/github-copilot-account-origin.test.ts +16117 tests/providers/github-copilot/github-copilot-oauth.test.ts +98 tests/providers/github-copilot/github-copilot-sse-rewrite.test.ts +174 tests/providers/github-copilot/github-copilot-stream-contract.test.ts +289 tests/providers/github-copilot/github-copilot-wire-defaults.test.ts +155 tests/providers/hyperbolic-provider.test.ts +546 tests/providers/initial-model-selection.test.ts +298 tests/providers/initial-selection-write-fence.test.ts +101 tests/providers/kimi-oauth-identity.test.ts +118 tests/providers/kimi-responses-adjacency.test.ts +128 tests/providers/kiro/kiro-account-quota.test.ts +255 tests/providers/kiro/kiro-adapter.test.ts +291 tests/providers/kiro/kiro-auth-context-continuation.test.ts +142 tests/providers/kiro/kiro-builder-id-profile.test.ts +80 tests/providers/kiro/kiro-calibration.test.ts +103 tests/providers/kiro/kiro-fallback-error-body.test.ts +1745 tests/providers/kiro/kiro-images.test.ts +293 tests/providers/kiro/kiro-oauth.test.ts +136 tests/providers/kiro/kiro-pool-rank.test.ts +131 tests/providers/kiro/kiro-reasoning-roundtrip.test.ts +82 tests/providers/kiro/kiro-remote-image.test.ts +600 tests/providers/kiro/kiro-retry.test.ts +189 tests/providers/kiro/kiro-review-regressions.test.ts +478 tests/providers/kiro/kiro-stream.test.ts +120 tests/providers/kiro/kiro-usage-quota.test.ts +77 tests/providers/kiro/kiro-windows-cli-db-path.test.ts +91 tests/providers/kiro/kiro-windows-cli-executable-path.test.ts +80 tests/providers/kiro/kiro-wire-estimate.test.ts +135 tests/providers/meta-model-api-provider.test.ts +139 tests/providers/meta-muse-device.test.ts +122 tests/providers/meta-muse-login-order.test.ts +135 tests/providers/meta-muse-oauth.test.ts +85 tests/providers/mimo-effort.test.ts +213 tests/providers/mimo-free-provider.test.ts +118 tests/providers/mimo-token-plan-provider.test.ts +1531 tests/providers/minimax-clients.test.ts +286 tests/providers/minimax-reasoning-split.test.ts +95 tests/providers/model-presets.test.ts +199 tests/providers/model-rename-migration.test.ts +78 tests/providers/moonshot-endpoints.test.ts +518 tests/providers/moonshot-tool-schema.test.ts +125 tests/providers/muse-key-quota.test.ts +933 tests/providers/muse-passive-quota-cache.test.ts +283 tests/providers/muse-passive-quota-observation.test.ts +127 tests/providers/muse-spark-web-search-compat.test.ts +94 tests/providers/muse-subscription-usage.test.ts +126 tests/providers/muse-tool-name-alias.test.ts +87 tests/providers/new-model-policy.test.ts +101 tests/providers/nous-oauth-live.test.ts +8242 tests/providers/nous-oauth.test.ts +148 tests/providers/novita-provider.test.ts +150 tests/providers/nscale-vultr-provider.test.ts +220 tests/providers/nvidia-nim-hardening.test.ts +782 tests/providers/ollama/ollama-native-parser.test.ts +115 tests/providers/ollama/ollama-native-reasoning-wire.test.ts +89 tests/providers/ollama/ollama-native-structured-output.test.ts +846 tests/providers/ollama/ollama-native-v4.test.ts +151 tests/providers/ollama/ollama-native.test.ts +491 tests/providers/ollama/ollama-show-enrichment-v7.test.ts +151 tests/providers/ollama/ollama-show-enrichment.test.ts +355 tests/providers/ollama/ollama-show-ignore-abort.test.ts +434 tests/providers/opencode-cli.test.ts +122 tests/providers/opencode-free-provider.test.ts +135 tests/providers/opencode-go-deepseek.test.ts +197 tests/providers/opencode-go-grok46-responses.test.ts +429 tests/providers/opencode-go-luna-wire.test.ts +122 tests/providers/opencode-go-muse-context.test.ts +123 tests/providers/opencode-go-muse-vision.test.ts +123 tests/providers/opencode-go-quota.test.ts +1078 tests/providers/opencode-go-session-header.test.ts +234 tests/providers/opencode-management-transport.test.ts +86 tests/providers/opencode-muse-terminal-repair.test.ts +144 tests/providers/opencode-zen-deepseek-reasoning.test.ts +97 tests/providers/opencode-zen-rate-limit.test.ts +137 tests/providers/openrouter-provider-routing.test.ts +260 tests/providers/openrouter-quota-reset-cooldown-4024.test.ts +165 tests/providers/opper-provider.test.ts +303 tests/providers/orcarouter-provider.test.ts +2088 tests/providers/provider-account-quota-persistence.test.ts +264 tests/providers/provider-account-quota.test.ts +745 tests/providers/provider-api-keys.test.ts +86 tests/providers/provider-capacity.test.ts +280 tests/providers/provider-config-batch-management.test.ts +101 tests/providers/provider-config-validation.test.ts +229 tests/providers/provider-connection-test.test.ts +136 tests/providers/provider-cost-overlay-config.test.ts +179 tests/providers/provider-discovery-log-suppression.test.ts +99 tests/providers/provider-egress-outbound.test.ts +113 tests/providers/provider-id-rewrite.test.ts +402 tests/providers/provider-key-store.test.ts +139 tests/providers/provider-live-models.test.ts +176 tests/providers/provider-model-aliases.test.ts +225 tests/providers/provider-model-discovery-contract.test.ts +89 tests/providers/provider-outbound-private-network.test.ts +1879 tests/providers/provider-outbound.test.ts +135 tests/providers/provider-quota-label-sanitize.test.ts +138 tests/providers/provider-quota-observed-marker.test.ts +477 tests/providers/provider-quota.test.ts +284 tests/providers/provider-registry-parity.test.ts +114 tests/providers/provider-static-model-discovery.test.ts +108 tests/providers/qoder-adapter.test.ts +109 tests/providers/qoder-live-models.test.ts +90 tests/providers/qoder-scaffold-guard.test.ts +89 tests/providers/qwen38-preserve-reasoning.test.ts +298 tests/providers/rate-limit-retry.test.ts +221 tests/providers/resolved-model-policy.test.ts +171 tests/providers/sambanova-nebius-provider.test.ts +89 tests/providers/sponsor-presets.test.ts +307 tests/providers/stepfun-provider.test.ts +140 tests/providers/umans-provider.test.ts +7212 tests/providers/upstream-transient-retry.test.ts +141 tests/providers/vercel-gateway-provider-routing.test.ts +137 tests/providers/vision-classification-seed-repair.test.ts +95 tests/providers/volcengine-ark-assistant-content.test.ts +180 tests/providers/xai/grok-attribution.test.ts +201 tests/providers/xai/grok-config-inject.test.ts +220 tests/providers/xai/grok-effort-inject.test.ts +146 tests/providers/xai/grok-lifecycle.test.ts +531 tests/providers/xai/grok-management-api.test.ts +480 tests/providers/xai/grok-models-effort-list.test.ts +188 tests/providers/xai/grok-orphan-adoption.test.ts +112 tests/providers/xai/grok-reset-coupon-cli.test.ts +84 tests/providers/xai/grok-reset-coupons.test.ts +132 tests/providers/xai/grok-selection.test.ts +121 tests/providers/xai/grok-status.test.ts +144 tests/providers/xai/grok-sync.test.ts +137 tests/providers/xai/grok-writer-boundary.test.ts +106 tests/providers/xai/xai-empty-catalog-tool-choice.test.ts +101 tests/providers/xai/xai-oauth-retry.test.ts +228 tests/providers/xai/xai-refresh-lock.test.ts +115 tests/providers/xai/xai-responses-adjacency.test.ts +120 tests/providers/xai/xai-tool-schema.test.ts +192 tests/providers/xai/xai-transport.test.ts +135 tests/providers/xai/xai-web-search-compat.test.ts +141 tests/providers/xai/xai-web-search.test.ts +250 tests/providers/zai-reasoning-replay.test.ts +156 tests/providers/zcode-client.test.ts +263 tests/providers/zhipu-bigmodel-provider.test.ts +117 tests/providers/zhipu-bigmodel-responses-quota.test.ts +84 tests/responses/apply-patch-envelope.test.ts +152 tests/responses/bare-echo-alias.test.ts +411 tests/responses/chat-completions-deferred-tools.test.ts +11752 tests/responses/chat-completions-endpoint.test.ts +3782 tests/responses/chat-conversation-affinity.test.ts +261 tests/responses/chat-inbound-developer-position.test.ts +87 tests/responses/chat-inbound-reasoning-none.test.ts +160 tests/responses/chat-inbound-reasoning-replay.test.ts +127 tests/responses/chat-inline-document-bytes.test.ts +413 tests/responses/chat-json-sse-fallback.test.ts +347 tests/responses/chat-media-translation.test.ts +485 tests/responses/chat-native-developer-role.test.ts +160 tests/responses/chat-native-image-normalization.test.ts +90 tests/responses/chat-native-lenient-finish.test.ts +716 tests/responses/chat-native-spend.test.ts +85 tests/responses/chat-refusal-scope.test.ts +351 tests/responses/chat-refusal.test.ts +594 tests/responses/chat-responses-control-integration.test.ts +123 tests/responses/chat-responses-control-scope.test.ts +90 tests/responses/chat-tool-choice-allowed-tools.test.ts +121 tests/responses/citation-markers.test.ts +117 tests/responses/compaction-progress.test.ts +105 tests/responses/continuation-dedup.test.ts +98 tests/responses/custom-tool-compat.test.ts +512 tests/responses/empty-completion-core.test.ts +93 tests/responses/empty-completion-guard.test.ts +192 tests/responses/empty-completion-hardening.test.ts +192 tests/responses/eventstream-decoder.test.ts +421 tests/responses/fresh-connection-optout.test.ts +84 tests/responses/legacy-shell-compat.test.ts +106 tests/responses/namespace-tool-compat.test.ts +162 tests/responses/opaque-blob-wrapped-rejection.test.ts +1098 tests/responses/openai-responses-passthrough.test.ts +108 tests/responses/parser-content-audio.test.ts +258 tests/responses/passthrough-abort.test.ts +177 tests/responses/passthrough-headers.test.ts +121 tests/responses/passthrough-override.test.ts +142 tests/responses/plaintext-v2-agent-messages.test.ts +99 tests/responses/provider-egress-fetch.test.ts +152 tests/responses/reasoning-effort-summary-default.test.ts +204 tests/responses/reasoning-envelope.test.ts +118 tests/responses/reserve-dispatch-ws.test.ts +106 tests/responses/responses-4546-incident-regression.test.ts +102 tests/responses/responses-account-change-scrub.test.ts +2729 tests/responses/responses-account-label.test.ts +236 tests/responses/responses-anthropic-fast-downgrade.test.ts +146 tests/responses/responses-bare-echo-helper-fence.test.ts +112 tests/responses/responses-canonical-only-top-level-fields.test.ts +100 tests/responses/responses-code-mode-patch-compile.test.ts +169 tests/responses/responses-code-mode-shell-compile.test.ts +6286 tests/responses/responses-compact-handoff-admission.test.ts +955 tests/responses/responses-compaction-override.test.ts +141 tests/responses/responses-compaction-policy-identity.test.ts +5958 tests/responses/responses-compaction-routing.test.ts +395 tests/responses/responses-compaction.test.ts +5884 tests/responses/responses-console-go-upload-retry.test.ts +2358 tests/responses/responses-context-overflow.test.ts +142 tests/responses/responses-continuation-boundaries.test.ts +128 tests/responses/responses-core-modules.test.ts +182 tests/responses/responses-custom-tool-guidance.test.ts +175 tests/responses/responses-custom-tool-historical-replay.test.ts +281 tests/responses/responses-custom-tool-repair-dispatch.test.ts +110 tests/responses/responses-custom-tool-repair.test.ts +106 tests/responses/responses-custom-tool-stream-consistency.test.ts +87 tests/responses/responses-default-namespace-emit-normalize.test.ts +114 tests/responses/responses-fetch-helpers-boundary.test.ts +98 tests/responses/responses-field-backfill.test.ts +114 tests/responses/responses-forward-dangling-call.test.ts +259 tests/responses/responses-forward-incomplete-quota.test.ts +123 tests/responses/responses-forward-posit-continuation.test.ts +133 tests/responses/responses-forward-prompt-envelope.test.ts +150 tests/responses/responses-freeform-wrapper-keys.test.ts +210 tests/responses/responses-function-tool-repair.test.ts +118 tests/responses/responses-hosted-tool-declaration.test.ts +125 tests/responses/responses-hosted-tool-min-spread.test.ts +181 tests/responses/responses-image-gen-repair.test.ts +945 tests/responses/responses-inbound-store-default.test.ts +103 tests/responses/responses-item-id-repair.test.ts +106 tests/responses/responses-json-events.test.ts +82 tests/responses/responses-legacy-dotted-tool-name-repair.test.ts +228 tests/responses/responses-muse-tool-name-alias.test.ts +234 tests/responses/responses-native-main-refresh.test.ts +4869 tests/responses/responses-opaque-blob-recovery.test.ts +84 tests/responses/responses-parser-agent-message.test.ts +123 tests/responses/responses-parser-malformed-content.test.ts +207 tests/responses/responses-parser.test.ts +120 tests/responses/responses-passthrough-transient-policy.test.ts +1249 tests/responses/responses-pool-401-refresh.test.ts +151 tests/responses/responses-pool-refresh-attribution.test.ts +553 tests/responses/responses-preview-main-read-fence.test.ts +216 tests/responses/responses-reasoning-effort-downgrade.test.ts +179 tests/responses/responses-reasoning-summary-passthrough.test.ts +119 tests/responses/responses-reset-replay.test.ts +133 tests/responses/responses-routed-web-search-fields.test.ts +2175 tests/responses/responses-self-named-namespace-scrub.test.ts +5621 tests/responses/responses-send-budget-counts.test.ts +92 tests/responses/responses-send-budget-errors.test.ts +304 tests/responses/responses-shadow-intercept.test.ts +229 tests/responses/responses-show-thinking-summary.test.ts +558 tests/responses/responses-snapshot-repair-server.test.ts +176 tests/responses/responses-snapshot-repair.test.ts +122 tests/responses/responses-sparse-terminal-tool-scope.test.ts +117 tests/responses/responses-spend-ledger-wiring.test.ts +309 tests/responses/responses-spill-shutdown-clock.test.ts +293 tests/responses/responses-state-write-amplification.test.ts +7918 tests/responses/responses-state.test.ts +858 tests/responses/responses-stateless-dangling-call-repair.test.ts +96 tests/responses/responses-stream-tool-events.test.ts +123 tests/responses/responses-terminal-repair.test.ts +101 tests/responses/responses-tool-conformance.test.ts +86 tests/responses/responses-tool-groups.test.ts +239 tests/responses/responses-tool-search-repair.test.ts +810 tests/responses/responses-undeclared-tool-guard.test.ts +141 tests/responses/responses-usage-passthrough.test.ts +141 tests/responses/responses-xai-request-compat.test.ts +218 tests/responses/sse-client-frame-bounds.test.ts +516 tests/responses/sse-decoder.test.ts +278 tests/responses/sse-failed-tail.test.ts +501 tests/responses/sse-inspector-bounds.test.ts +126 tests/responses/sse-null-data-frame.test.ts +138 tests/responses/sse-payload-rewrite.test.ts +108 tests/responses/sse-unspaced-data-fields.test.ts +96 tests/responses/thought-signature-credential-scope.test.ts +157 tests/responses/ws-endpoint.test.ts +118 tests/responses/ws-failure-stage.test.ts +415 tests/responses/ws-native-injection.test.ts +266 tests/responses/ws-native-result-continuations.test.ts +3315 tests/responses/ws-native-steering.test.ts +282 tests/responses/ws-steering-completion.test.ts +202 tests/responses/ws-steering-smoke.test.ts +261 tests/responses/ws-steering-stability.test.ts +152 tests/responses/ws-upstream-reuse.test.ts +111 tests/responses/ws-upstream-socks5.test.ts +760 tests/responses/ws-upstream.test.ts +146 tests/routing/always-on-429-failover.test.ts +131 tests/routing/anthropic-quorum-cache.test.ts +91 tests/routing/cl01-claude-outbound-review-regressions.test.ts +190 tests/routing/cl01-openai-chat-review-regressions.test.ts +159 tests/routing/cl01-review-regressions.test.ts +361 tests/routing/combo-child-headers.test.ts +588 tests/routing/combo-management-api.test.ts +214 tests/routing/combo-stream-preflight.test.ts +249 tests/routing/compatibility-provider-equivalence.test.ts +102 tests/routing/destination-policy-resolved.test.ts +142 tests/routing/fastwire-characterization-routing.test.ts +403 tests/routing/fastwire-characterization-wire.test.ts +248 tests/routing/fastwire-observability.test.ts +151 tests/routing/fastwire-policy.test.ts +169 tests/routing/policy-execution.test.ts +462 tests/routing/probe-lease-dispatch-wiring.test.ts +318 tests/routing/probe-lease.test.ts +144 tests/routing/resolved-model-policy-consumers.test.ts +111 tests/routing/router-combo-failover-classification.test.ts +154 tests/routing/router-discarded-baseurl-warning.test.ts +129 tests/routing/router-template-baseurl.test.ts +157 tests/routing/router.test.ts +639 tests/routing/routing-analytics.test.ts +95 tests/routing/routing-capability-catalog.test.ts +429 tests/routing/routing-capability-model-matching.test.ts +100 tests/routing/routing-compatibility-auth-identity.test.ts +259 tests/routing/routing-compatibility-boundaries.test.ts +84 tests/routing/routing-compatibility-model-matching.test.ts +220 tests/routing/routing-compatibility.test.ts +181 tests/routing/routing-identity-domains.test.ts +361 tests/routing/routing-policy-fallback.test.ts +116 tests/routing/routing-policy-pool-quota.test.ts +344 tests/routing/routing-policy-surface-parity.test.ts +473 tests/routing/routing-profile-management-editor.test.ts +346 tests/routing/routing-profile.test.ts +130 tests/routing/subagent-context-staleness.test.ts +99 tests/routing/subagent-defaults.test.ts +3828 tests/routing/subagent-fallback-handle-responses.test.ts +194 tests/routing/subagent-fallback-preview-sites.test.ts +209 tests/routing/subagent-model-fallback-api.test.ts +465 tests/routing/subagent-model-fallback.test.ts +182 tests/routing/subagent-roster-retention.test.ts +128 tests/server/account-import.test.ts +1168 tests/server/account-pool-management-api.test.ts +147 tests/server/adapter-resolve.test.ts +177 tests/server/agent-task-recovery-cache.test.ts +394 tests/server/agent-task-recovery-combo.test.ts +202 tests/server/agent-task-recovery-fallback.test.ts +242 tests/server/agent-task-recovery-security.test.ts +1379 tests/server/agent-task-recovery.test.ts +170 tests/server/alias-management-api.test.ts +120 tests/server/api-access-endpoints.test.ts +701 tests/server/api-catalog-route.test.ts +338 tests/server/api-codex-log-guard-compact.test.ts +269 tests/server/api-codex-log-guard-protection.test.ts +170 tests/server/api-codex-log-guard.test.ts +689 tests/server/api-debug.test.ts +2391 tests/server/api-key-attribution.test.ts +130 tests/server/api-key-model-scope.test.ts +169 tests/server/api-key-scope-alpha-search.test.ts +158 tests/server/api-key-scope-audio.test.ts +169 tests/server/api-key-scope-images.test.ts +157 tests/server/api-key-scope-live.test.ts +1559 tests/server/api-keys-routes.test.ts +510 tests/server/aside-profiles-routes.test.ts +112 tests/server/audio-client.test.ts +693 tests/server/audio-dictation.test.ts +823 tests/server/audio-transcriptions.test.ts +371 tests/server/bounded-body.test.ts +145 tests/server/bridge-live-delivery.test.ts +2359 tests/server/cancel-body-on-abort.test.ts +651 tests/server/claude-intercept-integration.test.ts +144 tests/server/companion-settings.test.ts +427 tests/server/config.test.ts +138 tests/server/consume-for-inspection-cancel.test.ts +255 tests/server/context-history-ownership.test.ts +121 tests/server/context-history.test.ts +536 tests/server/data-plane-admission-identity.test.ts +87 tests/server/debug-settings.test.ts +173 tests/server/error-fidelity.test.ts +227 tests/server/errors-adapter-failure.test.ts +336 tests/server/fetch-header-timeout.test.ts +414 tests/server/health-scoring.test.ts +364 tests/server/hub-usage.test.ts +126 tests/server/input-admission.test.ts +141 tests/server/live-call-bindings.test.ts +343 tests/server/local-aside-sync-capability.test.ts +79 tests/server/local-management-attestation.test.ts +84 tests/server/local-management-capability.test.ts +700 tests/server/local-management-direct-transport.test.ts +81 tests/server/local-provider-reload-client.test.ts +580 tests/server/logs-timezone.test.ts +134 tests/server/loopback-companion-client-targets.test.ts +138 tests/server/loopback-listener-admission.test.ts +2199 tests/server/loopback-listener-integration.test.ts +147 tests/server/management-anthropic-reset-grants.test.ts +185 tests/server/management-api-logs-metrics.test.ts +437 tests/server/management-client-config-route.test.ts +180 tests/server/management-google-tool-schema-policy.test.ts +264 tests/server/management-integration-journal-delete.test.ts +641 tests/server/management-integration-routes.test.ts +1379 tests/server/management-metrics-export.test.ts +150 tests/server/management-model-roster-gather-race.test.ts +136 tests/server/management-model-roster.test.ts +103 tests/server/management-origin-tls.test.ts +256 tests/server/management-provider-pinsless-validation.test.ts +245 tests/server/management-provider-proto-override.test.ts +91 tests/server/management-provider-reset-replay.test.ts +370 tests/server/management-provider-synthetic-max.test.ts +6724 tests/server/management-provider-validation.test.ts +322 tests/server/management-provider-verbosity.test.ts +205 tests/server/management-route-registry.test.ts +141 tests/server/management-workflow-budget-routes.test.ts +538 tests/server/memory-watchdog.test.ts +200 tests/server/model-costs-management-api.test.ts +346 tests/server/model-discovery-management-api.test.ts +124 tests/server/outbound-body-guard.test.ts +88 tests/server/owned-service-home.test.ts +94 tests/server/passive-route-linker.test.ts +5380 tests/server/plaintext-v2-agent-messages-server.test.ts +922 tests/server/port-reclaim.test.ts +543 tests/server/ports.test.ts +298 tests/server/provider-account-quota-routes.test.ts +97 tests/server/provider-egress-management-validation.test.ts +358 tests/server/provider-send-path-import.test.ts +129 tests/server/proxy-env.test.ts +121 tests/server/proxy-liveness.test.ts +711 tests/server/relay-eager.test.ts +707 tests/server/replay-refusal-parity.test.ts +412 tests/server/reserve-claude-policy.test.ts +4966 tests/server/reserve-ingress.test.ts +787 tests/server/response-log-inspection.test.ts +208 tests/server/response-model-identity.test.ts +161 tests/server/retry-after-429.test.ts +99 tests/server/retry-delay-hardening.test.ts +245 tests/server/route-decision-trace.test.ts +642 tests/server/server-403-permission-e2e.test.ts +269 tests/server/server-agent-task-recovery-replay.test.ts +234 tests/server/server-auth-localhost-bind.test.ts +1652 tests/server/server-auth-scoped-quota.test.ts +17291 tests/server/server-auth.test.ts +629 tests/server/server-background-lifecycle.test.ts +81 tests/server/server-clickjacking-headers.test.ts +8206 tests/server/server-combo-failover-e2e.test.ts +93 tests/server/server-combo-held-response.test.ts +3804 tests/server/server-combo-reasoning-replay-eligibility.test.ts +219 tests/server/server-combo-zero-output-failover.test.ts +1175 tests/server/server-google-antigravity-oauth-401-replay.test.ts +108 tests/server/server-gui-bundle-freshness.test.ts +144 tests/server/server-images-bodyless-content-length.test.ts +3645 tests/server/server-images.test.ts +3733 tests/server/server-key-failover-e2e.test.ts +894 tests/server/server-kiro-completion-e2e.test.ts +512 tests/server/server-kiro-oauth-401-replay.test.ts +164 tests/server/server-live-frame-log.test.ts +826 tests/server/server-live-realtime-fixtures.test.ts +4860 tests/server/server-live.test.ts +190 tests/server/server-loopback-host-gate.test.ts +1442 tests/server/server-management-auth.test.ts +374 tests/server/server-opencode-go-goal-streaming.test.ts +1232 tests/server/server-rate-limit-retry-e2e.test.ts +1183 tests/server/server-request-body-size.test.ts +1263 tests/server/server-search.test.ts +688 tests/server/server-startup-reconcile-resilience.test.ts +376 tests/server/server-stop-config-hardening.test.ts +298 tests/server/server-xai-chat-reasoning-streaming.test.ts +651 tests/server/server-xai-header-parity.test.ts +603 tests/server/server-xai-oauth-401-replay.test.ts +747 tests/server/server-xai-responses-streaming.test.ts +157 tests/server/session-affinity.test.ts +296 tests/server/session-lane-recall-harness.test.ts +178 tests/server/sidebar-routes.test.ts +81 tests/server/sidebar-star-state.test.ts +108 tests/server/spend-instrumentation-log.test.ts +91 tests/server/spend-ledger-lifecycle.test.ts +291 tests/server/spend-ledger-owner-startup.test.ts +115 tests/server/startup-action-control-elevation.test.ts +142 tests/server/startup-action-control.test.ts +100 tests/server/startup-prompt.test.ts +122 tests/server/stream-aborted-marker.test.ts +172 tests/server/system-env.test.ts +193 tests/server/system-restart.test.ts +347 tests/server/system-routes.test.ts +4191 tests/server/terminal-guard-server.test.ts +170 tests/server/terminal-guard.test.ts +87 tests/server/upstream-connect-error.test.ts +105 tests/server/upstream-http-version.test.ts +577 tests/server/v1-hub-state.test.ts +277 tests/server/v2-agent-message-failfast.test.ts +145 tests/service/autostart-health.test.ts +3644 tests/service/container-bootstrap.test.ts +184 tests/service/crash-guard.test.ts +134 tests/service/doctor-codex-envkey-readiness.test.ts +134 tests/service/doctor-oauth.test.ts +143 tests/service/doctor-provider-apikey.test.ts +85 tests/service/doctor-service-memory-contract.test.ts +116 tests/service/init-backup-cleanup.test.ts +7056 tests/service/init-eof.test.ts +242 tests/service/launchd-repair.test.ts +117 tests/service/live-service-manager-guard.test.ts +89 tests/service/managing-cli.test.ts +89 tests/service/process-state.test.ts +151 tests/service/service-auth-qualified-localhost.test.ts +104 tests/service/service-claim.test.ts +82 tests/service/service-ownership-compatibility.test.ts +189 tests/service/service-ownership-handover.test.ts +351 tests/service/service-ownership-state.test.ts +87 tests/service/service-probe-docker.test.ts +99 tests/service/service-secrets.test.ts +262 tests/service/service-sqlite-home.test.ts +191 tests/service/service-start-environment.test.ts +115 tests/service/service-stop-verification.test.ts +323 tests/service/service-tier-capability.test.ts +124 tests/service/service-wsl-home-ownership.test.ts +496 tests/service/service.test.ts +337 tests/service/shutdown-drain.test.ts +4745 tests/service/shutdown-launcher.test.ts +74 tests/service/stale-state-purge.test.ts +82 tests/service/standalone-service.test.ts +3590 tests/service/stop-deferred-teardown.test.ts +92 tests/service/systemd-install-cleanup-hardening.test.ts +127 tests/service/winsw.test.ts +3107 tests/storage/api-storage-cleanup.test.ts +1221 tests/storage/storage-cleanup.test.ts +6567 tests/storage/storage-mutation-race.test.ts +116 tests/storage/storage-policy-config-race.test.ts +2167 tests/storage/storage-policy-job-responsive.test.ts +160 tests/storage/storage-policy.test.ts +96 tests/storage/storage-restore-job-errors.test.ts +5122 tests/storage/storage-restore-job-responsive.test.ts +175 tests/storage/storage-scanner.test.ts +1048 tests/storage/storage-worker-lifecycle.test.ts +186 tests/storage/storage-worker-os-join-settle.test.ts +4270 tests/storage/storage-worker-teardown-isolate.test.ts +285 tests/test-layout-tooling.test.ts +202 tests/test-layout.test.ts +92 tests/update/update-badge.test.ts +5263 tests/update/update-bun-ownership-lease.test.ts +99 tests/update/update-desktop-owner.test.ts +861 tests/update/update-job.test.ts +106 tests/update/update-notify.test.ts +274 tests/update/update-npm-cache-preflight.test.ts +82 tests/update/update-npm-invocation.test.ts +172 tests/update/update-pnpm.test.ts +1319 tests/update/update-stop-classification.test.ts +3647 tests/update/update-stop-first.test.ts +304 tests/update/update-transactional.test.ts +89 tests/update/update-tray-handoff.test.ts +259 tests/update/update-tree-ownership.test.ts +129 tests/usage/cache-diagnostic.test.ts +199 tests/usage/cost-cap-unknown-evidence.test.ts +122 tests/usage/cost-scoring.test.ts +120 tests/usage/key-attribution.test.ts +399 tests/usage/quota-401-recovery-runtime.test.ts +78 tests/usage/quota-401-recovery.test.ts +114 tests/usage/quota-reset-account-key.test.ts +402 tests/usage/quota-reset-core-boundary.test.ts +92 tests/usage/quota-reset-detector.test.ts +84 tests/usage/quota-reset-notify-config.test.ts +440 tests/usage/quota-reset-notify.test.ts +626 tests/usage/quota-reset-observation.test.ts +1690 tests/usage/quota-reset-seen-store.test.ts +170 tests/usage/quota-scoring.test.ts +1388 tests/usage/request-decompress.test.ts +184 tests/usage/request-evidence.test.ts +1073 tests/usage/request-history-index.test.ts +154 tests/usage/request-log-conversation.test.ts +116 tests/usage/request-log-estimate-cap.test.ts +317 tests/usage/request-log-nonstream.test.ts +123 tests/usage/request-log-served-model.test.ts +322 tests/usage/request-log.test.ts +182 tests/usage/request-outcome-agreement.test.ts +248 tests/usage/request-pacing.test.ts +264 tests/usage/usage-aggregate-cache.test.ts +92 tests/usage/usage-anthropic-fast-pricing.test.ts +85 tests/usage/usage-attempt-delivery.test.ts +115 tests/usage/usage-cost.test.ts +253 tests/usage/usage-debug.test.ts +88 tests/usage/usage-failure-fingerprint.test.ts +147 tests/usage/usage-failure-persistence.test.ts +82 tests/usage/usage-failure-projection.test.ts +138 tests/usage/usage-ledger-retention.test.ts +301 tests/usage/usage-ledger-scanner.test.ts +188 tests/usage/usage-log-ws-stage.test.ts +2374 tests/usage/usage-log.test.ts +97 tests/usage/usage-provider-label.test.ts +332 tests/usage/usage-shape-extraction.test.ts +105 tests/usage/usage-spend-cache-provenance.test.ts +222 tests/usage/usage-summary.test.ts +80 tests/usage/usage-surfaces.test.ts +185 tests/usage/usage-time-range.test.ts +96 tests/usage/usage-timeline.test.ts +143 tests/usage/user-cost-overlay-coderabbit-regressions.test.ts +1767 tests/usage/user-cost-overlay-live-reconcile.test.ts +127 tests/usage/user-cost-overlay-provider-delete.test.ts +108 tests/videos/fulfill-video.test.ts +113 tests/videos/plan-video.test.ts +118 tests/videos/xai-video-client.test.ts +151 tests/vision/sidecar-abort.test.ts +143 tests/vision/sidecar-auth.test.ts +139 tests/vision/sidecar-candidates.test.ts +200 tests/vision/sidecar-settings-vision-controls.test.ts +216 tests/vision/sidecar-settings-vision-filter.test.ts +250 tests/vision/sidecar-settings-web-search-gate.test.ts +221 tests/vision/sidecar-settings-web-search-stream.test.ts +85 tests/vision/sidecar-tracker.test.ts +183 tests/vision/vision-anthropic.test.ts +253 tests/vision/vision-backend-union.test.ts +152 tests/vision/vision-cache.test.ts +124 tests/vision/vision-custom-row-precedence.test.ts +105 tests/vision/vision-eligibility.test.ts +128 tests/vision/vision-fail-closed.test.ts +181 tests/vision/vision-reasoning-contract.test.ts +395 tests/vision/vision-routed.test.ts +873 tests/vision/vision-sidecar-e2e.test.ts +121 tests/vision/vision-text-only-predicate.test.ts +86 tests/web-search/format-result.test.ts +129 tests/web-search/web-search-anthropic.test.ts +309 tests/web-search/web-search-backend-union.test.ts +133 tests/web-search/web-search-bridge-replay.test.ts +132 tests/web-search/web-search-candidates.test.ts +96 tests/web-search/web-search-parse.test.ts +490 tests/web-search/web-search-passthrough-bridge.test.ts +2620 tests/web-search/web-search-progress-stream.test.ts +1644 tests/web-search/web-search-retry-heartbeat.test.ts +7323 tests/web-search/web-search-sidecar-429.test.ts +89 tests/web-search/web-search-sources.test.ts +449 tests/web-search/web-search-timeout-contract.test.ts +133 tests/web-search/web-search-timeout-plan.test.ts +5412 tests/web-search/web-search.test.ts +187 tests/windows/tray-proxy-deadline.test.ts +98 tests/windows/tray-proxy.test.ts +92 tests/windows/win-exec.test.ts +186 tests/windows/win-paths.test.ts +99 tests/windows/windows-acl-start-cost.test.ts +185 tests/windows/windows-atomic-replace.test.ts +95 tests/windows/windows-deploy-close-regressions.test.ts +371 tests/windows/windows-elevation-spawn.test.ts +98 tests/windows/windows-elevation.test.ts +139 tests/windows/windows-popup-fix.test.ts +163 tests/windows/windows-scheduler-install-verification.test.ts +380 tests/windows/windows-secret-acl.test.ts +159 tests/windows/windows-service-mutation-lock.test.ts +87 tests/windows/windows-service-wrappers.test.ts +175 tests/windows/windows-text-decoding.test.ts +88 tests/windows/windows-tray-restart-hardening.test.ts +98 tests/windows/windows-tray-run-limit.test.ts +245 tests/windows/windows-tray.test.ts +84 tests/windows/windows-user-principal-nonascii.test.ts +181 tests/windows/windows-user-principal.test.ts +75 tests/windows/winsw-stop-hardening.test.ts diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 726c72808c8..895510d88c4 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -336,6 +336,8 @@ "ci-privacy-gate.test.ts": "ci-workflows", "ci-review-lanes.test.ts": "ci-workflows", "ci-scope-reduction.test.ts": "ci-workflows", + "ci-scope-gaps.test.ts": "ci-workflows", + "ci-shard-balance.test.ts": "ci-workflows", "ci-structure-gate.test.ts": "ci-workflows", "ci-workflows.test.ts": "ci-workflows", "citation-markers.test.ts": "responses", @@ -1289,7 +1291,9 @@ "release-desktop-scripts.test.ts": "ci-workflows", "release-helper.test.ts": "ci-workflows", "release-notes.test.ts": "ci-workflows", + "release-outcome-report.test.ts": "ci-workflows", "release-pipeline-contract.test.ts": "ci-workflows", + "release-preflight.test.ts": "ci-workflows", "release-resume-identity.test.ts": "ci-workflows", "release-version-line.test.ts": "ci-workflows", "release-version-sources.test.ts": "ci-workflows", diff --git a/structure/ops/cross-platform-ci.md b/structure/ops/cross-platform-ci.md index 753fb463cd3..2423ca34560 100644 --- a/structure/ops/cross-platform-ci.md +++ b/structure/ops/cross-platform-ci.md @@ -20,6 +20,15 @@ the Rust toolchain, or the app bundle. Those regressions are caught at the promo `preview` or `main`, before publication, and on demand by explicit dispatch — a pull request that is green is not full-platform proof. +Two paths sit outside the `ci` filter on purpose and get narrow jobs instead of the full matrix. +A change under `.github/actions/` runs `setup-action` on Linux, Windows and macOS: it runs the +composite Bun setup and requires the installed runtime to equal the version `package.json` +declares. A change under `native/remote-workspace-helper/` runs `remote-helper` on the same three +runners: `cargo fmt` on Linux, then `cargo clippy -D warnings` and `cargo test` everywhere, where +the live confinement tests compile only on macOS and Windows. Both filters also list `ci.yml`, +both stay pull-request scope like `docs` and `structure`, their outputs are validated before any +job reads them, and the aggregate gate expects each job exactly when its filter output is `true`. + No recovery retry can turn a failed workflow green. Linux, Windows, macOS shards and macOS control use `scripts/ci/run-bun-test-batches.sh`, but each lane owns its measured process shape: Linux keeps the default twelve files and 120 seconds; @@ -33,6 +42,12 @@ manager guards remain active because the preload installs them before the lock b Test teardown follows the [sandbox cleanup contract](test-sandbox-cleanup.md). `tests/preload.ts` resolves cleanup dependencies after home/lock admission and before test cases; teardown awaits native-main startup releases and config hardening, then the sandbox's registered ACL child reaps before removing that root. Its synchronous exit fallback leaves an undrained root for ownership-checked stale recovery instead of blocking child cleanup with removal retries. `tests/ci-workflows/test-sandbox-cleanup.test.ts` pins that ordering with a delayed reap. `tests/helpers/test-sandbox-cleanup.ts` exposes case-scoped lifecycle ownership: cancellation starts listener stops while owned asynchronous work settles, and repeated close/stop calls share one promise. After teardown starts, only the lifecycle's own abort reason is absorbed; any other error, including a foreign AbortError, still fails its case. Callers settle that lifecycle before draining producers/reaps and restoring or removing a home. The helper does not replace fixture-specific cleanup or claim OS ACL coverage for synthetic tests. +Shard membership follows recorded duration: `scripts/ci/test-durations.tsv` weighs each file, +the heaviest file goes to the least-loaded shard, and a file without a row weighs the table's +median, so an empty table reproduces sorted round-robin exactly. Every shard computes the whole +assignment and refuses to run unless it covers every selected file once. A batch also closes +before its predicted duration passes half the process timeout, which only adds process +boundaries. `scripts/ci/test-durations.ts refresh` regenerates the table from hosted job logs. A test failure, a process timeout and a Bun runtime crash each fail their job on the first occurrence; the batch runner still sweeps a crashed or timed-out batch one file per process, but only to attribute a failure the shard has already taken. The aggregate `ci` gate derives, from the event and the `changes` outputs, which @@ -67,10 +82,20 @@ ocx help The CI intentionally does not build docs, run coverage, or perform remote Ubuntu/RDP smoke tests. Those stay outside the default gate until a concrete regression justifies the extra runtime. -The Release workflow remains manual and publish-focused. Before any dry-run or publish step, it -checks that the exact release commit (`GITHUB_SHA`) already has a successful push-event -Cross-platform CI run — a pull-request run does not qualify — that `dev` already outranks the -target, and that the target passes the fresh global tag-ordering gate. +The Release workflow remains manual and publish-focused. Its `preflight` job runs right after +dispatch validation and before either packaging job: `scripts/ci/release-preflight.sh` checks the +channel and dist-tag, every version source, the tag, the GitHub release, npm, the fresh global +tag ordering and the `dev` pre-move, so a release that can never publish fails in its first minute +instead of after the packaging matrix. The workflow-level `release` concurrency group is one +constant slot shared by every ref, which serialises stable and preview runs; the preflight +therefore sees whatever the previous release run published. The publish job repeats every one of +those checks immediately before publishing, because tags, releases and registry state can still +move while a run packages, and additionally requires a successful push-event Cross-platform CI run +for the exact release commit (`GITHUB_SHA`) — a pull-request run does not qualify. +After publication the registry smoke records the npm version read-back and the dist-tag as +separate outputs, and the `release-outcomes` job, which runs after `publish` and `attach-release` +whatever their result, reports the public GitHub release, the npm version and the npm dist-tag as +separate summary rows. A row that is not confirmed warns without changing the run's result. This keeps release runs short and makes release a deployment of a verified commit after the required `dev` pre-move rather than a second CI pipeline. diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index dda26659c64..bcc051a9e32 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -186,9 +186,9 @@ Those controls still have no owner, so there is no image-publish workflow or off | Workflow | Trigger | Purpose | | --- | --- | --- | -| `.github/workflows/ci.yml` | Any `pull_request`; runtime/package `push` to `main`/`preview`; manual dispatch | A pull request verifies Linux and TypeScript: Linux runs four suite shards plus `gates` alongside the scoped docs, structure, packaging, keyring, and npm-global jobs. The `platform-macos` macOS suite, the `widget` macOS widget + Tauri app-bundle build, and the `desktop-shell` Rust toolchain build are native-gated: they run on `main`/`preview` pushes and manual dispatch, and on a pull request only when the `changes` job's native path filter selects the change. `dev` pushes start nothing; dev integration is covered by the pull-request run, while `main` and `preview` must stay push triggers because `release.yml` requires a push-event run for the exact release SHA. Windows runs nine shards only on manual dispatch with `lane=all` (or empty), not on push events. Linux runs at-most-12-file processes with a 120-second process bound; Windows uses measured six-file/480-second processes and all-file scope so its full-suite contract is unchanged. The dedicated Windows batch step sets `OCX_TEST_NO_QUEUE=1` because its sequential processes are one logical runner; each process still creates an isolated home and arms the test guards before the lock boundary. No lane retries: a test failure, a process timeout and a Bun runtime crash each fail their job on the first occurrence. Aggregate `ci` is event-aware — it derives which jobs this event requested and requires `success` from each of them and `skipped` from the rest, and on a `lane=all` dispatch it reads the run's own job list and requires nine concrete successful `windows N/9` results. `npm-global-smoke` remains GitHub-hosted because it mutates the global package prefix. Manual `lane=release-gates` keeps the ordinary native-gated jobs and selected dynamic keyring/packaging matrix legs, but skips the Windows suite and unsharded macOS control. Default `all` (or empty) still requests both diagnostics; `macos-control` requests the control without Windows suite shards. Release eligibility requires successful push-event CI on the exact release SHA; a manual lane does not authorize publishing. | +| `.github/workflows/ci.yml` | Any `pull_request`; runtime/package `push` to `main`/`preview`; manual dispatch | A pull request verifies Linux and TypeScript: Linux runs four suite shards plus `gates` alongside the scoped docs, structure, packaging, keyring, and npm-global jobs. The `platform-macos` macOS suite, the `widget` macOS widget + Tauri app-bundle build, and the `desktop-shell` Rust toolchain build are native-gated: they run on `main`/`preview` pushes and manual dispatch, and on a pull request only when the `changes` job's native path filter selects the change. `dev` pushes start nothing; dev integration is covered by the pull-request run, while `main` and `preview` must stay push triggers because `release.yml` requires a push-event run for the exact release SHA. Windows runs nine shards only on manual dispatch with `lane=all` (or empty), not on push events. Linux runs at-most-12-file processes with a 120-second process bound; Windows uses measured six-file/480-second processes and all-file scope so its full-suite contract is unchanged. The dedicated Windows batch step sets `OCX_TEST_NO_QUEUE=1` because its sequential processes are one logical runner; each process still creates an isolated home and arms the test guards before the lock boundary. No lane retries: a test failure, a process timeout and a Bun runtime crash each fail their job on the first occurrence. Aggregate `ci` is event-aware — it derives which jobs this event requested and requires `success` from each of them and `skipped` from the rest, and on a `lane=all` dispatch it reads the run's own job list and requires nine concrete successful `windows N/9` results. `npm-global-smoke` remains GitHub-hosted because it mutates the global package prefix. Manual `lane=release-gates` keeps the ordinary native-gated jobs and selected dynamic keyring/packaging matrix legs, but skips the Windows suite and unsharded macOS control. Default `all` (or empty) still requests both diagnostics; `macos-control` requests the control without Windows suite shards. Release eligibility requires successful push-event CI on the exact release SHA; a manual lane does not authorize publishing. Changes that touch only `.github/actions/` or `native/remote-workspace-helper/` run the narrow `setup-action` and `remote-helper` jobs instead of the full matrix, and Linux shard membership follows the per-file durations in `scripts/ci/test-durations.tsv`. | | `.github/workflows/dev-version-bump.yml` | Manual dispatch with an intended version and `pre-move` or `repair` mode | Opens the reviewed pull request that moves `dev` past a release target. The default `pre-move` mode runs before promotion and publication; explicit `repair` mode retains the post-publish catch-up path. It is neither called by `release.yml` nor triggered by publication. | -| `.github/workflows/release.yml` | Manual dispatch only | npm publish/dry-run workflow. It requires a successful push-event Cross-platform CI run for the exact `GITHUB_SHA` (a pull-request run does not qualify), requires `dev` to outrank the target, then checks the target against the freshly fetched global tag set before publish or dry-run. | +| `.github/workflows/release.yml` | Manual dispatch only | npm publish/dry-run workflow. The `preflight` job checks channel, version sources, tag, GitHub release, npm, global tag ordering and the `dev` pre-move before any packaging job starts. The publish job repeats those checks, requires a successful push-event Cross-platform CI run for the exact `GITHUB_SHA` (a pull-request run does not qualify), requires `dev` to outrank the target, then checks the target against the freshly fetched global tag set before publish or dry-run. After a real publish, `release-outcomes` reports the public GitHub release, the npm version read-back and the npm dist-tag as separate rows. | | `.github/workflows/deploy-docs.yml` | `push` to `main` touching `docs-site/**` or the workflow, or manual dispatch | Build and publish the Astro/Starlight docs site to GitHub Pages. This is the deploy path; the pull-request build gate is the `docs-site-build` job in `ci.yml`. | | `.github/workflows/service-lifecycle.yml` | `pull_request` to `main`/`dev` and `push` to `main`/`preview`, both filtered on the service path set (`src/service.ts`, `src/cli.ts`, `src/cli/index.ts`, `src/lib/bun-runtime.ts`, `package.json`, `bun.lock`, the workflow), or manual dispatch | Service-lifecycle smoke on three platforms: Linux systemd, macOS launchd, and Windows Scheduled Tasks. Each installs, verifies, stops via `ocx stop`, and uninstalls. The path list is kept in sync with the `release.yml` service-gate regex. | | `.github/workflows/enforce-pr-target.yml` | `pull_request_target` (opened, reopened, edited, labeled, unlabeled, ready_for_review, synchronize) plus default-branch `status` events filtered to successful `CodeRabbit` statuses | The `enforce-target` gate: rejects pull requests whose head ancestry sits on the `main` tip while far behind `dev`, rejects empty or malformed descriptions, requires a GUI screenshot when the title/body mentions `gui` (immediately waivable with the maintainer-controlled `gui-screenshot-waived` label; legacy maintainer comments remain compatibility evidence on later PR events), keeps contributor PRs in draft until a four-box readiness checklist is complete, verifies the CI / latest-dev / Codex+CodeRabbit-findings claims (review threads plus current-head CodeRabbit review-body findings outside the diff range), and adds a `review-ready` status label at the ready moment. CodeRabbit status SHAs must resolve to exactly one open current-head PR before writes. Stacked child PRs targeting another open PR's head skip the wrong-base gate. | @@ -205,10 +205,11 @@ it is promoted, so those files follow the promotion model rather than ordinary i `scripts/test.ts` owns `SERIAL_FULL_SUITE_FILES`, the shared process-isolation roster. Local full-suite runs, both macOS paths, and `scripts/ci/run-bun-test-batches.sh` execute those files -alone with fresh process homes. Hosted batches preserve sorted round-robin shard membership -and split only process boundaries; every selected file still runs once. Ordinary macOS shards -select 1/2 and 2/2 from the full sorted file list; macOS control selects 1/1. Both execute -sequential batches of at most 12 files with one worker. +alone with fresh process homes. Hosted batches assign shard membership by the per-file durations +in `scripts/ci/test-durations.tsv` (sorted round-robin when nothing is recorded), run each shard's +files in sorted order and split only process boundaries; every selected file still runs once. +Ordinary macOS shards select 1/2 and 2/2 from the full file list; macOS control selects 1/1. Both +execute sequential batches of at most 12 files with one worker. Storage-policy and API-usage families run as singletons, as do manifest-declared files. This preserves full test membership but does not claim cross-batch shared-process coverage. Every primary assertion failure, timeout or crash fails the run; diagnostic singleton @@ -364,7 +365,7 @@ from the dispatch input, so a `package.json`-only move ships an app that reports version under a manifest naming the new one, and the updater re-offers that release forever. `scripts/release-version-sources.ts` owns the list and the one-line rewrite of each file. `scripts/release.ts` and `scripts/bump-dev-version.ts` rewrite through it, `release.yml` runs its -`check` in `package-desktop` before anything is built and again in `publish`, and +`check` in `preflight` and `package-desktop` before anything is built and again in `publish`, and `dev-version-bump.yml` stages exactly those four paths and refuses a reused bump branch that touches anything else. `tests/ci-workflows/release-version-sources.test.ts` fails on drift in the working tree and pins that wiring. diff --git a/tests/ci-workflows/ci-privacy-gate.test.ts b/tests/ci-workflows/ci-privacy-gate.test.ts index 75a4c5205b7..c05b859a81c 100644 --- a/tests/ci-workflows/ci-privacy-gate.test.ts +++ b/tests/ci-workflows/ci-privacy-gate.test.ts @@ -148,6 +148,8 @@ describe.skipIf(cannotRunAggregate)("the aggregate ci gate, executed", () => { CHANGES_DOCS: "false", CHANGES_STRUCTURE: "false", CHANGES_PRIVACY: "false", + CHANGES_SETUP_ACTION: "false", + CHANGES_REMOTE_HELPER: "false", ...scope, // needs serializes as an object per job; the gate reads .value.result. RESULTS: JSON.stringify(Object.fromEntries(Object.entries(results).map(([job, result]) => [job, { result }]))), diff --git a/tests/ci-workflows/ci-review-lanes.test.ts b/tests/ci-workflows/ci-review-lanes.test.ts index 3696c93bcdc..8b0408d4519 100644 --- a/tests/ci-workflows/ci-review-lanes.test.ts +++ b/tests/ci-workflows/ci-review-lanes.test.ts @@ -249,6 +249,8 @@ describe("CI review lanes", () => { results["docs-site-build"] = { result: "skipped" }; results["structure-gate"] = { result: "skipped" }; results["privacy-gate"] = { result: "skipped" }; + results["setup-action"] = { result: "skipped" }; + results["remote-helper"] = { result: "skipped" }; const run = (value: typeof results, packaging = "true") => spawnSync("bash", ["-c", step!.run!], { encoding: "utf8", env: { @@ -256,6 +258,7 @@ describe("CI review lanes", () => { EVENT_NAME: "workflow_dispatch", LANE: "release-gates", CHANGES_CI: "true", CHANGES_NATIVE: "true", CHANGES_PACKAGING: packaging, CHANGES_DOCS: "false", CHANGES_STRUCTURE: "false", CHANGES_PRIVACY: "false", + CHANGES_SETUP_ACTION: "false", CHANGES_REMOTE_HELPER: "false", }, timeout: 5_000, }); diff --git a/tests/ci-workflows/ci-scope-gaps.test.ts b/tests/ci-workflows/ci-scope-gaps.test.ts new file mode 100644 index 00000000000..345b715c73a --- /dev/null +++ b/tests/ci-workflows/ci-scope-gaps.test.ts @@ -0,0 +1,108 @@ +/** + * Paths the ci filter leaves out still reach a job that exercises them. + * + * The `ci` path filter omits `.github/actions/**` and `native/**`, so a pull request that changed + * only the composite Bun setup action, or only the Rust remote-workspace helper, ran nothing that + * used what it changed while the aggregate check reported success over skips. Each now has a + * narrow filter and a small job, in the shape `structure-gate` set: pull-request scope, no full + * suite, and an arm in the aggregate gate. + */ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { repoPath } from "../helpers/repo-root"; + +type Step = { id?: string; name?: string; uses?: string; run?: string; env?: Record; with?: Record }; +type Job = { if?: string; needs?: string | string[]; outputs?: Record; steps?: Step[]; strategy?: { matrix?: { os?: string[] } } }; +const workflow = Bun.YAML.parse(readFileSync(repoPath(".github", "workflows", "ci.yml"), "utf8")) as { + jobs: Record; +}; +const jobs = workflow.jobs; +const changes = jobs.changes; +const filterStep = (changes?.steps ?? []).find(step => step.uses?.startsWith("dorny/paths-filter@")); +const filters = Bun.YAML.parse(String(filterStep?.with?.filters ?? "")) as Record; + +const matches = (patterns: readonly string[] | undefined, path: string): boolean => + (patterns ?? []).some(pattern => new Bun.Glob(pattern).match(path)); +const filtersMatching = (path: string): string[] => + Object.entries(filters).filter(([, patterns]) => matches(patterns, path)).map(([name]) => name); +/** Jobs whose condition reads one of the named changes outputs. */ +const jobsSelectedBy = (outputs: string[]): Array<[string, Job]> => + Object.entries(jobs).filter((entry): entry is [string, Job] => + entry[1] !== undefined && outputs.some(output => (entry[1]!.if ?? "").includes(`needs.changes.outputs.${output} == 'true'`))); +const scriptOf = (job: Job): string => (job.steps ?? []).map(step => step.run ?? "").join("\n"); + +const ACTION_PATH = ".github/actions/setup-project-bun/action.yml"; +const HELPER_PATH = "native/remote-workspace-helper/src/main.rs"; + +describe("an edit to the setup action alone", () => { + test("selects a job that runs the action and checks what it installed", () => { + const selected = jobsSelectedBy(filtersMatching(ACTION_PATH)) + .filter(([, job]) => (job.steps ?? []).some(step => step.uses === "./.github/actions/setup-project-bun")); + expect(selected.map(([name]) => name)).toEqual(["setup-action"]); + const [, job] = selected[0]!; + expect(scriptOf(job)).toContain("bun --version"); + expect(job.strategy?.matrix?.os).toEqual(["ubuntu-latest", "windows-latest", "macos-latest"]); + }); +}); + +describe("an edit to the remote-workspace helper alone", () => { + test("selects a job that lints and tests the crate", () => { + const selected = jobsSelectedBy(filtersMatching(HELPER_PATH)) + .filter(([, job]) => scriptOf(job).includes("native/remote-workspace-helper/Cargo.toml")); + expect(selected.map(([name]) => name)).toEqual(["remote-helper"]); + const script = scriptOf(selected[0]![1]); + expect(script).toContain("cargo clippy --locked"); + expect(script).toContain("cargo test --locked"); + expect(script).toContain("cargo fmt"); + }); +}); + +describe("the narrow checks stay narrow", () => { + test("neither path starts the full suite or the native macOS jobs", () => { + for (const path of [ACTION_PATH, HELPER_PATH]) { + expect(`${path}:ci=${matches(filters.ci, path)}`).toBe(`${path}:ci=false`); + expect(`${path}:native=${matches(filters.native, path)}`).toBe(`${path}:native=false`); + } + }); + + test("an ordinary source change selects neither job", () => { + for (const path of ["src/router.ts", "tests/lab/core-lab-boundary.test.ts", "package.json"]) { + expect(`${path}:${filtersMatching(path).filter(name => name === "setup_action" || name === "remote_helper")}`).toBe(`${path}:`); + } + }); + + test("each filter output is validated before a job reads it", () => { + const narrow = (changes?.steps ?? []).find(step => step.id === "narrow"); + expect(changes?.outputs?.setup_action).toBe("${{ steps.narrow.outputs.setup_action }}"); + expect(changes?.outputs?.remote_helper).toBe("${{ steps.narrow.outputs.remote_helper }}"); + expect(narrow?.env?.SETUP_ACTION).toBe("${{ steps.filter.outputs.setup_action }}"); + expect(narrow?.env?.REMOTE_HELPER).toBe("${{ steps.filter.outputs.remote_helper }}"); + expect(narrow?.run).toContain("exit 1"); + }); +}); + +function gateExpectation(job: string, env: Record): string { + const gate = (jobs.ci?.steps ?? []).map(step => step.run ?? "").join("\n"); + const start = gate.indexOf("scoped=requested"); + const end = gate.indexOf("bad=\"\""); + if (start < 0 || end < 0) throw new Error("cannot locate the aggregate expectation block in ci.yml"); + const result = Bun.spawnSync(["bash", "-c", `${gate.slice(start, end)}\nexpected_for "$1"\necho "GATED=$GATED_JOBS"`, "gate", job], { + env: { PATH: process.env.PATH ?? "/usr/bin:/bin", EVENT_NAME: "pull_request", CHANGES_CI: "false", ...env }, + }); + return result.stdout.toString().trim(); +} + +describe.skipIf(process.platform === "win32")("the aggregate gate", () => { + test("requires each narrow job exactly when its filter selected it", () => { + const needs = Array.isArray(jobs.ci?.needs) ? jobs.ci!.needs : []; + for (const [job, variable] of [["setup-action", "CHANGES_SETUP_ACTION"], ["remote-helper", "CHANGES_REMOTE_HELPER"]] as const) { + expect(needs).toContain(job); + expect(gateExpectation(job, { [variable]: "true" })).toStartWith("requested\n"); + expect(gateExpectation(job, { [variable]: "false" })).toStartWith("not-requested\n"); + expect(gateExpectation(job, {})).toContain(` ${job}`); + } + const step = (jobs.ci?.steps ?? []).find(candidate => (candidate.run ?? "").includes("scoped=requested")); + expect(step?.env?.CHANGES_SETUP_ACTION).toBe("${{ needs.changes.outputs.setup_action }}"); + expect(step?.env?.CHANGES_REMOTE_HELPER).toBe("${{ needs.changes.outputs.remote_helper }}"); + }); +}); diff --git a/tests/ci-workflows/ci-shard-balance.test.ts b/tests/ci-workflows/ci-shard-balance.test.ts new file mode 100644 index 00000000000..42fca7d7c41 --- /dev/null +++ b/tests/ci-workflows/ci-shard-balance.test.ts @@ -0,0 +1,183 @@ +/** + * Shard assignment by recorded duration, asserted by execution. + * + * Sorted round-robin split the suite evenly by count while file durations differ by three orders of + * magnitude, so the slowest Linux shard carried 394 s of tests against 248 s on another (run + * 35816902207). The batch runner now weighs each file by `scripts/ci/test-durations.tsv` and puts + * the heaviest file on the least-loaded shard. These cases run the real runner with a fake `bun` + * and `timeout`, the same way ci-crash-disposition.test.ts does, and exercise the refresh tool that + * keeps the table current. + */ +import { describe, expect, test } from "bun:test"; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { delimiter, join } from "node:path"; +import { mergeDurations, parseJobLog, parseTable, renderTable } from "../../scripts/ci/test-durations"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { repoPath } from "../helpers/repo-root"; +import { SPAWN_BUDGET_MS } from "../helpers/test-budget"; + +const RUNNER = repoPath("scripts", "ci", "run-bun-test-batches.sh"); +const FILES = ["alpha", "bravo", "charlie", "delta", "echo", "foxtrot"].map(name => `${name}.test.ts`); +const ISOLATED = "api-usage.test.ts"; + +// Answers the isolated-manifest query and records every other call's test files. +const FAKE_BUN = [ + "#!/bin/sh", + 'if [ "$1" = "-e" ]; then printf "%s\\n" "$FIXTURE_ISOLATED"; exit 0; fi', + "files=''", + 'for arg in "$@"; do case "$arg" in *.test.ts) files="$files $arg" ;; esac; done', + 'echo "${files# }" >> "$FIXTURE_CALLS"', + "exit 0", + "", +].join("\n"); +const FAKE_TIMEOUT = [ + "#!/bin/sh", + "while [ $# -gt 0 ]; do", + ' case "$1" in --*) shift ;; *) shift; break ;; esac', + "done", + 'exec "$@"', + "", +].join("\n"); + +function runShard(shard: string, options: { durations?: Record | null; batchSize?: string; timeoutSeconds?: string } = {}): string[][] { + const directory = mkdtempSync(join(tmpdir(), "ocx-shard-balance-")); + try { + const bin = join(directory, "bin"); + mkdirSync(bin); + mkdirSync(join(directory, "tmp")); + mkdirSync(join(directory, "tests")); + for (const file of [...FILES, ISOLATED]) writeFileSync(join(directory, "tests", file), ""); + writeFileSync(join(bin, "bun"), FAKE_BUN, { mode: 0o755 }); + writeFileSync(join(bin, "timeout"), FAKE_TIMEOUT, { mode: 0o755 }); + const durations = join(directory, "durations.tsv"); + if (options.durations) { + writeFileSync(durations, [ + "# fixture", + ...Object.entries(options.durations).map(([file, ms]) => `${ms}\ttests/${file}`), + "", + ].join("\n")); + } + const calls = join(directory, "calls.log"); + writeFileSync(calls, ""); + const result = Bun.spawnSync(["bash", RUNNER, shard], { + cwd: directory, + env: { + PATH: `${bin}${delimiter}${process.env.PATH ?? ""}`, + HOME: directory, + TMPDIR: join(directory, "tmp"), + BUN_TEST_BATCH_SIZE: options.batchSize ?? "12", + ...(options.timeoutSeconds ? { BUN_TEST_BATCH_TIMEOUT_SECONDS: options.timeoutSeconds } : {}), + BUN_TEST_DURATIONS_FILE: durations, + OPENCODEX_BUN_PATH: join(bin, "bun"), + FIXTURE_ISOLATED: ISOLATED, + FIXTURE_CALLS: calls, + }, + stdout: "pipe", + stderr: "pipe", + }); + const output = `${result.stdout.toString()}${result.stderr.toString()}`; + expect(`status:${result.exitCode}`, output).toBe("status:0"); + return readFileSync(calls, "utf8").split("\n").filter(Boolean).map(line => line.split(" ")); + } finally { + removeTreeWithRetry(directory); + } +} + +const filesOf = (batches: string[][]): string[] => batches.flat(); +const tests = (names: string[]): string[] => names.map(name => `tests/${name}`); + +describe.skipIf(process.platform === "win32")("duration-weighted shards, executed", () => { + test("the heaviest file gets a shard to itself when it outweighs the rest", () => { + const durations = Object.fromEntries(FILES.map(file => [file, file === "alpha.test.ts" ? 10_000 : 1_000])); + expect(filesOf(runShard("1/2", { durations }))).toEqual(tests(["alpha.test.ts"])); + expect(filesOf(runShard("2/2", { durations }))).toEqual(tests(FILES.filter(file => file !== "alpha.test.ts"))); + }, SPAWN_BUDGET_MS); + + test("without a table every file weighs the same and membership is the old sorted round-robin", () => { + for (const index of [1, 2]) { + expect(filesOf(runShard(`${index}/2`, { durations: null }))) + .toEqual(tests(FILES.filter((_, position) => position % 2 === index - 1))); + } + }, SPAWN_BUDGET_MS); + + test("the shards still tile the suite exactly under a skewed table", () => { + const durations = { "alpha.test.ts": 9_000, "delta.test.ts": 4_000, "foxtrot.test.ts": 3_500 }; + const seen = [1, 2, 3].flatMap(index => filesOf(runShard(`${index}/3`, { durations }))); + expect([...seen].sort()).toEqual(tests(FILES)); + }, SPAWN_BUDGET_MS); + + test("each shard runs its files in sorted order", () => { + const durations = { "foxtrot.test.ts": 9_000, "alpha.test.ts": 1 }; + const files = filesOf(runShard("2/2", { durations })); + expect(files).toEqual([...files].sort()); + }, SPAWN_BUDGET_MS); + + test("a batch closes before its predicted time passes half the process timeout", () => { + // Budget: 2 s timeout / 2 = 1,000 ms. Two 600 ms files would predict 1,200 ms, so every file + // runs in its own process even though the size cap would allow three. + const durations = Object.fromEntries(FILES.map(file => [file, 600])); + const batches = runShard("1/1", { durations, batchSize: "3", timeoutSeconds: "2" }); + expect(batches.map(batch => batch.length)).toEqual([1, 1, 1, 1, 1, 1]); + }, SPAWN_BUDGET_MS); + + test("light files still fill a batch up to the size cap", () => { + const durations = Object.fromEntries(FILES.map(file => [file, 100])); + const batches = runShard("1/1", { durations, batchSize: "3", timeoutSeconds: "2" }); + expect(batches.map(batch => batch.length)).toEqual([3, 3]); + }, SPAWN_BUDGET_MS); +}); + +describe("the duration table tool", () => { + const apiLog = [ + "2026-09-23T04:06:19.0000000Z ##[group]shard 1/4 batch 1/2 (2 files)", + "2026-09-23T04:06:19.1000000Z tests/a/one.test.ts", + "2026-09-23T04:06:19.2000000Z ##[group]tests/a/one.test.ts:", + "2026-09-23T04:06:19.9000000Z (pass) one [1.00ms]", + "2026-09-23T04:06:20.5000000Z ##[endgroup]", + "2026-09-23T04:06:20.5100000Z ##[group]tests/a/two.test.ts:", + "2026-09-23T04:06:21.0000000Z ##[endgroup]", + "2026-09-23T04:06:21.1000000Z ##[endgroup]", + "2026-09-23T04:06:22.0000000Z ##[group]shard 1/4 batch 1/2 attribution 1/2 (1 files)", + "2026-09-23T04:06:22.1000000Z ##[group]tests/a/one.test.ts:", + "2026-09-23T04:06:40.0000000Z ##[endgroup]", + ].join("\n"); + + test("charges process start to the first file and ignores attribution sweeps", () => { + const samples = parseJobLog(apiLog); + expect(samples.get("tests/a/one.test.ts")).toEqual([1_500]); + expect(samples.get("tests/a/two.test.ts")).toEqual([500]); + }); + + test("reads gh run view --log output, keeping each job's timeline separate", () => { + const prefixed = [ + "test 1/4\tTest in fresh-process batches\t2026-09-23T04:06:19.0000000Z ##[group]shard 1/4 batch 1/1 (1 files)", + "test 2/4\tTest in fresh-process batches\t2026-09-23T04:06:30.0000000Z ##[group]shard 2/4 batch 1/1 (1 files)", + "test 1/4\tTest in fresh-process batches\t2026-09-23T04:06:19.1000000Z ##[group]tests/b/one.test.ts:", + "test 2/4\tTest in fresh-process batches\t2026-09-23T04:06:30.1000000Z ##[group]tests/b/two.test.ts:", + "test 1/4\tTest in fresh-process batches\t2026-09-23T04:06:21.0000000Z ##[endgroup]", + "test 2/4\tTest in fresh-process batches\t2026-09-23T04:06:30.4000000Z ##[endgroup]", + ].join("\n"); + const samples = parseJobLog(prefixed); + expect(samples.get("tests/b/one.test.ts")).toEqual([2_000]); + expect(samples.get("tests/b/two.test.ts")).toEqual([400]); + }); + + test("merges new measurements over kept rows and drops files that no longer exist", () => { + const previous = new Map([["tests/kept.test.ts", 70], ["tests/gone.test.ts", 90], ["tests/remeasured.test.ts", 5]]); + const measured = new Map([["tests/remeasured.test.ts", [100, 300]], ["tests/zero.test.ts", [0]]]); + const merged = mergeDurations(previous, measured, path => path !== "tests/gone.test.ts"); + expect(Object.fromEntries(merged)).toEqual({ "tests/kept.test.ts": 70, "tests/remeasured.test.ts": 200, "tests/zero.test.ts": 1 }); + expect(parseTable(renderTable(merged, "fixture"))).toEqual(merged); + }); + + test("the committed table is well formed", () => { + const text = readFileSync(repoPath("scripts", "ci", "test-durations.tsv"), "utf8"); + const rows = text.split("\n").filter(line => line !== "" && !line.startsWith("#")); + expect(rows.length).toBeGreaterThan(0); + for (const row of rows) expect(row).toMatch(/^\d+\ttests\/\S+$/); + const paths = rows.map(row => row.split("\t")[1]!); + expect(paths).toEqual([...new Set(paths)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))); + expect(parseTable(text).size).toBe(rows.length); + }); +}); diff --git a/tests/ci-workflows/release-outcome-report.test.ts b/tests/ci-workflows/release-outcome-report.test.ts new file mode 100644 index 00000000000..c11747f83de --- /dev/null +++ b/tests/ci-workflows/release-outcome-report.test.ts @@ -0,0 +1,189 @@ +/** + * Release outcomes are reported one fact at a time. + * + * The registry smoke warns "Registry lookup not confirmed" and the run still continues to the + * GitHub release, which is the intended publishing behaviour. What was missing is a record that + * says which facts a green run actually established. The publish job now exposes the registry + * read-back and the dist-tag as separate outputs, and an always-run `release-outcomes` job puts the + * GitHub release, the npm version and the npm dist-tag on separate rows. It is a job of its own + * because a failed publish skips `attach-release`, which is when the rows matter most. + */ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { delimiter, join } from "node:path"; +import { repoPath } from "../helpers/repo-root"; +import { SPAWN_BUDGET_MS } from "../helpers/test-budget"; + +type Step = { name?: string; id?: string; if?: string; run?: string; env?: Record }; +type Job = { outputs?: Record; permissions?: Record; steps?: Step[] }; +const release = Bun.YAML.parse(readFileSync(repoPath(".github", "workflows", "release.yml"), "utf8")) as { + jobs: Record; +}; +const publishSteps = release.jobs.publish?.steps ?? []; +const smoke = publishSteps.find(step => step.id === "registry-smoke"); +const outcomes = release.jobs["release-outcomes"] as (Job & { needs?: string[]; if?: string }) | undefined; +const report = (outcomes?.steps ?? []).find(step => step.name === "Report release outcomes"); +const REPORT_SCRIPT = repoPath("scripts", "ci", "release-outcome-report.sh"); + +describe("release outcome wiring", () => { + test("the registry smoke records the version and the dist-tag separately", () => { + expect(smoke?.run).toContain('echo "npm_version=confirmed" >> "$GITHUB_OUTPUT"'); + expect(smoke?.run).toContain('echo "npm_version=unconfirmed" >> "$GITHUB_OUTPUT"'); + expect(smoke?.run).toContain('echo "npm_dist_tag=${dist_tag_state}" >> "$GITHUB_OUTPUT"'); + expect(smoke?.env?.NPM_DIST_TAG).toBe("${{ inputs.tag }}"); + }); + + test("the publish job exposes both registry outcomes", () => { + expect(release.jobs.publish?.outputs).toMatchObject({ + npm_version: "${{ steps.registry-smoke.outputs.npm_version }}", + npm_dist_tag: "${{ steps.registry-smoke.outputs.npm_dist_tag }}", + }); + }); + + test("a read-only job reports after publish and attach whatever their result", () => { + expect(outcomes?.needs).toEqual(["publish", "attach-release"]); + expect(outcomes?.if).toBe("${{ always() && inputs.dry-run != true }}"); + expect(outcomes?.permissions).toEqual({ contents: "read" }); + expect(report?.run?.trim()).toBe("bash scripts/ci/release-outcome-report.sh"); + expect(report?.env).toMatchObject({ + NPM_VERSION_STATE: "${{ needs.publish.outputs.npm_version }}", + NPM_DIST_TAG_STATE: "${{ needs.publish.outputs.npm_dist_tag }}", + RELEASE_VERSION: "${{ inputs.version }}", + NPM_DIST_TAG: "${{ inputs.tag }}", + PUBLISH_RESULT: "${{ needs.publish.result }}", + ATTACH_RESULT: "${{ needs.attach-release.result }}", + }); + expect(release.jobs["attach-release"]?.permissions).toEqual({ contents: "write" }); + }); +}); + +type SmokeResult = { status: number; outputs: Record; stdout: string; summary: string }; + +async function runSmoke(mode: { tags: string | null; pending?: boolean }): Promise { + const directory = mkdtempSync(join(tmpdir(), "ocx-registry-outcome-")); + const output = join(directory, "output"); + const summary = join(directory, "summary"); + for (const path of [output, summary]) writeFileSync(path, ""); + const prelude = String.raw` + node() { echo "@fixture/pkg"; } + npm() { + case "$1" in + view) [ "$FIXTURE_PENDING" = "yes" ] && return 1; echo "$RELEASE_VERSION" ;; + dist-tag) [ "$FIXTURE_TAGS" = "fail" ] && return 1; printf '%b\n' "$FIXTURE_TAGS" ;; + esac + } + timeout() { shift 2; "$@"; } + sleep() { :; } + `; + try { + const child = Bun.spawn(["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", `${prelude}\n${smoke!.run!}`], { + env: { + ...process.env, + RELEASE_VERSION: "9.8.7", + NPM_DIST_TAG: "latest", + PUBLISHED: "true", + GITHUB_OUTPUT: output, + GITHUB_STEP_SUMMARY: summary, + FIXTURE_PENDING: mode.pending ? "yes" : "no", + FIXTURE_TAGS: mode.tags ?? "fail", + }, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }); + const [status, stdout] = await Promise.all([child.exited, new Response(child.stdout).text()]); + const outputs = Object.fromEntries(readFileSync(output, "utf8").split("\n").filter(Boolean) + .map(line => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)])); + return { status, outputs, stdout, summary: readFileSync(summary, "utf8") }; + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +describe.skipIf(process.platform === "win32")("the registry smoke, executed", () => { + test("a dist-tag that names the release is confirmed", async () => { + const result = await runSmoke({ tags: "latest: 9.8.7\\npreview: 9.9.0-preview.20260923" }); + expect(result.status, result.stdout).toBe(0); + expect(result.outputs).toMatchObject({ verification: "verified", npm_version: "confirmed", npm_dist_tag: "confirmed" }); + }, SPAWN_BUDGET_MS); + + test("a dist-tag that names another version is a mismatch, not a confirmation", async () => { + const result = await runSmoke({ tags: "latest: 9.8.6" }); + expect(result.status, result.stdout).toBe(0); + expect(result.outputs.npm_dist_tag).toBe("mismatch"); + expect(result.stdout).toContain("::warning::npm dist-tag latest points at 9.8.6, not 9.8.7"); + }, SPAWN_BUDGET_MS); + + test("an unreadable dist-tag list stays unconfirmed", async () => { + const result = await runSmoke({ tags: null }); + expect(result.status, result.stdout).toBe(0); + expect(result.outputs).toMatchObject({ npm_version: "confirmed", npm_dist_tag: "unconfirmed" }); + }, SPAWN_BUDGET_MS); + + test("pending registry reads leave both npm outcomes unconfirmed and still continue", async () => { + const result = await runSmoke({ tags: "latest: 9.8.7", pending: true }); + expect(result.status, result.stdout).toBe(0); + expect(result.outputs).toMatchObject({ verification: "pending", npm_version: "unconfirmed", npm_dist_tag: "unconfirmed" }); + }, SPAWN_BUDGET_MS); +}); + +function runReport(github: "false" | "true" | "missing", versionState: string, tagState: string): { status: number | null; stdout: string; summary: string } { + const directory = mkdtempSync(join(tmpdir(), "ocx-release-report-")); + try { + const summary = join(directory, "summary"); + writeFileSync(summary, ""); + writeFileSync(join(directory, "gh"), [ + "#!/bin/sh", + 'case "$FIXTURE_GH" in false|true) echo "$FIXTURE_GH" ;; *) exit 1 ;; esac', + "", + ].join("\n"), { mode: 0o755 }); + const result = Bun.spawnSync(["bash", REPORT_SCRIPT], { + env: { + PATH: `${directory}${delimiter}${process.env.PATH ?? ""}`, + RELEASE_VERSION: "9.8.7", + NPM_DIST_TAG: "latest", + NPM_VERSION_STATE: versionState, + NPM_DIST_TAG_STATE: tagState, + GITHUB_STEP_SUMMARY: summary, + FIXTURE_GH: github, + PUBLISH_RESULT: "success", + ATTACH_RESULT: "success", + }, + stdout: "pipe", + stderr: "pipe", + }); + return { status: result.exitCode, stdout: result.stdout.toString(), summary: readFileSync(summary, "utf8") }; + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +describe.skipIf(process.platform === "win32")("scripts/ci/release-outcome-report.sh, executed", () => { + test("a fully confirmed release reads as three confirmed rows and no warning", () => { + const result = runReport("false", "confirmed", "confirmed"); + expect(result.status).toBe(0); + expect(result.summary).toContain("| GitHub release `v9.8.7` | published |"); + expect(result.summary).toContain("| npm version `9.8.7` read back from the registry | confirmed |"); + expect(result.summary).toContain("| npm dist-tag `latest` points at `9.8.7` | confirmed |"); + expect(result.stdout).not.toContain("::warning::"); + }, SPAWN_BUDGET_MS); + + test("each unconfirmed fact gets its own row and warning", () => { + const result = runReport("true", "confirmed", "mismatch"); + expect(result.status).toBe(0); + expect(result.summary).toContain("| GitHub release `v9.8.7` | draft (not public) |"); + expect(result.summary).toContain("| npm dist-tag `latest` points at `9.8.7` | points at another version |"); + expect(result.stdout).toContain("::warning::GitHub release v9.8.7 is draft (not public)"); + expect(result.stdout).toContain("::warning::npm dist-tag latest points at another version for 9.8.7"); + }, SPAWN_BUDGET_MS); + + test("a run that never reached the registry reports nothing as confirmed and does not fail", () => { + const result = runReport("missing", "", ""); + expect(result.status).toBe(0); + expect(result.summary).toContain("| GitHub release `v9.8.7` | not public (draft, missing or unreadable) |"); + expect(result.summary).toContain("Publish job: success. Attach job: success."); + expect(result.summary).toContain("| npm version `9.8.7` read back from the registry | not confirmed |"); + expect(result.stdout).toContain("::warning::npm version 9.8.7 was not read back from the registry"); + }, SPAWN_BUDGET_MS); +}); diff --git a/tests/ci-workflows/release-preflight.test.ts b/tests/ci-workflows/release-preflight.test.ts new file mode 100644 index 00000000000..e53989bb6f8 --- /dev/null +++ b/tests/ci-workflows/release-preflight.test.ts @@ -0,0 +1,243 @@ +/** + * The release preflight, asserted by shape and by execution. + * + * Run 35783865160 packaged 2.62.0 for nineteen minutes and then failed its ordering gate in + * `publish` on `v2.63.0-preview.20260923`, a tag that already existed when the run's first job + * started: the workflow-level `release` concurrency group had held the stable run until the + * preview run finished. The runs were serialised; the check sat in the wrong place. These cases pin + * the `preflight` job in front of every packaging job, keep the publish-job gate as the final + * check, pin the shared concurrency group that makes the early answer trustworthy, and execute + * `scripts/ci/release-preflight.sh` against a real tag set with fake `gh` and `npm`. + */ +import { describe, expect, test } from "bun:test"; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { delimiter, join } from "node:path"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { repoPath } from "../helpers/repo-root"; +import { SPAWN_BUDGET_MS } from "../helpers/test-budget"; + +type Step = { name?: string; uses?: string; run?: string; env?: Record; with?: Record }; +type Job = { needs?: string | string[]; permissions?: Record; steps?: Step[]; "runs-on"?: string }; +type Workflow = { concurrency?: { group?: string; "cancel-in-progress"?: boolean }; jobs?: Record }; + +const release = Bun.YAML.parse(readFileSync(repoPath(".github", "workflows", "release.yml"), "utf8")) as Workflow; +const needsOf = (job: Job | undefined): string[] => + job?.needs === undefined ? [] : typeof job.needs === "string" ? [job.needs] : job.needs; +const PREFLIGHT = repoPath("scripts", "ci", "release-preflight.sh"); + +describe("the release preflight job", () => { + const preflight = release.jobs?.preflight; + + test("runs after dispatch validation with read-only permissions", () => { + expect(needsOf(preflight)).toEqual(["validate-dispatch"]); + expect(preflight?.permissions).toEqual({ contents: "read" }); + expect(preflight?.["runs-on"]).toBe("ubuntu-latest"); + }); + + test("gates every packaging job", () => { + for (const name of ["package-standalone", "package-desktop"]) { + expect(`${name}:${needsOf(release.jobs?.[name]).includes("preflight")}`).toBe(`${name}:true`); + } + }); + + test("runs the preflight script with dispatch inputs passed through env", () => { + const step = (preflight?.steps ?? []).find(candidate => candidate.run?.includes("scripts/ci/release-preflight.sh")); + expect(step?.run?.trim()).toBe("bash scripts/ci/release-preflight.sh"); + expect(step?.env).toMatchObject({ + RELEASE_VERSION: "${{ inputs.version }}", + NPM_DIST_TAG: "${{ inputs.tag }}", + DRY_RUN: "${{ inputs.dry-run }}", + RESUME: "${{ inputs.resume-after-npm-publish }}", + }); + // The script reads origin/dev and the tag set; both must be present in the checkout. + const checkout = (preflight?.steps ?? []).find(candidate => candidate.uses?.startsWith("actions/checkout@")); + expect(checkout?.with?.["fetch-tags"]).toBe(true); + expect(checkout?.with?.["persist-credentials"]).toBe(false); + expect((preflight?.steps ?? []).some(candidate => candidate.run?.includes("+refs/heads/dev:refs/remotes/origin/dev"))).toBe(true); + }); + + test("leaves the publish-time ordering gate in place as the final check", () => { + const steps = release.jobs?.publish?.steps ?? []; + const ordering = steps.find(step => step.name === "Refuse a release the current tag set already outranks"); + const publish = steps.find(step => step.name === "Publish (or dry-run)"); + expect(ordering?.run).toContain("assert-releasable"); + expect(steps.indexOf(ordering!)).toBeLessThan(steps.indexOf(publish!)); + }); + + test("keeps one release slot shared by every ref", () => { + // A constant group is what serialised the stable run behind the preview one. A per-ref group + // would let a main and a preview release run at once, and then no early check could be final. + expect(release.concurrency?.group).toBe("release"); + expect(release.concurrency?.["cancel-in-progress"]).toBe(false); + }); +}); + +type Scenario = { + version?: string; + ref?: string; + distTag?: string; + tags?: string[]; + devVersion?: string | null; + npm?: "absent" | "present" | "unreadable"; + githubRelease?: boolean; + dryRun?: boolean; + resume?: boolean; +}; + +// The script checks the real version sources (package.json and the desktop manifests) through its own +// repository root, so the scenarios release the checkout's own version; only the tag set, dev, gh +// and npm are fixtures. +const OWN_VERSION = (JSON.parse(readFileSync(repoPath("package.json"), "utf8")) as { version: string }).version; +const [MAJOR, MINOR] = OWN_VERSION.split(/[.-]/).map(Number) as [number, number]; +const OWN_IS_PREVIEW = OWN_VERSION.includes("-preview."); +const OWN_REF = OWN_IS_PREVIEW ? "refs/heads/preview" : "refs/heads/main"; +const OWN_TAG = OWN_IS_PREVIEW ? "preview" : "latest"; +const NEXT_CORE = `${MAJOR}.${MINOR + 1}.0`; + +const FAKE_GH = [ + "#!/bin/sh", + '[ "$FIXTURE_GH_RELEASE" = "yes" ] && exit 0', + 'echo "release not found" >&2', + "exit 1", + "", +].join("\n"); +const FAKE_NPM = [ + "#!/bin/sh", + 'case "$FIXTURE_NPM" in', + ' present) echo "$RELEASE_VERSION" ;;', + ' unreadable) echo "npm error code ETIMEDOUT" >&2; exit 1 ;;', + ' *) echo "npm error code E404" >&2; exit 1 ;;', + "esac", + "", +].join("\n"); + +function run(cwd: string, env: Record, ...command: string[]): string { + const result = Bun.spawnSync(command, { cwd, env, stdout: "pipe", stderr: "pipe" }); + if (result.exitCode !== 0) throw new Error(`${command.join(" ")} failed: ${result.stderr.toString()}`); + return result.stdout.toString().trim(); +} + +function preflight(scenario: Scenario): { status: number | null; output: string; summary: string } { + const directory = mkdtempSync(join(tmpdir(), "ocx-release-preflight-")); + try { + const repo = join(directory, "repo"); + const bin = join(directory, "bin"); + mkdirSync(repo); + mkdirSync(bin); + writeFileSync(join(bin, "gh"), FAKE_GH, { mode: 0o755 }); + writeFileSync(join(bin, "npm"), FAKE_NPM, { mode: 0o755 }); + const gitEnv = { + PATH: process.env.PATH ?? "/usr/bin:/bin", + HOME: directory, + GIT_CONFIG_NOSYSTEM: "1", + GIT_AUTHOR_NAME: "fixture", + GIT_AUTHOR_EMAIL: "fixture@example.test", + GIT_COMMITTER_NAME: "fixture", + GIT_COMMITTER_EMAIL: "fixture@example.test", + }; + run(repo, gitEnv, "git", "init", "-q", "-b", "release"); + writeFileSync(join(repo, "package.json"), JSON.stringify({ version: scenario.version ?? OWN_VERSION })); + run(repo, gitEnv, "git", "add", "package.json"); + run(repo, gitEnv, "git", "commit", "-q", "-m", "release"); + const head = run(repo, gitEnv, "git", "rev-parse", "HEAD"); + for (const tag of scenario.tags ?? []) run(repo, gitEnv, "git", "tag", tag); + if (scenario.devVersion !== null) { + run(repo, gitEnv, "git", "checkout", "-q", "-b", "dev"); + writeFileSync(join(repo, "package.json"), JSON.stringify({ version: scenario.devVersion ?? NEXT_CORE })); + run(repo, gitEnv, "git", "commit", "-q", "--allow-empty", "-am", "dev pre-move"); + run(repo, gitEnv, "git", "update-ref", "refs/remotes/origin/dev", "HEAD"); + run(repo, gitEnv, "git", "checkout", "-q", "release"); + } + const summary = join(directory, "summary.md"); + writeFileSync(summary, ""); + const result = Bun.spawnSync(["bash", PREFLIGHT], { + cwd: repo, + env: { + ...gitEnv, + PATH: `${bin}${delimiter}${gitEnv.PATH}`, + RELEASE_VERSION: scenario.version ?? OWN_VERSION, + NPM_DIST_TAG: scenario.distTag ?? OWN_TAG, + GITHUB_REF: scenario.ref ?? OWN_REF, + GITHUB_SHA: head, + DRY_RUN: String(scenario.dryRun ?? false), + RESUME: String(scenario.resume ?? false), + GITHUB_STEP_SUMMARY: summary, + FIXTURE_GH_RELEASE: scenario.githubRelease ? "yes" : "no", + FIXTURE_NPM: scenario.npm ?? "absent", + }, + stdout: "pipe", + stderr: "pipe", + }); + return { + status: result.exitCode, + output: `${result.stdout.toString()}${result.stderr.toString()}`, + summary: readFileSync(summary, "utf8"), + }; + } finally { + removeTreeWithRetry(directory); + } +} + +describe.skipIf(process.platform === "win32")("scripts/ci/release-preflight.sh, executed", () => { + test("replays run 35783865160: a higher-core tag refuses the release before packaging", () => { + const blocker = OWN_IS_PREVIEW ? `v${NEXT_CORE}` : `v${NEXT_CORE}-preview.20260923`; + const result = preflight({ tags: ["v0.0.1", blocker] }); + expect(result.status, result.output).toBe(1); + expect(result.output).toContain(`${OWN_VERSION} does not outrank the current tag set`); + expect(result.output).toContain("found 1 blocking problem(s)"); + expect(result.summary).toContain(`Release preflight refused ${OWN_VERSION}`); + }, SPAWN_BUDGET_MS); + + test("passes a release every check can already approve", () => { + const result = preflight({ tags: ["v0.0.1"] }); + expect(result.status, result.output).toBe(0); + expect(result.output).toContain("Release preflight passed"); + expect(result.summary).toBe(""); + }, SPAWN_BUDGET_MS); + + test("refuses a version npm already has unless the run is a dry run", () => { + const real = preflight({ tags: ["v0.0.1"], npm: "present" }); + expect(real.status, real.output).toBe(1); + expect(real.output).toContain("already exists on npm"); + const dry = preflight({ tags: ["v0.0.1"], npm: "present", dryRun: true }); + expect(dry.status, dry.output).toBe(0); + expect(dry.output).toContain("::notice::"); + }, SPAWN_BUDGET_MS); + + test("refuses an existing GitHub release outside the resume path", () => { + const result = preflight({ tags: ["v0.0.1"], githubRelease: true }); + expect(result.status, result.output).toBe(1); + expect(result.output).toContain("GitHub Release"); + }, SPAWN_BUDGET_MS); + + test("refuses a resume with nothing on npm to resume from", () => { + const result = preflight({ tags: ["v0.0.1"], resume: true }); + expect(result.status, result.output).toBe(1); + expect(result.output).toContain("is not on npm"); + }, SPAWN_BUDGET_MS); + + test("warns rather than blocks when npm cannot be read", () => { + const result = preflight({ tags: ["v0.0.1"], npm: "unreadable" }); + expect(result.status, result.output).toBe(0); + expect(result.output).toContain("::warning::Could not read npm"); + }, SPAWN_BUDGET_MS); + + test("requires the dev pre-move", () => { + const behind = preflight({ tags: ["v0.0.1"], devVersion: OWN_VERSION }); + expect(behind.status, behind.output).toBe(1); + expect(behind.output).toContain("merge the dev pre-move first"); + const missing = preflight({ tags: ["v0.0.1"], devVersion: null }); + expect(missing.status, missing.output).toBe(1); + expect(missing.output).toContain("refs/remotes/origin/dev"); + }, SPAWN_BUDGET_MS); + + test("reports every problem in one run", () => { + const wrongTag = OWN_TAG === "latest" ? "preview" : "latest"; + const result = preflight({ tags: ["v0.0.1"], distTag: wrongTag, npm: "present" }); + expect(result.status, result.output).toBe(1); + expect(result.output).toContain(`npm dist-tag '${OWN_TAG}'`); + expect(result.output).toContain("already exists on npm"); + expect(result.output).toContain("found 2 blocking problem(s)"); + }, SPAWN_BUDGET_MS); +}); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index a62ab80eb1b..729a15b53e6 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -168,6 +168,8 @@ "ci-privacy-gate.test.ts": "ci-workflows", "ci-review-lanes.test.ts": "ci-workflows", "ci-scope-reduction.test.ts": "ci-workflows", + "ci-scope-gaps.test.ts": "ci-workflows", + "ci-shard-balance.test.ts": "ci-workflows", "ci-structure-gate.test.ts": "ci-workflows", "ci-workflows.test.ts": "ci-workflows", "citation-markers.test.ts": "responses", @@ -1121,7 +1123,9 @@ "release-desktop-scripts.test.ts": "ci-workflows", "release-helper.test.ts": "ci-workflows", "release-notes.test.ts": "ci-workflows", + "release-outcome-report.test.ts": "ci-workflows", "release-pipeline-contract.test.ts": "ci-workflows", + "release-preflight.test.ts": "ci-workflows", "release-resume-identity.test.ts": "ci-workflows", "release-version-line.test.ts": "ci-workflows", "release-version-sources.test.ts": "ci-workflows",