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:
2026-04-11 08:48:07 +08:00
parent ac49c13965
commit 39a9058de1
3 changed files with 356 additions and 4 deletions

View File

@@ -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'

View File

@@ -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()

View File

@@ -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)