Skip to content

Testing

The test suite lives in tests/, outside the pplx_export package, and runs fully offline — all input data is committed under tests/fixtures/, and no test ever touches the network or a real user-level config. This page covers running the suite, its layout, and how to add tests. For the mechanism view (how snapshot regression interlocks with the renderer's idempotency promise) see Test system architecture; for the input data itself see Test fixtures.

Running the tests

uv run pytest tests

pytest is a declared dev dependency, so uv run needs no extra setup. What you can rely on:

  • Zero network — raw API captures are checked in; online code paths are exercised through offline re-rendering or tmp_path / monkeypatch constructions.
  • No real user config — an autouse fixture (tests/conftest.py:40-53) loads a placeholder account config (alice / bob, aligned with config.example.toml), so results never depend on a real ~/.config.
  • Fast — 300+ tests in about a second; the case count keeps growing as regressions are added.

Useful selections:

Command Effect
uv run pytest tests full suite
uv run pytest tests/test_units.py one file
uv run pytest tests -k snapshot only tests whose name matches snapshot
uv run pytest tests -x -q stop at the first failure, quiet output

Suite layout

The suite has three families:

  • Render snapshot teststest_render_snapshots.py: end-to-end regression from fixture raw JSON through normalize → parse → render, compared byte-for-byte against committed golden snapshots. Covers all five modes (search / deep-research / computer / council / study) plus trimmed defect scenarios.
  • Unit teststest_units.py and the topical files: class/function-level behavior of state, throttling, incremental planning, normalization, and the maintenance commands.
  • Fix-regression filestest_fix_*.py: one file per external-review finding (N-xx / V3-xx / V4-xx / V5-xx); each file's header docstring recaps the finding, and the test pins the fixed behavior.
File Coverage
conftest.py shared fixtures: render_fixture / rendered; placeholder user config
test_render_snapshots.py render snapshot regression: five-mode full-thread fixtures + trimmed defect scenarios; byte-for-byte golden comparison; dict-repr residue signature check
test_units.py BatchState (trailing-zero normalization, expired terminal state, corrupt backup), Throttle (backoff formula/cap/reset), plan_incremental, AssetDownloader._final_name, normalize_math_delims, TestDetectMode, TestSafeFolder, TestFetchMissingBlocks, TestExternalReviewFixes
test_interruptions.py interruption semantics: classify_wf_status five classes, attribution waterfall ③ appendix, render annotation, interruption registration
test_stub_workflows.py stub-turn association: match_stub_workflows (10 s window, single consumption, anchored exclusion), render_wf_item nested-rendering recursion/dedup guard
test_relations.py relations graph: same_prompt / references / subagent_of edge construction
test_search_mode_backfill.py search-mode-backfill: specificity priority, original values preserved, idempotent resume, index merge
test_sync_deleted.py sync-deleted state machine: cross-account candidate determination, terminal-state handling
test_answer_variants.py
test_answer_variant_logging.py
answer_variants detection chain and ANSWER_VARIANT_DETECTED jsonl logging
test_config_external.py user-level external config: load priority, missing-file degradation, resolve_cli_account
test_fix_n01…n11_*.py (9 files) second-round review regressions (N-01..N-12)
test_fix_v301_*.py / test_fix_v305_*.py third-round review regressions (V3-01 / V3-05)
test_fix_v4*.py fourth-round review regressions (V4-01..V4-06)
test_fix_v5_review.py fifth-round review regressions (V5-01..V5-10)

How the snapshot tests reuse the production rerender path

The snapshot tests contain no re-implemented pipeline — they call the production renderer:

  1. render_fixture (tests/conftest.py:56-68) copies the fixture's raw JSON (raw_entries.json / raw_blocks.json / thread.json) into a temporary directory and calls rerender (pplx_export/commands/rerender_cmd.py:105) — the same function the pplx-export re-render command runs in production.
  2. The rendered fixture (tests/conftest.py:71-78) is a factory: rendered("search_demo") returns (re-rendered thread dir, golden dir in fixtures). The test then diffs conversation.md and every turns/turn_*.md byte-for-byte.
  3. Because the goldens are regenerated through the same offline path (see Test fixtures), the tests always diff fresh renderer output against the committed products: any render-layer change that alters artifact bytes turns the suite red — interlocked with the re-render idempotency promise.

Beyond byte equality, test_render_snapshots.py pins content invariants: answers are never the empty placeholder (无), and a dict-repr residue signature ({'type': ... leaking into rendered text) fails the test.

Adding a test

  • Unit test for existing logic — add a test_* function to the matching topical file (or a Test* class in test_units.py). Use tmp_path / monkeypatch; never the network, never a real ~/.config (the placeholder config is autouse).
  • Regression for a bug fix — create tests/test_fix_<round><nn>_<slug>.py with a header docstring recapping the finding (old behavior → fixed behavior), then pin the fixed behavior. Construct raw JSON directly in tmp_path, as the existing test_fix_* files do.
  • Render regression — you need a fixture: add (or trim) one under tests/fixtures/, refresh its golden with the maintenance tool (see Test fixtures), then either add the directory name to FULL / SCENARIOS in test_render_snapshots.py or write a dedicated test_* function consuming the rendered fixture for scenario-specific assertions.

Match the house style: type annotations, from __future__ import annotations, and bilingual module docstrings like the neighboring files.

See also