from __future__ import annotations import asyncio from datetime import datetime, timezone from fastapi.responses import JSONResponse from fastapi.testclient import TestClient import app_runtime.core.runtime as runtime_module from app_runtime.control.base import ControlActionRequest, TraceQueryRequest from app_runtime.control.http_app import HttpControlAppFactory from app_runtime.contracts.trace import TraceLogRecord, TraceLogView from app_runtime.core.runtime import RuntimeManager from app_runtime.tracing.reader import MySqlTraceLogReader def _trace_record( *, row_id: int, level: str, message: str, step: str = "process", status: str = "failed", attrs_json: object | None = None, ) -> TraceLogRecord: return TraceLogRecord( id=row_id, trace_id="trace-1", event_time=datetime(2026, 4, 28, 10, 11, 12, tzinfo=timezone.utc), step=step, status=status, level=level, # type: ignore[arg-type] message=message, attrs_json=attrs_json if attrs_json is not None else {}, ) def _build_client(trace_provider=None) -> TestClient: async def health_provider(): return {"status": "ok"} async def action_provider(_action: str, _client_source: str, _request: ControlActionRequest) -> JSONResponse: return JSONResponse(content={"status": "ok"}) app = HttpControlAppFactory().create(health_provider, action_provider, trace_provider) return TestClient(app) def test_trace_endpoint_returns_text_with_default_levels() -> None: captured: list[tuple[str, TraceQueryRequest]] = [] async def trace_provider(trace_id: str, request: TraceQueryRequest) -> TraceLogView: captured.append((trace_id, request)) return TraceLogView( trace_id="trace-1", parent_id="root-trace", child_ids=("child-1", "child-2"), records=( _trace_record(row_id=1, level="ERROR", message="first error"), _trace_record(row_id=2, level="WARNING", message="second warning"), ), ) client = _build_client(trace_provider) try: response = client.get("/traces/trace-1") finally: client.close() assert response.status_code == 200 assert response.text == ( "trace_id: trace-1\n" "parent_id: root-trace\n" "child_ids:\n" " - child-1\n" " - child-2\n" "--------------------------------------------------\n" "step: process\n" "first error\n" "second warning" ) assert captured == [("trace-1", TraceQueryRequest(levels=("ERROR", "WARNING", "INFO"), include_attrs_json=False, response_format="text"))] def test_trace_endpoint_appends_attrs_json_in_text_mode() -> None: async def trace_provider(_trace_id: str, _request: TraceQueryRequest) -> TraceLogView: return TraceLogView( trace_id="trace-1", parent_id=None, child_ids=(), records=( _trace_record(row_id=1, level="ERROR", message="failure", attrs_json={"attempt": 2, "source": "crm"}), ), ) client = _build_client(trace_provider) try: response = client.get("/traces/trace-1?attrs_json=true") finally: client.close() assert response.status_code == 200 assert response.text == ( "trace_id: trace-1\n" "parent_id: \n" "child_ids:\n" "--------------------------------------------------\n" "step: process\n" 'failure, {"attempt":2,"source":"crm"}' ) def test_trace_endpoint_separates_messages_by_step_in_text_mode() -> None: async def trace_provider(_trace_id: str, _request: TraceQueryRequest) -> TraceLogView: return TraceLogView( trace_id="trace-1", parent_id=None, child_ids=(), records=( _trace_record(row_id=1, level="INFO", message="load first", step="load_stocks"), _trace_record(row_id=2, level="INFO", message="load second", step="load_stocks"), _trace_record(row_id=3, level="INFO", message="filter first", step="filter_stocks"), ), ) client = _build_client(trace_provider) try: response = client.get("/traces/trace-1") finally: client.close() assert response.status_code == 200 assert response.text == ( "trace_id: trace-1\n" "parent_id: \n" "child_ids:\n" "--------------------------------------------------\n" "step: load_stocks\n" "load first\n" "load second\n" "--------------------------------------------------\n" "step: filter_stocks\n" "filter first" ) def test_trace_endpoint_returns_json_payload() -> None: async def trace_provider(_trace_id: str, _request: TraceQueryRequest) -> TraceLogView: return TraceLogView( trace_id="trace-1", parent_id="parent-1", child_ids=("child-1",), records=( _trace_record(row_id=3, level="INFO", message="done", attrs_json={"batch": 7}), ), ) client = _build_client(trace_provider) try: response = client.get("/traces/trace-1?format=json&attrs_json=true&levels=info") finally: client.close() assert response.status_code == 200 assert response.json() == { "trace_id": "trace-1", "parent_id": "parent-1", "child_ids": ["child-1"], "messages": [ { "id": 3, "trace_id": "trace-1", "event_time": "2026-04-28T10:11:12+00:00", "step": "process", "status": "failed", "level": "INFO", "message": "done", "attrs_json": {"batch": 7}, } ], } def test_trace_endpoint_returns_html_page_with_related_links() -> None: async def trace_provider(_trace_id: str, _request: TraceQueryRequest) -> TraceLogView: return TraceLogView( trace_id="trace-1", parent_id="parent-1", child_ids=("child-1", "child-2"), records=( _trace_record(row_id=1, level="INFO", message="loaded prices", step="load_stocks", status="ok"), _trace_record(row_id=2, level="WARNING", message="filtered suspicious ticker", step="filter_stocks", status="degraded"), ), ) client = _build_client(trace_provider) try: response = client.get("/traces/trace-1?format=html&attrs_json=true") finally: client.close() assert response.status_code == 200 assert response.headers["content-type"].startswith("text/html") assert "