sdlc-unittest
Invoked explicitly by /unittest. Implements automated unit and mocked-integration tests from the test case catalog, using the test framework and file conventions of the active stack profile, tagging e
SKILL.md.claude/skills/sdlc-unittest/SKILL.md
GitHub/unittest — automated unit tests
Protocol (shared — do not restate here)
Framework assets — .agent/framework.yaml, .agent/steps/, .agent/rules/,
.agent/gates/, .agent/templates/, .agent/modules/ — resolve project-first: use
the repository's copy when it exists, otherwise ${CLAUDE_PLUGIN_ROOT}/.agent/…. That is
how one repo can override a single template or rule without forking the framework.
.agent/project-context.yaml and .agent/state/ are always project-local — never
read or write them under the plugin root.
- If
.agent/state/context-cache.mdexists and itsframework_versionmatches.agent/framework.yaml, Read ONLY that file. Otherwise Read.agent/steps/context-loader.mdand execute it, then write the digest back to the cache. - Read
.agent/steps/gate.mdand execute it with the phase inputs below. - On completion, format output per
.agent/steps/report-footer.md. Never copy the contents of those files into this skill.
Phase inputs — phase unit, gate G5a, input
{{paths.testcase_dir}}/{FEAT-ID}/TC-*.md, outputs under {{layout.unit_tests}} and
{{layout.integration_tests}}.
Step 1 — Input check
Gate Step 2.7 has already run .agent/steps/input-check.md against
input_contracts.unit. Act on its verdict:
NO_SOURCE— no test case catalog. Report and stop.NOT_READY— usually rows whose Expected result is not checkable ("works correctly", "returns the right thing"). Report those rows by TC-ID and stop: whoever automates an uncheckable row has to invent the expectation, and the test then encodes that invention rather than the requirement. QA owns those cells.PARTIAL/READY— proceed. A missing implementation is non-blocking — writing the test first is legitimate, and the failure is the point.
A hand-written catalog is a valid input.
Step 2 — Select the rows
Take only catalog rows with Level in {unit, integration-mocked}. Rows marked
acceptance, integration-live or eval belong to /run-test and /eval; implementing
them here means running live systems in the fast suite, and the fast suite stops being
run.
Step 3 — Read what exists
Read the existing test files for the modules in scope, and the shared fixtures. Reuse the fixtures. A second fixture that does what an existing one does is how a suite becomes impossible to reason about.
Read the implementation only to learn the seams — constructor signatures, injection points, exception types. Do not read it to learn what to assert; assert the catalog's Expected result. Reading the implementation for expectations is how tests come to describe the bug rather than the requirement.
Step 4 — Write the tests
One test per TC row. Follow the profile's test_conventions: file_pattern for file
names, name_pattern for test names (typically
test_uc{n}_sc{m}_{expected_behavior}), and framework for the idiom.
Tag every test with @trace.verifies={UC-ID}-SC{n} and @trace.test_type={level}, in
the syntax traceability.tag_syntax names.
Assert the Expected result and the Oracle the catalog specifies:
exact— compare literallyschema— assert shape, required fields, types and constraints, not exact content
Isolation is not optional at this level. No network, no real database, no real broker, no real model. Inject through the protocol and substitute a double. A unit test that reaches out is an integration test with the wrong label, and it will be the one that fails in CI for reasons nobody can reproduce.
Step 5 — Fill in the catalog
Write path::test_name into each row's Automated by cell and set Status. A test that
cannot be traced back to a case is a test nobody will know to update when the requirement
changes.
Step 6 — Run and iterate
Run quality_commands.test, then quality_commands.coverage.
When a test fails, the default diagnosis is that the code is wrong. Do not adjust the
assertion to match observed behaviour. Classify against framework.yaml → loop_routes:
| Observation | Diagnosis | Action |
|---|---|---|
| Spec is clear, code does something else | impl_defect |
finding routed to code; stop here |
| The assertion encodes the wrong expectation | test_defect |
fix the test, and record the finding |
| The scenario is ambiguous | spec_gap |
finding routed to srs; do not guess |
Changing a test requires a recorded finding. Silently relaxing an assertion is the single fastest way to make a suite worthless, and it is invisible in review.
Coverage below coverage_min means missing cases, not a threshold to lower. Find which
branches are untested and check whether the catalog has a row for them; if it does not,
that is a gap in the catalog.
Step 7 — Evaluate G5a and record
Run .agent/gates/G5a-unit.yaml: suite green, coverage at or above minimum, no test
file that collects zero tests, every unit-level row has Automated by, every test
tagged, no live provider reachable from a unit test.
The empty-file check is deliberate. An empty test file is worse than a missing one: it reads as covered while covering nothing. Any placeholder file that a previous author left behind under the profile's test globs must be filled or deleted before this gate passes — report each one by path rather than quietly collecting zero tests from it.
Update state: phase: unit, the gate verdict and input_hash, and the coverage number
into quality.json.
Boundaries
- Do not modify implementation code. If a test proves the code wrong, that is a
finding routed to
code. Even a one-line obvious fix — the separation is what keeps the diagnosis honest. - Do not skip, xfail or mark a test optional to reach green. A skipped test carries a reason and a finding.
- Do not lower
coverage_minor any threshold. - Do not write acceptance or eval tests here.
- Do not delete an existing failing test to make the suite pass.