"""TDD tests for commit.sign config setting. Covers: - get_config_value("commit.sign") reads from [commit] section in config.toml - set_config_value("commit.sign", "true") writes the setting - muse commit auto-signs when commit.sign = true, without --sign flag - muse commit does NOT sign when commit.sign is absent or false - muse config set commit.sign true / false round-trips via CLI - muse config get commit.sign reads the value back """ from __future__ import annotations import json import os import pathlib import pytest from muse.core.paths import config_toml_path, muse_dir from muse.core.types import fake_id from muse.core.refs import get_head_commit_id from muse.core.commits import read_commit from tests.cli_test_helper import CliRunner runner = CliRunner() _TEST_MNEMONIC = ( "abandon abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon about" ) def _inject_signing_key(monkeypatch: pytest.MonkeyPatch, account: int = 1) -> None: """Inject a deterministic Ed25519 sub-seed via MUSE_AGENT_KEY_FD.""" from muse.core.bip39 import mnemonic_to_seed from muse.core.hdkeys import DOMAIN_IDENTITY, derive_agent_sub_seed seed = mnemonic_to_seed(_TEST_MNEMONIC) sub_seed = derive_agent_sub_seed(seed, domain=DOMAIN_IDENTITY, agent_id=account) r_fd, w_fd = os.pipe() os.write(w_fd, sub_seed) os.close(w_fd) monkeypatch.setenv("MUSE_AGENT_KEY_FD", str(r_fd)) # ── Helpers ────────────────────────────────────────────────────────────────── def _invoke(repo: pathlib.Path, args: list[str]) -> "InvokeResult": saved = os.getcwd() try: os.chdir(repo) return runner.invoke(None, args) finally: os.chdir(saved) def _init_repo(tmp_path: pathlib.Path) -> pathlib.Path: result = _invoke(tmp_path, ["init"]) assert result.exit_code == 0, result.output return tmp_path def _make_file(repo: pathlib.Path, name: str = "a.txt") -> pathlib.Path: p = repo / name p.write_text("hello") _invoke(repo, ["code", "add", name]) return p def _write_hub_section(repo: pathlib.Path, url: str = "https://localhost:1337") -> None: """Add a [hub] url so identity lookup succeeds (needed for author resolution).""" cp = config_toml_path(repo) text = cp.read_text() if cp.exists() else "" if "[hub]" not in text: cp.write_text(text + f'\n[hub]\nurl = "{url}"\n') # ── Unit: get_config_value("commit.sign") ──────────────────────────────────── class TestGetCommitSign: def test_returns_none_when_section_absent(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) from muse.cli.config import get_config_value assert get_config_value("commit.sign", tmp_path) is None def test_returns_true_string_when_set(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) cp = config_toml_path(tmp_path) cp.write_text(cp.read_text() + '\n[commit]\nsign = true\n') from muse.cli.config import get_config_value assert get_config_value("commit.sign", tmp_path) == "true" def test_returns_false_string_when_set_false(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) cp = config_toml_path(tmp_path) cp.write_text(cp.read_text() + '\n[commit]\nsign = false\n') from muse.cli.config import get_config_value assert get_config_value("commit.sign", tmp_path) == "false" # ── Unit: set_config_value("commit.sign") ──────────────────────────────────── class TestSetCommitSign: def test_set_true_writes_toml(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) from muse.cli.config import set_config_value, get_config_value set_config_value("commit.sign", "true", tmp_path) assert get_config_value("commit.sign", tmp_path) == "true" def test_set_false_writes_toml(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) from muse.cli.config import set_config_value, get_config_value set_config_value("commit.sign", "false", tmp_path) assert get_config_value("commit.sign", tmp_path) == "false" def test_set_invalid_value_raises(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) from muse.cli.config import set_config_value with pytest.raises(ValueError, match="must be 'true' or 'false'"): set_config_value("commit.sign", "yes", tmp_path) def test_set_unknown_commit_subkey_raises(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) from muse.cli.config import set_config_value with pytest.raises(ValueError, match=r"Unknown \[commit\]"): set_config_value("commit.bogus", "true", tmp_path) # ── CLI: muse config set / get commit.sign ─────────────────────────────────── class TestCliCommitSign: def test_cli_set_and_get_true(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) r = _invoke(tmp_path, ["config", "set", "commit.sign", "true"]) assert r.exit_code == 0 r = _invoke(tmp_path, ["config", "get", "commit.sign"]) assert r.exit_code == 0 assert "true" in r.output def test_cli_set_false(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) _invoke(tmp_path, ["config", "set", "commit.sign", "true"]) r = _invoke(tmp_path, ["config", "set", "commit.sign", "false"]) assert r.exit_code == 0 r = _invoke(tmp_path, ["config", "get", "commit.sign"]) assert "false" in r.output def test_cli_set_invalid_exits_nonzero(self, tmp_path: pathlib.Path) -> None: _init_repo(tmp_path) r = _invoke(tmp_path, ["config", "set", "commit.sign", "maybe"]) assert r.exit_code != 0 # ── Integration: auto-sign on commit when config is set ────────────────────── class TestAutoSignFromConfig: def test_commit_signed_when_config_true(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None: """commit.sign = true in config → commit carries signer_public_key without --sign flag.""" _init_repo(tmp_path) _write_hub_section(tmp_path) from muse.cli.config import set_config_value set_config_value("commit.sign", "true", tmp_path) _make_file(tmp_path) _inject_signing_key(monkeypatch) # inject right before commit so fd is fresh r = _invoke(tmp_path, ["commit", "-m", "auto-signed", "--agent-id", "claude-code", "--model-id", "claude-sonnet-4-6", "--json"]) assert r.exit_code == 0 data = json.loads(r.output) assert data.get("signer_public_key", "") != "", "expected signer_public_key to be set" def test_commit_not_signed_when_config_false(self, tmp_path: pathlib.Path) -> None: """commit.sign = false → no signing even if identity available.""" _init_repo(tmp_path) _write_hub_section(tmp_path) from muse.cli.config import set_config_value set_config_value("commit.sign", "false", tmp_path) _make_file(tmp_path) r = _invoke(tmp_path, ["commit", "-m", "unsigned", "--agent-id", "claude-code", "--model-id", "claude-sonnet-4-6", "--json"]) assert r.exit_code == 0 data = json.loads(r.output) assert data.get("signer_public_key", "") == "", "expected no signer_public_key" def test_commit_not_signed_when_config_absent(self, tmp_path: pathlib.Path) -> None: """No commit.sign config → default unsigned (backward compat).""" _init_repo(tmp_path) _make_file(tmp_path) r = _invoke(tmp_path, ["commit", "-m", "no config", "--agent-id", "claude-code", "--model-id", "claude-sonnet-4-6", "--json"]) assert r.exit_code == 0 data = json.loads(r.output) assert data.get("signer_public_key", "") == "" def test_explicit_flag_overrides_config_false(self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None: """--sign flag always wins even when config says false.""" _init_repo(tmp_path) _write_hub_section(tmp_path) from muse.cli.config import set_config_value set_config_value("commit.sign", "false", tmp_path) _make_file(tmp_path) _inject_signing_key(monkeypatch) # inject right before commit so fd is fresh r = _invoke(tmp_path, ["commit", "-m", "explicit sign", "--agent-id", "claude-code", "--model-id", "claude-sonnet-4-6", "--sign", "--json"]) assert r.exit_code == 0 data = json.loads(r.output) assert data.get("signer_public_key", "") != ""