fix(tasks): isolate worker execution scopes
Persist a required Server-owned execution scope and enforce it for listing, claiming, lease recovery and SCADA verification. Backfill existing tasks explicitly through migration 034 so local development workers no longer claim production tasks from the shared database.
This commit is contained in:
@@ -235,6 +235,7 @@ async def test_targeted_notification_cannot_be_acknowledged_without_permission()
|
||||
def _task_document(context, now, *, lease=None):
|
||||
task_id = uuid4()
|
||||
return OperationalTaskSummary(
|
||||
execution_scope="production",
|
||||
project_id=context.project_id,
|
||||
task_id=task_id,
|
||||
task_type="event_investigation",
|
||||
@@ -436,16 +437,39 @@ def _active_run(task, now):
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_run_claim_persists_agent_instance_ownership():
|
||||
@pytest.mark.parametrize("task_scope,worker_scope,allowed", [
|
||||
(None, "production", False),
|
||||
(None, "development", False),
|
||||
("production", "production", True),
|
||||
("production", "development", False),
|
||||
("development", "production", False),
|
||||
("development", "development", True),
|
||||
])
|
||||
async def test_run_claim_persists_agent_instance_ownership(task_scope, worker_scope, allowed):
|
||||
now = datetime(2026, 9, 23, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=None)
|
||||
task.update(status="queued", agent_session_id=None, agent_lease_expires_at=None)
|
||||
if task_scope is None:
|
||||
task.pop("execution_scope", None)
|
||||
else:
|
||||
task["execution_scope"] = task_scope
|
||||
run = _active_run(task, now)
|
||||
run.update(status="queued", revision=1, agent_session_id=None,
|
||||
lease_expires_at=None, heartbeat_at=None, started_at=None)
|
||||
repository = RunTaskRepository(task, run)
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now,
|
||||
execution_scope=worker_scope)
|
||||
|
||||
if not allowed:
|
||||
with pytest.raises(DomainError) as failure:
|
||||
await service.claim_run(task["task_id"], run["run_id"], AgentRunCommand(
|
||||
request_id=run["execution_request_id"], expected_revision=1,
|
||||
source_session_id="session-1", agent_instance_id="agent-a"))
|
||||
assert failure.value.code == "EXECUTION_SCOPE_MISMATCH"
|
||||
assert repository.run["status"] == "queued"
|
||||
assert repository.task["status"] == "queued"
|
||||
return
|
||||
|
||||
claimed = await service.claim_run(
|
||||
task["task_id"], run["run_id"],
|
||||
@@ -2371,3 +2395,35 @@ async def test_recovery_resolves_alerts_with_original_facts_and_stable_event_id(
|
||||
assert command.event_id == uuid5(NAMESPACE_URL, f"recovery:{task_id}:recovery:evidence-1:incident-1")
|
||||
assert command.source.model_dump(mode="json") == alert["source"]
|
||||
assert command.title == alert["title"] and command.description == alert["description"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("scope", ["production", "development"])
|
||||
async def test_task_creation_persists_server_execution_scope(monkeypatch, scope):
|
||||
from app.domain.schemas.operational_task import OperationalTaskCreate
|
||||
|
||||
now = datetime(2026, 11, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
document = _task_document(context, now)
|
||||
repository = TaskRepository(document)
|
||||
repository.insert_task = AsyncMock()
|
||||
repository.insert_run = AsyncMock()
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now,
|
||||
execution_scope=scope)
|
||||
monkeypatch.setattr(service, "_task_event", AsyncMock())
|
||||
monkeypatch.setattr(service, "_run_event", AsyncMock())
|
||||
monkeypatch.setattr(service, "detail", AsyncMock())
|
||||
command = OperationalTaskCreate(request_id=uuid4(), **{
|
||||
key: document[key] for key in ("task_type", "workflow", "title", "summary", "source", "trigger")
|
||||
})
|
||||
await service.create(command)
|
||||
assert repository.insert_task.await_args.args[0]["execution_scope"] == scope
|
||||
|
||||
|
||||
def test_task_scope_is_required_in_persisted_contract():
|
||||
task = _task_document(_context(), datetime(2026, 11, 8, tzinfo=timezone.utc))
|
||||
task.pop("execution_scope")
|
||||
with pytest.raises(ValidationError) as error:
|
||||
OperationalTaskSummary.model_validate(task)
|
||||
assert any(item["loc"] == ("execution_scope",) and item["type"] == "missing"
|
||||
for item in error.value.errors())
|
||||
|
||||
Reference in New Issue
Block a user