feat: export guidelines - the guideline pages as a generated GUIDELINES.md, pushed into the captured repositories behind the Guideline Push Gate (#179)
Files changed: - AGENTS.md - CHANGES.md - README.md - VERSION - docs/why-gates-are-code.md - instructions/gates.md - instructions/kb-profiles.md - kb/CONTRACT.md - kb/CONVENTIONS.md - kb/CONVENTIONS.md.template - raw/CONTRACT.md - tools/CONTRACT.md - tools/README.md - tools/chemenu/cli.py - tools/chemenu/cli_contract.py - tools/chemenu/commands/export_cmd.py - tools/chemenu/commands/page_ops.py - tools/chemenu/commands/raw_cmd.py - tools/chemenu/commands/search.py - tools/chemenu/guideline_export.py - tools/chemenu/kb_scan.py - tools/chemenu/repo_capture.py - tools/chemenu/search/filters.py - tools/chemenu/tests/test_cli.py - tools/chemenu/tests/test_export_guidelines.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
ccede04b97
commit
283cdae8be
25 files changed
+1753
-68
No files matched your search
@@ -340,13 +340,15 @@ def test_network_yes_is_exactly_the_commands_that_can_reach_outside_this_checkou
|
||||
not only the two `version_cmd.py` used to claim exclusivity for. `dist upgrade` is on the list
|
||||
since `--latest`, which asks the release feed and downloads from it, and `raw fetch`
|
||||
since it exists (Gitea #120) - its `--html` form stays offline, which does not turn the
|
||||
command back to `no`. `raw capture` and `raw status` reach a git remote (Gitea #177). Pinned as an explicit
|
||||
command back to `no`. `raw capture` and `raw status` reach a git remote (Gitea #177), and so
|
||||
does `export guidelines --push` (Gitea #179). Pinned as an explicit
|
||||
set so a command gaining or losing that reach is a deliberate edit here, not a silent
|
||||
drift between the property and what the command actually does."""
|
||||
expected = {
|
||||
"raw fetch",
|
||||
"raw capture",
|
||||
"raw status",
|
||||
"export guidelines",
|
||||
"sync",
|
||||
"publish",
|
||||
"version check",
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
"""`export guidelines` and the Guideline Push Gate, against local repositories
|
||||
over `file://` - never the network.
|
||||
|
||||
The instance is the `kb_dir` fixture tree made into a git checkout with its own
|
||||
identity and a copy of the shipped `types/`, so the cleanliness check sees the
|
||||
same two directories it sees in a real instance. Target repositories are bare
|
||||
repositories, each fed from a work clone, because a push into a checked-out
|
||||
branch is refused by git itself.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from chemenu import config, guideline_export, repo_capture
|
||||
from chemenu.cli import app
|
||||
from chemenu.commands import export_cmd
|
||||
from chemenu.commands.raw_cmd import raw_accept_command, raw_capture_command, raw_status_command
|
||||
from chemenu.frontmatter_io import write_page
|
||||
from chemenu.markdown_code import strip_code_spans
|
||||
from chemenu.search import filters
|
||||
from chemenu.search.registry import resolve
|
||||
from chemenu.search.service import load_pages_by_path, run_search
|
||||
from chemenu.search.types import SearchQuery
|
||||
from chemenu.type_resolver import resolver
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
TAG = ["--tag", "guideline"]
|
||||
|
||||
|
||||
def _git(cwd: Path, *args: str, check: bool = True) -> str:
|
||||
return subprocess.run(
|
||||
["git", "-c", "user.name=Fixture", "-c", "user.email=f@example.org", *args],
|
||||
cwd=cwd, check=check, capture_output=True, text=True,
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
def _bare(bare: Path, *args: str) -> str:
|
||||
return subprocess.run(
|
||||
["git", f"--git-dir={bare}", *args], check=True, capture_output=True, text=True
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
class Target:
|
||||
"""A bare repository - what the export pushes into - fed from a work
|
||||
clone that plays the project's own developers."""
|
||||
|
||||
def __init__(self, base: Path, guidelines: bytes | None = None) -> None:
|
||||
self.work = base / "work"
|
||||
self.bare = base / "remote.git"
|
||||
self.work.mkdir(parents=True)
|
||||
_git(self.work, "init", "-q", "-b", "main")
|
||||
(self.work / "README.md").write_text("# Project\n", encoding="utf-8")
|
||||
(self.work / "docs").mkdir()
|
||||
(self.work / "docs" / "a.md").write_text("# A\n", encoding="utf-8")
|
||||
if guidelines is not None:
|
||||
(self.work / "GUIDELINES.md").write_bytes(guidelines)
|
||||
self.commit("initial")
|
||||
_git(base, "clone", "-q", "--bare", str(self.work), str(self.bare))
|
||||
_git(self.work, "remote", "add", "origin", self.url)
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
return self.bare.as_uri()
|
||||
|
||||
def commit(self, message: str = "change") -> None:
|
||||
_git(self.work, "add", "-A")
|
||||
_git(self.work, "commit", "-q", "--allow-empty", "-m", message)
|
||||
|
||||
def advance(self, message: str = "upstream change") -> str:
|
||||
"""A commit by someone else, pushed to the remote branch."""
|
||||
_git(self.work, "pull", "-q", "--ff-only", "origin", "main", check=False)
|
||||
(self.work / "docs" / "a.md").write_text(f"# A\n\n{message}\n", encoding="utf-8")
|
||||
self.commit(message)
|
||||
_git(self.work, "push", "-q", "origin", "HEAD:main")
|
||||
return self.tip()
|
||||
|
||||
def tip(self) -> str:
|
||||
return _bare(self.bare, "rev-parse", "refs/heads/main")
|
||||
|
||||
def refs(self) -> str:
|
||||
return _bare(self.bare, "for-each-ref", "--format=%(refname) %(objectname)")
|
||||
|
||||
def file(self, rev: str = "refs/heads/main") -> bytes:
|
||||
return subprocess.run(
|
||||
["git", f"--git-dir={self.bare}", "show", f"{rev}:GUIDELINES.md"],
|
||||
check=True, capture_output=True,
|
||||
).stdout
|
||||
|
||||
|
||||
STUB = b"<!-- wikitool:export kind=guidelines -->\n"
|
||||
|
||||
ALPHA_BODY = """
|
||||
# Alpha Rule
|
||||
|
||||
Keep secrets in `.env` only[^s-handbook]. See [[Beta Rule]] and [[Beta Rule|the beta]],
|
||||
also [[Beta Rule#Scope]] and [[Beta Rule#Scope|its scope]]. Legacy^[[Source - Handbook]] too.
|
||||
|
||||
## Details
|
||||
|
||||
Inline `[[Not A Link]]` and `[^not-a-cite]` stay as written.
|
||||
|
||||
```
|
||||
[[Fenced|kept]] and [^fenced] and ^[[Fenced]]
|
||||
```
|
||||
|
||||
<!-- wikitool:links -->
|
||||
## Beziehungen
|
||||
|
||||
- **related:** [[Beta Rule]]
|
||||
<!-- /wikitool:links -->
|
||||
|
||||
<!-- wikitool:footnotes -->
|
||||
## Fußnoten
|
||||
|
||||
[^s-handbook]: [[Source - Handbook]]
|
||||
<!-- /wikitool:footnotes -->
|
||||
"""
|
||||
|
||||
BETA_BODY = """
|
||||
Beta has no heading of its own.
|
||||
|
||||
### Scope
|
||||
|
||||
Everything.
|
||||
|
||||
## Footnotes
|
||||
|
||||
[^s-handbook]: [[Source - Handbook]]
|
||||
"""
|
||||
|
||||
|
||||
def _guideline(kb: Path, title: str, body: str, tags=("guideline",)) -> Path:
|
||||
path = kb / "concepts" / f"{title}.md"
|
||||
write_page(path, {"type": "types/concept.md", "tags": list(tags), "summary": f"{title}."}, body)
|
||||
return path
|
||||
|
||||
|
||||
def _commit_instance(root: Path, message: str = "instance change") -> None:
|
||||
_git(root, "add", "-A")
|
||||
_git(root, "commit", "-q", "-m", message)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def instance(kb_dir, monkeypatch):
|
||||
root = kb_dir.parent
|
||||
shutil.copytree(config._PACKAGE_ROOT / "types", root / "types")
|
||||
monkeypatch.setattr(config, "TYPES_DIR", root / "types")
|
||||
monkeypatch.setattr(resolver, "_repo_root", root)
|
||||
monkeypatch.setattr(repo_capture, "ALLOWED_SCHEMES", ("ssh", "https", "file"))
|
||||
(root / "raw").mkdir(exist_ok=True)
|
||||
(root / "incoming").mkdir(exist_ok=True)
|
||||
(root / ".gitignore").write_text("tools/\n", encoding="utf-8")
|
||||
_guideline(kb_dir, "Alpha Rule", ALPHA_BODY)
|
||||
_guideline(kb_dir, "beta rule", BETA_BODY)
|
||||
_git(root, "init", "-q", "-b", "main")
|
||||
_git(root, "config", "user.name", "Instance Author")
|
||||
_git(root, "config", "user.email", "author@example.org")
|
||||
_commit_instance(root, "instance")
|
||||
return root
|
||||
|
||||
|
||||
def _manifest(root: Path, name: str, repo: str, ref: str = "main") -> Path:
|
||||
bundle = root / "raw" / "2026" / "10" / name
|
||||
bundle.mkdir(parents=True)
|
||||
data = {
|
||||
"schema": 1, "repo": repo, "ref": ref, "commit": "0" * 40, "paths": ["docs/**"],
|
||||
"captured": "2026-10-05T00:00:00Z", "fidelity": "verbatim", "authority": "normative",
|
||||
"files": [],
|
||||
}
|
||||
(bundle / repo_capture.MANIFEST_NAME).write_text(json.dumps(data), encoding="utf-8")
|
||||
return bundle
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def target(instance, tmp_path):
|
||||
t = Target(tmp_path / "targets" / "one", STUB)
|
||||
_manifest(instance, "one", t.url)
|
||||
_commit_instance(instance, "capture one")
|
||||
return t
|
||||
|
||||
|
||||
def _run(*args: str):
|
||||
return runner.invoke(app, ["export", "guidelines", *args])
|
||||
|
||||
|
||||
def _token(output: str) -> str:
|
||||
match = re.search(r"--confirm (\w+) --push", output)
|
||||
assert match, output
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _push(*extra: str):
|
||||
"""Gate run, then the confirmed run - what an approved push looks like."""
|
||||
gated = _run(*TAG, "--push", *extra)
|
||||
assert gated.exit_code == 42, gated.output
|
||||
return _run(*TAG, "--push", "--confirm", _token(gated.output), *extra)
|
||||
|
||||
|
||||
# --- selection ----------------------------------------------------------------
|
||||
|
||||
|
||||
def test_selection_is_searchs_own(instance, kb_dir):
|
||||
_guideline(kb_dir, "Gamma Elsewhere", "\nNot a guideline.\n", tags=("other",))
|
||||
_commit_instance(instance)
|
||||
result = _run(*TAG)
|
||||
assert result.exit_code == 0, result.output
|
||||
exported = re.findall(r"^# (.+)$", result.output, re.MULTILINE)
|
||||
predicates = tuple(filters.parse_predicate(r) for r in filters.raw_predicates(tag="guideline"))
|
||||
search = run_search(SearchQuery(text=None, predicates=predicates, limit=0),
|
||||
load_pages_by_path(), resolve(None))
|
||||
expected = sorted((h.title for h in search.hits), key=lambda t: (t.casefold(), t))
|
||||
assert exported == expected == ["Alpha Rule", "beta rule"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("args, needle", [
|
||||
((), "No predicate"),
|
||||
(("--tag", "nothing-has-this"), "No page matches"),
|
||||
(("--field", "no_such_field=x"), "unknown field"),
|
||||
])
|
||||
def test_refusals_print_no_export(instance, target, args, needle):
|
||||
before = target.refs()
|
||||
for extra in ((), ("--push",)):
|
||||
result = _run(*args, *extra)
|
||||
assert result.exit_code == 1, result.output
|
||||
assert needle in result.output
|
||||
assert "<!-- wikitool:export" not in result.output
|
||||
assert target.refs() == before
|
||||
|
||||
|
||||
def test_unreadable_frontmatter_anywhere_refuses(instance, kb_dir, target):
|
||||
(kb_dir / "concepts" / "Broken.md").write_text("---\ntype: [unclosed\n---\nBody\n", encoding="utf-8")
|
||||
_commit_instance(instance)
|
||||
before = target.refs()
|
||||
result = _run(*TAG, "--push")
|
||||
assert result.exit_code == 1
|
||||
assert "Broken.md" in result.output and "<!-- wikitool:export" not in result.output
|
||||
assert target.refs() == before
|
||||
|
||||
|
||||
@pytest.mark.parametrize("change", ["modified", "untracked-kb", "untracked-types"])
|
||||
def test_dirty_kb_or_types_refuses(instance, kb_dir, target, change):
|
||||
if change == "modified":
|
||||
page = kb_dir / "concepts" / "Alpha Rule.md"
|
||||
page.write_text(page.read_text(encoding="utf-8") + "\nmore\n", encoding="utf-8")
|
||||
elif change == "untracked-kb":
|
||||
(kb_dir / "concepts" / "draft.txt").write_text("x\n", encoding="utf-8")
|
||||
else:
|
||||
(instance / "types" / "draft.md").write_text("x\n", encoding="utf-8")
|
||||
before = target.refs()
|
||||
for extra in ((), ("--push",)):
|
||||
result = _run(*TAG, *extra)
|
||||
assert result.exit_code == 1, result.output
|
||||
assert "uncommitted changes" in result.output
|
||||
assert "<!-- wikitool:export" not in result.output
|
||||
assert target.refs() == before
|
||||
|
||||
|
||||
# --- rendering ----------------------------------------------------------------
|
||||
|
||||
|
||||
def _export(*args: str) -> str:
|
||||
result = _run(*(args or TAG))
|
||||
assert result.exit_code == 0, result.output
|
||||
return result.output
|
||||
|
||||
|
||||
def test_rendering_resolves_links_and_drops_citations_outside_code(instance):
|
||||
out = _export()
|
||||
assert out.endswith("\n") and not out.endswith("\n\n") and "\r" not in out
|
||||
assert "See Beta Rule and the beta,\nalso Beta Rule and its scope. Legacy too." in out
|
||||
assert "Keep secrets in `.env` only." in out
|
||||
assert "Inline `[[Not A Link]]` and `[^not-a-cite]` stay as written." in out
|
||||
assert "```\n[[Fenced|kept]] and [^fenced] and ^[[Fenced]]\n```" in out
|
||||
prose = strip_code_spans(out)
|
||||
for forbidden in ("[[", "[^", "^[[", "wikitool:links", "wikitool:footnotes", "Beziehungen",
|
||||
"Fußnoten", "Footnotes", "type: types/", "---\n"):
|
||||
assert forbidden not in prose, forbidden
|
||||
|
||||
|
||||
def test_headings(instance):
|
||||
out = _export()
|
||||
lines = out.split("\n")
|
||||
assert lines[1] == "" and lines[2] == "# Alpha Rule" and lines[3] == ""
|
||||
assert out.count("# Alpha Rule") == 1
|
||||
assert "\n## Details\n" in out and "\n### Scope\n" in out
|
||||
assert "\n\n# beta rule\n\nBeta has no heading of its own." in out
|
||||
|
||||
|
||||
def test_header_line(instance):
|
||||
first = _export().split("\n", 1)[0]
|
||||
commit = _git(instance, "log", "-1", "--format=%H", "--", "kb/concepts/Alpha Rule.md",
|
||||
"kb/concepts/beta rule.md")
|
||||
assert first == (
|
||||
f"<!-- wikitool:export kind=guidelines instance=local commit={commit} "
|
||||
"- generated, do not edit by hand -->"
|
||||
)
|
||||
|
||||
|
||||
def test_instance_is_origin_without_userinfo(instance):
|
||||
_git(instance, "remote", "add", "origin", "https://user:s3cret@git.example.org/team/wiki.git")
|
||||
first = _export().split("\n", 1)[0]
|
||||
assert "instance=https://git.example.org/team/wiki.git " in first
|
||||
assert "s3cret" not in first and "user" not in first
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url, expected", [
|
||||
("https://user:tok@host/x.git", "https://host/x.git"),
|
||||
("ssh://git@host:2222/x.git", "ssh://host:2222/x.git"),
|
||||
("git@host:team/x.git", "host:team/x.git"),
|
||||
("https://host/x.git", "https://host/x.git"),
|
||||
])
|
||||
def test_strip_userinfo(url, expected):
|
||||
assert guideline_export.strip_userinfo(url) == expected
|
||||
|
||||
|
||||
def test_output_is_deterministic_and_ignores_unrelated_commits(instance, kb_dir):
|
||||
first = _export()
|
||||
assert _export() == first
|
||||
(kb_dir / "concepts" / "Unrelated.md").write_text(
|
||||
"---\ntype: types/concept.md\ntags: [other]\n---\n\nx\n", encoding="utf-8"
|
||||
)
|
||||
_commit_instance(instance, "unrelated")
|
||||
assert _export() == first
|
||||
|
||||
|
||||
# --- the gate and the push ----------------------------------------------------
|
||||
|
||||
|
||||
def test_push_without_identity_refuses_before_any_target(instance, target):
|
||||
_git(instance, "config", "--unset", "user.email")
|
||||
before = target.refs()
|
||||
result = _run(*TAG, "--push")
|
||||
assert result.exit_code == 1, result.output
|
||||
assert "user.email" in result.output
|
||||
assert not re.search(r"--confirm \w{12}", result.output)
|
||||
assert "NEEDS USER CLEARANCE" not in result.output
|
||||
assert target.refs() == before
|
||||
|
||||
|
||||
def test_push_without_token_is_gated(instance, target, tmp_path):
|
||||
hand = Target(tmp_path / "targets" / "hand", b"# Our own rules\n")
|
||||
_manifest(instance, "hand", hand.url)
|
||||
_commit_instance(instance)
|
||||
before = (target.refs(), hand.refs())
|
||||
result = _run(*TAG, "--push")
|
||||
assert result.exit_code == 42, result.output
|
||||
assert (target.refs(), hand.refs()) == before
|
||||
assert f"{target.url} main: new" in result.output
|
||||
assert f"{hand.url} main: skipped" in result.output
|
||||
assert "+# Alpha Rule" in result.output and "-<!-- wikitool:export kind=guidelines -->" in result.output
|
||||
assert re.search(r"tools/wikitool export guidelines --confirm \w{12} --push --field tags=guideline",
|
||||
result.output)
|
||||
|
||||
|
||||
def test_confirmed_push_writes_one_commit_with_only_guidelines(instance, target):
|
||||
old = target.tip()
|
||||
stdout = _export()
|
||||
result = _push()
|
||||
assert result.exit_code == 0, result.output
|
||||
new = target.tip()
|
||||
assert f"written {old[:12]} -> {new[:12]}" in result.output
|
||||
assert _bare(target.bare, "rev-parse", f"{new}^") == old
|
||||
assert _bare(target.bare, "diff", "--name-only", old, new) == "GUIDELINES.md"
|
||||
assert target.file() == stdout.encode("utf-8")
|
||||
assert _bare(target.bare, "log", "-1", "--format=%an <%ae>|%cn <%ce>", new) == (
|
||||
"Instance Author <author@example.org>|Instance Author <author@example.org>"
|
||||
)
|
||||
commit = stdout.split("commit=", 1)[1][:12]
|
||||
assert _bare(target.bare, "log", "-1", "--format=%s", new) == (
|
||||
f"Update GUIDELINES.md from local at {commit}"
|
||||
)
|
||||
|
||||
again = _run(*TAG, "--push")
|
||||
assert again.exit_code == 0, again.output
|
||||
assert f"{target.url} main: unchanged" in again.output
|
||||
assert "--confirm" not in again.output
|
||||
assert target.tip() == new
|
||||
|
||||
|
||||
def test_stale_tokens_are_gated_again(instance, kb_dir, target, tmp_path):
|
||||
token = _token(_run(*TAG, "--push").output)
|
||||
|
||||
target.advance()
|
||||
moved = _run(*TAG, "--push", "--confirm", token)
|
||||
assert moved.exit_code == 42 and "does not match" in moved.output
|
||||
token = _token(moved.output)
|
||||
|
||||
page = kb_dir / "concepts" / "beta rule.md"
|
||||
page.write_text(page.read_text(encoding="utf-8") + "\nAmended.\n", encoding="utf-8")
|
||||
_commit_instance(instance, "amend beta")
|
||||
changed = _run(*TAG, "--push", "--confirm", token)
|
||||
assert changed.exit_code == 42 and "+Amended." in changed.output
|
||||
token = _token(changed.output)
|
||||
|
||||
second = Target(tmp_path / "targets" / "two", STUB)
|
||||
_manifest(instance, "two", second.url)
|
||||
_commit_instance(instance, "capture two")
|
||||
added = _run(*TAG, "--push", "--confirm", token)
|
||||
assert added.exit_code == 42 and f"{second.url} main: new" in added.output
|
||||
|
||||
before = (target.refs(), second.refs())
|
||||
invented = _run(*TAG, "--push", "--confirm", "000000000000")
|
||||
assert invented.exit_code == 42
|
||||
assert (target.refs(), second.refs()) == before
|
||||
|
||||
|
||||
def test_targets_that_do_not_take_part_are_left_alone(instance, target, tmp_path):
|
||||
hand = Target(tmp_path / "targets" / "hand", b"# Our own rules\n")
|
||||
none = Target(tmp_path / "targets" / "none")
|
||||
foreign = Target(
|
||||
tmp_path / "targets" / "foreign",
|
||||
b"<!-- wikitool:export kind=guidelines instance=https://other.example/wiki commit=abc -->\n",
|
||||
)
|
||||
tagged = Target(tmp_path / "targets" / "tagged", STUB)
|
||||
for name, t, ref in (("hand", hand, "main"), ("none", none, "main"),
|
||||
("foreign", foreign, "main"), ("tagged", tagged, "v*")):
|
||||
_manifest(instance, name, t.url, ref)
|
||||
_commit_instance(instance)
|
||||
before = {t.url: t.refs() for t in (hand, none, foreign, tagged)}
|
||||
result = _push()
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "hand-written" in result.output
|
||||
assert "not opted in" in result.output
|
||||
assert "another instance (https://other.example/wiki)" in result.output
|
||||
assert "tag pattern" in result.output
|
||||
assert {t.url: t.refs() for t in (hand, none, foreign, tagged)} == before
|
||||
assert "written" in result.output # the participating target was still served
|
||||
|
||||
|
||||
def test_a_branch_that_moved_is_rejected_not_forced(instance, target, monkeypatch):
|
||||
# Translated git messages must not change the verdict: it is read from the
|
||||
# porcelain status flag, not from the text.
|
||||
monkeypatch.setenv("LANGUAGE", "de")
|
||||
monkeypatch.setenv("LANG", "de_DE.UTF-8")
|
||||
token = _token(_run(*TAG, "--push").output)
|
||||
real_commit_file = repo_capture.commit_file
|
||||
foreign: list[str] = []
|
||||
|
||||
def commit_then_race(*args, **kwargs):
|
||||
foreign.append(target.advance("raced in"))
|
||||
return real_commit_file(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(repo_capture, "commit_file", commit_then_race)
|
||||
result = _run(*TAG, "--push", "--confirm", token)
|
||||
assert result.exit_code == 1, result.output
|
||||
assert f"{target.url} main: rejected" in result.output
|
||||
assert target.tip() == foreign[0]
|
||||
|
||||
|
||||
def test_an_unreachable_target_does_not_stop_the_others(instance, target, tmp_path):
|
||||
_manifest(instance, "gone", (tmp_path / "does-not-exist.git").as_uri())
|
||||
_commit_instance(instance)
|
||||
result = _push()
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "does-not-exist.git main: skipped (not reachable" in result.output
|
||||
assert f"{target.url} main: written" in result.output
|
||||
|
||||
|
||||
def test_bundle_narrows_the_targets(instance, target, tmp_path):
|
||||
other = Target(tmp_path / "targets" / "other", STUB)
|
||||
_manifest(instance, "other", other.url)
|
||||
_commit_instance(instance)
|
||||
before = other.refs()
|
||||
result = _push("--bundle", "raw/2026/10/one")
|
||||
assert result.exit_code == 0, result.output
|
||||
assert other.url not in result.output
|
||||
assert other.refs() == before
|
||||
unknown = _run(*TAG, "--push", "--bundle", "raw/2026/10/nope")
|
||||
assert unknown.exit_code == 1 and "not a captured bundle" in unknown.output
|
||||
|
||||
|
||||
def test_bundle_and_confirm_need_push(instance):
|
||||
assert _run(*TAG, "--confirm", "abc").exit_code == 1
|
||||
assert _run(*TAG, "--bundle", "raw/x").exit_code == 1
|
||||
|
||||
|
||||
def test_raw_status_does_not_report_a_pushed_export(instance, tmp_path, capsys):
|
||||
t = Target(tmp_path / "targets" / "captured", STUB)
|
||||
raw_capture_command(repo_url=t.url, ref="main", paths=["**/*.md"], name="captured",
|
||||
fidelity="verbatim", authority="normative", update=None)
|
||||
raw_accept_command(files=[instance / "incoming" / "captured"], fidelity=None, authority=None,
|
||||
page=None, replaces=None, dry_run=False)
|
||||
_commit_instance(instance, "capture")
|
||||
capsys.readouterr()
|
||||
result = _push()
|
||||
assert result.exit_code == 0 and "written" in result.output, result.output
|
||||
raw_status_command(json_out=True)
|
||||
rows = json.loads(capsys.readouterr().out)
|
||||
assert len(rows) == 1 and rows[0]["error"] is None and rows[0]["changed"] is False
|
||||
|
||||
|
||||
# --- no way back in -----------------------------------------------------------
|
||||
|
||||
|
||||
def test_export_marker_is_defined_once():
|
||||
package = Path(guideline_export.__file__).parent
|
||||
definitions = [
|
||||
path for path in package.rglob("*.py")
|
||||
if "tests" not in path.parts and re.search(r'= b"<!-- wikitool:export"', path.read_text("utf-8"))
|
||||
]
|
||||
assert definitions == [Path(guideline_export.__file__)]
|
||||
|
||||
|
||||
def test_a_real_export_is_excluded_from_capture(instance):
|
||||
data = _export().encode("utf-8")
|
||||
assert repo_capture._content_exclusion(data) is not None
|
||||
assert repo_capture._content_exclusion(b"\xef\xbb\xbf" + data) is not None
|
||||
|
||||
|
||||
def _snapshot(*dirs: Path) -> dict[str, bytes]:
|
||||
return {p.as_posix(): p.read_bytes() for d in dirs for p in sorted(d.rglob("*")) if p.is_file()}
|
||||
|
||||
|
||||
def test_raw_accept_refuses_an_export_file_and_a_folder_holding_one(instance):
|
||||
incoming, raw = instance / "incoming", instance / "raw"
|
||||
export = _export().encode("utf-8")
|
||||
(incoming / "GUIDELINES.md").write_bytes(export)
|
||||
(incoming / "pack").mkdir()
|
||||
(incoming / "pack" / "notes.md").write_text("# Notes\n", encoding="utf-8")
|
||||
(incoming / "pack" / "sub").mkdir()
|
||||
(incoming / "pack" / "sub" / "rules.md").write_bytes(b"\xef\xbb\xbf" + export)
|
||||
before = _snapshot(incoming, raw)
|
||||
for path in (incoming / "GUIDELINES.md", incoming / "pack"):
|
||||
result = runner.invoke(app, ["raw", "accept", str(path), "--fidelity", "verbatim",
|
||||
"--authority", "normative"])
|
||||
assert result.exit_code == 1, result.output
|
||||
assert "guideline export" in result.output
|
||||
assert _snapshot(incoming, raw) == before
|
||||
|
||||
|
||||
def test_is_export_reads_the_first_line_only():
|
||||
assert guideline_export.is_export(b"<!-- wikitool:export kind=x -->\nbody")
|
||||
assert not guideline_export.is_export(b"# Title\n<!-- wikitool:export kind=x -->\n")
|
||||
assert guideline_export.guidelines_header(STUB) == {}
|
||||
assert guideline_export.guidelines_header(b"<!-- wikitool:export kind=other -->") is None
|
||||
|
||||
|
||||
def test_confirm_token_ignores_targets_it_does_not_write():
|
||||
t = export_cmd.Target("u", "main")
|
||||
writes = export_cmd.Plan(t, "new", tip="a", blob="b")
|
||||
skipped = export_cmd.Plan(export_cmd.Target("v", "main"), "skipped", "x")
|
||||
assert export_cmd.confirm_token([writes, skipped]) == export_cmd.confirm_token([writes])
|
||||
assert export_cmd.confirm_token([writes]) != export_cmd.confirm_token(
|
||||
[export_cmd.Plan(t, "new", tip="a2", blob="b")]
|
||||
)
|
||||
Reference in new issue
Block a user