Files
chemenu/tools/chemenu/tests/test_doctor.py
T
torbenandClaude Code cceb9a7bfe
CI / verify (push) Successful in 8m41s
CI / pwsh (push) Successful in 2m44s
Release / release (push) Successful in 35s
Release / wiki-mirror (push) Skipped
intake: several configured intake channels (mcp, folder, command) all end in quarantine/ (was mcp-upload/), and upload collect pulls the synced-folder and command channels into it (#213); 10.0.0-beta.11
Files changed:
- .gitignore
- CHANGES.md
- VERSION
- docs/explanation/architecture.md
- docs/explanation/pipeline-rationale.md
- docs/how-to/README.md
- docs/how-to/back-up-to-a-synced-folder.md
- docs/how-to/collect-from-a-synced-folder.md
- docs/how-to/ingest-a-source.md
- docs/how-to/set-up-the-mcp-server.md
- docs/reference/commands/README.md
- docs/reference/commands/git.md
- docs/reference/commands/instance-health.md
- docs/reference/commands/raw-material-and-uploads.md
- docs/reference/configuration.md
- instructions/gates.md
- instructions/ingest-queue.md
- instructions/mcp-read-server.md
- instructions/migrations/10.0.0-settings-tiers.md
- instructions/wiki-ingest/SKILL.md
- raw/CONTRACT.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli_contract.py
- tools/chemenu/collect.py
- tools/chemenu/commands/backup_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/raw/pending.py
- tools/chemenu/commands/upload_cmd.py
- tools/chemenu/config.py
- tools/chemenu/config_shapes.py
- tools/chemenu/legacy_settings.py
- tools/chemenu/mcp/server.py
- tools/chemenu/reference_doc.py
- tools/chemenu/tests/test_backup.py
- tools/chemenu/tests/test_collect.py
- tools/chemenu/tests/test_config_shapes.py
- tools/chemenu/tests/test_doctor.py
- tools/chemenu/tests/test_legacy_settings.py
- tools/chemenu/tests/test_mcp_server.py
- tools/chemenu/tests/test_upload.py
- tools/chemenu/tests/test_upload_cmd.py
- tools/chemenu/upload.py

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
2026-10-12 22:04:18 +02:00

816 lines
33 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 json
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
from chemenu import config, conventions, prerequisites, toolpaths
from chemenu.commands import doctor, instructions_cmd
from chemenu.tests.conftest import write_settings
def _git(root: Path, *args: str) -> None:
subprocess.run(["git", *args], cwd=root, check=True, capture_output=True)
def _write_tools_file(path: Path, tools: dict, complete: bool = True) -> Path:
path.write_text(
json.dumps({"schema": 1, "complete": complete, "tools": tools}, indent=2) + "\n",
encoding="utf-8",
)
return path
def _real_tools() -> dict:
return {"python": sys.executable, "git": shutil.which("git"), "rg": shutil.which("rg")}
@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"---\nprofile: {sub}\nrequired_by_stack: "
f"{'true' if sub == 'sources' else 'false'}\n---\n\n# {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")
(kb / "CONVENTIONS.md").write_text(
"---\nlanguage: en\nprofile: none\nsections:\n"
" links: Relationships\n footnotes: Footnotes\n"
"---\n\n# conventions\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",
"sites/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")
# What the preflight would have written: every tool this platform needs,
# at the path it really has on this machine.
tools_file = _write_tools_file(root / toolpaths.FILE_NAME, _real_tools())
monkeypatch.setattr(toolpaths, "tools_file", lambda: tools_file)
instructions_cmd.sync(force=False)
return root
def _status(checks, name):
return next(c.status for c in checks if c.name == name)
def _detail(checks, name) -> str:
return next(c.detail for c in checks if c.name == name)
def _fix(checks, name) -> str:
return next(c.fix or "" 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_a_running_candidate_is_named_as_such(instance):
(config.ROOT / "VERSION").write_text("0.2.0-beta.1\n", encoding="utf-8")
detail = next(c.detail for c in doctor.run_doctor() if c.name == "stack-version")
assert "0.2.0-beta.1" in detail
assert "candidate" in detail
def test_kb_version_chain_still_reaches_a_target_matching_a_running_candidate(instance):
"""Regression for finding #3: comparing the chain against the raw
candidate would sort `2.0.0` (the migration's target) *before*
`2.0.0-beta.1` (what is installed), dropping it out of the owed range."""
(config.ROOT / "VERSION").write_text("2.0.0-beta.1\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")
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_conventions_are_ok_when_declared(instance):
checks = doctor.run_doctor()
assert _status(checks, "conventions") == "OK"
assert "Relationships" in next(c.detail for c in checks if c.name == "conventions")
def test_missing_conventions_fail(instance):
"""Unlike `ENVIRONMENT.md`, this one is not optional: `xref add` and
`cite add` write headings out of it, so an instance without it is being
answered by whatever the stack hardcoded before the file existed."""
(config.KB_DIR / conventions.CONVENTIONS_FILENAME).unlink()
checks = doctor.run_doctor()
assert _status(checks, "conventions") == "FAIL"
assert "missing" in next(c.detail for c in checks if c.name == "conventions")
def test_conventions_with_an_incomplete_sections_block_fail(instance):
"""Present and deciding nothing - the same failure mode the personalization
sentinel check exists for, one directory down."""
(config.KB_DIR / conventions.CONVENTIONS_FILENAME).write_text(
"---\nlanguage: en\nsections:\n links: Relationships\n---\n", encoding="utf-8"
)
conventions.reset_cache()
assert _status(doctor.run_doctor(), "conventions") == "FAIL"
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")
_THRESHOLDS = {"stalled_waiting_days": 14, "unpaged_project_weeks": 3, "someday_stale_months": 5}
def _remotes_detail(checks) -> str:
return next(c.detail for c in checks if c.name == "publish-remotes")
def test_publish_remotes_says_not_armed_when_the_section_is_absent(instance):
"""Absence stays OK - a single-remote checkout has nothing to protect - but
the line has to say the gate is off. AGENTS.md lists it among the limits
enforced in code, so "no remotes section" alone leaves a reader
trusting a safeguard that is not running."""
checks = doctor.run_doctor()
assert _status(checks, "publish-remotes") == "OK"
assert "not armed" in _remotes_detail(checks)
def test_publish_remotes_says_armed_when_the_section_lists_a_target(instance, monkeypatch):
write_settings(config.ROOT, monkeypatch,
remotes={"allowed_push_urls": ["ssh://git@example.net/one.git"]})
checks = doctor.run_doctor()
assert _status(checks, "publish-remotes") == "OK"
detail = _remotes_detail(checks)
assert "armed" in detail and "not armed" not in detail
def test_publish_remotes_warns_on_several_remotes_without_an_allowlist(instance):
"""The shape a private instance has once it adds the public upstream."""
_git(config.ROOT, "remote", "add", "origin", "ssh://git@example.net/mine.git")
_git(config.ROOT, "remote", "add", "upstream", "ssh://git@example.net/theirs.git")
checks = doctor.run_doctor()
assert _status(checks, "publish-remotes") == "WARN"
assert "not armed" in _remotes_detail(checks)
def test_upload_intake_ok_when_no_section(instance):
checks = doctor.run_doctor()
assert _status(checks, "upload-intake") == "OK"
assert "no channel" in _detail(checks, "upload-intake")
_MCP = {"handler": "mcp", "max_bytes": 100, "allowed_extensions": [".md"],
"quota_submissions_per_day": 1, "quota_bytes_per_day": 100}
def test_upload_intake_names_each_channel_with_its_handler(instance, monkeypatch, tmp_path_factory):
folder = tmp_path_factory.mktemp("synced")
write_settings(config.ROOT, monkeypatch, intake={"channels": {
"mcp": _MCP,
"onedrive": {"handler": "folder", "path": str(folder), "max_bytes": 100, "allowed_extensions": [".md"]},
}})
checks = doctor.run_doctor()
assert _status(checks, "upload-intake") == "OK"
detail = _detail(checks, "upload-intake")
assert "mcp (mcp)" in detail and "onedrive (folder)" in detail
assert "0 submission(s) waiting" in detail
def test_upload_intake_warns_when_a_folder_does_not_exist(instance, monkeypatch, tmp_path_factory):
write_settings(config.ROOT, monkeypatch, intake={"channels": {
"onedrive": {"handler": "folder", "path": str(tmp_path_factory.mktemp("outside") / "gone"), "max_bytes": 100,
"allowed_extensions": [".md"]},
}})
checks = doctor.run_doctor()
assert _status(checks, "upload-intake") == "WARN"
assert "onedrive" in _detail(checks, "upload-intake")
def test_upload_intake_fails_on_a_malformed_section(instance, monkeypatch):
write_settings(config.ROOT, monkeypatch, intake={"channels": {"mcp": {"handler": "mcp", "max_bytes": "many"}}})
checks = doctor.run_doctor()
assert _status(checks, "upload-intake") == "FAIL"
def test_upload_intake_fails_on_a_leftover_mcp_upload_directory(instance):
waiting = config.ROOT / "mcp-upload" / "sub-1"
waiting.mkdir(parents=True)
(waiting / "a.md").write_bytes(b"waiting")
checks = doctor.run_doctor()
assert _status(checks, "upload-intake") == "FAIL"
assert "mv mcp-upload quarantine" in _detail(checks, "upload-intake")
assert (waiting / "a.md").read_bytes() == b"waiting"
def test_tasks_provider_ok_when_no_section(instance):
checks = doctor.run_doctor()
assert _status(checks, "tasks-provider") == "OK"
assert "no task tracker configured" in _detail(checks, "tasks-provider")
def test_tasks_provider_fails_on_a_malformed_settings_file(instance):
(config.ROOT / config.LOCAL_SETTINGS_FILENAME).write_text("{not json", encoding="utf-8")
checks = doctor.run_doctor()
assert _status(checks, "tasks-provider") == "FAIL"
def test_tasks_provider_fails_on_a_bad_superproductivity_section(instance, monkeypatch):
write_settings(config.ROOT, monkeypatch, tasks={"provider": "superproductivity", **_THRESHOLDS})
checks = doctor.run_doctor()
assert _status(checks, "tasks-provider") == "FAIL"
def test_tasks_provider_ok_but_names_the_unready_read_path_when_configured(instance, monkeypatch):
"""`access: snapshot` - only the read path is a finding; the API is not
even attempted (Gitea #133)."""
backups = config.ROOT / "sp-backups"
write_settings(config.ROOT, monkeypatch,
tasks={"provider": "superproductivity", **_THRESHOLDS},
superproductivity={"access": "snapshot", "backups_dir": str(backups)})
checks = doctor.run_doctor()
assert _status(checks, "tasks-provider") == "OK"
detail = _detail(checks, "tasks-provider")
assert "access=snapshot" in detail
assert "not ready" in detail
def test_tasks_provider_ok_but_names_api_unreachable_when_configured(instance, monkeypatch):
"""`access: api` - the API reachability is the finding; the (irrelevant)
backup file is not even looked at (Gitea #133)."""
write_settings(config.ROOT, monkeypatch,
tasks={"provider": "superproductivity", **_THRESHOLDS},
superproductivity={"access": "api", "api_base_url": "http://127.0.0.1:1",
"api_token": "t"})
checks = doctor.run_doctor()
assert _status(checks, "tasks-provider") == "OK"
detail = _detail(checks, "tasks-provider")
assert "access=api" in detail
assert "not reachable" in detail
def test_tasks_provider_fails_on_a_bad_caldav_section(instance, monkeypatch):
write_settings(config.ROOT, monkeypatch, tasks={"provider": "caldav", **_THRESHOLDS})
checks = doctor.run_doctor()
assert _status(checks, "tasks-provider") == "FAIL"
def test_tasks_provider_ok_but_names_caldav_unreachable_when_configured(instance, monkeypatch):
write_settings(config.ROOT, monkeypatch, tasks={"provider": "caldav", **_THRESHOLDS}, caldav={
"url": "http://127.0.0.1:1/cal/", "username": "u", "app_password": "p",
"inbox_list": "Inbox", "someday_list": "Someday",
})
checks = doctor.run_doctor()
assert _status(checks, "tasks-provider") == "OK"
detail = _detail(checks, "tasks-provider")
assert "caldav" in detail
assert "unreachable" in detail
@pytest.mark.parametrize("name", [".wikitool-remotes.json", ".wikitool-telemetry.json",
".wikitool-upload.json", ".wikitool-tasks.json"])
def test_a_leftover_settings_file_of_an_earlier_version_fails(instance, name):
(config.ROOT / name).write_text("{}", encoding="utf-8")
checks = doctor.run_doctor()
failing = [c for c in checks if c.name == "legacy-settings"]
assert [c.status for c in failing] == ["FAIL"]
assert name in failing[0].detail
assert "tools/wikitool config migrate" in (failing[0].fix or "")
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_a_changed_conversation_language_fails_skills_and_names_the_block(instance):
(config.KB_DIR / conventions.CONVENTIONS_FILENAME).write_text(
"---\nlanguage: de\nprofile: none\nsections:\n links: Relationships\n"
" footnotes: Footnotes\n---\n\n# conventions\n",
encoding="utf-8",
)
conventions.reset_cache()
checks = doctor.run_doctor()
assert _status(checks, "skills") == "FAIL"
assert "2 published copy/copies drifted" in _detail(checks, "skills")
assert "language block is for `en`, conventions say `de`" in _detail(checks, "skills")
instructions_cmd.sync(force=False)
assert _status(doctor.run_doctor(), "skills") == "OK"
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_telemetry_is_on_by_default_for_a_dev_checkout(instance):
checks = doctor.run_doctor()
assert _status(checks, "telemetry") == "OK"
detail = next(c.detail for c in checks if c.name == "telemetry")
assert detail.startswith("on (")
assert "0/250 session(s)" in detail
def test_telemetry_is_off_by_default_for_a_stamped_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 == "telemetry")
assert detail.startswith("off (")
assert ".wikitool-release.json present" in detail
def test_telemetry_never_fails_either_way(instance):
(config.ROOT / ".wikitool-release.json").write_text(
'{"version": "0.1.0", "exported_at": "2026-08-29"}', encoding="utf-8"
)
assert _status(doctor.run_doctor(), "telemetry") == "OK"
def _telemetry_with(section: "str | None", raw: "str | None" = None):
"""`check_telemetry` against a `telemetry` section of `.wikitool-local.json`
holding the JSON text `section` (absent for None; `raw` replaces the whole
file instead), plus what the policy itself makes of it."""
from chemenu.telemetry import policy
path = config.ROOT / config.LOCAL_SETTINGS_FILENAME
if raw is not None:
path.write_text(raw, encoding="utf-8")
elif section is None:
path.unlink(missing_ok=True)
else:
path.write_text('{"schema": 1, "telemetry": %s}' % section, encoding="utf-8")
policy.reset_cache()
try:
return doctor.check_telemetry(), policy.resolve(config.ROOT)
finally:
policy.reset_cache()
@pytest.mark.parametrize("text", [None, '{"enabled": false}', '{"enabled": true, '
'"max_session_bytes": 1000, "keep_sessions": 9}'])
def test_telemetry_stays_ok_for_an_absent_or_conforming_section(instance, text):
check, _ = _telemetry_with(text)
assert check.status == "OK"
assert check.fix is None
@pytest.mark.parametrize("text,named,policy_reads", [
("[true]", "object", (True, 5 * 1024 * 1024, 250)),
('{"enabled": "no"}', "enabled", (True, 5 * 1024 * 1024, 250)),
('{"max_session_bytes": 0}', "max_session_bytes", (True, 5 * 1024 * 1024, 250)),
('{"keep_sessions": "12"}', "keep_sessions", (True, 5 * 1024 * 1024, 12)),
('{"enabled": false, "keep_session": 12}', "keep_session", (False, 5 * 1024 * 1024, 250)),
])
def test_telemetry_warns_on_what_the_policy_reads_differently(instance, text, named, policy_reads):
"""The policy stays silent and reads the section exactly as before; `doctor`
is where the deviation becomes visible, by field."""
check, pol = _telemetry_with(text)
assert check.status == "WARN"
assert named in check.detail
assert "telemetry" in check.fix
assert (pol.enabled, pol.max_session_bytes, pol.keep_sessions) == policy_reads
def test_telemetry_warns_on_an_unreadable_settings_file_and_the_policy_stays_on_its_default(instance):
check, pol = _telemetry_with(None, raw="{not json")
assert check.status == "WARN"
assert "unreadable" in check.detail
assert (pol.enabled, pol.max_session_bytes, pol.keep_sessions) == (True, 5 * 1024 * 1024, 250)
def test_telemetry_reports_the_current_session_count_and_byte_total(instance, monkeypatch):
from chemenu.telemetry.writer import TRACE_FILE, trace_root
trace_dir = config.ROOT / "traces"
monkeypatch.setenv("WIKI_TRACE_DIR", str(trace_dir))
for name, content in (("a", "111"), ("b", "22222")):
session_dir = trace_root() / name
session_dir.mkdir(parents=True)
(session_dir / TRACE_FILE).write_text(content, encoding="utf-8")
detail = next(c.detail for c in doctor.run_doctor() if c.name == "telemetry")
assert "2/250 session(s)" in detail
assert "8 byte(s)" in detail # "111" + "22222"
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)
def test_tasks_provider_fails_when_the_override_names_a_missing_file(instance, monkeypatch):
monkeypatch.setenv("WIKITOOL_TASKS_CONFIG", str(config.ROOT / "gone.json"))
checks = doctor.run_doctor()
assert _status(checks, "tasks-provider") == "FAIL"
assert "gone.json" in _detail(checks, "tasks-provider")
def test_tasks_provider_names_an_active_override(instance, monkeypatch):
profile = config.ROOT / "profile.json"
profile.write_text(
json.dumps({
"schema": 1,
"tasks": {"provider": "superproductivity", **_THRESHOLDS},
"superproductivity": {"access": "api", "api_base_url": "http://127.0.0.1:1",
"api_token": {"ref": "profile_token"}},
}),
encoding="utf-8",
)
monkeypatch.setenv("WIKITOOL_SECRET_PROFILE_TOKEN", "t")
monkeypatch.setenv("WIKITOOL_TASKS_CONFIG", str(profile))
checks = doctor.run_doctor()
assert _status(checks, "tasks-provider") == "OK"
assert "WIKITOOL_TASKS_CONFIG" in _detail(checks, "tasks-provider")
# --- tool paths and the install folder (Gitea #151) --------------------------
def test_tool_paths_missing_file_is_a_fail_naming_the_preflight(instance):
(instance / toolpaths.FILE_NAME).unlink()
checks = doctor.run_doctor()
assert _status(checks, "tool-paths") == "FAIL"
fix = next(c.fix for c in checks if c.name == "tool-paths")
assert "tools/preflight.sh" in fix
def test_tool_paths_incomplete_run_is_a_fail(instance):
_write_tools_file(instance / toolpaths.FILE_NAME, _real_tools(), complete=False)
assert _status(doctor.run_doctor(), "tool-paths") == "FAIL"
def test_tool_paths_vanished_path_is_a_fail(instance):
tools = {**_real_tools(), "rg": str(instance / "gone" / "rg")}
_write_tools_file(instance / toolpaths.FILE_NAME, tools)
checks = doctor.run_doctor()
assert _status(checks, "tool-paths") == "FAIL"
assert "no longer there: rg" in _detail(checks, "tool-paths")
# The same broken path makes the ripgrep check fail instead of finding rg on PATH.
assert _status(checks, "ripgrep") == "FAIL"
def test_tool_paths_unrecorded_tool_is_a_fail(instance):
tools = _real_tools()
del tools["git"]
_write_tools_file(instance / toolpaths.FILE_NAME, tools)
checks = doctor.run_doctor()
assert _status(checks, "tool-paths") == "FAIL"
assert "not recorded: git" in _detail(checks, "tool-paths")
def test_tool_paths_complete_file_is_ok(instance):
assert _status(doctor.run_doctor(), "tool-paths") == "OK"
def _folder_of(length: int) -> str:
return "C:\\" + "x" * (length - 3)
@pytest.mark.parametrize("length, longpaths, expected", [
(95, "0", "OK"),
(96, "0", "FAIL"),
(96, "1", "OK"),
])
def test_install_dir_limit_at_the_boundary(instance, monkeypatch, length, longpaths, expected):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "windows")
monkeypatch.setenv(prerequisites.ENV_LONGPATHS, longpaths)
folder = _folder_of(length)
assert len(folder) == length
monkeypatch.setattr(prerequisites, "install_dir", lambda: folder)
assert _status(doctor.run_doctor(), "install-dir") == expected
def test_install_dir_is_not_limited_off_windows(instance, monkeypatch):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "linux")
monkeypatch.setattr(prerequisites, "install_dir", lambda: "/" + "x" * 300)
assert _status(doctor.run_doctor(), "install-dir") == "OK"
# --- PowerShell: execution policy and Mark of the Web (Gitea #151) -----------------
OPEN_POLICY = "MachinePolicy=Undefined,UserPolicy=Undefined,Process=Undefined,CurrentUser=Undefined,LocalMachine=RemoteSigned"
def _windows(monkeypatch, policy=None, marked=None):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "windows")
if policy is not None:
monkeypatch.setenv(prerequisites.ENV_POLICY, policy)
if marked is not None:
monkeypatch.setenv(prerequisites.ENV_MARKED, marked)
@pytest.mark.parametrize("check", ["execution-policy", "script-marks"])
def test_powershell_checks_do_not_apply_off_windows(instance, monkeypatch, check):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "linux")
monkeypatch.setenv(prerequisites.ENV_POLICY, "CurrentUser=AllSigned")
monkeypatch.setenv(prerequisites.ENV_MARKED, "wikitool.ps1")
assert _status(doctor.run_doctor(), check) == "OK"
@pytest.mark.parametrize("policy, expected", [
(OPEN_POLICY, "OK"),
("CurrentUser=Unrestricted,LocalMachine=Restricted", "OK"),
("Process=Restricted,LocalMachine=RemoteSigned", "OK"),
("CurrentUser=Undefined,LocalMachine=Undefined", "OK"),
("CurrentUser=Restricted,LocalMachine=RemoteSigned", "FAIL"),
("LocalMachine=AllSigned", "FAIL"),
])
def test_execution_policy_blocks_only_restricted_and_allsigned(instance, monkeypatch, policy, expected):
_windows(monkeypatch, policy=policy)
assert _status(doctor.run_doctor(), "execution-policy") == expected
def test_execution_policy_fix_names_the_one_line_command(instance, monkeypatch):
_windows(monkeypatch, policy="CurrentUser=Restricted")
fix = _fix(doctor.run_doctor(), "execution-policy")
assert "Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned" in fix
@pytest.mark.parametrize("scope", ["MachinePolicy", "UserPolicy"])
def test_a_group_policy_gets_no_command_it_could_not_obey(instance, monkeypatch, scope):
_windows(monkeypatch, policy=f"{scope}=AllSigned,LocalMachine=RemoteSigned")
checks = doctor.run_doctor()
assert _status(checks, "execution-policy") == "FAIL"
assert "Set-ExecutionPolicy" not in _fix(checks, "execution-policy")
assert "Git Bash" in _fix(checks, "execution-policy")
def test_execution_policy_that_cannot_be_read_is_a_warning(instance, monkeypatch):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "windows")
assert _status(doctor.run_doctor(), "execution-policy") == "WARN"
def test_marked_scripts_fail_with_the_unblock_command(instance, monkeypatch):
_windows(monkeypatch, marked="wikitool.ps1,preflight.ps1")
checks = doctor.run_doctor()
assert _status(checks, "script-marks") == "FAIL"
assert "wikitool.ps1" in _detail(checks, "script-marks")
assert "Unblock-File" in _fix(checks, "script-marks")
def test_unmarked_scripts_are_ok(instance, monkeypatch):
_windows(monkeypatch)
assert _status(doctor.run_doctor(), "script-marks") == "OK"
# --- the optional browser component --------------------------------------------
def _without_browser(monkeypatch) -> None:
from chemenu import browser
monkeypatch.setattr(browser, "missing", lambda: "the Python package `playwright` is not installed in the venv")
def test_a_missing_browser_component_nothing_needs_is_ok(instance, monkeypatch):
_without_browser(monkeypatch)
checks = doctor.run_doctor()
assert _status(checks, "browser") == "OK"
assert "--with browser" in _detail(checks, "browser")
def test_a_missing_browser_component_a_session_route_needs_is_a_warning_not_a_failure(instance, monkeypatch):
_without_browser(monkeypatch)
write_settings(config.ROOT, monkeypatch, fetch={"routes": {"www.example.org": "session"}})
checks = doctor.run_doctor()
assert _status(checks, "browser") == "WARN"
assert "www.example.org" in _detail(checks, "browser")
assert "--with browser" in _fix(checks, "browser")
def test_an_installed_browser_component_is_ok(instance, monkeypatch):
from chemenu import browser
monkeypatch.setattr(browser, "missing", lambda: None)
assert _status(doctor.run_doctor(), "browser") == "OK"