gabriel / muse public
test_cmd_release_coord.py python
796 lines 30.9 KB
Raw
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b fix: try fetch/presign before fetch/mpack to avoid Cloudfla… Sonnet 4.6 patch 8 days ago
1 """Comprehensive tests for ``muse coord release``.
2
3 Coverage matrix
4 ---------------
5 Unit
6 ~~~~
7 * create_release roundtrip — write, load, fields intact
8 * Release.to_dict — required keys present
9 * Valid reasons accepted — completed, cancelled, superseded
10 * Invalid reason rejected — ValueError raised
11 * Double release raises FileExistsError
12 * Path traversal in reservation_id rejected before file I/O
13
14 Integration — CLI
15 ~~~~~~~~~~~~~~~~~
16 * Single release — text output, exit 0
17 * --reason cancelled — accepted
18 * --reason superseded — accepted
19 * --all-for-run — releases all active reservations for run-id
20 * --all-for-run with no active reservations — 0 released, exit 0
21 * Double release exits 0 (already released — idempotent)
22 * Not-found sha256 ID exits ExitCode.NOT_FOUND (4)
23 * Invalid ID exits ExitCode.USER_ERROR (1)
24 * --format json — valid JSON, required keys, compact (no indent)
25 * --json shorthand — valid JSON
26
27 Input validation
28 ~~~~~~~~~~~~~~~~
29 * --run-id at exactly 256 chars accepted
30 * --run-id over 256 chars rejected with USER_ERROR (1)
31 * --run-id validation fires before any file I/O (no reservation file left)
32 * Mutual exclusion errors use USER_ERROR (1)
33
34 Security
35 ~~~~~~~~
36 * sha256 ID path traversal rejected before file I/O
37 * ANSI escape sequences in run_id are safe (sanitized in output)
38 * null byte in reservation_id rejected
39 * sha256 ID validation before require_repo — no side effects on bad input
40
41 Concurrent
42 ~~~~~~~~~~
43 * Two threads racing to release the same reservation — both exit 0
44
45 Stress
46 ~~~~~~
47 * 200 reservations released via --all-for-run < 3 s
48 * 50 concurrent --all-for-run reads do not corrupt each other
49 """
50
51 from __future__ import annotations
52
53 import datetime
54 import json
55 import pathlib
56 import time
57 from muse.core.types import fake_id
58 from muse.core.paths import muse_dir
59
60 import threading
61
62 import pytest
63
64 from tests.cli_test_helper import CliRunner
65 from muse.core.coordination import (
66 Release,
67 Reservation,
68 create_release,
69 create_reservation,
70 load_all_reservations,
71 load_released_ids,
72 )
73 from muse.cli.commands.release_coord import _MAX_RUN_ID_LEN
74 from muse.core.errors import ExitCode
75
76 cli = None
77 runner = CliRunner()
78
79
80 # ---------------------------------------------------------------------------
81 # Helpers
82 # ---------------------------------------------------------------------------
83
84
85 def _now_utc() -> datetime.datetime:
86 return datetime.datetime.now(datetime.timezone.utc)
87
88
89 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
90 dot_muse = muse_dir(tmp_path)
91 dot_muse.mkdir()
92 (dot_muse / "HEAD").write_text("ref: refs/heads/main\n")
93 return tmp_path
94
95
96 @pytest.fixture()
97 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
98 monkeypatch.chdir(tmp_path)
99 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
100 r = runner.invoke(cli, ["init", "--domain", "code"])
101 assert r.exit_code == 0, r.output
102 return tmp_path
103
104
105 @pytest.fixture()
106 def reservation(repo: pathlib.Path) -> Reservation:
107 return create_reservation(
108 repo,
109 run_id="agent-test",
110 branch="feat/test",
111 addresses=["src/billing.py::compute_total"],
112 ttl_seconds=3600,
113 )
114
115
116 # ---------------------------------------------------------------------------
117 # Unit — core helpers
118 # ---------------------------------------------------------------------------
119
120
121 class TestCreateReleaseRoundtrip:
122 def test_roundtrip_fields(self, tmp_path: pathlib.Path) -> None:
123 root = _make_repo(tmp_path)
124 res = create_reservation(root, run_id="agent-1", branch="main",
125 addresses=["a.py::f"], ttl_seconds=3600)
126 rid = res.reservation_id
127 rel = create_release(root, rid, run_id="agent-1", reason="completed")
128 assert rel.reservation_id == rid
129 assert rel.run_id == "agent-1"
130 assert rel.reason == "completed"
131 assert isinstance(rel.released_at, datetime.datetime)
132
133 def test_release_persisted_to_disk(self, tmp_path: pathlib.Path) -> None:
134 root = _make_repo(tmp_path)
135 res = create_reservation(root, run_id="agent-1", branch="main",
136 addresses=["a.py::f"], ttl_seconds=3600)
137 rid = res.reservation_id
138 create_release(root, rid, run_id="agent-1", reason="completed")
139 released = load_released_ids(root)
140 assert rid in released
141
142 def test_to_dict_keys(self, tmp_path: pathlib.Path) -> None:
143 root = _make_repo(tmp_path)
144 res = create_reservation(root, run_id="agent-1", branch="main",
145 addresses=["a.py::f"], ttl_seconds=3600)
146 rel = create_release(root, res.reservation_id, run_id="agent-1")
147 d = rel.to_dict()
148 assert "reservation_id" in d
149 assert "run_id" in d
150 assert "released_at" in d
151 assert "reason" in d
152 assert "schema_version" in d
153
154 def test_valid_reason_completed(self, tmp_path: pathlib.Path) -> None:
155 root = _make_repo(tmp_path)
156 res = create_reservation(root, run_id="a", branch="main",
157 addresses=["x.py::g"], ttl_seconds=3600)
158 rel = create_release(root, res.reservation_id, run_id="a", reason="completed")
159 assert rel.reason == "completed"
160
161 def test_valid_reason_cancelled(self, tmp_path: pathlib.Path) -> None:
162 root = _make_repo(tmp_path)
163 res = create_reservation(root, run_id="a", branch="main",
164 addresses=["x.py::g"], ttl_seconds=3600)
165 rel = create_release(root, res.reservation_id, run_id="a", reason="cancelled")
166 assert rel.reason == "cancelled"
167
168 def test_valid_reason_superseded(self, tmp_path: pathlib.Path) -> None:
169 root = _make_repo(tmp_path)
170 res = create_reservation(root, run_id="a", branch="main",
171 addresses=["x.py::g"], ttl_seconds=3600)
172 rel = create_release(root, res.reservation_id, run_id="a", reason="superseded")
173 assert rel.reason == "superseded"
174
175 def test_invalid_reason_raises(self, tmp_path: pathlib.Path) -> None:
176 root = _make_repo(tmp_path)
177 res = create_reservation(root, run_id="a", branch="main",
178 addresses=["x.py::g"], ttl_seconds=3600)
179 with pytest.raises(ValueError, match="reason must be one of"):
180 create_release(root, res.reservation_id, run_id="a", reason="bogus")
181
182 def test_double_release_raises_file_exists(self, tmp_path: pathlib.Path) -> None:
183 root = _make_repo(tmp_path)
184 res = create_reservation(root, run_id="a", branch="main",
185 addresses=["x.py::g"], ttl_seconds=3600)
186 create_release(root, res.reservation_id, run_id="a")
187 with pytest.raises(FileExistsError):
188 create_release(root, res.reservation_id, run_id="a")
189
190 def test_path_traversal_reservation_id_rejected(self, tmp_path: pathlib.Path) -> None:
191 root = _make_repo(tmp_path)
192 with pytest.raises(ValueError, match="sha256"):
193 create_release(root, "../../etc/passwd", run_id="a")
194
195 def test_path_traversal_with_slashes_rejected(self, tmp_path: pathlib.Path) -> None:
196 root = _make_repo(tmp_path)
197 with pytest.raises(ValueError):
198 create_release(root, "../traversal/path", run_id="a")
199
200 def test_non_sha256_string_rejected(self, tmp_path: pathlib.Path) -> None:
201 root = _make_repo(tmp_path)
202 with pytest.raises(ValueError):
203 create_release(root, "not-a-valid-id", run_id="a")
204
205
206 # ---------------------------------------------------------------------------
207 # Integration — CLI
208 # ---------------------------------------------------------------------------
209
210
211 class TestReleaseSingleCli:
212 def test_basic_release_exits_zero(self, repo: pathlib.Path, reservation: Reservation) -> None:
213 rid = reservation.reservation_id
214 result = runner.invoke(cli, ["coord", "release", rid, "--run-id", "agent-1"])
215 assert result.exit_code == 0, result.output
216
217 def test_basic_release_text_contains_id(self, repo: pathlib.Path, reservation: Reservation) -> None:
218 rid = reservation.reservation_id
219 result = runner.invoke(cli, ["coord", "release", rid, "--run-id", "agent-1"])
220 assert rid[:8] in result.output or rid in result.output
221
222 def test_basic_release_text_contains_released(self, repo: pathlib.Path, reservation: Reservation) -> None:
223 rid = reservation.reservation_id
224 result = runner.invoke(cli, ["coord", "release", rid, "--run-id", "agent-1"])
225 assert "released" in result.output.lower()
226
227 def test_reason_cancelled(self, repo: pathlib.Path, reservation: Reservation) -> None:
228 rid = reservation.reservation_id
229 result = runner.invoke(
230 cli, ["coord", "release", rid, "--run-id", "agent-1", "--reason", "cancelled"]
231 )
232 assert result.exit_code == 0, result.output
233 assert "cancelled" in result.output
234
235 def test_reason_superseded(self, repo: pathlib.Path, reservation: Reservation) -> None:
236 rid = reservation.reservation_id
237 result = runner.invoke(
238 cli, ["coord", "release", rid, "--run-id", "agent-1", "--reason", "superseded"]
239 )
240 assert result.exit_code == 0, result.output
241 assert "superseded" in result.output
242
243 def test_double_release_exits_zero_idempotent(self, repo: pathlib.Path, reservation: Reservation) -> None:
244 rid = reservation.reservation_id
245 r1 = runner.invoke(cli, ["coord", "release", rid, "--run-id", "agent-1"])
246 assert r1.exit_code == 0, r1.output
247 r2 = runner.invoke(cli, ["coord", "release", rid, "--run-id", "agent-1"])
248 # Already released — idempotent path returns 0
249 assert r2.exit_code == 0, r2.output
250 assert "already released" in r2.output.lower()
251
252 def test_not_found_id_exits_nonzero(self, repo: pathlib.Path) -> None:
253 nonexistent = fake_id("nonexistent-reservation")
254 result = runner.invoke(
255 cli, ["coord", "release", nonexistent, "--run-id", "agent-1"]
256 )
257 assert result.exit_code != 0
258 combined = result.output + (result.stderr or "")
259 assert "not found" in combined.lower()
260
261 def test_invalid_id_exits_nonzero(self, repo: pathlib.Path) -> None:
262 result = runner.invoke(
263 cli, ["coord", "release", "not-a-valid-id", "--run-id", "agent-1"]
264 )
265 assert result.exit_code != 0
266
267 def test_no_reservation_id_and_no_all_for_run_exits_nonzero(
268 self, repo: pathlib.Path
269 ) -> None:
270 result = runner.invoke(cli, ["coord", "release", "--run-id", "agent-1"])
271 assert result.exit_code != 0
272
273 def test_both_reservation_id_and_all_for_run_exits_nonzero(
274 self, repo: pathlib.Path, reservation: Reservation
275 ) -> None:
276 rid = reservation.reservation_id
277 result = runner.invoke(
278 cli,
279 ["coord", "release", rid, "--run-id", "agent-1", "--all-for-run", "agent-1"],
280 )
281 assert result.exit_code != 0
282
283
284 class TestReleaseFormatJson:
285 def test_format_json_flag(self, repo: pathlib.Path, reservation: Reservation) -> None:
286 rid = reservation.reservation_id
287 result = runner.invoke(
288 cli,
289 ["coord", "release", rid, "--run-id", "agent-1", "--json"],
290 )
291 assert result.exit_code == 0, result.output
292 data = json.loads(result.output)
293 assert data["reservation_id"] == rid
294 assert data["run_id"] == "agent-1"
295 assert "reason" in data
296 assert "released_at" in data
297
298 def test_json_shorthand(self, repo: pathlib.Path, reservation: Reservation) -> None:
299 rid = reservation.reservation_id
300 result = runner.invoke(
301 cli, ["coord", "release", rid, "--run-id", "agent-1", "--json"]
302 )
303 assert result.exit_code == 0, result.output
304 data = json.loads(result.output)
305 assert data["reservation_id"] == rid
306
307 def test_json_status_released(self, repo: pathlib.Path, reservation: Reservation) -> None:
308 rid = reservation.reservation_id
309 result = runner.invoke(
310 cli, ["coord", "release", rid, "--run-id", "agent-1", "--json"]
311 )
312 data = json.loads(result.output)
313 assert data["status"] == "released"
314
315 def test_json_duration_ms_present(self, repo: pathlib.Path, reservation: Reservation) -> None:
316 rid = reservation.reservation_id
317 result = runner.invoke(
318 cli, ["coord", "release", rid, "--run-id", "agent-1", "--json"]
319 )
320 data = json.loads(result.output)
321 assert "duration_ms" in data
322 assert isinstance(data["duration_ms"], float)
323
324 def test_json_not_found_has_status(self, repo: pathlib.Path) -> None:
325 nonexistent = fake_id("nonexistent-reservation")
326 result = runner.invoke(
327 cli, ["coord", "release", nonexistent, "--run-id", "agent-1", "--json"]
328 )
329 data = json.loads(result.output)
330 assert data["status"] == "not_found"
331
332 def test_json_already_released_has_status(self, repo: pathlib.Path, reservation: Reservation) -> None:
333 rid = reservation.reservation_id
334 runner.invoke(cli, ["coord", "release", rid, "--run-id", "agent-1"])
335 result = runner.invoke(
336 cli, ["coord", "release", rid, "--run-id", "agent-1", "--json"]
337 )
338 data = json.loads(result.output)
339 assert data["status"] == "already_released"
340
341
342 class TestReleaseAllForRun:
343 def test_all_for_run_releases_all(self, repo: pathlib.Path) -> None:
344 for i in range(5):
345 create_reservation(
346 repo, run_id="batch-agent", branch="main",
347 addresses=[f"src/m{i}.py::f"], ttl_seconds=3600,
348 )
349 result = runner.invoke(
350 cli, ["coord", "release", "--all-for-run", "batch-agent", "--run-id", "batch-agent"]
351 )
352 assert result.exit_code == 0, result.output
353 released = load_released_ids(repo)
354 all_res = load_all_reservations(repo)
355 batch_ids = {r.reservation_id for r in all_res if r.run_id == "batch-agent"}
356 assert batch_ids == batch_ids & released
357
358 def test_all_for_run_no_active_exits_zero(self, repo: pathlib.Path) -> None:
359 result = runner.invoke(
360 cli,
361 ["coord", "release", "--all-for-run", "nonexistent-run", "--run-id", "agent-1"],
362 )
363 assert result.exit_code == 0, result.output
364
365 def test_all_for_run_no_active_text_shows_zero(self, repo: pathlib.Path) -> None:
366 result = runner.invoke(
367 cli,
368 ["coord", "release", "--all-for-run", "nonexistent-run", "--run-id", "agent-1"],
369 )
370 assert "0" in result.output
371
372 def test_all_for_run_json_schema(self, repo: pathlib.Path) -> None:
373 for i in range(3):
374 create_reservation(
375 repo, run_id="run-j", branch="main",
376 addresses=[f"src/x{i}.py::f"], ttl_seconds=3600,
377 )
378 result = runner.invoke(
379 cli,
380 ["coord", "release", "--all-for-run", "run-j", "--run-id", "run-j", "--json"],
381 )
382 assert result.exit_code == 0, result.output
383 data = json.loads(result.output)
384 assert "released" in data
385 assert "skipped_already_released" in data
386 assert "duration_ms" in data
387 assert data["status"] == "ok"
388 assert len(data["released"]) == 3
389
390 def test_all_for_run_released_entries_have_required_keys(self, repo: pathlib.Path) -> None:
391 create_reservation(
392 repo, run_id="run-k", branch="main",
393 addresses=["a.py::b"], ttl_seconds=3600,
394 )
395 result = runner.invoke(
396 cli,
397 ["coord", "release", "--all-for-run", "run-k", "--run-id", "run-k", "--json"],
398 )
399 data = json.loads(result.output)
400 entry = data["released"][0]
401 assert "reservation_id" in entry
402 assert "run_id" in entry
403 assert "released_at" in entry
404 assert "reason" in entry
405
406 def test_all_for_run_only_targets_matching_run_id(self, repo: pathlib.Path) -> None:
407 res_a = create_reservation(
408 repo, run_id="run-a", branch="main",
409 addresses=["a.py::x"], ttl_seconds=3600,
410 )
411 create_reservation(
412 repo, run_id="run-b", branch="main",
413 addresses=["b.py::y"], ttl_seconds=3600,
414 )
415 runner.invoke(
416 cli,
417 ["coord", "release", "--all-for-run", "run-a", "--run-id", "run-a"],
418 )
419 released = load_released_ids(repo)
420 assert res_a.reservation_id in released
421 # run-b should not be released
422 all_res = load_all_reservations(repo)
423 run_b_ids = {r.reservation_id for r in all_res if r.run_id == "run-b"}
424 assert not run_b_ids & released
425
426 def test_all_for_run_with_reason_cancelled(self, repo: pathlib.Path) -> None:
427 create_reservation(
428 repo, run_id="run-c", branch="main",
429 addresses=["c.py::f"], ttl_seconds=3600,
430 )
431 result = runner.invoke(
432 cli,
433 [
434 "coord", "release", "--all-for-run", "run-c",
435 "--run-id", "run-c", "--reason", "cancelled", "--json",
436 ],
437 )
438 data = json.loads(result.output)
439 assert data["released"][0]["reason"] == "cancelled"
440
441
442 # ---------------------------------------------------------------------------
443 # Security
444 # ---------------------------------------------------------------------------
445
446
447 class TestReleaseSecurity:
448 def test_path_traversal_in_reservation_id_rejected(self, repo: pathlib.Path) -> None:
449 result = runner.invoke(
450 cli, ["coord", "release", "../../etc/passwd", "--run-id", "agent-1"]
451 )
452 assert result.exit_code != 0
453 # Must not have created any file outside coord dir
454 assert not (repo / "etc").exists()
455 assert not (repo.parent / "etc").exists()
456
457 def test_path_traversal_with_dots_rejected(self, repo: pathlib.Path) -> None:
458 result = runner.invoke(
459 cli, ["coord", "release", "../sneaky", "--run-id", "agent-1"]
460 )
461 assert result.exit_code != 0
462
463 def test_ansi_in_run_id_does_not_crash(self, repo: pathlib.Path, reservation: Reservation) -> None:
464 rid = reservation.reservation_id
465 ansi_run_id = "\x1b[31magent-malicious\x1b[0m"
466 result = runner.invoke(
467 cli, ["coord", "release", rid, "--run-id", ansi_run_id]
468 )
469 # Should complete without crashing (exit 0 or 1 depending on sanitization)
470 # The important thing is it doesn't raise an unhandled exception
471 assert result.exit_code in (0, 1, 2)
472
473 def test_null_byte_in_reservation_id_rejected(self, repo: pathlib.Path) -> None:
474 result = runner.invoke(
475 cli, ["coord", "release", "abc\x00def", "--run-id", "agent-1"]
476 )
477 assert result.exit_code != 0
478
479
480 # ---------------------------------------------------------------------------
481 # Stress
482 # ---------------------------------------------------------------------------
483
484
485 class TestReleaseStress:
486 def test_200_reservations_all_for_run_under_3s(self, repo: pathlib.Path) -> None:
487 for i in range(200):
488 create_reservation(
489 repo,
490 run_id="stress-agent",
491 branch="feat/stress",
492 addresses=[f"src/stress_{i}.py::func"],
493 ttl_seconds=3600,
494 )
495 t0 = time.monotonic()
496 result = runner.invoke(
497 cli,
498 ["coord", "release", "--all-for-run", "stress-agent", "--run-id", "stress-agent"],
499 )
500 elapsed = time.monotonic() - t0
501 assert result.exit_code == 0, result.output
502 assert elapsed < 3.0, f"Batch release took {elapsed:.2f}s (> 3s limit)"
503 released = load_released_ids(repo)
504 all_res = load_all_reservations(repo)
505 stress_ids = {r.reservation_id for r in all_res if r.run_id == "stress-agent"}
506 assert len(stress_ids) == 200
507 assert stress_ids <= released
508
509 def test_50_concurrent_list_reads_do_not_corrupt(self, repo: pathlib.Path) -> None:
510 """Concurrent reads of the released-IDs set must never raise."""
511 for i in range(20):
512 res = create_reservation(
513 repo,
514 run_id="concurrent-read-agent",
515 branch="main",
516 addresses=[f"src/r{i}.py::f"],
517 ttl_seconds=3600,
518 )
519 create_release(repo, res.reservation_id, run_id="concurrent-read-agent")
520
521 errors: list[Exception] = []
522
523 def _reader() -> None:
524 try:
525 ids = load_released_ids(repo)
526 assert len(ids) >= 20
527 except Exception as exc: # noqa: BLE001
528 errors.append(exc)
529
530 threads = [threading.Thread(target=_reader) for _ in range(50)]
531 for t in threads:
532 t.start()
533 for t in threads:
534 t.join()
535 assert not errors, f"Concurrent read errors: {errors}"
536
537
538 # ---------------------------------------------------------------------------
539 # Input validation
540 # ---------------------------------------------------------------------------
541
542
543 class TestReleaseInputValidation:
544 def test_run_id_at_max_length_accepted(self, repo: pathlib.Path, reservation: Reservation) -> None:
545 rid = reservation.reservation_id
546 long_run_id = "x" * _MAX_RUN_ID_LEN
547 result = runner.invoke(
548 cli, ["coord", "release", rid, "--run-id", long_run_id]
549 )
550 assert result.exit_code == 0, result.output
551
552 def test_run_id_over_max_length_exits_user_error(self, repo: pathlib.Path) -> None:
553 too_long = "x" * (_MAX_RUN_ID_LEN + 1)
554 result = runner.invoke(
555 cli, ["coord", "release", "--run-id", too_long, "--all-for-run", "agent-1"]
556 )
557 assert result.exit_code == ExitCode.USER_ERROR
558
559 def test_run_id_over_max_length_message_on_stderr(self, repo: pathlib.Path) -> None:
560 too_long = "x" * (_MAX_RUN_ID_LEN + 1)
561 result = runner.invoke(
562 cli, ["coord", "release", "--run-id", too_long, "--all-for-run", "agent-1"]
563 )
564 combined = result.output + (result.stderr or "")
565 assert "run-id" in combined.lower() or "too long" in combined.lower()
566
567 def test_run_id_over_max_leaves_no_release_file(self, repo: pathlib.Path, reservation: Reservation) -> None:
568 """Validation must fire before any file I/O."""
569 too_long = "x" * (_MAX_RUN_ID_LEN + 1)
570 runner.invoke(
571 cli,
572 ["coord", "release", reservation.reservation_id, "--run-id", too_long],
573 )
574 released = load_released_ids(repo)
575 assert reservation.reservation_id not in released
576
577 def test_mutual_exclusion_exits_user_error(self, repo: pathlib.Path, reservation: Reservation) -> None:
578 rid = reservation.reservation_id
579 result = runner.invoke(
580 cli,
581 ["coord", "release", rid, "--run-id", "agent-1", "--all-for-run", "agent-1"],
582 )
583 assert result.exit_code == ExitCode.USER_ERROR
584
585 def test_missing_both_exits_user_error(self, repo: pathlib.Path) -> None:
586 result = runner.invoke(cli, ["coord", "release", "--run-id", "agent-1"])
587 assert result.exit_code == ExitCode.USER_ERROR
588
589 def test_invalid_id_exits_user_error(self, repo: pathlib.Path) -> None:
590 result = runner.invoke(
591 cli, ["coord", "release", "not-a-valid-id", "--run-id", "agent-1"]
592 )
593 assert result.exit_code == ExitCode.USER_ERROR
594
595 def test_not_found_id_exits_not_found(self, repo: pathlib.Path) -> None:
596 nonexistent = fake_id("nonexistent-reservation")
597 result = runner.invoke(
598 cli, ["coord", "release", nonexistent, "--run-id", "agent-1"]
599 )
600 assert result.exit_code == ExitCode.NOT_FOUND
601
602 def test_not_found_id_json_status(self, repo: pathlib.Path) -> None:
603 nonexistent = fake_id("nonexistent-reservation")
604 result = runner.invoke(
605 cli, ["coord", "release", nonexistent, "--run-id", "agent-1", "--json"]
606 )
607 data = json.loads(result.output)
608 assert data["status"] == "not_found"
609 assert result.exit_code == ExitCode.NOT_FOUND
610
611 def test_invalid_reason_rejected_by_parser(self, repo: pathlib.Path, reservation: Reservation) -> None:
612 rid = reservation.reservation_id
613 result = runner.invoke(
614 cli, ["coord", "release", rid, "--run-id", "agent-1", "--reason", "bogus-reason"]
615 )
616 assert result.exit_code != 0
617
618
619 # ---------------------------------------------------------------------------
620 # JSON output format
621 # ---------------------------------------------------------------------------
622
623
624 class TestReleaseJsonFormat:
625 def test_single_release_json_is_compact(self, repo: pathlib.Path, reservation: Reservation) -> None:
626 """JSON output must be compact (no indent=2 pretty-printing)."""
627 rid = reservation.reservation_id
628 result = runner.invoke(
629 cli, ["coord", "release", rid, "--run-id", "agent-1", "--json"]
630 )
631 assert result.exit_code == 0, result.output
632 # Compact JSON has no newlines inside the object
633 assert "\n" not in result.output.strip()
634
635 def test_batch_json_is_compact(self, repo: pathlib.Path) -> None:
636 for i in range(3):
637 create_reservation(
638 repo, run_id="compact-agent", branch="main",
639 addresses=[f"src/c{i}.py::f"], ttl_seconds=3600,
640 )
641 result = runner.invoke(
642 cli,
643 ["coord", "release", "--all-for-run", "compact-agent",
644 "--run-id", "compact-agent", "--json"],
645 )
646 assert result.exit_code == 0, result.output
647 assert "\n" not in result.output.strip()
648
649 def test_already_released_json_released_at_is_null(
650 self, repo: pathlib.Path, reservation: Reservation
651 ) -> None:
652 rid = reservation.reservation_id
653 runner.invoke(cli, ["coord", "release", rid, "--run-id", "agent-1"])
654 result = runner.invoke(
655 cli, ["coord", "release", rid, "--run-id", "agent-1", "--json"]
656 )
657 data = json.loads(result.output)
658 assert data["status"] == "already_released"
659 assert data["released_at"] is None
660
661 def test_batch_json_skipped_already_released_count(self, repo: pathlib.Path) -> None:
662 reservations = [
663 create_reservation(
664 repo, run_id="skip-agent", branch="main",
665 addresses=[f"src/s{i}.py::f"], ttl_seconds=3600,
666 )
667 for i in range(4)
668 ]
669 # Pre-release 2 of the 4
670 for res in reservations[:2]:
671 create_release(repo, res.reservation_id, run_id="skip-agent")
672
673 result = runner.invoke(
674 cli,
675 ["coord", "release", "--all-for-run", "skip-agent",
676 "--run-id", "skip-agent", "--json"],
677 )
678 data = json.loads(result.output)
679 assert data["skipped_already_released"] == 2
680 assert len(data["released"]) == 2
681
682 def test_single_json_has_duration_ms(self, repo: pathlib.Path, reservation: Reservation) -> None:
683 rid = reservation.reservation_id
684 result = runner.invoke(
685 cli, ["coord", "release", rid, "--run-id", "agent-1", "--json"]
686 )
687 data = json.loads(result.output)
688 assert isinstance(data["duration_ms"], float)
689 assert data["duration_ms"] >= 0.0
690
691 def test_batch_json_has_duration_ms(self, repo: pathlib.Path) -> None:
692 create_reservation(
693 repo, run_id="elapsed-agent", branch="main",
694 addresses=["x.py::f"], ttl_seconds=3600,
695 )
696 result = runner.invoke(
697 cli,
698 ["coord", "release", "--all-for-run", "elapsed-agent",
699 "--run-id", "elapsed-agent", "--json"],
700 )
701 data = json.loads(result.output)
702 assert isinstance(data["duration_ms"], float)
703
704
705 # ---------------------------------------------------------------------------
706 # Concurrent
707 # ---------------------------------------------------------------------------
708
709
710 class TestReleaseConcurrent:
711 def test_two_threads_race_to_release_same_reservation(
712 self, repo: pathlib.Path, reservation: Reservation
713 ) -> None:
714 """Both threads must exit 0; one writes the tombstone, the other sees already_released."""
715 rid = reservation.reservation_id
716 exit_codes: list[int] = []
717 lock = threading.Lock()
718
719 def _release() -> None:
720 result = runner.invoke(
721 cli, ["coord", "release", rid, "--run-id", "agent-race"]
722 )
723 with lock:
724 exit_codes.append(result.exit_code)
725
726 t1 = threading.Thread(target=_release)
727 t2 = threading.Thread(target=_release)
728 t1.start()
729 t2.start()
730 t1.join()
731 t2.join()
732
733 assert exit_codes == [0, 0], f"Expected both exits 0, got {exit_codes}"
734 released = load_released_ids(repo)
735 assert rid in released
736
737 def test_concurrent_batch_releases_idempotent(self, repo: pathlib.Path) -> None:
738 """Multiple agents calling --all-for-run concurrently produce the same final state."""
739 for i in range(10):
740 create_reservation(
741 repo, run_id="concurrent-batch", branch="main",
742 addresses=[f"src/cb{i}.py::f"], ttl_seconds=3600,
743 )
744
745 results: list[int] = []
746 lock = threading.Lock()
747
748 def _batch() -> None:
749 result = runner.invoke(
750 cli,
751 ["coord", "release", "--all-for-run", "concurrent-batch",
752 "--run-id", "concurrent-batch"],
753 )
754 with lock:
755 results.append(result.exit_code)
756
757 threads = [threading.Thread(target=_batch) for _ in range(5)]
758 for t in threads:
759 t.start()
760 for t in threads:
761 t.join()
762
763 assert all(c == 0 for c in results), f"Non-zero exit in concurrent batch: {results}"
764 released = load_released_ids(repo)
765 all_res = load_all_reservations(repo)
766 batch_ids = {r.reservation_id for r in all_res if r.run_id == "concurrent-batch"}
767 assert batch_ids == batch_ids & released
768
769
770 class TestRegisterFlags:
771 def test_default_json_out_is_false(self) -> None:
772 import argparse
773 from muse.cli.commands.release_coord import register
774 p = argparse.ArgumentParser()
775 subs = p.add_subparsers()
776 register(subs)
777 args = p.parse_args(["release", "--run-id", "agent-1"])
778 assert args.json_out is False
779
780 def test_json_flag_sets_json_out(self) -> None:
781 import argparse
782 from muse.cli.commands.release_coord import register
783 p = argparse.ArgumentParser()
784 subs = p.add_subparsers()
785 register(subs)
786 args = p.parse_args(["release", "--run-id", "agent-1", "--json"])
787 assert args.json_out is True
788
789 def test_j_shorthand_sets_json_out(self) -> None:
790 import argparse
791 from muse.cli.commands.release_coord import register
792 p = argparse.ArgumentParser()
793 subs = p.add_subparsers()
794 register(subs)
795 args = p.parse_args(["release", "--run-id", "agent-1", "-j"])
796 assert args.json_out is True
File History 1 commit
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b fix: try fetch/presign before fetch/mpack to avoid Cloudfla… Sonnet 4.6 patch 8 days ago