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. Exact-target dispatches using the current shard runner retain all three shards, including Full Release Validation's complete Node and Bun passes. Only compatibility targets using the historical package test command keep their singleton, unsharded envelope. The window.open lint runs once in row one. Each row retains its 20-minute deadline and the matrix admits at most three rows concurrently. The extra two jobs add two Blacksmith registrations per selected automatic run on Blacksmith routes, and none on hosted routes. Exact-target dispatches add two hosted rows without changing automatic-run budgets. 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. The workflow pins the runner's Go version so frozen targets cannot select a different compiler. Updates keep this pin aligned with the current translation module's preferred toolchain and both Testbox bootstrap workflows.

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: 70 Node rows for canonical pushes and 130 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. Node rows list each striped test file explicitly. The manifest projects the fields consumed by the shard runner, then uses gzip+base64 (groups_gzip_base64) when the target contains the codec. Flat configuration rows, including manual CI and exact-head PR release gates, use one packed group while retaining their original jobs and test selections. Targets without that capability retain their original flat fields or projected 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 four for canonical Blacksmith push and PR first attempts, including forks; the GitHub override, retries, manual dispatches, schedules, and noncanonical repositories retain two. 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. Explicit core-stripe invocations keep src/agents, src/gateway, src/infra, and ui in separate lint processes from their stripe's remaining targets. Their semantic caches can exceed the Go soft limit when combined with neighboring targets. Constrained runners execute these processes serially and release each process tree before starting the next. The five stripe assignments, workflow rows, lint rules, and file coverage stay unchanged; this adds no runner registrations. Automatic full lint (pnpm lint) retains its original five aggregated core Programs, including when it inherits CI environment variables. Published Git updaters call this full pipeline with a fixed 20-minute command deadline; the explicit stripe caller must not add compiler startups to that preflight.

Regular Android PR/main CI and PR release_gate dispatches use four rows: Play and Wear-shared unit tests; third-party unit tests; Wear unit tests/lint plus third-party app lint; and Kotlin lint for all four modules plus Play app and Wear-shared lint. 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. Every native invocation remains sequential within its row, and all selected Gradle tasks run exactly once.

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. All other Android tasks use 20 minutes, including the now unit-only phone rows. 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?