177c7e9ce8
Files changed: - .gitea/workflows/ci.yml - AGENTS.md - CHANGES.md - VERSION - instructions/CONTRACT.md - instructions/link-taxonomy.md - instructions/migrations/4.0.0-link-taxonomy.md - instructions/setup-instance.md - kb/CONTRACT.md - kb/CONVENTIONS.md - kb/CONVENTIONS.md.template - kb/comparisons/COLLECTION.md - kb/concepts/COLLECTION.md - kb/entities/COLLECTION.md - kb/sources/COLLECTION.md - tools/CONTRACT.md - tools/README.md - tools/chemenu/blocks.py - tools/chemenu/cli.py - tools/chemenu/commands/cite_cmd.py - tools/chemenu/commands/dist_cmd.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/links_cmd.py - tools/chemenu/commands/migrate_cmd.py - tools/chemenu/commands/new_page.py - tools/chemenu/commands/page_ops.py - tools/chemenu/commands/run_budget.py - tools/chemenu/commands/xref.py - tools/chemenu/conventions.py - tools/chemenu/corpus_diff.py - tools/chemenu/frontmatter_io.py - tools/chemenu/kb_collections.py - tools/chemenu/kb_state.py - tools/chemenu/links.py - tools/chemenu/lint_core.py - tools/chemenu/provenance.py - tools/chemenu/sections.py - tools/chemenu/tests/conftest.py - tools/chemenu/tests/test_blocks.py - tools/chemenu/tests/test_cite_cmd.py - tools/chemenu/tests/test_conventions.py - tools/chemenu/tests/test_dist_cmd.py - tools/chemenu/tests/test_doctor.py - tools/chemenu/tests/test_migrate_cmd.py - tools/chemenu/tests/test_new_page.py - tools/chemenu/tests/test_pipeline_l0.py - tools/chemenu/tests/test_types_cmd.py - tools/chemenu/tests/test_xref.py - types/concept.schema.yaml - types/entity.md - types/entity.schema.yaml - types/instruction.schema.yaml - types/type-spec.md - work/link-taxonomy-migration/README.md - work/link-taxonomy-migration/plan.md
195 lines
7.8 KiB
Python
195 lines
7.8 KiB
Python
"""Tests for `kb/CONVENTIONS.md` - the instance-owned half of the authoring rules.
|
|
|
|
Two things are under test here, and they are the two the split exists for: the
|
|
headings the compiler *renders* come from the corpus rather than from Python,
|
|
and a collection declares who owns its rules rather than having it inferred from
|
|
the directory name.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from chemenu import blocks, config, conventions, kb_collections
|
|
from chemenu.tests.conftest import use_shipped_type_specs
|
|
|
|
GERMAN = (
|
|
"---\n"
|
|
"language: de\n"
|
|
"profile: german\n"
|
|
"sections:\n"
|
|
" links: Beziehungen\n"
|
|
" footnotes: Fußnoten\n"
|
|
"---\n\n# conventions\n"
|
|
)
|
|
|
|
FRENCH = (
|
|
"---\n"
|
|
"language: fr\n"
|
|
"profile: none\n"
|
|
"sections:\n"
|
|
" links: Relations\n"
|
|
" footnotes: Notes\n"
|
|
"---\n\n# conventions\n"
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def kb_root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
kb = tmp_path / "kb"
|
|
kb.mkdir()
|
|
monkeypatch.setattr(config, "ROOT", tmp_path)
|
|
monkeypatch.setattr(config, "KB_DIR", kb)
|
|
# Which collection the stack requires is *derived* from where the required
|
|
# `source` type writes, so these tests need the shipped `types/` reachable -
|
|
# a fixture tree without one derives an empty requirement and would assert
|
|
# against a rule that is not running. See conftest.use_shipped_type_specs.
|
|
use_shipped_type_specs(monkeypatch)
|
|
conventions.reset_cache()
|
|
yield kb
|
|
conventions.reset_cache()
|
|
|
|
|
|
def _write(kb: Path, text: str) -> None:
|
|
(kb / conventions.CONVENTIONS_FILENAME).write_text(text, encoding="utf-8")
|
|
conventions.reset_cache()
|
|
|
|
|
|
def _collection(kb: Path, name: str, profile: str = "none", required: bool = False) -> Path:
|
|
directory = kb / name
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
(directory / kb_collections.CONTRACT_NAME).write_text(
|
|
f"---\nprofile: {profile}\nrequired_by_stack: {str(required).lower()}\n---\n\n# {name}\n",
|
|
encoding="utf-8",
|
|
)
|
|
return directory
|
|
|
|
|
|
def test_a_missing_file_renders_under_a_cosmetic_default(kb_root):
|
|
"""The window between installing the machinery and writing the conventions
|
|
file. It has to render *something*, and a wrong heading is now merely wrong
|
|
words: the marker pair carries the region's identity, so the next write
|
|
repairs it once the instance declares one. Before markers, the same mistake
|
|
split a page into two sections."""
|
|
assert conventions.heading(blocks.FOOTNOTES) == "Footnotes"
|
|
assert conventions.heading(blocks.LINKS) == "Relationships"
|
|
|
|
|
|
def test_the_compiler_renders_the_headings_the_instance_declared(kb_root):
|
|
_write(kb_root, FRENCH)
|
|
assert conventions.heading(blocks.LINKS) == "Relations"
|
|
assert conventions.heading(blocks.FOOTNOTES) == "Notes"
|
|
|
|
|
|
def test_a_rewritten_file_is_not_answered_out_of_the_cache(kb_root):
|
|
_write(kb_root, GERMAN)
|
|
assert conventions.heading(blocks.FOOTNOTES) == "Fußnoten"
|
|
_write(kb_root, FRENCH)
|
|
assert conventions.heading(blocks.FOOTNOTES) == "Notes"
|
|
|
|
|
|
def test_a_region_is_found_by_its_marker_not_by_its_heading(kb_root):
|
|
"""The point of the whole change. A page whose heading says something the
|
|
instance never declared - an untranslated page, a hand-edit, another
|
|
language entirely - is still located exactly."""
|
|
_write(kb_root, FRENCH)
|
|
body = blocks.replace(
|
|
"# Page\n\nProse.\n",
|
|
blocks.LINKS,
|
|
blocks.render(blocks.LINKS, "Ganz andere Wörter", ["- **uses:** [[X]]"]),
|
|
)
|
|
assert "- **uses:** [[X]]" in blocks.find(body, blocks.LINKS)
|
|
|
|
|
|
def test_an_unknown_section_key_is_reported(kb_root):
|
|
_write(
|
|
kb_root,
|
|
"---\nsections:\n links: L\n footnotes: F\n see_also: S\n---\n",
|
|
)
|
|
assert any("see_also" in issue for issue in conventions.declaration_issues())
|
|
|
|
|
|
def test_an_incomplete_sections_block_is_reported(kb_root):
|
|
_write(kb_root, "---\nlanguage: de\nsections:\n links: Beziehungen\n---\n")
|
|
issues = conventions.declaration_issues()
|
|
assert any("sections.footnotes" in issue for issue in issues)
|
|
|
|
|
|
def test_an_unfilled_template_is_reported_like_a_missing_one(kb_root):
|
|
_write(kb_root, GERMAN.replace("language: de", f"# {config.TEMPLATE_SENTINEL}\nlanguage: de"))
|
|
assert any(config.TEMPLATE_SENTINEL in issue for issue in conventions.declaration_issues())
|
|
|
|
|
|
def test_an_absent_file_is_not_a_declaration_issue(kb_root):
|
|
"""`doctor` FAILs on absence; `docs verify` must not, or a fresh export
|
|
would be unverifiable before the setup step that writes the file."""
|
|
assert conventions.declaration_issues() == []
|
|
|
|
|
|
def test_a_collection_must_declare_its_profile_and_stack_dependence(kb_root):
|
|
_collection(kb_root, "sources", profile="sources", required=True)
|
|
(kb_root / "notes").mkdir()
|
|
(kb_root / "notes" / kb_collections.CONTRACT_NAME).write_text("# notes\n", encoding="utf-8")
|
|
issues = kb_collections.declaration_issues(kb_root)
|
|
assert any("kb/notes/COLLECTION.md has no frontmatter" in issue for issue in issues)
|
|
|
|
|
|
def test_required_by_stack_is_checked_against_the_stack_not_taken_on_trust(kb_root):
|
|
"""The one field an instance may not choose. A collection claiming the stack
|
|
depends on it would make a rename look unsafe when it is not - and, worse,
|
|
`sources` claiming otherwise would make one look safe when it is not."""
|
|
_collection(kb_root, "sources", required=False)
|
|
_collection(kb_root, "entities", required=True)
|
|
issues = kb_collections.declaration_issues(kb_root)
|
|
assert any("kb/sources/COLLECTION.md" in issue and "must be true" in issue for issue in issues)
|
|
assert any("kb/entities/COLLECTION.md" in issue and "must be false" in issue for issue in issues)
|
|
|
|
|
|
def test_a_missing_stack_required_collection_is_reported(kb_root):
|
|
_collection(kb_root, "entities")
|
|
assert any("kb/sources/ is missing" in issue for issue in kb_collections.declaration_issues(kb_root))
|
|
|
|
|
|
def test_a_correct_declaration_reports_nothing(kb_root):
|
|
_collection(kb_root, "sources", profile="sources", required=True)
|
|
_collection(kb_root, "entities", profile="entities")
|
|
assert kb_collections.declaration_issues(kb_root) == []
|
|
|
|
|
|
# --- outbound authorisation ------------------------------------------------
|
|
|
|
|
|
def _authorising(kb: Path, name: str, outbound: str, required: bool = False) -> Path:
|
|
directory = kb / name
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
(directory / kb_collections.CONTRACT_NAME).write_text(
|
|
f"---\nprofile: {name}\nrequired_by_stack: {str(required).lower()}\n"
|
|
f"outbound:\n{outbound}\n---\n\n# {name}\n",
|
|
encoding="utf-8",
|
|
)
|
|
return directory
|
|
|
|
|
|
def test_the_source_collection_decides_which_labels_may_be_used(kb_root):
|
|
"""Commonplace ADR-019, adopted: the rules that govern an edge are the rules
|
|
of the collection the *asserting* page lives in. That is also why the reverse
|
|
edge cannot be written automatically - it would be governed by a contract the
|
|
author never read."""
|
|
_authorising(kb_root, "entities", " concepts: [implements]\n entities: [uses]")
|
|
assert kb_collections.authorised_labels("entities", "concepts") == {"implements"}
|
|
assert kb_collections.authorised_labels("entities", "entities") == {"uses"}
|
|
|
|
|
|
def test_any_widens_every_destination(kb_root):
|
|
_authorising(kb_root, "entities", " any: [see-also]\n concepts: [implements]")
|
|
assert kb_collections.authorised_labels("entities", "concepts") == {"implements", "see-also"}
|
|
assert kb_collections.authorised_labels("entities", "sources") == {"see-also"}
|
|
|
|
|
|
def test_an_undeclared_destination_authorises_nothing(kb_root):
|
|
"""An empty result is a real answer - "do not link there from here" - not a
|
|
missing declaration to be filled in with a permissive default."""
|
|
_authorising(kb_root, "entities", " concepts: [implements]")
|
|
assert kb_collections.authorised_labels("entities", "sources") == set()
|