| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886 |
- import json
- import tempfile
- import unittest
- from pydantic import ValidationError
- from cyber_agent.core.validation import (
- LLMValidator,
- ScopeValidationResult,
- ValidationCheck,
- ValidationPolicy,
- ValidatorSettings,
- aggregate_validation_results,
- build_validation_packet,
- parse_scope_validation_result,
- scope_validation_error,
- persist_validation_policy,
- require_validation_policy,
- )
- from cyber_agent.core.artifacts import MaterialIssue
- from cyber_agent.core.validator_web import (
- ValidatorToolLimits,
- ValidatorToolSession,
- )
- from cyber_agent.trace.models import Message, Trace
- from cyber_agent.trace.store import FileSystemTraceStore
- BRIEF = {
- "objective": "verify one claim",
- "reason": "parent needs a checked fact",
- "completion_criteria": ["the claim is checked"],
- "expected_outputs": ["one conclusion"],
- "constraints": [],
- "validation_scopes": [],
- }
- ANCHOR = {
- "objective": "publish a reliable answer",
- "completion_criteria": ["all facts are supported"],
- "constraints": ["do not invent sources"],
- }
- def plan_for(*, scopes=None, root=False, head=2):
- brief = dict(BRIEF, validation_scopes=scopes or [])
- policy = ValidationPolicy()
- return policy.compile_plan(
- task_brief=brief,
- task_brief_version=1,
- root_task_anchor=ANCHOR,
- task_report={"summary": "done"},
- candidate_output="candidate" if root else None,
- evaluated_head_sequence=head,
- materials=[],
- material_issues=[],
- model_by_scope={scope: "fake" for scope in (
- "evidence", "hypothesis", "output", "task", "root"
- )},
- root=root,
- )
- def passed_scope_response(plan, scope, *, evidence_refs=None):
- return json.dumps({
- "scope": scope,
- "outcome": "passed",
- "checks": [
- {
- "check_id": check.check_id,
- "status": "passed",
- "evidence_refs": list(evidence_refs or []),
- "issue": None,
- }
- for check in plan.checks_for_scope(scope)
- ],
- "reason": "every planned check passed",
- "retry_from": None,
- })
- class ValidationPolicyTest(unittest.TestCase):
- def test_policy_version_invalidates_old_checkpoints(self):
- self.assertEqual("recursive-validator-2.2", ValidationPolicy().policy_version)
- def test_scope_order_and_mandatory_task_are_stable(self):
- plan = plan_for(scopes=["output", "evidence", "output"])
- self.assertEqual(["evidence", "output", "task"], plan.effective_scopes)
- self.assertTrue(any(item.check_id == "task.criterion.1" for item in plan.checks))
- same = plan_for(scopes=["evidence", "output"])
- self.assertEqual(plan.plan_hash, same.plan_hash)
- changed = plan_for(scopes=["evidence", "output"], head=3)
- self.assertNotEqual(plan.plan_hash, changed.plan_hash)
- def test_root_is_framework_owned(self):
- self.assertEqual(["root"], plan_for(root=True).effective_scopes)
- with self.assertRaises(ValueError):
- ValidationPolicy().compile_plan(
- task_brief=dict(BRIEF, validation_scopes=["root"]),
- task_brief_version=1,
- root_task_anchor=ANCHOR,
- task_report={},
- candidate_output=None,
- evaluated_head_sequence=1,
- materials=[],
- material_issues=[],
- model_by_scope={},
- root=False,
- )
- def test_parser_binds_plan_checks_and_framework_ids(self):
- plan = plan_for()
- result = parse_scope_validation_result(
- passed_scope_response(plan, "task"),
- plan=plan,
- expected_scope="task",
- validator_trace_id="validator-1",
- )
- self.assertEqual("validator-1", result.validator_trace_id)
- forged = json.loads(passed_scope_response(plan, "task"))
- forged["checks"].append({
- "check_id": "task.forged",
- "status": "passed",
- "evidence_refs": [],
- "issue": None,
- })
- with self.assertRaisesRegex(ValueError, "do not match plan"):
- parse_scope_validation_result(
- json.dumps(forged),
- plan=plan,
- expected_scope="task",
- validator_trace_id="validator-1",
- )
- def test_evidence_pass_requires_opened_source_ids(self):
- plan = plan_for(scopes=["evidence"])
- content = passed_scope_response(plan, "evidence", evidence_refs=["src-1"])
- with self.assertRaisesRegex(ValueError, "opened"):
- parse_scope_validation_result(
- content,
- plan=plan,
- expected_scope="evidence",
- validator_trace_id="validator",
- opened_source_ids=set(),
- )
- result = parse_scope_validation_result(
- content,
- plan=plan,
- expected_scope="evidence",
- validator_trace_id="validator",
- opened_source_ids={"src-1"},
- )
- self.assertEqual("passed", result.outcome)
- def test_aggregate_priority_and_retry_are_framework_owned(self):
- plan = plan_for(scopes=["evidence", "output"])
- results = []
- for scope, outcome in zip(plan.effective_scopes, ["failed", "unknown", "passed"]):
- results.append(ScopeValidationResult(
- validator_trace_id=f"validator-{scope}",
- scope=scope,
- outcome=outcome,
- checks=[ValidationCheck(
- check_id=check.check_id,
- status=outcome,
- issue=None if outcome == "passed" else f"{scope} gap",
- ) for check in plan.checks_for_scope(scope)],
- reason=f"{scope} result",
- retry_from=(
- None if outcome == "passed"
- else "evidence" if scope == "evidence"
- else "output" if scope == "output"
- else "task_definition"
- ),
- plan_hash=plan.plan_hash,
- ))
- aggregate = aggregate_validation_results(
- evaluated_trace_id="child",
- plan=plan,
- scope_results=results,
- )
- self.assertEqual("failed", aggregate.outcome)
- self.assertEqual("evidence", aggregate.retry_from)
- errored = list(results)
- errored[-1] = scope_validation_error(
- validator_trace_id="validator-task",
- scope="task",
- plan=plan,
- reason="invalid JSON",
- )
- aggregate = aggregate_validation_results(
- evaluated_trace_id="child",
- plan=plan,
- scope_results=errored,
- )
- self.assertEqual("error", aggregate.outcome)
- self.assertIsNone(aggregate.retry_from)
- def test_packet_keeps_contract_and_newest_main_path(self):
- trajectory = [
- {
- "sequence": index,
- "role": "tool",
- "name": "read_file",
- "content": f"marker-{index}-" + ("x" * 300),
- }
- for index in range(1, 8)
- ]
- trajectory.append({
- "sequence": 99,
- "role": "assistant",
- "content": "side-secret",
- "branch_type": "compression",
- })
- packet = build_validation_packet(
- validation_scope="task",
- validation_plan=plan_for(),
- task_brief=BRIEF,
- task_report={"summary": "report-kept"},
- trajectory=trajectory,
- max_chars=2_500,
- )
- self.assertLessEqual(len(packet), 2_500)
- self.assertIn("report-kept", packet)
- self.assertIn("marker-7", packet)
- self.assertNotIn("marker-1", packet)
- self.assertNotIn("side-secret", packet)
- def test_each_scope_has_fixed_rubric_and_task_has_dynamic_checks(self):
- plan = plan_for(scopes=["evidence", "hypothesis", "output"])
- for scope in ("evidence", "hypothesis", "output", "task"):
- self.assertTrue(any(
- item.scope == scope and item.check_id.startswith(f"{scope}.policy.")
- for item in plan.checks
- ))
- self.assertTrue(any(
- item.check_id == "task.criterion.1"
- and item.method == "llm"
- for item in plan.checks
- ))
- self.assertTrue(any(
- item.check_id == "task.expected_output.1"
- and item.method == "llm"
- for item in plan.checks
- ))
- self.assertNotIn(
- "deterministic_and_llm",
- {item.method for item in plan.checks},
- )
- root_plan = plan_for(root=True)
- self.assertTrue(all(
- item.method == "llm"
- for item in root_plan.checks
- if item.check_id.startswith(("root.criterion.", "root.constraint."))
- ))
- def test_only_material_resolution_failures_compile_as_deterministic(self):
- issue = MaterialIssue(
- artifact_id="missing-output",
- outcome="failed",
- reason="artifact does not exist",
- scopes=["output"],
- )
- plan = ValidationPolicy().compile_plan(
- task_brief=dict(BRIEF, validation_scopes=["output"]),
- task_brief_version=1,
- root_task_anchor=ANCHOR,
- task_report={"summary": "done"},
- candidate_output=None,
- evaluated_head_sequence=1,
- materials=[],
- material_issues=[issue],
- model_by_scope={"output": "fake", "task": "fake"},
- root=False,
- )
- deterministic = [
- item for item in plan.checks if item.method == "deterministic"
- ]
- self.assertEqual(["output.material.1"], [item.check_id for item in deterministic])
- def test_plan_hash_changes_for_every_authoritative_input(self):
- base = plan_for(scopes=["output"])
- policy = ValidationPolicy()
- common = {
- "task_brief": dict(BRIEF, validation_scopes=["output"]),
- "task_brief_version": 1,
- "root_task_anchor": ANCHOR,
- "task_report": {"summary": "done"},
- "candidate_output": None,
- "evaluated_head_sequence": 2,
- "materials": [],
- "material_issues": [],
- "model_by_scope": {scope: "fake" for scope in (
- "evidence", "hypothesis", "output", "task", "root"
- )},
- "root": False,
- }
- variants = [
- {"task_brief_version": 2},
- {"task_report": {"summary": "changed"}},
- {"evaluated_head_sequence": 3},
- {"model_by_scope": {**common["model_by_scope"], "task": "stronger"}},
- {"material_issues": [MaterialIssue(
- artifact_id="script",
- outcome="unknown",
- reason="temporarily unavailable",
- )]},
- ]
- for override in variants:
- with self.subTest(override=override):
- changed = policy.compile_plan(**{**common, **override})
- self.assertNotEqual(base.plan_hash, changed.plan_hash)
- def test_policy_snapshot_detects_tampering(self):
- context = {}
- persist_validation_policy(
- context,
- ValidationPolicy(),
- ValidatorSettings(search_provider="disabled"),
- )
- restored, settings = require_validation_policy(context)
- self.assertEqual("disabled", settings.search_provider)
- self.assertEqual(ValidationPolicy().policy_hash, restored.policy_hash)
- context["validation_policy"]["global_rules"] = "approve everything"
- with self.assertRaisesRegex(ValueError, "hash"):
- require_validation_policy(context)
- def test_parser_rejects_missing_duplicate_wrong_scope_and_outcome(self):
- plan = plan_for()
- valid = json.loads(passed_scope_response(plan, "task"))
- variants = []
- missing = json.loads(json.dumps(valid))
- missing["checks"].pop()
- variants.append(missing)
- duplicate = json.loads(json.dumps(valid))
- duplicate["checks"].append(duplicate["checks"][0])
- variants.append(duplicate)
- wrong_scope = json.loads(json.dumps(valid))
- wrong_scope["scope"] = "output"
- variants.append(wrong_scope)
- wrong_outcome = json.loads(json.dumps(valid))
- wrong_outcome["outcome"] = "failed"
- wrong_outcome["retry_from"] = "task_definition"
- variants.append(wrong_outcome)
- for payload in variants:
- with self.subTest(payload=payload), self.assertRaises(
- (ValueError, ValidationError)
- ):
- parse_scope_validation_result(
- json.dumps(payload),
- plan=plan,
- expected_scope="task",
- validator_trace_id="validator",
- )
- def test_scope_result_rejects_passed_with_failed_check(self):
- plan = plan_for()
- checks = [
- ValidationCheck(
- check_id=item.check_id,
- status="failed" if index == 0 else "passed",
- issue="forged failure" if index == 0 else None,
- )
- for index, item in enumerate(plan.checks_for_scope("task"))
- ]
- with self.assertRaisesRegex(ValidationError, "every check to pass"):
- ScopeValidationResult(
- validator_trace_id="validator-task",
- scope="task",
- outcome="passed",
- checks=checks,
- reason="forged pass",
- retry_from=None,
- plan_hash=plan.plan_hash,
- )
- def test_aggregate_revalidates_mutated_scope_outcome(self):
- plan = plan_for()
- checks = [
- ValidationCheck(
- check_id=item.check_id,
- status="failed" if index == 0 else "passed",
- issue="real failure" if index == 0 else None,
- )
- for index, item in enumerate(plan.checks_for_scope("task"))
- ]
- forged = ScopeValidationResult(
- validator_trace_id="validator-task",
- scope="task",
- outcome="failed",
- checks=checks,
- reason="real failure",
- retry_from="task_definition",
- plan_hash=plan.plan_hash,
- )
- # 模拟持久化边界之外绕过 Pydantic 的内存篡改;聚合仍必须失败关闭。
- object.__setattr__(forged, "outcome", "passed")
- object.__setattr__(forged, "retry_from", None)
- with self.assertRaisesRegex(ValueError, "every check to pass"):
- aggregate_validation_results(
- evaluated_trace_id="child",
- plan=plan,
- scope_results=[forged],
- )
- def test_unknown_aggregate_preserves_recoverable_retry(self):
- plan = plan_for()
- scope = ScopeValidationResult(
- validator_trace_id="validator-task",
- scope="task",
- outcome="unknown",
- checks=[ValidationCheck(
- check_id=check.check_id,
- status="unknown",
- issue="required material is unavailable",
- ) for check in plan.checks_for_scope("task")],
- reason="insufficient material",
- retry_from="task_definition",
- plan_hash=plan.plan_hash,
- )
- aggregate = aggregate_validation_results(
- evaluated_trace_id="child",
- plan=plan,
- scope_results=[scope],
- )
- self.assertEqual("unknown", aggregate.outcome)
- self.assertEqual("task_definition", aggregate.retry_from)
- class FakeLLM:
- def __init__(self, responses):
- self.responses = list(responses)
- self.calls = []
- async def __call__(self, **kwargs):
- self.calls.append(kwargs)
- response = self.responses.pop(0)
- if isinstance(response, Exception):
- raise response
- return response
- def response(content="", tool_calls=None):
- return {
- "content": content,
- "tool_calls": tool_calls,
- "prompt_tokens": 10,
- "completion_tokens": 5,
- "cost": 0.01,
- "finish_reason": "tool_calls" if tool_calls else "stop",
- }
- class LLMValidatorTest(unittest.IsolatedAsyncioTestCase):
- async def asyncSetUp(self):
- self.temp = tempfile.TemporaryDirectory()
- self.store = FileSystemTraceStore(self.temp.name)
- self.evaluated = Trace(
- trace_id="root@delegate-child",
- mode="agent",
- task="child task",
- uid="user-1",
- model="fake",
- current_goal_id="1",
- context={
- "agent_mode": "recursive",
- "agent_mode_revision": 2,
- "root_trace_id": "root",
- "agent_depth": 1,
- },
- )
- await self.store.create_trace(self.evaluated)
- self.trajectory = [Message.create(
- trace_id=self.evaluated.trace_id,
- role="tool",
- sequence=1,
- content={"tool_name": "read_file", "result": "actual output"},
- )]
- async def asyncTearDown(self):
- self.temp.cleanup()
- async def test_task_scope_is_one_tool_free_call(self):
- plan = plan_for()
- llm = FakeLLM([response(passed_scope_response(plan, "task"))])
- validator = LLMValidator(
- llm_call=llm,
- trace_store=self.store,
- policy=ValidationPolicy(),
- )
- run = await validator.validate_plan(
- evaluated_trace=self.evaluated,
- trajectory=self.trajectory,
- plan=plan,
- root_task_anchor=ANCHOR,
- task_brief=BRIEF,
- task_report={"summary": "done"},
- candidate_output=None,
- materials=[],
- material_issues=[],
- model_by_scope={"task": "fake"},
- )
- self.assertEqual("passed", run.result.outcome, run.result.model_dump())
- self.assertEqual(1, len(llm.calls))
- self.assertEqual([], llm.calls[0]["tools"])
- trace = await self.store.get_trace(run.trace_id)
- self.assertEqual("completed", trace.status)
- self.assertEqual("task", trace.context["validation_scope"])
- async def test_evidence_scope_runs_private_search_and_open_loop(self):
- plan = plan_for(scopes=["evidence"])
- class Provider:
- async def search(self, query, max_results):
- return [{
- "title": "Official",
- "link": "https://example.com/fact",
- "snippet": "official page",
- }]
- async def page_fetcher(url, resolver=None):
- del resolver
- return {
- "source_id": "src-official",
- "url": url,
- "final_url": url,
- "title": "Official",
- "content_type": "text/plain",
- "retrieved_at": "2026-01-01T00:00:00+00:00",
- "content_sha256": "b" * 64,
- "text": "The official figure is 12%.",
- "truncated": False,
- "untrusted_material": True,
- }
- def session_factory(scope, allowed_urls, trace_id):
- del scope, trace_id
- return ValidatorToolSession(
- provider=Provider(),
- allowed_urls=allowed_urls,
- limits=ValidatorToolLimits(5, 10, 15),
- page_fetcher=page_fetcher,
- )
- llm = FakeLLM([
- response(tool_calls=[{
- "id": "search-1",
- "type": "function",
- "function": {
- "name": "validator_web_search",
- "arguments": json.dumps({"query": "official figure"}),
- },
- }]),
- response(tool_calls=[{
- "id": "open-1",
- "type": "function",
- "function": {
- "name": "validator_open_url",
- "arguments": json.dumps({"url": "https://example.com/fact"}),
- },
- }]),
- response(passed_scope_response(
- plan,
- "evidence",
- evidence_refs=["src-official"],
- )),
- response(passed_scope_response(plan, "task")),
- ])
- validator = LLMValidator(
- llm_call=llm,
- trace_store=self.store,
- policy=ValidationPolicy(),
- tool_session_factory=session_factory,
- )
- run = await validator.validate_plan(
- evaluated_trace=self.evaluated,
- trajectory=self.trajectory,
- plan=plan,
- root_task_anchor=ANCHOR,
- task_brief=dict(BRIEF, validation_scopes=["evidence"]),
- task_report={"summary": "done"},
- candidate_output=None,
- materials=[],
- material_issues=[],
- model_by_scope={"evidence": "fake", "task": "fake"},
- )
- self.assertEqual("passed", run.result.outcome, run.result.model_dump())
- self.assertEqual(2, len(run.trace_ids))
- evidence_messages = await self.store.get_trace_messages(run.trace_ids[0])
- self.assertEqual(
- ["system", "user", "assistant", "tool", "assistant", "tool", "assistant"],
- [item.role for item in evidence_messages],
- )
- self.assertEqual(
- {"validator_web_search", "validator_open_url"},
- {
- item["function"]["name"]
- for item in (await self.store.get_trace(run.trace_ids[0])).tools
- },
- )
- async def test_invalid_output_fails_without_format_correction(self):
- plan = plan_for()
- llm = FakeLLM([response("not-json")])
- validator = LLMValidator(
- llm_call=llm,
- trace_store=self.store,
- policy=ValidationPolicy(),
- )
- run = await validator.validate_plan(
- evaluated_trace=self.evaluated,
- trajectory=[],
- plan=plan,
- root_task_anchor=ANCHOR,
- task_brief=BRIEF,
- task_report={"summary": "done"},
- candidate_output=None,
- materials=[],
- material_issues=[],
- model_by_scope={"task": "fake"},
- )
- self.assertEqual("error", run.result.outcome)
- self.assertEqual(1, len(llm.calls))
- scope_result = run.result.scope_results[0]
- self.assertEqual(
- {item.check_id for item in plan.checks_for_scope("task")},
- {item.check_id for item in scope_result.checks},
- )
- self.assertTrue(all(item.status == "unknown" for item in scope_result.checks))
- async def test_input_packet_failure_uses_only_all_planned_check_ids(self):
- plan = plan_for()
- llm = FakeLLM([])
- validator = LLMValidator(
- llm_call=llm,
- trace_store=self.store,
- policy=ValidationPolicy(),
- max_input_chars=8,
- )
- run = await validator.validate_plan(
- evaluated_trace=self.evaluated,
- trajectory=self.trajectory,
- plan=plan,
- root_task_anchor=ANCHOR,
- task_brief=BRIEF,
- task_report={"summary": "done"},
- candidate_output=None,
- materials=[],
- material_issues=[],
- model_by_scope={"task": "fake"},
- )
- self.assertEqual([], llm.calls)
- self.assertEqual("unknown", run.result.outcome)
- scope_result = run.result.scope_results[0]
- self.assertEqual(
- [item.check_id for item in plan.checks_for_scope("task")],
- [item.check_id for item in scope_result.checks],
- )
- self.assertTrue(all(item.status == "unknown" for item in scope_result.checks))
- self.assertFalse(any(
- item.check_id.endswith("input_limit") for item in scope_result.checks
- ))
- async def test_deterministic_material_failure_skips_all_llm_calls(self):
- plan = ValidationPolicy().compile_plan(
- task_brief=dict(BRIEF, validation_scopes=["output"]),
- task_brief_version=1,
- root_task_anchor=ANCHOR,
- task_report={"summary": "claimed output"},
- candidate_output=None,
- evaluated_head_sequence=1,
- materials=[],
- material_issues=[MaterialIssue(
- artifact_id="script:missing",
- outcome="failed",
- reason="artifact does not exist",
- )],
- model_by_scope={"output": "fake", "task": "fake"},
- root=False,
- )
- llm = FakeLLM([])
- validator = LLMValidator(
- llm_call=llm,
- trace_store=self.store,
- policy=ValidationPolicy(),
- )
- run = await validator.validate_plan(
- evaluated_trace=self.evaluated,
- trajectory=[],
- plan=plan,
- root_task_anchor=ANCHOR,
- task_brief=dict(BRIEF, validation_scopes=["output"]),
- task_report={"summary": "claimed output"},
- candidate_output=None,
- materials=[],
- material_issues=[MaterialIssue(
- artifact_id="script:missing",
- outcome="failed",
- reason="artifact does not exist",
- )],
- model_by_scope={"output": "fake", "task": "fake"},
- )
- self.assertEqual("failed", run.result.outcome)
- self.assertEqual(2, len(run.trace_ids))
- self.assertEqual([], llm.calls)
- for trace_id in run.trace_ids:
- self.assertEqual("failed", (await self.store.get_trace(trace_id)).status)
- for result in run.result.scope_results:
- self.assertEqual(
- {item.check_id for item in plan.checks_for_scope(result.scope)},
- {item.check_id for item in result.checks},
- )
- self.assertEqual(1, sum(item.status == "failed" for item in result.checks))
- async def test_material_failure_only_skips_affected_scopes(self):
- brief = dict(BRIEF, validation_scopes=["hypothesis", "output"])
- issue = MaterialIssue(
- artifact_id="script:missing",
- outcome="failed",
- reason="script does not exist",
- scopes=["output", "task", "root"],
- )
- plan = ValidationPolicy().compile_plan(
- task_brief=brief,
- task_brief_version=1,
- root_task_anchor=ANCHOR,
- task_report={"summary": "claimed output"},
- candidate_output=None,
- evaluated_head_sequence=1,
- materials=[],
- material_issues=[issue],
- model_by_scope={
- "hypothesis": "fake", "output": "fake", "task": "fake",
- },
- root=False,
- )
- llm = FakeLLM([response(passed_scope_response(plan, "hypothesis"))])
- validator = LLMValidator(
- llm_call=llm,
- trace_store=self.store,
- policy=ValidationPolicy(),
- )
- run = await validator.validate_plan(
- evaluated_trace=self.evaluated,
- trajectory=[],
- plan=plan,
- root_task_anchor=ANCHOR,
- task_brief=brief,
- task_report={"summary": "claimed output"},
- candidate_output=None,
- materials=[],
- material_issues=[issue],
- model_by_scope={
- "hypothesis": "fake", "output": "fake", "task": "fake",
- },
- )
- self.assertEqual(1, len(llm.calls))
- self.assertEqual(
- ["passed", "failed", "failed"],
- [item.outcome for item in run.result.scope_results],
- )
- for result in run.result.scope_results:
- self.assertEqual(
- {item.check_id for item in plan.checks_for_scope(result.scope)},
- {item.check_id for item in result.checks},
- )
- async def test_matching_scope_checkpoint_is_not_run_again(self):
- plan = plan_for(scopes=["output"])
- output_result = ScopeValidationResult(
- validator_trace_id="existing-validator-output",
- scope="output",
- outcome="passed",
- checks=[ValidationCheck(
- check_id=item.check_id,
- status="passed",
- ) for item in plan.checks_for_scope("output")],
- reason="already checked",
- retry_from=None,
- plan_hash=plan.plan_hash,
- )
- llm = FakeLLM([response(passed_scope_response(plan, "task"))])
- validator = LLMValidator(
- llm_call=llm,
- trace_store=self.store,
- policy=ValidationPolicy(),
- )
- run = await validator.validate_plan(
- evaluated_trace=self.evaluated,
- trajectory=[],
- plan=plan,
- root_task_anchor=ANCHOR,
- task_brief=dict(BRIEF, validation_scopes=["output"]),
- task_report={"summary": "done"},
- candidate_output=None,
- materials=[],
- material_issues=[],
- model_by_scope={"output": "fake", "task": "fake"},
- resume_scope_results=[output_result],
- )
- self.assertEqual("passed", run.result.outcome)
- self.assertEqual(1, len(llm.calls))
- self.assertEqual(
- ["existing-validator-output", run.trace_ids[1]],
- run.trace_ids,
- )
- async def test_checkpoint_revalidates_mutated_scope_outcome(self):
- plan = plan_for()
- checks = [
- ValidationCheck(
- check_id=item.check_id,
- status="failed" if index == 0 else "passed",
- issue="real failure" if index == 0 else None,
- )
- for index, item in enumerate(plan.checks_for_scope("task"))
- ]
- forged = ScopeValidationResult(
- validator_trace_id="existing-validator-task",
- scope="task",
- outcome="failed",
- checks=checks,
- reason="real failure",
- retry_from="task_definition",
- plan_hash=plan.plan_hash,
- )
- object.__setattr__(forged, "outcome", "passed")
- object.__setattr__(forged, "retry_from", None)
- llm = FakeLLM([])
- validator = LLMValidator(
- llm_call=llm,
- trace_store=self.store,
- policy=ValidationPolicy(),
- )
- with self.assertRaisesRegex(ValueError, "every check to pass"):
- await validator.validate_plan(
- evaluated_trace=self.evaluated,
- trajectory=[],
- plan=plan,
- root_task_anchor=ANCHOR,
- task_brief=BRIEF,
- task_report={"summary": "done"},
- candidate_output=None,
- materials=[],
- material_issues=[],
- model_by_scope={"task": "fake"},
- resume_scope_results=[forged],
- )
- self.assertEqual([], llm.calls)
- async def test_task_scope_forged_private_tool_is_an_error(self):
- plan = plan_for()
- llm = FakeLLM([response(tool_calls=[{
- "id": "forged",
- "type": "function",
- "function": {
- "name": "validator_web_search",
- "arguments": json.dumps({"query": "should not run"}),
- },
- }])])
- validator = LLMValidator(
- llm_call=llm,
- trace_store=self.store,
- policy=ValidationPolicy(),
- )
- run = await validator.validate_plan(
- evaluated_trace=self.evaluated,
- trajectory=[],
- plan=plan,
- root_task_anchor=ANCHOR,
- task_brief=BRIEF,
- task_report={"summary": "done"},
- candidate_output=None,
- materials=[],
- material_issues=[],
- model_by_scope={"task": "fake"},
- )
- self.assertEqual("error", run.result.outcome)
- self.assertIn("unavailable tool", run.result.issues[0])
- if __name__ == "__main__":
- unittest.main()
|