- name
- apply-test-doubles
- description
- Apply `/cd-test-architecture`'s Step 4b build-vs-document decision logic against an existing, saved assessment report — or, when no valid report path is given, against a target to assess first — without re-running the full Steps 0-6 assessment each time. Use when the user wants to revisit or change a component's Build/Document choice from a saved cd-test-architecture report, says "apply the test doubles", "re-run Step 4b", "change the build-vs-document decision", or cites the `/apply-test-doubles <path>` command from a test-double setup guide.
- argument-hint
- [<report-path-or-target>] [--component <name>] [--yes]
- role
- worker
- user-invocable
- true
# Apply Test Doubles
Role: worker. Applies `../cd-test-architecture/SKILL.md`'s Step 4b
build-vs-document decision logic against a resolved report — one already
saved on disk, or a fresh one produced by assessing a target — without
re-running the full Steps 0-6 assessment when a valid report already
exists.
You have been invoked with the `/apply-test-doubles` command.
## Parse Arguments
Arguments: an optional positional `<report-path-or-target>`, an optional
`--component <name>`, and an optional `--yes`.
- `<report-path-or-target>` — a path to a previously saved
`/cd-test-architecture` report, or a target application/repo path to
assess when no such report exists yet. May be omitted entirely — see the
absent-path row below.
- `--yes` — run non-interactively: this step's own re-entrant Step 4b
prompt (Step 2) is skipped, per `human-oversight-protocol`'s `--yes` /
`DEV_TEAM_AUTO_APPROVE=1` / no-TTY convention — see Step 2 for the exact
per-component behavior.
- `--component <name>` — scope processing to one named component. Applied
as a post-hoc filter over the resolved report's already-existing rows,
identically on both paths — **never** forwarded as `cd-test-architecture`'s
own `--component` argument on the target path. A single-component-scoped
target-path assessment can legitimately produce no report **file** at all
(`cd-test-architecture`'s Output is `.dev-team-reports/cd-test-architecture-<app>.md`,
"or chat for a single component"), which would leave this skill's own
fast path with nothing to read — so the target-path self-invocation
always assesses the full target, and `--component` narrows the result
afterward, identically to the fast path.
## Steps
### 1. Resolve report or target
Every invocation resolves to exactly one **report** before anything else in
this skill runs — either an existing one read directly (the fast path), or
a fresh one produced by assessing a target (the target path). This is one
branch point, not two independently-described procedures: whichever state
below applies, resolution ends at the same "read the report" step.
**Resolution states:**
| `<report-path-or-target>` | Resolves to | Path |
|---|---|---|
| Absent | the current repo, passed to `/cd-test-architecture` as an explicit resolved cwd/repo path argument — never a bare invocation, which would trigger `cd-test-architecture`'s own interactive target prompt | target |
| Given, non-empty, resolves to no file or directory on disk | the given string, passed to `/cd-test-architecture` as its explicit target argument | target |
| Given, resolves to an existing directory | the given directory, passed to `/cd-test-architecture` as its explicit target argument — a directory is not a report; there is no file there to read | target |
| Given, resolves to an existing file | that file, checked against the structural validity check below | fast (if valid) / target (if not) |
**Target path.** Invoke `/cd-test-architecture` against the resolved target
(current repo / given string / given directory) — a full-target
assessment, never scoped by `--component` at invocation (see Parse
Arguments above for why) — always forwarding `--yes` so its own inline
Step 4b prompt never fires; this skill's own re-entrant Step 4b step (a
later step of this skill) is the sole place the operator is prompted, on
both paths uniformly. `cd-test-architecture`'s own error handling governs
an unresolvable target string; this skill does not duplicate that
validation. Resolve the fresh report at
`.dev-team-reports/cd-test-architecture-<app>.md`, per
`../cd-test-architecture/SKILL.md`'s own Output Path rule (cited, not
restated), then proceed through the fast path below exactly as an
existing report would — including the `--component <name>` filter,
applied post-hoc there.
**Structural validity check (existing-file case only).** A real
`cd-test-architecture` report carries three markers, checked together as
one cohesive procedure — not three independently-worded checks that could
drift out of sync with each other:
1. The `# CD Test Architecture` title line.
2. The `### Target architecture (per component)` heading — level 3, with
the `(per component)` suffix.
3. A `Build/Document status` column in that section's table.
All three present → the fast path: read that report's Target architecture
table directly, and do not invoke `/cd-test-architecture`. Missing any one
of the three takes the target path too, treating the file's path string as
a target to assess — not a malformed-report error.
**Fast path: locate the companion setup guide.** Locate the companion
setup-guide artifact
(`.dev-team-reports/cd-test-architecture-<app>-test-double-setup.md`,
#1436) via the resolved report's own `**Target**` header field — the
`<app>` value recorded in the report's content — never by the report
file's name on disk, so a renamed or relocated report still resolves its
companion file correctly. This intentionally differs from how
`cd-test-architecture` itself names the companion file at write time (from
the main report's own filename, per `../cd-test-architecture/SKILL.md`'s
Path rule): that skill always knows the filename it is about to give the
main report, so deriving from it is reliable; this skill instead reads a
report whose file may have been renamed or relocated since it was written,
so it cannot trust the on-disk filename and must recover `<app>` from
content instead. **Before interpolating `<app>` into a path, slugify it**
(strip or reject `/`, `\`, `..`, and a leading `~`) and confirm the
resulting path still resolves inside `.dev-team-reports/`; if it would
not, treat the companion file as not found (the non-error cases below)
rather than writing outside that directory. When no companion file is
found at the derived path, state plainly which of two non-error cases
applies:
- The resolved report's Target architecture table has zero
off-gate-eligible rows (no row with a non-blank `Build/Document status`
cell) — nothing was ever expected to be written for this report; take no
further action.
- The report has eligible rows but predates #1436 (no companion file was
ever written for it) — state this plainly, then create the setup-guide
artifact fresh at the derived path, exactly as `cd-test-architecture`
itself would on a first run.
### 2. Apply Step 4b's decision logic
This step is the **only** place a decision is made this invocation,
regardless of which path produced the resolved report:
- **Fast path** — Steps 0, 1, 2, 2b, 3, 3b, 4, 5, and 6 never ran at all
this invocation; the resolved report was already sitting on disk and was
read directly, including Step 4's own recommendations — this step reads
what Step 4 already concluded, it does not re-derive it.
- **Target path** — those same steps did run, as part of producing the
fresh report (the `/cd-test-architecture` self-invocation in Step 1
above), but their own inline Step 4b was a no-op — every row defaulted to
`Document-only` with no prompt — because Step 1 already forwards `--yes`.
This step is still the first and only place the operator is actually
prompted.
**Cite, don't restate — the one architecturally significant decision in
this skill, made explicit so it survives independent of any one plan
document.** Apply exactly the decision procedure in
`../cd-test-architecture/SKILL.md`'s `### 4b. Build-vs-document decision
(off-gate adapter test doubles)` heading — its Database-specific branch, its
Downstream-service branch, and that branch's library-vs-hand-rolled
sub-question — as the single source of the build-vs-document branching
rules. That procedure is not restated here. This citation was chosen over
physically relocating Step 4b's content into a shared file: Step 4b's
already-reviewed, multi-branch prose (its Database-specific branch,
Downstream-service branch, and their sub-question) would otherwise need
extracting into a new home touching every existing
`cd-test-architecture` test file, for a purely organizational move this
skill's actual scope doesn't require; citing it in place fully satisfies
"don't duplicate the branching rules" without that blast radius.
**Off-gate-eligibility parsing rule — cited from `cd-test-architecture`'s
own Output section, not independently derived.** `../cd-test-architecture/SKILL.md`'s
Target architecture table documentation states that a row's
`Build/Document status` cell is set to one of the three enum values only
for a row Step 4 flagged as needing an off-gate adapter test double; an
unflagged row carries no value in that cell at all. A component is
therefore off-gate-eligible **iff** it has at least one Target
architecture row with a non-blank `Build/Document status` cell — this
skill cites that blank-vs-set convention rather than asserting it as an
independent rule.
**Every eligible component is re-offered, regardless of prior status.**
Every off-gate-eligible component is re-offered this decision on every run,
regardless of its currently-recorded `Build/Document status` — including a
component already resolved to `Build (testcontainers)` or `Build (Fake)` —
so the operator may change a prior decision.
**`--component <name>` scoping.** When `--component <name>` was given and it
matches an off-gate-eligible component (per the parsing rule above), only
that component's row resolves through this decision — no other component's
row is touched. When no `--component` was given, every off-gate-eligible
component in the resolved report is processed in one batched pass, mirroring
Step 4b's own batching rule (cited, not restated).
**Unmatched or non-eligible `--component <name>`.** Two distinct cases, both
stated by the exact name given, with no action taken — never a fuzzy or
nearest-match substitution:
1. `<name>` does not appear anywhere in the resolved report's Target
architecture table — state that `<name>` was not found in the resolved
report, and take no further action.
2. `<name>` appears in the report but has no Target architecture row with a
non-blank `Build/Document status` cell — state that `<name>` has no
build-vs-document decision to apply, and take no further action.
**Zero eligible components.** Same family of outcome as the two cases above,
just with no `--component` name to name: when the batched pass (no
`--component`) or the named lookup (`--component <name>`, once past the
unmatched/non-eligible check above) finds no off-gate-eligible component,
state that there is nothing to apply and take no further action.
**Non-interactive behavior for this step's own prompt.** Per
`human-oversight-protocol`'s `--yes` / `DEV_TEAM_AUTO_APPROVE=1` / no-TTY
convention (the same convention `cd-test-architecture`'s own Step 4b
documents at its Parse Arguments section): under any of those conditions,
surface no prompt for this step either. A component with **no prior
recorded decision** defaults to `Document-only`, exactly mirroring what a
non-interactive `cd-test-architecture` run would have produced; a
component with an **existing recorded decision** is left exactly as
recorded, not silently re-resolved to `Document-only` — this step never
reports an outcome that contradicts what the report already says. Either
way, no Story is proposed and no report write-back happens (see Step 3)
for a component this run did not actually decide.
**Version-skew advisory (non-blocking).** Resolve the currently installed
plugin version with the same resolver `/version` already uses:
```bash
sh "$CLAUDE_PLUGIN_ROOT/hooks/py.sh" "$CLAUDE_PLUGIN_ROOT/hooks/lib/plugin_version.py"
```
This prints one line on success, e.g. `dev-team@bfinster v10.4.0 (scope:
user)` — parse the version out of the token between `v` and ` (scope:`
(here, `10.4.0`). If the resolver instead exits non-zero (plugin not
installed, or the install record is missing/malformed), prints nothing, or
the parsed value is the literal `unknown` (its own fallback when the
version field is absent), emit no advisory and proceed silently — never
fabricate a comparison from a value the resolver itself couldn't resolve.
Otherwise, compare the resolved version against the resolved report's
Provenance `` `dev-team` plugin version `` field (exact field name and
formatting per `knowledge/report-template.md`'s Provenance section). If
they differ, emit one non-blocking advisory line naming both versions —
e.g. "this report was generated by dev-team vX.Y.Z; you're running
vA.B.C — Step 4b's decision logic may have changed since this report was
written." — before applying Step 4b's current logic, then proceed
unchanged. This never blocks or alters the decision logic itself.
### 3. Dispatch
`cd-test-architecture` is advisory only (its own Constraints: "it proposes
a downstream Story, it never invokes `/build` or edits code directly") —
Step 4b's live invocation proposes a Story within its own report/chat
output (Step 6's only documented output channel), never as a separate
file. This step's dispatch mechanics match that exactly, plus one
write-back this skill's own premise requires that Step 4b's live
invocation has no reason to need:
1. **Write back the resolved report's `Build/Document status` cell** for
each component this run resolved, updating the on-disk report in place
so it reflects the new decision — matching the **full cell contract**
`../cd-test-architecture/SKILL.md`'s Output section defines (cited
above), not just the enum value: a `Build (Fake)` write carries that
branch's caveat verbatim in the same cell (the hand-rolled or
library-backed variant, per the Downstream-service branch, cited not
restated), and when the library-vs-hand-rolled sub-question resolves a
tool, the `Double (to run config-free)` column is updated to name it,
per the Output section's own tool-citation rule. A row reverting to
`Document-only` or `Build (testcontainers)`, or a `Build (Fake)` row
whose sub-question now resolves to hand-rolled, has any
previously-written caveat text removed from the status cell **and**
any previously-cited tool name removed from the `Double (to run
config-free)` column — both cells always reflect only the current
decision, never a stale one layered under it. Without this
write-back, the report and any Story proposed below would disagree
about what was just decided, and the next `/apply-test-doubles` run
against the same report would re-read the stale status. This
write-back is unique to this skill: Step 4b's live invocation writes
the cell once, at report-creation time, and never needs to revise it.
If the write fails (permission, read-only path), state that plainly
and surface the resolved decisions in chat instead — per
`knowledge/report-output-location.md`'s non-fatal-write convention,
never proceed as if the write succeeded. The report's own header and
Provenance block (`knowledge/report-template.md`) are deliberately
left untouched by this write-back — they continue to describe the
original assessment run, not this later revision; the version-skew
advisory (Step 2) compares against that same original-run Provenance
value on every subsequent invocation, which stays meaningful since a
write-back only ever changes a recorded outcome, never Step 4b's own
decision logic.
2. **For a component resolving to a Build outcome, propose a Story** —
rendered as output text, using exactly the title and description
conventions `../cd-test-architecture/SKILL.md`'s Database-specific
branch and Downstream-service branch already define (cited from Step
2's own citation above, not restated here). This is advisory only,
identical in kind to Step 4b's own live invocation — it never invokes
`/build` or writes a Story file itself.
3. **The setup-guide artifact (#1436)'s per-component *inclusion* is
independent of this decision; its *Classification* is not.**
`../cd-test-architecture/SKILL.md`'s Companion section states a
component's section is included regardless of whether the operator
chose Document-only for it (cited, not restated) — this step never
adds or removes a section on that basis. But that same section's
Classification is keyed to the resolved Build variant and, for a
Downstream-service `Build (Fake)` row, the library-vs-hand-rolled
sub-question's answer (cited from the Companion section's own
Classification rule, not restated) — both of which Step 2 explicitly
lets the operator change. When this run changes a component's
Classification (e.g. `Build (testcontainers)` ↔ `Build (Fake)`, or a
different library resolves than the guide currently records),
re-emit that component's setup-guide section per the Classification
rule so the guide never names a tool the report no longer
recommends. A component whose Classification is unchanged this run
has its setup-guide section left as-is.
4. **This proposed Story is advisory chat/report output only — there is
no automated materialization path today, the same as Step 4b's own
live invocation.** `../issues-from-assessment/SKILL.md` derives its
own Stories directly from the Target architecture table (one per
(component, layer), per its own Step 2), independent of the
Build/Document decision or this step's proposed Story text — it is
not a consumer of what this step just proposed, and this skill does
not invent a second materialization mechanism to bridge that gap. An
operator who wants this proposed Story tracked creates it themselves
from the proposed text, exactly as they would from a live
`cd-test-architecture` run.
**Single ready-to-run command.** Invocation is exactly `/apply-test-doubles
<path>` — `--component <name>` is the only other **scoping** argument
(`--yes`, per Parse Arguments, controls interactivity, not scope),
matching what #1436's setup-guide artifact already emits with the real
path filled in. No other argument is ever required.
View on GitHub