cgcardona / muse public
test_coordination.py python
344 lines 13.0 KB
dfa7b7aa Add comprehensive docs and supercharged tests for Code Domain V2 (#70) Gabriel Cardona <cgcardona@gmail.com> 1d ago
1 """Tests for muse/core/coordination.py — multi-agent coordination layer.
2
3 Coverage
4 --------
5 Directory helpers
6 - _ensure_coord_dirs creates .muse/coordination/reservations/ and intents/.
7
8 Reservation
9 - create_reservation writes a valid JSON file.
10 - Reservation.from_dict / to_dict round-trip.
11 - Reservation.is_active() returns True for non-expired, False for expired.
12 - load_all_reservations loads all files including expired.
13 - active_reservations filters out expired reservations.
14 - Corrupt reservation file is skipped with a warning.
15 - Multiple reservations can coexist for the same address.
16
17 Intent
18 - create_intent writes a valid JSON file.
19 - Intent.from_dict / to_dict round-trip.
20 - load_all_intents loads all files.
21 - Corrupt intent file is skipped.
22
23 Schema
24 - All records have schema_version == 1.
25 - created_at and expires_at are ISO 8601 strings.
26 - operation field is None-able for reservations.
27 """
28 from __future__ import annotations
29
30 import datetime
31 import json
32 import pathlib
33
34 import pytest
35
36 from muse.core.coordination import (
37 Intent,
38 Reservation,
39 active_reservations,
40 create_intent,
41 create_reservation,
42 load_all_intents,
43 load_all_reservations,
44 )
45
46
47 # ---------------------------------------------------------------------------
48 # Helpers
49 # ---------------------------------------------------------------------------
50
51
52 def _now() -> datetime.datetime:
53 return datetime.datetime.now(datetime.timezone.utc)
54
55
56 def _future(seconds: int = 3600) -> datetime.datetime:
57 return _now() + datetime.timedelta(seconds=seconds)
58
59
60 def _past(seconds: int = 60) -> datetime.datetime:
61 return _now() - datetime.timedelta(seconds=seconds)
62
63
64 # ---------------------------------------------------------------------------
65 # Reservation — create and load
66 # ---------------------------------------------------------------------------
67
68
69 class TestCreateReservation:
70 def test_creates_json_file(self, tmp_path: pathlib.Path) -> None:
71 res = create_reservation(
72 tmp_path,
73 run_id="agent-1",
74 branch="feature-x",
75 addresses=["src/billing.py::compute_total"],
76 )
77 rdir = tmp_path / ".muse" / "coordination" / "reservations"
78 assert rdir.exists()
79 files = list(rdir.glob("*.json"))
80 assert len(files) == 1
81 data = json.loads(files[0].read_text())
82 assert data["reservation_id"] == res.reservation_id
83 assert data["run_id"] == "agent-1"
84 assert data["branch"] == "feature-x"
85 assert data["addresses"] == ["src/billing.py::compute_total"]
86 assert data["schema_version"] == 1
87
88 def test_default_ttl_sets_future_expiry(self, tmp_path: pathlib.Path) -> None:
89 res = create_reservation(tmp_path, run_id="r", branch="main", addresses=[])
90 assert res.expires_at > _now()
91
92 def test_custom_ttl(self, tmp_path: pathlib.Path) -> None:
93 res = create_reservation(
94 tmp_path, run_id="r", branch="main", addresses=[], ttl_seconds=7200
95 )
96 delta = res.expires_at - res.created_at
97 assert abs(delta.total_seconds() - 7200) < 5
98
99 def test_operation_stored(self, tmp_path: pathlib.Path) -> None:
100 res = create_reservation(
101 tmp_path, run_id="r", branch="main",
102 addresses=["src/a.py::f"], operation="rename"
103 )
104 assert res.operation == "rename"
105 rdir = tmp_path / ".muse" / "coordination" / "reservations"
106 data = json.loads(list(rdir.glob("*.json"))[0].read_text())
107 assert data["operation"] == "rename"
108
109 def test_none_operation(self, tmp_path: pathlib.Path) -> None:
110 res = create_reservation(tmp_path, run_id="r", branch="main", addresses=[])
111 assert res.operation is None
112
113 def test_multiple_addresses(self, tmp_path: pathlib.Path) -> None:
114 addrs = ["src/a.py::f", "src/b.py::g", "src/c.py::h"]
115 res = create_reservation(tmp_path, run_id="r", branch="main", addresses=addrs)
116 assert res.addresses == addrs
117
118 def test_multiple_reservations_coexist(self, tmp_path: pathlib.Path) -> None:
119 create_reservation(tmp_path, run_id="a1", branch="main", addresses=["src/a.py::f"])
120 create_reservation(tmp_path, run_id="a2", branch="main", addresses=["src/a.py::f"])
121 rdir = tmp_path / ".muse" / "coordination" / "reservations"
122 assert len(list(rdir.glob("*.json"))) == 2
123
124
125 # ---------------------------------------------------------------------------
126 # Reservation — to_dict / from_dict
127 # ---------------------------------------------------------------------------
128
129
130 class TestReservationRoundTrip:
131 def test_to_dict_from_dict(self) -> None:
132 now = _now()
133 future = _future()
134 res = Reservation(
135 reservation_id="test-uuid",
136 run_id="agent-7",
137 branch="feature-y",
138 addresses=["src/x.py::func"],
139 created_at=now,
140 expires_at=future,
141 operation="move",
142 )
143 d = res.to_dict()
144 res2 = Reservation.from_dict(d)
145 assert res2.reservation_id == "test-uuid"
146 assert res2.run_id == "agent-7"
147 assert res2.branch == "feature-y"
148 assert res2.addresses == ["src/x.py::func"]
149 assert res2.operation == "move"
150 # Timestamps round-trip via ISO 8601
151 assert abs((res2.expires_at - future).total_seconds()) < 1
152
153 def test_schema_version_in_dict(self) -> None:
154 res = Reservation(
155 reservation_id="x", run_id="r", branch="b",
156 addresses=[], created_at=_now(), expires_at=_future(), operation=None
157 )
158 assert res.to_dict()["schema_version"] == 1
159
160
161 # ---------------------------------------------------------------------------
162 # Reservation — is_active
163 # ---------------------------------------------------------------------------
164
165
166 class TestReservationIsActive:
167 def test_active_when_future_expiry(self, tmp_path: pathlib.Path) -> None:
168 res = create_reservation(tmp_path, run_id="r", branch="main", addresses=[], ttl_seconds=3600)
169 assert res.is_active()
170
171 def test_inactive_when_past_expiry(self) -> None:
172 res = Reservation(
173 reservation_id="x", run_id="r", branch="b",
174 addresses=[], created_at=_past(120), expires_at=_past(60), operation=None
175 )
176 assert not res.is_active()
177
178
179 # ---------------------------------------------------------------------------
180 # load_all_reservations / active_reservations
181 # ---------------------------------------------------------------------------
182
183
184 class TestLoadReservations:
185 def test_load_all_includes_expired(self, tmp_path: pathlib.Path) -> None:
186 create_reservation(tmp_path, run_id="r1", branch="main", addresses=[], ttl_seconds=3600)
187 # Manually write an expired reservation.
188 past = _past(120)
189 expired = Reservation(
190 reservation_id="expired-uuid",
191 run_id="r2",
192 branch="main",
193 addresses=[],
194 created_at=_past(200),
195 expires_at=past,
196 operation=None,
197 )
198 rdir = tmp_path / ".muse" / "coordination" / "reservations"
199 rdir.mkdir(parents=True, exist_ok=True)
200 (rdir / "expired-uuid.json").write_text(json.dumps(expired.to_dict()) + "\n")
201
202 all_res = load_all_reservations(tmp_path)
203 assert len(all_res) == 2
204
205 def test_active_reservations_filters_expired(self, tmp_path: pathlib.Path) -> None:
206 create_reservation(tmp_path, run_id="r1", branch="main", addresses=[], ttl_seconds=3600)
207 past = _past(120)
208 expired = Reservation(
209 reservation_id="expired-uuid",
210 run_id="r2", branch="main", addresses=[],
211 created_at=_past(200), expires_at=past, operation=None,
212 )
213 rdir = tmp_path / ".muse" / "coordination" / "reservations"
214 rdir.mkdir(parents=True, exist_ok=True)
215 (rdir / "expired-uuid.json").write_text(json.dumps(expired.to_dict()) + "\n")
216
217 active = active_reservations(tmp_path)
218 assert len(active) == 1
219 assert active[0].run_id == "r1"
220
221 def test_empty_dir_returns_empty_list(self, tmp_path: pathlib.Path) -> None:
222 rdir = tmp_path / ".muse" / "coordination" / "reservations"
223 rdir.mkdir(parents=True, exist_ok=True)
224 assert load_all_reservations(tmp_path) == []
225
226 def test_nonexistent_dir_returns_empty_list(self, tmp_path: pathlib.Path) -> None:
227 assert load_all_reservations(tmp_path) == []
228
229 def test_corrupt_file_skipped(self, tmp_path: pathlib.Path) -> None:
230 rdir = tmp_path / ".muse" / "coordination" / "reservations"
231 rdir.mkdir(parents=True, exist_ok=True)
232 (rdir / "bad.json").write_text("not valid json{{{")
233 result = load_all_reservations(tmp_path)
234 assert result == []
235
236
237 # ---------------------------------------------------------------------------
238 # Intent — create and load
239 # ---------------------------------------------------------------------------
240
241
242 class TestCreateIntent:
243 def test_creates_json_file(self, tmp_path: pathlib.Path) -> None:
244 intent = create_intent(
245 tmp_path,
246 reservation_id="res-uuid",
247 run_id="agent-2",
248 branch="feature-z",
249 addresses=["src/billing.py::Invoice"],
250 operation="rename",
251 detail="rename to InvoiceRecord",
252 )
253 idir = tmp_path / ".muse" / "coordination" / "intents"
254 assert idir.exists()
255 files = list(idir.glob("*.json"))
256 assert len(files) == 1
257 data = json.loads(files[0].read_text())
258 assert data["intent_id"] == intent.intent_id
259 assert data["reservation_id"] == "res-uuid"
260 assert data["operation"] == "rename"
261 assert data["detail"] == "rename to InvoiceRecord"
262 assert data["schema_version"] == 1
263
264 def test_empty_detail_defaults_to_empty_string(self, tmp_path: pathlib.Path) -> None:
265 intent = create_intent(
266 tmp_path, reservation_id="", run_id="r", branch="main",
267 addresses=[], operation="modify",
268 )
269 assert intent.detail == ""
270
271 def test_multiple_intents(self, tmp_path: pathlib.Path) -> None:
272 create_intent(tmp_path, reservation_id="", run_id="a", branch="main",
273 addresses=["x.py::f"], operation="rename")
274 create_intent(tmp_path, reservation_id="", run_id="b", branch="main",
275 addresses=["x.py::g"], operation="delete")
276 idir = tmp_path / ".muse" / "coordination" / "intents"
277 assert len(list(idir.glob("*.json"))) == 2
278
279
280 # ---------------------------------------------------------------------------
281 # Intent — to_dict / from_dict
282 # ---------------------------------------------------------------------------
283
284
285 class TestIntentRoundTrip:
286 def test_to_dict_from_dict(self) -> None:
287 now = _now()
288 intent = Intent(
289 intent_id="intent-uuid",
290 reservation_id="res-uuid",
291 run_id="agent-3",
292 branch="dev",
293 addresses=["src/y.py::Bar"],
294 operation="extract",
295 created_at=now,
296 detail="extract helper",
297 )
298 d = intent.to_dict()
299 intent2 = Intent.from_dict(d)
300 assert intent2.intent_id == "intent-uuid"
301 assert intent2.reservation_id == "res-uuid"
302 assert intent2.operation == "extract"
303 assert intent2.detail == "extract helper"
304 assert intent2.addresses == ["src/y.py::Bar"]
305
306 def test_schema_version_in_dict(self) -> None:
307 intent = Intent(
308 intent_id="x", reservation_id="", run_id="r", branch="b",
309 addresses=[], operation="modify", created_at=_now(), detail="",
310 )
311 assert intent.to_dict()["schema_version"] == 1
312
313
314 # ---------------------------------------------------------------------------
315 # load_all_intents
316 # ---------------------------------------------------------------------------
317
318
319 class TestLoadAllIntents:
320 def test_empty_dir(self, tmp_path: pathlib.Path) -> None:
321 idir = tmp_path / ".muse" / "coordination" / "intents"
322 idir.mkdir(parents=True, exist_ok=True)
323 assert load_all_intents(tmp_path) == []
324
325 def test_nonexistent_dir(self, tmp_path: pathlib.Path) -> None:
326 assert load_all_intents(tmp_path) == []
327
328 def test_loads_created_intents(self, tmp_path: pathlib.Path) -> None:
329 create_intent(tmp_path, reservation_id="r", run_id="a", branch="main",
330 addresses=["x.py::f"], operation="rename")
331 create_intent(tmp_path, reservation_id="r", run_id="b", branch="dev",
332 addresses=["y.py::g"], operation="modify")
333 intents = load_all_intents(tmp_path)
334 assert len(intents) == 2
335 ops = {i.operation for i in intents}
336 assert "rename" in ops
337 assert "modify" in ops
338
339 def test_corrupt_intent_skipped(self, tmp_path: pathlib.Path) -> None:
340 idir = tmp_path / ".muse" / "coordination" / "intents"
341 idir.mkdir(parents=True, exist_ok=True)
342 (idir / "bad.json").write_text("{invalid")
343 result = load_all_intents(tmp_path)
344 assert result == []