Files
chemenu/tools/chemenu/tests/test_doctor.py
T
torben 18ae28f918
CI / verify (push) Failing after 32s
Release / release (push) Successful in 38s
Chemenu 2.1.0 - deterministischer Wissenskompiler
Chemenu kompiliert Rohnotizen zu einem verlinkten, quellengebundenen Wiki:
raw/ -> types/ + tools/ -> kb/ -> reports/. Was mechanisch ist, macht
tools/wikitool; was Urteil braucht, macht ein Agent unter Contracts, deren
Grenzen in Code durchgesetzt sind statt im Prompt.

Dieser Commit ist der Startpunkt der oeffentlichen Historie. Die vorherige
Entwicklung fand in einer privaten Instanz statt und ist nicht Teil dieses
Repositorys; ihre Erzaehlung steht vollstaendig in CHANGES.md, das mit 44
Eintraegen von 0.1.0 bis 2.1.0 erhalten geblieben ist.

Der mitgelieferte Korpus ist ein Testbett und eine Demo: 170 Seiten ueber den
Stack selbst - Gates, Lint, Versionierung, Suche, das Wiki-Muster. Er
dokumentiert das Werkzeug mit den eigenen Mitteln des Werkzeugs.

Lizenz: AGPL-3.0 fuer den Stack (tools/, types/), CC-BY-4.0 fuer die Inhalte.
Die Grenze zwischen beiden ist der Dateiplan, den dist export berechnet -
siehe NOTICE.
2026-09-01 16:26:14 +02:00

271 lines
10 KiB
Python

"""Tests for `wikitool doctor`: a healthy instance reports all OK/WARN and
never FAIL, and each check independently reports FAIL when its precondition
is missing."""
from __future__ import annotations
import subprocess
from pathlib import Path
import pytest
from chemenu import config
from chemenu.commands import doctor, instructions_cmd
def _git(root: Path, *args: str) -> None:
subprocess.run(["git", *args], cwd=root, check=True, capture_output=True)
@pytest.fixture
def instance(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""A minimal, fully-configured wiki instance: a git repo with identity,
the required stage contracts, one kb collection, generated files, and a
published skill."""
root = tmp_path
kb = root / "kb"
for sub in ("entities", "concepts", "sources", "comparisons"):
(kb / sub).mkdir(parents=True)
(kb / sub / "COLLECTION.md").write_text(f"# {sub}\n", encoding="utf-8")
(kb / "index.md").write_text("# Index\n", encoding="utf-8")
(kb / "log.md").write_text("# Log\n", encoding="utf-8")
(kb / "provenance.md").write_text("# Provenance\n", encoding="utf-8")
(kb / "CONTRACT.md").write_text("# kb contract\n", encoding="utf-8")
(root / "VERSION").write_text("0.1.0\n", encoding="utf-8")
(root / "USER.md").write_text("# USER.md - Fixture\n", encoding="utf-8")
(root / "SOUL.md").write_text("# SOUL.md - Fixture\n", encoding="utf-8")
for relative in ("raw/CONTRACT.md", "reports/CONTRACT.md", "work/CONTRACT.md",
"instructions/CONTRACT.md", "types/type-spec.md"):
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("# contract\n", encoding="utf-8")
instructions = root / "instructions"
(instructions / "wiki-demo").mkdir(parents=True)
(instructions / "wiki-demo" / "SKILL.md").write_text(
"---\nname: wiki-demo\ndescription: Demo skill.\n---\n", encoding="utf-8"
)
monkeypatch.setattr(config, "ROOT", root)
monkeypatch.setattr(config, "KB_DIR", kb)
monkeypatch.setattr(config, "INDEX_FILE", kb / "index.md")
monkeypatch.setattr(config, "LOG_FILE", kb / "log.md")
monkeypatch.setattr(config, "PROVENANCE_FILE", kb / "provenance.md")
monkeypatch.setattr(config, "INSTRUCTIONS_DIR", instructions)
monkeypatch.setattr(config, "AGENTS_SKILLS_DIR", root / ".agents" / "skills")
monkeypatch.setattr(config, "CLAUDE_SKILLS_DIR", root / ".claude" / "skills")
monkeypatch.setenv("WIKI_AUTHOR", "Fixture Author")
monkeypatch.delenv("WIKITOOL_SESSION_ID", raising=False)
_git(root, "init", "-b", "main")
_git(root, "config", "user.name", "Fixture Author")
_git(root, "config", "user.email", "fixture@example.com")
instructions_cmd.sync(force=False)
return root
def _status(checks, name):
return next(c.status for c in checks if c.name == name)
def test_healthy_instance_has_no_fail(instance):
checks = doctor.run_doctor()
assert not any(c.status == "FAIL" for c in checks)
assert _status(checks, "structure") == "OK"
assert _status(checks, "generated-files") == "OK"
assert _status(checks, "skills") == "OK"
assert _status(checks, "author") == "OK"
assert _status(checks, "git-identity") == "OK"
def test_healthy_instance_warns_on_missing_remote_and_session_id(instance):
checks = doctor.run_doctor()
assert _status(checks, "git-remote") == "WARN"
assert _status(checks, "session-id") == "WARN"
def test_stack_version_is_reported(instance):
checks = doctor.run_doctor()
assert _status(checks, "stack-version") == "OK"
detail = next(c.detail for c in checks if c.name == "stack-version")
assert "0.1.0" in detail and "development tree" in detail
def test_a_stamped_instance_reports_itself_as_a_distribution(instance):
(config.ROOT / ".wikitool-release.json").write_text(
'{"version": "0.1.0", "exported_at": "2026-08-29"}', encoding="utf-8"
)
detail = next(c.detail for c in doctor.run_doctor() if c.name == "stack-version")
assert "distribution" in detail and "2026-08-29" in detail
def test_a_missing_version_warns_rather_than_fails(instance):
"""Instances exported before the stack was versioned are still perfectly
functional - they just cannot answer `version check`."""
(config.ROOT / "VERSION").unlink()
checks = doctor.run_doctor()
assert _status(checks, "stack-version") == "WARN"
assert not any(c.status == "FAIL" for c in checks)
def test_a_malformed_version_fails(instance):
(config.ROOT / "VERSION").write_text("v1\n", encoding="utf-8")
assert _status(doctor.run_doctor(), "stack-version") == "FAIL"
def test_kb_version_warns_when_the_content_is_undeclared(instance):
checks = doctor.run_doctor()
assert _status(checks, "kb-version") == "WARN"
assert not any(c.status == "FAIL" for c in checks)
def test_kb_version_is_ok_when_it_matches_the_machinery(instance):
(config.ROOT / ".wikitool-kb.json").write_text(
'{"schema": 1, "kb_version": "0.1.0", "applied": []}', encoding="utf-8"
)
assert _status(doctor.run_doctor(), "kb-version") == "OK"
def test_kb_version_warns_while_a_migration_is_outstanding(instance):
"""The normal, transient state in the middle of an upgrade - a WARN that
names the chain, not a fault."""
(config.ROOT / "VERSION").write_text("2.0.0\n", encoding="utf-8")
(config.ROOT / ".wikitool-kb.json").write_text(
'{"schema": 1, "kb_version": "1.0.0", "applied": []}', encoding="utf-8"
)
migrations = config.INSTRUCTIONS_DIR / "migrations"
migrations.mkdir(parents=True, exist_ok=True)
(migrations / "2.0.0-retype.md").write_text(
"---\ntype: types/instruction.md\nname: 2.0.0-retype\n"
"description: Retype.\nmanual: true\nmigrates_to: 2.0.0\n---\n",
encoding="utf-8",
)
checks = doctor.run_doctor()
assert _status(checks, "kb-version") == "WARN"
assert "outstanding" in next(c.detail for c in checks if c.name == "kb-version")
assert not any(c.status == "FAIL" for c in checks)
def test_an_unreadable_kb_state_fails(instance):
(config.ROOT / ".wikitool-kb.json").write_text("{broken", encoding="utf-8")
assert _status(doctor.run_doctor(), "kb-version") == "FAIL"
def test_no_collections_at_all_fails_structure(instance):
"""Removing one collection is a legitimate state - collections are
discovered by COLLECTION.md presence, not a fixed list (kb/CONTRACT.md).
Having none at all means the kb layer never got its Areas populated."""
import shutil
for sub in ("entities", "concepts", "sources", "comparisons"):
shutil.rmtree(config.KB_DIR / sub)
checks = doctor.run_doctor()
assert _status(checks, "structure") == "FAIL"
def test_missing_stage_contract_fails_structure(instance):
(config.ROOT / "raw" / "CONTRACT.md").unlink()
checks = doctor.run_doctor()
assert _status(checks, "structure") == "FAIL"
def test_personalization_is_ok_when_both_files_are_filled(instance):
assert _status(doctor.run_doctor(), "personalization") == "OK"
def test_missing_personalization_fails(instance):
"""`USER.md`/`SOUL.md` are read every session, so an instance without them
runs a generic agent against a wiki built for one person."""
(config.ROOT / "SOUL.md").unlink()
checks = doctor.run_doctor()
assert _status(checks, "personalization") == "FAIL"
assert "SOUL.md" in next(c.detail for c in checks if c.name == "personalization")
def test_a_renamed_but_unfilled_template_fails(instance):
"""The failure mode a plain existence check would miss: the file is
present and answers nothing."""
(config.ROOT / "USER.md").write_text(
f"<!-- {config.TEMPLATE_SENTINEL} -->\n# USER.md - <Name>\n", encoding="utf-8"
)
checks = doctor.run_doctor()
assert _status(checks, "personalization") == "FAIL"
assert "template" in next(c.detail for c in checks if c.name == "personalization")
def test_environment_is_ok_when_absent(instance):
"""The file is optional, so absence is a healthy end state - a FAIL here
would make it mandatory through the back door."""
assert not (config.ROOT / config.ENVIRONMENT_FILE).exists()
assert _status(doctor.run_doctor(), "environment") == "OK"
def test_environment_is_ok_when_filled(instance):
(config.ROOT / config.ENVIRONMENT_FILE).write_text(
"# ENVIRONMENT.md - Fixture\n\n- Harness: none\n", encoding="utf-8"
)
assert _status(doctor.run_doctor(), "environment") == "OK"
def test_a_renamed_but_unfilled_environment_template_warns(instance):
"""Present, loaded into every session, and answering nothing - worse than
absent, which is at least honest. A WARN, not a FAIL: the fix may well be
to delete the file again."""
(config.ROOT / config.ENVIRONMENT_FILE).write_text(
f"<!-- {config.TEMPLATE_SENTINEL} -->\n# ENVIRONMENT.md - <Instanz>\n", encoding="utf-8"
)
checks = doctor.run_doctor()
assert _status(checks, "environment") == "WARN"
assert "template" in next(c.detail for c in checks if c.name == "environment")
def test_missing_generated_file_fails(instance):
config.LOG_FILE.unlink()
checks = doctor.run_doctor()
assert _status(checks, "generated-files") == "FAIL"
def test_unpublished_skills_fail(instance):
import shutil
shutil.rmtree(config.AGENTS_SKILLS_DIR)
shutil.rmtree(config.CLAUDE_SKILLS_DIR)
checks = doctor.run_doctor()
assert _status(checks, "skills") == "FAIL"
def test_no_author_fails(instance, monkeypatch):
monkeypatch.delenv("WIKI_AUTHOR", raising=False)
monkeypatch.setattr(config, "default_author", lambda: None)
checks = doctor.run_doctor()
assert _status(checks, "author") == "FAIL"
def test_not_a_git_repo_fails(tmp_path, monkeypatch):
monkeypatch.setattr(config, "ROOT", tmp_path)
checks = doctor.run_doctor()
assert _status(checks, "git-repo") == "FAIL"
def test_doctor_command_exits_nonzero_only_on_fail(instance, capsys):
import typer
doctor.doctor_command(json_out=False) # no FAIL - must not raise
out = capsys.readouterr().out
assert "OK" in out
with pytest.raises(typer.Exit) as excinfo:
config.LOG_FILE.unlink()
doctor.doctor_command(json_out=False)
assert excinfo.value.exit_code == 1
def test_doctor_json_is_machine_readable(instance, capsys):
import json
doctor.doctor_command(json_out=True)
rows = json.loads(capsys.readouterr().out)
assert any(row["name"] == "structure" for row in rows)
assert all({"name", "status", "detail", "fix"} <= row.keys() for row in rows)