mirror of
https://github.com/Xe138/AI-Trader.git
synced 2026-04-02 17:37:24 -04:00
- Add Pydantic models for reasoning API responses - Implement GET /reasoning with job_id, date, model filters - Support include_full_conversation parameter - Add comprehensive unit tests (8 tests) - Return deployment mode info in responses 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
318 lines
11 KiB
Python
318 lines
11 KiB
Python
"""
|
|
Unit tests for GET /reasoning API endpoint.
|
|
|
|
Coverage target: 95%+
|
|
|
|
Tests verify:
|
|
- Filtering by job_id, date, and model
|
|
- Full conversation vs summaries only
|
|
- Error handling (404, 400)
|
|
- Deployment mode info in responses
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime
|
|
from api.database import get_db_connection
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_trading_session(clean_db):
|
|
"""Create a sample trading session with positions and reasoning logs."""
|
|
conn = get_db_connection(clean_db)
|
|
cursor = conn.cursor()
|
|
|
|
# Create job
|
|
cursor.execute("""
|
|
INSERT INTO jobs (job_id, config_path, status, date_range, models, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
"test-job-123",
|
|
"configs/test.json",
|
|
"completed",
|
|
'["2025-10-02"]',
|
|
'["gpt-5"]',
|
|
"2025-10-02T10:00:00Z"
|
|
))
|
|
|
|
# Create trading session
|
|
cursor.execute("""
|
|
INSERT INTO trading_sessions (job_id, date, model, session_summary, started_at, completed_at, total_messages)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
"test-job-123",
|
|
"2025-10-02",
|
|
"gpt-5",
|
|
"Analyzed AI infrastructure market. Bought NVDA and GOOGL based on secular AI trends.",
|
|
"2025-10-02T10:00:00Z",
|
|
"2025-10-02T10:05:23Z",
|
|
4
|
|
))
|
|
|
|
session_id = cursor.lastrowid
|
|
|
|
# Create positions linked to session
|
|
cursor.execute("""
|
|
INSERT INTO positions (
|
|
job_id, date, model, action_id, action_type, symbol, amount, price,
|
|
cash, portfolio_value, daily_profit, daily_return_pct, session_id, created_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
"test-job-123", "2025-10-02", "gpt-5", 1, "buy", "NVDA", 10, 189.60,
|
|
8104.00, 10000.00, 0.0, 0.0, session_id, "2025-10-02T10:05:00Z"
|
|
))
|
|
|
|
cursor.execute("""
|
|
INSERT INTO positions (
|
|
job_id, date, model, action_id, action_type, symbol, amount, price,
|
|
cash, portfolio_value, daily_profit, daily_return_pct, session_id, created_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
"test-job-123", "2025-10-02", "gpt-5", 2, "buy", "GOOGL", 6, 245.15,
|
|
6633.10, 10104.00, 104.00, 1.04, session_id, "2025-10-02T10:05:10Z"
|
|
))
|
|
|
|
# Create reasoning logs
|
|
cursor.execute("""
|
|
INSERT INTO reasoning_logs (session_id, message_index, role, content, summary, timestamp)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
session_id, 0, "user",
|
|
"Please analyze and update today's (2025-10-02) positions.",
|
|
None,
|
|
"2025-10-02T10:00:00Z"
|
|
))
|
|
|
|
cursor.execute("""
|
|
INSERT INTO reasoning_logs (session_id, message_index, role, content, summary, timestamp)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
session_id, 1, "assistant",
|
|
"Key intermediate steps\n\n- Read yesterday's positions...",
|
|
"Analyzed market conditions and decided to buy NVDA (10 shares) and GOOGL (6 shares).",
|
|
"2025-10-02T10:05:20Z"
|
|
))
|
|
|
|
cursor.execute("""
|
|
INSERT INTO reasoning_logs (session_id, message_index, role, content, summary, tool_name, tool_input, timestamp)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
session_id, 2, "tool",
|
|
"Successfully bought 10 shares of NVDA at $189.60",
|
|
None,
|
|
"trade",
|
|
'{"action": "buy", "symbol": "NVDA", "amount": 10}',
|
|
"2025-10-02T10:05:21Z"
|
|
))
|
|
|
|
cursor.execute("""
|
|
INSERT INTO reasoning_logs (session_id, message_index, role, content, summary, tool_name, tool_input, timestamp)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
session_id, 3, "tool",
|
|
"Successfully bought 6 shares of GOOGL at $245.15",
|
|
None,
|
|
"trade",
|
|
'{"action": "buy", "symbol": "GOOGL", "amount": 6}',
|
|
"2025-10-02T10:05:22Z"
|
|
))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return {
|
|
"session_id": session_id,
|
|
"job_id": "test-job-123",
|
|
"date": "2025-10-02",
|
|
"model": "gpt-5"
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def multiple_sessions(clean_db):
|
|
"""Create multiple trading sessions for testing filters."""
|
|
conn = get_db_connection(clean_db)
|
|
cursor = conn.cursor()
|
|
|
|
# Create job
|
|
cursor.execute("""
|
|
INSERT INTO jobs (job_id, config_path, status, date_range, models, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
"test-job-456",
|
|
"configs/test.json",
|
|
"completed",
|
|
'["2025-10-03", "2025-10-04"]',
|
|
'["gpt-5", "claude-4"]',
|
|
"2025-10-03T10:00:00Z"
|
|
))
|
|
|
|
# Session 1: gpt-5, 2025-10-03
|
|
cursor.execute("""
|
|
INSERT INTO trading_sessions (job_id, date, model, session_summary, started_at, completed_at, total_messages)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
"test-job-456", "2025-10-03", "gpt-5",
|
|
"Session 1 summary", "2025-10-03T10:00:00Z", "2025-10-03T10:05:00Z", 2
|
|
))
|
|
session1_id = cursor.lastrowid
|
|
|
|
# Session 2: claude-4, 2025-10-03
|
|
cursor.execute("""
|
|
INSERT INTO trading_sessions (job_id, date, model, session_summary, started_at, completed_at, total_messages)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
"test-job-456", "2025-10-03", "claude-4",
|
|
"Session 2 summary", "2025-10-03T10:00:00Z", "2025-10-03T10:05:00Z", 2
|
|
))
|
|
session2_id = cursor.lastrowid
|
|
|
|
# Session 3: gpt-5, 2025-10-04
|
|
cursor.execute("""
|
|
INSERT INTO trading_sessions (job_id, date, model, session_summary, started_at, completed_at, total_messages)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
"test-job-456", "2025-10-04", "gpt-5",
|
|
"Session 3 summary", "2025-10-04T10:00:00Z", "2025-10-04T10:05:00Z", 2
|
|
))
|
|
session3_id = cursor.lastrowid
|
|
|
|
# Add positions for each session
|
|
for session_id, date, model in [(session1_id, "2025-10-03", "gpt-5"),
|
|
(session2_id, "2025-10-03", "claude-4"),
|
|
(session3_id, "2025-10-04", "gpt-5")]:
|
|
cursor.execute("""
|
|
INSERT INTO positions (
|
|
job_id, date, model, action_id, action_type, symbol, amount, price,
|
|
cash, portfolio_value, session_id, created_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
"test-job-456", date, model, 1, "buy", "AAPL", 5, 250.00,
|
|
8750.00, 10000.00, session_id, f"{date}T10:05:00Z"
|
|
))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return {
|
|
"job_id": "test-job-456",
|
|
"session_ids": [session1_id, session2_id, session3_id]
|
|
}
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestGetReasoningEndpoint:
|
|
"""Test GET /reasoning endpoint."""
|
|
|
|
def test_get_reasoning_with_job_id_filter(self, client, sample_trading_session):
|
|
"""Should return sessions filtered by job_id."""
|
|
response = client.get(f"/reasoning?job_id={sample_trading_session['job_id']}")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["count"] == 1
|
|
assert len(data["sessions"]) == 1
|
|
assert data["sessions"][0]["job_id"] == sample_trading_session["job_id"]
|
|
assert data["sessions"][0]["date"] == sample_trading_session["date"]
|
|
assert data["sessions"][0]["model"] == sample_trading_session["model"]
|
|
assert data["sessions"][0]["session_summary"] is not None
|
|
assert len(data["sessions"][0]["positions"]) == 2
|
|
|
|
def test_get_reasoning_with_date_filter(self, client, multiple_sessions):
|
|
"""Should return sessions filtered by date."""
|
|
response = client.get("/reasoning?date=2025-10-03")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["count"] == 2 # Both gpt-5 and claude-4 on 2025-10-03
|
|
assert all(s["date"] == "2025-10-03" for s in data["sessions"])
|
|
|
|
def test_get_reasoning_with_model_filter(self, client, multiple_sessions):
|
|
"""Should return sessions filtered by model."""
|
|
response = client.get("/reasoning?model=gpt-5")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["count"] == 2 # gpt-5 on both dates
|
|
assert all(s["model"] == "gpt-5" for s in data["sessions"])
|
|
|
|
def test_get_reasoning_with_full_conversation(self, client, sample_trading_session):
|
|
"""Should include full conversation when requested."""
|
|
response = client.get(
|
|
f"/reasoning?job_id={sample_trading_session['job_id']}&include_full_conversation=true"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["count"] == 1
|
|
|
|
session = data["sessions"][0]
|
|
assert session["conversation"] is not None
|
|
assert len(session["conversation"]) == 4 # 1 user + 1 assistant + 2 tool messages
|
|
|
|
# Verify message structure
|
|
messages = session["conversation"]
|
|
assert messages[0]["role"] == "user"
|
|
assert messages[0]["message_index"] == 0
|
|
assert messages[0]["summary"] is None
|
|
|
|
assert messages[1]["role"] == "assistant"
|
|
assert messages[1]["message_index"] == 1
|
|
assert messages[1]["summary"] is not None
|
|
|
|
assert messages[2]["role"] == "tool"
|
|
assert messages[2]["message_index"] == 2
|
|
assert messages[2]["tool_name"] == "trade"
|
|
assert messages[2]["tool_input"] is not None
|
|
|
|
def test_get_reasoning_summaries_only(self, client, sample_trading_session):
|
|
"""Should not include conversation when include_full_conversation=false (default)."""
|
|
response = client.get(f"/reasoning?job_id={sample_trading_session['job_id']}")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["count"] == 1
|
|
|
|
session = data["sessions"][0]
|
|
assert session["conversation"] is None
|
|
assert session["session_summary"] is not None
|
|
assert session["total_messages"] == 4
|
|
|
|
def test_get_reasoning_no_results_returns_404(self, client, clean_db):
|
|
"""Should return 404 when no sessions match filters."""
|
|
response = client.get("/reasoning?job_id=nonexistent-job")
|
|
|
|
assert response.status_code == 404
|
|
assert "No trading sessions found" in response.json()["detail"]
|
|
|
|
def test_get_reasoning_invalid_date_returns_400(self, client, clean_db):
|
|
"""Should return 400 for invalid date format."""
|
|
response = client.get("/reasoning?date=invalid-date")
|
|
|
|
assert response.status_code == 400
|
|
assert "Invalid date format" in response.json()["detail"]
|
|
|
|
def test_get_reasoning_includes_deployment_mode(self, client, sample_trading_session):
|
|
"""Should include deployment mode info in response."""
|
|
response = client.get(f"/reasoning?job_id={sample_trading_session['job_id']}")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "deployment_mode" in data
|
|
assert "is_dev_mode" in data
|
|
assert isinstance(data["is_dev_mode"], bool)
|
|
|
|
|
|
@pytest.fixture
|
|
def client(clean_db):
|
|
"""Create FastAPI test client with clean database."""
|
|
from fastapi.testclient import TestClient
|
|
from api.main import create_app
|
|
|
|
app = create_app(db_path=clean_db)
|
|
app.state.test_mode = True # Prevent background worker from starting
|
|
|
|
return TestClient(app)
|