Writing rules that keep stories honest
- Plain words, exact checks. "Keep exact mutation-resistant
assertions in the TypeScript step definition. A plain-language
Then the payment is refunded once can assert the exact provider
call count, stored note, terminal result, and lack of a duplicate."
- Drive through the real rendered form. A step that submits an
admin edit reads the production HTML, parses its fields and CSRF
token, and posts exactly what a browser would. Building the request
from database rows would skip the very layer the story exists to
prove.
- Validate tables at the boundary. Every DataTable cell starts as
a string. Parse it with a typed schema. "Never cast and hope."
- Business setup in Given steps, not hooks. Hooks hold only
technical fixture work a reader does not need.
- No ambiguity, no retries. Undefined, ambiguous, pending and
skipped steps fail. Unused step definitions fail the full run. "Do
not add retries; a pass after retry is still a flaky failure."
- No speculative vocabulary. A new shared step must be used by the
current story or one immediately included with it.
- Use the site's words. A story calls a thing what its label calls
it, copied from the message catalog.
Strict tags, checked
deno task specs:check parses every Feature and validates the tags
against a registry: exactly one globally unique @story: per
Feature, one @rule: per Rule, one @case: per Scenario (or a
case_id column for outlines), and known owner, risk, actor and
edition values. Ad-hoc tags are forbidden. Those ids are durable: the
repository's screenshot evidence is keyed by them, and they never move
when a scenario is renamed.
Migrating an old test without losing claims
The most valuable section of E2E_TESTS.md is a checklist that exists
because the same mistake kept happening:
A migration is a replacement, and the risk is always the same:
the story reads better than the test it replaced while quietly
proving less.
The rule is to build the list of claims from the old file, as it
stood on the merge base, never from the new story:
git fetch origin main
base=$(git merge-base origin/main HEAD)
git diff --name-only --no-renames --diff-filter=a "$base" HEAD
file=test/features/admin/settings-email.test.ts
git diff "$base" HEAD -- "$file"
git show "$base:$file"
git diff "$base" HEAD -- "$file" | grep '^-' | grep -E 'expect|assert'
Each claim then lands in exactly one place: in the story, in a direct
test, or deliberately dropped and said out loud. The document lists
claims that were lost in real migrations, including "the double-entry
conservation check after each money action" and "that a race was
settled by capacity rather than by one request simply failing". One
batch lost three more claims after the checklist existed, because
the author built the list from the new files.
That last detail is typical of working with agents. A finished story
reads as if it covers everything. Only the old file can say what is
missing, so the procedure forces the agent to read it.