Skip to content
Back to skills

Mutation Review

ASecurity

Propose→execute mutation-review protocol for co-located unit tests — mock-boundary discipline, assertion-strength anti-patterns, the propose/execute split, and the blocker-mutant emission contract. Runner commands and repo-specific blocker mutants come from the consumer config and extension files.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 19, 2026
ai-agentstestinggitsecurity

Security analysis

A100/100

Scanned September 25, 2026

npx -y skills add manoldonev/second-shift --skill mutation-review --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Mutation Review?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Mutation Review
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/manoldonev-mutation-review/badge)](https://www.skillsdirectory.com/skills/manoldonev-mutation-review)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: mutation-review
description: Propose→execute mutation-review protocol for co-located unit tests — mock-boundary discipline, assertion-strength anti-patterns, the propose/execute split, and the blocker-mutant emission contract. Runner commands and repo-specific blocker mutants come from the consumer config and extension files.
---

# Mutation review (propose → execute)

This skill defines the **generic** mutation-review protocol: `unit-test-mutation-reviewer` proposes, and the propose-mode orchestrator that dispatched it executes. (The pipeline's shipped sequencer, mutation-gate.mjs, was retired unreachable in #574 — the protocol binds whichever orchestrator adopts it; #482 tracks the consumer on-ramp.) It is repo-agnostic: the test-runner command comes from the consumer config, and any repo-specific blocker-mutant classes come from `.claude/second-shift/blocker-mutants.md`. Repo conventions (which test framework, co-location rules, fixture tricks) live in the consuming repo's own testing skill, not here.

**Runner commands come from config.** The executor runs the repo's configured `test` command (from `<repo-root>/.claude/second-shift.config.json` `commands.<id>.test` — the sole key, else the key equal to the main checkout's directory name (also from a linked worktree); env override `SECOND_SHIFT_CONFIG`) scoped to the spec path — this skill never hardcodes a package manager or command. Where a step below says "run the spec", it means: invoke the configured `test` command against that spec file.

## Mock boundary

| Layer                  | Prefer                                                                                                                                                                               |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Service business logic | Real collaborators where cheap; mock only I/O boundaries (the data-access handle, external HTTP, the queue/broker)                                                                   |
| Data access            | Unit tests may mock the injected data-access handle when testing query assembly; assert the **filter predicates** (especially the tenant/owner-scope predicate) and returned rows, not just that a query builder was called |
| Controller / handler   | Unit test with mocked service; assert input→output mapping (DTO/response shape + status)                                                                                             |
| Worker / processor     | Mock queue/logger; assert **job payload shape and chaining**, not just call count                                                                                                   |

## Assertion strength (anti-patterns)

Idioms below are illustrated with a common matcher vocabulary; use your test framework's equivalents.

**Weak — mutation survives:**

- Asserting a mock was called with no argument assertion (`toHaveBeenCalled` without `toHaveBeenCalledWith`)
- Asserting only that a method was invoked once when the bug is in **what** was passed or returned
- Testing the mock's return value instead of the SUT's output
- Single happy-path when the diff adds `if/else` branches

**Strong — mutant killed:**

- Argument assertions on meaningful values (the tenant key, a query predicate, a queue payload)
- Equality / throw assertions (`toEqual` / `toThrow` / `rejects.toThrow`) on the SUT's return value or error
- Table-driven cases (`it.each` / parametrize) over the branch matrix (status codes, guard combinations, domain edge cases)
- Multi-tenant services: seed two tenants, assert cross-tenant isolation (no leakage)

## Mutation review process (propose → execute split — no full-matrix mutation tool)

The reviewer **proposes**; the sequencer **executes**. The mutation reviewer agent does NOT apply mutants or run the test command — keeping execution out of the schema-forced agent turn is what prevents a StructuredOutput staller. The agent emits machine-applicable patches; the sequencer executes each blocker patch via a sequential **schema-free** executor agent (apply via Edit → run the spec → revert → emit a plain-text `MUTANT_RESULT` line parsed in JS) and computes the verdict deterministically — the pipeline session never applies/runs/reverts mutants itself.

The mutation stage runs after all commits land, so the worktree is clean — use `git diff <base>...<head>` (an explicit commit range from the dispatching stage), not `git diff HEAD`. Scope mutants to lines changed in that range only. Three dots, not two: three-dot measures from `merge-base(<base>, <head>)`, so commits that landed on the base branch after the branch point stay out of the range. Under two-dot they appear as deletions and you would propose mutants for code this ticket never touched.

For each changed production file in the diff (the **reviewer's** job, propose-only):

1. List mutation targets: conditionals, operators, filters, early returns, thrown errors, tenant/owner-scope predicates.
2. Propose 5–15 concrete mutants (e.g. "remove the tenant-scope predicate from the `where`", "`&&` → `||`", "delete the throw branch").
3. Trace each mutant against the spec: **killed** (assertion fails), **survived** (tests still pass), **untested** (no path reaches code).
4. For each **blocker-class** mutant predicted survived/untested, emit a uniquely-matching `{ originalSnippet, mutatedSnippet }` patch + the `specPath` — so the executor can verify it. If you cannot produce a unique snippet, downgrade to `warning` (an unverifiable mutant must never block).
5. Flag mock-only specs where every assertion is call-count without argument inspection.
6. **Decorative tests** — the mirror question, nearly free once step 3 is done. For each test **added in the range**, name the mutant it alone kills. If there is none — it kills nothing, or a cheaper sibling kills everything it does — report it on the propose-mode advisory channel with the remedy: delete it, or fold its fixture into the case that already covers the rule. `warning`/`note` only, never blocker-class, so this axis can never discount a coverage floor.

The **sequencer's executor agent** then, per blocker-class patch: apply → run the configured `test` command against `<specPath>` → test failure means **killed** (drop) / all pass means **survived** (keep blocker) → revert (always, even on error). A handful of targeted runs — not a full mutation matrix.

Non-blocker mutants (logic branches, error paths, operators on low-risk paths) stay LLM-predicted and advisory.

## Blocker-class mutants (always Blocker if survived/untested)

These generic classes are **always** blocker-class when the mutant survives or is untested — the mutant reaching production would be a security or correctness breach:

- **Tenant-isolation / owner-scope predicate removed or wrong** — the filter that restricts a query to the current tenant/owner is deleted or compares the wrong key (multi-tenant data leak).
- **Auth guard / ownership check bypass** — a guard annotation or an ownership comparison removed or negated.
- **Validation throw → silent return** — a `throw` on a validation or not-found path mutated to `return undefined` / `return null`.

**Repo-specific blocker instances are additive.** If `.claude/second-shift/blocker-mutants.md` exists in the repo under review, load it — it lists the repo's concrete instances of these classes (e.g. the exact ORM predicate, guard decorator, and the tables it scopes) plus any domain-specific blocker classes (financial rounding guards, permission-scope mappings, etc.). Those add blocker instances; they never relax these generic classes. A repo with no such file uses the three generic classes above.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.