Skip to content

testing

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

Testing

Relevant source files

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

Overview

The test suite lives under tests/ and is run with pytest (declared as the test optional dependency group, pytest>=8, in pyproject.toml). It has two distinct shapes: one large end-to-end module built around a single selftest() function ported from a former in-module self-check, and a set of smaller, narrowly targeted test_fix_*.py modules that each pin down one cubic-review regression against a specific source file.

Sources: pyproject.toml:L21-L22, tests/test_repodocs.py:L1-L1

tests/test_repodocs.py and selftest()

test_repodocs.py imports nearly every public symbol from the repodocs package (plan, render, _util, backend, citations, cli, generate, gitlinks, publish, scan, translate) and defines a single long function, selftest(), that exercises them all in sequence using plain assert statements rather than individual pytest test functions. The only pytest entry point in the file is test_repodocs(), a one-line wrapper that calls selftest().

Sources: tests/test_repodocs.py:L12-L26, tests/test_repodocs.py:L589-L590

selftest() uses two small local helpers, _raises(fn, *a) and _raises_exit(fn, *a), to assert that a callable raises ValueError or SystemExit (with a nonzero code) respectively, since bare assert cannot express "and it raised."

Sources: tests/test_repodocs.py:L28-L41

The body of selftest() walks through the pipeline roughly in the order a repo would be processed, each block wrapped in its own tempfile.TemporaryDirectory():

Area What is asserted
Planning (plan_pages, scan, scan_inventory) mandatory slugs (overview, installation, architecture, development, changelog) appear; overview is first; small files are excluded from component pages
validate_pages bad slugs, non-dict entries, and duplicate slugs are dropped; missing files fall back to README.md
parse_pages JSON fenced in ```json ``` blocks parses; non-JSON input raises
CITATION_RE matches real citation links, ignores plain markdown links
compute_file_hash known bytes hash to a known sha256
generate_decision pure skip/regenerate decision table (new/unchanged/changed/added/removed)
llm_plan idempotency unchanged inventory reuses cached plan.json; changed inventory retries the planner; a fallback plan is never cached with .plan.hash
graph_digest absent/corrupt graphify-out/graph.json yields ""; a real graph yields a compact digest naming import counts
decide_page a shared hcache memoizes file hashes across pages, even after a file mutates
build_html embeds page titles and markdown, excludes index.md, and exits via SystemExit on an empty output dir
vendor_assets failure a network OSError during asset vendoring produces a friendly die() exit, not a raw traceback
gitlinks helpers _github_slug, rewrite_citation_links (including HTML/attribute escaping of injected markup), citations_safe
group_pages deterministic nav grouping into Overview/Features/Reference/Development
translate_prompt embeds the source markdown verbatim plus the named target language
lang_labels falls back to English for unknown language codes/out-dir names
jobs_count REPODOCS_JOBS env var parsing/clamping (default 4, range 1..16)
backend selection backend_name, effective_model, backend_contract behavior across omp/claude/codex
publish staging stage_publish/stage_wiki file layout, staged_secret_findings, publish_branch_safe, symlinked-source rejection
publish_wiki_push end-to-end against a local bare git remote: clone-missing, no-op, normal push, and push-failure paths
setup_install fresh/identical/differs-kept/force-overwrite install states
security regressions safe_repo_file traversal/absolute/NUL/symlink rejection; validate_pages traversal drop; scan self-ingestion exclusion; citation_error dishonest-citation detection; citation_problems; XSS/DOMPurify wiring; THIRD_PARTY_NOTICES completeness

Sources: tests/test_repodocs.py:L44-L586

flowchart TD
    A[selftest] --> B[plan_pages / validate_pages / scan]
    A --> C[generate_decision / decide_page / compute_file_hash]
    A --> D[build_html / vendor_assets failure]
    A --> E[gitlinks: citation link rewrite + safety]
    A --> F[backend selection + effective_model]
    A --> G[publish: stage_publish / stage_wiki / publish_wiki_push]
    A --> H[security regressions: safe_repo_file, scan self-ingestion, citation_error]
    I[test_repodocs] --> A
Loading

Sources: tests/test_repodocs.py:L44-L590

Targeted test_fix_* regression modules

Alongside selftest, eight test_fix_*.py modules use ordinary pytest test functions (with fixtures like tmp_path, monkeypatch, capsys) to pin individual bugs found during cubic code review, each documented with a docstring naming the PR and the review comment it addresses.

test_fix_publish.py

Covers repodocs.publish: publish_branch_safe normalizes a refs/heads/ prefix and is case-insensitive against main/master; PUBLISH_SECRET_PATTERNS catches an ENCRYPTED PRIVATE KEY block; publish_push can be called twice against the same branch without failing on a leftover local orphan ref, and cleans up its repodocs-publish-* branches; _reject_symlink_dest and publish_wiki_push refuse a symlinked file or symlinked parent directory inside a wiki clone before copying; clone failures are classified correctly (WikiPublishError for an auth failure, WikiNotInitialized for a missing local path); cmd_publish_wiki reaches its documented Home fallback for an index-only out/ dir, and rejects a symlinked overview.md before the citation scanner ever reads through it; _publishable_subdir_mds skips a translated subdirectory that has no rendered wiki.html yet.

Sources: tests/test_fix_publish.py:L1-L28, tests/test_fix_publish.py:L50-L94, tests/test_fix_publish.py:L99-L240

test_fix_translate_cite.py

Covers repodocs.translate and repodocs.citations: localize_headings skips a ## Relevant source files heading text that appears inside a fenced code block while still translating the real H2 outside it; translate_plan_file rejects a non-list plan.json (falling back to a verbatim copy without calling the LLM); cmd_translate rejects path-like language codes (.., ., ../x, a/b, /etc) via SystemExit; propagates a plan-translation failure as a nonzero return code while still writing the fallback copy; and blocks (exit code 1) a translation that drops the Sources: citation line while allowing one that keeps it. On the citations side, requires_evidence still exempts a ## Relevant source files bullet list but flags an ordinary body bullet list; citation_problems flags an all-bullets page with no citation, rejects a citation-shaped label with no linked href, ignores a citation hidden in an HTML comment or code fence, and accepts a real rendered citation.

Sources: tests/test_fix_translate_cite.py:L1-L121, tests/test_fix_translate_cite.py:L124-L191

test_fix_generate.py

Covers repodocs.generate: _safe_page rejects a traversal slug (../evil) and prints "unsafe slug" to stderr; cmd_generate refuses to write outside out even when an unsafe slug slips past plan.py's own validation; decide_page drops an escaping candidate file (via symlink or ../ path) before hashing it; page_prompt omits the graphify-out/graph.json hint when that file is a symlink escaping the repo, but includes it for a real in-repo graph; decide_page falls back to "generate" on a corrupt .hashes.json record instead of crashing; index.md lists the full plan even when cmd_generate is invoked with --pages/only restricting which pages are (re)generated; _gitignore_notice does not raise when out == repo (an empty relative path).

Sources: tests/test_fix_generate.py:L1-L146

test_fix_plan.py

Covers repodocs.plan: graph_digest returns "" for invalid UTF-8 in graph.json instead of raising; validate_pages drops a null files field into [] instead of crashing; load_plan validates a cached plan.json, dropping a traversal slug; load_plan recovers from a corrupt (non-JSON) cache by falling back to the heuristic plan (overview first); llm_plan falls back to the heuristic plan when the LLM's plan omits a mandatory slug (e.g. installation for a repo with a README), and does not cache that fallback via .plan.hash; plan_pages allocates collision-safe component-* slugs for two directories whose slugified names collide (Foo Bar and foo-bar), producing component-foo-bar and component-foo-bar-2.

Sources: tests/test_fix_plan.py:L1-L93

test_fix_render_links.py

Covers repodocs.render, repodocs.gitlinks, and repodocs.backend: render_html HTML-escapes a malicious breadcrumb so it cannot break out of <title> or the inline <script> block (verified against the exact REPO = ... JSON literal with </ escaped), and HTML-attribute-escapes a malicious ghroot href; CDN_ASSETS entries are pinned to exact semver (no floating major version) and every CDN <script>/<link> carries a matching sha384- SRI hash from SRI, while vendored (local) assets never carry that SRI value; rewrite_citation_links escapes a malicious base URL so it cannot inject a live onmouseover attribute; _github_slug rejects HTML metacharacters in the repo name and accepts dotted/underscored/dashed names; citations_safe treats an untracked source file outside the output dir as dirty, allows untracked entries under the output dir, and defaults to dirty when no out_rel is given; the codex backend's run_llm prints a read-boundary warning ("read-only", "does NOT restrict reads", "untrusted repositories") exactly once per process.

Sources: tests/test_fix_render_links.py:L1-L133

test_fix_scan_util.py

Covers repodocs.scan and repodocs._util: a dangling symlink and (where os.mkfifo exists) a FIFO are excluded from src_files/line_counts; passing out=repo/src/generated to scan prunes only that subtree, leaving sibling directories intact; a README.md or package.json that is a symlink escaping the repo is dropped (has_readme false, manifest excluded); a CI workflow file that is an escaping symlink is dropped while a real sibling workflow is kept, and the resulting ci list is alphabetically sorted; safe_repo_file returns None on a self-referencing symlink loop (ELOOP) rather than raising.

Sources: tests/test_fix_scan_util.py:L1-L112

test_fix_citation_repair.py

Covers repair_citations in repodocs.citations: a citation that overshoots end-of-file by exactly one line (the model's "+1" off-by-one) is clamped to the file's real last line, after which citation_error reports it as valid; a citation that is already valid, one whose file does not exist, and one whose start line itself is past EOF are all left untouched by repair_citations (the last two are unrepairable and are left for citation_error/citation_problems to flag as invalid).

Sources: tests/test_fix_citation_repair.py:L1-L35

Test runner configuration

pyproject.toml declares pytest>=8 under [project.optional-dependencies] test, and configures ruff (not pytest) with a py310 target and a lint rule selection of E4, E7, E9, F — line length is intentionally not enforced. There is no [tool.pytest.ini_options] section in pyproject.toml, so pytest runs with its defaults (test discovery over the tests/ directory's test_*.py files).

Sources: pyproject.toml:L21-L22, pyproject.toml:L43-L50

Clone this wiki locally