Add backlog audit and task update command
- agents/prompts/backlog_audit.md: QA analyst prompt for checking
which pending tasks are already implemented in the codebase
- agents/runner.py: run_audit() — project-level agent that reads
all pending tasks, inspects code, returns classification
- cli/main.py: kin audit <project_id> — runs audit, offers to mark
done tasks; kin task update <id> --status --priority
- web/api.py: POST /api/projects/{id}/audit (runs audit inline),
POST /api/projects/{id}/audit/apply (batch mark as done)
- Frontend: "Audit backlog" button on ProjectView with results
modal showing already_done/still_pending/unclear categories
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e755a19633
commit
96509dcafc
9 changed files with 548 additions and 2 deletions
|
|
@ -201,3 +201,35 @@ def test_project_summary_includes_review(client):
|
|||
r = client.get("/api/projects")
|
||||
projects = r.json()
|
||||
assert projects[0]["review_tasks"] == 1
|
||||
|
||||
|
||||
def test_audit_not_found(client):
|
||||
r = client.post("/api/projects/NOPE/audit")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_audit_apply(client):
|
||||
"""POST /audit/apply should mark tasks as done."""
|
||||
r = client.post("/api/projects/p1/audit/apply",
|
||||
json={"task_ids": ["P1-001"]})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["count"] == 1
|
||||
assert "P1-001" in r.json()["updated"]
|
||||
|
||||
# Verify task is done
|
||||
r = client.get("/api/tasks/P1-001")
|
||||
assert r.json()["status"] == "done"
|
||||
|
||||
|
||||
def test_audit_apply_not_found(client):
|
||||
r = client.post("/api/projects/NOPE/audit/apply",
|
||||
json={"task_ids": ["P1-001"]})
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_audit_apply_wrong_project(client):
|
||||
"""Tasks not belonging to the project should be skipped."""
|
||||
r = client.post("/api/projects/p1/audit/apply",
|
||||
json={"task_ids": ["WRONG-001"]})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["count"] == 0
|
||||
|
|
|
|||
|
|
@ -205,3 +205,38 @@ def test_cost_with_data(runner):
|
|||
assert r.exit_code == 0
|
||||
assert "p1" in r.output
|
||||
assert "$0.1000" in r.output
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# task update
|
||||
# ===========================================================================
|
||||
|
||||
def test_task_update_status(runner):
|
||||
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
|
||||
invoke(runner, ["task", "add", "p1", "Fix bug"])
|
||||
r = invoke(runner, ["task", "update", "P1-001", "--status", "done"])
|
||||
assert r.exit_code == 0
|
||||
assert "done" in r.output
|
||||
|
||||
r = invoke(runner, ["task", "show", "P1-001"])
|
||||
assert "done" in r.output
|
||||
|
||||
|
||||
def test_task_update_priority(runner):
|
||||
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
|
||||
invoke(runner, ["task", "add", "p1", "Fix bug"])
|
||||
r = invoke(runner, ["task", "update", "P1-001", "--priority", "1"])
|
||||
assert r.exit_code == 0
|
||||
assert "priority=1" in r.output
|
||||
|
||||
|
||||
def test_task_update_not_found(runner):
|
||||
r = invoke(runner, ["task", "update", "NOPE", "--status", "done"])
|
||||
assert r.exit_code != 0
|
||||
|
||||
|
||||
def test_task_update_no_fields(runner):
|
||||
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
|
||||
invoke(runner, ["task", "add", "p1", "Fix bug"])
|
||||
r = invoke(runner, ["task", "update", "P1-001"])
|
||||
assert r.exit_code != 0
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import pytest
|
|||
from unittest.mock import patch, MagicMock
|
||||
from core.db import init_db
|
||||
from core import models
|
||||
from agents.runner import run_agent, run_pipeline, _try_parse_json
|
||||
from agents.runner import run_agent, run_pipeline, run_audit, _try_parse_json
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -335,3 +335,82 @@ class TestNonInteractive:
|
|||
run_agent(conn, "debugger", "VDOL-001", "vdol", allow_write=False)
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert "--dangerously-skip-permissions" not in cmd
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# run_audit
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestRunAudit:
|
||||
@patch("agents.runner.subprocess.run")
|
||||
def test_audit_success(self, mock_run, conn):
|
||||
"""Audit should return parsed already_done/still_pending/unclear."""
|
||||
audit_output = json.dumps({
|
||||
"already_done": [{"id": "VDOL-001", "reason": "Fixed in runner.py"}],
|
||||
"still_pending": [],
|
||||
"unclear": [],
|
||||
})
|
||||
mock_run.return_value = _mock_claude_success({"result": audit_output})
|
||||
|
||||
result = run_audit(conn, "vdol")
|
||||
|
||||
assert result["success"] is True
|
||||
assert len(result["already_done"]) == 1
|
||||
assert result["already_done"][0]["id"] == "VDOL-001"
|
||||
|
||||
@patch("agents.runner.subprocess.run")
|
||||
def test_audit_logs_to_db(self, mock_run, conn):
|
||||
"""Audit should log to agent_logs with role=backlog_audit."""
|
||||
mock_run.return_value = _mock_claude_success({
|
||||
"result": json.dumps({"already_done": [], "still_pending": [], "unclear": []}),
|
||||
})
|
||||
|
||||
run_audit(conn, "vdol")
|
||||
|
||||
logs = conn.execute(
|
||||
"SELECT * FROM agent_logs WHERE agent_role='backlog_audit'"
|
||||
).fetchall()
|
||||
assert len(logs) == 1
|
||||
assert logs[0]["action"] == "audit"
|
||||
|
||||
def test_audit_no_pending_tasks(self, conn):
|
||||
"""If no pending tasks, return success with empty lists."""
|
||||
# Mark existing task as done
|
||||
models.update_task(conn, "VDOL-001", status="done")
|
||||
|
||||
result = run_audit(conn, "vdol")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["already_done"] == []
|
||||
assert "No pending tasks" in result.get("message", "")
|
||||
|
||||
def test_audit_project_not_found(self, conn):
|
||||
result = run_audit(conn, "nonexistent")
|
||||
assert result["success"] is False
|
||||
assert "not found" in result["error"]
|
||||
|
||||
@patch("agents.runner.subprocess.run")
|
||||
def test_audit_uses_sonnet(self, mock_run, conn):
|
||||
"""Audit should use sonnet model."""
|
||||
mock_run.return_value = _mock_claude_success({
|
||||
"result": json.dumps({"already_done": [], "still_pending": [], "unclear": []}),
|
||||
})
|
||||
|
||||
run_audit(conn, "vdol")
|
||||
|
||||
cmd = mock_run.call_args[0][0]
|
||||
model_idx = cmd.index("--model")
|
||||
assert cmd[model_idx + 1] == "sonnet"
|
||||
|
||||
@patch("agents.runner.subprocess.run")
|
||||
def test_audit_includes_tasks_in_prompt(self, mock_run, conn):
|
||||
"""The prompt should contain the task title."""
|
||||
mock_run.return_value = _mock_claude_success({
|
||||
"result": json.dumps({"already_done": [], "still_pending": [], "unclear": []}),
|
||||
})
|
||||
|
||||
run_audit(conn, "vdol")
|
||||
|
||||
prompt = mock_run.call_args[0][0][2] # -p argument
|
||||
assert "VDOL-001" in prompt
|
||||
assert "Fix bug" in prompt
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue