#!/usr/bin/env python3 """ How a directory of markdown becomes a page tree, and that the tree survives a round trip through the wiki layer's bookkeeping. python3 -m unittest discover -s tests -v Stdlib unittest, no third-party anything. `skills/*/scripts/` are not packages, so the modules under test are imported by path. Nothing here touches tmp/wiki/. The subprocess cases build a throwaway repository in a temp directory — a `.git` marker, a copy of both script layers, a directory of fixture artifacts — and run the real scripts inside it. That is the only honest way to test behaviour that depends on where a script is run from, and it keeps the developer's own cache out of the blast radius. """ import json import os import shutil import subprocess import sys import tempfile import unittest REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PAGE_SCRIPTS = os.path.join(REPO, "skills", "page", "scripts") WIKI_SCRIPTS = os.path.join(REPO, "skills", "wiki", "scripts") SYNC_SCRIPTS = os.path.join(REPO, "skills", "sync", "scripts") sys.path.insert(0, PAGE_SCRIPTS) sys.path.insert(0, WIKI_SCRIPTS) import page # noqa: E402 import wikimap # noqa: E402 # The fixture mirrors the shape a real discussion leaves behind: numbered files # for ordering, a `00-` file standing in for its directory, headings that no # mechanical rule could derive from the file names. FIXTURE = { "handoff.md": "# handoff — notification chains\n\nEntry point.\n", "ideas/00-intro.md": "# Ideas for chain business requirements\n\nFlat list.\n", "ideas/02-chain-core.md": "## Chain core\n\n- **B-01.** Something.\n", "ideas/01-relations.md": "## Relations\n\nHow they relate.\n", "questions/00-intro.md": "# Questions\n\nOpen questions.\n", "questions/03-q-01-do-we-know-the-participant.md": "## Q-01. Do We Know the Chain Participant by Name\n\n**Question.** …\n", "notes/plain.md": "No heading here, only prose.\n", } def build_artifacts(root): for rel, text in FIXTURE.items(): p = os.path.join(root, rel.replace("/", os.sep)) os.makedirs(os.path.dirname(p), exist_ok=True) with open(p, "w", encoding="utf-8") as f: f.write(text) return root # -------------------------------------------------------------------------- # names, titles, order — pure # -------------------------------------------------------------------------- class TestNames(unittest.TestCase): def test_order_comes_from_a_numeric_prefix(self): self.assertEqual(page.order_of("02-chain-core.md"), 2) self.assertEqual(page.order_of("00-intro.md"), 0) self.assertIsNone(page.order_of("handoff.md")) def test_zero_is_an_order_and_not_a_missing_one(self): """`00-` means "this is the directory's own page", so the difference between 0 and None decides where a page lands in the tree.""" self.assertIsNot(page.order_of("00-intro.md"), None) def test_the_prefix_never_reaches_the_title(self): self.assertEqual(page.title_from_name("02-chain-core.md"), "Chain core") def test_only_the_first_letter_is_raised(self): """Title-casing would wreck every name that already knows how it is spelled.""" self.assertEqual(page.title_from_name("sqlc-and-APNs.md"), "Sqlc and APNs") def test_a_heading_beats_a_file_name(self): text = "## Q-01. Do We Know the Chain Participant by Name\n" self.assertEqual(page.title_from_body(text), "Q-01. Do We Know the Chain Participant by Name") def test_only_the_first_heading_counts(self): self.assertEqual(page.title_from_body("# One\n\n## Two\n"), "One") def test_a_heading_after_prose_is_a_section_not_a_name(self): self.assertIsNone(page.title_from_body("Prose first.\n\n# Late\n")) def test_markup_is_stripped_from_a_title(self): """A page list does not render markdown, so inline code in a heading is noise in the name.""" self.assertEqual(page.sanitize_title("Inventory — `P-NN`"), "Inventory — P-NN") def test_a_slash_in_a_heading_does_not_invent_hierarchy(self): self.assertEqual(page.sanitize_title("Send/receive timing"), "Send-receive timing") class TestPaths(unittest.TestCase): def test_a_title_becomes_one_path_component_per_segment(self): self.assertEqual(page.path_for_title("Simple Chains/Ideas/Chain core"), os.path.join("Simple-Chains", "Ideas", "Chain-core.md")) def test_shell_hostile_characters_leave_the_path_but_not_the_title(self): title = "Simple Chains/Don't send to this one" self.assertEqual(page.path_for_title(title), os.path.join("Simple-Chains", "Dont-send-to-this-one.md")) self.assertIn("'", title) def test_an_empty_title_still_produces_a_file(self): self.assertEqual(page.path_for_title(""), "untitled.md") # -------------------------------------------------------------------------- # importing # -------------------------------------------------------------------------- class TestPlanImport(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.src = build_artifacts(os.path.join(self.tmp.name, "artifacts")) self.pages, self.collisions = page.plan_import(self.src, "Simple Chains") self.titles = {p["title"] for p in self.pages} def tearDown(self): self.tmp.cleanup() def test_nothing_collides(self): self.assertEqual(self.collisions, []) def test_an_order_zero_file_becomes_the_directorys_own_page(self): self.assertIn("Simple Chains/Ideas", self.titles) def test_that_page_is_named_for_the_directory_not_its_heading(self): """`ideas/00-intro.md` opens with "Ideas for chain business requirements". A child's title must extend its parent's exactly, and no child would ever be prefixed by that.""" self.assertNotIn("Simple Chains/Ideas for chain business requirements", self.titles) def test_every_child_extends_its_parents_title(self): self.assertIn("Simple Chains/Ideas/Chain core", self.titles) self.assertIn("Simple Chains/Questions/" "Q-01. Do We Know the Chain Participant by Name", self.titles) def test_a_file_without_a_heading_falls_back_to_its_name(self): self.assertIn("Simple Chains/Notes/Plain", self.titles) def test_the_prefix_hangs_everything_under_one_title(self): self.assertTrue(all(t.startswith("Simple Chains/") for t in self.titles)) def test_numeric_prefixes_order_siblings(self): ideas = [p for p in self.pages if p["title"].startswith("Simple Chains/Ideas/")] self.assertEqual([p["title"].split("/")[-1] for p in ideas], ["Relations", "Chain core"]) def test_a_collision_is_reported_and_not_resolved(self): """Two headings that sanitize to one path. Picking a winner is how a discussion loses a document.""" d = os.path.join(self.tmp.name, "clash") os.makedirs(d) for name, heading in (("a.md", "# Send timing"), ("b.md", "# Send/timing")): with open(os.path.join(d, name), "w") as f: f.write(heading + "\n") _, collisions = page.plan_import(d) self.assertEqual(len(collisions), 1) # -------------------------------------------------------------------------- # the index # -------------------------------------------------------------------------- class TestIndex(unittest.TestCase): def setUp(self): self.m = page.blank_manifest("s") for title, order in (("Top", None), ("Top/Ideas", 0), ("Top/Ideas/Relations", 1), ("Top/Ideas/Chain core", 2), ("Top/Zeta", None), ("Top/Parked/Decisions", None)): self.m["pages"][page.path_for_title(title)] = page.entry(title, order) def test_nesting_follows_titles_not_manifest_path_order(self): """On disk `Top/Zeta.md` sorts before `Top/Ideas/Chain-core.md`; in the hierarchy Zeta is a child and Chain core a grandchild.""" body = page.render_index(self.m, "Top") lines = [l for l in body.splitlines() if l.strip().startswith("- ") or l.strip().startswith(" - ")] ideas = next(i for i, l in enumerate(lines) if "|Ideas]]" in l) core = next(i for i, l in enumerate(lines) if "Chain core]]" in l) zeta = next(i for i, l in enumerate(lines) if "|Zeta]]" in l) self.assertLess(ideas, core) self.assertLess(core, zeta) def test_a_parent_with_no_page_still_holds_its_children(self): """Nothing is published at `Top/Parked`; dropping it would hide Decisions entirely.""" body = page.render_index(self.m, "Top") self.assertIn("- [[Top/Parked|Parked]]", body) self.assertIn(" - [[Top/Parked/Decisions|Decisions]]", body) def test_an_unpublished_page_is_linked_by_wiki_syntax(self): self.assertIn("[[Top/Ideas|Ideas]]", page.render_index(self.m, "Top")) def test_a_published_page_is_linked_by_its_sub_url(self): """sub_url is the only address Gitea guarantees, and it appears only after a push — so rebuilding the index after publishing upgrades the links.""" rel = page.path_for_title("Top/Ideas") self.m["pages"][rel]["sub_url"] = "Top%2FIdeas" self.assertIn("- [Ideas](Top%2FIdeas)", page.render_index(self.m, "Top")) def test_the_prefix_itself_is_not_listed_inside_its_own_index(self): self.assertNotIn("|Top]]", page.render_index(self.m, "Top")) # -------------------------------------------------------------------------- # the manifest # -------------------------------------------------------------------------- class TestManifest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() def tearDown(self): self.tmp.cleanup() def test_a_missing_manifest_loads_blank(self): m = page.load_manifest("a/b", self.tmp.name) self.assertEqual(m["pages"], {}) def test_wiki_bookkeeping_survives_a_round_trip(self): """The domain never reads sub_url, and must never drop it either — a lost sub_url is a duplicate page on the next push.""" m = page.blank_manifest("a/b") m["pages"]["X.md"] = page.entry("X", 1, sub_url="X", pushed="deadbeef") page.save_manifest(m, self.tmp.name) back = page.load_manifest("a/b", self.tmp.name) self.assertEqual(back["pages"]["X.md"]["sub_url"], "X") self.assertEqual(back["pages"]["X.md"]["pushed"], "deadbeef") self.assertEqual(back["pages"]["X.md"]["order"], 1) def test_domain_keys_are_written_first(self): """The manifest lands in a diff on every sync; a readable one gets checked.""" m = page.blank_manifest("a/b") m["pages"]["X.md"] = page.entry("X", 1, sub_url="X") with open(page.save_manifest(m, self.tmp.name), encoding="utf-8") as f: raw = f.read() self.assertLess(raw.index('"title"'), raw.index('"sub_url"')) def test_children_of_is_a_prefix_test_and_not_a_substring_one(self): m = page.blank_manifest("s") for t in ("Top", "Top/A", "Topaz", "Topaz/B"): m["pages"][page.path_for_title(t)] = page.entry(t) got = {e["title"] for _, e in page.children_of(m, "Top")} self.assertEqual(got, {"Top", "Top/A"}) # -------------------------------------------------------------------------- # md <-> wiki JSON # -------------------------------------------------------------------------- class TestWikiMap(unittest.TestCase): def test_a_body_survives_encode_and_decode(self): text = "# Заголовок — DC\n\n- [x] пункт\n" self.assertEqual(wikimap.decode({"content_base64": wikimap.encode(text)}), text) def test_an_empty_page_decodes_to_an_empty_string(self): """A page that exists with no body is a real state; the caller writing a file should not have to tell it from a missing key.""" self.assertEqual(wikimap.decode({}), "") self.assertEqual(wikimap.decode({"content_base64": None}), "") def test_from_payload_takes_the_address_gitea_returned(self): got = wikimap.from_payload({ "title": "A/B", "sub_url": "A%2FB.-", "html_url": "https://x/A%2FB.-", "last_commit": {"sha": "abc", "author": {"date": "2026-08-10T11:15:39Z"}}, }) self.assertEqual(got["sub_url"], "A%2FB.-") self.assertEqual(got["sha"], "abc") self.assertEqual(got["remote-updated"], "2026-08-10T11:15:39Z") def test_a_sub_url_goes_into_the_endpoint_verbatim(self): """Gitea hands it back already escaped; re-encoding it produces a path that resolves to nothing.""" self.assertEqual( wikimap.page_endpoint("repos/o/r", "A%2FB.-"), "repos/o/r/wiki/page/A%2FB.-") def test_prefix_matching_needs_a_separator(self): self.assertTrue(wikimap.matches_prefix("Top", "Top")) self.assertTrue(wikimap.matches_prefix("Top/A", "Top")) self.assertFalse(wikimap.matches_prefix("Topaz", "Top")) def test_an_empty_prefix_matches_everything(self): self.assertTrue(wikimap.matches_prefix("anything", "")) def test_a_payload_carries_the_operators_message(self): p = wikimap.new_payload("A/B", "body", "why it changed") self.assertEqual(p["message"], "why it changed") self.assertEqual(wikimap.decode(p), "body") # -------------------------------------------------------------------------- # the scripts, in a throwaway repository # -------------------------------------------------------------------------- class TestImportScript(unittest.TestCase): """The real scripts, run as subprocesses inside a scratch repo.""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.root = self.tmp.name os.makedirs(os.path.join(self.root, ".git")) # auth is in the list because the transport resolves the login pin # through skills/auth/scripts/pin.py — one search order, one module. for layer in ("page", "wiki", "sync", "auth"): shutil.copytree(os.path.join(REPO, "skills", layer, "scripts"), os.path.join(self.root, "skills", layer, "scripts"), ignore=shutil.ignore_patterns("__pycache__")) self.src = build_artifacts(os.path.join(self.root, "artifacts")) self.scripts = os.path.join(self.root, "skills", "page", "scripts") self.space = os.path.join(self.root, "tmp", "wiki", "s") def tearDown(self): self.tmp.cleanup() def run_script(self, name, *args, cwd=None): return subprocess.run( [sys.executable, os.path.join(self.scripts, name)] + list(args), capture_output=True, text=True, cwd=cwd or self.root) def manifest(self): with open(os.path.join(self.space, ".pages.json"), encoding="utf-8") as f: return json.load(f) def do_import(self, *extra): return self.run_script("page_import.py", "--from", self.src, "--space", "s", "--prefix", "Top", *extra) def test_dry_run_writes_nothing(self): r = self.do_import("--dry-run") self.assertEqual(r.returncode, 0, r.stderr) self.assertFalse(os.path.exists(self.space)) def test_import_writes_the_tree_and_the_manifest(self): self.assertEqual(self.do_import().returncode, 0) self.assertTrue(os.path.isfile( os.path.join(self.space, "Top", "Ideas", "Chain-core.md"))) self.assertIn("Top/Ideas/Chain core", {e["title"] for e in self.manifest()["pages"].values()}) def test_creating_a_space_is_announced(self): """Nothing creates a store as a silent side effect of a write — that is how a typo in --space makes a second one nobody notices.""" self.assertIn("created space", self.do_import().stderr) def test_the_cache_is_found_from_a_subdirectory(self): """The anchor is the script's own location, not cwd. A `cd` outlives the command that ran it.""" self.do_import() deep = os.path.join(self.src, "ideas") r = self.run_script("page_ls.py", "--space", "s", cwd=deep) self.assertEqual(r.returncode, 0, r.stderr) self.assertIn("Chain core", r.stdout) def test_a_reimport_keeps_the_title_and_the_wiki_bookkeeping(self): self.do_import() m = self.manifest() rel = "Top/Ideas/Chain-core.md" m["pages"][rel]["sub_url"] = "Top%2FIdeas%2FChain-core" with open(os.path.join(self.space, ".pages.json"), "w") as f: json.dump(m, f) # The heading changes. Without the manifest that would rename a # published page, which does not rename it — it publishes a second one. with open(os.path.join(self.src, "ideas", "02-chain-core.md"), "w") as f: f.write("## A completely different heading\n\nchanged\n") self.do_import() after = self.manifest()["pages"][rel] self.assertEqual(after["title"], "Top/Ideas/Chain core") self.assertEqual(after["sub_url"], "Top%2FIdeas%2FChain-core") with open(os.path.join(self.space, rel), encoding="utf-8") as f: self.assertIn("A completely different heading", f.read()) def test_retitle_moves_the_page_and_keeps_its_address(self): """A retitle changes the path, so the entry has to be found by source. Found by path it would look new, and the next push would publish a duplicate beside the page it was meant to rename.""" self.do_import() m = self.manifest() m["pages"]["Top/Ideas/Chain-core.md"]["sub_url"] = "Top%2FIdeas%2FChain-core" m["pages"]["Top/Ideas/Chain-core.md"]["pushed"] = "deadbeef" with open(os.path.join(self.space, ".pages.json"), "w") as f: json.dump(m, f) with open(os.path.join(self.src, "ideas", "02-chain-core.md"), "w") as f: f.write("## Chain core, renamed\n") self.do_import("--retitle") pages = self.manifest()["pages"] self.assertNotIn("Top/Ideas/Chain-core.md", pages) moved = pages["Top/Ideas/Chain-core-renamed.md"] self.assertEqual(moved["title"], "Top/Ideas/Chain core, renamed") self.assertEqual(moved["sub_url"], "Top%2FIdeas%2FChain-core") self.assertFalse(os.path.exists( os.path.join(self.space, "Top", "Ideas", "Chain-core.md"))) def test_a_rename_makes_the_next_push_send_the_page(self): """The body can be byte-identical after a rename, and push decides by body hash alone — so a stale `pushed` would skip the rename forever.""" self.do_import() m = self.manifest() rel = "Top/Ideas/Chain-core.md" with open(os.path.join(self.space, rel), encoding="utf-8") as f: body = f.read() m["pages"][rel]["sub_url"] = "x" m["pages"][rel]["pushed"] = __import__("hashlib").sha1( body.encode()).hexdigest() with open(os.path.join(self.space, ".pages.json"), "w") as f: json.dump(m, f) src = os.path.join(self.src, "ideas", "02-chain-core.md") with open(src, encoding="utf-8") as f: text = f.read() with open(src, "w") as f: f.write(text.replace("## Chain core", "## Chain core renamed")) self.do_import("--retitle") moved = self.manifest()["pages"]["Top/Ideas/Chain-core-renamed.md"] self.assertNotIn("pushed", moved) def test_ls_reports_an_unpublished_page_as_local(self): self.do_import() r = self.run_script("page_ls.py", "--space", "s") self.assertIn("local", r.stdout) self.assertNotIn("synced", r.stdout) def test_ls_distinguishes_a_missing_space_from_an_empty_one(self): r = self.run_script("page_ls.py", "--space", "nope") self.assertNotEqual(r.returncode, 0) self.assertIn("no such space", r.stderr) def test_index_is_written_as_an_ordinary_page(self): self.do_import() r = self.run_script("page_index.py", "--space", "s", "--prefix", "Top") self.assertEqual(r.returncode, 0, r.stderr) self.assertIn("Top.md", self.manifest()["pages"]) with open(os.path.join(self.space, "Top.md"), encoding="utf-8") as f: body = f.read() self.assertIn("- [[Top/Ideas|Ideas]]", body) self.assertIn(" - [[Top/Ideas/Chain core|Chain core]]", body) # -------------------------------------------------------------------------- # the layering rule, mechanically # -------------------------------------------------------------------------- class TestLayering(unittest.TestCase): def test_the_page_layer_is_stdlib_only(self): """skills/page must keep working with skills/wiki deleted — so no transport, and above all no subprocess, in the domain layer.""" imported = set() for name in sorted(os.listdir(PAGE_SCRIPTS)): if not name.endswith(".py"): continue with open(os.path.join(PAGE_SCRIPTS, name)) as f: for line in f: if line.startswith(("import ", "from ")): imported.add(line.split()[1].split(".")[0]) foreign = imported - {"page"} - sys.stdlib_module_names self.assertEqual(foreign, set(), "non-stdlib import in the page layer: %s" % ", ".join(sorted(foreign))) self.assertNotIn("subprocess", imported) def test_the_page_layer_never_mentions_a_tracker(self): """A sub_url, a login, an HTTP verb in skills/page means the concept is in the wrong layer.""" banned = ("tea api", "_gitea", "GITEA_LOGIN", "content_base64") for name in sorted(os.listdir(PAGE_SCRIPTS)): if not name.endswith(".py"): continue with open(os.path.join(PAGE_SCRIPTS, name)) as f: body = f.read() for word in banned: self.assertNotIn(word, body, "%s mentions %r" % (name, word)) def test_wikimap_is_pure(self): """The translation layer holds no transport and no I/O: give it a payload, get a page; give it a page, get a request body. Checked on the imports, not on the prose — the docstring names the things it refuses to do.""" with open(os.path.join(WIKI_SCRIPTS, "wikimap.py")) as f: imported = {line.split()[1].split(".")[0] for line in f if line.startswith(("import ", "from "))} self.assertEqual(imported, {"base64"}, "wikimap.py imports more than the translation needs") if __name__ == "__main__": unittest.main()