73 lines
3.7 KiB
Python
73 lines
3.7 KiB
Python
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)
|