Apply response_language to all user-facing prose — narration between tool calls, status updates, progress notes, and findings included; preserve code, paths, and identifiers.

# Step: suite tune-up (strategy-governed, four ordered passes)

The repair half of suite health. `/gsd:testing-strategy` decides how fast a suite is
**born**; `transition`'s `suite_health_compare` decides **when** it has decayed; this
flow is what actually fixes it — and it is the only place allowed to write a new
`## Suite health` row.

**Order is doctrine: config before tests, and every test change justified by the
strategy — never merely because it made the suite faster.** A suite that got faster by
deleting the tests that would have caught the next bug did not get healthier.

<entry_points>

Three, and only three, ways to arrive here:

| Entry | Who fires it | Timing |
|---|---|---|
| **T1 — tier budget breach** | `transition`'s `suite_health_compare` writes `.planning/todos/pending/{date}-suite-health-t1.md` | **immediately** — a TDD-ergonomics emergency |
| **T2 / T3 / T4** | the same step writes a `Suite tune-up (milestone close):` todo; `complete-milestone`'s `audit-open` scan surfaces it | at **milestone close** |
| **Manual** | `/gsd:testing-strategy --tune-up` | whenever a human suspects decay |

Whichever route brought you here, the passes below are identical. If a todo triggered
this run, mark it complete when Pass 4 lands its row.

</entry_points>

<required_reading>
@~/.claude/gsd-core/references/test-strategy.md
`.planning/TEST-STRATEGY.md` — the project's own strategy. Without it there is nothing
to audit the suite *against*, and this flow degenerates into a stopwatch. Stop and say so.
</required_reading>

<process>

## Pass 1: Profile — evidence first

**No change without a measurement.** Produce three artifacts before touching anything:

1. **The slowest files.** Use the runner's own tail reporter, not intuition — `--durations=N`
   (pytest), `--reporter=verbose` / the slow-test threshold (vitest), `cargo nextest run`'s
   structured timings, `go test -v` package timings. Record the top ~10 with their times.
2. **The setup-vs-test split — three buckets, not two.** How much of the wall clock is
   `beforeAll` / fixtures / migrations / image pulls; how much is assertions; and how much
   is **waits inside test bodies** — fixed sleeps, retries, polling backoff? Count the
   fixed-duration waits (`sleep`, `setTimeout`-style delays) as a profile artifact. A
   suite that is 80 % setup has a config problem (Pass 2); 80 % assertions is a volume
   problem (tiering/sharding, not tuning); 80 % waits inside bodies is a serialization
   problem (Pass 3's serialization/waits class). **No bucket at 80 %? The dominant bucket
   wins** — run its pass first, re-profile, and route again; mixed profiles clear in two
   rounds, and re-profiling after each pass is what keeps the second round honest. They
   have different remedies, and picking the wrong one wastes the tune-up — the most common
   wall-clock sink in async suites is the third bucket, which a two-way split misroutes to
   sharding.
3. **The container lifecycle map.** How many containers start, when, and per what — per
   run, per suite, per file, or (the pathology) per test. Compare against
   `containers_started` in the latest SUMMARY's `suite-metrics:` block.

Re-measure the whole suite once, timed, as this run's "before" number. Write all three
down; Pass 3 and Pass 4 both read them.

## Pass 2: Config / cache — the predictable half

Work the per-stack born-fast checklist in `@~/.claude/gsd-core/references/test-strategy.md`
(`## Suite health` → born-fast defaults). It is class-based and explicitly non-exhaustive,
and it names **current APIs** — check the framework's current docs before emitting a flag,
because a perf recipe with no version attached is a bug with a delay fuse.

The classes it covers: container lifecycle (one per suite run, never per file; **reuse is
local-only**), the JS/TS pool model, Rust's per-test process runner, Python distribution
vs. order-independence, Go's parallelism knobs and result cache, and CI-side image/layer
caching.

Two rules that survive every stack:

- **Measure each change on its own.** A batch of five config edits that nets 30 % faster
  teaches nothing about which one to keep.
- **`isolate: false`-class trades are measured trades, never defaults.** Shared module
  state can turn a real failure into a false pass, and a false pass is strictly worse
  than a slow suite.

Re-measure. If Pass 2 alone cleared the trigger, the fix-class is **config-drift** —
record it in Pass 4 and stop; do not go looking for tests to delete.

## Pass 3: Suite audit against the strategy

Only now do tests change, and every change is **justified by the strategy, never merely
"made faster"**: each one cites `.planning/TEST-STRATEGY.md` — the level emphasis per
subdomain, what-not-to-test, and the no-duplicate-coverage rule. Five classes, each a
**strategy violation first and a performance cost second**:

| Class | What it looks like | Strategy basis |
|---|---|---|
| **Implementation-detail tests** | assertions on internals/mocks rather than behavior; they break on refactor | behavior-over-implementation; mock only at ports |
| **Duplicated coverage across tiers** | the same behavior proven at e2e *and* integration *and* unit | test each behavior **once**, at the cheapest level that gives confidence — push it down the pyramid where the strategy permits |
| **Obsolete tests** | cover a removed feature, a migrated adapter, or a decision the project reversed | nothing in the strategy asks for them |
| **Over-broad shared fixtures** | one fixture seeding the world so twelve tests can use three rows of it | the setup share Pass 1 measured |
| **Accidental serialization / unconditional waits** | a shared port/file/DB/global that forces workers into a queue — or fixed `sleep()`s standing in for condition polling | parallel-safe isolation is a standard, not an optimization; **poll, never sleep** (`flaky-test-checklist.md`) |

**Never delete a test to hit a number.** A test that is slow *and* load-bearing gets
moved, not removed. If a deletion or demotion cannot cite a line of the strategy, it does
not happen in this flow — raise it as a strategy question instead (run
`/gsd:testing-strategy` and take the Update path), because changing what the project
tests is a strategy decision, not a tune-up decision.

## Pass 4: Re-baseline — append the row, record the fix-class

Re-measure the whole suite once, timed, exactly as Pass 1 did, and **append a new dated
row** to `.planning/TEST-STRATEGY.md`'s `## Suite health` table:

```markdown
| YYYY-MM-DD | {test_count} | {wall_clock_ms} | {containers_started \| —} | {config-drift \| test-debt \| mixed (dominant: …) \| none (volume/mix — routed to C1)} |
```

`wall_clock` is **integer milliseconds, minimum 1**, from a real clock bracket — the
same rule the SUMMARY schema and Step 6.5 state, because this table is the compare's
baseline and second-resolution rounding is precisely the blindness that once made a
126 % sub-second regression read as 0.0 %. `ms/test` is derived at compare time, never
recorded (one source per number).

**Append. Never rewrite, overwrite, or replace the previous row.** The history *is* the
trend the T2/T4 triggers compare against — a table with one row can only ever answer
"how fast is it now", never "is it decaying". The same append-only rule the
`## Coverage debt` section follows.

**Record the fix-class** — where the win actually came from:

- **`config-drift`** — Pass 2 alone cleared it. The suite's *configuration* fell behind
  (stale framework API, lost cache, container lifecycle regression). Expect recurrence at
  the next framework major; the remedy is watching release notes.
- **`test-debt`** — Pass 3 was needed and supplied the win. The suite accumulated tests
  the strategy does not ask for. Expect recurrence as the team grows; the remedy is
  review discipline.
- **`mixed (dominant: config-drift | test-debt)`** — both passes contributed materially.
  Record the dominant contributor; a 90 %-config win filed as pure test-debt mis-teaches
  the trend this column exists to teach.
- **`none (volume/mix — routed to C1)`** — the honest no-fix outcome. All four passes ran,
  Pass 2 found no lever that paid, Pass 3 found nothing the strategy does not already ask
  for, and the correct conclusion was "it was volume all along — tiering, not tuning; route
  to the CI ladder's C1". This is a **real recorded outcome**, not `—`: a tune-up *did* run
  and correctly concluded there is nothing to tune. Recording `config-drift` or `test-debt`
  here would fabricate a failure mode the run disproved; recording `—` (no class) reverts
  the T4 backstop's baseline to the previous tune-up and re-fires it forever (e2e-10 F1/F2).
  `transition`'s compare treats this as a tune-up-happened row for T4.

Recording it is the point: over several milestones the column tells the project which
failure mode it actually has, which is the only way the *next* tune-up starts in the
right pass.

Then report, in this shape:

```
Suite tune-up complete — {trigger} cleared.

  Before: {test_count} tests · {wall_clock_ms} ms · {ms/test derived} ms/test · {containers_started} containers
  After:  {test_count} tests · {wall_clock_ms} ms · {ms/test derived} ms/test · {containers_started} containers
  Fix-class: {config-drift | test-debt | mixed (dominant: …)}
  Changes: {config edits} · {tests moved/demoted/deleted, each with its strategy citation}

  New Suite-health row appended to .planning/TEST-STRATEGY.md (history preserved).
```

If the trigger did **not** clear, say so plainly and append the row anyway with the real
numbers — an honest unmoved baseline is data; a row that flatters the run is not. Record its
fix-class as **`none (volume/mix — routed to C1)`** (never `—`, which jams the T4 backstop —
e2e-10 F2), and name what is left (commonly: it was volume all along → the remedy is
tiering/sharding via `/gsd:cicd-strategy`'s C1, not tuning).

</process>
