feat(investigations): refine report authoring and replay flushing
This commit is contained in:
@@ -2363,3 +2363,77 @@ async def test_external_sync_only_mutates_tasks_in_its_execution_scope(operation
|
||||
await getattr(other, method)(task["task_id"], command)
|
||||
assert error.value.code == "EXECUTION_SCOPE_MISMATCH" and error.value.status == 403
|
||||
assert repository.task == before
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("stage", ["asset_localization", "source_control", "flushing_plan", "clearance_verification"])
|
||||
async def test_presentation_narration_is_evidence_backed_saved_and_replayed_without_mutating_task(stage):
|
||||
now = datetime(2026, 11, 14, tzinfo=timezone.utc)
|
||||
context = _context()
|
||||
task = _task_document(context, now, lease=now + timedelta(minutes=1))
|
||||
repository = TaskRepository(task, _active_run(task, now))
|
||||
service = OperationalTaskService(repository, context, set(), clock=lambda: now)
|
||||
evidence, _ = await service.record_data_snapshot(task['task_id'], TaskDataSnapshot(
|
||||
source='query_scada_readings', source_ref='observations', queried_at=now,
|
||||
origin='synthetic', data={'items': []}, truncated=False,
|
||||
))
|
||||
narration = {'stage': stage, 'summary': '本阶段保存的候选方案及核验依据。',
|
||||
'evidence_refs': [evidence.evidence_id], 'next_step': '提出现场工单草稿,并行开展影响分析。'}
|
||||
command = TaskPresentationEventCommand(
|
||||
request_id=uuid4(), run_id=repository.run['run_id'], source_session_id='session-1',
|
||||
command={'action': 'locate_features', 'layer': 'pipes', 'feature_ids': ['P1']}, narration=narration,
|
||||
purpose='核对候选管段与异常测点的空间关系,供后续影响分析。',
|
||||
)
|
||||
result = await service.publish_presentation_event(task['task_id'], command)
|
||||
assert repository.events[-1]['metadata']['narration'] == narration
|
||||
assert repository.events[-1]['metadata']['purpose'] == command.purpose
|
||||
assert await service.publish_presentation_event(task['task_id'], command) == result
|
||||
page = await service.presentation_history(task['task_id'])
|
||||
assert page.items[-1].narration.model_dump(exclude_none=True) == narration
|
||||
assert page.items[-1].purpose == command.purpose
|
||||
assert repository.task['revision'] == task['revision']
|
||||
with pytest.raises(DomainError) as conflict:
|
||||
await service.publish_presentation_event(task['task_id'], command.model_copy(update={
|
||||
'narration': command.narration.model_copy(update={'summary': '不同判断'}),
|
||||
}))
|
||||
assert conflict.value.code == 'IDEMPOTENCY_CONFLICT'
|
||||
foreign = TaskPresentationEventCommand(
|
||||
request_id=uuid4(), run_id=repository.run['run_id'], source_session_id='session-1',
|
||||
command={'action': 'clear_highlights'}, narration={**narration, 'evidence_refs': ['other-task']},
|
||||
)
|
||||
before = len(repository.events)
|
||||
with pytest.raises(DomainError) as rejected:
|
||||
await service.publish_presentation_event(task['task_id'], foreign)
|
||||
assert rejected.value.code == 'EVIDENCE_NOT_FOUND'
|
||||
assert len(repository.events) == before
|
||||
|
||||
|
||||
@pytest.mark.parametrize('changes', [
|
||||
{'stage': 'invented_stage'}, {'summary': 'x' * 161}, {'summary': ' '},
|
||||
{'evidence_refs': []}, {'evidence_refs': ['e1', 'e1']}, {'next_step': 'x' * 121},
|
||||
])
|
||||
def test_presentation_narration_rejects_unbounded_or_unsubstantiated_fields(changes):
|
||||
with pytest.raises(ValidationError):
|
||||
TaskPresentationEventCommand(
|
||||
request_id=uuid4(), run_id=uuid4(), source_session_id='session-1',
|
||||
command={'action': 'clear_highlights'},
|
||||
narration={'stage': 'evidence_verification', 'summary': '测点异常待核验',
|
||||
'evidence_refs': ['e1'], **changes},
|
||||
)
|
||||
|
||||
|
||||
def test_legacy_presentation_idempotency_body_does_not_gain_an_empty_narration_field():
|
||||
command = TaskPresentationEventCommand(
|
||||
request_id=uuid4(), run_id=uuid4(), source_session_id='session-1',
|
||||
command={'action': 'clear_highlights'},
|
||||
)
|
||||
assert 'narration' not in command.model_dump(mode='json')
|
||||
assert 'purpose' not in command.model_dump(mode='json')
|
||||
|
||||
@pytest.mark.parametrize('purpose', ['', ' ', 'x' * 161, 123])
|
||||
def test_presentation_purpose_rejects_invalid_intent(purpose):
|
||||
with pytest.raises(ValidationError):
|
||||
TaskPresentationEventCommand(
|
||||
request_id=uuid4(), run_id=uuid4(), source_session_id='session-1',
|
||||
command={'action': 'zoom_to_map', 'center': [121, 31], 'zoom': 15}, purpose=purpose,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user