Files
chemenu/tools/chemenu/tests/test_reference_doc.py
T
torbenandClaude Opus 5.5 070cae6aad
CI / verify (push) Successful in 6m10s
CI / pwsh (push) Successful in 1m59s
Release / release (push) Successful in 34s
feat: docs reference generates a skill catalogue and a page-type reference under docs/reference/ from the shipped defaults, checked and written in the origin repo only (#189)
Files changed:
- CHANGES.md
- README.md
- VERSION
- docs/reference/README.md
- docs/reference/commands/README.md
- docs/reference/commands/types-instructions-and-docs.md
- docs/reference/page-types.md
- docs/reference/skills.md
- instructions/dev/doc-pull-through.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/reference_doc.py
- tools/chemenu/tests/test_reference_doc.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
2026-10-06 22:10:09 +02:00

463 lines
20 KiB
Python

import json
import re
import pytest
from typer.testing import CliRunner
from chemenu import blocks, cli, cli_contract, config, reference_doc, toc
from chemenu import version as version_mod
from chemenu.cli import app
from chemenu.commands import dist_cmd, docs_verify, instructions_cmd
from chemenu.type_resolver import resolver
runner = CliRunner()
# --- the real tree --------------------------------------------------------
def _group_pages() -> dict[str, str]:
return {
title: (config.ROOT / reference_doc.group_page(title)).read_text(encoding="utf-8")
for title, _ in cli_contract.GROUPS
}
def test_the_real_reference_is_current():
"""What a session that changed a record and forgot `docs reference --apply`
is caught by."""
assert docs_verify.check_reference_regions() == []
def test_every_group_has_one_page_and_every_command_one_section():
pages = _group_pages()
for path in cli_contract.all_records():
heading = f"## `{path}`\n"
hits = [title for title, text in pages.items() if text.count(heading) == 1]
assert len(hits) == 1, path
assert sum(text.count(heading) for text in pages.values()) == 1, path
def test_group_slugs_are_unique():
slugs = [reference_doc.group_slug(title) for title, _ in cli_contract.GROUPS]
assert len(slugs) == len(set(slugs))
def test_no_page_carries_an_agent_addressed_field():
"""ON FAILURE reactions and NEVER tell an agent what to do; the human
reference keeps only the cause of each failure."""
text = "\n".join(_group_pages().values())
text += (config.ROOT / reference_doc.EXIT_CODES).read_text(encoding="utf-8")
for rec in cli_contract.all_records().values():
for failure in rec.failures:
if failure.reaction:
assert failure.reaction not in text, (rec.path, failure.reaction)
for line in rec.never:
assert line not in text, (rec.path, line)
def test_options_match_the_options_section_of_h():
texts = cli.options_texts(sorted(cli_contract.all_records()))
for path, options in texts.items():
result = runner.invoke(app, [*path.split(" "), "-h"])
assert result.exit_code == 0, (path, result.output)
if options.strip():
assert f"OPTIONS\n{options.rstrip()}\n\n" in result.output, path
else:
assert "\nOPTIONS\n" not in result.output, path
def _section(markdown: str, heading: str) -> str:
start = markdown.index(f"## {heading}\n")
end = markdown.find("\n## ", start + 1)
return markdown[start:end if end != -1 else None]
def test_exit_codes_names_every_gate_and_links_rather_than_repeats_agents_md():
page = (config.ROOT / reference_doc.EXIT_CODES).read_text(encoding="utf-8")
for rec in cli_contract.all_records().values():
for gate in rec.properties.gates:
assert f"### {gate}\n" in page, gate
assert "(../../AGENTS.md#tool-error-contract)" in page
assert "(../../AGENTS.md#gates)" in page
agents = (config.ROOT / "AGENTS.md").read_text(encoding="utf-8")
for heading in ("Tool error contract", "Gates"):
for sentence in re.split(r"(?<=[.:])\s+", _section(agents, heading)):
sentence = sentence.strip()
if len(sentence) >= 30:
assert sentence not in page, sentence
def test_the_reference_pages_survive_dist_export_unchanged():
"""`dist export` strips `dist:strip` spans from every shipped text file; a
record quoting the marker pair verbatim would be cut in the exported page
and the fresh instance's `docs verify` would call it stale."""
plan = dist_cmd.build_plan()
for region in reference_doc.regions():
assert plan[region.path].content == (config.ROOT / region.path).read_text(encoding="utf-8")
def _type_sections(page: str) -> dict[str, str]:
region = blocks.find(page, reference_doc.PAGE_TYPES_REGION)
return {
match.group(1): match.group(2)
for match in re.finditer(r"^## `([^`]+)`\n(.*?)(?=^## |\Z)", region, re.M | re.S)
}
def test_the_skill_catalogue_has_one_row_per_shipped_skill():
shipped = {
d.name for d in instructions_cmd.skill_dirs() if not instructions_cmd.is_dev_only(d)
}
assert shipped
region = blocks.find(
(config.ROOT / reference_doc.SKILLS).read_text(encoding="utf-8"), reference_doc.SKILLS_REGION
)
rows = re.findall(r"^\| `([^`]+)` \|", region, re.M)
assert sorted(rows) == sorted(shipped)
for directory in instructions_cmd.skill_dirs():
if instructions_cmd.is_dev_only(directory):
assert f"`{directory.name}`" not in region
def test_page_types_has_one_section_per_page_type_matching_types_describe():
"""Required fields and subtypes are what `types describe --json` says, and
each subtype's directory is where `new` would write it."""
sections = _type_sections((config.ROOT / reference_doc.PAGE_TYPES).read_text(encoding="utf-8"))
names = {
fm["name"] for path, fm in resolver.list_type_specs()
if path.rsplit("/", 1)[-1][:-3] in dist_cmd.instance_owned_type_stems()
}
assert names and set(sections) == names
for name, section in sections.items():
result = runner.invoke(app, ["types", "describe", name, "--json"])
assert result.exit_code == 0, result.output
described = json.loads(result.output)
required = [f["field"] for f in described["fields"] if f["required"]]
listed = re.findall(r"^- `([^`]+)` - ", section.split("**Required fields**")[1], re.M)
assert listed == required, name
field = described["subtype_field"]
rows = re.findall(r"^\| `([^`]+)` \| `([^`]+)` \|", section, re.M)
if not field:
assert rows == [], name
continue
enum = next(f["enum"] for f in described["fields"] if f["field"] == field)
assert [value for value, _ in rows] == enum, name
for value, written_to in rows:
target = resolver.compute_target_dir(described["type_path"], {field: value})
assert written_to == target.relative_to(config.ROOT).as_posix() + "/", (name, value)
def test_a_description_is_split_at_its_use_when_sentence():
assert reference_doc.split_description("Does X. Use when Y happens.") == ("Does X.", "Y happens.")
assert reference_doc.split_description("Does X.") == ("Does X.", "only when asked for by name")
def test_a_table_cell_escapes_pipes_and_tags_but_not_inside_code():
cell = reference_doc._cell('says "ingest <file>" or `a <b> | c` | d')
assert cell == 'says "ingest &lt;file&gt;" or `a <b> \\| c` \\| d'
@pytest.fixture
def exported_instance(tmp_path, monkeypatch):
"""A `dist export` of this tree, adopted and then changed the way an
instance may: a type-spec of its own making, and a skill of its own."""
target = tmp_path / "instance"
dist_cmd.run_export(target)
monkeypatch.setattr(config, "ROOT", target)
monkeypatch.setattr(resolver, "_repo_root", target)
for template in sorted((target / "types").glob("*.template")):
template.rename(template.with_suffix(""))
entity = target / "types" / "entity.md"
entity.write_text(
entity.read_text(encoding="utf-8").replace(
"description: Base type for entity pages", "description: This instance's entities"
),
encoding="utf-8",
)
own = target / "instructions" / "own-skill"
own.mkdir()
(own / "SKILL.md").write_text(
"---\nname: own-skill\ndescription: Does this instance's thing. Use when asked.\n---\n# Own\n",
encoding="utf-8",
)
return target
def test_an_adapted_instance_leaves_both_pages_alone(exported_instance):
root = exported_instance
assert version_mod.stamp_file().is_file()
pages = {
relative: (root / relative).read_text(encoding="utf-8")
for relative in (reference_doc.SKILLS, reference_doc.PAGE_TYPES)
}
assert docs_verify.check_reference_regions() == []
result = runner.invoke(app, ["docs", "reference", "--apply"])
assert result.exit_code == 0, result.output
assert "Left alone in this instance" in result.output
for relative, before in pages.items():
assert (root / relative).read_text(encoding="utf-8") == before, relative
assert docs_verify.check_reference_regions() == []
# --- a fixture tree -------------------------------------------------------
GROUPS = (("Fixture things", ("frobnicate",)), ("Other", ("gate keep",)))
def _record(path: str, gates: tuple[str, ...] = (), summary: str = "Does a thing.") -> cli_contract.CommandRecord:
failures = [cli_contract.Failure(cause="It broke", reaction="Show the user and stop")]
if gates:
failures.append(cli_contract.Failure(cause="Needs a person", reaction="Wait", code=42))
return cli_contract.CommandRecord(
path=path,
summary=summary,
synopsis=(cli_contract.Variant(usage=f"{path} [--flag]"),),
properties=cli_contract.Properties(
effect=cli_contract.Effect.WRITE,
idempotent=cli_contract.Idempotent.NO,
atomic="Yes",
budget=cli_contract.Budget.EXEMPT_WITHOUT_ARGS,
gates=gates,
),
notes=("Does a thing, mechanically.",),
failures=tuple(failures),
examples=("tools/wikitool frobnicate --flag",),
never=("Never frobnicate twice.",),
see_also=("`wikitool other` - a neighbour",),
)
HEADS = {
"docs/reference/commands/fixture-things.md": "# Fixture things\n\nThings, by hand.\n",
"docs/reference/commands/other.md": "# Other\n\nThe other one.\n",
"docs/reference/commands/README.md": "# Command reference\n\nAll of it.\n",
"docs/reference/exit-codes.md": "# Exit codes and gates\n\nWhere it stops.\n",
"docs/reference/skills.md": "# Skill catalogue\n\nThe skills.\n",
"docs/reference/page-types.md": "# Page types\n\nThe types.\n",
}
REGION_OF = {
"docs/reference/commands/fixture-things.md": reference_doc.COMMANDS_REGION,
"docs/reference/commands/other.md": reference_doc.COMMANDS_REGION,
"docs/reference/commands/README.md": reference_doc.INDEX_REGION,
"docs/reference/exit-codes.md": reference_doc.EXIT_CODES_REGION,
"docs/reference/skills.md": reference_doc.SKILLS_REGION,
"docs/reference/page-types.md": reference_doc.PAGE_TYPES_REGION,
}
def _page_type(description: str = "A fixture type.") -> reference_doc.PageType:
return reference_doc.PageType(
described={
"name": "gadget", "description": description, "subtype_field": "gadget_type",
"root": "kb", "base_dir": "gadgets", "title_prefix": None,
"fields": [
{"field": "type", "required": True, "type": "string", "enum": None},
{"field": "gadget_type", "required": True, "type": "string", "enum": ["widget"]},
{"field": "tags", "required": False, "type": "array", "enum": None},
],
},
subtypes=(reference_doc.Subtype("widget", "widgets", "Widgets"),),
)
@pytest.fixture
def tree(tmp_path, monkeypatch):
monkeypatch.setattr(config, "ROOT", tmp_path)
records = {"frobnicate": _record("frobnicate"), "gate keep": _record("gate keep", ("keep",))}
monkeypatch.setattr(cli_contract, "all_records", lambda: dict(records))
monkeypatch.setattr(cli_contract, "GROUPS", GROUPS)
monkeypatch.setattr(cli, "options_texts", lambda paths: {p: " --flag A flag." for p in paths})
monkeypatch.setattr(
reference_doc, "shipped_skills",
lambda: [reference_doc.Skill("fixture-skill", "Does a thing. Use when asked.")],
)
monkeypatch.setattr(reference_doc, "page_types", lambda: [_page_type()])
for relative, head in HEADS.items():
path = tmp_path / relative
path.parent.mkdir(parents=True, exist_ok=True)
name = REGION_OF[relative]
path.write_text(
f"{head}\n{blocks.open_marker(name)}\n{blocks.close_marker(name)}\n", encoding="utf-8"
)
return tmp_path, records
def _snapshot(root) -> dict[str, str]:
return {
p.relative_to(root).as_posix(): p.read_text(encoding="utf-8")
for p in sorted(root.rglob("*.md"))
}
def test_a_fresh_tree_is_reported_stale_until_applied(tree):
root, _ = tree
issues = docs_verify.check_reference_regions()
assert len(issues) == len(HEADS)
assert all("stale" in issue and "docs reference --apply" in issue for issue in issues)
dry = runner.invoke(app, ["docs", "reference"])
assert dry.exit_code == 0, dry.output
assert "would change" in dry.output
assert docs_verify.check_reference_regions() != []
result = runner.invoke(app, ["docs", "reference", "--apply"])
assert result.exit_code == 0, result.output
assert docs_verify.check_reference_regions() == []
def test_apply_twice_changes_nothing_the_second_time(tree):
root, _ = tree
runner.invoke(app, ["docs", "reference", "--apply"])
first = _snapshot(root)
second_run = runner.invoke(app, ["docs", "reference", "--apply"])
assert second_run.exit_code == 0
assert "already current" in second_run.output
assert _snapshot(root) == first
def test_the_head_survives_apply_byte_for_byte(tree):
root, _ = tree
runner.invoke(app, ["docs", "reference", "--apply"])
for relative, head in HEADS.items():
text = (root / relative).read_text(encoding="utf-8")
assert text.startswith(head), relative
def test_a_changed_record_makes_verify_name_the_page_and_the_command(tree, monkeypatch):
root, records = tree
runner.invoke(app, ["docs", "reference", "--apply"])
records["frobnicate"] = _record("frobnicate", summary="Does a different thing.")
monkeypatch.setattr(cli_contract, "all_records", lambda: dict(records))
issues = docs_verify.check_reference_regions()
assert any(
"docs/reference/commands/fixture-things.md" in issue and "docs reference --apply" in issue
for issue in issues
)
def test_a_generated_page_leaves_out_reactions_and_never(tree):
root, _ = tree
runner.invoke(app, ["docs", "reference", "--apply"])
text = (root / "docs/reference/commands/fixture-things.md").read_text(encoding="utf-8")
assert "It broke" in text
assert "Show the user and stop" not in text
assert "Never frobnicate twice." not in text
assert "- Counts toward the session call budget: only when called with arguments" in text
assert "```text\n --flag A flag.\n```" in text
def test_a_long_page_gets_its_toc_outside_the_generated_region(tree, monkeypatch):
"""The first `##` of a group page is generated; a TOC inserted before it
would sit inside the region and be wiped by the next regeneration."""
root, records = tree
many = tuple(f"cmd{i}" for i in range(12))
for path in many:
records[path] = _record(path)
monkeypatch.setattr(cli_contract, "all_records", lambda: dict(records))
monkeypatch.setattr(cli_contract, "GROUPS", (("Fixture things", many), GROUPS[1]))
assert runner.invoke(app, ["docs", "reference", "--apply"]).exit_code == 0
page = root / "docs/reference/commands/fixture-things.md"
text = page.read_text(encoding="utf-8")
assert toc.needs_toc(text)
assert text.index(blocks.close_marker(toc.REGION_NAME)) < text.index(
blocks.open_marker(reference_doc.COMMANDS_REGION)
)
assert toc.upsert(text) == text
assert docs_verify.check_reference_regions() == []
def test_a_group_without_a_page_fails_and_nothing_is_created(tree, monkeypatch):
root, _ = tree
monkeypatch.setattr(cli_contract, "GROUPS", GROUPS + (("New group", ()),))
issues = docs_verify.check_reference_regions()
assert any("docs/reference/commands/new-group.md is missing" in issue for issue in issues)
result = runner.invoke(app, ["docs", "reference", "--apply"])
assert result.exit_code == 1
assert "docs/reference/commands/new-group.md" in result.output
assert not (root / "docs/reference/commands/new-group.md").exists()
def test_a_page_without_its_marker_pair_fails_and_is_left_alone(tree):
root, _ = tree
page = root / "docs/reference/exit-codes.md"
page.write_text("# Exit codes and gates\n\nNo region here.\n", encoding="utf-8")
issues = docs_verify.check_reference_regions()
assert any("exit-codes.md has no" in issue for issue in issues)
result = runner.invoke(app, ["docs", "reference", "--apply"])
assert result.exit_code == 1
assert page.read_text(encoding="utf-8") == "# Exit codes and gates\n\nNo region here.\n"
def test_a_page_without_a_group_fails(tree):
root, _ = tree
(root / "docs/reference/commands/gone.md").write_text("# Gone\n", encoding="utf-8")
issues = docs_verify.check_reference_regions()
assert any("gone.md belongs to no group" in issue for issue in issues)
def test_a_tree_without_docs_passes(tmp_path, monkeypatch):
monkeypatch.setattr(config, "ROOT", tmp_path)
assert docs_verify.check_reference_regions() == []
def test_exit_codes_lists_each_gate_and_its_exit_42_causes(tree):
root, _ = tree
runner.invoke(app, ["docs", "reference", "--apply"])
page = (root / "docs/reference/exit-codes.md").read_text(encoding="utf-8")
assert "### keep\n\n- [`gate keep`](commands/other.md#gate-keep)" in page
assert "- [`gate keep`](commands/other.md#gate-keep): Needs a person" in page
assert "frobnicate" not in page
def test_the_index_links_every_command_to_its_section(tree):
root, _ = tree
runner.invoke(app, ["docs", "reference", "--apply"])
index = (root / "docs/reference/commands/README.md").read_text(encoding="utf-8")
assert "- [`frobnicate`](fixture-things.md#frobnicate) - Does a thing." in index
assert "- [`gate keep`](other.md#gate-keep) - Does a thing." in index
def test_a_changed_skill_or_type_makes_verify_name_its_page(tree, monkeypatch):
runner.invoke(app, ["docs", "reference", "--apply"])
assert docs_verify.check_reference_regions() == []
monkeypatch.setattr(
reference_doc, "shipped_skills",
lambda: [reference_doc.Skill("fixture-skill", "Does another thing. Use when asked.")],
)
monkeypatch.setattr(reference_doc, "page_types", lambda: [_page_type("A changed type.")])
issues = docs_verify.check_reference_regions()
for page in ("docs/reference/skills.md", "docs/reference/page-types.md"):
assert any(page in issue and "docs reference --apply" in issue for issue in issues), page
def test_the_fixture_skill_and_type_render(tree):
root, _ = tree
runner.invoke(app, ["docs", "reference", "--apply"])
skills = (root / "docs/reference/skills.md").read_text(encoding="utf-8")
assert "| `fixture-skill` | Does a thing. | asked. |" in skills
types = (root / "docs/reference/page-types.md").read_text(encoding="utf-8")
assert "- Written to: `kb/gadgets/<subtype directory>/`" in types
assert "| `widget` | `kb/gadgets/widgets/` | Widgets |" in types
assert "- `gadget_type` - string, one of `widget`" in types
assert "`tags`" not in types
def test_an_instance_skips_a_missing_skill_or_type_page(tree):
"""In an instance the two pages are not the tree's to check - not even
whether they exist."""
root, _ = tree
runner.invoke(app, ["docs", "reference", "--apply"])
(root / version_mod.RELEASE_STAMP_FILENAME).write_text("{}", encoding="utf-8")
(root / "docs/reference/skills.md").unlink()
(root / "docs/reference/page-types.md").write_text("# Page types\n", encoding="utf-8")
assert docs_verify.check_reference_regions() == []
result = runner.invoke(app, ["docs", "reference", "--apply"])
assert result.exit_code == 0, result.output
assert "Left alone in this instance: docs/reference/skills.md, docs/reference/page-types.md" in result.output
assert not (root / "docs/reference/skills.md").exists()