ci: shard both suites by measured duration and split the unit suite

Vitest's --shard cut the file list into slices of equal length, which gave
the integration shards 954 s to 1 449 s. A sequencer now deals files out by
their measured duration, plus a fixed per-file cost for import and setup,
longest first to the shard with the least work, and starts the longest files
first inside a shard. Timings live in tests/durations/*.json and are
regenerated from Vitest JSON reports with scripts/test-durations.mjs; every
CI shard uploads its report.

Integration shards run three workers each, one cloned database per worker.
The unit suite runs as three projects decided per file from its source:
guards and unit share a module graph inside a worker, isolated keeps a fresh
one for every file that mocks, spies, stubs or resets. In CI it is three
duration-balanced shards next to a job for lint, format, types and the
OpenAPI drift check.

The required check names stay as they were. "Lint, Typecheck & Test" is now
the job that answers for the static checks and every unit shard, the way
"integration" already answers for its shards. The browser-install guard
recognises a direct, flags-only vitest run as a run of the unit suite.
This commit is contained in:
Marc-André Bombeck committed 2026-10-22 05:39:35 +02:00
1 parent 28f9b1948b
commit e9f89bbebf
10 files changed
+3736 -47

No files matched your search

+30 -13
View File
@@ -10,20 +10,23 @@ permissions:
contents: read
jobs:
# The suite outgrew a single runner, then a half-suite outgrew a slow one,
# and then a third of it did too. Three shards of about 68 files cleared in
# 13 minutes on an ordinary runner, but a slow runner day ran every test at
# twice its usual duration and the first shard was still working through
# its list when the 25-minute guard cut it off: three of four pushes to main
# between 2026-08-31 and 2026-09-01 failed that way, with green tests logged
# up to the last second. Four shards keep each slice around 50 files, and
# the guard sits at three times the ordinary run rather than under two. A
# wedged test still surfaces through vitest's own 60-second budget, so the
# job timeout only needs to catch a container that never boots. Each shard
# boots its own Postgres testcontainer.
# Four shards of about equal duration, each running its files in parallel
# against one database per worker. The shards used to be four slices of
# equal file count, run serially: 954 s to 1 449 s, because a slice holding
# the sharing matrix and the backup files took half again as long as one
# holding small files. `--shard` now goes through the duration sequencer
# (`tests/durations/sequencer.ts`, timings in
# `tests/durations/integration.json`), and every file in a shard runs on
# one of three workers, each against its own clone of the migrated template
# (`tests/integration/worker-database.ts`). Three workers leave a core of
# the four-core runner to the Postgres container. The job guard stays well
# above an ordinary run; a wedged test still surfaces through vitest's own
# 60-second budget, so the guard only needs to catch a container that never
# boots. Each shard boots its own Postgres testcontainer and uploads its
# JSON report, which `scripts/test-durations.mjs` turns into the timing file.
shard:
runs-on: ubuntu-24.04
timeout-minutes: 40
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
@@ -48,8 +51,13 @@ jobs:
# vitest a stray positional argument and the shard flag never applies —
# both "shards" then run the full suite. Proven by a paired local run
# before this workflow shipped.
- run: pnpm exec vitest run --config vitest.integration.config.mts --shard=${{ matrix.shard }}/4
- run: >-
pnpm exec vitest run --config vitest.integration.config.mts
--shard=${{ matrix.shard }}/4
--reporter=default --reporter=json
--outputFile.json=integration-report-${{ matrix.shard }}.json
env:
HEALTHLOG_INTEGRATION_WORKERS: "3"
# The integration suite boots a Postgres testcontainer and points
# Prisma at it via DATABASE_URL set inside `tests/integration/setup.ts`.
# These env vars satisfy code paths that read them at module load.
@@ -63,6 +71,15 @@ jobs:
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000"
SESSION_SECRET: "integration-tests"
- name: Upload the timing report
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: integration-report-${{ matrix.shard }}
path: integration-report-${{ matrix.shard }}.json
if-no-files-found: ignore
retention-days: 14
# Branch protection requires a check named exactly "integration". This job
# carries that name and answers for every shard: it fails when any shard
# failed or was cancelled, so a half-green matrix cannot satisfy the gate.
+94 -30
View File
@@ -15,11 +15,72 @@ permissions:
contents: read
jobs:
quality:
name: Lint, Typecheck & Test
# The static checks: everything in the old single quality job except the
# unit suite, which runs sharded below.
static:
name: Lint, Typecheck & OpenAPI
runs-on: ubuntu-24.04
if: github.event_name != 'schedule'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 22
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm db:generate
# Blocking since v1.15.20 — the settings-split refactor that kept
# this warn-only is long done and the tree lints clean.
- name: ESLint
run: pnpm lint
# Prettier formatting gate. The format pass excludes generator-owned
# artifacts via .prettierignore (notably docs/api/openapi.yaml, which
# the OpenAPI drift check below enforces byte-for-byte).
- name: Prettier format check
run: pnpm format:check
- name: TypeScript strict check
run: pnpm typecheck
# OpenAPI spec drift gate — hard-fail (flipped from warn-only in v1.4.25).
#
# The Zod registry is the source of truth for the public API contract.
# Any route signature change must be reflected in `docs/api/openapi.yaml`
# so downstream consumers (notably the v1.5 iOS Swift codegen) stay in
# lockstep. If this step fails, run `pnpm openapi:generate` locally and
# commit the regenerated spec.
- name: OpenAPI spec drift check
run: |
pnpm openapi:generate
if ! git diff --exit-code docs/api/openapi.yaml; then
echo "::error::OpenAPI drift detected — regenerate the spec via 'pnpm openapi:generate' and commit the result. The Zod registry under src/lib/openapi/ is the source of truth for the public API contract."
exit 1
fi
echo "::notice::OpenAPI spec in sync with Zod source schemas."
# The unit suite in three slices of about equal duration. `--shard` goes
# through the duration sequencer in `vitest.config.mts`, which deals files
# out by their measured time (`tests/durations/unit.json`) instead of by
# count. Each slice uploads its JSON report, which is what
# `scripts/test-durations.mjs` regenerates the timing file from.
unit:
runs-on: ubuntu-24.04
if: github.event_name != 'schedule'
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
name: unit shard ${{ matrix.shard }}/3
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -38,7 +99,8 @@ jobs:
# real browser and measures the boxes. Without a browser here it caught
# its own launch failure and passed anyway, so from its first day until
# v1.38 it measured nothing on any run of this job. The browser is part
# of the gate now; the test refuses to skip when CI is set.
# of the gate now; the test refuses to skip when CI is set. Every slice
# installs it, because the sequencer decides which slice the file lands in.
#
# Headless shell only: `chromium.launch()` resolves to
# chromium_headless_shell, which is what the failing runs named, and the
@@ -54,38 +116,40 @@ jobs:
- run: pnpm exec playwright install --only-shell chromium
# Blocking since v1.15.20 — the settings-split refactor that kept
# this warn-only is long done and the tree lints clean.
- name: ESLint
run: pnpm lint
# Prettier formatting gate. The format pass excludes generator-owned
# artifacts via .prettierignore (notably docs/api/openapi.yaml, which
# the OpenAPI drift check below enforces byte-for-byte).
- name: Prettier format check
run: pnpm format:check
- name: TypeScript strict check
run: pnpm typecheck
# vitest directly, not `pnpm test --`: pnpm hands the separator through
# literally and the shard flag would never reach vitest.
- name: Unit tests
run: pnpm test
run: >-
pnpm exec vitest run --shard=${{ matrix.shard }}/3
--reporter=default --reporter=json
--outputFile.json=unit-report-${{ matrix.shard }}.json
# OpenAPI spec drift gate — hard-fail (flipped from warn-only in v1.4.25).
#
# The Zod registry is the source of truth for the public API contract.
# Any route signature change must be reflected in `docs/api/openapi.yaml`
# so downstream consumers (notably the v1.5 iOS Swift codegen) stay in
# lockstep. If this step fails, run `pnpm openapi:generate` locally and
# commit the regenerated spec.
- name: OpenAPI spec drift check
- name: Upload the timing report
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: unit-report-${{ matrix.shard }}
path: unit-report-${{ matrix.shard }}.json
if-no-files-found: ignore
retention-days: 14
# Branch protection requires a check named exactly "Lint, Typecheck & Test".
# That used to be one job running everything in turn; it is now this one,
# which answers for the static checks and every unit slice, so a half-green
# set cannot satisfy the gate.
quality:
name: Lint, Typecheck & Test
runs-on: ubuntu-24.04
needs: [static, unit]
if: always() && github.event_name != 'schedule'
timeout-minutes: 5
steps:
- name: The static checks and every unit slice finished green
run: |
pnpm openapi:generate
if ! git diff --exit-code docs/api/openapi.yaml; then
echo "::error::OpenAPI drift detected — regenerate the spec via 'pnpm openapi:generate' and commit the result. The Zod registry under src/lib/openapi/ is the source of truth for the public API contract."
if [ "${{ needs.static.result }}" != "success" ] || [ "${{ needs.unit.result }}" != "success" ]; then
echo "static: ${{ needs.static.result }}, unit: ${{ needs.unit.result }}"
exit 1
fi
echo "::notice::OpenAPI spec in sync with Zod source schemas."
# Not on pull requests. `pnpm audit` answers a question about the published
# advisory database, not about the diff: the same lockfile passes on Monday
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env node
/**
* Regenerate a timing file for the duration-balanced sequencer
* (`tests/durations/sequencer.ts`) from Vitest JSON reports.
*
* node scripts/test-durations.mjs <suite> <report.json> [<report.json> …]
*
* `<suite>` is `unit` or `integration` and names the file written,
* `tests/durations/<suite>.json`. Pass every shard's report of one run; a
* file measured twice keeps the longer reading. Files the reports do not
* mention keep their previous value, so a partial run refines the table
* rather than emptying it, and files that no longer exist are dropped.
*
* Produce a report with
* pnpm exec vitest run [--config …] --reporter=default \
* --reporter=json --outputFile.json=report.json
* The nightly shuffled workflow uploads the reports of a CI run, which is
* the better source: CI is where the shards have to balance.
*/
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { relative, resolve } from "node:path";
const root = resolve(import.meta.dirname, "..");
const [suite, ...reports] = process.argv.slice(2);
if (!["unit", "integration"].includes(suite ?? "") || reports.length === 0) {
console.error(
"usage: node scripts/test-durations.mjs <unit|integration> <report.json>…",
);
process.exit(2);
}
const target = resolve(root, "tests/durations", `${suite}.json`);
const previous = existsSync(target)
? JSON.parse(readFileSync(target, "utf8")).files
: {};
const measured = new Map();
for (const report of reports) {
const parsed = JSON.parse(readFileSync(report, "utf8"));
for (const result of parsed.testResults ?? []) {
if (typeof result.startTime !== "number") continue;
if (typeof result.endTime !== "number") continue;
const file = relative(root, result.name).split("\\").join("/");
const seconds = Math.max(0, (result.endTime - result.startTime) / 1000);
measured.set(file, Math.max(measured.get(file) ?? 0, seconds));
}
}
if (measured.size === 0) {
console.error("the reports carry no per-file timings; nothing written");
process.exit(1);
}
const files = {};
const keys = new Set([...Object.keys(previous), ...measured.keys()]);
for (const file of [...keys].sort()) {
if (!existsSync(resolve(root, file))) continue;
const seconds = measured.get(file) ?? previous[file];
files[file] = Math.round(seconds * 10) / 10;
}
writeFileSync(
target,
JSON.stringify(
{
source: `vitest JSON report of ${measured.size} files, written ${new Date().toISOString().slice(0, 10)}`,
files,
},
null,
2,
) + "\n",
);
const total = Object.values(files).reduce((a, b) => a + b, 0);
console.log(
`${target}: ${Object.keys(files).length} files, ${Math.round(total)} s in total`,
);
@@ -23,8 +23,14 @@ import { join } from "node:path";
const WORKFLOWS = join(process.cwd(), ".github", "workflows");
/** `run: pnpm test` — the whole unit suite, geometry check included. */
const RUNS_UNIT_SUITE = /^\s*(?:-\s*)?run:\s*pnpm test\s*$/m;
/**
* `run: pnpm test`, or a direct `vitest run` of the unit config with flags
* only (the sharded CI job, the nightly shuffled run): the whole unit suite
* or a slice of it, geometry check included. A run that names a path or
* another `--config` is a different, narrower suite and does not count.
*/
const RUNS_UNIT_SUITE =
/^\s*(?:-\s*)?run:\s*pnpm test\s*$|pnpm exec vitest run(?![^\n]*--config)(?=\s*$|\s+--)/m;
const INSTALLS_BROWSER = /playwright install --only-shell chromium/;
describe("the browser the unit suite measures with", () => {
+448
View File
@@ -0,0 +1,448 @@
{
"source": "vitest JSON report of 443 files, written 2026-10-22",
"files": {
"tests/integration/account-grant-lifecycle.test.ts": 46.8,
"tests/integration/achievements-expansion.test.ts": 2.4,
"tests/integration/acting-account-resolver.test.ts": 25.6,
"tests/integration/activity-calendar-read.test.ts": 0,
"tests/integration/admin-backups-audit.test.ts": 2.9,
"tests/integration/admin-backups-canonical-roundtrip.test.ts": 10.9,
"tests/integration/admin-backups-download.test.ts": 2.1,
"tests/integration/admin-backups-restore.test.ts": 5.7,
"tests/integration/admin-backups-upload-too-large.test.ts": 1.6,
"tests/integration/admin-backups-upload.test.ts": 5.5,
"tests/integration/admin-data-wipe.test.ts": 2.6,
"tests/integration/admin-drain-cumulative-endpoint.test.ts": 3.2,
"tests/integration/admin-host-metrics.test.ts": 4.9,
"tests/integration/admin-reset-password.test.ts": 5.5,
"tests/integration/admin-support-report.test.ts": 2.4,
"tests/integration/ai-capability-switches-migration.test.ts": 5.1,
"tests/integration/ai-consent-explicit.test.ts": 2,
"tests/integration/ai-extraction-capability.test.ts": 17.2,
"tests/integration/ai-optional-actions.test.ts": 9.7,
"tests/integration/ai-optional-data-routes.test.ts": 24.3,
"tests/integration/ai-optional-jobs.test.ts": 20.6,
"tests/integration/ai-optional-mixed-routes.test.ts": 19.7,
"tests/integration/ai-provider-key-removal.test.ts": 4.4,
"tests/integration/ai-usage-levels.test.ts": 8.2,
"tests/integration/all-time-extremes-tier-parity.test.ts": 1.4,
"tests/integration/analytics-bp-aggregate-paged.test.ts": 13,
"tests/integration/analytics-health-score.test.ts": 5.5,
"tests/integration/analytics-sleep-stages.test.ts": 2.9,
"tests/integration/analytics-summaries-slice.test.ts": 3.4,
"tests/integration/analytics-weekday-tz.test.ts": 2.2,
"tests/integration/apns-dispatch.test.ts": 8.4,
"tests/integration/apple-health-distance-repair.test.ts": 7.3,
"tests/integration/apple-health-import-ecg.test.ts": 8.6,
"tests/integration/apple-health-import-natural-key.test.ts": 3,
"tests/integration/apple-health-import-own-samples.test.ts": 26.8,
"tests/integration/apple-health-import-reconcile.test.ts": 9.8,
"tests/integration/apple-health-import-rollup-window.test.ts": 1.1,
"tests/integration/apple-health-import-worker-failures.test.ts": 4.2,
"tests/integration/arrival-reaction-claim.test.ts": 14.8,
"tests/integration/auth-flow.test.ts": 9,
"tests/integration/auth-ledger-cleanup.test.ts": 1.7,
"tests/integration/auth-me-ai-capabilities.test.ts": 12.3,
"tests/integration/auth-me-coach-reasoning.test.ts": 6.1,
"tests/integration/auth-password-change.test.ts": 6.3,
"tests/integration/backfill-workout-sport.test.ts": 3.2,
"tests/integration/background-switch-freshness.test.ts": 1.1,
"tests/integration/backup-chunked-storage.test.ts": 54.4,
"tests/integration/backup-custom-cycle-symptom-restore.test.ts": 1.2,
"tests/integration/backup-cycle-cross-key-restore.test.ts": 2,
"tests/integration/backup-cycle-symptom-severity-restore.test.ts": 2.8,
"tests/integration/backup-document-streaming.test.ts": 7.5,
"tests/integration/backup-key-needs.test.ts": 1.3,
"tests/integration/backup-legacy-rmssd-restore.test.ts": 1.2,
"tests/integration/backup-missing-section-restore.test.ts": 2.7,
"tests/integration/backup-mood-label-cross-key-restore.test.ts": 2,
"tests/integration/backup-portable-file-restore.test.ts": 1.1,
"tests/integration/backup-preview-memory.test.ts": 2.7,
"tests/integration/backup-preview.test.ts": 5,
"tests/integration/backup-qualitative-lab-restore.test.ts": 1.1,
"tests/integration/backup-restore-boundaries.test.ts": 3,
"tests/integration/backup-restore-job.test.ts": 10.2,
"tests/integration/backup-restore-memory.test.ts": 10.4,
"tests/integration/backup-restore-statement-timeout.test.ts": 39.7,
"tests/integration/backup-round-trip.test.ts": 19,
"tests/integration/backup-streaming-memory.test.ts": 3.9,
"tests/integration/backup-unknown-catalogue-key-restore.test.ts": 1.5,
"tests/integration/backup-visits-cross-key-restore.test.ts": 1.9,
"tests/integration/bearer-scope-enforcement.test.ts": 57.3,
"tests/integration/biomarker-backup-completeness.test.ts": 2.2,
"tests/integration/biomarker-detail.test.ts": 6.8,
"tests/integration/biomarker-merge.test.ts": 10.4,
"tests/integration/body-sites.test.ts": 14.5,
"tests/integration/boot-discovery-scans.test.ts": 4,
"tests/integration/bp-in-target.test.ts": 23,
"tests/integration/briefing-cache-locale.test.ts": 3.1,
"tests/integration/bulk-delete-source-filter.test.ts": 5.8,
"tests/integration/cache-invalidation-coverage.test.ts": 3.9,
"tests/integration/cascade-delete.test.ts": 1,
"tests/integration/changepoint-extended.test.ts": 2.2,
"tests/integration/chart-all-range-meaning.test.ts": 3.2,
"tests/integration/chart-overlay-prefs.test.ts": 2.2,
"tests/integration/chart-range-dense.test.ts": 0.5,
"tests/integration/checkup-stays-due.test.ts": 9.7,
"tests/integration/ci-environment.test.ts": 0.1,
"tests/integration/coach-availability-probe.test.ts": 5.1,
"tests/integration/coach-budget-owner.test.ts": 9.8,
"tests/integration/coach-budget-reservation.test.ts": 12,
"tests/integration/coach-chat.test.ts": 10.6,
"tests/integration/coach-conversation-delete.test.ts": 3.2,
"tests/integration/coach-conversation-documents.test.ts": 5,
"tests/integration/coach-conversation-updated-at-repair.test.ts": 2.9,
"tests/integration/coach-conversations.test.ts": 5.4,
"tests/integration/coach-feedback.test.ts": 3.6,
"tests/integration/coach-history-reach.test.ts": 5.9,
"tests/integration/coach-memory-loop.test.ts": 12.5,
"tests/integration/coach-metric-table.test.ts": 7.1,
"tests/integration/coach-prefs.test.ts": 2.9,
"tests/integration/coach-reminder-surfacing.test.ts": 5,
"tests/integration/coach-series-long-window.test.ts": 1.1,
"tests/integration/coach-stale-rollup-tail.test.ts": 1.1,
"tests/integration/coach-summary-keeps-updated-at.test.ts": 3.8,
"tests/integration/coach-trail.test.ts": 4.7,
"tests/integration/codex-credential-park.test.ts": 7.2,
"tests/integration/compaction-tombstones.test.ts": 15.7,
"tests/integration/consent-withdrawal-purge.test.ts": 8.5,
"tests/integration/consolidate-daily-mean.test.ts": 4.4,
"tests/integration/consolidate-legacy-steps.test.ts": 5.6,
"tests/integration/correlation-extended-channels.test.ts": 3.5,
"tests/integration/correlation-findings-before-seasonal-adjustment.test.ts": 8.6,
"tests/integration/correlations-rollup-swap.test.ts": 1,
"tests/integration/credential-enrollment-proof.test.ts": 8.5,
"tests/integration/csv-import-contextless-glucose.test.ts": 4.9,
"tests/integration/csv-reimport-idempotent.test.ts": 2.9,
"tests/integration/cumulative-pr-rederive-atomic.test.ts": 2.3,
"tests/integration/cycle-absorbed-restored.test.ts": 6.2,
"tests/integration/cycle-crosstab-day-aggregates.test.ts": 0.2,
"tests/integration/cycle-flow-opens-cycle.test.ts": 17.1,
"tests/integration/cycle-import-export.test.ts": 3.5,
"tests/integration/cycle-insights-dense-reads.test.ts": 0.9,
"tests/integration/cycle-insights-scoped-delegate.test.ts": 6.2,
"tests/integration/cycle-routes.test.ts": 24.6,
"tests/integration/cycle-start-round-trip.test.ts": 8.9,
"tests/integration/daily-digest-today-overview.test.ts": 3.5,
"tests/integration/daily-series-user-tz.test.ts": 9.1,
"tests/integration/dashboard-chart-overlay-concurrency.test.ts": 3,
"tests/integration/dashboard-glucose-untagged.test.ts": 3.5,
"tests/integration/dashboard-pulse-latest-day.test.ts": 0.5,
"tests/integration/dashboard-snapshot-pre-window-types.test.ts": 2.7,
"tests/integration/dashboard-summary-reads.test.ts": 22.4,
"tests/integration/dashboard-widgets-save.test.ts": 11.5,
"tests/integration/dawarich-context.test.ts": 15.1,
"tests/integration/day-aggregates-pulse-hourly.test.ts": 0.1,
"tests/integration/day-view.test.ts": 50.1,
"tests/integration/day-weight-unanalysed-scale.test.ts": 3,
"tests/integration/delegated-intake-notification.test.ts": 3.4,
"tests/integration/demo-mode-profile-write.test.ts": 3.3,
"tests/integration/dense-glucose-raw-window-series.test.ts": 0.2,
"tests/integration/dense-intraday-hourly-rebuild.test.ts": 7.4,
"tests/integration/dense-intraday-retention.test.ts": 17.9,
"tests/integration/dense-pulse-raw-window-reads.test.ts": 0.4,
"tests/integration/dense-read-bounded.test.ts": 0.9,
"tests/integration/dense-series-hrv-spo2.test.ts": 0.6,
"tests/integration/dense-targets-reads.test.ts": 0.6,
"tests/integration/device-register.test.ts": 8.9,
"tests/integration/doctor-report-dense-data.test.ts": 1.2,
"tests/integration/doctor-report-fixed-window.test.ts": 1.9,
"tests/integration/doctor-report-pulse-hourly-mean.test.ts": 0,
"tests/integration/doctor-report-sections.test.ts": 9.8,
"tests/integration/document-ai-runs.test.ts": 13.3,
"tests/integration/document-sources.test.ts": 41.2,
"tests/integration/document-thumbnail-backfill.test.ts": 3,
"tests/integration/documents-content-index.test.ts": 4.1,
"tests/integration/documents-import.test.ts": 12.4,
"tests/integration/documents-vault.test.ts": 21.1,
"tests/integration/dose-history-pre-creation-slots.test.ts": 4.4,
"tests/integration/drain-per-sample-cumulative.test.ts": 5.9,
"tests/integration/drop-plaintext-location-migration.test.ts": 0.1,
"tests/integration/ecg-ingest-roundtrip.test.ts": 11.7,
"tests/integration/ecg-recording-identity-migration.test.ts": 4.3,
"tests/integration/emergency-profile-migration.test.ts": 0.1,
"tests/integration/emergency-profile-route.test.ts": 2.9,
"tests/integration/encounter-appointment-reminder.test.ts": 44.9,
"tests/integration/encounter-phase2-surfaces.test.ts": 15.5,
"tests/integration/encounter-preparation.test.ts": 4.8,
"tests/integration/encounter-procedures.test.ts": 31,
"tests/integration/encounter-scoped-delegate.test.ts": 8.4,
"tests/integration/encryption-corpus-cursor-row-deleted.test.ts": 0.9,
"tests/integration/encryption-key-check.test.ts": 33.2,
"tests/integration/encryption-key-rotation-blobs.test.ts": 6.6,
"tests/integration/encryption-key-rotation-contact-and-route.test.ts": 0.9,
"tests/integration/environment-air-quality-history.test.ts": 17,
"tests/integration/environment-coarse-location.test.ts": 2.4,
"tests/integration/environment-location-encryption.test.ts": 6.5,
"tests/integration/environment-location-rotation.test.ts": 1,
"tests/integration/export-per-type.test.ts": 7.6,
"tests/integration/feature-blocks-pulse-day-mean.test.ts": 0,
"tests/integration/fhir-series-and-medication-effect.test.ts": 0.1,
"tests/integration/fitbit-activity-sleep-workout-sync.test.ts": 4.6,
"tests/integration/fitbit-metrics-sync.test.ts": 3.7,
"tests/integration/fitbit-retirement.test.ts": 2.1,
"tests/integration/fold-complete-windows.test.ts": 9,
"tests/integration/fold-horizon.test.ts": 5.5,
"tests/integration/fold-leftover-provenance.test.ts": 17.7,
"tests/integration/free-text-encryption-backfill.test.ts": 8.4,
"tests/integration/full-backup-nutrients.test.ts": 2.4,
"tests/integration/geo-backfill-attempt.test.ts": 1.3,
"tests/integration/google-health-backfill-multi-year.test.ts": 86,
"tests/integration/google-health-backfill-progress-bound.test.ts": 9.9,
"tests/integration/google-health-backfill-rate-limit.test.ts": 14.9,
"tests/integration/google-health-dense-backfill-memory.test.ts": 60.9,
"tests/integration/google-health-sleep-overlap.test.ts": 4.1,
"tests/integration/google-health-sync-progress.test.ts": 3,
"tests/integration/google-health-workout-terminal.test.ts": 1.8,
"tests/integration/graded-series-pulse-day-mean.test.ts": 0,
"tests/integration/guardian-notification-fanout.test.ts": 12.2,
"tests/integration/health-connect-import.test.ts": 66.6,
"tests/integration/health-profile-backup-roundtrip.test.ts": 2.8,
"tests/integration/health-score-configured-flag.test.ts": 9.2,
"tests/integration/health-score-labs-pillars.test.ts": 2.9,
"tests/integration/health-score-record.test.ts": 14.4,
"tests/integration/health-status-implausible-values.test.ts": 1.3,
"tests/integration/healthkit-type-sync-migration.test.ts": 0,
"tests/integration/healthkit-type-sync.test.ts": 9.6,
"tests/integration/heavy-lane-defer.test.ts": 10.1,
"tests/integration/hour-buckets-and-backfill.test.ts": 10.1,
"tests/integration/hrv-rmssd-fitbit-google-migration.test.ts": 0,
"tests/integration/hrv-rmssd-ingest.test.ts": 2.5,
"tests/integration/idempotency-body-fingerprint.test.ts": 6.4,
"tests/integration/idempotency-record-scoped-cell.test.ts": 3.1,
"tests/integration/idempotency-replay.test.ts": 7.3,
"tests/integration/index-hygiene.test.ts": 4.1,
"tests/integration/insight-status-cache-migration.test.ts": 1.9,
"tests/integration/insights-comprehensive-cache.test.ts": 4.7,
"tests/integration/insights-feedback.test.ts": 4.1,
"tests/integration/insights-provider-chain-put.test.ts": 2.2,
"tests/integration/insights-provider-chain.test.ts": 3.3,
"tests/integration/insights-recent-readings.test.ts": 1.6,
"tests/integration/insights-status-real-text.test.ts": 2.9,
"tests/integration/intake-backfill-missed-slot.test.ts": 2.8,
"tests/integration/intake-late-take-future-slot.test.ts": 2.7,
"tests/integration/intake-scheduled-for-targeting.test.ts": 2.3,
"tests/integration/intake-slot-dedup.test.ts": 6.8,
"tests/integration/integration-backfill-lane-expiry.test.ts": 21.4,
"tests/integration/integration-failing-legs-backfill.test.ts": 2.4,
"tests/integration/integration-status.test.ts": 8.4,
"tests/integration/integration-sync-escalation.test.ts": 22.4,
"tests/integration/integration-watchdog.test.ts": 12.5,
"tests/integration/inventory-expiry-container-type-repair.test.ts": 9.7,
"tests/integration/job-failures.test.ts": 0.3,
"tests/integration/job-success-outcome.test.ts": 0.5,
"tests/integration/lab-biomarker-backfill-budget.test.ts": 4.9,
"tests/integration/lab-source-reference-range.test.ts": 6.3,
"tests/integration/lab-unit-mismatch.test.ts": 14.9,
"tests/integration/labs-analyte-key.test.ts": 0.6,
"tests/integration/labs-overview-long-history.test.ts": 4.2,
"tests/integration/labs-unit-conversion.test.ts": 0.7,
"tests/integration/large-account-jobs.test.ts": 5.4,
"tests/integration/large-backup-round-trip.test.ts": 5.5,
"tests/integration/latest-canonical-read.test.ts": 0,
"tests/integration/life-circumstances.test.ts": 18.4,
"tests/integration/link-service.test.ts": 34.6,
"tests/integration/list-pagination-tiebreaker.test.ts": 4.1,
"tests/integration/local-day-keys.test.ts": 1.8,
"tests/integration/login-insecure-transport.test.ts": 6.5,
"tests/integration/managed-profile-edit.test.ts": 8.5,
"tests/integration/managed-profile-guardian-concurrency.test.ts": 1,
"tests/integration/managed-profile-handover-sso.test.ts": 18.2,
"tests/integration/managed-profile-handover-stepup-sso.test.ts": 4.5,
"tests/integration/managed-profile-handover.test.ts": 30.9,
"tests/integration/managed-profile-lifecycle.test.ts": 32.9,
"tests/integration/managed-profile-oauth-connect.test.ts": 7.4,
"tests/integration/managed-profile-passkey-step-up.test.ts": 2.5,
"tests/integration/managed-profile-surface.test.ts": 55.2,
"tests/integration/managed-record-capabilities.test.ts": 3.3,
"tests/integration/managed-record-modules.test.ts": 11.2,
"tests/integration/manual-workout-edit.test.ts": 2.9,
"tests/integration/mcp-metric-series-source-snapshot.test.ts": 3.4,
"tests/integration/mcp-oauth-refresh-race.test.ts": 1.9,
"tests/integration/mcp-output-schema-conformance.test.ts": 1,
"tests/integration/mcp-write-and-range-read.test.ts": 8.7,
"tests/integration/measurement-bulk-insert.test.ts": 6.6,
"tests/integration/measurement-hot-update.test.ts": 4.1,
"tests/integration/measurement-identity-reconcile.test.ts": 5.4,
"tests/integration/measurement-rollups.test.ts": 16.6,
"tests/integration/measurement-soft-delete.test.ts": 4.7,
"tests/integration/measurement-source-write-smoke.test.ts": 1.4,
"tests/integration/measurements-aggregate-daily.test.ts": 4.8,
"tests/integration/measurements-batch-delete.test.ts": 5,
"tests/integration/measurements-batch-perf.test.ts": 4.2,
"tests/integration/measurements-batch-set-insert-golden.test.ts": 1.6,
"tests/integration/measurements-batch.test.ts": 25.5,
"tests/integration/measurements-day-grouped-paging.test.ts": 4.2,
"tests/integration/measurements-duplicate-conflict.test.ts": 10.3,
"tests/integration/measurements-named-sleep-stage.test.ts": 8.5,
"tests/integration/measurements-sleep-list.test.ts": 12,
"tests/integration/medication-administrations.test.ts": 1,
"tests/integration/medication-auto-mark-taken.test.ts": 9.2,
"tests/integration/medication-cadence-worker.test.ts": 10.3,
"tests/integration/medication-courses.test.ts": 6,
"tests/integration/medication-custom-categories.test.ts": 9.2,
"tests/integration/medication-dose-history-import.test.ts": 21,
"tests/integration/medication-extra-dose-apple-health.test.ts": 6.2,
"tests/integration/medication-intake-import-job.test.ts": 1.6,
"tests/integration/medication-intake-import-reported-export.test.ts": 3.1,
"tests/integration/medication-intake-tracking.test.ts": 8.4,
"tests/integration/medication-per-slot-consumption.test.ts": 1,
"tests/integration/medication-put-course-race.test.ts": 1.5,
"tests/integration/medication-reminder-cross-midnight.test.ts": 1.9,
"tests/integration/medication-rolling-overdue-consumers.test.ts": 4.8,
"tests/integration/medication-weekly-compliance.test.ts": 2.4,
"tests/integration/medication-write-idempotency.test.ts": 4.4,
"tests/integration/medications-intake-bulk.test.ts": 9.7,
"tests/integration/mfa-login-flow.test.ts": 3.6,
"tests/integration/migrate-deploy-lock-retry.test.ts": 10,
"tests/integration/migrate-deploy-utc.test.ts": 0.9,
"tests/integration/migration-0430-insights-stamp.test.ts": 3.8,
"tests/integration/mood-a1-backfill.test.ts": 3.2,
"tests/integration/mood-backup-round-trip.test.ts": 12.7,
"tests/integration/mood-context-linkage.test.ts": 18.4,
"tests/integration/mood-context.test.ts": 13.2,
"tests/integration/mood-dimension-features.test.ts": 5.9,
"tests/integration/mood-entries-bulk.test.ts": 6.7,
"tests/integration/mood-entries-external-id.test.ts": 5.3,
"tests/integration/mood-entries-note-column.test.ts": 3.5,
"tests/integration/mood-insights-crosstab.test.ts": 1.9,
"tests/integration/mood-patterns.test.ts": 2.3,
"tests/integration/mood-prognosis-ladder.test.ts": 23,
"tests/integration/mood-tag-groups.test.ts": 7.7,
"tests/integration/mood-writers-agree.test.ts": 7.5,
"tests/integration/note-encryption-backfill.test.ts": 2,
"tests/integration/notification-action-mood.test.ts": 6.2,
"tests/integration/notification-attribution.test.ts": 21.5,
"tests/integration/notification-event-defaults.test.ts": 2.9,
"tests/integration/notification-global-channel-switch.test.ts": 3.1,
"tests/integration/nutrients-batch.test.ts": 9.4,
"tests/integration/oauth-callback-single-use.test.ts": 5.6,
"tests/integration/offhost-backup-ledger.test.ts": 5.6,
"tests/integration/offhost-backup-streaming-memory.test.ts": 3.7,
"tests/integration/offhost-purge-on-deletion.test.ts": 4.9,
"tests/integration/oidc-pending-link.test.ts": 8.4,
"tests/integration/onboarding-needs.test.ts": 20.3,
"tests/integration/openapi-get-contract.test.ts": 5.7,
"tests/integration/openapi-response-shape.test.ts": 3,
"tests/integration/oura-backfill.test.ts": 31.6,
"tests/integration/oura-refresh-serialization.test.ts": 2,
"tests/integration/oura-resilience-recovery.test.ts": 2.4,
"tests/integration/oura-retired-sleep.test.ts": 1.5,
"tests/integration/oura-scope-denied.test.ts": 1.2,
"tests/integration/partial-unique-live-rows.test.ts": 1.2,
"tests/integration/passkey-register.test.ts": 13.7,
"tests/integration/pending-migrations-notice.test.ts": 0.4,
"tests/integration/pr-detection-cumulative-paged.test.ts": 2.9,
"tests/integration/pr-detection-end-to-end.test.ts": 4,
"tests/integration/practitioner-contact-encryption.test.ts": 6.3,
"tests/integration/practitioner-specialty-search.test.ts": 2.3,
"tests/integration/prisma-schema-name-drift.test.ts": 2.6,
"tests/integration/profile-display-name.test.ts": 4.3,
"tests/integration/provider-pause.test.ts": 6.6,
"tests/integration/provider-token-refresh-serialization.test.ts": 6.2,
"tests/integration/pulse-comparison-availability.test.ts": 0,
"tests/integration/pulse-day-mean-method.test.ts": 25.3,
"tests/integration/pulse-day-mean-zones.test.ts": 0.4,
"tests/integration/pulse-hourly-mean-day.test.ts": 0.5,
"tests/integration/pulse-hourly-mean-migration.test.ts": 0,
"tests/integration/pulse-window-mean-readers.test.ts": 0.4,
"tests/integration/queue-singleton-policy.integration.test.ts": 0.3,
"tests/integration/rate-limit-429-headers.test.ts": 4.1,
"tests/integration/rate-limit.test.ts": 2,
"tests/integration/recent-proof.test.ts": 4.9,
"tests/integration/record-csv-import.test.ts": 4,
"tests/integration/record-search.test.ts": 9.2,
"tests/integration/record-session-epoch-trigger.test.ts": 7.4,
"tests/integration/record-session-fence-idempotency.test.ts": 3.1,
"tests/integration/record-session-fence.test.ts": 13.5,
"tests/integration/record-settings-authorization.test.ts": 0,
"tests/integration/record-settings-integrations-health.test.ts": 1.7,
"tests/integration/refresh-rotation-access-sunset.test.ts": 3.3,
"tests/integration/refresh-rotation-revoke-race.test.ts": 2.4,
"tests/integration/relation-links-portable-restore.test.ts": 2.3,
"tests/integration/relation-links.test.ts": 15.5,
"tests/integration/reset-password-revocation.test.ts": 3,
"tests/integration/rest-mode-chronic.test.ts": 15.6,
"tests/integration/restore-consolidation-deadlock.test.ts": 5.1,
"tests/integration/retired-index-drop.test.ts": 2.5,
"tests/integration/rollup-concurrent-recompute.test.ts": 2.4,
"tests/integration/rollup-coverage-probe.test.ts": 1.2,
"tests/integration/rollup-keyed-refold-session-tz.test.ts": 2.1,
"tests/integration/rollup-per-day-canonical.test.ts": 6.6,
"tests/integration/rollup-regression-parity.integration.test.ts": 3.1,
"tests/integration/score-history.test.ts": 7.8,
"tests/integration/second-factor-ends-other-sessions.test.ts": 3.1,
"tests/integration/series-source-ladder.test.ts": 0.6,
"tests/integration/server-cache-routes.test.ts": 3.8,
"tests/integration/session-cookie-validation.test.ts": 4.2,
"tests/integration/session-time-zone.test.ts": 6.9,
"tests/integration/sessions-and-known-devices.test.ts": 9.3,
"tests/integration/settings-account-delete.test.ts": 10.7,
"tests/integration/settings-data-wipe.test.ts": 4.3,
"tests/integration/settings-erasure-bearer-step-up.test.ts": 8.3,
"tests/integration/settings-prefs-routes.test.ts": 6.7,
"tests/integration/share-link-documents.test.ts": 3,
"tests/integration/sharing-account-access.test.ts": 21.3,
"tests/integration/sharing-audit-actor.test.ts": 9.9,
"tests/integration/sharing-delegable-routes.test.ts": 203.5,
"tests/integration/sharing-delegable-writes.test.ts": 116.8,
"tests/integration/sharing-front-door.test.ts": 33.5,
"tests/integration/sharing-idempotency-revocation.test.ts": 8.6,
"tests/integration/sharing-import-attribution.test.ts": 2.1,
"tests/integration/sharing-lifecycle.test.ts": 36.4,
"tests/integration/sharing-manage-handler-matrix.test.ts": 424.4,
"tests/integration/sharing-provider-origin.test.ts": 7.1,
"tests/integration/sharing-scope-fence.test.ts": 65.9,
"tests/integration/sharing-scope-routes.test.ts": 1.1,
"tests/integration/sleep-entry-hours-round-trip.test.ts": 4.6,
"tests/integration/sleep-night-sql-parity.test.ts": 10.1,
"tests/integration/soft-delete-copy-parity.test.ts": 2.3,
"tests/integration/soft-delete-purge.test.ts": 9.1,
"tests/integration/source-aware-rollups.integration.test.ts": 9.4,
"tests/integration/source-ladder-new-sources.test.ts": 0,
"tests/integration/source-priority-two-axis.test.ts": 3.5,
"tests/integration/source-provenance.test.ts": 0.1,
"tests/integration/sources-in-use.test.ts": 2.4,
"tests/integration/step-consolidation-repair.test.ts": 3.3,
"tests/integration/step-up-elevation.test.ts": 57,
"tests/integration/summaries-pre-window-buckets.test.ts": 3.3,
"tests/integration/supplement-nutrients.test.ts": 19.8,
"tests/integration/switched-session-destructive-refusal.test.ts": 2.9,
"tests/integration/symptoms.test.ts": 16.3,
"tests/integration/sync-changes.test.ts": 10.1,
"tests/integration/sync-reset-after-restore.test.ts": 3,
"tests/integration/sync-state.test.ts": 3.4,
"tests/integration/tiered-context-local-day.test.ts": 0,
"tests/integration/timeline-rollup-boundary-zone.test.ts": 1.8,
"tests/integration/timeline-sleep-nights.test.ts": 2.7,
"tests/integration/timezone-per-user.test.ts": 12.9,
"tests/integration/tombstone-cleanup-restore-lock.test.ts": 5.6,
"tests/integration/user-avatar.test.ts": 1.4,
"tests/integration/vaccination-booster-mint.test.ts": 3.3,
"tests/integration/vaccination-booster-satisfy.test.ts": 10.7,
"tests/integration/vaccination-booster-scoped-delegate.test.ts": 4.7,
"tests/integration/vaccination-custom.test.ts": 7.9,
"tests/integration/vaccination-managed-profile.test.ts": 2.8,
"tests/integration/vaccination-renewal.test.ts": 5.2,
"tests/integration/visit-link-picker-long-history.test.ts": 5.1,
"tests/integration/visit-link-picker-period-search.test.ts": 9.9,
"tests/integration/webauthn-challenge-claim.test.ts": 5.3,
"tests/integration/whoop-connect-ticket.test.ts": 5.5,
"tests/integration/whoop-owner-identity.test.ts": 1.8,
"tests/integration/withings-activity-sync.test.ts": 3.1,
"tests/integration/withings-api-plan.test.ts": 4.2,
"tests/integration/withings-oauth-flow.test.ts": 5.9,
"tests/integration/withings-oauth.test.ts": 4.8,
"tests/integration/withings-sleep-stage-composite.test.ts": 4.6,
"tests/integration/withings-sleep-sync.test.ts": 2.2,
"tests/integration/withings-sync-volume.test.ts": 2.4,
"tests/integration/workout-batch-create.test.ts": 18.5,
"tests/integration/workout-batch-enrich.test.ts": 7.1,
"tests/integration/workout-batch-race.test.ts": 2,
"tests/integration/workout-detail-hr-series.test.ts": 5.2,
"tests/integration/workout-insight-claim.test.ts": 14,
"tests/integration/workout-manual-entry.test.ts": 16.3,
"tests/integration/workout-routes-opt-in-migration.test.ts": 3.2,
"tests/integration/write-meter.test.ts": 5.8
}
}
+156
View File
@@ -0,0 +1,156 @@
/**
* Shard and order test files by how long they take, not by how many there are.
*
* Vitest's own `--shard` cuts the sorted file list into slices of equal
* length. With files ranging from a tenth of a second to several minutes,
* equal counts gave the integration shards 954 s to 1 449 s: the run waited
* on the slowest slice while the fastest sat idle for eight minutes.
*
* This sequencer reads the measured duration of every file from a committed
* timing file (`tests/durations/<suite>.json`, regenerated with
* `node scripts/test-durations.mjs`) and deals the files out longest first,
* each to the shard with the least work so far (the classic longest-
* processing-time rule, within 4/3 of the best possible split). Every shard
* job computes the same partition independently, because the input is the
* same commit. A file with no measurement yet counts as the median, so a new
* file lands somewhere sensible and the next regeneration fixes it.
*
* Inside a shard, files start longest first, so the slow ones are not left
* for the end of a parallel run. `--sequence.shuffle` replaces that order
* with a seeded shuffle; that is how the nightly run looks for order
* dependence.
*/
import { readFileSync } from "node:fs";
import { relative, resolve } from "node:path";
import { BaseSequencer, type TestSpecification } from "vitest/node";
export interface DurationFile {
/** What produced the numbers, for whoever wonders whether they are stale. */
source: string;
/** Seconds per file, keyed by the path relative to the repository root. */
files: Record<string, number>;
}
export function readDurations(path: string): Map<string, number> {
try {
const parsed = JSON.parse(readFileSync(path, "utf8")) as DurationFile;
return new Map(Object.entries(parsed.files));
} catch {
return new Map();
}
}
function median(values: number[]): number {
if (values.length === 0) return 1;
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.floor(sorted.length / 2)];
}
/**
* Split `items` into `count` groups of near-equal total weight. Pure, so the
* partition can be checked without a Vitest run.
*/
export function partitionByDuration<T>(
items: readonly T[],
count: number,
weight: (item: T) => number,
key: (item: T) => string,
): T[][] {
const groups: { total: number; items: T[] }[] = Array.from(
{ length: count },
() => ({ total: 0, items: [] }),
);
const ordered = [...items].sort(
(a, b) => weight(b) - weight(a) || key(a).localeCompare(key(b)),
);
for (const item of ordered) {
let target = groups[0];
for (const group of groups) {
if (group.total < target.total) target = group;
}
target.items.push(item);
target.total += weight(item);
}
return groups.map((g) => g.items);
}
/**
* `perFileSeconds` is what a file costs beyond its own reported duration:
* the worker importing the module graph and running the setup file, which
* the JSON report does not count. It dominates the unit suite (most files
* finish in milliseconds and spend half a second importing), so without it
* the split would hand one shard the thousands of quick files. Measured as
* (wall time × workers − the sum of file durations) / files.
*/
export function createDurationSequencer(
durationFile: string,
perFileSeconds: number,
) {
return class DurationSequencer extends BaseSequencer {
private durations: Map<string, number> | null = null;
private weightOf(spec: TestSpecification): number {
if (!this.durations) {
this.durations = readDurations(
resolve(this.ctx.config.root, durationFile),
);
}
const known = this.durations.get(this.pathOf(spec));
return (known ?? median([...this.durations.values()])) + perFileSeconds;
}
private pathOf(spec: TestSpecification): string {
return relative(this.ctx.config.root, spec.moduleId)
.split("\\")
.join("/");
}
override async shard(
files: TestSpecification[],
): Promise<TestSpecification[]> {
const { index, count } = this.ctx.config.shard ?? { index: 1, count: 1 };
const groups = partitionByDuration(
files,
count,
(spec) => this.weightOf(spec),
(spec) => `${spec.project.name}:${this.pathOf(spec)}`,
);
return groups[index - 1] ?? [];
}
override async sort(
files: TestSpecification[],
): Promise<TestSpecification[]> {
const { sequence } = this.ctx.config;
if (sequence.shuffle) return seededShuffle(files, sequence.seed);
return [...files].sort((a, b) => {
if (a.project.name !== b.project.name) {
return a.project.name < b.project.name ? -1 : 1;
}
return (
this.weightOf(b) - this.weightOf(a) ||
this.pathOf(a).localeCompare(this.pathOf(b))
);
});
}
};
}
/** Fisher-Yates driven by a small deterministic generator (mulberry32). */
function seededShuffle<T>(items: readonly T[], seed: number): T[] {
let state = seed >>> 0;
const next = () => {
state = (state + 0x6d2b79f5) >>> 0;
let t = state;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
const out = [...items];
for (let i = out.length - 1; i > 0; i--) {
const j = Math.floor(next() * (i + 1));
[out[i], out[j]] = [out[j], out[i]];
}
return out;
}
File diff suppressed because it is too large. Load diff
+64
View File
@@ -0,0 +1,64 @@
/**
* Which unit-test files can share a module graph, decided from their source.
*
* Vitest isolates every test file by default: each one re-imports the
* application modules it touches. In this suite that import is most of the
* run (63 % of the 869 s CI wall time before the split), and most files do
* not need it, because they neither replace a module nor change process
* state. Those files run in projects with `isolate: false`, where a worker
* keeps its module cache from one file to the next; everything else keeps
* full isolation.
*
* A file needs isolation when its source does anything that outlives the
* file in a shared graph: mocks or spies on a module, stubbed env or
* globals, fake timers, a module reset, an assignment to `process.env` or a
* global, or a `__reset…ForTests` hook. The match is deliberately broad; a
* false positive costs a little speed, a false negative costs a flaky
* neighbour. A file that turns out to need isolation for a reason this list
* misses says so with a `// vitest-isolate` comment.
*/
import { readFileSync } from "node:fs";
const NEEDS_ISOLATION = new RegExp(
[
String.raw`\bvi\s*\.\s*(?:mock|doMock|unmock|doUnmock|hoisted|stubEnv|stubGlobal|unstubAllEnvs|unstubAllGlobals|spyOn|useFakeTimers|setSystemTime|resetModules|importMock)\b`,
String.raw`\bprocess\.env(?:\.\w+|\[[^\]]+\])\s*=(?!=)`,
String.raw`\bdelete\s+process\.env`,
String.raw`\b(?:globalThis|global|window)(?:\.\w+|\[[^\]]+\])\s*=(?!=)`,
String.raw`Object\.defineProperty\(\s*(?:globalThis|global|window|process)`,
String.raw`__\w*[Rr]eset\w*ForTests`,
String.raw`process\.chdir\(`,
String.raw`vitest-isolate`,
].join("|"),
);
export function needsIsolation(source: string): boolean {
return NEEDS_ISOLATION.test(source);
}
export interface UnitProjectFiles {
/** Structural guards that share a module graph. */
guards: string[];
/** Behavioural unit and component tests that share a module graph. */
shared: string[];
/** Everything that mocks, stubs or resets; one fresh graph per file. */
isolated: string[];
}
const GUARD_LOCATIONS =
/^(?:src\/__tests__\/|scripts\/__tests__\/|eslint-plugins\/|tests\/)/;
/** Split `files` (paths relative to `root`) into the three projects. */
export function classifyUnitFiles(
root: string,
files: readonly string[],
): UnitProjectFiles {
const result: UnitProjectFiles = { guards: [], shared: [], isolated: [] };
for (const file of files) {
const source = readFileSync(`${root}/${file}`, "utf8");
if (needsIsolation(source)) result.isolated.push(file);
else if (GUARD_LOCATIONS.test(file)) result.guards.push(file);
else result.shared.push(file);
}
return result;
}
+47
View File
@@ -1,10 +1,38 @@
import { defineConfig } from "vitest/config";
import { globSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createDurationSequencer } from "./tests/durations/sequencer";
import { classifyUnitFiles } from "./tests/unit-projects";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Three projects, decided per file from its source (`tests/unit-projects.ts`):
* `guards` and `unit` share a module graph inside a worker (`isolate:
* false`), because their files neither mock nor stub anything; `isolated`
* keeps one fresh graph per file for the ones that do. Run one with
* `pnpm test --project guards`.
*/
const projectFiles = classifyUnitFiles(
__dirname,
globSync(
[
"src/**/*.test.{ts,tsx}",
"scripts/__tests__/**/*.test.ts",
"eslint-plugins/**/__tests__/**/*.test.mjs",
"tests/**/*.test.ts",
],
{
cwd: __dirname,
exclude: (path) =>
path.startsWith("tests/integration") || path.includes("node_modules"),
},
).sort(),
);
export default defineConfig({
test: {
globals: true,
@@ -22,6 +50,11 @@ export default defineConfig({
// locale synchronously (production gets the active bundle as an RSC
// prop instead — see src/lib/i18n/load-locale.ts).
setupFiles: ["./vitest.setup.ts"],
// `--shard` splits by measured duration rather than file count, and files
// start longest first. See `tests/durations/sequencer.ts`.
sequence: {
sequencer: createDurationSequencer("tests/durations/unit.json", 0.4),
},
// Default `pnpm test` keeps unit tests only — integration suite uses
// testcontainers and runs separately via `pnpm test:integration`.
exclude: [
@@ -41,6 +74,20 @@ export default defineConfig({
".claude/worktrees/**",
".worktrees/**",
],
projects: [
{
extends: true,
test: { name: "guards", include: projectFiles.guards, isolate: false },
},
{
extends: true,
test: { name: "unit", include: projectFiles.shared, isolate: false },
},
{
extends: true,
test: { name: "isolated", include: projectFiles.isolated },
},
],
coverage: {
provider: "v8",
// `include` enumerates the full source tree so the report shows
+13 -2
View File
@@ -3,6 +3,8 @@ import { dirname, resolve } from "node:path";
import { availableParallelism } from "node:os";
import { fileURLToPath } from "node:url";
import { createDurationSequencer } from "./tests/durations/sequencer";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -49,8 +51,17 @@ export default defineConfig({
pool: "forks",
fileParallelism: true,
maxWorkers: integrationWorkers(),
// Order dependence is looked for on purpose by shuffling file order
// (`--sequence.shuffle`), not by keeping the suite serial.
// `--shard` deals files out by measured duration (CI runs four shards), and
// files start longest first so a parallel run does not end on one slow
// file. Order dependence is surfaced on purpose by the nightly shuffled
// run (`.github/workflows/test-nightly.yml`), which passes
// `--sequence.shuffle` and prints the seed it used.
sequence: {
sequencer: createDurationSequencer(
"tests/durations/integration.json",
0.6,
),
},
// v1.4.37 — switched from `isolate: false` to `isolate: true`.
// The previous setting reused the module graph across files inside
// one fork; mock declarations (`vi.mock("@/lib/notifications/...")`,