Allow idempotent reconfirmation of resolved investigation questions
This commit is contained in:
1 parent
089ac3ac5b
commit
02a7364c58
3 files changed
+54
-2
No files matched your search
@@ -205,3 +205,10 @@ crashes after evidence publication, external test confirmation and byte-preservi
|
||||
output. The BUG-20 integration uses actual wowdiag/wowsrc data and simulated model
|
||||
decisions; it tests orchestration, not real model reasoning or discovery of the
|
||||
known fix. No paid/cloud model is called by these tests.
|
||||
|
||||
Question resolution is idempotent: reconfirming an already resolved question
|
||||
does not invalidate an otherwise valid decision and needs no new evidence.
|
||||
Resolving any still-open question requires evidence references. Unknown question
|
||||
IDs invalidate the entire decision before assessments, tools or budget changes
|
||||
are applied. Mixing old confirmations with new resolutions follows the same
|
||||
rules; existing question history is preserved.
|
||||
@@ -420,8 +420,11 @@ class Investigator:
|
||||
if next_action["kind"]=="reject" and update["confidence"] in {"supported","strongly_supported"}:
|
||||
raise InvestigationError("INVESTIGATION_CONTRADICTORY_UPDATE")
|
||||
questions={q["id"]:q for q in h["questions"]}
|
||||
if (any(q not in questions or questions[q]["resolved"] for q in update["resolve_questions"])
|
||||
or (update["resolve_questions"] and not update["evidence_refs"])):raise InvestigationError("INVESTIGATION_QUESTION")
|
||||
# Reconfirming an already resolved question is an idempotent no-op.
|
||||
# Only a new resolution needs evidence; unknown IDs remain invalid.
|
||||
if (any(q not in questions for q in update["resolve_questions"])
|
||||
or (any(not questions[q]["resolved"] for q in update["resolve_questions"])
|
||||
and not update["evidence_refs"])):raise InvestigationError("INVESTIGATION_QUESTION")
|
||||
if len(h["questions"])+len(update["add_questions"])>state["policy"]["max_questions_per_hypothesis"]:
|
||||
raise InvestigationError("INVESTIGATION_QUESTION_LIMIT")
|
||||
if any(text.casefold().strip() in {q["text"].casefold().strip() for q in h["questions"]} for text in update["add_questions"]):
|
||||
|
||||
@@ -154,6 +154,48 @@ def test_question_updates_keep_history_and_require_evidence(tool_case):
|
||||
assert questions[0]["resolved"] and questions[1]["id"]=="Q0002" and not questions[1]["resolved"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('repeat_with_evidence',[False,True])
|
||||
def test_resolved_question_can_be_reconfirmed_without_blocking(tool_case,repeat_with_evidence):
|
||||
def change(reply,context):
|
||||
if not context['previous_result']:
|
||||
return
|
||||
h=context['hypotheses'][context['selected_hypothesis']]
|
||||
reply['update']['resolve_questions']=['Q0001']
|
||||
if not h['questions'][0]['resolved']:
|
||||
reply['update']['add_questions']=['A later question requiring evidence']
|
||||
reply['next']={'kind':'tool','operation':'source.read_context',
|
||||
'arguments':{'source_ref':'tool/code.cpp','start_line':1,'end_line':1}}
|
||||
elif repeat_with_evidence:
|
||||
# Mix the old confirmation with a genuinely new resolution.
|
||||
reply['update']['resolve_questions'].append('Q0002')
|
||||
else:
|
||||
reply['update']['evidence_refs']=[]
|
||||
result=run(Investigator(tool_case[0],gateway(LoopAdapter(change))))
|
||||
assert result.status=='COMPLETED'
|
||||
for h in state(result)['hypotheses'].values():
|
||||
assert h['invalid_responses']==0
|
||||
assert len(h['questions'])==2 and h['questions'][0]['resolved']
|
||||
assert h['questions'][1]['resolved'] is repeat_with_evidence
|
||||
snapshot=BudgetManager(tool_case[0]).snapshot(*CASE)
|
||||
assert all(len(a['actions'])==2 for a in snapshot.state['accounts'].values())
|
||||
assert PackageValidator().validate(result.package).valid
|
||||
|
||||
|
||||
@pytest.mark.parametrize('question',['Q0001','Q9999'])
|
||||
def test_new_or_unknown_resolution_rejected_atomically_without_evidence(tool_case,question):
|
||||
def change(reply,context):
|
||||
if context['previous_result']:
|
||||
reply['update']['resolve_questions']=[question]
|
||||
reply['update']['evidence_refs']=[]
|
||||
loop=Investigator(tool_case[0],gateway(LoopAdapter(change)))
|
||||
before=run(loop,steps=3)
|
||||
budget=BudgetManager(tool_case[0]).snapshot(*CASE).state
|
||||
result=asyncio.run(loop.step(*CASE))
|
||||
assert result.status=='REJECTED' and result.code=='INVESTIGATION_QUESTION'
|
||||
assert BudgetManager(tool_case[0]).snapshot(*CASE).state==budget
|
||||
assert state(result)['hypotheses']['H1']['questions']==state(before)['hypotheses']['H1']['questions']
|
||||
|
||||
|
||||
def test_state_pointer_deletion_and_policy_change_do_not_restart(tool_case):
|
||||
loop=Investigator(tool_case[0],gateway(LoopAdapter()))
|
||||
first=run(loop,steps=1)
|
||||
|
||||
Reference in new issue
Block a user