import copy import json import pytest from viano_bug_analysis.analysis import ChangeRequestBuilder, OutputValidator from viano_bug_analysis.analysis.change_request_contracts import ( build_prompt, compact_handoff_context, scope_evidence, validate_request, ChangeRequestError, ) from viano_bug_analysis.package import PackageReader from test_change_request_builder import diagnosed, build, RequestAdapter from test_case_interpreter import gateway from test_case_builder import case_setup from test_investigation_tools import tool_case from test_handoff_outcomes import minimal from test_investigator import CASE def test_capture_scope_is_explicit_and_schema_similarity_is_not_provenance(): payload, context = minimal() context['local_context'] = {'capture.json': {'kind': 'database'}, 'creature-schema.json': {'kind': 'database'}} context['evidence']['E000001']['content']['items'] = [ {'query': {'kind': 'database.rows', 'table': 'creature'}, 'result': {'items': [], 'coverage': 'complete_filtered_rows'}}] assert scope_evidence(context) == {'capture.json': ['E000001']} payload['affected_scope'] = [{'kind': 'database', 'source_ref': 'capture.json', 'evidence_refs': ['E000001']}] validate_request(payload, context) payload['affected_scope'][0]['source_ref'] = 'creature-schema.json' with pytest.raises(ChangeRequestError, match='SCOPE_EVIDENCE'): validate_request(payload, context) def test_compact_view_keeps_findings_observations_uncertainty_and_context_unchanged(): _, context = minimal() context.update(case_id='case', package_id='package', analysis_revision=1, diagnosis_ref='diagnosis.json', diagnosis_sha256='0'*64, interpretation={'unknowns': ['Event membership not fully checked']}, policy={'rules': ['No invented data']}, case={'human_observations': ['Creatures absent'], 'data_quality': {'warnings': ['Snapshot only']}, 'inputs': ['unused input'] * 10000, 'source_context': {'refs': ['unused catalog'] * 10000}}) context['evidence']['E000001']['record']['content_sha256'] = '1'*64 original = copy.deepcopy(context) view = compact_handoff_context(context) assert view['evidence']['E000001']['content'] == context['evidence']['E000001']['content'] assert view['diagnosis'] == context['diagnosis'] assert view['interpretation'] == context['interpretation'] assert view['case']['human_observations'] == ['Creatures absent'] assert view['case']['data_quality'] == {'warnings': ['Snapshot only']} request = build_prompt(context, compact=True) assert json.loads(request['messages'][1]['content'])['output_schema'] == request['output_schema'] assert len(json.dumps(request)) < len(json.dumps(build_prompt(context))) / 5 assert context == original @pytest.mark.parametrize('first_version', ['change_request_v2', 'change_request_v3']) def test_retry_gets_actionable_feedback_and_portable_replay(diagnosed, tmp_path, first_version): import asyncio from viano_bug_analysis.lifecycle import CheckpointStore, Workspace class Correcting(RequestAdapter): async def generate(self, config, request, **kwargs): response = await super().generate(config, request, **kwargs) assert request['prompt_version'] == (first_version if self.calls == 1 else 'change_request_v6') if self.calls == 1: response.document['affected_scope'][0]['source_ref'] = 'wrong.cpp' else: feedback = self.contexts[-1]['validation_feedback'] assert feedback[0]['code'] == 'CHANGE_REQUEST_SCOPE' assert feedback[0]['rejected_scope'][0]['source_ref'] == 'wrong.cpp' assert self.contexts[-1]['local_context']['tool/code.cpp']['allowed_evidence_refs'] return response store, _ = diagnosed adapter = Correcting() g = gateway(adapter) builder = ChangeRequestBuilder(store, g) original = builder._request def first_request(reader, context, operation, **kwargs): if not adapter.calls: kwargs['version'] = first_version return original(reader, context, operation, **kwargs) builder._request = first_request assert build(builder).status == 'REJECTED' result = build(builder) assert result.status == 'COMPLETED' assert build(builder).reused and adapter.calls == 2 exported = asyncio.run(OutputValidator(store, g).publish(*CASE)) portable = CheckpointStore(Workspace(tmp_path / 'recipient')) portable.intake(exported.exported) assert asyncio.run(OutputValidator(portable, g).publish(*CASE)).status == 'OUTPUT_READY' assert adapter.calls == 2 def test_existing_v2_call_stays_frozen_after_upgrade(diagnosed): store, _ = diagnosed adapter = RequestAdapter() builder = ChangeRequestBuilder(store, gateway(adapter)) original = builder._request builder._request = lambda reader, context, operation, **kw: original( reader, context, operation, version='change_request_v2') publish = builder.publish builder.publish = lambda *a, **kw: (_ for _ in ()).throw(RuntimeError('crash')) with pytest.raises(ChangeRequestError): build(builder) builder._request = original builder.publish = publish assert build(builder).status == 'COMPLETED' assert build(builder).reused and adapter.calls == 1