# 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"] )