120 lines
6.4 KiB
Python
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)
|