169 lines
6.0 KiB
Python
169 lines
6.0 KiB
Python
# 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"]
|
|
)
|