mirror of
https://github.com/Xe138/AI-Trader.git
synced 2026-04-01 17:17:24 -04:00
feat: add GET /reasoning API endpoint
- 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>
This commit is contained in:
221
api/main.py
221
api/main.py
@@ -114,6 +114,51 @@ class HealthResponse(BaseModel):
|
|||||||
preserve_dev_data: Optional[bool] = None
|
preserve_dev_data: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ReasoningMessage(BaseModel):
|
||||||
|
"""Individual message in a reasoning conversation."""
|
||||||
|
message_index: int
|
||||||
|
role: str
|
||||||
|
content: str
|
||||||
|
summary: Optional[str] = None
|
||||||
|
tool_name: Optional[str] = None
|
||||||
|
tool_input: Optional[str] = None
|
||||||
|
timestamp: str
|
||||||
|
|
||||||
|
|
||||||
|
class PositionSummary(BaseModel):
|
||||||
|
"""Trading position summary."""
|
||||||
|
action_id: int
|
||||||
|
action_type: Optional[str] = None
|
||||||
|
symbol: Optional[str] = None
|
||||||
|
amount: Optional[int] = None
|
||||||
|
price: Optional[float] = None
|
||||||
|
cash_after: float
|
||||||
|
portfolio_value: float
|
||||||
|
|
||||||
|
|
||||||
|
class TradingSessionResponse(BaseModel):
|
||||||
|
"""Single trading session with positions and optional conversation."""
|
||||||
|
session_id: int
|
||||||
|
job_id: str
|
||||||
|
date: str
|
||||||
|
model: str
|
||||||
|
session_summary: Optional[str] = None
|
||||||
|
started_at: str
|
||||||
|
completed_at: Optional[str] = None
|
||||||
|
total_messages: Optional[int] = None
|
||||||
|
positions: List[PositionSummary]
|
||||||
|
conversation: Optional[List[ReasoningMessage]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ReasoningResponse(BaseModel):
|
||||||
|
"""Response body for GET /reasoning."""
|
||||||
|
sessions: List[TradingSessionResponse]
|
||||||
|
count: int
|
||||||
|
deployment_mode: str
|
||||||
|
is_dev_mode: bool
|
||||||
|
preserve_dev_data: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
def create_app(
|
def create_app(
|
||||||
db_path: str = "data/jobs.db",
|
db_path: str = "data/jobs.db",
|
||||||
config_path: str = "/tmp/runtime_config.json" if Path("/tmp/runtime_config.json").exists() else "configs/default_config.json"
|
config_path: str = "/tmp/runtime_config.json" if Path("/tmp/runtime_config.json").exists() else "configs/default_config.json"
|
||||||
@@ -482,6 +527,182 @@ def create_app(
|
|||||||
logger.error(f"Failed to query results: {e}", exc_info=True)
|
logger.error(f"Failed to query results: {e}", exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||||
|
|
||||||
|
@app.get("/reasoning", response_model=ReasoningResponse)
|
||||||
|
async def get_reasoning(
|
||||||
|
job_id: Optional[str] = Query(None, description="Filter by job ID"),
|
||||||
|
date: Optional[str] = Query(None, description="Filter by date (YYYY-MM-DD)"),
|
||||||
|
model: Optional[str] = Query(None, description="Filter by model signature"),
|
||||||
|
include_full_conversation: bool = Query(False, description="Include full conversation history")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Query reasoning logs from trading sessions.
|
||||||
|
|
||||||
|
Supports filtering by job_id, date, and/or model.
|
||||||
|
Returns session summaries with positions and optionally full conversation history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: Optional job UUID filter
|
||||||
|
date: Optional date filter (YYYY-MM-DD)
|
||||||
|
model: Optional model signature filter
|
||||||
|
include_full_conversation: Include all messages (default: false, only returns summaries)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of trading sessions with positions and optional conversation
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException 400: Invalid date format
|
||||||
|
HTTPException 404: No sessions found matching filters
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Validate date format if provided
|
||||||
|
if date:
|
||||||
|
try:
|
||||||
|
datetime.strptime(date, "%Y-%m-%d")
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Invalid date format: {date}. Expected YYYY-MM-DD"
|
||||||
|
)
|
||||||
|
|
||||||
|
conn = get_db_connection(app.state.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Build query for trading sessions with filters
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
ts.id,
|
||||||
|
ts.job_id,
|
||||||
|
ts.date,
|
||||||
|
ts.model,
|
||||||
|
ts.session_summary,
|
||||||
|
ts.started_at,
|
||||||
|
ts.completed_at,
|
||||||
|
ts.total_messages
|
||||||
|
FROM trading_sessions ts
|
||||||
|
WHERE 1=1
|
||||||
|
"""
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if job_id:
|
||||||
|
query += " AND ts.job_id = ?"
|
||||||
|
params.append(job_id)
|
||||||
|
|
||||||
|
if date:
|
||||||
|
query += " AND ts.date = ?"
|
||||||
|
params.append(date)
|
||||||
|
|
||||||
|
if model:
|
||||||
|
query += " AND ts.model = ?"
|
||||||
|
params.append(model)
|
||||||
|
|
||||||
|
query += " ORDER BY ts.date, ts.model"
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
|
session_rows = cursor.fetchall()
|
||||||
|
|
||||||
|
if not session_rows:
|
||||||
|
conn.close()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="No trading sessions found matching the provided filters"
|
||||||
|
)
|
||||||
|
|
||||||
|
sessions = []
|
||||||
|
for session_row in session_rows:
|
||||||
|
session_id = session_row[0]
|
||||||
|
|
||||||
|
# Fetch positions for this session
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
p.action_id,
|
||||||
|
p.action_type,
|
||||||
|
p.symbol,
|
||||||
|
p.amount,
|
||||||
|
p.price,
|
||||||
|
p.cash,
|
||||||
|
p.portfolio_value
|
||||||
|
FROM positions p
|
||||||
|
WHERE p.session_id = ?
|
||||||
|
ORDER BY p.action_id
|
||||||
|
""", (session_id,))
|
||||||
|
|
||||||
|
position_rows = cursor.fetchall()
|
||||||
|
positions = [
|
||||||
|
PositionSummary(
|
||||||
|
action_id=row[0],
|
||||||
|
action_type=row[1],
|
||||||
|
symbol=row[2],
|
||||||
|
amount=row[3],
|
||||||
|
price=row[4],
|
||||||
|
cash_after=row[5],
|
||||||
|
portfolio_value=row[6]
|
||||||
|
)
|
||||||
|
for row in position_rows
|
||||||
|
]
|
||||||
|
|
||||||
|
# Optionally fetch full conversation
|
||||||
|
conversation = None
|
||||||
|
if include_full_conversation:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
rl.message_index,
|
||||||
|
rl.role,
|
||||||
|
rl.content,
|
||||||
|
rl.summary,
|
||||||
|
rl.tool_name,
|
||||||
|
rl.tool_input,
|
||||||
|
rl.timestamp
|
||||||
|
FROM reasoning_logs rl
|
||||||
|
WHERE rl.session_id = ?
|
||||||
|
ORDER BY rl.message_index
|
||||||
|
""", (session_id,))
|
||||||
|
|
||||||
|
message_rows = cursor.fetchall()
|
||||||
|
conversation = [
|
||||||
|
ReasoningMessage(
|
||||||
|
message_index=row[0],
|
||||||
|
role=row[1],
|
||||||
|
content=row[2],
|
||||||
|
summary=row[3],
|
||||||
|
tool_name=row[4],
|
||||||
|
tool_input=row[5],
|
||||||
|
timestamp=row[6]
|
||||||
|
)
|
||||||
|
for row in message_rows
|
||||||
|
]
|
||||||
|
|
||||||
|
sessions.append(
|
||||||
|
TradingSessionResponse(
|
||||||
|
session_id=session_row[0],
|
||||||
|
job_id=session_row[1],
|
||||||
|
date=session_row[2],
|
||||||
|
model=session_row[3],
|
||||||
|
session_summary=session_row[4],
|
||||||
|
started_at=session_row[5],
|
||||||
|
completed_at=session_row[6],
|
||||||
|
total_messages=session_row[7],
|
||||||
|
positions=positions,
|
||||||
|
conversation=conversation
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Get deployment mode info
|
||||||
|
deployment_info = get_deployment_mode_dict()
|
||||||
|
|
||||||
|
return ReasoningResponse(
|
||||||
|
sessions=sessions,
|
||||||
|
count=len(sessions),
|
||||||
|
**deployment_info
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to query reasoning logs: {e}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||||
|
|
||||||
@app.get("/health", response_model=HealthResponse)
|
@app.get("/health", response_model=HealthResponse)
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""
|
"""
|
||||||
|
|||||||
317
tests/unit/test_api_reasoning_endpoint.py
Normal file
317
tests/unit/test_api_reasoning_endpoint.py
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
"""
|
||||||
|
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)
|
||||||
Reference in New Issue
Block a user