diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 000000000..40c5739e6 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,33 @@ +name: Coverage + +# Measures coverage for every package on main. The numbers themselves are not +# asserted here — the point is to populate the Nx Cloud cache, so that when a +# PR re-measures its merge-base the `test:coverage` task hashes already exist +# remotely and the summaries are restored instead of recomputed. +on: + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + +permissions: + contents: read + +jobs: + coverage: + name: Warm Coverage Cache + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Setup Tools + uses: TanStack/config/.github/setup@190f659075ff0845850e330883eb26d7ffd0671f # main + - name: Run Coverage + run: pnpm run test:coverage:all diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a83eb33a8..265e2136b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -31,6 +31,78 @@ jobs: main-branch-name: main - name: Run Checks run: pnpm run test:pr + coverage: + name: Coverage + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + - name: Setup Tools + uses: TanStack/config/.github/setup@190f659075ff0845850e330883eb26d7ffd0671f # main + - name: Get base and head commits for `nx affected` + uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4.4.0 + with: + main-branch-name: main + # Stashed outside the working tree because checking out the merge base + # would otherwise remove it — the script may be added or changed by the + # very PR under test. Both runs and the comparison use this copy, so all + # three agree on the version. + - name: Stage the coverage script + run: cp scripts/coverage-check.mjs "$RUNNER_TEMP/coverage-check.mjs" + # The set of packages to measure is decided once, here, so that the base + # run measures exactly what the head run does. `--with-target` keeps out + # projects that have no test:coverage script, so a docs-only PR resolves + # to an empty list rather than erroring in run-many. + - name: Resolve affected packages + id: affected + run: | + list=$(pnpm exec nx show projects --affected --with-target=test:coverage --exclude='examples/**,testing/**' --json | jq -r 'sort | join(",")') + echo "list=$list" >> "$GITHUB_OUTPUT" + echo "Measuring: ${list:-}" + - name: Coverage for this PR + if: steps.affected.outputs.list != '' + # Via env, not `${{ }}` inside run: project names come from the PR's own + # files, so interpolating them into the shell would be injectable. + env: + PROJECTS: ${{ steps.affected.outputs.list }} + run: | + rm -rf packages/*/coverage + pnpm exec nx run-many --targets=test:coverage --projects="$PROJECTS" + node "$RUNNER_TEMP/coverage-check.mjs" --collect "$RUNNER_TEMP/cov-head" + # Re-measuring the merge-base is normally a set of Nx Cloud cache + # restores seeded by the Coverage workflow on main: the task hashes are + # identical, and `test:coverage` declares its coverage dir as an output, + # so the summaries come back without the tests re-running. + - name: Coverage for the merge base + if: steps.affected.outputs.list != '' + env: + PROJECTS: ${{ steps.affected.outputs.list }} + run: | + git checkout --force --detach "$NX_BASE" + # coverage/ is gitignored, so the checkout leaves this PR's results in + # place — without this the base collection silently re-reads head's + # numbers and every delta comes out as 0.00. + rm -rf packages/*/coverage + pnpm install --frozen-lockfile + # Packages added by this PR do not exist on the base commit. + printf '%s\n' "$PROJECTS" | tr ',' '\n' | sort > /tmp/wanted + pnpm exec nx show projects --with-target=test:coverage --json | jq -r '.[]' | sort > /tmp/available + base_list=$(comm -12 /tmp/wanted /tmp/available | paste -sd, -) + if [ -z "$base_list" ]; then + echo "No affected package exists on the base commit; nothing to compare against." + exit 0 + fi + pnpm exec nx run-many --targets=test:coverage --projects="$base_list" + node "$RUNNER_TEMP/coverage-check.mjs" --collect "$RUNNER_TEMP/cov-base" + - name: Compare + if: steps.affected.outputs.list != '' + run: | + node "$RUNNER_TEMP/coverage-check.mjs" \ + --base "$RUNNER_TEMP/cov-base" \ + --head "$RUNNER_TEMP/cov-head" preview: name: Preview runs-on: ubuntu-latest diff --git a/CLAUDE.md b/CLAUDE.md index 4a34c8978..955c8f666 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,7 +42,7 @@ pnpm test:lib:dev # Watch mode for unit tests pnpm test:oxlint # Lint affected packages (oxlint, incl. type-aware) pnpm test:types # Type check affected packages pnpm test:build # Verify build artifacts with publint -pnpm test:coverage # Generate coverage reports +# Coverage is CI-only — see the Coverage section below. Don't run it locally. pnpm test:knip # Check for unused dependencies pnpm test:sherif # Check pnpm workspace consistency pnpm test:docs # Verify documentation links @@ -306,7 +306,16 @@ Each package uses `exports` field in package.json for subpath exports (e.g., `@t - Unit tests in `*.test.ts` files alongside source - Uses Vitest with happy-dom for DOM testing -- Test coverage via `pnpm test:coverage` +- **Coverage is CI-only.** Don't run it locally and don't add it to local + gates — it is deliberately absent from `test`, `test:pr`, `test:ci` and the + git hooks. The `Coverage` job on each PR measures every affected package + twice, on the PR head and on its merge-base with `main`, and fails when a + metric drops more than 0.5pp between them. There is **no baseline file** — + don't reintroduce one, it was removed precisely because it needed manual + syncing and was platform-sensitive. The only remedy for a drop is tests. + `.github/workflows/coverage.yml` runs coverage on pushes to `main` purely to + warm the Nx Cloud cache so the base-side run is mostly cache restores. See + CONTRIBUTING.md. - **E2E tests are mandatory** — see E2E Testing section below ### E2E Testing (REQUIRED) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 021698cae..609125b2c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,7 +68,7 @@ All commands are run from the repo root. Nx handles affected detection and cachi | Run unit tests (affected) | `pnpm test:lib` | | Watch unit tests | `pnpm test:lib:dev` | | Type-check (affected) | `pnpm test:types` | -| Lint (affected) | `pnpm test:eslint` | +| Lint (affected) | `pnpm test:oxlint` | | Verify build artifacts | `pnpm test:build` | | Format the repo | `pnpm format` | | Build (affected) | `pnpm build` | @@ -80,6 +80,29 @@ All commands are run from the repo root. Nx handles affected detection and cachi Working on a single package? `cd packages/` and use its scripts directly (`pnpm test:lib`, `pnpm test:types`, etc.). +## Coverage + +**Coverage runs in CI only. It is not part of `pnpm test`, `pnpm test:pr`, or any git hook, and you are not expected to run it locally.** + +The `Coverage` job on every PR measures each affected package **twice** — once on your branch and once on its merge-base with `main` — and compares the two. A drop of more than 0.5 percentage points in any metric (statements, branches, functions, lines) fails the job. Packages your PR didn't affect are never measured. + +There is no baseline file to keep in sync, and nothing to update when a package is added or removed: both numbers come from the same job on the same runner. There are also no target percentages to hit — the gate only catches coverage getting _worse_ in what you touched, and never blocks a PR for being below some repo-wide bar. + +Read the numbers from the PR's Checks tab: open the `Coverage` job and its summary has a per-package table with deltas, on every run whether it passed or failed. It is not posted as a PR comment. + +Re-measuring the merge-base is usually close to free. A separate `Coverage` workflow runs `test:coverage:all` on every push to `main`, which populates the Nx Cloud cache; because `test:coverage` declares its `coverage/` directory as a task output, the base-side run generally restores cached summaries rather than re-running any tests. + +### If the job says coverage dropped + +Add tests covering the code you changed. That's the whole remedy — there is no number to override. If you genuinely deleted well-tested code and the drop is expected, say so in the PR and a maintainer can merge past the failing check. + +Two known limitations: + +- Uncovered `.tsx` files can't be remapped by the coverage provider and are dropped from the report with a `Failed to parse ... Excluding it from coverage` warning. `.tsx` files that tests _do_ load are measured normally, so the UI packages read higher than their real coverage. +- `preact-ai-devtools`, `react-ai-devtools`, and `solid-ai-devtools` have no tests and sit at 0%. + +A package your PR adds shows as `new` and can't fail the job — there is no base-commit coverage to compare it against. The same is true for a package whose suite can't run on the base commit. + ## TypeScript configuration There is a single `tsconfig.base.json` at the repo root with the shared `compilerOptions`. Every package extends it and overrides only what's unique to that package (e.g. `outDir`, JSX runtime, framework lib). @@ -104,7 +127,7 @@ Tests are included in typecheck. `vite.config.ts` / `vitest.config.ts` are not - Place tests under `packages//tests/` with the suffix `.test.ts` (or `.test.tsx` for JSX). - Vitest's defaults discover anything matching `**/*.{test,spec}.?(c|m)[jt]s?(x)` — no per-package config is needed. -- Tests are typechecked by `tsc` and linted by ESLint. +- Tests are typechecked by `tsc` and linted by oxlint. ## Adding E2E test coverage (required) @@ -146,7 +169,7 @@ The defensive `ignore` list in `.changeset/config.json` blocks accidental public ## Pull request flow 1. Push your branch and open a PR against `main`. -2. CI runs: `pnpm test:pr` (sherif workspace check, knip dead-code, docs link verification, ESLint, unit tests, typecheck, build artifacts, build) + the full E2E suite. +2. CI runs: `pnpm test:pr` (sherif workspace check, knip dead-code, docs link verification, oxlint, unit tests, typecheck, build artifacts, build), the `Coverage` regression gate, and the full E2E suite. 3. Address review comments. 4. A maintainer merges. Releases are cut via Changesets — your changeset entry lands in the next release. diff --git a/package.json b/package.json index 446793d8c..d9cb2e643 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "test:lib": "nx affected --targets=test:lib --exclude=examples/**,testing/**", "test:lib:dev": "pnpm test:lib && nx watch --all -- pnpm test:lib", "test:coverage": "nx affected --targets=test:coverage --exclude=examples/**,testing/**", + "test:coverage:all": "nx run-many --targets=test:coverage --exclude=examples/**,testing/**", "test:build": "nx affected --target=test:build --exclude=examples/**,testing/**", "test:types": "nx affected --targets=test:types --exclude=examples/**,testing/**", "test:knip": "knip", diff --git a/packages/ai-acp/package.json b/packages/ai-acp/package.json index 9da225b75..881aab573 100644 --- a/packages/ai-acp/package.json +++ b/packages/ai-acp/package.json @@ -38,6 +38,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run --testTimeout=30000", "test:lib:dev": "vitest --watch", "test:types": "tsc" diff --git a/packages/ai-angular/package.json b/packages/ai-angular/package.json index 0ae199a25..ef7b66c3d 100644 --- a/packages/ai-angular/package.json +++ b/packages/ai-angular/package.json @@ -46,6 +46,7 @@ "scripts": { "clean": "premove ./build ./dist", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "vitest", "test:types": "tsc", diff --git a/packages/ai-anthropic/package.json b/packages/ai-anthropic/package.json index 84a66285a..f3787c0e4 100644 --- a/packages/ai-anthropic/package.json +++ b/packages/ai-anthropic/package.json @@ -53,6 +53,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-bedrock/package.json b/packages/ai-bedrock/package.json index ad929a557..1eb8be9cf 100644 --- a/packages/ai-bedrock/package.json +++ b/packages/ai-bedrock/package.json @@ -28,6 +28,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-claude-code/package.json b/packages/ai-claude-code/package.json index feed08af8..b97ccffd8 100644 --- a/packages/ai-claude-code/package.json +++ b/packages/ai-claude-code/package.json @@ -42,6 +42,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-client/package.json b/packages/ai-client/package.json index 81acd1427..7df06aa8c 100644 --- a/packages/ai-client/package.json +++ b/packages/ai-client/package.json @@ -53,9 +53,9 @@ "clean": "premove ./build ./dist", "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", - "test:coverage": "vitest run --coverage", - "test:coverage:watch": "vitest --coverage --watch", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", + "test:coverage:watch": "vitest --coverage --watch", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-code-mode-snippets/package.json b/packages/ai-code-mode-snippets/package.json index 727153954..c113bdd1d 100644 --- a/packages/ai-code-mode-snippets/package.json +++ b/packages/ai-code-mode-snippets/package.json @@ -44,6 +44,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc", diff --git a/packages/ai-code-mode/package.json b/packages/ai-code-mode/package.json index ca4572c8d..c46505ec0 100644 --- a/packages/ai-code-mode/package.json +++ b/packages/ai-code-mode/package.json @@ -41,6 +41,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-codex/package.json b/packages/ai-codex/package.json index ac07eccb1..3cdb0c690 100644 --- a/packages/ai-codex/package.json +++ b/packages/ai-codex/package.json @@ -41,6 +41,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-devtools/package.json b/packages/ai-devtools/package.json index 87760494c..59c2242b2 100644 --- a/packages/ai-devtools/package.json +++ b/packages/ai-devtools/package.json @@ -73,6 +73,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-durable-stream/package.json b/packages/ai-durable-stream/package.json index 69f2161b8..95ba04d60 100644 --- a/packages/ai-durable-stream/package.json +++ b/packages/ai-durable-stream/package.json @@ -36,6 +36,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-elevenlabs/package.json b/packages/ai-elevenlabs/package.json index 437447b9e..20b10cc20 100644 --- a/packages/ai-elevenlabs/package.json +++ b/packages/ai-elevenlabs/package.json @@ -53,6 +53,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-event-client/package.json b/packages/ai-event-client/package.json index ae1c70b01..df8ebfbbb 100644 --- a/packages/ai-event-client/package.json +++ b/packages/ai-event-client/package.json @@ -37,6 +37,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-fal/package.json b/packages/ai-fal/package.json index 7e9bd7413..e36ffc619 100644 --- a/packages/ai-fal/package.json +++ b/packages/ai-fal/package.json @@ -36,6 +36,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-gemini/package.json b/packages/ai-gemini/package.json index 55e02d00c..291ade6bb 100644 --- a/packages/ai-gemini/package.json +++ b/packages/ai-gemini/package.json @@ -44,6 +44,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-grok-build/package.json b/packages/ai-grok-build/package.json index 0c95e4c5f..6e09554f1 100644 --- a/packages/ai-grok-build/package.json +++ b/packages/ai-grok-build/package.json @@ -42,6 +42,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-grok/package.json b/packages/ai-grok/package.json index 38d245914..c12e8c611 100644 --- a/packages/ai-grok/package.json +++ b/packages/ai-grok/package.json @@ -40,6 +40,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-groq/package.json b/packages/ai-groq/package.json index db6e9845a..8c12442df 100644 --- a/packages/ai-groq/package.json +++ b/packages/ai-groq/package.json @@ -40,6 +40,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-isolate-cloudflare/package.json b/packages/ai-isolate-cloudflare/package.json index dbc798ac0..fc05f4ec0 100644 --- a/packages/ai-isolate-cloudflare/package.json +++ b/packages/ai-isolate-cloudflare/package.json @@ -48,6 +48,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-isolate-node/package.json b/packages/ai-isolate-node/package.json index 5a3d6103f..0ba666184 100644 --- a/packages/ai-isolate-node/package.json +++ b/packages/ai-isolate-node/package.json @@ -40,6 +40,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-isolate-quickjs/package.json b/packages/ai-isolate-quickjs/package.json index 6c1acdb34..55b1e2e8b 100644 --- a/packages/ai-isolate-quickjs/package.json +++ b/packages/ai-isolate-quickjs/package.json @@ -37,6 +37,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-mcp/package.json b/packages/ai-mcp/package.json index e7719920c..8323ba3e2 100644 --- a/packages/ai-mcp/package.json +++ b/packages/ai-mcp/package.json @@ -56,8 +56,8 @@ "clean": "premove ./build ./dist", "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", - "test:coverage": "vitest run --coverage", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-memory/package.json b/packages/ai-memory/package.json index d3dafd191..b4518a13d 100644 --- a/packages/ai-memory/package.json +++ b/packages/ai-memory/package.json @@ -50,6 +50,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-mistral/package.json b/packages/ai-mistral/package.json index a40cdc5ca..a6455c2ab 100644 --- a/packages/ai-mistral/package.json +++ b/packages/ai-mistral/package.json @@ -36,6 +36,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-ollama/package.json b/packages/ai-ollama/package.json index b4950d4c8..ac56d69e7 100644 --- a/packages/ai-ollama/package.json +++ b/packages/ai-ollama/package.json @@ -36,6 +36,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-openai/package.json b/packages/ai-openai/package.json index c225f7d8b..9d80cf020 100644 --- a/packages/ai-openai/package.json +++ b/packages/ai-openai/package.json @@ -44,6 +44,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-opencode/package.json b/packages/ai-opencode/package.json index b3a72b4b5..4867ef305 100644 --- a/packages/ai-opencode/package.json +++ b/packages/ai-opencode/package.json @@ -40,6 +40,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-openrouter/package.json b/packages/ai-openrouter/package.json index a37469607..5efc24ffb 100644 --- a/packages/ai-openrouter/package.json +++ b/packages/ai-openrouter/package.json @@ -44,6 +44,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-persistence/package.json b/packages/ai-persistence/package.json index 8cbf8ea7d..39cbef309 100644 --- a/packages/ai-persistence/package.json +++ b/packages/ai-persistence/package.json @@ -43,6 +43,7 @@ "clean": "premove ./build ./dist", "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc", diff --git a/packages/ai-preact/package.json b/packages/ai-preact/package.json index 90fc8f117..e94d2957b 100644 --- a/packages/ai-preact/package.json +++ b/packages/ai-preact/package.json @@ -38,6 +38,7 @@ "clean": "premove ./build ./dist", "lint:fix": "oxlint src --type-aware --fix", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc", diff --git a/packages/ai-react-ui/package.json b/packages/ai-react-ui/package.json index 28c767708..26ea2e267 100644 --- a/packages/ai-react-ui/package.json +++ b/packages/ai-react-ui/package.json @@ -34,6 +34,7 @@ "clean": "premove ./build ./dist", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-react/package.json b/packages/ai-react/package.json index 5eb097cb7..c833076c0 100644 --- a/packages/ai-react/package.json +++ b/packages/ai-react/package.json @@ -38,6 +38,7 @@ "clean": "premove ./build ./dist", "lint:fix": "oxlint src --type-aware --fix", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc", diff --git a/packages/ai-sandbox-cloudflare/package.json b/packages/ai-sandbox-cloudflare/package.json index 9b966de96..9809db8df 100644 --- a/packages/ai-sandbox-cloudflare/package.json +++ b/packages/ai-sandbox-cloudflare/package.json @@ -46,6 +46,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-sandbox-daytona/package.json b/packages/ai-sandbox-daytona/package.json index 097ad1222..4a780a337 100644 --- a/packages/ai-sandbox-daytona/package.json +++ b/packages/ai-sandbox-daytona/package.json @@ -37,6 +37,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-sandbox-docker/package.json b/packages/ai-sandbox-docker/package.json index 2864f03a5..df3516648 100644 --- a/packages/ai-sandbox-docker/package.json +++ b/packages/ai-sandbox-docker/package.json @@ -38,6 +38,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-sandbox-local-process/package.json b/packages/ai-sandbox-local-process/package.json index 236575f15..8211525e3 100644 --- a/packages/ai-sandbox-local-process/package.json +++ b/packages/ai-sandbox-local-process/package.json @@ -36,6 +36,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-sandbox-sprites/package.json b/packages/ai-sandbox-sprites/package.json index cc8fd3531..b58e32129 100644 --- a/packages/ai-sandbox-sprites/package.json +++ b/packages/ai-sandbox-sprites/package.json @@ -38,6 +38,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-sandbox-vercel/package.json b/packages/ai-sandbox-vercel/package.json index 4c1de18c0..d76bce12c 100644 --- a/packages/ai-sandbox-vercel/package.json +++ b/packages/ai-sandbox-vercel/package.json @@ -37,6 +37,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-sandbox/package.json b/packages/ai-sandbox/package.json index 4177c32f3..46f1b11e9 100644 --- a/packages/ai-sandbox/package.json +++ b/packages/ai-sandbox/package.json @@ -50,6 +50,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-solid-ui/package.json b/packages/ai-solid-ui/package.json index 13c56355b..ebf0d2688 100644 --- a/packages/ai-solid-ui/package.json +++ b/packages/ai-solid-ui/package.json @@ -36,6 +36,7 @@ "clean": "premove ./build ./dist", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-solid/package.json b/packages/ai-solid/package.json index 88e573808..850df8627 100644 --- a/packages/ai-solid/package.json +++ b/packages/ai-solid/package.json @@ -33,6 +33,7 @@ "scripts": { "clean": "premove ./build ./dist", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc", diff --git a/packages/ai-svelte/package.json b/packages/ai-svelte/package.json index 12862366b..375f74216 100644 --- a/packages/ai-svelte/package.json +++ b/packages/ai-svelte/package.json @@ -36,6 +36,7 @@ "clean": "rm -rf ./dist", "lint:fix": "oxlint src --type-aware --fix", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "svelte-check", diff --git a/packages/ai-utils/package.json b/packages/ai-utils/package.json index f3c738318..46c4384e9 100644 --- a/packages/ai-utils/package.json +++ b/packages/ai-utils/package.json @@ -36,6 +36,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-vue-ui/package.json b/packages/ai-vue-ui/package.json index 05d7979f0..c46e45e16 100644 --- a/packages/ai-vue-ui/package.json +++ b/packages/ai-vue-ui/package.json @@ -35,6 +35,7 @@ "clean": "premove ./build ./dist", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "vue-tsc --noEmit" diff --git a/packages/ai-vue/package.json b/packages/ai-vue/package.json index f7c4f1062..1d902b1b2 100644 --- a/packages/ai-vue/package.json +++ b/packages/ai-vue/package.json @@ -33,6 +33,7 @@ "scripts": { "clean": "premove ./build ./dist", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc", diff --git a/packages/ai/package.json b/packages/ai/package.json index e11254775..6b797688b 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -64,9 +64,9 @@ "clean": "premove ./build ./dist", "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", - "test:coverage": "vitest run --coverage", - "test:coverage:watch": "vitest --coverage --watch", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", + "test:coverage:watch": "vitest --coverage --watch", "test:lib": "vitest", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/openai-base/package.json b/packages/openai-base/package.json index 5f3112a21..eb5a05eba 100644 --- a/packages/openai-base/package.json +++ b/packages/openai-base/package.json @@ -36,6 +36,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest run", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/preact-ai-devtools/package.json b/packages/preact-ai-devtools/package.json index e6226f65a..62f65aff4 100644 --- a/packages/preact-ai-devtools/package.json +++ b/packages/preact-ai-devtools/package.json @@ -21,6 +21,7 @@ "clean": "premove ./build ./dist", "lint:fix": "oxlint src --type-aware --fix", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc", diff --git a/packages/react-ai-devtools/package.json b/packages/react-ai-devtools/package.json index 0330960b8..8761296fa 100644 --- a/packages/react-ai-devtools/package.json +++ b/packages/react-ai-devtools/package.json @@ -21,6 +21,7 @@ "clean": "premove ./build ./dist", "lint:fix": "oxlint src --type-aware --fix", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc", diff --git a/packages/solid-ai-devtools/package.json b/packages/solid-ai-devtools/package.json index 0583e859d..8a8c1117b 100644 --- a/packages/solid-ai-devtools/package.json +++ b/packages/solid-ai-devtools/package.json @@ -42,6 +42,7 @@ "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:oxlint": "oxlint src --type-aware", + "test:coverage": "vitest run --passWithNoTests --coverage --coverage.include='src/**' --coverage.reporter=text-summary --coverage.reporter=json-summary", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/scripts/coverage-check.mjs b/scripts/coverage-check.mjs new file mode 100644 index 000000000..6f8ac6dbf --- /dev/null +++ b/scripts/coverage-check.mjs @@ -0,0 +1,216 @@ +// Compares coverage between two runs and fails when a package regressed. +// +// Both sides are measured in the same CI job — the PR head and its merge-base +// with main — so there is no baseline file to keep in sync, no per-platform +// skew, and packages can be added or removed without anyone updating a +// checked-in number. +// +// Usage: +// node scripts/coverage-check.mjs --collect # after a coverage run +// node scripts/coverage-check.mjs --base --head +// +// ponytail: two directories of json-summary files and a subtraction. No +// coverage service, no history, no baseline to maintain. +import { + appendFileSync, + mkdirSync, + readFileSync, + readdirSync, + writeFileSync, +} from 'node:fs' +import { join } from 'node:path' + +const METRICS = ['statements', 'branches', 'functions', 'lines'] + +// Coverage percentages wobble slightly between runs (v8 attributes some bytes +// differently depending on JIT timing), so require a real drop. +const TOLERANCE = 0.5 + +function arg(name) { + const i = process.argv.indexOf(name) + return i === -1 ? undefined : process.argv[i + 1] +} + +/** Reduce a vitest json-summary to the four totals we compare. */ +function totals(summary) { + // A package with no source loaded reports totals of 0/0 as 100%; that is not + // a number worth comparing. + if (summary.total.statements.total === 0) return undefined + return Object.fromEntries( + METRICS.map((metric) => [metric, summary.total[metric].pct]), + ) +} + +/** + * Copy this run's summaries into `dir`, one small file per package, so they + * survive the `git checkout` that happens between the two coverage runs. + */ +function collect(dir) { + mkdirSync(dir, { recursive: true }) + let n = 0 + for (const entry of readdirSync('packages', { withFileTypes: true })) { + if (!entry.isDirectory()) continue + let summary + try { + summary = JSON.parse( + readFileSync( + join('packages', entry.name, 'coverage', 'coverage-summary.json'), + 'utf8', + ), + ) + } catch (error) { + // No summary means the package was not measured in this run, which is + // different from it measuring 0%. + if (error.code === 'ENOENT') continue + throw error + } + const metrics = totals(summary) + if (!metrics) continue + writeFileSync(join(dir, `${entry.name}.json`), JSON.stringify(metrics)) + n++ + } + console.log(`Collected ${n} coverage summary/summaries into ${dir}`) +} + +function read(dir) { + let files + try { + files = readdirSync(dir) + } catch (error) { + // A missing directory means that side measured nothing at all. + if (error.code === 'ENOENT') return {} + throw error + } + return Object.fromEntries( + files + .filter((file) => file.endsWith('.json')) + .map((file) => [ + file.replace(/\.json$/, ''), + JSON.parse(readFileSync(join(dir, file), 'utf8')), + ]), + ) +} + +const collectDir = arg('--collect') +if (collectDir) { + collect(collectDir) + process.exit(0) +} + +const baseDir = arg('--base') +const headDir = arg('--head') +if (!baseDir || !headDir) { + console.error( + 'Usage: coverage-check.mjs --collect | --base --head ', + ) + process.exit(2) +} + +const base = read(baseDir) +const head = read(headDir) +const names = Object.keys(head).sort() + +if (names.length === 0) { + console.log('No packages were measured — nothing to compare.') + process.exit(0) +} + +const regressions = [] +const additions = [] +const rows = [] + +for (const name of names) { + const before = base[name] + const after = head[name] + if (!before) { + // New package, or one whose suite could not be measured on the base + // commit. There is nothing to regress against. + additions.push(name) + rows.push([ + name, + ...METRICS.map((metric) => `${after[metric].toFixed(2)}%`), + 'new', + ]) + continue + } + const deltas = METRICS.map((metric) => after[metric] - before[metric]) + const dropped = METRICS.filter((metric, i) => deltas[i] < -TOLERANCE) + if (dropped.length > 0) regressions.push({ name, before, after, dropped }) + rows.push([ + name, + ...METRICS.map((metric, i) => { + const sign = deltas[i] > 0 ? '+' : '' + return `${after[metric].toFixed(2)}% (${sign}${deltas[i].toFixed(2)})` + }), + dropped.length > 0 ? 'DROP' : 'ok', + ]) +} + +const removed = Object.keys(base) + .filter((name) => !head[name]) + .sort() + +const header = ['package', ...METRICS, ''] +const widths = header.map((_, column) => + Math.max(header[column].length, ...rows.map((row) => row[column].length)), +) +const line = (cells) => + cells + .map((cell, i) => cell.padEnd(widths[i])) + .join(' ') + .trimEnd() + +console.log(line(header)) +console.log(widths.map((width) => '-'.repeat(width)).join(' ')) +for (const row of rows) console.log(line(row)) + +if (additions.length > 0) { + console.log( + `\n${additions.length} package(s) had no coverage on the base commit` + + ` (new, or unmeasurable there): ${additions.join(', ')}`, + ) +} +if (removed.length > 0) { + console.log(`\nNot measured on this PR: ${removed.join(', ')}`) +} + +if (process.env.GITHUB_STEP_SUMMARY) { + const md = [ + `## Coverage`, + ``, + regressions.length > 0 + ? `❌ Coverage dropped in ${regressions.length} package(s).` + : `✅ Coverage held across ${names.length} measured package(s).`, + ``, + `Each package is measured twice in this job — on this PR and on its merge-base` + + ` with \`main\` — and compared. A drop of more than ${TOLERANCE}pp in any metric fails.` + + ` Packages your PR doesn't affect are not measured and not listed.`, + ``, + `| package | ${METRICS.join(' | ')} | |`, + `| --- | ${METRICS.map(() => '---:').join(' | ')} | --- |`, + ...rows.map((row) => `| ${row.join(' | ')} |`), + ] + if (additions.length > 0) { + md.push( + ``, + `> No coverage on the base commit, so nothing to compare against:` + + ` ${additions.join(', ')}.`, + ) + } + appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${md.join('\n')}\n`) +} + +if (regressions.length > 0) { + console.error(`\nCoverage dropped in ${regressions.length} package(s):`) + for (const { name, before, after, dropped } of regressions) { + for (const metric of dropped) { + console.error( + ` ${name} ${metric}: ${before[metric].toFixed(2)}% -> ${after[metric].toFixed(2)}%`, + ) + } + } + console.error(`\nAdd tests covering the code this PR changed.`) + process.exit(1) +} + +console.log(`\nCoverage held across ${names.length} measured package(s).`)