test_user_identity_migration.py
python
sha256:ff478cfdcdd4b7fd6de89cb68896601a981f945634463275ec333bd20ca36402
Merge branch 'dev' into main
Human
20 days ago
| 1 | """Tests for user identity config behaviour. |
| 2 | |
| 3 | User identity — handle, type, display_name, email — lives exclusively in |
| 4 | ~/.muse/identity.toml, keyed per hub hostname. config.toml has no [user] |
| 5 | section. |
| 6 | |
| 7 | Coverage |
| 8 | -------- |
| 9 | - _dump_toml does not emit a [user] section. |
| 10 | - set_config_value("user.*") raises ValueError / exits non-zero. |
| 11 | - get_config_value("user.type") reads from identity.toml. |
| 12 | - get_config_value("user.handle") reads from identity.toml. |
| 13 | - A [user] section in config.toml is never loaded (not part of the schema). |
| 14 | - muse config set user.type agent --json exits non-zero with structured error. |
| 15 | - muse config get user.type --json returns identity.toml value. |
| 16 | - commit --author falls back to identity.toml handle. |
| 17 | """ |
| 18 | |
| 19 | from __future__ import annotations |
| 20 | |
| 21 | import json |
| 22 | import os |
| 23 | import pathlib |
| 24 | import tempfile |
| 25 | from unittest.mock import patch |
| 26 | |
| 27 | import pytest |
| 28 | |
| 29 | from muse.core.paths import config_toml_path |
| 30 | from tests.cli_test_helper import CliRunner, InvokeResult |
| 31 | |
| 32 | runner = CliRunner() |
| 33 | |
| 34 | |
| 35 | # --------------------------------------------------------------------------- |
| 36 | # Helpers |
| 37 | # --------------------------------------------------------------------------- |
| 38 | |
| 39 | |
| 40 | def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult: |
| 41 | saved = os.getcwd() |
| 42 | try: |
| 43 | os.chdir(repo) |
| 44 | return runner.invoke(None, args) |
| 45 | finally: |
| 46 | os.chdir(saved) |
| 47 | |
| 48 | |
| 49 | def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: |
| 50 | saved = os.getcwd() |
| 51 | try: |
| 52 | os.chdir(tmp_path) |
| 53 | runner.invoke(None, ["init"]) |
| 54 | finally: |
| 55 | os.chdir(saved) |
| 56 | return tmp_path |
| 57 | |
| 58 | |
| 59 | def _write_config(repo: pathlib.Path, content: str) -> None: |
| 60 | config_toml_path(repo).write_text(content, encoding="utf-8") |
| 61 | |
| 62 | |
| 63 | def _write_identity(identity_path: pathlib.Path, content: str) -> None: |
| 64 | identity_path.parent.mkdir(parents=True, exist_ok=True) |
| 65 | identity_path.write_text(content, encoding="utf-8") |
| 66 | |
| 67 | |
| 68 | # --------------------------------------------------------------------------- |
| 69 | # 1. _dump_toml never writes [user] |
| 70 | # --------------------------------------------------------------------------- |
| 71 | |
| 72 | |
| 73 | class TestDumpTomlNoUserSection: |
| 74 | def test_dump_toml_never_emits_user_section(self) -> None: |
| 75 | from muse.cli.config import _dump_toml |
| 76 | |
| 77 | config = { |
| 78 | "user": {"handle": "gabriel", "type": "human", "email": "[email protected]"}, |
| 79 | "hub": {"url": "https://localhost:1337"}, |
| 80 | } |
| 81 | output = _dump_toml(config) |
| 82 | |
| 83 | assert "[user]" not in output |
| 84 | |
| 85 | def test_dump_toml_without_user_key_is_unchanged(self) -> None: |
| 86 | from muse.cli.config import _dump_toml |
| 87 | |
| 88 | config = {"hub": {"url": "https://localhost:1337"}} |
| 89 | output = _dump_toml(config) |
| 90 | |
| 91 | assert "[user]" not in output |
| 92 | assert "localhost:1337" in output |
| 93 | |
| 94 | def test_writing_config_does_not_persist_user_section( |
| 95 | self, tmp_path: pathlib.Path |
| 96 | ) -> None: |
| 97 | """Round-trip: writing config never leaves a [user] section on disk.""" |
| 98 | from muse.cli.config import write_branch_meta |
| 99 | import tomllib |
| 100 | |
| 101 | repo = _make_repo(tmp_path) |
| 102 | # Pre-seed a [user] section the old way. |
| 103 | _write_config(repo, '[user]\nhandle = "gabriel"\ntype = "agent"\n') |
| 104 | |
| 105 | # Trigger any config write (branch meta is the simplest). |
| 106 | write_branch_meta(repo, "feat/x", intent="test") |
| 107 | |
| 108 | cp = config_toml_path(repo) |
| 109 | with cp.open("rb") as f: |
| 110 | cfg = tomllib.load(f) |
| 111 | assert "user" not in cfg |
| 112 | |
| 113 | |
| 114 | # --------------------------------------------------------------------------- |
| 115 | # 2. set_config_value("user.*") is rejected |
| 116 | # --------------------------------------------------------------------------- |
| 117 | |
| 118 | |
| 119 | class TestSetUserConfigRejected: |
| 120 | def test_set_user_type_raises(self, tmp_path: pathlib.Path) -> None: |
| 121 | from muse.cli.config import set_config_value |
| 122 | |
| 123 | repo = _make_repo(tmp_path) |
| 124 | with pytest.raises((ValueError, SystemExit)): |
| 125 | set_config_value("user.type", "agent", repo) |
| 126 | |
| 127 | def test_set_user_handle_raises(self, tmp_path: pathlib.Path) -> None: |
| 128 | from muse.cli.config import set_config_value |
| 129 | |
| 130 | repo = _make_repo(tmp_path) |
| 131 | with pytest.raises((ValueError, SystemExit)): |
| 132 | set_config_value("user.handle", "someone", repo) |
| 133 | |
| 134 | def test_set_user_email_raises(self, tmp_path: pathlib.Path) -> None: |
| 135 | from muse.cli.config import set_config_value |
| 136 | |
| 137 | repo = _make_repo(tmp_path) |
| 138 | with pytest.raises((ValueError, SystemExit)): |
| 139 | set_config_value("user.email", "[email protected]", repo) |
| 140 | |
| 141 | def test_cli_set_user_type_exits_nonzero(self, tmp_path: pathlib.Path) -> None: |
| 142 | repo = _make_repo(tmp_path) |
| 143 | result = _invoke(repo, ["config", "set", "user.type", "agent", "--json"]) |
| 144 | assert result.exit_code != 0 |
| 145 | |
| 146 | def test_cli_set_user_type_json_error_mentions_auth( |
| 147 | self, tmp_path: pathlib.Path |
| 148 | ) -> None: |
| 149 | """The error message should direct the user to muse auth.""" |
| 150 | repo = _make_repo(tmp_path) |
| 151 | result = _invoke(repo, ["config", "set", "user.type", "agent", "--json"]) |
| 152 | assert result.exit_code != 0 |
| 153 | try: |
| 154 | data = json.loads(result.output.strip().splitlines()[-1]) |
| 155 | msg = data.get("message", "") |
| 156 | except (json.JSONDecodeError, IndexError): |
| 157 | msg = result.output + result.stderr |
| 158 | assert "auth" in msg.lower() |
| 159 | |
| 160 | |
| 161 | # --------------------------------------------------------------------------- |
| 162 | # 3. get_config_value("user.*") reads from identity.toml |
| 163 | # --------------------------------------------------------------------------- |
| 164 | |
| 165 | |
| 166 | class TestGetUserConfigFromIdentity: |
| 167 | def _identity_toml(self, handle: str, utype: str = "human") -> str: |
| 168 | return ( |
| 169 | f'["localhost:1337"]\n' |
| 170 | f'type = "{utype}"\n' |
| 171 | f'handle = "{handle}"\n' |
| 172 | f'algorithm = "ed25519"\n' |
| 173 | f'fingerprint = "sha256:abc123"\n' |
| 174 | f'hd_path = "m/0\'"\n' |
| 175 | ) |
| 176 | |
| 177 | def test_get_user_type_returns_identity_value( |
| 178 | self, tmp_path: pathlib.Path |
| 179 | ) -> None: |
| 180 | from muse.cli.config import get_config_value |
| 181 | from muse.core.identity import get_identity_path |
| 182 | |
| 183 | repo = _make_repo(tmp_path) |
| 184 | _write_config(repo, '[hub]\nurl = "https://localhost:1337"\n') |
| 185 | |
| 186 | with patch("muse.core.identity._IDENTITY_FILE", tmp_path / "identity.toml"): |
| 187 | _write_identity( |
| 188 | tmp_path / "identity.toml", self._identity_toml("gabriel", "human") |
| 189 | ) |
| 190 | result = get_config_value("user.type", repo) |
| 191 | |
| 192 | assert result == "human" |
| 193 | |
| 194 | def test_get_user_type_ignores_stale_config_value( |
| 195 | self, tmp_path: pathlib.Path |
| 196 | ) -> None: |
| 197 | """A stale type="agent" in config.toml must NOT be returned.""" |
| 198 | from muse.cli.config import get_config_value |
| 199 | |
| 200 | repo = _make_repo(tmp_path) |
| 201 | _write_config( |
| 202 | repo, |
| 203 | '[hub]\nurl = "https://localhost:1337"\n\n' |
| 204 | '[user]\nhandle = "gabriel"\ntype = "agent"\n', |
| 205 | ) |
| 206 | |
| 207 | with patch("muse.core.identity._IDENTITY_FILE", tmp_path / "identity.toml"): |
| 208 | _write_identity( |
| 209 | tmp_path / "identity.toml", self._identity_toml("gabriel", "human") |
| 210 | ) |
| 211 | result = get_config_value("user.type", repo) |
| 212 | |
| 213 | assert result == "human" |
| 214 | assert result != "agent" |
| 215 | |
| 216 | def test_get_user_handle_returns_identity_value( |
| 217 | self, tmp_path: pathlib.Path |
| 218 | ) -> None: |
| 219 | from muse.cli.config import get_config_value |
| 220 | |
| 221 | repo = _make_repo(tmp_path) |
| 222 | _write_config(repo, '[hub]\nurl = "https://localhost:1337"\n') |
| 223 | |
| 224 | with patch("muse.core.identity._IDENTITY_FILE", tmp_path / "identity.toml"): |
| 225 | _write_identity( |
| 226 | tmp_path / "identity.toml", self._identity_toml("gabriel") |
| 227 | ) |
| 228 | result = get_config_value("user.handle", repo) |
| 229 | |
| 230 | assert result == "gabriel" |
| 231 | |
| 232 | def test_get_user_type_none_when_no_identity( |
| 233 | self, tmp_path: pathlib.Path |
| 234 | ) -> None: |
| 235 | """No identity registered → get_config_value returns None, not a crash.""" |
| 236 | from muse.cli.config import get_config_value |
| 237 | |
| 238 | repo = _make_repo(tmp_path) |
| 239 | _write_config(repo, '[hub]\nurl = "https://localhost:1337"\n') |
| 240 | |
| 241 | with patch("muse.core.identity._IDENTITY_FILE", tmp_path / "identity.toml"): |
| 242 | result = get_config_value("user.type", repo) |
| 243 | |
| 244 | assert result is None |
| 245 | |
| 246 | |
| 247 | # --------------------------------------------------------------------------- |
| 248 | # 4. Stale [user] in config.toml is silently ignored |
| 249 | # --------------------------------------------------------------------------- |
| 250 | |
| 251 | |
| 252 | class TestUserSectionNotLoaded: |
| 253 | def test_load_config_ignores_user_section(self, tmp_path: pathlib.Path) -> None: |
| 254 | """_load_config never loads [user] — it is not part of the config schema.""" |
| 255 | from muse.cli.config import _load_config |
| 256 | |
| 257 | cp = config_toml_path(tmp_path) |
| 258 | cp.parent.mkdir(parents=True) |
| 259 | cp.write_text( |
| 260 | '[user]\nhandle = "gabriel"\ntype = "agent"\n\n' |
| 261 | '[hub]\nurl = "https://localhost:1337"\n', |
| 262 | encoding="utf-8", |
| 263 | ) |
| 264 | |
| 265 | config = _load_config(cp) |
| 266 | |
| 267 | assert "user" not in config |
| 268 | assert config.get("hub", {}).get("url") == "https://localhost:1337" |
| 269 | |
| 270 | |
| 271 | # --------------------------------------------------------------------------- |
| 272 | # 5. Commit author from identity.toml |
| 273 | # --------------------------------------------------------------------------- |
| 274 | |
| 275 | |
| 276 | class TestCommitAuthorFromIdentity: |
| 277 | def test_commit_uses_identity_handle_not_config( |
| 278 | self, tmp_path: pathlib.Path |
| 279 | ) -> None: |
| 280 | """commit --author must fall back to identity.toml handle, not config [user].""" |
| 281 | from muse.cli.config import get_config_value |
| 282 | |
| 283 | repo = _make_repo(tmp_path) |
| 284 | # config.toml has a wrong/stale handle |
| 285 | _write_config( |
| 286 | repo, |
| 287 | '[hub]\nurl = "https://localhost:1337"\n\n' |
| 288 | '[user]\nhandle = "wrong-handle"\n', |
| 289 | ) |
| 290 | |
| 291 | with patch("muse.core.identity._IDENTITY_FILE", tmp_path / "identity.toml"): |
| 292 | (tmp_path / "identity.toml").write_text( |
| 293 | '["localhost:1337"]\ntype = "human"\nhandle = "gabriel"\n' |
| 294 | 'algorithm = "ed25519"\nfingerprint = "sha256:abc"\nhd_path = "m/0\'"\n', |
| 295 | encoding="utf-8", |
| 296 | ) |
| 297 | handle = get_config_value("user.handle", repo) |
| 298 | |
| 299 | assert handle == "gabriel" |
| 300 | assert handle != "wrong-handle" |
File History
1 commit
sha256:ff478cfdcdd4b7fd6de89cb68896601a981f945634463275ec333bd20ca36402
Merge branch 'dev' into main
Human
20 days ago