Files
VianoBugAnalysis/tests/test_diagnosis_reasoning.py

120 lines
6.4 KiB
Python

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)