gabriel / muse public
test_agent_supercharge.py python
1,019 lines 41.2 KB
Raw
sha256:b5ec4e4a3a73cae0cd08224f32090f2a4836afa0a804cb3231e70c42a3e89295 fix adapter for agent config Human patch 15 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) ────────────────────────────────────
730
731 def test_agent_config_sync_j_exits_zero(self, cfg_repo: pathlib.Path) -> None:
732 _invoke(cfg_repo, ["agent-config", "init"])
733 r = _invoke(cfg_repo, ["agent-config", "sync", "-j"])
734 assert r.exit_code == 0, r.output
735
736 def test_agent_config_sync_j_has_schema_version(self, cfg_repo: pathlib.Path) -> None:
737 _invoke(cfg_repo, ["agent-config", "init"])
738 r = _invoke(cfg_repo, ["agent-config", "sync", "-j"])
739 assert r.exit_code == 0, r.output
740 d = json.loads(r.output)
741 assert "schema" in d
742
743 def test_agent_config_sync_j_has_exit_code(self, cfg_repo: pathlib.Path) -> None:
744 _invoke(cfg_repo, ["agent-config", "init"])
745 r = _invoke(cfg_repo, ["agent-config", "sync", "-j"])
746 d = json.loads(r.output)
747 assert "exit_code" in d
748
749 def test_agent_config_sync_j_has_duration_ms(self, cfg_repo: pathlib.Path) -> None:
750 _invoke(cfg_repo, ["agent-config", "init"])
751 r = _invoke(cfg_repo, ["agent-config", "sync", "-j"])
752 d = json.loads(r.output)
753 assert "duration_ms" in d
754
755
756 # ===========================================================================
757 # Tier 7b — TestStress
758 # ===========================================================================
759
760
761 class TestStress:
762 """High-volume calls to verify no crashes, leaks, or race conditions."""
763
764 def test_fingerprint_1000_calls_no_crash(self) -> None:
765 from muse.core.types import public_key_fingerprint
766 for i in range(1000):
767 result = public_key_fingerprint(i.to_bytes(4, "big"))
768 assert len(result) == 71
769
770 def test_emit_error_json_500_calls(self) -> None:
771 from muse.cli.commands.agent import _emit_error
772 for i in range(500):
773 stdout_buf = io.StringIO()
774 orig = sys.stdout
775 sys.stdout = stdout_buf
776 try:
777 try:
778 _emit_error(f"err_{i}", f"message_{i}", True)
779 except SystemExit:
780 pass
781 finally:
782 sys.stdout = orig
783 d = json.loads(stdout_buf.getvalue())
784 assert d["error"] == f"err_{i}"
785
786 def test_agent_config_status_50_times(self, cfg_repo: pathlib.Path) -> None:
787 _invoke(cfg_repo, ["agent-config", "init"])
788 for _ in range(50):
789 r = _invoke(cfg_repo, ["agent-config", "status", "--json"])
790 assert r.exit_code == 0, r.output
791 d = json.loads(r.output)
792 assert "schema" in d
793
794 def test_fingerprint_thread_safety(self) -> None:
795 from muse.core.types import public_key_fingerprint
796 results: list[str] = []
797 errors: list[Exception] = []
798 lock = threading.Lock()
799
800 def worker(n: int) -> None:
801 try:
802 fp = public_key_fingerprint(n.to_bytes(4, "big"))
803 with lock:
804 results.append(fp)
805 except Exception as exc:
806 with lock:
807 errors.append(exc)
808
809 threads = [threading.Thread(target=worker, args=(i,)) for i in range(100)]
810 for t in threads:
811 t.start()
812 for t in threads:
813 t.join()
814
815 assert not errors, f"Thread errors: {errors}"
816 assert len(results) == 100
817
818
819 # ===========================================================================
820 # Tier 7c — TestDataIntegrity
821 # ===========================================================================
822
823
824 class TestDataIntegrity:
825 """Type invariants that the JSON envelope must satisfy."""
826
827 def test_agent_list_schema_version_is_str(self) -> None:
828 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
829 assert r.exit_code == 0, r.output
830 d = json.loads(r.output)
831 assert isinstance(d["schema"], int)
832
833 def test_agent_list_exit_code_is_int(self) -> None:
834 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
835 d = json.loads(r.output)
836 assert isinstance(d["exit_code"], int)
837
838 def test_agent_list_duration_ms_is_float(self) -> None:
839 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
840 d = json.loads(r.output)
841 assert isinstance(d["duration_ms"], float)
842
843 def test_agent_list_slots_is_list(self) -> None:
844 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
845 d = json.loads(r.output)
846 assert isinstance(d["slots"], list)
847
848 def test_agent_list_duration_ms_nonnegative(self) -> None:
849 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
850 d = json.loads(r.output)
851 assert d["duration_ms"] >= 0.0
852
853 def test_agent_config_init_schema_version_is_str(self, cfg_repo: pathlib.Path) -> None:
854 r = _invoke(cfg_repo, ["agent-config", "init", "--json"])
855 assert r.exit_code == 0, r.output
856 d = json.loads(r.output)
857 assert isinstance(d["schema"], int)
858
859 def test_agent_config_init_exit_code_is_int(self, cfg_repo: pathlib.Path) -> None:
860 r = _invoke(cfg_repo, ["agent-config", "init", "--json"])
861 d = json.loads(r.output)
862 assert isinstance(d["exit_code"], int)
863
864 def test_agent_config_init_duration_ms_is_float(self, cfg_repo: pathlib.Path) -> None:
865 r = _invoke(cfg_repo, ["agent-config", "init", "--json"])
866 d = json.loads(r.output)
867 assert isinstance(d["duration_ms"], float)
868
869 def test_agent_config_status_exit_code_mirrors_process(self, cfg_repo: pathlib.Path) -> None:
870 _invoke(cfg_repo, ["agent-config", "init"])
871 r = _invoke(cfg_repo, ["agent-config", "status", "--json"])
872 d = json.loads(r.output)
873 assert d["exit_code"] == r.exit_code
874
875 def test_agent_list_schema_version_nonempty(self) -> None:
876 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
877 d = json.loads(r.output)
878 assert d["schema"] > 0
879
880
881 # ===========================================================================
882 # Tier 7d — TestSecurity
883 # ===========================================================================
884
885
886 class TestSecurity:
887 """Hostile input must not crash or corrupt JSON output."""
888
889 def test_hostile_hub_url_in_emit_error_json(self) -> None:
890 from muse.cli.commands.agent import _emit_error
891 hostile = "'; DROP TABLE users; --"
892 stdout_buf = io.StringIO()
893 orig = sys.stdout
894 sys.stdout = stdout_buf
895 try:
896 try:
897 _emit_error("sql_test", hostile, True)
898 except SystemExit:
899 pass
900 finally:
901 sys.stdout = orig
902 # Must still be valid JSON
903 d = json.loads(stdout_buf.getvalue())
904 assert d["message"] == hostile
905
906 def test_sql_injection_in_slot_name(self) -> None:
907 """A slot name with SQL injection chars must not crash fingerprint or emit_error."""
908 from muse.core.types import public_key_fingerprint
909 malicious_name = b"'; DROP TABLE slots; --"
910 fp = public_key_fingerprint(malicious_name)
911 assert len(fp) == 71
912
913 def test_unicode_in_track_path_emit_error(self) -> None:
914 from muse.cli.commands.agent import _emit_error
915 unicode_path = "tracks/\u4e2d\u6587\u97f3\u4e50.mid"
916 stdout_buf = io.StringIO()
917 orig = sys.stdout
918 sys.stdout = stdout_buf
919 try:
920 try:
921 _emit_error("unicode_test", unicode_path, True)
922 except SystemExit:
923 pass
924 finally:
925 sys.stdout = orig
926 d = json.loads(stdout_buf.getvalue())
927 assert d["message"] == unicode_path
928
929 def test_very_long_account_name_in_emit_error(self) -> None:
930 from muse.cli.commands.agent import _emit_error
931 long_msg = "x" * 10_000
932 stdout_buf = io.StringIO()
933 orig = sys.stdout
934 sys.stdout = stdout_buf
935 try:
936 try:
937 _emit_error("long_test", long_msg, True)
938 except SystemExit:
939 pass
940 finally:
941 sys.stdout = orig
942 d = json.loads(stdout_buf.getvalue())
943 assert d["message"] == long_msg
944
945 def test_null_bytes_in_fingerprint_input(self) -> None:
946 from muse.core.types import public_key_fingerprint
947 data = b"\x00" * 64
948 fp = public_key_fingerprint(data)
949 assert len(fp) == 71
950
951 def test_newline_in_error_message_survives_json(self) -> None:
952 from muse.cli.commands.agent import _emit_error
953 newline_msg = "line one\nline two\nline three"
954 stdout_buf = io.StringIO()
955 orig = sys.stdout
956 sys.stdout = stdout_buf
957 try:
958 try:
959 _emit_error("newline_test", newline_msg, True)
960 except SystemExit:
961 pass
962 finally:
963 sys.stdout = orig
964 d = json.loads(stdout_buf.getvalue())
965 assert d["message"] == newline_msg
966
967 def test_agent_list_with_encoded_url_returns_json(self) -> None:
968 encoded_url = "http://test.example.com%2Fpath"
969 r = runner.invoke(None, ["agent", "list", "--hub", encoded_url, "--json"])
970 # Should either succeed with JSON or emit a JSON error — not a crash
971 output = r.output.strip()
972 assert output, "no output produced"
973 json.loads(output) # must be valid JSON regardless
974
975
976 # ===========================================================================
977 # Tier 7e — TestPerformance
978 # ===========================================================================
979
980
981 class TestPerformance:
982 """Timing bounds for performance-sensitive paths."""
983
984 def test_fingerprint_1000_under_500ms(self) -> None:
985 from muse.core.types import public_key_fingerprint
986 data = b"perf test input" * 4
987 start = time.perf_counter()
988 for _ in range(1000):
989 public_key_fingerprint(data)
990 elapsed_ms = (time.perf_counter() - start) * 1000
991 assert elapsed_ms < 500, f"1000 public_key_fingerprint calls took {elapsed_ms:.1f}ms"
992
993 def test_agent_config_status_completes_quickly(self, cfg_repo: pathlib.Path) -> None:
994 _invoke(cfg_repo, ["agent-config", "init"])
995 start = time.perf_counter()
996 r = _invoke(cfg_repo, ["agent-config", "status", "--json"])
997 elapsed_ms = (time.perf_counter() - start) * 1000
998 assert r.exit_code == 0, r.output
999 assert elapsed_ms < 2000, f"agent-config status took {elapsed_ms:.1f}ms"
1000
1001 def test_agent_list_completes_quickly(self) -> None:
1002 start = time.perf_counter()
1003 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
1004 elapsed_ms = (time.perf_counter() - start) * 1000
1005 assert r.exit_code == 0, r.output
1006 assert elapsed_ms < 2000, f"agent list took {elapsed_ms:.1f}ms"
1007
1008 def test_duration_ms_in_list_output_is_plausible(self) -> None:
1009 r = runner.invoke(None, ["agent", "list", "--hub", _TEST_HUB, "--json"])
1010 assert r.exit_code == 0, r.output
1011 d = json.loads(r.output)
1012 # duration_ms must be >= 0 and < 5000 (very generous upper bound)
1013 assert 0.0 <= d["duration_ms"] < 5000.0
1014
1015 def test_duration_ms_in_agent_config_init_is_plausible(self, cfg_repo: pathlib.Path) -> None:
1016 r = _invoke(cfg_repo, ["agent-config", "init", "--json"])
1017 assert r.exit_code == 0, r.output
1018 d = json.loads(r.output)
1019 assert 0.0 <= d["duration_ms"] < 5000.0
File History 1 commit
sha256:b5ec4e4a3a73cae0cd08224f32090f2a4836afa0a804cb3231e70c42a3e89295 fix adapter for agent config Human patch 15 days ago