Commit cac67c9ab770dd904f019a6a5c14e2d697c8f247
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() | ... | ... |