test_commit_sign_config.py
python
sha256:06dba78c2a78e251b580422dd1fd547f3c8357ff18f7709a860873b2d24dbbbf
chore: bump version to 0.2.0rc14
Sonnet 4.6
patch
15 hours 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
1 commit
sha256:06dba78c2a78e251b580422dd1fd547f3c8357ff18f7709a860873b2d24dbbbf
chore: bump version to 0.2.0rc14
Sonnet 4.6
patch
15 hours ago