new-golden — decide the split, then build a fixture that can fail
Two different tools, constantly confused:
- Unit test — pure logic, no network, no DB, no model. Fast, runs in CI on
every PR, asserts exact behaviour.
- Golden test — an LLM surface (routing, extraction, synthesis, an
enrichment pass) against a real model. Opt-in, excluded from CI, slow,
asserts structure, not text.
The split
Put in a unit test everything that does not need a model: parsing, tree
building, prompt construction (not its answer), digests and cache keys,
triviality gates, ordering, rendering, pruning. Most of a pipeline is this, and
it should be — pure functions are where cheap certainty lives.
Put in a golden test only what a model's behaviour decides: does retrieval
rank the right thing, does the pass produce a well-formed artifact, does the
router pick the right agent, does the JSON parse.
If you find yourself wanting a golden to check something deterministic, extract
the deterministic part into a pure function and unit-test that. A golden is
the most expensive test you own — spend it only where a real model is the thing
under test.
The fixture is the test
A fixture that only contains shapes the code already handles proves nothing.
This is the single most common way a suite passes while the code is broken: the
sample repo/document/payload is small, flat and friendly, and every hard case
lives only in production data.
Real example: a documentation pipeline had a small flat fixture repo. Three
defects shipped anyway and only surfaced on a 148-class codebase — an aggregate
step ran on the wrong size budget (the fixture's inputs were never big enough to
overflow), a model swap silently skipped regeneration (the fixture was never
re-run under a second model), and a deep nesting chain cost N redundant calls
(the fixture was two levels deep).
So when you build one, deliberately include:
- Depth and degenerate shapes — a long single-child chain, an empty
container, a leaf at the top level, a cycle if the domain allows one.
- Something big enough to hit a limit — a batch, window, timeout or page
size must be reachable, or the budget is untested by construction.
- A second run under changed conditions — different model/config/version —
to prove incremental keys invalidate instead of silently skipping.
- The boring-but-wrong case — the input that looks processable and is not.
Ask out loud: "which shape, if it appeared in real data, would break this
code?" Then put that shape in the fixture.
Assertions: structure, not text
A model's wording changes between runs and versions; asserting it makes the test
flaky and worthless. Assert instead:
- the right item was selected/ranked/produced (identity, not phrasing);
- the artifact's shape — required fields present, parses, non-empty, within
bounds;
- relationships — the child resolved, the reference points somewhere real;
- counts and tiers — N produced, the trivial ones skipped;
- idempotence — a rerun with nothing changed does no work.
Never assert a full generated sentence. If you need semantic quality, assert a
required token the answer cannot be right without (a domain identifier), and
accept anything around it.
Procedure
- Classify what you are testing: deterministic → unit; model-decided →
golden. Split the thing in two if it is both.
- Extend the existing fixture rather than inventing a parallel one, unless
the new shape would distort the old assertions — then add a second, named for
the shape it carries (
…-deep-chain, …-oversized).
- Write the failure first: state the defect this test would have caught. If
you cannot name one, you are writing a test that can only pass.
- Gate it the way the repo gates goldens (marker/env flag), so it stays out
of CI and does not need a model on a laptop.
- Run it against a real model before shipping. Do not commit a golden you
have not seen pass — an unverified golden is worse than none, because it
looks like coverage. If no engine is available right now, say so explicitly
and record it as pending rather than merging it green-by-assumption.
- Close the loop back to the spec. A golden usually exists to assert some
acceptance criterion (a
Scenario: / WHEN-THEN / requirement written before
the code). Once the test exists and passes, name it in that criterion —
write the test's class into the scenario line, e.g. THEN … (asserted by MyFlowTest). This makes the spec→test link traceable both ways and is the
hook a drift-lint can check (a criterion naming a test that no longer exists
is a caught rename). A criterion that can never name a test was never a
criterion — delete it or make it testable. Skip only when the repo keeps no
written spec for the change.
- Record the cost: goldens are slow. Note roughly how long the lane takes,
so the next person knows what they are opting into.
Triggering contract
Should fire: "нужен golden на новый пайплайн" · "unit or golden for this?" ·
"почему фикстура это не поймала".
Should NOT fire: "прогони голдены" (that is run-goldens) · "this golden is
flaky, rerun it" (running/diagnosing, not authoring).
1---2name: new-golden3description: Use when deciding what to cover with a unit test vs a golden test, when writing a NEW golden test or fixture, or after a bug escaped to production/a real repo and you are asking why the tests missed it. Fires on: "write a golden test", "нужен golden на это", "add a fixture", "what should this test assert", "почему тесты это не поймали", "unit or golden?". Produces the split, a fixture that can actually fail, and structure-not-text assertions. To *run* existing goldens, use `run-goldens` instead.4---56# new-golden — decide the split, then build a fixture that can fail78Two different tools, constantly confused:910- **Unit test** — pure logic, no network, no DB, no model. Fast, runs in CI on11 every PR, asserts exact behaviour.12- **Golden test** — an **LLM surface** (routing, extraction, synthesis, an13 enrichment pass) against a **real model**. Opt-in, excluded from CI, slow,14 asserts **structure, not text**.1516## The split1718Put in a **unit test** everything that does not need a model: parsing, tree19building, prompt *construction* (not its answer), digests and cache keys,20triviality gates, ordering, rendering, pruning. Most of a pipeline is this, and21it should be — pure functions are where cheap certainty lives.2223Put in a **golden test** only what a model's behaviour decides: does retrieval24rank the right thing, does the pass produce a well-formed artifact, does the25router pick the right agent, does the JSON parse.2627If you find yourself wanting a golden to check something deterministic, extract28the deterministic part into a pure function and unit-test *that*. A golden is29the most expensive test you own — spend it only where a real model is the thing30under test.3132## The fixture is the test3334**A fixture that only contains shapes the code already handles proves nothing.**35This is the single most common way a suite passes while the code is broken: the36sample repo/document/payload is small, flat and friendly, and every hard case37lives only in production data.3839Real example: a documentation pipeline had a small flat fixture repo. Three40defects shipped anyway and only surfaced on a 148-class codebase — an aggregate41step ran on the wrong size budget (the fixture's inputs were never big enough to42overflow), a model swap silently skipped regeneration (the fixture was never43re-run under a second model), and a deep nesting chain cost N redundant calls44(the fixture was two levels deep).4546So when you build one, deliberately include:4748- **Depth and degenerate shapes** — a long single-child chain, an empty49 container, a leaf at the top level, a cycle if the domain allows one.50- **Something big enough to hit a limit** — a batch, window, timeout or page51 size must be reachable, or the budget is untested by construction.52- **A second run under changed conditions** — different model/config/version —53 to prove incremental keys invalidate instead of silently skipping.54- **The boring-but-wrong case** — the input that *looks* processable and is not.5556Ask out loud: **"which shape, if it appeared in real data, would break this57code?"** Then put that shape in the fixture.5859## Assertions: structure, not text6061A model's wording changes between runs and versions; asserting it makes the test62flaky and worthless. Assert instead:6364- the right **item** was selected/ranked/produced (identity, not phrasing);65- the artifact's **shape** — required fields present, parses, non-empty, within66 bounds;67- **relationships** — the child resolved, the reference points somewhere real;68- **counts and tiers** — N produced, the trivial ones skipped;69- **idempotence** — a rerun with nothing changed does no work.7071Never assert a full generated sentence. If you need semantic quality, assert a72required *token* the answer cannot be right without (a domain identifier), and73accept anything around it.7475## Procedure76771. **Classify** what you are testing: deterministic → unit; model-decided →78 golden. Split the thing in two if it is both.792. **Extend the existing fixture** rather than inventing a parallel one, unless80 the new shape would distort the old assertions — then add a second, named for81 the shape it carries (`…-deep-chain`, `…-oversized`).823. **Write the failure first**: state the defect this test would have caught. If83 you cannot name one, you are writing a test that can only pass.844. **Gate it** the way the repo gates goldens (marker/env flag), so it stays out85 of CI and does not need a model on a laptop.865. **Run it against a real model** before shipping. Do not commit a golden you87 have not seen pass — an unverified golden is worse than none, because it88 looks like coverage. If no engine is available right now, say so explicitly89 and record it as pending rather than merging it green-by-assumption.906. **Close the loop back to the spec.** A golden usually exists to assert some91 acceptance criterion (a `Scenario:` / WHEN-THEN / requirement written before92 the code). Once the test exists and passes, **name it in that criterion** —93 write the test's class into the scenario line, e.g. `THEN … (asserted by94 MyFlowTest)`. This makes the spec→test link traceable both ways and is the95 hook a drift-lint can check (a criterion naming a test that no longer exists96 is a caught rename). A criterion that can never name a test was never a97 criterion — delete it or make it testable. Skip only when the repo keeps no98 written spec for the change.997. **Record the cost**: goldens are slow. Note roughly how long the lane takes,100 so the next person knows what they are opting into.101102## Triggering contract103104Should fire: "нужен golden на новый пайплайн" · "unit or golden for this?" ·105"почему фикстура это не поймала".106Should NOT fire: "прогони голдены" (that is `run-goldens`) · "this golden is107flaky, rerun it" (running/diagnosing, not authoring).