feat(mcp): queue long-running calculations
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user