Skip to content

ci: make PR checks the enforced source of truth with coverage ratchets - #105

Merged
LukasParke merged 2 commits into
mainfrom
chore/bulletproof-coverage-gate
Aug 10, 2026
Merged

ci: make PR checks the enforced source of truth with coverage ratchets#105
LukasParke merged 2 commits into
mainfrom
chore/bulletproof-coverage-gate

Conversation

@LukasParke

Copy link
Copy Markdown
Contributor

Summary

Makes CI checks on PRs the actual gate they were pretending to be. Previously coverage was collected but nothing enforced it, the most critical file (tool-orchestrator.ts) had 0% coverage, and the e2e job silently passed whenever the API key was missing.

What changes

1. Coverage thresholds, pinned and enforced

  • Both packages' vitest configs now declare v8 thresholds; any PR that lowers coverage fails the unit-tests job (verified: exit 1 on breach).
  • Floors are pinned at current levels — a ratchet, so coverage can only go up:
Package Stmts Branch Funcs Lines Thresholds
agent 83.0% 74.6% 88.6% 82.9% 82 / 74 / 88 / 82
mcp 93.4% 82.4% 94.9% 93.5% 90 / 80 / 92 / 90
  • Test files (src/**/*.test.ts) excluded from coverage accounting.
  • CI posts a per-package coverage table to the job summary and uploads HTML/JSON reports as artifacts.

2. +76 unit tests closing the worst gaps (@openrouter/agent)

File Before After
tool-orchestrator.ts (the tool execution loop, public API) 0% 89%
claude-type-guards.ts 0% 100%
stop-conditions.ts 32% (0% branch) 100%
next-turn-params.ts 34% 100%
turn-context.ts 40% 100%

Orchestrator tests cover: multi-round loops, unknown/manual/failing tools, HITL null-pauses, background/deferred lifecycle rejection (asserting run is never invoked), generator preliminary-result forwarding, and nextTurnParams conversation steering.

3. CI workflow hardening

  • E2E no longer fails open: missing OPENROUTER_API_KEY is now a hard error — except on fork PRs, where secrets are legitimately unavailable (skip with warning).
  • New triggers: push to main + nightly schedule (full suite incl. live e2e).
  • New ci-status aggregator job: point branch protection at this single check. The required set lives in one needs: list, so a renamed or deleted job can never leave the gate green, and adding a check automatically extends it.
  • permissions: contents: read, PR concurrency cancellation.

4. Drive-by fix

Removed a write-only steered variable in agent-tool.test.ts that was failing biome — the lint gate on main is currently red, which this PR also fixes.

Verification

  • pnpm lint, pnpm typecheck, pnpm test all green (909 agent + 168 mcp tests).
  • Threshold breach confirmed to fail the run (exit 1).
  • ci.yaml parses; coverage-summary script output verified.

Not done (deliberately)

  • E2E tests still hit the live OpenRouter API — converting them to hermetic/recorded tests is a follow-up. They're now at least honest: required when secrets exist, explicitly skipped only on forks.
  • Branch protection settings (ci-status as required check) must be flipped in repo settings after merge.

Remaining known gaps

  • stream-transformers.ts (50%, 38% branch) and stream-type-guards.ts (69%) are the next biggest under-tested areas.
  • The allSettled rejection path in executeToolLoop is effectively unreachable via public APIs (executor converts failures to error results) — noted in tests rather than covered with mocks.

Coverage enforcement:
- Pin v8 coverage thresholds in both packages' vitest configs
  (agent: 82/74/88/82, mcp: 90/80/92/90 for stmts/branches/funcs/lines).
  Any PR that lowers coverage now fails the unit-test job.
- Exclude src/**/*.test.ts from coverage accounting.
- Emit json-summary + html reports; CI uploads them as artifacts and
  posts a per-package coverage table to the job summary.

New unit tests (+76) closing the biggest gaps in @openrouter/agent:
- tool-orchestrator.ts: 0% -> 89% (executeToolLoop rounds, unknown/manual/
  failing tools, HITL null-pauses, async-lifecycle rejection, generator
  preliminary results, nextTurnParams steering, result helpers)
- claude-type-guards.ts: 0% -> 100% (isClaudeStyleMessages detection and
  rejection paths)
- stop-conditions.ts: 32% -> 100% (all conditions incl. OR logic, async
  conditions, rejection propagation)
- next-turn-params.ts: 34% -> 100% (context building, composition order,
  invalid keys, non-object arguments)
- turn-context.ts: 40% -> 100%

CI hardening (.github/workflows/ci.yaml):
- e2e job no longer silently passes without OPENROUTER_API_KEY: it fails
  loudly, except on fork PRs where secrets are legitimately unavailable.
- Add push-to-main and nightly schedule triggers.
- Add a ci-status aggregator job as the single branch-protection check;
  the required set is defined in one 'needs' list so a renamed or
  deleted job can never leave the gate green.
- Add permissions: contents: read and PR concurrency cancellation.

Also removes a write-only 'steered' variable in agent-tool.test.ts that
was failing biome on main (lint gate was red before this change).
Coverage is a unit-test gate; the e2e project exercises a thin slice of
src against the live API and can never meet the pinned thresholds.
Pass --coverage.enabled=false in both packages' test:e2e scripts.
@LukasParke
LukasParke marked this pull request as ready for review August 10, 2026 19:01
@LukasParke
LukasParke merged commit 3ace074 into main Aug 10, 2026
6 of 7 checks passed

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment thread .github/workflows/ci.yaml
Comment on lines +166 to +170
ci-status:
name: CI status
if: always()
needs: [lint, typecheck, unit-tests, e2e-tests, structural-gate]
runs-on: ubuntu-latest

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The new single required check has a different name than the one maintainers are told to require

The aggregated check is published under the display name "CI status" (name: CI status at .github/workflows/ci.yaml:167) while the workflow comment and PR instructions tell maintainers to require it as "ci-status", so a required check with that name would never report and every pull request would stay blocked as pending.
Impact: If branch protection is configured exactly as instructed, all PRs become permanently unmergeable because the required check never appears.

Job id vs job display name in GitHub required status checks

GitHub creates the check run using the job's name when one is set, falling back to the job id otherwise. Here the job id is ci-status but name: CI status overrides the reported check name, so the status check visible to branch protection is CI status.

The comment at .github/workflows/ci.yaml:162-165 says: Point branch protection at THIS job ("ci-status"). Selecting ci-status in branch protection (it can be typed in manually even if not in the dropdown) yields a required check that is never reported → PRs remain in a pending state forever.

Either drop the name: key so the check is literally ci-status, or update the comment to reference CI status.

Suggested change
ci-status:
name: CI status
if: always()
needs: [lint, typecheck, unit-tests, e2e-tests, structural-gate]
runs-on: ubuntu-latest
ci-status:
if: always()
needs: [lint, typecheck, unit-tests, e2e-tests, structural-gate]
runs-on: ubuntu-latest
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant