"""Тесты журнала действий по задачам (ТЗ 3.20)."""
from typing import Any
from fastapi.testclient import TestClient
from app import mcp_server
from tests.conftest import agent_ctx, mcp_ctx # type: ignore[attr-defined]
def _events(client: TestClient, **params: Any) -> dict[str, Any]:
resp = client.get("/api/task-events", params=params)
assert resp.status_code == 200, resp.text
return resp.json()
def _create(client: TestClient, title: str = "задача") -> dict[str, Any]:
resp = client.post("/api/tasks", json={"title": title})
assert resp.status_code == 200, resp.text
return resp.json()
def test_create_logs_event_from_ui(client: TestClient) -> None:
task = _create(client, "позвонить маме")
page = _events(client, kind="created")
assert page["total"] == 1
row = page["items"][0]
assert row["task_id"] == task["id"]
assert row["task_title"] == "позвонить маме"
assert row["actor_kind"] == "user"
assert row["via"] == "ui"
def test_status_and_eligibility_changes_logged(client: TestClient) -> None:
task = _create(client)
client.patch(f"/api/tasks/{task['id']}", json={"status": "in_progress"})
client.patch(f"/api/tasks/{task['id']}", json={"ai_eligible": True})
# PATCH, ничего не изменивший, журнал не засоряет
client.patch(f"/api/tasks/{task['id']}", json={"ai_eligible": True})
client.patch(f"/api/tasks/{task['id']}", json={"priority": 5})
kinds = [row["kind"] for row in _events(client)["items"]]
assert kinds == ["eligible_changed", "status_changed", "created"]
status_row = _events(client, kind="status_changed")["items"][0]
assert status_row["comment"] == "to_do→in_progress"
assert _events(client, kind="eligible_changed")["items"][0]["comment"] == "on"
def test_close_logs_completed_with_comment(client: TestClient) -> None:
task = _create(client)
resp = client.patch(f"/api/tasks/{task['id']}", json={"status": "done", "close_comment": "всё"})
assert resp.status_code == 200, resp.text
row = _events(client, kind="completed")["items"][0]
assert row["comment"] == "всё"
def test_delete_keeps_event_with_title_snapshot(client: TestClient) -> None:
task = _create(client, "удаляемая")
assert client.delete(f"/api/tasks/{task['id']}").status_code == 200
row = _events(client, kind="deleted")["items"][0]
# Ссылки на задачу нет — она удалена; запись живёт снапшотом заголовка
assert row["task_id"] is None
assert row["task_title"] == "удаляемая"
def test_events_are_scoped_to_owner(client: TestClient) -> None:
"""Журнал чужого пользователя не виден (изоляция по user_id, ТЗ 1.2)."""
from app.dependencies import require_user
_create(client)
overrides = client.app.dependency_overrides # type: ignore[attr-defined]
saved = overrides[require_user]
overrides[require_user] = lambda: {"user_id": "2", "email": "other@example.com"}
try:
assert _events(client)["total"] == 0
finally:
overrides[require_user] = saved
assert _events(client)["total"] == 1
def test_pagination_envelope_and_filters(client: TestClient) -> None:
project_id = client.post("/api/projects", json={"name": "дом"}).json()["id"]
for i in range(5):
task = _create(client, f"задача {i}")
client.patch(
f"/api/tasks/{task['id']}", json={"project_id": project_id, "status": "in_progress"}
)
# 5 созданий + 5 смен статуса; смена проекта — не событие журнала
page = _events(client, page=2, per_page=3)
assert (page["total"], page["page"], page["per_page"]) == (10, 2, 3)
assert len(page["items"]) == 3
assert _events(client, project_id=project_id)["total"] == 5
# Потолок per_page: журнал растёт быстро, отдавать его целиком нельзя
assert client.get("/api/task-events", params={"per_page": 101}).status_code == 422
assert client.get("/api/task-events", params={"page": 0}).status_code == 422
def test_mcp_agent_actions_are_marked(client: TestClient) -> None:
"""Действия агента отличимы от действий владельца: актор и канал."""
ctx = agent_ctx(label="Нави")
created = mcp_server.create_task("от агента", ai_eligible=True, ctx=ctx)
mcp_server.update_task(created["id"], status="in_progress", ctx=ctx)
rows = _events(client)["items"]
assert [row["kind"] for row in rows] == ["status_changed", "created"]
assert {(row["actor_kind"], row["actor_name"], row["via"]) for row in rows} == {
("agent", "Нави", "mcp")
}
def test_mcp_is_user_marks_owner(client: TestClient) -> None:
"""is_user=True — действие от имени владельца, но канал остаётся mcp."""
ctx = agent_ctx(label="Нави", owner="Хозяин")
created = mcp_server.create_task("как я", is_user=True, ctx=ctx)
row = _events(client, kind="created")["items"][0]
assert (row["actor_kind"], row["actor_name"], row["via"]) == ("user", "Хозяин", "mcp")
assert created["id"]
def test_mcp_close_comment_lands_in_journal(client: TestClient) -> None:
ctx = mcp_ctx()
created = mcp_server.create_task("задача", ctx=ctx)
mcp_server.complete_task(created["id"], comment="сделано", ctx=ctx)
assert _events(client, kind="completed")["items"][0]["comment"] == "сделано"