import asyncio import copy import json import pytest from viano_bug_analysis.analysis import ChangeRequestBuilder, OutputValidator, OutputError from viano_bug_analysis.analysis.change_request_contracts import validate_request, make_document, render_human_request, ChangeRequestError from viano_bug_analysis.budget import BudgetManager from viano_bug_analysis.evidence.store import digest from viano_bug_analysis.lifecycle import State, CheckpointStore, Workspace from viano_bug_analysis.lifecycle.storage import json_bytes from viano_bug_analysis.package import PackageReader, PackageWriter, PackageValidator from test_case_builder import case_setup from test_case_interpreter import gateway from test_investigation_tools import tool_case from test_change_request_builder import diagnosed, RequestAdapter, build from test_investigator import CASE def human(): return {'status':'assigned','title':'Collect approved placement data', 'reason':'The cause is established but remediation data is missing.', 'instructions':['Record approved positions and complete the template externally.'], 'expected_results':['Placement data and a completed remediation template for the next instance.']} def template(payload, context): payload.update(status='template',missing_information=['Approved placement coordinate.'], placeholders=[{'token':'{{POSITION_X}}','description':'Approved horizontal coordinate.'}], implementation_guidance=['Create a template using {{POSITION_X}}; do not guess the coordinate.'], human_dev=human()) def no_coding(payload, context): payload.update(status='none',affected_scope=[],validation_requirements=[],implementation_guidance=[], required_change='Kein Codingauftrag vorhanden',implementation_intent='No coding assignment is needed.', reason='Remediation is an external human task.',human_dev=human()) def test_template_and_human_data_task_are_completed_handoffs(diagnosed, tmp_path): store, original=diagnosed before=BudgetManager(store).snapshot(*CASE).state adapter=RequestAdapter(template);g=gateway(adapter) result=build(ChangeRequestBuilder(store,g)) assert result.status=='COMPLETED' and result.failure_ref is None with PackageReader(original) as old, PackageReader(result.package) as reader: coding=json.loads(reader.read_bytes(reader.manifest['artifacts']['change_request'])) assert coding['assignment']=='assigned' and coding['request']['status']=='template' assert coding['human_dev_assigned'] is True and 'human_dev' not in coding['request'] assert coding['request']['placeholders'][0]['token']=='{{POSITION_X}}' text=reader.read_bytes(reader.manifest['artifacts']['human_dev_request']).decode() assert 'Auftrag vorhanden' in text and 'complete the template externally' in text assert not reader.manifest['artifacts']['failure_report'] for name in old.files: if name!='manifest.json':assert reader.read_bytes(name)==old.read_bytes(name) # No readiness/dependency or human-completion gate: the unresolved template # and data task are exported immediately, with unchanged research budgets. exported=asyncio.run(OutputValidator(store,g).publish(*CASE)) assert exported.status=='OUTPUT_READY' and exported.exported.parent==store.workspace.outbox assert BudgetManager(store).snapshot(*CASE).state==before assert PackageValidator().validate(exported.exported).valid portable=CheckpointStore(Workspace(tmp_path/'recipient'));portable.intake(exported.exported) assert asyncio.run(OutputValidator(portable,g).publish(*CASE)).status=='OUTPUT_READY' assert adapter.calls==1 store.advance(*CASE,State.HANDED_OFF,step_id='recipient-ack') @pytest.mark.parametrize('human_assigned',[False,True]) def test_no_coding_assignment_still_exports_both_documents(diagnosed,human_assigned): store,_=diagnosed def change(p,c): no_coding(p,c) if not human_assigned: p['human_dev']={'status':'none','title':'Kein Human-Dev-Auftrag vorhanden', 'reason':'No additional human work is needed.','instructions':[],'expected_results':[]} adapter=RequestAdapter(change);g=gateway(adapter) assert build(ChangeRequestBuilder(store,g)).status=='COMPLETED' result=asyncio.run(OutputValidator(store,g).publish(*CASE)) assert result.status=='OUTPUT_READY' with PackageReader(result.package) as reader: doc=json.loads(reader.read_bytes(reader.manifest['artifacts']['change_request'])) assert doc['assignment']=='none' and doc['human_dev_assigned']==human_assigned assert reader.read_bytes(reader.manifest['artifacts']['human_dev_request']) assert reader.manifest['routing']['destination']=='outbox' def test_missing_human_document_pointer_prevents_export(diagnosed,tmp_path): store,_=diagnosed;g=gateway(RequestAdapter()) result=build(ChangeRequestBuilder(store,g));bad=tmp_path/'pointer.zip' PackageWriter().write(result.package,bad,manifest_updates={'artifacts':{'human_dev_request':None}}) store.intake(bad) with pytest.raises(OutputError):asyncio.run(OutputValidator(store,g).publish(*CASE)) assert not list(store.workspace.outbox.glob('*.zip')) @pytest.mark.parametrize('legacy_insufficient',[False,True]) def test_legacy_pending_request_keeps_its_prompt_and_portable_replay(diagnosed,legacy_insufficient): def legacy_change(p,c): if legacy_insufficient: p.update(status='insufficient_context',affected_scope=[],validation_requirements=[], missing_information=['Approved placement data is missing.']) store,_=diagnosed;adapter=RequestAdapter(legacy_change);g=gateway(adapter);builder=ChangeRequestBuilder(store,g) with PackageReader(store.latest(*CASE)) as reader:context=builder.context(reader,3) ref='analysis/change-requests/0001/context.json';raw=json_bytes(context) store.advance(*CASE,State.BUILDING_DIAGNOSIS,step_id='change-request:v1:1:start', details={'operation':'change-request-start','context_ref':ref,'context_sha256':digest(context)}, additions={ref:raw},artifacts={'change_request_context':ref}) result=build(builder) assert result.status=='COMPLETED' and build(builder).reused and adapter.calls==1 with PackageReader(result.package) as reader: doc=json.loads(reader.read_bytes(result.change_request_ref)) assert doc['format_version']==2 and reader.read_bytes(ref)==raw assert reader.manifest['artifacts']['human_dev_request'] assert doc['assignment']==('none' if legacy_insufficient else 'assigned') assert doc['human_dev_assigned']==legacy_insufficient assert reader.manifest['state']==State.BUILDING_DIAGNOSIS assert asyncio.run(OutputValidator(store,g).publish(*CASE)).status=='OUTPUT_READY' assert adapter.calls==1 def minimal(): context={'handoff_protocol':2,'local_context':{'code.cpp':{'kind':'source'}}, 'evidence':{'E000001':{'record':{'content_ref':'capture.json'},'content':{'items':[{'source_ref':'code.cpp'}]}}}, 'diagnosis':{'diagnosis':{'root_cause':{'evidence_refs':['E000001']}}}} payload={'status':'ready','problem_summary':'Observed defect','affected_scope':[{'kind':'source','source_ref':'code.cpp','evidence_refs':['E000001']}], 'required_change':'Correct the defect','implementation_intent':'Bounded correction', 'implementation_guidance':['Use the verified context'],'constraints':[],'preserve':[],'forbidden_changes':[], 'do_not_fix_by':[],'validation_requirements':[{'description':'Expected behavior restored','evidence_refs':['E000001']}], 'missing_information':[],'reason':'Sufficient coding context','placeholders':[], 'human_dev':{'status':'none','title':'No human work','reason':'No human task is needed','instructions':[],'expected_results':[]}} return payload,context @pytest.mark.parametrize('change,code',[ (lambda p: p.update(status='template',missing_information=['Coordinate']), 'PLACEHOLDERS'), (lambda p: p.update(status='none'), 'NO_TASK_CONTRADICTORY'), (lambda p: p['human_dev'].update(status='assigned'), 'HUMAN_DEV_INCOMPLETE'), (lambda p: p['human_dev'].update(instructions=['Unexpected hidden work']), 'HUMAN_DEV_CONTRADICTORY'), (lambda p: p.update(placeholders=[{'token':'{{X}}','description':'Value'}]), 'PLACEHOLDERS'), ]) def test_inconsistent_handoff_rejected(change,code): payload,context=minimal();change(payload) with pytest.raises(ChangeRequestError,match=code):validate_request(payload,context) def test_coding_fields_do_not_contain_human_task_body(): payload,context=minimal();payload['human_dev']=human() context.update(case_id='case',package_id='package',analysis_revision=1,diagnosis_ref='diagnosis.json',diagnosis_sha256='0'*64, interpretation={'observed_behavior':[],'expected_behavior':[]}) context['local_context']['code.cpp'].update(source_ref='code.cpp',sha256='0'*64,size=1) doc=make_document(context,payload,'context.json','call.json') assert doc['human_dev_assigned'] assert 'Record approved positions' not in json.dumps(doc) assert 'Record approved positions' in render_human_request(context,payload) def test_verified_project_capture_can_ground_a_database_template(): payload,context=minimal() template(payload,context) payload['affected_scope']=[{'kind':'database','source_ref':'capture.json','evidence_refs':['E000001']}] context['local_context']={'capture.json':{'kind':'database'}} context['evidence']['E000001']['content']['items']=[{'query':{'kind':'database.rows'},'result':{'items':[{'row':{'id':1}}]}}] validate_request(payload,context)