Files
chemenu/tools/chemenu/tests/test_export_run.py
T
torbenandClaude Sonnet 5.5 db09e89223
CI / verify (push) Successful in 8m17s
CI / pwsh (push) Successful in 2m54s
Release / converters (push) Successful in 28s
Release / release (push) Successful in 36s
Release / wiki-mirror (push) Skipped
tools: Export - include "all" wählt die ganze KB, index: true schreibt index.md und INDEX.md in den Zielordner (#217); 10.1.0-beta.21
Files changed:
- CHANGES.md
- VERSION
- docs/how-to/export-to-a-synced-folder.md
- docs/reference/commands/exports.md
- docs/reference/configuration.md
- raw/CONTRACT.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/catalog.py
- tools/chemenu/commands/export_cmd.py
- tools/chemenu/commands/index_build.py
- tools/chemenu/config_shapes.py
- tools/chemenu/export_index.py
- tools/chemenu/guideline_export.py
- tools/chemenu/page_export.py
- tools/chemenu/tests/test_config_shapes.py
- tools/chemenu/tests/test_export_run.py
- tools/chemenu/tests/test_index_build.py

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
2026-10-16 15:44:21 +02:00

1221 lines
49 KiB
Python

"""`export run`, the `export` settings section, the `after_publish` step of
`publish`, the `doctor` line, and the digest check of `raw accept` - against a
real checkout and target folders in `tmp_path`.
The target's path carries a space on purpose: the folder a sync client keeps on
Windows is `OneDrive - <company>`."""
from __future__ import annotations
import datetime
import hashlib
import json
import os
import posixpath
import re
import shutil
import subprocess
import sys
import textwrap
from pathlib import Path
from urllib.parse import unquote
import pytest
import typer
import yaml
from typer.testing import CliRunner
from chemenu import catalog, config, config_shapes, guideline_export, page_export
from chemenu.cli import app
from chemenu.commands import doctor
from chemenu.commands.git_publish import publish_command
from chemenu.frontmatter_io import write_page
from chemenu.search.service import load_pages_by_path
from chemenu.type_resolver import resolver
runner = CliRunner()
ALPHA = """
# Alpha
See [[Beta]], [[Beta|the beta page]] and [[Gamma]] for more, also [[Beta#Scope]].
Cited[^s-one]. Inline `[[Beta]]` stays.
```
[[Beta]] and [[Gamma]]
```
<!-- wikitool:footnotes -->
## Footnotes
[^s-one]: [[Source - One]]
<!-- /wikitool:footnotes -->
"""
BETA = """
# Beta
Back to [[Alpha]].
"""
def _git(root: Path, *args: str) -> str:
result = subprocess.run(["git", *args], cwd=root, capture_output=True, text=True)
assert result.returncode == 0, result.stderr
return result.stdout.strip()
def _commit(root: Path, message: str = "change") -> str:
_git(root, "add", "-A")
_git(root, "commit", "-q", "-m", message)
return _git(root, "rev-parse", "HEAD")
def _page(root: Path, rel: str, body: str, **frontmatter) -> Path:
path = root / "kb" / rel
meta = {"type": "types/concept.md", "tags": ["docs"], "summary": f"About {path.stem}.",
"modified": "2026-10-01", "related": ["Beta"], **frontmatter}
write_page(path, meta, body)
return path
@pytest.fixture
def instance(tmp_path, monkeypatch):
root = tmp_path / "instance"
root.mkdir()
_git(root, "init", "-q", "-b", "main")
_git(root, "config", "user.name", "Test")
_git(root, "config", "user.email", "test@example.com")
(root / "incoming").mkdir()
(root / "raw").mkdir()
(root / "incoming" / ".gitkeep").write_text("", encoding="utf-8")
(root / ".gitignore").write_text(
"/incoming/*\n!/incoming/.gitkeep\n/.wikitool-local.json\n/quarantine/\n/reports/\n",
encoding="utf-8")
shutil.copytree(config._PACKAGE_ROOT / "types", root / "types")
monkeypatch.setattr(config, "TYPES_DIR", root / "types")
monkeypatch.setattr(resolver, "_repo_root", root)
_page(root, "concepts/Alpha.md", ALPHA)
_page(root, "concepts/protocols/Beta.md", BETA)
_page(root, "concepts/Gamma.md", "\n# Gamma\n\nPrivate.\n", tags=["private"])
_commit(root, "init")
(tmp_path / "OneDrive - Example").mkdir()
monkeypatch.setattr(config, "ROOT", root)
monkeypatch.setenv("WIKITOOL_SESSION_ID", "test-session")
return root
def _out(root: Path, name: str = "Pages") -> Path:
return root.parent / "OneDrive - Example" / name
def _configure(root: Path, **targets) -> None:
"""One target `pages` by default, with `docs` pages selected."""
if not targets:
targets = {"pages": {}}
section = {}
for name, extra in targets.items():
body = {"handler": "markdown", "path": str(_out(root, name)), "include": [["tags=docs"]]}
body.update(extra)
section[name] = body
commands = {body["command"]: ["unset"] for body in section.values() if body.get("command")}
export = {"targets": section, **({"commands": commands} if commands else {})}
(root / config.LOCAL_SETTINGS_FILENAME).write_text(
json.dumps({"schema": 1, "export": export}), encoding="utf-8")
def _set_commands(root: Path, commands: dict) -> None:
path = root / config.LOCAL_SETTINGS_FILENAME
data = json.loads(path.read_text(encoding="utf-8"))
data["export"]["commands"] = commands
path.write_text(json.dumps(data), encoding="utf-8")
def _run(*names: str):
return runner.invoke(app, ["export", "run", *names])
def _flat(text: str) -> str:
return " ".join(text.split())
def _snapshot(directory: Path) -> dict:
return {p.relative_to(directory).as_posix(): p.read_bytes()
for p in sorted(directory.rglob("*")) if p.is_file()}
def _mtimes(directory: Path) -> dict:
return {p.relative_to(directory).as_posix(): p.stat().st_mtime_ns
for p in sorted(directory.rglob("*")) if p.is_file()}
def _manifest(folder: Path) -> dict:
return json.loads((folder / page_export.MANIFEST_NAME).read_text(encoding="utf-8"))
# --- the section -------------------------------------------------------------
def test_no_section_is_an_error_naming_it_and_doctor_says_ok(instance):
result = _run()
assert result.exit_code == 1
assert "no export section" in _flat(result.output)
assert _check("export").status == "OK"
@pytest.mark.parametrize("target, needle", [
({"path": "relative/folder"}, "absolute"),
({"include": []}, "include"),
({"include": [[]]}, "include"),
({"include": [["not a predicate"]]}, "predicate"),
({"handler": "pdf"}, "handler"),
({"frontmatter": "everything"}, "frontmatter"),
({"links": "hyper"}, "links"),
({"after_publish": "yes"}, "after_publish"),
({"command": "convert"}, "command"),
({"handler": "command"}, "command"),
])
def test_a_broken_section_stops_run_and_doctor_fails(instance, target, needle):
_configure(instance, pages=target)
result = _run()
assert result.exit_code == 1, result.output
assert needle in _flat(result.output)
assert _check("export").status == "FAIL"
assert not _out(instance, "pages").exists()
def test_a_target_inside_the_instance_is_refused(instance):
_configure(instance, pages={"path": str(instance / "site")})
assert _run().exit_code == 1
assert _check("export").status == "FAIL"
def test_targets_lying_in_one_another_are_refused(instance):
_configure(instance, one={"path": str(_out(instance, "one"))},
two={"path": str(_out(instance, "one") / "inner")})
result = _run()
assert result.exit_code == 1
assert "lie in one another" in _flat(result.output)
def _local(root: Path) -> dict:
return json.loads((root / config.LOCAL_SETTINGS_FILENAME).read_text(encoding="utf-8"))
def _save(root: Path, data: dict) -> None:
(root / config.LOCAL_SETTINGS_FILENAME).write_text(json.dumps(data), encoding="utf-8")
def _backup_section(root: Path) -> Path:
"""A backup whose target is the shared folder itself; returns that folder."""
shared = _out(root, "Chemenu")
data = _local(root)
data["backup"] = {"target": str(shared), "machine_data": False, "machine_name": "WORK-LAPTOP"}
_save(root, data)
return shared
def _channel(root: Path, path: Path) -> None:
data = _local(root)
data["intake"] = {"channels": {"inbox": {
"handler": "folder", "path": str(path), "max_bytes": 1024, "allowed_extensions": [".md"]}}}
_save(root, data)
def test_export_intake_and_backup_may_share_one_synced_folder(instance):
shared = _out(instance, "Chemenu")
_configure(instance, pages={"path": str(shared / "Export")})
(shared / "Inbox").mkdir(parents=True)
_backup_section(instance)
_channel(instance, shared / "Inbox")
result = _run()
assert result.exit_code == 0, result.output
assert (shared / "Export" / ".wikitool-export.json").is_file()
for check in (doctor.check_export(), doctor.check_upload_intake(), doctor.check_backup()):
assert check.status != "FAIL", (check.name, check.detail)
@pytest.mark.parametrize("where", ["git", "git/inner", "WORK-LAPTOP", "WORK-LAPTOP/inner", ""])
def test_an_export_equal_to_in_or_over_a_folder_the_backup_writes_is_refused(instance, where):
shared = _out(instance, "Chemenu")
_configure(instance, pages={"path": str(shared / where) if where else str(shared)})
_backup_section(instance)
result = _run()
assert result.exit_code == 1
assert "lie in one another" in _flat(result.output)
assert "the backup's" in _flat(result.output)
assert not (shared / "git").exists()
@pytest.mark.parametrize("where", ["git", "git/inner", "WORK-LAPTOP", "WORK-LAPTOP/inner"])
def test_doctor_fails_an_intake_folder_in_a_folder_the_backup_writes(instance, where):
shared = _out(instance, "Chemenu")
_configure(instance, pages={"path": str(shared / "Export")})
_backup_section(instance)
_channel(instance, shared / where)
check = doctor.check_upload_intake()
assert check.status == "FAIL" and "the backup's" in check.detail
def test_a_target_in_an_intake_folder_is_refused(instance):
_configure(instance, pages={"path": str(_out(instance, "pages"))})
_channel(instance, _out(instance, "pages") / "in")
assert _run().exit_code == 1
_channel(instance, _out(instance, "pages").parent)
assert _run().exit_code == 1
def test_a_target_below_another_machines_folder_is_refused_before_anything_is_written(instance):
shared = _out(instance, "Chemenu")
foreign = shared / "OTHER-PC"
foreign.mkdir(parents=True)
(foreign / ".chemenu-backup.json").write_text("{}", encoding="utf-8")
_configure(instance, pages={"path": str(foreign / "deeper" / "Export")})
_backup_section(instance)
before = {p: p.read_bytes() for p in shared.rglob("*") if p.is_file()}
result = _run()
assert result.exit_code == 1
assert "backup data" in _flat(result.output)
assert {p: p.read_bytes() for p in shared.rglob("*") if p.is_file()} == before
assert not (foreign / "deeper").exists()
def test_a_target_in_a_bare_repository_is_refused(instance):
bare = _out(instance, "repo.git")
(bare / "objects").mkdir(parents=True)
(bare / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
_configure(instance, pages={"path": str(bare / "Export")})
result = _run()
assert result.exit_code == 1
assert "is a git repository" in _flat(result.output)
assert not (bare / "Export").exists()
def test_the_operator_only_fields_cannot_be_set_by_the_agent():
export = config_shapes.EXPORT
assert export.field("targets").user_only and export.field("commands").user_only
assert export.field("targets").tier == config_shapes.CHECKOUT
assert export.field("commands").tier == config_shapes.CHECKOUT
def test_an_unknown_target_name_is_an_error_and_changes_nothing(instance):
_configure(instance)
result = _run("pages", "nothing")
assert result.exit_code == 1
assert "unknown export target" in _flat(result.output)
assert not _out(instance, "pages").exists()
# --- what a run writes -------------------------------------------------------
def test_a_run_writes_the_selected_pages_with_marker_and_links(instance):
_configure(instance)
result = _run()
assert result.exit_code == 0, result.output
folder = _out(instance, "pages")
assert set(_snapshot(folder)) == {
"concepts/Alpha.md", "concepts/protocols/Beta.md", page_export.MANIFEST_NAME}
commit = _git(instance, "rev-parse", "HEAD")
alpha = (folder / "concepts" / "Alpha.md").read_text(encoding="utf-8")
lines = alpha.split("\n")
assert lines[0] == "---"
assert lines[1] == f'wikitool_export: "kind=pages instance=local commit={commit}"'
assert "title: Alpha" in alpha and "summary: About Alpha." in alpha
assert "tags" not in alpha and "related" not in alpha
assert "[Beta](protocols/Beta.md)" in alpha and "[the beta page](protocols/Beta.md)" in alpha
assert "also [Beta](protocols/Beta.md)" in alpha
assert "and Gamma for more" in alpha
assert "Cited[^s-one]." in alpha and "[^s-one]: " in alpha
assert "Inline `[[Beta]]` stays." in alpha
assert "```\n[[Beta]] and [[Gamma]]\n```" in alpha
assert alpha.endswith("\n") and not alpha.endswith("\n\n") and "\r" not in alpha
beta = (folder / "concepts" / "protocols" / "Beta.md").read_text(encoding="utf-8")
assert "[Alpha](../Alpha.md)" in beta
assert guideline_export.file_is_export(folder / "concepts" / "Alpha.md")
out = _flat(result.output)
assert "new (2)" in out and "Alpha" in out and "unchanged: 0" in out
def test_frontmatter_modes_and_links_text(instance):
_configure(instance, minimal={"frontmatter": "minimal", "links": "text"},
full={"frontmatter": "full"})
assert _run().exit_code == 0
minimal = (_out(instance, "minimal") / "concepts" / "Alpha.md").read_text(encoding="utf-8")
assert minimal.startswith('---\nwikitool_export: "kind=pages')
assert minimal.split("---\n")[1].count("\n") == 1
assert "[Beta](" not in minimal and "See Beta, the beta page and Gamma" in minimal
full = (_out(instance, "full") / "concepts" / "Alpha.md").read_text(encoding="utf-8")
assert "tags:" in full and "type: types/concept.md" in full and "related:" in full
def test_selections_are_or_and_predicates_are_and(instance):
_configure(instance, pages={"include": [["tags=docs", "summary~Alpha"], ["tags=private"]]})
assert _run().exit_code == 0
assert set(_snapshot(_out(instance, "pages"))) == {
"concepts/Alpha.md", "concepts/Gamma.md", page_export.MANIFEST_NAME}
def test_an_unknown_field_is_an_error_at_run(instance):
_configure(instance, pages={"include": [["no_such_field=x"]]})
result = _run()
assert result.exit_code == 1
assert "unknown field" in _flat(result.output)
assert not _out(instance, "pages").exists()
def test_a_rerun_on_an_unchanged_corpus_touches_nothing(instance):
_configure(instance)
assert _run().exit_code == 0
folder = _out(instance, "pages")
before, stamps = _snapshot(folder), _mtimes(folder)
result = _run()
assert result.exit_code == 0
assert _snapshot(folder) == before and _mtimes(folder) == stamps
assert "unchanged: 2" in _flat(result.output)
assert "new (" not in result.output and "changed (" not in result.output
def test_an_instance_commit_that_touches_no_selected_page_changes_nothing(instance):
_configure(instance)
assert _run().exit_code == 0
folder = _out(instance, "pages")
stamps = _mtimes(folder)
(instance / "README.md").write_text("other\n", encoding="utf-8")
_commit(instance, "unrelated")
assert _run().exit_code == 0
assert _mtimes(folder) == stamps
def _marker_commit(path: Path) -> str:
line = path.read_text(encoding="utf-8").split("\n")[1]
return line.rsplit("commit=", 1)[1].rstrip('"')
def test_a_commit_to_one_page_rewrites_that_file_alone(instance):
_configure(instance, pages={"include": [["tags=docs"], ["tags=private"]]})
assert _run().exit_code == 0
folder = _out(instance, "pages")
before, stamps = _snapshot(folder), _mtimes(folder)
_page(instance, "concepts/Alpha.md", ALPHA + "\nMore.\n")
commit = _commit(instance, "edit alpha")
result = _run()
assert result.exit_code == 0, result.output
out = _flat(result.output)
assert "changed (1): Alpha" in out and "unchanged: 2" in out and "new (" not in out
after, after_stamps = _snapshot(folder), _mtimes(folder)
for rel in ("concepts/protocols/Beta.md", "concepts/Gamma.md"):
assert after[rel] == before[rel] and after_stamps[rel] == stamps[rel]
assert _marker_commit(folder / "concepts" / "Alpha.md") == commit
def test_each_file_carries_its_own_pages_commit_and_the_manifest_the_newest(instance):
_page(instance, "concepts/Löwe [1].md", "\n# Löwe\n\nText.\n")
first = _commit(instance, "lion")
_page(instance, "concepts/protocols/Beta.md", BETA + "\nMore.\n")
newest = _commit(instance, "edit beta")
(instance / "README.md").write_text("other\n", encoding="utf-8")
_commit(instance, "unrelated")
_configure(instance)
assert _run().exit_code == 0
folder = _out(instance, "pages")
init = _git(instance, "rev-list", "--max-parents=0", "HEAD")
assert _marker_commit(folder / "concepts" / "Alpha.md") == init
assert _marker_commit(folder / "concepts" / "Löwe [1].md") == first
assert _marker_commit(folder / "concepts" / "protocols" / "Beta.md") == newest
assert _manifest(folder)["commit"] == newest
def test_a_page_entering_the_selection_rewrites_only_the_files_whose_text_changes(instance):
_configure(instance, pages={"include": [["tags=docs"], ["summary~Gamma"]]})
_page(instance, "concepts/Gamma.md", "\n# Gamma\n\nPrivate.\n", tags=["private"], summary="Hidden.")
_commit(instance, "hide gamma")
assert _run().exit_code == 0
folder = _out(instance, "pages")
before, stamps = _snapshot(folder), _mtimes(folder)
assert "and Gamma for more" in before["concepts/Alpha.md"].decode("utf-8")
_page(instance, "concepts/Gamma.md", "\n# Gamma\n\nPrivate.\n", tags=["private"])
_commit(instance, "show gamma")
result = _run()
assert result.exit_code == 0, result.output
out = _flat(result.output)
assert "new (1): Gamma" in out and "changed (1): Alpha" in out and "unchanged: 1" in out
after, after_stamps = _snapshot(folder), _mtimes(folder)
assert "[Gamma](Gamma.md)" in after["concepts/Alpha.md"].decode("utf-8")
beta = "concepts/protocols/Beta.md"
assert after[beta] == before[beta] and after_stamps[beta] == stamps[beta]
def test_a_renamed_page_carries_the_commit_of_the_rename(instance):
(instance / "README.md").write_text("other\n", encoding="utf-8")
_commit(instance, "unrelated")
result = runner.invoke(app, ["rename", "--from", "Beta", "--to", "Epsilon"])
assert result.exit_code == 0, result.output
renamed = _commit(instance, "rename")
_configure(instance)
assert _run().exit_code == 0
folder = _out(instance, "pages")
assert _marker_commit(folder / "concepts" / "protocols" / "Epsilon.md") == renamed
def test_the_git_calls_do_not_grow_with_the_pages(instance, monkeypatch):
_configure(instance)
calls = []
real = guideline_export._git
def counted(args, root, stdin=None):
calls.append(args)
return real(args, root, stdin)
monkeypatch.setattr(guideline_export, "_git", counted)
assert _run().exit_code == 0
few = len(calls)
for index in range(30):
_page(instance, f"concepts/Many{index}.md", f"\n# Many{index}\n\nText.\n")
_commit(instance, "many")
calls.clear()
result = _run()
assert result.exit_code == 0, result.output
assert "new (30)" in _flat(result.output)
assert len(calls) == few
assert sum(1 for args in calls if "log" in args) == 1
def test_a_changed_page_a_new_page_and_a_removed_page(instance):
_configure(instance)
assert _run().exit_code == 0
folder = _out(instance, "pages")
_page(instance, "concepts/Alpha.md", ALPHA + "\nMore.\n")
_page(instance, "concepts/Delta.md", "\n# Delta\n\nNew.\n")
_page(instance, "concepts/protocols/Beta.md", BETA, tags=["private"])
commit = _commit(instance, "edit")
result = _run()
assert result.exit_code == 0, result.output
out = _flat(result.output)
assert "new (1): Delta" in out and "changed (1):" in out and "removed (1):" in out
assert (folder / "concepts" / "Delta.md").is_file()
assert not (folder / "concepts" / "protocols" / "Beta.md").exists()
assert not (folder / "concepts" / "protocols").exists()
assert "More." in (folder / "concepts" / "Alpha.md").read_text(encoding="utf-8")
assert _manifest(folder)["commit"] == commit
assert [e["path"] for e in _manifest(folder)["files"]] == ["concepts/Alpha.md", "concepts/Delta.md"]
def test_the_selection_growing_needs_no_gate(instance):
_configure(instance)
assert _run().exit_code == 0
for index in range(30):
_page(instance, f"concepts/Many{index}.md", f"\n# Many{index}\n\nText.\n")
_commit(instance, "many")
result = _run()
assert result.exit_code == 0, result.output
assert "new (30)" in _flat(result.output)
def test_foreign_files_stay_and_only_created_directories_are_pruned(instance):
_configure(instance)
assert _run().exit_code == 0
folder = _out(instance, "pages")
(folder / "notes.txt").write_bytes(b"mine\r\n")
(folder / "concepts" / "mine.txt").write_bytes(b"also mine")
(folder / "concepts" / "protocols" / "mine.txt").write_bytes(b"in a created dir")
_page(instance, "concepts/protocols/Beta.md", BETA, tags=["private"])
_commit(instance, "drop beta")
assert _run().exit_code == 0
assert (folder / "notes.txt").read_bytes() == b"mine\r\n"
assert (folder / "concepts" / "mine.txt").read_bytes() == b"also mine"
assert (folder / "concepts" / "protocols" / "mine.txt").read_bytes() == b"in a created dir"
assert (folder / "concepts" / "Alpha.md").is_file()
def test_a_directory_that_existed_before_the_export_is_kept(instance):
_page(instance, "concepts/protocols/Beta.md", BETA, tags=["private"])
_commit(instance, "no beta yet")
_configure(instance)
assert _run().exit_code == 0
folder = _out(instance, "pages")
(folder / "concepts" / "protocols").mkdir()
_page(instance, "concepts/protocols/Beta.md", BETA)
_commit(instance, "beta")
assert _run().exit_code == 0
assert "concepts/protocols" not in _manifest(folder)["dirs"]
_page(instance, "concepts/protocols/Beta.md", BETA, tags=["private"])
_commit(instance, "drop beta")
assert _run().exit_code == 0
assert (folder / "concepts" / "protocols").is_dir()
def test_a_non_empty_folder_without_a_manifest_is_refused(instance):
folder = _out(instance, "pages")
folder.mkdir()
(folder / "mine.md").write_text("mine\n", encoding="utf-8")
_configure(instance)
result = _run()
assert result.exit_code == 1
assert "not empty" in _flat(result.output) and "mine.md" not in _snapshot(folder).keys() - {"mine.md"}
assert _snapshot(folder) == {"mine.md": b"mine\n"}
def test_an_empty_existing_folder_is_taken(instance):
_out(instance, "pages").mkdir()
_configure(instance)
assert _run().exit_code == 0
def test_a_file_changed_by_hand_aborts_the_target_before_any_write(instance):
_configure(instance)
assert _run().exit_code == 0
folder = _out(instance, "pages")
edited = folder / "concepts" / "protocols" / "Beta.md"
edited.write_text("my own words\n", encoding="utf-8")
_page(instance, "concepts/Alpha.md", ALPHA + "\nMore.\n")
_commit(instance, "edit")
before = _snapshot(folder)
result = _run()
assert result.exit_code == 1
assert "concepts/protocols/Beta.md" in _flat(result.output)
assert _snapshot(folder) == before
def test_a_missing_manifest_file_is_rewritten(instance):
_configure(instance)
assert _run().exit_code == 0
folder = _out(instance, "pages")
expected = _snapshot(folder)
(folder / "concepts" / "Alpha.md").unlink()
result = _run()
assert result.exit_code == 0, result.output
assert _snapshot(folder) == expected
def test_a_crash_between_the_files_and_the_manifest_is_recovered(instance):
_configure(instance)
assert _run().exit_code == 0
folder = _out(instance, "pages")
old_manifest = (folder / page_export.MANIFEST_NAME).read_bytes()
_page(instance, "concepts/Alpha.md", ALPHA + "\nMore.\n")
_commit(instance, "edit")
assert _run().exit_code == 0
new_state = _snapshot(folder)
(folder / page_export.MANIFEST_NAME).write_bytes(old_manifest)
assert _run().exit_code == 0
assert _snapshot(folder) == new_state
def test_zero_selected_pages_is_an_error_and_deletes_nothing(instance):
_configure(instance)
assert _run().exit_code == 0
folder = _out(instance, "pages")
before = _snapshot(folder)
_configure(instance, pages={"include": [["tags=nothing-has-this"]]})
result = _run()
assert result.exit_code == 1
assert "no page matches" in _flat(result.output)
assert _snapshot(folder) == before
def test_a_dirty_kb_is_refused(instance):
_configure(instance)
_page(instance, "concepts/Alpha.md", ALPHA + "\nunsaved\n")
result = _run()
assert result.exit_code == 1
assert "uncommitted" in _flat(result.output)
assert not _out(instance, "pages").exists()
def test_a_page_with_unreadable_frontmatter_stops_the_run(instance):
_configure(instance)
(instance / "kb" / "concepts" / "Broken.md").write_text("---\ntype: [unclosed\n---\nBody\n", encoding="utf-8")
_commit(instance, "broken")
result = _run()
assert result.exit_code == 1
assert "Broken.md" in result.output
assert not _out(instance, "pages").exists()
def test_targets_are_independent_and_any_failure_is_exit_1(instance):
bad = _out(instance, "bad")
bad.mkdir()
(bad / "mine.md").write_text("mine\n", encoding="utf-8")
_configure(instance, bad={}, good={})
result = _run()
assert result.exit_code == 1
assert (_out(instance, "good") / "concepts" / "Alpha.md").is_file()
assert _snapshot(bad) == {"mine.md": b"mine\n"}
assert "bad" in result.output and "failed" in result.output
def test_named_targets_run_alone(instance):
_configure(instance, one={}, two={})
assert _run("two").exit_code == 0
assert _out(instance, "two").is_dir() and not _out(instance, "one").exists()
def test_a_case_only_collision_is_refused(instance):
_page(instance, "concepts/alpha.md", "\n# alpha\n\nLower.\n")
_commit(instance, "case")
_configure(instance)
result = _run()
assert result.exit_code == 1
assert "differ only in case" in _flat(result.output)
assert not _out(instance, "pages").exists()
# --- the command handler -----------------------------------------------------
def _converter(root: Path, body: str) -> list[str]:
script = root.parent / "convert.py"
script.write_text(textwrap.dedent(body), encoding="utf-8")
return [sys.executable, str(script)]
def _command_target(root: Path, program: list[str]) -> None:
_configure(root, pages={"handler": "command", "command": "convert"})
_set_commands(root, {"convert": program})
COPY_AS_TXT = """
import pathlib, sys
src, out = map(pathlib.Path, sys.argv[1:3])
for f in src.rglob("*.md"):
dest = out / f.relative_to(src).with_suffix(".txt")
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(f.read_text(encoding="utf-8").upper(), encoding="utf-8")
"""
def test_the_command_handler_exports_what_the_program_leaves(instance):
_command_target(instance, _converter(instance, COPY_AS_TXT))
result = _run()
assert result.exit_code == 0, result.output
folder = _out(instance, "pages")
assert set(_snapshot(folder)) == {
"concepts/Alpha.txt", "concepts/protocols/Beta.txt", page_export.MANIFEST_NAME}
assert "WIKITOOL_EXPORT" in (folder / "concepts" / "Alpha.txt").read_text(encoding="utf-8")
stamps = _mtimes(folder)
assert _run().exit_code == 0 and _mtimes(folder) == stamps
def test_the_command_gets_two_directories_no_stdin_and_the_staging_is_removed(instance, tmp_path):
record = tmp_path / "record.json"
program = _converter(instance, f"""
import json, os, pathlib, sys
src, out = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
stdin = sys.stdin.read()
pathlib.Path({str(record)!r}).write_text(json.dumps({{
"args": sys.argv[1:], "stdin": stdin, "cwd": os.getcwd(),
"src_files": sorted(p.name for p in src.rglob("*.md")), "out_files": os.listdir(out)}}))
(out / "all.txt").write_text("done")
""")
_command_target(instance, program)
assert _run().exit_code == 0
seen = json.loads(record.read_text(encoding="utf-8"))
assert seen["stdin"] == "" and seen["out_files"] == [] and seen["src_files"] == ["Alpha.md", "Beta.md"]
assert Path(seen["args"][0]).name == "markdown" and Path(seen["args"][1]).name == "output"
assert not Path(seen["args"][0]).parent.exists()
def test_a_failing_command_leaves_the_target_as_it_was(instance):
_command_target(instance, _converter(instance, COPY_AS_TXT))
assert _run().exit_code == 0
folder = _out(instance, "pages")
before = _snapshot(folder)
_page(instance, "concepts/Alpha.md", ALPHA + "\nMore.\n")
_commit(instance, "edit")
_set_commands(instance, {"convert": _converter(instance, """
import sys
sys.stderr.write("x" * 5000 + "boom at the end")
sys.exit(3)
""")})
result = _run()
assert result.exit_code == 1
text = _flat(result.output)
assert "exit 3" in text and "boom at the end" in text and "x" * 3000 not in text
assert _snapshot(folder) == before
def test_a_missing_program_and_a_timeout_are_target_errors(instance, monkeypatch):
_command_target(instance, ["/nonexistent/converter"])
result = _run()
assert result.exit_code == 1 and "could not be started" in _flat(result.output)
_set_commands(instance, {"convert": _converter(instance, "import time; time.sleep(30)")})
monkeypatch.setattr(page_export, "COMMAND_TIMEOUT_SECONDS", 1)
result = _run()
assert result.exit_code == 1 and "did not finish" in _flat(result.output)
assert not _out(instance, "pages").exists()
@pytest.mark.skipif(os.name == "nt", reason="symlinks")
def test_symlinks_in_the_output_are_skipped_and_reported(instance):
_command_target(instance, _converter(instance, """
import os, pathlib, sys
out = pathlib.Path(sys.argv[2])
(out / "real.txt").write_text("real")
os.symlink("/etc/hostname", out / "link.txt")
"""))
result = _run()
assert result.exit_code == 0, result.output
assert set(_snapshot(_out(instance, "pages"))) == {"real.txt", page_export.MANIFEST_NAME}
assert "link.txt" in result.output
def test_a_converter_that_leaves_nothing_is_an_error(instance):
_command_target(instance, _converter(instance, "pass"))
assert _run().exit_code == 1
assert not _out(instance, "pages").exists()
# --- publish -----------------------------------------------------------------
def _publish(**overrides):
kwargs = dict(message="change", push=False, confirm=None, confirm_rebase=None, yes=False,
threshold=10, remote="origin", branch="main", path=None)
kwargs.update(overrides)
publish_command(**kwargs)
def test_publish_exports_after_the_commit_with_no_push_too(instance):
_configure(instance, pages={"after_publish": True}, manual={})
_page(instance, "concepts/Alpha.md", ALPHA + "\nMore.\n")
_publish()
folder = _out(instance, "pages")
head = _git(instance, "rev-parse", "HEAD")
assert _manifest(folder)["commit"] == head
assert "More." in (folder / "concepts" / "Alpha.md").read_text(encoding="utf-8")
assert not _out(instance, "manual").exists()
def test_a_failed_export_after_publish_is_a_warning_and_the_commit_stands(instance, capsys):
folder = _out(instance, "pages")
folder.mkdir()
(folder / "mine.md").write_text("mine\n", encoding="utf-8")
_configure(instance, pages={"after_publish": True})
head = _git(instance, "rev-parse", "HEAD")
_page(instance, "concepts/Alpha.md", ALPHA + "\nMore.\n")
_publish()
out = capsys.readouterr().out
assert "WARN" in out and "export run" in out
assert _git(instance, "rev-parse", "HEAD") != head
def test_publish_without_after_publish_exports_nothing(instance):
_configure(instance)
_page(instance, "concepts/Alpha.md", ALPHA + "\nMore.\n")
_publish()
assert not _out(instance, "pages").exists()
def test_a_malformed_section_stops_publish_only_when_a_target_asks_for_it(instance):
_configure(instance, pages={"after_publish": True, "include": []})
_page(instance, "concepts/Alpha.md", ALPHA + "\nMore.\n")
head = _git(instance, "rev-parse", "HEAD")
with pytest.raises(typer.Exit) as exc:
_publish()
assert exc.value.exit_code == 1 and _git(instance, "rev-parse", "HEAD") == head
_configure(instance, pages={"include": []})
_publish()
assert _git(instance, "rev-parse", "HEAD") != head
# --- doctor ------------------------------------------------------------------
def _check(name: str):
assert name == "export"
return doctor.check_export()
def test_doctor_lists_the_targets(instance):
_configure(instance, pages={"after_publish": True})
check = _check("export")
assert check.status == "OK"
assert "pages" in check.detail and "after publish" in check.detail
# --- raw accept --------------------------------------------------------------
def _accept(path: Path):
return runner.invoke(app, ["raw", "accept", str(path), "--fidelity", "verbatim",
"--authority", "normative"])
def test_raw_accept_refuses_a_file_carrying_the_page_export_marker(instance):
_configure(instance)
assert _run().exit_code == 0
exported = (_out(instance, "pages") / "concepts" / "Alpha.md").read_bytes()
(instance / "incoming" / "Alpha.md").write_bytes(exported)
result = _accept(instance / "incoming" / "Alpha.md")
assert result.exit_code == 1
assert "export" in _flat(result.output) and "nothing was moved" in _flat(result.output)
assert (instance / "incoming" / "Alpha.md").is_file()
assert not any((instance / "raw").rglob("*.md"))
def test_raw_accept_refuses_an_exported_file_whose_marker_a_converter_dropped(instance):
_command_target(instance, _converter(instance, COPY_AS_TXT))
assert _run().exit_code == 0
exported = (_out(instance, "pages") / "concepts" / "Alpha.txt").read_bytes()
folder = instance / "incoming" / "pack"
folder.mkdir()
(folder / "note.txt").write_text("ordinary\n", encoding="utf-8")
(folder / "Alpha.txt").write_bytes(exported)
(instance / "incoming" / "Alpha.txt").write_bytes(exported)
for target in (folder, instance / "incoming" / "Alpha.txt"):
before = _snapshot(instance / "incoming")
result = _accept(target)
assert result.exit_code == 1, result.output
assert "configured export target" in _flat(result.output)
assert "Alpha.txt" in result.output
assert _snapshot(instance / "incoming") == before
assert not any(p.is_file() for p in (instance / "raw").rglob("*"))
def test_raw_accept_warns_and_goes_on_when_a_manifest_is_missing(instance):
_configure(instance)
(instance / "incoming" / "note.md").write_text("# Note\n\nOrdinary.\n", encoding="utf-8")
result = _accept(instance / "incoming" / "note.md")
assert "WARN" in result.output and "no manifest" in _flat(result.output)
assert "configured export target" not in result.output
def test_raw_accept_warns_and_goes_on_when_the_section_is_broken(instance):
_configure(instance, pages={"include": []})
(instance / "incoming" / "note.md").write_text("# Note\n\nOrdinary.\n", encoding="utf-8")
result = _accept(instance / "incoming" / "note.md")
assert "WARN" in result.output and "export section is broken" in _flat(result.output)
assert "configured export target" not in result.output
def test_known_digests_lists_the_manifest_files(instance):
_configure(instance)
assert _run().exit_code == 0
digests, warnings = page_export.known_digests()
alpha = (_out(instance, "pages") / "concepts" / "Alpha.md").read_bytes()
assert hashlib.sha256(alpha).hexdigest() in digests and warnings == []
# --- the marker --------------------------------------------------------------
FRONT = b'---\nwikitool_export: "kind=pages instance=local commit=abc"\ntitle: A\n---\n\nBody\n'
@pytest.mark.parametrize("data, expected", [
(b"<!-- wikitool:export kind=guidelines -->\nbody", True),
(FRONT, True),
(b"\xef\xbb\xbf" + FRONT, True),
(FRONT.replace(b"\n", b"\r\n"), True),
(b"---\ntitle: A\n---\nwikitool_export: x\n", False),
(b"---\ntitle: A\nwikitool_export: x\n", False),
(b"# Title\n---\nwikitool_export: x\n---\n", False),
(b"---\ntitle: A\n---\n\nBody\n", False),
(b"", False),
])
def test_is_export_recognises_both_forms(data, expected):
assert guideline_export.is_export(data) is expected
def test_the_key_beyond_the_scan_window_is_not_seen():
padding = b"x: " + b"y" * 100 + b"\n"
data = b"---\n" + padding * (guideline_export.SCAN_BYTES // len(padding) + 2) + b"wikitool_export: z\n---\n"
assert guideline_export.is_export(data) is False
def test_file_is_export_reads_files(tmp_path):
(tmp_path / "a.md").write_bytes(FRONT)
(tmp_path / "b.md").write_bytes(b"plain\n")
assert guideline_export.file_is_export(tmp_path / "a.md")
assert not guideline_export.file_is_export(tmp_path / "b.md")
def test_the_marker_key_is_defined_once():
package = Path(guideline_export.__file__).parent
definitions = [
path for path in package.rglob("*.py")
if "tests" not in path.parts and "wikitool_export:" in path.read_text("utf-8")
and 'b"wikitool_export:"' in path.read_text("utf-8")
]
assert definitions == [Path(guideline_export.__file__)]
# --- include "all" and the index files -----------------------------------------
META_FILES = {
"index.md": "# Wiki Index\n", "log.md": "# Log\n", "provenance.md": "# Provenance\n",
"CONTRACT.md": "# Contract\n", "CONVENTIONS.md": "# Conventions\n",
"concepts/INDEX.md": "# concepts/ - Index\n", "concepts/COLLECTION.md": "# Concepts\n",
}
INDEX_NAMES = ("index.md", "INDEX.md")
def _all(root: Path, **extra) -> dict:
return {"include": "all", **extra}
def _add_meta_and_stray(root: Path) -> None:
for rel, text in META_FILES.items():
(root / "kb" / rel).write_text(text, encoding="utf-8")
_page(root, "Stray.md", "\n# Stray\n\nDirectly under kb.\n")
_commit(root, "meta and stray")
def _links(folder: Path, rel: str) -> list[str]:
text = (folder / rel).read_text(encoding="utf-8")
return re.findall(r"\]\(([^)]*)\)", text)
def _resolve(rel: str, link: str) -> tuple[str, str]:
path, _, fragment = link.partition("#")
return posixpath.normpath(posixpath.join(posixpath.dirname(rel), unquote(path))), unquote(fragment)
def _headings(text: str) -> set[str]:
return {catalog.anchor(m.group(1)) for m in re.finditer(r"^#+ (.+)$", text, re.MULTILINE)}
def _index_files(folder: Path) -> list[str]:
return sorted(rel for rel in _snapshot(folder) if posixpath.basename(rel) in INDEX_NAMES
and guideline_export.file_is_export(folder / rel)
and "kind=index" in (folder / rel).read_text(encoding="utf-8").split("\n")[1])
def test_all_writes_every_page_search_knows_and_no_generated_or_meta_file(instance):
_add_meta_and_stray(instance)
_configure(instance, whole=_all(instance))
result = _run()
assert result.exit_code == 0, result.output
pages = load_pages_by_path(instance / "kb", instance)
expected = {page.path.relative_to(instance / "kb").as_posix() for page in pages.values()}
assert "Stray.md" in expected and "concepts/Gamma.md" in expected
written = set(_snapshot(_out(instance, "whole")))
assert written == expected | {page_export.MANIFEST_NAME}
for meta in META_FILES:
assert meta not in written
@pytest.mark.parametrize("include", [None, [], [[]], "All", "*", "everything", "", 1, True])
def test_every_other_include_than_all_or_selections_is_an_error_and_writes_nothing(instance, include):
_configure(instance, pages={})
path = instance / config.LOCAL_SETTINGS_FILENAME
data = json.loads(path.read_text(encoding="utf-8"))
if include is None:
del data["export"]["targets"]["pages"]["include"]
else:
data["export"]["targets"]["pages"]["include"] = include
path.write_text(json.dumps(data), encoding="utf-8")
result = _run()
assert result.exit_code == 1, result.output
message = _flat(result.output)
assert '"all"' in message and "array of selections" in message
assert _check("export").status == "FAIL"
assert not _out(instance, "pages").exists()
@pytest.mark.parametrize("value", ["yes", 1, "true", None])
def test_index_must_be_a_boolean(instance, value):
_configure(instance, pages={"index": value})
result = _run()
assert result.exit_code == 1 and "`index` must be true or false" in _flat(result.output)
assert _check("export").status == "FAIL"
def test_without_index_or_with_false_no_index_file_is_written(instance):
_add_meta_and_stray(instance)
_configure(instance, absent=_all(instance), off=_all(instance, index=False))
assert _run().exit_code == 0
for name in ("absent", "off"):
assert not [rel for rel in _snapshot(_out(instance, name))
if posixpath.basename(rel) in INDEX_NAMES]
def _many_pages(root: Path, count: int) -> None:
for number in range(count):
_page(root, f"concepts/protocols/P{number:02d}.md", f"\n# P{number:02d}\n\nText.\n")
_commit(root, "many")
def test_every_link_of_every_index_file_lands_and_every_page_is_reachable(instance):
_add_meta_and_stray(instance)
_many_pages(instance, catalog.SHARD_THRESHOLD)
_page(instance, "concepts/areas/Solo.md", "\n# Solo\n\nSmall area.\n")
_commit(instance, "small area")
_configure(instance, whole=_all(instance, index=True))
assert _run().exit_code == 0
folder = _out(instance, "whole")
files = _snapshot(folder)
indexes = _index_files(folder)
assert "index.md" in indexes and "concepts/INDEX.md" in indexes
assert "concepts/protocols/INDEX.md" in indexes # over the threshold: a file of its own
assert "concepts/areas/INDEX.md" not in indexes # under it: an anchor
reached, queue = {"index.md"}, ["index.md"]
while queue:
rel = queue.pop()
for link in _links(folder, rel):
target, fragment = _resolve(rel, link)
assert target in files, f"{rel} links to {link}, which is not in the target"
if fragment:
assert fragment in _headings(files[target].decode("utf-8")), f"{rel}: no heading for #{fragment}"
if target in indexes and target not in reached:
reached.add(target)
queue.append(target)
pages = set(files) - set(indexes) - {page_export.MANIFEST_NAME}
linked = {_resolve(rel, link)[0] for rel in reached for link in _links(folder, rel)}
assert pages <= linked
root_text = files["index.md"].decode("utf-8")
assert "concepts/INDEX.md#" in root_text and "concepts/protocols/INDEX.md" in root_text
assert "## Pages outside any collection" in root_text and "(Stray.md)" in root_text
def test_the_index_names_only_what_is_exported(instance):
_page(instance, "other/Delta.md", "\n# Delta\n\nNot shared.\n", tags=["private"])
_page(instance, "concepts/areas/Hidden.md", "\n# Hidden\n\nNot shared.\n", tags=["private"])
_commit(instance, "private pages")
_configure(instance, pages={"index": True})
assert _run().exit_code == 0
folder = _out(instance, "pages")
assert not (folder / "other").exists()
for rel in _index_files(folder):
text = (folder / rel).read_text(encoding="utf-8")
assert not any(word in text for word in ("Gamma", "Delta", "Hidden", "other/", "areas")), rel
assert "Alpha" in (folder / "concepts" / "INDEX.md").read_text(encoding="utf-8")
def test_an_index_file_is_marked_and_cannot_come_back_in(instance):
_configure(instance, pages={"index": True, "frontmatter": "full"})
assert _run().exit_code == 0
folder = _out(instance, "pages")
commit = _git(instance, "rev-parse", "HEAD")
for rel in ("index.md", "concepts/INDEX.md"):
text = (folder / rel).read_text(encoding="utf-8")
assert text.startswith("---\n")
frontmatter = text.split("---\n")[1]
assert yaml.safe_load(frontmatter) == {
"wikitool_export": f"kind=index instance=local commit={commit}"}
assert guideline_export.file_is_export(folder / rel)
(instance / "incoming" / "index.md").write_bytes((folder / "index.md").read_bytes())
before = (instance / "incoming" / "index.md").read_bytes()
result = _accept(instance / "incoming" / "index.md")
assert result.exit_code == 1 and "export" in _flat(result.output)
assert (instance / "incoming" / "index.md").read_bytes() == before
assert not any((instance / "raw").rglob("*.md"))
def test_a_second_run_touches_nothing_and_no_index_holds_a_date_of_its_own(instance):
_configure(instance, pages={"index": True})
assert _run().exit_code == 0
folder = _out(instance, "pages")
stamps, before = _mtimes(folder), _snapshot(folder)
assert _run().exit_code == 0
assert _mtimes(folder) == stamps and _snapshot(folder) == before
for rel in _index_files(folder):
text = (folder / rel).read_text(encoding="utf-8")
assert datetime.date.today().isoformat() not in text.replace("2026-10-01", "")
assert "Generated" not in text and "wikitool" not in text.split("---\n", 2)[2]
def test_a_page_without_dates_or_summary_has_empty_cells_and_never_the_file_time(instance):
path = instance / "kb" / "concepts" / "Bare.md"
write_page(path, {"type": "types/concept.md", "tags": ["docs"]}, "\n# Bare\n\nNothing.\n")
_commit(instance, "bare")
os.utime(path, (86400 * 365, 86400 * 365))
_configure(instance, pages={"index": True})
assert _run().exit_code == 0
text = (_out(instance, "pages") / "concepts" / "INDEX.md").read_text(encoding="utf-8")
row = next(line for line in text.splitlines() if "[Bare]" in line)
assert row.endswith("| | |") and "1971" not in text
def test_a_commit_to_one_collection_leaves_the_index_of_another_byte_equal(instance):
_page(instance, "other/Delta.md", "\n# Delta\n\nSecond collection.\n")
_commit(instance, "second collection")
_configure(instance, whole=_all(instance, index=True))
assert _run().exit_code == 0
folder = _out(instance, "whole")
before = _snapshot(folder)
_page(instance, "concepts/Alpha.md", ALPHA + "\nMore.\n")
_commit(instance, "touch concepts only")
assert _run().exit_code == 0
after = _snapshot(folder)
assert after["other/INDEX.md"] == before["other/INDEX.md"]
assert after["other/Delta.md"] == before["other/Delta.md"]
assert after["concepts/INDEX.md"] != before["concepts/INDEX.md"]
assert after["index.md"] != before["index.md"]
def test_switching_index_off_removes_the_index_files_and_nothing_else(instance):
_configure(instance, pages={"index": True})
assert _run().exit_code == 0
folder = _out(instance, "pages")
indexes = _index_files(folder)
assert indexes
kept = {rel: data for rel, data in _snapshot(folder).items()
if rel not in indexes and rel != page_export.MANIFEST_NAME}
_configure(instance, pages={"index": False})
result = _run()
assert result.exit_code == 0, result.output
after = _snapshot(folder)
assert not [rel for rel in after if posixpath.basename(rel) in INDEX_NAMES]
assert {rel: data for rel, data in after.items() if rel != page_export.MANIFEST_NAME} == kept
assert f"removed ({len(indexes)})" in _flat(result.output)
def test_a_page_index_md_beside_the_generated_one_stops_the_target_before_any_write(instance):
_configure(instance, pages={})
assert _run().exit_code == 0
folder = _out(instance, "pages")
before = _snapshot(folder)
_page(instance, "concepts/index.md", "\n# Index\n\nA page called index.\n")
_commit(instance, "index page")
_configure(instance, pages={"index": True})
result = _run()
assert result.exit_code == 1
message = _flat(result.output)
assert "concepts/index.md" in message and "concepts/INDEX.md" in message
assert _snapshot(folder) == before
def test_a_stray_page_that_differs_from_index_md_only_in_case_stops_the_target(instance):
_page(instance, "Index.md", "\n# Index\n\nStray.\n")
_commit(instance, "stray")
_configure(instance, whole=_all(instance, index=True))
result = _run()
assert result.exit_code == 1
assert "Index.md" in _flat(result.output) and "index.md" in _flat(result.output)
assert not _out(instance, "whole").exists()
def test_with_links_text_pages_hold_no_link_to_an_exported_page_but_the_index_links(instance):
_configure(instance, pages={"index": True, "links": "text"})
assert _run().exit_code == 0
folder = _out(instance, "pages")
indexes = set(_index_files(folder))
for rel in _snapshot(folder):
if rel in indexes or rel == page_export.MANIFEST_NAME:
continue
assert not re.search(r"\]\([^)]*\.md[^)]*\)", (folder / rel).read_text(encoding="utf-8")), rel
assert "](Alpha.md)" in (folder / "concepts" / "INDEX.md").read_text(encoding="utf-8")
def test_the_command_handler_gets_the_index_files_with_the_pages(instance):
record = instance.parent / "files.json"
program = _converter(instance, f"""
import json, pathlib, sys
src, out = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
pathlib.Path({str(record)!r}).write_text(json.dumps(sorted(
p.relative_to(src).as_posix() for p in src.rglob("*") if p.is_file())))
(out / "done.txt").write_text("done")
""")
_configure(instance, pages={"handler": "command", "command": "convert", "index": True})
_set_commands(instance, {"convert": program})
assert _run().exit_code == 0
seen = json.loads(record.read_text(encoding="utf-8"))
assert "index.md" in seen and "concepts/INDEX.md" in seen and "concepts/Alpha.md" in seen
def test_the_run_names_the_index_files_by_path(instance):
_configure(instance, pages={"index": True})
out = _flat(_run().output)
assert "index.md" in out and "concepts/INDEX.md" in out