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