From 4d7cc1bea8d4d57508447a778e3e75e4d5ceda95 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 31 Jul 2026 14:28:29 +0200 Subject: [PATCH 1/9] feat(ci): coverage reporting with a committed baseline ratchet Adds a `test:coverage` script to every package and a `Coverage` job to the PR workflow that fails when coverage drops. Coverage was previously unrunnable: `@vitest/coverage-v8` was pinned to 4.0.14 in all 50 packages while `vitest` resolved to 4.1.10, so vitest could not resolve the provider at all (`ERR_MODULE_NOT_FOUND`). The two existing `test:coverage` scripts (ai, ai-client) could not have worked. Aligning the provider on 4.1.10 fixes it. - every package gains `test:coverage`, measured over `src/**` with json-summary + text-summary reporters (flags live in the script so no per-package vitest config changes were needed) - `scripts/coverage-check.mjs` diffs the fresh summaries against `coverage-baseline.json` and exits non-zero on a >0.5pp drop in statements/branches/functions/lines; unmeasured packages are skipped rather than counted as 0% - root gains `test:coverage:all`, `test:coverage:check`, `test:coverage:update` - `coverage-baseline.json` seeded from a full run of the 48 packages whose suites pass on this machine - CONTRIBUTING.md documents the workflow and the two known gaps --- .github/workflows/pr.yml | 17 + CLAUDE.md | 10 +- CONTRIBUTING.md | 49 ++- coverage-baseline.json | 290 ++++++++++++++++++ package.json | 3 + packages/ai-acp/package.json | 1 + packages/ai-angular/package.json | 1 + packages/ai-anthropic/package.json | 1 + packages/ai-bedrock/package.json | 1 + packages/ai-claude-code/package.json | 1 + packages/ai-client/package.json | 4 +- packages/ai-code-mode-snippets/package.json | 1 + packages/ai-code-mode/package.json | 1 + packages/ai-codex/package.json | 1 + packages/ai-devtools/package.json | 1 + packages/ai-durable-stream/package.json | 1 + packages/ai-elevenlabs/package.json | 1 + packages/ai-event-client/package.json | 1 + packages/ai-fal/package.json | 1 + packages/ai-gemini/package.json | 1 + packages/ai-grok-build/package.json | 1 + packages/ai-grok/package.json | 1 + packages/ai-groq/package.json | 1 + packages/ai-isolate-cloudflare/package.json | 1 + packages/ai-isolate-node/package.json | 1 + packages/ai-isolate-quickjs/package.json | 1 + packages/ai-mcp/package.json | 2 +- packages/ai-memory/package.json | 1 + packages/ai-mistral/package.json | 1 + packages/ai-ollama/package.json | 1 + packages/ai-openai/package.json | 1 + packages/ai-opencode/package.json | 1 + packages/ai-openrouter/package.json | 1 + packages/ai-persistence/package.json | 1 + packages/ai-preact/package.json | 1 + packages/ai-react-ui/package.json | 1 + packages/ai-react/package.json | 1 + packages/ai-sandbox-cloudflare/package.json | 1 + packages/ai-sandbox-daytona/package.json | 1 + packages/ai-sandbox-docker/package.json | 1 + .../ai-sandbox-local-process/package.json | 1 + packages/ai-sandbox-sprites/package.json | 1 + packages/ai-sandbox-vercel/package.json | 1 + packages/ai-sandbox/package.json | 1 + packages/ai-solid-ui/package.json | 1 + packages/ai-solid/package.json | 1 + packages/ai-svelte/package.json | 1 + packages/ai-utils/package.json | 1 + packages/ai-vue-ui/package.json | 1 + packages/ai-vue/package.json | 1 + packages/ai/package.json | 4 +- packages/openai-base/package.json | 1 + packages/preact-ai-devtools/package.json | 1 + packages/react-ai-devtools/package.json | 1 + packages/solid-ai-devtools/package.json | 1 + scripts/coverage-check.mjs | 151 +++++++++ 56 files changed, 556 insertions(+), 21 deletions(-) create mode 100644 coverage-baseline.json create mode 100644 scripts/coverage-check.mjs diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a83eb33a8..a0fcde815 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -31,6 +31,23 @@ 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 + - name: Check Coverage + run: pnpm run test:coverage:check preview: name: Preview runs-on: ubuntu-latest diff --git a/CLAUDE.md b/CLAUDE.md index 4a34c8978..fa04ffa34 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,7 +42,10 @@ 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 +pnpm test:coverage # Generate coverage reports (affected) +pnpm test:coverage:all # Generate coverage reports (every package) +pnpm test:coverage:check # Coverage + fail if it dropped vs coverage-baseline.json +pnpm test:coverage:update # Re-baseline coverage-baseline.json pnpm test:knip # Check for unused dependencies pnpm test:sherif # Check pnpm workspace consistency pnpm test:docs # Verify documentation links @@ -306,7 +309,10 @@ 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` +- Test coverage via `pnpm test:coverage`; the `Coverage` CI job runs + `pnpm test:coverage:check`, which fails when any package drops more than + 0.5pp below the percentages committed in `coverage-baseline.json`. Re-baseline + intentional drops with `pnpm test:coverage:update`. 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..462d928ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,23 +63,44 @@ The workflow pushes with `GITHUB_TOKEN`, so GitHub does not start Test / E2E on All commands are run from the repo root. Nx handles affected detection and caching. -| Goal | Command | -| ----------------------------- | ------------------- | -| 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` | -| Verify build artifacts | `pnpm test:build` | -| Format the repo | `pnpm format` | -| Build (affected) | `pnpm build` | -| Build everything | `pnpm build:all` | -| Run the full CI suite locally | `pnpm test` | -| Run the affected-PR check | `pnpm test:pr` | -| E2E suite | `pnpm test:e2e` | -| E2E with Playwright UI | `pnpm test:e2e:ui` | +| Goal | Command | +| ----------------------------- | --------------------------- | +| Run unit tests (affected) | `pnpm test:lib` | +| Watch unit tests | `pnpm test:lib:dev` | +| Coverage (affected) | `pnpm test:coverage` | +| Coverage for every package | `pnpm test:coverage:all` | +| Coverage + regression check | `pnpm test:coverage:check` | +| Re-baseline coverage | `pnpm test:coverage:update` | +| Type-check (affected) | `pnpm test:types` | +| Lint (affected) | `pnpm test:eslint` | +| Verify build artifacts | `pnpm test:build` | +| Format the repo | `pnpm format` | +| Build (affected) | `pnpm build` | +| Build everything | `pnpm build:all` | +| Run the full CI suite locally | `pnpm test` | +| Run the affected-PR check | `pnpm test:pr` | +| E2E suite | `pnpm test:e2e` | +| E2E with Playwright UI | `pnpm test:e2e:ui` | Working on a single package? `cd packages/` and use its scripts directly (`pnpm test:lib`, `pnpm test:types`, etc.). +## Coverage + +Every package has a `test:coverage` script (`vitest run --coverage`, measured over `src/**` with the v8 provider). Per-package percentages are committed to `coverage-baseline.json` at the repo root. + +The `Coverage` job on every PR runs `pnpm test:coverage:check`, which measures the affected packages and fails if any metric (statements, branches, functions, lines) drops more than 0.5 percentage points below the baseline. Packages that weren't affected are skipped, not treated as 0%. + +When a drop is intentional — deleting well-tested code, for instance — re-baseline and commit the result: + +```bash +pnpm test:coverage:update +``` + +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%. + ## 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). diff --git a/coverage-baseline.json b/coverage-baseline.json new file mode 100644 index 000000000..f4c280aeb --- /dev/null +++ b/coverage-baseline.json @@ -0,0 +1,290 @@ +{ + "ai": { + "statements": 88.66, + "branches": 77.81, + "functions": 90.11, + "lines": 89.92 + }, + "ai-angular": { + "statements": 76.57, + "branches": 60.81, + "functions": 63.33, + "lines": 80.17 + }, + "ai-anthropic": { + "statements": 83.87, + "branches": 74.88, + "functions": 85.24, + "lines": 84.4 + }, + "ai-bedrock": { + "statements": 83.7, + "branches": 74.31, + "functions": 82.35, + "lines": 84.16 + }, + "ai-claude-code": { + "statements": 83.2, + "branches": 67.06, + "functions": 86, + "lines": 85.59 + }, + "ai-client": { + "statements": 84.97, + "branches": 76.47, + "functions": 83.01, + "lines": 86.47 + }, + "ai-code-mode": { + "statements": 91.58, + "branches": 85.07, + "functions": 86, + "lines": 92.53 + }, + "ai-code-mode-skills": { + "statements": 80.68, + "branches": 72.75, + "functions": 86.36, + "lines": 81.59 + }, + "ai-codex": { + "statements": 91.72, + "branches": 80.49, + "functions": 89.13, + "lines": 92.61 + }, + "ai-devtools": { + "statements": 80.25, + "branches": 69.87, + "functions": 82.85, + "lines": 81.77 + }, + "ai-durable-stream": { + "statements": 87.42, + "branches": 80.97, + "functions": 93.18, + "lines": 89.7 + }, + "ai-elevenlabs": { + "statements": 65.26, + "branches": 57.6, + "functions": 50.68, + "lines": 66.42 + }, + "ai-event-client": { + "statements": 28.8, + "branches": 19.69, + "functions": 44, + "lines": 29.16 + }, + "ai-fal": { + "statements": 90.12, + "branches": 83.05, + "functions": 90.47, + "lines": 90.19 + }, + "ai-gemini": { + "statements": 59.35, + "branches": 60.25, + "functions": 51.24, + "lines": 60 + }, + "ai-grok": { + "statements": 41.78, + "branches": 43.95, + "functions": 51.63, + "lines": 42.56 + }, + "ai-grok-build": { + "statements": 69.63, + "branches": 51.47, + "functions": 71.02, + "lines": 71.01 + }, + "ai-groq": { + "statements": 39.08, + "branches": 42.6, + "functions": 59.45, + "lines": 40.74 + }, + "ai-isolate-cloudflare": { + "statements": 94.11, + "branches": 70.66, + "functions": 100, + "lines": 93.96 + }, + "ai-isolate-node": { + "statements": 84, + "branches": 43.39, + "functions": 100, + "lines": 83.78 + }, + "ai-isolate-quickjs": { + "statements": 87.23, + "branches": 65.07, + "functions": 88.23, + "lines": 89.05 + }, + "ai-mcp": { + "statements": 95.6, + "branches": 84.23, + "functions": 98.63, + "lines": 98.8 + }, + "ai-memory": { + "statements": 71.42, + "branches": 52.53, + "functions": 77.69, + "lines": 73.21 + }, + "ai-mistral": { + "statements": 68, + "branches": 55.64, + "functions": 76.47, + "lines": 69.16 + }, + "ai-ollama": { + "statements": 28.31, + "branches": 68.12, + "functions": 86.95, + "lines": 28.15 + }, + "ai-openai": { + "statements": 48, + "branches": 50.58, + "functions": 50, + "lines": 48.31 + }, + "ai-opencode": { + "statements": 61.79, + "branches": 52.9, + "functions": 56.62, + "lines": 63.67 + }, + "ai-openrouter": { + "statements": 47.35, + "branches": 49.65, + "functions": 81.7, + "lines": 47.38 + }, + "ai-persistence": { + "statements": 92.15, + "branches": 79.17, + "functions": 99.4, + "lines": 95.44 + }, + "ai-preact": { + "statements": 88.99, + "branches": 78.37, + "functions": 92.85, + "lines": 92.55 + }, + "ai-react": { + "statements": 77.82, + "branches": 66.03, + "functions": 75.86, + "lines": 77.75 + }, + "ai-react-ui": { + "statements": 12.14, + "branches": 5.47, + "functions": 14.28, + "lines": 11.42 + }, + "ai-sandbox-cloudflare": { + "statements": 37.92, + "branches": 32.13, + "functions": 41.98, + "lines": 38.4 + }, + "ai-sandbox-daytona": { + "statements": 12.42, + "branches": 3.07, + "functions": 6.25, + "lines": 13.54 + }, + "ai-sandbox-docker": { + "statements": 62.01, + "branches": 47.36, + "functions": 56.6, + "lines": 64.32 + }, + "ai-sandbox-local-process": { + "statements": 87.71, + "branches": 66.66, + "functions": 89.47, + "lines": 91.33 + }, + "ai-sandbox-sprites": { + "statements": 70.75, + "branches": 55.46, + "functions": 70.87, + "lines": 73.88 + }, + "ai-sandbox-vercel": { + "statements": 5.44, + "branches": 15.47, + "functions": 2.27, + "lines": 6.01 + }, + "ai-solid": { + "statements": 95.27, + "branches": 70.13, + "functions": 93.87, + "lines": 95.81 + }, + "ai-solid-ui": { + "statements": 2.87, + "branches": 3.63, + "functions": 0.94, + "lines": 4.02 + }, + "ai-svelte": { + "statements": 73.51, + "branches": 59.25, + "functions": 72.99, + "lines": 74.72 + }, + "ai-utils": { + "statements": 77.33, + "branches": 75, + "functions": 90.9, + "lines": 77.61 + }, + "ai-vue": { + "statements": 78.18, + "branches": 56.42, + "functions": 81.91, + "lines": 83.95 + }, + "ai-vue-ui": { + "statements": 4.43, + "branches": 4.41, + "functions": 2.56, + "lines": 4.76 + }, + "openai-base": { + "statements": 73.66, + "branches": 57.91, + "functions": 63.82, + "lines": 74.15 + }, + "preact-ai-devtools": { + "statements": 0, + "branches": 0, + "functions": 100, + "lines": 0 + }, + "react-ai-devtools": { + "statements": 0, + "branches": 0, + "functions": 100, + "lines": 0 + }, + "solid-ai-devtools": { + "statements": 0, + "branches": 0, + "functions": 100, + "lines": 0 + } +} diff --git a/package.json b/package.json index 446793d8c..a01cba265 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,9 @@ "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:coverage:check": "pnpm run test:coverage && node scripts/coverage-check.mjs", + "test:coverage:update": "pnpm run test:coverage:all && node scripts/coverage-check.mjs --update", "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..e141cae97 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..624d95676 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..093f832be 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..0ed582f99 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..c974d0abc 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..a815646da 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..ce3cd696c 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..369ed5bf5 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..4bd2312d2 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..3b797827b 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..f515a9ade 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..227485205 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..44c122124 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..d8a80cf7e 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..f28e3cbae 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..2f46e6d42 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..9c6897635 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..01d4f43b7 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..f03bd01e7 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..cf5d12368 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..e888ae676 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..71205a30b 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..fe073ebf0 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..bc08bb141 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..5843b9ca0 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..b36b236a1 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..0e76dcf9b 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..a5735d994 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..3a0d8b3a6 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..cfe369cab 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..27eaddfaa 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..3ed4e5d59 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..14f17cd7c 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..949768a0a 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..ba51dfab5 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..f8ae523f5 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..f5153cfd6 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..f75e1c77e 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..b163f824d 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..9eefee15b 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..240811b82 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..7c656303a 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..8f052e4d7 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..4f55bd47b 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..ee37284a5 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..a9a4a5776 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..152f5632d 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..3f430f1b1 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..3f606d608 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..c44a36d76 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..947c10972 --- /dev/null +++ b/scripts/coverage-check.mjs @@ -0,0 +1,151 @@ +// Compares the coverage produced by `test:coverage` against the committed +// baseline in coverage-baseline.json and fails when a package regressed. +// +// Usage: +// node scripts/coverage-check.mjs # compare, exit 1 on a drop +// node scripts/coverage-check.mjs --update # rewrite the baseline +// +// ponytail: a committed baseline file is the whole ratchet. No coverage +// service, no PR comments, no historical database. If we ever want per-PR +// annotations, upload the lcov files a CI step already produces. +import { readFileSync, readdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' + +const BASELINE = 'coverage-baseline.json' +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 + +const update = process.argv.includes('--update') + +/** @returns {Record>} */ +function readBaseline() { + try { + return JSON.parse(readFileSync(BASELINE, 'utf8')) + } catch (error) { + if (error.code === 'ENOENT') return {} + throw error + } +} + +/** Collect the fresh coverage summaries written by this run. */ +function readCurrent() { + const current = {} + for (const entry of readdirSync('packages', { withFileTypes: true })) { + if (!entry.isDirectory()) continue + const summaryPath = join( + 'packages', + entry.name, + 'coverage', + 'coverage-summary.json', + ) + let summary + try { + summary = JSON.parse(readFileSync(summaryPath, 'utf8')) + } catch (error) { + // Not every package is affected on every run — no summary means the + // package was not measured, which is different from measuring 0%. + if (error.code === 'ENOENT') continue + throw error + } + // A package with no source loaded reports totals of 0/0 as 100%; that is + // not a number worth ratcheting against. + if (summary.total.statements.total === 0) continue + current[entry.name] = Object.fromEntries( + METRICS.map((metric) => [metric, summary.total[metric].pct]), + ) + } + return current +} + +const baseline = readBaseline() +const current = readCurrent() +const names = Object.keys(current).sort() + +if (names.length === 0) { + console.error( + `No coverage summaries found under packages/*/coverage/. Run \`pnpm test:coverage\` first.`, + ) + process.exit(1) +} + +if (update) { + const merged = { ...baseline, ...current } + const sorted = Object.fromEntries( + Object.keys(merged) + .sort() + .map((key) => [key, merged[key]]), + ) + writeFileSync(BASELINE, `${JSON.stringify(sorted, null, 2)}\n`) + console.log(`Updated ${BASELINE} with ${names.length} package(s).`) + process.exit(0) +} + +const regressions = [] +const additions = [] +const rows = [] + +for (const name of names) { + const before = baseline[name] + const after = current[name] + if (!before) { + additions.push(name) + rows.push([name, ...METRICS.map((m) => `${after[m].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 delta = deltas[i] + const sign = delta > 0 ? '+' : '' + return `${after[metric].toFixed(2)}% (${sign}${delta.toFixed(2)})` + }), + dropped.length > 0 ? 'DROP' : 'ok', + ]) +} + +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) missing from ${BASELINE}: ${additions.join(', ')}` + + `\nRun \`pnpm test:coverage:update\` and commit the baseline.`, + ) +} + +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 to restore coverage, or run \`pnpm test:coverage:update\` and commit` + + ` the new baseline if the drop is intentional.`, + ) + process.exit(1) +} + +console.log(`\nCoverage held for ${names.length} measured package(s).`) From afb1b1ad30d7af8b3e5026985468c6490e4f8050 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 31 Jul 2026 14:43:27 +0200 Subject: [PATCH 2/9] feat(ci): surface the coverage table on the PR run summary The check previously only printed to the CI log, so the numbers were invisible unless the job went red. Writing the same per-package table with deltas to $GITHUB_STEP_SUMMARY puts it on the PR's Checks tab on every run, pass or fail, with no token and no `pull-requests: write` permission. --- CONTRIBUTING.md | 2 ++ scripts/coverage-check.mjs | 43 ++++++++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 462d928ec..076e633c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,6 +90,8 @@ Every package has a `test:coverage` script (`vitest run --coverage`, measured ov The `Coverage` job on every PR runs `pnpm test:coverage:check`, which measures the affected packages and fails if any metric (statements, branches, functions, lines) drops more than 0.5 percentage points below the baseline. Packages that weren't affected are skipped, not treated as 0%. +The job writes a per-package table with deltas to the workflow run summary, so you can read the numbers from the PR's Checks tab whether the job passed or failed — click the `Coverage` job and the table is at the top. It is not posted as a PR comment. + When a drop is intentional — deleting well-tested code, for instance — re-baseline and commit the result: ```bash diff --git a/scripts/coverage-check.mjs b/scripts/coverage-check.mjs index 947c10972..603c4d221 100644 --- a/scripts/coverage-check.mjs +++ b/scripts/coverage-check.mjs @@ -5,10 +5,16 @@ // node scripts/coverage-check.mjs # compare, exit 1 on a drop // node scripts/coverage-check.mjs --update # rewrite the baseline // -// ponytail: a committed baseline file is the whole ratchet. No coverage -// service, no PR comments, no historical database. If we ever want per-PR -// annotations, upload the lcov files a CI step already produces. -import { readFileSync, readdirSync, writeFileSync } from 'node:fs' +// ponytail: a committed baseline file is the whole ratchet — no coverage +// service and no historical database. Results surface on the PR via +// $GITHUB_STEP_SUMMARY; for per-line annotations, upload the lcov files that +// `test:coverage` already writes. +import { + appendFileSync, + readFileSync, + readdirSync, + writeFileSync, +} from 'node:fs' import { join } from 'node:path' const BASELINE = 'coverage-baseline.json' @@ -132,6 +138,35 @@ if (additions.length > 0) { ) } +// ponytail: $GITHUB_STEP_SUMMARY renders on the run page with no token, no +// permissions and no API call. A sticky PR comment needs `pull-requests: +// write` — add that only if the summary tab turns out to be too easy to miss. +if (process.env.GITHUB_STEP_SUMMARY) { + const verdict = + regressions.length > 0 + ? `❌ Coverage dropped in ${regressions.length} package(s).` + : `✅ Coverage held for ${names.length} measured package(s).` + const md = [ + `## Coverage`, + ``, + verdict, + ``, + `Baseline: \`${BASELINE}\`. Fails on a drop of more than ${TOLERANCE}pp. Packages not affected by this PR are not measured and not listed.`, + ``, + `| package | ${METRICS.join(' | ')} | |`, + `| --- | ${METRICS.map(() => '---:').join(' | ')} | --- |`, + ...rows.map((row) => `| ${row.join(' | ')} |`), + ] + if (additions.length > 0) { + md.push( + ``, + `> ${additions.length} package(s) are not in the baseline yet: ${additions.join(', ')}.`, + `> Run \`pnpm test:coverage:update\` and commit the baseline.`, + ) + } + 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) { From a6c1c54d5dbe5be951a5e3304b137e001e423d5a Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 31 Jul 2026 14:55:42 +0200 Subject: [PATCH 3/9] fix(ci): baseline the three platform-divergent packages from Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first CI run confirmed the gate works — 47 of 50 packages reproduced the locally generated numbers to the hundredth — and surfaced the three that could not be measured correctly on Windows: - ai-acp and ai-sandbox have tests that fail on Windows for reasons unrelated to coverage, so they were missing from the baseline entirely - ai-sandbox-local-process reads ~20pp higher on Windows because its process-spawn code branches on platform, which tripped the gate All three now carry the numbers from the Linux CI run. CONTRIBUTING.md spells out that the baseline must come from Linux, since CI is what enforces it. --- CONTRIBUTING.md | 5 ++++- coverage-baseline.json | 20 ++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 076e633c9..ff23a902b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -98,10 +98,13 @@ When a drop is intentional — deleting well-tested code, for instance — re-ba pnpm test:coverage:update ``` -Two known limitations: +**The baseline is Linux numbers, because CI is the thing enforcing it.** A few packages cover different amounts on different platforms — `ai-sandbox-local-process` reads ~20pp higher on Windows, since its process-spawn code branches on platform. If you re-baseline on macOS or Windows you will commit numbers CI can't reproduce and the gate will fail for everyone. Take the values from the `Coverage` job's table on a green run instead, or re-baseline in a Linux container. + +Three 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%. +- `ai-acp` and `ai-sandbox` have tests that fail on Windows for unrelated reasons (`EBUSY` on temp-dir unlink; a POSIX-vs-Windows path assertion), so they can't be measured there at all. ## TypeScript configuration diff --git a/coverage-baseline.json b/coverage-baseline.json index f4c280aeb..3dc14f80b 100644 --- a/coverage-baseline.json +++ b/coverage-baseline.json @@ -5,6 +5,12 @@ "functions": 90.11, "lines": 89.92 }, + "ai-acp": { + "statements": 75.93, + "branches": 62.82, + "functions": 68.75, + "lines": 77.73 + }, "ai-angular": { "statements": 76.57, "branches": 60.81, @@ -191,6 +197,12 @@ "functions": 14.28, "lines": 11.42 }, + "ai-sandbox": { + "statements": 84.69, + "branches": 75.7, + "functions": 83.18, + "lines": 86.53 + }, "ai-sandbox-cloudflare": { "statements": 37.92, "branches": 32.13, @@ -210,10 +222,10 @@ "lines": 64.32 }, "ai-sandbox-local-process": { - "statements": 87.71, - "branches": 66.66, - "functions": 89.47, - "lines": 91.33 + "statements": 68.42, + "branches": 50.57, + "functions": 82.45, + "lines": 71.33 }, "ai-sandbox-sprites": { "statements": 70.75, From 12c84ece5400c93a8b6168c11e817610474cbd4c Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 31 Jul 2026 15:10:26 +0200 Subject: [PATCH 4/9] docs(ci): make coverage explicitly CI-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Coverage was already absent from `test`, `test:pr`, `test:ci` and the git hooks, but the docs still presented it as part of the local workflow — four coverage rows in CONTRIBUTING's day-to-day command table, and an instruction to re-baseline with `pnpm test:coverage:update` on your own machine. That last one was an active trap: the committed numbers are Linux numbers and a few packages measure differently per platform, so following the doc on a Mac or Windows box reintroduces exactly the ai-sandbox-local-process failure the previous commit fixed. The check now prints a paste-ready JSON block for every package the baseline needs to absorb, in both the log and the run summary, so re-baselining is a copy out of CI rather than a local run. `test:coverage:update` stays as the maintainer escape hatch but is no longer the documented path. --- CLAUDE.md | 19 ++++++++------- CONTRIBUTING.md | 48 +++++++++++++++++--------------------- scripts/coverage-check.mjs | 32 +++++++++++++++++++------ 3 files changed, 58 insertions(+), 41 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fa04ffa34..389be1244 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,10 +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 (affected) -pnpm test:coverage:all # Generate coverage reports (every package) -pnpm test:coverage:check # Coverage + fail if it dropped vs coverage-baseline.json -pnpm test:coverage:update # Re-baseline coverage-baseline.json +# 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 @@ -309,10 +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`; the `Coverage` CI job runs - `pnpm test:coverage:check`, which fails when any package drops more than - 0.5pp below the percentages committed in `coverage-baseline.json`. Re-baseline - intentional drops with `pnpm test:coverage:update`. See CONTRIBUTING.md. +- **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 runs `test:coverage:check`, which + fails when a package drops more than 0.5pp below the numbers committed in + `coverage-baseline.json`. There are no target percentages; each package's + current number is its own floor. When a drop is intentional, copy the + paste-ready JSON block the job prints into `coverage-baseline.json` — never + regenerate the baseline on a dev machine, the committed numbers are Linux + numbers and some packages measure differently per platform. See + CONTRIBUTING.md. - **E2E tests are mandatory** — see E2E Testing section below ### E2E Testing (REQUIRED) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff23a902b..512a3100d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,42 +63,38 @@ The workflow pushes with `GITHUB_TOKEN`, so GitHub does not start Test / E2E on All commands are run from the repo root. Nx handles affected detection and caching. -| Goal | Command | -| ----------------------------- | --------------------------- | -| Run unit tests (affected) | `pnpm test:lib` | -| Watch unit tests | `pnpm test:lib:dev` | -| Coverage (affected) | `pnpm test:coverage` | -| Coverage for every package | `pnpm test:coverage:all` | -| Coverage + regression check | `pnpm test:coverage:check` | -| Re-baseline coverage | `pnpm test:coverage:update` | -| Type-check (affected) | `pnpm test:types` | -| Lint (affected) | `pnpm test:eslint` | -| Verify build artifacts | `pnpm test:build` | -| Format the repo | `pnpm format` | -| Build (affected) | `pnpm build` | -| Build everything | `pnpm build:all` | -| Run the full CI suite locally | `pnpm test` | -| Run the affected-PR check | `pnpm test:pr` | -| E2E suite | `pnpm test:e2e` | -| E2E with Playwright UI | `pnpm test:e2e:ui` | +| Goal | Command | +| ----------------------------- | ------------------- | +| 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` | +| Verify build artifacts | `pnpm test:build` | +| Format the repo | `pnpm format` | +| Build (affected) | `pnpm build` | +| Build everything | `pnpm build:all` | +| Run the full CI suite locally | `pnpm test` | +| Run the affected-PR check | `pnpm test:pr` | +| E2E suite | `pnpm test:e2e` | +| E2E with Playwright UI | `pnpm test:e2e:ui` | Working on a single package? `cd packages/` and use its scripts directly (`pnpm test:lib`, `pnpm test:types`, etc.). ## Coverage -Every package has a `test:coverage` script (`vitest run --coverage`, measured over `src/**` with the v8 provider). Per-package percentages are committed to `coverage-baseline.json` at the repo root. +**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 runs `pnpm test:coverage:check`, which measures the affected packages and fails if any metric (statements, branches, functions, lines) drops more than 0.5 percentage points below the baseline. Packages that weren't affected are skipped, not treated as 0%. +The `Coverage` job on every PR runs `test:coverage:check`: it measures the affected packages with the v8 provider over `src/**` and fails if any metric (statements, branches, functions, lines) drops more than 0.5 percentage points below `coverage-baseline.json`. Packages your PR didn't affect are skipped, not treated as 0%. -The job writes a per-package table with deltas to the workflow run summary, so you can read the numbers from the PR's Checks tab whether the job passed or failed — click the `Coverage` job and the table is at the top. It is not posted as a PR comment. +There are no target percentages to hit. Each package's committed number is its own floor, so the gate only stops coverage getting _worse_ — it never blocks a PR for being below some repo-wide bar. -When a drop is intentional — deleting well-tested code, for instance — re-baseline and commit the result: +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. -```bash -pnpm test:coverage:update -``` +### If the job says coverage dropped + +Add tests to cover what you changed. If the drop is genuinely intended — you deleted well-tested code, say — the job prints a paste-ready JSON block; copy those entries into `coverage-baseline.json` and push. -**The baseline is Linux numbers, because CI is the thing enforcing it.** A few packages cover different amounts on different platforms — `ai-sandbox-local-process` reads ~20pp higher on Windows, since its process-spawn code branches on platform. If you re-baseline on macOS or Windows you will commit numbers CI can't reproduce and the gate will fail for everyone. Take the values from the `Coverage` job's table on a green run instead, or re-baseline in a Linux container. +**Don't regenerate the baseline on your own machine.** The committed numbers are Linux numbers because Linux is what enforces them, and a few packages genuinely measure differently per platform — `ai-sandbox-local-process` reads ~20pp higher on Windows, since its process-spawn code branches on platform. Re-baselining locally commits numbers CI can't reproduce, which breaks the gate for everyone. (`test:coverage:update` exists for maintainers running in a Linux container; it is not the normal path.) Three known limitations: diff --git a/scripts/coverage-check.mjs b/scripts/coverage-check.mjs index 603c4d221..34d24550a 100644 --- a/scripts/coverage-check.mjs +++ b/scripts/coverage-check.mjs @@ -131,10 +131,20 @@ console.log(line(header)) console.log(widths.map((width) => '-'.repeat(width)).join(' ')) for (const row of rows) console.log(line(row)) +// Anything the baseline needs to absorb, rendered paste-ready. Coverage is a +// CI-only job and a few packages measure differently per platform, so a +// contributor must never regenerate the baseline on their own machine — they +// copy these entries out of the run instead. +const staleNames = [ + ...new Set([...additions, ...regressions.map((r) => r.name)]), +].sort() +const pasteBlock = staleNames + .map((name) => ` ${JSON.stringify(name)}: ${JSON.stringify(current[name])},`) + .join('\n') + if (additions.length > 0) { console.log( - `\n${additions.length} package(s) missing from ${BASELINE}: ${additions.join(', ')}` + - `\nRun \`pnpm test:coverage:update\` and commit the baseline.`, + `\n${additions.length} package(s) missing from ${BASELINE}: ${additions.join(', ')}`, ) } @@ -157,11 +167,18 @@ if (process.env.GITHUB_STEP_SUMMARY) { `| --- | ${METRICS.map(() => '---:').join(' | ')} | --- |`, ...rows.map((row) => `| ${row.join(' | ')} |`), ] - if (additions.length > 0) { + if (pasteBlock) { md.push( ``, - `> ${additions.length} package(s) are not in the baseline yet: ${additions.join(', ')}.`, - `> Run \`pnpm test:coverage:update\` and commit the baseline.`, + `### Updating the baseline`, + ``, + `Coverage runs in CI only, and some packages measure differently per platform —`, + `do not regenerate the baseline on your own machine. If these numbers are the`, + `intended ones, paste the entries below into \`${BASELINE}\` and push:`, + ``, + '```json', + pasteBlock, + '```', ) } appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${md.join('\n')}\n`) @@ -177,8 +194,9 @@ if (regressions.length > 0) { } } console.error( - `\nAdd tests to restore coverage, or run \`pnpm test:coverage:update\` and commit` + - ` the new baseline if the drop is intentional.`, + `\nAdd tests to restore coverage. If the drop is intentional, paste these` + + ` entries into ${BASELINE} and push — do not regenerate the baseline` + + ` locally, some packages measure differently per platform:\n\n${pasteBlock}`, ) process.exit(1) } From 89d9acc284385cd1f3584abbe99a992824bc8a3c Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 31 Jul 2026 15:21:27 +0200 Subject: [PATCH 5/9] fix(ci): quote the coverage include glob, correct stale lint references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback: - quote `--coverage.include='src/**'` in all 50 test:coverage scripts so a POSIX shell can't expand the pattern before vitest parses it. Verified the quoted form behaves identically on Windows (pnpm normalises it) and yields byte-identical percentages, so it is safe cross-platform. - CONTRIBUTING referenced `pnpm test:eslint`, which does not exist — the repo moved to oxlint. Fixed that and the two other stale ESLint mentions, and added the Coverage gate to the list of what CI runs. --- CONTRIBUTING.md | 6 +++--- packages/ai-acp/package.json | 2 +- packages/ai-angular/package.json | 2 +- packages/ai-anthropic/package.json | 2 +- packages/ai-bedrock/package.json | 2 +- packages/ai-claude-code/package.json | 2 +- packages/ai-client/package.json | 2 +- packages/ai-code-mode-snippets/package.json | 2 +- packages/ai-code-mode/package.json | 2 +- packages/ai-codex/package.json | 2 +- packages/ai-devtools/package.json | 2 +- packages/ai-durable-stream/package.json | 2 +- packages/ai-elevenlabs/package.json | 2 +- packages/ai-event-client/package.json | 2 +- packages/ai-fal/package.json | 2 +- packages/ai-gemini/package.json | 2 +- packages/ai-grok-build/package.json | 2 +- packages/ai-grok/package.json | 2 +- packages/ai-groq/package.json | 2 +- packages/ai-isolate-cloudflare/package.json | 2 +- packages/ai-isolate-node/package.json | 2 +- packages/ai-isolate-quickjs/package.json | 2 +- packages/ai-mcp/package.json | 2 +- packages/ai-memory/package.json | 2 +- packages/ai-mistral/package.json | 2 +- packages/ai-ollama/package.json | 2 +- packages/ai-openai/package.json | 2 +- packages/ai-opencode/package.json | 2 +- packages/ai-openrouter/package.json | 2 +- packages/ai-persistence/package.json | 2 +- packages/ai-preact/package.json | 2 +- packages/ai-react-ui/package.json | 2 +- packages/ai-react/package.json | 2 +- packages/ai-sandbox-cloudflare/package.json | 2 +- packages/ai-sandbox-daytona/package.json | 2 +- packages/ai-sandbox-docker/package.json | 2 +- packages/ai-sandbox-local-process/package.json | 2 +- packages/ai-sandbox-sprites/package.json | 2 +- packages/ai-sandbox-vercel/package.json | 2 +- packages/ai-sandbox/package.json | 2 +- packages/ai-solid-ui/package.json | 2 +- packages/ai-solid/package.json | 2 +- packages/ai-svelte/package.json | 2 +- packages/ai-utils/package.json | 2 +- packages/ai-vue-ui/package.json | 2 +- packages/ai-vue/package.json | 2 +- packages/ai/package.json | 2 +- packages/openai-base/package.json | 2 +- packages/preact-ai-devtools/package.json | 2 +- packages/react-ai-devtools/package.json | 2 +- packages/solid-ai-devtools/package.json | 2 +- 51 files changed, 53 insertions(+), 53 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 512a3100d..08bf142b9 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` | @@ -126,7 +126,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) @@ -168,7 +168,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/packages/ai-acp/package.json b/packages/ai-acp/package.json index e141cae97..881aab573 100644 --- a/packages/ai-acp/package.json +++ b/packages/ai-acp/package.json @@ -38,7 +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: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 624d95676..ef7b66c3d 100644 --- a/packages/ai-angular/package.json +++ b/packages/ai-angular/package.json @@ -46,7 +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: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 093f832be..f3787c0e4 100644 --- a/packages/ai-anthropic/package.json +++ b/packages/ai-anthropic/package.json @@ -53,7 +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: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 0ed582f99..1eb8be9cf 100644 --- a/packages/ai-bedrock/package.json +++ b/packages/ai-bedrock/package.json @@ -28,7 +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: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 c974d0abc..b97ccffd8 100644 --- a/packages/ai-claude-code/package.json +++ b/packages/ai-claude-code/package.json @@ -42,7 +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: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 a815646da..7df06aa8c 100644 --- a/packages/ai-client/package.json +++ b/packages/ai-client/package.json @@ -54,7 +54,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: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", diff --git a/packages/ai-code-mode-snippets/package.json b/packages/ai-code-mode-snippets/package.json index ce3cd696c..c113bdd1d 100644 --- a/packages/ai-code-mode-snippets/package.json +++ b/packages/ai-code-mode-snippets/package.json @@ -44,7 +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: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 369ed5bf5..c46505ec0 100644 --- a/packages/ai-code-mode/package.json +++ b/packages/ai-code-mode/package.json @@ -41,7 +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: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 4bd2312d2..3cdb0c690 100644 --- a/packages/ai-codex/package.json +++ b/packages/ai-codex/package.json @@ -41,7 +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: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 3b797827b..59c2242b2 100644 --- a/packages/ai-devtools/package.json +++ b/packages/ai-devtools/package.json @@ -73,7 +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: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 f515a9ade..95ba04d60 100644 --- a/packages/ai-durable-stream/package.json +++ b/packages/ai-durable-stream/package.json @@ -36,7 +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: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 227485205..20b10cc20 100644 --- a/packages/ai-elevenlabs/package.json +++ b/packages/ai-elevenlabs/package.json @@ -53,7 +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: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 44c122124..df8ebfbbb 100644 --- a/packages/ai-event-client/package.json +++ b/packages/ai-event-client/package.json @@ -37,7 +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: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 d8a80cf7e..e36ffc619 100644 --- a/packages/ai-fal/package.json +++ b/packages/ai-fal/package.json @@ -36,7 +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: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 f28e3cbae..291ade6bb 100644 --- a/packages/ai-gemini/package.json +++ b/packages/ai-gemini/package.json @@ -44,7 +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: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 2f46e6d42..6e09554f1 100644 --- a/packages/ai-grok-build/package.json +++ b/packages/ai-grok-build/package.json @@ -42,7 +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: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 9c6897635..c12e8c611 100644 --- a/packages/ai-grok/package.json +++ b/packages/ai-grok/package.json @@ -40,7 +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: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 01d4f43b7..8c12442df 100644 --- a/packages/ai-groq/package.json +++ b/packages/ai-groq/package.json @@ -40,7 +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: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 f03bd01e7..fc05f4ec0 100644 --- a/packages/ai-isolate-cloudflare/package.json +++ b/packages/ai-isolate-cloudflare/package.json @@ -48,7 +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: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 cf5d12368..0ba666184 100644 --- a/packages/ai-isolate-node/package.json +++ b/packages/ai-isolate-node/package.json @@ -40,7 +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: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 e888ae676..55b1e2e8b 100644 --- a/packages/ai-isolate-quickjs/package.json +++ b/packages/ai-isolate-quickjs/package.json @@ -37,7 +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: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 71205a30b..8323ba3e2 100644 --- a/packages/ai-mcp/package.json +++ b/packages/ai-mcp/package.json @@ -57,7 +57,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: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 fe073ebf0..b4518a13d 100644 --- a/packages/ai-memory/package.json +++ b/packages/ai-memory/package.json @@ -50,7 +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: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 bc08bb141..a6455c2ab 100644 --- a/packages/ai-mistral/package.json +++ b/packages/ai-mistral/package.json @@ -36,7 +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: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 5843b9ca0..ac56d69e7 100644 --- a/packages/ai-ollama/package.json +++ b/packages/ai-ollama/package.json @@ -36,7 +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: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 b36b236a1..9d80cf020 100644 --- a/packages/ai-openai/package.json +++ b/packages/ai-openai/package.json @@ -44,7 +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: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 0e76dcf9b..4867ef305 100644 --- a/packages/ai-opencode/package.json +++ b/packages/ai-opencode/package.json @@ -40,7 +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: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 a5735d994..5efc24ffb 100644 --- a/packages/ai-openrouter/package.json +++ b/packages/ai-openrouter/package.json @@ -44,7 +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: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 3a0d8b3a6..39cbef309 100644 --- a/packages/ai-persistence/package.json +++ b/packages/ai-persistence/package.json @@ -43,7 +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: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 cfe369cab..e94d2957b 100644 --- a/packages/ai-preact/package.json +++ b/packages/ai-preact/package.json @@ -38,7 +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: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 27eaddfaa..26ea2e267 100644 --- a/packages/ai-react-ui/package.json +++ b/packages/ai-react-ui/package.json @@ -34,7 +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: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 3ed4e5d59..c833076c0 100644 --- a/packages/ai-react/package.json +++ b/packages/ai-react/package.json @@ -38,7 +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: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 14f17cd7c..9809db8df 100644 --- a/packages/ai-sandbox-cloudflare/package.json +++ b/packages/ai-sandbox-cloudflare/package.json @@ -46,7 +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: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 949768a0a..4a780a337 100644 --- a/packages/ai-sandbox-daytona/package.json +++ b/packages/ai-sandbox-daytona/package.json @@ -37,7 +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: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 ba51dfab5..df3516648 100644 --- a/packages/ai-sandbox-docker/package.json +++ b/packages/ai-sandbox-docker/package.json @@ -38,7 +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: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 f8ae523f5..8211525e3 100644 --- a/packages/ai-sandbox-local-process/package.json +++ b/packages/ai-sandbox-local-process/package.json @@ -36,7 +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: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 f5153cfd6..b58e32129 100644 --- a/packages/ai-sandbox-sprites/package.json +++ b/packages/ai-sandbox-sprites/package.json @@ -38,7 +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: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 f75e1c77e..d76bce12c 100644 --- a/packages/ai-sandbox-vercel/package.json +++ b/packages/ai-sandbox-vercel/package.json @@ -37,7 +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: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 b163f824d..46f1b11e9 100644 --- a/packages/ai-sandbox/package.json +++ b/packages/ai-sandbox/package.json @@ -50,7 +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: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 9eefee15b..ebf0d2688 100644 --- a/packages/ai-solid-ui/package.json +++ b/packages/ai-solid-ui/package.json @@ -36,7 +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: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 240811b82..850df8627 100644 --- a/packages/ai-solid/package.json +++ b/packages/ai-solid/package.json @@ -33,7 +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: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 7c656303a..375f74216 100644 --- a/packages/ai-svelte/package.json +++ b/packages/ai-svelte/package.json @@ -36,7 +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: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 8f052e4d7..46c4384e9 100644 --- a/packages/ai-utils/package.json +++ b/packages/ai-utils/package.json @@ -36,7 +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: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 4f55bd47b..c46e45e16 100644 --- a/packages/ai-vue-ui/package.json +++ b/packages/ai-vue-ui/package.json @@ -35,7 +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: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 ee37284a5..1d902b1b2 100644 --- a/packages/ai-vue/package.json +++ b/packages/ai-vue/package.json @@ -33,7 +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: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 a9a4a5776..6b797688b 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -65,7 +65,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: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", diff --git a/packages/openai-base/package.json b/packages/openai-base/package.json index 152f5632d..eb5a05eba 100644 --- a/packages/openai-base/package.json +++ b/packages/openai-base/package.json @@ -36,7 +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: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 3f430f1b1..62f65aff4 100644 --- a/packages/preact-ai-devtools/package.json +++ b/packages/preact-ai-devtools/package.json @@ -21,7 +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: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 3f606d608..8761296fa 100644 --- a/packages/react-ai-devtools/package.json +++ b/packages/react-ai-devtools/package.json @@ -21,7 +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: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 c44a36d76..8a8c1117b 100644 --- a/packages/solid-ai-devtools/package.json +++ b/packages/solid-ai-devtools/package.json @@ -42,7 +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: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" From d4691863ae1c42f5f27b34895f8533cceba08de8 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 31 Jul 2026 15:58:16 +0200 Subject: [PATCH 6/9] refactor(ci)!: compute the coverage baseline in CI instead of committing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The committed baseline had the maintenance problems it looked like it would: it needed manual syncing, it was platform-sensitive (ai-sandbox-local-process reads ~20pp higher on Windows, which broke the gate on this very PR), and adding or removing a package meant hand-editing a generated file. The Coverage job now measures each affected package twice in one job — on the PR head and on its merge-base with main — and compares those. Nothing to keep in sync, no platform skew (both sides run on the same runner), and package add/remove needs no action. - delete coverage-baseline.json, `test:coverage:check`, `test:coverage:update` and the paste-a-JSON-block flow; all of it existed only to tend the file - coverage-check.mjs gains `--collect ` (snapshot summaries so they survive the checkout between runs) and `--base --head ` - resolve the package list once, with `--with-target=test:coverage`, so both sides measure the same set and a docs-only PR resolves to an empty list rather than erroring in run-many; the base side intersects that list with the projects that exist there, so PR-added packages report as `new` - new coverage.yml runs test:coverage:all on pushes to main purely to populate the Nx Cloud cache: the base-side task hashes then already exist remotely and, because test:coverage declares its coverage dir as an output, restore without re-running tests Verified locally: collect/compare round-trip, a drop failing with only the out-of-tolerance metrics listed, PR-added packages reporting `new` without failing, removed packages ignored, missing base dir, empty head, bad args. --- .github/workflows/coverage.yml | 33 ++++ .github/workflows/pr.yml | 43 ++++- CLAUDE.md | 14 +- CONTRIBUTING.md | 15 +- coverage-baseline.json | 302 --------------------------------- package.json | 2 - scripts/coverage-check.mjs | 212 ++++++++++++----------- 7 files changed, 201 insertions(+), 420 deletions(-) create mode 100644 .github/workflows/coverage.yml delete mode 100644 coverage-baseline.json 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 a0fcde815..6b7aa6b3a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -46,8 +46,47 @@ jobs: uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4.4.0 with: main-branch-name: main - - name: Check Coverage - run: pnpm run test:coverage:check + # 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 != '' + run: | + pnpm exec nx run-many --targets=test:coverage --projects="${{ steps.affected.outputs.list }}" + node scripts/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 != '' + run: | + git checkout --force --detach "$NX_BASE" + pnpm install --frozen-lockfile + # Packages added by this PR do not exist on the base commit. + printf '%s\n' "${{ steps.affected.outputs.list }}" | 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 scripts/coverage-check.mjs --collect "$RUNNER_TEMP/cov-base" + - name: Compare + if: steps.affected.outputs.list != '' + run: | + git checkout --force "$GITHUB_SHA" -- scripts/coverage-check.mjs + node scripts/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 389be1244..955c8f666 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -308,13 +308,13 @@ Each package uses `exports` field in package.json for subpath exports (e.g., `@t - Uses Vitest with happy-dom for DOM testing - **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 runs `test:coverage:check`, which - fails when a package drops more than 0.5pp below the numbers committed in - `coverage-baseline.json`. There are no target percentages; each package's - current number is its own floor. When a drop is intentional, copy the - paste-ready JSON block the job prints into `coverage-baseline.json` — never - regenerate the baseline on a dev machine, the committed numbers are Linux - numbers and some packages measure differently per platform. See + 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08bf142b9..609125b2c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,23 +84,24 @@ Working on a single package? `cd packages/` and use its scripts directly (` **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 runs `test:coverage:check`: it measures the affected packages with the v8 provider over `src/**` and fails if any metric (statements, branches, functions, lines) drops more than 0.5 percentage points below `coverage-baseline.json`. Packages your PR didn't affect are skipped, not treated as 0%. +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 are no target percentages to hit. Each package's committed number is its own floor, so the gate only stops coverage getting _worse_ — it never blocks a PR for being below some repo-wide bar. +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. -### If the job says coverage dropped +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. -Add tests to cover what you changed. If the drop is genuinely intended — you deleted well-tested code, say — the job prints a paste-ready JSON block; copy those entries into `coverage-baseline.json` and push. +### If the job says coverage dropped -**Don't regenerate the baseline on your own machine.** The committed numbers are Linux numbers because Linux is what enforces them, and a few packages genuinely measure differently per platform — `ai-sandbox-local-process` reads ~20pp higher on Windows, since its process-spawn code branches on platform. Re-baselining locally commits numbers CI can't reproduce, which breaks the gate for everyone. (`test:coverage:update` exists for maintainers running in a Linux container; it is not the normal path.) +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. -Three known limitations: +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%. -- `ai-acp` and `ai-sandbox` have tests that fail on Windows for unrelated reasons (`EBUSY` on temp-dir unlink; a POSIX-vs-Windows path assertion), so they can't be measured there at all. + +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 diff --git a/coverage-baseline.json b/coverage-baseline.json deleted file mode 100644 index 3dc14f80b..000000000 --- a/coverage-baseline.json +++ /dev/null @@ -1,302 +0,0 @@ -{ - "ai": { - "statements": 88.66, - "branches": 77.81, - "functions": 90.11, - "lines": 89.92 - }, - "ai-acp": { - "statements": 75.93, - "branches": 62.82, - "functions": 68.75, - "lines": 77.73 - }, - "ai-angular": { - "statements": 76.57, - "branches": 60.81, - "functions": 63.33, - "lines": 80.17 - }, - "ai-anthropic": { - "statements": 83.87, - "branches": 74.88, - "functions": 85.24, - "lines": 84.4 - }, - "ai-bedrock": { - "statements": 83.7, - "branches": 74.31, - "functions": 82.35, - "lines": 84.16 - }, - "ai-claude-code": { - "statements": 83.2, - "branches": 67.06, - "functions": 86, - "lines": 85.59 - }, - "ai-client": { - "statements": 84.97, - "branches": 76.47, - "functions": 83.01, - "lines": 86.47 - }, - "ai-code-mode": { - "statements": 91.58, - "branches": 85.07, - "functions": 86, - "lines": 92.53 - }, - "ai-code-mode-skills": { - "statements": 80.68, - "branches": 72.75, - "functions": 86.36, - "lines": 81.59 - }, - "ai-codex": { - "statements": 91.72, - "branches": 80.49, - "functions": 89.13, - "lines": 92.61 - }, - "ai-devtools": { - "statements": 80.25, - "branches": 69.87, - "functions": 82.85, - "lines": 81.77 - }, - "ai-durable-stream": { - "statements": 87.42, - "branches": 80.97, - "functions": 93.18, - "lines": 89.7 - }, - "ai-elevenlabs": { - "statements": 65.26, - "branches": 57.6, - "functions": 50.68, - "lines": 66.42 - }, - "ai-event-client": { - "statements": 28.8, - "branches": 19.69, - "functions": 44, - "lines": 29.16 - }, - "ai-fal": { - "statements": 90.12, - "branches": 83.05, - "functions": 90.47, - "lines": 90.19 - }, - "ai-gemini": { - "statements": 59.35, - "branches": 60.25, - "functions": 51.24, - "lines": 60 - }, - "ai-grok": { - "statements": 41.78, - "branches": 43.95, - "functions": 51.63, - "lines": 42.56 - }, - "ai-grok-build": { - "statements": 69.63, - "branches": 51.47, - "functions": 71.02, - "lines": 71.01 - }, - "ai-groq": { - "statements": 39.08, - "branches": 42.6, - "functions": 59.45, - "lines": 40.74 - }, - "ai-isolate-cloudflare": { - "statements": 94.11, - "branches": 70.66, - "functions": 100, - "lines": 93.96 - }, - "ai-isolate-node": { - "statements": 84, - "branches": 43.39, - "functions": 100, - "lines": 83.78 - }, - "ai-isolate-quickjs": { - "statements": 87.23, - "branches": 65.07, - "functions": 88.23, - "lines": 89.05 - }, - "ai-mcp": { - "statements": 95.6, - "branches": 84.23, - "functions": 98.63, - "lines": 98.8 - }, - "ai-memory": { - "statements": 71.42, - "branches": 52.53, - "functions": 77.69, - "lines": 73.21 - }, - "ai-mistral": { - "statements": 68, - "branches": 55.64, - "functions": 76.47, - "lines": 69.16 - }, - "ai-ollama": { - "statements": 28.31, - "branches": 68.12, - "functions": 86.95, - "lines": 28.15 - }, - "ai-openai": { - "statements": 48, - "branches": 50.58, - "functions": 50, - "lines": 48.31 - }, - "ai-opencode": { - "statements": 61.79, - "branches": 52.9, - "functions": 56.62, - "lines": 63.67 - }, - "ai-openrouter": { - "statements": 47.35, - "branches": 49.65, - "functions": 81.7, - "lines": 47.38 - }, - "ai-persistence": { - "statements": 92.15, - "branches": 79.17, - "functions": 99.4, - "lines": 95.44 - }, - "ai-preact": { - "statements": 88.99, - "branches": 78.37, - "functions": 92.85, - "lines": 92.55 - }, - "ai-react": { - "statements": 77.82, - "branches": 66.03, - "functions": 75.86, - "lines": 77.75 - }, - "ai-react-ui": { - "statements": 12.14, - "branches": 5.47, - "functions": 14.28, - "lines": 11.42 - }, - "ai-sandbox": { - "statements": 84.69, - "branches": 75.7, - "functions": 83.18, - "lines": 86.53 - }, - "ai-sandbox-cloudflare": { - "statements": 37.92, - "branches": 32.13, - "functions": 41.98, - "lines": 38.4 - }, - "ai-sandbox-daytona": { - "statements": 12.42, - "branches": 3.07, - "functions": 6.25, - "lines": 13.54 - }, - "ai-sandbox-docker": { - "statements": 62.01, - "branches": 47.36, - "functions": 56.6, - "lines": 64.32 - }, - "ai-sandbox-local-process": { - "statements": 68.42, - "branches": 50.57, - "functions": 82.45, - "lines": 71.33 - }, - "ai-sandbox-sprites": { - "statements": 70.75, - "branches": 55.46, - "functions": 70.87, - "lines": 73.88 - }, - "ai-sandbox-vercel": { - "statements": 5.44, - "branches": 15.47, - "functions": 2.27, - "lines": 6.01 - }, - "ai-solid": { - "statements": 95.27, - "branches": 70.13, - "functions": 93.87, - "lines": 95.81 - }, - "ai-solid-ui": { - "statements": 2.87, - "branches": 3.63, - "functions": 0.94, - "lines": 4.02 - }, - "ai-svelte": { - "statements": 73.51, - "branches": 59.25, - "functions": 72.99, - "lines": 74.72 - }, - "ai-utils": { - "statements": 77.33, - "branches": 75, - "functions": 90.9, - "lines": 77.61 - }, - "ai-vue": { - "statements": 78.18, - "branches": 56.42, - "functions": 81.91, - "lines": 83.95 - }, - "ai-vue-ui": { - "statements": 4.43, - "branches": 4.41, - "functions": 2.56, - "lines": 4.76 - }, - "openai-base": { - "statements": 73.66, - "branches": 57.91, - "functions": 63.82, - "lines": 74.15 - }, - "preact-ai-devtools": { - "statements": 0, - "branches": 0, - "functions": 100, - "lines": 0 - }, - "react-ai-devtools": { - "statements": 0, - "branches": 0, - "functions": 100, - "lines": 0 - }, - "solid-ai-devtools": { - "statements": 0, - "branches": 0, - "functions": 100, - "lines": 0 - } -} diff --git a/package.json b/package.json index a01cba265..d9cb2e643 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,6 @@ "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:coverage:check": "pnpm run test:coverage && node scripts/coverage-check.mjs", - "test:coverage:update": "pnpm run test:coverage:all && node scripts/coverage-check.mjs --update", "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/scripts/coverage-check.mjs b/scripts/coverage-check.mjs index 34d24550a..6f8ac6dbf 100644 --- a/scripts/coverage-check.mjs +++ b/scripts/coverage-check.mjs @@ -1,91 +1,117 @@ -// Compares the coverage produced by `test:coverage` against the committed -// baseline in coverage-baseline.json and fails when a package regressed. +// 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 # compare, exit 1 on a drop -// node scripts/coverage-check.mjs --update # rewrite the baseline +// node scripts/coverage-check.mjs --collect # after a coverage run +// node scripts/coverage-check.mjs --base --head // -// ponytail: a committed baseline file is the whole ratchet — no coverage -// service and no historical database. Results surface on the PR via -// $GITHUB_STEP_SUMMARY; for per-line annotations, upload the lcov files that -// `test:coverage` already writes. +// 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 BASELINE = 'coverage-baseline.json' 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. +// Coverage percentages wobble slightly between runs (v8 attributes some bytes +// differently depending on JIT timing), so require a real drop. const TOLERANCE = 0.5 -const update = process.argv.includes('--update') +function arg(name) { + const i = process.argv.indexOf(name) + return i === -1 ? undefined : process.argv[i + 1] +} -/** @returns {Record>} */ -function readBaseline() { - try { - return JSON.parse(readFileSync(BASELINE, 'utf8')) - } catch (error) { - if (error.code === 'ENOENT') return {} - throw error - } +/** 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]), + ) } -/** Collect the fresh coverage summaries written by this run. */ -function readCurrent() { - const current = {} +/** + * 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 - const summaryPath = join( - 'packages', - entry.name, - 'coverage', - 'coverage-summary.json', - ) let summary try { - summary = JSON.parse(readFileSync(summaryPath, 'utf8')) + summary = JSON.parse( + readFileSync( + join('packages', entry.name, 'coverage', 'coverage-summary.json'), + 'utf8', + ), + ) } catch (error) { - // Not every package is affected on every run — no summary means the - // package was not measured, which is different from measuring 0%. + // 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 } - // A package with no source loaded reports totals of 0/0 as 100%; that is - // not a number worth ratcheting against. - if (summary.total.statements.total === 0) continue - current[entry.name] = Object.fromEntries( - METRICS.map((metric) => [metric, summary.total[metric].pct]), - ) + const metrics = totals(summary) + if (!metrics) continue + writeFileSync(join(dir, `${entry.name}.json`), JSON.stringify(metrics)) + n++ } - return current + console.log(`Collected ${n} coverage summary/summaries into ${dir}`) } -const baseline = readBaseline() -const current = readCurrent() -const names = Object.keys(current).sort() +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')), + ]), + ) +} -if (names.length === 0) { +const collectDir = arg('--collect') +if (collectDir) { + collect(collectDir) + process.exit(0) +} + +const baseDir = arg('--base') +const headDir = arg('--head') +if (!baseDir || !headDir) { console.error( - `No coverage summaries found under packages/*/coverage/. Run \`pnpm test:coverage\` first.`, + 'Usage: coverage-check.mjs --collect | --base --head ', ) - process.exit(1) + process.exit(2) } -if (update) { - const merged = { ...baseline, ...current } - const sorted = Object.fromEntries( - Object.keys(merged) - .sort() - .map((key) => [key, merged[key]]), - ) - writeFileSync(BASELINE, `${JSON.stringify(sorted, null, 2)}\n`) - console.log(`Updated ${BASELINE} with ${names.length} package(s).`) +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) } @@ -94,29 +120,36 @@ const additions = [] const rows = [] for (const name of names) { - const before = baseline[name] - const after = current[name] + 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((m) => `${after[m].toFixed(2)}%`), 'new']) + 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 }) - } + if (dropped.length > 0) regressions.push({ name, before, after, dropped }) rows.push([ name, ...METRICS.map((metric, i) => { - const delta = deltas[i] - const sign = delta > 0 ? '+' : '' - return `${after[metric].toFixed(2)}% (${sign}${delta.toFixed(2)})` + 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)), @@ -131,54 +164,37 @@ console.log(line(header)) console.log(widths.map((width) => '-'.repeat(width)).join(' ')) for (const row of rows) console.log(line(row)) -// Anything the baseline needs to absorb, rendered paste-ready. Coverage is a -// CI-only job and a few packages measure differently per platform, so a -// contributor must never regenerate the baseline on their own machine — they -// copy these entries out of the run instead. -const staleNames = [ - ...new Set([...additions, ...regressions.map((r) => r.name)]), -].sort() -const pasteBlock = staleNames - .map((name) => ` ${JSON.stringify(name)}: ${JSON.stringify(current[name])},`) - .join('\n') - if (additions.length > 0) { console.log( - `\n${additions.length} package(s) missing from ${BASELINE}: ${additions.join(', ')}`, + `\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(', ')}`) +} -// ponytail: $GITHUB_STEP_SUMMARY renders on the run page with no token, no -// permissions and no API call. A sticky PR comment needs `pull-requests: -// write` — add that only if the summary tab turns out to be too easy to miss. if (process.env.GITHUB_STEP_SUMMARY) { - const verdict = - regressions.length > 0 - ? `❌ Coverage dropped in ${regressions.length} package(s).` - : `✅ Coverage held for ${names.length} measured package(s).` const md = [ `## Coverage`, ``, - verdict, + regressions.length > 0 + ? `❌ Coverage dropped in ${regressions.length} package(s).` + : `✅ Coverage held across ${names.length} measured package(s).`, ``, - `Baseline: \`${BASELINE}\`. Fails on a drop of more than ${TOLERANCE}pp. Packages not affected by this PR are not measured and not listed.`, + `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 (pasteBlock) { + if (additions.length > 0) { md.push( ``, - `### Updating the baseline`, - ``, - `Coverage runs in CI only, and some packages measure differently per platform —`, - `do not regenerate the baseline on your own machine. If these numbers are the`, - `intended ones, paste the entries below into \`${BASELINE}\` and push:`, - ``, - '```json', - pasteBlock, - '```', + `> No coverage on the base commit, so nothing to compare against:` + + ` ${additions.join(', ')}.`, ) } appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${md.join('\n')}\n`) @@ -193,12 +209,8 @@ if (regressions.length > 0) { ) } } - console.error( - `\nAdd tests to restore coverage. If the drop is intentional, paste these` + - ` entries into ${BASELINE} and push — do not regenerate the baseline` + - ` locally, some packages measure differently per platform:\n\n${pasteBlock}`, - ) + console.error(`\nAdd tests covering the code this PR changed.`) process.exit(1) } -console.log(`\nCoverage held for ${names.length} measured package(s).`) +console.log(`\nCoverage held across ${names.length} measured package(s).`) From d11759ce3c500ebcae6d4963067f1431e8da93f5 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 31 Jul 2026 16:15:26 +0200 Subject: [PATCH 7/9] fix(ci): stage the coverage script outside the working tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge-base step ran `node scripts/coverage-check.mjs --collect` after checking out the base commit, where that file does not exist — this PR is what adds it: Error: Cannot find module '/home/runner/work/ai/ai/scripts/coverage-check.mjs' Not specific to this PR either: any PR that adds or moves the script would hit the same thing. Copying it to $RUNNER_TEMP before the checkout fixes it and also guarantees both runs and the comparison use the same version, which the previous `git checkout $GITHUB_SHA -- scripts/...` in the compare step only approximated. The base coverage run itself was fine, at 3/6 Nx cache hits before coverage.yml has ever run on main. --- .github/workflows/pr.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 6b7aa6b3a..01e2b19e1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -46,6 +46,12 @@ jobs: 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 @@ -60,7 +66,7 @@ jobs: if: steps.affected.outputs.list != '' run: | pnpm exec nx run-many --targets=test:coverage --projects="${{ steps.affected.outputs.list }}" - node scripts/coverage-check.mjs --collect "$RUNNER_TEMP/cov-head" + 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, @@ -79,12 +85,11 @@ jobs: exit 0 fi pnpm exec nx run-many --targets=test:coverage --projects="$base_list" - node scripts/coverage-check.mjs --collect "$RUNNER_TEMP/cov-base" + node "$RUNNER_TEMP/coverage-check.mjs" --collect "$RUNNER_TEMP/cov-base" - name: Compare if: steps.affected.outputs.list != '' run: | - git checkout --force "$GITHUB_SHA" -- scripts/coverage-check.mjs - node scripts/coverage-check.mjs \ + node "$RUNNER_TEMP/coverage-check.mjs" \ --base "$RUNNER_TEMP/cov-base" \ --head "$RUNNER_TEMP/cov-head" preview: From fa1b3d01ac9262387f579ce335238ef61b0aa816 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 31 Jul 2026 16:20:59 +0200 Subject: [PATCH 8/9] fix(ci): clear coverage dirs before each run so the base side is real MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous run passed for the wrong reason. `coverage/` is gitignored, so checking out the merge base left this PR's coverage files on disk, and the base-side `--collect` read them back: 47 packages were compared against themselves, which is why every delta was exactly 0.00. Both runs now clear `packages/*/coverage` first. Nx restores the directory for cached tasks, so genuine cache hits still work. On this PR the base side will now legitimately collect almost nothing — the merge base predates the json-summary reporter, so there is no base data to compare against and packages report as `new`. That resolves itself once this lands and main has the machinery. --- .github/workflows/pr.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 01e2b19e1..0ddcc2ce9 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -65,6 +65,7 @@ jobs: - name: Coverage for this PR if: steps.affected.outputs.list != '' run: | + rm -rf packages/*/coverage pnpm exec nx run-many --targets=test:coverage --projects="${{ steps.affected.outputs.list }}" node "$RUNNER_TEMP/coverage-check.mjs" --collect "$RUNNER_TEMP/cov-head" # Re-measuring the merge-base is normally a set of Nx Cloud cache @@ -75,6 +76,10 @@ jobs: if: 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' "${{ steps.affected.outputs.list }}" | tr ',' '\n' | sort > /tmp/wanted From df32eb165069e92b03b3cfaa432c01c97a7b0ad2 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 31 Jul 2026 16:44:07 +0200 Subject: [PATCH 9/9] fix(ci): pass the project list via env to satisfy zizmor zizmor flagged both `${{ steps.affected.outputs.list }}` uses inside run blocks as code injection via template expansion (pr.yml:69, pr.yml:85), and it's right: the list is built from package names in the PR's own files, so on a fork PR a package named with shell metacharacters would be interpolated straight into the script. Passing it through env keeps it out of the shell source entirely. Confirmed clean by running zizmor v1.28.0 locally over both workflows. --- .github/workflows/pr.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 0ddcc2ce9..265e2136b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -64,9 +64,13 @@ jobs: 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="${{ steps.affected.outputs.list }}" + 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 @@ -74,6 +78,8 @@ jobs: # 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 @@ -82,7 +88,7 @@ jobs: rm -rf packages/*/coverage pnpm install --frozen-lockfile # Packages added by this PR do not exist on the base commit. - printf '%s\n' "${{ steps.affected.outputs.list }}" | tr ',' '\n' | sort > /tmp/wanted + 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