import copy import pytest from viano_bug_analysis.analysis.diagnosis_builder import DiagnosisError, validate_evaluation from viano_bug_analysis.analysis.early_stop import accepts_evaluation, check_request def example(): context = { 'evaluation_protocol': 2, 'investigation': {'early_stop': {'hypothesis_id': 'H1'}, 'hypotheses': { h: {'disposition': 'active'} for h in ('H1', 'H2', 'H3')}}, 'evidence': {'E000001': {'record': {'hypothesis_id': 'H1'}, 'content': {'items': [{'finding': 'Required activation record absent in complete filtered snapshot'}], 'coverage': 'complete_filtered_rows'}}}, 'budget': {'accounts': {h: {'actions': {}} for h in ('H1', 'H2', 'H3')}}, } payload = { 'status': 'confirmed', 'selected_hypothesis': 'H1', 'confidence': 'strongly_supported', 'root_cause': {'summary': 'Required activation data missing', 'technical_description': 'The missing static activation record explains the observed failure within the checked snapshot.', 'evidence_refs': ['E000001']}, 'evaluations': [{'hypothesis_id': h, 'verdict': 'supported' if h == 'H1' else 'unresolved', 'reason': 'Absence of required activation data supports H1 and weakens the hidden-static-instance alternative.', 'evidence_refs': ['E000001'], 'countercheck': {'adequate': h == 'H2', 'reason': 'The same query checks the prerequisite of an existing static instance. Dynamic paths remain untested.', 'evidence_refs': ['E000001']}} for h in ('H1', 'H2', 'H3')], 'remaining_uncertainty': ['An additional dynamic activation path has not been excluded.'], 'reason': 'A bounded correction is justified without claiming exhaustive exclusion.', 'recommended_human_actions': ['Validate the expected interaction after the bounded correction.'], 'decision_basis': {'causal_explanation': 'The required static activation input is missing.', 'scope_limits': ['Complete filtered snapshot query, no exhaustive runtime audit.'], 'sufficient_for_change': True, 'contradictions': []}, } return payload, context def test_shared_evidence_and_unfinished_alternatives_allow_early_completion(): payload, context = example() validate_evaluation(payload, context) assert accepts_evaluation(payload, context) # A new uninvestigated possibility does not create an obligation to exclude it. context['investigation']['hypotheses']['H4'] = {'disposition': 'active'} evaluation = copy.deepcopy(payload['evaluations'][-1]) evaluation['hypothesis_id'] = 'H4' payload['evaluations'].append(evaluation) validate_evaluation(payload, context) @pytest.mark.parametrize('change,code', [ (lambda p,c: p['decision_basis']['contradictions'].append({'evidence_refs':['E000001'], 'resolution':'unresolved', 'reason':'Observed runtime contradicts the explanation.'}), 'UNRESOLVED_CONTRADICTION'), (lambda p,c: p['decision_basis'].update(sufficient_for_change=False), 'INSUFFICIENT_CAUSAL_SUPPORT'), (lambda p,c: p.update(remaining_uncertainty=[]), 'UNCERTAINTY_REQUIRED'), (lambda p,c: p['root_cause'].update(evidence_refs=['E999999']), 'EVIDENCE_REFERENCE'), (lambda p,c: [e['countercheck'].update(adequate=False) for e in p['evaluations']], 'COUNTERCHECK'), (lambda p,c: c['evidence']['E000001']['content'].update(coverage='schema_only'), 'UNGROUNDED'), (lambda p,c: p.pop('decision_basis'), 'DECISION_BASIS'), ]) def test_material_validation_requirements_remain(change, code): payload, context = example() change(payload, context) with pytest.raises(DiagnosisError, match=code): validate_evaluation(payload, context) def test_contradiction_must_have_known_references_even_if_addressed(): payload, context = example() payload['decision_basis']['contradictions'] = [{'evidence_refs':['E000001'], 'resolution':'addressed', 'reason':'The apparent conflict concerns a different snapshot scope.'}] validate_evaluation(payload, context) payload['decision_basis']['contradictions'][0]['evidence_refs'] = ['E999999'] with pytest.raises(DiagnosisError, match='EVIDENCE_REFERENCE'): validate_evaluation(payload, context) def test_request_can_reuse_another_hypothesis_finding(): from test_early_diagnosis import gate_fixture saved, context, decision, budget = gate_fixture() decision['update']['evidence_refs'] = ['E000002'] decision['next']['countercheck_refs'] = ['E000002'] check_request(saved, context, decision, budget) def test_supported_alternative_is_not_automatically_a_contradiction(): payload, context = example() payload['evaluations'][1]['verdict'] = 'supported' validate_evaluation(payload, context) assert accepts_evaluation(payload, context) from test_early_diagnosis import gate_fixture saved, ctx, decision, budget = gate_fixture() saved['hypotheses']['H2']['confidence'] = 'supported' check_request(saved, ctx, decision, budget) def test_lean_normalization_adds_root_refs_without_mutating_raw_reply(): from viano_bug_analysis.analysis.diagnosis_builder import normalize_evaluation payload, context = example() context['evidence']['E000004'] = {'content': {'items':[{'column':'guid'}], 'coverage':'schema_only'}} payload['root_cause']['evidence_refs'].append('E000004') payload.pop('reason') original = copy.deepcopy(payload) result = normalize_evaluation(payload) validate_evaluation(result, context) assert result['evaluations'][0]['evidence_refs'] == ['E000001','E000004'] assert result['reason'] == payload['decision_basis']['causal_explanation'] assert payload == original assert normalize_evaluation(result) == result @pytest.mark.parametrize('mode', ['unknown','schema_only','contradiction']) def test_normalization_does_not_bypass_material_controls(mode): from viano_bug_analysis.analysis.diagnosis_builder import normalize_evaluation payload, context = example() if mode == 'unknown': payload['root_cause']['evidence_refs'] = ['E999999'] if mode == 'schema_only': context['evidence']['E000001']['content']['coverage'] = 'schema_only' if mode == 'contradiction': payload['decision_basis']['contradictions'] = [{'evidence_refs':['E000001'], 'resolution':'unresolved','reason':'Runtime contradicts this cause.'}] with pytest.raises(DiagnosisError): validate_evaluation(normalize_evaluation(payload), context)