test_org_level_loop.py 6.12 KB
"""U-017 tests: the full Org-level Loop wiring (judge -> record -> route ->
fix -> re-verify -> done), mocked at the subprocess boundary (CLAUDE.md
Testing constraint). Real coverage is in src/test_org_level_loop_e2e.py
(RUN_REAL_AGENT_E2E=1).

**F (2026-08-06)**: root-cause judgment now runs through
root_cause_trace.judge_root_cause_via_work_graph (a real Work Graph fan-out,
see docs/done/verifier-fan-out-jury-design.md), so the 3 perspective calls
are mocked at work_graph_execute.write_workflow_script/execute_work_graph
(same boundary as test_work_graph_orchestrate.py), not at
org_node_trigger.trigger_org_node -- that mock is now only used for the
fixer/verify calls, which are unchanged.

Run: python3 -m unittest src.test_org_level_loop -v
"""

import unittest
from unittest.mock import patch

from src import org_level_loop, root_cause_trace, schema, work_graph_execute


def _make_perspective_execute(perspective_category):
    """Fakes work_graph_execute.execute_work_graph for the 3 perspective
    Work Nodes -- all agree on perspective_category, real schema-shaped
    output (no more 'CATEGORY:'/'DETAIL:' text parsing)."""
    def fake_execute(cwd, name, args, output_schema, timeout=300):
        return {"category": perspective_category, "detail": "because reasons"}
    return fake_execute


def _make_trigger(fix_ok=True):
    """Fakes org_node_trigger.trigger_org_node for the fixer/verify calls only."""
    def fake(org_node, prompt, target_folder_path, timeout=300):
        if org_node in ("coder", "planner"):
            return "Fixed it."
        if org_node == "validator":
            return "VERDICT: PASS\nlooks fixed" if fix_ok else "VERDICT: FAIL\nstill broken"
        raise AssertionError("unexpected call: " + org_node)

    return fake


class RunFixLoopTestBase(unittest.TestCase):
    def setUp(self):
        # check_same_thread=False: the 3 root-cause perspective nodes form
        # one concurrent layer under G, run via ThreadPoolExecutor.
        self.conn = schema.connect(":memory:", check_same_thread=False)
        schema.create_schema(self.conn)
        self.conn.execute(
            "INSERT INTO project (project_id, name, target_folder_path, has_git_repo, status) "
            "VALUES ('proj-1', 'P', '/tmp/proj-1', 1, 'active')"
        )
        self.conn.execute(
            "INSERT INTO task (task_id, project_id, goal_text, task_type, status, loop_round, assigned_org_node) "
            "VALUES ('task-1', 'proj-1', 'goal', 'goal', 'validating', 0, 'validator')"
        )
        self.conn.commit()

    def tearDown(self):
        self.conn.close()

    def _run_fix_loop(self, perspective_category, fix_ok=True):
        with patch.object(work_graph_execute, "write_workflow_script"), \
             patch.object(
                 work_graph_execute, "execute_work_graph",
                 side_effect=_make_perspective_execute(perspective_category),
             ), \
             patch("src.org_node_trigger.trigger_org_node", side_effect=_make_trigger(fix_ok=fix_ok)):
            return org_level_loop.run_fix_loop(
                self.conn, "task-1", "/tmp/proj-1", "bug desc", "context", "fix it", "verify it"
            )


class CoderCategoryFastLaneTest(RunFixLoopTestBase):
    def test_coder_category_routes_fast_lane_and_reaches_done(self):
        result = self._run_fix_loop("coder", fix_ok=True)
        self.assertEqual(result["root_cause_category"], "coder")
        self.assertEqual(result["route"], "fast_lane")
        self.assertTrue(result["verified"])
        self.assertEqual(result["final_status"], "done")

        row = self.conn.execute("SELECT status, loop_round FROM task WHERE task_id = 'task-1'").fetchone()
        self.assertEqual(row, ("done", 1))

        bug_row = self.conn.execute(
            "SELECT root_cause_category, suggested_route FROM bug_record WHERE task_id = 'task-1'"
        ).fetchone()
        self.assertEqual(bug_row, ("coder", "fast_lane"))


class PlanningCategoryFullReplanTest(RunFixLoopTestBase):
    def test_planning_category_routes_full_replan(self):
        result = self._run_fix_loop("planning", fix_ok=True)
        self.assertEqual(result["route"], "full_replan")
        self.assertEqual(result["final_status"], "done")
        assigned = self.conn.execute("SELECT assigned_org_node FROM task WHERE task_id = 'task-1'").fetchone()[0]
        # after close_task_done the task is 'done'; assigned_org_node was set
        # to 'planner' by route_org_loop and close_task_done doesn't touch it
        self.assertEqual(assigned, "planner")


class VerificationFailsTest(RunFixLoopTestBase):
    def test_fix_not_verified_leaves_task_out_of_done(self):
        result = self._run_fix_loop("coder", fix_ok=False)
        self.assertFalse(result["verified"])
        self.assertNotEqual(result["final_status"], "done")
        status = self.conn.execute("SELECT status FROM task WHERE task_id = 'task-1'").fetchone()[0]
        self.assertNotEqual(status, "done")


class InconclusiveDisagreementTest(RunFixLoopTestBase):
    """Real Work Graph fan-out still surfaces InconclusiveEscalateError the
    same way the closure-based version did -- run_fix_loop adds no fallback
    of its own (04-P: disagreement escalates to a human, it isn't silently
    routed anywhere)."""

    def test_three_way_split_raises_and_creates_no_bug_record(self):
        categories = iter(["planning", "coder", "skill_defect"])

        def fake_execute(cwd, name, args, output_schema, timeout=300):
            return {"category": next(categories), "detail": "d"}

        with patch.object(work_graph_execute, "write_workflow_script"), \
             patch.object(work_graph_execute, "execute_work_graph", side_effect=fake_execute), \
             patch("src.org_node_trigger.trigger_org_node", side_effect=_make_trigger(fix_ok=True)):
            with self.assertRaises(root_cause_trace.InconclusiveEscalateError):
                org_level_loop.run_fix_loop(
                    self.conn, "task-1", "/tmp/proj-1", "bug desc", "context", "fix it", "verify it"
                )

        count = self.conn.execute("SELECT COUNT(*) FROM bug_record").fetchone()[0]
        self.assertEqual(count, 0)


if __name__ == "__main__":
    unittest.main()