kin/tests/test_cli.py
Gros Frumos 96509dcafc 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>
2026-03-15 17:44:16 +02:00

242 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for cli/main.py using click's CliRunner with in-memory-like temp DB."""
import json
import tempfile
from pathlib import Path
import pytest
from click.testing import CliRunner
from cli.main import cli
@pytest.fixture
def runner(tmp_path):
"""CliRunner that uses a temp DB file."""
db_path = tmp_path / "test.db"
return CliRunner(), ["--db", str(db_path)]
def invoke(runner_tuple, args):
runner, base = runner_tuple
result = runner.invoke(cli, base + args)
return result
# -- project --
def test_project_add_and_list(runner):
r = invoke(runner, ["project", "add", "vdol", "В долю поперёк",
"~/projects/vdolipoperek", "--tech-stack", '["vue3","nuxt"]'])
assert r.exit_code == 0
assert "vdol" in r.output
r = invoke(runner, ["project", "list"])
assert r.exit_code == 0
assert "vdol" in r.output
assert "В долю поперёк" in r.output
def test_project_list_empty(runner):
r = invoke(runner, ["project", "list"])
assert r.exit_code == 0
assert "No projects" in r.output
def test_project_list_filter_status(runner):
invoke(runner, ["project", "add", "a", "A", "/a", "--status", "active"])
invoke(runner, ["project", "add", "b", "B", "/b", "--status", "paused"])
r = invoke(runner, ["project", "list", "--status", "active"])
assert "a" in r.output
assert "b" not in r.output
def test_project_show(runner):
invoke(runner, ["project", "add", "vdol", "В долю", "/vdol",
"--tech-stack", '["vue3"]', "--priority", "2"])
r = invoke(runner, ["project", "show", "vdol"])
assert r.exit_code == 0
assert "vue3" in r.output
assert "Priority: 2" in r.output
def test_project_show_not_found(runner):
r = invoke(runner, ["project", "show", "nope"])
assert r.exit_code == 1
assert "not found" in r.output
# -- task --
def test_task_add_and_list(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
r = invoke(runner, ["task", "add", "p1", "Fix login bug", "--type", "debug"])
assert r.exit_code == 0
assert "P1-001" in r.output
r = invoke(runner, ["task", "add", "p1", "Add search"])
assert "P1-002" in r.output
r = invoke(runner, ["task", "list"])
assert "P1-001" in r.output
assert "P1-002" in r.output
def test_task_add_project_not_found(runner):
r = invoke(runner, ["task", "add", "nope", "Some task"])
assert r.exit_code == 1
assert "not found" in r.output
def test_task_list_filter(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
invoke(runner, ["project", "add", "p2", "P2", "/p2"])
invoke(runner, ["task", "add", "p1", "A"])
invoke(runner, ["task", "add", "p2", "B"])
r = invoke(runner, ["task", "list", "--project", "p1"])
assert "P1-001" in r.output
assert "P2-001" not in r.output
def test_task_show(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
invoke(runner, ["task", "add", "p1", "Fix bug", "--type", "debug"])
r = invoke(runner, ["task", "show", "P1-001"])
assert r.exit_code == 0
assert "Fix bug" in r.output
def test_task_show_not_found(runner):
r = invoke(runner, ["task", "show", "X-999"])
assert r.exit_code == 1
assert "not found" in r.output
# -- decision --
def test_decision_add_and_list(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
r = invoke(runner, ["decision", "add", "p1", "gotcha",
"Safari bug", "position:fixed breaks",
"--category", "ui", "--tags", '["ios","css"]'])
assert r.exit_code == 0
assert "gotcha" in r.output
r = invoke(runner, ["decision", "list", "p1"])
assert "Safari bug" in r.output
def test_decision_list_filter(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
invoke(runner, ["decision", "add", "p1", "gotcha", "A", "a", "--category", "ui"])
invoke(runner, ["decision", "add", "p1", "decision", "B", "b", "--category", "arch"])
r = invoke(runner, ["decision", "list", "p1", "--type", "gotcha"])
assert "A" in r.output
assert "B" not in r.output
# -- module --
def test_module_add_and_list(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
r = invoke(runner, ["module", "add", "p1", "search", "frontend", "src/search/",
"--description", "Search UI"])
assert r.exit_code == 0
assert "search" in r.output
r = invoke(runner, ["module", "list", "p1"])
assert "search" in r.output
assert "Search UI" in r.output
# -- status --
def test_status_all(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
invoke(runner, ["task", "add", "p1", "A"])
invoke(runner, ["task", "add", "p1", "B"])
r = invoke(runner, ["status"])
assert r.exit_code == 0
assert "p1" in r.output
assert "2" in r.output # total tasks
def test_status_single_project(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
invoke(runner, ["task", "add", "p1", "A"])
r = invoke(runner, ["status", "p1"])
assert r.exit_code == 0
assert "P1-001" in r.output
assert "pending" in r.output
def test_status_not_found(runner):
r = invoke(runner, ["status", "nope"])
assert r.exit_code == 1
assert "not found" in r.output
# -- cost --
def test_cost_empty(runner):
r = invoke(runner, ["cost"])
assert r.exit_code == 0
assert "No agent runs" in r.output
def test_cost_with_data(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
# Insert agent log directly via models (no CLI command for this)
from core.db import init_db
from core import models as m
# Re-open the same DB the runner uses
db_path = runner[1][1]
conn = init_db(Path(db_path))
m.log_agent_run(conn, "p1", "dev", "implement",
cost_usd=0.10, tokens_used=5000)
conn.close()
r = invoke(runner, ["cost", "--last", "7d"])
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