import copy import pytest from types import SimpleNamespace from viano_bug_analysis.analysis.investigation_view import working_view from viano_bug_analysis.analysis.investigator import Investigator def context(): proposal={'title':'A','mechanism':'May be absent','distinction':'Static versus dynamic', 'falsifying_evidence':['A present object would disconfirm absence'], 'expected_evidence':['No rows'],'open_questions':['Exists?'],'rationale':'Untested', 'source_ids':['F0029'],'interpretation_ids':['I0001']} result={'record_ref':'evidence/one.json','record':{'id':'E000001','hypothesis_id':'H1', 'action_id':'A1','novel_atoms':['internal-hash']}, 'content':{'items':[{'row':{'entry':23487,'name':'Wild subject'}}], 'scope':'map 0','coverage':'complete_filtered_rows','truncated':False}} return {'case':{'case':{'case_id':'case','package_id':'package','analysis_revision':1}, 'inputs':{'huge':'not needed'},'source_context':{'refs':[{'path':'original/'+'a'*64+'/code.cpp'}]}, 'database_context':{'refs':[]}}, 'case_sources':{'F0029':{'kind':'fact','data':{'name':'entity_name_lookup','value':{'entry':23487}}}, 'O0001':{'kind':'observation','data':{'text':'Nothing visible','provenance':{'author':'Tester'}}}, 'X0001':{'kind':'unread_reference','data':{'path':'original/'+'a'*64+'/code.cpp','size':100}}}, 'hypotheses':{h:{'proposal':copy.deepcopy(proposal),'notes':['Counterevidence matters'], 'questions':[{'id':'Q1','text':'Exists?','resolved':False}], 'confidence':'unassessed','evidence_refs':['E000001']} for h in ['H1','H2']}, 'selected_hypothesis':'H1','budget':{'seen_evidence':['hash'], 'accounts':{'H1':{ 'remaining':90,'spent':10,'rewarded':0,'status':'investigating','actions':{ 'A1':{'status':'settled'},'A2':{'status':'result_stored','cost':10}}}}}, 'evidence':{'E000001':result,'E000002':{**copy.deepcopy(result),'record':{ **result['record'],'id':'E000002','hypothesis_id':'H2'}}}, 'previous_result':result,'tool_schema':{'x-project-bindings':{'database':{'snapshot_id':'snapshot'}}}} def test_working_view_preserves_sources_counterevidence_and_pending_budget(): c=context();before=copy.deepcopy(c);aliases={} view=working_view(c,aliases_out=aliases) assert c==before assert 'inputs' not in view['case'] assert view['sources']['F0029']['data']==c['case_sources']['F0029']['data'] assert view['sources']['O0001']==c['case_sources']['O0001'] assert set(view['unread_sources'])=={'X0001'} for eid in c['evidence']: assert view['evidence'][eid]['content']==c['evidence'][eid]['content'] assert 'novel_atoms' not in view['evidence'][eid]['record'] assert view['previous_result']=={'evidence_id':'E000001','see':'evidence'} assert view['hypotheses']['H2']['proposal']['falsifying_evidence']==c['hypotheses']['H2']['proposal']['falsifying_evidence'] assert view['hypotheses']['H2']['notes']==c['hypotheses']['H2']['notes'] account=view['budget']['accounts']['H1'] assert account['remaining']==90 and account['spent']==10 assert account['actions']=={'A2':{'status':'result_stored','cost':10}} assert account['settled_action_count']==1 assert 'seen_evidence' not in view['budget'] assert aliases[view['unread_sources']['X0001']['data']['path']]==c['case_sources']['X0001']['data']['path'] assert working_view(c)==view @pytest.mark.parametrize('version',[None,'investigation_project_v11']) def test_tool_reference_alias_resolution_is_scoped_and_preserves_model_decision(monkeypatch,version): c=context() if version:c['prompt_version']=version aliases={};view=working_view(c,aliases_out=aliases) alias=view['unread_sources']['X0001']['data']['path'] state={'case_id':'case','package_id':'package','analysis_revision':1,'prompt_protocol':2, 'active':{'hypothesis_id':'H1','iteration':1,'context_ref':'context.json'}} class Reader: def __init__(self,*args):pass def __enter__(self):return self def __exit__(self,*args):pass monkeypatch.setattr('viano_bug_analysis.analysis.investigator.PackageReader',Reader) fake=SimpleNamespace(store=SimpleNamespace(limits=None),_latest=lambda *args:'package.zip', _read=lambda *args:c) action={'operation':'source.find_text','arguments':{'source_refs':[alias,'UNKNOWN_ALIAS'],'text':'needle'}} original=copy.deepcopy(action) request=Investigator._tool_request(fake,state,action) assert action==original assert request['requests'][0]['arguments']['source_refs']==[c['case_sources']['X0001']['data']['path'],'UNKNOWN_ALIAS'] def test_unlisted_file_inventory_is_explicit_not_a_negative_finding(): c=context() c['case_sources']['X0002']={'kind':'unread_reference','data':{'path':'other/raw.json','size':50}} v=working_view(c) assert 'X0002' in v['unread_inventory_scope']['unlisted_source_ids'] assert 'X0002' not in v['unread_sources'] assert v['sources']['F0029']['data']['value']['entry']==23487 assert v['evidence']['E000002']['content']==c['evidence']['E000002']['content'] def test_attribution_is_preserved_without_archive_navigation_scaffolding(): c=context() c['case_sources']['O0001']['data']['provenance']={ 'kind':'tester','author':'Tester','observed_at':'2026-10-08T13:00:00Z', 'source_ref':'original/'+'a'*64+'/report.json', 'origin':{'source_timestamp':'2026-10-08T12:59:00Z','json_pointer':'/report/0', 'archive_ref':'original/'+'b'*64+'/archive.tar.gz'}} c['tool_schema']['x-project-bindings']['query_schema']={'duplicated':'contract'} v=working_view(c) p=v['sources']['O0001']['data']['provenance'] assert p=={'kind':'tester','author':'Tester','observed_at':'2026-10-08T13:00:00Z', 'origin':{'source_timestamp':'2026-10-08T12:59:00Z'}} assert 'query_schema' not in v['tool_schema']['project'] assert 'source_ref' in c['case_sources']['O0001']['data']['provenance']