Files
chemenu/tools/chemenu/tests/test_export_run.py
T
torbenandClaude Code 99945c4841
CI / verify (push) Successful in 9m2s
CI / pwsh (push) Successful in 2m48s
Release / release (push) Successful in 34s
Release / wiki-mirror (push) Skipped
export: export run writes selected kb/ pages as a derived, read-only version into configured target folders (markdown and command handlers, manifest, after_publish), raw accept also refuses manifest-listed files, doctor checks the section (#214); 10.0.0-beta.13
Files changed:
- CHANGES.md
- VERSION
- docs/how-to/README.md
- docs/how-to/back-up-to-a-synced-folder.md
- docs/how-to/collect-from-a-synced-folder.md
- docs/how-to/export-guidelines.md
- docs/how-to/export-to-a-synced-folder.md
- docs/reference/commands/README.md
- docs/reference/commands/exports.md
- docs/reference/commands/git.md
- docs/reference/commands/guideline-export.md
- docs/reference/commands/instance-health.md
- docs/reference/commands/raw-material-and-uploads.md
- docs/reference/configuration.md
- docs/reference/exit-codes.md
- instructions/setup-instance.md
- raw/CONTRACT.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/backup.py
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/export_cmd.py
- tools/chemenu/commands/git_publish.py
- tools/chemenu/commands/raw/_common.py
- tools/chemenu/commands/raw/accept.py
- tools/chemenu/commands/raw/capture.py
- tools/chemenu/config_shapes.py
- tools/chemenu/guideline_export.py
- tools/chemenu/page_export.py
- tools/chemenu/reference_doc.py
- tools/chemenu/repo_capture.py
- tools/chemenu/tests/test_config_shapes.py
- tools/chemenu/tests/test_export_run.py

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
2026-10-13 05:53:48 +02:00

765 lines
29 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 hashlib
import json
import os
import shutil
import subprocess
import sys
import textwrap
from pathlib import Path
import pytest
import typer
from typer.testing import CliRunner
from chemenu import 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.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 test_a_target_in_the_backup_target_or_an_intake_folder_is_refused(instance):
backup_target = _out(instance, "backup")
path = instance / config.LOCAL_SETTINGS_FILENAME
_configure(instance, pages={"path": str(backup_target / "pages")})
data = json.loads(path.read_text(encoding="utf-8"))
data["backup"] = {"target": str(backup_target)}
path.write_text(json.dumps(data), encoding="utf-8")
assert _run().exit_code == 1
data.pop("backup")
data["intake"] = {"channels": {"inbox": {
"handler": "folder", "path": str(_out(instance, "pages") / "in"), "max_bytes": 1024,
"allowed_extensions": [".md"]}}}
data["export"]["targets"]["pages"]["path"] = str(_out(instance, "pages"))
path.write_text(json.dumps(data), encoding="utf-8")
assert _run().exit_code == 1
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 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__)]