utils/todo_hooks/tests/test_main.py
Krzysztof kuhy Rudnicki aec96eefd3 Add todo_hooks closer, drift sweep, PR import
Python 3.14 stdlib package wiring git to the todo MCP server:
- closer (post-commit): runs the done_check of each Todo: trailer
  task and close_task's it only on exit 0. It FAILS OPEN: the commit
  already happened, so a down server, bad token or no gh prints one
  warning and exits 0 instead of failing the hook.
- sweep: nightly drift report (systemd units shipped, not enabled).
- import-pr: a merged PR's '## Todo:' items become tasks, deduped.
- A done_check runs only if its sha256 is in
  ~/.config/todo-hooks/trusted-checks; 'Check: manual' notes are
  never auto-closed.
- Hooks exposed in .pre-commit-hooks.yaml (post-commit, post-merge,
  post-rewrite).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 12:24:29 +01:00

174 lines
5.7 KiB
Python

"""Tests for the CLI dispatcher and the trust subcommand."""
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
import pytest
from todo_hooks import __main__ as cli
from todo_hooks import trust_cmd
from todo_hooks.client import McpToolError, McpUnavailable
from todo_hooks.tests._fakes import (
TOKEN,
FakeClient,
FakeMcp,
detail,
line,
task_client,
)
from todo_hooks.trust import check_hash, is_trusted
if TYPE_CHECKING:
from collections.abc import Callable
from pathlib import Path
from todo_hooks.context import Context
from todo_hooks.paths import Paths
ID = "0123abcd-0000-4000-8000-000000000001"
ID2 = "4567ef01-0000-4000-8000-000000000002"
def test_trust_one(
make_ctx: Callable[..., tuple[Context, list[str]]], paths: Paths, tmp_path: Path
) -> None:
client = task_client(detail(ID, done_check="make test"))
ctx, out = make_ctx(client, tmp_path)
assert trust_cmd.run(ctx, ["0123abcd"]) == 0
assert is_trusted(paths.trust_file, "make test")
assert out == [
f"todo-hooks: trusted 0123abcd (sha256 {check_hash('make test')[:12]})"
]
def test_trust_one_failures(
make_ctx: Callable[..., tuple[Context, list[str]]], tmp_path: Path
) -> None:
ctx, out = make_ctx(task_client(detail(ID, done_check=None)), tmp_path)
assert trust_cmd.run(ctx, ["0123abcd"]) == 1
assert "has no done_check" in out[-1]
assert trust_cmd.run(ctx, ["ffffffff"]) == 1
assert "not found" in out[-1]
assert trust_cmd.run(ctx, []) == 2
assert "usage" in out[-1]
assert trust_cmd.run(ctx, ["a", "b"]) == 2
def test_trust_server_down(
make_ctx: Callable[..., tuple[Context, list[str]]], tmp_path: Path
) -> None:
ctx, out = make_ctx(FakeClient({"get_task": McpUnavailable("refused")}), tmp_path)
assert trust_cmd.run(ctx, ["0123abcd"]) == 1
assert "unavailable" in out[-1]
def test_seed_open_trusts_all_open_with_checks(
make_ctx: Callable[..., tuple[Context, list[str]]], paths: Paths, tmp_path: Path
) -> None:
client = task_client(
detail(ID, done_check="first"),
detail(ID2, done_check=None),
list_tasks="\n".join([line(ID), line(ID2), "garbage"]),
)
ctx, out = make_ctx(client, tmp_path)
assert trust_cmd.run(ctx, ["--seed-open"]) == 0
assert is_trusted(paths.trust_file, "first")
assert out[-1] == "todo-hooks: seeded 1 new check(s) from 2 open note(s)"
# Re-seeding is idempotent.
assert trust_cmd.run(ctx, ["--seed-open"]) == 0
assert out[-1] == "todo-hooks: seeded 0 new check(s) from 2 open note(s)"
assert client.calls[0] == ("list_tasks", {"status": "open"})
def test_seed_open_tool_error(
make_ctx: Callable[..., tuple[Context, list[str]]], tmp_path: Path
) -> None:
ctx, out = make_ctx(FakeClient({"list_tasks": McpToolError("error: x")}), tmp_path)
assert trust_cmd.run(ctx, ["--seed-open"]) == 1
assert "error: x" in out[-1]
def test_main_usage_and_unknown(capsys: pytest.CaptureFixture[str]) -> None:
assert cli.main([]) == 2
assert cli.main(["wat"]) == 2
assert "usage: python3 -m todo_hooks" in capsys.readouterr().out
def test_main_closer_end_to_end_against_fake_server(
mcp: FakeMcp,
make_repo: Callable[..., Path],
paths: Paths,
capsys: pytest.CaptureFixture[str],
) -> None:
root = make_repo(
"demo", origin="git@github.com:kuhyx/demo.git", message="x\n\nTodo: 0123abcd"
)
mcp.tools["get_task"] = lambda _a: detail(ID, done_check="true")
closed: list[dict[str, object]] = []
mcp.tools["close_task"] = lambda a: closed.append(a) or "closed"
env = {
"TODO_MCP_URL": mcp.url,
"TODO_MCP_TOKEN": TOKEN,
"TODO_HOOKS_TRUST_FILE": str(paths.trust_file),
}
assert cli.main(["trust", "0123abcd"], env=env, cwd=root) == 0
assert cli.main(["closer"], env=env, cwd=root) == 0
assert closed[0]["id"] == ID
assert "closed 0123abcd" in capsys.readouterr().out
def test_main_closer_without_token_fails_open(
make_repo: Callable[..., Path], paths: Paths, capsys: pytest.CaptureFixture[str]
) -> None:
root = make_repo("demo", message="x\n\nTodo: 0123abcd")
env = {"TODO_MCP_TOKEN_FILE": str(paths.token_file)}
assert cli.main(["closer"], env=env, cwd=root) == 0
assert "warning" in capsys.readouterr().out
def test_main_sweep_flags(
mcp: FakeMcp,
paths: Paths,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
mcp.tools["list_tasks"] = lambda _a: "no tasks match"
env = {"TODO_MCP_URL": mcp.url, "TODO_MCP_TOKEN": TOKEN}
report = tmp_path / "r.txt"
args = [
"sweep",
"--report",
str(report),
"--dry-run",
"--trust-file",
str(tmp_path / "t"),
]
assert cli.main(args, env=env, cwd=tmp_path) == 0
assert report.read_text().startswith("# dry-run: 0 open notes")
assert "dry-run" in capsys.readouterr().out
assert cli.main(["sweep", "--bogus"], env=env, cwd=tmp_path) == 2
def test_main_import_pr_dispatch(
make_repo: Callable[..., Path],
paths: Paths,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
root = make_repo("demo", origin="git@github.com:kuhyx/demo.git")
monkeypatch.setattr("todo_hooks.gh.run_gh", lambda _a: "[]")
env = {"TODO_MCP_TOKEN_FILE": str(paths.token_file)}
assert cli.main(["import-pr", "post-merge"], env=env, cwd=root, stdin="") == 0
def test_module_entrypoint_runs(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("sys.argv", ["todo_hooks"])
monkeypatch.delitem(sys.modules, "todo_hooks.__main__", raising=False)
import runpy
with pytest.raises(SystemExit) as info:
runpy.run_module("todo_hooks", run_name="__main__", alter_sys=True)
assert info.value.code == 2