208 lines
6 KiB
Python
208 lines
6 KiB
Python
|
|
"""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
|