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