gabriel / muse public
test_user_identity_migration.py python
300 lines 10.3 KB
Raw
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