gabriel / muse public
test_branch_intent_created_by.py python
619 lines 24.6 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 20 days ago
1 """TDD tests for two new ``muse branch`` features.
2
3 Feature 1 — Branch intent + resumable
4 --------------------------------------
5 ``muse branch <name> [--intent TEXT] [--resumable]``
6
7 - Stores ``intent`` and ``resumable`` in ``.muse/config.toml`` under
8 ``[branch."<name>"]`` on create.
9 - Surfaces both fields in ``branch --json`` listing output.
10 - ``muse branch --resumable`` filters the listing to resumable branches only.
11
12 Feature 2 — created_by from tip commit
13 ---------------------------------------
14 ``branch --json`` surfaces ``created_by`` (the ``agent_id`` from the tip
15 commit's :class:`CommitRecord`) on every listing entry. Falls back to
16 ``""`` when the branch has no commits or the commit has no agent attribution.
17
18 Test categories
19 ---------------
20 - unit : config.py helpers (write_branch_meta, read_branch_meta)
21 - integration : parser flags, config.toml round-trip, listing JSON schema
22 - e2e : full CLI round-trips via CliRunner
23 - security : intent injection (ANSI, newlines, TOML metacharacters)
24 - data_integrity: resumable flag, intent survive save → list cycle
25 - performance : listing 50 branches with intent under 1 s
26 """
27
28 from __future__ import annotations
29 from collections.abc import Mapping
30
31 import json
32 import os
33 import pathlib
34 import time
35 import tomllib
36
37 import pytest
38
39 from tests.cli_test_helper import CliRunner, InvokeResult
40 from muse.core.refs import get_head_commit_id
41 from muse.core.paths import config_toml_path, heads_dir
42
43 runner = CliRunner()
44
45
46 # ---------------------------------------------------------------------------
47 # Helpers
48 # ---------------------------------------------------------------------------
49
50
51 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
52 saved = os.getcwd()
53 try:
54 os.chdir(repo)
55 return runner.invoke(None, args)
56 finally:
57 os.chdir(saved)
58
59
60 def _branch(repo: pathlib.Path, *extra: str) -> InvokeResult:
61 return _invoke(repo, ["branch", *extra])
62
63
64 def _commit(repo: pathlib.Path, msg: str = "commit") -> InvokeResult:
65 return _invoke(repo, ["commit", "-m", msg])
66
67
68 def _config(repo: pathlib.Path) -> Mapping[str, object]:
69 p = config_toml_path(repo)
70 if not p.exists():
71 return {}
72 with p.open("rb") as f:
73 return tomllib.load(f)
74
75
76 @pytest.fixture()
77 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
78 saved = os.getcwd()
79 try:
80 os.chdir(tmp_path)
81 runner.invoke(None, ["init"])
82 finally:
83 os.chdir(saved)
84 (tmp_path / "a.py").write_text("x = 1\n")
85 _commit(tmp_path, "initial")
86 return tmp_path
87
88
89 @pytest.fixture()
90 def agent_repo(tmp_path: pathlib.Path) -> pathlib.Path:
91 """Repo with an agent-attributed commit on main."""
92 saved = os.getcwd()
93 try:
94 os.chdir(tmp_path)
95 runner.invoke(None, ["init"])
96 finally:
97 os.chdir(saved)
98 (tmp_path / "a.py").write_text("x = 1\n")
99 _invoke(tmp_path, [
100 "commit", "-m", "agent commit",
101 "--agent-id", "claude-code",
102 "--model-id", "claude-sonnet-4-6",
103 ])
104 return tmp_path
105
106
107 # ===========================================================================
108 # Unit: config.py helpers
109 # ===========================================================================
110
111
112 class TestWriteBranchMeta:
113 """write_branch_meta persists intent + resumable to config.toml."""
114
115 def test_writes_intent_to_config(self, repo: pathlib.Path) -> None:
116 from muse.cli.config import write_branch_meta
117 write_branch_meta(repo, "feat/x", intent="refactor auth")
118 data = _config(repo)
119 assert data["branch"]["feat/x"]["intent"] == "refactor auth"
120
121 def test_writes_resumable_true(self, repo: pathlib.Path) -> None:
122 from muse.cli.config import write_branch_meta
123 write_branch_meta(repo, "task/y", resumable=True)
124 data = _config(repo)
125 assert data["branch"]["task/y"]["resumable"] is True
126
127 def test_writes_resumable_false(self, repo: pathlib.Path) -> None:
128 from muse.cli.config import write_branch_meta
129 write_branch_meta(repo, "task/z", resumable=False)
130 data = _config(repo)
131 assert data["branch"]["task/z"]["resumable"] is False
132
133 def test_writes_both_fields(self, repo: pathlib.Path) -> None:
134 from muse.cli.config import write_branch_meta
135 write_branch_meta(repo, "feat/both", intent="doing X", resumable=True)
136 data = _config(repo)
137 sec = data["branch"]["feat/both"]
138 assert sec["intent"] == "doing X"
139 assert sec["resumable"] is True
140
141 def test_does_not_clobber_upstream_fields(self, repo: pathlib.Path) -> None:
142 """Existing remote/merge keys must survive a write_branch_meta call."""
143 p = config_toml_path(repo)
144 p.write_text(
145 '[branch."main"]\nremote = "origin"\nmerge = "refs/heads/main"\n'
146 )
147 from muse.cli.config import write_branch_meta
148 write_branch_meta(repo, "main", intent="track origin")
149 data = _config(repo)
150 sec = data["branch"]["main"]
151 assert sec.get("remote") == "origin"
152 assert sec.get("merge") == "refs/heads/main"
153 assert sec.get("intent") == "track origin"
154
155 def test_updates_existing_entry(self, repo: pathlib.Path) -> None:
156 from muse.cli.config import write_branch_meta
157 write_branch_meta(repo, "feat/up", intent="first intent", resumable=False)
158 write_branch_meta(repo, "feat/up", intent="updated intent", resumable=True)
159 data = _config(repo)
160 sec = data["branch"]["feat/up"]
161 assert sec["intent"] == "updated intent"
162 assert sec["resumable"] is True
163
164 def test_multiple_branches_independent(self, repo: pathlib.Path) -> None:
165 from muse.cli.config import write_branch_meta
166 write_branch_meta(repo, "feat/a", intent="alpha")
167 write_branch_meta(repo, "feat/b", intent="beta", resumable=True)
168 data = _config(repo)
169 assert data["branch"]["feat/a"]["intent"] == "alpha"
170 assert "resumable" not in data["branch"]["feat/a"]
171 assert data["branch"]["feat/b"]["intent"] == "beta"
172 assert data["branch"]["feat/b"]["resumable"] is True
173
174 def test_creates_config_file_if_absent(self, repo: pathlib.Path) -> None:
175 p = config_toml_path(repo)
176 p.unlink(missing_ok=True)
177 from muse.cli.config import write_branch_meta
178 write_branch_meta(repo, "new-branch", intent="fresh")
179 assert p.exists()
180 data = _config(repo)
181 assert data["branch"]["new-branch"]["intent"] == "fresh"
182
183
184 class TestReadBranchMeta:
185 """read_branch_meta returns the stored dict (or empty) for a branch."""
186
187 def test_returns_intent_and_resumable(self, repo: pathlib.Path) -> None:
188 from muse.cli.config import write_branch_meta, read_branch_meta
189 write_branch_meta(repo, "feat/r", intent="do X", resumable=True)
190 meta = read_branch_meta(repo, "feat/r")
191 assert meta.get("intent") == "do X"
192 assert meta.get("resumable") is True
193
194 def test_returns_empty_for_unknown_branch(self, repo: pathlib.Path) -> None:
195 from muse.cli.config import read_branch_meta
196 assert read_branch_meta(repo, "nonexistent") == {}
197
198 def test_returns_empty_when_no_config(self, repo: pathlib.Path) -> None:
199 from muse.cli.config import read_branch_meta
200 (config_toml_path(repo)).unlink(missing_ok=True)
201 assert read_branch_meta(repo, "main") == {}
202
203
204 # ===========================================================================
205 # Unit: parser flags
206 # ===========================================================================
207
208
209 class TestParserFlags:
210 def _parse(self, *args: str) -> "argparse.Namespace":
211 import argparse
212 from muse.cli.commands.branch import register
213 p = argparse.ArgumentParser()
214 sub = p.add_subparsers()
215 register(sub)
216 return p.parse_args(["branch", *args])
217
218 def test_intent_flag(self) -> None:
219 ns = self._parse("new-branch", "--intent", "refactor the thing")
220 assert ns.intent == "refactor the thing"
221
222 def test_intent_default_none(self) -> None:
223 ns = self._parse("new-branch")
224 assert ns.intent is None
225
226 def test_resumable_flag(self) -> None:
227 ns = self._parse("new-branch", "--resumable")
228 assert ns.resumable is True
229
230 def test_resumable_default_false(self) -> None:
231 ns = self._parse("new-branch")
232 assert ns.resumable is False
233
234 def test_resumable_filter_flag(self) -> None:
235 ns = self._parse("--resumable")
236 assert ns.resumable is True
237
238
239 # ===========================================================================
240 # Integration: --intent / --resumable on create
241 # ===========================================================================
242
243
244 class TestCreateWithIntent:
245 def test_create_with_intent_exits_0(self, repo: pathlib.Path) -> None:
246 result = _branch(repo, "feat/x", "--intent", "do the thing")
247 assert result.exit_code == 0
248
249 def test_create_stores_intent_in_config(self, repo: pathlib.Path) -> None:
250 _branch(repo, "feat/config-test", "--intent", "store me")
251 data = _config(repo)
252 assert data["branch"]["feat/config-test"]["intent"] == "store me"
253
254 def test_create_stores_resumable_in_config(self, repo: pathlib.Path) -> None:
255 _branch(repo, "feat/res", "--resumable")
256 data = _config(repo)
257 assert data["branch"]["feat/res"]["resumable"] is True
258
259 def test_create_without_intent_no_config_entry(self, repo: pathlib.Path) -> None:
260 _branch(repo, "feat/plain")
261 data = _config(repo)
262 branch_sec = data.get("branch", {})
263 assert "feat/plain" not in branch_sec
264
265 def test_create_json_includes_intent(self, repo: pathlib.Path) -> None:
266 result = _branch(repo, "feat/j", "--intent", "json intent", "--json")
267 assert result.exit_code == 0
268 data = json.loads(result.output)
269 assert data.get("intent") == "json intent"
270
271 def test_create_json_includes_resumable(self, repo: pathlib.Path) -> None:
272 result = _branch(repo, "feat/jr", "--resumable", "--json")
273 data = json.loads(result.output)
274 assert data.get("resumable") is True
275
276 def test_create_json_resumable_false_when_not_set(self, repo: pathlib.Path) -> None:
277 result = _branch(repo, "feat/nores", "--json")
278 data = json.loads(result.output)
279 assert data.get("resumable") is False
280
281
282 # ===========================================================================
283 # Integration: listing JSON includes intent, resumable, created_by
284 # ===========================================================================
285
286
287 class TestListJsonNewFields:
288 def test_list_json_has_intent_field(self, repo: pathlib.Path) -> None:
289 _branch(repo, "feat/listed", "--intent", "listed intent")
290 result = _branch(repo, "--json")
291 data = json.loads(result.output)
292 entry = next(b for b in data if b["name"] == "feat/listed")
293 assert "intent" in entry
294 assert entry["intent"] == "listed intent"
295
296 def test_list_json_intent_null_for_plain_branch(self, repo: pathlib.Path) -> None:
297 _branch(repo, "feat/no-intent")
298 result = _branch(repo, "--json")
299 data = json.loads(result.output)
300 entry = next(b for b in data if b["name"] == "feat/no-intent")
301 assert entry.get("intent") is None
302
303 def test_list_json_has_resumable_field(self, repo: pathlib.Path) -> None:
304 _branch(repo, "feat/reslist", "--resumable")
305 result = _branch(repo, "--json")
306 data = json.loads(result.output)
307 entry = next(b for b in data if b["name"] == "feat/reslist")
308 assert "resumable" in entry
309 assert entry["resumable"] is True
310
311 def test_list_json_resumable_false_for_plain_branch(self, repo: pathlib.Path) -> None:
312 result = _branch(repo, "--json")
313 data = json.loads(result.output)
314 main = next(b for b in data if b["name"] == "main")
315 assert main.get("resumable") is False
316
317 def test_list_json_has_created_by_field(self, repo: pathlib.Path) -> None:
318 result = _branch(repo, "--json")
319 data = json.loads(result.output)
320 assert "created_by" in data[0]
321
322 def test_list_json_created_by_from_agent_commit(
323 self, agent_repo: pathlib.Path
324 ) -> None:
325 result = _branch(agent_repo, "--json")
326 data = json.loads(result.output)
327 main = next(b for b in data if b["name"] == "main")
328 assert main["created_by"] == "claude-code"
329
330 def test_list_json_created_by_empty_for_human_commit(
331 self, repo: pathlib.Path
332 ) -> None:
333 result = _branch(repo, "--json")
334 data = json.loads(result.output)
335 main = next(b for b in data if b["name"] == "main")
336 # Human commit has no agent_id — empty string or null
337 assert main["created_by"] in ("", None)
338
339 def test_list_json_created_by_empty_for_empty_branch(
340 self, repo: pathlib.Path
341 ) -> None:
342 (heads_dir(repo) / "empty").write_text("")
343 result = _branch(repo, "--json")
344 data = json.loads(result.output)
345 entry = next(b for b in data if b["name"] == "empty")
346 assert entry["created_by"] in ("", None)
347
348 def test_schema_complete(self, repo: pathlib.Path) -> None:
349 """All new fields must appear in the listing schema."""
350 result = _branch(repo, "--json")
351 data = json.loads(result.output)
352 required = {"name", "current", "commit_id", "committed_at",
353 "last_message", "upstream", "intent", "resumable", "created_by"}
354 missing = required - set(data[0].keys())
355 assert not missing, f"branch --json missing fields: {missing}"
356
357
358 # ===========================================================================
359 # E2E: --resumable listing filter
360 # ===========================================================================
361
362
363 class TestResumableFilter:
364 def test_resumable_filter_shows_only_resumable(self, repo: pathlib.Path) -> None:
365 _branch(repo, "task/resumable-1", "--resumable")
366 _branch(repo, "task/resumable-2", "--resumable")
367 _branch(repo, "task/not-resumable")
368 result = _branch(repo, "--resumable", "--json")
369 assert result.exit_code == 0
370 data = json.loads(result.output)
371 names = [b["name"] for b in data]
372 assert "task/resumable-1" in names
373 assert "task/resumable-2" in names
374 assert "task/not-resumable" not in names
375 assert "main" not in names
376
377 def test_resumable_filter_empty_when_none(self, repo: pathlib.Path) -> None:
378 result = _branch(repo, "--resumable", "--json")
379 assert result.exit_code == 0
380 data = json.loads(result.output)
381 assert data == []
382
383 def test_resumable_filter_text_output(self, repo: pathlib.Path) -> None:
384 _branch(repo, "task/res", "--resumable")
385 result = _branch(repo, "--resumable")
386 assert result.exit_code == 0
387 assert "task/res" in result.output
388
389 def test_resumable_filter_all_resumable_returned(self, repo: pathlib.Path) -> None:
390 for i in range(5):
391 _branch(repo, f"task/r-{i}", "--resumable")
392 result = _branch(repo, "--resumable", "--json")
393 data = json.loads(result.output)
394 assert len(data) == 5
395
396 def test_resumable_combined_with_merged_filter(self, repo: pathlib.Path) -> None:
397 """--resumable and --merged can be combined."""
398 _branch(repo, "task/merged-resumable", "--resumable")
399 result = _branch(repo, "--resumable", "--merged", "--json")
400 assert result.exit_code == 0
401 data = json.loads(result.output)
402 names = [b["name"] for b in data]
403 # task/merged-resumable shares HEAD with main, so it's merged
404 assert "task/merged-resumable" in names
405
406
407 # ===========================================================================
408 # E2E: full round-trips
409 # ===========================================================================
410
411
412 class TestE2eRoundTrips:
413 def test_intent_survives_list_cycle(self, repo: pathlib.Path) -> None:
414 _branch(repo, "feat/rt", "--intent", "round-trip test", "--resumable")
415 result = _branch(repo, "--json")
416 data = json.loads(result.output)
417 entry = next(b for b in data if b["name"] == "feat/rt")
418 assert entry["intent"] == "round-trip test"
419 assert entry["resumable"] is True
420
421 def test_created_by_survives_new_branch_on_agent_repo(
422 self, agent_repo: pathlib.Path
423 ) -> None:
424 _branch(agent_repo, "child-branch")
425 result = _branch(agent_repo, "--json")
426 data = json.loads(result.output)
427 # child-branch points at same commit as main
428 child = next(b for b in data if b["name"] == "child-branch")
429 assert child["created_by"] == "claude-code"
430
431 def test_create_intent_resumable_json_schema(self, repo: pathlib.Path) -> None:
432 result = _branch(repo, "feat/full", "--intent", "full schema", "--resumable", "--json")
433 data = json.loads(result.output)
434 assert data["action"] == "created"
435 assert data["intent"] == "full schema"
436 assert data["resumable"] is True
437 assert "branch" in data
438 assert "commit_id" in data
439
440 def test_resumable_filter_with_r_flag(self, repo: pathlib.Path) -> None:
441 """--resumable must not conflict with -r (remote-tracking) flag."""
442 _branch(repo, "task/local-res", "--resumable")
443 # -r with no remotes returns empty; should not crash
444 result = _branch(repo, "-r", "--resumable", "--json")
445 assert result.exit_code == 0
446 assert json.loads(result.output) == []
447
448
449 # ===========================================================================
450 # Security: intent injection
451 # ===========================================================================
452
453
454 class TestIntentSecurity:
455 def _has_ansi(self, s: str) -> bool:
456 return "\x1b[" in s
457
458 def test_ansi_in_intent_stripped_from_output(self, repo: pathlib.Path) -> None:
459 _branch(repo, "sec/ansi", "--intent", "\x1b[31mmalicious\x1b[0m")
460 result = _branch(repo, "--json")
461 data = json.loads(result.output)
462 entry = next(b for b in data if b["name"] == "sec/ansi")
463 assert not self._has_ansi(str(entry.get("intent", "")))
464
465 def test_newline_in_intent_escaped_in_toml(self, repo: pathlib.Path) -> None:
466 """Intent with newline must not break TOML file structure."""
467 _branch(repo, "sec/nl", "--intent", "line1\nline2")
468 # Config file must still be parseable
469 data = _config(repo)
470 assert isinstance(data, dict)
471
472 def test_toml_metachar_in_intent_safe(self, repo: pathlib.Path) -> None:
473 """TOML-special chars in intent must not allow section injection."""
474 _branch(repo, "sec/toml", '--intent', '[malicious]\nkey = "injected"')
475 data = _config(repo)
476 # No top-level 'malicious' section should have been injected
477 assert "malicious" not in data
478
479 def test_intent_truncated_to_reasonable_length(self, repo: pathlib.Path) -> None:
480 """Very long intent must not crash or produce a corrupt config."""
481 long_intent = "x" * 10_000
482 result = _branch(repo, "sec/long", "--intent", long_intent)
483 assert result.exit_code == 0
484 data = _config(repo)
485 stored = data.get("branch", {}).get("sec/long", {}).get("intent", "")
486 assert isinstance(stored, str)
487
488
489 # ===========================================================================
490 # Data integrity
491 # ===========================================================================
492
493
494 class TestDataIntegrity:
495 def test_intent_not_lost_on_second_branch_create(self, repo: pathlib.Path) -> None:
496 """Creating a second branch must not overwrite the first's intent."""
497 _branch(repo, "feat/first", "--intent", "first intent")
498 _branch(repo, "feat/second", "--intent", "second intent")
499 data = _config(repo)
500 assert data["branch"]["feat/first"]["intent"] == "first intent"
501 assert data["branch"]["feat/second"]["intent"] == "second intent"
502
503 def test_resumable_preserved_across_other_branch_operations(
504 self, repo: pathlib.Path
505 ) -> None:
506 _branch(repo, "task/keep", "--resumable")
507 _branch(repo, "task/other", "--intent", "unrelated")
508 data = _config(repo)
509 assert data["branch"]["task/keep"]["resumable"] is True
510
511 def test_config_toml_valid_toml_after_write(self, repo: pathlib.Path) -> None:
512 _branch(repo, "feat/valid", "--intent", 'quotes "and" stuff', "--resumable")
513 # tomllib.load must succeed
514 p = config_toml_path(repo)
515 with p.open("rb") as f:
516 parsed = tomllib.load(f)
517 assert isinstance(parsed, dict)
518
519
520 # ===========================================================================
521 # Performance
522 # ===========================================================================
523
524
525 class TestPerformance:
526 def test_list_50_branches_with_intent_under_1s(self, repo: pathlib.Path) -> None:
527 for i in range(50):
528 _branch(repo, f"perf/task-{i:03d}", "--intent", f"task {i}", "--resumable")
529
530 start = time.monotonic()
531 result = _branch(repo, "--json")
532 elapsed = time.monotonic() - start
533
534 assert result.exit_code == 0
535 data = json.loads(result.output)
536 assert len(data) == 51 # main + 50
537 assert elapsed < 1.0, f"listing 51 branches with intent took {elapsed:.2f}s"
538
539 def test_resumable_filter_50_branches_under_500ms(
540 self, repo: pathlib.Path
541 ) -> None:
542 for i in range(50):
543 _branch(repo, f"filter/task-{i:03d}", "--resumable")
544
545 start = time.monotonic()
546 result = _branch(repo, "--resumable", "--json")
547 elapsed = time.monotonic() - start
548
549 assert result.exit_code == 0
550 data = json.loads(result.output)
551 assert len(data) == 50
552 assert elapsed < 0.5, f"--resumable filter on 50 branches took {elapsed:.2f}s"
553
554
555 # ---------------------------------------------------------------------------
556 # Metadata update on existing branch
557 # ---------------------------------------------------------------------------
558
559
560 class TestBranchMetaUpdate:
561 """--intent / --resumable on an already-existing branch update metadata."""
562
563 def test_set_intent_on_existing_branch(self, repo: pathlib.Path) -> None:
564 _branch(repo, "existing")
565 result = _branch(repo, "existing", "--intent", "added later")
566 assert result.exit_code == 0
567
568 def test_update_action_in_json(self, repo: pathlib.Path) -> None:
569 _branch(repo, "upd")
570 result = _branch(repo, "upd", "--intent", "my intent", "--json")
571 assert result.exit_code == 0
572 data = json.loads(result.output)
573 assert data["action"] == "updated"
574 assert data["branch"] == "upd"
575 assert data["intent"] == "my intent"
576
577 def test_intent_visible_in_listing_after_update(self, repo: pathlib.Path) -> None:
578 _branch(repo, "later")
579 _branch(repo, "later", "--intent", "set after creation")
580 listing = json.loads(_branch(repo, "--json").output)
581 entry = next(e for e in listing if e["name"] == "later")
582 assert entry["intent"] == "set after creation"
583
584 def test_set_resumable_on_existing_branch(self, repo: pathlib.Path) -> None:
585 _branch(repo, "checkpoint")
586 result = _branch(repo, "checkpoint", "--resumable", "--json")
587 assert result.exit_code == 0
588 data = json.loads(result.output)
589 assert data["resumable"] is True
590
591 def test_resumable_visible_in_listing_after_update(self, repo: pathlib.Path) -> None:
592 _branch(repo, "chkpt2")
593 _branch(repo, "chkpt2", "--resumable")
594 listing = json.loads(_branch(repo, "--json").output)
595 entry = next(e for e in listing if e["name"] == "chkpt2")
596 assert entry["resumable"] is True
597
598 def test_update_does_not_overwrite_unspecified_fields(
599 self, repo: pathlib.Path
600 ) -> None:
601 """Setting resumable later must not wipe a previously stored intent."""
602 _branch(repo, "preserve", "--intent", "keep me")
603 _branch(repo, "preserve", "--resumable")
604 listing = json.loads(_branch(repo, "--json").output)
605 entry = next(e for e in listing if e["name"] == "preserve")
606 assert entry["intent"] == "keep me"
607 assert entry["resumable"] is True
608
609 def test_update_with_start_point_still_errors(self, repo: pathlib.Path) -> None:
610 """Passing a start_point to an existing branch is still an error."""
611 _branch(repo, "existing2")
612 result = _branch(repo, "existing2", "main", "--intent", "x")
613 assert result.exit_code != 0
614
615 def test_no_meta_flags_still_errors_on_existing(self, repo: pathlib.Path) -> None:
616 """Plain `muse branch <existing>` (no --intent/--resumable) still errors."""
617 _branch(repo, "plain")
618 result = _branch(repo, "plain")
619 assert result.exit_code != 0
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 20 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 28 days ago