gabriel / muse public
test_cmd_reconcile.py python
723 lines 29.2 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 27 days ago
1 """Comprehensive tests for ``muse coord reconcile``.
2
3 Coverage
4 --------
5 Unit — internal helpers
6 _BranchSummary.to_dict: all keys present and correctly typed
7 strategy fast-forward: branch with 0 conflicts → fast-forward
8 strategy rebase: branch with 1–2 conflicts → rebase
9 strategy manual: branch with 3+ conflicts → manual
10 hotspot detection: same address on 2 branches → hotspot
11
12 Integration — CLI
13 empty repo: exits 0, "no active coordination data" in output
14 single branch: exits 0, branch name in output
15 multiple branches no conflict: each branch listed, 0 hotspots
16 multiple branches with hotspot: hotspot address in output
17 --json output schema: all required top-level keys present
18 --json shorthand: same schema as --format json
19 --format json explicit: same schema as --json
20
21 JSON schema
22 compact (single line): output is exactly one line
23 elapsed_seconds present: field exists and is a non-negative float
24 elapsed_seconds type: float, not string or int
25 branch entry has run_ids: each branch entry includes run_ids list
26 merge order matches branches: recommended_merge_order matches branch names
27
28 Security
29 ANSI in branch name (text): stripped from text output (merge order)
30 ANSI in branch name (hotspot): stripped from hotspot text
31 ANSI in address: stripped from text output
32
33 E2E — strategy thresholds
34 0 conflicts → fast-forward
35 1 conflict → rebase
36 2 conflicts → rebase
37 3 conflicts → manual
38
39 Stress
40 50 reservations 5 branches < 1 s: throughput baseline
41 100 reservations 10 branches: all in JSON, elapsed_seconds present
42 hotspot-heavy 20 branches 1 addr: all share same address
43 """
44
45 from __future__ import annotations
46
47 type _AddrBranches = dict[str, list[str]]
48
49 import json
50 import pathlib
51 import time
52 import uuid
53
54 import pytest
55
56 from tests.cli_test_helper import CliRunner
57 from muse.core.coordination import (
58 create_intent,
59 create_reservation,
60 active_reservations,
61 load_all_intents,
62 )
63 from muse.cli.commands.reconcile import _BranchSummary
64
65 cli = None
66 runner = CliRunner()
67
68 _REQUIRED_JSON_KEYS = {
69 "schema_version",
70 "active_reservations",
71 "active_intents",
72 "conflict_hotspots",
73 "branches",
74 "recommended_merge_order",
75 "strategies",
76 "hotspots",
77 }
78
79 _REQUIRED_BRANCH_KEYS = {
80 "branch",
81 "reserved_addresses",
82 "intents",
83 "run_ids",
84 "predicted_conflicts",
85 }
86
87
88 # ---------------------------------------------------------------------------
89 # Fixtures
90 # ---------------------------------------------------------------------------
91
92
93 @pytest.fixture()
94 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
95 muse_dir = tmp_path / ".muse"
96 muse_dir.mkdir()
97 (muse_dir / "HEAD").write_text("ref: refs/heads/main\n")
98 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
99 return tmp_path
100
101
102 # ---------------------------------------------------------------------------
103 # Helpers
104 # ---------------------------------------------------------------------------
105
106
107 def _make_reservation(
108 root: pathlib.Path,
109 *,
110 run_id: str = "agent-1",
111 branch: str = "main",
112 addresses: list[str] | None = None,
113 ttl_seconds: int = 3600,
114 ) -> None:
115 create_reservation(
116 root,
117 run_id=run_id,
118 branch=branch,
119 addresses=addresses or ["src/mod.py::foo"],
120 ttl_seconds=ttl_seconds,
121 )
122
123
124 def _make_intent(
125 root: pathlib.Path,
126 *,
127 run_id: str = "agent-1",
128 branch: str = "main",
129 addresses: list[str] | None = None,
130 operation: str = "modify",
131 ) -> None:
132 create_intent(
133 root,
134 reservation_id=str(uuid.uuid4()),
135 run_id=run_id,
136 branch=branch,
137 addresses=addresses or ["src/mod.py::foo"],
138 operation=operation,
139 )
140
141
142 # ---------------------------------------------------------------------------
143 # Unit — _BranchSummary
144 # ---------------------------------------------------------------------------
145
146
147 class TestBranchSummaryToDict:
148 def test_all_required_keys_present(self) -> None:
149 bs = _BranchSummary("feature/billing")
150 d = bs.to_dict()
151 assert _REQUIRED_BRANCH_KEYS.issubset(d.keys())
152
153 def test_branch_field_matches_constructor(self) -> None:
154 bs = _BranchSummary("feature/auth")
155 assert bs.to_dict()["branch"] == "feature/auth"
156
157 def test_reserved_addresses_starts_empty(self) -> None:
158 bs = _BranchSummary("main")
159 assert bs.to_dict()["reserved_addresses"] == []
160
161 def test_intents_starts_empty(self) -> None:
162 bs = _BranchSummary("main")
163 assert bs.to_dict()["intents"] == []
164
165 def test_run_ids_sorted(self) -> None:
166 bs = _BranchSummary("main")
167 bs.run_ids = {"agent-3", "agent-1", "agent-2"}
168 run_ids_list = bs.to_dict()["run_ids"]
169 assert run_ids_list == sorted(run_ids_list)
170
171 def test_predicted_conflicts_starts_zero(self) -> None:
172 bs = _BranchSummary("main")
173 assert bs.to_dict()["predicted_conflicts"] == 0
174
175 def test_conflict_count_reflected_in_dict(self) -> None:
176 bs = _BranchSummary("main")
177 bs.conflict_count = 5
178 assert bs.to_dict()["predicted_conflicts"] == 5
179
180 def test_reserved_addresses_reflects_mutations(self) -> None:
181 bs = _BranchSummary("main")
182 bs.reserved_addresses.extend(["src/a.py::foo", "src/b.py::bar"])
183 assert len(bs.to_dict()["reserved_addresses"]) == 2
184
185
186 class TestStrategySelection:
187 """Verify strategy labels for known conflict counts (mirrors reconcile.run logic)."""
188
189 def _strategy_for(self, conflict_count: int) -> str:
190 if conflict_count == 0:
191 return "fast-forward (no conflicts predicted)"
192 elif conflict_count <= 2:
193 return "rebase onto main before merging"
194 else:
195 return "manual conflict resolution required"
196
197 def test_zero_conflicts_fast_forward(self) -> None:
198 assert self._strategy_for(0) == "fast-forward (no conflicts predicted)"
199
200 def test_one_conflict_rebase(self) -> None:
201 assert self._strategy_for(1) == "rebase onto main before merging"
202
203 def test_two_conflicts_rebase(self) -> None:
204 assert self._strategy_for(2) == "rebase onto main before merging"
205
206 def test_three_conflicts_manual(self) -> None:
207 assert self._strategy_for(3) == "manual conflict resolution required"
208
209 def test_ten_conflicts_manual(self) -> None:
210 assert self._strategy_for(10) == "manual conflict resolution required"
211
212
213 class TestHotspotDetection:
214 def test_same_address_two_branches_is_hotspot(self, tmp_path: pathlib.Path) -> None:
215 _make_reservation(
216 tmp_path, run_id="agent-1", branch="feature/a",
217 addresses=["src/billing.py::compute_total"],
218 )
219 _make_reservation(
220 tmp_path, run_id="agent-2", branch="feature/b",
221 addresses=["src/billing.py::compute_total"],
222 )
223 reservations = active_reservations(tmp_path)
224 addr_branches: _AddrBranches = {}
225 for res in reservations:
226 for addr in res.addresses:
227 addr_branches.setdefault(addr, []).append(res.branch)
228 hotspots = {
229 addr: branches
230 for addr, branches in addr_branches.items()
231 if len(set(branches)) > 1
232 }
233 assert "src/billing.py::compute_total" in hotspots
234
235 def test_same_address_same_branch_not_hotspot(self, tmp_path: pathlib.Path) -> None:
236 _make_reservation(
237 tmp_path, run_id="agent-1", branch="feature/a",
238 addresses=["src/billing.py::compute_total"],
239 )
240 _make_reservation(
241 tmp_path, run_id="agent-2", branch="feature/a",
242 addresses=["src/billing.py::compute_total"],
243 )
244 reservations = active_reservations(tmp_path)
245 addr_branches: _AddrBranches = {}
246 for res in reservations:
247 for addr in res.addresses:
248 addr_branches.setdefault(addr, []).append(res.branch)
249 hotspots = {
250 addr: branches
251 for addr, branches in addr_branches.items()
252 if len(set(branches)) > 1
253 }
254 assert "src/billing.py::compute_total" not in hotspots
255
256 def test_distinct_addresses_no_hotspot(self, tmp_path: pathlib.Path) -> None:
257 _make_reservation(
258 tmp_path, run_id="agent-1", branch="feature/a",
259 addresses=["src/a.py::foo"],
260 )
261 _make_reservation(
262 tmp_path, run_id="agent-2", branch="feature/b",
263 addresses=["src/b.py::bar"],
264 )
265 reservations = active_reservations(tmp_path)
266 addr_branches: _AddrBranches = {}
267 for res in reservations:
268 for addr in res.addresses:
269 addr_branches.setdefault(addr, []).append(res.branch)
270 hotspots = {
271 addr: branches
272 for addr, branches in addr_branches.items()
273 if len(set(branches)) > 1
274 }
275 assert len(hotspots) == 0
276
277
278 # ---------------------------------------------------------------------------
279 # Integration — CLI
280 # ---------------------------------------------------------------------------
281
282
283 class TestReconcileCLIEmpty:
284 def test_empty_repo_exits_zero(self, repo: pathlib.Path) -> None:
285 result = runner.invoke(cli, ["coord", "reconcile"])
286 assert result.exit_code == 0
287
288 def test_empty_repo_prints_no_active_data(self, repo: pathlib.Path) -> None:
289 result = runner.invoke(cli, ["coord", "reconcile"])
290 assert "no active coordination data" in result.output
291
292 def test_empty_repo_json_exits_zero(self, repo: pathlib.Path) -> None:
293 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
294 assert result.exit_code == 0
295
296 def test_empty_repo_json_has_required_keys(self, repo: pathlib.Path) -> None:
297 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
298 data = json.loads(result.output)
299 assert _REQUIRED_JSON_KEYS.issubset(data.keys())
300
301 def test_empty_repo_json_counts_are_zero(self, repo: pathlib.Path) -> None:
302 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
303 data = json.loads(result.output)
304 assert data["active_reservations"] == 0
305 assert data["active_intents"] == 0
306 assert data["conflict_hotspots"] == 0
307
308
309 class TestReconcileCLISingleBranch:
310 def test_single_branch_exits_zero(self, repo: pathlib.Path) -> None:
311 _make_reservation(repo, run_id="agent-1", branch="feature/billing")
312 result = runner.invoke(cli, ["coord", "reconcile"])
313 assert result.exit_code == 0
314
315 def test_single_branch_name_in_output(self, repo: pathlib.Path) -> None:
316 _make_reservation(repo, run_id="agent-1", branch="feature/billing")
317 result = runner.invoke(cli, ["coord", "reconcile"])
318 assert "feature/billing" in result.output
319
320 def test_single_branch_no_hotspots(self, repo: pathlib.Path) -> None:
321 _make_reservation(repo, run_id="agent-1", branch="feature/billing")
322 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
323 data = json.loads(result.output)
324 assert data["conflict_hotspots"] == 0
325
326 def test_single_branch_fast_forward_strategy(self, repo: pathlib.Path) -> None:
327 _make_reservation(repo, run_id="agent-1", branch="feature/billing")
328 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
329 data = json.loads(result.output)
330 strategy = data["strategies"].get("feature/billing", "")
331 assert "fast-forward" in strategy
332
333
334 class TestReconcileCLIMultipleBranches:
335 def test_multiple_branches_no_conflict_exits_zero(self, repo: pathlib.Path) -> None:
336 _make_reservation(repo, run_id="agent-1", branch="feature/a", addresses=["src/a.py::foo"])
337 _make_reservation(repo, run_id="agent-2", branch="feature/b", addresses=["src/b.py::bar"])
338 result = runner.invoke(cli, ["coord", "reconcile"])
339 assert result.exit_code == 0
340
341 def test_multiple_branches_both_listed(self, repo: pathlib.Path) -> None:
342 _make_reservation(repo, run_id="agent-1", branch="feature/a", addresses=["src/a.py::foo"])
343 _make_reservation(repo, run_id="agent-2", branch="feature/b", addresses=["src/b.py::bar"])
344 result = runner.invoke(cli, ["coord", "reconcile"])
345 assert "feature/a" in result.output
346 assert "feature/b" in result.output
347
348 def test_multiple_branches_no_conflict_zero_hotspots(self, repo: pathlib.Path) -> None:
349 _make_reservation(repo, run_id="agent-1", branch="feature/a", addresses=["src/a.py::foo"])
350 _make_reservation(repo, run_id="agent-2", branch="feature/b", addresses=["src/b.py::bar"])
351 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
352 data = json.loads(result.output)
353 assert data["conflict_hotspots"] == 0
354
355 def test_hotspot_address_in_text_output(self, repo: pathlib.Path) -> None:
356 _make_reservation(
357 repo, run_id="agent-1", branch="feature/a",
358 addresses=["src/billing.py::compute_total"],
359 )
360 _make_reservation(
361 repo, run_id="agent-2", branch="feature/b",
362 addresses=["src/billing.py::compute_total"],
363 )
364 result = runner.invoke(cli, ["coord", "reconcile"])
365 assert "src/billing.py::compute_total" in result.output
366
367 def test_hotspot_count_nonzero_in_json(self, repo: pathlib.Path) -> None:
368 _make_reservation(
369 repo, run_id="agent-1", branch="feature/a",
370 addresses=["src/billing.py::compute_total"],
371 )
372 _make_reservation(
373 repo, run_id="agent-2", branch="feature/b",
374 addresses=["src/billing.py::compute_total"],
375 )
376 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
377 data = json.loads(result.output)
378 assert data["conflict_hotspots"] == 1
379
380 def test_hotspot_present_in_hotspots_list(self, repo: pathlib.Path) -> None:
381 _make_reservation(
382 repo, run_id="agent-1", branch="feature/a",
383 addresses=["src/billing.py::compute_total"],
384 )
385 _make_reservation(
386 repo, run_id="agent-2", branch="feature/b",
387 addresses=["src/billing.py::compute_total"],
388 )
389 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
390 data = json.loads(result.output)
391 addresses = [h["address"] for h in data["hotspots"]]
392 assert "src/billing.py::compute_total" in addresses
393
394
395 class TestReconcileCLIJSONSchema:
396 def test_json_flag_exits_zero(self, repo: pathlib.Path) -> None:
397 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
398 assert result.exit_code == 0
399
400 def test_json_flag_is_valid_json(self, repo: pathlib.Path) -> None:
401 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
402 data = json.loads(result.output)
403 assert isinstance(data, dict)
404
405 def test_json_flag_has_required_keys(self, repo: pathlib.Path) -> None:
406 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
407 data = json.loads(result.output)
408 assert _REQUIRED_JSON_KEYS.issubset(data.keys())
409
410 def test_format_json_explicit_has_required_keys(self, repo: pathlib.Path) -> None:
411 result = runner.invoke(cli, ["coord", "reconcile", "--format", "json"])
412 data = json.loads(result.output)
413 assert _REQUIRED_JSON_KEYS.issubset(data.keys())
414
415 def test_branches_field_is_list(self, repo: pathlib.Path) -> None:
416 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
417 data = json.loads(result.output)
418 assert isinstance(data["branches"], list)
419
420 def test_recommended_merge_order_is_list(self, repo: pathlib.Path) -> None:
421 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
422 data = json.loads(result.output)
423 assert isinstance(data["recommended_merge_order"], list)
424
425 def test_strategies_is_dict(self, repo: pathlib.Path) -> None:
426 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
427 data = json.loads(result.output)
428 assert isinstance(data["strategies"], dict)
429
430 def test_hotspots_is_list(self, repo: pathlib.Path) -> None:
431 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
432 data = json.loads(result.output)
433 assert isinstance(data["hotspots"], list)
434
435 def test_branch_entry_has_required_keys(self, repo: pathlib.Path) -> None:
436 _make_reservation(repo, run_id="agent-1", branch="feature/billing")
437 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
438 data = json.loads(result.output)
439 assert len(data["branches"]) >= 1
440 branch_entry = data["branches"][0]
441 assert _REQUIRED_BRANCH_KEYS.issubset(branch_entry.keys())
442
443 def test_schema_version_is_string(self, repo: pathlib.Path) -> None:
444 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
445 data = json.loads(result.output)
446 assert isinstance(data["schema_version"], str)
447
448 def test_active_reservations_count_matches(self, repo: pathlib.Path) -> None:
449 _make_reservation(repo, run_id="agent-1", branch="feature/a", addresses=["src/a.py::foo"])
450 _make_reservation(repo, run_id="agent-2", branch="feature/b", addresses=["src/b.py::bar"])
451 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
452 data = json.loads(result.output)
453 assert data["active_reservations"] == 2
454
455 def test_active_intents_count_matches(self, repo: pathlib.Path) -> None:
456 _make_intent(repo, run_id="agent-1", branch="feature/a")
457 _make_intent(repo, run_id="agent-2", branch="feature/b")
458 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
459 data = json.loads(result.output)
460 assert data["active_intents"] == 2
461
462
463 # ---------------------------------------------------------------------------
464 # Stress
465 # ---------------------------------------------------------------------------
466
467
468 class TestReconcileStress:
469 def test_50_reservations_5_branches_under_1_second(self, repo: pathlib.Path) -> None:
470 branches = [f"feature/branch-{i}" for i in range(5)]
471 start = time.monotonic()
472 for i in range(50):
473 create_reservation(
474 repo,
475 run_id=f"agent-{i}",
476 branch=branches[i % len(branches)],
477 addresses=[f"src/mod{i}.py::sym{i}"],
478 ttl_seconds=3600,
479 )
480 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
481 elapsed = time.monotonic() - start
482 assert result.exit_code == 0
483 data = json.loads(result.output)
484 assert data["active_reservations"] == 50
485 assert elapsed < 1.0, f"50 reservations across 5 branches took {elapsed:.2f}s (limit 1s)"
486
487 def test_100_reservations_10_branches_json_complete(self, repo: pathlib.Path) -> None:
488 branches = [f"feature/branch-{i}" for i in range(10)]
489 for i in range(100):
490 create_reservation(
491 repo,
492 run_id=f"agent-{i}",
493 branch=branches[i % len(branches)],
494 addresses=[f"src/mod{i}.py::sym{i}"],
495 ttl_seconds=3600,
496 )
497 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
498 assert result.exit_code == 0
499 data = json.loads(result.output)
500 assert data["active_reservations"] == 100
501 assert "elapsed_seconds" in data
502 assert isinstance(data["elapsed_seconds"], float)
503 assert len(data["branches"]) == 10
504
505 def test_hotspot_heavy_20_branches_all_share_one_address(
506 self, repo: pathlib.Path
507 ) -> None:
508 shared = "src/core.py::shared_sym"
509 for i in range(20):
510 create_reservation(
511 repo,
512 run_id=f"agent-{i}",
513 branch=f"feature/branch-{i}",
514 addresses=[shared],
515 ttl_seconds=3600,
516 )
517 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
518 assert result.exit_code == 0
519 data = json.loads(result.output)
520 assert data["conflict_hotspots"] == 1
521 assert data["hotspots"][0]["address"] == shared
522 assert len(data["hotspots"][0]["branches"]) == 20
523
524
525 # ---------------------------------------------------------------------------
526 # JSON schema — compact + elapsed_seconds
527 # ---------------------------------------------------------------------------
528
529
530 class TestReconcileJsonCompact:
531 def test_json_output_is_single_line(self, repo: pathlib.Path) -> None:
532 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
533 assert result.exit_code == 0
534 lines = [ln for ln in result.output.splitlines() if ln.strip()]
535 assert len(lines) == 1, f"Expected 1 JSON line, got {len(lines)}: {result.output!r}"
536
537 def test_elapsed_seconds_present(self, repo: pathlib.Path) -> None:
538 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
539 data = json.loads(result.output)
540 assert "elapsed_seconds" in data
541
542 def test_elapsed_seconds_is_float(self, repo: pathlib.Path) -> None:
543 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
544 data = json.loads(result.output)
545 assert isinstance(data["elapsed_seconds"], float)
546
547 def test_elapsed_seconds_non_negative(self, repo: pathlib.Path) -> None:
548 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
549 data = json.loads(result.output)
550 assert data["elapsed_seconds"] >= 0.0
551
552 def test_branch_entry_has_run_ids(self, repo: pathlib.Path) -> None:
553 _make_reservation(repo, run_id="agent-x", branch="feature/x")
554 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
555 data = json.loads(result.output)
556 branch_entry = data["branches"][0]
557 assert "run_ids" in branch_entry
558 assert isinstance(branch_entry["run_ids"], list)
559 assert "agent-x" in branch_entry["run_ids"]
560
561 def test_merge_order_matches_branch_names(self, repo: pathlib.Path) -> None:
562 _make_reservation(repo, run_id="agent-1", branch="feature/a", addresses=["src/a.py::foo"])
563 _make_reservation(repo, run_id="agent-2", branch="feature/b", addresses=["src/b.py::bar"])
564 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
565 data = json.loads(result.output)
566 branch_names = {entry["branch"] for entry in data["branches"]}
567 assert set(data["recommended_merge_order"]) == branch_names
568
569 def test_json_shorthand_same_structure_as_format_json(
570 self, repo: pathlib.Path
571 ) -> None:
572 _make_reservation(repo, run_id="agent-1", branch="feature/a")
573 r1 = runner.invoke(cli, ["coord", "reconcile", "--json"])
574 r2 = runner.invoke(cli, ["coord", "reconcile", "--format", "json"])
575 d1 = json.loads(r1.output)
576 d2 = json.loads(r2.output)
577 structural_keys = {
578 "schema_version", "active_reservations", "active_intents",
579 "conflict_hotspots", "recommended_merge_order", "strategies",
580 }
581 for key in structural_keys:
582 assert d1[key] == d2[key], f"Mismatch on {key!r}"
583
584
585 # ---------------------------------------------------------------------------
586 # E2E — strategy thresholds
587 # ---------------------------------------------------------------------------
588
589
590 class TestReconcileE2EStrategies:
591 def test_zero_conflicts_fast_forward(self, repo: pathlib.Path) -> None:
592 _make_reservation(repo, run_id="agent-1", branch="feature/clean", addresses=["src/a.py::foo"])
593 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
594 data = json.loads(result.output)
595 assert "fast-forward" in data["strategies"]["feature/clean"]
596
597 def test_one_conflict_rebase(self, repo: pathlib.Path) -> None:
598 _make_reservation(repo, run_id="agent-1", branch="feature/a", addresses=["src/shared.py::x"])
599 _make_reservation(repo, run_id="agent-2", branch="feature/b", addresses=["src/shared.py::x"])
600 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
601 data = json.loads(result.output)
602 for branch in ("feature/a", "feature/b"):
603 assert "rebase" in data["strategies"][branch]
604
605 def test_two_conflicts_still_rebase(self, repo: pathlib.Path) -> None:
606 # Two hotspot addresses shared between same two branches → conflict_count == 2.
607 _make_reservation(
608 repo, run_id="agent-1", branch="feature/a",
609 addresses=["src/shared.py::x", "src/shared.py::y"],
610 )
611 _make_reservation(
612 repo, run_id="agent-2", branch="feature/b",
613 addresses=["src/shared.py::x", "src/shared.py::y"],
614 )
615 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
616 data = json.loads(result.output)
617 for branch in ("feature/a", "feature/b"):
618 assert "rebase" in data["strategies"][branch]
619
620 def test_three_conflicts_manual(self, repo: pathlib.Path) -> None:
621 shared_addrs = [f"src/shared.py::sym{i}" for i in range(3)]
622 _make_reservation(repo, run_id="agent-1", branch="feature/a", addresses=shared_addrs)
623 _make_reservation(repo, run_id="agent-2", branch="feature/b", addresses=shared_addrs)
624 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
625 data = json.loads(result.output)
626 for branch in ("feature/a", "feature/b"):
627 assert "manual" in data["strategies"][branch]
628
629 def test_merge_order_clean_branch_first(self, repo: pathlib.Path) -> None:
630 _make_reservation(repo, run_id="agent-1", branch="feature/clean", addresses=["src/a.py::foo"])
631 _make_reservation(repo, run_id="agent-2", branch="feature/conflict-a", addresses=["src/shared.py::x"])
632 _make_reservation(repo, run_id="agent-3", branch="feature/conflict-b", addresses=["src/shared.py::x"])
633 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
634 data = json.loads(result.output)
635 order = data["recommended_merge_order"]
636 assert order.index("feature/clean") < order.index("feature/conflict-a")
637 assert order.index("feature/clean") < order.index("feature/conflict-b")
638
639 def test_intents_count_in_json(self, repo: pathlib.Path) -> None:
640 _make_intent(repo, run_id="agent-1", branch="feature/x", operation="rename")
641 _make_intent(repo, run_id="agent-2", branch="feature/x", operation="modify")
642 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
643 data = json.loads(result.output)
644 assert data["active_intents"] == 2
645
646 def test_branch_intents_list_populated(self, repo: pathlib.Path) -> None:
647 _make_intent(repo, run_id="agent-1", branch="feature/x", operation="rename")
648 result = runner.invoke(cli, ["coord", "reconcile", "--json"])
649 data = json.loads(result.output)
650 branch_entry = next(b for b in data["branches"] if b["branch"] == "feature/x")
651 assert "rename" in branch_entry["intents"]
652
653 def test_text_output_includes_elapsed(self, repo: pathlib.Path) -> None:
654 _make_reservation(repo, run_id="agent-1", branch="feature/a")
655 result = runner.invoke(cli, ["coord", "reconcile"])
656 assert result.exit_code == 0
657 # Elapsed appears at the bottom as (X.XXXs)
658 assert "s)" in result.output
659
660
661 # ---------------------------------------------------------------------------
662 # Security — merge order branch sanitization
663 # ---------------------------------------------------------------------------
664
665
666 class TestReconcileMergeOrderSecurity:
667 def test_ansi_in_branch_stripped_from_merge_order(self, repo: pathlib.Path) -> None:
668 ansi_branch = "\x1b[31mfeature/evil\x1b[0m"
669 create_reservation(
670 repo,
671 run_id="agent-evil",
672 branch=ansi_branch,
673 addresses=["src/mod.py::foo"],
674 ttl_seconds=3600,
675 )
676 result = runner.invoke(cli, ["coord", "reconcile"])
677 assert result.exit_code == 0
678 assert "\x1b[31m" not in result.output
679 assert "\x1b[0m" not in result.output
680
681 def test_ansi_in_branch_stripped_from_hotspot_text(self, repo: pathlib.Path) -> None:
682 ansi_branch = "\x1b[35mfeature/badactor\x1b[0m"
683 create_reservation(
684 repo,
685 run_id="agent-1",
686 branch=ansi_branch,
687 addresses=["src/billing.py::compute_total"],
688 ttl_seconds=3600,
689 )
690 create_reservation(
691 repo,
692 run_id="agent-2",
693 branch="feature/clean",
694 addresses=["src/billing.py::compute_total"],
695 ttl_seconds=3600,
696 )
697 result = runner.invoke(cli, ["coord", "reconcile"])
698 assert result.exit_code == 0
699 assert "\x1b[35m" not in result.output
700 assert "\x1b[0m" not in result.output
701
702 def test_ansi_in_address_stripped_from_hotspot_text(
703 self, repo: pathlib.Path
704 ) -> None:
705 ansi_addr = "\x1b[32msrc/billing.py::compute_total\x1b[0m"
706 create_reservation(
707 repo,
708 run_id="agent-1",
709 branch="feature/a",
710 addresses=[ansi_addr],
711 ttl_seconds=3600,
712 )
713 create_reservation(
714 repo,
715 run_id="agent-2",
716 branch="feature/b",
717 addresses=[ansi_addr],
718 ttl_seconds=3600,
719 )
720 result = runner.invoke(cli, ["coord", "reconcile"])
721 assert result.exit_code == 0
722 assert "\x1b[32m" not in result.output
723 assert "\x1b[0m" not in result.output
File History 5 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 27 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 27 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e Merge branch 'dev' into main Human 30 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 49 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 101 days ago