fix(agent-ops): 리뷰 확정 불일치를 다음 loop로 재분류한다
This commit is contained in:
parent
3a3c2aa530
commit
eba60a3db7
2 changed files with 149 additions and 3 deletions
|
|
@ -6081,10 +6081,22 @@ async def run_review(
|
|||
banner("작업완료", task.name, [f"archive={outcome['path']}", f"locator={locator}"])
|
||||
return outcome["path"]
|
||||
if outcome["verdict"] == "UNKNOWN" or outcome["state"] == "changed":
|
||||
raise RuntimeError(
|
||||
"official review가 판정과 다음 파일 상태를 materialize하지 않았다; "
|
||||
f"locator={locator}"
|
||||
# The review agent may have changed the active pair without
|
||||
# materializing a verdict/finalization in the same one-shot.
|
||||
# This is task-local review-finalization recovery: return normally
|
||||
# so dispatch_with_store clears this attempt and reclassifies only
|
||||
# this task on the next loop. Raising here incorrectly promoted a
|
||||
# recoverable review state to a dispatcher-wide exit-3 condition.
|
||||
banner(
|
||||
"디스패치추적대기",
|
||||
task.name,
|
||||
[
|
||||
"reason=review-finalization-recovery",
|
||||
"active PLAN/CODE_REVIEW pair를 다음 loop에서 재분류",
|
||||
f"locator={locator}",
|
||||
],
|
||||
)
|
||||
return None
|
||||
if outcome["state"] == "archived":
|
||||
raise RuntimeError(
|
||||
f"PASS가 아닌 review가 완료 archive로 이동했다: "
|
||||
|
|
|
|||
|
|
@ -4859,6 +4859,47 @@ class RepetitionLimitTest(unittest.IsolatedAsyncioTestCase):
|
|||
finally:
|
||||
store.close()
|
||||
|
||||
async def test_review_finalization_mismatch_is_reclassified_without_raising(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
(root / ".git").mkdir()
|
||||
task = TaskStageTest().make_task(root)
|
||||
store = dispatch.StateStore(root)
|
||||
locator = root / "locator.json"
|
||||
try:
|
||||
with (
|
||||
mock.patch.object(
|
||||
dispatch,
|
||||
"run_escalating",
|
||||
new=mock.AsyncMock(return_value=(True, locator)),
|
||||
),
|
||||
mock.patch.object(
|
||||
dispatch,
|
||||
"task_signature",
|
||||
side_effect=["before", "after"],
|
||||
),
|
||||
mock.patch.object(
|
||||
dispatch, "review_fingerprints", return_value=set()
|
||||
),
|
||||
mock.patch.object(
|
||||
dispatch,
|
||||
"review_outcome",
|
||||
return_value={
|
||||
"verdict": "UNKNOWN",
|
||||
"state": "changed",
|
||||
"path": str(task.directory),
|
||||
"review_log": "unknown",
|
||||
},
|
||||
),
|
||||
):
|
||||
result = await dispatch.run_review(root, store, task)
|
||||
|
||||
self.assertIsNone(result)
|
||||
self.assertIsNone(store.task_state(task).get("blocked"))
|
||||
self.assertEqual(store.task_state(task).get("review_no_progress"), 0)
|
||||
finally:
|
||||
store.close()
|
||||
|
||||
|
||||
class BlockerDrainTest(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_user_review_only_holds_its_dependency_closure(self):
|
||||
|
|
@ -8160,6 +8201,99 @@ class DispatcherConvergenceSimulationTest(unittest.IsolatedAsyncioTestCase):
|
|||
"final timeline\n",
|
||||
)
|
||||
|
||||
async def test_review_finalization_mismatch_keeps_dispatcher_running(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
workspace = Path(temporary)
|
||||
(workspace / ".git").mkdir()
|
||||
(workspace / "agent-task").mkdir()
|
||||
self.write_task(workspace, "sim/01_reclassify", "src/reclassify.go")
|
||||
|
||||
review_attempts = 0
|
||||
|
||||
async def fake_worker(workspace_path, store, task, *args, **kwargs):
|
||||
decision = {
|
||||
"work_unit_id": dispatch.work_unit_id_from_file(task.plan),
|
||||
"stage": "worker",
|
||||
"selected": {
|
||||
"adapter": "codex",
|
||||
"target": "gpt-5.6-sol",
|
||||
"execution_class": "cloud_model",
|
||||
"selfcheck_required": False,
|
||||
},
|
||||
}
|
||||
store.update_task(
|
||||
task,
|
||||
worker_done=True,
|
||||
worker_cli="codex",
|
||||
worker_model="gpt-5.6-sol",
|
||||
completing_decision=decision,
|
||||
execution_class="cloud_model",
|
||||
selfcheck_done=True,
|
||||
blocked=None,
|
||||
)
|
||||
|
||||
async def fake_run_escalating(
|
||||
workspace_path, store, task, role, spec, **kwargs
|
||||
):
|
||||
nonlocal review_attempts
|
||||
review_attempts += 1
|
||||
locator = workspace_path / f"review-{review_attempts}.json"
|
||||
if review_attempts == 1:
|
||||
target = workspace_path / "src" / "reclassify.go"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text("reclassified\n", encoding="utf-8")
|
||||
return True, locator
|
||||
|
||||
task.review.write_text(
|
||||
"<!-- task=sim/01_reclassify plan=0 tag=SIM -->\n"
|
||||
"## Code Review Result\n\n"
|
||||
"- Overall Verdict: PASS\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(task.directory / "complete.log").write_text(
|
||||
"simulation complete\n", encoding="utf-8"
|
||||
)
|
||||
archive = (
|
||||
workspace_path
|
||||
/ "agent-task"
|
||||
/ "archive"
|
||||
/ "2026"
|
||||
/ "08"
|
||||
/ "sim"
|
||||
/ "01_reclassify"
|
||||
)
|
||||
archive.parent.mkdir(parents=True, exist_ok=True)
|
||||
task.directory.rename(archive)
|
||||
return True, locator
|
||||
|
||||
args = SimpleNamespace(
|
||||
workspace=str(workspace),
|
||||
task_group="sim",
|
||||
dry_run=False,
|
||||
retry_blocked=False,
|
||||
)
|
||||
review_spec = dispatch.AgentSpec(
|
||||
"codex", "gpt-5.6-sol", "codex/gpt-5.6-sol xhigh"
|
||||
)
|
||||
with (
|
||||
mock.patch.object(dispatch, "run_worker", new=fake_worker),
|
||||
mock.patch.object(
|
||||
dispatch, "run_escalating", new=fake_run_escalating
|
||||
),
|
||||
mock.patch.object(
|
||||
dispatch,
|
||||
"persisted_execution_decision",
|
||||
return_value=({}, review_spec),
|
||||
),
|
||||
mock.patch.object(dispatch, "ensure_review_shared_state"),
|
||||
):
|
||||
result = await asyncio.wait_for(
|
||||
dispatch.dispatch(args), timeout=2
|
||||
)
|
||||
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(review_attempts, 2)
|
||||
|
||||
|
||||
|
||||
class DynamicFailoverBudgetTest(unittest.TestCase):
|
||||
|
|
|
|||
Loading…
Reference in a new issue