kin: KIN-021 Аудит-лог для --dangerously-skip-permissions в auto mode

This commit is contained in:
Gros Frumos 2026-03-16 07:13:32 +02:00
parent 67071c757d
commit a0b0976d8d
16 changed files with 1477 additions and 14 deletions

View file

@ -608,6 +608,42 @@ def test_run_kin_040_allow_write_true_ignored(client):
# KIN-058 — регрессионный тест: stderr=DEVNULL у Popen в web API
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# KIN-020 — manual_escalation задачи: PATCH status='done' резолвит задачу
# ---------------------------------------------------------------------------
def test_patch_manual_escalation_task_to_done(client):
"""PATCH status='done' на manual_escalation задаче — статус обновляется — KIN-020."""
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
models.create_task(conn, "P1-002", "p1", "Fix .dockerignore manually",
brief={"task_type": "manual_escalation",
"source": "followup:P1-001",
"description": "Ручное применение .dockerignore"})
conn.close()
r = client.patch("/api/tasks/P1-002", json={"status": "done"})
assert r.status_code == 200
assert r.json()["status"] == "done"
def test_manual_escalation_task_brief_preserved_after_patch(client):
"""PATCH не затирает brief.task_type — поле manual_escalation сохраняется — KIN-020."""
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
models.create_task(conn, "P1-002", "p1", "Fix manually",
brief={"task_type": "manual_escalation",
"source": "followup:P1-001"})
conn.close()
client.patch("/api/tasks/P1-002", json={"status": "done"})
r = client.get("/api/tasks/P1-002")
assert r.status_code == 200
assert r.json()["brief"]["task_type"] == "manual_escalation"
def test_run_sets_stderr_devnull(client):
"""Регрессионный тест KIN-058: stderr=DEVNULL всегда устанавливается в Popen,
чтобы stderr дочернего процесса не загрязнял логи uvicorn."""
@ -626,3 +662,186 @@ def test_run_sets_stderr_devnull(client):
"Регрессия KIN-058: stderr у Popen должен быть DEVNULL, "
"иначе вывод агента попадает в логи uvicorn"
)
# ---------------------------------------------------------------------------
# KIN-065 — PATCH /api/projects/{id} — autocommit_enabled toggle
# ---------------------------------------------------------------------------
def test_patch_project_autocommit_enabled_true(client):
"""PATCH с autocommit_enabled=true → 200, поле установлено в 1."""
r = client.patch("/api/projects/p1", json={"autocommit_enabled": True})
assert r.status_code == 200
assert r.json()["autocommit_enabled"] == 1
def test_patch_project_autocommit_enabled_false(client):
"""После включения PATCH с autocommit_enabled=false → 200, поле установлено в 0."""
client.patch("/api/projects/p1", json={"autocommit_enabled": True})
r = client.patch("/api/projects/p1", json={"autocommit_enabled": False})
assert r.status_code == 200
assert r.json()["autocommit_enabled"] == 0
def test_patch_project_autocommit_persisted_via_sql(client):
"""После PATCH autocommit_enabled=True прямой SQL подтверждает значение 1."""
client.patch("/api/projects/p1", json={"autocommit_enabled": True})
from core.db import init_db
conn = init_db(api_module.DB_PATH)
row = conn.execute("SELECT autocommit_enabled FROM projects WHERE id = 'p1'").fetchone()
conn.close()
assert row is not None
assert row[0] == 1
def test_patch_project_autocommit_false_persisted_via_sql(client):
"""После PATCH autocommit_enabled=False прямой SQL подтверждает значение 0."""
client.patch("/api/projects/p1", json={"autocommit_enabled": True})
client.patch("/api/projects/p1", json={"autocommit_enabled": False})
from core.db import init_db
conn = init_db(api_module.DB_PATH)
row = conn.execute("SELECT autocommit_enabled FROM projects WHERE id = 'p1'").fetchone()
conn.close()
assert row is not None
assert row[0] == 0
def test_patch_project_autocommit_null_before_first_update(client):
"""Новый проект имеет autocommit_enabled=NULL/0 (falsy) до первого обновления."""
client.post("/api/projects", json={"id": "p_new", "name": "New", "path": "/new"})
from core.db import init_db
conn = init_db(api_module.DB_PATH)
row = conn.execute("SELECT autocommit_enabled FROM projects WHERE id = 'p_new'").fetchone()
conn.close()
assert row is not None
assert not row[0] # DEFAULT 0 или NULL — в любом случае falsy
def test_patch_project_empty_body_returns_400(client):
"""PATCH проекта без полей → 400."""
r = client.patch("/api/projects/p1", json={})
assert r.status_code == 400
def test_patch_project_not_found(client):
"""PATCH несуществующего проекта → 404."""
r = client.patch("/api/projects/NOPE", json={"autocommit_enabled": True})
assert r.status_code == 404
def test_patch_project_autocommit_and_execution_mode_together(client):
"""PATCH с autocommit_enabled и execution_mode → оба поля обновлены."""
r = client.patch("/api/projects/p1", json={
"autocommit_enabled": True,
"execution_mode": "auto_complete",
})
assert r.status_code == 200
data = r.json()
assert data["autocommit_enabled"] == 1
assert data["execution_mode"] == "auto_complete"
def test_patch_project_returns_full_project_object(client):
"""PATCH возвращает полный объект проекта с id, name и autocommit_enabled."""
r = client.patch("/api/projects/p1", json={"autocommit_enabled": True})
assert r.status_code == 200
data = r.json()
assert data["id"] == "p1"
assert data["name"] == "P1"
assert "autocommit_enabled" in data
# ---------------------------------------------------------------------------
# KIN-008 — PATCH priority и route_type задачи
# ---------------------------------------------------------------------------
def test_patch_task_priority(client):
"""PATCH priority задачи обновляет поле и возвращает задачу."""
r = client.patch("/api/tasks/P1-001", json={"priority": 3})
assert r.status_code == 200
assert r.json()["priority"] == 3
def test_patch_task_priority_persisted(client):
"""После PATCH priority повторный GET возвращает новое значение."""
client.patch("/api/tasks/P1-001", json={"priority": 7})
r = client.get("/api/tasks/P1-001")
assert r.status_code == 200
assert r.json()["priority"] == 7
def test_patch_task_priority_invalid_zero(client):
"""PATCH с priority=0 → 400."""
r = client.patch("/api/tasks/P1-001", json={"priority": 0})
assert r.status_code == 400
def test_patch_task_priority_invalid_eleven(client):
"""PATCH с priority=11 → 400."""
r = client.patch("/api/tasks/P1-001", json={"priority": 11})
assert r.status_code == 400
def test_patch_task_route_type_set(client):
"""PATCH route_type сохраняет значение в brief."""
r = client.patch("/api/tasks/P1-001", json={"route_type": "feature"})
assert r.status_code == 200
data = r.json()
assert data["brief"]["route_type"] == "feature"
def test_patch_task_route_type_all_valid(client):
"""Все допустимые route_type принимаются."""
for rt in ("debug", "feature", "refactor", "hotfix"):
r = client.patch("/api/tasks/P1-001", json={"route_type": rt})
assert r.status_code == 200, f"route_type={rt} rejected"
assert r.json()["brief"]["route_type"] == rt
def test_patch_task_route_type_invalid(client):
"""Недопустимый route_type → 400."""
r = client.patch("/api/tasks/P1-001", json={"route_type": "unknown"})
assert r.status_code == 400
def test_patch_task_route_type_clear(client):
"""PATCH route_type='' очищает поле из brief."""
client.patch("/api/tasks/P1-001", json={"route_type": "debug"})
r = client.patch("/api/tasks/P1-001", json={"route_type": ""})
assert r.status_code == 200
data = r.json()
brief = data.get("brief")
if brief:
assert "route_type" not in brief
def test_patch_task_route_type_merges_brief(client):
"""route_type сохраняется вместе с другими полями brief без перезаписи."""
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
models.update_task(conn, "P1-001", brief={"extra": "data"})
conn.close()
r = client.patch("/api/tasks/P1-001", json={"route_type": "hotfix"})
assert r.status_code == 200
brief = r.json()["brief"]
assert brief["route_type"] == "hotfix"
assert brief["extra"] == "data"
def test_patch_task_priority_and_route_type_together(client):
"""PATCH может обновить priority и route_type одновременно."""
r = client.patch("/api/tasks/P1-001", json={"priority": 2, "route_type": "refactor"})
assert r.status_code == 200
data = r.json()
assert data["priority"] == 2
assert data["brief"]["route_type"] == "refactor"
def test_patch_task_empty_body_still_returns_400(client):
"""Пустое тело по-прежнему возвращает 400 (регрессия KIN-008)."""
r = client.patch("/api/tasks/P1-001", json={})
assert r.status_code == 400