Files
chemenu/tools/chemenu/tests/test_toc.py
T
torben c64479fe02
CI / verify (push) Successful in 45s
Release / release (push) Successful in 36s
stack: TOC-Scope auf types/ und docs/, Sprachregeln nach AGENTS.md zentralisiert, alle Templates auf Control-Plane-Sprache, --breaking akkumuliert (schliesst #99)
Files changed:
- AGENTS.md
- CHANGES.md
- ENVIRONMENT.md.template
- SOUL.md
- SOUL.md.template
- USER.md.template
- VERSION
- docs/ownership-and-templates.md
- docs/version-model.md
- instructions/CONTRACT.md
- instructions/dev/doc-pull-through.md
- instructions/dev/stack-close/SKILL.md
- instructions/dev/stack-dev/SKILL.md
- instructions/dev/version-parts.md
- instructions/setup-instance.md
- kb/CONVENTIONS.md
- kb/CONVENTIONS.md.template
- kb/concepts/COLLECTION.md
- kb/sources/COLLECTION.md
- tools/CONTRACT.md
- tools/chemenu/commands/types_cmd.py
- tools/chemenu/commands/version_cmd.py
- tools/chemenu/tests/test_toc.py
- tools/chemenu/tests/test_version_cmd.py
- tools/chemenu/toc.py
- tools/chemenu/version.py
- types/comparison.md
- types/concept.md
- types/entity.md
- types/lint-report.md
- types/source.md
2026-09-15 16:21:02 +02:00

200 lines
7.4 KiB
Python

from chemenu import blocks, toc
def _body(n_lines: int, sections: int) -> str:
"""A synthetic `# Title` + intro + `sections` `##` headings, `n_lines` long."""
lines = ["# Title", "", "Intro paragraph."]
per_section = max(1, (n_lines - len(lines) - sections) // sections) if sections else 0
for i in range(sections):
lines += ["", f"## Section {i}", ""]
lines += [f"Body line {j}." for j in range(per_section)]
while len(lines) < n_lines:
lines.append("Padding.")
return "\n".join(lines[:n_lines]) + "\n"
def test_needs_toc_is_false_at_the_threshold():
body = _body(toc.THRESHOLD, 3)
assert len(body.splitlines()) == toc.THRESHOLD
assert not toc.needs_toc(body)
def test_needs_toc_is_true_just_over_the_threshold():
body = _body(toc.THRESHOLD + 1, 3)
assert toc.needs_toc(body)
def test_needs_toc_measures_without_an_existing_region():
"""A file that shrank back under the threshold but still carries a stale
TOC region from when it was much longer must not count the region's own
lines toward keeping itself around - `needs_toc` strips it before
measuring. A 150-entry region is long enough on its own to push the
file over the threshold if it were (wrongly) counted."""
huge_region = toc.render([toc.Heading(level=2, text=f"Section {i}") for i in range(150)])
shrunk_with_stale_region = (
"# Title\n\nIntro paragraph.\n\n" + huge_region + "\n\n## Only Section\n\nOne line.\n"
)
assert len(shrunk_with_stale_region.splitlines()) > toc.THRESHOLD
assert not toc.needs_toc(shrunk_with_stale_region)
def test_iter_headings_skips_fenced_code():
body = "\n".join(
[
"# Title",
"",
"## Real Section",
"",
"```bash",
"# Not a heading",
"## Also not a heading",
"```",
"",
"### Real Subsection",
]
)
headings = toc.iter_headings(body)
assert [h.text for h in headings] == ["Real Section", "Real Subsection"]
assert [h.level for h in headings] == [2, 3]
def test_iter_headings_preserves_inline_code_in_heading_text():
"""Masking blanks inline code spans for *detection*, but the text has to
come back from the unmasked line or `` `instructions/dev/` `` would
render as blank space instead of its real content."""
body = "# Title\n\n## `instructions/dev/`\n"
headings = toc.iter_headings(body)
assert headings == [toc.Heading(level=2, text="`instructions/dev/`")]
def test_render_produces_nested_marker_region():
headings = [
toc.Heading(level=2, text="First"),
toc.Heading(level=3, text="Nested"),
toc.Heading(level=2, text="Second"),
]
region = toc.render(headings)
assert region.startswith(blocks.open_marker(toc.REGION_NAME))
assert region.endswith(blocks.close_marker(toc.REGION_NAME))
assert f"## {toc.HEADING_TEXT}" in region
assert "- [First](#first)" in region
assert " - [Nested](#nested)" in region
assert "- [Second](#second)" in region
def test_render_deduplicates_repeated_slugs():
headings = [toc.Heading(level=2, text="Steps"), toc.Heading(level=2, text="Steps")]
region = toc.render(headings)
assert "(#steps)" in region
assert "(#steps-1)" in region
def test_render_is_empty_for_no_headings():
assert toc.render([]) == ""
def test_upsert_inserts_before_the_first_level2_heading():
body = _body(toc.THRESHOLD + 20, 4)
result = toc.upsert(body)
assert result != body
marker_pos = result.index(blocks.open_marker(toc.REGION_NAME))
first_heading_pos = result.index("## Section 0")
assert marker_pos < first_heading_pos
# Title and intro paragraph still precede the region.
assert result.index("# Title") < marker_pos
assert result.index("Intro paragraph.") < marker_pos
def test_upsert_is_idempotent():
body = _body(toc.THRESHOLD + 30, 5)
once = toc.upsert(body)
twice = toc.upsert(once)
assert once == twice
def test_upsert_is_idempotent_without_a_trailing_newline():
"""A file missing its final newline (tools/CONTRACT.md and
types/type-spec.md both did, in the real repo) must not make the
"no region yet" and "region already exists" paths disagree on
round two."""
body = _body(toc.THRESHOLD + 20, 4).rstrip("\n")
once = toc.upsert(body)
twice = toc.upsert(once)
assert once == twice
def test_upsert_refreshes_a_stale_region_in_place():
body = _body(toc.THRESHOLD + 20, 3)
stale = toc.upsert(body)
# Rename a heading without touching the (now stale) TOC region.
changed = stale.replace("## Section 1", "## Renamed Section", 1)
refreshed = toc.upsert(changed)
assert "[Renamed Section](#renamed-section)" in refreshed
assert "[Section 1]" not in refreshed
def test_upsert_removes_the_region_once_the_file_shrinks():
body = _body(toc.THRESHOLD + 20, 3)
with_region = toc.upsert(body)
assert blocks.find(with_region, toc.REGION_NAME) is not None
shrunk = "# Title\n\nIntro paragraph.\n\n## Only Section\n\nOne line.\n"
result = toc.upsert(shrunk)
assert blocks.find(result, toc.REGION_NAME) is None
def test_stale_regions_is_false_right_after_upsert():
body = _body(toc.THRESHOLD + 20, 3)
once = toc.upsert(body)
assert toc.stale_regions(once) is False
def test_target_files_matches_the_documented_scope():
"""Integration check against the real repo: every agent-loaded category
AGENTS.md § File naming names - AGENTS.md itself, every stage contract,
kb/CONVENTIONS.md, every COLLECTION.md, the flat `instructions/**.md` form,
every type-spec, every `docs/` page - with `SKILL.md` the one exception."""
from chemenu import config
files = toc.target_files()
relatives = {f.relative_to(config.ROOT).as_posix() for f in files}
assert "AGENTS.md" in relatives
assert "kb/CONTRACT.md" in relatives
assert "kb/CONVENTIONS.md" in relatives
assert "types/type-spec.md" in relatives
assert "instructions/CONTRACT.md" in relatives
assert "instructions/dev/version-parts.md" in relatives # flat, still instructions/**.md
assert "types/source.md" in relatives # a page type-spec, not only the stage contract
assert "docs/version-model.md" in relatives
assert not any(rel.endswith("SKILL.md") for rel in relatives)
def test_target_files_lists_type_spec_once_despite_two_sources():
"""`types/type-spec.md` is both a stage contract and a `types/*.md` file;
the set in `target_files` is what keeps that from being a special case."""
from chemenu import config
relatives = [f.relative_to(config.ROOT).as_posix() for f in toc.target_files()]
assert relatives.count("types/type-spec.md") == 1
def test_strip_region_removes_what_types_describe_would_otherwise_echo():
body = (
"# A type\n\n"
f"{blocks.open_marker(toc.REGION_NAME)}\n## Contents\n\n- [X](#x)\n"
f"{blocks.close_marker(toc.REGION_NAME)}\n\n## X\n\nProse.\n"
)
stripped = toc.strip_region(body)
assert toc.REGION_NAME not in stripped
assert "## Contents" not in stripped
assert "## X" in stripped and "Prose." in stripped
# The heading keeps the blank line the region used to sit in, rather than
# being welded to the line above it.
assert "# A type\n\n## X" in stripped
def test_strip_region_leaves_a_body_that_never_had_one_alone():
body = "# A type\n\n## X\n\nProse.\n"
assert toc.strip_region(body) == body