mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 21:27:14 +01:00
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>
174 lines
5.7 KiB
Python
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
|