Prepare a brief the model can actually use
Start with one feature or business decision. A whole backlog usually contains different actors, conflicting assumptions, and requirements at different stages of approval. For a reset feature, include the request flow, link lifetime, replacement behavior, request limits, and session policy. Attach the relevant API contract or screen copy when it constrains the outcome.
Give each statement an ID and record its document version. Keep the source sentence alongside any rewritten rule so reviewers can see whether the rewrite changed its meaning. Add the actor, relevant account state, existing constraints, and exclusions. Use synthetic emails and accounts when you share an example with an AI tool; production tokens and customer records are unnecessary for this task.
Start a chat in an AI assistant your team permits. Paste the brief with this plain-text cover sheet, replacing the brackets with your own details. You can use a document or a readable table throughout this workflow; JSON is useful only if another tool needs that format.
Feature: [the action or decision this ticket covers]
Actor and starting state: [who acts, on what, in which state]
Source and version: [ticket/document name and revision]
Source priority: [which approved source wins if statements conflict]
Approved requirements: [original wording, with reference IDs]
Approved decisions: [answers already agreed by the responsible owner]
Open questions: [unknowns, owner role, and the behavior they affect]
Observable results: [screen, API, stored result, email, or report]
Out of scope: [behaviors belonging to another ticket]
A useful input packet answers these questions:
- Which document is authoritative, and who can resolve conflicting statements?
- Which behaviors have approval, and which are proposals or questions?
- What outputs can a tester observe through the interface, API, email sink, or another external system?
- Which behaviors are outside this feature, such as changing an MFA method?
The downloadable source packet begins with a deliberately rough, synthetic brief: customers recover accounts by email; links expire after 30 minutes and work once; replacements stop old links; responses hide account existence; five requests in 15 minutes trigger a 15-minute cooldown; successful resets clear existing sessions. It also asks for fast delivery and a secure password policy without defining either. Those last phrases cannot become measurable criteria until someone supplies the missing decisions.
Separate rules, decisions, and open questions
Use three kinds of source IDs. A REQ identifies a requirement. A DEC records an approved clarification. An OPEN records an unresolved question and the person or role responsible for answering it. Do not let the model quietly turn a plausible answer into a rule.
| Requirement | Approved behavior in the example | Clarification needed to test it |
|---|---|---|
| REQ-01 | Links expire 30 minutes after issue. | Whether a link is accepted exactly at expiry; how the test controls time. |
| REQ-02 | A link can be used once. | How rejection is observed on a second submission; simultaneous submissions need an implementation check. |
| REQ-03 | A newly issued link invalidates older unused links for that account. | Whether a blocked request issues a replacement or affects existing links. |
| REQ-04 | The request response does not reveal account existence. | Which response fields are compared and how timing differences will be assessed. |
| REQ-05 | Five requests in 15 minutes trigger a 15-minute cooldown. | Whether the fifth request sends email, when the window starts, and whether blocked requests extend cooldown. |
| REQ-06 | A successful reset revokes existing sessions. | Which sessions are included and how denied access is observed afterward. |
The fictional exercise makes the following decisions explicit. They are teaching choices, not default policies for every reset service. In your project, record the responsible owner's answer before using it as expected behavior.
| Decision | Behavior used by this example |
|---|---|
| DEC-01 | An unused link is valid before 1,800 seconds and expired exactly at that deadline. Tests supply the clock. |
| DEC-02 | The demo request returns the same 202 status and message for known and unknown addresses. A reset returns 200/password_changed on success, 400/expired_link on expiry, or 400/invalid_link for an unknown, used, or revoked link. Access with a revoked session returns 401. These are demo observations. |
| DEC-03 | Only an issued replacement invalidates older unused links for that account. A blocked request issues no replacement; other accounts are unaffected. |
| DEC-04 | The fifth request is blocked without email and starts a 900-second cooldown. A request at the cooldown deadline is eligible again. |
| DEC-05 | Blocked requests do not send email, invalidate links, count toward a new window, or extend cooldown. |
| DEC-06 | The rolling window includes requests less than 900 seconds old; a request exactly 900 seconds old is excluded. An unsuccessful reset leaves sessions unchanged. |
The exercise leaves six questions open: password rules and storage, additional IP limits, delivery delay and retries, support overrides, concurrent token consumption, and response-time measurement. A real team must settle the relevant questions before accepting the feature. Keep an owner and an impact note with each question: an unanswered delivery deadline blocks a delivery-latency criterion, while an undecided IP limit should not block drafting the already approved account-limit scenarios.
If a ticket says 30 minutes but an API document says 60, record both source locations and request a decision. Do not average the values or assume the newer timestamp has authority. Once the owner decides, add a decision ID and regenerate only the affected cases.
Ask for clarification before asking for scenarios
Use a first pass to expose gaps. This keeps uncertain business choices out of otherwise tidy scenarios.
Review the attached brief and its source version.
List approved rules, contradictions, and unanswered questions.
For each question, state which outcome cannot yet be specified
and which owner role should answer it.
Do not choose thresholds, permissions, response codes, or policies.
Keep source wording and IDs with every extracted rule.
After the owners answer, paste their decisions with the approved rules. Ask for a readable draft that your reviewers can inspect directly:
Draft acceptance criteria from these approved requirements and decisions.
Return a table with: ID, source references, starting condition,
action, expected result, and how a tester can verify it.
Include relevant success, rejection, boundary, and state-change cases.
Use concrete example values without changing the business rules.
Separate conditions that lead to different outcomes.
List unanswered questions separately, with the owner role and
the criterion each question prevents us from finishing.
Mark the criteria as drafts. Keep test execution status untested.
Keep the source packet, prompt, output, and review edits together. If you change the prompt, compare the resulting cases against the same brief. A longer output is useful only when it distinguishes another condition or corrects a missing outcome.
Work one requirement through to an observable result
Consider the link-expiry rule. An acceptance criterion states the condition the feature must satisfy: an unused link can complete a reset before its approved expiry time and is rejected at or after that time. Scenarios provide concrete examples. The test implementation supplies the clock, creates the account and link, invokes the reset action, and checks the result.
Gherkin's reference describes the starting context, event, and expected outcome in Given/When/Then form. In Cucumber, step definitions connect those sentences to code. You can also review the same scenarios manually or implement them in a different test framework.
AC-02: link expires exactly at its deadline
Sources: REQ-01, DEC-01, DEC-02
Given an unused link issued at t=0
When the reset is submitted at t=1800 seconds
Then the reset is rejected as expired
Observable in this demo: 400/expired_link
Use separate, freshly issued links for the boundary examples. Reusing the link that already succeeded would test single-use behavior as well as expiry and could hide an expiry defect.
| Case | Age of fresh unused link | Expected reset result |
|---|---|---|
| AC-01 | 1,799 seconds | Accepted |
| AC-02 | 1,800 seconds | Rejected as expired |
| AC-03 | 1,801 seconds | Rejected as expired |
These values surround the chosen boundary at one-second precision. If the implementation uses milliseconds, retain the same rule and choose nearby values at that precision. Inject or freeze time in the test environment instead of waiting 30 minutes. Record which clock determines validity; a browser clock and a server clock may disagree.
The response codes above belong to the demo's observation contract. For your service, use its approved API response or visible screen state. "The token is marked expired in a database" is usually a weaker acceptance observation than "the expired link cannot reset the password." Keep internal storage assertions in lower-level tests when they are needed.
Cover the other rules without hiding missing behavior
One scenario per requirement is a starting inventory, not evidence of complete behavior coverage. The worked case packet contains 16 scenarios. Several rules need multiple examples because their result depends on time, previous actions, or another account.
| Rule | Cases in the packet | What the tester observes |
|---|---|---|
| Expiry | AC-01 to AC-03; AC-15 | Before, at, and after expiry; an unissued token is rejected. |
| Single use | AC-04 | The first submission succeeds; the second submission of that token fails. |
| Replacement | AC-05 and AC-06 | The older link fails, the replacement works, and another account's link still works. |
| Account privacy | AC-07 | Known and unknown addresses receive identical public status and message. |
| Request limits | AC-08 to AC-13 | Four issued emails, no fifth email, blocked requests, cooldown recovery, and window boundaries. |
| Sessions | AC-14 and AC-16 | Old sessions fail after success; another account and an unsuccessful reset remain unaffected. |
For single use, a two-submission sequence is appropriate: the first action establishes the used state that the second action needs. For replacement, test the old and new links after the same replacement event. Give the cases distinct names so a failure points to the behavior that broke.
For cooldown, count messages at a controlled mail sink. A generic public response may intentionally be identical for an accepted and blocked request, so that response alone cannot prove whether a new link was sent. At t=4 the exercise starts cooldown; t=903 is blocked and t=904 is eligible again. A request at t=100 must not move the recovery deadline.
For sessions, create two sessions for the reset account and one for a different account. After a successful reset, call a protected resource with each old session. A success message on the reset screen does not establish session revocation. Also attempt an expired reset and check that the existing session remains usable.
Privacy needs a separate security review. OWASP's reset guidance addresses consistent messages and response timing, as well as expiring single-use tokens and request protections. The demo compares status and message only. Its in-memory execution time cannot establish resistance to account enumeration in a deployed service.
Repair weak AI output with a specific review request
Compare each result to its cited source. An accurate ID next to a wrong sentence is still wrong. Check that the starting state makes the action possible and that the expected result answers what happened during that action.
| Weak output | Problem | Useful correction request |
|---|---|---|
| "The reset works correctly." | No observable success or rejection condition. | Name the approved response and the action used to verify it. |
| A before-expiry action followed by an after-expiry result. | The scenario never reaches the rejected condition. | Split the time conditions into independent examples. |
| Require a particular password length while citing OPEN-01. | An unresolved policy became expected behavior. | Remove that requirement and keep the password-policy question open. |
| "A cooldown starts." | No indication of what requests during cooldown do. | Ask for the approved send/block behavior and deadline, then add the relevant cases. |
| "All requirements are covered." | The output may only mention every ID. | Map each rule to its conditions, rejected outcomes, and remaining gaps. |
For a deliberately incorrect draft of AC-02, a repair request can say: "This draft invokes the reset before expiry but expects an expired result. Move the action to the approved deadline and update the source references. Preserve REQ-01, DEC-01, and DEC-02, and leave OPEN-01 unanswered." This hypothetical bad draft is different from the correct AC-02 shown above and included in the download. Review the repair against the brief again; the model may fix that sentence while changing a neighboring policy.
Assign review by responsibility. Product checks intended behavior and unresolved choices. QA checks starting states, boundaries, and observations. Engineering checks whether the environment can create and inspect those states. Security reviews account recovery, permissions, and possible disclosure. Record the actual reviewer and decision in a real project; the fictional exercise's decision IDs do not substitute for approval.
Follow one draft through to a finished ticket
This example puts the earlier steps together for the expiry and single-use portion of the reset feature. The exchange is illustrative, not a recorded response from a particular AI model. The other reset requirements remain separate work.
Submit the brief and answer the questions
Paste the cover sheet and this requirement: "Customers recover their account by email. Reset links expire after 30 minutes and work once. Use a secure password policy." Ask the clarification prompt first. Useful questions include whether the exact expiry time is allowed, what a tester should observe after rejection, and which password policy has approval.
Supply the exercise's answers: DEC-01 rejects a link at exactly 1,800 seconds; DEC-02 defines the demo's success and rejection responses. Leave the password-policy question OPEN-01. Then use the drafting prompt. An unanswered password policy does not prevent drafting expiry cases, but it prevents treating a proposed password rule as approved.
Correct a draft that contradicts its source
Suppose the draft puts an unused link at 1,799 seconds and expects an expired rejection. Compare that result to DEC-01: the action is before expiry, so this draft is wrong. Ask for the action to occur at 1,800 seconds for the expiry-rejection case, plus independent examples just before and after the deadline. Check that the repair has kept the single-use rule and has not invented a password policy.
For your own feature, the same review asks three concrete questions: does the starting condition allow the action, does the result follow the approved rule, and could the proposed observation reveal a wrong result? If the AI produces several near-identical cases, ask it to identify the distinct condition each one covers before retaining all of them.
Copy the finished draft into the work item
The resulting ticket can use ordinary language. The sample below remains a draft awaiting product and QA review; no real project sign-off or service execution is implied.
Title: Reset-link expiry and single use
Source version: reset-demo-v2
Scope: REQ-01 and REQ-02, clarified by DEC-01 and DEC-02
Status: Draft for product and QA review
Automation and execution: Not attached; service tests not run
Data: A fresh unused link for each independent expiry example;
a separate link for the two-submission single-use sequence.
Open item: OPEN-01, password policy, owned by product and security.
Draft no password-policy criterion until it is decided.
| Criterion | Starting condition and action | Expected outcome and check |
|---|---|---|
| AC-01: before expiry REQ-01, DEC-01, DEC-02 | Submit a fresh unused link at age 1,799 seconds. | The reset succeeds. Check the agreed success result; the demo reports 200/password_changed. |
| AC-02: at expiry REQ-01, DEC-01, DEC-02 | Submit a fresh unused link at age 1,800 seconds. | The reset is rejected as expired. The demo reports 400/expired_link. |
| AC-03: after expiry REQ-01, DEC-01, DEC-02 | Submit a fresh unused link at age 1,801 seconds. | The reset is rejected as expired. The demo reports 400/expired_link. |
| AC-04: single use REQ-02, DEC-02 | Complete a reset with a valid unused link, then submit that same link again while it is still before expiry. | The first reset succeeds and the second is rejected as invalid. Inspect both action results; the demo reports 200/password_changed followed by 400/invalid_link. |
QA can now prepare those states and observations. Engineering can attach manual instructions or automated bindings for the actual service. Product resolves the remaining policy and records approval. When handing off your own ticket, replace the demo response codes with the approved interface or API contract. Keep routine team-wide release checks in the team's completion checklist, so the feature's acceptance criteria remain about its behavior.
Choose which scenarios matter and when to stop
There is no universal target of 16 cases. Start with the approved behaviors and the consequences of getting them wrong. Review the highest-impact outcomes first, then distinguish the boundaries and states that could change those outcomes.
| Review priority | Reason to include a case | Example |
|---|---|---|
| Harmful or unauthorized outcomes | A defect could expose an account, move money twice, or permit a forbidden action. | Unknown-address privacy; a reused reset link; a second refund. |
| Main approved behavior | The feature must deliver the requested result in its allowed starting state. | An unused valid link succeeds; a valid import row is stored. |
| Boundaries and state changes | The expected result changes with a deadline, count, prior action, or account state. | Before/at expiry; the fifth request; a paid versus unpaid order. |
| Additional data variations | A different input could exercise a different parsing or validation rule. | A blank email and a quoted comma deserve separate reasoning; a second ordinary name may add no new condition. |
For a short review, make the high-impact cases visible first and list what remains unreviewed. A limited review is not a reason to accept the feature while its important checks are missing. If a performance threshold or concurrent-operation policy is undecided, give that gap an owner and keep the affected criterion pending.
Keep separate cases when their starting state, approved outcome, observation, or failure diagnosis differs. A parameterized table can group similar input examples when the same action and rule apply. Keep the before/at/after rows distinguishable even if one automated test loops over them. Two cases that differ only in an arbitrary sample name may be duplicates; two emails belonging to different accounts can be essential to an account-isolation check.
Stop expanding the draft when each approved behavior has a usable observation, relevant changes in outcome have distinct examples, serious failure paths are represented, and remaining gaps have an owner and status. Review a proposed extra case by asking what defect or rule difference it could expose that the existing cases would miss. If there is no such difference, merge it or leave it out. Finishing the draft does not mean the feature has passed its tests.
Adapt the method to permissions, retries, and performance
The same extraction and review process works for other feature types, but the questions and observations change.
| Situation | Ask before drafting | Include in the scenarios |
|---|---|---|
| Permissions | Which roles can act on which resource and account? | Allowed owner, denied non-owner, expired session, and relevant administrative exceptions. |
| State transitions | Which source states permit the action? | Valid transition, repeated action, cancelled item, and forbidden transition. |
| Asynchronous work | What event means queued, delivered, failed, or completed? | Delayed completion, failure, retry, and the observable final state. |
| Duplicate requests | Is repeating an operation meant to repeat its effect? | Same operation submitted twice and the approved number of effects. |
| Performance | Which load, dataset, percentile, measurement point, and duration apply? | The agreed workload and threshold; keep unspecified numbers open. |
| Conflicting sources | Who decides which statement wins? | A blocked decision record, followed by cases for the approved resolution. |
For a webhook-driven update, "processed successfully" could mean receipt, queue insertion, or the customer-visible change. Choose the event the requirement promises. If retries are allowed, define whether an already processed event changes the account again. Give the tester a known event ID and a way to inspect the final result.
For "search must be fast," ask for the measurement contract before writing a number. A criterion such as a response-time percentile is useful only with the approved workload and measurement point. If those details are unavailable, prepare the functional scenarios and flag the performance criterion as blocked.
For token races or simultaneous payments, sequential scenarios cannot establish atomic behavior. Specify the intended result of competing operations, then ask engineering to create a concurrent integration test. The reset demo deliberately uses a single thread and leaves that implementation question open.
A refund example: time, money, and repeated requests
Suppose a fictional store approves this rule: a customer may request one full refund for a paid, unrefunded order strictly before 14 days have elapsed since payment. In this exercise a full refund equals the captured amount in the order's currency. Label the eligibility and amount rule REF-01. REF-02 says a repeated submission must not create a second refund. These are example policies, not consumer-law advice or a payment-provider guarantee.
Extract the eligibility state, time comparison, amount, and duplicate behavior separately. Ask whether shipment status changes eligibility, how partial refunds are handled, and whether a failed provider request can be retried. Do not have the model infer answers from the phrase "full refund."
REF-AC-01
Sources: REF-01, REF-02
Given a paid, unrefunded USD order captured for 49.99
And its approved refund deadline has not been reached
When its customer submits a refund request
Then one refund for USD 49.99 is created
Observable: the provider stub records one refund and the order
shows the approved refund state.
Add separate cases just before and exactly at the deadline. Add an unpaid order, an already refunded order, and a second submission after the first succeeds. For duplicate behavior, inspect the count and amount of provider-stub calls as well as the customer-visible result. If the same screen message can appear after two charges or refunds, screen text alone misses the damaging behavior.
Use integer minor units or the implementation's approved decimal representation in executable tests. Keep currency conversion, tax treatment, partial amounts, and rounding rules in the decision log if the brief does not settle them. The refund example is a worked scenario only; it is not included in the reset demo's executed test count.
A CSV-import example: partial failure and useful errors
Imagine a fictional contact importer with three approved rules: the header must contain email and name; valid rows are imported even when another row is invalid; rejected rows are reported by their source line number and reason. The exercise separately decides that a duplicate email within the file rejects the later row and that line numbers include the header.
Build a tiny input with a header, one valid record, one blank email, and a later duplicate of the valid email. It should yield one imported contact and two reported errors: line 3 has a blank email, and line 4 duplicates line 2. Label the header rule IMP-01, partial-success rule IMP-02, and error-reporting rule IMP-03. Record the duplicate and line-number decisions separately. Compare the stored contact or exported result to the input, and inspect the error report for the correct line references.
email,name
member@example.test,Ada
,Bo
member@example.test,Chen
IMP-AC-01
Sources: IMP-01, IMP-02, IMP-03; duplicate and line-number decisions
Given a file with a valid header, one valid contact,
one blank email, and a later duplicate email
When the import completes
Then the valid contact is imported once
And the blank and duplicate rows are rejected with their
source line numbers and approved reasons.
Add a missing-header case that rejects the file before importing rows. Ask about existing contacts, encoding, field limits, quoted commas, and importing the same file twice. Each can change the result; none should be decided just because the model recognizes a familiar CSV format.
If the requirement says imports are atomic, this partial-success scenario is wrong. Change it to assert that one invalid row leaves the entire import unapplied, and include the relevant source ID. If duplicate updates are approved, distinguish an update from a new contact. The importer example is not executed by the reset test script; adapt its cases to the actual importer and test data.
Keep the criteria current after requirements change
Before handoff, include the approved source version, rule and decision log, reviewed scenarios, open questions with owners, coverage map, and the actual test state. Use separate states for a draft, a reviewed criterion, an automated test, and an executed result. A criterion can be reviewed before its automation exists, but its execution state must remain untested.
If the link lifetime changes to 20 minutes, update REQ-01 and its decision record, then review the before/at/after values and the corresponding assertions. Check downstream effects: a delayed email may now arrive with too little usable lifetime. Do not silently replace the old source version or keep the previous test result as evidence for the changed behavior.
For your own packet, finish these checks:
- Every expected outcome follows an approved source; unresolved questions have owners.
- Each case starts in a possible state and performs the action whose result it asserts.
- Important boundaries, rejected actions, and affected accounts are distinguishable.
- Observations can expose the defect, including unwanted emails or repeated provider calls.
- The test environment identifies its clock, data, implementation version, and external-service substitutes.
- Recorded failures name the assertion that broke; crashes and missing dependencies are investigated separately.
If the structure check passes but the case reads incorrectly, return to the source and semantic review. If the baseline code test fails, repair the implementation or the approved test expectation before using mutation results. If a controlled defect survives, inspect whether the relevant action was exercised and whether the assertion observes its effect. Attach the resulting evidence to that case and source version.
Optional: run the teaching example and inspect its tests
You can draft, review, and hand off the ticket without running Python. Use this section when you want to inspect how the example scenarios connect to executable checks. The kit includes JSON-format prompts for its checker; your own reviewed criteria can stay in the document or ticket format your team uses.
Download and extract the acceptance-criteria review kit. It includes both JSON packets, the structure checker, the in-memory reset model, explicit Python tests, prompts, a case-to-test map, the recorded result, and file hashes. Python 3.10 or later is sufficient; no external package or service is required. Run these commands from the extracted directory:
python3 acceptance-criteria-check.py \
acceptance-criteria-fixture-v2.json \
acceptance-criteria-cases-v2.json
python3 acceptance-criteria-demo.py
The structure checker validates nonempty fields, unique case IDs, source version, approved source IDs, the presence of all six requirement IDs, and the six-question inventory. Its successful result is structure_pass_review_required, with semantic_review: not_performed and implementation_tests: not_run. It cannot tell whether the prose contradicts its source. The kit intentionally retains a wrong-prose control to demonstrate that limit.
The separate demo script runs 16 explicit unittest tests against a small reset model, then reruns them with nine controlled implementation changes. The recorded Python 3.13.2 run passed all 16 baseline tests and detected each change through assertion failures without test errors. Changes include accepting the exact expiry boundary, allowing reuse, keeping older links, disclosing account existence, bypassing or shortening cooldown, keeping sessions, sending the fifth email, and extending cooldown on blocked requests.
You can inspect one failure directly:
python3 acceptance-criteria-demo.py --mutant expiry_boundary
That command should exit with code 1 and name the failed expiry assertion. The normal command should exit with code 0. Google's mutation-testing discussion explains why changing implementation behavior and observing test failures provides evidence beyond simply executing code. The test record lists the actual failures for each controlled change.
The model uses deterministic demo tokens, a fake mail sink, injected timestamps, and a dictionary of sessions. It has no real HTTP server, password storage, mail provider, or concurrent token handling. The Python assertions are written explicitly; editing a prose scenario does not regenerate them. Passing this example demonstrates the chosen teaching behaviors, while a real implementation still needs its own bindings and integration checks.
