test_org_node_lifecycle.py
7.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""U-010 tests: c_org_node_trigger's full guard + transition logic, mocked at
the subprocess boundary (src.org_node_trigger.trigger_org_node) per CLAUDE.md's
Testing constraint. Real chained-trigger coverage (all three Org Agents,
actually invoked) is in src/test_org_node_lifecycle_e2e.py (RUN_REAL_AGENT_E2E=1).
Run: python3 -m unittest src.test_org_node_lifecycle -v
"""
import unittest
from unittest.mock import patch
from src import org_node_lifecycle, schema, task_create
class LifecycleTestBase(unittest.TestCase):
def setUp(self):
self.conn = schema.connect(":memory:")
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.commit()
self.task_id = task_create.create_task(self.conn, "proj-1", "实现贪吃蛇游戏")["task_id"]
def tearDown(self):
self.conn.close()
def _pass_artifact(self, artifact_type, run_id="run-1"):
self.conn.execute(
"INSERT INTO work_graph_run (run_id, task_id, org_node, graph_manifest, status) "
"VALUES (?, ?, 'planner', '{}', 'done') ON CONFLICT(run_id) DO NOTHING",
(run_id, self.task_id),
)
self.conn.execute(
"INSERT INTO artifact (artifact_id, artifact_type, task_id, produced_by_run_id, "
"file_path, git_commit_sha, validation_status) VALUES (?, ?, ?, ?, 'f.md', 'sha', 'passed')",
(artifact_type + "-artifact", artifact_type, self.task_id, run_id),
)
self.conn.commit()
class PlannerTriggerTest(LifecycleTestBase):
def test_success_transitions_queued_to_planning(self):
with patch("src.org_node_lifecycle.trigger_org_node", return_value="ok"):
result = org_node_lifecycle.trigger_org_node_full(
self.conn, "planner", self.task_id, "/tmp/proj-1", "go"
)
self.assertEqual(result["status"], "planning_brd_prd_sdd")
self.assertEqual(result["assigned_org_node"], "planner")
row = self.conn.execute("SELECT status FROM task WHERE task_id = ?", (self.task_id,)).fetchone()
self.assertEqual(row[0], "planning_brd_prd_sdd")
def test_writes_task_status_changed_event(self):
with patch("src.org_node_lifecycle.trigger_org_node", return_value="ok"):
org_node_lifecycle.trigger_org_node_full(self.conn, "planner", self.task_id, "/tmp/proj-1", "go")
event = self.conn.execute(
"SELECT event_type FROM execution_event WHERE task_id = ? ORDER BY created_at DESC LIMIT 1",
(self.task_id,),
).fetchone()
self.assertEqual(event[0], "task_status_changed")
def test_wrong_status_raises_task_not_ready(self):
self.conn.execute("UPDATE task SET status = 'coding' WHERE task_id = ?", (self.task_id,))
self.conn.commit()
with self.assertRaises(org_node_lifecycle.TaskNotReadyError):
org_node_lifecycle.trigger_org_node_full(self.conn, "planner", self.task_id, "/tmp/proj-1", "go")
def test_concurrency_limit_reached_blocks_trigger(self):
self.conn.execute(
"INSERT INTO org_concurrency_config (config_id, max_concurrent_goals, updated_by) "
"VALUES ('default', 1, 'test')"
)
other_task = task_create.create_task(self.conn, "proj-1", "other goal")["task_id"]
self.conn.execute("UPDATE task SET status = 'coding' WHERE task_id = ?", (other_task,))
self.conn.commit()
with self.assertRaises(org_node_lifecycle.ConcurrencyLimitReachedError):
org_node_lifecycle.trigger_org_node_full(self.conn, "planner", self.task_id, "/tmp/proj-1", "go")
def test_unknown_org_node_raises_value_error(self):
with self.assertRaises(ValueError):
org_node_lifecycle.trigger_org_node_full(self.conn, "not-a-node", self.task_id, "/tmp/proj-1", "go")
def test_usage_limit_phrase_maps_to_usage_limit_reached(self):
from src.org_node_trigger import OrgNodeTriggerError
with patch(
"src.org_node_lifecycle.trigger_org_node",
side_effect=OrgNodeTriggerError("claude --agent planner: You've hit your session limit"),
):
with self.assertRaises(org_node_lifecycle.UsageLimitReachedError):
org_node_lifecycle.trigger_org_node_full(self.conn, "planner", self.task_id, "/tmp/proj-1", "go")
def test_other_trigger_errors_are_not_reclassified(self):
from src.org_node_trigger import OrgNodeTriggerError
with patch(
"src.org_node_lifecycle.trigger_org_node", side_effect=OrgNodeTriggerError("some other failure")
):
with self.assertRaises(OrgNodeTriggerError):
org_node_lifecycle.trigger_org_node_full(self.conn, "planner", self.task_id, "/tmp/proj-1", "go")
class CoderTriggerTest(LifecycleTestBase):
def setUp(self):
super().setUp()
self.conn.execute("UPDATE task SET status = 'planning_brd_prd_sdd' WHERE task_id = ?", (self.task_id,))
self.conn.commit()
def test_blocked_until_all_four_artifacts_pass(self):
with self.assertRaises(org_node_lifecycle.TaskNotReadyError):
org_node_lifecycle.trigger_org_node_full(self.conn, "coder", self.task_id, "/tmp/proj-1", "go")
for artifact_type in ("brd", "prd", "sdd"): # missing benchmark on purpose
self._pass_artifact(artifact_type)
with self.assertRaises(org_node_lifecycle.TaskNotReadyError):
org_node_lifecycle.trigger_org_node_full(self.conn, "coder", self.task_id, "/tmp/proj-1", "go")
def test_succeeds_once_all_four_artifacts_pass(self):
for artifact_type in ("brd", "prd", "sdd", "benchmark"):
self._pass_artifact(artifact_type)
with patch("src.org_node_lifecycle.trigger_org_node", return_value="ok"):
result = org_node_lifecycle.trigger_org_node_full(
self.conn, "coder", self.task_id, "/tmp/proj-1", "go"
)
self.assertEqual(result["status"], "coding")
self.assertEqual(result["assigned_org_node"], "coder")
class ValidatorTriggerTest(LifecycleTestBase):
def setUp(self):
super().setUp()
self.conn.execute("UPDATE task SET status = 'coding' WHERE task_id = ?", (self.task_id,))
self.conn.commit()
def test_blocked_until_code_diff_and_test_report_pass(self):
with self.assertRaises(org_node_lifecycle.TaskNotReadyError):
org_node_lifecycle.trigger_org_node_full(self.conn, "validator", self.task_id, "/tmp/proj-1", "go")
def test_succeeds_once_both_artifacts_pass(self):
self._pass_artifact("code_diff")
self._pass_artifact("test_report")
with patch("src.org_node_lifecycle.trigger_org_node", return_value="ok"):
result = org_node_lifecycle.trigger_org_node_full(
self.conn, "validator", self.task_id, "/tmp/proj-1", "go"
)
self.assertEqual(result["status"], "validating")
self.assertEqual(result["assigned_org_node"], "validator")
if __name__ == "__main__":
unittest.main()