Skip to content

Run the test suite against TypeScript sources behind an uncheatable coverage gate - #44

Merged
unbraind merged 5 commits into
mainfrom
coverage-gate-typescript-sources
Jul 29, 2026
Merged

Run the test suite against TypeScript sources behind an uncheatable coverage gate#44
unbraind merged 5 commits into
mainfrom
coverage-gate-typescript-sources

Conversation

@unbraind

@unbraind unbraind commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Why

Coverage in this package was never measured. Tests imported the compiled output from dist/, so the only obtainable numbers pointed at generated JavaScript offsets rather than lines an author can act on, and no threshold was wired into any gate.

What changed

Tests run against the TypeScript source. Node has stripped types without a flag since 22.18.0, the floor this package already declares in engines.

scripts/coverage-gate.ts is the gate. It enforces per-dimension thresholds and reconciles the reported file list against a walk of the source tree:

Node omits files that never load from its coverage report entirely, rather than reporting them at zero. A threshold flag alone is therefore satisfiable by narrowing what the suite imports — a package can show 100% while an entire module goes unexercised.

The gate carries four further hardenings, each from a review finding on the sibling PRs in this rollout: the source list is walked dynamically rather than frozen (so a module added later is required automatically); TZ=UTC is pinned when spawning the runner (coverage was otherwise a property of the host's timezone); the previous lcov report is deleted first (a run producing no report was otherwise reconciled against the last one's file); and coverageGate.ignore entries are validated against their compiled output, since a type-only module emits export {}; and nothing else — that exemption was previously the one supported way to remove executable code from both the measured and the required set.

The test tree stays typechecked. coverage chains build:test, since release:check no longer runs npm test.

Thresholds ratchet. Pinned in package.json under coverageGate. CI runs the gate on a Node 22 + 26 matrix.

Measured baseline

63.21% line / 76.46% branch / 77.40% function over the single source module.

Line and function coverage are the weakest in this package and are the target of the follow-up work toward the mandated 100.

Verification

  • npm run release:check — passes end to end (typecheck, coverage gate, prod audit, pack dry-run, changelog check)
  • Negative control — an untested file under src/ makes the gate exit 1 naming it; exit 0 again once removed

CI legs are unobserved locally and are confirmed by this PR.

pm items

  • pm-graph-5cu7 — Run the test suite against TypeScript sources behind an uncheatable coverage gate

Summary by Sourcery

Run tests directly against TypeScript sources under a strict, configuration-driven coverage gate and wire it into local scripts and CI.

New Features:

  • Add a configurable coverage gate script that runs Node’s built-in test coverage against TypeScript sources and enforces line, branch, and function thresholds.
  • Configure coverage sources, test patterns, thresholds, and ignore list in package.json for explicit, reviewable coverage policy.

Enhancements:

  • Update tests to import from the TypeScript source entrypoint instead of the compiled dist output for author-meaningful coverage data.
  • Adjust TypeScript compiler options to support importing .ts extensions directly in tests.
  • Tighten the release:check workflow to run the coverage gate instead of plain tests and record the change in the changelog.

Build:

  • Add a coverage npm script that builds sources and tests before invoking the coverage gate script.

CI:

  • Extend the CI test job to run the coverage gate on a Node 22 and 26 matrix instead of a single Node version.

Summary by cubic

Run tests against TypeScript sources and add a coverage gate that enforces thresholds and fails if any src file isn’t loaded. CI runs the gate on Node 22 and 26. Meets pm-graph-5cu7 acceptance criteria.

  • New Features

    • Tests import src/*.ts directly; Node 22+ runs .ts natively.
    • scripts/coverage-gate.ts enforces thresholds and requires all src files via a dynamic walk.
    • Hardenings: pins TZ=UTC, removes stale coverage/lcov.info, exits with runner status first, strips block comments for type‑only checks, and resolves effective tsconfig via tsc --showConfig (fails closed if it cannot be resolved).
    • Adds npm run coverage; wired into release:check and CI; thresholds pinned in package.json (63 lines / 76 branches / 77 functions).
  • Migration

    • Use npm run coverage locally; npm test still runs .ts tests directly.
    • If a file is reported missing, add a test or exempt truly type-only modules in coverageGate.ignore.
    • Requires Node >=22.

Written for commit 31a620e. Summary will update on new commits.

Review in cubic

…overage gate

Coverage was unmeasured here. The suite imported the compiled output from
dist/, so the only numbers obtainable pointed at generated JavaScript offsets
rather than lines an author edits, and no threshold was wired into any gate.

Tests now import the TypeScript source directly. Node has stripped types
without a flag since 22.18.0, the floor this package already declares in
engines, so this needs no new tooling.

The gate is scripts/coverage-gate.ts. Beyond enforcing per-dimension
thresholds it reconciles the reported file list against a walk of the source
tree, because Node omits files that never load from its coverage report
entirely instead of reporting them at zero - a threshold alone is therefore
satisfiable by narrowing what the suite imports. The walk is dynamic, so a
module added later is required automatically. It also pins TZ=UTC so the
measurement is machine-independent, chains build:test so the test tree and the
gate script stay typechecked now that release:check no longer runs npm test,
deletes any previous lcov report so a run producing none cannot pass on stale
data, and rejects an `ignore` entry whose compiled output contains runtime
code.

Thresholds are pinned at the measured baseline (63 line, 76 branch, 77
function) so the number can only ratchet up. CI runs the gate on Node 22 and
26.

Tracked in pm-graph-5cu7.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @unbraind, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Tests now execute TypeScript sources directly through a new coverage gate. The gate validates required modules, enforces thresholds, and integrates with release checks and CI across Node.js 22 and 26.

Changes

TypeScript coverage enforcement

Layer / File(s) Summary
Source-based test wiring
package.json, tsconfig*.json, test/*.test.ts
Tests import TypeScript sources directly, compiler settings support .ts imports, and package scripts define the coverage workflow and thresholds.
Coverage gate implementation
scripts/coverage-gate.ts
The gate discovers required sources, validates ignores, runs Node coverage with UTC and thresholds, removes stale reports, and rejects unreported modules.
CI, release, and feature records
.github/workflows/ci.yml, .gitignore, CHANGELOG.md, .agents/pm/...
Release checks and CI run the gate on Node.js 22 and 26, coverage output is ignored, and project records document the completed feature.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CI
  participant CoverageGate
  participant NodeTest
  participant Lcov
  CI->>CoverageGate: Run npm run coverage
  CoverageGate->>CoverageGate: Enumerate required TypeScript sources
  CoverageGate->>NodeTest: Run tests with thresholds and TZ=UTC
  NodeTest->>Lcov: Write coverage/lcov.info
  CoverageGate->>Lcov: Parse reported source files
  CoverageGate-->>CI: Pass or fail the coverage gate
Loading

Possibly related PRs

  • unbraind/pm-graph#24: Modifies the same export contract test while adding CLI contract coverage.
  • unbraind/pm-graph#35: Adds analytics and impact command tests affected by the shared source-entrypoint changes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: running tests on TypeScript sources behind a coverage gate.
Description check ✅ Passed The description directly matches the changeset and explains the coverage gate, CI updates, and source-based testing.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch coverage-gate-typescript-sources

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces a TypeScript-native coverage gate that runs tests directly against the TS sources, enforces ratcheting coverage thresholds configured in package.json, validates ignore entries against compiled output, and wires this gate into both local scripts and CI (including a Node 22/26 matrix), while updating tests to import from src instead of dist and pinning Node TS import options.

Sequence diagram for running tests through the TypeScript coverage gate

sequenceDiagram
  actor Developer
  participant Npm as NpmScripts
  participant Gate as coverage_gate_ts
  participant Node as node_test_runner
  participant Lcov as lcov_report

  Developer->>Npm: npm run coverage
  Npm->>Gate: node scripts/coverage-gate.ts

  Gate->>Gate: readFileSync(package.json)
  Gate->>Gate: collectSources()
  Gate->>Gate: readFileSync(tsconfig.json)
  Gate->>Gate: validate ignore entries

  Gate->>Node: spawnSync(process.execPath, ["--test", "--experimental-test-coverage", ...])
  Node-->>Lcov: write lcov.info
  Node-->>Gate: exit status

  Gate->>Gate: readFileSync(lcovPath)
  Gate->>Gate: parse reported source files
  Gate->>Gate: compare reported vs required

  alt missing required sources
    Gate-->>Developer: exit 1 (list missing files)
  else thresholds met and all files reported
    Gate-->>Developer: exit 0 (coverage-gate: N source file(s) reported)
  end
Loading

Flow diagram for coverage-gate.ts enforcing uncheatable coverage

flowchart LR
  A[Start coverage-gate.ts] --> B["readFileSync(package.json) and load coverageGate"]
  B --> C["collectSources() from coverageGate.sources"]
  C --> D["readFileSync(tsconfig.json) to get outDir and rootDir"]
  D --> E[Validate coverageGate.ignore entries against compiled JS]
  E --> F{Validation ok?}
  F -->|No| X[Exit 1 with error]
  F -->|Yes| G["Prepare coverage/lcov.info (mkdirSync, rmSync)"]
  G --> H["spawnSync(process.execPath, --test ... thresholds ... --test-coverage-include=files)"]
  H --> I{Test runner status == 0?}
  I -->|No| X
  I -->|Yes| J["readFileSync(lcovPath) and parse SF: lines"]
  J --> K[Build reported set of source files]
  K --> L{All required files reported?}
  L -->|No| M[Print list of missing files and guidance]
  M --> X
  L -->|Yes| N[Log thresholds met message]
  N --> O[Exit 0]
Loading

File-Level Changes

Change Details Files
Add a coverage gate script that runs node --test with V8 coverage against TypeScript sources and enforces thresholds plus presence of all source files.
  • Implement scripts/coverage-gate.ts to run node --test with --experimental-test-coverage and lcov reporting, targeting TS source files directly.
  • Walk configured source directories at runtime to collect .ts (non-.d.ts) files, respecting default and configurable skip directories, and treat them as the required coverage set.
  • Parse the lcov report, normalize paths, and fail if any required source file did not appear in the report, emitting a clear list of missing files.
  • Set TZ=UTC in the child process environment to make coverage deterministic across machines and delete any previous lcov.info before each run to avoid stale data.
  • Validate coverageGate.ignore entries by mapping TS files to their compiled JS output under tsconfig outDir/rootDir and rejecting ignores whose emitted JS contains runtime code.
scripts/coverage-gate.ts
Wire the coverage gate into npm scripts and CI, and define configurable thresholds and sources in package.json.
  • Add a coverage npm script that builds sources and tests, then runs the coverage-gate script.
  • Change release:check to run coverage instead of npm test so coverage is enforced in the release pipeline.
  • Define a coverageGate block in package.json specifying source roots, test entry globs, minimum thresholds (lines/branches/functions), and an ignore list.
  • Update the GitHub Actions CI workflow to run the coverage script instead of plain tests and to test against a Node 22 and 26 matrix.
package.json
.github/workflows/ci.yml
Run tests directly against TypeScript sources instead of compiled dist output and enable TS compiler features needed for TS imports in tests.
  • Update the npm test script to run node --test against TypeScript test files (*.test.ts) instead of compiled dist-test JS files.
  • Change all test imports that previously pointed at dist/index.js to import from src/index.ts, ensuring coverage is recorded on source lines.
  • Adjust tsconfig to allow importing .ts extensions and rewrite relative import extensions so TS-based tests remain type-safe and runnable under Node's TS loading behavior.
package.json
test/analytics.test.ts
test/diagram-commands.test.ts
test/explain-command.test.ts
test/export-and-contract.test.ts
test/id-resolution.test.ts
test/impact-command.test.ts
test/smoke.test.ts
tsconfig.json
tsconfig.test.json
Keep project metadata and PM artifacts in sync with the new feature and current version.
  • Update the changelog with an Unreleased entry describing the new TS-based coverage gate feature.
  • Adjust the dist/index.js bundled metadata to hardcode the current extension version string.
  • Add pm agent feature and history artifacts for pm-graph-5cu7 corresponding to this change.
CHANGELOG.md
dist/index.js
.agents/pm/features/pm-graph-5cu7.toon
.agents/pm/history/pm-graph-5cu7.jsonl
dist/index.js.map
.gitignore

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown

Greptile Summary

Adds an uncheatable TypeScript coverage gate and runs tests against src instead of dist.

  • New scripts/coverage-gate.ts enforces line/branch/function thresholds, reconciles lcov against a dynamic src walk, pins TZ=UTC, clears stale lcov, and verifies coverageGate.ignore entries are type-only via tsc --showConfig.
  • package.json adds coverage / coverageGate config and wires the gate into release:check; CI runs it on Node 22 and 26.
  • Tests import ../src/index.ts; tsconfig allows .ts import extensions; tsconfig.test.json typechecks tests and scripts with noEmit.

Confidence Score: 5/5

This PR appears safe to merge; no blocking failures remain from prior Greptile findings or eligible follow-up issues.

No blocking failure remains.

Important Files Changed

Filename Overview
scripts/coverage-gate.ts New gate script runs Node coverage on TS sources, enforces thresholds, and fails if any required source never loads.
package.json Adds coverage script and coverageGate thresholds; release:check and test now exercise TS sources.
.github/workflows/ci.yml CI test job matrices Node 22/26 and runs npm run coverage instead of npm test.
tsconfig.json Enables allowImportingTsExtensions and rewriteRelativeImportExtensions for direct .ts test imports.
tsconfig.test.json Typechecks test and scripts trees with noEmit instead of emitting dist-test.
test/smoke.test.ts Switches imports from dist to src TypeScript entrypoint (same pattern across the test suite).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[npm run coverage] --> B[build + build:test]
  B --> C[scripts/coverage-gate.ts]
  C --> D[Walk coverageGate.sources]
  C --> E[Validate ignore via tsc --showConfig emit]
  C --> F[rm stale coverage/lcov.info]
  C --> G["node --test --experimental-test-coverage TZ=UTC"]
  G --> H{runner status}
  H -->|non-zero| I[exit with runner status]
  H -->|0| J[Parse lcov SF paths]
  J --> K{all required files reported?}
  K -->|no| L[fail: name missing sources]
  K -->|yes| M[pass: thresholds already enforced by node]
Loading

Reviews (5): Last reviewed commit: "Fail closed when the effective tsconfig ..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 17: Update the release:check script to remove the redundant npm run build
step, allowing the coverage command to own the project build while preserving
the remaining validation commands and their order.
- Around line 19-23: Retain source coverage while adding release-only validation
of the compiled or packed artifact. In package.json, update the release workflow
to build and execute an artifact smoke or contract suite. Apply equivalent
built-artifact validation in test/analytics.test.ts,
test/diagram-commands.test.ts, test/explain-command.test.ts,
test/export-and-contract.test.ts, and test/impact-command.test.ts, including
extension activation/entry-point checks and package contract checks as
applicable.
- Around line 106-109: Update the coverage thresholds in the thresholds
configuration to the stated decimal values: 63.21 for lines, 76.46 for branches,
and 77.40 for functions, preserving the existing threshold keys and structure.

In `@scripts/coverage-gate.ts`:
- Around line 228-230: Preserve decimal coverage thresholds through the
coverageGate configuration used by the flags in scripts/coverage-gate.ts: keep
lines, branches, and functions at the measured 63.21, 76.46, and 77.40 values
rather than truncating them to integers in package.json. Update the
corresponding baseline note in .agents/pm/features/pm-graph-5cu7.toon to record
the same decimal thresholds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 13d20787-5966-49aa-a2f4-c77bced345f6

📥 Commits

Reviewing files that changed from the base of the PR and between f169ce4 and 13456a8.

⛔ Files ignored due to path filters (2)
  • dist/index.js is excluded by !**/dist/**
  • dist/index.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (16)
  • .agents/pm/features/pm-graph-5cu7.toon
  • .agents/pm/history/pm-graph-5cu7.jsonl
  • .github/workflows/ci.yml
  • .gitignore
  • CHANGELOG.md
  • package.json
  • scripts/coverage-gate.ts
  • test/analytics.test.ts
  • test/diagram-commands.test.ts
  • test/explain-command.test.ts
  • test/export-and-contract.test.ts
  • test/id-resolution.test.ts
  • test/impact-command.test.ts
  • test/smoke.test.ts
  • tsconfig.json
  • tsconfig.test.json

Comment thread package.json
Comment thread package.json
Comment thread package.json
Comment thread scripts/coverage-gate.ts
unbraind added 4 commits July 29, 2026 03:30
… at all

The status check sat between the report parse and the presence check, so it
covered the "sources missing from the report" diagnostic but not the "no report
was written" one directly above it. A runner that exits non-zero without
producing lcov - a test file that fails to load, an aborted run - still hit the
missing-report branch first and exited with that message, so CI showed a
coverage-report failure where the author needed to see a test failure.

The check now runs immediately after the spawn, before the report is touched.
…only

The type-only check normalised away line comments but not block comments, so a
documented type-only module whose JSDoc survived emit would have been read as
runtime code and rejected - a false failure on the exemption path.

Applied as hardening rather than as a reproduced bug: with tsc's default emit
settings the leading JSDoc on a fully-erased module is not carried into the
output. Comment-preserving configurations exist, the normalisation is a single
expression, and a false rejection on this path is confusing enough to pre-empt.
… as JSON

The ignored-module check located a file's emitted output by reading outDir and
rootDir straight out of tsconfig.json with JSON.parse. That is wrong twice
over: tsconfig may be JSONC, where JSON.parse throws on a perfectly valid
config, and either option may be inherited through an `extends` chain, where
reading direct fields silently yields the wrong paths and the check then looks
for compiled output that was never going to be there.

It now asks `tsc --showConfig`, which returns the flattened configuration the
compiler actually used, and warns and falls back to the conventional defaults
if the compiler cannot be reached. The resolution runs only when
`coverageGate.ignore` is non-empty, so no package pays for it unless it uses
the exemption.
The fallback warned and assumed outDir dist / rootDir "." when
`tsc --showConfig` could not be reached. That was the wrong default for this
consumer: the resolved paths feed the check deciding whether an exempted module
is genuinely type-only, so guessing the emit layout can clear an executable
module by inspecting the wrong file - the one outcome this gate exists to make
impossible. Leniency here is indistinguishable from a bypass.

It now exits 1 and prints the compiler's stderr.
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