Files
handler/tests/test_cli_phase2.py
T
0xWheatyz 6fb26115ce feat(phase-2): forge integration — credentials, role skills, approval gate, CI poller
Phase 2 configures forge for the agents (operator only sets a credential_ref +
optional version pin) and lets them drive a junior→senior→deploy workflow:

- Credential resolution/injection (control/credentials.py): credential_ref pointers
  (env:/file:/cmd:) resolved only at spawn, injected as FORGE_TOKEN + host var, with a
  forge-host-scoped git credential helper reading the token from env (never on disk / in
  the DB). Resolution is a fail-fast spawn gate.
- Role-based forge skills committed into the managed repo (control/skills_gen.py,
  `handler forge-init`): forge-junior/senior/deploy + a workflow overview.
- Hard approval gate (hooks/gate.py, approvals table, migration 0002): merge/deploy —
  and direct pushes to protected branches — are denied unless a DIFFERENT agent has an
  `approved` record for the branch, pinned to the reviewed commit (approved_sha). Senior
  records verdicts via `handler approve`/`reject`.
- forge/git seams (control/forge.py, control/gitops.py) matching the Phase 1 seam pattern.
- CI status poller (control/poller.py, `handler poll-ci [--watch]`) backfilling
  ci_status/ci_checked_at via `forge ci list`.
- Fix: migrations/env.py commits explicitly after run_migrations — pysqlite on Py 3.12+
  was rolling back the final migration's DDL + alembic_version stamp (latent in Phase 1).

Reviewed via a separate code-reviewer pass; gate-bypass and credential-scoping findings
addressed. 106 tests, ruff clean, verified end-to-end against real git + migrations.
2026-07-08 22:05:49 -04:00

80 lines
3.0 KiB
Python

"""Phase 2 CLI: approve/reject (env identity), poll-ci, forge-init."""
from __future__ import annotations
from handler.control import cli
from handler.db import repository as repo
from handler.db.engine import get_engine
def _seed_project_agent(role="senior", name="senior"):
with get_engine().begin() as conn:
repo.create_project(conn, "p", "/tmp/p")
return repo.create_agent(conn, "p", name, "/tmp/p/s", role=role)
def test_approve_via_cli_uses_env_identity(env, monkeypatch, capsys):
agent = _seed_project_agent()
monkeypatch.setenv("HANDLER_PROJECT_ID", "p")
monkeypatch.setenv("HANDLER_AGENT_ID", str(agent["id"]))
rc = cli.main(["approve", "--branch", "feat/x", "--pr", "7", "--note", "lgtm"])
assert rc == 0
with get_engine().begin() as conn:
latest = repo.get_latest_approval(conn, "p", "feat/x")
assert latest["status"] == "approved"
assert latest["approved_by_agent_id"] == agent["id"]
assert latest["pr_ref"] == "7"
def test_reject_via_cli(env, monkeypatch):
agent = _seed_project_agent()
monkeypatch.setenv("HANDLER_PROJECT_ID", "p")
monkeypatch.setenv("HANDLER_AGENT_ID", str(agent["id"]))
assert cli.main(["reject", "--branch", "feat/x", "--note", "fix it"]) == 0
with get_engine().begin() as conn:
assert repo.get_latest_approval(conn, "p", "feat/x")["status"] == "rejected"
def test_approve_without_identity_errors(env, monkeypatch, capsys):
_seed_project_agent()
monkeypatch.delenv("HANDLER_PROJECT_ID", raising=False)
monkeypatch.delenv("HANDLER_AGENT_ID", raising=False)
assert cli.main(["approve", "--branch", "feat/x"]) == 1
assert "no project" in capsys.readouterr().err
def test_approve_rejects_unknown_agent(env, monkeypatch, capsys):
_seed_project_agent()
monkeypatch.setenv("HANDLER_PROJECT_ID", "p")
monkeypatch.setenv("HANDLER_AGENT_ID", "9999")
assert cli.main(["approve", "--branch", "feat/x"]) == 1
assert "not found" in capsys.readouterr().err
def test_poll_ci_cli(env, fake_forge, capsys):
with get_engine().begin() as conn:
repo.create_project(conn, "p", "/tmp/p")
a = repo.create_agent(conn, "p", "a", "/tmp/p/a")
repo.insert_log_entry(conn, a["id"], status="working", push_sha="s", ci_status="pending")
fake_forge["runs"] = [{"conclusion": "success"}]
assert cli.main(["poll-ci"]) == 0
assert "resolved=1" in capsys.readouterr().out
def test_forge_init_writes_and_commits(env, fake_gitops, capsys):
root = env["tmp"] / "proj"
root.mkdir(parents=True, exist_ok=True)
with get_engine().begin() as conn:
repo.create_project(conn, "proj", str(root))
assert cli.main(["forge-init", "--project", "proj"]) == 0
assert (root / ".claude" / "skills" / "forge-junior" / "SKILL.md").exists()
# Auto-committed via the git seam.
assert len(fake_gitops["commit"]) == 1
def test_forge_init_unknown_project_errors(env, capsys):
assert cli.main(["forge-init", "--project", "nope"]) == 1
assert "not registered" in capsys.readouterr().err