Skip to content
Back to skills

Test Case Contract

ASecurity

Spec format contract for specs/**/*.md: the YAML schema, the closed 16-key oracle vocabulary and argument shapes, fault:/clock: situation steps. Load before writing, compiling or reviewing a spec.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 19, 2026
testingrustgoexpressgitapifrontendbackend

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 19, 2026

npx -y skills add its-magdy/qa-warden --skill test-case-contract --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Test Case Contract?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Test Case Contract
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/its-magdy-test-case-contract/badge)](https://www.skillsdirectory.com/skills/its-magdy-test-case-contract)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: test-case-contract
description: "Spec format contract for specs/**/*.md: the YAML schema, the closed 16-key oracle vocabulary and argument shapes, fault:/clock: situation steps. Load before writing, compiling or reviewing a spec."
user-invocable: false
disable-model-invocation: false
---

# Test case format contract

The canonical format of a QA spec (`specs/<area>/<feature>.md`). Preloaded into the planner,
generator, verifier, reviewer and ideation agents; `/qa-warden:import-cases` invokes it. It used
to be a section of the project's `CLAUDE.md`. If that file still carries a
`## Test case format contract` section, it is a stale copy: this skill wins where they differ.

Specs are Markdown prose plus one or more fenced YAML blocks. The YAML block schema:

```yaml
name: <short scenario name>
tags: [smoke, <area>]               # viewport is a TAG, never an oracle key — `mobile` in `tags:` emits `@mobile`; the opt-in `mobile` config project routes on it (desktop-only until a human uncomments it)
site: app                           # MUST match a sites[].id in specs/_context/app.context.md. Cross-site: `sites: [admin, app]`.
basis: checkout/coupon              # OPTIONAL — set on fanned-out specs; pairs the spec back to <feature>.cases.md/.basis.md
compliance_relevant: false          # OPTIONAL — true for PCI/HIPAA/SOX-flagged flows (enables the reviewer's cross-vendor note)
data:
  user:    { email: "...", password_env: "QA_USER_PASSWORD" }   # CREDENTIAL form: a pre-seeded login account. Exactly {email|username, password_env} — carved out of reviewer Check 10, no factory needed.
  order:   { factory: "order", overrides: { units: 1 } }        # FACTORY form: REQUIRED for any entity you INSTANTIATE with non-credential fields. Inline literals (`order: { units: 1, currency: "USD" }`) where fixtures/factories/order.ts exists = reviewer Check 10 FAIL. `overrides:` pins only what the oracle asserts; the factory generates the rest, and the generator creates the schema+factory on demand.
  expected_total_range: [49.00, 50.50]
steps:                       # prose lines, plus THREE structured forms: `include:`, `fault:`, `clock:`
  - include: login
  - "Navigate to {{base_url}}/cart"
  - "Apply coupon QA20"
  # `fault:` (stub a network dependency) and `clock:` (control time) are SITUATION steps —
  # the Interfaces/Operations and Time lens forms. Shapes + the fired-proof rule that keeps a
  # silently-non-firing stub from passing green are in §"Situation steps" below.
oracle:                      # closed vocabulary only
  text_visible: "Coupon applied"
  # locator is SEMANTIC only (getByTestId/getByRole/getByLabel) — never raw CSS
  value_between: { locator: "getByTestId('order-total')", range: [39.00, 40.50] }
  no_order_created: false
must_fail_when:                # OPTIONAL to declare — but once declared it IS enforced:
  - confirmation text removed   # reviewer Check 2b (reified as oracle) + verifier step 8b fault injection (proves the oracle goes RED on the defect)
  - order total mutated outside range
fail_if:                       # OPTIONAL — negative/error-path extra red-flags (scenario-level)
  - "the order confirmation page appears"
prompt_guardrail: |            # OPTIONAL — stops an agent "healing" an expected failure away
  This test expects an ERROR. Do not declare passed=true unless the error surface was observed.
invariant_holds_when:          # OPTIONAL — precondition domain of an equality oracle (reviewer Check 2e).
                               # LIST of {invariant, holds_when} pairs, mirroring the basis
                               # integrity_invariants[]. Never a bare scalar: a spec may assert
                               # more than one equality, and a scalar cannot say which one it scopes.
  - invariant: "order-total == cart-total"
    holds_when: "single-unit order — data.order.units == 1"
output_schema:
  required: [passed, evidence, failed_step]
  evidence: { order_id: string, observed_total: number, screenshot_path: string }
scenarios: [{ name: "...", data: {...}, tags: [smoke] }]   # OPTIONAL — N data-driven variants; each entry MAY carry its own `oracle:` (same vocab + shapes), `negative: true` for error-path scenarios, and its own `tags:` (per-scenario tags — emitted by the generator as `@<tag>` on THAT scenario's test only; this is how a multi-scenario spec tags exactly one governed happy-path `@smoke` while siblings stay `@regression`)
mr: ["<invariant, e.g. 'reordering cart items does not change the total'>"]   # OPTIONAL opt-out — self-declared metamorphic relations; PRESENCE makes the verifier skip auto-twins (step 8a) and reviewer Check 6 trust these instead
```

**Smoke-lane rule.** `@smoke` is the fast nightly gate (`/qa-warden:run mode=smoke` runs ONLY `--grep @smoke`). The **primary / governed happy-path of a P1 / critical area MUST be tagged `smoke`** — a P1 flow (checkout/payment/login) tagged `regression`-only leaves the smoke gate blind to a fully-broken feature. Edge/negative cases and metamorphic twins stay `regression`. **In a multi-scenario spec, put that `smoke` tag on the ONE governed happy-path via its per-scenario `tags: [smoke]` (see the `scenarios:` field above) — NOT on the spec-level `tags:`, which would tag every scenario's test and make `--grep @smoke` select the whole file instead of just the happy path.**

Closed **oracle vocabulary** (THE canonical list — the single source of truth every agent/command mirrors; planner, generator, reviewer Check 4, and `scripts/oracle-keys.txt` must all match this exact set; `/qa-warden:coverage` builds its `KEYS_RE` from that manifest at runtime, so it is not a mirror) — assertions must be one of these **16**: `text_visible`, `url_matches`, `count_equals`, `value_between`, `error_shown`, `no_order_created`, `attribute_equals`, `element_state`, `network_response_status`, `response_body_contains`, `download_received`, `clipboard_contains`, `storage_state`, `upload_accepted`, `dialog_dismissed`, `a11y_violations_below`. No free-form `"should look right"`. No free-form assertions. Negative/error-path specs add `fail_if:` and a `prompt_guardrail:` stanza so the agent does not "heal" the failure away. **Adding/removing a key means editing all sites above in the same change — a divergent list means the reviewer FAILs specs its own planner authors.**

**Oracle argument shapes (the SoT for reviewer Check 4's arg-shape sub-check).** The reviewer cannot read the plugin's `agents/generator.md`, so the canonical per-key argument shapes are pinned here, in the skill it preloads. A right key with a wrong-named arg silently mis-compiles — the reviewer FAILs a divergent shape:

| key | argument shape |
|---|---|
| `text_visible` | `"<string>"` |
| `url_matches` | `"<regex-or-literal>"` |
| `count_equals` | `{ locator, n }`  (NOT `{ count }`) |
| `value_between` | `{ locator, range: [min, max] }` |
| `error_shown` | `"<string>"` (exact) |
| `no_order_created` | `<bool>` — generic "no record persisted" guard: the `order` in the name is historical; use it for ANY entity a failed flow must not create (order / ticket / signup / booking / task). `false` = the record MUST exist; `true` = MUST NOT. |
| `attribute_equals` | `{ locator, attr, value }` |
| `element_state` | `{ locator, state }` |
| `network_response_status` | `{ url, status }` |
| `response_body_contains` | `{ url, text }` — asserts the response BODY of the matched request contains the exact `text` (JSON error body, API message). Complements `network_response_status` (which asserts only the STATUS code): for API-level authz/error specs where the exact `{"error":"Not authorized"}` string is the oracle, not just the 403. Generator compiles it against an `APIRequestContext`/`waitForResponse` body read + `expect(body).toContain(text)`. |
| `download_received` | `{ filename?, min_bytes? }` — a file download fired (`page.waitForEvent('download')`); optional `filename` regex + `min_bytes` size floor. Both predicates are optional, but each is one paired-expect that together cover the single oracle item (Check 1). Must match `planner.md` + `generator.md` (`min_bytes`, NOT `size`). |
| `clipboard_contains` | `"<string>"` |
| `storage_state` | `{ cookies?: [...], localStorage?: { key: value } }`  (camelCase `localStorage`, per Playwright `storageState()`). **Presence sentinel:** for a server-generated value you cannot predict (a JWT, a session id), assert the value literal `"<non-null>"` — e.g. `localStorage: { tm_token: "<non-null>" }`. The generator compiles `"<non-null>"` to `.toBeTruthy()` (and its negation `"<null>"` to `.toBeFalsy()` — not `.toBeNull()`, so an absent key resolving to `undefined` via `storageState()` still passes the "token absent" assertion instead of false-redding); the reviewer treats it as a canonical shape, not a divergence. This is the ONLY way the closed vocab expresses "key present, any value." |
| `upload_accepted` | `{ selector_role, name }` |
| `dialog_dismissed` | `{ type: 'confirm'\|'alert'\|'prompt', accept? }` |
| `a11y_violations_below` | `{ max_critical, max_serious }` |

**Two assertions with the same key → write `oracle:` as a LIST of single-key maps, never a bare mapping.** A YAML mapping silently collapses duplicate keys, so `oracle: { element_state: {…aria-pressed=true}, element_state: {…aria-pressed=false} }` keeps only the last — a real assertion is dropped and the spec reads as covered (a false-pass, the exact failure this vocab guards against). When a scenario needs the same key twice (two `element_state`, two `text_visible`, …), emit `oracle:` as a sequence of one-key maps: `oracle:\n  - element_state: {…=true}\n  - element_state: {…=false}`. The reviewer FAILs a bare-mapping `oracle:` that carries a duplicate key.

## Situation steps — `fault:` (network) and `clock:` (time)

Two `steps:` entries are STRUCTURED maps rather than prose, alongside `include:`. They set up the
*situation* the oracle is then asserted against — they are **never** assertions, and neither adds an
oracle key (the fired-proof rule below is why none is needed). They exist so the SFDIPOT
**Interfaces/Operations** and **Time** lenses are generatable at all: without them the planner has no
way to author *"the payments API returns 503"* or *"the session expires after 30 minutes"* — cases
`/qa-warden:ideate` routinely enumerates.

Argument shapes (verified against the pinned Playwright 1.63.0 type declarations). **The key column
deliberately carries the trailing colon** (`` `fault:` ``, not `` `clock` ``) — these are `steps:`
keys, not oracle keys, and the colon is also what keeps them out of the plugin's `bin/qa-selfcheck` Check 9bb
oracle-arg-shape extractor, which matches a bare `` `key` `` in column 1 and would otherwise count
them as a 17th and 18th member of the closed vocabulary.

| step key | argument shape |
|---|---|
| `fault:` | `{ url, status?, body?, json?, abort?, times? }` — `url` is a glob (`**/api/pay/**`) or `/regex/` matched against the request URL. Supply EITHER a fabricated response (`status` and/or `body`/`json`) OR `abort: "<errorCode>"` (a transport failure: `connectionfailed`, `connectionreset`, `internetdisconnected`, `namenotresolved`, `timedout`, `failed`, …) — never both. `times: n` retires the stub after `n` matching requests, which is how a *retry* case is expressed (fail once, let the real server answer the retry). |
| `clock:` | `{ install_at?, advance?, advance_idle?, pause_at?, fixed_time?, system_time? }` — **exactly one per step**, so a scenario that installs then advances writes two `clock:` steps. `install_at:` starts fake time at an ISO instant and **MUST precede the first navigation** (Playwright's own guidance: install before `goto`, at a time slightly before the intended test time, or the page can hang during load). `advance:` fires every timer it passes through; `advance_idle:` jumps and fires each due timer *at most once* (the "closed the laptop lid" case). Durations are milliseconds or `"SS"` / `"MM:SS"` / `"HH:MM:SS"`. |

**The fired-proof rule — a stub that does not fire is the false-green this feature risks.** A
`fault:` whose `url` matches nothing is *silent*: the real server answers, the app behaves normally,
and every oracle in the scenario passes for the wrong reason. So a `fault:` that fabricates a
**response** MUST be paired with a `network_response_status: { url, status }` oracle item naming the
same URL and the injected status. That key is already one of the 16 — **no new vocabulary** — and it
is discriminating by construction: if the stub misses, the real response arrives with a different
status and the test goes **RED**, not green. An `abort:` fault produces no response at all and cannot
be proven that way, so the generator compiles a handler hit-count plus a barrier `expect(...)`
(the R-26 carve-out) instead. Reviewer **Check 2** treats a `fault:` step carrying neither
fired-proof as an **orphan step** and FAILs it.

**A scenario with a `fault:` step may not be tagged `smoke`.** The smoke lane is the nightly
"does this feature actually work" gate; a scenario whose dependency is fabricated cannot answer that,
and a green smoke run standing on a stub is reassurance without evidence. Author fault scenarios as
`regression`. `clock:` carries no such restriction — fake time still exercises the real stack.

**HAR replay is BANNED.** `page.routeFromHAR(...)` / `browserContext.routeFromHAR(...)` anywhere under
`tests/**` or `page-objects/**` is a reviewer **Check 11** FAIL and a write-time hook deny. Recording
is not the problem, replay is: a suite served from a recorded HAR asserts the frontend against a
frozen backend, so it stays green through every server-side regression — the exact inversion of what
the nightly replay promises. (A recorded HAR is also a credential-bearing artifact: `recordHar`
captures `Authorization` and `Cookie` request headers, and `mode: 'minimal'` omits the HAR's own
cookie fields but is not a redaction guarantee. Nothing in this toolkit writes one.)

## Authoring notes and honest limits

Explanatory material behind the rules above. It comes last on purpose: after a context
compaction only the first ~5,000 tokens of a skill are re-attached, so the rules lead.

**Natively-validated inputs — author the native-block oracle, not a server-error-string that can't fire (F-17).** For an invalid-input partition on a field with HTML5 native constraints (`<input type="email">`, `required`, `pattern`, `min`/`max`/`maxlength`), a non-conforming payload (`not-an-email`, an empty required field, an over-max value) is blocked by the browser **before submit** — no request fires, no app error alert renders, the page does not navigate. A provisional `error_shown: "Invalid …"` oracle for that partition is therefore **structurally unsatisfiable** (the app never emits that string → it ships as a mystery red). Author EITHER **(a)** the *native-block* oracle — `url_matches` still on the form route + `storage_state` token `"<null>"` + no error surface, and name the scenario for what it proves (`…-native-block`, not `…-server-reject`) — OR **(b)** a payload that actually **reaches the server** (a syntactically-valid-but-unknown email) so a server `error_shown` oracle can fire. The real observable for such a field is the browser's `ValidityState`/no-navigation, not an app message. (Planner authors this; the verifier's step-8b catches a mis-authored one live and leaves it honestly red.)

**Honest limit — the relational / exact-equality gap (no `value_equals`, no cross-value oracle).** The closed vocab has **no** key for "value A is byte-identical to value B" or "this value **equals** a computed/known value exactly." `value_between` bounds a single number against author-supplied constants; it cannot express "the created order total equals the cart's computed total" (an *integrity* invariant) or "the refusal message on branch A is identical to branch B" (an *anti-enumeration relational* invariant). This is a **known, deliberate** limit (keeping the vocab closed and mechanically checkable is worth more than a relational key the reviewer couldn't validate structurally). Sanctioned ways to express it **today**, in order of preference:
- **Exact-value-against-a-known-value:** when the expected value is seeded/known, use a `value_between` with a **tight** range around it (e.g. `[80.99, 81.01]` for an $81.00 total) that **excludes the wrong value** (e.g. the undiscounted $89.99). A wide band that also admits the wrong value is a false-pass — reviewer Check 2c WARNs on it.
- **Relational (A == B):** assert the **same literal** on both branches (e.g. one shared `REFUSAL_MESSAGE` constant asserted via `toHaveText`, not `toContainText`, in both scenarios) **and** declare a `must_fail_when:` ("branch A and B messages diverge" / "the total is not carried") so the verifier's step-8b injection and reviewer Check 2b/2c force it to be discriminating. Note the residual hole: same-literal-both-branches cannot catch the case where *both* branches drift to the same *new* leaky string — record that under the spec's `# Open questions` / `# waived:`.
- **Scope the invariant to its precondition domain (`invariant_holds_when:`).** An equality/relational invariant almost never holds *universally* — "order-total == cart-total" is true only for a **single-unit** order; across multiple units the surfaces legitimately differ. Declare the domain the equality holds over in the spec's optional `invariant_holds_when:` block (mirrored from the basis `integrity_invariants[].holds_when`), and pin the `data:` **inside** that domain. An **unscoped** equality is the over-broad `require true` default that Design-by-Contract (Meyer) and metamorphic-relation input-domains warn against: green on the pinned row, **false as a stated invariant**, and no other check catches it (Check 2c only sees a wide band, 2d only the literal). Reviewer **Check 2e (WARN)** flags a tight-band (`value_between range:[x,x]`) or same-literal-both-branches equality oracle that declares **no** `invariant_holds_when:` domain, or whose pinned `data:` visibly contradicts the declared one. WARN, not FAIL — the domain predicate is free-text and not always mechanically evaluable; it is a completeness reminder, not a gate.
- **Never** invent an out-of-vocab key for this — reviewer Check 4 FAILs it. If a project hits this often enough to justify a dedicated relational key (`value_equals: { locator, expected }` / `value_matches_api: { locator, source }`) — this would be a *new* key beyond the current 16 — that is a real vocab change: add it to **every enumerating** mirror in the same commit — this skill's list + arg-shape table, `planner.md`, `generator.md`, reviewer Check 4, `scripts/oracle-keys.txt`, and `/qa-warden:coverage`'s `KEYS_RE` grep — so the byte-for-byte consistency the reviewer depends on is preserved. (The `response_body_contains` key above was added by exactly this process to close the API-body-assertion gap; the relational `value_equals` gap remains deliberately unfilled.) (`/qa-warden:new-spec` and `/qa-warden:review url=<url>` **defer** to this skill and hold no key list — nothing to edit there.)

**Honest limit — no request-side oracle.** The closed vocab can assert what the server *answered*
(`network_response_status`, `response_body_contains`) but not what the client *sent*: there is no key
for "the checkout POST carried `coupon=QA20`" or "the request set an `Idempotency-Key` header".
`network_response_status` covers the *did it call that endpoint at all* half — a status assertion on
`**/api/orders` cannot pass unless a request to it actually fired — but the payload is out of reach.
This is the same deliberate gap as the relational `value_equals` one above, and the same rule
applies: **do not invent a key for it**; record it under `# Open questions` / `# waived:`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.