Commit cac67c9ab770dd904f019a6a5c14e2d697c8f247

Authored by bboonnee
1 parent 1a8f99c2

test: cover the planning-call work_node_execution recorded by _record_planning_node

Adds targeted coverage for the synthetic "__planning__" node work_graph_plan.py
now records (real work_node_execution + log, task_query.py display-only
prepend) -- the row exists and is done, the persisted work_graph_run.graph_manifest
stays pure for execution/fork/append-mode, the log carries the real prompt and
manifest, task_query surfaces it as nodes[0] with a clickable node_exec_id, and
append mode doesn't duplicate it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing 1 changed file with 104 additions and 1 deletions
src/test_work_graph_plan.py
... ... @@ -12,7 +12,7 @@ import subprocess
12 12 import unittest
13 13 from unittest.mock import patch
14 14  
15   -from src import org_node_trigger, schema, work_graph_orchestrate, work_graph_plan
  15 +from src import execution_log, org_node_trigger, schema, task_query, work_graph_orchestrate, work_graph_plan
16 16  
17 17 _MANIFEST = {
18 18 "nodes": [
... ... @@ -686,5 +686,108 @@ class GenericPathEndToEndViaRunGraphTest(PlanWorkGraphTestBase):
686 686 self.assertEqual(mock_trigger.call_count, 3)
687 687  
688 688  
  689 +class PlanningNodeRecordedTest(PlanWorkGraphTestBase):
  690 + """The planning call itself (this class's own `run.return_value`
  691 + subprocess call) must show up as a real, clickable work_node_execution
  692 + -- not just as the graph_manifest it produced -- so v_task_detail's
  693 + to-do tree/DAG have a first node for "how was this run planned",
  694 + per the user's own request (2026-08-15, task b243dfe8 discussion)."""
  695 +
  696 + def test_planning_node_execution_row_is_real_and_done(self):
  697 + with patch("src.work_graph_plan.subprocess.run") as run:
  698 + run.return_value = _completed_process(
  699 + stdout_obj={"is_error": False, "result": _fenced_result(_MANIFEST)}
  700 + )
  701 + result = work_graph_plan.plan_work_graph(
  702 + self.conn, "planner", "task-1", "实现贪吃蛇游戏", "/tmp/proj-1"
  703 + )
  704 + row = self.conn.execute(
  705 + "SELECT run_id, subagent_type, skill_ref, status, output_payload "
  706 + "FROM work_node_execution WHERE node_id = ?",
  707 + (work_graph_plan.PLANNING_NODE_ID,),
  708 + ).fetchone()
  709 + self.assertIsNotNone(row)
  710 + self.assertEqual(row[0], result["run_id"])
  711 + self.assertEqual(row[1], "planner")
  712 + self.assertEqual(row[2], "design-dynamic-work-graph")
  713 + self.assertEqual(row[3], "done")
  714 + self.assertEqual(json.loads(row[4])["graph_manifest"], _MANIFEST)
  715 +
  716 + def test_persisted_work_graph_run_manifest_stays_pure_for_execution(self):
  717 + # run_graph()/fork/append-mode all read work_graph_run.graph_manifest
  718 + # straight off the DB -- it must never gain the synthetic planning
  719 + # node, or a later append_to_run_id call / a fork would try to treat
  720 + # "planning itself" as executable work. See _record_planning_node's
  721 + # own docstring.
  722 + with patch("src.work_graph_plan.subprocess.run") as run:
  723 + run.return_value = _completed_process(
  724 + stdout_obj={"is_error": False, "result": _fenced_result(_MANIFEST)}
  725 + )
  726 + result = work_graph_plan.plan_work_graph(
  727 + self.conn, "planner", "task-1", "实现贪吃蛇游戏", "/tmp/proj-1"
  728 + )
  729 + persisted = self.conn.execute(
  730 + "SELECT graph_manifest FROM work_graph_run WHERE run_id = ?", (result["run_id"],)
  731 + ).fetchone()[0]
  732 + self.assertEqual(json.loads(persisted), _MANIFEST)
  733 +
  734 + def test_log_viewer_content_includes_prompt_and_graph_manifest(self):
  735 + with patch("src.work_graph_plan.subprocess.run") as run:
  736 + run.return_value = _completed_process(
  737 + stdout_obj={"is_error": False, "result": _fenced_result(_MANIFEST)}
  738 + )
  739 + work_graph_plan.plan_work_graph(self.conn, "planner", "task-1", "实现贪吃蛇游戏", "/tmp/proj-1")
  740 + node_exec_id = self.conn.execute(
  741 + "SELECT node_exec_id FROM work_node_execution WHERE node_id = ?",
  742 + (work_graph_plan.PLANNING_NODE_ID,),
  743 + ).fetchone()[0]
  744 + log = execution_log.get_log(self.conn, node_exec_id)
  745 + self.assertIn("node_start", log["content"])
  746 + self.assertIn("node_completed", log["content"])
  747 + self.assertIn("实现贪吃蛇游戏", log["content"])
  748 + self.assertIn("n1", log["content"]) # a real node_id from _MANIFEST, proving the manifest is in there
  749 +
  750 + def test_task_query_prepends_planning_node_as_the_first_node(self):
  751 + with patch("src.work_graph_plan.subprocess.run") as run:
  752 + run.return_value = _completed_process(
  753 + stdout_obj={"is_error": False, "result": _fenced_result(_MANIFEST)}
  754 + )
  755 + work_graph_plan.plan_work_graph(self.conn, "planner", "task-1", "实现贪吃蛇游戏", "/tmp/proj-1")
  756 +
  757 + task = task_query.get_task(self.conn, "task-1")
  758 + display_nodes = task["work_graph_runs"][0]["graph_manifest"]["nodes"]
  759 + self.assertEqual(display_nodes[0]["node_id"], work_graph_plan.PLANNING_NODE_ID)
  760 + self.assertEqual([n["node_id"] for n in display_nodes[1:]], [n["node_id"] for n in _MANIFEST["nodes"]])
  761 + # the synthetic node is clickable exactly like any other -- it has a
  762 + # matching work_node_execution row in "nodes" with a real node_exec_id.
  763 + wne_by_id = {n["node_id"]: n for n in task["work_graph_runs"][0]["nodes"]}
  764 + self.assertIn(work_graph_plan.PLANNING_NODE_ID, wne_by_id)
  765 + self.assertTrue(wne_by_id[work_graph_plan.PLANNING_NODE_ID]["node_exec_id"])
  766 +
  767 + def test_append_mode_does_not_record_a_second_planning_node(self):
  768 + # YAGNI (see _record_planning_node's own docstring): only the fresh
  769 + # branch gets one. Confirms append mode doesn't crash or duplicate.
  770 + with patch("src.work_graph_plan.subprocess.run") as run:
  771 + run.return_value = _completed_process(
  772 + stdout_obj={"is_error": False, "result": _fenced_result(_MANIFEST)}
  773 + )
  774 + first = work_graph_plan.plan_work_graph(
  775 + self.conn, "planner", "task-1", "实现贪吃蛇游戏", "/tmp/proj-1"
  776 + )
  777 + run.return_value = _completed_process(
  778 + stdout_obj={"is_error": False, "result": _fenced_result(
  779 + {"nodes": [{"node_id": "n5", "artifact_type": "test_dataset", "depends_on": []}], "edges": []}
  780 + )}
  781 + )
  782 + work_graph_plan.plan_work_graph(
  783 + self.conn, "coder", "task-1", "goal", "/tmp/proj-1", append_to_run_id=first["run_id"],
  784 + )
  785 + count = self.conn.execute(
  786 + "SELECT COUNT(*) FROM work_node_execution WHERE run_id = ? AND node_id = ?",
  787 + (first["run_id"], work_graph_plan.PLANNING_NODE_ID),
  788 + ).fetchone()[0]
  789 + self.assertEqual(count, 1)
  790 +
  791 +
689 792 if __name__ == "__main__":
690 793 unittest.main()
... ...