gabriel / muse public
test_cli_new_commands.py python
603 lines 23.1 KB
Raw
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.ids import hash_commit, hash_snapshot
12 from muse.core.commits import (
13 CommitRecord,
14 write_commit,
15 )
16 from muse.core.snapshots import (
17 SnapshotRecord,
18 write_snapshot,
19 )
20 from muse.core.types import NULL_COMMIT_ID, blob_id, split_id
21 from muse.core.object_store import object_path, write_object
22 from muse.core.paths import heads_dir, muse_dir
23
24 cli = None # argparse migration — CliRunner ignores this arg
25
26 runner = CliRunner()
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 = muse_dir(tmp_path)
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 oid = blob_id(content)
48 write_object(tmp_path, oid, content)
49
50 snap_id = hash_snapshot({"hello.txt": oid})
51 write_snapshot(tmp_path, SnapshotRecord(snapshot_id=snap_id, manifest={"hello.txt": oid}))
52
53 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
54 commit_id = hash_commit(
55 parent_ids=[],
56 snapshot_id=snap_id,
57 message="initial commit",
58 committed_at_iso=committed_at.isoformat(),
59 author="Test User",
60 )
61 write_commit(tmp_path, CommitRecord(
62 commit_id=commit_id,
63 branch="main",
64 snapshot_id=snap_id,
65 message="initial commit",
66 committed_at=committed_at,
67 author="Test User",
68 ))
69 (muse / "refs" / "heads" / "main").write_text(commit_id)
70 return tmp_path, commit_id
71
72
73 def _add_commits(repo: pathlib.Path, n: int, parent: str) -> list[str]:
74 """Append *n* commits to the main branch, return all commit IDs.
75
76 Uses content-addressed IDs (``hash_commit``) so every commit
77 passes ``_verify_commit_id`` on read-back. Timestamps are pinned to
78 deterministic values (2026-01-02 + i days) to keep IDs stable.
79 """
80 snap_id = hash_snapshot({})
81 write_snapshot(repo, SnapshotRecord(snapshot_id=snap_id, manifest={}))
82 commit_ids = [parent]
83 prev = parent
84 for i in range(n):
85 at = datetime.datetime(2026, 1, 2 + i, tzinfo=datetime.timezone.utc)
86 msg = f"commit {i + 1}"
87 cid = hash_commit(
88 parent_ids=[prev],
89 snapshot_id=snap_id,
90 message=msg,
91 committed_at_iso=at.isoformat(),
92 author="Test",
93 )
94 write_commit(repo, CommitRecord(
95 commit_id=cid,
96 branch="main",
97 snapshot_id=snap_id,
98 message=msg,
99 committed_at=at,
100 parent_commit_id=prev,
101 author="Test",
102 ))
103 commit_ids.append(cid)
104 prev = cid
105 (heads_dir(repo) / "main").write_text(commit_ids[-1])
106 return commit_ids
107
108
109 # ---------------------------------------------------------------------------
110 # muse reflog
111 # ---------------------------------------------------------------------------
112
113
114 class TestReflogCli:
115 def test_reflog_no_entries_exits_ok(
116 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
117 ) -> None:
118 _make_repo(tmp_path, monkeypatch)
119 result = runner.invoke(cli, ["reflog"], catch_exceptions=False)
120 assert result.exit_code == 0
121 assert "No reflog entries" in result.output
122
123 def test_reflog_shows_entries(
124 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
125 ) -> None:
126 from muse.core.reflog import append_reflog
127
128 _make_repo(tmp_path, monkeypatch)
129 append_reflog(tmp_path, "main", old_id=None, new_id="c" * 64, author="A", operation="commit: test")
130 result = runner.invoke(cli, ["reflog"], catch_exceptions=False)
131 assert result.exit_code == 0
132 assert "commit: test" in result.output
133
134 def test_reflog_all_flag(
135 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
136 ) -> None:
137 from muse.core.reflog import append_reflog
138
139 _make_repo(tmp_path, monkeypatch)
140 append_reflog(tmp_path, "main", old_id=None, new_id="c" * 64, author="A", operation="commit: x")
141 result = runner.invoke(cli, ["reflog", "--all"], catch_exceptions=False)
142 assert result.exit_code == 0
143 assert "refs/heads/main" in result.output
144
145 def test_reflog_branch_filter(
146 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
147 ) -> None:
148 from muse.core.reflog import append_reflog
149
150 _make_repo(tmp_path, monkeypatch)
151 append_reflog(tmp_path, "dev", old_id=None, new_id="d" * 64, author="A", operation="commit: dev")
152 result = runner.invoke(cli, ["reflog", "--branch", "dev"], catch_exceptions=False)
153 assert result.exit_code == 0
154 assert "commit: dev" in result.output
155
156 def test_reflog_limit(
157 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
158 ) -> None:
159 from muse.core.reflog import append_reflog
160
161 _make_repo(tmp_path, monkeypatch)
162 for i in range(10):
163 append_reflog(tmp_path, "main", old_id=None, new_id="c" * 64, author="A", operation=f"commit: {i}")
164 result = runner.invoke(cli, ["reflog", "--limit", "3"], catch_exceptions=False)
165 assert result.exit_code == 0
166 # At most 3 @{N} entries.
167 lines = [l for l in result.output.splitlines() if l.startswith("@{")]
168 assert len(lines) <= 3
169
170 def test_reflog_shows_at_index_format(
171 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
172 ) -> None:
173 from muse.core.reflog import append_reflog
174
175 _make_repo(tmp_path, monkeypatch)
176 append_reflog(tmp_path, "main", old_id=None, new_id="c" * 64, author="A", operation="commit: x")
177 result = runner.invoke(cli, ["reflog"], catch_exceptions=False)
178 # Format is @{N:...} so just check the @ prefix.
179 assert "@{" in result.output
180
181
182 # ---------------------------------------------------------------------------
183 # muse gc
184 # ---------------------------------------------------------------------------
185
186
187 class TestGcCli:
188 def test_gc_empty_reports_zero(
189 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
190 ) -> None:
191 _make_repo(tmp_path, monkeypatch)
192 result = runner.invoke(cli, ["gc"], catch_exceptions=False)
193 assert result.exit_code == 0
194 assert "0 object" in result.output
195
196 def test_gc_dry_run_does_not_delete(
197 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
198 ) -> None:
199 _make_repo(tmp_path, monkeypatch)
200 orphan_content = b"totally orphaned"
201 oid = blob_id(orphan_content)
202 write_object(tmp_path, oid, orphan_content)
203 obj_file = object_path(tmp_path, oid)
204
205 result = runner.invoke(cli, ["gc", "--dry-run"], catch_exceptions=False)
206 assert result.exit_code == 0
207 assert "dry-run" in result.output
208 assert obj_file.exists()
209
210 def test_gc_removes_orphan(
211 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
212 ) -> None:
213 _make_repo(tmp_path, monkeypatch)
214 orphan_content = b"not referenced anywhere at all"
215 oid = blob_id(orphan_content)
216 write_object(tmp_path, oid, orphan_content)
217 obj_file = object_path(tmp_path, oid)
218
219 result = runner.invoke(cli, ["gc", "--grace-period", "0"], catch_exceptions=False)
220 assert result.exit_code == 0
221 assert not obj_file.exists()
222
223 def test_gc_verbose_lists_objects(
224 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
225 ) -> None:
226 _make_repo(tmp_path, monkeypatch)
227 orphan_content = b"verbose orphan"
228 oid = blob_id(orphan_content)
229 write_object(tmp_path, oid, orphan_content)
230
231 result = runner.invoke(cli, ["gc", "--verbose", "--grace-period", "0"], catch_exceptions=False)
232 assert result.exit_code == 0
233 assert split_id(oid)[1] in result.output
234
235 def test_gc_preserves_reachable(
236 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
237 ) -> None:
238 # The hello.txt object in the initial commit must survive GC.
239 _make_repo(tmp_path, monkeypatch)
240 content = b"hello world\n"
241 oid = blob_id(content)
242 result = runner.invoke(cli, ["gc"], catch_exceptions=False)
243 assert result.exit_code == 0
244 assert object_path(tmp_path, oid).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.stderr or "Unknown format" in result.stderr
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.stderr
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_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", "--json", "hello.txt"], catch_exceptions=False)
441 assert result.exit_code == 0
442 parsed = json.loads(result.output)
443 assert "lines" in parsed
444 assert len(parsed["lines"]) >= 1
445 line = parsed["lines"][0]
446 assert "lineno" in line
447 assert "commit_id" in line
448 assert "content" in line
449
450 def test_blame_lineno_starts_at_1(
451 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
452 ) -> None:
453 _make_repo(tmp_path, monkeypatch)
454 result = runner.invoke(cli, ["blame", "--json", "hello.txt"], catch_exceptions=False)
455 assert result.exit_code == 0
456 parsed = json.loads(result.output)
457 assert parsed["lines"][0]["lineno"] == 1
458
459
460 # ---------------------------------------------------------------------------
461 # muse worktree
462 # ---------------------------------------------------------------------------
463
464
465 class TestWorktreeCli:
466 def _make_named_repo(
467 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
468 ) -> pathlib.Path:
469 """Create a repo in myproject/ subdirectory."""
470 repo_dir = tmp_path / "myproject"
471 repo_dir.mkdir()
472 muse = muse_dir(repo_dir)
473 for d in ("objects", "commits", "snapshots", "refs/heads"):
474 (muse / d).mkdir(parents=True, exist_ok=True)
475 (muse / "repo.json").write_text(json.dumps({"repo_id": "test"}))
476 (muse / "HEAD").write_text("ref: refs/heads/main\n")
477 (muse / "refs" / "heads" / "main").write_text(NULL_COMMIT_ID)
478 monkeypatch.chdir(repo_dir)
479 return repo_dir
480
481 def test_worktree_list_shows_main(
482 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
483 ) -> None:
484 self._make_named_repo(tmp_path, monkeypatch)
485 result = runner.invoke(cli, ["worktree", "list"], catch_exceptions=False)
486 assert result.exit_code == 0
487 assert "(main)" in result.output
488
489 def test_worktree_add_and_list(
490 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
491 ) -> None:
492 repo = self._make_named_repo(tmp_path, monkeypatch)
493 (heads_dir(repo) / "dev").write_text(NULL_COMMIT_ID)
494 result = runner.invoke(cli, ["worktree", "add", "mydev", "dev"], catch_exceptions=False)
495 assert result.exit_code == 0
496 result2 = runner.invoke(cli, ["worktree", "list"], catch_exceptions=False)
497 assert "mydev" in result2.output
498
499 def test_worktree_remove(
500 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
501 ) -> None:
502 repo = self._make_named_repo(tmp_path, monkeypatch)
503 (heads_dir(repo) / "dev").write_text(NULL_COMMIT_ID)
504 runner.invoke(cli, ["worktree", "add", "mydev", "dev"])
505 result = runner.invoke(cli, ["worktree", "remove", "mydev"], catch_exceptions=False)
506 assert result.exit_code == 0
507 assert "mydev" in result.output
508
509 def test_worktree_prune_empty(
510 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
511 ) -> None:
512 self._make_named_repo(tmp_path, monkeypatch)
513 result = runner.invoke(cli, ["worktree", "prune"], catch_exceptions=False)
514 assert result.exit_code == 0
515 assert "Nothing to prune" in result.output
516
517 def test_worktree_remove_nonexistent(
518 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
519 ) -> None:
520 self._make_named_repo(tmp_path, monkeypatch)
521 result = runner.invoke(cli, ["worktree", "remove", "nonexistent"])
522 assert result.exit_code != 0
523
524
525 # ---------------------------------------------------------------------------
526 # muse workspace
527 # ---------------------------------------------------------------------------
528
529
530 class TestWorkspaceCli:
531 def test_workspace_add_and_list(
532 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
533 ) -> None:
534 _make_repo(tmp_path, monkeypatch)
535 result = runner.invoke(
536 cli, ["workspace", "add", "core", "https://musehub.ai/acme/core"],
537 catch_exceptions=False,
538 )
539 assert result.exit_code == 0
540 assert "Added workspace member" in result.output
541
542 result2 = runner.invoke(cli, ["workspace", "list"], catch_exceptions=False)
543 assert result2.exit_code == 0
544 assert "core" in result2.output
545
546 def test_workspace_remove(
547 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
548 ) -> None:
549 _make_repo(tmp_path, monkeypatch)
550 runner.invoke(cli, ["workspace", "add", "core", "https://musehub.ai/acme/core"])
551 result = runner.invoke(cli, ["workspace", "remove", "core"], catch_exceptions=False)
552 assert result.exit_code == 0
553 assert "Removed" in result.output
554
555 def test_workspace_status_empty(
556 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
557 ) -> None:
558 _make_repo(tmp_path, monkeypatch)
559 result = runner.invoke(cli, ["workspace", "status"], catch_exceptions=False)
560 assert result.exit_code == 0
561 assert "No workspace members" in result.output
562
563 def test_workspace_list_empty(
564 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
565 ) -> None:
566 _make_repo(tmp_path, monkeypatch)
567 result = runner.invoke(cli, ["workspace", "list"], catch_exceptions=False)
568 assert result.exit_code == 0
569 assert "No workspace members" in result.output
570
571 def test_workspace_add_with_branch(
572 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
573 ) -> None:
574 _make_repo(tmp_path, monkeypatch)
575 runner.invoke(
576 cli, ["workspace", "add", "data", "https://example.com/data", "--branch", "v2"],
577 )
578 result = runner.invoke(cli, ["workspace", "list"], catch_exceptions=False)
579 assert "v2" in result.output
580
581 def test_workspace_remove_nonexistent(
582 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
583 ) -> None:
584 _make_repo(tmp_path, monkeypatch)
585 runner.invoke(cli, ["workspace", "add", "core", "https://example.com/core"])
586 result = runner.invoke(cli, ["workspace", "remove", "nonexistent"])
587 assert result.exit_code != 0
588
589 def test_workspace_sync_empty(
590 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
591 ) -> None:
592 _make_repo(tmp_path, monkeypatch)
593 result = runner.invoke(cli, ["workspace", "sync"], catch_exceptions=False)
594 assert result.exit_code == 0
595 assert "No members" in result.output
596
597 def test_workspace_add_duplicate(
598 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
599 ) -> None:
600 _make_repo(tmp_path, monkeypatch)
601 runner.invoke(cli, ["workspace", "add", "core", "https://example.com/core"])
602 result = runner.invoke(cli, ["workspace", "add", "core", "https://example.com/other"])
603 assert result.exit_code != 0
File History 1 commit