fix(tasks): isolate peripheral feedback writes by execution scope
An old development synchronizer could overwrite a production recovery timestamp in a shared project database even after production deployment. Check task scope before feedback or source-withdrawal writes and before idempotency replay. Cover cross-scope rejection, same-scope success and replay, and preserve actual field feedback time when later order metadata changes.
This commit is contained in:
@@ -2487,3 +2487,59 @@ def test_task_scope_is_required_in_persisted_contract():
|
||||
OperationalTaskSummary.model_validate(task)
|
||||
assert any(item["loc"] == ("execution_scope",) and item["type"] == "missing"
|
||||
for item in error.value.errors())
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("operation", ["feedback", "withdrawal"])
|
||||
@pytest.mark.parametrize("task_scope,server_scope,allowed", [
|
||||
(None, "production", False),
|
||||
(None, "development", False),
|
||||
("production", "production", True),
|
||||
("production", "development", False),
|
||||
("development", "production", False),
|
||||
("development", "development", True),
|
||||
])
|
||||
async def test_external_sync_only_mutates_tasks_in_its_execution_scope(operation, task_scope, server_scope, allowed):
|
||||
from copy import deepcopy
|
||||
|
||||
now = datetime(2026, 11, 8, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=None)
|
||||
source_id = f"peripheral_sample:{uuid4()}"
|
||||
task.update(task_type="external_work_order", source={
|
||||
"kind": "external_system", "external_id": source_id,
|
||||
"external_revision": 1, "event_kind": "general",
|
||||
})
|
||||
if task_scope is None:
|
||||
task.pop("execution_scope")
|
||||
else:
|
||||
task["execution_scope"] = task_scope
|
||||
repository = TaskRepository(task)
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now, execution_scope=server_scope)
|
||||
if operation == "feedback":
|
||||
command = ExternalFeedbackCommand(request_id=uuid4(), source_event_id=source_id,
|
||||
source_revision=2, status="completed", occurred_at=now, detail="已关阀修复管道")
|
||||
method = "external_feedback"
|
||||
else:
|
||||
command = ExternalSourceWithdrawalCommand(request_id=uuid4(), source_event_id=source_id)
|
||||
method = "withdraw_external_source"
|
||||
before = deepcopy(repository.task)
|
||||
if not allowed:
|
||||
with pytest.raises(DomainError) as error:
|
||||
await getattr(service, method)(task["task_id"], command)
|
||||
assert error.value.code == "EXECUTION_SCOPE_MISMATCH" and error.value.status == 403
|
||||
assert repository.task == before
|
||||
assert repository.feedback == repository.evidence == repository.requests == {}
|
||||
return
|
||||
|
||||
await getattr(service, method)(task["task_id"], command)
|
||||
assert repository.task["status"] == ("completed" if operation == "feedback" else "cancelled")
|
||||
# Check scope before idempotency replay as well as before the first write.
|
||||
await getattr(service, method)(task["task_id"], command)
|
||||
before = deepcopy(repository.task)
|
||||
other = OperationalTaskService(repository, context, set(), clock=lambda: now,
|
||||
execution_scope="development" if server_scope == "production" else "production")
|
||||
with pytest.raises(DomainError) as error:
|
||||
await getattr(other, method)(task["task_id"], command)
|
||||
assert error.value.code == "EXECUTION_SCOPE_MISMATCH" and error.value.status == 403
|
||||
assert repository.task == before
|
||||
|
||||
Reference in New Issue
Block a user