kin/tests/test_cli.py
Gros Frumos 6705b302f7 test(KIN-005): parameterize task status update test for all valid statuses
Expand test_task_update_status to test all 7 valid statuses including
'cancelled' via CLI. Each status now has its own test case through
pytest parametrization.

Test suite now: 208 → 214 tests (all passing ✓)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-15 18:48:16 +02:00

323 lines
10 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
# ===========================================================================
@pytest.mark.parametrize("status", ["pending", "in_progress", "review", "done", "blocked", "decomposed", "cancelled"])
def test_task_update_status(runner, status):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
invoke(runner, ["task", "add", "p1", "Fix bug"])
r = invoke(runner, ["task", "update", "P1-001", "--status", status])
assert r.exit_code == 0
assert status in r.output
r = invoke(runner, ["task", "show", "P1-001"])
assert status 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
# ===========================================================================
# hook
# ===========================================================================
def test_hook_add_and_list(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
r = invoke(runner, ["hook", "add",
"--project", "p1",
"--name", "rebuild",
"--event", "pipeline_completed",
"--command", "npm run build"])
assert r.exit_code == 0
assert "rebuild" in r.output
assert "pipeline_completed" in r.output
r = invoke(runner, ["hook", "list", "--project", "p1"])
assert r.exit_code == 0
assert "rebuild" in r.output
assert "npm run build" in r.output
def test_hook_add_with_module_path(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
r = invoke(runner, ["hook", "add",
"--project", "p1",
"--name", "fe-build",
"--event", "pipeline_completed",
"--command", "make build",
"--module-path", "web/frontend/*",
"--working-dir", "/tmp"])
assert r.exit_code == 0
r = invoke(runner, ["hook", "list", "--project", "p1"])
assert "web/frontend/*" in r.output
def test_hook_add_project_not_found(runner):
r = invoke(runner, ["hook", "add",
"--project", "nope",
"--name", "x",
"--event", "pipeline_completed",
"--command", "echo hi"])
assert r.exit_code == 1
assert "not found" in r.output
def test_hook_list_empty(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
r = invoke(runner, ["hook", "list", "--project", "p1"])
assert r.exit_code == 0
assert "No hooks" in r.output
def test_hook_remove(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
invoke(runner, ["hook", "add",
"--project", "p1",
"--name", "rebuild",
"--event", "pipeline_completed",
"--command", "make"])
r = invoke(runner, ["hook", "remove", "1"])
assert r.exit_code == 0
assert "Removed" in r.output
r = invoke(runner, ["hook", "list", "--project", "p1"])
assert "No hooks" in r.output
def test_hook_remove_not_found(runner):
r = invoke(runner, ["hook", "remove", "999"])
assert r.exit_code == 1
assert "not found" in r.output
def test_hook_logs_empty(runner):
invoke(runner, ["project", "add", "p1", "P1", "/p1"])
r = invoke(runner, ["hook", "logs", "--project", "p1"])
assert r.exit_code == 0
assert "No hook logs" in r.output