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 4e8635c694..399cff6617 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,6 +153,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 6dee2e3d76..a86dbbdb00 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,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 @@ -319,7 +319,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 ee03bd6b15..67fee1c0f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,7 +77,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` | @@ -89,6 +89,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). @@ -113,7 +138,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) @@ -173,10 +198,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. + ### Automated Grok review A Grok agent comments on open, non-draft PRs. The first lines of that comment say it is automated. It is not a maintainer review. diff --git a/package.json b/package.json index 77d9e58bd0..d5b8a0ecec 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", "test:ai-review": "vitest run --root agent-scripts/ai-review", "ai-review": "tsx agent-scripts/ai-review/run.ts", "maintainer:sweep": "tsx scripts/maintainer/sweep.ts", diff --git a/packages/ai-acp/package.json b/packages/ai-acp/package.json index 1219138c54..93944c1c4d 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 2935c73a99..da9eadb2e3 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 8a8d86bd9d..1643e9b96d 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 3f6a81892e..c9cf22e8e3 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 aecf32e4e8..75534a4f1d 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 ad75e4eb05..f762cf96b4 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 0831d0e7a0..b0ce64d42e 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 830c7803b7..4467073318 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 29267ef032..6eaf35efbd 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 0c474d5e0b..7a10683593 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 43f4a048ad..a791970d49 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 b55453b46f..f06174e22b 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 5784c05bc3..ef964de8d8 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 3e7088a91e..fb1df1f3c2 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 411dd0433b..d95ce2bd2d 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 4dc62e1e45..54ebb9e778 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 98309ec7af..cb16a3e610 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 f64c576395..60be1c785f 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 4e138a2481..a84aec9642 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 000cd5b95e..61834c1cbd 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 d4626e0b0d..5797a5341d 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 af2b215910..503db2f533 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 e1bccda116..69d1e6b107 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 6f2981348e..28f85c9589 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 8c9b7d57dc..8bc48febe8 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 aa30d0904c..b78c2ef636 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 f11498e5d7..abf009c518 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 2fb2cfe307..c52f71aa11 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 13ca18ab9c..0c27f1b329 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 72551b64c7..c7b9a7c3a8 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 bb720c29c1..2903bc2e20 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 1053d61907..60c1b7f89c 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 abe8955b9b..49114f6b57 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 cb1c9377d4..adb03bacf2 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 922a4cbcd3..9fa6f9299a 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 59099d3a8e..7343aef22d 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 1c9f6a4b88..3ea86a43d2 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 9783088bdb..3d77a83da0 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 11eee71a7e..80d6d6e665 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 ad8231d710..138058ea4b 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 998dd3c84f..686c748b87 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 21b0ce38c0..ebe2392178 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, + ) + }) +})