import copy import json import pytest from test_investigation_lean import lean_context from test_case_builder import case_setup from test_investigation_startup import Adapter from test_investigation_tools import tool_case from test_investigator import CASE, run, state from test_working_brief import service_for from test_case_interpreter import gateway from viano_bug_analysis.analysis import Investigator, InvestigationConfig, InvestigationError from viano_bug_analysis.analysis.investigation_prompt import build_prompt from viano_bug_analysis.analysis.investigation_lean_v20 import validate_reply from viano_bug_analysis.analysis.investigation_window import evidence_page, pages, PAGE_CHARACTERS from viano_bug_analysis.llm import LLMError, ProviderResponse from viano_bug_analysis.llm.investigation_size import characters, check, check_wire, LIMIT from viano_bug_analysis.llm.gemini import GeminiProvider from viano_bug_analysis.tools import InvestigationToolRouter from viano_bug_analysis.package import PackageReader, PackageValidator def context(): c = lean_context() c['lean_protocol'] = 2 c['policy']['prompt_window_version'] = 1 return c def test_window_is_small_and_canonical_context_is_untouched(): c = context() before = copy.deepcopy(c) request = build_prompt(c) assert characters(request) <= 10_000 body = json.loads(request['messages'][1]['content']) assert 'response_contract' in body and 'tool_schema' not in body['input'] assert c == before def test_evidence_pages_are_lossless_and_explicitly_partial(): entry = {'record': {'hypothesis_id': 'H1'}, 'content': {'items': [ {'text': '\u00e4\U0001f600' * 8000, 'coverage': 'bounded'}]}} raw = pages(entry) first = evidence_page(entry) assert first['complete_payload'] is False fragments = [evidence_page(entry,i) for i in range(first['pages'])] assert ''.join(p['fragment'] for p in fragments) == raw assert all(len(p['fragment']) <= PAGE_CHARACTERS for p in fragments) assert len({p['sha256'] for p in fragments}) == 1 with pytest.raises(InvestigationError, match='INVESTIGATION_EVIDENCE_PAGE'): evidence_page(entry, first['pages']) def test_long_history_does_not_accumulate_raw_results(): c = context() seed = copy.deepcopy(next(iter(c['evidence'].values()))) sizes = [] for count in (3, 20, 100, 138): c['evidence'] = {f'E{i:06d}': copy.deepcopy(seed) for i in range(1,count+1)} for eid, entry in c['evidence'].items(): entry['record']['id'] = eid entry['content']['items'] = [{'text': eid + 'x' * 30_000}] c['previous_result'] = c['evidence'][f'E{count:06d}'] c['lean_session']['turns'] = [] request = build_prompt(c) sizes.append(characters(request)) body = json.loads(request['messages'][1]['content']) assert body['input']['archive']['omitted_from_view'] > 0 assert f'E{count:06d}' in body['input']['evidence'] assert max(sizes) < 20_000 assert max(sizes) - min(sizes) < 1500 def test_strict_controller_still_rejects_unknown_actions(): c = context() with pytest.raises(InvestigationError, match='INVESTIGATION_OUTPUT'): validate_reply(c, {'assessment': None, 'observation': '', 'next': {'kind': 'execute_shell', 'command': 'anything'}}) def test_size_guard_counts_schema_and_cached_wire_prefix(): request = {'messages': [{'role': 'system', 'content': 'a' * 10_000}], 'output_schema': {'type': 'object', 'description': 'b' * 10_000}} with pytest.raises(LLMError, match='INVESTIGATION_PROMPT_LIMIT'): check(request) with pytest.raises(LLMError, match='INVESTIGATION_PROMPT_LIMIT'): check_wire({'parts': [{'text': 'a' * 10_000}]}, [{'parts': [{'text': 'b' * 10_001}]}]) @pytest.mark.parametrize('version', ['investigation_project_v20', 'investigation_startup_v1', 'investigation_startup_review_v1', 'investigation_format_repair_v1']) def test_oversized_requests_never_reach_adapter(version): import asyncio adapter = Adapter() request = {'prompt_version':version,'privacy':'configured', 'messages':[{'role':'user','content':'x' * 20_001}], 'output_schema':{'type':'object'}} with pytest.raises(LLMError, match='INVESTIGATION_PROMPT_LIMIT'): asyncio.run(gateway(adapter).complete_round(request)) assert adapter.calls == [] request['prompt_version'] = 'case_plan_v2' assert gateway(adapter).validate_request(request)['prompt_version'] == 'case_plan_v2' def test_exact_character_limit_and_diagnostics(): from viano_bug_analysis.llm.investigation_size import compact schema = {'type':'object'} request = {'messages':[{'role':'user','content':'x' * (LIMIT-len(compact(schema)))}], 'output_schema':schema} assert check(request) == LIMIT request['messages'][0]['content'] += 'x' with pytest.raises(LLMError) as error: check(request) assert error.value.diagnostics['prompt_characters'] == LIMIT+1 def loop_for(tool_case, adapter): return Investigator(tool_case[0], gateway(adapter), InvestigationConfig( lean_loop=True, lean_protocol=2, startup_batch_review=True, lean_checkpoint_steps=0, prompt_window_version=1), router=InvestigationToolRouter(project_research=service_for(tool_case))) def test_bounded_startup_review_and_import(tool_case, tmp_path): adapter = Adapter() loop = loop_for(tool_case, adapter) result = run(loop, steps=4) assert state(result)['startup']['done'] assert len(adapter.calls) == 4 assert all('tool_schema' not in body['input'] for body in adapter.calls) assert PackageValidator().validate(result.package).valid # Replaying the stored bounded requests after an import retains bindings. from viano_bug_analysis.lifecycle import CheckpointStore, Workspace recipient = CheckpointStore(Workspace(tmp_path/'recipient')) recipient.intake(result.package) restored = Investigator(recipient, gateway(adapter), router=loop.router) with PackageReader(recipient.latest(*CASE)) as reader: assert restored._load(reader)['startup']['done'] def test_bounded_completion_after_checkpoints(tool_case): class Finish(Adapter): async def generate(self, config, request, **kwargs): body = json.loads(request['messages'][1]['content']) assert characters(request) <= LIMIT if body['task'] == 'investigate_one_step': out = {'assessment': None, 'observation': 'Ready to consolidate.', 'next': {'kind': 'checkpoint', 'intent': 'completion'}} elif body['task'] == 'consolidate_investigation_session': out = {'assessment': None, 'confidence': 'plausible', 'findings': [], 'uncertainties': ['No proven cause.'], 'next_question': None, 'retire': [], 'next': {'kind': 'finish', 'reason': 'Bounded investigation complete.'}} else: return await super().generate(config, request, **kwargs) return ProviderResponse(out, 'fixture', 100, 50) result = run(loop_for(tool_case, Finish()), steps=20) assert result.status == 'COMPLETED' assert PackageValidator().validate(result.package).valid def test_recall_page_is_persisted_and_replayed(tool_case): class Recall(Adapter): async def generate(self, config, request, **kwargs): body = json.loads(request['messages'][1]['content']) if body['task'] == 'investigate_one_step': self.calls.append(body) out = {'assessment': None, 'observation': 'Read archived evidence.', 'next': {'kind':'recall','evidence_refs':['E000001'],'page':0}} return ProviderResponse(out,'fixture',100,50) return await super().generate(config,request,**kwargs) loop = loop_for(tool_case, Recall()) result = run(loop, steps=5) assert state(result)['recalled_pages']['H1'] == {'E000001':0} result = run(loop, steps=1) assert result.status != 'BLOCKED' def test_bounded_cache_partition_is_exact_and_wire_is_bounded(): from viano_bug_analysis.llm.stable_context import split_document, restore_document request = build_prompt(context()) body = json.loads(request['messages'][1]['content']) parts = split_document(body,request['prompt_version']) assert parts and restore_document(*parts) == body system,prefix,live = GeminiProvider._messages(request,stable_case_context=True) assert check_wire(system,prefix+live) <= LIMIT def test_early_review_keeps_the_low_reasoning_route(): from viano_bug_analysis.llm.gateway import is_investigation_review c = context() c['lean_session']['checkpoint'] = True c['early_review'] = {'request':{'status':'preparing'}} request = build_prompt(c) assert is_investigation_review(request)