Install
$ agentstack add skill-siddharth00-agent-revamp-skills-test-coverage-baseline ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
About
1. Purpose
This skill establishes a behavioral baseline — a committed coverage report and a CI gate — on the module being migrated, before any Phase 4 migration skill touches the code. The core principle is: you cannot know whether a migration broke something unless you had a passing test before you started. The baseline is not about achieving perfect coverage; it is about capturing enough behavioral evidence on the current stack that regressions on the new stack produce a signal. The focus is deliberately on integration and contract tests rather than unit tests: unit tests are typically implementation-coupled and will need to be rewritten anyway when the stack changes, whereas a test that exercises a route handler or a service boundary end-to-end survives a rewrite because it tests observable behavior, not internal structure. When coverage cannot be achieved through tests (external APIs, legacy code with no seams), this skill provides a protocol for snapshotting API behavior using recording tools (Polly.js, VCR, nock-record, supertest snapshots) so that the snapshot library itself becomes the behavioral contract.
2. Trigger Conditions
Use when:
- A Phase 2 migration manifest has been produced and identifies a specific module, service, or route group as the next migration target.
- The current test suite for
target_modulepasses on the current commit but has no committed coverage report — there is no way to detect coverage regression in CI today. - The coverage percentage for
target_moduleis unknown or below 70% line coverage — migration would begin on an untested foundation. - The team is about to run any Phase 4 skill (
/migrate,express-to-fastify,js-to-typescript,cra-to-vite, etc.) — this skill is the mandatory gate before Phase 4 begins for any module.
Do NOT use when:
- A coverage baseline for
target_modulealready exists as a committed artifact (coverage/baseline-.json) and the CI gate is already configured — check withgrep coveragebefore running. - You have already started migrating
target_module— running this skill after migration has begun defeats its purpose. Write tests against the original stack, not the in-progress migration. If migration has started, stop, revert to the pre-migration state, run this skill, then resume. - The intent is to improve test quality rather than establish a baseline — use a dedicated test-quality skill. This skill writes only the minimum tests needed to detect regressions; it deliberately does not refactor, optimize, or clean up the existing test suite.
- The coverage tool cannot run against
target_modulein isolation (e.g., circular dependency prevents loading the module in the test environment) — resolve the dependency issue first.
3. Inputs
Required:
| Input | Type | Description | |-------|------|-------------| | target_module | string | Identifier for the module, service, or route group being baselined. Used to scope coverage collection and name output artifacts. Example: src/services/AuthService or api/users. | | repo_root | file-path | Absolute path to the repository root. All commands run from here. | | coverage_tool | enum(c8, istanbul, pytest-cov, coverage.py, simplecov, other) | Coverage tool for the repo's language and test framework. Determines the exact CLI commands used in Steps 4 and 9. | | ci_config_path | file-path | Repo-root-relative path to the CI pipeline configuration file where the coverage gate will be added (e.g., .github/workflows/test.yml, Jenkinsfile, Makefile). |
Optional:
| Input | Type | Default | Description | |-------|------|---------|-------------| | line_coverage_threshold | integer | 70 | Minimum line coverage percentage required on target_module before this skill declares done. Raise to 80+ for high-risk modules (auth, payments); 70 is the floor, not the target. | | api_coverage_threshold | integer | 100 | Minimum line coverage required specifically on the public API surface (route handlers, exported service methods, public class methods). Always 100 unless the surface has untestable branches (e.g., vendor callbacks with no seam). | | test_dir | file-path | tests/ or src/__tests__/ (auto-detect) | Directory where new test files are written. Defaults to the first existing test directory found under repo_root. | | snapshot_tool | enum(polly, nock-record, msw, vcr, supertest-snapshots, none) | none | API behavior snapshotting tool to configure when coverage cannot be reached through pure tests. See Step 7 for detail. Set to none if all code paths can be exercised without external calls. | | coverage_reporter | string | text-summary,json,lcov | Comma-separated list of coverage reporters to pass to the coverage tool. json is required for the baseline artifact; lcov is required if the CI provider (Codecov, Coveralls) reads LCOV files. |
4. Steps
- → Hand off to
code-archaeologist(see Section 5. Agent Handoffs) to produce the public API surface inventory fortarget_module. Wait foroutput/test-coverage-baseline-api-surface-.mdbefore continuing.
- Run the existing test suite with coverage collection scoped to
target_module. Use the command appropriate forcoverage_tool:
c8 (Node.js): ``bash npx c8 \ --include='/**' \ --reporter=text-summary \ --reporter=json \ --reporter=lcov \ --report-dir=coverage/current \ npx jest --testPathPattern='' ``
istanbul / nyc (Node.js): ``bash npx nyc \ --include='/**' \ --reporter=text-summary \ --reporter=json \ --reporter=lcov \ --report-dir=coverage/current \ npx jest --testPathPattern='' ``
pytest-cov (Python): ``bash pytest \ --cov= \ --cov-report=term-missing \ --cov-report=json:coverage/current/coverage.json \ --cov-report=lcov:coverage/current/lcov.info \ tests/ ``
coverage.py (Python, without pytest): ``bash coverage run --source= -m unittest discover tests/ coverage report --format=text coverage json -o coverage/current/coverage.json coverage lcov -o coverage/current/lcov.info ``
Record: (a) current overall line coverage percentage for target_module, (b) current coverage percentage for each file in the public API surface (from Step 1), (c) which lines are uncovered.
- If this fails: check that the test runner can find tests scoped to
target_module. If no tests exist yet, coverage will be 0% — record that and proceed to Step 3.
- Read
output/test-coverage-baseline-api-surface-.md. For each item in the public API surface (exported functions, route handlers, public class methods), cross-reference with the coverage output from Step 2. Produce two lists:
- Surface gaps: API surface items with .md`. If both lists are empty (all thresholds already met), skip to Step 8.
- For each item in the surface gaps list: write an integration or contract test that exercises the item through its observable interface — not by calling internal helpers directly. Follow these rules:
- Prefer integration depth over unit isolation. A test that calls
POST /api/usersthrough a real HTTP handler (supertest, pytesttest_client, Rackrack-test) is more migration-resilient than one that callsUserService.create()directly, because it survives a handler rewrite. - Test every status code / return type variant. If a handler returns 200 and 422, there must be at least one test for each. Untested branches are invisible to equivalence tests.
- Never mock the database for surface-gap tests unless the database is genuinely unavailable. A test that mocks the DB teaches nothing about what the system actually does; it will pass on the new stack even if the queries are wrong.
- Do mock external services (payment gateways, email providers, third-party APIs) — but use recording-based mocks (Step 7), not hand-written stubs, so the mock captures real behavior.
- Name tests to describe observable behavior, not implementation:
it('returns 401 when the session cookie is missing'), notit('calls requireAuth middleware').
Write each new test file to ` using the naming convention .baseline.test.`. This suffix distinguishes baseline tests from pre-existing tests and makes them grep-able in CI.
- Run coverage again (same command as Step 2) after writing the new tests. Check whether surface gaps are closed. For any surface item still below 100%:
- If the gap is in an error path that requires infrastructure unavailable in the test environment (e.g., a DB failure branch): proceed to Step 7 (API snapshotting) for that path.
- If the gap is reachable but complex: write the test. Do not skip.
- If the gap is in dead code that is never reachable in production: document it in the migration log as confirmed-dead and exclude it from coverage collection using the tool's ignore directive (
/* c8 ignore next */,# pragma: no cover). Do not add ignore directives speculatively.
- Check coverage for module gaps. Write additional integration-level tests to raise line coverage toward
line_coverage_threshold. Apply the same rules as Step 4. Stop when:
- The overall line coverage for
target_modulemeets or exceedsline_coverage_threshold, or - All remaining uncovered lines are confirmed dead code (documented in the migration log), or
- The only way to cover remaining lines would require writing implementation-coupled unit tests (testing private methods, mocking internal state). If so: document each uncovered line in the migration log as
impl-coupled — acceptable gap, and reduce the effective threshold accordingly. Do not write implementation-coupled tests to hit a coverage number.
- API Behavior Snapshotting — for any code path that cannot be covered by tests (external API calls, vendor webhooks, legacy code with no injectable seam): configure the snapshot tool specified in
snapshot_toolto record real interactions and replay them as behavioral contracts.
Polly.js (Node.js — records HTTP interactions at the adapter level): ```typescript // tests/baseline/AuthService.snapshot.test.ts import { Polly } from '@pollyjs/core'; import NodeHttpAdapter from '@pollyjs/adapter-node-http'; import FSPersister from '@pollyjs/persister-fs';
Polly.register(NodeHttpAdapter); Polly.register(FSPersister);
describe('AuthService — OAuth callback (snapshot)', () => { let polly: Polly;
beforeEach(() => { polly = new Polly('auth-oauth-callback', { adapters: ['node-http'], persister: 'fs', persisterOptions: { fs: { recordingsDir: 'tests/recordings' }, }, // RECORD mode on first run; REPLAY on subsequent runs. // Commit recordings/ to the repo — they ARE the behavioral contract. mode: process.env.POLLY_MODE === 'record' ? 'record' : 'replay', }); });
afterEach(() => polly.stop());
it('exchanges OAuth code for tokens and returns user profile', async () => { const result = await AuthService.handleOAuthCallback({ code: 'test-code', state: 'csrf' }); expect(result).toMatchObject({ userId: expect.any(String), email: expect.any(String) }); }); }); ```
nock-record (Node.js — simpler, records nock interceptors as JSON): ```typescript // tests/baseline/payments.snapshot.test.ts import nockRecord from 'nock-record';
describe('PaymentService — charge (snapshot)', () => { const recorder = nockRecord.setupRecorder();
it('charges a card and returns a transaction ID', async () => { // On first run with NOCKRECORD=true: makes real HTTP calls and saves fixtures. // On subsequent runs: replays from fixtures/payments-charge.json. const { completeRecording } = await recorder.record('payments-charge'); const result = await PaymentService.charge({ amount: 100, token: 'toktest' }); completeRecording(); expect(result.transactionId).toMatch(/^txn_/); }); }); ```
msw (Mock Service Worker — works in Node.js test environments via msw/node): ```typescript // tests/baseline/notifications.snapshot.test.ts import { setupServer } from 'msw/node'; import { http, HttpResponse } from 'msw'; // Recorded response fixtures — committed to the repo. import sendgridFixture from '../fixtures/sendgrid-send-202.json';
const server = setupServer( http.post('https://api.sendgrid.com/v3/mail/send', () => { return HttpResponse.json(sendgridFixture, { status: 202 }); }) );
beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); afterAll(() => server.close());
it('sends a transactional email and returns 202', async () => { const result = await NotificationService.sendWelcomeEmail('alice@example.com'); expect(result.statusCode).toBe(202); }); ```
supertest snapshots (for route-level behavioral contracts): ```typescript // tests/baseline/users-api.snapshot.test.ts import request from 'supertest'; import app from '../../src/app';
describe('GET /api/users/:id — behavioral snapshot', () => { it('matches the committed response snapshot', async () => { const res = await request(app) .get('/api/users/user123') .set('Authorization', 'Bearer test-token'); // Jest snapshot stored in _snapshots__/users-api.snapshot.test.ts.snap // This IS the behavioral contract — commit it, review diffs in PRs. expect({ status: res.status, body: res.body, headers: { 'content-type': res.headers['content-type'], 'cache-control': res.headers['cache-control'], }, }).toMatchSnapshot(); }); }); ```
VCR (Ruby — cassette-based HTTP recording): ```ruby # spec/baseline/authservicespec.rb require 'vcr'
VCR.configure do |config| config.cassettelibrarydir = 'spec/cassettes' config.hookinto :webmock config.defaultcassetteoptions = { record: :newepisodes } end
RSpec.describe AuthService, '#oauthcallback' do it 'exchanges code for a user profile', :vcr do # First run records the HTTP interaction to spec/cassettes/AuthServiceoauthcallback.yml # Subsequent runs replay from the cassette — commit cassettes/ to the repo. result = AuthService.oauthcallback(code: 'test-code') expect(result).to include(:user_id, :email) end end ```
Rules for all snapshot/recording approaches:
- Commit recordings to the repo. The recordings directory (
tests/recordings/,spec/cassettes/,__snapshots__/) is the behavioral contract. It must be reviewed in PRs like code — a recording change means behavior changed. - Record once against real external services in a controlled environment. Use a test account, sandbox credentials, or a staging external service. Never record against production.
- Document the recording date in a
RECORDING_NOTES.mdin the recordings directory. Recordings become stale when external APIs change; flag them for re-recording in Phase 7 (Stabilize). - Do not re-record during migration. If an external service returns different data during migration, that is a real behavioral change — investigate before updating the recording.
- Run the full test suite with coverage one final time. Confirm:
- Overall line coverage for
target_module≥line_coverage_threshold. - Coverage for every public API surface item = 100% (or documented as confirmed-dead / impl-coupled gap).
- All tests pass (exit code 0).
Write the final coverage JSON to coverage/baseline-.json. This file is the committed baseline artifact.
- Configure the CI coverage gate. Add a coverage threshold check to `ci_confi
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Siddharth00
- Source: Siddharth00/agent-revamp-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.