f63a72cb24
Files changed: - CHANGES.md - VERSION - tools/chemenu/tests/conftest.py - tools/chemenu/tests/test_corpus_diff.py - tools/chemenu/tests/test_index_build.py - tools/chemenu/tests/test_kb_collections.py - tools/chemenu/tests/test_lint.py - tools/chemenu/tests/test_migrate_cmd.py - tools/chemenu/tests/test_new_page.py - tools/chemenu/tests/test_page_ops.py - tools/chemenu/tests/test_provenance.py - tools/chemenu/tests/test_search.py - tools/chemenu/tests/test_touch.py
233 lines
9.3 KiB
Python
233 lines
9.3 KiB
Python
import datetime
|
|
|
|
import pytest
|
|
import typer
|
|
|
|
from chemenu import config
|
|
from chemenu.commands.touch import touch_command
|
|
from chemenu.frontmatter_io import read_page
|
|
|
|
|
|
@pytest.fixture
|
|
def touch_wiki(kb_dir, monkeypatch):
|
|
monkeypatch.setattr(config, "KB_DIR", kb_dir)
|
|
return kb_dir
|
|
|
|
|
|
def _touch(**overrides):
|
|
"""Call the Typer callback with every option supplied.
|
|
|
|
A callback invoked directly from a test receives `OptionInfo` objects for
|
|
whatever the caller leaves out, so the defaults live here instead of being
|
|
repeated in each test - and a new option costs one line rather than one per
|
|
call site. Same hazard `dist_cmd` avoids by keeping its logic beside the
|
|
wrapper.
|
|
"""
|
|
kwargs = dict(
|
|
page_title=None,
|
|
summary=None,
|
|
provenance=None,
|
|
confidence_base=None,
|
|
date=None,
|
|
set_fields=None,
|
|
add_fields=None,
|
|
remove_fields=None,
|
|
no_date=False,
|
|
dry_run=False,
|
|
)
|
|
kwargs.update(overrides)
|
|
return touch_command(**kwargs)
|
|
|
|
|
|
def test_touch_bumps_modified(touch_wiki):
|
|
_touch(page_title="aurora")
|
|
frontmatter, _ = read_page(touch_wiki / "entities/systems/aurora.md")
|
|
# Unquoted YAML dates round-trip as date objects, matching the rest of wiki/.
|
|
assert str(frontmatter["modified"]) == datetime.date.today().isoformat()
|
|
|
|
|
|
def test_touch_updates_summary_and_provenance(touch_wiki):
|
|
_touch(
|
|
page_title="aurora", summary="Now with a better summary", provenance="mixed",
|
|
date="2026-08-13",
|
|
)
|
|
frontmatter, _ = read_page(touch_wiki / "entities/systems/aurora.md")
|
|
assert frontmatter["summary"] == "Now with a better summary"
|
|
assert frontmatter["provenance"] == "mixed"
|
|
assert str(frontmatter["modified"]) == "2026-08-13"
|
|
|
|
|
|
def test_touch_rejects_invalid_provenance(touch_wiki):
|
|
"""Schema validation runs before the write, so a bad value can never land
|
|
on disk the way a hand-edit could."""
|
|
with pytest.raises(typer.Exit):
|
|
_touch(page_title="aurora", provenance="hearsay")
|
|
frontmatter, _ = read_page(touch_wiki / "entities/systems/aurora.md")
|
|
assert "provenance" not in frontmatter or frontmatter["provenance"] != "hearsay"
|
|
|
|
|
|
def test_touch_dry_run_writes_nothing(touch_wiki):
|
|
path = touch_wiki / "entities/systems/aurora.md"
|
|
before = path.read_text(encoding="utf-8")
|
|
_touch(page_title="aurora", summary="ignored", dry_run=True)
|
|
assert path.read_text(encoding="utf-8") == before
|
|
|
|
|
|
def test_touch_fails_on_unknown_page(touch_wiki):
|
|
with pytest.raises(typer.Exit):
|
|
_touch(page_title="Nope")
|
|
|
|
|
|
def test_touch_uses_date_field_for_source_pages(touch_wiki):
|
|
"""Source pages declare `date:`, not `modified:` - the field comes from
|
|
the type's schema rather than a hardcoded name."""
|
|
_touch(page_title="Source - Aurora", date="2026-08-13")
|
|
frontmatter, _ = read_page(touch_wiki / "sources/Source - Aurora.md")
|
|
assert str(frontmatter["date"]) == "2026-08-13"
|
|
assert "modified" not in frontmatter
|
|
|
|
|
|
def test_touch_leaves_a_sources_publication_date_alone(touch_wiki):
|
|
"""A source's `date:` is the publication date of the raw material, not a
|
|
record of when the page was last edited. Auto-bumping it to today replaced a
|
|
fact about the world and left the page contradicting the date printed in its
|
|
own body - so it moves only on an explicit --date."""
|
|
before, _ = read_page(touch_wiki / "sources/Source - Aurora.md")
|
|
_touch(page_title="Source - Aurora", summary="Neue Zusammenfassung")
|
|
frontmatter, _ = read_page(touch_wiki / "sources/Source - Aurora.md")
|
|
assert frontmatter["summary"] == "Neue Zusammenfassung"
|
|
assert str(frontmatter["date"]) == str(before["date"])
|
|
|
|
|
|
# --- --set / --add / --remove -------------------------------------------------
|
|
|
|
|
|
def test_set_replaces_a_field_new_wrote_once(touch_wiki):
|
|
"""The defect this exists for: `tags:` was writable at `new` and never
|
|
again, so a mistyped list was permanent."""
|
|
_touch(page_title="aurora", set_fields=["tags=k8s,storage"])
|
|
frontmatter, _ = read_page(touch_wiki / "entities/systems/aurora.md")
|
|
assert frontmatter["tags"] == ["k8s", "storage"]
|
|
|
|
|
|
def test_set_replaces_rather_than_merging(touch_wiki):
|
|
before, _ = read_page(touch_wiki / "entities/systems/aurora.md")
|
|
assert before["tags"] == ["server"]
|
|
_touch(page_title="aurora", set_fields=["tags=only-this"])
|
|
frontmatter, _ = read_page(touch_wiki / "entities/systems/aurora.md")
|
|
assert frontmatter["tags"] == ["only-this"]
|
|
|
|
|
|
def test_repeated_set_appends_within_one_call(touch_wiki):
|
|
"""Same rule as `new --set`: the separator-free way to pass an element
|
|
containing a comma."""
|
|
_touch(page_title="aurora", set_fields=["tags=a", "tags=b"])
|
|
frontmatter, _ = read_page(touch_wiki / "entities/systems/aurora.md")
|
|
assert frontmatter["tags"] == ["a", "b"]
|
|
|
|
|
|
def test_set_honours_the_comma_escape(touch_wiki):
|
|
_touch(page_title="aurora", set_fields=[r"tags=one\, two,three"])
|
|
frontmatter, _ = read_page(touch_wiki / "entities/systems/aurora.md")
|
|
assert frontmatter["tags"] == ["one, two", "three"]
|
|
|
|
|
|
def test_add_extends_without_naming_the_whole_list(touch_wiki):
|
|
_touch(page_title="aurora", add_fields=["tags=monitoring"])
|
|
frontmatter, _ = read_page(touch_wiki / "entities/systems/aurora.md")
|
|
assert frontmatter["tags"] == ["server", "monitoring"]
|
|
|
|
|
|
def test_add_is_idempotent(touch_wiki):
|
|
_touch(page_title="aurora", add_fields=["tags=server"])
|
|
frontmatter, _ = read_page(touch_wiki / "entities/systems/aurora.md")
|
|
assert frontmatter["tags"] == ["server"]
|
|
|
|
|
|
def test_remove_drops_an_element(touch_wiki):
|
|
_touch(page_title="aurora", add_fields=["tags=temporary"])
|
|
_touch(page_title="aurora", remove_fields=["tags=temporary"])
|
|
frontmatter, _ = read_page(touch_wiki / "entities/systems/aurora.md")
|
|
assert frontmatter["tags"] == ["server"]
|
|
|
|
|
|
def test_remove_of_an_absent_element_succeeds_and_says_so(touch_wiki, capsys):
|
|
"""Idempotent like `xref remove`, but never silent: a silent no-op looks
|
|
exactly like a successful removal, which is how a typo hides."""
|
|
_touch(page_title="aurora", remove_fields=["tags=never-there"])
|
|
out = capsys.readouterr().out
|
|
assert "not present, nothing removed" in out
|
|
assert "never-there" in out
|
|
frontmatter, _ = read_page(touch_wiki / "entities/systems/aurora.md")
|
|
assert frontmatter["tags"] == ["server"]
|
|
|
|
|
|
def test_add_refuses_a_scalar_field(touch_wiki):
|
|
with pytest.raises(typer.Exit):
|
|
_touch(page_title="aurora", add_fields=["summary=more"])
|
|
|
|
|
|
def test_page_reference_fields_are_refused_and_name_xref(touch_wiki, capsys):
|
|
"""`xref` maintains the reverse direction and the body bullets; a bare
|
|
frontmatter write would leave the other half stale."""
|
|
for field in ("related", "sources", "entities", "concepts"):
|
|
with pytest.raises(typer.Exit):
|
|
_touch(page_title="aurora", set_fields=[f"{field}=Borealis"])
|
|
assert "xref" in capsys.readouterr().out
|
|
|
|
|
|
def test_type_and_confidence_are_refused_with_their_owner(touch_wiki, capsys):
|
|
with pytest.raises(typer.Exit):
|
|
_touch(page_title="aurora", set_fields=["type=types/concept.md"])
|
|
assert "page-lifecycle" in capsys.readouterr().out
|
|
|
|
with pytest.raises(typer.Exit):
|
|
_touch(page_title="aurora", set_fields=["confidence=0.99"])
|
|
assert "confidence-base" in capsys.readouterr().out
|
|
|
|
|
|
def test_unknown_field_lists_what_the_page_actually_has(touch_wiki, capsys):
|
|
"""A typo, not a routing problem - so the message answers 'what did I mean'
|
|
rather than naming another command."""
|
|
with pytest.raises(typer.Exit):
|
|
_touch(page_title="aurora", set_fields=["tag=k8s"])
|
|
out = capsys.readouterr().out
|
|
assert "declares no field 'tag'" in out
|
|
assert "tags" in out
|
|
|
|
|
|
def test_set_validates_against_the_schema_before_writing(touch_wiki):
|
|
path = touch_wiki / "entities/systems/aurora.md"
|
|
before = path.read_text(encoding="utf-8")
|
|
with pytest.raises(typer.Exit):
|
|
_touch(page_title="aurora", set_fields=["entity_type=not-a-real-entity-type"])
|
|
assert path.read_text(encoding="utf-8") == before
|
|
|
|
|
|
def test_set_raw_files_checks_the_path_exists(touch_wiki, raw_dir, monkeypatch):
|
|
"""`touch` writes this field now, so it owes the same filesystem check
|
|
`new` does - the schema cannot express it."""
|
|
monkeypatch.setattr(config, "ROOT", touch_wiki.parent)
|
|
with pytest.raises(typer.Exit):
|
|
_touch(page_title="Source - Aurora", set_fields=["raw_files=raw/notes/absent.md"])
|
|
|
|
|
|
def test_set_raw_files_accepts_an_existing_path_with_a_comma(touch_wiki, raw_dir, monkeypatch):
|
|
"""The repair the whole issue started from: a raw file moved, and the page
|
|
pointing at it has to follow without anyone editing frontmatter."""
|
|
monkeypatch.setattr(config, "ROOT", touch_wiki.parent)
|
|
(raw_dir / "notes" / "Versioning, CI-CD.md").write_text("# notes\n", encoding="utf-8")
|
|
_touch(
|
|
page_title="Source - Aurora",
|
|
set_fields=[r"raw_files=raw/notes/Versioning\, CI-CD.md"],
|
|
)
|
|
frontmatter, _ = read_page(touch_wiki / "sources/Source - Aurora.md")
|
|
assert frontmatter["raw_files"] == ["raw/notes/Versioning, CI-CD.md"]
|
|
|
|
|
|
def test_dry_run_covers_set_too(touch_wiki):
|
|
path = touch_wiki / "entities/systems/aurora.md"
|
|
before = path.read_text(encoding="utf-8")
|
|
_touch(page_title="aurora", set_fields=["tags=nope"], dry_run=True)
|
|
assert path.read_text(encoding="utf-8") == before
|