mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
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:
1 parent
28f9b1948b
commit
e9f89bbebf
10 files changed
+3736
-47
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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/...")`,
|
||||
|
||||
Reference in new issue
Block a user