All jobs

Add Test Coverage Reporting to Nuxt CI

Yes. An agent runs your Nuxt unit tests with v8 coverage, adds a threshold and a coverage artifact to the GitHub Actions workflow, and opens a pull request.

Run this on RushIn a Project that has your repository.

What the agent does

The agent starts from what the repository already has. In a Nuxt app shaped like nuxt/movies, package.json defines test:unit as vitest, test:unit-coverage as vitest --coverage and test:unit-web as vitest --coverage --ui, with @vitest/coverage-v8 4.1.11 pinned beside vitest 4.1.11 and @nuxt/test-utils 4.2.0 in devDependencies. vitest.config.ts uses defineVitestConfig from @nuxt/test-utils/config, names the project unit, includes **/tests/unit/**/*.test.ts plus **/components/**/*.test.ts and **/composables/**/*.test.ts, and already has a coverage.include of **/components/**/*.vue and **/composables/**/*.ts. The tests exist: 13 *.nuxt.test.ts files sit next to the components and composables under app/, such as app/components/StarsRate.nuxt.test.ts, which mounts the component with @vue/test-utils and asserts the clip-path of the filled stars image. What is missing is the number and the gate.

.github/workflows/tests-unit.yml checks out the code, sets up pnpm through pnpm/setup with caching, runs pnpm run test:unit, then pnpm run lint and pnpm run typecheck. No coverage is collected and nothing is uploaded, while the sibling tests-e2e.yml already uploads a playwright-report artifact with a 30-day retention. The agent runs pnpm install with the pnpm version devEngines pins, then pnpm run test:unit-coverage in the sandbox and reads the total line, branch and function percentages from the v8 summary for the included .vue and .ts files.

With those numbers it edits two files. In vitest.config.ts it adds coverage.reporter set to text, lcov and json-summary and a coverage.thresholds block with the measured figures rounded down, so the suite fails when coverage drops below where it is today rather than at an aspirational number nobody has reached. In tests-unit.yml it changes the test step to pnpm run test:unit-coverage and adds an actions/upload-artifact step for the coverage/ directory, pinned by commit hash the way the workflow pins actions/checkout and pnpm/setup, with a retention matching the e2e workflow. The coverage.include list stays as it is, so app/pages/ and server/api/index.ts, which have no unit tests, do not drag the number down.

It runs pnpm run lint and pnpm run typecheck as the workflow does, because eslint.config.js extends @antfu/eslint-config and will object to a stray comma in the Vitest config, and it adds coverage/ to .gitignore if it is not there. The pull request carries the measured numbers in its description and the checks from the edited workflow run against it.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "claude",
  "input": "Add coverage reporting to this Nuxt project's CI. Read package.json, vitest.config.ts, .github/workflows/tests-unit.yml and .github/workflows/tests-e2e.yml first. Run pnpm install and pnpm run test:unit-coverage in the sandbox and record the line, branch, function and statement totals for the files coverage.include already names. In vitest.config.ts add coverage.reporter with text, lcov and json-summary, and coverage.thresholds set to the measured totals rounded down to whole numbers; do not change coverage.include. In tests-unit.yml switch the test step to pnpm run test:unit-coverage and add an upload-artifact step for the coverage directory, pinned by commit hash like the other actions, with the same retention the e2e workflow uses. Add coverage/ to .gitignore if missing. Run pnpm run lint and pnpm run typecheck. Open a pull request whose description quotes the measured totals and explains that the thresholds hold the current level. Do not write new tests and do not change any test file.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
  1. Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
  2. Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
  3. Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

What comes back

What comes back is a pull request on a new branch that changes vitest.config.ts, .github/workflows/tests-unit.yml and, if needed, .gitignore. The description quotes the totals the sandbox measured for the components and composables, states the thresholds that were set from them, and shows the upload-artifact step with its pinned hash. No test file changes and no dependency is added, because @vitest/coverage-v8 is already in package.json.

The session read names the repository and the pull request URL; the transcript keeps the test:unit-coverage run with its coverage table, and the lint and typecheck runs. Once the pull request's own workflow runs, the coverage artifact on that run is the first one the project has.

Before merging, a reviewer compares the thresholds against the quoted totals, decides whether to round down by a point or two to leave room for refactors, and checks that coverage.include still excludes the pages, which would otherwise show as untested and fail the gate immediately.

Run it on every new Nuxt repository

Start one run per repository from your own script with this body and resources pointing at the new repository, with a request_id, one UUID kept per repository, so a retry returns the same run. Read the pull request URL from the session when status leaves running, or from the run.completed webhook if the key has a webhook_url.

The same request also fits a scratch run from a terminal when someone notices a workflow with no coverage step: send it, keep working, and come back to the pull request. The sandbox parks after the turn, so a follow-up message such as "raise the branch threshold by five points and tell me which files fall short" continues in the same clone.

The use case this job follows, with its automation and trigger: Write the missing tests.

Why run it on Rush?

Measuring coverage for a Nuxt app means a pnpm install at the pinned version, a nuxt prepare to generate the .nuxt/ tsconfig references the project's tsconfig.json points at, and a Vitest run through @nuxt/test-utils that boots a Nuxt environment for every .nuxt.test.ts file. On Rush that happens in the run's own sandbox with the output kept, so the threshold in the pull request comes from a run anyone can read, not from a number typed from memory.

The result is a pull request with the measurement behind it and the checks from the edited workflow, and several repositories can get the same treatment at once, each in its own sandbox. When a threshold later fails, the coverage artifact on that workflow run says which file moved.

Frequently asked questions

Where does the threshold number come from?

From running pnpm run test:unit-coverage in the sandbox on your current code. The agent rounds each total down and writes it into coverage.thresholds, so the gate holds the level you have today; the pull request description quotes the raw numbers so you can lower them before merging.

Will it write tests to raise coverage?

No. This run only measures and gates. Writing tests for the components under app/components/media/ that have none is a separate request, and running it after this one gives the new tests a threshold to move.

Does it need Codecov or another service?

No. The lcov and json-summary reporters write to coverage/, and the workflow uploads that directory as an artifact on each run. If you later add a service, its action reads the same lcov.info.

Why keep the coverage.include list unchanged?

It already limits the measurement to components and composables, the parts the 13 unit tests exercise. Widening it to app/pages/ would count server-rendered pages with no unit tests and fail the new threshold on the first run.