Skip to content

Test fixtures

tests/fixtures/ holds the deterministic input data for the test suite: synthetic raw API captures of representative threads (raw_entries.json / raw_blocks.json / thread.json), plus their rendered products committed as golden snapshots (conversation.md + turns/). Any rendering-layer regression breaks the byte-for-byte comparison against the goldens.

What a fixture is

Each fixture is one directory with raw captures plus a golden/ tree:

Path Role
raw_entries.json raw API capture: the thread's entries
raw_blocks.json raw API capture: workflow blocks (absent where the mode has none, e.g. search_demo, study_demo)
thread.json archived thread metadata
golden/conversation.md + golden/turns/turn_*.md committed rendered products, compared byte-for-byte

All fixtures are synthetic and deterministic:

  • Account identities — usernames / display names / emails / user_ids are shared placeholders (alice / bob, Alice Example / Bob Example, alice@example.com / bob@example.com, 00000000-0000-4000-8000-0000000000aa / 00000000-0000-4000-8000-0000000000bb); the BOT space is 00000000-0000-4000-8000-0000000000b0 / bot-EXAMPLE; read_write_token is the fixed placeholder 00000000-0000-4000-8000-0000000000ff.
  • Identifier mapping — thread / entry / asset UUIDs are uuid5-derived (fixed namespace + original value) synthetic values tagged with the 5cbeef00 prefix, consistent repo-wide: the same input always maps to the same output, preserving cross-references. toolu_ run ids likewise become fixed-length synthetic ids tagged with 5crub0.
  • User text — queries / titles / space names / Q&A payloads / file-handle paths carry generic synthetic text; CloudFront/S3 signed URLs have their signature query strings stripped.

The following generic placeholder strings are test-semantic markers and are kept intentionally: 已推送, All done, Add analysis tool shell wrapper, 这是子代理的最终结论段落, Answer skipped.

Naming

Fixture directories carry descriptive names only — they identify the mode or the defect scenario, never a thread identifier:

  • Full-thread fixtures: <mode>_demo (e.g. search_demo, council_demo).
  • Trimmed defect scenarios: scenario_<defect-scenario>.

Inventory

Full-thread snapshots (golden = rendered products, byte-for-byte)

fixture coverage
search_demo search, single turn; R code fence / inline code
deep_research_demo deep-research; raw contains \(, golden contains 26× $$ — end-to-end math-delimiter conversion
computer_demo computer, multi-turn (7 turns) workflow rendering
council_demo council model-committee rendering (~1.0 MB raw capture)
study_demo study (generic study question)

Trimmed scenario fixtures (single-entry trims; golden = corresponding turn files, turns renumbered)

Specific defect scenarios trimmed into a single entry (thread_metadata preserved, background_entries cleared — verified that none of the trimmed entries' workflow blocks carry sub-agent step dependencies, so trimming does not change the render path).

fixture coverage
scenario_computer_answer_fallback computer answer fallback: when the plain FINAL path is guard-skipped (workflow_snapshots only), the answer is recovered from the schematized workflow_block's variant=answer TEXT (parsers.wf_block_answer)
scenario_subagent_fallback sub-agent fallback: with no background match, render the sub-agent headline + the step's own items (sub-agent summary is not lost)
scenario_user_response WORKFLOW_ITEM_USER_RESPONSE Q&A rendering (4 questions / 4 answers); turn with an empty query
scenario_subagent_stub subagent_result stub turn: 10-second time-window association of unanchored background sub-agents
scenario_workflow_item_nested nested WORKFLOW_ITEM_WORKFLOW collapsed-block rendering

Interruption / cancellation scenarios (multi-entry + background_entries trims, relations intact)

Interruption semantics depend on the entry ↔ background attribution relation, so background_entries cannot be cleared; the trim is a uuid subset (at least one payload each for the anchored / stub-window / appendix classes).

fixture coverage
scenario_limit_interrupted spending-limit interruption (locked_reason=spending_limit_exceeded + AWAITING): ⏸ annotations on the workflow header / sub-agent headlines / stub-turn summaries; attribution waterfall level ③ — the unconsumed 38-step payload lands in the conversation.md appendix; anchored / stub-window payloads stay out of the appendix and are never double-rendered
scenario_canceled WORKFLOW_CANCELED: ⛔ annotation on the workflow header

Maintaining fixtures

tests/scrub_fixtures.py is the deterministic maintenance tool (tests/scrub_fixtures.py:529): positional text replacement, identifier remapping, and golden snapshot regeneration. It is idempotent and safe to re-run.

uv run python tests/scrub_fixtures.py            # apply replacements + regenerate goldens
uv run python tests/scrub_fixtures.py --check    # gate only, no writes
  • Golden regeneration (tests/scrub_fixtures.py:500-524) — each fixture's raw JSON is re-rendered in a temporary directory through the same offline rerender path used in production and by the test suite; golden/ is refreshed byte-consistently, and a turn-count mismatch aborts with an error.
  • Replacement sources live outside the repo — account identity values are read at runtime from the user-level config (~/.config/pplx-export/config.toml via pplx_export.config; see Configuration); content-class replacement pairs come from a local override mapping tests/scrub_pairs.local.json — never committed; the committed template is tests/scrub_pairs.example.json (pairs: [old, new] entries applied in order; gate: zero-tolerance substrings).
  • Deterministic remapping — remaining UUIDs → 5cbeef00-tagged uuid5 values, toolu_ ids → 5crub0-tagged fixed-length ids, read_write_token → the fixed placeholder; values already carrying a marker prefix are skipped (idempotent). Scope: positional pairs apply only inside their fixture directory; the local content pairs and the identity / token / UUID / toolu_ mappings apply globally.
  • --check gate mode — scans every file under tests/fixtures/ for the union of all replacement sources, gate patterns, and local-absolute-path / signed-URL residue, and exits non-zero on any hit without writing anything. On a re-run, a mapping whose new value is already present counts as applied.

Run it after changing a fixture's raw JSON or the renderer, and run --check before committing fixture changes. Inside the test suite, tests/conftest.py::render_fixture uses the same offline re-render path, so snapshot tests always diff fresh renderer output against the committed goldens.

See also