mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 17:16:25 +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
65 lines
2.2 KiB
Python
65 lines
2.2 KiB
Python
"""Project + agent routes, and project isolation (README 3.4)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
def _mk_project(client, auth, pid="proj", root="/tmp/proj"):
|
|
return client.post("/projects", json={"id": pid, "root_dir": root}, headers=auth)
|
|
|
|
|
|
def test_create_and_list_project(client, auth):
|
|
r = _mk_project(client, auth)
|
|
assert r.status_code == 201
|
|
assert r.json()["id"] == "proj"
|
|
listing = client.get("/projects", headers=auth).json()
|
|
assert [p["id"] for p in listing] == ["proj"]
|
|
|
|
|
|
def test_duplicate_project_conflicts(client, auth):
|
|
_mk_project(client, auth)
|
|
assert _mk_project(client, auth).status_code == 409
|
|
|
|
|
|
def test_create_and_list_agent(client, auth):
|
|
_mk_project(client, auth)
|
|
r = client.post(
|
|
"/projects/proj/agents",
|
|
json={"name": "api", "working_dir": "/tmp/proj/api"},
|
|
headers=auth,
|
|
)
|
|
assert r.status_code == 201
|
|
agents = client.get("/projects/proj/agents", headers=auth).json()
|
|
assert [a["name"] for a in agents] == ["api"]
|
|
|
|
|
|
def test_agent_under_missing_project_is_404(client, auth):
|
|
r = client.get("/projects/ghost/agents", headers=auth)
|
|
assert r.status_code == 404
|
|
|
|
|
|
def test_project_isolation_same_agent_name(client, auth):
|
|
# Two projects can each have an agent named "api"; neither leaks into the other.
|
|
_mk_project(client, auth, "a", "/tmp/a")
|
|
_mk_project(client, auth, "b", "/tmp/b")
|
|
client.post(
|
|
"/projects/a/agents",
|
|
json={"name": "api", "working_dir": "/tmp/a/api"},
|
|
headers=auth,
|
|
)
|
|
a_agents = client.get("/projects/a/agents", headers=auth).json()
|
|
b_agents = client.get("/projects/b/agents", headers=auth).json()
|
|
assert [x["name"] for x in a_agents] == ["api"]
|
|
assert b_agents == []
|
|
# The agent is invisible under project b.
|
|
assert client.get("/projects/b/agents/api/checkmark", headers=auth).status_code == 404
|
|
|
|
|
|
def test_checkmark_404_before_any_checkpoint(client, auth):
|
|
_mk_project(client, auth)
|
|
client.post(
|
|
"/projects/proj/agents",
|
|
json={"name": "api", "working_dir": "/tmp/proj/api"},
|
|
headers=auth,
|
|
)
|
|
assert client.get("/projects/proj/agents/api/checkmark", headers=auth).status_code == 404
|