Files
VianoBugAnalysis/tests/test_evidence_store.py

231 lines
12 KiB
Python

import copy
import json
from importlib.resources import files
import pytest
from jsonschema import Draft202012Validator
from viano_bug_analysis.budget import BudgetManager, BudgetError
from viano_bug_analysis.case import CaseBuilder
from viano_bug_analysis.evidence import EvidenceStore, EvidenceError, EvidenceLimits
from viano_bug_analysis.lifecycle import CheckpointStore, Workspace, State
from viano_bug_analysis.package import PackageReader, PackageValidator
from viano_bug_analysis.tools import InvestigationToolRouter
from test_investigation_tools import tool_case, batch
from test_case_builder import case_setup
from test_case_interpreter import built_case
from test_hypothesis_builder import interpreted_case
from test_budget_manager import hypotheses
from test_async_tests import setup as async_setup
CASE=("case-A","stable-package")
def prepare(tool_case, *, action="A1", text="activate", budget=None):
store=tool_case[0]
budget=budget or BudgetManager(store)
budget.initialize(*CASE)
reserved=budget.reserve(*CASE,"H1",action,"source.find_text")
request=batch()
request["requests"][0].update(action_id=action,request_id=action)
request["requests"][0]["arguments"]["text"]=text
result=InvestigationToolRouter().query(reserved.package,request)
return budget,request,result
def test_roundtrip_stable_ids_provenance_preservation_and_budget(tool_case,tmp_path):
budget,request,result=prepare(tool_case)
before=budget.snapshot(*CASE)
evidence=EvidenceStore(tool_case[0])
saved=evidence.store_tool_results(*CASE,"query1",request,result)
assert PackageValidator().validate(saved.package).valid
assert budget.snapshot(*CASE).state==before.state
with PackageReader(before.package) as old, PackageReader(saved.package) as reader:
record=json.loads(reader.read_bytes(saved.record_refs[0]))
content=json.loads(reader.read_bytes(record["content_ref"]))
assert record["id"]=="E000001" and record["duplicate_of"] is None
assert len(content["items"])==3
assert record["origin"]["position"]==0
assert reader.manifest["package_generation"]==old.manifest["package_generation"]+1
for name in old.files:
if name!="manifest.json": assert old.read_bytes(name)==reader.read_bytes(name)
assert evidence.store_tool_results(*CASE,"query1",request,result).reused
assert CaseBuilder(tool_case[0]).build(*CASE).reused
budget.record_evidence(*CASE,"H1","A1",saved.record_refs[0])
settled=budget.settle(*CASE,"H1","A1",semantic_score=100,reason="Relevant excerpt")
assert settled.state["accounts"]["H1"]["remaining"]==98
portable=CheckpointStore(Workspace(tmp_path/"portable"))
portable.intake(settled.package)
restored=EvidenceStore(portable).snapshot(*CASE)
assert restored.record_refs==saved.record_refs
assert BudgetManager(portable).snapshot(*CASE).state==settled.state
def test_metadata_changes_do_not_make_duplicates_novel(tool_case):
budget,request,result=prepare(tool_case)
evidence=EvidenceStore(tool_case[0])
first=evidence.store_tool_results(*CASE,"query1",request,result)
budget.record_evidence(*CASE,"H1","A1",first.record_refs[0])
budget.settle(*CASE,"H1","A1",semantic_score=100,reason="Relevant")
budget,request,result=prepare(tool_case,action="A2",budget=budget)
second=evidence.store_tool_results(*CASE,"query2",request,result)
with PackageReader(second.package) as reader:
one=json.loads(reader.read_bytes(first.record_refs[0]))
two=json.loads(reader.read_bytes(second.record_refs[0]))
assert two["id"]=="E000002" and two["duplicate_of"]==one["id"]
assert two["content_ref"]==one["content_ref"]
budget.record_evidence(*CASE,"H1","A2",second.record_refs[0])
settled=budget.settle(*CASE,"H1","A2",semantic_score=100,reason="Duplicate")
assert settled.state["accounts"]["H1"]["actions"]["A2"]["reward"]==0
def test_empty_results_remain_auditable_without_novelty_reward(tool_case):
budget,request,result=prepare(tool_case,text="not-present")
saved=EvidenceStore(tool_case[0]).store_tool_results(*CASE,"empty",request,result)
budget.record_evidence(*CASE,"H1","A1",saved.record_refs[0])
settled=budget.settle(*CASE,"H1","A1",semantic_score=100,reason="Scoped absence")
action=settled.state["accounts"]["H1"]["actions"]["A1"]
assert action["novelty_score"]==action["reward"]==0
def test_overlapping_excerpts_only_reward_new_items(tool_case):
store=tool_case[0]; budget=BudgetManager(store); budget.initialize(*CASE)
first=budget.reserve(*CASE,"H1","A1","source.find_text")
request=batch();request["requests"][0]["arguments"]["limit"]=1
result=InvestigationToolRouter().query(first.package,request)
evidence=EvidenceStore(store)
saved=evidence.store_tool_results(*CASE,"one-line",request,result)
budget.record_evidence(*CASE,"H1","A1",saved.record_refs[0])
budget.settle(*CASE,"H1","A1",semantic_score=100,reason="First line")
second=budget.reserve(*CASE,"H1","A2","source.read_context")
request=batch("source.read_context",{"source_ref":"tool/code.cpp","start_line":1,"end_line":2})
request["requests"][0].update(action_id="A2")
result=InvestigationToolRouter().query(second.package,request)
saved=evidence.store_tool_results(*CASE,"overlap",request,result)
with PackageReader(saved.package) as reader:
record=json.loads(reader.read_bytes(saved.record_refs[0]))
assert record["novelty_score"]==50 and len(record["novel_atoms"])==1
budget.record_evidence(*CASE,"H1","A2",saved.record_refs[0])
settled=budget.settle(*CASE,"H1","A2",semantic_score=100,reason="Only one new line")
assert settled.state["accounts"]["H1"]["actions"]["A2"]["reward"]==4
def test_history_readable_after_reopen_and_ids_not_reset(tool_case):
import asyncio
from viano_bug_analysis.analysis import CaseInterpreter,HypothesisBuilder
from test_case_interpreter import gateway,FixtureAdapter
from test_hypothesis_builder import ProposalAdapter
budget,request,result=prepare(tool_case)
store=tool_case[0];evidence=EvidenceStore(store)
first=evidence.store_tool_results(*CASE,"query1",request,result)
budget.record_evidence(*CASE,"H1","A1",first.record_refs[0])
budget.settle(*CASE,"H1","A1",semantic_score=100,reason="Known excerpt")
store.advance(*CASE,State.FAILED_TO_ANALYZE,step_id="review",reason="Review")
store.advance(*CASE,State.REOPENED,step_id="reopen")
assert evidence.snapshot(*CASE).record_refs==first.record_refs
CaseBuilder(store).build(*CASE)
asyncio.run(CaseInterpreter(store,gateway(FixtureAdapter())).interpret(*CASE))
asyncio.run(HypothesisBuilder(store,gateway(ProposalAdapter())).build(*CASE))
budget=BudgetManager(store);budget.initialize(*CASE)
reserved=budget.reserve(*CASE,"H1","A1","source.find_text")
request=batch();request["analysis_revision"]=2
result=InvestigationToolRouter().query(reserved.package,request)
second=evidence.store_tool_results(*CASE,"query1",request,result)
with PackageReader(second.package) as reader:
record=json.loads(reader.read_bytes(second.record_refs[0]))
assert record["id"]=="E000002" and record["duplicate_of"]=="E000001"
budget.record_evidence(*CASE,"H1","A1",second.record_refs[0])
settled=budget.settle(*CASE,"H1","A1",semantic_score=100,reason="Old evidence")
assert settled.state["accounts"]["H1"]["rewarded"]==0
@pytest.mark.parametrize("change",[
lambda r:r.update(package_generation=0),
lambda r:r["results"][0]["data"]["matches"][0].update(text="Invented code"),
lambda r:r["results"][0].update(hypothesis_id="H2"),
lambda r:r.update(batch_sha256="0"*64),
])
def test_forged_results_rejected_atomically(tool_case,change):
budget,request,result=prepare(tool_case)
before=budget.snapshot(*CASE)
change(result)
with pytest.raises(EvidenceError):
EvidenceStore(tool_case[0]).store_tool_results(*CASE,"bad",request,result)
assert budget.snapshot(*CASE).package==before.package
def test_conflicting_operation_and_missing_reservation(tool_case):
store=tool_case[0]
budget,request,result=prepare(tool_case)
evidence=EvidenceStore(store)
saved=evidence.store_tool_results(*CASE,"query1",request,result)
changed=copy.deepcopy(result); changed["results"][0]["data"]["total_matches"]=999
with pytest.raises(EvidenceError,match="EVIDENCE_STEP_CONFLICT"):
evidence.store_tool_results(*CASE,"query1",request,changed)
assert evidence.snapshot(*CASE).package==saved.package
fresh=copy.deepcopy(request); fresh["requests"][0]["action_id"]="unreserved"
result=InvestigationToolRouter().query(saved.package,fresh)
with pytest.raises(EvidenceError,match="EVIDENCE_RESERVATION"):
evidence.store_tool_results(*CASE,"unreserved",fresh,result)
def test_deleted_index_cannot_reset_ids(tool_case):
budget,request,result=prepare(tool_case)
evidence=EvidenceStore(tool_case[0])
evidence.store_tool_results(*CASE,"query1",request,result)
tool_case[0].advance(*CASE,State.GENERATING_HYPOTHESES,step_id="deleted-index",artifacts={"evidence_index":None})
with pytest.raises(EvidenceError,match="EVIDENCE_POINTER_CHANGED"):
evidence.snapshot(*CASE)
def test_budget_rejects_wrong_action_evidence_binding(tool_case):
budget,request,result=prepare(tool_case)
saved=EvidenceStore(tool_case[0]).store_tool_results(*CASE,"query1",request,result)
with pytest.raises(BudgetError):
budget.record_evidence(*CASE,"H2","A1",saved.record_refs[0])
def test_limits_and_invalid_operation_ids_do_not_publish(tool_case):
budget,request,result=prepare(tool_case)
before=budget.snapshot(*CASE)
for limits in [EvidenceLimits(max_json_bytes=10),EvidenceLimits(max_total_bytes=10)]:
with pytest.raises(EvidenceError):
EvidenceStore(tool_case[0],limits).store_tool_results(*CASE,"query1",request,result)
for operation in [None,"",True]:
with pytest.raises(EvidenceError):
EvidenceStore(tool_case[0]).store_tool_results(*CASE,operation,request,result)
assert budget.snapshot(*CASE).package==before.package
def test_publication_index_crash_replay_does_not_create_new_ids(tool_case,monkeypatch):
budget,request,result=prepare(tool_case)
store=tool_case[0]; evidence=EvidenceStore(store)
original=store._index
def fail(path,manifest):
if manifest["artifacts"].get("evidence_index"):
raise OSError("Simulated crash after archive publication")
original(path,manifest)
monkeypatch.setattr(store,"_index",fail)
with pytest.raises(EvidenceError): evidence.store_tool_results(*CASE,"query1",request,result)
monkeypatch.setattr(store,"_index",original)
restored=evidence.store_tool_results(*CASE,"query1",request,result)
assert restored.reused and len(evidence.snapshot(*CASE).record_refs)==1
def test_confirmed_test_feedback_evidence_does_not_refill_budget(hypotheses):
store,budget,coordinator,before,request,feedback=async_setup(hypotheses)
coordinator.submit(request)
resumed=coordinator.confirm_feedback(feedback)
evidence=EvidenceStore(store)
saved=evidence.store_feedback(*CASE,"test-result",resumed.feedback_ref)
assert budget.snapshot(*CASE).state==before.state
budget.record_evidence(*CASE,"H1","test-action",saved.record_refs[0])
settled=budget.settle(*CASE,"H1","test-action",semantic_score=50,reason="Useful observation")
assert settled.state["accounts"]["H1"]["remaining"]==76
assert evidence.snapshot(*CASE).record_refs==saved.record_refs
@pytest.mark.parametrize("name",["evidence-content","evidence-record","evidence-index"])
def test_evidence_schemas_valid(name):
Draft202012Validator.check_schema(json.loads(files("viano_bug_analysis.schemas").joinpath(name+'-v1.schema.json').read_text()))