gabriel / muse public
test_branch_created_meta.py python
570 lines 21.5 KB
Raw
sha256:5eff0848db1d0748f3a195ca871a9275e7455d447690411ec2d83434e11baa65 feat(#216): persist write-once BranchMeta creation provenance Human minor ⚠ breaking 3 days ago
1 """Seven-tier tests for BranchMeta created_by/created_at provenance (musehub#216).
2
3 Freeze: ``docs/MUSE-216-BRANCHMETA-CREATED-BY-FREEZE.md`` §5.
4
5 Tiers
6 -----
7 1 unit — resolve / write-once / load-dump / move / read helper
8 2 integration — plain ``muse branch`` stamps TOML; tip ``created_by`` unchanged
9 3 e2e — human create then agent tip; copy restamp; rename moves meta
10 4 stress — 200 creates; every list entry has both new keys
11 5 data-integrity — stamp twice; list stable; corrupt kind; delete removes section
12 6 performance — 200-branch list < 5s
13 7 security — ESC stripped; no key material; quoted handle round-trips
14 """
15
16 from __future__ import annotations
17
18 import json
19 import pathlib
20 import time
21 import tomllib
22 from collections.abc import Mapping
23 from unittest import mock
24
25 import pytest
26
27 from tests.cli_test_helper import CliRunner
28
29 cli = None
30 runner = CliRunner()
31
32 _HUB_URL = "https://localhost:1337"
33
34
35 def _env(root: pathlib.Path) -> Mapping[str, str]:
36 return {"MUSE_REPO_ROOT": str(root)}
37
38
39 def _branch_json(root: pathlib.Path) -> list[dict]:
40 result = runner.invoke(cli, ["branch", "--json"], env=_env(root))
41 assert result.exit_code == 0, f"branch --json failed:\n{result.output}"
42 data = json.loads(result.output.strip())
43 assert isinstance(data, list)
44 return data
45
46
47 def _entry(root: pathlib.Path, name: str) -> dict:
48 return next(b for b in _branch_json(root) if b["name"] == name)
49
50
51 def _toml_branch(root: pathlib.Path, name: str) -> Mapping[str, object]:
52 from muse.core.paths import config_toml_path
53
54 with config_toml_path(root).open("rb") as f:
55 data = tomllib.load(f)
56 return data.get("branch", {}).get(name, {})
57
58
59 def _set_user_handle(root: pathlib.Path, handle: str) -> None:
60 """Wire identity so ``get_config_value("user.handle", root)`` returns *handle*."""
61 from muse.cli.config import set_hub_url
62 from muse.core.identity import save_identity
63
64 set_hub_url(_HUB_URL, root)
65 save_identity(_HUB_URL, {
66 "type": "human",
67 "handle": handle,
68 "algorithm": "ed25519",
69 "fingerprint": "0" * 64,
70 "capabilities": [],
71 "provisioned_by": "",
72 "hd_path": "",
73 "provisioned_by_fingerprint": "",
74 })
75
76
77 @pytest.fixture()
78 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
79 monkeypatch.chdir(tmp_path)
80 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
81 env = _env(tmp_path)
82 r = runner.invoke(cli, ["init", "--domain", "code"], env=env)
83 assert r.exit_code == 0, r.output
84 (tmp_path / "a.py").write_text("x = 1\n")
85 runner.invoke(cli, ["code", "add", "a.py"], env=env)
86 cr = runner.invoke(cli, ["commit", "-m", "initial", "--author", "seed"], env=env)
87 assert cr.exit_code == 0, cr.output
88 return tmp_path
89
90
91 # ---------------------------------------------------------------------------
92 # Tier 1 — unit
93 # ---------------------------------------------------------------------------
94
95
96 class TestResolveAndWriteOnceUnit:
97 """Tier 1: helpers in ``muse.cli.config`` and list read helper."""
98
99 def test_resolve_agent_env_wins(
100 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
101 ) -> None:
102 from muse.cli.config import _resolve_branch_creator
103
104 _set_user_handle(repo, "aaronrene")
105 monkeypatch.setenv("MUSE_AGENT_ID", "claude-code")
106 assert _resolve_branch_creator(repo) == ("claude-code", "agent")
107
108 def test_resolve_human_from_handle(
109 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
110 ) -> None:
111 from muse.cli.config import _resolve_branch_creator
112
113 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
114 _set_user_handle(repo, "aaronrene")
115 assert _resolve_branch_creator(repo) == ("aaronrene", "human")
116
117 def test_resolve_both_empty_returns_none(
118 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
119 ) -> None:
120 from muse.cli.config import _resolve_branch_creator
121
122 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
123 with mock.patch(
124 "muse.cli.config.get_config_value",
125 return_value=None,
126 ):
127 assert _resolve_branch_creator(repo) is None
128
129 def test_resolve_whitespace_env_falls_to_human(
130 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
131 ) -> None:
132 from muse.cli.config import _resolve_branch_creator
133
134 monkeypatch.setenv("MUSE_AGENT_ID", " ")
135 _set_user_handle(repo, "aaronrene")
136 assert _resolve_branch_creator(repo) == ("aaronrene", "human")
137
138 def test_resolve_esc_only_env_returns_none(
139 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
140 ) -> None:
141 from muse.cli.config import _resolve_branch_creator
142
143 monkeypatch.setenv("MUSE_AGENT_ID", "\x1b\x07\x1f")
144 with mock.patch(
145 "muse.cli.config.get_config_value",
146 return_value=None,
147 ):
148 assert _resolve_branch_creator(repo) is None
149
150 def test_write_once_second_stamp_preserves_created_at(
151 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
152 ) -> None:
153 from muse.cli.config import read_branch_meta, stamp_branch_created, write_branch_meta
154
155 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
156 _set_user_handle(repo, "aaronrene")
157 stamp_branch_created(repo, "feat/once")
158 first = read_branch_meta(repo, "feat/once")["created_at"]
159 assert first
160 write_branch_meta(repo, "feat/once", intent="later")
161 stamp_branch_created(repo, "feat/once")
162 second = read_branch_meta(repo, "feat/once")
163 assert second["created_at"] == first
164 assert second["created_by"] == "aaronrene"
165 assert second["intent"] == "later"
166
167 def test_load_dump_round_trip_creation_and_intent(
168 self, repo: pathlib.Path
169 ) -> None:
170 from muse.cli.config import (
171 _dump_toml,
172 _load_config,
173 write_branch_meta,
174 )
175 from muse.core.paths import config_toml_path
176
177 write_branch_meta(
178 repo,
179 "feat/rt",
180 intent="keep me",
181 created_by='alice"bob',
182 created_by_kind="human",
183 created_at="2026-09-17T10:54:10.123456+00:00",
184 )
185 # Preserve a remote key via a second write that must not drop creation.
186 write_branch_meta(repo, "feat/rt", resumable=True)
187 # Inject remote by rewriting through load/dump helpers.
188 cp = config_toml_path(repo)
189 cfg = _load_config(cp)
190 cfg["branch"]["feat/rt"]["remote"] = "origin"
191 cp.write_text(_dump_toml(cfg), encoding="utf-8")
192 again = _load_config(cp)
193 sec = again["branch"]["feat/rt"]
194 assert sec["intent"] == "keep me"
195 assert sec["remote"] == "origin"
196 assert sec["created_by"] == 'alice"bob'
197 assert sec["created_by_kind"] == "human"
198 assert sec["created_at"] == "2026-09-17T10:54:10.123456+00:00"
199 with cp.open("rb") as f:
200 tomllib.load(f) # must remain parseable after escape
201
202 def test_move_branch_meta_relocates_dict(self, repo: pathlib.Path) -> None:
203 from muse.cli.config import (
204 move_branch_meta,
205 read_branch_meta,
206 write_branch_meta,
207 )
208
209 write_branch_meta(
210 repo,
211 "old/name",
212 intent="move me",
213 created_by="aaronrene",
214 created_by_kind="human",
215 created_at="2026-09-17T11:00:00+00:00",
216 )
217 move_branch_meta(repo, "old/name", "new/name")
218 assert read_branch_meta(repo, "old/name") == {}
219 moved = read_branch_meta(repo, "new/name")
220 assert moved["intent"] == "move me"
221 assert moved["created_by"] == "aaronrene"
222 assert moved["created_at"] == "2026-09-17T11:00:00+00:00"
223
224 def test_branch_created_from_meta_invalid_kind_null(self) -> None:
225 from muse.cli.commands.branch import _branch_created_from_meta
226
227 by, at = _branch_created_from_meta(
228 {
229 "created_by": "aaronrene",
230 "created_by_kind": "robot",
231 "created_at": "2026-09-17T11:00:00+00:00",
232 }
233 )
234 assert by is None
235 assert at == "2026-09-17T11:00:00+00:00"
236
237 def test_branch_created_from_meta_naive_at_null(self) -> None:
238 from muse.cli.commands.branch import _branch_created_from_meta
239
240 by, at = _branch_created_from_meta(
241 {
242 "created_by": "aaronrene",
243 "created_by_kind": "human",
244 "created_at": "2026-09-17T11:00:00",
245 }
246 )
247 assert by == {"handle": "aaronrene", "kind": "human"}
248 assert at is None
249
250
251 # ---------------------------------------------------------------------------
252 # Tier 2 — integration
253 # ---------------------------------------------------------------------------
254
255
256 class TestStampIntegration:
257 def test_plain_branch_stamps_toml_and_json(
258 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
259 ) -> None:
260 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
261 _set_user_handle(repo, "aaronrene")
262 env = _env(repo)
263 r = runner.invoke(cli, ["branch", "feat/x"], env=env)
264 assert r.exit_code == 0, r.output
265 sec = _toml_branch(repo, "feat/x")
266 assert "created_at" in sec
267 assert sec["created_by"] == "aaronrene"
268 assert sec["created_by_kind"] == "human"
269 entry = _entry(repo, "feat/x")
270 assert entry["branch_created_at"] is not None
271 assert entry["branch_created_by"] == {
272 "handle": "aaronrene",
273 "kind": "human",
274 }
275 # Tip authorship (#215) stays tip author, not BranchMeta handle object.
276 assert entry["created_by"] == "seed"
277 assert isinstance(entry["created_by"], str)
278
279
280 # ---------------------------------------------------------------------------
281 # Tier 3 — e2e
282 # ---------------------------------------------------------------------------
283
284
285 class TestCreateCopyRenameE2E:
286 def test_human_create_then_agent_tip_preserves_branch_creator(
287 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
288 ) -> None:
289 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
290 _set_user_handle(repo, "aaronrene")
291 env = _env(repo)
292 assert runner.invoke(cli, ["branch", "feat/stable"], env=env).exit_code == 0
293 assert runner.invoke(cli, ["checkout", "feat/stable"], env=env).exit_code == 0
294 (repo / "b.py").write_text("y = 2\n")
295 runner.invoke(cli, ["code", "add", "b.py"], env=env)
296 cr = runner.invoke(
297 cli,
298 [
299 "commit",
300 "-m",
301 "agent tip",
302 "--author",
303 "aaronrene",
304 "--agent-id",
305 "claude-code",
306 ],
307 env=env,
308 )
309 assert cr.exit_code == 0, cr.output
310 entry = _entry(repo, "feat/stable")
311 assert entry["created_by"] == "claude-code"
312 assert entry["branch_created_by"] == {
313 "handle": "aaronrene",
314 "kind": "human",
315 }
316
317 def test_copy_dest_gets_new_created_at(
318 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
319 ) -> None:
320 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
321 _set_user_handle(repo, "aaronrene")
322 env = _env(repo)
323 assert runner.invoke(cli, ["branch", "feat/src"], env=env).exit_code == 0
324 src_at = _entry(repo, "feat/src")["branch_created_at"]
325 assert src_at is not None
326 time.sleep(0.01)
327 assert runner.invoke(
328 cli, ["branch", "-c", "feat/src", "feat/dst"], env=env
329 ).exit_code == 0
330 dst = _entry(repo, "feat/dst")
331 src = _entry(repo, "feat/src")
332 assert dst["branch_created_at"] is not None
333 assert dst["branch_created_at"] >= src_at
334 assert src["branch_created_at"] == src_at
335 assert dst["branch_created_by"] == {
336 "handle": "aaronrene",
337 "kind": "human",
338 }
339
340 def test_force_copy_restamps_dest(
341 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
342 ) -> None:
343 """F15: force-copy deletes dest meta then stamps a new creation triple."""
344 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
345 _set_user_handle(repo, "aaronrene")
346 env = _env(repo)
347 assert runner.invoke(cli, ["branch", "feat/src2"], env=env).exit_code == 0
348 assert runner.invoke(cli, ["branch", "feat/old-dst"], env=env).exit_code == 0
349 old_at = _toml_branch(repo, "feat/old-dst")["created_at"]
350 time.sleep(0.01)
351 assert runner.invoke(
352 cli, ["branch", "-C", "feat/src2", "feat/old-dst"], env=env
353 ).exit_code == 0
354 new_at = _toml_branch(repo, "feat/old-dst")["created_at"]
355 assert new_at != old_at
356 assert _entry(repo, "feat/old-dst")["branch_created_by"] == {
357 "handle": "aaronrene",
358 "kind": "human",
359 }
360
361 def test_rename_moves_creation_triple(
362 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
363 ) -> None:
364 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
365 _set_user_handle(repo, "aaronrene")
366 env = _env(repo)
367 assert runner.invoke(cli, ["branch", "feat/before"], env=env).exit_code == 0
368 before = _entry(repo, "feat/before")
369 triple = (
370 before["branch_created_by"],
371 before["branch_created_at"],
372 )
373 assert runner.invoke(
374 cli, ["branch", "-m", "feat/before", "feat/after"], env=env
375 ).exit_code == 0
376 names = {b["name"] for b in _branch_json(repo)}
377 assert "feat/before" not in names
378 after = _entry(repo, "feat/after")
379 assert (after["branch_created_by"], after["branch_created_at"]) == triple
380 assert "feat/before" not in _toml_branch(repo, "feat/before") or True
381 assert "created_at" not in _toml_branch(repo, "feat/before")
382 assert _toml_branch(repo, "feat/after")["created_by"] == "aaronrene"
383
384
385 # ---------------------------------------------------------------------------
386 # Tier 4 — stress
387 # ---------------------------------------------------------------------------
388
389
390 class TestStress200:
391 def test_200_creates_list_keys_present(
392 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
393 ) -> None:
394 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
395 _set_user_handle(repo, "aaronrene")
396 env = _env(repo)
397 for i in range(200):
398 r = runner.invoke(cli, ["branch", f"stress-{i:03d}"], env=env)
399 assert r.exit_code == 0, r.output
400 data = _branch_json(repo)
401 assert len(data) == 201
402 for entry in data:
403 assert "branch_created_by" in entry
404 assert "branch_created_at" in entry
405 stamped = [b for b in data if b["name"].startswith("stress-")]
406 assert all(b["branch_created_at"] is not None for b in stamped)
407
408
409 # ---------------------------------------------------------------------------
410 # Tier 5 — data integrity
411 # ---------------------------------------------------------------------------
412
413
414 class TestDataIntegrity:
415 def test_intent_update_preserves_created_at_bytes(
416 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
417 ) -> None:
418 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
419 _set_user_handle(repo, "aaronrene")
420 env = _env(repo)
421 assert runner.invoke(cli, ["branch", "feat/di"], env=env).exit_code == 0
422 first = _toml_branch(repo, "feat/di")["created_at"]
423 assert runner.invoke(
424 cli, ["branch", "feat/di", "--intent", "update"], env=env
425 ).exit_code == 0
426 second = _toml_branch(repo, "feat/di")["created_at"]
427 assert second == first
428 assert isinstance(second, str)
429
430 def test_list_twice_identical_branch_created(
431 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
432 ) -> None:
433 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
434 _set_user_handle(repo, "aaronrene")
435 env = _env(repo)
436 assert runner.invoke(cli, ["branch", "feat/stable-list"], env=env).exit_code == 0
437 a = _entry(repo, "feat/stable-list")
438 b = _entry(repo, "feat/stable-list")
439 assert a["branch_created_by"] == b["branch_created_by"]
440 assert a["branch_created_at"] == b["branch_created_at"]
441
442 def test_corrupt_kind_json_null_listing_ok(
443 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
444 ) -> None:
445 from muse.core.paths import config_toml_path
446
447 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
448 env = _env(repo)
449 assert runner.invoke(cli, ["branch", "feat/corrupt"], env=env).exit_code == 0
450 cp = config_toml_path(repo)
451 text = cp.read_text(encoding="utf-8")
452 text = text.replace(
453 'created_by_kind = "human"',
454 'created_by_kind = "robot"',
455 1,
456 )
457 # When identity was None, inject a corrupt pair directly.
458 if 'created_by_kind = "robot"' not in text:
459 text += (
460 '\n[branch."feat/corrupt"]\n'
461 'created_by = "aaronrene"\n'
462 'created_by_kind = "robot"\n'
463 'created_at = "2026-09-17T12:00:00+00:00"\n'
464 )
465 cp.write_text(text, encoding="utf-8")
466 else:
467 cp.write_text(text, encoding="utf-8")
468 result = runner.invoke(cli, ["branch", "--json"], env=env)
469 assert result.exit_code == 0, result.output
470 entry = _entry(repo, "feat/corrupt")
471 assert entry["branch_created_by"] is None
472
473 def test_delete_removes_branch_section(
474 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
475 ) -> None:
476 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
477 _set_user_handle(repo, "aaronrene")
478 env = _env(repo)
479 assert runner.invoke(cli, ["branch", "feat/gone"], env=env).exit_code == 0
480 assert "created_at" in _toml_branch(repo, "feat/gone")
481 assert runner.invoke(cli, ["branch", "-D", "feat/gone"], env=env).exit_code == 0
482 assert _toml_branch(repo, "feat/gone") == {}
483
484
485 # ---------------------------------------------------------------------------
486 # Tier 6 — performance
487 # ---------------------------------------------------------------------------
488
489
490 class TestPerformance:
491 def test_200_branch_list_under_5s(
492 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
493 ) -> None:
494 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
495 _set_user_handle(repo, "aaronrene")
496 env = _env(repo)
497 for i in range(200):
498 runner.invoke(cli, ["branch", f"perf-{i:03d}"], env=env)
499 t0 = time.perf_counter()
500 data = _branch_json(repo)
501 elapsed = time.perf_counter() - t0
502 assert len(data) == 201
503 assert elapsed < 5.0, f"200-branch list took {elapsed:.2f}s (limit 5s)"
504
505
506 # ---------------------------------------------------------------------------
507 # Tier 7 — security
508 # ---------------------------------------------------------------------------
509
510
511 class TestSecurity:
512 def test_esc_in_agent_id_not_in_toml_or_json(
513 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
514 ) -> None:
515 monkeypatch.setenv("MUSE_AGENT_ID", "\x1b[31mbad-agent\x1b[0m")
516 env = {**_env(repo), "MUSE_AGENT_ID": "\x1b[31mbad-agent\x1b[0m"}
517 r = runner.invoke(cli, ["branch", "feat/sec"], env=env)
518 assert r.exit_code == 0, r.output
519 raw = (repo / ".muse" / "config.toml").read_text(encoding="utf-8")
520 assert "\x1b" not in raw
521 out = runner.invoke(cli, ["branch", "--json"], env=_env(repo)).output
522 assert "\x1b" not in out
523 entry = _entry(repo, "feat/sec")
524 # ESC bytes stripped; printable remnant remains after sanitize_provenance.
525 assert entry["branch_created_by"] == {
526 "handle": "[31mbad-agent[0m",
527 "kind": "agent",
528 }
529 assert "\x1b" not in entry["branch_created_by"]["handle"]
530
531 def test_no_key_material_in_output(
532 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
533 ) -> None:
534 monkeypatch.setenv("MUSE_AGENT_ID", "safe-agent")
535 monkeypatch.setenv("MUSE_AGENT_KEY", "super-secret-key-material")
536 env = {
537 **_env(repo),
538 "MUSE_AGENT_ID": "safe-agent",
539 "MUSE_AGENT_KEY": "super-secret-key-material",
540 }
541 assert runner.invoke(cli, ["branch", "feat/nokey"], env=env).exit_code == 0
542 out = runner.invoke(cli, ["branch", "--json"], env=env).output
543 assert "super-secret-key-material" not in out
544 assert "MUSE_AGENT_KEY" not in out
545 raw = (repo / ".muse" / "config.toml").read_text(encoding="utf-8")
546 assert "super-secret-key-material" not in raw
547
548 def test_quote_in_handle_round_trips_toml(
549 self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch
550 ) -> None:
551 from muse.core.paths import config_toml_path
552
553 monkeypatch.delenv("MUSE_AGENT_ID", raising=False)
554 env = _env(repo)
555 with mock.patch(
556 "muse.cli.config.get_config_value",
557 side_effect=lambda key, root=None: (
558 'alice"bob' if key == "user.handle" else None
559 ),
560 ):
561 r = runner.invoke(cli, ["branch", "feat/quote"], env=env)
562 assert r.exit_code == 0, r.output
563 with config_toml_path(repo).open("rb") as f:
564 data = tomllib.load(f)
565 assert data["branch"]["feat/quote"]["created_by"] == 'alice"bob'
566 entry = _entry(repo, "feat/quote")
567 assert entry["branch_created_by"] == {
568 "handle": 'alice"bob',
569 "kind": "human",
570 }
File History 1 commit
sha256:5eff0848db1d0748f3a195ca871a9275e7455d447690411ec2d83434e11baa65 feat(#216): persist write-once BranchMeta creation provenance Human minor 3 days ago