gabriel / muse public
test_bridge_harmony_shelf.py python
761 lines 27.4 KB
Raw
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b fix: try fetch/presign before fetch/mpack to avoid Cloudfla… Sonnet 4.6 patch 6 days ago
1 """Phase 8 TDD tests — Harmony↔rerere and Shelf↔Stash bridge.
2
3 NOTE: git subprocess calls in this file are INTENTIONAL — they create real
4 git repositories used as import/export targets. The bridge command itself
5 converts between Muse and git formats. The Muse codebase otherwise never
6 uses git.
7
8 Tiers
9 -----
10 Tier 1 Harmony ↔ rerere unit tests
11 Tier 2 Shelf ↔ Stash unit tests
12 Tier 3 Argparse flag presence tests
13 """
14
15 from __future__ import annotations
16
17 import os
18 import pathlib
19 import subprocess
20
21 import pytest
22
23 from tests.cli_test_helper import CliRunner
24
25 runner = CliRunner()
26
27
28 # ---------------------------------------------------------------------------
29 # Helpers
30 # ---------------------------------------------------------------------------
31
32
33 def _invoke(*args: str, cwd: pathlib.Path | None = None) -> "CliRunner":
34 """Invoke the muse CLI from *cwd* (or CWD if None)."""
35 return runner.invoke(None, list(args), cwd=cwd)
36
37
38 def _make_muse_repo(path: pathlib.Path) -> pathlib.Path:
39 """Initialise a Muse repository at *path*."""
40 path.mkdir(parents=True, exist_ok=True)
41 result = _invoke("init", cwd=path)
42 assert result.exit_code == 0, f"muse init failed: {result.stderr}"
43 return path
44
45
46 def _make_git_repo(path: pathlib.Path) -> pathlib.Path:
47 """Create a minimal git repo at *path* (single initial commit)."""
48 path.mkdir(parents=True, exist_ok=True)
49 subprocess.run(["git", "init", str(path)], check=True, capture_output=True)
50 subprocess.run(
51 ["git", "-C", str(path), "config", "user.email", "[email protected]"],
52 check=True, capture_output=True,
53 )
54 subprocess.run(
55 ["git", "-C", str(path), "config", "user.name", "Test User"],
56 check=True, capture_output=True,
57 )
58 # Initial commit so stash has something to work from.
59 (path / "README.md").write_text("initial")
60 subprocess.run(["git", "-C", str(path), "add", "."], check=True, capture_output=True)
61 subprocess.run(
62 ["git", "-C", str(path), "commit", "-m", "initial"],
63 check=True, capture_output=True,
64 env={**os.environ, "GIT_AUTHOR_EMAIL": "[email protected]",
65 "GIT_AUTHOR_NAME": "Test User",
66 "GIT_COMMITTER_EMAIL": "[email protected]",
67 "GIT_COMMITTER_NAME": "Test User"},
68 )
69 return path
70
71
72 def _make_rr_cache_entry(
73 git_dir: pathlib.Path,
74 name: str,
75 preimage: bytes,
76 postimage: bytes,
77 *,
78 only_preimage: bool = False,
79 ) -> pathlib.Path:
80 """Create a fake rerere cache entry under .git/rr-cache/<name>/."""
81 entry_dir = git_dir / ".git" / "rr-cache" / name
82 entry_dir.mkdir(parents=True, exist_ok=True)
83 (entry_dir / "preimage").write_bytes(preimage)
84 if not only_preimage:
85 (entry_dir / "postimage").write_bytes(postimage)
86 return entry_dir
87
88
89 # ---------------------------------------------------------------------------
90 # Tier 1 — Harmony ↔ rerere
91 # ---------------------------------------------------------------------------
92
93
94 class TestImportRerereToHarmony:
95
96 def test_import_rerere_empty_rr_cache_returns_zero(
97 self, tmp_path: pathlib.Path
98 ) -> None:
99 """Empty rr-cache dir → 0 patterns imported, no error."""
100 from muse.core.bridge.harmony_shelf import import_rerere_to_harmony
101
102 muse_root = _make_muse_repo(tmp_path / "muse")
103 git_root = _make_git_repo(tmp_path / "git")
104
105 # Create an empty rr-cache dir.
106 (git_root / ".git" / "rr-cache").mkdir(parents=True, exist_ok=True)
107
108 count = import_rerere_to_harmony(muse_root, git_root)
109 assert count == 0
110
111 def test_import_rerere_missing_rr_cache_raises(
112 self, tmp_path: pathlib.Path
113 ) -> None:
114 """If rr-cache does not exist at all, FileNotFoundError is raised."""
115 from muse.core.bridge.harmony_shelf import import_rerere_to_harmony
116
117 muse_root = _make_muse_repo(tmp_path / "muse")
118 git_root = _make_git_repo(tmp_path / "git")
119 # rr-cache intentionally absent.
120
121 with pytest.raises(FileNotFoundError):
122 import_rerere_to_harmony(muse_root, git_root)
123
124 def test_import_rerere_imports_one_entry(
125 self, tmp_path: pathlib.Path
126 ) -> None:
127 """Single complete rr-cache entry → 1 pattern + 1 resolution created."""
128 from muse.core.bridge.harmony_shelf import import_rerere_to_harmony
129 from muse.core.harmony import list_patterns, list_resolutions
130
131 muse_root = _make_muse_repo(tmp_path / "muse")
132 git_root = _make_git_repo(tmp_path / "git")
133
134 _make_rr_cache_entry(
135 git_root,
136 name="abc123conflict",
137 preimage=b"<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> feat\n",
138 postimage=b"resolved\n",
139 )
140
141 count = import_rerere_to_harmony(muse_root, git_root)
142 assert count == 1
143
144 patterns = list_patterns(muse_root)
145 assert len(patterns) == 1
146
147 resolutions = list_resolutions(muse_root, patterns[0].pattern_id)
148 assert len(resolutions) == 1
149 assert resolutions[0].confidence == pytest.approx(0.7)
150
151 def test_import_rerere_dry_run_writes_nothing(
152 self, tmp_path: pathlib.Path
153 ) -> None:
154 """dry_run=True returns the count but writes no patterns."""
155 from muse.core.bridge.harmony_shelf import import_rerere_to_harmony
156 from muse.core.harmony import list_patterns
157
158 muse_root = _make_muse_repo(tmp_path / "muse")
159 git_root = _make_git_repo(tmp_path / "git")
160
161 _make_rr_cache_entry(
162 git_root,
163 name="dryrun_entry",
164 preimage=b"conflict content",
165 postimage=b"resolved content",
166 )
167
168 count = import_rerere_to_harmony(muse_root, git_root, dry_run=True)
169 assert count == 1
170 assert list_patterns(muse_root) == []
171
172 def test_import_rerere_missing_postimage_skipped(
173 self, tmp_path: pathlib.Path
174 ) -> None:
175 """Entry with only preimage (no postimage) is skipped silently."""
176 from muse.core.bridge.harmony_shelf import import_rerere_to_harmony
177
178 muse_root = _make_muse_repo(tmp_path / "muse")
179 git_root = _make_git_repo(tmp_path / "git")
180
181 _make_rr_cache_entry(
182 git_root,
183 name="partial_entry",
184 preimage=b"conflict\n",
185 postimage=b"",
186 only_preimage=True,
187 )
188
189 count = import_rerere_to_harmony(muse_root, git_root)
190 assert count == 0
191
192 def test_import_rerere_custom_confidence(
193 self, tmp_path: pathlib.Path
194 ) -> None:
195 """Custom confidence score is stored on the resolution."""
196 from muse.core.bridge.harmony_shelf import import_rerere_to_harmony
197 from muse.core.harmony import list_patterns, list_resolutions
198
199 muse_root = _make_muse_repo(tmp_path / "muse")
200 git_root = _make_git_repo(tmp_path / "git")
201
202 _make_rr_cache_entry(
203 git_root,
204 name="highconf_entry",
205 preimage=b"conflict data",
206 postimage=b"resolved data",
207 )
208
209 import_rerere_to_harmony(muse_root, git_root, confidence=0.9)
210 patterns = list_patterns(muse_root)
211 resolutions = list_resolutions(muse_root, patterns[0].pattern_id)
212 assert resolutions[0].confidence == pytest.approx(0.9)
213
214 def test_import_rerere_idempotent(
215 self, tmp_path: pathlib.Path
216 ) -> None:
217 """Importing the same rr-cache entry twice does not create duplicates."""
218 from muse.core.bridge.harmony_shelf import import_rerere_to_harmony
219 from muse.core.harmony import list_patterns, list_resolutions
220
221 muse_root = _make_muse_repo(tmp_path / "muse")
222 git_root = _make_git_repo(tmp_path / "git")
223
224 _make_rr_cache_entry(
225 git_root,
226 name="idem_entry",
227 preimage=b"conflict bytes",
228 postimage=b"resolved bytes",
229 )
230
231 import_rerere_to_harmony(muse_root, git_root)
232 import_rerere_to_harmony(muse_root, git_root)
233
234 patterns = list_patterns(muse_root)
235 assert len(patterns) == 1
236 resolutions = list_resolutions(muse_root, patterns[0].pattern_id)
237 assert len(resolutions) == 1
238
239
240 class TestExportHarmonyToRerere:
241
242 def test_export_harmony_no_patterns_returns_zero(
243 self, tmp_path: pathlib.Path
244 ) -> None:
245 """Empty harmony store → 0 exported."""
246 from muse.core.bridge.harmony_shelf import export_harmony_to_rerere
247
248 muse_root = _make_muse_repo(tmp_path / "muse")
249 git_root = _make_git_repo(tmp_path / "git")
250
251 count = export_harmony_to_rerere(muse_root, git_root)
252 assert count == 0
253
254 def test_export_harmony_low_confidence_skipped(
255 self, tmp_path: pathlib.Path
256 ) -> None:
257 """Pattern with confidence < 0.8 and not human_verified → not exported."""
258 from muse.core.bridge.harmony_shelf import export_harmony_to_rerere
259 from muse.core.harmony import (
260 AgentProvenance, ConflictPattern, ConflictType, Resolution,
261 ResolutionStrategy, blob_fingerprint, compute_pattern_id,
262 compute_resolution_id, record_pattern, save_resolution,
263 )
264 from muse.core.object_store import write_object
265 from muse.core.types import blob_id, fake_id
266 import datetime
267
268 muse_root = _make_muse_repo(tmp_path / "muse")
269 git_root = _make_git_repo(tmp_path / "git")
270
271 preimage_bytes = b"conflict content low conf"
272 postimage_bytes = b"resolved low conf"
273
274 ours_id = blob_id(preimage_bytes)
275 write_object(muse_root, ours_id, preimage_bytes)
276 outcome_blob = blob_id(postimage_bytes)
277 write_object(muse_root, outcome_blob, postimage_bytes)
278
279 theirs_id = fake_id("theirs-low")
280 blob_fp = blob_fingerprint(ours_id, theirs_id)
281 pattern_id = compute_pattern_id("src/foo.py", blob_fp, blob_fp)
282 now = datetime.datetime.now(datetime.timezone.utc)
283
284 pattern = ConflictPattern(
285 pattern_id=pattern_id,
286 path="src/foo.py",
287 domain="code",
288 conflict_type=ConflictType.CONTENT,
289 blob_fingerprint=blob_fp,
290 semantic_fingerprint=blob_fp,
291 ours_id=ours_id,
292 theirs_id=theirs_id,
293 description={},
294 recorded_at=now,
295 recorded_by="test",
296 )
297 record_pattern(muse_root, pattern)
298
299 prov = AgentProvenance.agent("test", "test-model")
300 res_id = compute_resolution_id(pattern_id, outcome_blob, ResolutionStrategy.MANUAL, prov, now)
301 resolution = Resolution(
302 resolution_id=res_id,
303 pattern_id=pattern_id,
304 strategy=ResolutionStrategy.MANUAL,
305 policy_id=None,
306 outcome_blob=outcome_blob,
307 resolved_by=prov,
308 human_verified=False,
309 confidence=0.5, # low — should be skipped
310 rationale="low confidence",
311 resolved_at=now,
312 )
313 save_resolution(muse_root, resolution)
314
315 count = export_harmony_to_rerere(muse_root, git_root)
316 assert count == 0
317
318 def test_export_harmony_high_confidence_exported(
319 self, tmp_path: pathlib.Path
320 ) -> None:
321 """Pattern with confidence >= 0.8 → written to rr-cache."""
322 from muse.core.bridge.harmony_shelf import export_harmony_to_rerere
323 from muse.core.harmony import (
324 AgentProvenance, ConflictPattern, ConflictType, Resolution,
325 ResolutionStrategy, blob_fingerprint, compute_pattern_id,
326 compute_resolution_id, record_pattern, save_resolution,
327 )
328 from muse.core.object_store import write_object
329 from muse.core.types import blob_id, fake_id
330 import datetime
331
332 muse_root = _make_muse_repo(tmp_path / "muse")
333 git_root = _make_git_repo(tmp_path / "git")
334
335 preimage_bytes = b"conflict content high conf"
336 postimage_bytes = b"resolved high conf"
337
338 ours_id = blob_id(preimage_bytes)
339 write_object(muse_root, ours_id, preimage_bytes)
340 outcome_blob = blob_id(postimage_bytes)
341 write_object(muse_root, outcome_blob, postimage_bytes)
342
343 theirs_id = fake_id("theirs-high")
344 blob_fp = blob_fingerprint(ours_id, theirs_id)
345 pattern_id = compute_pattern_id("src/bar.py", blob_fp, blob_fp)
346 now = datetime.datetime.now(datetime.timezone.utc)
347
348 pattern = ConflictPattern(
349 pattern_id=pattern_id,
350 path="src/bar.py",
351 domain="code",
352 conflict_type=ConflictType.CONTENT,
353 blob_fingerprint=blob_fp,
354 semantic_fingerprint=blob_fp,
355 ours_id=ours_id,
356 theirs_id=theirs_id,
357 description={},
358 recorded_at=now,
359 recorded_by="test",
360 )
361 record_pattern(muse_root, pattern)
362
363 prov = AgentProvenance.agent("test", "test-model")
364 res_id = compute_resolution_id(pattern_id, outcome_blob, ResolutionStrategy.MANUAL, prov, now)
365 resolution = Resolution(
366 resolution_id=res_id,
367 pattern_id=pattern_id,
368 strategy=ResolutionStrategy.MANUAL,
369 policy_id=None,
370 outcome_blob=outcome_blob,
371 resolved_by=prov,
372 human_verified=False,
373 confidence=0.9, # high — should be exported
374 rationale="high confidence",
375 resolved_at=now,
376 )
377 save_resolution(muse_root, resolution)
378
379 count = export_harmony_to_rerere(muse_root, git_root)
380 assert count == 1
381
382 rr_cache = git_root / ".git" / "rr-cache"
383 entries = list(rr_cache.iterdir())
384 assert len(entries) == 1
385 assert (entries[0] / "preimage").read_bytes() == preimage_bytes
386 assert (entries[0] / "postimage").read_bytes() == postimage_bytes
387
388 def test_export_harmony_human_verified_exported_regardless_of_confidence(
389 self, tmp_path: pathlib.Path
390 ) -> None:
391 """human_verified=True bypasses the confidence threshold."""
392 from muse.core.bridge.harmony_shelf import export_harmony_to_rerere
393 from muse.core.harmony import (
394 AgentProvenance, ConflictPattern, ConflictType, Resolution,
395 ResolutionStrategy, blob_fingerprint, compute_pattern_id,
396 compute_resolution_id, record_pattern, save_resolution,
397 )
398 from muse.core.object_store import write_object
399 from muse.core.types import blob_id, fake_id
400 import datetime
401
402 muse_root = _make_muse_repo(tmp_path / "muse")
403 git_root = _make_git_repo(tmp_path / "git")
404
405 preimage_bytes = b"human verified pre"
406 postimage_bytes = b"human verified post"
407
408 ours_id = blob_id(preimage_bytes)
409 write_object(muse_root, ours_id, preimage_bytes)
410 outcome_blob = blob_id(postimage_bytes)
411 write_object(muse_root, outcome_blob, postimage_bytes)
412
413 theirs_id = fake_id("theirs-human")
414 blob_fp = blob_fingerprint(ours_id, theirs_id)
415 pattern_id = compute_pattern_id("src/baz.py", blob_fp, blob_fp)
416 now = datetime.datetime.now(datetime.timezone.utc)
417
418 pattern = ConflictPattern(
419 pattern_id=pattern_id,
420 path="src/baz.py",
421 domain="code",
422 conflict_type=ConflictType.CONTENT,
423 blob_fingerprint=blob_fp,
424 semantic_fingerprint=blob_fp,
425 ours_id=ours_id,
426 theirs_id=theirs_id,
427 description={},
428 recorded_at=now,
429 recorded_by="test",
430 )
431 record_pattern(muse_root, pattern)
432
433 prov = AgentProvenance.human()
434 res_id = compute_resolution_id(pattern_id, outcome_blob, ResolutionStrategy.MANUAL, prov, now)
435 resolution = Resolution(
436 resolution_id=res_id,
437 pattern_id=pattern_id,
438 strategy=ResolutionStrategy.MANUAL,
439 policy_id=None,
440 outcome_blob=outcome_blob,
441 resolved_by=prov,
442 human_verified=True,
443 confidence=0.3, # low confidence but human_verified → should export
444 rationale="human verified",
445 resolved_at=now,
446 )
447 save_resolution(muse_root, resolution)
448
449 count = export_harmony_to_rerere(muse_root, git_root)
450 assert count == 1
451
452 def test_export_harmony_dry_run_writes_nothing(
453 self, tmp_path: pathlib.Path
454 ) -> None:
455 """dry_run=True returns count but creates no rr-cache entries."""
456 from muse.core.bridge.harmony_shelf import export_harmony_to_rerere
457 from muse.core.harmony import (
458 AgentProvenance, ConflictPattern, ConflictType, Resolution,
459 ResolutionStrategy, blob_fingerprint, compute_pattern_id,
460 compute_resolution_id, record_pattern, save_resolution,
461 )
462 from muse.core.object_store import write_object
463 from muse.core.types import blob_id, fake_id
464 import datetime
465
466 muse_root = _make_muse_repo(tmp_path / "muse")
467 git_root = _make_git_repo(tmp_path / "git")
468
469 preimage_bytes = b"dry run pre"
470 postimage_bytes = b"dry run post"
471
472 ours_id = blob_id(preimage_bytes)
473 write_object(muse_root, ours_id, preimage_bytes)
474 outcome_blob = blob_id(postimage_bytes)
475 write_object(muse_root, outcome_blob, postimage_bytes)
476
477 theirs_id = fake_id("theirs-dry")
478 blob_fp = blob_fingerprint(ours_id, theirs_id)
479 pattern_id = compute_pattern_id("src/dry.py", blob_fp, blob_fp)
480 now = datetime.datetime.now(datetime.timezone.utc)
481
482 pattern = ConflictPattern(
483 pattern_id=pattern_id,
484 path="src/dry.py",
485 domain="code",
486 conflict_type=ConflictType.CONTENT,
487 blob_fingerprint=blob_fp,
488 semantic_fingerprint=blob_fp,
489 ours_id=ours_id,
490 theirs_id=theirs_id,
491 description={},
492 recorded_at=now,
493 recorded_by="test",
494 )
495 record_pattern(muse_root, pattern)
496
497 prov = AgentProvenance.agent("test", "test-model")
498 res_id = compute_resolution_id(pattern_id, outcome_blob, ResolutionStrategy.MANUAL, prov, now)
499 resolution = Resolution(
500 resolution_id=res_id,
501 pattern_id=pattern_id,
502 strategy=ResolutionStrategy.MANUAL,
503 policy_id=None,
504 outcome_blob=outcome_blob,
505 resolved_by=prov,
506 human_verified=False,
507 confidence=0.95,
508 rationale="high conf dry run",
509 resolved_at=now,
510 )
511 save_resolution(muse_root, resolution)
512
513 count = export_harmony_to_rerere(muse_root, git_root, dry_run=True)
514 assert count == 1
515
516 rr_cache = git_root / ".git" / "rr-cache"
517 assert not rr_cache.exists() or len(list(rr_cache.iterdir())) == 0
518
519
520 # ---------------------------------------------------------------------------
521 # Tier 2 — Shelf ↔ Stash
522 # ---------------------------------------------------------------------------
523
524
525 class TestImportStashesToShelf:
526
527 def test_import_stashes_no_stashes_returns_zero(
528 self, tmp_path: pathlib.Path
529 ) -> None:
530 """No git stashes → 0 shelf entries created."""
531 from muse.core.bridge.harmony_shelf import import_stashes_to_shelf
532 from muse.core.shelf import list_shelf_entries
533
534 muse_root = _make_muse_repo(tmp_path / "muse")
535 git_root = _make_git_repo(tmp_path / "git")
536
537 count = import_stashes_to_shelf(muse_root, git_root)
538 assert count == 0
539 assert list_shelf_entries(muse_root) == []
540
541 def test_import_one_stash_creates_shelf_entry(
542 self, tmp_path: pathlib.Path
543 ) -> None:
544 """One git stash → one Muse shelf entry with intent_type='handoff'."""
545 from muse.core.bridge.harmony_shelf import import_stashes_to_shelf
546 from muse.core.shelf import list_shelf_entries
547
548 muse_root = _make_muse_repo(tmp_path / "muse")
549 git_root = _make_git_repo(tmp_path / "git")
550
551 # Create a stash in the git repo.
552 (git_root / "work.py").write_text("in progress work")
553 subprocess.run(["git", "-C", str(git_root), "add", "."], check=True, capture_output=True)
554 subprocess.run(
555 ["git", "-C", str(git_root), "stash", "push", "-m", "WIP: some feature"],
556 check=True, capture_output=True,
557 )
558
559 count = import_stashes_to_shelf(muse_root, git_root)
560 assert count == 1
561
562 entries = list_shelf_entries(muse_root)
563 assert len(entries) == 1
564 entry = entries[0]
565 assert entry.get("intent_type") == "handoff"
566 assert "WIP: some feature" in str(entry.get("intent", ""))
567
568 def test_import_stash_dry_run_writes_nothing(
569 self, tmp_path: pathlib.Path
570 ) -> None:
571 """dry_run=True returns count but creates no shelf entries."""
572 from muse.core.bridge.harmony_shelf import import_stashes_to_shelf
573 from muse.core.shelf import list_shelf_entries
574
575 muse_root = _make_muse_repo(tmp_path / "muse")
576 git_root = _make_git_repo(tmp_path / "git")
577
578 (git_root / "wip.py").write_text("work in progress")
579 subprocess.run(["git", "-C", str(git_root), "add", "."], check=True, capture_output=True)
580 subprocess.run(
581 ["git", "-C", str(git_root), "stash", "push", "-m", "dry run stash"],
582 check=True, capture_output=True,
583 )
584
585 count = import_stashes_to_shelf(muse_root, git_root, dry_run=True)
586 assert count == 1
587 assert list_shelf_entries(muse_root) == []
588
589 def test_import_stashes_idempotent(
590 self, tmp_path: pathlib.Path
591 ) -> None:
592 """Calling import twice does not duplicate shelf entries."""
593 from muse.core.bridge.harmony_shelf import import_stashes_to_shelf
594 from muse.core.shelf import list_shelf_entries
595
596 muse_root = _make_muse_repo(tmp_path / "muse")
597 git_root = _make_git_repo(tmp_path / "git")
598
599 (git_root / "feature.py").write_text("feature code")
600 subprocess.run(["git", "-C", str(git_root), "add", "."], check=True, capture_output=True)
601 subprocess.run(
602 ["git", "-C", str(git_root), "stash", "push", "-m", "feature stash"],
603 check=True, capture_output=True,
604 )
605
606 first = import_stashes_to_shelf(muse_root, git_root)
607 second = import_stashes_to_shelf(muse_root, git_root)
608 assert first == 1
609 assert second == 0
610 assert len(list_shelf_entries(muse_root)) == 1
611
612
613 class TestExportShelvesToStash:
614
615 def test_export_shelves_no_entries_returns_zero(
616 self, tmp_path: pathlib.Path
617 ) -> None:
618 """No Muse shelf entries → 0 stashes created."""
619 from muse.core.bridge.harmony_shelf import export_shelves_to_stash
620
621 muse_root = _make_muse_repo(tmp_path / "muse")
622 git_root = _make_git_repo(tmp_path / "git")
623
624 count = export_shelves_to_stash(muse_root, git_root)
625 assert count == 0
626
627 # Verify no stashes were created.
628 result = subprocess.run(
629 ["git", "-C", str(git_root), "stash", "list"],
630 capture_output=True, text=True, check=True,
631 )
632 assert result.stdout.strip() == ""
633
634 def test_export_one_shelf_creates_git_stash(
635 self, tmp_path: pathlib.Path
636 ) -> None:
637 """One Muse shelf entry → one git stash."""
638 from muse.core.bridge.harmony_shelf import export_shelves_to_stash
639 from muse.core.object_store import write_object
640 from muse.core.types import blob_id, content_hash, now_utc_iso
641 from muse.core.shelf import write_shelf_entry
642
643 muse_root = _make_muse_repo(tmp_path / "muse")
644 git_root = _make_git_repo(tmp_path / "git")
645
646 # Write a blob into Muse object store.
647 blob_bytes = b"shelf file content"
648 obj_id = blob_id(blob_bytes)
649 write_object(muse_root, obj_id, blob_bytes)
650
651 snapshot = {"feature.py": obj_id}
652 snapshot_id = content_hash(snapshot)
653 now_iso = now_utc_iso()
654
655 entry_without_id = {
656 "name": "test-shelf-entry",
657 "snapshot": snapshot,
658 "deleted": [],
659 "snapshot_id": snapshot_id,
660 "parent_commit": "",
661 "branch": "dev",
662 "created_at": now_iso,
663 "created_by": "test",
664 "intent_type": "handoff",
665 "intent": "test shelf export",
666 "resumable": True,
667 "tags": [],
668 "expires_at": None,
669 "domain_state": {},
670 }
671 shelf_id = content_hash(entry_without_id)
672 entry = dict(entry_without_id)
673 entry["id"] = shelf_id
674 write_shelf_entry(muse_root, entry)
675
676 count = export_shelves_to_stash(muse_root, git_root)
677 assert count == 1
678
679 result = subprocess.run(
680 ["git", "-C", str(git_root), "stash", "list"],
681 capture_output=True, text=True, check=True,
682 )
683 assert "muse-shelf" in result.stdout
684
685 def test_export_shelves_dry_run_writes_nothing(
686 self, tmp_path: pathlib.Path
687 ) -> None:
688 """dry_run=True returns count but creates no git stashes."""
689 from muse.core.bridge.harmony_shelf import export_shelves_to_stash
690 from muse.core.object_store import write_object
691 from muse.core.types import blob_id, content_hash, now_utc_iso
692 from muse.core.shelf import write_shelf_entry
693
694 muse_root = _make_muse_repo(tmp_path / "muse")
695 git_root = _make_git_repo(tmp_path / "git")
696
697 blob_bytes = b"dry run blob"
698 obj_id = blob_id(blob_bytes)
699 write_object(muse_root, obj_id, blob_bytes)
700
701 snapshot = {"dry.py": obj_id}
702 snapshot_id = content_hash(snapshot)
703 now_iso = now_utc_iso()
704
705 entry_without_id = {
706 "name": "dry-shelf",
707 "snapshot": snapshot,
708 "deleted": [],
709 "snapshot_id": snapshot_id,
710 "parent_commit": "",
711 "branch": "dev",
712 "created_at": now_iso,
713 "created_by": "test",
714 "intent_type": "checkpoint",
715 "intent": "dry run test",
716 "resumable": False,
717 "tags": [],
718 "expires_at": None,
719 "domain_state": {},
720 }
721 shelf_id = content_hash(entry_without_id)
722 entry = dict(entry_without_id)
723 entry["id"] = shelf_id
724 write_shelf_entry(muse_root, entry)
725
726 count = export_shelves_to_stash(muse_root, git_root, dry_run=True)
727 assert count == 1
728
729 result = subprocess.run(
730 ["git", "-C", str(git_root), "stash", "list"],
731 capture_output=True, text=True, check=True,
732 )
733 assert result.stdout.strip() == ""
734
735
736 # ---------------------------------------------------------------------------
737 # Tier 3 — Argparse flag presence
738 # ---------------------------------------------------------------------------
739
740
741 class TestArgparseFlags:
742
743 def test_git_import_has_import_rerere_flag(self) -> None:
744 result = _invoke("bridge", "git-import", "--help")
745 assert "--import-rerere" in result.output
746
747 def test_git_import_has_rerere_confidence_flag(self) -> None:
748 result = _invoke("bridge", "git-import", "--help")
749 assert "--rerere-confidence" in result.output
750
751 def test_git_import_has_import_stashes_flag(self) -> None:
752 result = _invoke("bridge", "git-import", "--help")
753 assert "--import-stashes" in result.output
754
755 def test_git_export_has_export_rerere_flag(self) -> None:
756 result = _invoke("bridge", "git-export", "--help")
757 assert "--export-rerere" in result.output
758
759 def test_git_export_has_export_shelves_flag(self) -> None:
760 result = _invoke("bridge", "git-export", "--help")
761 assert "--export-shelves" in result.output
File History 1 commit
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b fix: try fetch/presign before fetch/mpack to avoid Cloudfla… Sonnet 4.6 patch 6 days ago