Testing and CI

Job budgets and platform lanes

Shard counts, concurrency and matrix budgets, lint memory policy, Android rows, and cache-key bounds. Part of the CI scope and routing index.

The standalone UI suite runs three native Vitest shards through the same group executor and cache leaf as its bounded four-file seed in the trusted warmer. Each row retains the root Node worker limit of three; Chromium uses its project default. The shards preserve the complete four-project inventory and each project's isolation and cleanup policy. Every frozen target keeps its original singleton, unsharded test command. The window.open lint runs once in row one. The extra two jobs add two Blacksmith registrations per selected non-frozen run on Blacksmith routes, and none on hosted routes. They do not guarantee an eight-minute workflow: preflight, setup, queue time and other jobs still apply. Its native reporter records runtime CPU and memory facts, configured project workers, module diagnostics, and observed queue/end events. Browser pool logs record actual Chromium sessions. Project counts at run start describe discovery before sharding; completed module identities and final counts prove coverage. Event intervals are not scheduler-admission times; repeated environment/prepare durations must not be summed into wall time. These receipts do not establish transform-cache hits. Tooling stripes install Go only when their selected files include the docs translation test; historical whole-config plans retain their existing setup.

Shell-heavy macOS signing and elevation cases admit up to three cases per file, capped at Node's available parallelism. Independent checkout fixture tables retain their two-case limit. Each case owns its commands and temporary roots; cleanup joins its process tree and callback work before removing those inputs. Outer suites and the remaining checkout contract cases stay sequential.

Once admitted, canonical Linux CI permits up to 96 concurrent Node test jobs. The manifest separately enforces total-job budgets: 64 Node rows for canonical pushes and 120 for canonical PRs, including precise and plugin plans. GitHub also caps one job's combined outputs at 1 MiB measured in UTF-16, so preflight has 524,288 characters for every matrix together. Grouped Node rows list each striped test file explicitly. The manifest projects the five fields consumed by the shard runner, then uses gzip+base64 (groups_gzip_base64) when the target contains the codec. Historical targets without that capability receive the same projection through legacy groups JSON. Workflow tests keep the complete generated output under half of the cap. The smaller fast/check lanes remain capped at 12; Windows is capped at two and Android at two because those runner pools are narrower. Compact whole-config batches run with a 120-minute batch timeout, while include-pattern groups share the same bounded job budget.

Type-aware lint on CI runners with fewer than 8 CPUs or 24 GiB of RAM uses the existing Go compiler memory policy (GOGC=30, GOMEMLIMIT=3GiB) to reduce swap pressure. Explicit Go settings remain authoritative. The limit is soft and applies only to the lint child; declaration preparation retains its own policy.

Regular Android PR/main CI and PR release_gate dispatches use four rows: Play and Wear-shared unit tests/lint, third-party unit tests/lint, Wear unit tests/lint, and Kotlin lint for all four modules. Each phone flavor has its own source set and SensitiveFeatureConfig; apps/android/app/src/thirdParty/AndroidManifest.xml declares additional permissions and components. The Kotlin-lint row also compiles the benchmark when benchmark or Android build/dependency inputs change; missing or unusable changed-path data keeps that build.

Ordinary full-scope manual validation retains six Android rows: the three test rows, build-play, build-wear, and Kotlin lint. The build rows own lint without repeating it in test rows. build-play assembles both phone flavors and the Wear shared module and compiles the benchmark; build-wear assembles Wear. build-play-compat retains Play-only packaging for older frozen targets. GitHub-hosted build-play gets a 35-minute job budget for its three memory-bounded Gradle invocations; Blacksmith build-play and all other Android tasks retain 20 minutes. The budget follows the current attempt's runner route even when a retry reuses the original preflight matrix. Each current Gradle task has one protected sticky disk; PR jobs use disposable clones, while protected runs refresh content-addressed Gradle entries in place. Existing Android opt-in and npm qualification deferral are unchanged.

Robolectric resolves Android SDK artifacts outside Gradle's dependency cache, so every Android test-* task receives a workflow-owned Gradle init script that points test JVMs at a dedicated Maven-local repository. Actions cache restores are task-, platform-, and Android-contract-scoped; a prefix restore can seed a changed contract, but only a successful trusted run may publish the completed exact cache after a miss. Cold runs may download missing SDK artifacts, while warm runs reuse the exact archive. Build and lint tasks do not receive the Robolectric init script.

Remaining Blacksmith sticky-disk keys are deliberately bounded by supported task dimensions, never PR number, commit, run, branch, or dependency hash. Dependency, runtime transform, and compile caches use Actions cache instead because immutable archives expose verifiable restore/save results and avoid mutable snapshot-promotion failures. After a sticky key-version migration, add only the exact obsolete key, architecture, and region identities to .github/retired-sticky-disks.json, dispatch Sticky Disk Cleanup from main with the same dimensions and confirmation, verify deletion, then remove those entries. The workflow routes ARM identities to an ARM runner, rejects runner-region mismatches, uses Blacksmith's exact-key deletion action, and never deletes Docker builder caches or wildcard prefixes. Actions cache archives use normal LRU and inactivity eviction.

The check-dependencies shard runs Knip dependency, unused-file, and unused-export checks. Both guards enforce zero findings across production and full-tree scans, with no unused-file allowlist. The export guard also audits script entry exports. Production excludes test-support consumers; the full-tree and script scans include tests as consumers. Model intentional dynamic consumers in config/knip.config.ts, config/knip.all-exports.config.ts, or config/knip.scripts-exports.config.ts as appropriate. Each guard reports every scan outcome and fails if any scan fails. Historical targets run the export guard when they provide it and retain their older dead-code fallback otherwise.

Was this useful?