Skip to content

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

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

unbraind merged 8 commits into
mainfrom
coverage-gate-typescript-sources

Conversation

@unbraind

@unbraind unbraind commented Jul 28, 2026 •

Copy link
Copy Markdown
Owner

Why

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

What changed

  • Tests import the TypeScript source directly (../index.ts) instead of ../dist/index.js, so the coverage report lands on editable source lines. Node strips types natively from 22.18.0, the floor this package already declares in engines — no new tooling.
  • tsconfig.json enables rewriteRelativeImportExtensions + allowImportingTsExtensions. This package has no relative source imports today, so the published dist/ artifact is byte-identical; the flag keeps the door open for future multi-file sources.
  • scripts/coverage-gate.ts (copied verbatim from pm-changelog) enforces per-dimension thresholds and reconciles the reported file list against the configured source list. That second check is why it is a script, not a bare flag: Node omits files that never load from its coverage report entirely rather than reporting them at zero, so a threshold alone is satisfiable by narrowing what the suite imports.
  • release:check runs the gate in place of a bare test run; CI runs it on a Node 22 + 26 matrix.

Measured baseline (single source file index.ts)

dimension measured threshold (floor)
lines 70.53 70
branches 80.42 80
functions 80.43 80

Thresholds are pinned at the measured floor so the number can only ratchet up.

Negative control

A throwaway __probe.ts source was added to coverageGate.sources. No test imported it, so the gate exited 1 and named __probe.ts as never loaded. After removing the probe the gate exited 0 again. The presence check is not vacuous.

pm item

Resolves pm-linear-v618 after merge.

Summary by Sourcery

Enforce a coverage gate over the TypeScript sources by running Node’s built-in test coverage against .ts test files and wiring the gate into local release checks and CI.

New Features:

  • Introduce a reusable coverage gate script that runs the Node test runner with coverage against TypeScript sources and enforces per-dimension thresholds plus source presence checks.

Enhancements:

  • Switch tests to import the TypeScript entrypoint instead of compiled JavaScript so coverage maps directly to authored source.
  • Add coverageGate configuration to package.json to declare covered sources, test entry patterns, thresholds, and ignores.
  • Enable rewriteRelativeImportExtensions and allowImportingTsExtensions in tsconfig to support direct .ts imports while keeping the emitted dist output stable.

Build:

  • Add a coverage npm script that builds the project and runs the coverage gate instead of a plain test run.
  • Update the release:check script to run the coverage gate in place of the bare test suite.

CI:

  • Update the CI workflow to run the coverage-gated test command on a Node 22 and 26 matrix instead of a simple Node 22 test run.

Chores:

  • Register the associated pm-linear item files under .agents/pm to track this change.

Summary by cubic

Run tests against the TypeScript source and enforce a coverage gate that also fails when untested files don’t appear in the report. Uses a dynamic source walk, pins TZ=UTC, and runs in CI on Node 22 and 26; satisfies Linear pm-linear-v618.

  • New Features

    • Tests import ../index.ts so coverage maps to authored lines; Node 22+ executes .ts.
    • Gate enforces thresholds (lines 70, branches 80, functions 80) and reconciles the report with a directory walk from coverageGate.sources; only required files are included.
    • npm run coverage builds, typechecks tests and the gate, then runs the gate; CI runs this job; coverage/ is gitignored; tsconfig enables rewriteRelativeImportExtensions and allowImportingTsExtensions; tests are noEmit with repo-root rootDir.
    • Baseline: lines 70.53, branches 80.42, functions 80.43.
  • Bug Fixes

    • Propagate the runner’s exit status before reading the coverage report, so real test failures show first.
    • Normalize lcov SF: paths to repo-relative POSIX to avoid false “never loaded” on Windows.
    • Clear error when a coverageGate.sources entry does not exist.
    • Delete previous coverage/lcov.info before the run to prevent passing on stale data.
    • Validate coverageGate.ignore entries are type-only against emitted JS (stripping block comments); resolve outDir/rootDir via tsc --showConfig and fail closed if it cannot be resolved; reject non-TS or .d.ts entries in coverageGate.sources.

Written for commit 7c3ba6c. 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 (the package is a single
index.ts at the repo root, so tests import ../index.ts). Node has stripped
types without a flag since 22.18.0, which is the floor this package already
declares in engines, so this needs no new tooling. Enabling
rewriteRelativeImportExtensions lets a source import its siblings as .ts
while tsc still emits .js into dist; this package has no relative source
imports today, so the published artifact is byte-identical, but the flag is
harmless to set and keeps the door open for future multi-file sources.

The gate is scripts/coverage-gate.ts. Beyond enforcing per-dimension
thresholds it reconciles the reported file list against the configured source
list, and that second check is the reason it is a script rather than a bare
flag: Node omits files that never load from its coverage report entirely
instead of reporting them at zero, so a threshold alone is satisfiable by
narrowing what the suite imports. A negative control confirms the check is
not vacuous - adding an untested __probe.ts source makes the gate exit 1 and
name it, and removing it returns exit 0.

Thresholds are pinned at the measured baseline (70 line, 80 branch, 80
function) so the number can only ratchet up. release:check now runs the gate
in place of a bare test run, and CI runs it on both Node 22 and 26.

Baseline across the single source file index.ts: 70.53 line, 80.42 branch,
80.43 function.

Tracked in pm-linear-v618.
@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 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR changes tests to import TypeScript sources directly, adds a configurable coverage gate with threshold and loaded-source checks, updates release and CI commands, and runs the test matrix on Node.js 22 and 26.

Changes

TypeScript coverage enforcement

Layer / File(s) Summary
Source-based test execution
package.json, test/*.test.ts, tsconfig*.json
Tests run against TypeScript sources directly, with compiler settings supporting .ts imports and no emitted test files.
Coverage collection and enforcement
package.json, scripts/coverage-gate.ts
The gate collects configured sources, validates ignored type-only files, runs Node coverage with line/branch/function thresholds, parses LCOV output, and fails for sources that were never reported.
CI, release, and verification wiring
.github/workflows/ci.yml, .gitignore, .agents/pm/...
CI runs the coverage gate for Node.js 22 and 26, release checks invoke it, coverage output is ignored, and project records document verification results.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • unbraind/pm-linear#36: Updates the same atomic test file and overlaps with its source-import and atomic test coverage 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 It clearly captures the main change: running tests on TypeScript sources with a coverage gate.
Description check ✅ Passed The description matches the changeset and summarizes the TypeScript coverage gate updates.
✨ 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 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a TypeScript-aware coverage gate that runs Node’s native test coverage against source .ts files, wires it into release and CI, and updates tests and TypeScript config to support direct TS imports while enforcing explicit coverage thresholds and presence checks.

Flow diagram for the new TypeScript coverage gate execution

flowchart LR
  A[npm run coverage] --> B[coverage-gate.ts]
  B --> C[Read coverageGate from package.json]
  C --> D[Collect required .ts sources]
  D --> E[Run node --test with --experimental-test-coverage<br>and thresholds for lines/branches/functions]
  E --> F[Write coverage/lcov.info]
  F --> G[Parse lcov and build reported file set]
  G --> H{All required sources reported?}
  H -- No --> I[Exit 1<br>and list missing files]
  H -- Yes --> J[Exit with test status<br>and print success message]
Loading

File-Level Changes

Change Details Files
Run tests directly against TypeScript sources instead of built JavaScript, and introduce a dedicated coverage script with configuration in package.json.
  • Update npm test script to invoke node --test on .test.ts files from the test directory rather than compiled dist-test/.js outputs.
  • Add a coverage npm script that builds once and then runs the new coverage gate script.
  • Introduce a coverageGate configuration block in package.json specifying source roots, test globs, thresholds for lines/branches/functions, and an ignore list.
package.json
Enforce coverage thresholds and source presence via a custom coverage gate script that drives node --test with V8 coverage and validates the lcov output.
  • Add scripts/coverage-gate.ts that reads coverageGate from package.json, walks configured sources (excluding known non-source dirs) to compute required .ts files, and uses node’s experimental test coverage flags to run tests with thresholds and lcov output.
  • Ensure coverage is scoped to the required sources via --test-coverage-include, parse lcov.info to see which files were reported, and fail if any required source is missing from the report.
  • Propagate the test runner’s exit code when tests or thresholds fail, and print explicit diagnostics for missing coverageGate config, no discovered sources, missing lcov output, or unreported files.
scripts/coverage-gate.ts
package.json
Wire the coverage gate into the release pipeline and CI matrix, replacing the previous bare test run.
  • Change release:check to run the coverage script instead of npm test, so releases are blocked on coverage thresholds and presence passing.
  • Update the GitHub Actions CI workflow to test using npm run coverage instead of building and then running npm test directly.
  • Introduce a Node version matrix (22 and 26) in CI so coverage is validated on both the minimum supported and current development Node versions.
package.json
.github/workflows/ci.yml
Adjust tests and TypeScript configuration so tests import the TypeScript entrypoint directly while keeping emitted artifacts stable.
  • Update test files to import implementation and types from ../index.ts instead of ../dist/index.js so coverage maps to editable TS lines.
  • Enable rewriteRelativeImportExtensions and allowImportingTsExtensions in tsconfig.json to allow .ts extension imports and future multi-file sources without changing emitted JavaScript.
  • Ensure the new compiler options do not alter current emitted dist output given there are no relative source imports yet.
test/atomic.test.ts
test/smoke.test.ts
tsconfig.json
Track the associated product management item for this coverage work.
  • Add pm-linear-v618 feature and history entries in the PM agent directory to document this change for the internal PM system.
.agents/pm/features/pm-linear-v618.toon
.agents/pm/history/pm-linear-v618.jsonl

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

@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: 2

🤖 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 @.agents/pm/features/pm-linear-v618.toon:
- Line 23: Correct the duplicated baseline measurement date from July 29, 2026
to July 28, 2026 in .agents/pm/features/pm-linear-v618.toon:23-23 and keep the
corresponding historical patch value consistent in
.agents/pm/history/pm-linear-v618.jsonl:1-1.

In `@package.json`:
- Around line 35-40: Update the coverage script in package.json (lines 35-40) to
run npm run build:test before scripts/coverage-gate.ts, allowing release:check
and CI to exercise the test build through coverage. The referenced
tsconfig.test.json (lines 7-10) and .github/workflows/ci.yml (lines 43-44)
require no direct changes because they are corrected through the package.json
coverage flow.
🪄 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: c59969fd-9a94-4657-b901-fe04596eba21

📥 Commits

Reviewing files that changed from the base of the PR and between bacf463 and 49932c2.

📒 Files selected for processing (10)
  • .agents/pm/features/pm-linear-v618.toon
  • .agents/pm/history/pm-linear-v618.jsonl
  • .github/workflows/ci.yml
  • .gitignore
  • package.json
  • scripts/coverage-gate.ts
  • test/atomic.test.ts
  • test/smoke.test.ts
  • tsconfig.json
  • tsconfig.test.json

Comment thread .agents/pm/features/pm-linear-v618.toon
Comment thread package.json Outdated
Comment thread .github/workflows/ci.yml
@greptile-apps

greptile-apps Bot commented Jul 28, 2026 •

Copy link
Copy Markdown

Greptile Summary

Wires a real coverage gate over TypeScript sources and CI.

  • Tests import ../index.ts so Node’s coverage maps to authored lines.
  • Adds scripts/coverage-gate.ts with thresholds, source-walk presence checks, and coverageGate config in package.json.
  • CI and release:check run npm run coverage (build + build:test + gate) on Node 22/26; tsconfig.test.json typechecks test/** and scripts/** with noEmit and rootDir: ".".

Confidence Score: 5/5

The PR appears safe to merge; the prior typecheck gap is addressed and no blocking failure remains.

coverage now runs build:test, tsconfig.test.json typechecks tests and scripts with a valid rootDir, and no remaining gate or typecheck hole is evident on the changed path.

Important Files Changed

Filename Overview
package.json coverage script chains build, build:test, and the gate; release:check uses coverage instead of bare test.
tsconfig.test.json noEmit typecheck for tests and scripts with rootDir at repo root so tsc accepts the tree.
scripts/coverage-gate.ts Runs node --test with coverage, enforces thresholds, and fails on never-loaded sources.
.github/workflows/ci.yml Matrix Node 22/26 and runs coverage instead of separate build/test steps.

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

unbraind added 7 commits July 29, 2026 01:57
…urce walk

Brings this package onto the final form of the gate after three defects were
found and fixed on the reference implementation while this rollout was in
flight.

Pin TZ=UTC when spawning the runner. Code that branches on a timestamp's UTC
offset takes different paths under a local offset than under UTC, so the
reported percentage moved between a contributor's machine and CI and a
threshold pinned to one failed on the other.

Chain build:test into the coverage script. release:check no longer runs
`npm test`, so tsconfig.test.json - the only config that typechecks the test
tree and the gate script itself - had dropped out of every gate, and Node
strips types without checking them. Adding it exposed that the config also
inherited rootDir from the base config, which tsc rejects once the test build
stops emitting; rootDir is now overridden to the repository root.

Enumerate sources by walking a directory instead of listing files. A file list
freezes the required set, so a module added later was never required and an
untested new file slipped the gate entirely - the exact blind spot the presence
check exists to close. The walk prunes non-source directories, and the coverage
include flags are derived from the enumerated list so the measured set and the
required set are the same set by construction.

Thresholds re-measured against the corrected gate.
…e edges

Three review findings raised on this rollout, all valid.

Report the runner's exit status before the never-loaded diagnostic. A failing
suite or an unmet threshold can legitimately leave sources out of the lcov
output, and the gate then blamed a missing import for what was actually a test
failure the author needed to see first.

Normalise lcov `SF:` paths to repository-relative POSIX form. The reporter
emits working-directory-relative paths on Linux, but that is not contractual
and absolute paths have been seen on Windows runners; unnormalised, the
presence check inverts into a permanently red build accusing every source file
of never loading.

Diagnose a non-existent `coverageGate.sources` entry instead of throwing a raw
ENOENT stack out of statSync, which was the one failure mode in this script
that did not produce a `coverage-gate:` message.
…s, ignore abuse

Delete the previous lcov report before the run. mkdirSync left an existing
coverage/lcov.info in place, so a run that produced no report at all was
checked against the previous run's file - the gate could pass by reading
history instead of by measuring anything.

Validate `coverageGate.ignore`. The exemption exists for type-only modules,
which erase to nothing and can never appear in a report, but it was trusted
blindly - making it the one supported way to remove an executable module from
both the measured set and the required set, the exact escape this gate exists
to prevent. Entries are now checked against the compiled output: TypeScript
emits `export {};` and nothing else for a module that erases completely.

Reject a non-TypeScript or declaration-file entry in `coverageGate.sources`
rather than requiring a file that can never be reported.
… 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.
@unbraind
unbraind merged commit 24fb162 into main Jul 29, 2026
8 checks passed
@unbraind
unbraind deleted the coverage-gate-typescript-sources branch July 29, 2026 02:29
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