> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Vitest CI sharding

> The unit-test suite is 1,657 test files / ~7,167 tests as of 2026-04-22, with a wall-clock cost of ~3 m 43 s unsharded (per reports/perf/baseline-2026-04-22.md…

**Status:** Active
**Owner:** Platform Architecture
**Phase:** 1.D of [`docs/recommendations/DEV_VELOCITY_AND_ARCHITECTURE_PLAN_2026-04-22.md`](https://github.com/Encore-OS/encoreos/blob/development/docs/recommendations/DEV_VELOCITY_AND_ARCHITECTURE_PLAN_2026-04-22.md)

The unit-test suite is **1,657 test files / \~7,167 tests** as of 2026-04-22, with a wall-clock cost of **\~3 m 43 s** unsharded (per `reports/perf/baseline-2026-04-22.md`). Vitest already runs files in parallel within a single process (`pool: 'threads'`, `maxWorkers: '50%'` in CI — see `vitest.config.ts`), but a single CI runner still pays the full \~4-minute wall-clock.

Sharding splits the suite across **N parallel CI runners**, each running `1/N` of the files. Combined with Vitest's per-file in-process parallelism, this can bring CI test wall-clock down to **\~1 minute** at N=4 (verified locally; see "Measurement" below).

## Quick reference

```bash theme={null}
# Local: run shard 2 of 4 (no env vars)
SKIP_SUPABASE_TESTS=true CI=true npx tsx scripts/ci/run-vitest-shard.ts --shard 2 --total 4

# Local: run the full suite (no sharding) via the wrapper
npm run test:unit:shard

# CI matrix (GitHub Actions sets the env vars for each matrix job)
VITEST_SHARD_INDEX=2 VITEST_SHARD_TOTAL=4 npm run test:unit:shard
```

CLI args win over env vars when both are set. Defaults to `1/1` (no sharding) which is safe to use locally.

The wrapper writes a per-shard JUnit XML report at `reports/test-shards/unit-${shard}-of-${total}.xml` so each CI matrix job's results can be uploaded as a separate artifact without clobbering siblings.

## How Vitest sharding works

`--shard=$INDEX/$TOTAL` partitions the discovered test **files** (not individual tests) deterministically by hash. Equal `TOTAL` values across runs produce stable distributions, so a flake in shard 3 can be re-run as `--shard=3/4` and pick up the same files.

Reference: [https://vitest.dev/guide/cli.html#shard](https://vitest.dev/guide/cli.html#shard)

## Recommended GitHub Actions wiring (not yet applied to `.github/workflows/build.yml`)

```yaml theme={null}
jobs:
  unit-tests:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci --legacy-peer-deps
      - name: Unit tests (shard ${{ matrix.shard }}/4)
        run: npm run test:unit:shard
        env:
          CI: true
          SKIP_SUPABASE_TESTS: 'true'
          VITEST_SHARD_INDEX: ${{ matrix.shard }}
          VITEST_SHARD_TOTAL: 4
      - name: Upload JUnit shard report
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: unit-shard-${{ matrix.shard }}
          path: reports/test-shards/unit-${{ matrix.shard }}-of-4.xml
          if-no-files-found: error
```

Switching the existing `npm run test:coverage` step to a sharded matrix is **not done in this PR** — it's a CI-workflow change that should land in its own PR after a project owner can validate against billing budgets (matrix runs scale CI minutes linearly; `4 shards × ~1 min ≈ 4 CI minutes` total vs `1 × ~4 min ≈ 4 CI minutes`, but with 4× the wall-clock parallelism).

### Coverage caveat

Coverage thresholds in `vitest.config.ts` (`statements: 40, branches: 35, functions: 40, lines: 40`) **only make sense on the full suite**, not per shard. If we wire sharding to the coverage step, we must also add a "merge coverage" job after the matrix that combines per-shard `coverage/coverage-final.json` files via `nyc merge` (or Vitest's own merge tooling) before checking thresholds.

For Phase 1.D, the recommendation is:

* Use sharding for the **fast-feedback** unit-tests gate (`test:unit:shard`).
* Keep the **coverage** step single-runner (`test:coverage`) until merge tooling is proven on this codebase.

## Measurement on this branch

Captured on the Cursor Cloud VM (Linux, 4 vCPU / 16 GB RAM), filtering with `SKIP_SUPABASE_TESTS=true CI=true`:

| Shard                  | Files | Tests | Wall-clock   | Notes                                                     |
| ---------------------- | ----- | ----- | ------------ | --------------------------------------------------------- |
| `1/100` (smoke)        | 6     | 48    | 2.78 s       | Confirms shard CLI works and JUnit emits correctly.       |
| `2/4`                  | 136   | 1,681 | **61 s**     | One quartile of the suite. Includes 5 pre-existing skips. |
| baseline (no sharding) | 544   | 7,167 | **3 m 43 s** | From `reports/perf/baseline-2026-04-22.md`.               |

Projected wall-clock at `--total=4` (in parallel CI matrix): \~62 s — a **\~3.6×** speedup vs the unsharded baseline.

## See also

* [`scripts/ci/run-vitest-shard.ts`](https://github.com/Encore-OS/encoreos/blob/development/scripts/ci/run-vitest-shard.ts)
* [`vitest.config.ts`](https://github.com/Encore-OS/encoreos/blob/development/vitest.config.ts)
* [`docs/testing/TESTING_SETUP_AND_RUN.md`](/testing/TESTING_SETUP_AND_RUN)
* [Vitest docs — `--shard`](https://vitest.dev/guide/cli.html#shard)
