cgcardona / muse public
test_muse_diff.py python
712 lines 23.5 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 """Tests for ``muse diff`` — music-dimension diff flags.
2
3 All CLI-level tests use ``typer.testing.CliRunner`` against the full ``muse``
4 app so argument parsing, flag handling, and exit codes are exercised end-to-end.
5
6 Async core tests call dimension functions directly with a minimal .muse/ layout
7 (defined in ``_init_muse_repo``). The session parameter is reserved for the full
8 implementation and is not exercised by the stub — it is injected only to keep
9 the function signatures stable.
10
11 Test naming follows the ``test_<behavior>_<scenario>`` convention.
12 """
13 from __future__ import annotations
14
15 import json
16 import os
17 import pathlib
18 import uuid
19
20 import pytest
21 import pytest_asyncio
22 from collections.abc import AsyncGenerator
23 from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
24 from sqlalchemy.pool import StaticPool
25 from typer.testing import CliRunner
26
27 from maestro.db.database import Base
28 import maestro.muse_cli.models # noqa: F401 — registers MuseCli* ORM models
29 from maestro.muse_cli.app import cli
30 from maestro.muse_cli.commands.diff import (
31 DynamicDiffResult,
32 HarmonicDiffResult,
33 MelodicDiffResult,
34 MusicDiffReport,
35 RhythmicDiffResult,
36 StructuralDiffResult,
37 _diff_all_async,
38 _dynamic_diff_async,
39 _harmonic_diff_async,
40 _melodic_diff_async,
41 _resolve_refs,
42 _rhythmic_diff_async,
43 _stub_dynamic,
44 _stub_harmonic,
45 _stub_melodic,
46 _stub_rhythmic,
47 _stub_structural,
48 _structural_diff_async,
49 _tension_label,
50 _render_dynamic,
51 _render_harmonic,
52 _render_melodic,
53 _render_rhythmic,
54 _render_structural,
55 _render_report,
56 )
57 from maestro.muse_cli.errors import ExitCode
58
59 runner = CliRunner()
60
61 # ---------------------------------------------------------------------------
62 # Helpers
63 # ---------------------------------------------------------------------------
64
65
66 def _init_muse_repo(root: pathlib.Path, branch: str = "main") -> str:
67 """Create a minimal .muse/ layout with one empty commit ref."""
68 rid = str(uuid.uuid4())
69 muse = root / ".muse"
70 (muse / "refs" / "heads").mkdir(parents=True)
71 (muse / "repo.json").write_text(json.dumps({"repo_id": rid, "schema_version": "1"}))
72 (muse / "HEAD").write_text(f"refs/heads/{branch}")
73 (muse / "refs" / "heads" / branch).write_text("")
74 return rid
75
76
77 def _commit_ref(root: pathlib.Path, branch: str = "main") -> None:
78 """Write a fake commit SHA into the branch ref file."""
79 muse = root / ".muse"
80 (muse / "refs" / "heads" / branch).write_text("a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2")
81
82
83 @pytest_asyncio.fixture
84 async def db_session() -> AsyncGenerator[AsyncSession, None]:
85 """In-memory SQLite session (stub diff does not query the DB)."""
86 engine = create_async_engine(
87 "sqlite+aiosqlite:///:memory:",
88 connect_args={"check_same_thread": False},
89 poolclass=StaticPool,
90 )
91 async with engine.begin() as conn:
92 await conn.run_sync(Base.metadata.create_all)
93 factory = async_sessionmaker(bind=engine, expire_on_commit=False)
94 async with factory() as session:
95 yield session
96 async with engine.begin() as conn:
97 await conn.run_sync(Base.metadata.drop_all)
98 await engine.dispose()
99
100
101 # ---------------------------------------------------------------------------
102 # Unit — _tension_label
103 # ---------------------------------------------------------------------------
104
105
106 def test_tension_label_low() -> None:
107 """Values below 0.33 are labelled Low."""
108 assert _tension_label(0.0) == "Low"
109 assert _tension_label(0.32) == "Low"
110
111
112 def test_tension_label_medium() -> None:
113 """Values in [0.33, 0.66) are labelled Medium."""
114 assert _tension_label(0.33) == "Medium"
115 assert _tension_label(0.50) == "Medium"
116
117
118 def test_tension_label_medium_high() -> None:
119 """Values in [0.66, 0.80) are labelled Medium-High."""
120 assert _tension_label(0.66) == "Medium-High"
121 assert _tension_label(0.79) == "Medium-High"
122
123
124 def test_tension_label_high() -> None:
125 """Values >= 0.80 are labelled High."""
126 assert _tension_label(0.80) == "High"
127 assert _tension_label(1.0) == "High"
128
129
130 # ---------------------------------------------------------------------------
131 # Unit — stub constructors
132 # ---------------------------------------------------------------------------
133
134
135 def test_stub_harmonic_fields_present() -> None:
136 """HarmonicDiffResult from stub has all required TypedDict fields."""
137 result = _stub_harmonic("abc1234", "def5678")
138 assert result["commit_a"] == "abc1234"
139 assert result["commit_b"] == "def5678"
140 assert result["key_a"] and result["key_b"]
141 assert result["mode_a"] and result["mode_b"]
142 assert result["chord_prog_a"] and result["chord_prog_b"]
143 assert 0.0 <= result["tension_a"] <= 1.0
144 assert 0.0 <= result["tension_b"] <= 1.0
145 assert result["summary"]
146 assert isinstance(result["changed"], bool)
147
148
149 def test_stub_rhythmic_fields_present() -> None:
150 """RhythmicDiffResult from stub has all required TypedDict fields."""
151 result = _stub_rhythmic("abc1234", "def5678")
152 assert result["tempo_a"] > 0
153 assert result["tempo_b"] > 0
154 assert result["meter_a"] and result["meter_b"]
155 assert 0.5 <= result["swing_a"] <= 0.67
156 assert 0.5 <= result["swing_b"] <= 0.67
157 assert result["summary"]
158
159
160 def test_stub_melodic_fields_present() -> None:
161 """MelodicDiffResult from stub has all required TypedDict fields."""
162 result = _stub_melodic("abc1234", "def5678")
163 assert isinstance(result["motifs_introduced"], list)
164 assert isinstance(result["motifs_removed"], list)
165 assert result["contour_a"] and result["contour_b"]
166 assert result["range_low_a"] < result["range_high_a"]
167 assert result["range_low_b"] < result["range_high_b"]
168
169
170 def test_stub_structural_fields_present() -> None:
171 """StructuralDiffResult from stub has all required TypedDict fields."""
172 result = _stub_structural("abc1234", "def5678")
173 assert isinstance(result["sections_added"], list)
174 assert isinstance(result["sections_removed"], list)
175 assert isinstance(result["instruments_added"], list)
176 assert isinstance(result["instruments_removed"], list)
177 assert result["form_a"] and result["form_b"]
178
179
180 def test_stub_dynamic_fields_present() -> None:
181 """DynamicDiffResult from stub has all required TypedDict fields."""
182 result = _stub_dynamic("abc1234", "def5678")
183 assert 0 <= result["avg_velocity_a"] <= 127
184 assert 0 <= result["avg_velocity_b"] <= 127
185 assert result["arc_a"] and result["arc_b"]
186 assert isinstance(result["tracks_louder"], list)
187 assert isinstance(result["tracks_softer"], list)
188 assert isinstance(result["tracks_silent"], list)
189
190
191 # ---------------------------------------------------------------------------
192 # Unit — _resolve_refs
193 # ---------------------------------------------------------------------------
194
195
196 def test_resolve_refs_defaults_to_head(tmp_path: pathlib.Path) -> None:
197 """When both refs are None, resolves to head commit (or HEAD) and HEAD~1."""
198 _init_muse_repo(tmp_path)
199 _commit_ref(tmp_path)
200 ref_a, ref_b = _resolve_refs(tmp_path, None, None)
201 assert ref_b == "a1b2c3d4"
202 assert ref_a == "a1b2c3d4~1"
203
204
205 def test_resolve_refs_explicit_refs_passthrough(tmp_path: pathlib.Path) -> None:
206 """Explicit ref strings are returned unchanged."""
207 _init_muse_repo(tmp_path)
208 _commit_ref(tmp_path)
209 ref_a, ref_b = _resolve_refs(tmp_path, "abc123", "def456")
210 assert ref_a == "abc123"
211 assert ref_b == "def456"
212
213
214 def test_resolve_refs_no_commits_falls_back(tmp_path: pathlib.Path) -> None:
215 """With no commits, falls back to symbolic HEAD token."""
216 _init_muse_repo(tmp_path)
217 ref_a, ref_b = _resolve_refs(tmp_path, None, None)
218 assert ref_b == "HEAD"
219 assert ref_a == "HEAD~1"
220
221
222 # ---------------------------------------------------------------------------
223 # Unit — renderers
224 # ---------------------------------------------------------------------------
225
226
227 def test_render_harmonic_contains_key_fields() -> None:
228 """_render_harmonic output contains commit refs and key change info."""
229 result = _stub_harmonic("abc1234", "def5678")
230 text = _render_harmonic(result)
231 assert "abc1234" in text
232 assert "def5678" in text
233 assert result["key_a"] in text
234 assert result["key_b"] in text
235 assert result["summary"] in text
236
237
238 def test_render_rhythmic_shows_tempo_delta() -> None:
239 """_render_rhythmic output shows tempo delta with sign."""
240 result = _stub_rhythmic("abc1234", "def5678")
241 text = _render_rhythmic(result)
242 assert "BPM" in text
243 assert result["summary"] in text
244
245
246 def test_render_melodic_lists_motifs() -> None:
247 """_render_melodic output includes introduced motif names."""
248 result = _stub_melodic("abc1234", "def5678")
249 text = _render_melodic(result)
250 for motif in result["motifs_introduced"]:
251 assert motif in text
252
253
254 def test_render_structural_shows_sections() -> None:
255 """_render_structural output lists added and removed sections."""
256 result = _stub_structural("abc1234", "def5678")
257 text = _render_structural(result)
258 for section in result["sections_added"]:
259 assert section in text
260
261
262 def test_render_dynamic_shows_velocity_delta() -> None:
263 """_render_dynamic output includes avg velocity and arc change."""
264 result = _stub_dynamic("abc1234", "def5678")
265 text = _render_dynamic(result)
266 assert str(result["avg_velocity_a"]) in text
267 assert str(result["avg_velocity_b"]) in text
268 assert result["arc_a"] in text
269 assert result["arc_b"] in text
270
271
272 def test_render_report_contains_all_dimensions() -> None:
273 """_render_report includes headers for all five dimensions."""
274 import asyncio
275 import pathlib
276
277 async def _make_report() -> MusicDiffReport:
278 return await _diff_all_async(
279 root=pathlib.Path("/tmp"),
280 commit_a="abc1234",
281 commit_b="def5678",
282 )
283
284 report = asyncio.run(_make_report())
285 text = _render_report(report)
286 for dim in ("Harmonic", "Rhythmic", "Melodic", "Structural", "Dynamic"):
287 assert dim in text
288
289
290 # ---------------------------------------------------------------------------
291 # Async core — individual dimension functions
292 # ---------------------------------------------------------------------------
293
294
295 @pytest.mark.anyio
296 async def test_harmonic_diff_async_returns_correct_type(
297 tmp_path: pathlib.Path,
298 ) -> None:
299 """_harmonic_diff_async returns a HarmonicDiffResult with correct commit refs."""
300 _init_muse_repo(tmp_path)
301 result = await _harmonic_diff_async(
302 root=tmp_path, commit_a="abc1234", commit_b="def5678"
303 )
304 assert result["commit_a"] == "abc1234"
305 assert result["commit_b"] == "def5678"
306 assert isinstance(result["changed"], bool)
307
308
309 @pytest.mark.anyio
310 async def test_rhythmic_diff_async_returns_correct_type(
311 tmp_path: pathlib.Path,
312 ) -> None:
313 """_rhythmic_diff_async returns a RhythmicDiffResult with correct commit refs."""
314 _init_muse_repo(tmp_path)
315 result = await _rhythmic_diff_async(
316 root=tmp_path, commit_a="abc1234", commit_b="def5678"
317 )
318 assert result["commit_a"] == "abc1234"
319 assert result["commit_b"] == "def5678"
320
321
322 @pytest.mark.anyio
323 async def test_melodic_diff_async_returns_correct_type(
324 tmp_path: pathlib.Path,
325 ) -> None:
326 """_melodic_diff_async returns a MelodicDiffResult with correct commit refs."""
327 _init_muse_repo(tmp_path)
328 result = await _melodic_diff_async(
329 root=tmp_path, commit_a="abc1234", commit_b="def5678"
330 )
331 assert result["commit_a"] == "abc1234"
332 assert result["commit_b"] == "def5678"
333
334
335 @pytest.mark.anyio
336 async def test_structural_diff_async_returns_correct_type(
337 tmp_path: pathlib.Path,
338 ) -> None:
339 """_structural_diff_async returns a StructuralDiffResult with correct commit refs."""
340 _init_muse_repo(tmp_path)
341 result = await _structural_diff_async(
342 root=tmp_path, commit_a="abc1234", commit_b="def5678"
343 )
344 assert result["commit_a"] == "abc1234"
345 assert result["commit_b"] == "def5678"
346
347
348 @pytest.mark.anyio
349 async def test_dynamic_diff_async_returns_correct_type(
350 tmp_path: pathlib.Path,
351 ) -> None:
352 """_dynamic_diff_async returns a DynamicDiffResult with correct commit refs."""
353 _init_muse_repo(tmp_path)
354 result = await _dynamic_diff_async(
355 root=tmp_path, commit_a="abc1234", commit_b="def5678"
356 )
357 assert result["commit_a"] == "abc1234"
358 assert result["commit_b"] == "def5678"
359
360
361 # ---------------------------------------------------------------------------
362 # Regression — test_muse_diff_harmonic_flag_produces_harmonic_report
363 # ---------------------------------------------------------------------------
364
365
366 @pytest.mark.anyio
367 async def test_muse_diff_harmonic_flag_produces_harmonic_report(
368 tmp_path: pathlib.Path,
369 capsys: pytest.CaptureFixture[str],
370 ) -> None:
371 """Regression: --harmonic flag produces a harmonic-dimension report, not a generic file diff."""
372 _init_muse_repo(tmp_path)
373 _commit_ref(tmp_path)
374
375 result = await _harmonic_diff_async(
376 root=tmp_path, commit_a="abc1234", commit_b="def5678"
377 )
378 text = _render_harmonic(result)
379 assert "Harmonic diff" in text
380 assert "Key:" in text
381 assert "Chord prog:" in text
382 assert "Tension:" in text
383 assert "Summary:" in text
384
385
386 # ---------------------------------------------------------------------------
387 # Regression — test_muse_diff_rhythmic_flag_produces_rhythmic_report
388 # ---------------------------------------------------------------------------
389
390
391 @pytest.mark.anyio
392 async def test_muse_diff_rhythmic_flag_produces_rhythmic_report(
393 tmp_path: pathlib.Path,
394 ) -> None:
395 """Regression: --rhythmic flag produces a rhythmic-dimension report."""
396 _init_muse_repo(tmp_path)
397 _commit_ref(tmp_path)
398
399 result = await _rhythmic_diff_async(
400 root=tmp_path, commit_a="abc1234", commit_b="def5678"
401 )
402 text = _render_rhythmic(result)
403 assert "Rhythmic diff" in text
404 assert "Tempo:" in text
405 assert "Swing:" in text
406
407
408 # ---------------------------------------------------------------------------
409 # Regression — test_muse_diff_all_flag_combines_all_dimensions
410 # ---------------------------------------------------------------------------
411
412
413 @pytest.mark.anyio
414 async def test_muse_diff_all_flag_combines_all_dimensions(
415 tmp_path: pathlib.Path,
416 ) -> None:
417 """Regression: --all produces a MusicDiffReport with all five dimensions populated."""
418 _init_muse_repo(tmp_path)
419 _commit_ref(tmp_path)
420
421 report = await _diff_all_async(
422 root=tmp_path, commit_a="abc1234", commit_b="def5678"
423 )
424 assert report["harmonic"] is not None
425 assert report["rhythmic"] is not None
426 assert report["melodic"] is not None
427 assert report["structural"] is not None
428 assert report["dynamic"] is not None
429 assert report["summary"]
430 assert isinstance(report["changed_dimensions"], list)
431 assert isinstance(report["unchanged_dimensions"], list)
432
433
434 # ---------------------------------------------------------------------------
435 # Regression — test_muse_diff_unchanged_dimension_reported_not_omitted
436 # ---------------------------------------------------------------------------
437
438
439 def test_muse_diff_unchanged_dimension_reported_not_omitted() -> None:
440 """Regression: dimensions with no change report 'Unchanged', not an omission."""
441 from maestro.muse_cli.commands.diff import (
442 HarmonicDiffResult,
443 _render_harmonic,
444 )
445
446 unchanged_result = HarmonicDiffResult(
447 commit_a="abc1234",
448 commit_b="def5678",
449 key_a="C major",
450 key_b="C major",
451 mode_a="Major",
452 mode_b="Major",
453 chord_prog_a="I-IV-V-I",
454 chord_prog_b="I-IV-V-I",
455 tension_a=0.2,
456 tension_b=0.2,
457 tension_label_a="Low",
458 tension_label_b="Low",
459 summary="No harmonic change detected.",
460 changed=False,
461 )
462 text = _render_harmonic(unchanged_result)
463 assert "Unchanged" in text
464
465
466 # ---------------------------------------------------------------------------
467 # Async — _diff_all_async JSON roundtrip
468 # ---------------------------------------------------------------------------
469
470
471 @pytest.mark.anyio
472 async def test_diff_all_json_roundtrip(tmp_path: pathlib.Path) -> None:
473 """MusicDiffReport from _diff_all_async is JSON-serializable."""
474 _init_muse_repo(tmp_path)
475 _commit_ref(tmp_path)
476
477 report = await _diff_all_async(
478 root=tmp_path, commit_a="abc1234", commit_b="def5678"
479 )
480 raw = json.dumps(dict(report))
481 parsed = json.loads(raw)
482 assert parsed["commit_a"] == "abc1234"
483 assert parsed["commit_b"] == "def5678"
484 assert "harmonic" in parsed
485 assert "rhythmic" in parsed
486 assert "melodic" in parsed
487 assert "structural" in parsed
488 assert "dynamic" in parsed
489
490
491 # ---------------------------------------------------------------------------
492 # CLI integration — CliRunner
493 # ---------------------------------------------------------------------------
494
495
496 def test_cli_diff_no_flags_exits_success(tmp_path: pathlib.Path) -> None:
497 """``muse diff`` with no dimension flags exits 0 and prints usage hint."""
498 _init_muse_repo(tmp_path)
499 _commit_ref(tmp_path)
500
501 prev = os.getcwd()
502 try:
503 os.chdir(tmp_path)
504 result = runner.invoke(cli, ["diff"], catch_exceptions=False)
505 finally:
506 os.chdir(prev)
507
508 assert result.exit_code == int(ExitCode.SUCCESS)
509 assert "--harmonic" in result.output or "dimension flag" in result.output
510
511
512 def test_cli_diff_outside_repo_exits_2(tmp_path: pathlib.Path) -> None:
513 """``muse diff --harmonic`` exits 2 when invoked outside a Muse repository."""
514 prev = os.getcwd()
515 try:
516 os.chdir(tmp_path)
517 result = runner.invoke(cli, ["diff", "--harmonic"], catch_exceptions=False)
518 finally:
519 os.chdir(prev)
520
521 assert result.exit_code == int(ExitCode.REPO_NOT_FOUND)
522 assert "not a muse repository" in result.output.lower()
523
524
525 def test_cli_diff_help_lists_all_flags() -> None:
526 """``muse diff --help`` documents all six dimension flags."""
527 result = runner.invoke(cli, ["diff", "--help"])
528 assert result.exit_code == 0
529 for flag in ("--harmonic", "--rhythmic", "--melodic", "--structural", "--dynamic", "--all", "--json"):
530 assert flag in result.output, f"Flag '{flag}' missing from help"
531
532
533 def test_cli_diff_appears_in_muse_help() -> None:
534 """``muse --help`` lists the diff subcommand."""
535 result = runner.invoke(cli, ["--help"])
536 assert result.exit_code == 0
537 assert "diff" in result.output
538
539
540 def test_cli_diff_harmonic_flag_shows_harmonic_report(tmp_path: pathlib.Path) -> None:
541 """``muse diff --harmonic`` shows a harmonic diff block."""
542 _init_muse_repo(tmp_path)
543 _commit_ref(tmp_path)
544
545 prev = os.getcwd()
546 try:
547 os.chdir(tmp_path)
548 result = runner.invoke(cli, ["diff", "--harmonic"], catch_exceptions=False)
549 finally:
550 os.chdir(prev)
551
552 assert result.exit_code == 0
553 assert "Harmonic diff" in result.output
554 assert "Key:" in result.output
555
556
557 def test_cli_diff_rhythmic_flag_shows_rhythmic_report(tmp_path: pathlib.Path) -> None:
558 """``muse diff --rhythmic`` shows a rhythmic diff block."""
559 _init_muse_repo(tmp_path)
560 _commit_ref(tmp_path)
561
562 prev = os.getcwd()
563 try:
564 os.chdir(tmp_path)
565 result = runner.invoke(cli, ["diff", "--rhythmic"], catch_exceptions=False)
566 finally:
567 os.chdir(prev)
568
569 assert result.exit_code == 0
570 assert "Rhythmic diff" in result.output
571 assert "Tempo:" in result.output
572
573
574 def test_cli_diff_melodic_flag_shows_melodic_report(tmp_path: pathlib.Path) -> None:
575 """``muse diff --melodic`` shows a melodic diff block."""
576 _init_muse_repo(tmp_path)
577 _commit_ref(tmp_path)
578
579 prev = os.getcwd()
580 try:
581 os.chdir(tmp_path)
582 result = runner.invoke(cli, ["diff", "--melodic"], catch_exceptions=False)
583 finally:
584 os.chdir(prev)
585
586 assert result.exit_code == 0
587 assert "Melodic diff" in result.output
588
589
590 def test_cli_diff_structural_flag_shows_structural_report(tmp_path: pathlib.Path) -> None:
591 """``muse diff --structural`` shows a structural diff block."""
592 _init_muse_repo(tmp_path)
593 _commit_ref(tmp_path)
594
595 prev = os.getcwd()
596 try:
597 os.chdir(tmp_path)
598 result = runner.invoke(cli, ["diff", "--structural"], catch_exceptions=False)
599 finally:
600 os.chdir(prev)
601
602 assert result.exit_code == 0
603 assert "Structural diff" in result.output
604
605
606 def test_cli_diff_dynamic_flag_shows_dynamic_report(tmp_path: pathlib.Path) -> None:
607 """``muse diff --dynamic`` shows a dynamic diff block."""
608 _init_muse_repo(tmp_path)
609 _commit_ref(tmp_path)
610
611 prev = os.getcwd()
612 try:
613 os.chdir(tmp_path)
614 result = runner.invoke(cli, ["diff", "--dynamic"], catch_exceptions=False)
615 finally:
616 os.chdir(prev)
617
618 assert result.exit_code == 0
619 assert "Dynamic diff" in result.output
620
621
622 def test_cli_diff_all_flag_shows_all_dimensions(tmp_path: pathlib.Path) -> None:
623 """``muse diff --all`` shows all five dimension blocks in one report."""
624 _init_muse_repo(tmp_path)
625 _commit_ref(tmp_path)
626
627 prev = os.getcwd()
628 try:
629 os.chdir(tmp_path)
630 result = runner.invoke(cli, ["diff", "--all"], catch_exceptions=False)
631 finally:
632 os.chdir(prev)
633
634 assert result.exit_code == 0
635 for dim in ("Harmonic", "Rhythmic", "Melodic", "Structural", "Dynamic"):
636 assert dim in result.output, f"Dimension '{dim}' missing from --all output"
637
638
639 def test_cli_diff_json_flag_produces_valid_json(tmp_path: pathlib.Path) -> None:
640 """``muse diff --harmonic --json`` emits parseable JSON."""
641 _init_muse_repo(tmp_path)
642 _commit_ref(tmp_path)
643
644 prev = os.getcwd()
645 try:
646 os.chdir(tmp_path)
647 result = runner.invoke(cli, ["diff", "--harmonic", "--json"], catch_exceptions=False)
648 finally:
649 os.chdir(prev)
650
651 assert result.exit_code == 0
652 payload = json.loads(result.output)
653 assert "commit_a" in payload
654 assert "commit_b" in payload
655 assert "key_a" in payload
656 assert "key_b" in payload
657
658
659 def test_cli_diff_all_json_flag_produces_valid_json(tmp_path: pathlib.Path) -> None:
660 """``muse diff --all --json`` emits parseable JSON with all dimensions."""
661 _init_muse_repo(tmp_path)
662 _commit_ref(tmp_path)
663
664 prev = os.getcwd()
665 try:
666 os.chdir(tmp_path)
667 result = runner.invoke(cli, ["diff", "--all", "--json"], catch_exceptions=False)
668 finally:
669 os.chdir(prev)
670
671 assert result.exit_code == 0
672 payload = json.loads(result.output)
673 assert "harmonic" in payload
674 assert "rhythmic" in payload
675 assert "melodic" in payload
676 assert "structural" in payload
677 assert "dynamic" in payload
678 assert "changed_dimensions" in payload
679 assert "unchanged_dimensions" in payload
680
681
682 def test_cli_diff_multiple_flags_shows_multiple_blocks(tmp_path: pathlib.Path) -> None:
683 """``muse diff --harmonic --rhythmic`` shows both dimension blocks."""
684 _init_muse_repo(tmp_path)
685 _commit_ref(tmp_path)
686
687 prev = os.getcwd()
688 try:
689 os.chdir(tmp_path)
690 result = runner.invoke(
691 cli, ["diff", "--harmonic", "--rhythmic"], catch_exceptions=False
692 )
693 finally:
694 os.chdir(prev)
695
696 assert result.exit_code == 0
697 assert "Harmonic diff" in result.output
698 assert "Rhythmic diff" in result.output
699
700
701 @pytest.mark.anyio
702 async def test_explicit_commits_appear_in_output(tmp_path: pathlib.Path) -> None:
703 """Explicit commit refs are threaded through to the harmonic diff output."""
704 _init_muse_repo(tmp_path)
705 _commit_ref(tmp_path)
706
707 result = await _harmonic_diff_async(
708 root=tmp_path, commit_a="aabbccdd", commit_b="eeffgghh"
709 )
710 text = _render_harmonic(result)
711 assert "aabbccdd" in text
712 assert "eeffgghh" in text