utils/todo_hooks/tests/test_closer_failures.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

102 lines
3.3 KiB
Python

"""Tests for the closer when the server or the close call misbehaves."""
from __future__ import annotations
from typing import TYPE_CHECKING
from todo_hooks import closer
from todo_hooks.client import McpAuthError, McpToolError, McpUnavailable
from todo_hooks.tests._closer_helpers import ID, ID2, MSG
from todo_hooks.tests._closer_helpers import setup as _setup
from todo_hooks.tests._fakes import FakeClient, detail, task_client
from todo_hooks.trust import add_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
def test_unknown_id_is_reported_and_next_continues(
make_repo: Callable[..., Path],
make_ctx: Callable[..., tuple[Context, list[str]]],
paths: Paths,
) -> None:
client = task_client(detail(ID2))
msg = "feat\n\nTodo: 0123abcd\nTodo: 4567ef01"
_, ctx, lines = _setup(make_repo, make_ctx, paths, client, message=msg)
closer.run(ctx)
assert lines[0].startswith("todo-hooks: 0123abcd not found")
assert lines[1] == "todo-hooks: closed 4567ef01"
def test_server_down_warns_once_and_exits_zero(
make_repo: Callable[..., Path],
make_ctx: Callable[..., tuple[Context, list[str]]],
paths: Paths,
) -> None:
client = FakeClient({"get_task": McpUnavailable("connection refused")})
msg = "feat\n\nTodo: 0123abcd\nTodo: 4567ef01"
_, ctx, lines = _setup(make_repo, make_ctx, paths, client, message=msg)
assert closer.run(ctx) == 0
assert client.tools() == ["get_task"]
assert lines == [
"todo-hooks: warning: todo server unavailable (connection refused); nothing closed"
]
def test_auth_failure_warns(
make_repo: Callable[..., Path],
make_ctx: Callable[..., tuple[Context, list[str]]],
paths: Paths,
) -> None:
client = FakeClient({"get_task": McpAuthError("bad token")})
_, ctx, lines = _setup(make_repo, make_ctx, paths, client)
assert closer.run(ctx) == 0
assert len(lines) == 1
assert "warning" in lines[0]
assert "bad token" in lines[0]
def test_client_construction_failure_warns(
make_repo: Callable[..., Path],
make_ctx: Callable[..., tuple[Context, list[str]]],
paths: Paths,
) -> None:
_, ctx, lines = _setup(make_repo, make_ctx, paths, FakeClient())
def boom() -> FakeClient:
raise McpAuthError("no token")
ctx.make_client = boom # type: ignore[assignment]
assert closer.run(ctx) == 0
assert "no token" in lines[0]
def test_close_refused_warns(
make_repo: Callable[..., Path],
make_ctx: Callable[..., tuple[Context, list[str]]],
paths: Paths,
) -> None:
client = task_client(
detail(ID), close_task=McpToolError("error: evidence: required")
)
_, ctx, lines = _setup(make_repo, make_ctx, paths, client)
closer.run(ctx)
assert lines == ["todo-hooks: 0123abcd close failed: error: evidence: required"]
def test_no_remote_uses_toplevel_basename(
make_repo: Callable[..., Path],
make_ctx: Callable[..., tuple[Context, list[str]]],
paths: Paths,
) -> None:
root = make_repo("solo", message=MSG)
add_trusted(paths.trust_file, "true", "t")
client = task_client(detail(ID, repo="kuhyx/solo"))
ctx, _ = make_ctx(client, root)
closer.run(ctx)
assert "close_task" in client.tools()