import copy import json from dataclasses import replace import pytest from jsonschema import Draft202012Validator from test_parameter_and_bootstrap_guard import bootstrap from test_lean_investigation_decision import reply from test_llm_caching import config,native,scripted,CACHE from viano_bug_analysis.analysis.investigation_prompt import build_prompt from viano_bug_analysis.analysis import InvestigationConfig,InvestigationError from viano_bug_analysis.llm.gemini import GeminiProvider,bootstrap_shape def nested_request(version=2): from test_investigation_lean import lean_context c=lean_context();c.pop('lean_session');c.update(bootstrap_json_framing=True,bootstrap_framing_version=version) return build_prompt(c) def nonempty_reply(): r=reply();r['update']['next_step']=None r['update']['working_brief']['findings']=[{'id':'B0001','direction':'neutral', 'text':'Observed bounded result.','scope':'Examined snapshot.','evidence_refs':['E000001']}] return r @pytest.mark.parametrize('bad',['plain finding',['B0001','neutral'],None,42,True]) def test_native_nested_shape_rejects_wrong_finding_type(bad): r=nonempty_reply();request=nested_request();before=copy.deepcopy(request['output_schema']) frame=bootstrap_shape(request['output_schema']);Draft202012Validator.check_schema(frame) assert request['output_schema']==before assert Draft202012Validator(frame).is_valid(r) r['update']['working_brief']['findings'][0]=bad assert not Draft202012Validator(frame).is_valid(r) @pytest.mark.parametrize('field,value',[('evidence_refs','E000001'),('id',42),('text',[]),('scope',None)]) def test_native_shape_covers_nested_finding_fields(field,value): r=nonempty_reply();r['update']['working_brief']['findings'][0][field]=value assert not Draft202012Validator(bootstrap_shape(nested_request()['output_schema'])).is_valid(r) @pytest.mark.parametrize('version',[2,3]) def test_new_native_contract_sent_with_cache_and_full_schema_retained(monkeypatch,version): request=nested_request(version);assert request['prompt_version']==f'investigation_bootstrap_v{20+version}' response=native();response['candidates'][0]['content']['parts'][0]['text']=json.dumps(nonempty_reply()) adapter=GeminiProvider();seen=scripted(adapter,monkeypatch,[CACHE,response,response]) settings=replace(config(),cache_min_prefix_chars=10**9,response_format='json_object') first=adapter._generate(settings,request,1000000);second=adapter._generate(settings,request,1000000) assert first.document==second.document==nonempty_reply() assert len(seen)==3 and seen[1][1]['cachedContent']==seen[2][1]['cachedContent'] frame=seen[1][1]['generationConfig']['responseJsonSchema'] item=frame['properties']['update']['properties']['working_brief']['properties']['findings']['items'] assert item['type']=='object' and set(item['required'])=={'id','direction','text','scope','evidence_refs'} assert 'oneOf' not in json.dumps(frame) and len(json.dumps(frame))<5000 # The complete local/cached contract still constrains enum values and IDs. bad=nonempty_reply();bad['update']['working_brief']['findings'][0]['direction']='invented' assert not Draft202012Validator(request['output_schema']).is_valid(bad) assert 'output_schema' in str(seen[0][1]['contents']) def test_legacy_request_remains_v21_and_schema_identical(): old=bootstrap();new=nested_request() assert old['prompt_version']=='investigation_bootstrap_v21' assert new['output_schema']==old['output_schema'] @pytest.mark.parametrize('version',[0,4,True,'2']) def test_invalid_bootstrap_version_rejected(version): with pytest.raises(InvestigationError):InvestigationConfig(bootstrap_framing_version=version)