kin/tests/test_api_pipeline_logs.py

172 lines
7.5 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 GET /api/pipelines/{id}/logs endpoint (KIN-084 Live Console).
Convention #418: since_id cursor pagination for append-only tables.
Convention #420: 404 for non-existent resources.
"""
import pytest
from fastapi.testclient import TestClient
import web.api as api_module
from core.db import init_db
from core import models
# ─────────────────────────────────────────────────────────────
# Fixtures
# ─────────────────────────────────────────────────────────────
@pytest.fixture
def client(tmp_path):
"""Bare TestClient с изолированной БД (без предварительных данных)."""
db_path = tmp_path / "test.db"
api_module.DB_PATH = db_path
from web.api import app
return TestClient(app)
@pytest.fixture
def pipeline_client(tmp_path):
"""TestClient с project + task + pipeline, готовый к тестированию логов."""
db_path = tmp_path / "test.db"
api_module.DB_PATH = db_path
from web.api import app
c = TestClient(app)
# Seed project + task через API
c.post("/api/projects", json={"id": "p1", "name": "P1", "path": "/p1"})
c.post("/api/tasks", json={"project_id": "p1", "title": "Task 1"})
# Создаём pipeline напрямую в БД
conn = init_db(db_path)
pipeline = models.create_pipeline(conn, "P1-001", "p1", "linear", ["step1"])
conn.close()
yield c, pipeline["id"], db_path
# ─────────────────────────────────────────────────────────────
# Тест: пустой pipeline → пустой список
# ─────────────────────────────────────────────────────────────
def test_get_pipeline_logs_empty_returns_empty_list(pipeline_client):
"""GET /api/pipelines/{id}/logs возвращает [] для pipeline без записей."""
c, pipeline_id, _ = pipeline_client
r = c.get(f"/api/pipelines/{pipeline_id}/logs")
assert r.status_code == 200
assert r.json() == []
# ─────────────────────────────────────────────────────────────
# Тест: несуществующий pipeline → 200 [] (KIN-OBS-023)
# ─────────────────────────────────────────────────────────────
def test_get_pipeline_logs_nonexistent_pipeline_returns_empty(client):
"""KIN-OBS-023: GET /api/pipelines/99999/logs → 200 [] (log collections return empty, not 404)."""
r = client.get("/api/pipelines/99999/logs")
assert r.status_code == 200
assert r.json() == []
# ─────────────────────────────────────────────────────────────
# Тест: 3 записи → правильные поля (id, ts, level, message, extra_json)
# ─────────────────────────────────────────────────────────────
def test_get_pipeline_logs_returns_three_entries_with_correct_fields(pipeline_client):
"""После write_log() x3 GET возвращает 3 записи с полями id, ts, level, message, extra_json."""
c, pipeline_id, db_path = pipeline_client
conn = init_db(db_path)
models.write_log(conn, pipeline_id, "PM started", level="INFO")
models.write_log(conn, pipeline_id, "Running agent", level="DEBUG")
models.write_log(conn, pipeline_id, "Agent error", level="ERROR", extra={"code": 500})
conn.close()
r = c.get(f"/api/pipelines/{pipeline_id}/logs")
assert r.status_code == 200
logs = r.json()
assert len(logs) == 3
# Проверяем наличие всех обязательных полей
first = logs[0]
for field in ("id", "ts", "level", "message", "extra_json"):
assert field in first, f"Поле '{field}' отсутствует в ответе"
assert first["message"] == "PM started"
assert first["level"] == "INFO"
assert first["extra_json"] is None
assert logs[2]["level"] == "ERROR"
assert logs[2]["extra_json"] == {"code": 500}
def test_get_pipeline_logs_returns_entries_in_chronological_order(pipeline_client):
"""Записи возвращаются в хронологическом порядке (по id ASC)."""
c, pipeline_id, db_path = pipeline_client
conn = init_db(db_path)
for msg in ("first", "second", "third"):
models.write_log(conn, pipeline_id, msg)
conn.close()
logs = c.get(f"/api/pipelines/{pipeline_id}/logs").json()
assert [e["message"] for e in logs] == ["first", "second", "third"]
# ─────────────────────────────────────────────────────────────
# Тест: since_id cursor pagination (Convention #418)
# ─────────────────────────────────────────────────────────────
def test_get_pipeline_logs_since_id_returns_entries_after_cursor(pipeline_client):
"""GET ?since_id=<id> возвращает только записи с id > since_id."""
c, pipeline_id, db_path = pipeline_client
conn = init_db(db_path)
for i in range(1, 6): # 5 записей
models.write_log(conn, pipeline_id, f"Message {i}")
conn.close()
# Получаем все записи
all_logs = c.get(f"/api/pipelines/{pipeline_id}/logs").json()
assert len(all_logs) == 5
# Берём id третьей записи как курсор
cursor_id = all_logs[2]["id"]
r = c.get(f"/api/pipelines/{pipeline_id}/logs?since_id={cursor_id}")
assert r.status_code == 200
partial = r.json()
assert len(partial) == 2
for entry in partial:
assert entry["id"] > cursor_id
def test_get_pipeline_logs_since_id_zero_returns_all(pipeline_client):
"""GET ?since_id=0 возвращает все записи (значение по умолчанию)."""
c, pipeline_id, db_path = pipeline_client
conn = init_db(db_path)
models.write_log(conn, pipeline_id, "A")
models.write_log(conn, pipeline_id, "B")
conn.close()
r = c.get(f"/api/pipelines/{pipeline_id}/logs?since_id=0")
assert r.status_code == 200
assert len(r.json()) == 2
def test_get_pipeline_logs_since_id_beyond_last_returns_empty(pipeline_client):
"""GET ?since_id=<last_id> возвращает [] (нет записей после последней)."""
c, pipeline_id, db_path = pipeline_client
conn = init_db(db_path)
models.write_log(conn, pipeline_id, "Only entry")
conn.close()
all_logs = c.get(f"/api/pipelines/{pipeline_id}/logs").json()
last_id = all_logs[-1]["id"]
r = c.get(f"/api/pipelines/{pipeline_id}/logs?since_id={last_id}")
assert r.status_code == 200
assert r.json() == []