diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000000..40c5739e6f --- /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 a83eb33a81..2eb32ac37b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -31,6 +31,82 @@ jobs: main-branch-name: main - name: Run Checks run: pnpm run test:pr + coverage: + name: Coverage + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + - name: Setup Tools + uses: TanStack/config/.github/setup@190f659075ff0845850e330883eb26d7ffd0671f # main + - name: Get base and head commits for `nx affected` + uses: nrwl/nx-set-shas@3e9ad7370203c1e93d109be57f3b72eb0eb511b1 # v4.4.0 + with: + main-branch-name: main + # Stashed outside the working tree because checking out the merge base + # would otherwise remove it — the script may be added or changed by the + # very PR under test. Both runs and the comparison use this copy, so all + # three agree on the version. + - name: Stage the coverage script + run: cp scripts/coverage-check.mjs "$RUNNER_TEMP/coverage-check.mjs" + # The set of packages to measure is decided once, here, so that the base + # run measures exactly what the head run does. `--with-target` keeps out + # projects that have no test:coverage script, so a docs-only PR resolves + # to an empty list rather than erroring in run-many. + - name: Resolve affected packages + id: affected + run: | + list=$(pnpm exec nx show projects --affected --with-target=test:coverage --exclude='examples/**,testing/**' --json | jq -r 'sort | join(",")') + echo "list=$list" >> "$GITHUB_OUTPUT" + echo "Measuring: ${list:-}" + - name: Coverage for this PR + if: steps.affected.outputs.list != '' + # Via env, not `${{ }}` inside run: project names come from the PR's own + # files, so interpolating them into the shell would be injectable. + env: + PROJECTS: ${{ steps.affected.outputs.list }} + run: | + rm -rf packages/*/coverage + pnpm exec nx run-many --targets=test:coverage --projects="$PROJECTS" -- --coverage.reporter=json-summary --coverage.include='src/**' + node "$RUNNER_TEMP/coverage-check.mjs" --collect "$RUNNER_TEMP/cov-head" --expect "$PROJECTS" + # Re-measuring the merge-base is normally a set of Nx Cloud cache + # restores seeded by the Coverage workflow on main: the task hashes are + # identical, and `test:coverage` declares its coverage dir as an output, + # so the summaries come back without the tests re-running. + - name: Coverage for the merge base + if: steps.affected.outputs.list != '' + env: + PROJECTS: ${{ steps.affected.outputs.list }} + run: | + git checkout --force --detach "$NX_BASE" + # coverage/ is gitignored, so the checkout leaves this PR's results in + # place — without this the base collection silently re-reads head's + # numbers and every delta comes out as 0.00. + rm -rf packages/*/coverage + pnpm install --frozen-lockfile + # Intersect with projects that have test:coverage on the base commit + # (added packages, or packages that gained the script in this PR). + printf '%s\n' "$PROJECTS" | tr ',' '\n' | sort > /tmp/wanted + pnpm exec nx show projects --with-target=test:coverage --json | jq -r '.[]' | sort > /tmp/available + base_list=$(comm -12 /tmp/wanted /tmp/available | paste -sd, -) + if [ -z "$base_list" ]; then + echo "No affected package has test:coverage on the base commit; treating all as new." + mkdir -p "$RUNNER_TEMP/cov-base" + exit 0 + fi + # Extra reporters/include so a merge-base whose scripts are still + # `vitest run --coverage` still writes coverage-summary.json. + pnpm exec nx run-many --targets=test:coverage --projects="$base_list" -- --coverage.reporter=json-summary --coverage.include='src/**' + node "$RUNNER_TEMP/coverage-check.mjs" --collect "$RUNNER_TEMP/cov-base" --expect "$base_list" + - name: Compare + if: steps.affected.outputs.list != '' + run: | + node "$RUNNER_TEMP/coverage-check.mjs" \ + --base "$RUNNER_TEMP/cov-base" \ + --head "$RUNNER_TEMP/cov-head" preview: name: Preview runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index 6fb6ac40f8..c631bb6ebe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -148,6 +148,13 @@ each of these and confirm they're green before pushing: Do **not** rely on CI as your first signal. Run locally, fix, then push. +## Coverage + +Coverage is CI-only. Do not run it locally and do not add it to `test`, +`test:pr`, `test:ci`, or git hooks. There is no baseline file. The PR +`Coverage` job compares affected packages on the branch against their +merge-base with `main`. See **Coverage** in `CONTRIBUTING.md`. + ## Documentation Load the `docs` skill first (see **Docs skill** above). Then also obey diff --git a/CLAUDE.md b/CLAUDE.md index deb7b04a42..93d1dc02ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,7 @@ pnpm test:lib:dev # Watch mode for unit tests pnpm test:oxlint # Lint affected packages (oxlint, incl. type-aware) pnpm test:types # Type check affected packages pnpm test:build # Verify build artifacts with publint -pnpm test:coverage # Generate coverage reports +# Coverage is CI-only. See CONTRIBUTING.md. 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 @@ -318,7 +318,16 @@ Each package uses `exports` field in package.json for subpath exports (e.g., `@t - Unit tests in `*.test.ts` files alongside source - Uses Vitest with happy-dom for DOM testing -- Test coverage via `pnpm test:coverage` +- **Coverage is CI-only.** Don't run it locally and don't add it to local + gates — it is deliberately absent from `test`, `test:pr`, `test:ci` and the + git hooks. The `Coverage` job on each PR measures every affected package + twice, on the PR head and on its merge-base with `main`, and fails when a + metric drops more than 0.5pp between them. There is **no baseline file** — + don't reintroduce one, it was removed precisely because it needed manual + syncing and was platform-sensitive. The only remedy for a drop is tests. + `.github/workflows/coverage.yml` runs coverage on pushes to `main` purely to + warm the Nx Cloud cache so the base-side run is mostly cache restores. See + CONTRIBUTING.md. - **E2E tests are mandatory** — see E2E Testing section below ### E2E Testing (REQUIRED) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 02200f4bf1..f86d20847c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,7 +76,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` | @@ -88,6 +88,31 @@ All commands are run from the repo root. Nx handles affected detection and cachi Working on a single package? `cd packages/` and use its scripts directly (`pnpm test:lib`, `pnpm test:types`, etc.). +## Coverage + +**Coverage runs in CI only. It is not part of `pnpm test`, `pnpm test:pr`, or any git hook, and you are not expected to run it locally.** + +The `Coverage` job on every PR measures each affected package that has a `test:coverage` target twice: once on your branch and once on its merge-base with `main`. 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, and packages without that target, are never measured. + +There is no baseline file to keep in sync, and nothing to update when a package is added or removed: both numbers come from the same job on the same runner. There are also no target percentages to hit — the gate only catches coverage getting _worse_ in what you touched, and never blocks a PR for being below some repo-wide bar. + +Read the numbers from the PR's Checks tab: open the `Coverage` job. When at least one package with `test:coverage` is affected, the job summary has a per-package table with deltas, pass or fail. It is not posted as a PR comment. + +Re-measuring the merge-base is usually close to free. A separate `Coverage` workflow runs `test:coverage:all` on every push to `main`, which populates the Nx Cloud cache; because `test:coverage` declares its `coverage/` directory as a task output, the base-side run generally restores cached summaries rather than re-running any tests. + +### If the job says coverage dropped + +Add tests covering the code you changed. That's the whole remedy — there is no number to override. If you genuinely deleted well-tested code and the drop is expected, say so in the PR and a maintainer can merge past the failing check. + +Two known limitations: + +- Uncovered `.tsx` files can't be remapped by the coverage provider and are dropped from the report with a `Failed to parse ... Excluding it from coverage` warning. `.tsx` files that tests _do_ load are measured normally, so the UI packages read higher than their real coverage. +- `preact-ai-devtools`, `react-ai-devtools`, and `solid-ai-devtools` have no tests. They may be omitted (0 statements after `.tsx` remap failure) or show ~0% of remaining `.ts`; they do not gate the job. + +A **new package that defines `test:coverage`** shows as `new` and cannot fail the comparison. Packages without that script are not measured. + +If a package was measured on the base commit and has no summary on this PR, the job fails. A crashing `test:coverage` suite on either side also fails the job. It is not treated as `new`. + ## 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). @@ -112,7 +137,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) @@ -172,10 +197,12 @@ The defensive `ignore` list in `.changeset/config.json` blocks accidental public 1. Push your branch and open a PR against `main`. 2. Fill the PR template. Tick **docs** and **changeset** honestly, or say why you skipped them. -3. 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. +3. 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. 4. Address review comments. 5. A maintainer merges. Releases are cut via Changesets. Your changeset entry lands in the next release. +The PR template lists the steps. The `Test plan` section is required — describe how a reviewer can verify your change. + ## Adding a new provider adapter The pattern lives in `packages/ai-openai/`, `packages/ai-anthropic/`, `packages/ai-gemini/`, etc. New core adapters typically: diff --git a/package.json b/package.json index bf7b24b987..0e8c6ff61e 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,13 @@ "test:lib": "nx affected --targets=test:lib --exclude=examples/**,testing/**", "test:lib:dev": "pnpm test:lib && nx watch --all -- pnpm test:lib", "test:coverage": "nx affected --targets=test:coverage --exclude=examples/**,testing/**", + "test:coverage:all": "nx run-many --targets=test:coverage --exclude=examples/**,testing/**", "test:build": "nx affected --target=test:build --exclude=examples/**,testing/**", "test:types": "nx affected --targets=test:types --exclude=examples/**,testing/**", "test:knip": "knip", "test:docs": "tsx scripts/verify-links.ts", "test:dts": "node scripts/scan-dangling-dts.mjs", - "test:maintainer": "vitest run --root scripts/maintainer && vitest run scripts/model-sync scripts/publish-staged-docs.test.ts", + "test:maintainer": "vitest run --root scripts/maintainer && vitest run scripts/model-sync scripts/publish-staged-docs.test.ts && vitest run scripts/coverage-check.test.ts", "maintainer:sweep": "tsx scripts/maintainer/sweep.ts", "maintainer:scorecard": "tsx scripts/maintainer/scorecard.ts", "test:kiira": "kiira check", diff --git a/packages/ai-acp/package.json b/packages/ai-acp/package.json index 614118680e..72f28ec904 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 e18f550973..fc792e1965 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 913893972c..690df1cebf 100644 --- a/packages/ai-anthropic/package.json +++ b/packages/ai-anthropic/package.json @@ -61,6 +61,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 e8e6d603ff..76f1a109f3 100644 --- a/packages/ai-bedrock/package.json +++ b/packages/ai-bedrock/package.json @@ -32,6 +32,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-byteplus/package.json b/packages/ai-byteplus/package.json index e887f6d767..806e0db824 100644 --- a/packages/ai-byteplus/package.json +++ b/packages/ai-byteplus/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-claude-code/package.json b/packages/ai-claude-code/package.json index 8356574c94..e25131885e 100644 --- a/packages/ai-claude-code/package.json +++ b/packages/ai-claude-code/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-client/package.json b/packages/ai-client/package.json index 476bed9921..9912b359e3 100644 --- a/packages/ai-client/package.json +++ b/packages/ai-client/package.json @@ -57,9 +57,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 e0480b2555..bf4029c24c 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 4c2d61cd0d..24764afac8 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 41b7de195d..1d006f876b 100644 --- a/packages/ai-codex/package.json +++ b/packages/ai-codex/package.json @@ -45,6 +45,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-cohere/package.json b/packages/ai-cohere/package.json index 3af1f45221..fb2a61cf1e 100644 --- a/packages/ai-cohere/package.json +++ b/packages/ai-cohere/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-devtools/package.json b/packages/ai-devtools/package.json index f2a29d58fa..82df67a7ca 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 34f67dc99d..e813442cfb 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 38619f88d1..96f407592e 100644 --- a/packages/ai-elevenlabs/package.json +++ b/packages/ai-elevenlabs/package.json @@ -57,6 +57,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 2ac7f20368..97ac7651e6 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 4912e76acd..0a3895250f 100644 --- a/packages/ai-fal/package.json +++ b/packages/ai-fal/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-gemini/package.json b/packages/ai-gemini/package.json index 96c5f126b7..eedd72fb3d 100644 --- a/packages/ai-gemini/package.json +++ b/packages/ai-gemini/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 --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 e64b3d7026..5620c4c569 100644 --- a/packages/ai-grok-build/package.json +++ b/packages/ai-grok-build/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-grok/package.json b/packages/ai-grok/package.json index b0a1c0c92b..3e013d75db 100644 --- a/packages/ai-grok/package.json +++ b/packages/ai-grok/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 --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 9628b55d48..966d2e26b7 100644 --- a/packages/ai-groq/package.json +++ b/packages/ai-groq/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-isolate-cloudflare/package.json b/packages/ai-isolate-cloudflare/package.json index dbc798ac08..fc05f4ec02 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-daytona/package.json b/packages/ai-isolate-daytona/package.json index 9463108e9e..38fe215f75 100644 --- a/packages/ai-isolate-daytona/package.json +++ b/packages/ai-isolate-daytona/package.json @@ -32,6 +32,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:live": "vitest --run tests/live.test.ts", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", diff --git a/packages/ai-isolate-node/package.json b/packages/ai-isolate-node/package.json index 5a3d6103ff..0ba6661848 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-bun/package.json b/packages/ai-isolate-quickjs-bun/package.json index 21a4b8abeb..28ca0779b4 100644 --- a/packages/ai-isolate-quickjs-bun/package.json +++ b/packages/ai-isolate-quickjs-bun/package.json @@ -41,6 +41,7 @@ "test:build": "publint --strict", "test:bun": "bun test ./tests", "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 6c1acdb344..55b1e2e8bd 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-llmgateway/package.json b/packages/ai-llmgateway/package.json index ee8ceed091..f0fc91dbef 100644 --- a/packages/ai-llmgateway/package.json +++ b/packages/ai-llmgateway/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-mcp/package.json b/packages/ai-mcp/package.json index efd68aa30a..4d23ece432 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 cf0cf91b33..5381884391 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 31e3e0df90..d4c8841c91 100644 --- a/packages/ai-mistral/package.json +++ b/packages/ai-mistral/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-octane/package.json b/packages/ai-octane/package.json index ce4a83eefb..1a0c61348d 100644 --- a/packages/ai-octane/package.json +++ b/packages/ai-octane/package.json @@ -48,6 +48,7 @@ "scripts": { "lint:fix": "oxlint src --type-aware --ignore-pattern '**/*.tsrx.d.ts' --fix", "test:oxlint": "oxlint src --type-aware --ignore-pattern '**/*.tsrx.d.ts'", + "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 && tsc -p typetests/tsconfig.json" diff --git a/packages/ai-ollama/package.json b/packages/ai-ollama/package.json index 9fa30ae1ce..3d42257a2f 100644 --- a/packages/ai-ollama/package.json +++ b/packages/ai-ollama/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-openai/package.json b/packages/ai-openai/package.json index 305a7cddc9..f9f9071707 100644 --- a/packages/ai-openai/package.json +++ b/packages/ai-openai/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 --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 0432b3e899..5eefc81a0b 100644 --- a/packages/ai-opencode/package.json +++ b/packages/ai-opencode/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-openrouter/package.json b/packages/ai-openrouter/package.json index 5c23a8e6de..7bc57cde2b 100644 --- a/packages/ai-openrouter/package.json +++ b/packages/ai-openrouter/package.json @@ -52,6 +52,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-perplexity/package.json b/packages/ai-perplexity/package.json index a6b549a8ba..9a2c34b5e9 100644 --- a/packages/ai-perplexity/package.json +++ b/packages/ai-perplexity/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 9d16df1b33..0538861e96 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 2a203618b0..3288ee5f32 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 a49b3d1525..451821a44d 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 c1f528cc8b..ed0f0b195b 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 1c233515e2..d29afe5117 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 da599519f1..51f0454654 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 29f2575ea7..f93578aa61 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 a0336621c6..78f100bdec 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 a33ffcc967..918df8db48 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 0751d96072..20ce9a226b 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 a371986999..0ece47c227 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 81d69c7582..50aae37c59 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 a58ca6dadf..b3309301fd 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 bbb918123b..752fcc1b73 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 f3c738318d..46c4384e96 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-vercel-gateway/package.json b/packages/ai-vercel-gateway/package.json index 358a67df6e..5f4f480aeb 100644 --- a/packages/ai-vercel-gateway/package.json +++ b/packages/ai-vercel-gateway/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-vertex/package.json b/packages/ai-vertex/package.json index 01bd1ac2ff..cf927ea126 100644 --- a/packages/ai-vertex/package.json +++ b/packages/ai-vertex/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 64ea5e45d7..ec29b072f2 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 0b9d92c66e..8b8d6da61b 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 7750bbf783..e240a5ff4c 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -72,9 +72,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 bdb1e0b9e9..646d74bde5 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 5d0bc3ee25..39e567fc18 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 a2951c2ad0..56a70f48f8 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 70ce24ca58..35f2205e6f 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 0000000000..b0e01278be --- /dev/null +++ b/scripts/coverage-check.mjs @@ -0,0 +1,286 @@ +// Compares coverage between two runs and fails when a package regressed. +// +// Both sides are measured in the same CI job — the PR head and its merge-base +// with main — so there is no baseline file to keep in sync, no per-platform +// skew, and packages can be added or removed without anyone updating a +// checked-in number. +// +// Usage: +// node scripts/coverage-check.mjs --collect [--expect pkg,pkg] +// node scripts/coverage-check.mjs --base --head +import { + appendFileSync, + mkdirSync, + readFileSync, + readdirSync, + writeFileSync, +} from 'node:fs' +import { join } from 'node:path' + +const METRICS = ['statements', 'branches', 'functions', 'lines'] + +// Coverage percentages wobble slightly between runs (v8 attributes some bytes +// differently depending on JIT timing), so require a real drop. +const TOLERANCE = 0.5 + +function arg(name) { + const i = process.argv.indexOf(name) + return i === -1 ? undefined : process.argv[i + 1] +} + +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]), + ) +} + +function packageDirs() { + const dirs = {} + for (const entry of readdirSync('packages', { withFileTypes: true })) { + if (!entry.isDirectory()) continue + dirs[entry.name] = entry.name + try { + const pkg = JSON.parse( + readFileSync(join('packages', entry.name, 'package.json'), 'utf8'), + ) + if (pkg.name) dirs[pkg.name] = entry.name + } catch { + // Directory with no package.json is still collectable by folder name. + } + } + return dirs +} + +function expectedDirs() { + const raw = arg('--expect') + if (raw === undefined) return undefined + const names = raw + .split(',') + .map((name) => name.trim()) + .filter(Boolean) + const dirs = packageDirs() + const resolved = [] + const unknown = [] + for (const name of names) { + const dir = dirs[name] + if (!dir) unknown.push(name) + else resolved.push(dir) + } + if (unknown.length > 0) { + console.error(`Unknown package(s): ${unknown.join(', ')}`) + process.exit(1) + } + return resolved +} + +function readSummary(dirName) { + try { + return JSON.parse( + readFileSync( + join('packages', dirName, 'coverage', 'coverage-summary.json'), + 'utf8', + ), + ) + } catch (error) { + if (error && error.code === 'ENOENT') return null + throw error + } +} + +function collect(dir) { + mkdirSync(dir, { recursive: true }) + const expected = expectedDirs() + const names = + expected ?? + readdirSync('packages', { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + const missing = [] + let n = 0 + for (const name of names) { + const summary = readSummary(name) + if (!summary) { + if (expected) missing.push(`${name} (no coverage-summary.json)`) + continue + } + const values = totals(summary) + if (!values) { + if (expected) missing.push(`${name} (0 statements)`) + continue + } + writeFileSync(join(dir, `${name}.json`), JSON.stringify(values)) + n++ + } + if (missing.length > 0) { + console.error( + `Coverage collect failed for ${missing.length} package(s):\n ${missing.join('\n ')}`, + ) + process.exit(1) + } + console.log(`Collected ${n} coverage summary/summaries into ${dir}`) +} + +function read(dir) { + let files + try { + files = readdirSync(dir) + } catch (error) { + if (error && error.code === 'ENOENT') return {} + throw error + } + return Object.fromEntries( + files + .filter((file) => file.endsWith('.json')) + .map((file) => [ + file.replace(/\.json$/, ''), + JSON.parse(readFileSync(join(dir, file), 'utf8')), + ]), + ) +} + +const collectDir = arg('--collect') +if (collectDir) { + collect(collectDir) + process.exit(0) +} + +const baseDir = arg('--base') +const headDir = arg('--head') +if (!baseDir || !headDir) { + console.error( + 'Usage: coverage-check.mjs --collect [--expect pkg,pkg] | --base --head ', + ) + process.exit(2) +} + +const base = read(baseDir) +const head = read(headDir) +const names = Object.keys(head).sort() + +if (names.length === 0) { + console.error('No packages were measured — nothing to compare.') + process.exit(1) +} + +const regressions = [] +const additions = [] +const rows = [] + +for (const name of names) { + const before = base[name] + const after = head[name] + if (!before) { + additions.push(name) + rows.push([ + name, + ...METRICS.map((metric) => `${after[metric].toFixed(2)}%`), + 'new', + ]) + continue + } + const deltas = METRICS.map((metric) => after[metric] - before[metric]) + const dropped = METRICS.filter((metric, i) => deltas[i] < -TOLERANCE) + if (dropped.length > 0) regressions.push({ name, before, after, dropped }) + rows.push([ + name, + ...METRICS.map((metric, i) => { + const sign = deltas[i] > 0 ? '+' : '' + return `${after[metric].toFixed(2)}% (${sign}${deltas[i].toFixed(2)})` + }), + dropped.length > 0 ? 'DROP' : 'ok', + ]) +} + +for (const name of Object.keys(base) + .filter((name) => !head[name]) + .sort()) { + const before = base[name] + regressions.push({ name, before, after: null, dropped: ['missing'] }) + rows.push([ + name, + ...METRICS.map((metric) => `missing (${before[metric].toFixed(2)}%)`), + 'DROP', + ]) +} + +const compared = names.filter((name) => base[name]).length + +const header = ['package', ...METRICS, ''] +const widths = header.map((_, column) => + Math.max(header[column].length, ...rows.map((row) => row[column].length)), +) +const line = (cells) => + cells + .map((cell, i) => cell.padEnd(widths[i])) + .join(' ') + .trimEnd() + +console.log(line(header)) +console.log(widths.map((width) => '-'.repeat(width)).join(' ')) +for (const row of rows) console.log(line(row)) + +if (additions.length > 0) { + console.log( + `\n${additions.length} package(s) had no coverage on the base commit` + + ` (new, or unmeasurable there): ${additions.join(', ')}`, + ) +} + +const headline = + regressions.length > 0 + ? `❌ Coverage dropped in ${regressions.length} package(s).` + : compared > 0 + ? `✅ Coverage held across ${compared} compared package(s).` + : `${additions.length} package(s) are new; nothing to compare against.` + +if (process.env.GITHUB_STEP_SUMMARY) { + const md = [ + `## Coverage`, + ``, + headline, + ``, + `Each package is measured twice in this job — on this PR and on its merge-base` + + ` with \`main\` — and compared. A drop of more than ${TOLERANCE}pp in any metric fails.` + + ` Packages your PR doesn't affect are not measured and not listed.`, + ``, + `| package | ${METRICS.join(' | ')} | |`, + `| --- | ${METRICS.map(() => '---:').join(' | ')} | --- |`, + ...rows.map((row) => `| ${row.join(' | ')} |`), + ] + if (additions.length > 0) { + md.push( + ``, + `> No coverage on the base commit, so nothing to compare against:` + + ` ${additions.join(', ')}.`, + ) + } + appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${md.join('\n')}\n`) +} + +if (regressions.length > 0) { + console.error(`\nCoverage dropped in ${regressions.length} package(s):`) + for (const { name, before, after, dropped } of regressions) { + if (!after) { + console.error(` ${name}: measured on base, missing on this PR`) + continue + } + for (const metric of dropped) { + console.error( + ` ${name} ${metric}: ${before[metric].toFixed(2)}% -> ${after[metric].toFixed(2)}%`, + ) + } + } + console.error(`\nAdd tests covering the code this PR changed.`) + process.exit(1) +} + +if (compared === 0) { + console.log( + `\n${additions.length} package(s) are new; nothing to compare against.`, + ) +} else { + console.log(`\nCoverage held across ${compared} compared package(s).`) +} diff --git a/scripts/coverage-check.test.ts b/scripts/coverage-check.test.ts new file mode 100644 index 0000000000..beeaa6662d --- /dev/null +++ b/scripts/coverage-check.test.ts @@ -0,0 +1,236 @@ +import { spawnSync } from 'node:child_process' +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { afterEach, describe, expect, it } from 'vitest' + +const SCRIPT = fileURLToPath(new URL('./coverage-check.mjs', import.meta.url)) + +const temps: Array = [] + +afterEach(() => { + for (const dir of temps.splice(0)) + rmSync(dir, { recursive: true, force: true }) +}) + +function tmp() { + const dir = mkdtempSync(join(tmpdir(), 'cov-check-')) + temps.push(dir) + return dir +} + +function run( + args: Array, + opts: { cwd?: string; env?: NodeJS.ProcessEnv } = {}, +) { + const env: NodeJS.ProcessEnv = { ...process.env, ...opts.env } + if (!opts.env || !('GITHUB_STEP_SUMMARY' in opts.env)) { + delete env.GITHUB_STEP_SUMMARY + } + return spawnSync(process.execPath, [SCRIPT, ...args], { + encoding: 'utf8', + cwd: opts.cwd, + env, + }) +} + +function metrics(pct: number) { + return { statements: pct, branches: pct, functions: pct, lines: pct } +} + +function writeSnapshot(dir: string, name: string, pct: number) { + mkdirSync(dir, { recursive: true }) + writeFileSync(join(dir, `${name}.json`), JSON.stringify(metrics(pct))) +} + +function vitestSummary(pct: number, total = 10) { + const cell = { total, covered: 1, pct } + return JSON.stringify({ + total: { + statements: cell, + branches: cell, + functions: cell, + lines: cell, + }, + }) +} + +function writePkg( + root: string, + dirName: string, + summary: string, + pkgName = `@tanstack/${dirName}`, +) { + mkdirSync(join(root, 'packages', dirName, 'coverage'), { recursive: true }) + writeFileSync( + join(root, 'packages', dirName, 'package.json'), + JSON.stringify({ name: pkgName }), + ) + writeFileSync( + join(root, 'packages', dirName, 'coverage', 'coverage-summary.json'), + summary, + ) +} + +describe('usage', () => { + it('exits 2 when --base or --head is missing', () => { + const r = run([]) + expect(r.status).toBe(2) + expect(r.stderr).toMatch(/Usage:/) + }) +}) + +describe('compare', () => { + it('passes a 0.4pp drop and a 0.5pp drop, fails 0.51pp', () => { + const base = tmp() + const head = tmp() + writeSnapshot(base, 'ok', 90) + writeSnapshot(head, 'ok', 89.5) + expect(run(['--base', base, '--head', head]).status).toBe(0) + + writeSnapshot(head, 'ok', 89.6) + expect(run(['--base', base, '--head', head]).status).toBe(0) + + writeSnapshot(head, 'ok', 89.49) + const r = run(['--base', base, '--head', head]) + expect(r.status).toBe(1) + expect(r.stderr).toMatch(/ok statements: 90\.00% -> 89\.49%/) + expect(r.stdout).toMatch(/DROP/) + }) + + it('fails when only branches drop more than 0.5pp', () => { + const base = tmp() + const head = tmp() + mkdirSync(base, { recursive: true }) + mkdirSync(head, { recursive: true }) + writeFileSync( + join(base, 'pkg.json'), + JSON.stringify({ + statements: 90, + branches: 80, + functions: 90, + lines: 90, + }), + ) + writeFileSync( + join(head, 'pkg.json'), + JSON.stringify({ + statements: 90, + branches: 79, + functions: 90, + lines: 90, + }), + ) + const r = run(['--base', base, '--head', head]) + expect(r.status).toBe(1) + expect(r.stderr).toMatch(/pkg branches: 80\.00% -> 79\.00%/) + expect(r.stderr).not.toMatch(/statements/) + }) + + it('treats a head-only package as new and exits 0', () => { + const base = tmp() + const head = tmp() + mkdirSync(base, { recursive: true }) + writeSnapshot(head, 'ai-new', 12) + const r = run(['--base', base, '--head', head]) + expect(r.status).toBe(0) + expect(r.stdout).toMatch(/\bnew\b/) + expect(r.stdout).toMatch(/nothing to compare against/) + expect(r.stdout).not.toMatch(/Coverage held/) + }) + + it('fails when a package was measured on base but not on head', () => { + const base = tmp() + const head = tmp() + writeSnapshot(base, 'ai-old', 90) + writeSnapshot(head, 'other', 90) + const r = run(['--base', base, '--head', head]) + expect(r.status).toBe(1) + expect(r.stderr).toMatch(/ai-old: measured on base, missing on this PR/) + }) + + it('fails when head measured nothing', () => { + const base = tmp() + const head = tmp() + mkdirSync(base, { recursive: true }) + mkdirSync(head, { recursive: true }) + const r = run(['--base', base, '--head', head]) + expect(r.status).toBe(1) + expect(r.stderr).toMatch(/nothing to compare/) + }) + + it('writes the step summary on a failing run', () => { + const base = tmp() + const head = tmp() + const summary = join(tmp(), 'summary.md') + writeSnapshot(base, 'pkg', 90) + writeSnapshot(head, 'pkg', 80) + const r = run(['--base', base, '--head', head], { + env: { ...process.env, GITHUB_STEP_SUMMARY: summary }, + }) + expect(r.status).toBe(1) + const md = require('node:fs').readFileSync(summary, 'utf8') as string + expect(md).toMatch(/Coverage dropped/) + expect(md).toMatch(/pkg/) + }) +}) + +describe('collect', () => { + it('writes pct-only snapshots and skips packages with no summary', () => { + const root = tmp() + const out = join(root, 'out') + writePkg(root, 'ai', vitestSummary(90)) + mkdirSync(join(root, 'packages', 'empty'), { recursive: true }) + writeFileSync( + join(root, 'packages', 'empty', 'package.json'), + JSON.stringify({ name: '@tanstack/empty' }), + ) + const r = run(['--collect', out], { cwd: root }) + expect(r.status).toBe(0) + const written = JSON.parse( + require('node:fs').readFileSync(join(out, 'ai.json'), 'utf8'), + ) + expect(written).toEqual(metrics(90)) + expect(r.stdout).toMatch(/Collected 1 /) + }) + + it('fails --expect when a named package has no summary', () => { + const root = tmp() + const out = join(root, 'out') + writePkg(root, 'ai', vitestSummary(90)) + const r = run(['--collect', out, '--expect', 'ai,missing-pkg'], { + cwd: root, + }) + expect(r.status).toBe(1) + expect(r.stderr).toMatch(/missing-pkg/) + }) + + it('fails --expect when statements.total is 0', () => { + const root = tmp() + const out = join(root, 'out') + writePkg(root, 'empty', vitestSummary(100, 0)) + const r = run(['--collect', out, '--expect', 'empty'], { cwd: root }) + expect(r.status).toBe(1) + expect(r.stderr).toMatch(/0 statements/) + }) + + it('resolves @tanstack scoped names to the package directory', () => { + const root = tmp() + const out = join(root, 'out') + writePkg( + root, + 'ai-devtools', + vitestSummary(81), + '@tanstack/ai-devtools-core', + ) + const r = run( + ['--collect', out, '--expect', '@tanstack/ai-devtools-core'], + { cwd: root }, + ) + expect(r.status).toBe(0) + expect(require('node:fs').existsSync(join(out, 'ai-devtools.json'))).toBe( + true, + ) + }) +})