mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 10:06:24 +00:00
eba0e19ec9
Implements the Phase 1 MVP from the README: a stateless control layer + HTTP API over a centralized database, with hook-enforced test/push gates. - DB layer: SQLAlchemy Core, one schema rendering both Postgres (BIGSERIAL / TIMESTAMPTZ / JSONB) and SQLite (INTEGER PK / TEXT / JSON) via portable types; native ON CONFLICT DO UPDATE checkmark upsert on both dialects. - Alembic dual-dialect migrations (render_as_batch for SQLite); tests run a real `alembic upgrade head`. - FastAPI: projects/agents/checkmark/log/answer/resume + shared log/context routes, single global bearer token, higher-trust token gating shared-context writes, project isolation on every route. - Hooks (`python -m handler.hooks <event>`): Stop test gate (block on red), PreToolUse AskUserQuestion defer + `git push` gate (tests then throwaway build), Notification generic webhook (no-op without WEBHOOK_URL). Identity via env injected at spawn; verify is the mock seam. - Control CLI: spawn/list/attach/kill, hard `.mise.toml [tasks.test]` gate, generated per-agent settings.json, identity + DATABASE_URL injected via tmux; tmux is the mock seam. - 45 tests (SQLite), ruff clean. Live claude/tmux/mise spawning deferred behind the mocked seams. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5ZuS5pV1NS6eKsRZHXonY
47 lines
1.4 KiB
Python
47 lines
1.4 KiB
Python
"""Notification hook: webhook POST only when WEBHOOK_URL is set; log always written."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import httpx
|
|
import respx
|
|
|
|
from handler.db import repository as repo
|
|
from handler.hooks import notify
|
|
from handler.hooks.context import HookInput, Identity
|
|
|
|
|
|
def _seed(conn):
|
|
repo.create_project(conn, "p", "/tmp/p")
|
|
a = repo.create_agent(conn, "p", "a", "/tmp/p/a")
|
|
return Identity(a["id"], "p", "a", "/tmp/p/a")
|
|
|
|
|
|
def test_notify_noop_without_webhook(conn, env):
|
|
ident = _seed(conn)
|
|
hi = HookInput({"message": "needs input", "session_id": "s1"}, "notification")
|
|
# WEBHOOK_URL is unset in the env fixture -> no HTTP call, but the log is recorded.
|
|
notify.handle(conn, ident, hi)
|
|
assert "notification: needs input" in repo.get_log(conn, ident.agent_id)[0]["summary"]
|
|
|
|
|
|
@respx.mock
|
|
def test_notify_posts_when_webhook_set(conn, env, monkeypatch):
|
|
monkeypatch.setenv("WEBHOOK_URL", "https://ntfy.example/topic")
|
|
from handler import config
|
|
|
|
config.get_settings.cache_clear()
|
|
|
|
route = respx.post("https://ntfy.example/topic").mock(return_value=httpx.Response(200))
|
|
ident = _seed(conn)
|
|
hi = HookInput({"message": "hello", "session_id": "s1"}, "notification")
|
|
notify.handle(conn, ident, hi)
|
|
|
|
assert route.called
|
|
sent = route.calls[0].request
|
|
import json
|
|
|
|
body = json.loads(sent.content)
|
|
assert body["project"] == "p"
|
|
assert body["agent"] == "a"
|
|
assert body["message"] == "hello"
|