gabriel / muse public
test_branch_intent_created_by.py python
625 lines 24.8 KB
Raw
sha256:5eff0848db1d0748f3a195ca871a9275e7455d447690411ec2d83434e11baa65 feat(#216): persist write-once BranchMeta creation provenance Human minor ⚠ breaking 2 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_stamps_creation_not_intent(
260 self, repo: pathlib.Path
261 ) -> None:
262 """Plain create stamps write-once creation (#216); no intent key."""
263 _branch(repo, "feat/plain")
264 data = _config(repo)
265 branch_sec = data.get("branch", {})
266 assert "feat/plain" in branch_sec
267 sec = branch_sec["feat/plain"]
268 assert "created_at" in sec
269 assert "intent" not in sec
270
271 def test_create_json_includes_intent(self, repo: pathlib.Path) -> None:
272 result = _branch(repo, "feat/j", "--intent", "json intent", "--json")
273 assert result.exit_code == 0
274 data = json.loads(result.output)
275 assert data.get("intent") == "json intent"
276
277 def test_create_json_includes_resumable(self, repo: pathlib.Path) -> None:
278 result = _branch(repo, "feat/jr", "--resumable", "--json")
279 data = json.loads(result.output)
280 assert data.get("resumable") is True
281
282 def test_create_json_resumable_false_when_not_set(self, repo: pathlib.Path) -> None:
283 result = _branch(repo, "feat/nores", "--json")
284 data = json.loads(result.output)
285 assert data.get("resumable") is False
286
287
288 # ===========================================================================
289 # Integration: listing JSON includes intent, resumable, created_by
290 # ===========================================================================
291
292
293 class TestListJsonNewFields:
294 def test_list_json_has_intent_field(self, repo: pathlib.Path) -> None:
295 _branch(repo, "feat/listed", "--intent", "listed intent")
296 result = _branch(repo, "--json")
297 data = json.loads(result.output)
298 entry = next(b for b in data if b["name"] == "feat/listed")
299 assert "intent" in entry
300 assert entry["intent"] == "listed intent"
301
302 def test_list_json_intent_null_for_plain_branch(self, repo: pathlib.Path) -> None:
303 _branch(repo, "feat/no-intent")
304 result = _branch(repo, "--json")
305 data = json.loads(result.output)
306 entry = next(b for b in data if b["name"] == "feat/no-intent")
307 assert entry.get("intent") is None
308
309 def test_list_json_has_resumable_field(self, repo: pathlib.Path) -> None:
310 _branch(repo, "feat/reslist", "--resumable")
311 result = _branch(repo, "--json")
312 data = json.loads(result.output)
313 entry = next(b for b in data if b["name"] == "feat/reslist")
314 assert "resumable" in entry
315 assert entry["resumable"] is True
316
317 def test_list_json_resumable_false_for_plain_branch(self, repo: pathlib.Path) -> None:
318 result = _branch(repo, "--json")
319 data = json.loads(result.output)
320 main = next(b for b in data if b["name"] == "main")
321 assert main.get("resumable") is False
322
323 def test_list_json_has_created_by_field(self, repo: pathlib.Path) -> None:
324 result = _branch(repo, "--json")
325 data = json.loads(result.output)
326 assert "created_by" in data[0]
327
328 def test_list_json_created_by_from_agent_commit(
329 self, agent_repo: pathlib.Path
330 ) -> None:
331 result = _branch(agent_repo, "--json")
332 data = json.loads(result.output)
333 main = next(b for b in data if b["name"] == "main")
334 assert main["created_by"] == "claude-code"
335
336 def test_list_json_created_by_empty_for_human_commit(
337 self, repo: pathlib.Path
338 ) -> None:
339 result = _branch(repo, "--json")
340 data = json.loads(result.output)
341 main = next(b for b in data if b["name"] == "main")
342 # Human commit has no agent_id — empty string or null
343 assert main["created_by"] in ("", None)
344
345 def test_list_json_created_by_empty_for_empty_branch(
346 self, repo: pathlib.Path
347 ) -> None:
348 (heads_dir(repo) / "empty").write_text("")
349 result = _branch(repo, "--json")
350 data = json.loads(result.output)
351 entry = next(b for b in data if b["name"] == "empty")
352 assert entry["created_by"] in ("", None)
353
354 def test_schema_complete(self, repo: pathlib.Path) -> None:
355 """All new fields must appear in the listing schema."""
356 result = _branch(repo, "--json")
357 data = json.loads(result.output)
358 required = {"name", "current", "commit_id", "committed_at",
359 "last_message", "upstream", "intent", "resumable", "created_by"}
360 missing = required - set(data[0].keys())
361 assert not missing, f"branch --json missing fields: {missing}"
362
363
364 # ===========================================================================
365 # E2E: --resumable listing filter
366 # ===========================================================================
367
368
369 class TestResumableFilter:
370 def test_resumable_filter_shows_only_resumable(self, repo: pathlib.Path) -> None:
371 _branch(repo, "task/resumable-1", "--resumable")
372 _branch(repo, "task/resumable-2", "--resumable")
373 _branch(repo, "task/not-resumable")
374 result = _branch(repo, "--resumable", "--json")
375 assert result.exit_code == 0
376 data = json.loads(result.output)
377 names = [b["name"] for b in data]
378 assert "task/resumable-1" in names
379 assert "task/resumable-2" in names
380 assert "task/not-resumable" not in names
381 assert "main" not in names
382
383 def test_resumable_filter_empty_when_none(self, repo: pathlib.Path) -> None:
384 result = _branch(repo, "--resumable", "--json")
385 assert result.exit_code == 0
386 data = json.loads(result.output)
387 assert data == []
388
389 def test_resumable_filter_text_output(self, repo: pathlib.Path) -> None:
390 _branch(repo, "task/res", "--resumable")
391 result = _branch(repo, "--resumable")
392 assert result.exit_code == 0
393 assert "task/res" in result.output
394
395 def test_resumable_filter_all_resumable_returned(self, repo: pathlib.Path) -> None:
396 for i in range(5):
397 _branch(repo, f"task/r-{i}", "--resumable")
398 result = _branch(repo, "--resumable", "--json")
399 data = json.loads(result.output)
400 assert len(data) == 5
401
402 def test_resumable_combined_with_merged_filter(self, repo: pathlib.Path) -> None:
403 """--resumable and --merged can be combined."""
404 _branch(repo, "task/merged-resumable", "--resumable")
405 result = _branch(repo, "--resumable", "--merged", "--json")
406 assert result.exit_code == 0
407 data = json.loads(result.output)
408 names = [b["name"] for b in data]
409 # task/merged-resumable shares HEAD with main, so it's merged
410 assert "task/merged-resumable" in names
411
412
413 # ===========================================================================
414 # E2E: full round-trips
415 # ===========================================================================
416
417
418 class TestE2eRoundTrips:
419 def test_intent_survives_list_cycle(self, repo: pathlib.Path) -> None:
420 _branch(repo, "feat/rt", "--intent", "round-trip test", "--resumable")
421 result = _branch(repo, "--json")
422 data = json.loads(result.output)
423 entry = next(b for b in data if b["name"] == "feat/rt")
424 assert entry["intent"] == "round-trip test"
425 assert entry["resumable"] is True
426
427 def test_created_by_survives_new_branch_on_agent_repo(
428 self, agent_repo: pathlib.Path
429 ) -> None:
430 _branch(agent_repo, "child-branch")
431 result = _branch(agent_repo, "--json")
432 data = json.loads(result.output)
433 # child-branch points at same commit as main
434 child = next(b for b in data if b["name"] == "child-branch")
435 assert child["created_by"] == "claude-code"
436
437 def test_create_intent_resumable_json_schema(self, repo: pathlib.Path) -> None:
438 result = _branch(repo, "feat/full", "--intent", "full schema", "--resumable", "--json")
439 data = json.loads(result.output)
440 assert data["action"] == "created"
441 assert data["intent"] == "full schema"
442 assert data["resumable"] is True
443 assert "branch" in data
444 assert "commit_id" in data
445
446 def test_resumable_filter_with_r_flag(self, repo: pathlib.Path) -> None:
447 """--resumable must not conflict with -r (remote-tracking) flag."""
448 _branch(repo, "task/local-res", "--resumable")
449 # -r with no remotes returns empty; should not crash
450 result = _branch(repo, "-r", "--resumable", "--json")
451 assert result.exit_code == 0
452 assert json.loads(result.output) == []
453
454
455 # ===========================================================================
456 # Security: intent injection
457 # ===========================================================================
458
459
460 class TestIntentSecurity:
461 def _has_ansi(self, s: str) -> bool:
462 return "\x1b[" in s
463
464 def test_ansi_in_intent_stripped_from_output(self, repo: pathlib.Path) -> None:
465 _branch(repo, "sec/ansi", "--intent", "\x1b[31mmalicious\x1b[0m")
466 result = _branch(repo, "--json")
467 data = json.loads(result.output)
468 entry = next(b for b in data if b["name"] == "sec/ansi")
469 assert not self._has_ansi(str(entry.get("intent", "")))
470
471 def test_newline_in_intent_escaped_in_toml(self, repo: pathlib.Path) -> None:
472 """Intent with newline must not break TOML file structure."""
473 _branch(repo, "sec/nl", "--intent", "line1\nline2")
474 # Config file must still be parseable
475 data = _config(repo)
476 assert isinstance(data, dict)
477
478 def test_toml_metachar_in_intent_safe(self, repo: pathlib.Path) -> None:
479 """TOML-special chars in intent must not allow section injection."""
480 _branch(repo, "sec/toml", '--intent', '[malicious]\nkey = "injected"')
481 data = _config(repo)
482 # No top-level 'malicious' section should have been injected
483 assert "malicious" not in data
484
485 def test_intent_truncated_to_reasonable_length(self, repo: pathlib.Path) -> None:
486 """Very long intent must not crash or produce a corrupt config."""
487 long_intent = "x" * 10_000
488 result = _branch(repo, "sec/long", "--intent", long_intent)
489 assert result.exit_code == 0
490 data = _config(repo)
491 stored = data.get("branch", {}).get("sec/long", {}).get("intent", "")
492 assert isinstance(stored, str)
493
494
495 # ===========================================================================
496 # Data integrity
497 # ===========================================================================
498
499
500 class TestDataIntegrity:
501 def test_intent_not_lost_on_second_branch_create(self, repo: pathlib.Path) -> None:
502 """Creating a second branch must not overwrite the first's intent."""
503 _branch(repo, "feat/first", "--intent", "first intent")
504 _branch(repo, "feat/second", "--intent", "second intent")
505 data = _config(repo)
506 assert data["branch"]["feat/first"]["intent"] == "first intent"
507 assert data["branch"]["feat/second"]["intent"] == "second intent"
508
509 def test_resumable_preserved_across_other_branch_operations(
510 self, repo: pathlib.Path
511 ) -> None:
512 _branch(repo, "task/keep", "--resumable")
513 _branch(repo, "task/other", "--intent", "unrelated")
514 data = _config(repo)
515 assert data["branch"]["task/keep"]["resumable"] is True
516
517 def test_config_toml_valid_toml_after_write(self, repo: pathlib.Path) -> None:
518 _branch(repo, "feat/valid", "--intent", 'quotes "and" stuff', "--resumable")
519 # tomllib.load must succeed
520 p = config_toml_path(repo)
521 with p.open("rb") as f:
522 parsed = tomllib.load(f)
523 assert isinstance(parsed, dict)
524
525
526 # ===========================================================================
527 # Performance
528 # ===========================================================================
529
530
531 class TestPerformance:
532 def test_list_50_branches_with_intent_under_1s(self, repo: pathlib.Path) -> None:
533 for i in range(50):
534 _branch(repo, f"perf/task-{i:03d}", "--intent", f"task {i}", "--resumable")
535
536 start = time.monotonic()
537 result = _branch(repo, "--json")
538 elapsed = time.monotonic() - start
539
540 assert result.exit_code == 0
541 data = json.loads(result.output)
542 assert len(data) == 51 # main + 50
543 assert elapsed < 1.0, f"listing 51 branches with intent took {elapsed:.2f}s"
544
545 def test_resumable_filter_50_branches_under_500ms(
546 self, repo: pathlib.Path
547 ) -> None:
548 for i in range(50):
549 _branch(repo, f"filter/task-{i:03d}", "--resumable")
550
551 start = time.monotonic()
552 result = _branch(repo, "--resumable", "--json")
553 elapsed = time.monotonic() - start
554
555 assert result.exit_code == 0
556 data = json.loads(result.output)
557 assert len(data) == 50
558 assert elapsed < 0.5, f"--resumable filter on 50 branches took {elapsed:.2f}s"
559
560
561 # ---------------------------------------------------------------------------
562 # Metadata update on existing branch
563 # ---------------------------------------------------------------------------
564
565
566 class TestBranchMetaUpdate:
567 """--intent / --resumable on an already-existing branch update metadata."""
568
569 def test_set_intent_on_existing_branch(self, repo: pathlib.Path) -> None:
570 _branch(repo, "existing")
571 result = _branch(repo, "existing", "--intent", "added later")
572 assert result.exit_code == 0
573
574 def test_update_action_in_json(self, repo: pathlib.Path) -> None:
575 _branch(repo, "upd")
576 result = _branch(repo, "upd", "--intent", "my intent", "--json")
577 assert result.exit_code == 0
578 data = json.loads(result.output)
579 assert data["action"] == "updated"
580 assert data["branch"] == "upd"
581 assert data["intent"] == "my intent"
582
583 def test_intent_visible_in_listing_after_update(self, repo: pathlib.Path) -> None:
584 _branch(repo, "later")
585 _branch(repo, "later", "--intent", "set after creation")
586 listing = json.loads(_branch(repo, "--json").output)
587 entry = next(e for e in listing if e["name"] == "later")
588 assert entry["intent"] == "set after creation"
589
590 def test_set_resumable_on_existing_branch(self, repo: pathlib.Path) -> None:
591 _branch(repo, "checkpoint")
592 result = _branch(repo, "checkpoint", "--resumable", "--json")
593 assert result.exit_code == 0
594 data = json.loads(result.output)
595 assert data["resumable"] is True
596
597 def test_resumable_visible_in_listing_after_update(self, repo: pathlib.Path) -> None:
598 _branch(repo, "chkpt2")
599 _branch(repo, "chkpt2", "--resumable")
600 listing = json.loads(_branch(repo, "--json").output)
601 entry = next(e for e in listing if e["name"] == "chkpt2")
602 assert entry["resumable"] is True
603
604 def test_update_does_not_overwrite_unspecified_fields(
605 self, repo: pathlib.Path
606 ) -> None:
607 """Setting resumable later must not wipe a previously stored intent."""
608 _branch(repo, "preserve", "--intent", "keep me")
609 _branch(repo, "preserve", "--resumable")
610 listing = json.loads(_branch(repo, "--json").output)
611 entry = next(e for e in listing if e["name"] == "preserve")
612 assert entry["intent"] == "keep me"
613 assert entry["resumable"] is True
614
615 def test_update_with_start_point_still_errors(self, repo: pathlib.Path) -> None:
616 """Passing a start_point to an existing branch is still an error."""
617 _branch(repo, "existing2")
618 result = _branch(repo, "existing2", "main", "--intent", "x")
619 assert result.exit_code != 0
620
621 def test_no_meta_flags_still_errors_on_existing(self, repo: pathlib.Path) -> None:
622 """Plain `muse branch <existing>` (no --intent/--resumable) still errors."""
623 _branch(repo, "plain")
624 result = _branch(repo, "plain")
625 assert result.exit_code != 0
File History 1 commit
sha256:5eff0848db1d0748f3a195ca871a9275e7455d447690411ec2d83434e11baa65 feat(#216): persist write-once BranchMeta creation provenance Human minor 2 days ago