Files
handler/tests/test_repository_web.py
T
Claude 4f05d09c2b feat(web): fully web-managed control plane via a DB command queue
Make credentials/hosts, projects, agents, and approvals manageable from the
dashboard. The API and control layer are separate containers, so the API can't
run control actions directly (no git/tmux/claude, doesn't own the tmux
sessions). Instead the API enqueues a command and a worker in the control
container executes it and writes the result back.

Data model (migration 0003):
- `commands` queue/audit table; `forge_hosts` registry; `approvals` gains a
  nullable approver id + `actor` so operator verdicts are first-class.

Control worker:
- `control/worker.py` claims commands and dispatches to the existing control
  functions (spawn/kill/resume/record_approval/write_skills/poller.sweep),
  plus a periodic CI sweep. New `handler worker` CLI subcommand; it becomes the
  control image's default command (subsumes `poll-ci --watch`).

API:
- `require_admin` gate + `ADMIN_TOKEN`; project GET/PATCH/DELETE; agent
  spawn/kill/delete; resume now enqueues (fixes a cross-container bug where the
  API tried to send tmux keys to a session in the control container); new
  approvals/commands/hosts routes; forge-init and poll-ci enqueue endpoints.

Credentials/hosts:
- host->token-env lookup consults the `forge_hosts` registry first (built-in
  map is the fallback); `resolve()` refactored to a scheme dispatch reserving
  `db:` for a future encrypted store. Web input restricts credential_ref to
  env:/file:/db: (cmd: stays CLI-only — it would run arbitrary commands).

Dashboard:
- New tabs for projects, agents (spawn/kill with live command-status polling),
  approvals, hosts, and an activity/audit view; shared context is now writable.

Tests: +33 (queue atomicity, worker dispatch, CRUD, hosts, admin gating,
cmd: rejection, host-aware credentials, and an API->queue->worker->spawn
end-to-end). README gains a Web management section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrhrBToauu4L2qG6jdnuFP
2026-07-10 16:32:45 +00:00

86 lines
3.4 KiB
Python

"""DAL for web management: project/agent mutation, the command queue, and hosts."""
from __future__ import annotations
from handler.db import repository as repo
def test_update_and_delete_project(conn):
repo.create_project(conn, "p", "/tmp/p", git_remote="https://github.com/me/p.git")
updated = repo.update_project(conn, "p", git_remote="https://gitea.corp/me/p.git",
credential_ref="env:TOK")
assert updated["git_remote"] == "https://gitea.corp/me/p.git"
assert updated["credential_ref"] == "env:TOK"
# An unknown field is ignored, not applied.
repo.update_project(conn, "p", nonsense="x")
assert repo.delete_project(conn, "p") is True
assert repo.get_project(conn, "p") is None
def test_delete_agent_row(conn):
repo.create_project(conn, "p", "/tmp/p")
repo.create_agent(conn, "p", "api", "/tmp/p/api")
assert repo.delete_agent(conn, "p", "api") is True
assert repo.get_agent_by_name(conn, "p", "api") is None
def test_enqueue_get_and_list_command(conn):
repo.create_project(conn, "p", "/tmp/p")
cmd = repo.enqueue_command(
conn, "spawn", project_id="p", agent_name="junior",
payload={"role": "junior"}, requested_by="operator:web",
)
assert cmd["status"] == "queued"
assert cmd["type"] == "spawn"
assert cmd["payload"] == {"role": "junior"}
assert repo.get_command(conn, cmd["id"])["agent_name"] == "junior"
assert [c["id"] for c in repo.list_commands(conn, project_id="p")] == [cmd["id"]]
def test_claim_is_atomic_and_fifo(conn):
repo.enqueue_command(conn, "poll_ci")
second = repo.enqueue_command(conn, "poll_ci")
first_claim = repo.claim_next_command(conn, "worker-1")
assert first_claim["status"] == "running"
assert first_claim["claimed_by"] == "worker-1"
# Oldest-first: the second claim gets the later row, never the same one twice.
second_claim = repo.claim_next_command(conn, "worker-2")
assert second_claim["id"] == second["id"]
assert second_claim["id"] != first_claim["id"]
# Queue drained -> None.
assert repo.claim_next_command(conn, "worker-3") is None
def test_finish_command_records_result(conn):
cmd = repo.enqueue_command(conn, "poll_ci")
repo.claim_next_command(conn, "w")
repo.finish_command(conn, cmd["id"], "done", result={"checked": 3})
done = repo.get_command(conn, cmd["id"])
assert done["status"] == "done"
assert done["result"] == {"checked": 3}
assert done["finished_at"] is not None
def test_hosts_crud(conn):
created = repo.create_host(conn, "git.corp", "gitea", token_env_var="GITEA_TOKEN")
assert created["forge_type"] == "gitea"
assert repo.get_host(conn, "git.corp")["token_env_var"] == "GITEA_TOKEN"
repo.update_host(conn, "git.corp", base_url="https://git.corp")
assert repo.get_host(conn, "git.corp")["base_url"] == "https://git.corp"
assert [h["hostname"] for h in repo.list_hosts(conn)] == ["git.corp"]
assert repo.delete_host(conn, "git.corp") is True
assert repo.get_host(conn, "git.corp") is None
def test_operator_approval_has_no_agent_and_lists(conn):
repo.create_project(conn, "p", "/tmp/p")
ap = repo.record_approval(conn, "p", "feat/x", "approved", actor="operator:web")
assert ap["approved_by_agent_id"] is None
assert ap["actor"] == "operator:web"
listed = repo.list_approvals(conn, "p")
assert [a["id"] for a in listed] == [ap["id"]]
assert repo.list_approvals(conn, "p", branch="other") == []