Skip to main content

author-bdd-steps

Write a new BDD step definition, or bind a new scenario line onto existing steps, correctly the first time: verify against the pinned AdCP spec, confirm the scenario is wireable through the cross-transport harness, use the guarded dispatch/assertion helpers (never hand-rolled wire parsing), and prove the scenario actually executes. Use before writing any step body under tests/bdd/steps/, or before letting a new scenario line bind to an existing step.

معلومات المصدر

المستودع
prebid/salesagent
آخر نشاط في المصدر
١٧ سبتمبر ٢٠٢٦ في ٢٢:٠٥
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٣٨
التفرعات
٥٤

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
author-bdd-steps
description
Write a new BDD step definition, or bind a new scenario line onto existing steps, correctly the first time: verify against the pinned AdCP spec, confirm the scenario is wireable through the cross-transport harness, use the guarded dispatch/assertion helpers (never hand-rolled wire parsing), and prove the scenario actually executes. Use before writing any step body under tests/bdd/steps/, or before letting a new scenario line bind to an existing step.
args
<feature-file-or-scenario-text>
# Authoring BDD Steps Normative rules live in `tests/CLAUDE.md` §"BDD authoring discipline (the five rules)" and §"Error Verification Policy" — read those first; this skill is the wiring guide for applying them while writing a step, not a restatement. `tests/bdd/CLAUDE.md` has the quick-reference helper table. **Do not follow the generic `qa-bdd:step-development` plugin skill for this project.** Its When-step template is `except Exception as exc: ctx["error"] = exc` — the exact hand-stash antipattern rule 2 forbids. It predates this repo's harness and doesn't know `dispatch_request` or the wire-envelope helpers exist. ## 1. Authority gate Locate the scenario in its generated `tests/bdd/features/BR-UC-*.feature` — that file (mirroring the pinned schema + storyboard) is the authority, not the step body and not a `docs/test-obligations/` bootstrap artifact. Spec-cite before writing code: ```bash uv run python -c "import adcp; print(adcp.get_adcp_spec_version())" # confirm the pin, docs/adcp-spec-version.md git -C ~/projects/adcp show v3.1.1:dist/schemas/3.1.1/core/<schema>.json ``` If the scenario's wording diverges from what the schema actually requires, fix the scenario (generated features can be edited locally — mirror the diff upstream) rather than coding to a wrong scenario. Cite the divergence with the spec version + path. ## 2. Wireability gate (HARD STOP) Before writing a `When`, confirm the domain env this scenario needs can reach the wire: 1. Which env class serves this scenario? (`tests/CLAUDE.md`'s environment table, or `tests/harness/*.py`.) 2. Does `dispatch_request` (`tests/bdd/steps/generic/_dispatch.py`) already route this tool/domain through `call_via`? If yes, use it — it's the ONE writer of `ctx["result"]` / `ctx["wire_response"]` / `ctx["wire_error_envelope"]`. 3. If the env or route doesn't exist yet: **wire it first** (add the env method, or extend `_dispatch.py`'s routing), or **single-transport-tag + xfail-route** the scenario with an explicit wiring-gap reason. Do this instead of reaching for a shortcut. **Never add a new `# TRANSPORT-BYPASS`.** Grep the two existing ones before you're tempted to add a third: ```bash grep -rn "TRANSPORT-BYPASS" tests/bdd/ ``` Each is a Given/When that skips wire dispatch and calls `_impl` directly — which means the scenario reports green across a2a/mcp/rest/e2e_rest while actually running the same in-process code four times. It is also why the "correct" wire-assertion helper is the one that fails on these scenarios (`assert_wire_error` raises loudly when no `wire_error_envelope` was captured) — reconstructed `ctx["error"]` becomes the only path that "works". That is a bug signal, not a reason to reach for the reconstructed path: wire the env instead. ## 3. Binding audit pytest-bdd binds a scenario line to a step by **text**, not by review. Before writing a new step function, check whether the scenario's wording already matches an existing one: ```bash grep -rn "the response is an error variant" tests/bdd/steps/ ``` If it does, **open that step's body and grade it against rule 3** (wire vs. reconstructed). A new scenario silently inheriting an old, pre-policy step is how weak assertions spread — the policy's boy-scout migration rule (`tests/CLAUDE.md` §"Migration path") is mandatory when a new scenario rides an old step, not optional. Upgrading the step is part of *this* change. ## 4. Authoring table | Step | Use | Never | |---|---|---| | Given | env-owned setup method; `realize_e2e` (`tests/harness/_realize.py`) for the e2e branch | hand-stashing wire data into `ctx` | | When | `dispatch_request(ctx, **kwargs)` (`tests/bdd/steps/generic/_dispatch.py`) | a local `_get_error*`/`call_impl` shortcut, a new `TRANSPORT-BYPASS` | | Then (error) | `ctx["result"].assert_wire_error(code, recovery=..., message_substr=..., field=...)` (`tests/harness/transport.py`) | `ctx["error"]`, `getattr(error, "error_code")` on a reconstructed exception, hand-rolled envelope `.get()` chains — even if the envelope came from `ctx["wire_error_envelope"]` first, always go through `assert_wire_error`/`assert_envelope_shape`, never parse it inline | | Then (success) | `wire_field(ctx, "x")` / `wire_dict(ctx)` / `wire_lookup(ctx, path)` (`tests/bdd/steps/_outcome_helpers.py`) | `result.payload.model_dump()` round-trips — proves serializer self-consistency, not what the buyer received | `recovery` on `assert_wire_error`/`assert_envelope_shape` is a required keyword — it pins the buyer-facing retry semantics, not a soft default. ## 5. Dormancy proof Run the touched slice serial with `-rxX` and read the output: ```bash uv run pytest tests/bdd/ -k "<scenario or file>" -rxX ``` Sub-second wall time, or a `StepDefinitionNotFoundError` / "No harness wired" reason, means the scenario never ran — it auto-xfailed at fixture setup. A green run that took no time is not a pass. ### Three ways to be ungraded, not one Auto-xfail at fixture setup is only the first. All three produce the same outcome — the scenario does not grade the behavior on that transport — and the aggregate count looks identical for all three: 1. **Auto-xfail at fixture setup.** Covered above. 2. **Excluded from a transport's parametrization.** A tag set consulted in `pytest_generate_tests` (`tests/bdd/conftest.py`) can drop a transport from the parametrize list *before collection*. The scenario is then not xfailed, not ledgered, and not in any allowlist — it simply has no `[e2e_rest]` test id, and neither escape-hatch detector in `test_architecture_e2e_rest_escape_hatches.py` can see it (detector 1 walks `pytest_collection_modifyitems` xfail conditions; detector 2 walks `tests/harness/` `E2EUnsupportedSetup` sites). **Exclusion from parametrization is exactly as ungraded as an xfail.** Both are banned. Real case: `_NO_E2E_REST_TAGS` carried a comment correctly noting that xfailing the scenario on `e2e_rest` would be a false-green, while the exclusion it implemented produced the identical outcome by another mechanism (GH #1892). The only accepted fixes are to re-express the assertion on a transport-observable signal, or — when the sub-claim genuinely has no wire surface — declare it via `E2EUnsupportedSetup` so it lands in `EXPECTED_UNSUPPORTED_DECLARATIONS` as a reviewed, pinned declaration instead of an invisible one. Check it directly rather than trusting the tag set: ```bash BDD_E2E_ENABLED=true uv run pytest <module> --collect-only -q -o addopts= -n0 \ | grep "<test_name>" # every sibling's transports should appear here too ``` 3. **Xfailed against the wrong issue.** A dormancy or xfail reason that cites a plausible-but-unrelated issue is worse than an uncited one: it reads as tracked work and nobody re-checks it. Real case: a `webhook_signing` / `request_signing` dormancy cited #1855, whose actual scope is `media_buy` presence-object sections and never mentions signing; the real tracking issue was #1291. **Before citing an issue number in a dormancy or xfail message, open it and confirm its body actually covers the dormant behavior:** ```bash gh issue view <n> --repo prebid/salesagent ``` Never cite from memory, and never pattern-match on a nearby issue that sounds right. Cite a GitHub issue, never a local beads id — beads ids do not resolve for outside contributors. ## 6. Antipattern ↔ guard map Each antipattern below names the guard that's supposed to catch it. If your step trips none of them but still does the thing on the left, the guard is silent for this spelling — say so and escalate (extend the guard), don't treat the pass as clean. | Antipattern | Guard | |---|---| | No-op / pass-only Then | `test_architecture_bdd_no_pass_steps.py` | | Truthiness-only assertion (`assert result`, not `assert x == y`) | `test_architecture_bdd_no_trivial_assertions.py` | | Raw dict used as a step registry instead of a factory | `test_architecture_bdd_no_dict_registry.py` | | 3+ steps with identical bodies | `test_architecture_bdd_no_duplicate_steps.py` | | `ctx.get("env")` / `hasattr(env, ...)` silent-env checks | `test_architecture_bdd_no_silent_env.py` | | `_get_error_code`/`_get_error_dict` called with no wire reference | `test_architecture_bdd_wire_discipline.py` (Check B — currently name-matched, not pattern-matched; a step with its own local `_get_error`-style helper is not yet caught) | | A `TRANSPORT-BYPASS`/allowlisted direct-`_impl` call | `test_architecture_bdd_no_direct_call_impl.py` | | Scenario dropped from a transport's parametrize list in `pytest_generate_tests` | **no guard yet** — neither escape-hatch detector sees a parametrize-time exclusion (GH #1892). Prove it by collection, §5.2 | | Dormancy/xfail reason citing an issue that doesn't cover the behavior | **no guard** — `gh issue view` it, §5.3 | | Dormancy/xfail reason citing a local beads id instead of a GitHub issue | `.pre-commit-hooks/check_fixme_citation_count.py` (ratcheting; only matches the `FIXME(...)`/`TODO(...)` spelling — a bare id used as an allowlist tuple value is not yet caught) | For the deeper semantic question — "does this Then actually verify what its text claims?" — run `/inspect-bdd-steps` after writing. ## See Also - `tests/bdd/CLAUDE.md` — quick-reference pointer + helper table - `.claude/rules/workflows/xpass-graduation.md` — per-scenario graduation once a scenario legitimately passes - `/inspect-bdd-steps` — post-hoc semantic assertion audit - `/derive-tests` — the sibling skill for the *obligation* harness (different helper vocabulary — `assert_envelope`, not `assert_wire_error`; don't mix the two)
عرض على GitHub