test(backend): update backend router tests for conversation, schedule center, and schema
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -95,3 +95,65 @@ async def test_chat_stream_emits_error_event_when_agent_service_fails_before_str
|
||||
assert response.status_code == 200
|
||||
assert 'event: error' in response.text
|
||||
assert 'stream boot failed' in response.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chat_stream_passes_runtime_to_agent_service(conversation_env, monkeypatch):
|
||||
recorded: dict[str, object] = {}
|
||||
|
||||
async def fake_chat(self, **kwargs):
|
||||
recorded.update(kwargs)
|
||||
|
||||
async def empty_stream():
|
||||
if False:
|
||||
yield None
|
||||
|
||||
return 'conv-rt', 'msg-rt', empty_stream()
|
||||
|
||||
monkeypatch.setattr('app.routers.conversation.AgentService.chat', fake_chat)
|
||||
|
||||
transport = ASGITransport(app=conversation_env)
|
||||
async with AsyncClient(transport=transport, base_url='http://testserver') as client:
|
||||
response = await client.post(
|
||||
'/api/conversations/chat/stream',
|
||||
json={'message': 'hello', 'runtime': 'hermes'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert recorded['runtime'] == 'hermes'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chat_defaults_agent_name_to_jarvis(conversation_env, monkeypatch):
|
||||
async def fake_chat_simple(self, **kwargs):
|
||||
return 'conv-id', 'msg-id', 'ok', 'test-model'
|
||||
|
||||
monkeypatch.setattr('app.routers.conversation.AgentService.chat_simple', fake_chat_simple)
|
||||
|
||||
transport = ASGITransport(app=conversation_env)
|
||||
async with AsyncClient(transport=transport, base_url='http://testserver') as client:
|
||||
response = await client.post(
|
||||
'/api/conversations/chat',
|
||||
json={'message': 'hello'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()['agent_name'] == 'jarvis'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chat_returns_hermes_agent_name_when_requested(conversation_env, monkeypatch):
|
||||
async def fake_chat_simple(self, **kwargs):
|
||||
return 'conv-id', 'msg-id', 'ok', 'hermes-model'
|
||||
|
||||
monkeypatch.setattr('app.routers.conversation.AgentService.chat_simple', fake_chat_simple)
|
||||
|
||||
transport = ASGITransport(app=conversation_env)
|
||||
async with AsyncClient(transport=transport, base_url='http://testserver') as client:
|
||||
response = await client.post(
|
||||
'/api/conversations/chat',
|
||||
json={'message': 'hello', 'runtime': 'hermes'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()['agent_name'] == 'hermes'
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from app.database import ensure_learning_artifact_tables, ensure_memory_columns, ensure_skill_columns
|
||||
from app.database import ensure_learning_artifact_tables, ensure_memory_columns, ensure_skill_columns, ensure_task_columns
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -113,3 +113,70 @@ async def test_ensure_learning_artifact_tables_creates_table_and_indexes(tmp_pat
|
||||
assert 'ix_learning_artifacts_artifact_type' in index_names
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_ensure_task_columns_adds_today_status_columns_and_subtask_table(tmp_path):
|
||||
db_path = tmp_path / 'test_tasks.db'
|
||||
engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", future=True)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.execute(text(
|
||||
'''
|
||||
CREATE TABLE tasks (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
description TEXT,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
priority VARCHAR(20) NOT NULL,
|
||||
due_date DATETIME,
|
||||
completed_at DATETIME,
|
||||
tags VARCHAR(1000),
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
)
|
||||
'''
|
||||
))
|
||||
await conn.execute(text(
|
||||
'''
|
||||
CREATE TABLE task_histories (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
task_id VARCHAR(36) NOT NULL,
|
||||
action VARCHAR(100) NOT NULL,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
)
|
||||
'''
|
||||
))
|
||||
|
||||
await ensure_task_columns(conn)
|
||||
|
||||
task_columns = await conn.execute(text("PRAGMA table_info(tasks)"))
|
||||
task_column_names = {row[1] for row in task_columns.fetchall()}
|
||||
assert 'source' in task_column_names
|
||||
assert 'conversation_id' in task_column_names
|
||||
assert 'quadrant' in task_column_names
|
||||
assert 'assignee_type' in task_column_names
|
||||
assert 'dispatch_status' in task_column_names
|
||||
assert 'dispatch_run_id' in task_column_names
|
||||
assert 'last_synced_at' in task_column_names
|
||||
|
||||
history_columns = await conn.execute(text("PRAGMA table_info(task_histories)"))
|
||||
history_column_names = {row[1] for row in history_columns.fetchall()}
|
||||
assert 'subtask_id' in history_column_names
|
||||
|
||||
subtask_columns = await conn.execute(text("PRAGMA table_info(task_subtasks)"))
|
||||
subtask_column_names = {row[1] for row in subtask_columns.fetchall()}
|
||||
assert 'task_id' in subtask_column_names
|
||||
assert 'order_index' in subtask_column_names
|
||||
assert 'dispatch_status' in subtask_column_names
|
||||
|
||||
indexes = await conn.execute(text("PRAGMA index_list(task_subtasks)"))
|
||||
index_names = {row[1] for row in indexes.fetchall()}
|
||||
assert 'ix_task_subtasks_task_id' in index_names
|
||||
assert 'ix_task_subtasks_dispatch_status' in index_names
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import sys
|
||||
from datetime import UTC, date, datetime
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
|
||||
sys.modules.setdefault('psutil', Mock())
|
||||
@@ -13,7 +14,7 @@ import app.models # noqa: F401
|
||||
from app.database import Base, get_db
|
||||
from app.models.goal import Goal
|
||||
from app.models.reminder import Reminder
|
||||
from app.models.task import Task, TaskPriority, TaskStatus
|
||||
from app.models.task import DispatchStatus, Task, TaskPriority, TaskQuadrant, TaskStatus, TaskSubTask
|
||||
from app.models.todo import DailyTodo, TodoSource
|
||||
from app.models.user import User
|
||||
from app.routers.auth import get_current_user
|
||||
@@ -50,7 +51,7 @@ async def schedule_env(tmp_path):
|
||||
session.add_all([user, other_user])
|
||||
await session.flush()
|
||||
|
||||
session.add_all([
|
||||
seeded_items = [
|
||||
DailyTodo(
|
||||
user_id=user.id,
|
||||
title='Legacy todo',
|
||||
@@ -78,13 +79,19 @@ async def schedule_env(tmp_path):
|
||||
title='High priority task',
|
||||
priority=TaskPriority.HIGH,
|
||||
status=TaskStatus.TODO,
|
||||
source='schedule_center',
|
||||
quadrant=TaskQuadrant.URGENT_IMPORTANT,
|
||||
due_date=datetime(2026, 4, 10, 14, 0, tzinfo=UTC),
|
||||
assignee_type='commander',
|
||||
assignee_id='master',
|
||||
dispatch_status=DispatchStatus.RUNNING,
|
||||
),
|
||||
Task(
|
||||
user_id=user.id,
|
||||
title='Urgent task next day',
|
||||
priority=TaskPriority.URGENT,
|
||||
status=TaskStatus.IN_PROGRESS,
|
||||
quadrant=TaskQuadrant.NOT_URGENT_IMPORTANT,
|
||||
due_date=datetime(2026, 4, 11, 10, 0, tzinfo=UTC),
|
||||
),
|
||||
Task(
|
||||
@@ -106,6 +113,30 @@ async def schedule_env(tmp_path):
|
||||
note='Ship MVP',
|
||||
goal_date='2026-04-10',
|
||||
),
|
||||
]
|
||||
session.add_all(seeded_items)
|
||||
await session.flush()
|
||||
high_priority_task = next(item for item in seeded_items if isinstance(item, Task) and item.title == 'High priority task')
|
||||
session.add_all([
|
||||
TaskSubTask(
|
||||
task_id=high_priority_task.id,
|
||||
title='Commander follow-up',
|
||||
status=TaskStatus.TODO,
|
||||
order_index=0,
|
||||
assignee_type='agent',
|
||||
assignee_id='executor',
|
||||
dispatch_status=DispatchStatus.QUEUED,
|
||||
),
|
||||
TaskSubTask(
|
||||
task_id=high_priority_task.id,
|
||||
title='Commander completed step',
|
||||
status=TaskStatus.DONE,
|
||||
order_index=1,
|
||||
assignee_type='agent',
|
||||
assignee_id='executor',
|
||||
dispatch_status=DispatchStatus.COMPLETED,
|
||||
completed_at=datetime(2026, 4, 10, 16, 0, tzinfo=UTC),
|
||||
),
|
||||
])
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
@@ -211,10 +242,143 @@ async def test_get_schedule_center_date_returns_aggregated_resources(schedule_en
|
||||
'reminder_total': 1,
|
||||
'goal_total': 1,
|
||||
}
|
||||
assert [item['title'] for item in payload['focus_tasks']] == ['High priority task']
|
||||
assert [item['id'] for item in payload['quadrants']] == [
|
||||
'urgent-important',
|
||||
'not-urgent-important',
|
||||
'urgent-not-important',
|
||||
'not-urgent-not-important',
|
||||
]
|
||||
assert payload['quadrants'][0]['tasks'][0]['title'] == 'High priority task'
|
||||
assert payload['commander_summary'] == {
|
||||
'total': 3,
|
||||
'queued': 1,
|
||||
'running': 1,
|
||||
'completed': 1,
|
||||
'failed': 0,
|
||||
'overall_status': 'running',
|
||||
}
|
||||
assert [item['title'] for item in payload['reminders']] == ['Doctor reminder']
|
||||
assert [item['title'] for item in payload['goals']] == ['Launch calendar beta']
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_task_detail_and_subtask_crud(schedule_env):
|
||||
transport = ASGITransport(app=schedule_env)
|
||||
async with AsyncClient(transport=transport, base_url='http://testserver') as client:
|
||||
create_response = await client.post(
|
||||
'/api/tasks',
|
||||
json={
|
||||
'title': 'Plan Today Status',
|
||||
'description': 'Wire task detail and quadrants',
|
||||
'priority': 'high',
|
||||
'quadrant': 'urgent-important',
|
||||
'source': 'today_status',
|
||||
'assignee_type': 'commander',
|
||||
},
|
||||
)
|
||||
task_id = create_response.json()['id']
|
||||
subtask_create = await client.post(
|
||||
f'/api/tasks/{task_id}/subtasks',
|
||||
json={'title': 'Model backend', 'assignee_type': 'executor'},
|
||||
)
|
||||
subtask_id = subtask_create.json()['id']
|
||||
subtask_update = await client.patch(
|
||||
f'/api/tasks/{task_id}/subtasks/{subtask_id}',
|
||||
json={'status': 'done'},
|
||||
)
|
||||
detail_response = await client.get(f'/api/tasks/{task_id}')
|
||||
reorder_response = await client.post(
|
||||
f'/api/tasks/{task_id}/subtasks/reorder',
|
||||
json={'items': [{'id': subtask_id, 'order_index': 0}]},
|
||||
)
|
||||
|
||||
assert create_response.status_code == 201
|
||||
assert subtask_create.status_code == 201
|
||||
assert subtask_update.status_code == 200
|
||||
assert detail_response.status_code == 200
|
||||
assert reorder_response.status_code == 200
|
||||
detail_payload = detail_response.json()
|
||||
assert detail_payload['source'] == 'today_status'
|
||||
assert detail_payload['quadrant'] == 'urgent-important'
|
||||
assert detail_payload['subtasks'][0]['title'] == 'Model backend'
|
||||
assert detail_payload['subtasks'][0]['status'] == 'done'
|
||||
assert any(entry['action'] == 'subtask_created' for entry in detail_payload['history'])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_task_dispatch_updates_summary_and_detail(schedule_env):
|
||||
transport = ASGITransport(app=schedule_env)
|
||||
async with AsyncClient(transport=transport, base_url='http://testserver') as client:
|
||||
create_response = await client.post(
|
||||
'/api/tasks',
|
||||
json={'title': 'Dispatch me', 'priority': 'urgent', 'due_date': '2026-04-10T09:00:00Z'},
|
||||
)
|
||||
task_id = create_response.json()['id']
|
||||
dispatch_response = await client.post(
|
||||
f'/api/tasks/{task_id}/dispatch',
|
||||
json={'assignee_type': 'commander', 'note': 'Send to runtime'},
|
||||
)
|
||||
detail_response = await client.get(f'/api/tasks/{task_id}')
|
||||
|
||||
assert dispatch_response.status_code == 200
|
||||
dispatch_payload = dispatch_response.json()
|
||||
assert dispatch_payload['status'] == 'queued'
|
||||
assert dispatch_payload['run_id']
|
||||
detail_payload = detail_response.json()
|
||||
assert detail_payload['dispatch_status'] == 'queued'
|
||||
assert detail_payload['dispatch_summary']['status'] == 'queued'
|
||||
assert any(entry['action'] == 'dispatched_to_commander' for entry in detail_payload['history'])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schedule_center_created_task_is_visible_in_today_status_aggregate(schedule_env):
|
||||
transport = ASGITransport(app=schedule_env)
|
||||
async with AsyncClient(transport=transport, base_url='http://testserver') as client:
|
||||
create_response = await client.post(
|
||||
'/api/tasks',
|
||||
json={
|
||||
'title': 'Schedule Center created task',
|
||||
'source': 'schedule_center',
|
||||
'priority': 'medium',
|
||||
'quadrant': 'not-urgent-important',
|
||||
'due_date': '2026-04-10T09:00:00Z',
|
||||
},
|
||||
)
|
||||
date_response = await client.get('/api/schedule-center/date', params={'date_str': '2026-04-10'})
|
||||
|
||||
assert create_response.status_code == 201
|
||||
titles = [item['title'] for item in date_response.json()['tasks']]
|
||||
assert 'Schedule Center created task' in titles
|
||||
focus_titles = [item['title'] for item in date_response.json()['focus_tasks']]
|
||||
assert 'Schedule Center created task' in focus_titles
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_today_status_created_task_is_visible_in_schedule_center_aggregate(schedule_env):
|
||||
transport = ASGITransport(app=schedule_env)
|
||||
async with AsyncClient(transport=transport, base_url='http://testserver') as client:
|
||||
create_response = await client.post(
|
||||
'/api/tasks',
|
||||
json={
|
||||
'title': 'Today Status created task',
|
||||
'source': 'today_status',
|
||||
'priority': 'high',
|
||||
'quadrant': 'urgent-important',
|
||||
'due_date': '2026-04-10T11:00:00Z',
|
||||
},
|
||||
)
|
||||
date_response = await client.get('/api/schedule-center/date', params={'date_str': '2026-04-10'})
|
||||
|
||||
assert create_response.status_code == 201
|
||||
quadrant_titles = [
|
||||
task['title']
|
||||
for quadrant in date_response.json()['quadrants']
|
||||
for task in quadrant['tasks']
|
||||
]
|
||||
assert 'Today Status created task' in quadrant_titles
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_schedule_center_month_returns_day_summaries(schedule_env):
|
||||
transport = ASGITransport(app=schedule_env)
|
||||
@@ -239,6 +403,65 @@ async def test_get_schedule_center_month_returns_day_summaries(schedule_env):
|
||||
assert day_11['high_priority_total'] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schedule_center_tolerates_legacy_lowercase_task_enum_values(schedule_env):
|
||||
app = schedule_env
|
||||
session_override = app.dependency_overrides[get_db]
|
||||
|
||||
async for session in session_override():
|
||||
user_id = (
|
||||
await session.execute(
|
||||
text("SELECT id FROM users WHERE email = 'schedule@example.com'")
|
||||
)
|
||||
).scalar_one()
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO tasks (
|
||||
id, created_at, updated_at, user_id, title, description,
|
||||
status, priority, due_date, completed_at, tags, source,
|
||||
conversation_id, quadrant, assignee_type, assignee_id,
|
||||
dispatch_status, dispatch_run_id, result_summary, started_at, last_synced_at
|
||||
) VALUES (
|
||||
:id, :created_at, :updated_at, :user_id, :title, NULL,
|
||||
:status, :priority, :due_date, NULL, NULL, :source,
|
||||
NULL, :quadrant, :assignee_type, :assignee_id,
|
||||
:dispatch_status, NULL, NULL, NULL, NULL
|
||||
)
|
||||
"""
|
||||
),
|
||||
{
|
||||
'id': 'legacy-task-1',
|
||||
'created_at': '2026-04-10 06:00:00',
|
||||
'updated_at': '2026-04-10 06:00:00',
|
||||
'user_id': user_id,
|
||||
'title': 'Legacy lowercase enum task',
|
||||
'status': 'todo',
|
||||
'priority': 'high',
|
||||
'due_date': '2026-04-10 09:00:00',
|
||||
'source': 'manual',
|
||||
'quadrant': 'urgent-important',
|
||||
'assignee_type': 'commander',
|
||||
'assignee_id': 'legacy-master',
|
||||
'dispatch_status': 'queued',
|
||||
},
|
||||
)
|
||||
await session.commit()
|
||||
break
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url='http://testserver') as client:
|
||||
date_response = await client.get('/api/schedule-center/date', params={'date_str': '2026-04-10'})
|
||||
month_response = await client.get('/api/schedule-center/month', params={'year': 2026, 'month': 4})
|
||||
|
||||
assert date_response.status_code == 200
|
||||
assert month_response.status_code == 200
|
||||
date_payload = date_response.json()
|
||||
month_payload = month_response.json()
|
||||
assert 'Legacy lowercase enum task' in [item['title'] for item in date_payload['tasks']]
|
||||
assert next(item for item in month_payload['days'] if item['date'] == '2026-04-10')['task_due_total'] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_reminder_with_naive_datetime_and_time_zone_appears_in_schedule_center(schedule_env):
|
||||
transport = ASGITransport(app=schedule_env)
|
||||
|
||||
Reference in New Issue
Block a user