gabriel / muse public
test_cmd_coord_sync.py python
1,589 lines 70.0 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Comprehensive tests for ``muse coord sync push`` and ``muse coord sync pull``.
2
3 Coverage matrix
4 ---------------
5 Unit
6 ~~~~
7 * _gather_local_records — reads reservations from disk
8 * _gather_local_records — reads heartbeats from disk
9 * _gather_local_records — kinds filter respected
10 * _gather_local_records — corrupt file skipped gracefully
11 * _gather_local_records — claims use claimer_run_id field
12 * _gather_local_records — all 7 kinds gathered
13 * _write_remote_records — writes correct paths under remote/
14 * _write_remote_records — overwrites existing files
15 * _write_remote_records — record with no uuid skipped
16 * _write_remote_records — unknown kind rejected (path traversal prevention)
17 * _write_remote_records — unsafe record_uuid rejected (path traversal prevention)
18 * _write_remote_records — compact JSON written (no indent)
19
20 Integration (all network calls mocked)
21 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22 * push with no local records — text says "(no local coordination records to push)"
23 * push with reservations — calls push_to_hub with correct args
24 * push CoordBusError — exits 1 with error message
25 * pull empty result — prints "Pulled 0 new record(s)"
26 * pull with records — writes files to remote dir
27 * pull CoordBusError — exits 1 with error message
28 * --format json push — valid JSON with inserted/skipped/total/failed/elapsed/schema_version
29 * --format json pull — valid JSON with count/cursor/records/elapsed/schema_version
30 * --json shorthand push — same as --format json
31 * --json shorthand pull — same as --format json
32 * --since-id N — passed through to pull_from_hub
33 * --kinds filter push — restricts gathered kinds
34 * --limit N — passed through to pull_from_hub
35 * duration_ms present in both push and pull JSON output
36
37 Input validation
38 ~~~~~~~~~~~~~~~~
39 * --owner too long → exit 1 before any I/O
40 * --slug too long → exit 1 before any I/O
41 * --since-id negative → exit 1 before any I/O
42 * --limit = 0 → exit 1 before any I/O
43 * --limit > 1000 → exit 1 before any I/O
44 * --limit at boundary 1 → accepted
45 * --limit at boundary 1000 → accepted
46 * push owner/slug validation fires before require_repo
47 * pull since-id/limit validation fires before require_repo
48
49 Security
50 ~~~~~~~~
51 * owner/slug with path traversal chars are passed as strings to push_to_hub
52 * token not echoed in output
53 * _write_remote_records rejects unknown kind (prevents escaping remote/)
54 * _write_remote_records rejects traversal in record_uuid
55
56 Stress
57 ~~~~~~
58 * push 600 records splits into multiple batches
59 * pull returns 1000 records, all written to disk
60 """
61
62 from __future__ import annotations
63
64 import json
65 import pathlib
66 import uuid
67 from typing import TYPE_CHECKING
68
69 import pytest
70 from unittest.mock import patch, MagicMock, call
71
72 from tests.cli_test_helper import CliRunner
73 from muse.core._types import MsgpackDict
74 from muse.core.coord_bus import CoordBusError, JsonDict
75
76 if TYPE_CHECKING:
77 from muse.core.transport import SigningIdentity
78 from muse.cli.commands.coord_sync import (
79 _MAX_OWNER_LEN,
80 _MAX_SLUG_LEN,
81 _MAX_PULL_LIMIT,
82 _ALL_KINDS,
83 )
84
85 runner = CliRunner()
86 cli = None
87
88 _PUSH_TARGET = "muse.cli.commands.coord_sync.push_to_hub"
89 _PULL_TARGET = "muse.cli.commands.coord_sync.pull_from_hub"
90
91
92 # ── Fixtures ──────────────────────────────────────────────────────────────────
93
94
95 @pytest.fixture()
96 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
97 muse_dir = tmp_path / ".muse"
98 muse_dir.mkdir()
99 (muse_dir / "HEAD").write_text("ref: refs/heads/main\n")
100 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
101 return tmp_path
102
103
104 # ── Helpers ───────────────────────────────────────────────────────────────────
105
106
107 def _write_local_claim(repo: pathlib.Path, task_id: str | None = None, run_id: str = "worker-1") -> str:
108 claim_dir = repo / ".muse" / "coordination" / "claims"
109 claim_dir.mkdir(parents=True, exist_ok=True)
110 tid = task_id or str(uuid.uuid4())
111 data = {
112 "task_id": tid,
113 "claimer_run_id": run_id,
114 "claimed_at": "2026-01-01T00:00:00+00:00",
115 "expires_at": "2026-12-31T00:00:00+00:00",
116 }
117 (claim_dir / f"{tid}.json").write_text(json.dumps(data))
118 return tid
119
120
121 def _write_local_reservation(repo: pathlib.Path, run_id: str = "agent-1") -> str:
122 coord_dir = repo / ".muse" / "coordination" / "reservations"
123 coord_dir.mkdir(parents=True, exist_ok=True)
124 rid = str(uuid.uuid4())
125 data = {
126 "reservation_id": rid,
127 "run_id": run_id,
128 "branch": "main",
129 "addresses": ["src/x.py::foo"],
130 "operation": None,
131 "created_at": "2026-01-01T00:00:00+00:00",
132 "expires_at": "2026-12-31T00:00:00+00:00",
133 }
134 (coord_dir / f"{rid}.json").write_text(json.dumps(data))
135 return rid
136
137
138 def _write_local_heartbeat(repo: pathlib.Path, run_id: str = "agent-1") -> str:
139 hb_dir = repo / ".muse" / "coordination" / "heartbeats"
140 hb_dir.mkdir(parents=True, exist_ok=True)
141 data = {
142 "run_id": run_id,
143 "last_seen": "2026-01-01T00:01:00+00:00",
144 "expires_at": "2026-12-31T00:00:00+00:00",
145 }
146 (hb_dir / f"{run_id}.json").write_text(json.dumps(data))
147 return run_id
148
149
150 _PUSH_ARGS = [
151 "coord", "sync", "push",
152 "--hub", "https://localhost:1337",
153 "--owner", "gabriel",
154 "--slug", "myrepo",
155
156 ]
157
158 _PULL_ARGS = [
159 "coord", "sync", "pull",
160 "--hub", "https://localhost:1337",
161 "--owner", "gabriel",
162 "--slug", "myrepo",
163
164 "--since-id", "0",
165 ]
166
167
168 def _push_ok(inserted: int = 1, skipped: int = 0) -> MsgpackDict:
169 return {"inserted": inserted, "skipped": skipped}
170
171
172 def _pull_ok(records: list[MsgpackDict] | None = None, cursor: int = 0) -> MsgpackDict:
173 return {"records": records or [], "cursor": cursor}
174
175
176 # ── Unit: _gather_local_records ───────────────────────────────────────────────
177
178
179 class TestGatherLocalRecords:
180 def test_empty_coordination_dir_returns_empty(self, repo: pathlib.Path) -> None:
181 from muse.cli.commands.coord_sync import _gather_local_records
182 records = _gather_local_records(repo, kinds=["reservation"])
183 assert records == []
184
185 def test_reads_reservation_from_disk(self, repo: pathlib.Path) -> None:
186 from muse.cli.commands.coord_sync import _gather_local_records
187 rid = _write_local_reservation(repo)
188 records = _gather_local_records(repo, kinds=["reservation"])
189 assert len(records) == 1
190 assert records[0]["kind"] == "reservation"
191 assert records[0]["record_uuid"] == rid
192
193 def test_reads_heartbeat_from_disk(self, repo: pathlib.Path) -> None:
194 from muse.cli.commands.coord_sync import _gather_local_records
195 run_id = _write_local_heartbeat(repo, "hb-agent")
196 records = _gather_local_records(repo, kinds=["heartbeat"])
197 assert len(records) == 1
198 assert records[0]["kind"] == "heartbeat"
199 assert records[0]["run_id"] == run_id
200
201 def test_kinds_filter_excludes_heartbeats(self, repo: pathlib.Path) -> None:
202 from muse.cli.commands.coord_sync import _gather_local_records
203 _write_local_reservation(repo)
204 _write_local_heartbeat(repo)
205 records = _gather_local_records(repo, kinds=["reservation"])
206 assert all(r["kind"] == "reservation" for r in records)
207
208 def test_kinds_filter_excludes_reservations(self, repo: pathlib.Path) -> None:
209 from muse.cli.commands.coord_sync import _gather_local_records
210 _write_local_reservation(repo)
211 _write_local_heartbeat(repo)
212 records = _gather_local_records(repo, kinds=["heartbeat"])
213 assert all(r["kind"] == "heartbeat" for r in records)
214
215 def test_corrupt_file_skipped_gracefully(self, repo: pathlib.Path) -> None:
216 from muse.cli.commands.coord_sync import _gather_local_records
217 coord_dir = repo / ".muse" / "coordination" / "reservations"
218 coord_dir.mkdir(parents=True, exist_ok=True)
219 (coord_dir / "bad.json").write_text("not-valid-json{{{{")
220 records = _gather_local_records(repo, kinds=["reservation"])
221 assert records == []
222
223 def test_multiple_records_all_returned(self, repo: pathlib.Path) -> None:
224 from muse.cli.commands.coord_sync import _gather_local_records
225 for i in range(5):
226 _write_local_reservation(repo, run_id=f"agent-{i}")
227 records = _gather_local_records(repo, kinds=["reservation"])
228 assert len(records) == 5
229
230 def test_payload_field_contains_original_data(self, repo: pathlib.Path) -> None:
231 from muse.cli.commands.coord_sync import _gather_local_records
232 rid = _write_local_reservation(repo, run_id="my-agent")
233 records = _gather_local_records(repo, kinds=["reservation"])
234 assert records[0]["payload"]["reservation_id"] == rid
235 assert records[0]["payload"]["run_id"] == "my-agent"
236
237
238 # ── Unit: _write_remote_records ───────────────────────────────────────────────
239
240
241 class TestWriteRemoteRecords:
242 def test_writes_file_to_correct_path(self, repo: pathlib.Path) -> None:
243 from muse.cli.commands.coord_sync import _write_remote_records
244 rec = {"kind": "reservation", "record_uuid": "abc-123", "payload": {"x": 1}}
245 _write_remote_records(repo, [rec])
246 target = repo / ".muse" / "coordination" / "remote" / "reservation" / "abc-123.json"
247 assert target.exists()
248
249 def test_written_file_contains_correct_data(self, repo: pathlib.Path) -> None:
250 from muse.cli.commands.coord_sync import _write_remote_records
251 rec = {"kind": "reservation", "record_uuid": "def-456", "payload": {"y": 2}}
252 _write_remote_records(repo, [rec])
253 target = repo / ".muse" / "coordination" / "remote" / "reservation" / "def-456.json"
254 data = json.loads(target.read_text())
255 assert data["payload"]["y"] == 2
256
257 def test_overwrites_existing_file(self, repo: pathlib.Path) -> None:
258 from muse.cli.commands.coord_sync import _write_remote_records
259 kind_dir = repo / ".muse" / "coordination" / "remote" / "reservation"
260 kind_dir.mkdir(parents=True, exist_ok=True)
261 (kind_dir / "ghi-789.json").write_text('{"old": true}')
262 rec = {"kind": "reservation", "record_uuid": "ghi-789", "payload": {"new": True}}
263 _write_remote_records(repo, [rec])
264 data = json.loads((kind_dir / "ghi-789.json").read_text())
265 assert data["payload"]["new"] is True
266
267 def test_record_without_uuid_skipped(self, repo: pathlib.Path) -> None:
268 from muse.cli.commands.coord_sync import _write_remote_records
269 rec = {"kind": "reservation", "record_uuid": "", "payload": {}}
270 _write_remote_records(repo, [rec])
271 remote_dir = repo / ".muse" / "coordination" / "remote"
272 assert not remote_dir.exists() or not any(remote_dir.rglob("*.json"))
273
274 def test_multiple_kinds_written_to_separate_dirs(self, repo: pathlib.Path) -> None:
275 from muse.cli.commands.coord_sync import _write_remote_records
276 records = [
277 {"kind": "reservation", "record_uuid": "r1", "payload": {}},
278 {"kind": "heartbeat", "record_uuid": "h1", "payload": {}},
279 ]
280 _write_remote_records(repo, records)
281 assert (repo / ".muse" / "coordination" / "remote" / "reservation" / "r1.json").exists()
282 assert (repo / ".muse" / "coordination" / "remote" / "heartbeat" / "h1.json").exists()
283
284
285 # ── Integration: push ─────────────────────────────────────────────────────────
286
287
288 class TestCoordSyncPushIntegration:
289 def test_push_no_local_records_text_output(self, repo: pathlib.Path) -> None:
290 with patch(_PUSH_TARGET) as mock_push:
291 result = runner.invoke(cli, _PUSH_ARGS)
292 assert result.exit_code == 0
293 assert "no local coordination records to push" in result.output
294 mock_push.assert_not_called()
295
296 def test_push_with_reservation_calls_push_to_hub(self, repo: pathlib.Path) -> None:
297 _write_local_reservation(repo)
298 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)) as mock_push:
299 result = runner.invoke(cli, _PUSH_ARGS)
300 assert result.exit_code == 0
301 mock_push.assert_called_once()
302 call_args = mock_push.call_args
303 assert call_args[0][1] == "gabriel" # owner
304 assert call_args[0][2] == "myrepo" # slug
305
306 def test_push_text_output_contains_inserted_skipped(self, repo: pathlib.Path) -> None:
307 _write_local_reservation(repo)
308 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
309 result = runner.invoke(cli, _PUSH_ARGS)
310 assert result.exit_code == 0
311 assert "inserted" in result.output
312 assert "skipped" in result.output
313
314 def test_push_coord_bus_error_exits_1(self, repo: pathlib.Path) -> None:
315 _write_local_reservation(repo)
316 with patch(_PUSH_TARGET, side_effect=CoordBusError("hub down")):
317 result = runner.invoke(cli, _PUSH_ARGS)
318 assert result.exit_code == 1
319 assert "error" in result.output.lower() or "hub down" in result.output
320
321 def test_push_format_json_valid_structure(self, repo: pathlib.Path) -> None:
322 _write_local_reservation(repo)
323 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
324 result = runner.invoke(cli, _PUSH_ARGS + ["--json"])
325 assert result.exit_code == 0
326 data = json.loads(result.output.strip())
327 assert "inserted" in data
328 assert "skipped" in data
329 assert "total" in data
330 assert "failed" in data
331
332 def test_push_json_shorthand(self, repo: pathlib.Path) -> None:
333 _write_local_reservation(repo)
334 with patch(_PUSH_TARGET, return_value=_push_ok(2, 1)):
335 r1 = runner.invoke(cli, _PUSH_ARGS + ["--json"])
336 r2 = runner.invoke(cli, _PUSH_ARGS + ["--json"])
337 d1 = json.loads(r1.output.strip())
338 d2 = json.loads(r2.output.strip())
339 d1.pop("timestamp", None)
340 d2.pop("timestamp", None)
341 d1.pop("duration_ms", None)
342 d2.pop("duration_ms", None)
343 assert d1 == d2
344
345 def test_push_json_no_records_returns_zeros(self, repo: pathlib.Path) -> None:
346 with patch(_PUSH_TARGET):
347 result = runner.invoke(cli, _PUSH_ARGS + ["--json"])
348 assert result.exit_code == 0
349 data = json.loads(result.output.strip())
350 assert data["total"] == 0
351 assert data["inserted"] == 0
352
353 def test_push_kinds_filter_passed_through(self, repo: pathlib.Path) -> None:
354 _write_local_reservation(repo)
355 _write_local_heartbeat(repo)
356 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)) as mock_push:
357 result = runner.invoke(cli, _PUSH_ARGS + ["--kinds", "reservation"])
358 assert result.exit_code == 0
359 # Only 1 kind → batch should contain only reservations
360 batch_arg = mock_push.call_args[0][3]
361 assert all(r["kind"] == "reservation" for r in batch_arg)
362
363 def test_push_coord_bus_error_json_failed_true(self, repo: pathlib.Path) -> None:
364 _write_local_reservation(repo)
365 with patch(_PUSH_TARGET, side_effect=CoordBusError("oops")):
366 result = runner.invoke(cli, _PUSH_ARGS + ["--json"])
367 # exit_code 1; summary JSON (with failed=True) is the last JSON line
368 assert result.exit_code == 1
369 json_lines = [ln for ln in result.output.splitlines() if ln.startswith("{")]
370 assert json_lines, "Expected at least one JSON output line"
371 # The summary JSON is last; the error JSON ({"error": ...}) may appear before it
372 summary = json.loads(json_lines[-1])
373 assert summary["failed"] is True
374
375
376 # ── Integration: pull ─────────────────────────────────────────────────────────
377
378
379 class TestCoordSyncPullIntegration:
380 def test_pull_empty_result_exits_0(self, repo: pathlib.Path) -> None:
381 with patch(_PULL_TARGET, return_value=_pull_ok()):
382 result = runner.invoke(cli, _PULL_ARGS)
383 assert result.exit_code == 0
384 assert "Pulled 0 new record(s)" in result.output
385
386 def test_pull_with_records_writes_files(self, repo: pathlib.Path) -> None:
387 records = [{"kind": "reservation", "record_uuid": "r1", "payload": {"x": 1}}]
388 with patch(_PULL_TARGET, return_value=_pull_ok(records, cursor=1)):
389 result = runner.invoke(cli, _PULL_ARGS)
390 assert result.exit_code == 0
391 target = repo / ".muse" / "coordination" / "remote" / "reservation" / "r1.json"
392 assert target.exists()
393
394 def test_pull_text_output_contains_cursor(self, repo: pathlib.Path) -> None:
395 records = [{"kind": "reservation", "record_uuid": "r42", "payload": {}}]
396 with patch(_PULL_TARGET, return_value=_pull_ok(records, cursor=42)):
397 result = runner.invoke(cli, _PULL_ARGS)
398 assert result.exit_code == 0
399 assert "cursor: 42" in result.output
400
401 def test_pull_coord_bus_error_exits_1(self, repo: pathlib.Path) -> None:
402 with patch(_PULL_TARGET, side_effect=CoordBusError("connection refused")):
403 result = runner.invoke(cli, _PULL_ARGS)
404 assert result.exit_code == 1
405 assert "connection refused" in result.output
406
407 def test_pull_format_json_valid_structure(self, repo: pathlib.Path) -> None:
408 with patch(_PULL_TARGET, return_value=_pull_ok(cursor=7)):
409 result = runner.invoke(cli, _PULL_ARGS + ["--json"])
410 assert result.exit_code == 0
411 data = json.loads(result.output.strip())
412 assert "count" in data
413 assert "cursor" in data
414 assert "records" in data
415
416 def test_pull_json_shorthand(self, repo: pathlib.Path) -> None:
417 with patch(_PULL_TARGET, return_value=_pull_ok(cursor=3)):
418 r1 = runner.invoke(cli, _PULL_ARGS + ["--json"])
419 r2 = runner.invoke(cli, _PULL_ARGS + ["--json"])
420 d1 = json.loads(r1.output.strip())
421 d2 = json.loads(r2.output.strip())
422 d1.pop("timestamp", None)
423 d2.pop("timestamp", None)
424 d1.pop("duration_ms", None)
425 d2.pop("duration_ms", None)
426 assert d1 == d2
427
428 def test_pull_since_id_passed_to_pull_from_hub(self, repo: pathlib.Path) -> None:
429 with patch(_PULL_TARGET, return_value=_pull_ok()) as mock_pull:
430 runner.invoke(cli, _PULL_ARGS[:-2] + ["--since-id", "99"])
431 assert mock_pull.called
432 call_args = mock_pull.call_args[0]
433 assert call_args[3] == 99 # since_id
434
435 def test_pull_limit_passed_to_pull_from_hub(self, repo: pathlib.Path) -> None:
436 with patch(_PULL_TARGET, return_value=_pull_ok()) as mock_pull:
437 runner.invoke(cli, _PULL_ARGS + ["--limit", "42"])
438 call_args = mock_pull.call_args[0]
439 assert call_args[5] == 42 # limit
440
441 def test_pull_kinds_filter_passed_to_pull_from_hub(self, repo: pathlib.Path) -> None:
442 with patch(_PULL_TARGET, return_value=_pull_ok()) as mock_pull:
443 runner.invoke(cli, _PULL_ARGS + ["--kinds", "reservation", "heartbeat"])
444 call_args = mock_pull.call_args[0]
445 assert "reservation" in call_args[4]
446 assert "heartbeat" in call_args[4]
447
448 def test_pull_json_count_matches_records_length(self, repo: pathlib.Path) -> None:
449 records = [
450 {"kind": "reservation", "record_uuid": f"r{i}", "payload": {}}
451 for i in range(5)
452 ]
453 with patch(_PULL_TARGET, return_value=_pull_ok(records, cursor=5)):
454 result = runner.invoke(cli, _PULL_ARGS + ["--json"])
455 data = json.loads(result.output.strip())
456 assert data["count"] == 5
457 assert len(data["records"]) == 5
458
459 def test_pull_signing_passed_to_pull_from_hub(self, repo: pathlib.Path) -> None:
460 with patch(_PULL_TARGET, return_value=_pull_ok()) as mock_pull:
461 runner.invoke(cli, _PULL_ARGS)
462 call_args = mock_pull.call_args[0]
463 assert call_args[6] is None # signing (no identity configured in test)
464
465
466 # ── Security ──────────────────────────────────────────────────────────────────
467
468
469 class TestCoordSyncSecurity:
470 def test_push_traversal_owner_passed_as_string(self, repo: pathlib.Path) -> None:
471 """Path-traversal chars in owner are passed as-is to push_to_hub (encoding is push_to_hub's job)."""
472 _write_local_reservation(repo)
473 evil_owner = "../evil"
474 args = [
475 "coord", "sync", "push",
476 "--hub", "https://localhost:1337",
477 "--owner", evil_owner,
478 "--slug", "myrepo",
479
480 ]
481 with patch(_PUSH_TARGET, return_value=_push_ok()) as mock_push:
482 runner.invoke(cli, args)
483 if mock_push.called:
484 call_args = mock_push.call_args[0]
485 assert call_args[1] == evil_owner # owner string passed verbatim
486
487 def test_token_not_in_output(self, repo: pathlib.Path) -> None:
488 _write_local_reservation(repo)
489 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
490 result = runner.invoke(cli, _PUSH_ARGS)
491 assert "tok" not in result.output
492
493 def test_pull_token_not_in_output(self, repo: pathlib.Path) -> None:
494 with patch(_PULL_TARGET, return_value=_pull_ok()):
495 result = runner.invoke(cli, _PULL_ARGS)
496 assert "tok" not in result.output
497
498 def test_write_remote_records_rejects_unknown_kind(self, repo: pathlib.Path) -> None:
499 """Server-supplied kind '../../evil' must not escape remote/ directory."""
500 from muse.cli.commands.coord_sync import _write_remote_records
501 rec = {"kind": "../../evil", "record_uuid": "abc123", "payload": {}}
502 _write_remote_records(repo, [rec])
503 remote_dir = repo / ".muse" / "coordination" / "remote"
504 assert not remote_dir.exists() or not any(remote_dir.rglob("*.json"))
505
506 def test_write_remote_records_rejects_traversal_uuid(self, repo: pathlib.Path) -> None:
507 """Server-supplied record_uuid '../../../etc/passwd' must not escape kind dir."""
508 from muse.cli.commands.coord_sync import _write_remote_records
509 rec = {"kind": "reservation", "record_uuid": "../../../etc/passwd", "payload": {}}
510 _write_remote_records(repo, [rec])
511 remote_dir = repo / ".muse" / "coordination" / "remote"
512 assert not remote_dir.exists() or not any(remote_dir.rglob("*.json"))
513
514
515 # ── Unit: _gather_local_records — claim field name ─────────────────────────────
516
517
518 class TestGatherLocalRecordsClaims:
519 def test_claim_uses_claimer_run_id_field(self, repo: pathlib.Path) -> None:
520 """Bug fix: claims must read 'claimer_run_id', not 'claimed_by'."""
521 from muse.cli.commands.coord_sync import _gather_local_records
522 tid = _write_local_claim(repo, run_id="correct-worker")
523 records = _gather_local_records(repo, kinds=["claim"])
524 assert len(records) == 1
525 assert records[0]["run_id"] == "correct-worker"
526
527 def test_claim_record_uuid_is_task_id(self, repo: pathlib.Path) -> None:
528 from muse.cli.commands.coord_sync import _gather_local_records
529 tid = _write_local_claim(repo)
530 records = _gather_local_records(repo, kinds=["claim"])
531 assert records[0]["record_uuid"] == tid
532
533 def test_claim_expires_at_included(self, repo: pathlib.Path) -> None:
534 from muse.cli.commands.coord_sync import _gather_local_records
535 _write_local_claim(repo)
536 records = _gather_local_records(repo, kinds=["claim"])
537 assert records[0]["expires_at"] is not None
538
539
540 # ── Unit: _write_remote_records — compact JSON ────────────────────────────────
541
542
543 class TestWriteRemoteRecordsCompact:
544 def test_written_json_is_compact(self, repo: pathlib.Path) -> None:
545 """No indent=2 — remote files must be compact single-line JSON."""
546 from muse.cli.commands.coord_sync import _write_remote_records
547 rec = {"kind": "reservation", "record_uuid": "compact-test", "payload": {"x": 1}}
548 _write_remote_records(repo, [rec])
549 target = repo / ".muse" / "coordination" / "remote" / "reservation" / "compact-test.json"
550 raw = target.read_text().strip()
551 # Compact JSON has no interior newlines
552 assert "\n" not in raw
553
554 def test_valid_kinds_all_accepted(self, repo: pathlib.Path) -> None:
555 """All 7 kinds are accepted by the allowlist."""
556 from muse.cli.commands.coord_sync import _write_remote_records
557 records = [
558 {"kind": k, "record_uuid": f"id-{i}", "payload": {}}
559 for i, k in enumerate(_ALL_KINDS)
560 ]
561 _write_remote_records(repo, records)
562 remote_dir = repo / ".muse" / "coordination" / "remote"
563 written = list(remote_dir.rglob("*.json"))
564 assert len(written) == len(_ALL_KINDS)
565
566 def test_empty_uuid_still_skipped(self, repo: pathlib.Path) -> None:
567 from muse.cli.commands.coord_sync import _write_remote_records
568 rec = {"kind": "reservation", "record_uuid": "", "payload": {}}
569 _write_remote_records(repo, [rec])
570 remote_dir = repo / ".muse" / "coordination" / "remote"
571 assert not remote_dir.exists() or not any(remote_dir.rglob("*.json"))
572
573 def test_uuid_with_dots_rejected(self, repo: pathlib.Path) -> None:
574 """Dots in record_uuid are not allowed (could be used for traversal)."""
575 from muse.cli.commands.coord_sync import _write_remote_records
576 rec = {"kind": "reservation", "record_uuid": "..evil", "payload": {}}
577 _write_remote_records(repo, [rec])
578 remote_dir = repo / ".muse" / "coordination" / "remote"
579 assert not remote_dir.exists() or not any(remote_dir.rglob("*.json"))
580
581 def test_uuid_with_slash_rejected(self, repo: pathlib.Path) -> None:
582 from muse.cli.commands.coord_sync import _write_remote_records
583 rec = {"kind": "reservation", "record_uuid": "a/b", "payload": {}}
584 _write_remote_records(repo, [rec])
585 remote_dir = repo / ".muse" / "coordination" / "remote"
586 assert not remote_dir.exists() or not any(remote_dir.rglob("*.json"))
587
588 def test_uuid_too_long_rejected(self, repo: pathlib.Path) -> None:
589 """UUIDs over 128 chars are rejected."""
590 from muse.cli.commands.coord_sync import _write_remote_records
591 rec = {"kind": "reservation", "record_uuid": "a" * 129, "payload": {}}
592 _write_remote_records(repo, [rec])
593 remote_dir = repo / ".muse" / "coordination" / "remote"
594 assert not remote_dir.exists() or not any(remote_dir.rglob("*.json"))
595
596
597 # ── Input validation ──────────────────────────────────────────────────────────
598
599
600 class TestSyncInputValidation:
601 def _push_args(self, owner: str = "gabriel", slug: str = "myrepo", extra: list[str] | None = None) -> list[str]:
602 args = [
603 "coord", "sync", "push",
604 "--hub", "https://localhost:1337",
605 "--owner", owner,
606 "--slug", slug,
607
608 ]
609 if extra:
610 args.extend(extra)
611 return args
612
613 def _pull_args(self, owner: str = "gabriel", slug: str = "myrepo", extra: list[str] | None = None) -> list[str]:
614 args = [
615 "coord", "sync", "pull",
616 "--hub", "https://localhost:1337",
617 "--owner", owner,
618 "--slug", slug,
619
620 ]
621 if extra:
622 args.extend(extra)
623 return args
624
625 def test_push_owner_too_long_exits_1(self, repo: pathlib.Path) -> None:
626 owner = "x" * (_MAX_OWNER_LEN + 1)
627 result = runner.invoke(cli, self._push_args(owner=owner))
628 assert result.exit_code == 1
629
630 def test_push_slug_too_long_exits_1(self, repo: pathlib.Path) -> None:
631 slug = "x" * (_MAX_SLUG_LEN + 1)
632 result = runner.invoke(cli, self._push_args(slug=slug))
633 assert result.exit_code == 1
634
635 def test_push_owner_at_max_accepted(self, repo: pathlib.Path) -> None:
636 owner = "x" * _MAX_OWNER_LEN
637 with patch(_PUSH_TARGET, return_value=_push_ok()):
638 result = runner.invoke(cli, self._push_args(owner=owner))
639 # No validation error — exits 0 (no records to push)
640 assert result.exit_code == 0
641
642 def test_push_slug_at_max_accepted(self, repo: pathlib.Path) -> None:
643 slug = "x" * _MAX_SLUG_LEN
644 with patch(_PUSH_TARGET, return_value=_push_ok()):
645 result = runner.invoke(cli, self._push_args(slug=slug))
646 assert result.exit_code == 0
647
648 def test_push_owner_too_long_json_error(self, repo: pathlib.Path) -> None:
649 owner = "x" * (_MAX_OWNER_LEN + 1)
650 result = runner.invoke(cli, self._push_args(owner=owner) + ["--json"])
651 assert result.exit_code == 1
652 data = json.loads(result.output.strip())
653 assert data["status"] == "bad_args"
654
655 def test_pull_owner_too_long_exits_1(self, repo: pathlib.Path) -> None:
656 owner = "x" * (_MAX_OWNER_LEN + 1)
657 result = runner.invoke(cli, self._pull_args(owner=owner))
658 assert result.exit_code == 1
659
660 def test_pull_slug_too_long_exits_1(self, repo: pathlib.Path) -> None:
661 slug = "x" * (_MAX_SLUG_LEN + 1)
662 result = runner.invoke(cli, self._pull_args(slug=slug))
663 assert result.exit_code == 1
664
665 def test_pull_since_id_negative_exits_1(self, repo: pathlib.Path) -> None:
666 result = runner.invoke(cli, self._pull_args(extra=["--since-id", "-1"]))
667 assert result.exit_code == 1
668
669 def test_pull_since_id_negative_json_error(self, repo: pathlib.Path) -> None:
670 result = runner.invoke(cli, self._pull_args(extra=["--since-id", "-1", "--json"]))
671 assert result.exit_code == 1
672 data = json.loads(result.output.strip())
673 assert data["status"] == "bad_args"
674
675 def test_pull_limit_zero_exits_1(self, repo: pathlib.Path) -> None:
676 result = runner.invoke(cli, self._pull_args(extra=["--limit", "0"]))
677 assert result.exit_code == 1
678
679 def test_pull_limit_over_max_exits_1(self, repo: pathlib.Path) -> None:
680 result = runner.invoke(cli, self._pull_args(extra=["--limit", str(_MAX_PULL_LIMIT + 1)]))
681 assert result.exit_code == 1
682
683 def test_pull_limit_at_min_accepted(self, repo: pathlib.Path) -> None:
684 with patch(_PULL_TARGET, return_value=_pull_ok()):
685 result = runner.invoke(cli, self._pull_args(extra=["--limit", "1"]))
686 assert result.exit_code == 0
687
688 def test_pull_limit_at_max_accepted(self, repo: pathlib.Path) -> None:
689 with patch(_PULL_TARGET, return_value=_pull_ok()):
690 result = runner.invoke(cli, self._pull_args(extra=["--limit", str(_MAX_PULL_LIMIT)]))
691 assert result.exit_code == 0
692
693 def test_pull_limit_over_max_json_error(self, repo: pathlib.Path) -> None:
694 result = runner.invoke(
695 cli,
696 self._pull_args(extra=["--limit", str(_MAX_PULL_LIMIT + 1), "--json"]),
697 )
698 assert result.exit_code == 1
699 data = json.loads(result.output.strip())
700 assert data["status"] == "bad_args"
701
702 def test_push_validation_fires_before_repo_lookup(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
703 """Validation must not touch filesystem — no repo needed."""
704 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) # no .muse dir
705 owner = "x" * (_MAX_OWNER_LEN + 1)
706 result = runner.invoke(cli, [
707 "coord", "sync", "push",
708 "--hub", "https://localhost:1337",
709 "--owner", owner,
710 "--slug", "myrepo",
711
712 ])
713 assert result.exit_code == 1
714
715 def test_pull_validation_fires_before_repo_lookup(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
716 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) # no .muse dir
717 result = runner.invoke(cli, [
718 "coord", "sync", "pull",
719 "--hub", "https://localhost:1337",
720 "--owner", "gabriel",
721 "--slug", "myrepo",
722
723 "--since-id", "-5",
724 ])
725 assert result.exit_code == 1
726
727
728 # ── JSON schema: schema_version and duration_ms ──────────────────────────
729
730
731 class TestSyncJsonSchema:
732 def test_push_json_includes_schema_version(self, repo: pathlib.Path) -> None:
733 _write_local_reservation(repo)
734 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
735 result = runner.invoke(cli, _PUSH_ARGS + ["--json"])
736 assert result.exit_code == 0
737 data = json.loads(result.output.strip())
738 assert "schema" in data
739
740 def test_push_json_includes_duration_ms(self, repo: pathlib.Path) -> None:
741 _write_local_reservation(repo)
742 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
743 result = runner.invoke(cli, _PUSH_ARGS + ["--json"])
744 assert result.exit_code == 0
745 data = json.loads(result.output.strip())
746 assert "duration_ms" in data
747 assert isinstance(data["duration_ms"], float)
748
749 def test_push_json_no_records_includes_elapsed(self, repo: pathlib.Path) -> None:
750 with patch(_PUSH_TARGET):
751 result = runner.invoke(cli, _PUSH_ARGS + ["--json"])
752 assert result.exit_code == 0
753 data = json.loads(result.output.strip())
754 assert "duration_ms" in data
755
756 def test_pull_json_includes_schema_version(self, repo: pathlib.Path) -> None:
757 with patch(_PULL_TARGET, return_value=_pull_ok(cursor=3)):
758 result = runner.invoke(cli, _PULL_ARGS + ["--json"])
759 assert result.exit_code == 0
760 data = json.loads(result.output.strip())
761 assert "schema" in data
762
763 def test_pull_json_includes_duration_ms(self, repo: pathlib.Path) -> None:
764 with patch(_PULL_TARGET, return_value=_pull_ok(cursor=3)):
765 result = runner.invoke(cli, _PULL_ARGS + ["--json"])
766 assert result.exit_code == 0
767 data = json.loads(result.output.strip())
768 assert "duration_ms" in data
769 assert isinstance(data["duration_ms"], float)
770
771 def test_push_text_includes_elapsed(self, repo: pathlib.Path) -> None:
772 _write_local_reservation(repo)
773 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
774 result = runner.invoke(cli, _PUSH_ARGS)
775 assert result.exit_code == 0
776 # elapsed is formatted as (Ns)
777 assert "s)" in result.output
778
779 def test_pull_text_includes_elapsed(self, repo: pathlib.Path) -> None:
780 with patch(_PULL_TARGET, return_value=_pull_ok(cursor=1)):
781 result = runner.invoke(cli, _PULL_ARGS)
782 assert result.exit_code == 0
783 assert "s)" in result.output
784
785
786 # ── Gather all 7 kinds ────────────────────────────────────────────────────────
787
788
789 class TestGatherAllKinds:
790 def _write_intent(self, repo: pathlib.Path) -> str:
791 d = repo / ".muse" / "coordination" / "intents"
792 d.mkdir(parents=True, exist_ok=True)
793 iid = str(uuid.uuid4())
794 data = {"intent_id": iid, "run_id": "agent", "expires_at": None}
795 (d / f"{iid}.json").write_text(json.dumps(data))
796 return iid
797
798 def _write_release(self, repo: pathlib.Path) -> str:
799 d = repo / ".muse" / "coordination" / "releases"
800 d.mkdir(parents=True, exist_ok=True)
801 rid = str(uuid.uuid4())
802 data = {"release_id": rid, "run_id": "agent"}
803 (d / f"{rid}.json").write_text(json.dumps(data))
804 return rid
805
806 def _write_dependency(self, repo: pathlib.Path) -> str:
807 d = repo / ".muse" / "coordination" / "dependencies"
808 d.mkdir(parents=True, exist_ok=True)
809 rid = str(uuid.uuid4())
810 data = {"reservation_id": rid}
811 (d / f"{rid}.json").write_text(json.dumps(data))
812 return rid
813
814 def _write_task(self, repo: pathlib.Path) -> str:
815 d = repo / ".muse" / "coordination" / "tasks"
816 d.mkdir(parents=True, exist_ok=True)
817 tid = str(uuid.uuid4())
818 data = {"task_id": tid, "run_id": "creator"}
819 (d / f"{tid}.json").write_text(json.dumps(data))
820 return tid
821
822 def test_all_kinds_gathered(self, repo: pathlib.Path) -> None:
823 from muse.cli.commands.coord_sync import _gather_local_records
824 _write_local_reservation(repo)
825 _write_local_heartbeat(repo, "hb-1")
826 self._write_intent(repo)
827 self._write_release(repo)
828 self._write_dependency(repo)
829 self._write_task(repo)
830 _write_local_claim(repo)
831 records = _gather_local_records(repo, kinds=list(_ALL_KINDS))
832 kinds_found = {r["kind"] for r in records}
833 assert kinds_found == set(_ALL_KINDS)
834
835 def test_each_kind_has_correct_record_uuid(self, repo: pathlib.Path) -> None:
836 from muse.cli.commands.coord_sync import _gather_local_records
837 rid = _write_local_reservation(repo)
838 records = _gather_local_records(repo, kinds=["reservation"])
839 assert records[0]["record_uuid"] == rid
840
841 def test_release_has_none_expires_at(self, repo: pathlib.Path) -> None:
842 from muse.cli.commands.coord_sync import _gather_local_records
843 self._write_release(repo)
844 records = _gather_local_records(repo, kinds=["release"])
845 assert records[0]["expires_at"] is None
846
847 def test_dependency_has_none_expires_at(self, repo: pathlib.Path) -> None:
848 from muse.cli.commands.coord_sync import _gather_local_records
849 self._write_dependency(repo)
850 records = _gather_local_records(repo, kinds=["dependency"])
851 assert records[0]["expires_at"] is None
852
853 def test_task_has_none_expires_at(self, repo: pathlib.Path) -> None:
854 from muse.cli.commands.coord_sync import _gather_local_records
855 self._write_task(repo)
856 records = _gather_local_records(repo, kinds=["task"])
857 assert records[0]["expires_at"] is None
858
859
860 # ── Stress tests ──────────────────────────────────────────────────────────────
861
862
863 class TestSyncStress:
864 def test_push_600_records_batched(self, repo: pathlib.Path) -> None:
865 """600 records must be split across ≥ 2 batches of MAX_PUSH_BATCH."""
866 from muse.core.coord_bus import MAX_PUSH_BATCH
867 for i in range(600):
868 _write_local_reservation(repo, run_id=f"agent-{i}")
869 call_count = 0
870
871 def fake_push(hub: str, owner: str, slug: str, batch: list[JsonDict], token: SigningIdentity | None) -> MsgpackDict:
872 nonlocal call_count
873 call_count += 1
874 assert len(batch) <= MAX_PUSH_BATCH
875 return {"inserted": len(batch), "skipped": 0}
876
877 with patch(_PUSH_TARGET, side_effect=fake_push):
878 result = runner.invoke(cli, _PUSH_ARGS + ["--kinds", "reservation"])
879 assert result.exit_code == 0
880 assert call_count >= 2
881
882 def test_push_600_records_inserted_count_correct(self, repo: pathlib.Path) -> None:
883 for i in range(600):
884 _write_local_reservation(repo, run_id=f"agent-{i}")
885 with patch(_PUSH_TARGET, return_value=_push_ok(inserted=1, skipped=0)) as mock:
886 result = runner.invoke(cli, _PUSH_ARGS + ["--kinds", "reservation", "--json"])
887 assert result.exit_code == 0
888 data = json.loads(result.output.strip())
889 assert data["total"] == 600
890
891 def test_pull_1000_records_all_written(self, repo: pathlib.Path) -> None:
892 records = [
893 {"kind": "reservation", "record_uuid": str(uuid.uuid4()), "payload": {}}
894 for _ in range(1000)
895 ]
896 with patch(_PULL_TARGET, return_value=_pull_ok(records, cursor=1000)):
897 result = runner.invoke(cli, _PULL_ARGS + ["--limit", "1000"])
898 assert result.exit_code == 0
899 remote_dir = repo / ".muse" / "coordination" / "remote" / "reservation"
900 written = list(remote_dir.glob("*.json"))
901 assert len(written) == 1000
902
903 def test_pull_mixed_invalid_records_skipped(self, repo: pathlib.Path) -> None:
904 """Records with invalid kind/uuid are skipped; valid ones still written."""
905 good = {"kind": "reservation", "record_uuid": "good-uuid-1", "payload": {}}
906 bad_kind = {"kind": "../evil", "record_uuid": "evil-uuid", "payload": {}}
907 bad_uuid = {"kind": "reservation", "record_uuid": "../etc/passwd", "payload": {}}
908 with patch(_PULL_TARGET, return_value=_pull_ok([good, bad_kind, bad_uuid], cursor=3)):
909 result = runner.invoke(cli, _PULL_ARGS)
910 assert result.exit_code == 0
911 good_file = repo / ".muse" / "coordination" / "remote" / "reservation" / "good-uuid-1.json"
912 assert good_file.exists()
913 # Only 1 file written (the good record)
914 remote_dir = repo / ".muse" / "coordination" / "remote"
915 all_files = list(remote_dir.rglob("*.json"))
916 assert len(all_files) == 1
917
918 def test_push_partial_failure_reports_failed_true(self, repo: pathlib.Path) -> None:
919 """If one batch fails, failed=True in JSON even if other batches succeed."""
920 from muse.core.coord_bus import MAX_PUSH_BATCH
921 # Write enough for 2 batches
922 for i in range(MAX_PUSH_BATCH + 1):
923 _write_local_reservation(repo, run_id=f"agent-{i}")
924
925 call_count = 0
926 def sometimes_fail(hub: str, owner: str, slug: str, batch: list[JsonDict], token: SigningIdentity | None) -> MsgpackDict:
927 nonlocal call_count
928 call_count += 1
929 if call_count == 1:
930 raise CoordBusError("first batch failed")
931 return {"inserted": len(batch), "skipped": 0}
932
933 with patch(_PUSH_TARGET, side_effect=sometimes_fail):
934 result = runner.invoke(
935 cli, _PUSH_ARGS + ["--kinds", "reservation", "--json"]
936 )
937 assert result.exit_code == 1
938 # Find JSON line (error from first batch goes to stdout too in JSON mode)
939 json_lines = [ln for ln in result.output.splitlines() if ln.startswith("{")]
940 final = json.loads(json_lines[-1])
941 assert final["failed"] is True
942
943
944 # ---------------------------------------------------------------------------
945 # Extended — muse coord sync push
946 # ---------------------------------------------------------------------------
947
948
949 class TestCoordSyncPushExtended:
950 def test_j_alias_works(self, repo: pathlib.Path) -> None:
951 """-j is equivalent to --json for push."""
952 with patch(_PUSH_TARGET, return_value=_push_ok(0, 0)):
953 result = runner.invoke(cli, _PUSH_ARGS + ["-j"])
954 assert result.exit_code == 0, result.output
955 data = json.loads(result.output.strip())
956 assert "inserted" in data
957
958 def test_help_flag(self, repo: pathlib.Path) -> None:
959 result = runner.invoke(cli, ["coord", "sync", "push", "--help"])
960 assert result.exit_code == 0
961
962 def test_json_compact_single_line(self, repo: pathlib.Path) -> None:
963 """JSON output is a single compact line — no indent=2."""
964 _write_local_reservation(repo)
965 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
966 result = runner.invoke(cli, _PUSH_ARGS + ["-j"])
967 assert result.exit_code == 0
968 lines = [l for l in result.output.splitlines() if l.strip().startswith("{")]
969 assert len(lines) == 1, f"Expected compact JSON, got: {result.output!r}"
970
971 def test_json_all_required_fields(self, repo: pathlib.Path) -> None:
972 """JSON always has schema_version, inserted, skipped, total, failed, duration_ms."""
973 _write_local_reservation(repo)
974 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
975 result = runner.invoke(cli, _PUSH_ARGS + ["-j"])
976 data = json.loads(result.output.strip())
977 for field in ("schema", "inserted", "skipped", "total", "failed", "duration_ms"):
978 assert field in data, f"Missing field: {field}"
979
980 def test_json_inserted_is_int(self, repo: pathlib.Path) -> None:
981 _write_local_reservation(repo)
982 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
983 result = runner.invoke(cli, _PUSH_ARGS + ["-j"])
984 data = json.loads(result.output.strip())
985 assert isinstance(data["inserted"], int)
986 assert isinstance(data["skipped"], int)
987 assert isinstance(data["total"], int)
988
989 def test_json_failed_is_bool(self, repo: pathlib.Path) -> None:
990 with patch(_PUSH_TARGET, return_value=_push_ok(0, 0)):
991 result = runner.invoke(cli, _PUSH_ARGS + ["-j"])
992 data = json.loads(result.output.strip())
993 assert isinstance(data["failed"], bool)
994 assert data["failed"] is False
995
996 def test_json_duration_ms_is_number(self, repo: pathlib.Path) -> None:
997 with patch(_PUSH_TARGET, return_value=_push_ok(0, 0)):
998 result = runner.invoke(cli, _PUSH_ARGS + ["-j"])
999 data = json.loads(result.output.strip())
1000 assert isinstance(data["duration_ms"], (int, float))
1001 assert data["duration_ms"] >= 0
1002
1003 def test_json_schema_is_int(self, repo: pathlib.Path) -> None:
1004 with patch(_PUSH_TARGET, return_value=_push_ok(0, 0)):
1005 result = runner.invoke(cli, _PUSH_ARGS + ["-j"])
1006 data = json.loads(result.output.strip())
1007 assert isinstance(data["schema"], int)
1008 assert data["schema"] >= 1
1009
1010 def test_json_total_matches_gathered_records(self, repo: pathlib.Path) -> None:
1011 _write_local_reservation(repo)
1012 _write_local_reservation(repo)
1013 with patch(_PUSH_TARGET, return_value=_push_ok(2, 0)):
1014 result = runner.invoke(cli, _PUSH_ARGS + ["-j"])
1015 data = json.loads(result.output.strip())
1016 assert data["total"] == 2
1017
1018 def test_json_no_records_total_is_zero(self, repo: pathlib.Path) -> None:
1019 result = runner.invoke(cli, _PUSH_ARGS + ["-j"])
1020 assert result.exit_code == 0
1021 data = json.loads(result.output.strip())
1022 assert data["total"] == 0
1023 assert data["inserted"] == 0
1024 assert data["skipped"] == 0
1025
1026 def test_idempotent_second_push_all_skipped(self, repo: pathlib.Path) -> None:
1027 """Second push: all records skipped (hub already has them)."""
1028 _write_local_reservation(repo)
1029 with patch(_PUSH_TARGET, return_value=_push_ok(0, 1)):
1030 result = runner.invoke(cli, _PUSH_ARGS + ["-j"])
1031 data = json.loads(result.output.strip())
1032 assert data["skipped"] == 1
1033 assert data["inserted"] == 0
1034
1035 def test_all_7_kinds_pushed(self, repo: pathlib.Path) -> None:
1036 """Push with all 7 kinds gathers records from every kind directory."""
1037 coord_dir = repo / ".muse" / "coordination"
1038 kind_dirs = {
1039 "reservations": ("reservation_id", "run_id"),
1040 "heartbeats": ("run_id", "run_id"),
1041 "intents": ("intent_id", "run_id"),
1042 "releases": ("release_id", "run_id"),
1043 "dependencies": ("reservation_id", "reservation_id"),
1044 "tasks": ("task_id", "run_id"),
1045 "claims": ("task_id", "claimer_run_id"),
1046 }
1047 import uuid as _uuid
1048 for subdir, (id_field, run_field) in kind_dirs.items():
1049 d = coord_dir / subdir
1050 d.mkdir(parents=True, exist_ok=True)
1051 rid = str(_uuid.uuid4())
1052 (d / f"{rid}.json").write_text(json.dumps({id_field: rid, run_field: "r1"}))
1053
1054 with patch(_PUSH_TARGET, return_value=_push_ok(7, 0)) as mock_push:
1055 result = runner.invoke(cli, _PUSH_ARGS + ["-j"])
1056 assert result.exit_code == 0
1057 data = json.loads(result.output.strip())
1058 assert data["total"] == 7
1059
1060 def test_text_output_shows_owner_slug(self, repo: pathlib.Path) -> None:
1061 _write_local_reservation(repo)
1062 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
1063 result = runner.invoke(cli, _PUSH_ARGS)
1064 assert "gabriel" in result.output
1065 assert "myrepo" in result.output
1066
1067 def test_text_output_shows_checkmark_on_success(self, repo: pathlib.Path) -> None:
1068 _write_local_reservation(repo)
1069 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
1070 result = runner.invoke(cli, _PUSH_ARGS)
1071 assert "✅" in result.output
1072
1073 def test_text_output_shows_cross_on_failure(self, repo: pathlib.Path) -> None:
1074 _write_local_reservation(repo)
1075 with patch(_PUSH_TARGET, side_effect=CoordBusError("hub down")):
1076 result = runner.invoke(cli, _PUSH_ARGS)
1077 assert "❌" in result.output or result.exit_code == 1
1078
1079 def test_help_shows_agent_quickstart(self, repo: pathlib.Path) -> None:
1080 result = runner.invoke(cli, ["coord", "sync", "push", "--help"])
1081 assert "Agent quickstart" in result.output
1082
1083 def test_help_shows_json_schema(self, repo: pathlib.Path) -> None:
1084 result = runner.invoke(cli, ["coord", "sync", "push", "--help"])
1085 assert "JSON output schema" in result.output
1086
1087 def test_help_shows_exit_codes(self, repo: pathlib.Path) -> None:
1088 result = runner.invoke(cli, ["coord", "sync", "push", "--help"])
1089 assert "Exit codes" in result.output
1090
1091
1092 # ---------------------------------------------------------------------------
1093 # Security — muse coord sync push
1094 # ---------------------------------------------------------------------------
1095
1096
1097 class TestCoordSyncPushSecurity:
1098 def test_ansi_in_owner_sanitized_in_text_output(self, repo: pathlib.Path) -> None:
1099 """ANSI codes in --owner must not bleed into text output."""
1100 _write_local_reservation(repo)
1101 ansi_owner = "\x1b[31mevil\x1b[0m"
1102 push_args = [
1103 "coord", "sync", "push",
1104 "--hub", "https://localhost:1337",
1105 "--owner", ansi_owner,
1106 "--slug", "myrepo",
1107
1108 ]
1109 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
1110 result = runner.invoke(cli, push_args)
1111 assert "\x1b" not in result.output
1112
1113 def test_ansi_in_slug_sanitized_in_text_output(self, repo: pathlib.Path) -> None:
1114 _write_local_reservation(repo)
1115 ansi_slug = "\x1b[32minjected\x1b[0m"
1116 push_args = [
1117 "coord", "sync", "push",
1118 "--hub", "https://localhost:1337",
1119 "--owner", "gabriel",
1120 "--slug", ansi_slug,
1121
1122 ]
1123 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
1124 result = runner.invoke(cli, push_args)
1125 assert "\x1b" not in result.output
1126
1127 def test_token_not_in_json_output(self, repo: pathlib.Path) -> None:
1128 """Auth token must never appear in JSON output."""
1129 _write_local_reservation(repo)
1130 secret = "super-secret-token-xyz"
1131 push_args = [
1132 "coord", "sync", "push",
1133 "--hub", "https://localhost:1337",
1134 "--owner", "gabriel",
1135 "--slug", "myrepo",
1136
1137 "--json",
1138 ]
1139 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
1140 result = runner.invoke(cli, push_args)
1141 assert secret not in result.output
1142
1143 def test_token_not_in_text_output(self, repo: pathlib.Path) -> None:
1144 _write_local_reservation(repo)
1145 secret = "super-secret-token-abc"
1146 push_args = [
1147 "coord", "sync", "push",
1148 "--hub", "https://localhost:1337",
1149 "--owner", "gabriel",
1150 "--slug", "myrepo",
1151
1152 ]
1153 with patch(_PUSH_TARGET, return_value=_push_ok(1, 0)):
1154 result = runner.invoke(cli, push_args)
1155 assert secret not in result.output
1156
1157 def test_no_traceback_on_coord_bus_error(self, repo: pathlib.Path) -> None:
1158 _write_local_reservation(repo)
1159 with patch(_PUSH_TARGET, side_effect=CoordBusError("network failure")):
1160 result = runner.invoke(cli, _PUSH_ARGS)
1161 assert "Traceback" not in result.output
1162
1163 def test_owner_length_cap_before_io(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
1164 """Owner length check fires before any file system access."""
1165 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
1166 long_owner = "x" * (_MAX_OWNER_LEN + 1)
1167 push_args = [
1168 "coord", "sync", "push",
1169 "--hub", "https://localhost:1337",
1170 "--owner", long_owner,
1171 "--slug", "myrepo",
1172
1173 ]
1174 result = runner.invoke(cli, push_args)
1175 assert result.exit_code == 1
1176 assert "Traceback" not in result.output
1177
1178
1179 # ---------------------------------------------------------------------------
1180 # Stress — muse coord sync push
1181 # ---------------------------------------------------------------------------
1182
1183
1184 class TestCoordSyncPushStress:
1185 def test_50_sequential_push_calls_no_records(self, repo: pathlib.Path) -> None:
1186 """50 sequential pushes with no records all exit 0."""
1187 for i in range(50):
1188 result = runner.invoke(cli, _PUSH_ARGS + ["-j"])
1189 assert result.exit_code == 0, f"Call {i}: {result.output}"
1190 data = json.loads(result.output.strip())
1191 assert data["total"] == 0
1192
1193 def test_push_1200_records_correct_batch_count(self, repo: pathlib.Path) -> None:
1194 """1200 records → ceil(1200/500) = 3 batches."""
1195 from muse.core.coord_bus import MAX_PUSH_BATCH
1196 for i in range(1200):
1197 _write_local_reservation(repo, run_id=f"agent-{i}")
1198 call_count = 0
1199 def counting_push(hub: str, owner: str, slug: str, batch: list[JsonDict], token: SigningIdentity | None) -> MsgpackDict:
1200 nonlocal call_count
1201 call_count += 1
1202 return {"inserted": len(batch), "skipped": 0}
1203 with patch(_PUSH_TARGET, side_effect=counting_push):
1204 result = runner.invoke(cli, _PUSH_ARGS + ["--kinds", "reservation", "-j"])
1205 assert result.exit_code == 0
1206 expected_batches = -(-1200 // MAX_PUSH_BATCH) # ceil division
1207 assert call_count == expected_batches
1208 data = json.loads(result.output.strip())
1209 assert data["inserted"] == 1200
1210
1211 def test_concurrent_push_8_threads(self, repo: pathlib.Path) -> None:
1212 """8 threads each call run_push directly; patches applied at test level.
1213
1214 The goal is to verify that concurrent calls to run_push do not crash
1215 or corrupt internal state. All threads share the same repo fixture;
1216 per-thread module mutation is intentionally avoided here because
1217 unguarded write-then-restore of a module attribute across threads is a
1218 race condition that can leave the module permanently patched after the
1219 test completes, polluting later tests.
1220 """
1221 import argparse
1222 import threading
1223
1224 from muse.cli.commands.coord_sync import run_push
1225
1226 _write_local_reservation(repo, run_id="shared-agent")
1227
1228 errors: list[str] = []
1229
1230 def worker(idx: int) -> None:
1231 args = argparse.Namespace(
1232 hub="https://localhost:1337",
1233 owner="gabriel",
1234 slug="myrepo",
1235 signing=None,
1236 kinds=list(_ALL_KINDS),
1237 json_out=True,
1238 )
1239 try:
1240 run_push(args)
1241 except SystemExit as exc:
1242 if exc.code != 0:
1243 errors.append(f"Thread {idx}: exit {exc.code}")
1244 except Exception as exc:
1245 errors.append(f"Thread {idx}: {exc}")
1246
1247 def fake_push(hub: str, owner: str, slug: str, batch: list[JsonDict], token: SigningIdentity | None) -> MsgpackDict:
1248 return {"inserted": len(batch), "skipped": 0}
1249
1250 push_p = patch(_PUSH_TARGET, side_effect=fake_push)
1251 repo_p = patch("muse.cli.commands.coord_sync.require_repo", return_value=repo)
1252 push_p.start()
1253 repo_p.start()
1254 try:
1255 threads = [threading.Thread(target=worker, args=(i,)) for i in range(8)]
1256 for t in threads:
1257 t.start()
1258 for t in threads:
1259 t.join()
1260 finally:
1261 push_p.stop()
1262 repo_p.stop()
1263 assert not errors, f"Concurrent failures: {errors}"
1264
1265
1266 # ---------------------------------------------------------------------------
1267 # Extended — muse coord sync pull
1268 # ---------------------------------------------------------------------------
1269
1270
1271 class TestCoordSyncPullExtended:
1272 def test_j_alias_works(self, repo: pathlib.Path) -> None:
1273 """-j is equivalent to --json for pull."""
1274 with patch(_PULL_TARGET, return_value=_pull_ok()):
1275 result = runner.invoke(cli, _PULL_ARGS + ["-j"])
1276 assert result.exit_code == 0, result.output
1277 data = json.loads(result.output.strip())
1278 assert "count" in data
1279
1280 def test_help_flag(self, repo: pathlib.Path) -> None:
1281 result = runner.invoke(cli, ["coord", "sync", "pull", "--help"])
1282 assert result.exit_code == 0
1283
1284 def test_json_compact_single_line(self, repo: pathlib.Path) -> None:
1285 """JSON output is a single compact line — no indent=2."""
1286 with patch(_PULL_TARGET, return_value=_pull_ok()):
1287 result = runner.invoke(cli, _PULL_ARGS + ["-j"])
1288 assert result.exit_code == 0
1289 lines = [l for l in result.output.splitlines() if l.strip().startswith("{")]
1290 assert len(lines) == 1, f"Expected compact JSON, got: {result.output!r}"
1291
1292 def test_json_all_required_fields(self, repo: pathlib.Path) -> None:
1293 """JSON always has schema, count, cursor, records, duration_ms."""
1294 with patch(_PULL_TARGET, return_value=_pull_ok()):
1295 result = runner.invoke(cli, _PULL_ARGS + ["-j"])
1296 data = json.loads(result.output.strip())
1297 for field in ("schema", "count", "cursor", "records", "duration_ms"):
1298 assert field in data, f"Missing field: {field}"
1299
1300 def test_json_count_is_int(self, repo: pathlib.Path) -> None:
1301 with patch(_PULL_TARGET, return_value=_pull_ok()):
1302 result = runner.invoke(cli, _PULL_ARGS + ["-j"])
1303 data = json.loads(result.output.strip())
1304 assert isinstance(data["count"], int)
1305 assert isinstance(data["cursor"], int)
1306
1307 def test_json_records_is_list(self, repo: pathlib.Path) -> None:
1308 with patch(_PULL_TARGET, return_value=_pull_ok()):
1309 result = runner.invoke(cli, _PULL_ARGS + ["-j"])
1310 data = json.loads(result.output.strip())
1311 assert isinstance(data["records"], list)
1312
1313 def test_json_duration_ms_is_number(self, repo: pathlib.Path) -> None:
1314 with patch(_PULL_TARGET, return_value=_pull_ok()):
1315 result = runner.invoke(cli, _PULL_ARGS + ["-j"])
1316 data = json.loads(result.output.strip())
1317 assert isinstance(data["duration_ms"], (int, float))
1318 assert data["duration_ms"] >= 0
1319
1320 def test_json_schema_is_int(self, repo: pathlib.Path) -> None:
1321 with patch(_PULL_TARGET, return_value=_pull_ok()):
1322 result = runner.invoke(cli, _PULL_ARGS + ["-j"])
1323 data = json.loads(result.output.strip())
1324 assert isinstance(data["schema"], int)
1325 assert data["schema"] >= 1
1326
1327 def test_json_count_matches_records_length(self, repo: pathlib.Path) -> None:
1328 fake_records = [
1329 {"kind": "reservation", "record_uuid": str(uuid.uuid4()), "run_id": "r1", "payload": {}},
1330 {"kind": "reservation", "record_uuid": str(uuid.uuid4()), "run_id": "r2", "payload": {}},
1331 ]
1332 with patch(_PULL_TARGET, return_value=_pull_ok(fake_records, cursor=2)):
1333 result = runner.invoke(cli, _PULL_ARGS + ["-j"])
1334 data = json.loads(result.output.strip())
1335 assert data["count"] == 2
1336 assert len(data["records"]) == 2
1337
1338 def test_json_cursor_reflects_hub_cursor(self, repo: pathlib.Path) -> None:
1339 with patch(_PULL_TARGET, return_value=_pull_ok([], cursor=42)):
1340 result = runner.invoke(cli, _PULL_ARGS + ["-j"])
1341 data = json.loads(result.output.strip())
1342 assert data["cursor"] == 42
1343
1344 def test_zero_records_exits_0(self, repo: pathlib.Path) -> None:
1345 """0 records returned is a valid success."""
1346 with patch(_PULL_TARGET, return_value=_pull_ok()):
1347 result = runner.invoke(cli, _PULL_ARGS + ["-j"])
1348 assert result.exit_code == 0
1349 data = json.loads(result.output.strip())
1350 assert data["count"] == 0
1351
1352 def test_incremental_pull_since_id_forwarded(self, repo: pathlib.Path) -> None:
1353 """--since-id is passed through to pull_from_hub."""
1354 with patch(_PULL_TARGET, return_value=_pull_ok()) as mock_pull:
1355 runner.invoke(cli, _PULL_ARGS + ["--since-id", "99"])
1356 _, kwargs = mock_pull.call_args
1357 assert mock_pull.call_args[0][3] == 99 or kwargs.get("since_id") == 99 or mock_pull.call_args[0][3] == 99
1358
1359 def test_records_written_to_remote_dir(self, repo: pathlib.Path) -> None:
1360 """Records returned by hub are written to .muse/coordination/remote/."""
1361 rid = str(uuid.uuid4())
1362 fake_records = [{"kind": "reservation", "record_uuid": rid, "run_id": "r1", "payload": {}}]
1363 with patch(_PULL_TARGET, return_value=_pull_ok(fake_records)):
1364 result = runner.invoke(cli, _PULL_ARGS)
1365 assert result.exit_code == 0
1366 written = repo / ".muse" / "coordination" / "remote" / "reservation" / f"{rid}.json"
1367 assert written.exists()
1368
1369 def test_text_output_shows_owner_slug(self, repo: pathlib.Path) -> None:
1370 with patch(_PULL_TARGET, return_value=_pull_ok()):
1371 result = runner.invoke(cli, _PULL_ARGS)
1372 assert "gabriel" in result.output
1373 assert "myrepo" in result.output
1374
1375 def test_text_output_shows_cursor(self, repo: pathlib.Path) -> None:
1376 with patch(_PULL_TARGET, return_value=_pull_ok([], cursor=7)):
1377 result = runner.invoke(cli, _PULL_ARGS)
1378 assert "7" in result.output
1379
1380 def test_text_output_shows_remote_path_when_records(self, repo: pathlib.Path) -> None:
1381 rid = str(uuid.uuid4())
1382 fake_records = [{"kind": "reservation", "record_uuid": rid, "run_id": "r1", "payload": {}}]
1383 with patch(_PULL_TARGET, return_value=_pull_ok(fake_records)):
1384 result = runner.invoke(cli, _PULL_ARGS)
1385 assert "remote" in result.output.lower()
1386
1387 def test_help_shows_agent_quickstart(self, repo: pathlib.Path) -> None:
1388 result = runner.invoke(cli, ["coord", "sync", "pull", "--help"])
1389 assert "Agent quickstart" in result.output
1390
1391 def test_help_shows_json_schema(self, repo: pathlib.Path) -> None:
1392 result = runner.invoke(cli, ["coord", "sync", "pull", "--help"])
1393 assert "JSON output schema" in result.output
1394
1395 def test_help_shows_exit_codes(self, repo: pathlib.Path) -> None:
1396 result = runner.invoke(cli, ["coord", "sync", "pull", "--help"])
1397 assert "Exit codes" in result.output
1398
1399
1400 # ---------------------------------------------------------------------------
1401 # Security — muse coord sync pull
1402 # ---------------------------------------------------------------------------
1403
1404
1405 class TestCoordSyncPullSecurity:
1406 def test_ansi_in_owner_sanitized_in_text_output(self, repo: pathlib.Path) -> None:
1407 """ANSI codes in --owner must not bleed into text output."""
1408 ansi_owner = "\x1b[31mevil\x1b[0m"
1409 pull_args = [
1410 "coord", "sync", "pull",
1411 "--hub", "https://localhost:1337",
1412 "--owner", ansi_owner,
1413 "--slug", "myrepo",
1414
1415 "--since-id", "0",
1416 ]
1417 with patch(_PULL_TARGET, return_value=_pull_ok()):
1418 result = runner.invoke(cli, pull_args)
1419 assert "\x1b" not in result.output
1420
1421 def test_ansi_in_slug_sanitized_in_text_output(self, repo: pathlib.Path) -> None:
1422 ansi_slug = "\x1b[32minjected\x1b[0m"
1423 pull_args = [
1424 "coord", "sync", "pull",
1425 "--hub", "https://localhost:1337",
1426 "--owner", "gabriel",
1427 "--slug", ansi_slug,
1428
1429 "--since-id", "0",
1430 ]
1431 with patch(_PULL_TARGET, return_value=_pull_ok()):
1432 result = runner.invoke(cli, pull_args)
1433 assert "\x1b" not in result.output
1434
1435 def test_token_not_in_json_output(self, repo: pathlib.Path) -> None:
1436 """Auth token must never appear in JSON output."""
1437 secret = "super-secret-pull-token"
1438 pull_args = [
1439 "coord", "sync", "pull",
1440 "--hub", "https://localhost:1337",
1441 "--owner", "gabriel",
1442 "--slug", "myrepo",
1443
1444 "--since-id", "0",
1445 "--json",
1446 ]
1447 with patch(_PULL_TARGET, return_value=_pull_ok()):
1448 result = runner.invoke(cli, pull_args)
1449 assert secret not in result.output
1450
1451 def test_no_traceback_on_coord_bus_error(self, repo: pathlib.Path) -> None:
1452 with patch(_PULL_TARGET, side_effect=CoordBusError("timeout")):
1453 result = runner.invoke(cli, _PULL_ARGS)
1454 assert "Traceback" not in result.output
1455
1456 def test_remote_records_with_traversal_uuid_skipped(self, repo: pathlib.Path) -> None:
1457 """A record with path-traversal UUID must not escape remote/."""
1458 evil_records = [
1459 {"kind": "reservation", "record_uuid": "../../evil", "run_id": "r1", "payload": {}},
1460 ]
1461 with patch(_PULL_TARGET, return_value=_pull_ok(evil_records)):
1462 result = runner.invoke(cli, _PULL_ARGS)
1463 assert result.exit_code == 0
1464 evil_path = repo / ".muse" / "coordination" / "remote" / "reservation" / "../../evil.json"
1465 assert not evil_path.exists()
1466 # Confirm nothing was written at all
1467 remote_dir = repo / ".muse" / "coordination" / "remote"
1468 if remote_dir.exists():
1469 assert list(remote_dir.rglob("*.json")) == []
1470
1471 def test_remote_records_with_unknown_kind_skipped(self, repo: pathlib.Path) -> None:
1472 """A record with an unknown kind must not be written anywhere."""
1473 evil_records = [
1474 {"kind": "../evil_dir", "record_uuid": "safe-uuid", "run_id": "r1", "payload": {}},
1475 ]
1476 with patch(_PULL_TARGET, return_value=_pull_ok(evil_records)):
1477 result = runner.invoke(cli, _PULL_ARGS)
1478 assert result.exit_code == 0
1479 remote_dir = repo / ".muse" / "coordination" / "remote"
1480 if remote_dir.exists():
1481 assert list(remote_dir.rglob("*.json")) == []
1482
1483
1484 # ---------------------------------------------------------------------------
1485 # Stress — muse coord sync pull
1486 # ---------------------------------------------------------------------------
1487
1488
1489 class TestCoordSyncPullStress:
1490 def test_50_sequential_pull_calls_no_records(self, repo: pathlib.Path) -> None:
1491 """50 sequential pulls with no records all exit 0."""
1492 for i in range(50):
1493 with patch(_PULL_TARGET, return_value=_pull_ok([], cursor=i)):
1494 result = runner.invoke(cli, _PULL_ARGS + ["-j"])
1495 assert result.exit_code == 0, f"Call {i}: {result.output}"
1496 data = json.loads(result.output.strip())
1497 assert data["count"] == 0
1498
1499 def test_incremental_cursor_chain_100_steps(self, repo: pathlib.Path) -> None:
1500 """100 incremental pulls each use the cursor from the previous step."""
1501 cursor = 0
1502 for i in range(100):
1503 rid = str(uuid.uuid4())
1504 fake_records = [{"kind": "reservation", "record_uuid": rid, "run_id": f"r{i}", "payload": {}}]
1505 with patch(_PULL_TARGET, return_value=_pull_ok(fake_records, cursor=cursor + 1)):
1506 args = _PULL_ARGS + ["--since-id", str(cursor), "-j"]
1507 result = runner.invoke(cli, args)
1508 assert result.exit_code == 0, f"Step {i}: {result.output}"
1509 data = json.loads(result.output.strip())
1510 cursor = data["cursor"]
1511 assert cursor == 100
1512
1513 def test_concurrent_pull_8_threads(self, repo: pathlib.Path) -> None:
1514 """8 threads each call run_pull concurrently; patches applied at test level."""
1515 import argparse
1516 import threading
1517
1518 from muse.cli.commands.coord_sync import run_pull
1519
1520 errors: list[str] = []
1521
1522 def worker(idx: int) -> None:
1523 args = argparse.Namespace(
1524 hub="https://localhost:1337",
1525 owner="gabriel",
1526 slug="myrepo",
1527 signing=None,
1528 since_id=0,
1529 kinds=[],
1530 limit=500,
1531 json_out=True,
1532 )
1533 try:
1534 run_pull(args)
1535 except SystemExit as exc:
1536 if exc.code != 0:
1537 errors.append(f"Thread {idx}: exit {exc.code}")
1538 except Exception as exc:
1539 errors.append(f"Thread {idx}: {exc}")
1540
1541 pull_p = patch(_PULL_TARGET, return_value=_pull_ok([], cursor=0))
1542 repo_p = patch("muse.cli.commands.coord_sync.require_repo", return_value=repo)
1543 pull_p.start()
1544 repo_p.start()
1545 try:
1546 threads = [threading.Thread(target=worker, args=(i,)) for i in range(8)]
1547 for t in threads:
1548 t.start()
1549 for t in threads:
1550 t.join()
1551 finally:
1552 pull_p.stop()
1553 repo_p.stop()
1554 assert not errors, f"Concurrent failures: {errors}"
1555
1556
1557 # ---------------------------------------------------------------------------
1558 # TestRegisterFlags — --json / -j normalized at argparse level
1559 # ---------------------------------------------------------------------------
1560
1561
1562 class TestRegisterFlags:
1563 """register() must expose --json/-j with dest=json_out on pull and push."""
1564
1565 def _make_parser(self):
1566 import argparse as ap
1567 from muse.cli.commands.coord_sync import register
1568 root = ap.ArgumentParser()
1569 subs = root.add_subparsers()
1570 register(subs)
1571 return root
1572
1573 # --json/-j lives on the pull/push sub-subparsers, not on sync itself.
1574 _PULL_REQUIRED = ["sync", "pull", "--owner", "o", "--slug", "s"]
1575
1576 def test_json_out_default_false(self) -> None:
1577 p = self._make_parser()
1578 ns = p.parse_args(self._PULL_REQUIRED)
1579 assert ns.json_out is False
1580
1581 def test_json_out_true_with_json_flag(self) -> None:
1582 p = self._make_parser()
1583 ns = p.parse_args(self._PULL_REQUIRED + ["--json"])
1584 assert ns.json_out is True
1585
1586 def test_json_out_true_with_j_flag(self) -> None:
1587 p = self._make_parser()
1588 ns = p.parse_args(self._PULL_REQUIRED + ["-j"])
1589 assert ns.json_out is True
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago