gabriel / muse public
test_agent_supercharge.py python
1,023 lines 41.5 KB
Raw
sha256:b5ec4e4a3a73cae0cd08224f32090f2a4836afa0a804cb3231e70c42a3e89295 fix adapter for agent config Human patch 3 days ago
1 """Supercharge tests for ``muse agent``, ``muse agent-config``, and ``muse agent-map``.
2
3 Seven-Tier Coverage Matrix
4 --------------------------
5
6 Tier 1 — TestTypedDictAgent
7 _KeygenJson, _RegisterJson, _ListJson must have schema_version, exit_code,
8 duration_ms annotations. _ListJson must exist (does not yet).
9
10 Tier 2 — TestTypedDictAgentConfig
11 _InitJson, _SyncJson, _ReadJson, _StatusJson, _InspectJson, _SetJson must
12 exist and carry schema_version, exit_code, duration_ms.
13
14 Tier 3 — TestTypedDictAgentMap
15 _AgentMapJson must exist and carry schema_version, exit_code, duration_ms,
16 mode.
17
18 Tier 4 — TestUnitEmitError
19 _emit_error json/human paths, error/message keys, always exits 1.
20
21 Tier 5 — TestAliasRegistration
22 -j alias present on agent keygen, agent list, agent register, agent-map.
23
24 Tier 6 — TestDocstrings
25 run_list, run_keygen, run_register mention schema_version; register()
26 docstring for agent mentions -j.
27
28 Tier 7 — TestEndToEnd / TestStress / TestDataIntegrity / TestSecurity / TestPerformance
29 Live CLI invocations, stress runs, type invariants, hostile input
30 survival, timing bounds.
31
32 All tests should be RED until the implementation adds the missing TypedDicts,
33 envelope fields, and -j aliases described in the task.
34 """
35
36 from __future__ import annotations
37
38 import argparse
39 import io
40 import json
41 import os
42 import pathlib
43 import sys
44 import threading
45 import time
46 from typing import get_type_hints
47
48 import pytest
49
50 from tests.cli_test_helper import CliRunner, InvokeResult
51
52 runner = CliRunner()
53
54 # ---------------------------------------------------------------------------
55 # Constants
56 # ---------------------------------------------------------------------------
57
58 _TEST_HUB = "http://test.example.com"
59 _TEST_MNEMONIC = (
60 "abandon abandon abandon abandon abandon abandon abandon abandon "
61 "abandon abandon abandon about"
62 )
63
64 # ---------------------------------------------------------------------------
65 # Helpers
66 # ---------------------------------------------------------------------------
67
68
69 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
70 """Invoke runner with CWD set to *repo*."""
71 saved = os.getcwd()
72 try:
73 os.chdir(repo)
74 return runner.invoke(None, args)
75 finally:
76 os.chdir(saved)
77
78
79 # ---------------------------------------------------------------------------
80 # Fixtures
81 # ---------------------------------------------------------------------------
82
83
84 @pytest.fixture()
85 def cfg_repo(tmp_path: pathlib.Path) -> pathlib.Path:
86 """Minimal muse repo suitable for agent-config tests."""
87 saved = os.getcwd()
88 try:
89 os.chdir(tmp_path)
90 r = runner.invoke(None, ["init"])
91 assert r.exit_code == 0, f"init failed: {r.output}"
92 finally:
93 os.chdir(saved)
94 return tmp_path
95
96
97 @pytest.fixture()
98 def identity_repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
99 """Repo with a synthetic identity entry for agent keygen/register tests."""
100 saved = os.getcwd()
101 try:
102 os.chdir(tmp_path)
103 r = runner.invoke(None, ["init"])
104 assert r.exit_code == 0, f"init failed: {r.output}"
105 finally:
106 os.chdir(saved)
107
108 # Inject a fake identity so _require_mnemonic does not bail
109 identity_dir = pathlib.Path.home() / ".muse"
110 identity_dir.mkdir(parents=True, exist_ok=True)
111 identity_file = identity_dir / "identity.toml"
112
113 # Back up existing identity if present
114 backup: bytes | None = None
115 if identity_file.exists():
116 backup = identity_file.read_bytes()
117
118 monkeypatch.setattr(
119 "muse.core.identity.load_identity",
120 lambda url: {"mnemonic": _TEST_MNEMONIC, "handle": "test-agent"},
121 )
122
123 yield tmp_path
124
125 # Restore backup
126 if backup is not None:
127 identity_file.write_bytes(backup)
128
129
130 # ===========================================================================
131 # Tier 1 — TestTypedDictAgent
132 # ===========================================================================
133
134
135 class TestTypedDictAgent:
136 """_KeygenJson/_RegisterJson/_ListJson must carry envelope fields."""
137
138 # ── _KeygenJson ──────────────────────────────────────────────────────────
139
140 def test_keygen_json_has_schema_version(self) -> None:
141 from muse.cli.commands.agent import _KeygenJson
142 assert "schema" in get_type_hints(_KeygenJson)
143
144 def test_keygen_json_has_exit_code(self) -> None:
145 from muse.cli.commands.agent import _KeygenJson
146 assert "exit_code" in get_type_hints(_KeygenJson)
147
148 def test_keygen_json_has_duration_ms(self) -> None:
149 from muse.cli.commands.agent import _KeygenJson
150 assert "duration_ms" in get_type_hints(_KeygenJson)
151
152 def test_keygen_json_existing_fields_preserved(self) -> None:
153 from muse.cli.commands.agent import _KeygenJson
154 hints = get_type_hints(_KeygenJson)
155 for field in ("status", "hub", "account", "msign_path", "public_key_b64",
156 "fingerprint", "hd_seed_b64"):
157 assert field in hints, f"_KeygenJson missing existing field: {field}"
158
159 # ── _RegisterJson ────────────────────────────────────────────────────────
160
161 def test_register_json_has_schema_version(self) -> None:
162 from muse.cli.commands.agent import _RegisterJson
163 assert "schema" in get_type_hints(_RegisterJson)
164
165 def test_register_json_has_exit_code(self) -> None:
166 from muse.cli.commands.agent import _RegisterJson
167 assert "exit_code" in get_type_hints(_RegisterJson)
168
169 def test_register_json_has_duration_ms(self) -> None:
170 from muse.cli.commands.agent import _RegisterJson
171 assert "duration_ms" in get_type_hints(_RegisterJson)
172
173 def test_register_json_existing_fields_preserved(self) -> None:
174 from muse.cli.commands.agent import _RegisterJson
175 hints = get_type_hints(_RegisterJson)
176 for field in ("status", "name", "account", "hub", "msign_path"):
177 assert field in hints, f"_RegisterJson missing existing field: {field}"
178
179 # ── _ListJson ────────────────────────────────────────────────────────────
180
181 def test_list_json_exists(self) -> None:
182 """_ListJson TypedDict must be importable — does not yet exist."""
183 from muse.cli.commands.agent import _ListJson # noqa: F401
184
185 def test_list_json_has_schema_version(self) -> None:
186 from muse.cli.commands.agent import _ListJson
187 assert "schema" in get_type_hints(_ListJson)
188
189 def test_list_json_has_exit_code(self) -> None:
190 from muse.cli.commands.agent import _ListJson
191 assert "exit_code" in get_type_hints(_ListJson)
192
193 def test_list_json_has_duration_ms(self) -> None:
194 from muse.cli.commands.agent import _ListJson
195 assert "duration_ms" in get_type_hints(_ListJson)
196
197 def test_list_json_has_slots_field(self) -> None:
198 from muse.cli.commands.agent import _ListJson
199 assert "slots" in get_type_hints(_ListJson)
200
201 def test_list_json_has_mode_field(self) -> None:
202 from muse.cli.commands.agent import _ListJson
203 assert "mode" in get_type_hints(_ListJson)
204
205
206 # ===========================================================================
207 # Tier 2 — TestTypedDictAgentConfig
208 # ===========================================================================
209
210
211 class TestTypedDictAgentConfig:
212 """_InitJson/_SyncJson/_ReadJson/_StatusJson/_InspectJson/_SetJson must exist."""
213
214 # ── _InitJson ─────────────────────────────────────────────────────────────
215
216 def test_init_json_exists(self) -> None:
217 from muse.cli.commands.agent_config import _InitJson # noqa: F401
218
219 def test_init_json_has_schema_version(self) -> None:
220 from muse.cli.commands.agent_config import _InitJson
221 assert "schema" in get_type_hints(_InitJson)
222
223 def test_init_json_has_exit_code(self) -> None:
224 from muse.cli.commands.agent_config import _InitJson
225 assert "exit_code" in get_type_hints(_InitJson)
226
227 def test_init_json_has_duration_ms(self) -> None:
228 from muse.cli.commands.agent_config import _InitJson
229 assert "duration_ms" in get_type_hints(_InitJson)
230
231 def test_init_json_has_path_field(self) -> None:
232 from muse.cli.commands.agent_config import _InitJson
233 assert "path" in get_type_hints(_InitJson)
234
235 # ── _SyncJson ─────────────────────────────────────────────────────────────
236
237 def test_sync_json_exists(self) -> None:
238 from muse.cli.commands.agent_config import _SyncJson # noqa: F401
239
240 def test_sync_json_has_schema_version(self) -> None:
241 from muse.cli.commands.agent_config import _SyncJson
242 assert "schema" in get_type_hints(_SyncJson)
243
244 def test_sync_json_has_exit_code(self) -> None:
245 from muse.cli.commands.agent_config import _SyncJson
246 assert "exit_code" in get_type_hints(_SyncJson)
247
248 def test_sync_json_has_duration_ms(self) -> None:
249 from muse.cli.commands.agent_config import _SyncJson
250 assert "duration_ms" in get_type_hints(_SyncJson)
251
252 def test_sync_json_has_adapters_field(self) -> None:
253 from muse.cli.commands.agent_config import _SyncJson
254 assert "adapters" in get_type_hints(_SyncJson)
255
256 # ── _ReadJson ─────────────────────────────────────────────────────────────
257
258 def test_read_json_exists(self) -> None:
259 from muse.cli.commands.agent_config import _ReadJson # noqa: F401
260
261 def test_read_json_has_schema_version(self) -> None:
262 from muse.cli.commands.agent_config import _ReadJson
263 assert "schema" in get_type_hints(_ReadJson)
264
265 def test_read_json_has_exit_code(self) -> None:
266 from muse.cli.commands.agent_config import _ReadJson
267 assert "exit_code" in get_type_hints(_ReadJson)
268
269 def test_read_json_has_duration_ms(self) -> None:
270 from muse.cli.commands.agent_config import _ReadJson
271 assert "duration_ms" in get_type_hints(_ReadJson)
272
273 def test_read_json_has_content_field(self) -> None:
274 from muse.cli.commands.agent_config import _ReadJson
275 assert "content" in get_type_hints(_ReadJson)
276
277 # ── _StatusJson ───────────────────────────────────────────────────────────
278
279 def test_status_json_exists(self) -> None:
280 from muse.cli.commands.agent_config import _StatusJson # noqa: F401
281
282 def test_status_json_has_schema_version(self) -> None:
283 from muse.cli.commands.agent_config import _StatusJson
284 assert "schema" in get_type_hints(_StatusJson)
285
286 def test_status_json_has_exit_code(self) -> None:
287 from muse.cli.commands.agent_config import _StatusJson
288 assert "exit_code" in get_type_hints(_StatusJson)
289
290 def test_status_json_has_duration_ms(self) -> None:
291 from muse.cli.commands.agent_config import _StatusJson
292 assert "duration_ms" in get_type_hints(_StatusJson)
293
294 def test_status_json_has_ready_field(self) -> None:
295 from muse.cli.commands.agent_config import _StatusJson
296 assert "ready" in get_type_hints(_StatusJson)
297
298 # ── _InspectJson ──────────────────────────────────────────────────────────
299
300 def test_inspect_json_exists(self) -> None:
301 from muse.cli.commands.agent_config import _InspectJson # noqa: F401
302
303 def test_inspect_json_has_schema_version(self) -> None:
304 from muse.cli.commands.agent_config import _InspectJson
305 assert "schema" in get_type_hints(_InspectJson)
306
307 def test_inspect_json_has_exit_code(self) -> None:
308 from muse.cli.commands.agent_config import _InspectJson
309 assert "exit_code" in get_type_hints(_InspectJson)
310
311 def test_inspect_json_has_duration_ms(self) -> None:
312 from muse.cli.commands.agent_config import _InspectJson
313 assert "duration_ms" in get_type_hints(_InspectJson)
314
315 def test_inspect_json_has_context_field(self) -> None:
316 from muse.cli.commands.agent_config import _InspectJson
317 assert "context" in get_type_hints(_InspectJson)
318
319 # ── _SetJson ──────────────────────────────────────────────────────────────
320
321 def test_set_json_exists(self) -> None:
322 from muse.cli.commands.agent_config import _SetJson # noqa: F401
323
324 def test_set_json_has_schema_version(self) -> None:
325 from muse.cli.commands.agent_config import _SetJson
326 assert "schema" in get_type_hints(_SetJson)
327
328 def test_set_json_has_exit_code(self) -> None:
329 from muse.cli.commands.agent_config import _SetJson
330 assert "exit_code" in get_type_hints(_SetJson)
331
332 def test_set_json_has_duration_ms(self) -> None:
333 from muse.cli.commands.agent_config import _SetJson
334 assert "duration_ms" in get_type_hints(_SetJson)
335
336 def test_set_json_has_adapters_field(self) -> None:
337 from muse.cli.commands.agent_config import _SetJson
338 assert "adapters" in get_type_hints(_SetJson)
339
340
341 # ===========================================================================
342 # Tier 3 — TestTypedDictAgentMap
343 # ===========================================================================
344
345
346 class TestTypedDictAgentMap:
347 """_AgentMapJson must exist and carry envelope fields."""
348
349 def test_agent_map_json_exists(self) -> None:
350 from muse.cli.commands.agent_map import _AgentMapJson # noqa: F401
351
352 def test_agent_map_json_has_schema_version(self) -> None:
353 from muse.cli.commands.agent_map import _AgentMapJson
354 assert "schema" in get_type_hints(_AgentMapJson)
355
356 def test_agent_map_json_has_exit_code(self) -> None:
357 from muse.cli.commands.agent_map import _AgentMapJson
358 assert "exit_code" in get_type_hints(_AgentMapJson)
359
360 def test_agent_map_json_has_duration_ms(self) -> None:
361 from muse.cli.commands.agent_map import _AgentMapJson
362 assert "duration_ms" in get_type_hints(_AgentMapJson)
363
364 def test_agent_map_json_has_mode(self) -> None:
365 from muse.cli.commands.agent_map import _AgentMapJson
366 assert "mode" in get_type_hints(_AgentMapJson)
367
368 def test_agent_map_json_has_track(self) -> None:
369 from muse.cli.commands.agent_map import _AgentMapJson
370 assert "track" in get_type_hints(_AgentMapJson)
371
372 def test_agent_map_json_has_attributions(self) -> None:
373 from muse.cli.commands.agent_map import _AgentMapJson
374 assert "attributions" in get_type_hints(_AgentMapJson)
375
376 def test_bar_attribution_existing_fields_preserved(self) -> None:
377 from muse.cli.commands.agent_map import BarAttribution
378 hints = get_type_hints(BarAttribution)
379 for field in ("bar", "author", "commit_id", "message"):
380 assert field in hints, f"BarAttribution missing field: {field}"
381
382
383 # ===========================================================================
384 # Tier 4 — TestUnitFingerprint / TestUnitEmitError
385 # ===========================================================================
386
387
388
389 class TestUnitEmitError:
390 """_emit_error json/human paths."""
391
392 def _capture_emit_error(self, error: str, message: str, as_json: bool) -> tuple[str, str, int]:
393 """Run _emit_error, return (stdout_text, stderr_text, exit_code)."""
394 from muse.cli.commands.agent import _emit_error
395 stdout_buf = io.StringIO()
396 stderr_buf = io.StringIO()
397 exit_code = 0
398 orig_stdout, orig_stderr = sys.stdout, sys.stderr
399 sys.stdout = stdout_buf
400 sys.stderr = stderr_buf
401 try:
402 _emit_error(error, message, as_json)
403 except SystemExit as exc:
404 exit_code = int(exc.code) if exc.code is not None else 0
405 finally:
406 sys.stdout = orig_stdout
407 sys.stderr = orig_stderr
408 return stdout_buf.getvalue(), stderr_buf.getvalue(), exit_code
409
410 def test_json_mode_writes_to_stdout(self) -> None:
411 stdout, _, _ = self._capture_emit_error("err_code", "msg text", True)
412 assert stdout.strip() != ""
413
414 def test_json_mode_valid_json(self) -> None:
415 stdout, _, _ = self._capture_emit_error("err_code", "msg text", True)
416 json.loads(stdout) # must not raise
417
418 def test_json_mode_has_error_key(self) -> None:
419 stdout, _, _ = self._capture_emit_error("err_code", "msg text", True)
420 d = json.loads(stdout)
421 assert "error" in d
422
423 def test_json_mode_has_message_key(self) -> None:
424 stdout, _, _ = self._capture_emit_error("err_code", "msg text", True)
425 d = json.loads(stdout)
426 assert "message" in d
427
428 def test_json_mode_error_value_correct(self) -> None:
429 stdout, _, _ = self._capture_emit_error("my_error", "some message", True)
430 d = json.loads(stdout)
431 assert d["error"] == "my_error"
432
433 def test_json_mode_message_value_correct(self) -> None:
434 stdout, _, _ = self._capture_emit_error("err", "expected message", True)
435 d = json.loads(stdout)
436 assert d["message"] == "expected message"
437
438 def test_json_mode_exits_1(self) -> None:
439 _, _, code = self._capture_emit_error("err", "msg", True)
440 assert code == 1
441
442 def test_human_mode_writes_to_stderr(self) -> None:
443 _, stderr, _ = self._capture_emit_error("err", "human message", False)
444 assert stderr.strip() != ""
445
446 def test_human_mode_exits_1(self) -> None:
447 _, _, code = self._capture_emit_error("err", "msg", False)
448 assert code == 1
449
450 def test_human_mode_stdout_empty(self) -> None:
451 stdout, _, _ = self._capture_emit_error("err", "msg", False)
452 assert stdout.strip() == ""
453
454
455 # ===========================================================================
456 # Tier 5 — TestAliasRegistration
457 # ===========================================================================
458
459
460 class TestAliasRegistration:
461 """``-j`` alias must be registered on all three agent subcommands and agent-map."""
462
463 def _build_agent_parser(self) -> argparse.ArgumentParser:
464 from muse.cli.commands.agent import register
465 p = argparse.ArgumentParser()
466 sub = p.add_subparsers()
467 register(sub)
468 return p
469
470 def test_agent_keygen_j_alias_parses(self) -> None:
471 p = self._build_agent_parser()
472 ns = p.parse_args(["agent", "keygen", "--account", "0", "--hub", _TEST_HUB, "-j"])
473 assert getattr(ns, "json_out", False) is True
474
475 def test_agent_list_j_alias_parses(self) -> None:
476 p = self._build_agent_parser()
477 ns = p.parse_args(["agent", "list", "--hub", _TEST_HUB, "-j"])
478 assert getattr(ns, "json_out", False) is True
479
480 def test_agent_register_j_alias_parses(self) -> None:
481 p = self._build_agent_parser()
482 ns = p.parse_args([
483 "agent", "register",
484 "--account", "1",
485 "--name", "test-agent",
486 "--hub", _TEST_HUB,
487 "-j",
488 ])
489 assert getattr(ns, "json_out", False) is True
490
491 def test_agent_map_j_alias_registered(self) -> None:
492 from muse.cli.commands.agent_map import register
493 p = argparse.ArgumentParser()
494 sub = p.add_subparsers()
495 register(sub)
496 ns = p.parse_args(["agent-map", "tracks/test.mid", "-j"])
497 # -j should set json_out/json/as_json to True
498 assert (
499 getattr(ns, "json_out", False) is True
500 or getattr(ns, "json", False) is True
501 or getattr(ns, "as_json", False) is True
502 )
503
504 def test_agent_keygen_json_flag_still_works(self) -> None:
505 p = self._build_agent_parser()
506 ns = p.parse_args(["agent", "keygen", "--account", "0", "--hub", _TEST_HUB, "--json"])
507 assert getattr(ns, "json_out", False) is True
508
509 def test_agent_list_json_flag_still_works(self) -> None:
510 p = self._build_agent_parser()
511 ns = p.parse_args(["agent", "list", "--hub", _TEST_HUB, "--json"])
512 assert getattr(ns, "json_out", False) is True
513
514 def test_agent_register_json_flag_still_works(self) -> None:
515 p = self._build_agent_parser()
516 ns = p.parse_args([
517 "agent", "register",
518 "--account", "1",
519 "--name", "test-agent",
520 "--hub", _TEST_HUB,
521 "--json",
522 ])
523 assert getattr(ns, "json_out", False) is True
524
525
526 # ===========================================================================
527 # Tier 6 — TestDocstrings
528 # ===========================================================================
529
530
531 class TestDocstrings:
532 """Key functions must document new envelope fields in their docstrings."""
533
534 def test_run_keygen_docstring_mentions_schema_version(self) -> None:
535 from muse.cli.commands.agent import run_keygen
536 assert run_keygen.__doc__ is not None
537 assert "schema" in run_keygen.__doc__
538
539 def test_run_list_docstring_mentions_schema_version(self) -> None:
540 from muse.cli.commands.agent import run_list
541 assert run_list.__doc__ is not None
542 assert "schema" in run_list.__doc__
543
544 def test_run_register_docstring_mentions_schema_version(self) -> None:
545 from muse.cli.commands.agent import run_register
546 assert run_register.__doc__ is not None
547 assert "schema" in run_register.__doc__
548
549 def test_register_docstring_mentions_j_alias(self) -> None:
550 from muse.cli.commands.agent import register
551 assert register.__doc__ is not None
552 assert "-j" in register.__doc__
553
554 def test_run_list_docstring_mentions_duration_ms(self) -> None:
555 from muse.cli.commands.agent import run_list
556 assert run_list.__doc__ is not None
557 assert "duration_ms" in run_list.__doc__
558
559 def test_run_keygen_docstring_mentions_duration_ms(self) -> None:
560 from muse.cli.commands.agent import run_keygen
561 assert run_keygen.__doc__ is not None
562 assert "duration_ms" in run_keygen.__doc__
563
564
565 # ===========================================================================
566 # Tier 7a — TestEndToEnd
567 # ===========================================================================
568
569
570 class TestEndToEnd:
571 """Live CLI invocations against real (tmp) repos."""
572
573 # ── agent list -j ────────────────────────────────────────────────────────
574
575 def test_agent_list_j_exits_zero(self) -> None:
576 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "-j"])
577 assert r.exit_code == 0, r.output
578
579 def test_agent_list_j_valid_json(self) -> None:
580 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "-j"])
581 assert r.exit_code == 0, r.output
582 json.loads(r.output)
583
584 def test_agent_list_j_has_schema_version(self) -> None:
585 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "-j"])
586 assert r.exit_code == 0, r.output
587 d = json.loads(r.output)
588 assert "schema" in d
589
590 def test_agent_list_j_has_exit_code(self) -> None:
591 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "-j"])
592 d = json.loads(r.output)
593 assert "exit_code" in d
594
595 def test_agent_list_j_has_duration_ms(self) -> None:
596 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "-j"])
597 d = json.loads(r.output)
598 assert "duration_ms" in d
599
600 def test_agent_list_j_has_slots(self) -> None:
601 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "-j"])
602 d = json.loads(r.output)
603 assert "slots" in d
604
605 def test_agent_list_j_slots_is_list(self) -> None:
606 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "-j"])
607 d = json.loads(r.output)
608 assert isinstance(d["slots"], list)
609
610 def test_agent_list_j_exit_code_matches_process(self) -> None:
611 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "-j"])
612 d = json.loads(r.output)
613 assert d["exit_code"] == r.exit_code
614
615 def test_agent_list_json_flag_same_as_j(self) -> None:
616 r1 = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
617 r2 = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "-j"])
618 d1 = json.loads(r1.output)
619 d2 = json.loads(r2.output)
620 d1.pop("duration_ms", None)
621 d2.pop("duration_ms", None)
622 assert set(d1.keys()) == set(d2.keys())
623
624 # ── agent-config init -j ──────────────────────────────────────────────────
625
626 def test_agent_config_init_j_exits_zero(self, cfg_repo: pathlib.Path) -> None:
627 r = _invoke(cfg_repo, ["agent-config", "init", "-j"])
628 assert r.exit_code == 0, r.output
629
630 def test_agent_config_init_j_valid_json(self, cfg_repo: pathlib.Path) -> None:
631 r = _invoke(cfg_repo, ["agent-config", "init", "-j"])
632 assert r.exit_code == 0, r.output
633 json.loads(r.output)
634
635 def test_agent_config_init_j_has_schema_version(self, cfg_repo: pathlib.Path) -> None:
636 r = _invoke(cfg_repo, ["agent-config", "init", "-j"])
637 assert r.exit_code == 0, r.output
638 d = json.loads(r.output)
639 assert "schema" in d
640
641 def test_agent_config_init_j_has_exit_code(self, cfg_repo: pathlib.Path) -> None:
642 r = _invoke(cfg_repo, ["agent-config", "init", "-j"])
643 d = json.loads(r.output)
644 assert "exit_code" in d
645
646 def test_agent_config_init_j_has_duration_ms(self, cfg_repo: pathlib.Path) -> None:
647 r = _invoke(cfg_repo, ["agent-config", "init", "-j"])
648 d = json.loads(r.output)
649 assert "duration_ms" in d
650
651 # ── agent-config status -j ────────────────────────────────────────────────
652
653 def test_agent_config_status_j_exits_zero(self, cfg_repo: pathlib.Path) -> None:
654 _invoke(cfg_repo, ["agent-config", "init", "-j"])
655 r = _invoke(cfg_repo, ["agent-config", "status", "-j"])
656 assert r.exit_code == 0, r.output
657
658 def test_agent_config_status_j_has_schema_version(self, cfg_repo: pathlib.Path) -> None:
659 _invoke(cfg_repo, ["agent-config", "init", "-j"])
660 r = _invoke(cfg_repo, ["agent-config", "status", "-j"])
661 assert r.exit_code == 0, r.output
662 d = json.loads(r.output)
663 assert "schema" in d
664
665 def test_agent_config_status_j_has_exit_code(self, cfg_repo: pathlib.Path) -> None:
666 _invoke(cfg_repo, ["agent-config", "init", "-j"])
667 r = _invoke(cfg_repo, ["agent-config", "status", "-j"])
668 d = json.loads(r.output)
669 assert "exit_code" in d
670
671 def test_agent_config_status_j_has_duration_ms(self, cfg_repo: pathlib.Path) -> None:
672 _invoke(cfg_repo, ["agent-config", "init", "-j"])
673 r = _invoke(cfg_repo, ["agent-config", "status", "-j"])
674 d = json.loads(r.output)
675 assert "duration_ms" in d
676
677 # ── agent-config inspect -j ────────────────────────────────────────────────
678
679 def test_agent_config_inspect_j_exits_zero(self, cfg_repo: pathlib.Path) -> None:
680 _invoke(cfg_repo, ["agent-config", "init", "-j"])
681 r = _invoke(cfg_repo, ["agent-config", "inspect", "-j"])
682 assert r.exit_code == 0, r.output
683
684 def test_agent_config_inspect_j_has_schema_version(self, cfg_repo: pathlib.Path) -> None:
685 _invoke(cfg_repo, ["agent-config", "init", "-j"])
686 r = _invoke(cfg_repo, ["agent-config", "inspect", "-j"])
687 assert r.exit_code == 0, r.output
688 d = json.loads(r.output)
689 assert "schema" in d
690
691 def test_agent_config_inspect_j_has_exit_code(self, cfg_repo: pathlib.Path) -> None:
692 _invoke(cfg_repo, ["agent-config", "init", "-j"])
693 r = _invoke(cfg_repo, ["agent-config", "inspect", "-j"])
694 d = json.loads(r.output)
695 assert "exit_code" in d
696
697 def test_agent_config_inspect_j_has_duration_ms(self, cfg_repo: pathlib.Path) -> None:
698 _invoke(cfg_repo, ["agent-config", "init", "-j"])
699 r = _invoke(cfg_repo, ["agent-config", "inspect", "-j"])
700 d = json.loads(r.output)
701 assert "duration_ms" in d
702
703 # ── agent-config read -j (after init) ────────────────────────────────────
704
705 def test_agent_config_read_j_exits_zero(self, cfg_repo: pathlib.Path) -> None:
706 _invoke(cfg_repo, ["agent-config", "init"])
707 r = _invoke(cfg_repo, ["agent-config", "read", "-j"])
708 assert r.exit_code == 0, r.output
709
710 def test_agent_config_read_j_has_schema_version(self, cfg_repo: pathlib.Path) -> None:
711 _invoke(cfg_repo, ["agent-config", "init"])
712 r = _invoke(cfg_repo, ["agent-config", "read", "-j"])
713 assert r.exit_code == 0, r.output
714 d = json.loads(r.output)
715 assert "schema" in d
716
717 def test_agent_config_read_j_has_exit_code(self, cfg_repo: pathlib.Path) -> None:
718 _invoke(cfg_repo, ["agent-config", "init"])
719 r = _invoke(cfg_repo, ["agent-config", "read", "-j"])
720 d = json.loads(r.output)
721 assert "exit_code" in d
722
723 def test_agent_config_read_j_has_duration_ms(self, cfg_repo: pathlib.Path) -> None:
724 _invoke(cfg_repo, ["agent-config", "init"])
725 r = _invoke(cfg_repo, ["agent-config", "read", "-j"])
726 d = json.loads(r.output)
727 assert "duration_ms" in d
728
729 # ── agent-config sync -j (after init + set) ──────────────────────────────
730
731 def test_agent_config_sync_j_exits_zero(self, cfg_repo: pathlib.Path) -> None:
732 _invoke(cfg_repo, ["agent-config", "init"])
733 _invoke(cfg_repo, ["agent-config", "set", "--adapters", "claude"])
734 r = _invoke(cfg_repo, ["agent-config", "sync", "-j"])
735 assert r.exit_code == 0, r.output
736
737 def test_agent_config_sync_j_has_schema_version(self, cfg_repo: pathlib.Path) -> None:
738 _invoke(cfg_repo, ["agent-config", "init"])
739 _invoke(cfg_repo, ["agent-config", "set", "--adapters", "claude"])
740 r = _invoke(cfg_repo, ["agent-config", "sync", "-j"])
741 assert r.exit_code == 0, r.output
742 d = json.loads(r.output)
743 assert "schema" in d
744
745 def test_agent_config_sync_j_has_exit_code(self, cfg_repo: pathlib.Path) -> None:
746 _invoke(cfg_repo, ["agent-config", "init"])
747 _invoke(cfg_repo, ["agent-config", "set", "--adapters", "claude"])
748 r = _invoke(cfg_repo, ["agent-config", "sync", "-j"])
749 d = json.loads(r.output)
750 assert "exit_code" in d
751
752 def test_agent_config_sync_j_has_duration_ms(self, cfg_repo: pathlib.Path) -> None:
753 _invoke(cfg_repo, ["agent-config", "init"])
754 _invoke(cfg_repo, ["agent-config", "set", "--adapters", "claude"])
755 r = _invoke(cfg_repo, ["agent-config", "sync", "-j"])
756 d = json.loads(r.output)
757 assert "duration_ms" in d
758
759
760 # ===========================================================================
761 # Tier 7b — TestStress
762 # ===========================================================================
763
764
765 class TestStress:
766 """High-volume calls to verify no crashes, leaks, or race conditions."""
767
768 def test_fingerprint_1000_calls_no_crash(self) -> None:
769 from muse.core.types import public_key_fingerprint
770 for i in range(1000):
771 result = public_key_fingerprint(i.to_bytes(4, "big"))
772 assert len(result) == 71
773
774 def test_emit_error_json_500_calls(self) -> None:
775 from muse.cli.commands.agent import _emit_error
776 for i in range(500):
777 stdout_buf = io.StringIO()
778 orig = sys.stdout
779 sys.stdout = stdout_buf
780 try:
781 try:
782 _emit_error(f"err_{i}", f"message_{i}", True)
783 except SystemExit:
784 pass
785 finally:
786 sys.stdout = orig
787 d = json.loads(stdout_buf.getvalue())
788 assert d["error"] == f"err_{i}"
789
790 def test_agent_config_status_50_times(self, cfg_repo: pathlib.Path) -> None:
791 _invoke(cfg_repo, ["agent-config", "init"])
792 for _ in range(50):
793 r = _invoke(cfg_repo, ["agent-config", "status", "--json"])
794 assert r.exit_code == 0, r.output
795 d = json.loads(r.output)
796 assert "schema" in d
797
798 def test_fingerprint_thread_safety(self) -> None:
799 from muse.core.types import public_key_fingerprint
800 results: list[str] = []
801 errors: list[Exception] = []
802 lock = threading.Lock()
803
804 def worker(n: int) -> None:
805 try:
806 fp = public_key_fingerprint(n.to_bytes(4, "big"))
807 with lock:
808 results.append(fp)
809 except Exception as exc:
810 with lock:
811 errors.append(exc)
812
813 threads = [threading.Thread(target=worker, args=(i,)) for i in range(100)]
814 for t in threads:
815 t.start()
816 for t in threads:
817 t.join()
818
819 assert not errors, f"Thread errors: {errors}"
820 assert len(results) == 100
821
822
823 # ===========================================================================
824 # Tier 7c — TestDataIntegrity
825 # ===========================================================================
826
827
828 class TestDataIntegrity:
829 """Type invariants that the JSON envelope must satisfy."""
830
831 def test_agent_list_schema_version_is_str(self) -> None:
832 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
833 assert r.exit_code == 0, r.output
834 d = json.loads(r.output)
835 assert isinstance(d["schema"], int)
836
837 def test_agent_list_exit_code_is_int(self) -> None:
838 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
839 d = json.loads(r.output)
840 assert isinstance(d["exit_code"], int)
841
842 def test_agent_list_duration_ms_is_float(self) -> None:
843 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
844 d = json.loads(r.output)
845 assert isinstance(d["duration_ms"], float)
846
847 def test_agent_list_slots_is_list(self) -> None:
848 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
849 d = json.loads(r.output)
850 assert isinstance(d["slots"], list)
851
852 def test_agent_list_duration_ms_nonnegative(self) -> None:
853 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
854 d = json.loads(r.output)
855 assert d["duration_ms"] >= 0.0
856
857 def test_agent_config_init_schema_version_is_str(self, cfg_repo: pathlib.Path) -> None:
858 r = _invoke(cfg_repo, ["agent-config", "init", "--json"])
859 assert r.exit_code == 0, r.output
860 d = json.loads(r.output)
861 assert isinstance(d["schema"], int)
862
863 def test_agent_config_init_exit_code_is_int(self, cfg_repo: pathlib.Path) -> None:
864 r = _invoke(cfg_repo, ["agent-config", "init", "--json"])
865 d = json.loads(r.output)
866 assert isinstance(d["exit_code"], int)
867
868 def test_agent_config_init_duration_ms_is_float(self, cfg_repo: pathlib.Path) -> None:
869 r = _invoke(cfg_repo, ["agent-config", "init", "--json"])
870 d = json.loads(r.output)
871 assert isinstance(d["duration_ms"], float)
872
873 def test_agent_config_status_exit_code_mirrors_process(self, cfg_repo: pathlib.Path) -> None:
874 _invoke(cfg_repo, ["agent-config", "init"])
875 r = _invoke(cfg_repo, ["agent-config", "status", "--json"])
876 d = json.loads(r.output)
877 assert d["exit_code"] == r.exit_code
878
879 def test_agent_list_schema_version_nonempty(self) -> None:
880 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
881 d = json.loads(r.output)
882 assert d["schema"] > 0
883
884
885 # ===========================================================================
886 # Tier 7d — TestSecurity
887 # ===========================================================================
888
889
890 class TestSecurity:
891 """Hostile input must not crash or corrupt JSON output."""
892
893 def test_hostile_hub_url_in_emit_error_json(self) -> None:
894 from muse.cli.commands.agent import _emit_error
895 hostile = "'; DROP TABLE users; --"
896 stdout_buf = io.StringIO()
897 orig = sys.stdout
898 sys.stdout = stdout_buf
899 try:
900 try:
901 _emit_error("sql_test", hostile, True)
902 except SystemExit:
903 pass
904 finally:
905 sys.stdout = orig
906 # Must still be valid JSON
907 d = json.loads(stdout_buf.getvalue())
908 assert d["message"] == hostile
909
910 def test_sql_injection_in_slot_name(self) -> None:
911 """A slot name with SQL injection chars must not crash fingerprint or emit_error."""
912 from muse.core.types import public_key_fingerprint
913 malicious_name = b"'; DROP TABLE slots; --"
914 fp = public_key_fingerprint(malicious_name)
915 assert len(fp) == 71
916
917 def test_unicode_in_track_path_emit_error(self) -> None:
918 from muse.cli.commands.agent import _emit_error
919 unicode_path = "tracks/\u4e2d\u6587\u97f3\u4e50.mid"
920 stdout_buf = io.StringIO()
921 orig = sys.stdout
922 sys.stdout = stdout_buf
923 try:
924 try:
925 _emit_error("unicode_test", unicode_path, True)
926 except SystemExit:
927 pass
928 finally:
929 sys.stdout = orig
930 d = json.loads(stdout_buf.getvalue())
931 assert d["message"] == unicode_path
932
933 def test_very_long_account_name_in_emit_error(self) -> None:
934 from muse.cli.commands.agent import _emit_error
935 long_msg = "x" * 10_000
936 stdout_buf = io.StringIO()
937 orig = sys.stdout
938 sys.stdout = stdout_buf
939 try:
940 try:
941 _emit_error("long_test", long_msg, True)
942 except SystemExit:
943 pass
944 finally:
945 sys.stdout = orig
946 d = json.loads(stdout_buf.getvalue())
947 assert d["message"] == long_msg
948
949 def test_null_bytes_in_fingerprint_input(self) -> None:
950 from muse.core.types import public_key_fingerprint
951 data = b"\x00" * 64
952 fp = public_key_fingerprint(data)
953 assert len(fp) == 71
954
955 def test_newline_in_error_message_survives_json(self) -> None:
956 from muse.cli.commands.agent import _emit_error
957 newline_msg = "line one\nline two\nline three"
958 stdout_buf = io.StringIO()
959 orig = sys.stdout
960 sys.stdout = stdout_buf
961 try:
962 try:
963 _emit_error("newline_test", newline_msg, True)
964 except SystemExit:
965 pass
966 finally:
967 sys.stdout = orig
968 d = json.loads(stdout_buf.getvalue())
969 assert d["message"] == newline_msg
970
971 def test_agent_list_with_encoded_url_returns_json(self) -> None:
972 encoded_url = "http://test.example.com%2Fpath"
973 r = runner.invoke(None, ["agent", "list", "--hub", encoded_url, "--json"])
974 # Should either succeed with JSON or emit a JSON error — not a crash
975 output = r.output.strip()
976 assert output, "no output produced"
977 json.loads(output) # must be valid JSON regardless
978
979
980 # ===========================================================================
981 # Tier 7e — TestPerformance
982 # ===========================================================================
983
984
985 class TestPerformance:
986 """Timing bounds for performance-sensitive paths."""
987
988 def test_fingerprint_1000_under_500ms(self) -> None:
989 from muse.core.types import public_key_fingerprint
990 data = b"perf test input" * 4
991 start = time.perf_counter()
992 for _ in range(1000):
993 public_key_fingerprint(data)
994 elapsed_ms = (time.perf_counter() - start) * 1000
995 assert elapsed_ms < 500, f"1000 public_key_fingerprint calls took {elapsed_ms:.1f}ms"
996
997 def test_agent_config_status_completes_quickly(self, cfg_repo: pathlib.Path) -> None:
998 _invoke(cfg_repo, ["agent-config", "init"])
999 start = time.perf_counter()
1000 r = _invoke(cfg_repo, ["agent-config", "status", "--json"])
1001 elapsed_ms = (time.perf_counter() - start) * 1000
1002 assert r.exit_code == 0, r.output
1003 assert elapsed_ms < 2000, f"agent-config status took {elapsed_ms:.1f}ms"
1004
1005 def test_agent_list_completes_quickly(self) -> None:
1006 start = time.perf_counter()
1007 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
1008 elapsed_ms = (time.perf_counter() - start) * 1000
1009 assert r.exit_code == 0, r.output
1010 assert elapsed_ms < 2000, f"agent list took {elapsed_ms:.1f}ms"
1011
1012 def test_duration_ms_in_list_output_is_plausible(self) -> None:
1013 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
1014 assert r.exit_code == 0, r.output
1015 d = json.loads(r.output)
1016 # duration_ms must be >= 0 and < 5000 (very generous upper bound)
1017 assert 0.0 <= d["duration_ms"] < 5000.0
1018
1019 def test_duration_ms_in_agent_config_init_is_plausible(self, cfg_repo: pathlib.Path) -> None:
1020 r = _invoke(cfg_repo, ["agent-config", "init", "--json"])
1021 assert r.exit_code == 0, r.output
1022 d = json.loads(r.output)
1023 assert 0.0 <= d["duration_ms"] < 5000.0
File History 1 commit
sha256:b5ec4e4a3a73cae0cd08224f32090f2a4836afa0a804cb3231e70c42a3e89295 fix adapter for agent config Human patch 3 days ago