gabriel / muse public
test_cli_new_commands.py python
602 lines 23.5 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """CLI integration tests for: reflog, gc, archive, bisect, blame, worktree, workspace."""
2
3 from __future__ import annotations
4
5 import datetime
6 import hashlib
7 import json
8 import pathlib
9
10 import pytest
11 from tests.cli_test_helper import CliRunner
12 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
13 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
14
15 cli = None # argparse migration — CliRunner ignores this arg
16
17 runner = CliRunner()
18
19
20 # ---------------------------------------------------------------------------
21 # Repo scaffold helpers
22 # ---------------------------------------------------------------------------
23
24
25 def _sha256(content: bytes) -> str:
26 return hashlib.sha256(content).hexdigest()
27
28
29 def _make_repo(
30 tmp_path: pathlib.Path,
31 monkeypatch: pytest.MonkeyPatch,
32 ) -> tuple[pathlib.Path, str]:
33 """Create a minimal repo with one commit, one file tracked. Sets cwd.
34
35 Returns ``(repo_path, commit_id)`` so callers that chain further commits
36 can pass the real content-addressed ID to ``_add_commits``.
37 """
38 monkeypatch.chdir(tmp_path)
39 muse = tmp_path / ".muse"
40 for d in ("objects", "commits", "snapshots", "refs/heads", "logs/refs/heads"):
41 (muse / d).mkdir(parents=True, exist_ok=True)
42
43 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
44 (muse / "HEAD").write_text("ref: refs/heads/main\n")
45
46 content = b"hello world\n"
47 sha = _sha256(content)
48 obj_dir = muse / "objects" / sha[:2]
49 obj_dir.mkdir(parents=True, exist_ok=True)
50 (obj_dir / sha[2:]).write_bytes(content)
51
52 snap_id = compute_snapshot_id({"hello.txt": sha})
53 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest={"hello.txt": sha}))
54
55 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
56 commit_id = compute_commit_id([], snap_id, "initial commit", committed_at.isoformat())
57 write_commit(tmp_path, CommitRecord(
58 commit_id=commit_id,
59 repo_id="test-repo",
60 branch="main",
61 snapshot_id=snap_id,
62 message="initial commit",
63 committed_at=committed_at,
64 author="Test User",
65 ))
66 (muse / "refs" / "heads" / "main").write_text(commit_id)
67 return tmp_path, commit_id
68
69
70 def _add_commits(repo: pathlib.Path, n: int, parent: str) -> list[str]:
71 """Append *n* commits to the main branch, return all commit IDs.
72
73 Uses content-addressed IDs (``compute_commit_id``) so every commit
74 passes ``_verify_commit_id`` on read-back. Timestamps are pinned to
75 deterministic values (2026-01-02 + i days) to keep IDs stable.
76 """
77 snap_id = compute_snapshot_id({})
78 write_snapshot(repo, SnapshotRecord(snapshot_id=snap_id, manifest={}))
79 commit_ids = [parent]
80 prev = parent
81 for i in range(n):
82 at = datetime.datetime(2026, 1, 2 + i, tzinfo=datetime.timezone.utc)
83 msg = f"commit {i + 1}"
84 cid = compute_commit_id([prev], snap_id, msg, at.isoformat())
85 write_commit(repo, CommitRecord(
86 commit_id=cid,
87 repo_id="test-repo",
88 branch="main",
89 snapshot_id=snap_id,
90 message=msg,
91 committed_at=at,
92 parent_commit_id=prev,
93 author="Test",
94 ))
95 commit_ids.append(cid)
96 prev = cid
97 (repo / ".muse" / "refs" / "heads" / "main").write_text(commit_ids[-1])
98 return commit_ids
99
100
101 # ---------------------------------------------------------------------------
102 # muse reflog
103 # ---------------------------------------------------------------------------
104
105
106 class TestReflogCli:
107 def test_reflog_no_entries_exits_ok(
108 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
109 ) -> None:
110 _make_repo(tmp_path, monkeypatch)
111 result = runner.invoke(cli, ["reflog"], catch_exceptions=False)
112 assert result.exit_code == 0
113 assert "No reflog entries" in result.output
114
115 def test_reflog_shows_entries(
116 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
117 ) -> None:
118 from muse.core.reflog import append_reflog
119
120 _make_repo(tmp_path, monkeypatch)
121 append_reflog(tmp_path, "main", old_id=None, new_id="c" * 64, author="A", operation="commit: test")
122 result = runner.invoke(cli, ["reflog"], catch_exceptions=False)
123 assert result.exit_code == 0
124 assert "commit: test" in result.output
125
126 def test_reflog_all_flag(
127 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
128 ) -> None:
129 from muse.core.reflog import append_reflog
130
131 _make_repo(tmp_path, monkeypatch)
132 append_reflog(tmp_path, "main", old_id=None, new_id="c" * 64, author="A", operation="commit: x")
133 result = runner.invoke(cli, ["reflog", "--all"], catch_exceptions=False)
134 assert result.exit_code == 0
135 assert "refs/heads/main" in result.output
136
137 def test_reflog_branch_filter(
138 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
139 ) -> None:
140 from muse.core.reflog import append_reflog
141
142 _make_repo(tmp_path, monkeypatch)
143 append_reflog(tmp_path, "dev", old_id=None, new_id="d" * 64, author="A", operation="commit: dev")
144 result = runner.invoke(cli, ["reflog", "--branch", "dev"], catch_exceptions=False)
145 assert result.exit_code == 0
146 assert "commit: dev" in result.output
147
148 def test_reflog_limit(
149 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
150 ) -> None:
151 from muse.core.reflog import append_reflog
152
153 _make_repo(tmp_path, monkeypatch)
154 for i in range(10):
155 append_reflog(tmp_path, "main", old_id=None, new_id="c" * 64, author="A", operation=f"commit: {i}")
156 result = runner.invoke(cli, ["reflog", "--limit", "3"], catch_exceptions=False)
157 assert result.exit_code == 0
158 # At most 3 @{N} entries.
159 lines = [l for l in result.output.splitlines() if l.startswith("@{")]
160 assert len(lines) <= 3
161
162 def test_reflog_shows_at_index_format(
163 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
164 ) -> None:
165 from muse.core.reflog import append_reflog
166
167 _make_repo(tmp_path, monkeypatch)
168 append_reflog(tmp_path, "main", old_id=None, new_id="c" * 64, author="A", operation="commit: x")
169 result = runner.invoke(cli, ["reflog"], catch_exceptions=False)
170 # Format is @{N:...} so just check the @ prefix.
171 assert "@{" in result.output
172
173
174 # ---------------------------------------------------------------------------
175 # muse gc
176 # ---------------------------------------------------------------------------
177
178
179 class TestGcCli:
180 def test_gc_empty_reports_zero(
181 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
182 ) -> None:
183 _make_repo(tmp_path, monkeypatch)
184 result = runner.invoke(cli, ["gc"], catch_exceptions=False)
185 assert result.exit_code == 0
186 assert "0 object" in result.output
187
188 def test_gc_dry_run_does_not_delete(
189 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
190 ) -> None:
191 _make_repo(tmp_path, monkeypatch)
192 # Write an orphan object with a valid 2+62 path.
193 orphan_content = b"totally orphaned"
194 sha = _sha256(orphan_content)
195 obj_dir = tmp_path / ".muse" / "objects" / sha[:2]
196 obj_dir.mkdir(parents=True, exist_ok=True)
197 obj_file = obj_dir / sha[2:]
198 obj_file.write_bytes(orphan_content)
199
200 result = runner.invoke(cli, ["gc", "--dry-run"], catch_exceptions=False)
201 assert result.exit_code == 0
202 assert "dry-run" in result.output
203 assert obj_file.exists()
204
205 def test_gc_removes_orphan(
206 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
207 ) -> None:
208 _make_repo(tmp_path, monkeypatch)
209 orphan_content = b"not referenced anywhere at all"
210 sha = _sha256(orphan_content)
211 obj_dir = tmp_path / ".muse" / "objects" / sha[:2]
212 obj_dir.mkdir(parents=True, exist_ok=True)
213 obj_file = obj_dir / sha[2:]
214 obj_file.write_bytes(orphan_content)
215
216 result = runner.invoke(cli, ["gc", "--grace-period", "0"], catch_exceptions=False)
217 assert result.exit_code == 0
218 assert not obj_file.exists()
219
220 def test_gc_verbose_lists_objects(
221 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
222 ) -> None:
223 _make_repo(tmp_path, monkeypatch)
224 orphan_content = b"verbose orphan"
225 sha = _sha256(orphan_content)
226 obj_dir = tmp_path / ".muse" / "objects" / sha[:2]
227 obj_dir.mkdir(parents=True, exist_ok=True)
228 (obj_dir / sha[2:]).write_bytes(orphan_content)
229
230 result = runner.invoke(cli, ["gc", "--verbose", "--grace-period", "0"], catch_exceptions=False)
231 assert result.exit_code == 0
232 assert sha in result.output
233
234 def test_gc_preserves_reachable(
235 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
236 ) -> None:
237 # The hello.txt object in the initial commit must survive GC.
238 _make_repo(tmp_path, monkeypatch)
239 content = b"hello world\n"
240 sha = _sha256(content)
241 obj_path = tmp_path / ".muse" / "objects" / sha[:2] / sha[2:]
242 result = runner.invoke(cli, ["gc"], catch_exceptions=False)
243 assert result.exit_code == 0
244 assert obj_path.exists()
245
246
247 # ---------------------------------------------------------------------------
248 # muse archive
249 # ---------------------------------------------------------------------------
250
251
252 class TestArchiveCli:
253 def test_archive_creates_targz(
254 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
255 ) -> None:
256 _make_repo(tmp_path, monkeypatch)
257 out = str(tmp_path / "snap.tar.gz")
258 result = runner.invoke(cli, ["archive", "--output", out], catch_exceptions=False)
259 assert result.exit_code == 0
260 assert pathlib.Path(out).exists()
261
262 def test_archive_creates_zip(
263 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
264 ) -> None:
265 _make_repo(tmp_path, monkeypatch)
266 out = str(tmp_path / "snap.zip")
267 result = runner.invoke(cli, ["archive", "--format", "zip", "--output", out], catch_exceptions=False)
268 assert result.exit_code == 0
269 assert pathlib.Path(out).exists()
270
271 def test_archive_invalid_format(
272 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
273 ) -> None:
274 _make_repo(tmp_path, monkeypatch)
275 result = runner.invoke(cli, ["archive", "--format", "rar"])
276 assert result.exit_code != 0
277 assert "invalid choice" in result.output or "Unknown format" in result.output
278
279 def test_archive_with_prefix(
280 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
281 ) -> None:
282 _make_repo(tmp_path, monkeypatch)
283 out = str(tmp_path / "out.tar.gz")
284 result = runner.invoke(
285 cli, ["archive", "--output", out, "--prefix", "myproject/"],
286 catch_exceptions=False,
287 )
288 assert result.exit_code == 0
289 import tarfile
290 with tarfile.open(out, "r:gz") as tar:
291 names = tar.getnames()
292 assert any("myproject/" in n for n in names)
293
294 def test_archive_output_shows_commit_info(
295 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
296 ) -> None:
297 _make_repo(tmp_path, monkeypatch)
298 out = str(tmp_path / "out.tar.gz")
299 result = runner.invoke(cli, ["archive", "--output", out], catch_exceptions=False)
300 assert result.exit_code == 0
301 assert "initial commit" in result.output
302
303 def test_archive_default_name_is_sha_based(
304 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
305 ) -> None:
306 _make_repo(tmp_path, monkeypatch)
307 result = runner.invoke(cli, ["archive"], catch_exceptions=False)
308 assert result.exit_code == 0
309 # Should create a .tar.gz file.
310 tar_files = list(tmp_path.glob("*.tar.gz"))
311 assert len(tar_files) == 1
312
313
314 # ---------------------------------------------------------------------------
315 # muse bisect
316 # ---------------------------------------------------------------------------
317
318
319 class TestBisectCli:
320 def _setup(
321 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch, n: int = 4
322 ) -> list[str]:
323 _, initial = _make_repo(tmp_path, monkeypatch)
324 return _add_commits(tmp_path, n, initial)
325
326 def test_bisect_start_requires_good(
327 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
328 ) -> None:
329 commits = self._setup(tmp_path, monkeypatch)
330 result = runner.invoke(cli, ["bisect", "start", "--bad", commits[-1]])
331 assert result.exit_code != 0
332
333 def test_bisect_start_success(
334 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
335 ) -> None:
336 commits = self._setup(tmp_path, monkeypatch, n=4)
337 result = runner.invoke(
338 cli, ["bisect", "start", "--bad", commits[-1], "--good", commits[0]],
339 catch_exceptions=False,
340 )
341 assert result.exit_code == 0
342 assert "Bisect session started" in result.output
343
344 def test_bisect_reset(
345 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
346 ) -> None:
347 commits = self._setup(tmp_path, monkeypatch, n=4)
348 runner.invoke(cli, ["bisect", "start", "--bad", commits[-1], "--good", commits[0]])
349 result = runner.invoke(cli, ["bisect", "reset"], catch_exceptions=False)
350 assert result.exit_code == 0
351 assert "reset" in result.output
352
353 def test_bisect_log_shows_entries(
354 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
355 ) -> None:
356 commits = self._setup(tmp_path, monkeypatch, n=4)
357 runner.invoke(
358 cli, ["bisect", "start", "--bad", commits[-1], "--good", commits[0]],
359 catch_exceptions=False,
360 )
361 result = runner.invoke(cli, ["bisect", "log"], catch_exceptions=False)
362 assert result.exit_code == 0
363 assert "bad" in result.output or "good" in result.output
364
365 def test_bisect_bad_without_session(
366 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
367 ) -> None:
368 _make_repo(tmp_path, monkeypatch)
369 result = runner.invoke(cli, ["bisect", "bad"])
370 assert result.exit_code != 0
371
372 def test_bisect_good_without_session(
373 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
374 ) -> None:
375 _make_repo(tmp_path, monkeypatch)
376 result = runner.invoke(cli, ["bisect", "good"])
377 assert result.exit_code != 0
378
379 def test_bisect_shows_next_to_test(
380 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
381 ) -> None:
382 commits = self._setup(tmp_path, monkeypatch, n=8)
383 result = runner.invoke(
384 cli, ["bisect", "start", "--bad", commits[-1], "--good", commits[0]],
385 catch_exceptions=False,
386 )
387 assert "Next to test:" in result.output
388
389 def test_bisect_skip(
390 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
391 ) -> None:
392 commits = self._setup(tmp_path, monkeypatch, n=4)
393 runner.invoke(
394 cli, ["bisect", "start", "--bad", commits[-1], "--good", commits[0]],
395 )
396 from muse.core.bisect import _load_state
397 state = _load_state(tmp_path)
398 assert state is not None
399 remaining = state.get("remaining", [])
400 if remaining:
401 mid = remaining[len(remaining) // 2]
402 result = runner.invoke(cli, ["bisect", "skip", mid], catch_exceptions=False)
403 assert result.exit_code == 0
404
405
406 # ---------------------------------------------------------------------------
407 # muse blame (core VCS)
408 # ---------------------------------------------------------------------------
409
410
411 class TestBlameCli:
412 def test_blame_missing_file(
413 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
414 ) -> None:
415 _make_repo(tmp_path, monkeypatch)
416 result = runner.invoke(cli, ["blame", "nonexistent.txt"])
417 assert result.exit_code != 0
418 assert "not found" in result.output
419
420 def test_blame_existing_file(
421 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
422 ) -> None:
423 _make_repo(tmp_path, monkeypatch)
424 result = runner.invoke(cli, ["blame", "hello.txt"], catch_exceptions=False)
425 assert result.exit_code == 0
426 assert "hello world" in result.output
427
428 def test_blame_shows_author(
429 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
430 ) -> None:
431 _make_repo(tmp_path, monkeypatch)
432 result = runner.invoke(cli, ["blame", "hello.txt"], catch_exceptions=False)
433 assert result.exit_code == 0
434 assert "Test User" in result.output
435
436 def test_blame_porcelain_json_output(
437 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
438 ) -> None:
439 _make_repo(tmp_path, monkeypatch)
440 result = runner.invoke(cli, ["blame", "--porcelain", "hello.txt"], catch_exceptions=False)
441 assert result.exit_code == 0
442 lines = [l for l in result.output.strip().split("\n") if l.strip()]
443 assert len(lines) >= 1
444 parsed = json.loads(lines[0])
445 assert "lineno" in parsed
446 assert "commit_id" in parsed
447 assert "content" in parsed
448
449 def test_blame_lineno_starts_at_1(
450 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
451 ) -> None:
452 _make_repo(tmp_path, monkeypatch)
453 result = runner.invoke(cli, ["blame", "--porcelain", "hello.txt"], catch_exceptions=False)
454 assert result.exit_code == 0
455 parsed = json.loads(result.output.strip().split("\n")[0])
456 assert parsed["lineno"] == 1
457
458
459 # ---------------------------------------------------------------------------
460 # muse worktree
461 # ---------------------------------------------------------------------------
462
463
464 class TestWorktreeCli:
465 def _make_named_repo(
466 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
467 ) -> pathlib.Path:
468 """Create a repo in myproject/ subdirectory."""
469 repo_dir = tmp_path / "myproject"
470 repo_dir.mkdir()
471 muse = repo_dir / ".muse"
472 for d in ("objects", "commits", "snapshots", "refs/heads"):
473 (muse / d).mkdir(parents=True, exist_ok=True)
474 (muse / "repo.json").write_text(json.dumps({"repo_id": "test"}))
475 (muse / "HEAD").write_text("ref: refs/heads/main\n")
476 (muse / "refs" / "heads" / "main").write_text("0" * 64)
477 monkeypatch.chdir(repo_dir)
478 return repo_dir
479
480 def test_worktree_list_shows_main(
481 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
482 ) -> None:
483 self._make_named_repo(tmp_path, monkeypatch)
484 result = runner.invoke(cli, ["worktree", "list"], catch_exceptions=False)
485 assert result.exit_code == 0
486 assert "(main)" in result.output
487
488 def test_worktree_add_and_list(
489 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
490 ) -> None:
491 repo = self._make_named_repo(tmp_path, monkeypatch)
492 (repo / ".muse" / "refs" / "heads" / "dev").write_text("0" * 64)
493 result = runner.invoke(cli, ["worktree", "add", "mydev", "dev"], catch_exceptions=False)
494 assert result.exit_code == 0
495 result2 = runner.invoke(cli, ["worktree", "list"], catch_exceptions=False)
496 assert "mydev" in result2.output
497
498 def test_worktree_remove(
499 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
500 ) -> None:
501 repo = self._make_named_repo(tmp_path, monkeypatch)
502 (repo / ".muse" / "refs" / "heads" / "dev").write_text("0" * 64)
503 runner.invoke(cli, ["worktree", "add", "mydev", "dev"])
504 result = runner.invoke(cli, ["worktree", "remove", "mydev"], catch_exceptions=False)
505 assert result.exit_code == 0
506 assert "mydev" in result.output
507
508 def test_worktree_prune_empty(
509 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
510 ) -> None:
511 self._make_named_repo(tmp_path, monkeypatch)
512 result = runner.invoke(cli, ["worktree", "prune"], catch_exceptions=False)
513 assert result.exit_code == 0
514 assert "Nothing to prune" in result.output
515
516 def test_worktree_remove_nonexistent(
517 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
518 ) -> None:
519 self._make_named_repo(tmp_path, monkeypatch)
520 result = runner.invoke(cli, ["worktree", "remove", "nonexistent"])
521 assert result.exit_code != 0
522
523
524 # ---------------------------------------------------------------------------
525 # muse workspace
526 # ---------------------------------------------------------------------------
527
528
529 class TestWorkspaceCli:
530 def test_workspace_add_and_list(
531 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
532 ) -> None:
533 _make_repo(tmp_path, monkeypatch)
534 result = runner.invoke(
535 cli, ["workspace", "add", "core", "https://musehub.ai/acme/core"],
536 catch_exceptions=False,
537 )
538 assert result.exit_code == 0
539 assert "Added workspace member" in result.output
540
541 result2 = runner.invoke(cli, ["workspace", "list"], catch_exceptions=False)
542 assert result2.exit_code == 0
543 assert "core" in result2.output
544
545 def test_workspace_remove(
546 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
547 ) -> None:
548 _make_repo(tmp_path, monkeypatch)
549 runner.invoke(cli, ["workspace", "add", "core", "https://musehub.ai/acme/core"])
550 result = runner.invoke(cli, ["workspace", "remove", "core"], catch_exceptions=False)
551 assert result.exit_code == 0
552 assert "Removed" in result.output
553
554 def test_workspace_status_empty(
555 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
556 ) -> None:
557 _make_repo(tmp_path, monkeypatch)
558 result = runner.invoke(cli, ["workspace", "status"], catch_exceptions=False)
559 assert result.exit_code == 0
560 assert "No workspace members" in result.output
561
562 def test_workspace_list_empty(
563 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
564 ) -> None:
565 _make_repo(tmp_path, monkeypatch)
566 result = runner.invoke(cli, ["workspace", "list"], catch_exceptions=False)
567 assert result.exit_code == 0
568 assert "No workspace members" in result.output
569
570 def test_workspace_add_with_branch(
571 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
572 ) -> None:
573 _make_repo(tmp_path, monkeypatch)
574 runner.invoke(
575 cli, ["workspace", "add", "data", "https://example.com/data", "--branch", "v2"],
576 )
577 result = runner.invoke(cli, ["workspace", "list"], catch_exceptions=False)
578 assert "v2" in result.output
579
580 def test_workspace_remove_nonexistent(
581 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
582 ) -> None:
583 _make_repo(tmp_path, monkeypatch)
584 runner.invoke(cli, ["workspace", "add", "core", "https://example.com/core"])
585 result = runner.invoke(cli, ["workspace", "remove", "nonexistent"])
586 assert result.exit_code != 0
587
588 def test_workspace_sync_empty(
589 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
590 ) -> None:
591 _make_repo(tmp_path, monkeypatch)
592 result = runner.invoke(cli, ["workspace", "sync"], catch_exceptions=False)
593 assert result.exit_code == 0
594 assert "No members" in result.output
595
596 def test_workspace_add_duplicate(
597 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
598 ) -> None:
599 _make_repo(tmp_path, monkeypatch)
600 runner.invoke(cli, ["workspace", "add", "core", "https://example.com/core"])
601 result = runner.invoke(cli, ["workspace", "add", "core", "https://example.com/other"])
602 assert result.exit_code != 0
File History 5 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e Merge branch 'dev' into main Human 29 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 48 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago