| name | raise-coverage |
| description | Raises JVM test coverage for a Gradle module or source path. Outside read-only `--triage`, first ensures the repo is on Kover — auto-migrating from vanilla JaCoCo, or installing Kover when absent (routine cases run without asking; manual-review surfaces are flagged). Then localizes uncovered lines and branches from Kover's JaCoCo-format XML report and generates policy-compliant unit tests: stubs not mocks, written in Kotlin with Kotest assertions even when the code under test is Java, class names suffixed `Spec`. Proposes a test-case list and waits for approval before writing any test (pass `--yes` to skip), then re-runs the report to confirm the gap closed. Use when asked to add missing tests, close coverage gaps, or raise a module's coverage.
|
Raise test coverage
You localize untested code with Kover's JaCoCo-format XML report and write
the unit tests that close the gap. Work on one Gradle module or path at a time,
always propose the test-case list and wait for approval before writing,
and verify the gap is actually closed afterward.
Copy this checklist into your reply and tick each item as you finish it:
Coverage progress:
- [ ] Step 0: Ensure Kover (migrate from JaCoCo / install if needed; smoke-check)
- [ ] 1. Resolve the target module/path (or run `--triage` and stop)
- [ ] 2. Localize uncovered lines/branches from the Kover XML
- [ ] 3. Read the code under test and existing tests before writing
- [ ] 4. Propose the test-case list — and WAIT for approval (unless `--yes`)
- [ ] 5. Generate tests per `kotlin-jvm-tester`
- [ ] 6. Verify the gap closed; no `.codecov.yml` target regressed
Before the main flow runs, you ensure the repo is on Kover. If vanilla JaCoCo
is detected anywhere, you auto-migrate the repo to Kover without asking for
the routine edits — still flagging the recipe's §7 manual-review surfaces for a
decision; if no coverage plugin is present, you install Kover. The mechanical
recipe lives in
references/migrate-to-kover.md.
The authoritative standards live in .agents/:
.agents/skills/kotlin-jvm-tester/SKILL.md — how to write the test: Kotlin
- JUnit 5 + Kotest,
Spec / XKtSpec naming, the backticked @Nested layout,
and which testlib helper fits the target. This skill owns those conventions;
follow it for every test you generate.
.agents/skills/kotlin-engineer/SKILL.md — Kotlin implementation baseline
for the body of every test you write, including null-safety, API design,
coroutines, Flow, and idioms.
.agents/guidelines/testing.md — stubs not mocks; Kotest assertions; cover API edge
cases; scaffold when/sealed-class branches.
.agents/guidelines/coding.md — Kotlin/Java idioms for the tests you write.
.agents/guidelines/version-policy.md — in a repo with root version.gradle.kts,
every PR (tests-only included) must bump the version or CI fails.
Mechanical detail (report paths, XML parsing, gap rules) lives in
references/coverage-signals.md. Keep this
file about what to do; that one is how to read the numbers.
Scope
- Coverage comes from Kover's local report. Spine consumer repos apply the
Kover Gradle plugin with
useJacoco(version = Jacoco.version), which makes
Kover compute coverage with the JaCoCo engine and emit JaCoCo-format XML.
Per-module task :<module>:koverXmlReport; XML at
<module>/build/reports/kover/report.xml. KMP modules configured by Spine's
kmp-module script plugin define only the total Kover report, so the
same koverXmlReport / report.xml pair applies — see
references/coverage-signals.md.
- Target human-written
src/main code only. Never write tests for generated
code (any path containing generated, e.g. Protobuf output), examples, or
existing test sources. These are excluded by .codecov.yml — respect that
boundary.
- One module or path per run.
Inputs
$ARGUMENTS names a target — one of:
- a Gradle module path — e.g.
:base, :core;
- a source file or directory — e.g.
base/src/main/kotlin/io/spine/...;
--triage — read-only: produce a ranked gap report for the repo (or the named
module) and stop, without proposing or writing tests.
Optional modifier:
--yes (alias --no-confirm) — pre-approve the proposed test-case list and
skip the step-4 wait. The list is still emitted in the Report for the record;
the skill proceeds straight to writing the tests. Ignored under --triage
(which never writes). A plain-language pre-approval in the prompt ("write the
tests without waiting for my confirmation") has the same effect.
If $ARGUMENTS is empty, ask which module or path to target (or offer
--triage to help choose).
Step 0 — Ensure Kover
Run this before the Workflow below. Behaviour depends on $ARGUMENTS:
Under --triage (read-only)
--triage is contractually read-only and must not write build files. If
Kover is not already applied everywhere, emit a "Setup required" report
and stop without writing anything (and without proposing a migration).
List the modules that still need migration, point at
references/migrate-to-kover.md, and tell
the user to re-run the raise-coverage skill without --triage to
perform the migration first. Once Kover is in place everywhere, --triage proceeds to
the Workflow.
Otherwise
Branch on the repo's current coverage setup (detection patterns and full
migration recipe in
references/migrate-to-kover.md):
- Kover applied everywhere already — silently proceed to the Workflow.
- No coverage plugin anywhere — silently install Kover (per the recipe).
Record "Migration: installed Kover" in the final Report. No approval gate
for this branch.
- Vanilla JaCoCo in ≥1 module (with or without Kover alongside) —
auto-migrate the repo to Kover per the recipe: no blanket approval gate
for the routine edits. Record them in the Migration report section, and
stop to flag the recipe's §7 manual-review surfaces for a decision
before applying those (below).
Apply the migration (vanilla-JaCoCo branch)
Apply the migration per
references/migrate-to-kover.md without
asking, logging edited <path> for each file touched: per-module
build.gradle.kts, root build.gradle.kts, .codecov.yml,
.github/workflows/*.yml, scripts/*.sh. Detect the JaCoCo surface first —
modules applying jacoco / JacocoPlugin / JacocoConfig.applyTo / a
jacoco-*.gradle.kts, plus any root jacocoRootReport. Treat a root-level
KoverConfig.applyTo(rootProject) as an existing Kover signal (the successor
to JacocoConfig.applyTo), not a JaCoCo one.
Stop and ask on the recipe's §7 manual-review surfaces (e.g. an intentional
dual JaCoCo/Kover pipeline, custom executionData, or a custom report
destination CI consumers depend on): flag each with "needs your call on <x>"
before applying that edit. Everything else — the routine, mechanical migration
— is applied without asking.
For the final Report's Migration section, capture the edited-file list, the
applicable translation-table rows, and any manual-review surface you skipped.
Verify (smoke check)
Pick the smallest migrated leaf module and run :<module>:koverXmlReport,
then inspect <module>/build/reports/kover/report.xml. KMP modules also use
this task — Spine's kmp-module script plugin configures only Kover's
total report, which for the JVM-only KMP target is identical in shape to
the JVM case (see references/migrate-to-kover.md §6).
Run ./gradlew :<module>:koverXmlReport --quiet; if the root was touched,
also run ./gradlew koverXmlReport --quiet.
Confirm the XML exists, is non-empty, and the first non-XML-declaration line
contains <report . If a DOCTYPE is present, confirm it points at JaCoCo's
report.dtd — that confirms useJacoco(...) is in effect. Failure → stop;
do not fall through to the Workflow.
On success, resume at Workflow step 1.
Workflow
-
Resolve the target.
- A module/path → map it to its owning Gradle module (the project directory
that contains it).
--triage → build the report — per-module koverXmlReport (or the
root-level Kover aggregation task koverXmlReport if the repo wires
one) — rank modules/files by uncovered %, output the ranked list, and
stop.
-
Localize the gaps (per references/coverage-signals.md):
- Run
:<module>:koverXmlReport (the same task on JVM and KMP modules
configured by Spine's convention plugins; see
references/coverage-signals.md). The report task runs the tests first.
- Parse the XML for uncovered lines (
ci == 0) and partially covered
branches (mb > 0). Prioritize methods whose BRANCH counter has
missed > 0.
- Drop any class under an excluded path (generated / examples / test).
- Discard non-actionable gaps the engine cannot credit even with a
perfect test (see
references/coverage-signals.md): Kotlin inline /
inline reified functions (their bytecode is inlined into each call
site, so the definition lines stay ci=0 regardless of tests),
unreachable guards (require/check/error paths the public API
cannot trigger), and throw helper(...) lines where the helper throws
internally. Report these as non-actionable instead of proposing tests for
them.
-
Read before you write.
- Read the class(es) under test in full — public API, constructors, branch
conditions,
when/sealed exhaustiveness, error paths.
- Read existing tests in the module to match structure, naming, fixtures,
and the test source set/layout you will add to.
- Read collaborators you will need to substitute, so you can write stubs
(hand-written fakes), not mocks.
-
Propose the test cases — then WAIT.
- For each target, list the concrete cases: the method/branch, the input,
the expected outcome, and the stub(s) required. Map each case back to the
uncovered line/branch it closes.
- Present this list and wait for the user's confirmation before writing
anything. (Under
--triage you already stopped at step 1.)
- Pre-approved? When
--yes / --no-confirm is set, or the invocation
explicitly pre-approves writing without confirmation, emit the list for the
record and proceed directly to step 5 — do not wait.
-
Generate the tests (after approval — or immediately when pre-approved via
--yes). Write them per .agents/skills/kotlin-jvm-tester/SKILL.md — it owns
how a Spine JVM test is written (Kotlin by default even for Java code; the only
sanctioned Java suite is an XJavaSpec bridge test), JUnit 5 structure + Kotest
assertions, stubs not mocks, the Spec / XKtSpec naming rule, the backticked
@Nested layout, and the right helper for the target (testlib bases like
UtilityClassTest / ClassTest / SingletonTest, plus Guava EqualsTester, …).
Coverage-specific points on top of that skill:
- Map each test back to the uncovered line/branch it closes; cover API edge cases
and add a case per
when/sealed-class branch.
- Place the test under
<module>/src/test/kotlin/..., mirroring the
package of the code under test (KMP: src/jvmTest/kotlin/... or
src/commonTest/kotlin/... per the module's target). Reuse the file's
copyright header.
-
Verify.
- Re-run
:<module>:koverXmlReport.
- Confirm the previously-listed uncovered
nr lines/branches no longer
appear as gaps, and the class's LINE / BRANCH missed counters
dropped.
- Confirm the module total does not regress against
.codecov.yml.
- If a test fails to compile or the gap is not closed, fix and re-run before
reporting done.
Report
Return five sections (the Migration section is emitted only when Step 0
actually did work):
- Migration — what Step 0 changed, with the list of edited files and the
smoke-check result. Omit when Step 0 was a no-op (Kover already in place).
- Gaps — uncovered lines/branches found (file → lines/branches).
- Proposed cases — the awaited list from step 4.
- Generated — test files added, with the cases each covers.
- Verification — before/after coverage for the target, and confirmation that
no
.codecov.yml target regressed.
Safety
--triage is read-only. Step 0 never writes under --triage; if
Kover is not in place, emit "Setup required" and stop.
- Kover is applied without asking — for routine cases. Outside
--triage,
a fresh Kover install (no coverage plugin) and the routine vanilla-JaCoCo →
Kover migration run without a blanket approval gate — the project policy is to
encourage coverage measurement. The migration still flags the §7
manual-review surfaces and asks before applying those.
- Read-only until approval — for tests. Do not write tests before the user
confirms the step-4 list, unless the run is pre-approved with
--yes /
--no-confirm (or an equivalent in the prompt), in which case the list is
reported but writing proceeds without waiting. The migration gate was
already removed; this is the test gate's documented opt-out.
- Never weaken a
.codecov.yml target or extend its ignore list to make a
check pass.
- Never add a mocking dependency (Mockito, MockK, …) — write stubs.
- Bump the version. In a repo with root
version.gradle.kts, CI rejects any PR
that does not bump the version — tests-only changes included. Run the
version-bumped / bump-version skill before opening the PR. (Repos without that
file are not versioned Gradle projects; the check is not applicable.) If you had to
touch production code to make it testable, that is a separate change needing its
own review.