Detect and collapse production code that was split into two pieces (a 'pure' inner function and a thin wrapper/handler/component that calls it) for the single purpose of letting a unit test reach the inner piece. The smell is structure shaped by the test suite, not by the system. Use when reviewing a helper paired with a `.test.ts` of similar or greater size, when an exported function has exactly one production caller next door, when a function takes optional `deps` whose only non-default value lives in the test, when a getter/setter pair injects state the production code already owns, or when the user says "is this earning its keep", "why is this exported", "trace the callers", "is this split for the test", "the test is shaping the API", "could this be one function". Prefer cohesion over unit-testability; pay the regression-coverage cost a different way (integration tests, type safety, deletion).
70
86%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
A unit test is supposed to verify production code. Sometimes the relationship inverts: the test reaches for an invariant the natural API won't surface, so the production code is split into a "pure inner" and a "thin outer", with the inner exported solely so the test can call it. The split is invisible from the outside (one production consumer, no reuse), but it's permanent shape. The test suite became an architecture client.
That's the smell. The cure is to glue the pieces back into one function and find a different way to pay the regression-coverage bill: integration tests through the real boundary, type-level invariants, or deletion of a test that was insuring trivial branch logic at high seam cost.
Related skills: greenfield-clean-breaks owns the decision once you find a candidate; refactoring for caller counting and inlining mechanics; radical-options when the helper is honoring a bad shape; testing for what the post-collapse tests should look like; one-sentence-test to confirm the inner function has no independent product sentence.
Load on demand based on the task:
Three things often co-occur and get confused:
The seam may be load-bearing (sockets, signals, filesystem, network, policy callbacks) OR ceremonial. Don't classify the seam first; classify the split. Ask: if the test didn't exist, would the inner function exist as a separate piece? If no, the split is testability theater regardless of what the seam carries.
A useful corollary: if the outer's body is "call the inner, then format the result", the outer adds nothing. The inner IS the outer in disguise. Two names for one thing.
Count production callers. grep for the inner function's name
across the workspace, excluding the test file. If the count is one,
continue. If it's two or more, the split may be earning its keep
through reuse; stop and reassess.
Read the outer. If its body is a call to the inner plus formatting / I/O / wiring, the inner can be inlined.
Read the test. For each expect(), ask what it observes. If the
observation is internal state visible only through the seam, the
test was driving the split.
Count LOC. Test LOC vs SUT LOC. If the test is the largest artifact and its only consumer is itself, the cost-benefit is inverted.
Inline. Move the inner's body into the outer. Drop the export. Drop the seam.
Pay the coverage bill differently. Pick one:
dispose() when X", encode it with T extends Disposable or a
branded return type.Don't inline when any of these hold:
The question to ask: without the test, would I have written this as two pieces? If yes, keep the split. If no, inline.
From @epicenter/svelte, commit d5b61aed8:
session-lifecycle.ts 47 LOC (the "pure" inner)
session-lifecycle.test.ts 187 LOC (the only direct caller)
session.svelte.ts 69 LOC (the production "outer")The inner took getPayload / setPayload so the test could supply its
own slot. The outer was a $state declaration plus a forwarding call.
The inner had zero production callers other than the outer; the test
had zero production functionality to verify other than what the outer
already did end-to-end.
Inline: drop the inner file, let $state live in the outer, delete the
unit test. Net: minus 265 LOC, three files to one, two injection points
to zero. The four invariants the test was asserting are now visible in
~6 lines of branch logic, type-enforced by T extends Disposable, and
exercised on every app boot.
Article: Don't Split for the Test.
Form 1: paired getter/setter. Helper takes getX and setX for
one slot. Production caller closes over a let and passes both
closures. Inline the slot into the caller; let the rune or class field
own it.
Form 2: optional deps for fake substitution. Function has an
optional deps parameter whose default is the real implementation and
whose only non-default value is supplied by the test. Inline the
function into its sole production caller; drop the deps type.
Form 3: pure-inner + thin-outer. A run*() function does the work;
a command handler / route handler / component is a one-line wrapper
calling it. The outer adds no logic. Inline the inner into the outer.
Form 4: state-only mock injection. A factory takes a clock,
now, random, idGenerator parameter so tests can supply
deterministic values. This is sometimes legitimate (deterministic IDs
across distributed systems) and sometimes ceremony. Ask: does any
production caller pass a non-default value? If no, it's Form 2.
Form 5: re-exported internal for tests. A module exports a private
helper "for testing." If the export comment says @internal or
/** @testonly */, the test owns the export. Inline the helper into
its sole user; remove the export.
For sweeping a codebase, look for two signals together:
# Functions exported next to a test file with high test:SUT ratio.
for t in $(find packages apps -name '*.test.ts' -not -path '*/node_modules/*'); do
s="${t%.test.ts}.ts"
[ -f "$s" ] || continue
tl=$(wc -l < "$t" | tr -d ' ')
sl=$(wc -l < "$s" | tr -d ' ')
[ "$sl" -lt 1 ] && continue
ratio=$(( tl * 10 / sl ))
[ "$ratio" -ge 25 ] && echo "$t ($tl) vs $s ($sl) ratio=${ratio}/10"
done
# Functions exported with optional `deps` / `overrides` parameters.
grep -rEn 'export (async )?function [a-zA-Z]+\([^)]*\bdeps\??: ' packages/ apps/ --include='*.ts'
grep -rEn 'export (async )?function [a-zA-Z]+\([^)]*\boverrides\??: ' packages/ apps/ --include='*.ts'
# Paired getter/setter parameters.
grep -rEn 'get[A-Z][a-zA-Z]+\??:.*\bset[A-Z][a-zA-Z]+\??:' packages/ apps/ --include='*.ts'Each match is a candidate, not a verdict. Run the six-step procedure
on each. The intersection of "high LOC ratio" and "exported function
with deps parameter" is the hottest place to look.
cb12bcc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.