feat(mcp): queue long-running calculations

This commit is contained in:
2026-10-29 18:25:15 +08:00
parent 2865811f1b
commit 63af998045
32 changed files with 1588 additions and 72 deletions
+92
View File
@@ -1842,3 +1842,95 @@ async def test_interactive_report_publication_is_idempotent_and_versioned_with_e
request_id=uuid4(), source_session_id="session-1", report=report,
))
assert error.value.code == "EVIDENCE_NOT_FOUND"
@pytest.mark.anyio
@pytest.mark.parametrize("report_factory", [_current_report, _insufficient_report])
async def test_final_report_rejected_while_original_run_has_pending_calculations(report_factory):
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repository = RunTaskRepository(task, _active_run(task, now))
pending_job = str(uuid4())
repository.pending_calculation_jobs = AsyncMock(return_value=[pending_job])
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
with pytest.raises(DomainError) as error:
await service.publish_artifacts(task['task_id'], AgentArtifactsCommand(
request_id=uuid4(), source_session_id='session-1', run_id=repository.run['run_id'], report=report_factory(),
))
assert error.value.code == 'PENDING_CALCULATIONS'
assert error.value.status == 409
assert error.value.recovery == 'retry_same_request'
assert error.value.refs == [pending_job]
repository.pending_calculation_jobs.assert_awaited_once_with(
str(task['task_id']), str(repository.run['run_id']), str(repository.run['execution_request_id']),
)
assert not repository.reports
assert repository.run['status'] in {'claimed', 'running'}
@pytest.mark.anyio
async def test_completion_rejected_until_calculations_reach_terminal_state():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repository = RunTaskRepository(task, _active_run(task, now))
repository.pending_calculation_jobs = AsyncMock(return_value=[str(uuid4())])
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
command = AgentRunCommand(request_id=uuid4(), expected_revision=repository.run['revision'], source_session_id='session-1')
with pytest.raises(DomainError) as error:
await service.complete_run(task['task_id'], repository.run['run_id'], command)
assert error.value.code == 'PENDING_CALCULATIONS'
assert repository.run['status'] in {'claimed', 'running'}
repository.pending_calculation_jobs.return_value = []
# Once terminal, the normal final-artifact requirement runs again.
completed = await service.complete_run(task['task_id'], repository.run['run_id'], command)
assert repository.run['failure_code'] == 'ARTIFACT_INCOMPLETE'
assert completed.lifecycle_status == 'failed'
@pytest.mark.anyio
async def test_pending_calculation_query_matches_exact_original_execution_binding():
job_id = uuid4()
connection = SimpleNamespace(execute=AsyncMock())
connection.execute.return_value.fetchall = AsyncMock(return_value=[{'job_id': job_id}])
project_id, task_id, run_id, request_id = (uuid4() for _ in range(4))
repo = OperationalTaskRepository(connection, project_id)
assert await repo.pending_calculation_jobs(str(task_id), str(run_id), str(request_id)) == [str(job_id)]
sql, parameters = connection.execute.await_args.args
assert parameters == (str(project_id), str(task_id), str(run_id), str(request_id))
assert "status IN ('queued','running','cancel_requested')" in sql
assert "binding->>'execution_request_id'=%s" in sql
@pytest.mark.anyio
async def test_interim_report_can_explain_progress_while_calculation_is_pending():
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
context = _context()
task = _task_document(context, now, lease=now + timedelta(minutes=1))
repository = RunTaskRepository(task, _active_run(task, now))
repository.pending_calculation_jobs = AsyncMock(return_value=[str(uuid4())])
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
result = await service.publish_artifacts(task['task_id'], AgentArtifactsCommand(
request_id=uuid4(), source_session_id='session-1', run_id=repository.run['run_id'],
report={**_current_report(), 'stage': 'interim'},
))
assert result.report_revision == 1
repository.pending_calculation_jobs.assert_not_awaited()
@pytest.mark.anyio
async def test_missing_calculation_migration_fails_finalization_closed(monkeypatch):
from psycopg.errors import UndefinedTable
from app.infra.db.postgresql import operational_tasks as storage
@asynccontextmanager
async def transaction():
yield
connection = SimpleNamespace(transaction=transaction, execute=AsyncMock(side_effect=UndefinedTable('calculation_jobs missing')))
monkeypatch.setattr(storage, 'lock_event_transaction', AsyncMock())
repo = OperationalTaskRepository(connection, uuid4())
with pytest.raises(DomainError) as error:
async with repo.transaction():
await repo.pending_calculation_jobs(str(uuid4()), str(uuid4()), str(uuid4()))
assert error.value.code == 'DEPENDENCY_UNAVAILABLE'
assert error.value.status == 503