feat(investigations): refine report authoring and replay flushing
Generic Container CI/CD / test-build-publish (push) Successful in 4m31s
Server CI/CD v2 / build-test-publish-and-deploy (push) Successful in 4m31s

This commit is contained in:
2026-11-14 23:00:15 +08:00
parent 2e4f6cb93b
commit b45c5c6f46
27 changed files with 1125 additions and 94 deletions
+74
View File
@@ -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,
)