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