# Pass or fail write-up

How to write up a consent test so a reviewer can repeat any case and an owner can act on any finding. Keep the layers separate: configuration (CMP, GTM, Google tag settings), consent state (Tag Assistant, CMP API), execution (requests and cookies), destination (GA4, Ads, UET, BigQuery). A failure recorded at the right layer has an obvious owner.

## Severity and verdict rules

- S1: data sent or storage written beyond what the visitor's current choice allows. Examples: vendor cookies before a choice in an opt-in region; vendor requests after reject or withdrawal; Google requests before a choice in a basic-mode design; pre-consent events released on grant when the specification forbids it; an implied grant in an opt-in region.
- S1 (functional): an essential journey (form, checkout, login) fails after a reject, a dismissal, a withdrawal or with the CMP blocked. Graded like S1, with the site owner as owner.
- S2: permitted data lost or duplicated. Examples: first page view lost after acceptance; duplicate page view; a returning visitor's first request carries denied state despite a stored grant; `ad_user_data` or `ad_personalization` missing so ads features stay off.
- S3: configuration risk with no wrong behaviour observed yet. Examples: two consent writers that currently agree; a vendor tag left "Not set" or marked "No additional consent required" that is not sending on any tested route; unclassified cookies.
- Specification gaps (an undefined region, an unstated rule for GPC, implied consent or pre-consent events): list them separately for the privacy owner. They are not test failures.
- Verdict, in this order:
  - Fail: any open S1, including functional S1, whatever the coverage.
  - Incomplete: no open S1, but at least one minimum-set case is "not run", or the specification is a draft the privacy owner has not approved.
  - Conditional pass: full coverage, an approved specification, no S1, and every S2 has an owner, a fix date and the user's explicit acceptance.
  - Pass: full coverage, an approved specification, and S3 only or nothing.
- A case that could not be run is "not run" with a reason. It never counts toward a pass.
- Race-sensitive cases pass only on three of three runs.

## Template

```markdown
# Consent test: <site>, <date>

Specification: <link or file and version>
Google mode: <basic | advanced>, per region if it differs
Build under test: GTM container version <n>, CMP configuration <ID or version, published date>, site release <tag>
Rig: <Mode A agent browser | Mode B user-run>, <browser and version>, fresh profile per case, egress per region <method>, throttling <preset>
Conversion gate: <approved by, date, where run, test markers, exclusion, cleanup owner | not approved>
Verdict: <Pass | Conditional pass | Incomplete | Fail>
Coverage: <complete | incomplete: minimum-set cases not run>

## Summary
- Cases planned <n>, run <n>, passed <n>, failed <n>, not run <n>, N/A <n>
- Open findings: S1 <n>, S2 <n>, S3 <n>

## Findings
### F1 <short title> (S1 | S2 | S3)
- Case: <region>-<choice>, run <n of 3>
- Layer: <configuration | consent state | execution | destination>
- Expected: <from the specification row>
- Observed: <what happened, with parameter values such as gcs, gcd letters, asc=D or asc=G>
- Evidence: <HAR file and entry, Tag Assistant event, snapshot output, cookie list>
- Owner: <team or role>
- Proposed fix: <change list item, not applied>
- Retest: <case IDs to rerun after the fix>

## Case log
- <case>: <pass | fail | not run> <runs passed>/<runs>; evidence <files>; note <one line>

## Specification gaps (for the privacy owner)
- <rule the specification does not define, and the case that exposed it>

## Not covered
- <regions, journeys, browsers or vendors not tested, and why>
```

## Worked example

Fictional data throughout. Acme Outdoor sells on `www.example.com`, with GTM-XXXXXXX, GA4 G-XXXXXXXXXX, Google Ads AW-XXXXXXXXX, a Meta pixel and a TikTok pixel. Cookiebot is deployed through the GTM template.

Specification v3 (approved by the privacy owner):

- R1 (EEA countries) and R2 (`GB`, its own region row in the Cookiebot template): all four Google signals denied by default; advanced mode for Google tags; Meta and TikTok on the marketing category; events before a choice are not sent to any non-Google vendor.
- R4 (rest of world): granted by default, no banner.
- R5 (location unknown): treated as R1.
- Withdrawal: vendors stop on the same page; existing cookies may remain until expiry.
- Closing the banner counts as reject. No implied consent anywhere.

Run on 2026-10-14 in a fresh agent-owned Chromium context per case (Mode A), egress through a VPN in Germany, the UK and the US. Twenty-eight cases planned, twenty-one run; four R4 cells are N/A because R4 shows no banner. The site owner approved the conversion gate in writing on 2026-10-13.

```markdown
# Consent test: www.example.com, 2026-10-14

Specification: martech-context/decisions.md, consent specification v3
Google mode: advanced in R1 and R2
Build under test: GTM container version 48, Cookiebot configuration published 2026-10-10, site release 2026.10.2
Rig: Mode A, Chromium, fresh context per case, VPN egress DE, GB, US, network throttling plus 4x CPU slowdown on run 3
Conversion gate: approved by the site owner 2026-10-13; run on www.example.com; leads named "Consent Test C9" and "Consent Test C12" with jane+consent-test@example.com; ads owner excluded the test conversions after the run; CRM owner deleted the records 2026-10-15
Verdict: Fail
Coverage: incomplete (R5 not run)

## Summary
- Cases planned 28, run 21, passed 17, failed 4, not run 7, N/A 4
- Open findings: S1 1, S2 2, S3 1

## Findings
### F1 TikTok releases pre-consent events on grant (S1)
- Case: R1-C2, runs 1 to 3; R1-C12, runs 1 to 3 (the lead submitted before any choice was sent on accept)
- Layer: execution
- Expected: no TikTok event from before the choice is sent
- Observed: the pixel loads on All Pages with ttq.holdConsent(); on accept, ttq.grantConsent() sends the queued Pageview and ViewContent from before the choice
- Evidence: R1-C2-run1.har entries 41 and 42; snapshot shows grantConsent called 1.8 s after accept
- Owner: web analytics
- Proposed fix: stop loading the TikTok base code before marketing consent (Require additional consent: ad_storage, trigger cookie_consent_update) and remove holdConsent
- Retest: R1-C0, R1-C2, R1-C7, R1-C12, R2-C2

### F2 Meta pixel never fires on the acceptance page (S2)
- Case: R1-C2, runs 1 to 3
- Layer: configuration
- Expected: Meta PageView once on the page where the visitor accepts
- Observed: tag requires ad_storage but fires on All Pages only; no pixel request until the next page
- Evidence: R1-C2-run1.har has no facebook.com/tr request before navigation; Tag Assistant Tags tab shows the tag not fired
- Owner: web analytics
- Proposed fix: replace All Pages with cookie_consent_update on that tag only, firing once per page
- Retest: R1-C2, R1-C6 (watch for a duplicate PageView)

### F3 Returning visitor's first GA4 request sent as denied (S2)
- Case: R1-C6, run 3 of 3 failed (throttled); runs 1 and 2 passed
- Layer: consent state
- Expected: first /g/collect request on the first page carries gcs=G111, gcd letters r (stored grant applied as an update before the first tag)
- Observed: first request gcs=G100 with gcd letters p (default only), the next G111 with r; Tag Assistant shows the update after the first page_view, arriving with the Cookiebot script rather than at Consent Initialization. Approximate gap from the consent default to the update: 740 ms
- Evidence: R1-C6-run3.har entries 12 and 19 (extractor output); Tag Assistant Consent events
- Owner: web analytics with the CMP owner
- Proposed fix: find out why the stored choice is not applied at Consent Initialization from the CookieConsent cookie before any tag fires. Do not raise wait_for_update until the cause is known; a longer wait delays every Google tag for every visitor and hides the race
- Retest: R1-C6 three runs, R2-C6

### F4 Second consent default from the theme (S3)
- Case: R1-C0, all runs
- Layer: configuration
- Expected: one writer of the Google default (the Cookiebot template)
- Observed: the theme also calls gtag('consent','default') with ad_storage and analytics_storage only, before GTM loads; values agree today, v2 signals absent from that call
- Evidence: header template source; first dataLayer entry
- Owner: site developer
- Proposed fix: remove the theme call
- Retest: R1-C0, R2-C0, R4-C0

## Case log
- R1-C0: pass 3/3; G100, no _ga, FPID or _gcl_au, no vendor events; asc=D
- R1-C1: pass 1/1; lead form submits after reject
- R1-C2: fail (F1, F2)
- R1-C3: pass 1/1; G101, Meta and TikTok silent
- R1-C5: pass 1/1
- R1-C6: fail 2/3 (F3)
- R1-C7: pass 1/1; update to denied on same page, no Meta automatic events after withdrawal
- R1-C8: pass 1/1
- R1-C9: pass 3/3; lead conversion request carries granted state
- R1-C10: pass 1/1; closing the banner behaved as reject
- R1-C11: pass 1/1; Cookiebot.consent.method stayed unset while scrolling and navigating
- R1-C12: fail (F1)
- R1-C14: pass 1/1; CMP host blocked, defaults denied, lead form usable
- R2-C0: pass 3/3
- R2-C1: pass 1/1
- R2-C2: fail (F1)
- R2-C6: pass 3/3
- R2-C7: pass 1/1
- R2-C10: pass 1/1
- R2-C11: pass 1/1
- R4-C0: pass 3/3; G111 from first request, CookieConsent -1
- R4-C1, R4-C2, R4-C6, R4-C7: N/A, no banner in R4
- R5-C0, R5-C1, R5-C2, R5-C6, R5-C7, R5-C10, R5-C11: not run; no egress the CMP could not place

## Specification gaps (for the privacy owner)
- No rule for US states with opt-out regimes or for Global Privacy Control, so no R3 row and no C13 case exist

## Not covered
- Unknown-location fallback (R5): needs an egress the CMP cannot place; a CMP location override would test the banner only, not Google's region default. Owner to supply one before the retest
- Safari returning-visitor cases (no Safari rig this round)
```

What makes this record useful: every finding names its case, layer, owner and retest set; F1 shows up in two different cases, which points at one cause; F3 shows why three runs matter and why the fix starts from the cause rather than a longer wait; F4 is recorded even though nothing is wrong yet, because the next theme change will make the two writers disagree. Without F1, R5 alone would cap the verdict at Incomplete.
