gabriel / muse public
test_commit_sign_config.py python
207 lines 8.8 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 22 days ago
1 """TDD tests for commit.sign config setting.
2
3 Covers:
4 - get_config_value("commit.sign") reads from [commit] section in config.toml
5 - set_config_value("commit.sign", "true") writes the setting
6 - muse commit auto-signs when commit.sign = true, without --sign flag
7 - muse commit does NOT sign when commit.sign is absent or false
8 - muse config set commit.sign true / false round-trips via CLI
9 - muse config get commit.sign reads the value back
10 """
11
12 from __future__ import annotations
13
14 import json
15 import os
16 import pathlib
17
18 import pytest
19
20 from muse.core.paths import config_toml_path, muse_dir
21 from muse.core.types import fake_id
22 from muse.core.refs import get_head_commit_id
23 from muse.core.commits import read_commit
24 from tests.cli_test_helper import CliRunner
25
26 runner = CliRunner()
27
28 _TEST_MNEMONIC = (
29 "abandon abandon abandon abandon abandon abandon abandon abandon "
30 "abandon abandon abandon about"
31 )
32
33
34 def _inject_signing_key(monkeypatch: pytest.MonkeyPatch, account: int = 1) -> None:
35 """Inject a deterministic Ed25519 sub-seed via MUSE_AGENT_KEY_FD."""
36 from muse.core.bip39 import mnemonic_to_seed
37 from muse.core.hdkeys import DOMAIN_IDENTITY, derive_agent_sub_seed
38 seed = mnemonic_to_seed(_TEST_MNEMONIC)
39 sub_seed = derive_agent_sub_seed(seed, domain=DOMAIN_IDENTITY, agent_id=account)
40 r_fd, w_fd = os.pipe()
41 os.write(w_fd, sub_seed)
42 os.close(w_fd)
43 monkeypatch.setenv("MUSE_AGENT_KEY_FD", str(r_fd))
44
45
46 # ── Helpers ──────────────────────────────────────────────────────────────────
47
48
49 def _invoke(repo: pathlib.Path, args: list[str]) -> "InvokeResult":
50 saved = os.getcwd()
51 try:
52 os.chdir(repo)
53 return runner.invoke(None, args)
54 finally:
55 os.chdir(saved)
56
57
58 def _init_repo(tmp_path: pathlib.Path) -> pathlib.Path:
59 result = _invoke(tmp_path, ["init"])
60 assert result.exit_code == 0, result.output
61 return tmp_path
62
63
64 def _make_file(repo: pathlib.Path, name: str = "a.txt") -> pathlib.Path:
65 p = repo / name
66 p.write_text("hello")
67 _invoke(repo, ["code", "add", name])
68 return p
69
70
71 def _write_hub_section(repo: pathlib.Path, url: str = "https://localhost:1337") -> None:
72 """Add a [hub] url so identity lookup succeeds (needed for author resolution)."""
73 cp = config_toml_path(repo)
74 text = cp.read_text() if cp.exists() else ""
75 if "[hub]" not in text:
76 cp.write_text(text + f'\n[hub]\nurl = "{url}"\n')
77
78
79 # ── Unit: get_config_value("commit.sign") ────────────────────────────────────
80
81
82 class TestGetCommitSign:
83 def test_returns_none_when_section_absent(self, tmp_path: pathlib.Path) -> None:
84 _init_repo(tmp_path)
85 from muse.cli.config import get_config_value
86 assert get_config_value("commit.sign", tmp_path) is None
87
88 def test_returns_true_string_when_set(self, tmp_path: pathlib.Path) -> None:
89 _init_repo(tmp_path)
90 cp = config_toml_path(tmp_path)
91 cp.write_text(cp.read_text() + '\n[commit]\nsign = true\n')
92 from muse.cli.config import get_config_value
93 assert get_config_value("commit.sign", tmp_path) == "true"
94
95 def test_returns_false_string_when_set_false(self, tmp_path: pathlib.Path) -> None:
96 _init_repo(tmp_path)
97 cp = config_toml_path(tmp_path)
98 cp.write_text(cp.read_text() + '\n[commit]\nsign = false\n')
99 from muse.cli.config import get_config_value
100 assert get_config_value("commit.sign", tmp_path) == "false"
101
102
103 # ── Unit: set_config_value("commit.sign") ────────────────────────────────────
104
105
106 class TestSetCommitSign:
107 def test_set_true_writes_toml(self, tmp_path: pathlib.Path) -> None:
108 _init_repo(tmp_path)
109 from muse.cli.config import set_config_value, get_config_value
110 set_config_value("commit.sign", "true", tmp_path)
111 assert get_config_value("commit.sign", tmp_path) == "true"
112
113 def test_set_false_writes_toml(self, tmp_path: pathlib.Path) -> None:
114 _init_repo(tmp_path)
115 from muse.cli.config import set_config_value, get_config_value
116 set_config_value("commit.sign", "false", tmp_path)
117 assert get_config_value("commit.sign", tmp_path) == "false"
118
119 def test_set_invalid_value_raises(self, tmp_path: pathlib.Path) -> None:
120 _init_repo(tmp_path)
121 from muse.cli.config import set_config_value
122 with pytest.raises(ValueError, match="must be 'true' or 'false'"):
123 set_config_value("commit.sign", "yes", tmp_path)
124
125 def test_set_unknown_commit_subkey_raises(self, tmp_path: pathlib.Path) -> None:
126 _init_repo(tmp_path)
127 from muse.cli.config import set_config_value
128 with pytest.raises(ValueError, match=r"Unknown \[commit\]"):
129 set_config_value("commit.bogus", "true", tmp_path)
130
131
132 # ── CLI: muse config set / get commit.sign ───────────────────────────────────
133
134
135 class TestCliCommitSign:
136 def test_cli_set_and_get_true(self, tmp_path: pathlib.Path) -> None:
137 _init_repo(tmp_path)
138 r = _invoke(tmp_path, ["config", "set", "commit.sign", "true"])
139 assert r.exit_code == 0
140 r = _invoke(tmp_path, ["config", "get", "commit.sign"])
141 assert r.exit_code == 0
142 assert "true" in r.output
143
144 def test_cli_set_false(self, tmp_path: pathlib.Path) -> None:
145 _init_repo(tmp_path)
146 _invoke(tmp_path, ["config", "set", "commit.sign", "true"])
147 r = _invoke(tmp_path, ["config", "set", "commit.sign", "false"])
148 assert r.exit_code == 0
149 r = _invoke(tmp_path, ["config", "get", "commit.sign"])
150 assert "false" in r.output
151
152 def test_cli_set_invalid_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
153 _init_repo(tmp_path)
154 r = _invoke(tmp_path, ["config", "set", "commit.sign", "maybe"])
155 assert r.exit_code != 0
156
157
158 # ── Integration: auto-sign on commit when config is set ──────────────────────
159
160
161 class TestAutoSignFromConfig:
162 def test_commit_signed_when_config_true(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
163 """commit.sign = true in config → commit carries signer_public_key without --sign flag."""
164 _init_repo(tmp_path)
165 _write_hub_section(tmp_path)
166 from muse.cli.config import set_config_value
167 set_config_value("commit.sign", "true", tmp_path)
168 _make_file(tmp_path)
169 _inject_signing_key(monkeypatch) # inject right before commit so fd is fresh
170 r = _invoke(tmp_path, ["commit", "-m", "auto-signed", "--agent-id", "claude-code", "--model-id", "claude-sonnet-4-6", "--json"])
171 assert r.exit_code == 0
172 data = json.loads(r.output)
173 assert data.get("signer_public_key", "") != "", "expected signer_public_key to be set"
174
175 def test_commit_not_signed_when_config_false(self, tmp_path: pathlib.Path) -> None:
176 """commit.sign = false → no signing even if identity available."""
177 _init_repo(tmp_path)
178 _write_hub_section(tmp_path)
179 from muse.cli.config import set_config_value
180 set_config_value("commit.sign", "false", tmp_path)
181 _make_file(tmp_path)
182 r = _invoke(tmp_path, ["commit", "-m", "unsigned", "--agent-id", "claude-code", "--model-id", "claude-sonnet-4-6", "--json"])
183 assert r.exit_code == 0
184 data = json.loads(r.output)
185 assert data.get("signer_public_key", "") == "", "expected no signer_public_key"
186
187 def test_commit_not_signed_when_config_absent(self, tmp_path: pathlib.Path) -> None:
188 """No commit.sign config → default unsigned (backward compat)."""
189 _init_repo(tmp_path)
190 _make_file(tmp_path)
191 r = _invoke(tmp_path, ["commit", "-m", "no config", "--agent-id", "claude-code", "--model-id", "claude-sonnet-4-6", "--json"])
192 assert r.exit_code == 0
193 data = json.loads(r.output)
194 assert data.get("signer_public_key", "") == ""
195
196 def test_explicit_flag_overrides_config_false(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
197 """--sign flag always wins even when config says false."""
198 _init_repo(tmp_path)
199 _write_hub_section(tmp_path)
200 from muse.cli.config import set_config_value
201 set_config_value("commit.sign", "false", tmp_path)
202 _make_file(tmp_path)
203 _inject_signing_key(monkeypatch) # inject right before commit so fd is fresh
204 r = _invoke(tmp_path, ["commit", "-m", "explicit sign", "--agent-id", "claude-code", "--model-id", "claude-sonnet-4-6", "--sign", "--json"])
205 assert r.exit_code == 0
206 data = json.loads(r.output)
207 assert data.get("signer_public_key", "") != ""
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 22 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 23 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 29 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 30 days ago