gabriel / muse public
test_auth_hd_persistence.py python
578 lines 24.5 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 138 days ago
1 """HD identity persistence tests.
2
3 Design invariants under test
4 ------------------------------
5 - Mnemonic lives in the OS keychain (or is ephemeral in CI); it is NEVER
6 written to ``identity.toml``.
7 - ``hd_path`` and other identity fields ARE written to ``identity.toml``.
8 - There is no ``key_source`` field — all keys are HD-derived; the distinction
9 is meaningless and the field does not exist.
10 - ``muse auth keygen`` always uses HD derivation.
11 - ``muse auth register`` must carry forward ``hd_path`` from the existing
12 provisional entry.
13
14 Test categories
15 ---------------
16 1. unit : _load_all field coverage; _dump_identity field coverage
17 2. integration : run_keygen writes hd_path; mnemonic absent from TOML
18 3. e2e : keygen → register field preservation round-trip
19 4. security : mnemonic absent from identity.toml and from JSON stdout
20 5. data_integrity: HD fields survive repeated register + TOML escaping
21 6. docstrings : public API has docstrings (smoke)
22 7. performance : save+load under 100 ms; keygen under 3 s
23 8. stress : 10 re-registrations; 20 successive saves without corruption
24 """
25
26 from __future__ import annotations
27
28 import json
29 import pathlib
30 import time
31 import tomllib
32
33 import pytest
34 from tests.cli_test_helper import CliRunner
35
36 from muse.core import keypair as kp_module
37
38 cli = None
39 runner = CliRunner()
40
41 # Use a non-standard port that will never match gabriel's real keychain entries.
42 # load_identity() injects the mnemonic from the OS keychain for the exact hub
43 # URL — using a port that has never been registered ensures clean isolation.
44 HUB = "http://localhost:19007"
45 HOSTNAME = "localhost:19007"
46 FAKE_MNEMONIC = (
47 "abandon abandon abandon abandon abandon abandon "
48 "abandon abandon abandon abandon abandon about"
49 )
50 FAKE_HD_PATH = "m/1075233755'/0'/0'/0'/0'/0'"
51 FAKE_FINGERPRINT = "a" * 64
52 FAKE_HANDLE = "gabriel"
53
54
55 # ---------------------------------------------------------------------------
56 # Shared fixtures
57 # ---------------------------------------------------------------------------
58
59
60 def _patch_home(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path:
61 """Redirect pathlib.Path.home() and module-level constants to a temp dir."""
62 fake_home = tmp_path / "home"
63 fake_home.mkdir(parents=True, exist_ok=True)
64 monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home))
65 monkeypatch.setattr(kp_module, "_KEYS_DIR", fake_home / ".muse" / "keys")
66 from muse.core import identity as id_module
67 monkeypatch.setattr(id_module, "_IDENTITY_DIR", fake_home / ".muse")
68 monkeypatch.setattr(id_module, "_IDENTITY_FILE", fake_home / ".muse" / "identity.toml")
69 return fake_home
70
71
72 def _mock_hub(monkeypatch: pytest.MonkeyPatch, handle: str = FAKE_HANDLE) -> None:
73 """Patch _json_post_raw to simulate a successful hub challenge-response."""
74 import muse.cli.commands.auth as auth_mod
75 challenge = {"challengeToken": "deadbeef" * 8, "isNewKey": True, "algorithm": "ed25519"}
76 verify = {"handle": handle, "identityId": "id-123", "isNewIdentity": False, "authMethod": "ed25519"}
77 monkeypatch.setattr(
78 auth_mod,
79 "_json_post_raw",
80 lambda base, path, payload: challenge if "challenge" in path else verify,
81 )
82
83
84 def _mock_bip39(monkeypatch: pytest.MonkeyPatch) -> None:
85 """Patch only mnemonic *generation* to avoid OS entropy.
86
87 ``mnemonic_to_seed`` and ``generate_hd_keypair`` run for real so that:
88 - A valid PEM key is written to disk (run_register can sign with it).
89 - The fingerprint stored in identity.toml is the genuine derived value.
90 - SLIP-0010 derivation is exercised, not bypassed.
91 """
92 import muse.core.bip39 as bip39_mod
93 monkeypatch.setattr(bip39_mod, "generate_mnemonic", lambda **kw: FAKE_MNEMONIC)
94
95
96 def _identity_file(fake_home: pathlib.Path) -> pathlib.Path:
97 return fake_home / ".muse" / "identity.toml"
98
99
100 def _read_identity_toml(fake_home: pathlib.Path) -> dict:
101 ifile = _identity_file(fake_home)
102 with ifile.open("rb") as fh:
103 return tomllib.load(fh)
104
105
106 # ---------------------------------------------------------------------------
107 # 1. Unit — _load_all field coverage
108 # ---------------------------------------------------------------------------
109
110
111 class TestLoadAllHdFields:
112 """_load_all must parse hd_path and provisioned_by from TOML.
113 Mnemonic is keychain-only — must NOT be loaded from TOML.
114 key_source does not exist — derivation is always HD."""
115
116 def _write(self, path: pathlib.Path, text: str) -> None:
117 path.write_text(text, encoding="utf-8")
118
119 def test_mnemonic_not_loaded_from_toml(self, tmp_path: pathlib.Path) -> None:
120 """Even when mnemonic appears in a legacy TOML file, _load_all must not surface it."""
121 p = tmp_path / "identity.toml"
122 self._write(p, f'["{HOSTNAME}"]\ntype="human"\nhandle="{FAKE_HANDLE}"\n'
123 f'key_path="/tmp/k.pem"\nalgorithm="ed25519"\n'
124 f'fingerprint="{FAKE_FINGERPRINT}"\nmnemonic="{FAKE_MNEMONIC}"\n')
125 from muse.core.identity import _load_all
126 entry = _load_all(p)[HOSTNAME]
127 assert "mnemonic" not in entry, (
128 "mnemonic must NOT be surfaced from TOML — it lives in the keychain"
129 )
130
131 def test_loads_hd_path(self, tmp_path: pathlib.Path) -> None:
132 p = tmp_path / "identity.toml"
133 self._write(p, f'["{HOSTNAME}"]\ntype="human"\nhandle="{FAKE_HANDLE}"\n'
134 f'key_path="/tmp/k.pem"\nalgorithm="ed25519"\n'
135 f'fingerprint="{FAKE_FINGERPRINT}"\nhd_path="{FAKE_HD_PATH}"\n')
136 from muse.core.identity import _load_all
137 assert _load_all(p)[HOSTNAME]["hd_path"] == FAKE_HD_PATH
138
139 def test_no_key_source_field_exists(self, tmp_path: pathlib.Path) -> None:
140 """key_source is not a valid field — derivation is always HD."""
141 p = tmp_path / "identity.toml"
142 self._write(p, f'["{HOSTNAME}"]\ntype="human"\nhandle="{FAKE_HANDLE}"\n'
143 f'key_path="/tmp/k.pem"\nalgorithm="ed25519"\n'
144 f'fingerprint="{FAKE_FINGERPRINT}"\nhd_path="{FAKE_HD_PATH}"\n')
145 from muse.core.identity import _load_all
146 entry = _load_all(p)[HOSTNAME]
147 assert "key_source" not in entry
148
149 def test_entry_without_hd_fields_has_no_hd_keys(self, tmp_path: pathlib.Path) -> None:
150 p = tmp_path / "identity.toml"
151 self._write(p, f'["{HOSTNAME}"]\ntype="human"\nhandle="{FAKE_HANDLE}"\n'
152 f'key_path="/tmp/k.pem"\nalgorithm="ed25519"\n'
153 f'fingerprint="{FAKE_FINGERPRINT}"\n')
154 from muse.core.identity import _load_all
155 entry = _load_all(p)[HOSTNAME]
156 assert "mnemonic" not in entry
157 assert "hd_path" not in entry
158 assert "key_source" not in entry
159
160 def test_loads_provisioned_by(self, tmp_path: pathlib.Path) -> None:
161 p = tmp_path / "identity.toml"
162 self._write(p, f'["{HOSTNAME}#agent"]\ntype="agent"\nhandle="agent"\n'
163 f'key_path="/tmp/k.pem"\nalgorithm="ed25519"\n'
164 f'fingerprint="{FAKE_FINGERPRINT}"\nprovisioned_by="alice"\n')
165 from muse.core.identity import _load_all
166 entry = _load_all(p)[f"{HOSTNAME}#agent"]
167 assert entry.get("provisioned_by") == "alice"
168
169
170 # ---------------------------------------------------------------------------
171 # Unit — _dump_identity field coverage
172 # ---------------------------------------------------------------------------
173
174
175 class TestDumpIdentityHdFields:
176 """_dump_identity must write hd_path; mnemonic and key_source are never written."""
177
178 def test_hd_path_serialised(self) -> None:
179 from muse.core.identity import _dump_identity
180 entry = {
181 "type": "human", "handle": FAKE_HANDLE,
182 "key_path": "/tmp/k.pem", "algorithm": "ed25519",
183 "fingerprint": FAKE_FINGERPRINT,
184 "hd_path": FAKE_HD_PATH,
185 }
186 toml_text = _dump_identity({HOSTNAME: entry})
187 assert "hd_path" in toml_text
188 assert FAKE_HD_PATH in toml_text
189
190 def test_mnemonic_never_serialised(self) -> None:
191 """_dump_identity must not write mnemonic even if the entry dict contains it."""
192 from muse.core.identity import _dump_identity
193 entry = {
194 "type": "human", "handle": FAKE_HANDLE,
195 "key_path": "/tmp/k.pem", "algorithm": "ed25519",
196 "fingerprint": FAKE_FINGERPRINT,
197 "hd_path": FAKE_HD_PATH,
198 "mnemonic": FAKE_MNEMONIC, # must be stripped
199 }
200 toml_text = _dump_identity({HOSTNAME: entry})
201 assert "mnemonic" not in toml_text
202 assert FAKE_MNEMONIC not in toml_text
203
204 def test_key_source_never_serialised(self) -> None:
205 """key_source is not a valid field and must never appear in TOML."""
206 from muse.core.identity import _dump_identity
207 entry = {
208 "type": "human", "handle": FAKE_HANDLE,
209 "key_path": "/tmp/k.pem", "algorithm": "ed25519",
210 "fingerprint": FAKE_FINGERPRINT,
211 }
212 toml_text = _dump_identity({HOSTNAME: entry})
213 assert "key_source" not in toml_text
214
215 def test_hd_path_round_trips(self, tmp_path: pathlib.Path) -> None:
216 """hd_path survives _dump_identity → write → _load_all."""
217 from muse.core.identity import _dump_identity, _load_all
218 identities = {HOSTNAME: {
219 "type": "human", "handle": FAKE_HANDLE,
220 "key_path": "/tmp/k.pem", "algorithm": "ed25519",
221 "fingerprint": FAKE_FINGERPRINT,
222 "hd_path": FAKE_HD_PATH,
223 }}
224 p = tmp_path / "identity.toml"
225 p.write_text(_dump_identity(identities), encoding="utf-8")
226 entry = _load_all(p)[HOSTNAME]
227 assert entry["hd_path"] == FAKE_HD_PATH
228 assert "mnemonic" not in entry
229 assert "key_source" not in entry
230
231
232 # ---------------------------------------------------------------------------
233 # 2. Integration — run_keygen writes HD fields to identity.toml
234 # ---------------------------------------------------------------------------
235
236
237 class TestKeygenWritesIdentity:
238 """run_keygen must persist hd_path to identity.toml.
239 Mnemonic and key_source must NOT appear in identity.toml."""
240
241 def _run(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, extra_args=None):
242 fake_home = _patch_home(monkeypatch, tmp_path)
243 _mock_bip39(monkeypatch)
244 args = ["auth", "keygen", "--hub", HUB] + (extra_args or [])
245 result = runner.invoke(cli, args, catch_exceptions=False)
246 return result, fake_home
247
248 def test_identity_toml_created(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> None:
249 result, fake_home = self._run(monkeypatch, tmp_path)
250 assert result.exit_code == 0, result.output
251 assert _identity_file(fake_home).exists()
252
253 def test_hd_path_written(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> None:
254 _, fake_home = self._run(monkeypatch, tmp_path)
255 data = _read_identity_toml(fake_home)
256 assert data[HOSTNAME].get("hd_path", "").startswith("m/")
257
258 def test_mnemonic_not_in_toml(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> None:
259 """After keygen, identity.toml must contain NO mnemonic field."""
260 _, fake_home = self._run(monkeypatch, tmp_path)
261 data = _read_identity_toml(fake_home)
262 assert "mnemonic" not in data[HOSTNAME], (
263 "mnemonic leaked into identity.toml — must live in keychain only"
264 )
265 raw_text = _identity_file(fake_home).read_text()
266 assert FAKE_MNEMONIC not in raw_text
267
268 def test_key_source_not_in_toml(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> None:
269 """key_source is not a field — must not appear in identity.toml."""
270 _, fake_home = self._run(monkeypatch, tmp_path)
271 data = _read_identity_toml(fake_home)
272 assert "key_source" not in data[HOSTNAME]
273
274 def test_force_overwrites_writes_hd_path(
275 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
276 ) -> None:
277 """--force overwrites the key; hd_path must be present in the new entry."""
278 self._run(monkeypatch, tmp_path)
279 result, fake_home = self._run(monkeypatch, tmp_path, extra_args=["--force"])
280 assert result.exit_code == 0, result.output
281 data = _read_identity_toml(fake_home)
282 assert data[HOSTNAME].get("hd_path", "").startswith("m/")
283
284
285 # ---------------------------------------------------------------------------
286 # 3. Security — mnemonic never in JSON stdout
287 # ---------------------------------------------------------------------------
288
289
290 class TestMnemonicNeverInJsonObject:
291 """Mnemonic must not appear in any JSON stdout object."""
292
293 def test_keygen_json_no_mnemonic_key(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> None:
294 _patch_home(monkeypatch, tmp_path)
295 _mock_bip39(monkeypatch)
296 result = runner.invoke(
297 cli, ["auth", "keygen", "--hub", HUB, "--json"],
298 catch_exceptions=False,
299 )
300 assert result.exit_code == 0
301 json_line = next(
302 (l for l in result.output.splitlines() if l.startswith("{")), None
303 )
304 assert json_line is not None, "No JSON in output"
305 obj = json.loads(json_line)
306 assert "mnemonic" not in obj
307
308 def test_keygen_json_mnemonic_content_absent(
309 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
310 ) -> None:
311 """The mnemonic phrase itself must not appear anywhere in the JSON dump."""
312 _patch_home(monkeypatch, tmp_path)
313 _mock_bip39(monkeypatch)
314 result = runner.invoke(
315 cli, ["auth", "keygen", "--hub", HUB, "--json"],
316 catch_exceptions=False,
317 )
318 json_line = next(
319 (l for l in result.output.splitlines() if l.startswith("{")), None
320 )
321 obj = json.loads(json_line)
322 assert FAKE_MNEMONIC not in json.dumps(obj)
323
324 def test_keygen_json_has_mnemonic_word_count(
325 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
326 ) -> None:
327 """JSON has mnemonic_word_count (count, not content); value equals actual word count."""
328 _patch_home(monkeypatch, tmp_path)
329 _mock_bip39(monkeypatch)
330 result = runner.invoke(
331 cli, ["auth", "keygen", "--hub", HUB, "--json"],
332 catch_exceptions=False,
333 )
334 json_line = next(
335 (l for l in result.output.splitlines() if l.startswith("{")), None
336 )
337 obj = json.loads(json_line)
338 assert "mnemonic_word_count" in obj
339 assert isinstance(obj["mnemonic_word_count"], int)
340 # FAKE_MNEMONIC has 12 words; run_keygen derives n_words from the
341 # actual mnemonic string so this must match.
342 assert obj["mnemonic_word_count"] == len(FAKE_MNEMONIC.split())
343
344 def test_mnemonic_not_in_toml_after_keygen(
345 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
346 ) -> None:
347 """TOML file on disk must contain no mnemonic after keygen."""
348 fake_home = _patch_home(monkeypatch, tmp_path)
349 _mock_bip39(monkeypatch)
350 runner.invoke(cli, ["auth", "keygen", "--hub", HUB], catch_exceptions=False)
351 raw = _identity_file(fake_home).read_text()
352 # Check no mnemonic *value* is stored — the key name itself must not
353 # appear as a TOML assignment (key_path may contain the word in its
354 # tmp-directory path, but no `mnemonic = ...` key should be written).
355 import re
356 assert re.search(r'^\s*mnemonic\s*=', raw, re.MULTILINE) is None, (
357 f"mnemonic key found in identity.toml:\n{raw}"
358 )
359 assert FAKE_MNEMONIC not in raw
360
361
362 # ---------------------------------------------------------------------------
363 # 4. E2E — register preserves HD fields
364 # ---------------------------------------------------------------------------
365
366
367 class TestRegisterPreservesHdFields:
368 """run_register must carry forward hd_path from the provisional entry
369 written by keygen."""
370
371 def _setup_keygen(
372 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
373 ) -> pathlib.Path:
374 """Run keygen so identity.toml gets HD fields. Return fake_home."""
375 fake_home = _patch_home(monkeypatch, tmp_path)
376 _mock_bip39(monkeypatch)
377 result = runner.invoke(cli, ["auth", "keygen", "--hub", HUB], catch_exceptions=False)
378 assert result.exit_code == 0, result.output
379 return fake_home
380
381 def _run_register(self, monkeypatch: pytest.MonkeyPatch) -> object:
382 _mock_hub(monkeypatch)
383 return runner.invoke(
384 cli, ["auth", "register", "--hub", HUB, "--handle", FAKE_HANDLE],
385 catch_exceptions=False,
386 )
387
388 def test_hd_path_preserved_after_register(
389 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
390 ) -> None:
391 fake_home = self._setup_keygen(monkeypatch, tmp_path)
392 result = self._run_register(monkeypatch)
393 assert result.exit_code == 0, result.output
394
395 data = _read_identity_toml(fake_home)
396 entry = data[HOSTNAME]
397 assert entry.get("hd_path", "").startswith("m/"), "hd_path lost after register"
398
399 def test_mnemonic_not_in_toml_after_register(
400 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
401 ) -> None:
402 """register must not write mnemonic to TOML even when carrying forward entry."""
403 fake_home = self._setup_keygen(monkeypatch, tmp_path)
404 self._run_register(monkeypatch)
405 data = _read_identity_toml(fake_home)
406 assert "mnemonic" not in data[HOSTNAME], (
407 "register wrote mnemonic to TOML — keychain-only invariant violated"
408 )
409
410 def test_handle_updated_after_register(
411 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
412 ) -> None:
413 """Provisional empty handle must be replaced by server-assigned handle."""
414 fake_home = self._setup_keygen(monkeypatch, tmp_path)
415 pre_data = _read_identity_toml(fake_home)
416 assert pre_data[HOSTNAME].get("handle", "") == ""
417
418 self._run_register(monkeypatch)
419 post_data = _read_identity_toml(fake_home)
420 assert post_data[HOSTNAME].get("handle") == FAKE_HANDLE
421
422
423 # ---------------------------------------------------------------------------
424 # 5. Data integrity — TOML escaping edge cases
425 # ---------------------------------------------------------------------------
426
427
428 class TestHdPathTomlEscaping:
429 """HD path with primes and slashes must survive _dump_identity → _load_all."""
430
431 def test_hd_path_prime_and_slash_preserved(self, tmp_path: pathlib.Path) -> None:
432 from muse.core.identity import _dump_identity, _load_all
433 entry = {
434 "type": "human", "handle": FAKE_HANDLE,
435 "key_path": "/tmp/k.pem", "algorithm": "ed25519",
436 "fingerprint": FAKE_FINGERPRINT,
437 "hd_path": FAKE_HD_PATH,
438 }
439 p = tmp_path / "identity.toml"
440 p.write_text(_dump_identity({HOSTNAME: entry}), encoding="utf-8")
441 assert _load_all(p)[HOSTNAME]["hd_path"] == FAKE_HD_PATH
442
443 def test_handle_with_special_chars_round_trips(self, tmp_path: pathlib.Path) -> None:
444 from muse.core.identity import _dump_identity, _load_all
445 entry = {
446 "type": "human", "handle": 'alice "the dev"',
447 "key_path": "/tmp/k.pem", "algorithm": "ed25519",
448 "fingerprint": FAKE_FINGERPRINT,
449 }
450 p = tmp_path / "identity.toml"
451 p.write_text(_dump_identity({HOSTNAME: entry}), encoding="utf-8")
452 assert _load_all(p)[HOSTNAME]["handle"] == 'alice "the dev"'
453
454
455 # ---------------------------------------------------------------------------
456 # 6. Docstring smoke tests
457 # ---------------------------------------------------------------------------
458
459
460 class TestDocstrings:
461 def test_load_all_has_docstring(self) -> None:
462 from muse.core.identity import _load_all
463 assert _load_all.__doc__
464
465 def test_save_identity_has_docstring(self) -> None:
466 from muse.core.identity import save_identity
467 assert save_identity.__doc__
468
469 def test_load_identity_has_docstring(self) -> None:
470 from muse.core.identity import load_identity
471 assert load_identity.__doc__
472
473 def test_generate_hd_keypair_has_docstring(self) -> None:
474 from muse.core.keypair import generate_hd_keypair
475 assert generate_hd_keypair.__doc__
476
477
478 # ---------------------------------------------------------------------------
479 # 7. Performance
480 # ---------------------------------------------------------------------------
481
482
483 class TestPersistencePerformance:
484 """Identity TOML read/write must add negligible latency."""
485
486 def test_save_and_load_identity_under_100ms(
487 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
488 ) -> None:
489 from muse.core import identity as id_module
490 identity_file = tmp_path / "identity.toml"
491 monkeypatch.setattr(id_module, "_IDENTITY_DIR", tmp_path)
492 monkeypatch.setattr(id_module, "_IDENTITY_FILE", identity_file)
493 entry = {
494 "type": "human", "handle": FAKE_HANDLE,
495 "key_path": "/tmp/k.pem", "algorithm": "ed25519",
496 "fingerprint": FAKE_FINGERPRINT,
497 "hd_path": FAKE_HD_PATH,
498 }
499 start = time.monotonic()
500 id_module.save_identity(HUB, entry)
501 # Use _load_all directly to avoid keychain injection for an isolated test.
502 from muse.core.identity import _load_all
503 loaded = _load_all(identity_file).get(HOSTNAME)
504 elapsed = time.monotonic() - start
505 assert loaded is not None
506 assert elapsed < 0.1, f"save+load took {elapsed * 1000:.1f}ms"
507
508 def test_keygen_then_load_identity_under_3s(
509 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
510 ) -> None:
511 """Full keygen including SLIP-0010 HD derivation must complete in under 3 s."""
512 _patch_home(monkeypatch, tmp_path)
513 _mock_bip39(monkeypatch)
514 start = time.monotonic()
515 result = runner.invoke(
516 cli, ["auth", "keygen", "--hub", HUB],
517 catch_exceptions=False,
518 )
519 elapsed = time.monotonic() - start
520 assert result.exit_code == 0, result.output
521 assert elapsed < 3.0, f"keygen took {elapsed:.2f}s"
522
523
524 # ---------------------------------------------------------------------------
525 # 8. Stress
526 # ---------------------------------------------------------------------------
527
528
529 class TestPersistenceStress:
530 """HD field persistence must hold under repeated writes and re-loads."""
531
532 def test_10_successive_registers_preserve_hd_fields(
533 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
534 ) -> None:
535 """hd_path must survive 10 consecutive register calls."""
536 fake_home = _patch_home(monkeypatch, tmp_path)
537 _mock_bip39(monkeypatch)
538 runner.invoke(cli, ["auth", "keygen", "--hub", HUB], catch_exceptions=False)
539
540 _mock_hub(monkeypatch)
541 for i in range(10):
542 result = runner.invoke(
543 cli, ["auth", "register", "--hub", HUB, "--handle", FAKE_HANDLE],
544 catch_exceptions=False,
545 )
546 assert result.exit_code == 0, f"iteration {i}: {result.output}"
547 data = _read_identity_toml(fake_home)
548 assert data[HOSTNAME].get("hd_path", "").startswith("m/"), \
549 f"hd_path lost after {i + 1} register calls"
550 assert "mnemonic" not in data[HOSTNAME], \
551 f"mnemonic leaked into TOML after {i + 1} register calls"
552
553 def test_20_successive_saves_preserve_hd_path(
554 self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
555 ) -> None:
556 """20 successive save_identity writes must not corrupt hd_path."""
557 from muse.core import identity as id_module
558 from muse.core.identity import _load_all
559 identity_file = tmp_path / "identity.toml"
560 monkeypatch.setattr(id_module, "_IDENTITY_DIR", tmp_path)
561 monkeypatch.setattr(id_module, "_IDENTITY_FILE", identity_file)
562
563 base_entry = {
564 "type": "human", "handle": FAKE_HANDLE,
565 "key_path": "/tmp/k.pem", "algorithm": "ed25519",
566 "fingerprint": FAKE_FINGERPRINT,
567 "hd_path": FAKE_HD_PATH,
568 }
569 for i in range(20):
570 entry = {**base_entry, "handle": f"user_{i}"}
571 id_module.save_identity(HUB, entry)
572
573 # Use _load_all to avoid keychain injection for the stress hub URL.
574 loaded = _load_all(identity_file).get(HOSTNAME)
575 assert loaded is not None
576 assert loaded.get("hd_path") == FAKE_HD_PATH
577 assert "mnemonic" not in loaded
578 assert "key_source" not in loaded
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 138 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 141 days ago