From 0bf01dfa595c6d2bf7012c049a253895d89de8b8 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 14 Oct 2026 04:25:57 +0800 Subject: [PATCH] perf(ci): share trusted caches and fail fast on PR test failures (#3007) * fix(ci): restore inherited validation failures * perf(ci): share trusted caches and fail fast on PR test failures * fix(ci): bound static cache and hash vitest config for knip - Bound the rolling .turbo/cache snapshot with 7d/500MB eviction so each trusted main save cannot accumulate history toward the GitHub limit. - Hash vitest.config.ts in knip inputs so removing the CiTestSequencer import invalidates the cached passing check. - Cover both in cache-policy and task-hash regressions. --- .github/actions/npm-cache/action.yml | 42 +++ .github/workflows/ci.yml | 64 +++- .github/workflows/pages.yml | 4 +- .github/workflows/real-binary-layout.yml | 4 +- .github/workflows/real-browser.yml | 4 +- .github/workflows/real-evm-interface.yml | 4 +- .github/workflows/real-go-binary.yml | 4 +- .github/workflows/real-hopper-linux.yml | 8 +- .github/workflows/real-hopper.yml | 4 +- .../workflows/real-javascript-recovery.yml | 4 +- .github/workflows/real-packet-capture.yml | 4 +- .github/workflows/real-recorded-crash.yml | 4 +- .github/workflows/real-signal-recording.yml | 4 +- .../workflows/real-web-network-captures.yml | 4 +- .github/workflows/real-web-runtime.yml | 4 +- .github/workflows/real-web-source-map.yml | 4 +- .github/workflows/sqlite-runtime.yml | 4 +- docs/readme/README_tr.md | 1 + docs/testing.md | 33 +- src/dotnet/ManagedCilDecoding.test.ts | 2 + .../filesystem/artifactComparison.test.ts | 2 + .../boundary/filesystem/ciCachePolicy.test.ts | 252 +++++++++++++++ .../filesystem/ciRequiredLanes.test.ts | 41 +++ .../filesystem/staticTaskCache.test.ts | 290 ++++++++++++++++++ ...4-uses-complete-inventories-inline.test.ts | 4 +- .../packagePlatformDiagnostics.test.ts | 5 +- turbo.json | 21 +- 27 files changed, 767 insertions(+), 54 deletions(-) create mode 100644 .github/actions/npm-cache/action.yml create mode 100644 tests/boundary/filesystem/ciCachePolicy.test.ts create mode 100644 tests/boundary/filesystem/staticTaskCache.test.ts diff --git a/.github/actions/npm-cache/action.yml b/.github/actions/npm-cache/action.yml new file mode 100644 index 0000000000..1af22da2b9 --- /dev/null +++ b/.github/actions/npm-cache/action.yml @@ -0,0 +1,42 @@ +name: Cache npm package downloads +description: Restore npm's download cache and save it only from trusted default-branch events. +outputs: + can-save: + description: Whether this run may update the shared npm download cache. + value: ${{ steps.context.outputs.can-save }} + path: + description: The npm download-cache directory. + value: ${{ steps.context.outputs.path }} +runs: + using: composite + steps: + - id: context + shell: bash + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + LOCKFILES_HASH: ${{ hashFiles('package-lock.json', 'npm-shrinkwrap.json', '.npmrc') }} + run: | + cache_path="$(npm config get cache)" + runtime_arch="$(node -p 'process.arch')" + can_save=false + if [[ -n "${DEFAULT_BRANCH}" && "${GITHUB_REF}" == "refs/heads/${DEFAULT_BRANCH}" && ( "${GITHUB_EVENT_NAME}" == "push" || "${GITHUB_EVENT_NAME}" == "schedule" || "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ) ]]; then + can_save=true + fi + cache_key="npm-download-${RUNNER_OS}-${runtime_arch}-${LOCKFILES_HASH}" + { + printf 'can-save=%s\n' "${can_save}" + printf 'path=%s\n' "${cache_path}" + printf 'key=%s\n' "${cache_key}" + } >> "${GITHUB_OUTPUT}" + - name: Restore and save npm package downloads + if: steps.context.outputs.can-save == 'true' + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ${{ steps.context.outputs.path }} + key: ${{ steps.context.outputs.key }} + - name: Restore npm package downloads without saving + if: steps.context.outputs.can-save != 'true' + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ${{ steps.context.outputs.path }} + key: ${{ steps.context.outputs.key }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b47fcf895..8b03866263 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,8 +122,19 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci + - name: Restore static task cache + if: needs.changes.outputs.code == 'true' + id: static-cache + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: .turbo/cache/ + key: rea-static-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc', '.npmrc', 'package-lock.json') }}-${{ github.sha }} + # Turbo validates each task's inputs after restoring this snapshot. + restore-keys: rea-static-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc', '.npmrc', 'package-lock.json') }}- - name: Validate workflow syntax and expressions if: needs.changes.outputs.workflows == 'true' run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 -shellcheck= -pyflakes= @@ -136,6 +147,15 @@ jobs: run: npm run test:python - if: needs.changes.outputs.code != 'true' run: npm run format:check + - name: Save validated static task cache + if: >- + success() && needs.changes.outputs.code == 'true' && + steps.npm-cache.outputs.can-save == 'true' && + steps.static-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: .turbo/cache/ + key: ${{ steps.static-cache.outputs.cache-primary-key }} - name: Upload complete static diagnostics if: always() && needs.changes.outputs.code == 'true' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 @@ -153,7 +173,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 strategy: - fail-fast: false + # Failed PRs release the remaining matrix slots; qualification retains + # every shard's diagnostics and coverage. Passing PRs run the full suite. + fail-fast: ${{ github.event_name == 'pull_request' && needs.changes.outputs.tier == 'fast' }} matrix: shard: [1/6, 2/6, 3/6, 4/6, 5/6, 6/6] steps: @@ -161,7 +183,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: @@ -212,7 +236,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - name: Reuse portable build outputs across runs uses: ./.github/actions/portable-build-cache @@ -286,7 +312,10 @@ jobs: if: needs.changes.outputs.code == 'true' && success() with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + if: needs.changes.outputs.code == 'true' && success() + id: npm-cache + uses: ./.github/actions/npm-cache - if: needs.changes.outputs.code == 'true' && success() run: npm ci - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -450,7 +479,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - name: Verify native runtime identity env: EXPECTED_CI_PLATFORM: ${{ matrix.platform }} @@ -472,11 +503,6 @@ jobs: - run: npm run verify:windows-native:artifact - name: Verify NativeAOT diagnostic applicability on this host run: npm run test:focused -- tests/evaluation/package/nativeAotDiagnostics.test.ts - - name: Resolve npm cache before verifier HOME isolation - id: npm-cache - run: | - cache_path="$(npm config get cache)" - echo "path=${cache_path}" >> "${GITHUB_OUTPUT}" - run: npm run verify:package env: npm_config_cache: ${{ steps.npm-cache.outputs.path }} @@ -506,7 +532,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: @@ -554,7 +582,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - name: Run the public CLI/MCP reconstruction journey env: @@ -583,7 +613,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: @@ -703,7 +735,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - run: npm run docs:check - name: Generation leaves authored files unchanged diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 6b8a05b46b..baa6ba785e 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -23,6 +23,8 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - run: npm run docs:build diff --git a/.github/workflows/real-binary-layout.yml b/.github/workflows/real-binary-layout.yml index 0218543f05..06fcc290a6 100644 --- a/.github/workflows/real-binary-layout.yml +++ b/.github/workflows/real-binary-layout.yml @@ -32,7 +32,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - uses: ./.github/actions/portable-build-cache with: diff --git a/.github/workflows/real-browser.yml b/.github/workflows/real-browser.yml index 3c3fe955ab..aa7e6ebb22 100644 --- a/.github/workflows/real-browser.yml +++ b/.github/workflows/real-browser.yml @@ -25,7 +25,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - uses: ./.github/actions/portable-build-cache with: diff --git a/.github/workflows/real-evm-interface.yml b/.github/workflows/real-evm-interface.yml index 8588a269f7..3a47b8f907 100644 --- a/.github/workflows/real-evm-interface.yml +++ b/.github/workflows/real-evm-interface.yml @@ -26,7 +26,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - uses: ./.github/actions/portable-build-cache with: diff --git a/.github/workflows/real-go-binary.yml b/.github/workflows/real-go-binary.yml index c7d74b0014..d99622067d 100644 --- a/.github/workflows/real-go-binary.yml +++ b/.github/workflows/real-go-binary.yml @@ -48,7 +48,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version: "1.26.1" diff --git a/.github/workflows/real-hopper-linux.yml b/.github/workflows/real-hopper-linux.yml index d5e5f8c337..417af2ca5a 100644 --- a/.github/workflows/real-hopper-linux.yml +++ b/.github/workflows/real-hopper-linux.yml @@ -19,7 +19,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - name: Pack CLI for setup verification shell: bash @@ -40,7 +42,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - name: Install packed CLI in isolation shell: bash diff --git a/.github/workflows/real-hopper.yml b/.github/workflows/real-hopper.yml index b3ceeb7d9c..da1c60c9f9 100644 --- a/.github/workflows/real-hopper.yml +++ b/.github/workflows/real-hopper.yml @@ -19,7 +19,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - name: Install packed CLI in isolation shell: bash diff --git a/.github/workflows/real-javascript-recovery.yml b/.github/workflows/real-javascript-recovery.yml index a5043b62e3..786d4a2d4a 100644 --- a/.github/workflows/real-javascript-recovery.yml +++ b/.github/workflows/real-javascript-recovery.yml @@ -27,7 +27,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - uses: ./.github/actions/portable-build-cache with: diff --git a/.github/workflows/real-packet-capture.yml b/.github/workflows/real-packet-capture.yml index 8daebfd8aa..12cb3d23db 100644 --- a/.github/workflows/real-packet-capture.yml +++ b/.github/workflows/real-packet-capture.yml @@ -47,7 +47,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y tshark util-linux python3 - run: npm ci - run: npm run build:cached diff --git a/.github/workflows/real-recorded-crash.yml b/.github/workflows/real-recorded-crash.yml index 0463df27d6..58cf90aed3 100644 --- a/.github/workflows/real-recorded-crash.yml +++ b/.github/workflows/real-recorded-crash.yml @@ -29,7 +29,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - uses: ./.github/actions/portable-build-cache with: diff --git a/.github/workflows/real-signal-recording.yml b/.github/workflows/real-signal-recording.yml index 5c1380e2df..1ddc2d5e0b 100644 --- a/.github/workflows/real-signal-recording.yml +++ b/.github/workflows/real-signal-recording.yml @@ -40,7 +40,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: sudo apt-get update && sudo apt-get install -y sigrok-cli util-linux python3 - run: npm ci - run: npm run build:cached diff --git a/.github/workflows/real-web-network-captures.yml b/.github/workflows/real-web-network-captures.yml index 3ee59d2c87..7901114f0b 100644 --- a/.github/workflows/real-web-network-captures.yml +++ b/.github/workflows/real-web-network-captures.yml @@ -26,7 +26,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - name: Use pinned package manager run: npm install --global npm@11.16.0 - run: npm ci diff --git a/.github/workflows/real-web-runtime.yml b/.github/workflows/real-web-runtime.yml index f5b9f201a1..010032c87a 100644 --- a/.github/workflows/real-web-runtime.yml +++ b/.github/workflows/real-web-runtime.yml @@ -21,7 +21,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - uses: ./.github/actions/portable-build-cache with: diff --git a/.github/workflows/real-web-source-map.yml b/.github/workflows/real-web-source-map.yml index 2a1e65dc0a..eb1ecde342 100644 --- a/.github/workflows/real-web-source-map.yml +++ b/.github/workflows/real-web-source-map.yml @@ -21,7 +21,9 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: .nvmrc - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - uses: ./.github/actions/portable-build-cache with: diff --git a/.github/workflows/sqlite-runtime.yml b/.github/workflows/sqlite-runtime.yml index fc379d1dc2..1ea6964b73 100644 --- a/.github/workflows/sqlite-runtime.yml +++ b/.github/workflows/sqlite-runtime.yml @@ -27,7 +27,9 @@ jobs: with: node-version: ${{ matrix.node }} check-latest: true - cache: npm + - name: Cache npm package downloads + id: npm-cache + uses: ./.github/actions/npm-cache - run: npm ci - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: diff --git a/docs/readme/README_tr.md b/docs/readme/README_tr.md index b90a0c3d3b..0a695396c1 100644 --- a/docs/readme/README_tr.md +++ b/docs/readme/README_tr.md @@ -141,6 +141,7 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç | Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](https://rea.tools/tr/guides/native/) | | Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](../binary-diagnostics.md) | | Go ikili dosyaları | Gömülü compiler sürümü, modüller, replacement'lar ve build ayarları | Yerleşik statik ELF/PE/thin Mach-O incelemesi; Go kurulumu gerekmez | +| Rust ikili dosyaları | Gömülü cargo-auditable crate adları, sürümleri, kaynakları ve bağımlılık ilişkileri | Yerleşik statik ELF/PE/thin Mach-O incelemesi; [Rust ikili dosyaları](../rust-binaries.md) | | WASM artefaktı | Doğrulama, bölümler, tam import/export WAT biçimleri ve decode edilmiş WAT | Çağıranın sağladığı WABT 1.0.42; `inspect_wasm_artifact` | | EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](../evm-bytecode.md) | | SQLite veritabanları | Tablolar, sütunlar, index'ler, view'lar, trigger'lar ve seçilen sıradan tablo satırları | Yerel database/WAL snapshot'ı; native SQLite sınırlarıyla Node.js 24.x >=24.15 veya 26+ | diff --git a/docs/testing.md b/docs/testing.md index 213c48e44c..08fe54c386 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -887,8 +887,13 @@ successful ancestor push run of this CI workflow, rather than the preceding push: changes from failed or cancelled intermediate runs remain in scope. Missing, untrusted, or non-ancestor history selects every lane. The lookup is bounded to the last 100 successful push runs; lookup failures broaden selection. -Every code change still runs all six portable shards, without coverage -instrumentation in this fast tier. This deliberately avoids relying on an +Every code change still selects all six portable shards, without coverage +instrumentation in this fast tier. Ordinary PR matrices cancel remaining +shards after the first failure; passing PRs execute the complete suite. Main, +`ci:full`, scheduled, manual, and release qualification retain every shard's +results after a failure. Available failed-shard reports remain diagnostic only: +failed, cancelled, skipped, or missing selected shards cannot pass the gate or +publish successful timing history. This deliberately avoids relying on an import graph for generated files, filesystem inputs, subprocesses, or bridges. Scheduled qualification is admitted hourly at minute 23. A forced full run at @@ -916,10 +921,11 @@ classification job records its selection and broad-fallback reasons in the run summary. Representative Git-diff cases live in `tests/boundary/filesystem/ciChangeScope.test.ts`. -`CI required` always runs and checks classification and every selected lane, +`CI required` checks classification and every selected lane, rejecting failed, cancelled, or unexpectedly skipped work. It rejects missing or unknown tiers and requires every canonical scope to be explicitly `true` or `false`; incomplete classification cannot silently omit verification lanes. +The aggregate remains cancellable so superseded runs release their slots. Branch protection should require this aggregate, `Static checks`, `Portable tests`, and `Conventional PR title`. Title validation runs separately on PR opening, @@ -968,7 +974,7 @@ filename cases. POSIX package verification resolves the runner's configured npm cache before creating disposable client homes and passes that location to its child installs. -This reuses the download cache restored by `setup-node` while keeping client +This reuses the download cache restored by the shared npm-cache action while keeping client configuration isolated. Installs retain their existing registry and integrity checks. @@ -998,7 +1004,24 @@ thresholds remain enforced in full qualification. Missing or unusable history falls back to Vitest's default assignment. Only timing data is reused; live provider results are never cached. Turbo outputs are restored by task hash, with trusted default-branch runs as cache writers and PRs -as restore-only consumers. npm caches hold downloads, not `node_modules`. +as restore-only consumers. The static job also restores its Turbo snapshot; +each task revalidates its own input hash before reusing outputs or diagnostics. +The Node pin and npm install settings are global hash inputs. Knip includes its ignore rules and VitePress +entry, so changes to those inputs cannot reuse an older successful check. + +The shared npm-cache action caches downloads, not `node_modules`, and every +consumer still runs `npm ci`. Keys cover the runner OS, actual Node architecture, +root lockfiles, and `.npmrc`. Only push, schedule, and manual events on the +repository's default branch may save shared npm caches; PRs restore only. +Static snapshots use the same write authority and are saved only after the +static job's validation succeeds. Main warms the new cache namespace after +merge; a cold cache or changed lockfile falls back to the normal install. +This prevents each PR from saving another branch-scoped copy of the same +download cache. It does not remove runner assignment waits. + +Portable owner-based test selection remains observational. Its reports compare +proposed omissions with the full run; incomplete or skipped observations do not +establish that fewer tests would preserve regression detection. The eight automatic browser, binary-layout, EVM, JavaScript-recovery, recorded-crash, and web provider lanes download `rea-build-cache` from their own diff --git a/src/dotnet/ManagedCilDecoding.test.ts b/src/dotnet/ManagedCilDecoding.test.ts index 852762b2c1..b24497db9a 100644 --- a/src/dotnet/ManagedCilDecoding.test.ts +++ b/src/dotnet/ManagedCilDecoding.test.ts @@ -331,7 +331,9 @@ describe("managed operand extent diagnostics", () => { }); }, ); +}); +describe("managed complete operand decoding", () => { // The extent check must also reject nothing that fits, so every multi-byte // width is exercised at exactly its own size with a known value. Each body // ends at its operand, so a width declared too large turns a valid operand diff --git a/tests/boundary/filesystem/artifactComparison.test.ts b/tests/boundary/filesystem/artifactComparison.test.ts index 98f136e946..3dc2edb66f 100644 --- a/tests/boundary/filesystem/artifactComparison.test.ts +++ b/tests/boundary/filesystem/artifactComparison.test.ts @@ -443,7 +443,9 @@ describe("artifact comparison completeness", () => { changes: [], }); }, 15_000); +}); +describe("artifact comparison fragment integrity", () => { it("assembles real inventory fragments and retains every citation", async () => { const parent = await createTestTempDirectory("rea-artifact-fragments-"); const leftPath = join(parent, "left.app"); diff --git a/tests/boundary/filesystem/ciCachePolicy.test.ts b/tests/boundary/filesystem/ciCachePolicy.test.ts new file mode 100644 index 0000000000..47e4f86dc4 --- /dev/null +++ b/tests/boundary/filesystem/ciCachePolicy.test.ts @@ -0,0 +1,252 @@ +import { execFileSync } from "node:child_process"; +import { chmod, readFile, readdir, writeFile } from "node:fs/promises"; +import { delimiter, join } from "node:path"; + +import { describe, expect, it } from "vitest"; +import { parse } from "yaml"; +import { z } from "zod"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const repository = process.cwd(); +const actionPath = join(repository, ".github/actions/npm-cache/action.yml"); +const readAction = async () => + z + .object({ + outputs: z.record(z.string(), z.object({ value: z.string() })), + runs: z.object({ + steps: z.array( + z.object({ + id: z.string().optional(), + if: z.string().optional(), + uses: z.string().optional(), + with: z.record(z.string(), z.string()).optional(), + run: z.string().optional(), + env: z.record(z.string(), z.string()).optional(), + }), + ), + }), + }) + .parse(parse(await readFile(actionPath, "utf8"))); + +const readWorkflows = async () => { + const directory = join(repository, ".github/workflows"); + const files = (await readdir(directory)).filter((file) => + /\.ya?ml$/u.test(file), + ); + return Promise.all( + files.map(async (file) => ({ + file, + workflow: z + .object({ + jobs: z + .record( + z.string(), + z.object({ + steps: z.array(z.record(z.string(), z.unknown())).optional(), + }), + ) + .optional(), + }) + .parse(parse(await readFile(join(directory, file), "utf8"))), + })), + ); +}; + +describe("hosted npm download-cache policy", () => { + it("only allows shared writes for trusted original default-branch events", async () => { + const action = await readAction(); + const policy = action.runs.steps.find((step) => step.id === "context"); + const command = z.string().parse(policy?.run); + + const outputDirectory = await createTestTempDirectory( + "rea-ci-cache-policy-", + ); + const npmStub = join(outputDirectory, "npm"); + await writeFile( + npmStub, + String.raw`#!/bin/sh +printf '%s\n' 'C:\Users\runner with spaces\AppData\Local\npm-cache' +`, + ); + await chmod(npmStub, 0o755); + + for (const [event, ref, defaultBranch, expected] of [ + ["push", "refs/heads/main", "main", "true"], + ["schedule", "refs/heads/main", "main", "true"], + ["workflow_dispatch", "refs/heads/main", "main", "true"], + ["push", "refs/heads/stable", "stable", "true"], + ["pull_request", "refs/heads/main", "main", "false"], + ["pull_request_target", "refs/heads/main", "main", "false"], + ["workflow_call", "refs/heads/main", "main", "false"], + ["push", "refs/heads/feature/cache", "main", "false"], + ["push", "refs/heads/main", "stable", "false"], + ["push", "refs/heads/main", "", "false"], + ]) { + const outputPath = join( + outputDirectory, + `policy-${Date.now()}-${Math.random()}`, + ); + execFileSync("bash", ["-euo", "pipefail", "-c", command], { + env: { + ...process.env, + PATH: `${outputDirectory}${delimiter}${process.env.PATH ?? ""}`, + GITHUB_EVENT_NAME: event, + GITHUB_REF: ref, + GITHUB_OUTPUT: outputPath, + DEFAULT_BRANCH: defaultBranch, + LOCKFILES_HASH: "lock-hash", + RUNNER_OS: "Windows", + }, + }); + const output = await readFile(outputPath, "utf8"); + expect(output).toContain(`can-save=${expected}\n`); + expect(output).toContain( + `path=${String.raw`C:\Users\runner with spaces\AppData\Local\npm-cache`}\n`, + ); + expect(output).toContain( + `key=npm-download-Windows-${process.arch}-lock-hash\n`, + ); + } + }); + + it("uses a lockfile-, npmrc-, OS-, and runtime-architecture-scoped download cache", async () => { + const action = await readAction(); + const policy = action.runs.steps.find((step) => step.id === "context"); + const restoreAndSave = action.runs.steps.find((step) => + step.uses?.startsWith("actions/cache@"), + ); + const restoreOnly = action.runs.steps.find((step) => + step.uses?.startsWith("actions/cache/restore@"), + ); + + expect(action.outputs["can-save"]?.value).toBe( + "${{ steps.context.outputs.can-save }}", + ); + expect(action.outputs.path?.value).toBe( + "${{ steps.context.outputs.path }}", + ); + expect(policy?.env?.DEFAULT_BRANCH).toBe( + "${{ github.event.repository.default_branch }}", + ); + expect(policy?.env?.LOCKFILES_HASH).toBe( + "${{ hashFiles('package-lock.json', 'npm-shrinkwrap.json', '.npmrc') }}", + ); + expect(policy?.run).toContain( + 'cache_key="npm-download-${RUNNER_OS}-${runtime_arch}-${LOCKFILES_HASH}"', + ); + expect(restoreAndSave?.uses).toBe( + "actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830", + ); + expect(restoreAndSave?.if).toBe("steps.context.outputs.can-save == 'true'"); + expect(restoreOnly?.uses).toBe( + "actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830", + ); + expect(restoreOnly?.if).toBe("steps.context.outputs.can-save != 'true'"); + expect(restoreAndSave?.with?.path).toBe( + "${{ steps.context.outputs.path }}", + ); + expect(restoreOnly?.with?.path).toBe(restoreAndSave?.with?.path); + expect(restoreAndSave?.with?.key).toBe("${{ steps.context.outputs.key }}"); + expect(restoreOnly?.with?.key).toBe(restoreAndSave?.with?.key); + expect(restoreAndSave?.with).not.toHaveProperty("restore-keys"); + expect(restoreOnly?.with).not.toHaveProperty("restore-keys"); + }); + + it("replaces every inline setup-node npm cache immediately and preserves conditions", async () => { + const workflows = await readWorkflows(); + let npmCacheSteps = 0; + + for (const { file, workflow } of workflows) { + const source = await readFile( + join(repository, ".github/workflows", file), + "utf8", + ); + expect(source).not.toMatch(/^\s+cache:\s*npm\s*$/mu); + for (const [jobName, job] of Object.entries(workflow.jobs ?? {})) { + const steps = job.steps ?? []; + for (let index = 0; index < steps.length; index++) { + const step = steps[index]; + if (step?.uses !== "./.github/actions/npm-cache") continue; + npmCacheSteps++; + expect(step.id).toBe("npm-cache"); + const setup = steps[index - 1]; + expect( + typeof setup?.uses === "string" && + setup.uses.startsWith("actions/setup-node@"), + `${file}:${jobName} npm cache action must immediately follow setup-node`, + ).toBe(true); + expect(step.if).toBe(setup?.if); + expect(setup?.with).not.toHaveProperty("cache", "npm"); + const npmCiIndex = steps.findIndex( + (candidate, candidateIndex) => + candidateIndex > index && candidate.run === "npm ci", + ); + expect( + npmCiIndex, + `${file}:${jobName} retains npm ci after cache`, + ).toBeGreaterThan(index); + } + } + } + + expect(npmCacheSteps).toBeGreaterThan(0); + }); +}); + +it("shares static task snapshots only after validation using the npm cache owner's write authority", async () => { + const workflows = await readWorkflows(); + const ci = workflows.find(({ file }) => file === "ci.yml"); + const steps = ci?.workflow.jobs?.static?.steps ?? []; + const restore = steps.find((step) => step.id === "static-cache"); + const save = steps.find( + (step) => step.name === "Save validated static task cache", + ); + expect(restore?.uses).toBe( + "actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830", + ); + expect(restore?.if).toBe("needs.changes.outputs.code == 'true'"); + expect(restore?.with).toMatchObject({ path: ".turbo/cache/" }); + expect(save?.uses).toBe( + "actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830", + ); + expect(save?.if).toContain("success()"); + expect(save?.if).toContain("steps.npm-cache.outputs.can-save == 'true'"); + expect(save?.with).toEqual({ + path: ".turbo/cache/", + key: "${{ steps.static-cache.outputs.cache-primary-key }}", + }); + expect(steps.indexOf(save!)).toBeGreaterThan( + steps.findIndex((step) => step.run === "npm run test:python"), + ); +}); + +it("bounds the rolling static snapshot with Turbo age and size eviction", async () => { + const turbo = z + .object({ + cacheMaxAge: z.string(), + cacheMaxSize: z.string(), + tasks: z.record( + z.string(), + z.object({ inputs: z.array(z.unknown()).optional() }), + ), + }) + .parse(JSON.parse(await readFile(join(repository, "turbo.json"), "utf8"))); + // The static job restores the previous snapshot by prefix and saves the + // whole .turbo/cache directory again. Without local eviction every main + // commit carries all historical entries into a new immutable GitHub cache, + // growing transfers toward the 10 GB repository limit. + expect(turbo.cacheMaxAge).toMatch(/^[1-9]\d*(s|m|h|d|w)$/u); + expect(turbo.cacheMaxAge).not.toBe("0"); + const size = /^(\d+(?:\.\d+)?)(MB|GB|TB)$/iu.exec(turbo.cacheMaxSize); + expect(size, "cacheMaxSize must use an explicit MB/GB/TB budget").not.toBe( + null, + ); + const value = Number(size?.[1] ?? "0"); + const unit = (size?.[2] ?? "MB").toUpperCase(); + const megabytes = + unit === "TB" ? value * 1024 * 1024 : unit === "GB" ? value * 1024 : value; + expect(megabytes).toBeGreaterThan(0); + expect(megabytes).toBeLessThanOrEqual(1024); + expect(turbo.tasks["knip"]?.inputs).toContain("vitest.config.ts"); +}); diff --git a/tests/boundary/filesystem/ciRequiredLanes.test.ts b/tests/boundary/filesystem/ciRequiredLanes.test.ts index 4c23844e76..d7ac1418ed 100644 --- a/tests/boundary/filesystem/ciRequiredLanes.test.ts +++ b/tests/boundary/filesystem/ciRequiredLanes.test.ts @@ -506,3 +506,44 @@ it("rejects incomplete or malformed classification even when available jobs succ }); } }); + +it("fails fast only for ordinary PR matrices and rejects partial portable feedback", async () => { + const { jobs } = await workflow(); + expect(jobs["test-shard"]?.strategy).toMatchObject({ + "fail-fast": + "${{ github.event_name == 'pull_request' && needs.changes.outputs.tier == 'fast' }}", + }); + expect(jobs["package-e2e"]?.strategy).toMatchObject({ "fail-fast": false }); + const flags = await scopeFlags(); + for (const result of ["failure", "cancelled", "skipped", "missing"]) { + // Even an incorrectly successful report merge cannot hide matrix failure. + const needs: Record = { + changes: { + result: "success", + outputs: { + ...Object.fromEntries(flags.map((flag) => [flag, "false"])), + tier: "fast", + code: "true", + }, + }, + static: { result: "success" }, + build: { result: "success" }, + test: { result: "success" }, + ...(result === "missing" ? {} : { "test-shard": { result } }), + }; + await expect( + executeWorkflowFixture( + process.execPath, + [ + fileURLToPath( + new URL("../../../scripts/ci/required.mjs", import.meta.url), + ), + ], + { env: { ...process.env, CI_JOBS: JSON.stringify(needs) } }, + ), + ).rejects.toMatchObject({ + code: 1, + stderr: expect.stringContaining("test-shard"), + }); + } +}); diff --git a/tests/boundary/filesystem/staticTaskCache.test.ts b/tests/boundary/filesystem/staticTaskCache.test.ts new file mode 100644 index 0000000000..2529f3bf42 --- /dev/null +++ b/tests/boundary/filesystem/staticTaskCache.test.ts @@ -0,0 +1,290 @@ +import { spawnSync } from "node:child_process"; +import { mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { expect, it } from "vitest"; +import { z } from "zod"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { workflowGit } from "../../support/workflowGit.js"; + +const repository = fileURLToPath(new URL("../../../", import.meta.url)); +const turbo = join(repository, "node_modules/turbo/bin/turbo"); +const reportScript = join(repository, "scripts/ci/static-check-report.mjs"); +const rootConfigSchema = z.object({ + globalDependencies: z.array(z.string()).optional(), + globalPassThroughEnv: z.array(z.string()).optional(), + tasks: z.record(z.string(), z.record(z.string(), z.unknown())), +}); +const dryRunSchema = z.object({ + tasks: z.array(z.object({ taskId: z.string(), hash: z.string() })), +}); + +const execute = (command: string, args: string[], cwd: string) => + spawnSync(process.execPath, [command, ...args], { + cwd, + encoding: "utf8", + timeout: 30_000, + maxBuffer: 16 * 1024 * 1024, + env: process.env, + }); + +const readRootConfig = async () => + rootConfigSchema.parse( + JSON.parse(await readFile(join(repository, "turbo.json"), "utf8")), + ); + +const writeTaskFixture = async ( + root: string, + taskNames: string[], + scripts: Record, +) => { + const config = await readRootConfig(); + const tasks = Object.fromEntries( + taskNames.map((name) => { + const task = config.tasks[name]; + if (task === undefined) throw new Error(`Missing Turbo task ${name}`); + // These tests measure a task's own inputs. Drop dependency edges so the + // unrelated compile task cannot add fixture-specific inputs or outputs. + const { dependsOn: _dependsOn, ...definition } = task; + return [name, definition]; + }), + ); + await writeFile( + join(root, "turbo.json"), + JSON.stringify( + { + ...(config.globalDependencies === undefined + ? {} + : { globalDependencies: config.globalDependencies }), + ...(config.globalPassThroughEnv === undefined + ? {} + : { globalPassThroughEnv: config.globalPassThroughEnv }), + tasks, + }, + null, + 2, + ), + ); + await writeFile( + join(root, "package.json"), + JSON.stringify({ + name: "rea-static-task-cache-fixture", + version: "1.0.0", + private: true, + packageManager: "npm@11.16.0", + scripts, + }), + ); + await writeFile( + join(root, "package-lock.json"), + JSON.stringify({ + name: "rea-static-task-cache-fixture", + version: "1.0.0", + lockfileVersion: 3, + packages: { + "": { name: "rea-static-task-cache-fixture", version: "1.0.0" }, + }, + }), + ); +}; + +const runTurbo = ( + root: string, + task: string, + cacheDirectory: string, + dryRun = false, +) => + execute( + turbo, + [ + "run", + task, + ...(dryRun ? ["--dry=json"] : ["--output-logs=errors-only"]), + "--cache=local:rw,remote:r", + "--cache-dir", + cacheDirectory, + ], + root, + ); + +const taskHash = (root: string, task: string, cacheDirectory: string) => { + const result = runTurbo(root, task, cacheDirectory, true); + expect(result.error).toBeUndefined(); + expect(result.status, result.stderr).toBe(0); + const { tasks } = dryRunSchema.parse(JSON.parse(result.stdout)); + const entry = tasks.find(({ taskId }) => taskId === task); + if (entry === undefined) throw new Error(`Turbo omitted task ${task}`); + return entry.hash; +}; + +it("restores a real lint report across cold, warm, and fresh consumer checkouts", async () => { + const producer = await createTestTempDirectory("rea-static-cache-producer-"); + const consumer = await createTestTempDirectory("rea-static-cache-consumer-"); + const cache = await createTestTempDirectory("rea-static-cache-store-"); + const script = "node scripts/ci/static-check-report.mjs lint"; + for (const root of [producer, consumer]) { + await mkdir(join(root, "scripts/ci"), { recursive: true }); + await mkdir(join(root, "src"), { recursive: true }); + await symlink( + join(repository, "node_modules"), + join(root, "node_modules"), + "junction", + ); + await writeTaskFixture(root, ["lint"], { lint: script }); + await writeFile( + join(root, ".nvmrc"), + await readFile(join(repository, ".nvmrc")), + ); + await writeFile(join(root, ".gitignore"), ".cache/\nnode_modules/\n"); + await writeFile( + join(root, ".oxlintrc.json"), + JSON.stringify({ rules: { "no-debugger": "error" } }), + ); + await writeFile(join(root, "src/check.js"), "export const ready = true;\n"); + await writeFile( + join(root, "scripts/ci/static-check-report.mjs"), + await readFile(reportScript, "utf8"), + ); + await workflowGit(root, ["init", "--initial-branch=main"]); + await workflowGit(root, ["add", "."]); + await workflowGit(root, ["commit", "-m", "test: seed static task cache"]); + } + + const cold = runTurbo(producer, "lint", cache); + expect(cold.error).toBeUndefined(); + expect(cold.status, cold.stderr).toBe(0); + expect(cold.stdout).toMatch(/Cached:\s+0 cached/u); + const report = join(producer, ".cache/static-checks/oxlint.json"); + const cleanReport = await readFile(report, "utf8"); + + await rm(join(producer, ".cache"), { recursive: true }); + const warm = runTurbo(producer, "lint", cache); + expect(warm.error).toBeUndefined(); + expect(warm.status, warm.stderr).toBe(0); + expect(warm.stdout).toMatch(/Cached:\s+1 cached/u); + expect(await readFile(report, "utf8")).toBe(cleanReport); + + const freshConsumer = runTurbo(consumer, "lint", cache); + expect(freshConsumer.error).toBeUndefined(); + expect(freshConsumer.status, freshConsumer.stderr).toBe(0); + expect(freshConsumer.stdout).toMatch(/Cached:\s+1 cached/u); + expect( + await readFile(join(consumer, ".cache/static-checks/oxlint.json"), "utf8"), + ).toBe(cleanReport); + + await writeFile(join(consumer, "src/check.js"), "debugger;\n"); + const invalid = runTurbo(consumer, "lint", cache); + expect(invalid.error).toBeUndefined(); + expect(invalid.status).not.toBe(0); + expect(invalid.stdout).toMatch(/Cached:\s+0 cached/u); + const { diagnostics } = z + .object({ + diagnostics: z.array( + z.object({ code: z.string().optional(), severity: z.string() }), + ), + }) + .parse( + JSON.parse( + await readFile( + join(consumer, ".cache/static-checks/oxlint.json"), + "utf8", + ), + ), + ); + expect(diagnostics).toContainEqual( + expect.objectContaining({ + code: "eslint(no-debugger)", + severity: "error", + }), + ); +}); + +it("invalidates checker task hashes for Node, ignore, and VitePress config changes", async () => { + const root = await createTestTempDirectory("rea-static-cache-inputs-"); + const cache = await createTestTempDirectory("rea-static-cache-hashes-"); + await mkdir(join(root, "docs/.vitepress"), { recursive: true }); + await mkdir(join(root, "src"), { recursive: true }); + await mkdir(join(root, "tests"), { recursive: true }); + await writeTaskFixture( + root, + ["jscpd", "knip", "metadata:check", "scan:todos"], + // This fixture probes only Turbo's real task hash; the checker commands + // are intentionally inert so the hash test cannot invoke large suites. + { + jscpd: 'node -e "process.exit(0)"', + knip: 'node -e "process.exit(0)"', + "metadata:check": 'node -e "process.exit(0)"', + "scan:todos": 'node -e "process.exit(0)"', + }, + ); + await writeFile(join(root, ".nvmrc"), "24.18.0\n"); + await writeFile(join(root, ".npmrc"), "engine-strict=true\n"); + await writeFile(join(root, ".gitignore"), ".cache/\n"); + await writeFile(join(root, ".jscpd.json"), "{}\n"); + await writeFile(join(root, "docs/.vitepress/config.ts"), "export {};\n"); + await writeFile( + join(root, "vitest.config.ts"), + 'import { CiTestSequencer } from "./tests/ciTestSequencer.js";\nexport const sequencer = CiTestSequencer;\n', + ); + await writeFile(join(root, "src/input.ts"), "export {};\n"); + await writeFile(join(root, "tests/input.ts"), "export {};\n"); + + const initial = new Map(); + for (const task of ["jscpd", "knip", "metadata:check", "scan:todos"]) + initial.set(task, taskHash(root, task, cache)); + + await writeFile(join(root, ".nvmrc"), "24.19.0\n"); + for (const task of ["jscpd", "metadata:check", "scan:todos"]) + expect( + taskHash(root, task, cache), + `${task} must hash the Node pin`, + ).not.toBe(initial.get(task)); + await writeFile(join(root, ".nvmrc"), "24.18.0\n"); + + await writeFile(join(root, ".npmrc"), "engine-strict=false\n"); + for (const task of ["jscpd", "knip", "metadata:check", "scan:todos"]) + expect( + taskHash(root, task, cache), + `${task} must hash npm settings`, + ).not.toBe(initial.get(task)); + await writeFile(join(root, ".npmrc"), "engine-strict=true\n"); + for (const task of ["jscpd", "metadata:check", "scan:todos"]) + expect(taskHash(root, task, cache), `${task} restores after Node pin`).toBe( + initial.get(task), + ); + + await writeFile(join(root, ".gitignore"), ".cache/\ncoverage/\n"); + for (const task of ["jscpd", "knip"]) + expect(taskHash(root, task, cache), `${task} must hash gitignore`).not.toBe( + initial.get(task), + ); + await writeFile(join(root, ".gitignore"), ".cache/\n"); + for (const task of ["jscpd", "knip"]) + expect( + taskHash(root, task, cache), + `${task} restores after gitignore`, + ).toBe(initial.get(task)); + + await writeFile( + join(root, "docs/.vitepress/config.ts"), + "export const configRevision = 2;\n", + ); + expect( + taskHash(root, "knip", cache), + "knip must hash its configured VitePress entry", + ).not.toBe(initial.get("knip")); + await writeFile(join(root, "docs/.vitepress/config.ts"), "export {};\n"); + + await writeFile(join(root, "vitest.config.ts"), "export {};\n"); + expect( + taskHash(root, "knip", cache), + "knip must hash vitest.config.ts so unused sequencer imports invalidate the cache", + ).not.toBe(initial.get("knip")); + for (const task of ["jscpd", "metadata:check", "scan:todos"]) + expect( + taskHash(root, task, cache), + `${task} must ignore vitest.config.ts`, + ).toBe(initial.get(task)); +}); diff --git a/tests/boundary/mcp/enhancedTools.04-uses-complete-inventories-inline.test.ts b/tests/boundary/mcp/enhancedTools.04-uses-complete-inventories-inline.test.ts index a9b6f11192..a08d35a342 100644 --- a/tests/boundary/mcp/enhancedTools.04-uses-complete-inventories-inline.test.ts +++ b/tests/boundary/mcp/enhancedTools.04-uses-complete-inventories-inline.test.ts @@ -343,9 +343,7 @@ describe("overview document identity through MCP", () => { details: { provider_id: "fixture", operation: "list_documents", - reason: expect.stringContaining( - "provider lacks document selection", - ), + reason: expect.stringContaining("provider lacks document selection"), }, }); }); diff --git a/tests/evaluation/package/packagePlatformDiagnostics.test.ts b/tests/evaluation/package/packagePlatformDiagnostics.test.ts index 3ffb1d0df1..d4f88b875b 100644 --- a/tests/evaluation/package/packagePlatformDiagnostics.test.ts +++ b/tests/evaluation/package/packagePlatformDiagnostics.test.ts @@ -25,7 +25,10 @@ const execution = (failure: unknown, status = 1) => ({ stdout: JSON.stringify(failure), }); -const overview = (procedure_count?: unknown, operation = "binary_overview") => ({ +const overview = ( + procedure_count?: unknown, + operation = "binary_overview", +) => ({ operation, ...(procedure_count === undefined ? {} diff --git a/turbo.json b/turbo.json index 758f31e327..352a608344 100644 --- a/turbo.json +++ b/turbo.json @@ -1,11 +1,13 @@ { "$schema": "https://turborepo.dev/schema.json", + "cacheMaxAge": "7d", + "cacheMaxSize": "500MB", + "globalDependencies": [".nvmrc", ".npmrc"], "globalPassThroughEnv": ["GOMAXPROCS", "GOMEMLIMIT", "UV_THREADPOOL_SIZE"], "tasks": { "compile": { "dependsOn": ["metadata:check"], "inputs": [ - ".nvmrc", "package.json", "package-lock.json", "scripts/check-dependency-install.mjs", @@ -22,7 +24,6 @@ }, "typecheck": { "inputs": [ - ".nvmrc", "package.json", "package-lock.json", "src/**/*.ts", @@ -49,7 +50,6 @@ "outputs": [".cache/static-checks/oxlint.json"], "inputs": [ ".gitignore", - ".nvmrc", ".oxlintrc.json", "package.json", "package-lock.json", @@ -71,7 +71,6 @@ "outputs": [".cache/static-checks/oxlint.json"], "inputs": [ ".gitignore", - ".nvmrc", ".oxlintrc.json", "package.json", "package-lock.json", @@ -92,7 +91,7 @@ "knip": { "dependsOn": ["compile"], "inputs": [ - ".nvmrc", + ".gitignore", "knip.json", "package.json", "package-lock.json", @@ -100,7 +99,9 @@ "src/**/*.ts", "tests/**/*.ts", "tsconfig.json", - "tsconfig.build.json" + "tsconfig.build.json", + "vitest.config.ts", + "docs/.vitepress/config.ts" ] }, "jscpd": { @@ -139,7 +140,6 @@ "artifacts:skills" ], "inputs": [ - ".nvmrc", "package.json", "package-lock.json", "scripts/generate-*.mjs", @@ -171,7 +171,6 @@ "artifacts:mcp-catalog": { "dependsOn": ["compile"], "inputs": [ - ".nvmrc", "package.json", "package-lock.json", "scripts/generate-mcp-tool-catalog.mjs", @@ -182,7 +181,6 @@ "artifacts:skills": { "dependsOn": ["compile"], "inputs": [ - ".nvmrc", "package.json", "package-lock.json", "scripts/generate-skill-metadata.mjs", @@ -194,7 +192,6 @@ "artifacts:product-catalog": { "dependsOn": ["compile"], "inputs": [ - ".nvmrc", "package.json", "package-lock.json", "scripts/generate-product-catalog.mjs", @@ -206,7 +203,6 @@ "artifacts:managed-evidence": { "dependsOn": ["artifacts:skills"], "inputs": [ - ".nvmrc", "package.json", "package-lock.json", "scripts/generate-completion-ledger.mjs", @@ -222,7 +218,6 @@ "artifacts:error-schema": { "dependsOn": ["compile"], "inputs": [ - ".nvmrc", "package.json", "package-lock.json", "scripts/generate-error-schema.mjs", @@ -235,7 +230,6 @@ "mcp-catalog:check:built": { "dependsOn": ["artifacts:mcp-catalog"], "inputs": [ - ".nvmrc", "package.json", "package-lock.json", "scripts/generate-mcp-tool-catalog.mjs", @@ -249,7 +243,6 @@ "evidence:check:built": { "dependsOn": ["artifacts:managed-evidence"], "inputs": [ - ".nvmrc", "package.json", "package-lock.json", "scripts/generate-completion-ledger.mjs",