Pipeline/backend/tests/test_codex_sessions_api.py

169 lines
6.0 KiB
Python
Raw Normal View History

# ruff: noqa: INP001
"""API tests for Codex session endpoints."""
from __future__ import annotations
import json
from pathlib import Path
from uuid import uuid4
import pytest
from fastapi import APIRouter, FastAPI
from httpx import ASGITransport, AsyncClient
from app.api.agent_sessions import router as agent_sessions_router
from app.api.codex_sessions import router as codex_router
from app.services.organizations import OrganizationContext
def _write_jsonl(path: Path, records: list[dict]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
"\n".join(json.dumps(record) for record in records) + "\n",
encoding="utf-8",
)
def _records() -> list[dict]:
return [
{
"timestamp": "2026-05-20T12:00:00Z",
"type": "session_meta",
"payload": {"id": "api-session", "cwd": "/work/pipeline", "source": "terminal"},
},
{
"timestamp": "2026-05-20T12:00:01Z",
"type": "turn_context",
"payload": {"model": "gpt-5.5", "cwd": "/work/pipeline"},
},
{
"timestamp": "2026-05-20T12:00:02Z",
"type": "response_item",
"payload": {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "hello"}],
},
},
{
"timestamp": "2026-05-20T12:00:03Z",
"type": "response_item",
"payload": {
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "hi"}],
},
},
{
"timestamp": "2026-05-20T12:00:04Z",
"type": "response_item",
"payload": {
"type": "function_call",
"call_id": "call-api",
"name": "exec_command",
"arguments": json.dumps({"cmd": "npm test"}),
},
},
{
"timestamp": "2026-05-20T12:00:05Z",
"type": "event_msg",
"payload": {
"type": "token_count",
"info": {
"last_token_usage": {"input_tokens": 1, "output_tokens": 2},
"total_token_usage": {"input_tokens": 3, "output_tokens": 4},
},
},
},
]
def _build_app() -> FastAPI:
app = FastAPI()
api_v1 = APIRouter(prefix="/api/v1")
api_v1.include_router(codex_router)
api_v1.include_router(agent_sessions_router)
app.include_router(api_v1)
async def _override_org_member() -> OrganizationContext:
from app.models.organization_members import OrganizationMember
from app.models.organizations import Organization
org = Organization(id=uuid4(), name="test-org")
member = OrganizationMember(organization_id=org.id, user_id=uuid4(), role="admin")
return OrganizationContext(organization=org, member=member)
from app.api.deps import require_org_member
app.dependency_overrides[require_org_member] = _override_org_member
return app
@pytest.mark.asyncio
async def test_codex_sessions_endpoints(tmp_path: Path, monkeypatch) -> None:
root = tmp_path / "sessions"
_write_jsonl(root / "2026/05/20/rollout-api.jsonl", _records())
monkeypatch.setenv("CODEX_SESSIONS_PATH", str(root))
async with AsyncClient(
transport=ASGITransport(app=_build_app()), base_url="http://test"
) as client:
list_response = await client.get("/api/v1/codex/sessions")
assert list_response.status_code == 200
list_data = list_response.json()
assert list_data["source"] == "codex_cli"
assert list_data["source_status"] == "available"
assert list_data["total"] == 1
assert list_data["sessions"][0]["session_id"] == "api-session"
assert list_data["sessions"][0]["provider_label"] == "Codex CLI"
messages_response = await client.get("/api/v1/codex/sessions/api-session/messages")
assert messages_response.status_code == 200
messages_data = messages_response.json()
assert messages_data["source"] == "codex_cli"
assert messages_data["total"] == 2
assert messages_data["messages"][1]["tool_uses"][0]["tool_name"] == "exec_command"
analytics_response = await client.get("/api/v1/codex/analytics/tools?days=30")
assert analytics_response.status_code == 200
assert analytics_response.json()["top_commands"] == [{"command": "npm", "count": 1}]
@pytest.mark.asyncio
async def test_missing_codex_history_returns_source_unavailable(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.setenv("CODEX_SESSIONS_PATH", str(tmp_path / "missing"))
async with AsyncClient(
transport=ASGITransport(app=_build_app()), base_url="http://test"
) as client:
response = await client.get("/api/v1/codex/sessions")
assert response.status_code == 200
data = response.json()
assert data["sessions"] == []
assert data["source_status"] == "unavailable"
assert data["unavailable_reason"]
@pytest.mark.asyncio
async def test_agent_session_sources_include_openai_unavailable(
tmp_path: Path, monkeypatch
) -> None:
root = tmp_path / "sessions"
_write_jsonl(root / "rollout-api.jsonl", _records())
monkeypatch.setenv("CODEX_SESSIONS_PATH", str(root))
monkeypatch.setenv("CLAUDE_PROJECTS_PATH", str(tmp_path / "claude-missing"))
async with AsyncClient(
transport=ASGITransport(app=_build_app()), base_url="http://test"
) as client:
response = await client.get("/api/v1/agent-sessions/sources")
assert response.status_code == 200
sources = {source["source"]: source for source in response.json()["sources"]}
assert sources["codex_cli"]["source_status"] == "available"
assert sources["openai_api"]["source_status"] == "unavailable"
assert (
"owned OpenAI API session event source" in sources["openai_api"]["unavailable_reason"]
)