Files
chemenu/tools/chemenu/tests/test_touch.py
T
torben 24cd221b21
CI / verify (push) Successful in 55s
Release / release (push) Successful in 39s
fix: stale wiki/ path literals nach kb/ nachgezogen, mit Test-Guard gegen die naechste Umbenennung
Files changed:
- CHANGES.md
- VERSION
- kb/entities/projects/Chemenu.md
- kb/log.md
- tools/chemenu/commands/_util.py
- tools/chemenu/commands/cite_cmd.py
- tools/chemenu/commands/git_publish.py
- tools/chemenu/commands/log_append.py
- tools/chemenu/commands/page_ops.py
- tools/chemenu/commands/provenance_cmd.py
- tools/chemenu/commands/raw_cmd.py
- tools/chemenu/commands/run_budget.py
- tools/chemenu/commands/touch.py
- tools/chemenu/commands/xref.py
- tools/chemenu/frontmatter_io.py
- tools/chemenu/lint_core.py
- tools/chemenu/tests/test_log_append.py
- tools/chemenu/tests/test_source_hygiene.py
- tools/chemenu/tests/test_touch.py
- tools/chemenu/tests/test_type_resolver.py
- tools/chemenu/type_resolver.py
- tools/wikitool
- types/type-spec.md
- types/type-spec.schema.yaml
2026-09-17 08:59:25 +02:00

277 lines
11 KiB
Python

import datetime
import pytest
import typer
from typer.testing import CliRunner
from chemenu import config
from chemenu.commands.touch import touch_command
from chemenu.frontmatter_io import read_page
runner = CliRunner()
@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,
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 kb/.
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/notes/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/notes/Source - Aurora.md")
_touch(page_title="Source - Aurora", summary="Neue Zusammenfassung")
frontmatter, _ = read_page(touch_wiki / "sources/notes/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_is_refused_with_its_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
def test_confidence_base_flag_no_longer_exists(touch_wiki, monkeypatch):
"""The confidence mechanism is gone (Gitea #60): `--confidence-base` is not
a denylisted field owned elsewhere, it simply does not exist as an option
any more - refused by the CLI parser itself, before `touch_command` runs."""
import chemenu.config as cfg
from chemenu.cli import app
monkeypatch.setattr(cfg, "KB_DIR", touch_wiki)
result = runner.invoke(app, ["touch", "--page", "aurora", "--confidence-base", "0.9"])
assert result.exit_code != 0
assert "confidence-base" in result.output.lower() or "no such option" in result.output.lower()
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/notes/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
# --- Capture fields: fill-once, not a UNSETTABLE denylist (Gitea #67) --------
def test_capture_field_is_writable_while_absent(touch_wiki):
"""The fixture 'Source - Aurora' predates #67 and carries no `fidelity:`
yet - the backfill path `touch --set fidelity=unknown` (or a real value)
must succeed exactly once."""
_touch(page_title="Source - Aurora", set_fields=["fidelity=unknown"])
frontmatter, _ = read_page(touch_wiki / "sources/notes/Source - Aurora.md")
assert frontmatter["fidelity"] == "unknown"
def test_capture_field_is_refused_once_already_set(touch_wiki, capsys):
_touch(page_title="Source - Aurora", set_fields=["authority=reporting"])
with pytest.raises(typer.Exit):
_touch(page_title="Source - Aurora", set_fields=["authority=opinion"])
out = capsys.readouterr().out
assert "capture field" in out
assert "raw accept --replaces" in out
frontmatter, _ = read_page(touch_wiki / "sources/notes/Source - Aurora.md")
assert frontmatter["authority"] == "reporting"
def test_capture_field_set_to_the_same_value_again_is_a_no_op(touch_wiki):
"""Not idempotent by accident: `_apply_set` already no-ops an unchanged
value before the write, and the fill-once refusal only fires on an actual
*change* - re-running the exact same backfill value must not error."""
_touch(page_title="Source - Aurora", set_fields=["fidelity=verbatim"])
_touch(page_title="Source - Aurora", set_fields=["fidelity=verbatim"])
frontmatter, _ = read_page(touch_wiki / "sources/notes/Source - Aurora.md")
assert frontmatter["fidelity"] == "verbatim"