import asyncio import copy import json import pytest from jsonschema import Draft202012Validator from test_compact_investigation import context,entry from test_lean_investigation_decision import reply from test_case_builder import case_setup from test_investigation_tools import tool_case from viano_bug_analysis.analysis.investigation_plan import framed_view,observed_keys from viano_bug_analysis.analysis.investigation_compact import model_view,validate_tool_shape from viano_bug_analysis.analysis.investigation_prompt import build_prompt,output_schema from viano_bug_analysis.analysis.investigation_decision import normalize from viano_bug_analysis.analysis.investigation_config import InvestigationError from viano_bug_analysis.case.contracts import validate def plan(): return {'goal':'Determine whether these observed objects are event-bound.', 'supports_if':'No objects exist in the affected scope.', 'weakens_if':'Verified objects already exist there.', 'investigate_if':'The result is incomplete or another creation path is untested.'} def current(): c=context();c['prompt_version']='investigation_project_v18';return c def test_model_goal_and_criteria_persist_unchanged_without_mutating_reply(): c=current();r=reply();r['update']['next_step']=plan();before=copy.deepcopy(r) d=normalize(c,r) assert r==before and d['update']['working_brief']['next_step']==plan() validate(d['update']['working_brief'],'investigation-working-brief') c['working_brief']['brief']=d['update']['working_brief'] view=framed_view(c,model_view(c)) assert view['investigation_step']['decision']==plan() assert view['investigation_step']['decision_source']=='previous_model_plan' assert 'next_step' not in view['working_brief'] assert c['working_brief']['brief']['next_step']==plan() def test_action_needs_a_plan_but_completion_can_use_null(): c=current();r=reply();r['update']['next_step']=None r['next']={'kind':'recall','evidence_refs':['E000002']} with pytest.raises(InvestigationError,match='INVESTIGATION_PLAN'):normalize(c,r) r['next']={'kind':'finish','reason':'Enough scoped evidence.'} assert normalize(c,r)['update']['working_brief']['next_step'] is None r['update'].pop('next_step') assert not Draft202012Validator(output_schema(c)).is_valid(r) def test_missing_plan_criteria_produce_precise_repair_fields(): c=current();r=reply();r['update']['next_step']={'goal':'PRIVATE_PLAN'} with pytest.raises(InvestigationError) as caught:validate_tool_shape(c,r) assert caught.value.feedback['validation_path']=='/update/next_step' assert caught.value.feedback['validation_fields']==['supports_if','weakens_if','investigate_if'] assert 'PRIVATE_PLAN' not in json.dumps(caught.value.feedback) def key_context(count=18,origin=None): c=current();c['evidence']={};origin=origin or {'snapshot_id':'verified'} schema=entry(1,'database.schema');capture=schema['content']['items'][0] capture['origin']=copy.deepcopy(origin) capture['result']['items'][0]['columns']=[{'COLUMN_NAME':'object_key','COLUMN_TYPE':'int','COLUMN_KEY':'PRI'}] rows=entry(20);rows['content']['items']=[{'query':{'kind':'database.rows','table':'creature','offset':0}, 'origin':copy.deepcopy(origin),'result':{'coverage':'complete_filtered_rows','truncated':False,'next_offset':None, 'items':[{'row':{'object_key':i,'irrelevant_payload':'large raw text'}} for i in range(count)]}}] c['evidence']={'E000001':schema,'E000020':rows};c['previous_result']=rows return c def test_known_inputs_are_observed_key_tuples_with_evidence_and_scope(): c=key_context();before=copy.deepcopy(c);items=observed_keys(c,model_view(c)) assert items==[{'evidence_id':'E000020','capture_index':0,'schema_id':'E000001', 'table':'creature','key_columns':['object_key'],'rows':[[i] for i in range(18)], 'all_returned_keys_included':True,'query_complete':True}] assert c==before def test_key_list_limit_and_query_paging_are_distinct_and_explicit(): c=key_context(40);items=observed_keys(c,model_view(c)) assert len(items[0]['rows'])==32 and items[0]['all_returned_keys_included'] is False assert items[0]['query_complete'] is True c['evidence']['E000020']['content']['items'][0]['result']['truncated']=True assert observed_keys(c,model_view(c))[0]['query_complete'] is False def test_composite_keys_preserve_tuple_relationships_and_scalar_types(): c=key_context(2);schema=c['evidence']['E000001']['content']['items'][0] schema['result']['items'][0]['columns'].append({'COLUMN_NAME':'group_key','COLUMN_TYPE':'varchar','COLUMN_KEY':'PRI'}) rows=c['evidence']['E000020']['content']['items'][0]['result']['items'] rows[0]['row']['group_key']='alpha';rows[1]['row']['group_key']='beta' item=observed_keys(c,model_view(c))[0] assert item['key_columns']==['object_key','group_key'] and item['rows']==[[0,'alpha'],[1,'beta']] @pytest.mark.parametrize('change',['origin','no_primary','unknown_shape','bool_value','missing_column']) def test_key_hints_do_not_guess_keys_values_or_snapshot_relationships(change): c=key_context();s=c['evidence']['E000001']['content']['items'][0];r=c['evidence']['E000020']['content']['items'][0] if change=='origin':s['origin']={'snapshot_id':'different'} elif change=='no_primary':s['result']['items'][0]['columns'][0].pop('COLUMN_KEY') elif change=='unknown_shape':s['result']['items']=['unknown provider format'] elif change=='bool_value':r['result']['items'][0]['row']['object_key']=True else:r['result']['items'][0]['row'].pop('object_key') assert observed_keys(c,model_view(c))==[] def test_unread_unrelated_archived_rows_are_not_silently_added_as_hints(): c=key_context();c['evidence']['E000020']['record']['hypothesis_id']='H2';c['previous_result']=None assert observed_keys(c,model_view(c))==[] def test_v18_prompt_is_shorter_and_all_existing_actions_still_available(): c=current();old=build_prompt(dict(c,prompt_version='investigation_project_v17'));new=build_prompt(c) assert len(new['messages'][0]['content'])