mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 09:46:24 +00:00
43e1fa3619
Removing a repo returned 500. `delete_project` deleted only the projects row, but `commands`, `agents`, `approvals`, and `schedules` all carry a foreign key to `projects.id` with no ON DELETE CASCADE — and every real project has at least the `sync` command queued at registration referencing it, so Postgres rejected the delete with a ForeignKeyViolation (commands_project_id_fkey). The existing tests only deleted dependent-free projects, so it went unnoticed (and SQLite, though it has FK enforcement on here, was never exercised with a referencing row). delete_project now clears dependents in FK-safe order: agent-owned rows (checkmarks before log_entries per the use_alter cycle, approvals authored by those agents, and shared_context attribution nulled since it's a global table), then schedules (which reference commands via last_command_id), then the project-scoped approvals/commands/agents, then the project. delete_agent had the same latent bug — a spawned agent always accrues a checkmark + log entries via the hooks, which the log_entries/checkmarks FKs would block — so it shares the same _purge_agent_dependents helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BxKY28XKCM6o4ag3nmaVsZ
131 lines
5.7 KiB
Python
131 lines
5.7 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_delete_project_cascades_all_dependents(conn):
|
|
"""A real project always has FK-referencing rows (the sync command, agent history,
|
|
approvals, schedules). Deleting it must clear them, not raise a ForeignKeyViolation."""
|
|
from datetime import UTC, datetime
|
|
|
|
repo.create_project(conn, "p", "/tmp/p", git_remote="https://github.com/me/p.git")
|
|
# The sync command queued at registration — the exact row that blocked the delete.
|
|
repo.enqueue_command(conn, "sync", project_id="p", requested_by="operator:web")
|
|
agent = repo.create_agent(conn, "p", "api", "/tmp/p/api")
|
|
repo.insert_log_entry(conn, agent_id=agent["id"], status="working", summary="did work")
|
|
repo.upsert_checkmark_row(conn, agent_id=agent["id"], status="working")
|
|
repo.set_shared_context(conn, "db", "postgres", agent["id"])
|
|
repo.record_approval(conn, "p", "feat/x", "approved", approved_by_agent_id=agent["id"])
|
|
repo.create_schedule(
|
|
conn, "p", "nightly", "run the thing", 3600, datetime.now(UTC)
|
|
)
|
|
|
|
assert repo.delete_project(conn, "p") is True
|
|
|
|
# Project and everything scoped to it are gone.
|
|
assert repo.get_project(conn, "p") is None
|
|
assert repo.get_agent_by_name(conn, "p", "api") is None
|
|
assert repo.list_commands(conn, project_id="p") == []
|
|
assert repo.list_approvals(conn, "p") == []
|
|
assert repo.list_schedules(conn) == []
|
|
# The global shared-context row survives, with its agent attribution cleared.
|
|
ctx = repo.get_shared_context_key(conn, "db")
|
|
assert ctx is not None and ctx["value"] == "postgres"
|
|
assert ctx["set_by_agent_id"] is None
|
|
|
|
|
|
def test_delete_agent_cascades_log_and_checkmark(conn):
|
|
"""Every spawned agent accrues a checkmark + log entries via the hooks; removing the
|
|
agent must clear them rather than trip the log_entries/checkmarks foreign keys."""
|
|
repo.create_project(conn, "p", "/tmp/p")
|
|
agent = repo.create_agent(conn, "p", "api", "/tmp/p/api")
|
|
repo.insert_log_entry(conn, agent_id=agent["id"], status="working", summary="x")
|
|
repo.upsert_checkmark_row(conn, agent_id=agent["id"], status="working")
|
|
|
|
assert repo.delete_agent(conn, "p", "api") is True
|
|
assert repo.get_agent_by_name(conn, "p", "api") is None
|
|
assert repo.get_checkmark(conn, agent["id"]) is None
|
|
assert repo.get_log(conn, agent["id"]) == []
|
|
|
|
|
|
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") == []
|