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