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