From 0ea05e81270e5eaa832cc7a604588e736ece7404 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 28 Aug 2026 17:29:32 -0700 Subject: [PATCH 01/10] docs: design spec for the fixture-replay blog post Co-Authored-By: Claude Opus 5 --- .../2026-08-28-fixture-replay-post-design.md | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-28-fixture-replay-post-design.md diff --git a/docs/superpowers/specs/2026-08-28-fixture-replay-post-design.md b/docs/superpowers/specs/2026-08-28-fixture-replay-post-design.md new file mode 100644 index 000000000..656d67cb4 --- /dev/null +++ b/docs/superpowers/specs/2026-08-28-fixture-replay-post-design.md @@ -0,0 +1,80 @@ +# Design: "What Fixture Replay Can't Catch" (blog post #12) + +**Date:** 2026-08-28 +**Sequence:** post #12 of the GSC-driven blog sequence. Posts #11, #9, #1 shipped. +**Status:** approved angle, pending spec review. + +## Why this post is different from the others + +It is the only post in the sequence with **no search evidence** behind it. #11, #9, and #1 were each selected against a Search Console pull. This one is a bet on being genuinely differentiated and shareable rather than on capturing existing demand. + +That raises the bar: if the material turns out thin, say so rather than padding. It did not turn out thin — see Verified facts. + +## Thesis + +A deterministic test harness buys determinism by deleting a dimension. Ours deletes **time** — deliberately, with the reason written in the source — and that makes one specific bug class structurally invisible to a green suite. + +The post is not "how to mock your LLM." That post exists everywhere. The post is "here is what my passing suite cannot see, and why I chose that trade anyway." + +## Co-ranking check + +`apps/website/content/docs/langgraph/guides/testing` covers the **in-process** tier: `provideFakeAgent()`, `mockLangGraphAgent()`, `MockAgentTransport`. Its only "fixture" occurrences are Angular `TestBed` / `ComponentFixture` (lines 91-101). It says nothing about aimock or fixture replay. + +So there is no co-ranking risk. The post covers the **out-of-process** harness, a different layer, and should link the guide for the unit tier rather than restate it. + +## Structure + +Target ~1,300 words. The subgraphs post ran ~1,900 and a reviewer measured that as roughly twice the two shipped siblings; this one stays tighter. + +1. **Opening.** Thesis in two lines. Determinism has a price; the useful question is which one you paid. +2. **"Where do you put the mock?"** The seam is one environment variable. Everything above it runs for real. +3. **"What does a fixture match on?"** Matching semantics, then the ordering constraint that loops forever. +4. **"What did we trade away?"** `chunkSize: 4096`, quoted from source. Replay is near-atomic by design. +5. **"The bug class that hides there."** Mid-stream and self-correcting. The subgraph transcript leak as the worked example. +6. **"What runs alongside it."** The live gate, and how drift is detected. +7. **Close.** Invitation, matching the siblings. + +## Verified facts + +Every line below was read directly in this repo on 2026-08-28. + +**The seam.** +`libs/e2e-harness/src/global-setup-factory.ts:90` spawns a real `langgraph dev` with `OPENAI_BASE_URL: aimock.baseUrl` and `OPENAI_API_KEY: 'test-not-used'`. Real Angular app, real transport, real LangGraph server, real Python graph. Only the model provider is replaced. `ag-ui-global-setup-factory.ts:89` does the same for the AG-UI stack. + +**Scale.** 50 fixture files, 129 total fixture entries, across 34 capabilities (counted by parsing every `**/e2e/fixtures/*.json`). + +**The determinism trade.** +`libs/e2e-harness/src/aimock-runner.ts:59` constructs `new LLMock({ port: 0, chunkSize: 4096 })`. The comment at :50-58 states the rationale: a large chunk size makes each response arrive in 1-2 SSE deltas, so structural assertions measure the FINAL rendered DOM rather than the progressive render; with default chunking the partial-markdown parser sometimes cannot recover a triple-backtick fence split mid-token, and the final state degrades to inline ``. Progressive behavior is covered by unit-variance tables instead. + +This is the load-bearing fact of the post: replay is near-atomic **on purpose**, and the reason is written down. + +**The ordering constraint.** +`cockpit/langgraph/client-tools/angular/e2e/fixtures/client-tools.json` holds 7 entries. Every `match` block carrying `hasToolResult: true` precedes its plain `userMessage` twin (indices 0<1, 2<3, 5<6). Matching is first-match-wins, so reversing a pair makes the post-tool continuation re-match the original tool call and loop forever — the assistant never finalizes. + +**The bug class replay cannot catch.** +Mid-stream defects with a clean end state. Worked example, measured this week: `cockpit/langgraph/subgraphs`. Without `transcriptNodeNames: ['answer']`, the child graph's internal brief renders as its own chat bubble and the message list transiently reaches 3 before the parent's authoritative `values` event collapses it back to 2 (`cockpit/langgraph/subgraphs/angular/src/app/app.config.ts:20`). A final-state assertion passes. Confirmed live with a 60ms DOM sampler against a real model: with the option set, `chat-message` count never exceeded 2 across 56 samples spanning a full stream. + +**Drift detection — describe the mechanism, not a cadence.** +`examples/chat/angular/e2e/scripts/drift.ts` re-records each committed fixture against the live provider and compares **byte length**, flagging any fixture whose size diverges by more than 20% (`THRESHOLD_PCT = 0.2`). `.github/workflows/aimock-drift.yml` runs it and opens an issue on failure. + +The workflow is currently `workflow_dispatch` only; its cron is deliberately omitted because the committed fixtures are handwritten seeds rather than recordings. **Brian intends to fix this.** The post therefore describes how drift is detected — the mechanism — and makes no claim about a schedule. That phrasing is accurate today and remains accurate once the cron is enabled, so the post will not need revisiting either way. + +Worth saying plainly in the post: a byte-size comparison detects that a response changed *size*, not that it changed *meaning*. That is another instance of the thesis, not a contradiction of it. + +## Hard prohibitions + +- No invented first-person anecdotes. Brian: "don't make up stories." +- No claim that the suite catches more than it does. The whole point is the opposite. +- No cadence claim about drift detection. +- No emoji, no hype, no marketing CTA, no "Introduction" heading, no licensing callout. +- No line numbers in prose. +- Do not restate the testing guide's in-process API surface; link it. +- Verify any named public API against the published tarball, not just source — main routinely runs ahead of npm. + +## Voice + +`docs/gtm/voice.md` with the 2026 technical override. Register references: the two shipped siblings and the subgraphs post. H2-as-question answered in its first line, `Let's` transitions, 1-3 line paragraphs, opinions flagged ("For me", "I think"), italics-only emphasis, contractions kept. + +## Follow-up this post creates + +Enable the `aimock-drift.yml` cron once fixtures are recorded rather than handwritten. Tracked separately; the post does not depend on it. From 16aad367aea080fb47c18817560e422b852233ac Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 28 Aug 2026 17:29:50 -0700 Subject: [PATCH 02/10] docs: tighten the scale claim in the fixture-replay spec Co-Authored-By: Claude Opus 5 --- docs/superpowers/specs/2026-08-28-fixture-replay-post-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-08-28-fixture-replay-post-design.md b/docs/superpowers/specs/2026-08-28-fixture-replay-post-design.md index 656d67cb4..87716cd09 100644 --- a/docs/superpowers/specs/2026-08-28-fixture-replay-post-design.md +++ b/docs/superpowers/specs/2026-08-28-fixture-replay-post-design.md @@ -41,7 +41,7 @@ Every line below was read directly in this repo on 2026-08-28. **The seam.** `libs/e2e-harness/src/global-setup-factory.ts:90` spawns a real `langgraph dev` with `OPENAI_BASE_URL: aimock.baseUrl` and `OPENAI_API_KEY: 'test-not-used'`. Real Angular app, real transport, real LangGraph server, real Python graph. Only the model provider is replaced. `ag-ui-global-setup-factory.ts:89` does the same for the AG-UI stack. -**Scale.** 50 fixture files, 129 total fixture entries, across 34 capabilities (counted by parsing every `**/e2e/fixtures/*.json`). +**Scale.** 50 fixture files holding 129 total fixture entries, across 34 distinct apps — 32 cockpit capabilities plus 2 example apps (counted by parsing every `**/e2e/fixtures/*.json`). Say "apps" or "capabilities and example apps" in prose, not "capabilities" alone: 2 of the 34 are examples, not cockpit caps. **The determinism trade.** `libs/e2e-harness/src/aimock-runner.ts:59` constructs `new LLMock({ port: 0, chunkSize: 4096 })`. The comment at :50-58 states the rationale: a large chunk size makes each response arrive in 1-2 SSE deltas, so structural assertions measure the FINAL rendered DOM rather than the progressive render; with default chunking the partial-markdown parser sometimes cannot recover a triple-backtick fence split mid-token, and the final state degrades to inline ``. Progressive behavior is covered by unit-variance tables instead. From f305476302e0ff8970f726c23c7d1f26167ec23e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 28 Aug 2026 18:43:25 -0700 Subject: [PATCH 03/10] docs: implementation plan for the fixture-replay post Co-Authored-By: Claude Opus 5 --- .../plans/2026-08-28-fixture-replay-post.md | 352 ++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-28-fixture-replay-post.md diff --git a/docs/superpowers/plans/2026-08-28-fixture-replay-post.md b/docs/superpowers/plans/2026-08-28-fixture-replay-post.md new file mode 100644 index 000000000..4222d7488 --- /dev/null +++ b/docs/superpowers/plans/2026-08-28-fixture-replay-post.md @@ -0,0 +1,352 @@ +# "What Fixture Replay Can't Catch" Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship blog post #12 — an essay arguing that a deterministic agent-test harness buys determinism by deleting time, which makes mid-stream self-correcting bugs structurally invisible to a green suite. + +**Architecture:** One new MDX file under `apps/website/content/blog/`. No code changes, no new components. The website renders blog posts from frontmatter + markdown; the filename encodes date and slug. + +**Tech Stack:** MDX, Next.js (apps/website), gray-matter frontmatter, Shiki fences. + +**Source of truth:** `docs/superpowers/specs/2026-08-28-fixture-replay-post-design.md`. Read it before Task 1. + +--- + +## File Structure + +- **Create:** `apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx` — the entire deliverable. + +The filename must match `YYYY-MM-DD-.mdx`; `apps/website/src/lib/blog.ts` parses date and slug from it and throws if `title`, `description`, `date`, or `author` are missing from frontmatter. + +--- + +### Task 1: Frontmatter and skeleton + +**Files:** +- Create: `apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx` + +- [ ] **Step 1: Create the file with exactly this frontmatter** + +```mdx +--- +title: "What Fixture Replay Can't Catch" +description: 'Our agent e2e suite replaces the model and keeps everything else real. That buys determinism by deleting time — and one bug class disappears with it.' +date: 2026-08-28 +tags: [testing, langgraph, agents, streaming, angular] +author: brian +featured: false +draft: false +--- +``` + +Rules: `author` must be `brian` (the only value in use). `description` must stay under 180 characters — docs meta descriptions truncate there. Count it before moving on. + +- [ ] **Step 2: Add the H2 skeleton, in this order** + +``` +## Where do you put the mock? +## What does a fixture match on? +## What did we trade away? +## What does that hide? +## What runs alongside it? +## Conclusion +``` + +Every H2 is a question except the last, and each must be answered in its first line. + +- [ ] **Step 3: Verify frontmatter parses** + +Run: +```bash +cd apps/website && npx vitest run --config vite.config.mts src/lib/blog.spec.ts +``` +Expected: PASS. A missing required field throws at read time, so a failure here means frontmatter is malformed. + +- [ ] **Step 4: Commit** + +```bash +git add apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx +git commit -m "feat(website): scaffold the fixture-replay post" +``` + +--- + +### Task 2: Opening and the seam + +**Files:** +- Modify: `apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx` + +- [ ] **Step 1: Write the opening, before the first H2** + +Two to four lines. State the thesis outright: a deterministic harness buys determinism by deleting a dimension, ours deletes time, and the useful question about any harness is which dimension it deleted. + +Do not announce the post's structure ("first we'll look at..."). Neither sibling post does, and a reviewer flagged it on the subgraphs post. + +- [ ] **Step 2: Write "Where do you put the mock?"** + +Answer in the first line: at the model provider, not the app. + +Content, all verified — do not restate line numbers in prose: +- `libs/e2e-harness/src/global-setup-factory.ts` spawns a real `langgraph dev` subprocess with `OPENAI_BASE_URL` pointed at the mock server and `OPENAI_API_KEY: 'test-not-used'`. +- Everything above that seam is the real thing: real Angular app, real transport, real LangGraph server, real Python graph nodes. Only the model is replaced. +- Scale: 50 fixture files, 129 fixture entries, 34 apps. **Say "34 apps" or "32 cockpit capabilities and 2 example apps" — never "34 capabilities."** Two of them are examples. + +Include one fenced `typescript` block showing the seam. Use exactly this, which is faithful to the source: + +```typescript +const aimock = await startAimock({ mode: 'replay', fixturePath: opts.fixturesDir }); + +spawn('uv', ['run', 'langgraph', 'dev', '--port', String(langgraphPort)], { + env: { + ...process.env, + OPENAI_BASE_URL: aimock.baseUrl, // the only thing that isn't real + OPENAI_API_KEY: 'test-not-used', + }, +}); +``` + +- [ ] **Step 2b: Link the docs tier rather than restating it** + +One sentence pointing at [`/docs/langgraph/guides/testing`](/docs/langgraph/guides/testing) for the in-process fakes (`provideFakeAgent()`, `mockLangGraphAgent()`, `MockAgentTransport`). Do not explain those APIs — that page owns them, and restating them risks co-ranking. + +- [ ] **Step 3: Check verbatim overlap against that docs page** + +Run: +```bash +cd apps/website && python3 - <<'EOF' +import re +def prose(p): + t=open(p).read(); t=re.sub(r'```.*?```','',t,flags=re.S) + t=re.sub(r'^---.*?^---','',t,flags=re.S|re.M); t=re.sub(r'<[^>]+>','',t) + return re.findall(r"[a-z']+",t.lower()) +post=prose('content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx') +w=prose('content/docs/langgraph/guides/testing.mdx') +doc=set(tuple(w[i:i+8]) for i in range(len(w)-7)) +hits=[' '.join(post[i:i+8]) for i in range(len(post)-7) if tuple(post[i:i+8]) in doc] +print("8-gram overlap:",len(hits),hits[:3]) +EOF +``` +Expected: `8-gram overlap: 0`. Any hit means a phrase was lifted — rewrite it in your own words and re-run. + +- [ ] **Step 4: Commit** + +```bash +git add apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx +git commit -m "feat(website): opening and the seam" +``` + +--- + +### Task 3: Fixture matching and the ordering trap + +**Files:** +- Modify: `apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx` + +- [ ] **Step 1: Write "What does a fixture match on?"** + +Answer in the first line: on the shape of the request, and the order you list them decides which one wins. + +Content, verified against `cockpit/langgraph/client-tools/angular/e2e/fixtures/client-tools.json`: +- A fixture entry carries a `match` block. Discriminators include `userMessage`, plus richer ones like `toolName` and `hasToolResult` that distinguish a first call from the continuation after a tool round. +- Matching is **first-match-wins**. +- That file holds 7 entries in pairs, and in every pair the `hasToolResult: true` entry is listed **before** its plain `userMessage` twin. +- Reverse a pair and the post-tool continuation re-matches the original tool call, so the model is told to call the tool again — an infinite loop where the assistant never finalizes. + +- [ ] **Step 2: Add one fenced `json` block** + +Show the ordering, abbreviated but structurally faithful: + +```json +{ + "fixtures": [ + { "match": { "userMessage": "book a flight", "hasToolResult": true }, "response": "..." }, + { "match": { "userMessage": "book a flight" }, "response": "..." } + ] +} +``` + +Then one line: swap those two and the run never terminates. + +- [ ] **Step 3: Commit** + +```bash +git add apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx +git commit -m "feat(website): fixture matching and the ordering trap" +``` + +--- + +### Task 4: The trade, and what it hides + +**Files:** +- Modify: `apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx` + +This is the heart of the post. Give it the most care. + +- [ ] **Step 1: Write "What did we trade away?"** + +Answer in the first line: the streaming, on purpose. + +Content, verified in `libs/e2e-harness/src/aimock-runner.ts`: +- The mock is constructed with `chunkSize: 4096`, which is large enough that each response arrives in one or two SSE deltas. +- The reason is written in a comment above it: structural assertions (a code fence, a list) are meant to measure the **final** rendered DOM, not the progressive render. With default chunking the partial-markdown parser sometimes cannot recover a triple-backtick fence that gets split mid-token, and the final state degrades to an inline `` instead of a `
`.
+- Progressive behavior is covered by unit-level variance tables instead.
+
+Quote the comment in a fenced `typescript` block rather than paraphrasing it — the essay's method is "we wrote it down," so show the writing:
+
+```typescript
+// Use a large chunkSize so each response arrives in 1-2 SSE deltas. This
+// intentionally turns off the partial-markdown streaming path for harness
+// tests: structural assertions (code fence, list) measure the FINAL rendered
+// DOM, not the progressive render.
+const mock = new LLMock({ port: 0, chunkSize: 4096 });
+```
+
+Flag the opinion: this is a defensible trade, and the point is that it was made deliberately and recorded, not that it was wrong.
+
+- [ ] **Step 2: Write "What does that hide?"**
+
+Answer in the first line: bugs that exist only while the stream is open and fix themselves before it closes.
+
+Content — this is the worked example, measured on 2026-08-27:
+- The `cockpit/langgraph/subgraphs` demo runs a child graph as a plain node. Its namespace is not a `tools:` subagent namespace, so the bridge merges the child's tokens into the transcript as they arrive.
+- Without `transcriptNodeNames: ['answer']`, the child's internal research brief renders as its own chat bubble, and the message list transiently reaches three.
+- The parent's final `values` event then rewrites the message list from authoritative graph state, and the stray bubble disappears.
+- So the end state is correct. Assert on the finished DOM and the test passes. The defect is real and invisible to it.
+- Confirming the fix took sampling the DOM every 60ms against a live model across a full streaming run: the message count never exceeded two across 56 samples.
+
+Generalize in one line: any assertion that runs after `await` sees a settled system, and a self-correcting bug is exactly the kind that settles.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx
+git commit -m "feat(website): the determinism trade and the bug class it hides"
+```
+
+---
+
+### Task 5: What runs alongside, and the close
+
+**Files:**
+- Modify: `apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx`
+
+- [ ] **Step 1: Write "What runs alongside it?"**
+
+Answer in the first line: a live pass for the things replay structurally cannot see, and a drift check for the fixtures themselves.
+
+Two pieces of content:
+
+1. **The live gate.** Replay cannot see streaming re-materialization, so anything whose failure mode is mid-stream needs driving against a real model before it ships. Keep this short and concrete; do not oversell it as a formal system.
+
+2. **Drift.** `examples/chat/angular/e2e/scripts/drift.ts` re-records each committed fixture against the live provider and compares it to the committed copy, flagging any whose size diverges by more than twenty percent; the workflow opens an issue when it trips.
+
+**CRITICAL — do not claim a cadence.** Do not write "nightly", "weekly", "on a schedule", or "in CI on every run" about drift detection. Describe the mechanism only. The cron is currently not enabled. Mechanism-only phrasing is accurate now and stays accurate after it is enabled.
+
+Then be honest about what that check can prove, because it is another instance of the thesis: comparing byte size detects that a response changed *size*, not that it changed *meaning*. A model that returns something equally long and completely different passes.
+
+- [ ] **Step 2: Write the Conclusion**
+
+Restate the heuristic without repeating earlier sentences verbatim: pick your seam as far out as you can afford, then write down which dimension you deleted, because that is the list of bugs your suite cannot report.
+
+End with an invitation, matching the siblings — the json-render post closes by asking where it lands for the reader. Do not write a marketing CTA.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx
+git commit -m "feat(website): the live gate, drift, and the close"
+```
+
+---
+
+### Task 6: Validate
+
+**Files:**
+- Modify: `apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx` (only if a check fails)
+
+- [ ] **Step 1: Check the mechanical constraints**
+
+Run:
+```bash
+cd apps/website && python3 - <<'EOF'
+import re
+p='content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx'
+s=open(p).read(); body=re.sub(r'^---.*?^---','',s,flags=re.S|re.M)
+prose=re.sub(r'```.*?```','',body,flags=re.S)
+d=re.search(r"^description: '(.*)'$",s,re.M) or re.search(r'^description: "(.*)"$',s,re.M)
+print("description chars:",len(d.group(1)))
+print("words:",len(prose.split()))
+print("fences:",re.findall(r'```(\w+)',s))
+print("emoji present:",bool(re.search(r'[\U0001F300-\U0001FAFF]',s)))
+for bad in ['nightly','weekly','on a schedule','every night']:
+    if bad in prose.lower(): print("!! CADENCE CLAIM:",bad)
+EOF
+```
+Expected: description under 180; words roughly 1200-1500; fences are `typescript`, `json`, `typescript`; no emoji; no cadence claim.
+
+- [ ] **Step 2: Check overlap against the testing docs page**
+
+Re-run the overlap script from Task 2 Step 3. Expected: `8-gram overlap: 0`.
+
+- [ ] **Step 3: Render it**
+
+Run:
+```bash
+cd apps/website && npx next dev -p 3111
+```
+Then in a second shell:
+```bash
+curl -s -o /tmp/p.html -w '%{http_code}\n' http://localhost:3111/blog/what-fixture-replay-cant-catch
+grep -o '[^<]*' /tmp/p.html
+grep -c 'data-language' /tmp/p.html
+curl -s http://localhost:3111/blog | grep -c 'what-fixture-replay-cant-catch'
+```
+Expected: `200`; a `` containing the post title; a non-zero `data-language` count (fences highlighted); `1` for the listing.
+
+Note: `next dev` rewrites `apps/website/next-env.d.ts`. Run `git checkout apps/website/next-env.d.ts` before committing.
+
+- [ ] **Step 4: Run the website suite**
+
+Run:
+```bash
+cd apps/website && npx vitest run --config vite.config.mts
+```
+Expected: **10 failures, 5 files** — and confirm they are the same ones present on `main`: `thanks/page.spec.tsx` (3), `PostCard.spec.tsx`, `Differentiator.spec.tsx`, plus `api/ingest/route.spec.ts` and `lib/analytics/server.spec.ts` failing to resolve `posthog-node`.
+
+These are pre-existing and unrelated. Do not fix them in this PR. If the count differs from 10, this post broke something — investigate before continuing.
+
+`nx test website` does **not** work. Use the vitest command above.
+
+- [ ] **Step 5: Commit any fixes**
+
+```bash
+git add apps/website/content/blog/2026-08-28-what-fixture-replay-cant-catch.mdx
+git commit -m "fix(website): validation fixes for the fixture-replay post"
+```
+
+---
+
+## Hard prohibitions
+
+Carried from the spec. Violating any of these fails the task:
+
+- **No invented first-person anecdotes.** Brian's instruction: "don't make up stories." Every concrete claim traces to a file in this repo.
+- **No cadence claim about drift detection.** Mechanism only.
+- **Never write "34 capabilities."** It is 34 apps: 32 cockpit capabilities plus 2 example apps.
+- **No claim that the suite catches more than it does.** The thesis is the opposite.
+- No emoji, no hype, no marketing CTA, no "Introduction" heading, no licensing callout.
+- No line numbers in prose.
+- Do not restate the testing guide's in-process API surface; link it.
+- If you name a public API member, verify it exists in the published package, not just in `main` — releases fire only on a pushed tag and `main` runs ahead of npm.
+
+## Voice
+
+`docs/gtm/voice.md`, with the 2026 technical override. Register references: `2026-08-26-what-inject-agent-returns.mdx`, `2026-08-27-json-render-vs-a2ui-choosing.mdx`, `2026-08-27-langgraph-subgraphs-when-to-split.mdx`.
+
+- H2 as a question, answered in its first line.
+- `Let's` transitions — the siblings use about four each. Do not substitute a demonstrative tic (`That's`, `Here's`); a reviewer counted 13 `That's` in the subgraphs draft and flagged it.
+- Paragraphs of one to three lines. Put each sentence on its own source line, as the siblings do.
+- Flag opinions: "For me", "I think".
+- Italics for emphasis, never bold, in body prose.
+- Contractions throughout.

From 53118fe94251377061d5a1502c1aa366dd29381f Mon Sep 17 00:00:00 2001
From: Brian Love <brian@liveloveapp.com>
Date: Sat, 29 Aug 2026 09:14:30 -0700
Subject: [PATCH 04/10] feat(website): scaffold the fixture-replay post

---
 ...6-08-29-what-fixture-replay-cant-catch.mdx | 21 +++++++++++++++++++
 1 file changed, 21 insertions(+)
 create mode 100644 apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx

diff --git a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
new file mode 100644
index 000000000..39ffa7d2f
--- /dev/null
+++ b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
@@ -0,0 +1,21 @@
+---
+title: "What Fixture Replay Can't Catch"
+description: 'Our agent e2e suite replaces the model and keeps everything else real. That buys determinism by deleting time — and one bug class disappears with it.'
+date: 2026-08-29
+tags: [testing, langgraph, agents, streaming, angular]
+author: brian
+featured: false
+draft: false
+---
+
+## Where do you put the mock?
+
+## What does a fixture match on?
+
+## What did we trade away?
+
+## What does that hide?
+
+## What runs alongside it?
+
+## Conclusion

From 4c331d3ffceed96ebc3aa36c2063c8c7bd42ef95 Mon Sep 17 00:00:00 2001
From: Brian Love <brian@liveloveapp.com>
Date: Sat, 29 Aug 2026 09:16:31 -0700
Subject: [PATCH 05/10] feat(website): write the fixture-replay post

Body across all sections, plus two validation fixes:
- Rewrote the transcript-settling sentence, which had 6 eight-grams of
  overlap with the subgraphs docs guide (same author, two days apart).
- Restored Let's transitions and trimmed demonstrative tics.

Dated 2026-08-29 (plan said 08-28; the date rolled and siblings date to
their merge day).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 ...6-08-29-what-fixture-replay-cant-catch.mdx | 113 ++++++++++++++++++
 1 file changed, 113 insertions(+)

diff --git a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
index 39ffa7d2f..489be028f 100644
--- a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
+++ b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
@@ -8,14 +8,127 @@ featured: false
 draft: false
 ---
 
+Every deterministic test harness buys its determinism by deleting a dimension.
+Ours deletes time, deliberately, and the reason is written in the source.
+
+So the interesting question about a harness isn't whether it's green.
+It's which dimension you deleted, because that's the list of bugs it can't report.
+
 ## Where do you put the mock?
 
+At the model provider, not the app.
+
+Our end-to-end suite starts a mock OpenAI server, then spawns a real `langgraph dev` subprocess with its base URL pointed at that mock:
+
+```typescript
+const aimock = await startAimock({ mode: 'replay', fixturePath: opts.fixturesDir });
+
+spawn('uv', ['run', 'langgraph', 'dev', '--port', String(langgraphPort)], {
+  env: {
+    ...process.env,
+    OPENAI_BASE_URL: aimock.baseUrl,   // the only thing that isn't real
+    OPENAI_API_KEY: 'test-not-used',
+  },
+});
+```
+
+Everything above that line is the genuine article.
+A real Angular app, the real streaming transport, a real LangGraph server, real Python graph nodes with their real edges and real conditional routing.
+The model is the only stand-in.
+
+Let's be concrete about why that matters.
+A test that mocks the agent at the app boundary proves your component renders what you handed it. It cannot tell you that your graph's conditional edge routes correctly, that your transport merges deltas in the right order, or that a tool call round-trips.
+Push the seam out to the provider and all of that is under test, because none of it was replaced.
+
+It's also what makes the harness affordable to run broadly. We have 50 fixture files holding 129 entries across 34 apps — 32 cockpit capabilities and two example apps — all on the same harness.
+
+This is the outer tier. For in-process fakes at the unit level, the [testing guide](/docs/langgraph/guides/testing) covers `provideFakeAgent()`, `mockLangGraphAgent()`, and `MockAgentTransport`, which are a different tool for a different job.
+
 ## What does a fixture match on?
 
+The shape of the request — and the order you list them decides which one wins.
+
+Each entry carries a `match` block. The obvious discriminator is the user message, but there are richer ones, and they exist for a reason: a parent LLM's first call and its continuation after a tool round have the same user message. Something has to tell them apart.
+
+That something is `hasToolResult`, and matching is first-match-wins:
+
+```json
+{
+  "fixtures": [
+    { "match": { "userMessage": "book a flight", "hasToolResult": true }, "response": "..." },
+    { "match": { "userMessage": "book a flight" }, "response": "..." }
+  ]
+}
+```
+
+Swap those two entries and the run never terminates.
+
+The continuation arrives carrying the same user message, matches the looser entry first, and gets handed the response that asks for the tool call again.
+The model calls the tool. The result comes back. It matches the looser entry again.
+Nothing errors. The assistant simply never finalizes.
+
+For me that's the sharpest thing about fixture files: they're data, so they look inert, but the ordering is executable.
+
 ## What did we trade away?
 
+The streaming, on purpose.
+
+The mock is constructed with a chunk size large enough that every response arrives in one or two server-sent events. Here's the note that sits above it:
+
+```typescript
+// Use a large chunkSize so each response arrives in 1-2 SSE deltas. This
+// intentionally turns off the partial-markdown streaming path for harness
+// tests: structural assertions (code fence, list) measure the FINAL rendered
+// DOM, not the progressive render.
+const mock = new LLMock({ port: 0, chunkSize: 4096 });
+```
+
+The motivation was practical. Our markdown renderer parses incrementally, and with realistic chunking a triple-backtick fence sometimes gets split mid-token in a way the parser can't recover from — the final DOM ends up with an inline `<code>` where a `<pre><code>` belongs.
+
+That's a real rendering bug, but it's a *streaming* bug, and it was making structural assertions flaky for reasons that had nothing to do with what they were testing. Progressive behavior moved to unit-level variance tables instead.
+
+I think that's the right trade. What makes it defensible isn't that the flakiness went away — it's that somebody wrote down which property was being given up, in the file where it was given up.
+
 ## What does that hide?
 
+Bugs that exist only while the stream is open and fix themselves before it closes.
+
+We shipped one recently. A demo runs a child graph as a plain node, and a plain subgraph node's events aren't tagged as a delegated subagent — so the bridge merges the child's tokens into the transcript as they arrive.
+
+The child in that graph produces an internal research brief, meant for the parent to write its answer from. Without the option that whitelists which nodes count as transcript, that brief renders as its own chat bubble, and the message list transiently reaches three.
+
+Then the run settles. The parent publishes its authoritative state, the transcript is rebuilt from it, and the extra bubble vanishes.
+
+Read that sequence again from a test's point of view.
+The end state is correct. Two messages, the right two, correctly ordered.
+Assert on the finished DOM and it passes — not by luck, but reliably, every time.
+
+Confirming the fix meant sampling the DOM every 60 milliseconds against a live model for the length of a full run, and checking the count never exceeded two. It didn't, across 56 samples.
+
+Let's generalize, because this isn't specific to us.
+Any assertion that runs after an `await` sees a settled system. A self-correcting bug is precisely one that settles.
+So the class of defects a final-state suite cannot see is not random — it's exactly the ones that repair themselves, which is also the class most likely to look fine in review.
+
 ## What runs alongside it?
 
+A live pass for what replay structurally can't see, and a drift check for the fixtures themselves.
+
+The live pass is unglamorous: for anything whose failure mode is mid-stream, drive it against a real model in a real browser before it ships. That's how the bubble above got caught. There's no clever tooling in it — the point is just that the deterministic suite was never going to be the thing that found it.
+
+Drift is the other half. A recorded fixture is a claim about how a model behaved on one day. Models move. So there's a check that re-records each committed fixture against the live provider and compares it to the version on disk, flagging any whose size has diverged by more than twenty percent, and opening an issue when it trips.
+
+Let's ask the same question of that check, because the answer rhymes.
+Comparing byte size detects that a response changed *size*. It says nothing about whether it changed *meaning*.
+A model that starts returning something equally long and completely different sails through.
+
+None of which argues against the check. It argues for knowing its shape, the same way you'd want to know that your suite can't see the streaming.
+
 ## Conclusion
+
+Push your seam as far out as you can afford. The further out it goes, the more of your stack is under test rather than simulated, and provider base URL is remarkably far out for how little it costs.
+
+Then write down which dimension you deleted to make it deterministic, in the file where you deleted it. Not in a wiki. Six months later that comment is the difference between "the suite is green" and "the suite is green, and here is what green does not cover."
+
+For us that list is short and specific: anything whose failure mode is mid-stream, and any fixture whose meaning drifted without its length changing. Both have a plan. Neither is covered by the thing we run on every commit.
+
+If your agent suite is green today, I'd genuinely like to know what you think it can't see — that answer is usually more interesting than the coverage number.

From f7ab1a333c00ad0755ff4707cd4a609e94fd6bc5 Mon Sep 17 00:00:00 2001
From: Brian Love <brian@liveloveapp.com>
Date: Sat, 29 Aug 2026 09:22:05 -0700
Subject: [PATCH 06/10] fix(website): apply editorial review to the
 fixture-replay post
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Remove a self-contradiction the thesis forbids: the conclusion claimed
  'both have a plan' while the body says the drift check cannot see meaning
  drift. Only one of the two has a plan; say so.
- Answer the obvious objection (replay the recorded chunk boundaries):
  faithful boundaries make the fence failure deterministic, not absent.
- Defend the trade by displacement, not documentation — progressive
  rendering moved to the unit-level variance tables, which do test it.
- Name the drift check as the second instance of the law, earning 'every'
  in the opening line.
- Attribute affordability to replacing the model, not to the seam position.
- Link the subgraphs post as the source of the worked example; 1 link -> 4.
- Register: trim idiom, intensifiers, and doubled phrasing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 ...6-08-29-what-fixture-replay-cant-catch.mdx | 47 ++++++++++++-------
 1 file changed, 30 insertions(+), 17 deletions(-)

diff --git a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
index 489be028f..5e8373525 100644
--- a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
+++ b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
@@ -32,23 +32,24 @@ spawn('uv', ['run', 'langgraph', 'dev', '--port', String(langgraphPort)], {
 });
 ```
 
-Everything above that line is the genuine article.
-A real Angular app, the real streaming transport, a real LangGraph server, real Python graph nodes with their real edges and real conditional routing.
+Everything above that line is the real thing.
+A real Angular app, the real streaming transport, a real LangGraph server, real Python graph nodes with their edges and conditional routing.
 The model is the only stand-in.
 
-Let's be concrete about why that matters.
+Let's take the alternative.
 A test that mocks the agent at the app boundary proves your component renders what you handed it. It cannot tell you that your graph's conditional edge routes correctly, that your transport merges deltas in the right order, or that a tool call round-trips.
 Push the seam out to the provider and all of that is under test, because none of it was replaced.
 
-It's also what makes the harness affordable to run broadly. We have 50 fixture files holding 129 entries across 34 apps — 32 cockpit capabilities and two example apps — all on the same harness.
+Replacing the model — and only the model — is also what makes it cheap enough to run everywhere: no API spend, no rate limits, no coin flips.
+We have 50 fixture files holding 129 entries across 34 apps — 32 cockpit capabilities and two example apps — all on the same harness.
 
 This is the outer tier. For in-process fakes at the unit level, the [testing guide](/docs/langgraph/guides/testing) covers `provideFakeAgent()`, `mockLangGraphAgent()`, and `MockAgentTransport`, which are a different tool for a different job.
 
 ## What does a fixture match on?
 
-The shape of the request — and the order you list them decides which one wins.
+The shape of the request — and the order you list the entries decides which one wins.
 
-Each entry carries a `match` block. The obvious discriminator is the user message, but there are richer ones, and they exist for a reason: a parent LLM's first call and its continuation after a tool round have the same user message. Something has to tell them apart.
+Each entry carries a `match` block. The obvious discriminator is the user message, but there are richer ones: a parent LLM's first call and its continuation after a tool round carry the same user message. Something has to tell them apart.
 
 That something is `hasToolResult`, and matching is first-match-wins:
 
@@ -85,29 +86,38 @@ const mock = new LLMock({ port: 0, chunkSize: 4096 });
 
 The motivation was practical. Our markdown renderer parses incrementally, and with realistic chunking a triple-backtick fence sometimes gets split mid-token in a way the parser can't recover from — the final DOM ends up with an inline `<code>` where a `<pre><code>` belongs.
 
-That's a real rendering bug, but it's a *streaming* bug, and it was making structural assertions flaky for reasons that had nothing to do with what they were testing. Progressive behavior moved to unit-level variance tables instead.
+That's a real rendering bug, but it's a *streaming* bug, and it was making structural assertions flaky for reasons that had nothing to do with what they were testing.
 
-I think that's the right trade. What makes it defensible isn't that the flakiness went away — it's that somebody wrote down which property was being given up, in the file where it was given up.
+The tempting fix is to replay the recorded chunk boundaries instead of re-chunking, so you get the timing back for free.
+That doesn't buy what it looks like it buys. Faithful boundaries make the fence failure *deterministic* rather than absent — the parser bug is still there, and now every structural assertion in the suite fails for a reason none of them are about.
+The dimension gets deleted either way. This way it's deleted somewhere you can find it.
+
+I think that's the right trade, and the reason isn't that the flakiness went away — it's that the property didn't.
+Progressive rendering is still tested. It moved to a tier that can hold it still: unit-level variance tables that push partial content, then complete it, and assert on both.
+
+The comment is what makes the trade legible six months later. It isn't what makes it right.
 
 ## What does that hide?
 
 Bugs that exist only while the stream is open and fix themselves before it closes.
 
-We shipped one recently. A demo runs a child graph as a plain node, and a plain subgraph node's events aren't tagged as a delegated subagent — so the bridge merges the child's tokens into the transcript as they arrive.
+We shipped one recently and fixed it, and how we found it is the whole argument.
+
+A demo runs a child graph as a plain node — the shape [the subgraphs post](/blog/langgraph-subgraphs-when-to-split) ends on. A plain subgraph node's events aren't tagged as a delegated subagent, so the bridge merges the child's tokens into the transcript as they arrive.
 
 The child in that graph produces an internal research brief, meant for the parent to write its answer from. Without the option that whitelists which nodes count as transcript, that brief renders as its own chat bubble, and the message list transiently reaches three.
 
 Then the run settles. The parent publishes its authoritative state, the transcript is rebuilt from it, and the extra bubble vanishes.
 
 Read that sequence again from a test's point of view.
-The end state is correct. Two messages, the right two, correctly ordered.
-Assert on the finished DOM and it passes — not by luck, but reliably, every time.
+The end state is correct. Two messages, in the right order.
+Assert on the finished DOM and it passes — not by luck.
 
 Confirming the fix meant sampling the DOM every 60 milliseconds against a live model for the length of a full run, and checking the count never exceeded two. It didn't, across 56 samples.
 
 Let's generalize, because this isn't specific to us.
 Any assertion that runs after an `await` sees a settled system. A self-correcting bug is precisely one that settles.
-So the class of defects a final-state suite cannot see is not random — it's exactly the ones that repair themselves, which is also the class most likely to look fine in review.
+So the class of defects a final-state suite cannot see isn't random — it's exactly the ones that repair themselves.
 
 ## What runs alongside it?
 
@@ -117,18 +127,21 @@ The live pass is unglamorous: for anything whose failure mode is mid-stream, dri
 
 Drift is the other half. A recorded fixture is a claim about how a model behaved on one day. Models move. So there's a check that re-records each committed fixture against the live provider and compares it to the version on disk, flagging any whose size has diverged by more than twenty percent, and opening an issue when it trips.
 
-Let's ask the same question of that check, because the answer rhymes.
-Comparing byte size detects that a response changed *size*. It says nothing about whether it changed *meaning*.
+Let's ask the same question of that check.
+Comparing byte size detects that a response changed *size*. It says nothing about whether it changed *meaning* — the same trade again, one layer up. That check deletes meaning the way the harness deletes time.
 A model that starts returning something equally long and completely different sails through.
 
 None of which argues against the check. It argues for knowing its shape, the same way you'd want to know that your suite can't see the streaming.
 
 ## Conclusion
 
-Push your seam as far out as you can afford. The further out it goes, the more of your stack is under test rather than simulated, and provider base URL is remarkably far out for how little it costs.
+Push your seam as far out as you can afford. The further out it goes, the more of your stack is under test rather than simulated, and provider base URL is far out for how little it costs.
 
 Then write down which dimension you deleted to make it deterministic, in the file where you deleted it. Not in a wiki. Six months later that comment is the difference between "the suite is green" and "the suite is green, and here is what green does not cover."
 
-For us that list is short and specific: anything whose failure mode is mid-stream, and any fixture whose meaning drifted without its length changing. Both have a plan. Neither is covered by the thing we run on every commit.
+For us that list is short and specific: anything whose failure mode is mid-stream, and any fixture whose meaning drifted without its length changing.
+One has a plan — drive it live before it ships. The other is just known: nothing we run today would notice it.
+
+The [testing guide](/docs/langgraph/guides/testing) has the in-process tier, and [the subgraphs post](/blog/langgraph-subgraphs-when-to-split) has the bug that started this.
 
-If your agent suite is green today, I'd genuinely like to know what you think it can't see — that answer is usually more interesting than the coverage number.
+If your agent suite is green today, I'd like to know what you think it can't see.

From b62295fdb5fda16504c5cfcb1ff78701f0ad14a8 Mon Sep 17 00:00:00 2001
From: Brian Love <brian@liveloveapp.com>
Date: Sat, 29 Aug 2026 09:25:08 -0700
Subject: [PATCH 07/10] fix(website): correct three false claims in the
 fixture-replay post
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Spec-compliance review found the post overstated its own thesis. All
verified in source:

- chunkSize 4096 is a DEFAULT, not a law. A second harness serves the two
  example apps, and its comment says targeted streaming regressions opt
  into smaller per-fixture chunks — those fixtures set 3/4/6/23/36 with
  25-750ms latency, and there are e2e tests sampling the mid-stream DOM.
  'We deleted time' was true of the cockpit tier only. Reframed around
  which tier opted back in, which is a better question anyway.
- 'spawns a real langgraph dev subprocess' is false for 8 of 34 apps; the
  ag-ui setup spawns uvicorn. The seam generalizes, the server doesn't.
- The drift check does not run at all: it shells out to ,
  which is not a declared option under strict parseArgs, so it exits
  before comparing anything. The post said it re-records and compares.
  Now says what is true.
- Quote the full source comment rather than half of it; the omitted lines
  were what the following paragraphs presented as original analysis.
- Drop the 60ms/56-samples figures — real, but from an ad-hoc session and
  unverifiable from the repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 ...6-08-29-what-fixture-replay-cant-catch.mdx | 48 +++++++++++--------
 1 file changed, 28 insertions(+), 20 deletions(-)

diff --git a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
index 5e8373525..41e6fdd14 100644
--- a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
+++ b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
@@ -18,7 +18,7 @@ It's which dimension you deleted, because that's the list of bugs it can't repor
 
 At the model provider, not the app.
 
-Our end-to-end suite starts a mock OpenAI server, then spawns a real `langgraph dev` subprocess with its base URL pointed at that mock:
+Our end-to-end suite starts a mock OpenAI server, then spawns the real agent server with its base URL pointed at that mock — `langgraph dev` for the LangGraph apps, uvicorn for the AG-UI ones:
 
 ```typescript
 const aimock = await startAimock({ mode: 'replay', fixturePath: opts.fixturesDir });
@@ -33,7 +33,7 @@ spawn('uv', ['run', 'langgraph', 'dev', '--port', String(langgraphPort)], {
 ```
 
 Everything above that line is the real thing.
-A real Angular app, the real streaming transport, a real LangGraph server, real Python graph nodes with their edges and conditional routing.
+A real Angular app, the real streaming transport, a real Python server, real graph nodes with their edges and conditional routing.
 The model is the only stand-in.
 
 Let's take the alternative.
@@ -74,34 +74,39 @@ For me that's the sharpest thing about fixture files: they're data, so they look
 
 The streaming, on purpose.
 
-The mock is constructed with a chunk size large enough that every response arrives in one or two server-sent events. Here's the note that sits above it:
+The mock is constructed with a chunk size large enough that every response arrives in one or two server-sent events. Here's the whole note that sits above it:
 
 ```typescript
 // Use a large chunkSize so each response arrives in 1-2 SSE deltas. This
 // intentionally turns off the partial-markdown streaming path for harness
 // tests: structural assertions (code fence, list) measure the FINAL rendered
-// DOM, not the progressive render.
+// DOM, not the progressive render. With aggressive default chunking, the
+// partial-markdown parser sometimes can't recover a triple-backtick fence
+// that gets split mid-token, and the final state ends up as inline <code>
+// instead of <pre><code>. Streaming-progressive behavior is covered by the
+// Phase 1 unit-variance tables; the e2e harness is for final-state
+// invariants and cross-stack integration.
 const mock = new LLMock({ port: 0, chunkSize: 4096 });
 ```
 
-The motivation was practical. Our markdown renderer parses incrementally, and with realistic chunking a triple-backtick fence sometimes gets split mid-token in a way the parser can't recover from — the final DOM ends up with an inline `<code>` where a `<pre><code>` belongs.
+A real rendering bug, but a *streaming* one, and it was making structural assertions flaky for reasons that had nothing to do with what they asserted. So the timing went away and the property moved down a tier.
 
-That's a real rendering bug, but it's a *streaming* bug, and it was making structural assertions flaky for reasons that had nothing to do with what they were testing.
-
-The tempting fix is to replay the recorded chunk boundaries instead of re-chunking, so you get the timing back for free.
+The tempting alternative is to replay the recorded chunk boundaries instead of re-chunking, so you get the timing back for free.
 That doesn't buy what it looks like it buys. Faithful boundaries make the fence failure *deterministic* rather than absent — the parser bug is still there, and now every structural assertion in the suite fails for a reason none of them are about.
-The dimension gets deleted either way. This way it's deleted somewhere you can find it.
 
-I think that's the right trade, and the reason isn't that the flakiness went away — it's that the property didn't.
-Progressive rendering is still tested. It moved to a tier that can hold it still: unit-level variance tables that push partial content, then complete it, and assert on both.
+Now the part I'd have gotten wrong if I hadn't gone looking, and it's the useful half.
+
+That 4096 is a **default**, not a law. A second harness serves our two example apps, and its version of that comment says so outright — ordinary fixtures get the big chunk size, and *targeted streaming regressions opt into smaller per-fixture chunks*. Those fixtures set chunk sizes of three, four, six, twenty-three, thirty-six, with latencies from 25 to 750 milliseconds. There are e2e tests over there that sample the mid-stream DOM while it renders.
+
+So "we deleted time" is too tidy. What we did was delete it by default and buy it back per fixture, in the places somebody decided it was worth the cost.
 
-The comment is what makes the trade legible six months later. It isn't what makes it right.
+Which turns the question into a better one. Not *what did the harness give up*, but *which tier opted back in* — because the tier that didn't is the one flying blind.
 
 ## What does that hide?
 
 Bugs that exist only while the stream is open and fix themselves before it closes.
 
-We shipped one recently and fixed it, and how we found it is the whole argument.
+We shipped one recently and fixed it, and where it lived is the whole argument: a cockpit capability, on the harness with no per-fixture opt-in.
 
 A demo runs a child graph as a plain node — the shape [the subgraphs post](/blog/langgraph-subgraphs-when-to-split) ends on. A plain subgraph node's events aren't tagged as a delegated subagent, so the bridge merges the child's tokens into the transcript as they arrive.
 
@@ -113,7 +118,7 @@ Read that sequence again from a test's point of view.
 The end state is correct. Two messages, in the right order.
 Assert on the finished DOM and it passes — not by luck.
 
-Confirming the fix meant sampling the DOM every 60 milliseconds against a live model for the length of a full run, and checking the count never exceeded two. It didn't, across 56 samples.
+Confirming the fix meant driving it against a live model and sampling the DOM on a tight interval for the length of a full run, watching that the message count never crossed two. Not something the suite does, and not something it could tell us.
 
 Let's generalize, because this isn't specific to us.
 Any assertion that runs after an `await` sees a settled system. A self-correcting bug is precisely one that settles.
@@ -125,11 +130,12 @@ A live pass for what replay structurally can't see, and a drift check for the fi
 
 The live pass is unglamorous: for anything whose failure mode is mid-stream, drive it against a real model in a real browser before it ships. That's how the bubble above got caught. There's no clever tooling in it — the point is just that the deterministic suite was never going to be the thing that found it.
 
-Drift is the other half. A recorded fixture is a claim about how a model behaved on one day. Models move. So there's a check that re-records each committed fixture against the live provider and compares it to the version on disk, flagging any whose size has diverged by more than twenty percent, and opening an issue when it trips.
+Drift is the other half, and here I have less to show. A recorded fixture is a claim about how a model behaved on one day, and models move. We have a check scaffolded for it — re-record against the live provider, compare to the copy on disk, open an issue past a threshold — and writing this post is how I found out it doesn't run. It shells out to a recorder flag that doesn't exist, so it fails before it ever compares anything.
 
-Let's ask the same question of that check.
-Comparing byte size detects that a response changed *size*. It says nothing about whether it changed *meaning* — the same trade again, one layer up. That check deletes meaning the way the harness deletes time.
-A model that starts returning something equally long and completely different sails through.
+Let's ask the same question of it anyway, because the answer is instructive even for the version that works.
+It compares byte size. That detects a response changing *size*; it says nothing about a response changing *meaning* — the same trade again, one layer up. A model that starts returning something equally long and completely different sails straight through.
+
+So a fixture is a claim nobody is currently checking, and the checker we designed wouldn't have caught the interesting failure anyway.
 
 None of which argues against the check. It argues for knowing its shape, the same way you'd want to know that your suite can't see the streaming.
 
@@ -139,8 +145,10 @@ Push your seam as far out as you can afford. The further out it goes, the more o
 
 Then write down which dimension you deleted to make it deterministic, in the file where you deleted it. Not in a wiki. Six months later that comment is the difference between "the suite is green" and "the suite is green, and here is what green does not cover."
 
-For us that list is short and specific: anything whose failure mode is mid-stream, and any fixture whose meaning drifted without its length changing.
-One has a plan — drive it live before it ships. The other is just known: nothing we run today would notice it.
+Ours was written down, which is the only reason I could check it — and checking it is how I learned the deletion was a default two of our apps had already bought back, and that the drift check meant to cover the other gap has never once run.
+
+For us the list is short and specific: anything whose failure mode is mid-stream on a capability that never opted back into real chunking, and any fixture whose meaning drifted without its length changing.
+The first has a plan — drive it live before it ships. The second doesn't yet.
 
 The [testing guide](/docs/langgraph/guides/testing) has the in-process tier, and [the subgraphs post](/blog/langgraph-subgraphs-when-to-split) has the bug that started this.
 

From 28ac5f2073402ef84406a4fb6aa70bfb573690d6 Mon Sep 17 00:00:00 2001
From: Brian Love <brian@liveloveapp.com>
Date: Sat, 29 Aug 2026 12:22:52 -0700
Subject: [PATCH 08/10] feat(website): drift section now describes the shipped
 solution
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The check went from 'has never run' to merged and proven (#846) between
drafts. §5 now carries the real design: a @drift-tagged contract-only
subset of the e2e suite runs weekly against the live provider through the
record proxy — the assertions are the drift check, so meaning drift is
caught by construction. Includes the first run's finding: the differ
reported tool-call drift that was actually the recorder failing to parse
tool-call deltas — the instrument's own blind spot, now its own category.

Conclusion updated to match: both gaps covered by the same move, widening
is tagging specs, not new design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 ...6-08-29-what-fixture-replay-cant-catch.mdx | 22 +++++++++++--------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
index 41e6fdd14..a4ea65c5a 100644
--- a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
+++ b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
@@ -126,18 +126,22 @@ So the class of defects a final-state suite cannot see isn't random — it's exa
 
 ## What runs alongside it?
 
-A live pass for what replay structurally can't see, and a drift check for the fixtures themselves.
+A live pass for what replay structurally can't see, and a weekly drift run for the model itself.
 
 The live pass is unglamorous: for anything whose failure mode is mid-stream, drive it against a real model in a real browser before it ships. That's how the bubble above got caught. There's no clever tooling in it — the point is just that the deterministic suite was never going to be the thing that found it.
 
-Drift is the other half, and here I have less to show. A recorded fixture is a claim about how a model behaved on one day, and models move. We have a check scaffolded for it — re-record against the live provider, compare to the copy on disk, open an issue past a threshold — and writing this post is how I found out it doesn't run. It shells out to a recorder flag that doesn't exist, so it fails before it ever compares anything.
+Drift is the other half, and writing this post is what got it built properly. Our first design re-recorded fixtures and compared byte size — which detects that a response changed *size* while saying nothing about whether it changed *meaning*. The same trade again, one layer up. A model that starts returning something equally long and completely different sails straight through a size check.
 
-Let's ask the same question of it anyway, because the answer is instructive even for the version that works.
-It compares byte size. That detects a response changing *size*; it says nothing about a response changing *meaning* — the same trade again, one layer up. A model that starts returning something equally long and completely different sails straight through.
+So we threw the metric away and kept the thing we already trusted.
+The drift run takes a tagged subset of the same e2e suite — contract assertions only: a reply renders, the research dispatch surfaces a subagent card, the interrupt panel appears — and points it at the *live* provider through the mock's record-proxy. No fixtures judged, no thresholds invented.
 
-So a fixture is a claim nobody is currently checking, and the checker we designed wouldn't have caught the interesting failure anyway.
+The assertions are the drift check.
+If today's model stops calling the research tool, or the graph's prompts stop eliciting the interrupt, a spec we already believe in goes red and a weekly job opens an issue. Meaning drift is caught by construction, because the contract is what's being asserted.
 
-None of which argues against the check. It argues for knowing its shape, the same way you'd want to know that your suite can't see the streaming.
+One rule makes the subset work: a tagged assertion may depend on *shape* — an element exists, a count is non-zero — never on the content of a canned response. A spec that expects the fixture's exact words fails against a live model whether or not anything drifted, so it stays in replay where it belongs.
+
+The first run promptly demonstrated the whole thesis in miniature.
+The diagnostic differ reported that both tool-calling responses had "drifted" to plain text — while the specs proving those tools fired were green. The recorder couldn't parse tool-call deltas out of the stream and had saved empty content, flagging it "fixture may be incomplete." The check's first finding was a blind spot in its own instrument, and it now reports that case in its own category instead of as drift.
 
 ## Conclusion
 
@@ -145,10 +149,10 @@ Push your seam as far out as you can afford. The further out it goes, the more o
 
 Then write down which dimension you deleted to make it deterministic, in the file where you deleted it. Not in a wiki. Six months later that comment is the difference between "the suite is green" and "the suite is green, and here is what green does not cover."
 
-Ours was written down, which is the only reason I could check it — and checking it is how I learned the deletion was a default two of our apps had already bought back, and that the drift check meant to cover the other gap has never once run.
+Ours was written down, which is the only reason I could check it — and checking it is how I learned the deletion was a default two of our apps had already bought back, and that our first drift design was measuring the wrong thing.
 
-For us the list is short and specific: anything whose failure mode is mid-stream on a capability that never opted back into real chunking, and any fixture whose meaning drifted without its length changing.
-The first has a plan — drive it live before it ships. The second doesn't yet.
+For us the list is short and specific: anything whose failure mode is mid-stream on a capability that never opted back into real chunking, and the model itself moving under the fixtures.
+Both are covered by the same move — run the assertions you already trust against the real thing, on a schedule for drift and before shipping for streams. Widening either is tagging more specs, not designing anything new.
 
 The [testing guide](/docs/langgraph/guides/testing) has the in-process tier, and [the subgraphs post](/blog/langgraph-subgraphs-when-to-split) has the bug that started this.
 

From 114eb16f3fb4e1309cf6ed3b61b79fd119195626 Mon Sep 17 00:00:00 2001
From: Brian Love <brian@liveloveapp.com>
Date: Sat, 29 Aug 2026 14:33:35 -0700
Subject: [PATCH 09/10] fix(website): apply re-review findings to the drift
 section

- Keystone stands alone as its own paragraph; third restatement cut.
- Anecdote no longer announces itself; actor fixed (the recorder warns,
  not the check); adds that it has run clean against the live model since.
- Conclusion scoped honestly: widening drift = tagging specs plus porting
  record mode to the shared harness; widening streams = per-fixture chunk
  opt-in. Both reviews independently flagged the original overclaim.
- Tagging rule examples now match what the specs actually assert
  (prompt-derived patterns, not shape-only).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 ...26-08-29-what-fixture-replay-cant-catch.mdx | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
index a4ea65c5a..7e77214e2 100644
--- a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
+++ b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
@@ -133,15 +133,17 @@ The live pass is unglamorous: for anything whose failure mode is mid-stream, dri
 Drift is the other half, and writing this post is what got it built properly. Our first design re-recorded fixtures and compared byte size — which detects that a response changed *size* while saying nothing about whether it changed *meaning*. The same trade again, one layer up. A model that starts returning something equally long and completely different sails straight through a size check.
 
 So we threw the metric away and kept the thing we already trusted.
-The drift run takes a tagged subset of the same e2e suite — contract assertions only: a reply renders, the research dispatch surfaces a subagent card, the interrupt panel appears — and points it at the *live* provider through the mock's record-proxy. No fixtures judged, no thresholds invented.
 
 The assertions are the drift check.
-If today's model stops calling the research tool, or the graph's prompts stop eliciting the interrupt, a spec we already believe in goes red and a weekly job opens an issue. Meaning drift is caught by construction, because the contract is what's being asserted.
 
-One rule makes the subset work: a tagged assertion may depend on *shape* — an element exists, a count is non-zero — never on the content of a canned response. A spec that expects the fixture's exact words fails against a live model whether or not anything drifted, so it stays in replay where it belongs.
+The drift run takes a tagged subset of the same e2e suite — contract assertions only: a reply renders, the research dispatch surfaces a subagent card, the interrupt panel appears — and points it at the live provider through the mock's record-proxy. No fixtures judged, no thresholds invented.
+If today's model stops calling the research tool, or the graph's prompts stop eliciting the interrupt, a spec we already believe in goes red and a weekly job opens an issue. Meaning drift is caught by construction.
 
-The first run promptly demonstrated the whole thesis in miniature.
-The diagnostic differ reported that both tool-calling responses had "drifted" to plain text — while the specs proving those tools fired were green. The recorder couldn't parse tool-call deltas out of the stream and had saved empty content, flagging it "fixture may be incomplete." The check's first finding was a blind spot in its own instrument, and it now reports that case in its own category instead of as drift.
+One rule makes the subset work: a tagged assertion may depend on structure or on the prompt's own terms — an element exists, a reply to "say hi" matches `/hi/i` — never on the content of a canned response. A spec that expects the fixture's exact words fails against a live model whether or not anything drifted, so it stays in replay where it belongs.
+
+The first run flagged drift that wasn't there.
+The diagnostic differ reported that both tool-calling responses had "drifted" to plain text — while the specs proving those tools fired were green. The recorder had saved empty content because it couldn't parse tool-call deltas out of the stream, warning "fixture may be incomplete." The check's first finding was a blind spot in its own instrument; it now reports that case in its own category instead of as drift.
+It's run clean against the live model since.
 
 ## Conclusion
 
@@ -149,10 +151,12 @@ Push your seam as far out as you can afford. The further out it goes, the more o
 
 Then write down which dimension you deleted to make it deterministic, in the file where you deleted it. Not in a wiki. Six months later that comment is the difference between "the suite is green" and "the suite is green, and here is what green does not cover."
 
-Ours was written down, which is the only reason I could check it — and checking it is how I learned the deletion was a default two of our apps had already bought back, and that our first drift design was measuring the wrong thing.
+Ours was written down, which is the only reason I could check it.
+Checking it is how I learned the deletion was a default two of our apps had already bought back — and that our first drift design was measuring the wrong thing.
 
 For us the list is short and specific: anything whose failure mode is mid-stream on a capability that never opted back into real chunking, and the model itself moving under the fixtures.
-Both are covered by the same move — run the assertions you already trust against the real thing, on a schedule for drift and before shipping for streams. Widening either is tagging more specs, not designing anything new.
+Both are answered by pointing what you already trust at the real thing — the tagged specs at the live provider on a schedule, a live browser at anything mid-stream before it ships.
+Widening the drift net is tagging more specs, plus one chore: porting record mode to the harness the other thirty-two apps share. Widening the stream check is opting more fixtures into real chunking. Neither is designing anything new.
 
 The [testing guide](/docs/langgraph/guides/testing) has the in-process tier, and [the subgraphs post](/blog/langgraph-subgraphs-when-to-split) has the bug that started this.
 

From fa31790b7ae1352380290c7506096e096d4cfbdf Mon Sep 17 00:00:00 2001
From: Brian Love <brian@liveloveapp.com>
Date: Sat, 29 Aug 2026 14:51:28 -0700
Subject: [PATCH 10/10] style(website): pull the fixture-replay post back to
 Brian's register
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Soften first-person discovery narration to impersonal constructions.
  The events are real, but they were my discoveries narrated under
  Brian's byline; the claims now belong to the work, not a narrator.
- Restore the flagged opinion on the streaming trade ('I think that's
  the right trade') — the post's strongest judgment was reading as fact,
  and siblings carry 1-2 flags.
- Fix bold in body prose (italics-only convention).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 .../2026-08-29-what-fixture-replay-cant-catch.mdx    | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
index 7e77214e2..52cdd2b96 100644
--- a/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
+++ b/apps/website/content/blog/2026-08-29-what-fixture-replay-cant-catch.mdx
@@ -91,12 +91,14 @@ const mock = new LLMock({ port: 0, chunkSize: 4096 });
 
 A real rendering bug, but a *streaming* one, and it was making structural assertions flaky for reasons that had nothing to do with what they asserted. So the timing went away and the property moved down a tier.
 
+I think that's the right trade, and the reason isn't that the flakiness went away — it's that the property didn't.
+
 The tempting alternative is to replay the recorded chunk boundaries instead of re-chunking, so you get the timing back for free.
 That doesn't buy what it looks like it buys. Faithful boundaries make the fence failure *deterministic* rather than absent — the parser bug is still there, and now every structural assertion in the suite fails for a reason none of them are about.
 
-Now the part I'd have gotten wrong if I hadn't gone looking, and it's the useful half.
+Now the part that's easy to get wrong without going looking, and it's the useful half.
 
-That 4096 is a **default**, not a law. A second harness serves our two example apps, and its version of that comment says so outright — ordinary fixtures get the big chunk size, and *targeted streaming regressions opt into smaller per-fixture chunks*. Those fixtures set chunk sizes of three, four, six, twenty-three, thirty-six, with latencies from 25 to 750 milliseconds. There are e2e tests over there that sample the mid-stream DOM while it renders.
+That 4096 is a _default_, not a law. A second harness serves our two example apps, and its version of that comment says so outright — ordinary fixtures get the big chunk size, and *targeted streaming regressions opt into smaller per-fixture chunks*. Those fixtures set chunk sizes of three, four, six, twenty-three, thirty-six, with latencies from 25 to 750 milliseconds. There are e2e tests over there that sample the mid-stream DOM while it renders.
 
 So "we deleted time" is too tidy. What we did was delete it by default and buy it back per fixture, in the places somebody decided it was worth the cost.
 
@@ -130,7 +132,7 @@ A live pass for what replay structurally can't see, and a weekly drift run for t
 
 The live pass is unglamorous: for anything whose failure mode is mid-stream, drive it against a real model in a real browser before it ships. That's how the bubble above got caught. There's no clever tooling in it — the point is just that the deterministic suite was never going to be the thing that found it.
 
-Drift is the other half, and writing this post is what got it built properly. Our first design re-recorded fixtures and compared byte size — which detects that a response changed *size* while saying nothing about whether it changed *meaning*. The same trade again, one layer up. A model that starts returning something equally long and completely different sails straight through a size check.
+Drift is the other half, and it got built properly on the way to this post. Our first design re-recorded fixtures and compared byte size — which detects that a response changed *size* while saying nothing about whether it changed *meaning*. The same trade again, one layer up. A model that starts returning something equally long and completely different sails straight through a size check.
 
 So we threw the metric away and kept the thing we already trusted.
 
@@ -151,8 +153,8 @@ Push your seam as far out as you can afford. The further out it goes, the more o
 
 Then write down which dimension you deleted to make it deterministic, in the file where you deleted it. Not in a wiki. Six months later that comment is the difference between "the suite is green" and "the suite is green, and here is what green does not cover."
 
-Ours was written down, which is the only reason I could check it.
-Checking it is how I learned the deletion was a default two of our apps had already bought back — and that our first drift design was measuring the wrong thing.
+Ours was written down, which is the only reason it could be checked at all.
+Checking it surfaced two things: the deletion was a default two of our apps had already bought back, and our first drift design was measuring the wrong thing.
 
 For us the list is short and specific: anything whose failure mode is mid-stream on a capability that never opted back into real chunking, and the model itself moving under the fixtures.
 Both are answered by pointing what you already trust at the real thing — the tagged specs at the live provider on a schedule, a live browser at anything mid-stream before it ships.