cgcardona / muse public
test_muse_reset.py python
651 lines 23.9 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 """Tests for ``muse reset`` — branch pointer reset with soft/mixed/hard modes.
2
3 Verifies:
4 - test_muse_reset_soft_moves_ref_only — soft: ref moved, muse-work/ unchanged
5 - test_muse_reset_mixed_resets_index — mixed: behaves like soft in current model
6 - test_muse_reset_hard_overwrites_worktree — hard: ref moved + files restored
7 - test_muse_reset_hard_deletes_extra_files — hard: files not in target snapshot deleted
8 - test_muse_reset_head_minus_n — HEAD~N syntax resolves correctly
9 - test_muse_reset_head_minus_n_too_far — HEAD~N beyond root returns error
10 - test_muse_reset_blocked_during_merge — blocked when MERGE_STATE.json exists
11 - test_muse_reset_hard_missing_object — hard fails cleanly on missing blob
12 - test_muse_reset_ref_not_found — unknown ref returns USER_ERROR
13 - test_muse_reset_hard_confirmation — hard prompts for confirmation
14 - test_muse_reset_abbreviated_sha — abbreviated SHA prefix resolves
15 - test_resolve_ref_head — resolve_ref("HEAD")
16 - test_resolve_ref_head_tilde_zero — resolve_ref("HEAD~0") = HEAD
17 - test_boundary_no_forbidden_imports — AST boundary seal
18
19 Object store unit tests live in ``tests/test_muse_object_store.py``.
20 Cross-command round-trip tests (commit → read-tree, commit → reset) also
21 live there, since they exercise the shared object store contract.
22 """
23 from __future__ import annotations
24
25 import ast
26 import datetime
27 import json
28 import pathlib
29 import uuid
30 from collections.abc import AsyncGenerator
31 from typing import Any
32
33 import pytest
34 from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
35
36 from maestro.db.database import Base
37 from maestro.muse_cli import models as cli_models # noqa: F401 — register tables
38 from maestro.muse_cli.errors import ExitCode
39 from maestro.muse_cli.merge_engine import write_merge_state
40 from maestro.muse_cli.models import MuseCliCommit, MuseCliObject, MuseCliSnapshot
41 from maestro.muse_cli.object_store import object_path, write_object
42 from maestro.services.muse_reset import (
43 MissingObjectError,
44 ResetMode,
45 ResetResult,
46 perform_reset,
47 resolve_ref,
48 )
49
50
51 # ---------------------------------------------------------------------------
52 # Fixtures
53 # ---------------------------------------------------------------------------
54
55
56 @pytest.fixture
57 async def async_session() -> AsyncGenerator[AsyncSession, None]:
58 """In-memory SQLite session with all CLI tables created."""
59 engine = create_async_engine("sqlite+aiosqlite:///:memory:")
60 async with engine.begin() as conn:
61 await conn.run_sync(Base.metadata.create_all)
62 Session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
63 async with Session() as session:
64 yield session
65 await engine.dispose()
66
67
68 @pytest.fixture
69 def repo_id() -> str:
70 return str(uuid.uuid4())
71
72
73 @pytest.fixture
74 def repo_root(tmp_path: pathlib.Path, repo_id: str) -> pathlib.Path:
75 """Minimal Muse repository structure with repo.json."""
76 muse_dir = tmp_path / ".muse"
77 muse_dir.mkdir()
78 (muse_dir / "HEAD").write_text("refs/heads/main")
79 (muse_dir / "refs" / "heads").mkdir(parents=True)
80 (muse_dir / "refs" / "heads" / "main").write_text("")
81 (muse_dir / "repo.json").write_text(json.dumps({"repo_id": repo_id}))
82 return tmp_path
83
84
85 def _sha(prefix: str, length: int = 64) -> str:
86 """Build a deterministic fake SHA of exactly *length* hex chars."""
87 return (prefix * (length // len(prefix) + 1))[:length]
88
89
90 async def _add_commit(
91 session: AsyncSession,
92 *,
93 repo_id: str,
94 branch: str = "main",
95 message: str = "commit",
96 manifest: dict[str, str] | None = None,
97 parent_commit_id: str | None = None,
98 committed_at: datetime.datetime | None = None,
99 ) -> MuseCliCommit:
100 """Insert a commit + its snapshot into the in-memory DB and return the commit."""
101 snapshot_id = _sha(str(uuid.uuid4()).replace("-", ""))
102 commit_id = _sha(str(uuid.uuid4()).replace("-", ""))
103 file_manifest: dict[str, str] = manifest or {"track.mid": _sha("ab")}
104
105 for object_id in file_manifest.values():
106 existing = await session.get(MuseCliObject, object_id)
107 if existing is None:
108 session.add(MuseCliObject(object_id=object_id, size_bytes=10))
109
110 session.add(MuseCliSnapshot(snapshot_id=snapshot_id, manifest=file_manifest))
111 await session.flush()
112
113 ts = committed_at or datetime.datetime.now(datetime.timezone.utc)
114 commit = MuseCliCommit(
115 commit_id=commit_id,
116 repo_id=repo_id,
117 branch=branch,
118 parent_commit_id=parent_commit_id,
119 snapshot_id=snapshot_id,
120 message=message,
121 author="",
122 committed_at=ts,
123 )
124 session.add(commit)
125 await session.flush()
126 return commit
127
128
129 def _write_ref(root: pathlib.Path, branch: str, commit_id: str) -> None:
130 """Update .muse/refs/heads/<branch> with *commit_id*."""
131 ref_path = root / ".muse" / "refs" / "heads" / branch
132 ref_path.parent.mkdir(parents=True, exist_ok=True)
133 ref_path.write_text(commit_id)
134
135
136 def _read_ref(root: pathlib.Path, branch: str = "main") -> str:
137 """Read the current commit SHA from .muse/refs/heads/<branch>."""
138 return (root / ".muse" / "refs" / "heads" / branch).read_text().strip()
139
140
141 def _seed_object_store(root: pathlib.Path, object_id: str, content: bytes) -> None:
142 """Manually write a blob into the .muse/objects/ store via the canonical module."""
143 write_object(root, object_id, content)
144
145
146 # ---------------------------------------------------------------------------
147 # resolve_ref tests
148 # ---------------------------------------------------------------------------
149
150
151 class TestResolveRef:
152
153 @pytest.mark.anyio
154 async def test_resolve_ref_head(
155 self, async_session: AsyncSession, repo_id: str, repo_root: pathlib.Path
156 ) -> None:
157 """resolve_ref('HEAD') returns the most recent commit on the branch."""
158 c1 = await _add_commit(async_session, repo_id=repo_id, message="first")
159 _write_ref(repo_root, "main", c1.commit_id)
160 result = await resolve_ref(async_session, repo_id, "main", "HEAD")
161 assert result is not None
162 assert result.commit_id == c1.commit_id
163
164 @pytest.mark.anyio
165 async def test_resolve_ref_head_tilde_zero(
166 self, async_session: AsyncSession, repo_id: str, repo_root: pathlib.Path
167 ) -> None:
168 """HEAD~0 resolves to HEAD itself."""
169 c1 = await _add_commit(async_session, repo_id=repo_id, message="first")
170 result = await resolve_ref(async_session, repo_id, "main", "HEAD~0")
171 assert result is not None
172 assert result.commit_id == c1.commit_id
173
174 @pytest.mark.anyio
175 async def test_resolve_ref_head_tilde_one(
176 self, async_session: AsyncSession, repo_id: str
177 ) -> None:
178 """HEAD~1 resolves to the parent commit."""
179 import datetime
180 t0 = datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
181 c1 = await _add_commit(async_session, repo_id=repo_id, message="parent",
182 committed_at=t0)
183 t1 = datetime.datetime(2024, 1, 2, 0, 0, 0, tzinfo=datetime.timezone.utc)
184 c2 = await _add_commit(async_session, repo_id=repo_id, message="child",
185 parent_commit_id=c1.commit_id, committed_at=t1)
186
187 result = await resolve_ref(async_session, repo_id, "main", "HEAD~1")
188 assert result is not None
189 assert result.commit_id == c1.commit_id
190
191 @pytest.mark.anyio
192 async def test_resolve_ref_abbreviated_sha(
193 self, async_session: AsyncSession, repo_id: str
194 ) -> None:
195 """A SHA prefix resolves to the matching commit."""
196 c1 = await _add_commit(async_session, repo_id=repo_id, message="first")
197 prefix = c1.commit_id[:8]
198 result = await resolve_ref(async_session, repo_id, "main", prefix)
199 assert result is not None
200 assert result.commit_id == c1.commit_id
201
202 @pytest.mark.anyio
203 async def test_resolve_ref_full_sha(
204 self, async_session: AsyncSession, repo_id: str
205 ) -> None:
206 """A full 64-char SHA resolves to the matching commit."""
207 c1 = await _add_commit(async_session, repo_id=repo_id, message="first")
208 result = await resolve_ref(async_session, repo_id, "main", c1.commit_id)
209 assert result is not None
210 assert result.commit_id == c1.commit_id
211
212 @pytest.mark.anyio
213 async def test_resolve_ref_nonexistent_returns_none(
214 self, async_session: AsyncSession, repo_id: str
215 ) -> None:
216 """An unknown ref returns None."""
217 result = await resolve_ref(async_session, repo_id, "main", "deadbeef")
218 assert result is None
219
220
221 # ---------------------------------------------------------------------------
222 # perform_reset — soft / mixed
223 # ---------------------------------------------------------------------------
224
225
226 class TestResetSoft:
227
228 @pytest.mark.anyio
229 async def test_muse_reset_soft_moves_ref_only(
230 self,
231 async_session: AsyncSession,
232 repo_id: str,
233 repo_root: pathlib.Path,
234 ) -> None:
235 """Soft reset moves the branch ref; muse-work/ is untouched."""
236 import datetime
237 t0 = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
238 t1 = datetime.datetime(2024, 1, 2, tzinfo=datetime.timezone.utc)
239 c1 = await _add_commit(async_session, repo_id=repo_id, message="v1", committed_at=t0)
240 c2 = await _add_commit(async_session, repo_id=repo_id, message="v2",
241 parent_commit_id=c1.commit_id, committed_at=t1)
242 _write_ref(repo_root, "main", c2.commit_id)
243
244 # Create a file in muse-work/ that should NOT be touched
245 workdir = repo_root / "muse-work"
246 workdir.mkdir()
247 sentinel = workdir / "sentinel.mid"
248 sentinel.write_bytes(b"untouched")
249
250 result = await perform_reset(
251 root=repo_root,
252 session=async_session,
253 ref=c1.commit_id,
254 mode=ResetMode.SOFT,
255 )
256
257 assert result.target_commit_id == c1.commit_id
258 assert result.mode is ResetMode.SOFT
259 assert result.branch == "main"
260 assert result.files_restored == 0
261 assert result.files_deleted == 0
262 # Ref updated
263 assert _read_ref(repo_root) == c1.commit_id
264 # muse-work/ untouched
265 assert sentinel.read_bytes() == b"untouched"
266
267
268 class TestResetMixed:
269
270 @pytest.mark.anyio
271 async def test_muse_reset_mixed_resets_index(
272 self,
273 async_session: AsyncSession,
274 repo_id: str,
275 repo_root: pathlib.Path,
276 ) -> None:
277 """Mixed reset (default) behaves like soft in the current Muse model."""
278 import datetime
279 t0 = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
280 t1 = datetime.datetime(2024, 1, 2, tzinfo=datetime.timezone.utc)
281 c1 = await _add_commit(async_session, repo_id=repo_id, message="v1", committed_at=t0)
282 c2 = await _add_commit(async_session, repo_id=repo_id, message="v2",
283 parent_commit_id=c1.commit_id, committed_at=t1)
284 _write_ref(repo_root, "main", c2.commit_id)
285
286 workdir = repo_root / "muse-work"
287 workdir.mkdir()
288 (workdir / "track.mid").write_bytes(b"original")
289
290 result = await perform_reset(
291 root=repo_root,
292 session=async_session,
293 ref=c1.commit_id,
294 mode=ResetMode.MIXED,
295 )
296
297 assert result.target_commit_id == c1.commit_id
298 assert result.mode is ResetMode.MIXED
299 assert _read_ref(repo_root) == c1.commit_id
300 # Files untouched
301 assert (workdir / "track.mid").read_bytes() == b"original"
302
303
304 # ---------------------------------------------------------------------------
305 # perform_reset — hard
306 # ---------------------------------------------------------------------------
307
308
309 class TestResetHard:
310
311 @pytest.mark.anyio
312 async def test_muse_reset_hard_overwrites_worktree(
313 self,
314 async_session: AsyncSession,
315 repo_id: str,
316 repo_root: pathlib.Path,
317 ) -> None:
318 """Hard reset restores muse-work/ files from the target snapshot."""
319 import datetime
320 object_id_v1 = "11" * 32
321 content_v1 = b"MIDI v1"
322 _seed_object_store(repo_root, object_id_v1, content_v1)
323
324 t0 = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
325 c1 = await _add_commit(
326 async_session, repo_id=repo_id, message="v1",
327 manifest={"track.mid": object_id_v1}, committed_at=t0,
328 )
329
330 object_id_v2 = "22" * 32
331 content_v2 = b"MIDI v2 - newer"
332 _seed_object_store(repo_root, object_id_v2, content_v2)
333
334 t1 = datetime.datetime(2024, 1, 2, tzinfo=datetime.timezone.utc)
335 c2 = await _add_commit(
336 async_session, repo_id=repo_id, message="v2",
337 manifest={"track.mid": object_id_v2},
338 parent_commit_id=c1.commit_id, committed_at=t1,
339 )
340 _write_ref(repo_root, "main", c2.commit_id)
341
342 # Current working tree has v2 content
343 workdir = repo_root / "muse-work"
344 workdir.mkdir(parents=True, exist_ok=True)
345 (workdir / "track.mid").write_bytes(content_v2)
346
347 result = await perform_reset(
348 root=repo_root,
349 session=async_session,
350 ref=c1.commit_id,
351 mode=ResetMode.HARD,
352 )
353
354 assert result.target_commit_id == c1.commit_id
355 assert result.mode is ResetMode.HARD
356 assert result.files_restored == 1
357 assert _read_ref(repo_root) == c1.commit_id
358 # muse-work/ now contains v1 content
359 assert (workdir / "track.mid").read_bytes() == content_v1
360
361 @pytest.mark.anyio
362 async def test_muse_reset_hard_deletes_extra_files(
363 self,
364 async_session: AsyncSession,
365 repo_id: str,
366 repo_root: pathlib.Path,
367 ) -> None:
368 """Hard reset deletes files in muse-work/ not present in target snapshot."""
369 import datetime
370 object_id = "33" * 32
371 _seed_object_store(repo_root, object_id, b"bass only")
372
373 t0 = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
374 c1 = await _add_commit(
375 async_session, repo_id=repo_id, message="bass only",
376 manifest={"bass.mid": object_id}, committed_at=t0,
377 )
378 _write_ref(repo_root, "main", c1.commit_id)
379
380 workdir = repo_root / "muse-work"
381 workdir.mkdir()
382 (workdir / "bass.mid").write_bytes(b"bass only")
383 (workdir / "extra.mid").write_bytes(b"should be deleted")
384
385 result = await perform_reset(
386 root=repo_root,
387 session=async_session,
388 ref=c1.commit_id,
389 mode=ResetMode.HARD,
390 )
391
392 assert result.files_deleted == 1
393 assert (workdir / "bass.mid").exists()
394 assert not (workdir / "extra.mid").exists()
395
396 @pytest.mark.anyio
397 async def test_muse_reset_hard_missing_object(
398 self,
399 async_session: AsyncSession,
400 repo_id: str,
401 repo_root: pathlib.Path,
402 ) -> None:
403 """Hard reset raises MissingObjectError when blob is absent from object store."""
404 import datetime
405 missing_object_id = "ff" * 32
406 # Intentionally NOT seeding the object store
407
408 t0 = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
409 c1 = await _add_commit(
410 async_session, repo_id=repo_id, message="v1",
411 manifest={"lead.mid": missing_object_id}, committed_at=t0,
412 )
413 _write_ref(repo_root, "main", c1.commit_id)
414
415 with pytest.raises(MissingObjectError) as exc_info:
416 await perform_reset(
417 root=repo_root,
418 session=async_session,
419 ref=c1.commit_id,
420 mode=ResetMode.HARD,
421 )
422 assert missing_object_id[:8] in str(exc_info.value)
423
424
425 # ---------------------------------------------------------------------------
426 # HEAD~N syntax
427 # ---------------------------------------------------------------------------
428
429
430 class TestResetHeadMinusN:
431
432 @pytest.mark.anyio
433 async def test_muse_reset_head_minus_n(
434 self,
435 async_session: AsyncSession,
436 repo_id: str,
437 repo_root: pathlib.Path,
438 ) -> None:
439 """HEAD~2 walks back two parents from the most recent commit."""
440 import datetime
441 t0 = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
442 t1 = datetime.datetime(2024, 1, 2, tzinfo=datetime.timezone.utc)
443 t2 = datetime.datetime(2024, 1, 3, tzinfo=datetime.timezone.utc)
444 c0 = await _add_commit(async_session, repo_id=repo_id, message="root", committed_at=t0)
445 c1 = await _add_commit(async_session, repo_id=repo_id, message="child",
446 parent_commit_id=c0.commit_id, committed_at=t1)
447 c2 = await _add_commit(async_session, repo_id=repo_id, message="grandchild",
448 parent_commit_id=c1.commit_id, committed_at=t2)
449 _write_ref(repo_root, "main", c2.commit_id)
450
451 result = await perform_reset(
452 root=repo_root,
453 session=async_session,
454 ref="HEAD~2",
455 mode=ResetMode.SOFT,
456 )
457
458 assert result.target_commit_id == c0.commit_id
459 assert _read_ref(repo_root) == c0.commit_id
460
461 @pytest.mark.anyio
462 async def test_muse_reset_head_minus_n_too_far(
463 self,
464 async_session: AsyncSession,
465 repo_id: str,
466 repo_root: pathlib.Path,
467 ) -> None:
468 """HEAD~N where N exceeds history depth surfaces USER_ERROR via typer.Exit."""
469 import datetime
470 import typer
471 t0 = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
472 c0 = await _add_commit(async_session, repo_id=repo_id, message="root", committed_at=t0)
473 _write_ref(repo_root, "main", c0.commit_id)
474
475 with pytest.raises(typer.Exit) as exc_info:
476 await perform_reset(
477 root=repo_root,
478 session=async_session,
479 ref="HEAD~5", # only 0 parents exist
480 mode=ResetMode.SOFT,
481 )
482 assert exc_info.value.exit_code == ExitCode.USER_ERROR
483
484
485 # ---------------------------------------------------------------------------
486 # Merge-in-progress guard
487 # ---------------------------------------------------------------------------
488
489
490 class TestResetBlockedDuringMerge:
491
492 @pytest.mark.anyio
493 async def test_muse_reset_blocked_during_merge(
494 self,
495 async_session: AsyncSession,
496 repo_id: str,
497 repo_root: pathlib.Path,
498 ) -> None:
499 """Reset is blocked when MERGE_STATE.json exists."""
500 import datetime
501 import typer
502 t0 = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
503 c0 = await _add_commit(async_session, repo_id=repo_id, message="root", committed_at=t0)
504 _write_ref(repo_root, "main", c0.commit_id)
505
506 # Simulate an in-progress merge
507 write_merge_state(
508 repo_root,
509 base_commit=c0.commit_id,
510 ours_commit=c0.commit_id,
511 theirs_commit="x" * 64,
512 conflict_paths=["bass.mid"],
513 )
514
515 with pytest.raises(typer.Exit) as exc_info:
516 await perform_reset(
517 root=repo_root,
518 session=async_session,
519 ref=c0.commit_id,
520 mode=ResetMode.SOFT,
521 )
522 assert exc_info.value.exit_code == ExitCode.USER_ERROR
523
524
525 # ---------------------------------------------------------------------------
526 # Ref resolution edge cases
527 # ---------------------------------------------------------------------------
528
529
530 class TestResetRefNotFound:
531
532 @pytest.mark.anyio
533 async def test_muse_reset_ref_not_found(
534 self,
535 async_session: AsyncSession,
536 repo_id: str,
537 repo_root: pathlib.Path,
538 ) -> None:
539 """An unknown ref string exits with USER_ERROR."""
540 import datetime
541 import typer
542 t0 = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
543 c0 = await _add_commit(async_session, repo_id=repo_id, message="root", committed_at=t0)
544 _write_ref(repo_root, "main", c0.commit_id)
545
546 with pytest.raises(typer.Exit) as exc_info:
547 await perform_reset(
548 root=repo_root,
549 session=async_session,
550 ref="nonexistent-ref",
551 mode=ResetMode.SOFT,
552 )
553 assert exc_info.value.exit_code == ExitCode.USER_ERROR
554
555 @pytest.mark.anyio
556 async def test_muse_reset_abbreviated_sha(
557 self,
558 async_session: AsyncSession,
559 repo_id: str,
560 repo_root: pathlib.Path,
561 ) -> None:
562 """An abbreviated SHA prefix resolves to the correct commit."""
563 import datetime
564 t0 = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
565 t1 = datetime.datetime(2024, 1, 2, tzinfo=datetime.timezone.utc)
566 c1 = await _add_commit(async_session, repo_id=repo_id, message="first", committed_at=t0)
567 c2 = await _add_commit(async_session, repo_id=repo_id, message="second",
568 parent_commit_id=c1.commit_id, committed_at=t1)
569 _write_ref(repo_root, "main", c2.commit_id)
570
571 result = await perform_reset(
572 root=repo_root,
573 session=async_session,
574 ref=c1.commit_id[:12], # abbreviated prefix
575 mode=ResetMode.SOFT,
576 )
577
578 assert result.target_commit_id == c1.commit_id
579
580
581 # ---------------------------------------------------------------------------
582 # ResetResult type
583 # ---------------------------------------------------------------------------
584
585
586 class TestResetResult:
587
588 def test_reset_result_defaults(self) -> None:
589 """ResetResult has sensible defaults for files_restored and files_deleted."""
590 r = ResetResult(
591 target_commit_id="a" * 64,
592 mode=ResetMode.SOFT,
593 branch="main",
594 )
595 assert r.files_restored == 0
596 assert r.files_deleted == 0
597
598 def test_reset_result_frozen(self) -> None:
599 """ResetResult is immutable (frozen dataclass)."""
600 r = ResetResult(
601 target_commit_id="a" * 64,
602 mode=ResetMode.HARD,
603 branch="main",
604 files_restored=5,
605 files_deleted=2,
606 )
607 with pytest.raises(Exception):
608 r.files_restored = 99 # type: ignore[misc]
609
610
611 # ---------------------------------------------------------------------------
612 # Boundary seal — AST checks
613 # ---------------------------------------------------------------------------
614
615
616 class TestBoundarySeals:
617
618 def _parse(self, rel_path: str) -> ast.Module:
619 root = pathlib.Path(__file__).resolve().parent.parent
620 return ast.parse((root / rel_path).read_text())
621
622 def test_boundary_no_forbidden_imports(self) -> None:
623 """muse_reset service must not import executor, state_store, mcp, or maestro_handlers."""
624 tree = self._parse("maestro/services/muse_reset.py")
625 forbidden = {"state_store", "executor", "maestro_handlers", "mcp"}
626 for node in ast.walk(tree):
627 if isinstance(node, ast.ImportFrom) and node.module:
628 for fb in forbidden:
629 assert fb not in node.module, (
630 f"muse_reset imports forbidden module: {node.module}"
631 )
632
633 def test_reset_service_has_future_import(self) -> None:
634 """muse_reset.py starts with 'from __future__ import annotations'."""
635 tree = self._parse("maestro/services/muse_reset.py")
636 first_import = next(
637 (n for n in ast.walk(tree) if isinstance(n, ast.ImportFrom)),
638 None,
639 )
640 assert first_import is not None
641 assert first_import.module == "__future__"
642
643 def test_reset_command_has_future_import(self) -> None:
644 """reset.py CLI command starts with 'from __future__ import annotations'."""
645 tree = self._parse("maestro/muse_cli/commands/reset.py")
646 first_import = next(
647 (n for n in ast.walk(tree) if isinstance(n, ast.ImportFrom)),
648 None,
649 )
650 assert first_import is not None
651 assert first_import.module == "__future__"