Skip to content

ci: enforce the ≥90% per-file coverage gate in CI #1550

Closed

Description

@cliffhall

Summary

Wire the per-file coverage gate into CI so that a PR fails when any file falls below the ≥ 90% threshold on all four dimensions (lines, statements, functions, branches), as directed in AGENTS.md.

Background

Per AGENTS.md, each client's test:coverage already enforces a uniform per-file gate of ≥ 90 on lines, statements, functions, and branches across clients/web, clients/cli, clients/tui, and clients/launcher. The aggregate npm run coverage runs the gate (and, for web, the integration project).

However, CI does NOT currently run coverage — the gate is local-only. CI runs validate (fast — format:check + lint + build + test, no coverage) plus a standalone web test:integration step and smoke. This means a PR can drop a file below 90% coverage without CI catching it; the gate is only enforced if a contributor remembers to run npm run coverage locally before pushing.

Goal

Add a CI step that runs the coverage gate so a PR is blocked if coverage drops below 90% across the board.

Acceptance criteria

  • CI runs the per-file coverage gate (e.g. npm run coverage, or per-client coverage:*) on PRs.
  • A PR that drops any file below 90% on lines, statements, functions, or branches fails CI.
  • The gate covers all four clients (web, cli, tui, launcher) consistent with the local coverage target.
  • Decide whether this replaces or supplements the existing validate + standalone test:integration CI steps (the web test:coverage already includes the integration project), and avoid running the integration suite twice.
  • Update AGENTS.md to reflect that the coverage gate now runs in CI (the doc currently states "CI does NOT run coverage — the gate is local-only").

Notes

  • Watch CI runtime: coverage is slower than validate (adds v8 instrumentation + the web integration project). Consider caching / parallelizing per-client jobs.
  • smoke:tui self-skips under CI (needs a real TTY) — unrelated to coverage but keep that constraint in mind when restructuring CI jobs.

Activity

  1. self-assigned this
    on Jun 29, 2026
  2. cliffhall commented on Jul 2, 2026

    @cliffhall
    MemberAuthor

    Smoke test & AGENTS.md audit — PR #1603

    Independent verification of the CI coverage gate (branch 1550-ci-coverage-gate, base 1579-wave-1, HEAD 61ca2200). The claim under test: CI now fails when per-file coverage drops below 90%. I confirmed the mechanism rather than trusting it.

    1. Workflow diff confirmation (.github/workflows/main.yml)

    • A step Enforce per-file coverage gate (≥90% on all four dimensions) running npm run coverage was ADDED.
    • The standalone Run web integration tests step (npm run test:integration) was REMOVED.
    • node-version: '22.x' unchanged; Validate, Run cross-client smokes (npm run smoke), and the Storybook play-function step all remain.
    • No double integration run: clients/web/package.json → test:coverage runs vitest run --project=unit --project=integration --coverage, so the integration project is already inside the gate — removing the standalone step is correct.
    • Scope: only two files changed — main.yml and AGENTS.md.

    2. CI-run evidence (the gate ran green)

    • Run id 28603853503 (workflow main.yml, headSha 61ca2200… = PR HEAD), overall conclusion success.
    • Step Enforce per-file coverage gate (≥90% on all four dimensions) → conclusion: success. All other steps (validate, smokes, Storybook) also success.

    3. Load-bearing demonstration (the key proof)

    Since CI runs npm run coverage, I proved that command fails on a coverage drop by temporarily injecting an untested exported function with dead branches into a gated source file (clients/web/src/lib/downloadFile.ts), then running the exact web gate npm run test:coverage. Not committed; reverted immediately.

    With injection → EXIT 1, explicit threshold errors:

      downloadFile.ts  |      75 |     64.7 |   85.71 |   74.07 | 77-84
    ERROR: Coverage for lines (74.07%) does not meet global threshold (90%) for src/lib/downloadFile.ts
    ERROR: Coverage for functions (85.71%) does not meet global threshold (90%) for src/lib/downloadFile.ts
    ERROR: Coverage for statements (75%) does not meet global threshold (90%) for src/lib/downloadFile.ts
    ERROR: Coverage for branches (64.7%) does not meet global threshold (90%) for src/lib/downloadFile.ts
    

    After revert → EXIT 0, zero threshold errors, downloadFile.ts back to 100 | 100 | 100 | 100.

    A coverage regression now fails the exact command CI runs (npm run coverage) → CI blocks the PR. Gate confirmed load-bearing.

    Injected probe (reverted, never committed)
    // Added above the ExportKind type in clients/web/src/lib/downloadFile.ts
    export function auditCoverageProbe(n: number): string {
      if (n > 100) {
        return "big";
      } else if (n > 10) {
        return "medium";
      } else if (n > 0) {
        return "small";
      }
      return "none";
    }

    Verification loop:

    # inject the fn above, then:
    cd clients/web && npm run test:coverage; echo "EXIT=$?"   # -> EXIT=1 + ERROR: ... threshold lines
    git checkout -- clients/web/src/lib/downloadFile.ts        # revert
    cd clients/web && npm run test:coverage; echo "EXIT=$?"   # -> EXIT=0, no threshold errors

    4. AGENTS.md accuracy audit

    All coverage-related claims were flipped correctly and no now-false statement remains:

    • "CI does NOT run coverage — the gate is local-only" → "CI runs coverage on every push (ci: enforce the ≥90% per-file coverage gate in CI #1550) … CI-enforced."
    • "Before pushing, also run npm run coverage" note updated: "CI runs coverage too (ci: enforce the ≥90% per-file coverage gate in CI #1550) … running it locally first just catches failures before CI does."
    • Both standalone test:integration CI references updated: "CI does not run test:integration as its own step — the integration project is covered by the CI coverage gate."
    • smoke section: "CI runs validate, then the coverage gate (which also covers the web integration project), then smoke."
    • Remaining local-only mention (smoke:tui self-skips under CI) is unrelated to coverage and still accurate.

    TypeScript / Mantine: N/A — this PR is CI config + docs only, no product/TS/React source touched.

    Verdict: PASS

    Both conditions met: (1) the gate command (npm run coverage / web test:coverage) fails with exit 1 + explicit threshold errors on an injected coverage drop and passes clean on revert, and (2) the actual CI run 28603853503 is green with the coverage-gate step = success. Scope is limited to main.yml + AGENTS.md; AGENTS.md edits are accurate. Working tree left clean, injection reverted, no commits.

  3. added 2 commits that reference this issue on Jul 28, 2026
    61ca220
    01cc79c
  4. added this to the v2.0.0 milestone on Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

v2Issues and PRs for v2

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions