Skip to content

testing

Ary Rabelo edited this page Jul 22, 2026 · 3 revisions

Now I have enough context to write the page.

Testing

Relevant source files

  • tests/test_repodocs.py
  • tests/test_fix_publish.py
  • tests/test_fix_generate.py
  • tests/test_fix_plan.py
  • tests/test_fix_diagrams.py
  • tests/test_fix_scan_util.py
  • tests/test_fix_render_links.py
  • tests/test_fix_translate_cite.py
  • tests/test_fix_citation_repair.py

Overview

The test suite lives entirely under tests/ and is run with pytest. It is split into two styles: a single large end-to-end selftest() function (tests/test_repodocs.py) that exercises the whole pipeline through temporary repos, and a set of test_fix_*.py files that each hold focused pytest-style regression tests tied to specific code-review fixes (mostly from PR #2).

Sources: tests/test_repodocs.py:L1-L27, tests/test_fix_publish.py:L1-L14

test_repodocs.py and selftest()

selftest() is the ported former in-module selftest of repodocs. It is invoked by the single pytest entry point test_repodocs() at the bottom of the file:

def test_repodocs():
    selftest()

Sources: tests/test_repodocs.py:L589-L590

It runs as one long sequence of assertions against temporary directories (tempfile.TemporaryDirectory()), rather than many small pytest test functions, and imports directly from the repodocs package modules (plan, render, _util, backend, citations, cli, generate, gitlinks, publish, scan, translate). Two small local helpers, _raises and _raises_exit, assert that a callable raises ValueError or exits with a non-zero SystemExit code respectively.

Sources: tests/test_repodocs.py:L12-L41

selftest() covers, in order:

Area What it asserts
plan_pages / scan / scan_inventory mandatory slugs (overview, installation, architecture, development, changelog) appear; overview is first; a component-* page exists; VERSION matches semver
validate_pages bad slugs, non-dict entries, and duplicate slugs are dropped/deduped; missing candidate files fall back to README.md
parse_pages extracts a JSON array from a fenced code block, raises on unparseable text
component-* page thresholds large files produce a component page, small ones don't
CITATION_RE matches [path:Lx-Ly](path#Lx-Ly) citation links, not plain markdown links
compute_file_hash known bytes hash to a known sha256
generate_decision pure generate/skip decision logic (new, unchanged, changed, added, removed)
llm_plan idempotency skip on matching fingerprint, replan on inventory change, no cache written on planner fallback, retried on next run
graph_digest empty digest with no graphify-out/graph.json or corrupt JSON; compact digest with node/edge counts otherwise
decide_page memoizes file hashes across pages via a shared hcache dict
build_html embeds page titles/markdown, excludes index.md, dies cleanly (SystemExit) on an empty dir or network failure in vendor_assets
_github_slug / rewrite_citation_links GitHub remote-URL parsing and citation-to-blob-URL rewriting, including HTML/attribute escaping of injected content
citations_safe untracked (??) files under the output dir are harmless; anything else untracked, or a dirty tree, is unsafe
group_pages deterministic nav grouping into Overview/Features/Reference/Development
translate_prompt / lang_labels prompt preserves source markdown verbatim and names the target language; label lookup falls back to English
jobs_count REPODOCS_JOBS parsing/clamping (default 4, range 1–16, invalid input falls back to 4)
backend_name / effective_model / backend_contract backend selection from REPODOCS_BACKEND, model resolution/override via REPODOCS_MODEL, and planner/writer contract text selection
stage_publish / staged_secret_findings / publish_branch_safe staging layout (index.html, .nojekyll, translated subdirs), secret scanning, protected-branch rejection
stage_wiki Home.md mapping, _Sidebar.md ordering/titles, absolute citation rewriting, index.md exclusion, index.md-as-Home fallback
Symlink rejection stage_publish/stage_wiki reject symlinked source files before any read or copy
wiki_remote_url / publish_wiki_push wiki remote URL derivation; full clone/commit/push orchestration against a local bare git remote covering WikiNotInitialized, no-op push, normal push, and push failure
setup_install fresh install, up-to-date, local-diverges-kept, --force overwrite
safe_repo_file rejects traversal, absolute paths, NUL bytes, symlink escapes; accepts real in-repo files
validate_pages traversal a ../../../etc/passwd candidate is dropped, falling back to README.md
scan self-ingestion repo-docs/, graphify-out/, and any configured out dir never leak into src_files; symlink escapes dropped
citation_error valid citation passes; range mismatch, path mismatch, zero start, reversed range, beyond-EOF, missing file, traversal, and malformed href are all caught
requires_evidence / citation_problems prose needs a citation; a heading + file list alone does not; unsourced or invalid-citation pages block, index.md is excluded
XSS wiring built HTML viewer sanitizes markdown via DOMPurify.sanitize(marked.parse(...)), fails closed if the sanitizer fails to load, loads purify.min.js before use, and runs Mermaid in "strict" security mode
THIRD_PARTY_NOTICES names every vendored library (marked, Mermaid, highlight.js, DOMPurify) and its SPDX license identifier

Every temporary fixture is created with tempfile.TemporaryDirectory() and torn down automatically; no test in this file touches the real filesystem outside its own tempdir except through explicit subprocess calls to git inside another tempdir.

Sources: tests/test_repodocs.py:L396-L468

The test_fix_*.py regression files

Each test_fix_*.py module targets a specific repodocs submodule and documents, in its module docstring, which review comments or PR it addresses. Unlike test_repodocs.py, these use ordinary pytest fixtures (tmp_path, monkeypatch, capsys, pytest.raises) and one assertion (or a few) per test function.

test_fix_publish.pyrepodocs.publish

Covers protected-branch normalization (publish_branch_safe treats refs/heads/main/MAIN as unsafe), an ENCRYPTED PRIVATE KEY gap in PUBLISH_SECRET_PATTERNS, repeat-publish to the same branch not colliding with a leftover orphan ref, symlink rejection in _reject_symlink_dest (both file and parent-dir symlinks) before publish_wiki_push copies anything, clone-failure classification (WikiNotInitialized vs. WikiPublishError), an index-only wiki reaching Home.md via cmd_publish_wiki, citation scanning happening only after symlink rejection in staging, and _publishable_subdir_mds skipping a translated subdir that has no rendered wiki.html yet.

Sources: tests/test_fix_publish.py:L1-L27, tests/test_fix_publish.py:L48-L240

test_fix_generate.pyrepodocs.generate

Covers _safe_page rejecting a traversal slug (../evil) and cmd_generate refusing to write outside out even if an unsafe slug slips past plan.py's own validation, decide_page skipping a candidate file that escapes the repo (via symlink or ../) before hashing it, page_prompt omitting the graphify-out/graph.json hint when that file is a symlink escaping the repo (but including it for a real in-repo graph), decide_page falling back to "generate" on a corrupt .hashes.json record instead of crashing, index.md listing the full plan even when --pages filters generation to a subset, and _gitignore_notice not raising when out == repo.

Sources: tests/test_fix_generate.py:L1-L8, tests/test_fix_generate.py:L17-L146

test_fix_plan.pyrepodocs.plan

Covers graph_digest surviving invalid UTF-8 in graph.json, validate_pages handling files: null instead of crashing, load_plan re-validating a cached plan.json (rejecting a traversal slug) and recovering from a corrupt cache, llm_plan falling back to the heuristic planner (and not caching a fingerprint) when the LLM's plan is missing the mandatory installation slug, and plan_pages generating collision-safe component-* slugs for directory names that normalize to the same slug (Foo Bar vs. foo-bar).

Sources: tests/test_fix_plan.py:L1-L5, tests/test_fix_plan.py:L12-L93

test_fix_diagrams.pyrepodocs.diagrams

Covers process_page swapping a rendered mermaid block for an image embed on success, keeping the block verbatim on render failure, numbering multiple blocks in one page, and _poster_yaml correctly indenting mermaid source and numbering "DIAGRAM n/total" for multi-diagram pages. The Bun renderer is stubbed via monkeypatch.setattr(dg, "_render_png", ...) so no Bun/Playwright process is spawned.

Sources: tests/test_fix_diagrams.py:L1-L46

test_fix_scan_util.pyrepodocs.scan / repodocs._util

Covers dangling symlinks and FIFOs being excluded from src_files, a nested --out directory pruning only its own subtree, escaping symlinks for README.md, manifest files, and CI workflow files being dropped, CI workflow lists being sorted, and safe_repo_file returning None on a self-referencing symlink loop (ELOOP).

Sources: tests/test_fix_scan_util.py:L1-L4, tests/test_fix_scan_util.py:L13-L112

test_fix_render_links.pyrepodocs.render / repodocs.gitlinks / repodocs.backend

Covers render_html HTML-escaping a malicious breadcrumb inside <title> and inside the inline REPO = ... script assignment (with </ escaped to <\/), escaping a malicious ghroot href/text, CDN_ASSETS being pinned to exact semver with sha384- SRI hashes wired into <script integrity=...>/<link integrity=...> tags (and vendored local assets omitting that SRI hash), rewrite_citation_links escaping a malicious base URL rather than injecting a live attribute, _github_slug rejecting HTML metacharacters in a repo name, citations_safe rejecting an untracked source file outside the output dir, and the codex backend emitting its "read-only... does NOT restrict reads... untrusted repositories" warning exactly once per process.

Sources: tests/test_fix_render_links.py:L1-L11, tests/test_fix_render_links.py:L15-L133

test_fix_translate_cite.pyrepodocs.translate / repodocs.citations

Covers localize_headings skipping a heading inside a fenced code block while translating a real ## Relevant source files heading, translate_plan_file falling back (without calling the LLM) on a non-list plan.json, cmd_translate rejecting path-like language codes (.., ., ../x, a/b, /etc) with SystemExit(1), propagating a plan-translation failure as a nonzero return code while still writing the fallback copy, and blocking (vs. allowing) a translated page depending on whether it dropped the Sources: citation line. The citations half covers requires_evidence exempting the ## Relevant source files bullet list but flagging any other bare bullet list, and citation_problems flagging pages with no citation, rejecting a citation-shaped label with no linked href, ignoring citations hidden inside HTML comments or code fences, and accepting a real rendered citation.

Sources: tests/test_fix_translate_cite.py:L1-L10, tests/test_fix_translate_cite.py:L14-L191

test_fix_citation_repair.pyrepodocs.citations.repair_citations

Covers repair_citations clamping an end-of-file line-range overshoot (e.g. a 5-line file cited to L6) down to the real file length, leaving an already-valid citation untouched, leaving a hallucinated missing-file citation untouched, leaving a citation whose start line is already past EOF untouched (not a clamp case), resolving a dropped path prefix via a unique tracked git suffix match (e.g. bare mod.py resolving to src/pkg/mod.py), combining prefix resolution with overshoot clamping in one pass, adding a missing L prefix to a range label, normalizing a single-line citation (L3) to a L3-L3 range, and leaving a citation alone when the label path and href path disagree (ambiguous — not guessed).

Sources: tests/test_fix_citation_repair.py:L1-L3, tests/test_fix_citation_repair.py:L9-L74

Running the tests

All test modules are plain pytest files under tests/; none define a custom runner or if __name__ == "__main__" block. They are run with:

pytest

or, to target a single module:

pytest tests/test_repodocs.py

test_repodocs.py's single test_repodocs() entry point runs the entire selftest() sequence; a failure anywhere inside it surfaces as a single failing test with an AssertionError at the specific failing assert line. The test_fix_*.py files instead expose one pytest test function per regression case, so pytest -k <name> can target an individual fix.

Sources: tests/test_repodocs.py:L44-L590

Tests that need a real git repository (publish/wiki push paths) create one in a tmp_path/tempfile.TemporaryDirectory() and set GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL/GIT_COMMITTER_NAME/GIT_COMMITTER_EMAIL via monkeypatch.setenv (or by mutating os.environ directly in test_repodocs.py) so commits do not depend on the machine's global git configuration.

Sources: tests/test_fix_publish.py:L36-L46, tests/test_repodocs.py:L396-L402

Network- and subprocess-dependent code paths (LLM backends, Bun-based diagram rendering, git clone/push) are stubbed with monkeypatch.setattr rather than exercised for real: plan_mod.run_llm, render_mod.vendor_assets, dg._render_png, translate_mod.run_llm/parallel_llm, gen.parallel_llm, and publish_mod.subprocess.run/tempfile.mkdtemp are all replaced with fakes in the relevant tests.

Sources: tests/test_repodocs.py:L112-L127, tests/test_repodocs.py:L184-L191, tests/test_fix_diagrams.py:L8-L17, tests/test_fix_generate.py:L10-L14, tests/test_fix_publish.py:L126-L130

Clone this wiki locally