gabriel / muse public
test_cmd_domains_hardening.py python
1,984 lines 82.0 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
1 """Comprehensive hardening tests for ``muse domains``.
2
3 Covers:
4 Unit tests:
5 - _validate_domain_name: valid, path-traversal, reserved, bad chars
6 - _validate_publish_url: http/https OK, file/ftp/data rejected
7 - _active_domain: no root, missing repo.json, explicit domain, missing key
8 - _build_entry: schema present, schema absent, active flag boolean
9 - _post_json: correct wire format, response size cap, non-object JSON
10 - _check_method / _run_validate_plugin: all checks pass, missing method
11
12 Integration tests:
13 - run (dashboard): text output, --json, --new validation + success
14 - run_info: known domain, unknown domain, --json schema
15 - run_use: no repo, unknown domain, known domain switches repo.json, --json
16 - run_validate: all-pass, missing method, --json, multi-domain
17
18 Security tests:
19 - Path traversal in --new rejected
20 - ANSI in domain name sanitized in dashboard
21 - file:// publish hub rejected
22 - Unsanitized server response never echoed raw
23
24 E2E tests (full CLI invocation via CliRunner):
25 - muse domains --json emits boolean 'active' field
26 - muse domains --json includes module_path
27 - muse domains info code --json schema present
28 - muse domains validate --json ok
29 - muse domains use code --json inside repo
30 - muse domains --new myplug creates directory
31
32 Stress tests:
33 - 8 concurrent validate calls on isolated registry snapshots
34 - 8 concurrent _active_domain reads on isolated tmp dirs
35 """
36 from __future__ import annotations
37
38 import argparse
39 import http.client
40 import json
41 import pathlib
42 import shutil
43 import threading
44 import urllib.error
45 import urllib.request
46 from contextlib import ExitStack
47 from typing import TYPE_CHECKING
48 from unittest.mock import MagicMock, patch
49
50 import pytest
51
52 from muse.cli.commands.domains import (
53 _DomainEntryJson,
54 _PublishResponse,
55 _ScaffoldJson,
56 _UseJson,
57 _ValidateJson,
58 )
59 from muse.domain import (
60 DriftReport,
61 LiveState,
62 MergeResult,
63 StateSnapshot,
64 StateDelta,
65 SnapshotManifest,
66 )
67 from muse.core.schema import DomainSchema
68 from tests.cli_test_helper import CliRunner, InvokeResult
69
70 if TYPE_CHECKING:
71 from muse.cli.commands.domains import _PublishPayload, _Capabilities, _DimensionDef
72 from muse.core.transport import SigningIdentity
73
74 cli = None # argparse migration — CliRunner ignores this argument
75
76 runner = CliRunner()
77
78 # ---------------------------------------------------------------------------
79 # JSON helpers — each returns a specific TypedDict to satisfy typing_audit
80 # ---------------------------------------------------------------------------
81
82
83 def _first_json_blob(result: InvokeResult) -> str:
84 """Return the first complete JSON blob string from result.output."""
85 output = result.output
86 depth = 0
87 start: int | None = None
88 for i, ch in enumerate(output):
89 if ch in "{[":
90 if start is None:
91 start = i
92 depth += 1
93 elif ch in "}]":
94 depth -= 1
95 if depth == 0 and start is not None:
96 return output[start : i + 1]
97 raise AssertionError(f"No JSON found in output:\n{output!r}")
98
99
100 def _parse_domains_list(result: InvokeResult) -> list[_DomainEntryJson]:
101 """Parse a JSON array of domain entries from result."""
102 parsed = json.loads(_first_json_blob(result))
103 if isinstance(parsed, dict) and "domains" in parsed:
104 parsed = parsed["domains"]
105 assert isinstance(parsed, list)
106 return parsed
107
108
109 def _parse_domain_entry(result: InvokeResult) -> _DomainEntryJson:
110 """Parse a single domain entry JSON dict from result."""
111 parsed: _DomainEntryJson = json.loads(_first_json_blob(result))
112 assert isinstance(parsed, dict)
113 return parsed
114
115
116 def _parse_scaffold(result: InvokeResult) -> _ScaffoldJson:
117 parsed: _ScaffoldJson = json.loads(_first_json_blob(result))
118 assert isinstance(parsed, dict)
119 return parsed
120
121
122 def _parse_use(result: InvokeResult) -> _UseJson:
123 parsed: _UseJson = json.loads(_first_json_blob(result))
124 assert isinstance(parsed, dict)
125 return parsed
126
127
128 def _parse_validate(result: InvokeResult) -> _ValidateJson:
129 parsed: _ValidateJson = json.loads(_first_json_blob(result))
130 assert isinstance(parsed, dict)
131 return parsed
132
133
134 def _parse_validate_list(result: InvokeResult) -> list[_ValidateJson]:
135 parsed = json.loads(_first_json_blob(result))
136 if isinstance(parsed, dict) and "results" in parsed:
137 parsed = parsed["results"]
138 assert isinstance(parsed, list)
139 return parsed
140
141
142 def _parse_publish(result: InvokeResult) -> _PublishResponse:
143 parsed: _PublishResponse = json.loads(_first_json_blob(result))
144 assert isinstance(parsed, dict)
145 return parsed
146
147
148 # ---------------------------------------------------------------------------
149 # Repository fixture
150 # ---------------------------------------------------------------------------
151
152
153 def _init_repo(tmp_path: pathlib.Path, domain: str = "code") -> pathlib.Path:
154 """Create a minimal .muse repo structure under tmp_path."""
155 muse = tmp_path / ".muse"
156 muse.mkdir(parents=True)
157 repo_json = {
158 "repo_id": "test-repo",
159 "schema_version": "0.1.5",
160 "created_at": "2026-01-01T00:00:00+00:00",
161 "domain": domain,
162 }
163 (muse / "repo.json").write_text(json.dumps(repo_json), encoding="utf-8")
164 return tmp_path
165
166
167 # ---------------------------------------------------------------------------
168 # Unit — _validate_domain_name
169 # ---------------------------------------------------------------------------
170
171
172 class TestValidateDomainName:
173 def _call(self, name: str) -> int:
174 from muse.cli.commands.domains import _validate_domain_name
175
176 with pytest.raises(SystemExit) as exc_info:
177 _validate_domain_name(name)
178 code = exc_info.value.code
179 assert isinstance(code, int)
180 return code
181
182 def test_valid_lowercase(self) -> None:
183 from muse.cli.commands.domains import _validate_domain_name
184
185 _validate_domain_name("genomics") # must not raise
186
187 def test_valid_with_hyphen(self) -> None:
188 from muse.cli.commands.domains import _validate_domain_name
189
190 _validate_domain_name("spatial-3d")
191
192 def test_valid_with_underscore(self) -> None:
193 from muse.cli.commands.domains import _validate_domain_name
194
195 _validate_domain_name("my_domain")
196
197 def test_path_traversal_dotdot_rejected(self) -> None:
198 assert self._call("../evil") == 1
199
200 def test_path_traversal_nested_rejected(self) -> None:
201 assert self._call("../../etc/passwd") == 1
202
203 def test_uppercase_rejected(self) -> None:
204 assert self._call("Genomics") == 1
205
206 def test_starts_with_digit_rejected(self) -> None:
207 assert self._call("3d-scenes") == 1
208
209 def test_slash_rejected(self) -> None:
210 assert self._call("evil/path") == 1
211
212 def test_null_byte_rejected(self) -> None:
213 assert self._call("evil\x00name") == 1
214
215 def test_space_rejected(self) -> None:
216 assert self._call("my domain") == 1
217
218 def test_reserved_scaffold_rejected(self) -> None:
219 assert self._call("scaffold") == 1
220
221 def test_max_length_64_accepted(self) -> None:
222 from muse.cli.commands.domains import _validate_domain_name
223
224 _validate_domain_name("a" * 64)
225
226 def test_max_length_65_rejected(self) -> None:
227 assert self._call("a" * 65) == 1
228
229 def test_empty_rejected(self) -> None:
230 assert self._call("") == 1
231
232
233 # ---------------------------------------------------------------------------
234 # Unit — _validate_publish_url
235 # ---------------------------------------------------------------------------
236
237
238 class TestValidatePublishUrl:
239 def _call(self, url: str) -> int:
240 from muse.cli.commands.domains import _validate_publish_url
241
242 with pytest.raises(SystemExit) as exc_info:
243 _validate_publish_url(url)
244 code = exc_info.value.code
245 assert isinstance(code, int)
246 return code
247
248 def test_https_ok(self) -> None:
249 from muse.cli.commands.domains import _validate_publish_url
250
251 _validate_publish_url("https://musehub.ai") # must not raise
252
253 def test_http_ok(self) -> None:
254 from muse.cli.commands.domains import _validate_publish_url
255
256 _validate_publish_url("https://localhost:1337")
257
258 def test_file_scheme_rejected(self) -> None:
259 assert self._call("file:///etc/passwd") == 1
260
261 def test_ftp_scheme_rejected(self) -> None:
262 assert self._call("ftp://evil.com") == 1
263
264 def test_data_uri_rejected(self) -> None:
265 assert self._call("data:text/plain,evil") == 1
266
267 def test_empty_scheme_rejected(self) -> None:
268 assert self._call("://evil") == 1
269
270
271 # ---------------------------------------------------------------------------
272 # Unit — _active_domain
273 # ---------------------------------------------------------------------------
274
275
276 class TestActiveDomain:
277 def test_none_root_returns_none(self) -> None:
278 from muse.cli.commands.domains import _active_domain
279
280 assert _active_domain(None) is None
281
282 def test_missing_repo_json_returns_none(self, tmp_path: pathlib.Path) -> None:
283 from muse.cli.commands.domains import _active_domain
284
285 (tmp_path / ".muse").mkdir()
286 assert _active_domain(tmp_path) is None
287
288 def test_explicit_domain_returned(self, tmp_path: pathlib.Path) -> None:
289 from muse.cli.commands.domains import _active_domain
290
291 _init_repo(tmp_path, domain="code")
292 assert _active_domain(tmp_path) == "code"
293
294 def test_missing_domain_key_returns_default(self, tmp_path: pathlib.Path) -> None:
295 from muse.cli.commands.domains import _active_domain, _DEFAULT_DOMAIN
296
297 muse = tmp_path / ".muse"
298 muse.mkdir()
299 (muse / "repo.json").write_text('{"repo_id": "x"}', encoding="utf-8")
300 assert _active_domain(tmp_path) == _DEFAULT_DOMAIN
301
302 def test_corrupt_json_returns_none(self, tmp_path: pathlib.Path) -> None:
303 from muse.cli.commands.domains import _active_domain
304
305 muse = tmp_path / ".muse"
306 muse.mkdir()
307 (muse / "repo.json").write_text("NOT JSON", encoding="utf-8")
308 assert _active_domain(tmp_path) is None
309
310 def test_no_midi_fallback(self, tmp_path: pathlib.Path) -> None:
311 """The old 'midi' fallback is gone — default is _DEFAULT_DOMAIN ('code')."""
312 from muse.cli.commands.domains import _active_domain
313
314 muse = tmp_path / ".muse"
315 muse.mkdir()
316 (muse / "repo.json").write_text('{"domain": ""}', encoding="utf-8")
317 result = _active_domain(tmp_path)
318 assert result != "midi"
319
320
321 # ---------------------------------------------------------------------------
322 # Unit — _build_entry
323 # ---------------------------------------------------------------------------
324
325
326 class TestBuildEntry:
327 def test_schema_present(self) -> None:
328 from muse.cli.commands.domains import _build_entry
329 from muse.plugins.registry import _REGISTRY
330
331 plugin = _REGISTRY["code"]
332 entry = _build_entry("code", plugin, "code")
333 assert entry["domain"] == "code"
334 assert entry["active"] is True
335 assert isinstance(entry["active"], bool)
336 assert "schema" in entry
337 schema = entry["schema"]
338 assert "schema_version" in schema
339 assert "dimensions" in schema
340
341 def test_schema_absent_when_not_implemented(self) -> None:
342 from muse.cli.commands.domains import _build_entry
343
344 mock_plugin = MagicMock()
345 mock_plugin.schema.side_effect = NotImplementedError
346 mock_plugin.__class__ = type("FakeDomain", (), {})
347 entry = _build_entry("fake", mock_plugin, None)
348 assert "schema" not in entry
349 assert entry["active"] is False
350
351 def test_module_path_included(self) -> None:
352 from muse.cli.commands.domains import _build_entry
353 from muse.plugins.registry import _REGISTRY
354
355 plugin = _REGISTRY["scaffold"]
356 entry = _build_entry("scaffold", plugin, None)
357 assert entry["module_path"] == "plugins/scaffold/plugin.py"
358
359 def test_active_false_for_inactive(self) -> None:
360 from muse.cli.commands.domains import _build_entry
361 from muse.plugins.registry import _REGISTRY
362
363 plugin = _REGISTRY["code"]
364 entry = _build_entry("code", plugin, "scaffold")
365 assert entry["active"] is False
366
367 def test_capabilities_list_of_strings(self) -> None:
368 from muse.cli.commands.domains import _build_entry
369 from muse.plugins.registry import _REGISTRY
370
371 plugin = _REGISTRY["code"]
372 entry = _build_entry("code", plugin, None)
373 caps = entry["capabilities"]
374 assert isinstance(caps, list)
375 assert all(isinstance(c, str) for c in caps)
376 assert "Typed Deltas" in caps
377
378
379 # ---------------------------------------------------------------------------
380 # Unit — _post_json
381 # ---------------------------------------------------------------------------
382
383
384 def _make_caps_payload() -> "_PublishPayload":
385 from muse.cli.commands.domains import _PublishPayload, _Capabilities, _DimensionDef
386
387 caps = _Capabilities(
388 dimensions=[_DimensionDef(name="notes", description="Note events")],
389 artifact_types=["mid"],
390 merge_semantics="three_way",
391 supported_commands=["commit"],
392 )
393 return _PublishPayload(
394 author_slug="user",
395 slug="music",
396 display_name="Music",
397 description="Desc",
398 capabilities=caps,
399 viewer_type="midi",
400 version="0.1.0",
401 )
402
403
404 def _make_signing() -> "SigningIdentity":
405 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
406 from muse.core.transport import SigningIdentity
407 return SigningIdentity(handle="testuser", private_key=Ed25519PrivateKey.generate())
408
409
410 class TestPostJson:
411 def test_correct_method_and_headers(self) -> None:
412 from muse.cli.commands.domains import _post_json
413
414 captured: list[urllib.request.Request] = []
415
416 def fake_urlopen(req: urllib.request.Request, timeout: float) -> MagicMock:
417 captured.append(req)
418 resp = MagicMock()
419 resp.read = MagicMock(return_value=b'{"domain_id":"1","scoped_id":"@u/s","manifest_hash":"abc"}')
420 resp.__enter__ = lambda s: s
421 resp.__exit__ = MagicMock(return_value=False)
422 return resp
423
424 with patch("urllib.request.urlopen", fake_urlopen):
425 result = _post_json("https://musehub.ai/api/v1/domains", _make_caps_payload(), _make_signing())
426
427 assert len(captured) == 1
428 req = captured[0]
429 assert req.get_method() == "POST"
430 assert req.get_header("Content-type") == "application/json"
431 assert req.get_header("Authorization").startswith("MSign ")
432 assert result["scoped_id"] == "@u/s"
433
434 def test_response_size_cap_applied(self) -> None:
435 """resp.read() is called with _MAX_RESPONSE_BYTES, not unlimited."""
436 from muse.cli.commands.domains import _post_json, _MAX_RESPONSE_BYTES
437
438 read_args: list[int] = []
439
440 def fake_urlopen(req: urllib.request.Request, timeout: float) -> MagicMock:
441 resp = MagicMock()
442
443 def _read(n: int) -> bytes:
444 read_args.append(n)
445 return b'{"domain_id":"1","scoped_id":"@u/s","manifest_hash":""}'
446
447 resp.read = _read
448 resp.__enter__ = lambda s: s
449 resp.__exit__ = MagicMock(return_value=False)
450 return resp
451
452 with patch("urllib.request.urlopen", fake_urlopen):
453 _post_json("https://musehub.ai/api/v1/domains", _make_caps_payload(), _make_signing())
454
455 assert read_args == [_MAX_RESPONSE_BYTES]
456
457 def test_non_object_json_raises_value_error(self) -> None:
458 from muse.cli.commands.domains import _post_json
459
460 def fake_urlopen(req: urllib.request.Request, timeout: float) -> MagicMock:
461 resp = MagicMock()
462 resp.read = MagicMock(return_value=b"[1,2,3]")
463 resp.__enter__ = lambda s: s
464 resp.__exit__ = MagicMock(return_value=False)
465 return resp
466
467 with patch("urllib.request.urlopen", fake_urlopen):
468 with pytest.raises(ValueError, match="Expected JSON object"):
469 _post_json("https://musehub.ai/api/v1/domains", _make_caps_payload(), _make_signing())
470
471 def test_missing_keys_normalised_to_empty_string(self) -> None:
472 from muse.cli.commands.domains import _post_json
473
474 def fake_urlopen(req: urllib.request.Request, timeout: float) -> MagicMock:
475 resp = MagicMock()
476 resp.read = MagicMock(return_value=b"{}")
477 resp.__enter__ = lambda s: s
478 resp.__exit__ = MagicMock(return_value=False)
479 return resp
480
481 with patch("urllib.request.urlopen", fake_urlopen):
482 result = _post_json("https://musehub.ai/api/v1/domains", _make_caps_payload(), _make_signing())
483
484 assert result["domain_id"] == ""
485 assert result["scoped_id"] == ""
486 assert result["manifest_hash"] == ""
487
488
489 # ---------------------------------------------------------------------------
490 # Unit — _run_validate_plugin
491 # ---------------------------------------------------------------------------
492
493
494 class _MinimalPlugin:
495 """Stub implementing all required MuseDomainPlugin methods; schema raises NotImplementedError."""
496
497 def snapshot(self, live_state: LiveState) -> StateSnapshot:
498 raise NotImplementedError
499
500 def diff(
501 self,
502 base: StateSnapshot,
503 target: StateSnapshot,
504 *,
505 repo_root: pathlib.Path | None = None,
506 ) -> StateDelta:
507 raise NotImplementedError
508
509 def merge(
510 self,
511 base: StateSnapshot,
512 left: StateSnapshot,
513 right: StateSnapshot,
514 *,
515 repo_root: pathlib.Path | None = None,
516 ) -> MergeResult:
517 raise NotImplementedError
518
519 def drift(self, committed: StateSnapshot, live: LiveState) -> DriftReport:
520 raise NotImplementedError
521
522 def apply(self, delta: StateDelta, live_state: LiveState) -> LiveState:
523 raise NotImplementedError
524
525 def schema(self) -> DomainSchema:
526 raise NotImplementedError
527
528
529 class TestRunValidatePlugin:
530 def test_all_checks_pass_for_code_plugin(self) -> None:
531 from muse.cli.commands.domains import _run_validate_plugin
532 from muse.plugins.registry import _REGISTRY
533
534 result = _run_validate_plugin("code", _REGISTRY["code"], None)
535 assert result["ok"] is True
536 assert result["domain"] == "code"
537 names = [c["name"] for c in result["checks"]]
538 assert "has_method:snapshot" in names
539 assert "schema()" in names
540
541 def test_schema_not_implemented_flagged(self) -> None:
542 """A plugin that has all methods but raises NotImplementedError for schema()
543 must have the schema check fail while all method checks pass."""
544 from muse.cli.commands.domains import _run_validate_plugin
545
546 result = _run_validate_plugin("min", _MinimalPlugin(), None)
547 schema_check = next(c for c in result["checks"] if c["name"] == "schema()")
548 assert schema_check["ok"] is False
549 # All protocol method checks should pass since methods exist and are callable
550 method_checks = [c for c in result["checks"] if c["name"].startswith("has_method:")]
551 assert all(c["ok"] for c in method_checks)
552
553 def test_scaffold_plugin_all_pass(self) -> None:
554 from muse.cli.commands.domains import _run_validate_plugin
555 from muse.plugins.registry import _REGISTRY
556
557 result = _run_validate_plugin("scaffold", _REGISTRY["scaffold"], None)
558 assert result["ok"] is True, [c for c in result["checks"] if not c["ok"]]
559
560
561 # ---------------------------------------------------------------------------
562 # Integration — muse domains (dashboard + --json)
563 # ---------------------------------------------------------------------------
564
565
566 class TestDomainsDashboard:
567 def test_default_text_output_has_registered_domains(self) -> None:
568 result = runner.invoke(cli, ["domains"])
569 assert result.exit_code == 0
570 assert "Registered domains:" in result.output
571
572 def test_json_flag_emits_list(self) -> None:
573 result = runner.invoke(cli, ["domains", "--json"])
574 assert result.exit_code == 0
575 domains = _parse_domains_list(result)
576 assert len(domains) >= 1
577
578 def test_json_active_field_is_boolean(self) -> None:
579 result = runner.invoke(cli, ["domains", "--json"])
580 assert result.exit_code == 0
581 for entry in _parse_domains_list(result):
582 assert isinstance(entry.get("active"), bool), (
583 f"'active' must be bool, got {type(entry.get('active')).__name__}"
584 )
585
586 def test_json_module_path_present(self) -> None:
587 result = runner.invoke(cli, ["domains", "--json"])
588 assert result.exit_code == 0
589 for entry in _parse_domains_list(result):
590 assert "module_path" in entry, f"missing 'module_path' in {entry}"
591
592 def test_json_schema_present_for_code(self) -> None:
593 result = runner.invoke(cli, ["domains", "--json"])
594 assert result.exit_code == 0
595 code_entry = next(e for e in _parse_domains_list(result) if e.get("domain") == "code")
596 assert "schema" in code_entry
597 schema = code_entry["schema"]
598 assert "dimensions" in schema
599
600 def test_json_no_string_true_false(self) -> None:
601 """Agents must never see 'active': 'true' — only 'active': true."""
602 result = runner.invoke(cli, ["domains", "--json"])
603 raw = result.output
604 assert '"active": "true"' not in raw
605 assert '"active": "false"' not in raw
606
607 def test_text_has_muse_domains_new_hint(self) -> None:
608 result = runner.invoke(cli, ["domains"])
609 assert "muse domains --new" in result.output
610
611 def test_text_has_info_hint(self) -> None:
612 result = runner.invoke(cli, ["domains"])
613 assert "muse domains info" in result.output
614
615 def test_text_has_validate_hint(self) -> None:
616 result = runner.invoke(cli, ["domains"])
617 assert "muse domains validate" in result.output
618
619
620 # ---------------------------------------------------------------------------
621 # Integration — muse domains --new
622 # ---------------------------------------------------------------------------
623
624
625 class TestDomainsNew:
626 def test_valid_name_creates_directory(self) -> None:
627 plugins_dir = pathlib.Path(__file__).parents[1] / "muse" / "plugins"
628 dest = plugins_dir / "testdomain9"
629 try:
630 result = runner.invoke(cli, ["domains", "--new", "testdomain9"])
631 assert result.exit_code == 0, result.output
632 assert dest.exists()
633 assert (dest / "plugin.py").exists()
634 finally:
635 if dest.exists():
636 shutil.rmtree(str(dest))
637
638 def test_valid_name_json_flag_emits_scaffold_json(self) -> None:
639 plugins_dir = pathlib.Path(__file__).parents[1] / "muse" / "plugins"
640 dest = plugins_dir / "testdomain10"
641 try:
642 result = runner.invoke(cli, ["domains", "--new", "testdomain10", "--json"])
643 assert result.exit_code == 0, result.output
644 data = _parse_scaffold(result)
645 assert data["name"] == "testdomain10"
646 assert data["status"] == "ok"
647 assert "class_name" in data
648 assert "path" in data
649 finally:
650 if dest.exists():
651 shutil.rmtree(str(dest))
652
653 def test_path_traversal_rejected(self) -> None:
654 result = runner.invoke(cli, ["domains", "--new", "../evil"])
655 assert result.exit_code != 0
656
657 def test_uppercase_name_rejected(self) -> None:
658 result = runner.invoke(cli, ["domains", "--new", "Genomics"])
659 assert result.exit_code != 0
660
661 def test_scaffold_reserved_rejected(self) -> None:
662 result = runner.invoke(cli, ["domains", "--new", "scaffold"])
663 assert result.exit_code != 0
664
665 def test_duplicate_name_rejected(self) -> None:
666 result = runner.invoke(cli, ["domains", "--new", "code"])
667 assert result.exit_code != 0
668
669 def test_no_pycache_in_created_directory(self) -> None:
670 plugins_dir = pathlib.Path(__file__).parents[1] / "muse" / "plugins"
671 dest = plugins_dir / "testdomain11"
672 try:
673 result = runner.invoke(cli, ["domains", "--new", "testdomain11"])
674 assert result.exit_code == 0
675 pycache = dest / "__pycache__"
676 assert not pycache.exists(), "__pycache__ must not be copied"
677 finally:
678 if dest.exists():
679 shutil.rmtree(str(dest))
680
681 def test_class_name_substituted(self) -> None:
682 plugins_dir = pathlib.Path(__file__).parents[1] / "muse" / "plugins"
683 dest = plugins_dir / "testdomain12"
684 try:
685 runner.invoke(cli, ["domains", "--new", "testdomain12"])
686 plugin_src = (dest / "plugin.py").read_text(encoding="utf-8")
687 assert "Testdomain12Plugin" in plugin_src
688 assert "ScaffoldPlugin" not in plugin_src
689 finally:
690 if dest.exists():
691 shutil.rmtree(str(dest))
692
693
694 # ---------------------------------------------------------------------------
695 # Integration — muse domains info
696 # ---------------------------------------------------------------------------
697
698
699 class TestDomainsInfo:
700 def test_known_domain_exits_zero(self) -> None:
701 result = runner.invoke(cli, ["domains", "info", "code"])
702 assert result.exit_code == 0
703
704 def test_unknown_domain_exits_nonzero(self) -> None:
705 result = runner.invoke(cli, ["domains", "info", "doesnotexist"])
706 assert result.exit_code != 0
707
708 def test_json_schema_has_required_keys(self) -> None:
709 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
710 assert result.exit_code == 0
711 data = _parse_domain_entry(result)
712 assert data["domain"] == "code"
713 assert isinstance(data["active"], bool)
714 assert "capabilities" in data
715 assert "module_path" in data
716
717 def test_scaffold_has_crdt_capability(self) -> None:
718 result = runner.invoke(cli, ["domains", "info", "scaffold", "--json"])
719 assert result.exit_code == 0
720 data = _parse_domain_entry(result)
721 assert "CRDT" in data["capabilities"]
722
723 def test_text_output_includes_module_path(self) -> None:
724 result = runner.invoke(cli, ["domains", "info", "code"])
725 assert "Module:" in result.output
726 assert "plugins/code/plugin.py" in result.output
727
728 def test_error_on_unknown_domain(self) -> None:
729 result = runner.invoke(cli, ["domains", "info", "ghost"])
730 assert result.exit_code != 0
731 assert "not registered" in result.output
732
733
734 # ---------------------------------------------------------------------------
735 # Integration — muse domains use
736 # ---------------------------------------------------------------------------
737
738
739 class TestDomainsUse:
740 def test_no_repo_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
741 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
742 result = runner.invoke(cli, ["domains", "use", "code"])
743 assert result.exit_code != 0
744
745 def test_unknown_domain_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
746 _init_repo(tmp_path)
747 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
748 result = runner.invoke(cli, ["domains", "use", "doesnotexist"])
749 assert result.exit_code != 0
750
751 def test_known_domain_switches_repo_json(self, tmp_path: pathlib.Path) -> None:
752 _init_repo(tmp_path, domain="scaffold")
753 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
754 result = runner.invoke(cli, ["domains", "use", "code"])
755 assert result.exit_code == 0, result.output
756 data: Manifest = json.loads((tmp_path / ".muse" / "repo.json").read_text(encoding="utf-8"))
757 assert data["domain"] == "code"
758
759 def test_switch_is_idempotent(self, tmp_path: pathlib.Path) -> None:
760 _init_repo(tmp_path, domain="code")
761 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
762 result = runner.invoke(cli, ["domains", "use", "code"])
763 assert result.exit_code == 0
764
765 def test_json_output_has_required_keys(self, tmp_path: pathlib.Path) -> None:
766 _init_repo(tmp_path, domain="scaffold")
767 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
768 result = runner.invoke(cli, ["domains", "use", "code", "--json"])
769 assert result.exit_code == 0, result.output
770 data = _parse_use(result)
771 assert data["domain"] == "code"
772 assert data["status"] == "switched"
773 assert "repo" in data
774
775 def test_existing_repo_fields_preserved(self, tmp_path: pathlib.Path) -> None:
776 _init_repo(tmp_path, domain="scaffold")
777 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
778 runner.invoke(cli, ["domains", "use", "code"])
779 data: Manifest = json.loads((tmp_path / ".muse" / "repo.json").read_text(encoding="utf-8"))
780 assert "repo_id" in data
781 assert "schema_version" in data
782
783
784 # ---------------------------------------------------------------------------
785 # Integration — muse domains validate
786 # ---------------------------------------------------------------------------
787
788
789 class TestDomainsValidate:
790 def test_code_plugin_passes(self) -> None:
791 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
792 result = runner.invoke(cli, ["domains", "validate", "code"])
793 assert result.exit_code == 0
794
795 def test_scaffold_plugin_passes(self) -> None:
796 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
797 result = runner.invoke(cli, ["domains", "validate", "scaffold"])
798 assert result.exit_code == 0
799
800 def test_unknown_domain_exits_nonzero(self) -> None:
801 result = runner.invoke(cli, ["domains", "validate", "ghost"])
802 assert result.exit_code != 0
803
804 def test_json_ok_field_is_boolean(self) -> None:
805 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
806 result = runner.invoke(cli, ["domains", "validate", "code", "--json"])
807 assert result.exit_code == 0
808 data = _parse_validate(result)
809 assert isinstance(data["ok"], bool)
810 assert data["ok"] is True
811
812 def test_json_checks_list_present(self) -> None:
813 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
814 result = runner.invoke(cli, ["domains", "validate", "scaffold", "--json"])
815 data = _parse_validate(result)
816 assert isinstance(data["checks"], list)
817 assert len(data["checks"]) >= 5
818
819 def test_broken_plugin_exits_nonzero(self) -> None:
820 from muse.plugins.registry import _REGISTRY
821
822 mock_plugin = MagicMock(spec=[]) # no methods at all
823 with patch.dict(_REGISTRY, {"broken": mock_plugin}):
824 result = runner.invoke(cli, ["domains", "validate", "broken"])
825 assert result.exit_code != 0
826
827 def test_no_name_validates_active_repo_domain(self, tmp_path: pathlib.Path) -> None:
828 _init_repo(tmp_path, domain="code")
829 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
830 result = runner.invoke(cli, ["domains", "validate"])
831 assert result.exit_code == 0
832 assert "code" in result.output
833
834 def test_no_name_no_repo_validates_all(self) -> None:
835 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
836 result = runner.invoke(cli, ["domains", "validate", "--json"])
837 assert result.exit_code == 0
838 from muse.plugins.registry import _REGISTRY
839 if len(_REGISTRY) > 1:
840 _parse_validate_list(result)
841 else:
842 _parse_validate(result)
843
844
845 # ---------------------------------------------------------------------------
846 # Security
847 # ---------------------------------------------------------------------------
848
849
850 class TestDomainsSecurity:
851 def test_ansi_in_domain_name_sanitized_in_dashboard(self) -> None:
852 """A registry entry with ANSI in its name must not bleed into output."""
853 from muse.plugins.registry import _REGISTRY
854 from muse.plugins.scaffold.plugin import ScaffoldPlugin
855
856 evil_name = "\x1b[31mevil\x1b[0m"
857 mock = ScaffoldPlugin()
858 with patch.dict(_REGISTRY, {evil_name: mock}):
859 result = runner.invoke(cli, ["domains"])
860 assert "\x1b[31m" not in result.output
861
862 def test_file_scheme_publish_hub_rejected(self, tmp_path: pathlib.Path) -> None:
863 """file:// hub URL must be blocked before any network call."""
864 urlopen_called = False
865
866 def _urlopen(req: urllib.request.Request, timeout: float) -> MagicMock:
867 nonlocal urlopen_called
868 urlopen_called = True
869 raise AssertionError("urlopen must not be called with file:// URL")
870
871 _init_repo(tmp_path, domain="code")
872
873 with ExitStack() as stack:
874 stack.enter_context(patch("urllib.request.urlopen", _urlopen))
875 stack.enter_context(
876 patch("muse.cli.commands.domains.find_repo_root", return_value=tmp_path)
877 )
878 stack.enter_context(
879 patch("muse.cli.commands.domains.get_signing_identity", return_value="tok")
880 )
881 result = runner.invoke(cli, [
882 "domains", "publish",
883 "--author", "user", "--slug", "music",
884 "--name", "Music", "--description", "Desc",
885 "--viewer-type", "midi",
886 "--hub", "file:///etc/passwd",
887 "--capabilities", '{"dimensions":[],"artifact_types":[],"merge_semantics":"three_way","supported_commands":[]}',
888 ])
889
890 assert result.exit_code != 0
891 assert not urlopen_called
892
893 def test_server_ansi_in_scoped_id_sanitized(self, tmp_path: pathlib.Path) -> None:
894 """Server-returned scoped_id with ANSI (unicode-escaped in JSON) must not appear raw."""
895 _init_repo(tmp_path, domain="code")
896 # JSON uses \u001b for the ESC character — valid JSON but contains ANSI.
897 evil_response = b'{"domain_id":"1","scoped_id":"\\u001b[31mhacked\\u001b[0m","manifest_hash":""}'
898
899 def fake_urlopen(req: urllib.request.Request, timeout: float) -> MagicMock:
900 resp = MagicMock()
901 resp.read = MagicMock(return_value=evil_response)
902 resp.__enter__ = lambda s: s
903 resp.__exit__ = MagicMock(return_value=False)
904 return resp
905
906 with ExitStack() as stack:
907 stack.enter_context(patch("urllib.request.urlopen", fake_urlopen))
908 stack.enter_context(
909 patch("muse.cli.commands.domains.find_repo_root", return_value=tmp_path)
910 )
911 stack.enter_context(
912 patch("muse.cli.commands.domains.get_signing_identity", return_value=_make_signing())
913 )
914 result = runner.invoke(cli, [
915 "domains", "publish",
916 "--author", "user", "--slug", "music",
917 "--name", "Music", "--description", "Desc",
918 "--viewer-type", "midi",
919 "--capabilities", '{"dimensions":[],"artifact_types":[],"merge_semantics":"three_way","supported_commands":[]}',
920 ])
921
922 assert result.exit_code == 0
923 assert "\x1b[31m" not in result.output
924
925 def test_path_traversal_new_rejected_without_touching_fs(self) -> None:
926 evil = "../" * 5 + "evil"
927 evil_path = pathlib.Path(__file__).parents[1] / "muse" / "plugins" / evil
928 result = runner.invoke(cli, ["domains", "--new", evil])
929 assert result.exit_code != 0
930 assert not evil_path.exists()
931
932 def test_null_byte_name_rejected(self) -> None:
933 result = runner.invoke(cli, ["domains", "--new", "evil\x00name"])
934 assert result.exit_code != 0
935
936
937 # ---------------------------------------------------------------------------
938 # E2E — verify the muse CLI entry point wires domains correctly
939 # ---------------------------------------------------------------------------
940
941
942 class TestDomainsE2E:
943 def test_domains_help_exits_zero(self) -> None:
944 result = runner.invoke(cli, ["domains", "--help"])
945 assert result.exit_code == 0
946 assert "domains" in result.output.lower()
947
948 def test_domains_info_help(self) -> None:
949 result = runner.invoke(cli, ["domains", "info", "--help"])
950 assert result.exit_code == 0
951
952 def test_domains_use_help(self) -> None:
953 result = runner.invoke(cli, ["domains", "use", "--help"])
954 assert result.exit_code == 0
955
956 def test_domains_validate_help(self) -> None:
957 result = runner.invoke(cli, ["domains", "validate", "--help"])
958 assert result.exit_code == 0
959
960 def test_domains_publish_help(self) -> None:
961 result = runner.invoke(cli, ["domains", "publish", "--help"])
962 assert result.exit_code == 0
963
964 def test_domains_json_is_valid_json(self) -> None:
965 result = runner.invoke(cli, ["domains", "--json"])
966 assert result.exit_code == 0
967 domains = _parse_domains_list(result)
968 assert len(domains) >= 1
969
970 def test_info_json_is_valid_json(self) -> None:
971 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
972 assert result.exit_code == 0
973 _parse_domain_entry(result) # must not raise
974
975 def test_validate_json_is_valid_json(self) -> None:
976 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
977 result = runner.invoke(cli, ["domains", "validate", "code", "--json"])
978 assert result.exit_code == 0
979 _parse_validate(result)
980
981 def test_use_requires_name_arg(self) -> None:
982 result = runner.invoke(cli, ["domains", "use"])
983 assert result.exit_code != 0
984
985
986 # ---------------------------------------------------------------------------
987 # Publish E2E — happy path and error paths
988 # ---------------------------------------------------------------------------
989
990
991 class TestPublishE2E:
992 _CAPS = json.dumps({
993 "dimensions": [{"name": "notes", "description": "Note events"}],
994 "artifact_types": ["mid"],
995 "merge_semantics": "three_way",
996 "supported_commands": ["commit", "diff"],
997 })
998
999 _BASE_ARGS = [
1000 "domains", "publish",
1001 "--author", "user", "--slug", "music",
1002 "--name", "Music", "--description", "Desc",
1003 "--viewer-type", "midi",
1004 "--capabilities", _CAPS,
1005 ]
1006
1007 def test_successful_publish_text_output(self, tmp_path: pathlib.Path) -> None:
1008 _init_repo(tmp_path, domain="code")
1009 fake_resp = _PublishResponse(domain_id="1", scoped_id="@user/music", manifest_hash="abc")
1010
1011 with ExitStack() as stack:
1012 stack.enter_context(
1013 patch("muse.cli.commands.domains.find_repo_root", return_value=tmp_path)
1014 )
1015 stack.enter_context(
1016 patch("muse.cli.commands.domains.get_signing_identity", return_value="tok")
1017 )
1018 stack.enter_context(
1019 patch("muse.cli.commands.domains._post_json", return_value=fake_resp)
1020 )
1021 result = runner.invoke(cli, self._BASE_ARGS)
1022
1023 assert result.exit_code == 0, result.output
1024 assert "@user/music" in result.output
1025
1026 def test_successful_publish_json_output(self, tmp_path: pathlib.Path) -> None:
1027 _init_repo(tmp_path, domain="code")
1028 fake_resp = _PublishResponse(domain_id="1", scoped_id="@user/music", manifest_hash="abc")
1029
1030 with ExitStack() as stack:
1031 stack.enter_context(
1032 patch("muse.cli.commands.domains.find_repo_root", return_value=tmp_path)
1033 )
1034 stack.enter_context(
1035 patch("muse.cli.commands.domains.get_signing_identity", return_value="tok")
1036 )
1037 stack.enter_context(
1038 patch("muse.cli.commands.domains._post_json", return_value=fake_resp)
1039 )
1040 result = runner.invoke(cli, [*self._BASE_ARGS, "--json"])
1041
1042 assert result.exit_code == 0
1043 data = _parse_publish(result)
1044 assert data["scoped_id"] == "@user/music"
1045
1046 def test_no_token_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
1047 _init_repo(tmp_path, domain="code")
1048 with ExitStack() as stack:
1049 stack.enter_context(
1050 patch("muse.cli.commands.domains.find_repo_root", return_value=tmp_path)
1051 )
1052 stack.enter_context(
1053 patch("muse.cli.commands.domains.get_signing_identity", return_value=None)
1054 )
1055 result = runner.invoke(cli, self._BASE_ARGS)
1056
1057 assert result.exit_code != 0
1058
1059 def test_http_409_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
1060 _init_repo(tmp_path, domain="code")
1061 exc = urllib.error.HTTPError(
1062 url="", code=409, msg="Conflict",
1063 hdrs=http.client.HTTPMessage(), fp=None,
1064 )
1065
1066 with ExitStack() as stack:
1067 stack.enter_context(
1068 patch("muse.cli.commands.domains.find_repo_root", return_value=tmp_path)
1069 )
1070 stack.enter_context(
1071 patch("muse.cli.commands.domains.get_signing_identity", return_value="tok")
1072 )
1073 stack.enter_context(
1074 patch("muse.cli.commands.domains._post_json", side_effect=exc)
1075 )
1076 result = runner.invoke(cli, self._BASE_ARGS)
1077
1078 assert result.exit_code != 0
1079
1080 def test_http_401_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
1081 _init_repo(tmp_path, domain="code")
1082 exc = urllib.error.HTTPError(
1083 url="", code=401, msg="Unauthorized",
1084 hdrs=http.client.HTTPMessage(), fp=None,
1085 )
1086
1087 with ExitStack() as stack:
1088 stack.enter_context(
1089 patch("muse.cli.commands.domains.find_repo_root", return_value=tmp_path)
1090 )
1091 stack.enter_context(
1092 patch("muse.cli.commands.domains.get_signing_identity", return_value="tok")
1093 )
1094 stack.enter_context(
1095 patch("muse.cli.commands.domains._post_json", side_effect=exc)
1096 )
1097 result = runner.invoke(cli, self._BASE_ARGS)
1098
1099 assert result.exit_code != 0
1100
1101 def test_url_error_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
1102 _init_repo(tmp_path, domain="code")
1103 exc = urllib.error.URLError("connection refused")
1104
1105 with ExitStack() as stack:
1106 stack.enter_context(
1107 patch("muse.cli.commands.domains.find_repo_root", return_value=tmp_path)
1108 )
1109 stack.enter_context(
1110 patch("muse.cli.commands.domains.get_signing_identity", return_value="tok")
1111 )
1112 stack.enter_context(
1113 patch("muse.cli.commands.domains._post_json", side_effect=exc)
1114 )
1115 result = runner.invoke(cli, self._BASE_ARGS)
1116
1117 assert result.exit_code != 0
1118
1119
1120 # ---------------------------------------------------------------------------
1121 # Stress tests
1122 # ---------------------------------------------------------------------------
1123
1124
1125 class TestStress:
1126 def test_8_concurrent_validate_calls_isolated(self) -> None:
1127 """Concurrent validate calls on independent plugin instances must not interfere."""
1128 from muse.cli.commands.domains import _run_validate_plugin
1129 from muse.plugins.registry import _REGISTRY
1130
1131 errors: list[str] = []
1132
1133 def _validate(idx: int) -> None:
1134 try:
1135 plugin = _REGISTRY["code"]
1136 result = _run_validate_plugin("code", plugin, None)
1137 assert result["ok"] is True, f"Thread {idx}: validate failed"
1138 except Exception as exc:
1139 errors.append(f"Thread {idx}: {exc}")
1140
1141 threads = [threading.Thread(target=_validate, args=(i,)) for i in range(8)]
1142 for t in threads:
1143 t.start()
1144 for t in threads:
1145 t.join()
1146 assert not errors, f"Concurrent validate failures: {errors}"
1147
1148 def test_8_concurrent_active_domain_reads(self, tmp_path: pathlib.Path) -> None:
1149 """Concurrent _active_domain reads on isolated dirs must all return correctly."""
1150 from muse.cli.commands.domains import _active_domain
1151
1152 roots: list[pathlib.Path] = []
1153 for i in range(8):
1154 rd = tmp_path / f"repo{i}"
1155 _init_repo(rd, domain="code")
1156 roots.append(rd)
1157
1158 errors: list[str] = []
1159
1160 def _read(idx: int) -> None:
1161 try:
1162 result = _active_domain(roots[idx])
1163 assert result == "code", f"Thread {idx}: expected 'code', got {result!r}"
1164 except Exception as exc:
1165 errors.append(f"Thread {idx}: {exc}")
1166
1167 threads = [threading.Thread(target=_read, args=(i,)) for i in range(8)]
1168 for t in threads:
1169 t.start()
1170 for t in threads:
1171 t.join()
1172 assert not errors, f"Concurrent read failures: {errors}"
1173
1174 def test_8_concurrent_build_entry_calls(self) -> None:
1175 """_build_entry must be re-entrant (no shared mutable state)."""
1176 from muse.cli.commands.domains import _build_entry
1177 from muse.plugins.registry import _REGISTRY
1178
1179 errors: list[str] = []
1180
1181 def _build(idx: int) -> None:
1182 try:
1183 plugin = _REGISTRY["scaffold"]
1184 entry = _build_entry("scaffold", plugin, "code" if idx % 2 == 0 else "scaffold")
1185 assert entry["domain"] == "scaffold"
1186 assert isinstance(entry["active"], bool)
1187 except Exception as exc:
1188 errors.append(f"Thread {idx}: {exc}")
1189
1190 threads = [threading.Thread(target=_build, args=(i,)) for i in range(8)]
1191 for t in threads:
1192 t.start()
1193 for t in threads:
1194 t.join()
1195 assert not errors, f"Concurrent build_entry failures: {errors}"
1196
1197
1198 # ---------------------------------------------------------------------------
1199 # Extended — muse domains info (deeper coverage)
1200 # ---------------------------------------------------------------------------
1201
1202
1203 class TestDomainsInfoExtended:
1204 def test_j_alias_works(self) -> None:
1205 result = runner.invoke(cli, ["domains", "info", "code", "-j"])
1206 assert result.exit_code == 0
1207 data = json.loads(result.output.strip())
1208 assert data["domain"] == "code"
1209
1210 def test_help_mentions_json_flag(self) -> None:
1211 result = runner.invoke(cli, ["domains", "info", "--help"])
1212 assert "--json" in result.output or "-j" in result.output
1213
1214 def test_help_shows_exit_codes(self) -> None:
1215 result = runner.invoke(cli, ["domains", "info", "--help"])
1216 assert "Exit code" in result.output or "exit code" in result.output
1217
1218 def test_json_is_compact_single_line(self) -> None:
1219 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
1220 assert result.exit_code == 0
1221 lines = [l for l in result.output.splitlines() if l.strip()]
1222 assert len(lines) == 1, f"Expected 1 non-empty line, got {len(lines)}"
1223
1224 def test_json_parses_cleanly(self) -> None:
1225 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
1226 data = json.loads(result.output.strip())
1227 assert isinstance(data, dict)
1228
1229 def test_json_domain_field_matches_arg(self) -> None:
1230 result = runner.invoke(cli, ["domains", "info", "scaffold", "--json"])
1231 assert result.exit_code == 0
1232 data = json.loads(result.output.strip())
1233 assert data["domain"] == "scaffold"
1234
1235 def test_json_active_is_bool(self) -> None:
1236 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
1237 data = json.loads(result.output.strip())
1238 assert isinstance(data["active"], bool)
1239
1240 def test_json_capabilities_is_list(self) -> None:
1241 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
1242 data = json.loads(result.output.strip())
1243 assert isinstance(data["capabilities"], list)
1244 assert len(data["capabilities"]) >= 1
1245
1246 def test_json_module_path_is_string(self) -> None:
1247 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
1248 data = json.loads(result.output.strip())
1249 assert isinstance(data["module_path"], str)
1250 assert len(data["module_path"]) > 0
1251
1252 def test_json_code_has_typed_deltas(self) -> None:
1253 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
1254 data = json.loads(result.output.strip())
1255 assert "Typed Deltas" in data["capabilities"]
1256
1257 def test_json_code_schema_present(self) -> None:
1258 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
1259 data = json.loads(result.output.strip())
1260 assert "schema" in data
1261
1262 def test_json_schema_has_required_keys(self) -> None:
1263 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
1264 data = json.loads(result.output.strip())
1265 schema = data["schema"]
1266 for key in ("schema_version", "merge_mode", "description", "dimensions"):
1267 assert key in schema, f"Missing schema key: {key}"
1268
1269 def test_json_schema_dimensions_is_list(self) -> None:
1270 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
1271 data = json.loads(result.output.strip())
1272 assert isinstance(data["schema"]["dimensions"], list)
1273 assert len(data["schema"]["dimensions"]) >= 1
1274
1275 def test_json_schema_dimension_has_name_and_description(self) -> None:
1276 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
1277 data = json.loads(result.output.strip())
1278 dim = data["schema"]["dimensions"][0]
1279 assert "name" in dim
1280 assert "description" in dim
1281
1282 def test_text_shows_module_path(self) -> None:
1283 result = runner.invoke(cli, ["domains", "info", "code"])
1284 assert "Module:" in result.output
1285
1286 def test_text_shows_capabilities(self) -> None:
1287 result = runner.invoke(cli, ["domains", "info", "code"])
1288 assert "Capabilities:" in result.output
1289 assert "Typed Deltas" in result.output
1290
1291 def test_unknown_domain_error_mentions_name(self) -> None:
1292 result = runner.invoke(cli, ["domains", "info", "nonexistent_domain_xyz"])
1293 assert result.exit_code == 1
1294 assert "nonexistent_domain_xyz" in result.output or "not registered" in result.output
1295
1296 def test_unknown_domain_lists_known_domains(self) -> None:
1297 result = runner.invoke(cli, ["domains", "info", "nope"])
1298 assert result.exit_code == 1
1299 # Should mention at least one known domain (e.g. "code")
1300 assert "code" in result.output
1301
1302
1303 # ---------------------------------------------------------------------------
1304 # Security — muse domains info
1305 # ---------------------------------------------------------------------------
1306
1307
1308 class TestDomainsInfoSecurity:
1309 def test_ansi_in_domain_name_stripped_error(self) -> None:
1310 """ANSI in the unknown domain name is stripped from the error message."""
1311 result = runner.invoke(cli, ["domains", "info", "\x1b[31mevil\x1b[0m"])
1312 assert result.exit_code == 1
1313 assert "\x1b[" not in result.output
1314
1315 def test_ansi_in_module_path_stripped_text(self) -> None:
1316 """module_path with ANSI injected by a plugin is sanitized in text output."""
1317 from unittest.mock import patch as _patch
1318 evil_entry = {
1319 "domain": "code",
1320 "module_path": "plugins/\x1b[31mevil\x1b[0m/plugin.py",
1321 "capabilities": ["Typed Deltas"],
1322 "active": False,
1323 }
1324 with _patch("muse.cli.commands.domains._build_entry", return_value=evil_entry):
1325 result = runner.invoke(cli, ["domains", "info", "code"])
1326 assert result.exit_code == 0
1327 assert "\x1b[" not in result.output
1328
1329 def test_ansi_in_schema_description_stripped_text(self) -> None:
1330 """ANSI in schema description from a plugin is sanitized in text output."""
1331 from unittest.mock import patch as _patch, MagicMock
1332 evil_entry = {
1333 "domain": "code",
1334 "module_path": "plugins/code/plugin.py",
1335 "capabilities": ["Typed Deltas", "Domain Schema"],
1336 "active": False,
1337 "schema": {
1338 "schema_version": "1.0",
1339 "merge_mode": "structured",
1340 "description": "Good domain \x1b[31mevil\x1b[0m",
1341 "dimensions": [{"name": "symbols", "description": "sym"}],
1342 },
1343 }
1344 with _patch("muse.cli.commands.domains._build_entry", return_value=evil_entry):
1345 result = runner.invoke(cli, ["domains", "info", "code"])
1346 assert result.exit_code == 0
1347 assert "\x1b[" not in result.output
1348
1349 def test_ansi_in_dimension_name_stripped_text(self) -> None:
1350 """ANSI in a dimension name from a plugin schema is sanitized in text output."""
1351 from unittest.mock import patch as _patch
1352 evil_entry = {
1353 "domain": "code",
1354 "module_path": "plugins/code/plugin.py",
1355 "capabilities": ["Typed Deltas", "Domain Schema"],
1356 "active": False,
1357 "schema": {
1358 "schema_version": "1.0",
1359 "merge_mode": "structured",
1360 "description": "desc",
1361 "dimensions": [{"name": "\x1b[31mevil\x1b[0m", "description": "bad"}],
1362 },
1363 }
1364 with _patch("muse.cli.commands.domains._build_entry", return_value=evil_entry):
1365 result = runner.invoke(cli, ["domains", "info", "code"])
1366 assert result.exit_code == 0
1367 assert "\x1b[" not in result.output
1368
1369 def test_json_stdout_only_no_stray_text(self) -> None:
1370 """In JSON mode, stdout must be parseable JSON with no surrounding noise."""
1371 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
1372 assert result.exit_code == 0
1373 json.loads(result.output.strip())
1374
1375 def test_unknown_domain_no_traceback(self) -> None:
1376 result = runner.invoke(cli, ["domains", "info", "no_such_domain_xyz"])
1377 assert result.exit_code == 1
1378 assert "Traceback" not in result.output
1379
1380
1381 # ---------------------------------------------------------------------------
1382 # Stress — muse domains info
1383 # ---------------------------------------------------------------------------
1384
1385
1386 class TestDomainsInfoStress:
1387 def test_50_sequential_info_calls(self) -> None:
1388 """50 sequential info invocations on 'code' all exit 0."""
1389 for i in range(50):
1390 result = runner.invoke(cli, ["domains", "info", "code", "--json"])
1391 assert result.exit_code == 0, f"Call {i} failed: {result.output}"
1392
1393 def test_all_registered_domains_info_exits_zero(self) -> None:
1394 """Every domain currently in the registry responds to info with exit 0."""
1395 from muse.plugins.registry import _REGISTRY
1396 for name in sorted(_REGISTRY):
1397 result = runner.invoke(cli, ["domains", "info", name, "--json"])
1398 assert result.exit_code == 0, f"domains info {name!r} failed: {result.output}"
1399
1400 def test_concurrent_info_calls_no_shared_state(self) -> None:
1401 """8 threads each call domains info in isolation — no race conditions."""
1402 import threading
1403 errors: list[str] = []
1404 from muse.cli.commands.domains import _build_entry, _active_domain, _find_repo_root
1405 from muse.plugins.registry import _REGISTRY
1406
1407 def worker(idx: int) -> None:
1408 try:
1409 plugin = _REGISTRY.get("code")
1410 if plugin is None:
1411 errors.append(f"Thread {idx}: code plugin not found")
1412 return
1413 active = _active_domain(_find_repo_root())
1414 entry = _build_entry("code", plugin, active)
1415 if entry["domain"] != "code":
1416 errors.append(f"Thread {idx}: wrong domain {entry['domain']!r}")
1417 if not isinstance(entry["active"], bool):
1418 errors.append(f"Thread {idx}: active is not bool")
1419 except Exception as exc:
1420 errors.append(f"Thread {idx}: {exc}")
1421
1422 threads = [threading.Thread(target=worker, args=(i,)) for i in range(8)]
1423 for t in threads:
1424 t.start()
1425 for t in threads:
1426 t.join()
1427 assert not errors, f"Concurrent failures: {errors}"
1428
1429
1430 # ---------------------------------------------------------------------------
1431 # Extended — muse domains use (deeper coverage)
1432 # ---------------------------------------------------------------------------
1433
1434
1435 class TestDomainsUseExtended:
1436 def test_j_alias_works(self, tmp_path: pathlib.Path) -> None:
1437 _init_repo(tmp_path, domain="scaffold")
1438 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1439 result = runner.invoke(cli, ["domains", "use", "code", "-j"])
1440 assert result.exit_code == 0, result.output
1441 data = json.loads(result.output.strip())
1442 assert data["domain"] == "code"
1443
1444 def test_help_mentions_json_flag(self) -> None:
1445 result = runner.invoke(cli, ["domains", "use", "--help"])
1446 assert "--json" in result.output or "-j" in result.output
1447
1448 def test_help_shows_exit_codes(self) -> None:
1449 result = runner.invoke(cli, ["domains", "use", "--help"])
1450 assert "Exit code" in result.output or "exit code" in result.output
1451
1452 def test_json_is_compact_single_line(self, tmp_path: pathlib.Path) -> None:
1453 _init_repo(tmp_path, domain="scaffold")
1454 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1455 result = runner.invoke(cli, ["domains", "use", "code", "--json"])
1456 assert result.exit_code == 0, result.output
1457 lines = [l for l in result.output.splitlines() if l.strip()]
1458 assert len(lines) == 1, f"Expected 1 non-empty line, got {len(lines)}"
1459
1460 def test_json_parses_cleanly(self, tmp_path: pathlib.Path) -> None:
1461 _init_repo(tmp_path)
1462 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1463 result = runner.invoke(cli, ["domains", "use", "code", "--json"])
1464 data = json.loads(result.output.strip())
1465 assert isinstance(data, dict)
1466
1467 def test_json_domain_matches_arg(self, tmp_path: pathlib.Path) -> None:
1468 _init_repo(tmp_path, domain="code")
1469 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1470 result = runner.invoke(cli, ["domains", "use", "scaffold", "--json"])
1471 data = json.loads(result.output.strip())
1472 assert data["domain"] == "scaffold"
1473
1474 def test_json_status_is_switched(self, tmp_path: pathlib.Path) -> None:
1475 _init_repo(tmp_path)
1476 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1477 result = runner.invoke(cli, ["domains", "use", "code", "--json"])
1478 data = json.loads(result.output.strip())
1479 assert data["status"] == "switched"
1480
1481 def test_json_repo_ends_with_muse(self, tmp_path: pathlib.Path) -> None:
1482 _init_repo(tmp_path)
1483 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1484 result = runner.invoke(cli, ["domains", "use", "code", "--json"])
1485 data = json.loads(result.output.strip())
1486 assert data["repo"].endswith(".muse")
1487
1488 def test_repo_json_domain_actually_updated(self, tmp_path: pathlib.Path) -> None:
1489 _init_repo(tmp_path, domain="scaffold")
1490 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1491 runner.invoke(cli, ["domains", "use", "code"])
1492 written = json.loads((tmp_path / ".muse" / "repo.json").read_text())
1493 assert written["domain"] == "code"
1494
1495 def test_existing_fields_preserved(self, tmp_path: pathlib.Path) -> None:
1496 _init_repo(tmp_path, domain="scaffold")
1497 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1498 runner.invoke(cli, ["domains", "use", "code"])
1499 written = json.loads((tmp_path / ".muse" / "repo.json").read_text())
1500 assert "repo_id" in written
1501 assert "schema_version" in written
1502
1503 def test_idempotent_same_domain_exits_zero(self, tmp_path: pathlib.Path) -> None:
1504 _init_repo(tmp_path, domain="code")
1505 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1506 result = runner.invoke(cli, ["domains", "use", "code"])
1507 assert result.exit_code == 0
1508
1509 def test_switch_code_to_scaffold(self, tmp_path: pathlib.Path) -> None:
1510 _init_repo(tmp_path, domain="code")
1511 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1512 result = runner.invoke(cli, ["domains", "use", "scaffold"])
1513 assert result.exit_code == 0
1514 written = json.loads((tmp_path / ".muse" / "repo.json").read_text())
1515 assert written["domain"] == "scaffold"
1516
1517 def test_text_success_mentions_domain_name(self, tmp_path: pathlib.Path) -> None:
1518 _init_repo(tmp_path)
1519 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1520 result = runner.invoke(cli, ["domains", "use", "code"])
1521 assert "code" in result.output
1522
1523 def test_text_success_mentions_repo_path(self, tmp_path: pathlib.Path) -> None:
1524 _init_repo(tmp_path)
1525 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1526 result = runner.invoke(cli, ["domains", "use", "code"])
1527 assert "Repo:" in result.output or ".muse" in result.output
1528
1529 def test_unknown_domain_lists_known(self, tmp_path: pathlib.Path) -> None:
1530 _init_repo(tmp_path)
1531 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1532 result = runner.invoke(cli, ["domains", "use", "no_such_domain"])
1533 assert result.exit_code == 1
1534 assert "code" in result.output # known domains listed
1535
1536 def test_no_repo_exits_1(self) -> None:
1537 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1538 result = runner.invoke(cli, ["domains", "use", "code"])
1539 assert result.exit_code == 1
1540
1541 def test_corrupt_repo_json_exits_1(self, tmp_path: pathlib.Path) -> None:
1542 _init_repo(tmp_path)
1543 (tmp_path / ".muse" / "repo.json").write_text("{{bad json}}", encoding="utf-8")
1544 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1545 result = runner.invoke(cli, ["domains", "use", "code"])
1546 assert result.exit_code == 1
1547
1548 def test_extra_repo_json_fields_preserved(self, tmp_path: pathlib.Path) -> None:
1549 """Custom extra fields in repo.json survive a domain switch."""
1550 muse = tmp_path / ".muse"
1551 muse.mkdir(parents=True, exist_ok=True)
1552 (muse / "repo.json").write_text(
1553 json.dumps({
1554 "repo_id": "my-repo",
1555 "schema_version": "1.0",
1556 "domain": "scaffold",
1557 "custom_field": "keep_me",
1558 }),
1559 encoding="utf-8",
1560 )
1561 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1562 result = runner.invoke(cli, ["domains", "use", "code"])
1563 assert result.exit_code == 0
1564 written = json.loads((tmp_path / ".muse" / "repo.json").read_text())
1565 assert written["custom_field"] == "keep_me"
1566 assert written["domain"] == "code"
1567
1568
1569 # ---------------------------------------------------------------------------
1570 # Security — muse domains use
1571 # ---------------------------------------------------------------------------
1572
1573
1574 class TestDomainsUseSecurity:
1575 def test_ansi_in_unknown_domain_name_stripped(self, tmp_path: pathlib.Path) -> None:
1576 _init_repo(tmp_path)
1577 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1578 result = runner.invoke(cli, ["domains", "use", "\x1b[31mevil\x1b[0m"])
1579 assert result.exit_code == 1
1580 assert "\x1b[" not in result.output
1581
1582 def test_unregistered_domain_never_written(self, tmp_path: pathlib.Path) -> None:
1583 """An unregistered domain name is rejected before repo.json is touched."""
1584 _init_repo(tmp_path, domain="code")
1585 original = (tmp_path / ".muse" / "repo.json").read_text()
1586 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1587 result = runner.invoke(cli, ["domains", "use", "malicious_domain"])
1588 assert result.exit_code == 1
1589 # repo.json must be unchanged
1590 assert (tmp_path / ".muse" / "repo.json").read_text() == original
1591
1592 def test_domain_name_sanitized_in_success_text(self, tmp_path: pathlib.Path) -> None:
1593 """Domain name in success text is sanitized (no raw ANSI from env)."""
1594 _init_repo(tmp_path)
1595 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1596 result = runner.invoke(cli, ["domains", "use", "code"])
1597 assert result.exit_code == 0
1598 assert "\x1b[" not in result.output
1599
1600 def test_repo_path_sanitized_in_success_text(self, tmp_path: pathlib.Path) -> None:
1601 """Repo path in success text is sanitized."""
1602 _init_repo(tmp_path)
1603 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1604 result = runner.invoke(cli, ["domains", "use", "code"])
1605 assert result.exit_code == 0
1606 assert "\x1b[" not in result.output
1607
1608 def test_json_stdout_only_no_stray_text(self, tmp_path: pathlib.Path) -> None:
1609 """In JSON mode, stdout must be valid JSON and nothing else."""
1610 _init_repo(tmp_path)
1611 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1612 result = runner.invoke(cli, ["domains", "use", "code", "--json"])
1613 assert result.exit_code == 0
1614 json.loads(result.output.strip())
1615
1616 def test_no_traceback_on_corrupt_repo_json(self, tmp_path: pathlib.Path) -> None:
1617 _init_repo(tmp_path)
1618 (tmp_path / ".muse" / "repo.json").write_text("{bad}", encoding="utf-8")
1619 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1620 result = runner.invoke(cli, ["domains", "use", "code"])
1621 assert result.exit_code == 1
1622 assert "Traceback" not in result.output
1623
1624
1625 # ---------------------------------------------------------------------------
1626 # Stress — muse domains use
1627 # ---------------------------------------------------------------------------
1628
1629
1630 class TestDomainsUseStress:
1631 def test_50_sequential_use_calls_all_succeed(self, tmp_path: pathlib.Path) -> None:
1632 _init_repo(tmp_path, domain="code")
1633 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1634 for i in range(50):
1635 result = runner.invoke(cli, ["domains", "use", "code", "--json"])
1636 assert result.exit_code == 0, f"Call {i} failed: {result.output}"
1637
1638 def test_alternate_domains_100_times(self, tmp_path: pathlib.Path) -> None:
1639 """Switch between code and scaffold 100 times — file integrity preserved."""
1640 _init_repo(tmp_path, domain="code")
1641 domains = ["code", "scaffold"]
1642 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1643 for i in range(100):
1644 target = domains[i % 2]
1645 result = runner.invoke(cli, ["domains", "use", target])
1646 assert result.exit_code == 0, f"Switch {i} to {target!r} failed"
1647 written = json.loads((tmp_path / ".muse" / "repo.json").read_text())
1648 assert written["domain"] in domains
1649
1650 def test_concurrent_use_isolated_repos(self, tmp_path: pathlib.Path) -> None:
1651 """8 threads each switch domain on their own isolated repo."""
1652 import argparse
1653 import threading
1654
1655 from muse.cli.commands.domains import run_use
1656
1657 errors: list[str] = []
1658
1659 def worker(idx: int) -> None:
1660 repo = tmp_path / f"repo_{idx}"
1661 _init_repo(repo, domain="scaffold")
1662 try:
1663 args = argparse.Namespace(use_name="code", json_out=True)
1664 with patch("muse.cli.commands.domains._find_repo_root", return_value=repo):
1665 run_use(args)
1666 written = json.loads((repo / ".muse" / "repo.json").read_text())
1667 if written["domain"] != "code":
1668 errors.append(f"Thread {idx}: domain is {written['domain']!r}")
1669 except SystemExit as exc:
1670 errors.append(f"Thread {idx}: exit {exc.code}")
1671 except Exception as exc:
1672 errors.append(f"Thread {idx}: {exc}")
1673
1674 threads = [threading.Thread(target=worker, args=(i,)) for i in range(8)]
1675 for t in threads:
1676 t.start()
1677 for t in threads:
1678 t.join()
1679 assert not errors, f"Concurrent failures: {errors}"
1680
1681
1682 # ---------------------------------------------------------------------------
1683 # Extended — muse domains validate
1684 # ---------------------------------------------------------------------------
1685
1686
1687 class TestDomainsValidateExtended:
1688 def test_j_alias_works(self) -> None:
1689 """-j is equivalent to --json."""
1690 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1691 result = runner.invoke(cli, ["domains", "validate", "code", "-j"])
1692 assert result.exit_code == 0
1693 data = json.loads(result.output.strip())
1694 assert data["domain"] == "code"
1695
1696 def test_help_flag(self) -> None:
1697 result = runner.invoke(cli, ["domains", "validate", "--help"])
1698 assert result.exit_code == 0
1699 assert "validate" in result.output.lower()
1700
1701 def test_json_compact_no_indent(self) -> None:
1702 """JSON output must be a single line (compact, no indent=2)."""
1703 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1704 result = runner.invoke(cli, ["domains", "validate", "code", "-j"])
1705 assert result.exit_code == 0
1706 lines = [l for l in result.output.splitlines() if l.strip()]
1707 assert len(lines) == 1, f"Expected compact JSON, got {len(lines)} lines"
1708
1709 def test_json_fields_present(self) -> None:
1710 """JSON object contains domain, ok, checks."""
1711 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1712 result = runner.invoke(cli, ["domains", "validate", "code", "-j"])
1713 data = json.loads(result.output.strip())
1714 assert set(data.keys()) >= {"domain", "ok", "checks"}
1715
1716 def test_json_domain_matches_arg(self) -> None:
1717 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1718 result = runner.invoke(cli, ["domains", "validate", "scaffold", "-j"])
1719 data = json.loads(result.output.strip())
1720 assert data["domain"] == "scaffold"
1721
1722 def test_json_checks_are_objects(self) -> None:
1723 """Each entry in checks has name, ok, detail."""
1724 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1725 result = runner.invoke(cli, ["domains", "validate", "code", "-j"])
1726 data = json.loads(result.output.strip())
1727 for c in data["checks"]:
1728 assert "name" in c
1729 assert "ok" in c
1730 assert "detail" in c
1731
1732 def test_text_output_shows_checkmarks(self) -> None:
1733 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1734 result = runner.invoke(cli, ["domains", "validate", "code"])
1735 assert "✅" in result.output or "✓" in result.output
1736
1737 def test_text_output_shows_domain_name(self) -> None:
1738 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1739 result = runner.invoke(cli, ["domains", "validate", "scaffold"])
1740 assert "scaffold" in result.output
1741
1742 def test_no_name_in_repo_validates_active_domain(self, tmp_path: pathlib.Path) -> None:
1743 """When inside a repo with domain=code, validate with no name validates code."""
1744 _init_repo(tmp_path, domain="code")
1745 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1746 result = runner.invoke(cli, ["domains", "validate", "-j"])
1747 assert result.exit_code == 0
1748 data = json.loads(result.output.strip())
1749 assert data["domain"] == "code"
1750
1751 def test_no_name_no_repo_validates_all_json(self) -> None:
1752 """With no name and no repo, all registered domains are validated."""
1753 from muse.plugins.registry import _REGISTRY
1754 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1755 result = runner.invoke(cli, ["domains", "validate", "-j"])
1756 assert result.exit_code == 0
1757 if len(_REGISTRY) == 1:
1758 data = json.loads(result.output.strip())
1759 assert "domain" in data
1760 else:
1761 data = json.loads(result.output.strip())
1762 if isinstance(data, dict) and "results" in data:
1763 data = data["results"]
1764 assert isinstance(data, list)
1765 assert len(data) == len(_REGISTRY)
1766
1767 def test_unregistered_domain_exit1(self) -> None:
1768 result = runner.invoke(cli, ["domains", "validate", "does-not-exist"])
1769 assert result.exit_code == 1
1770
1771 def test_broken_plugin_ok_false(self) -> None:
1772 """A plugin with no required methods emits ok=false."""
1773 from muse.plugins.registry import _REGISTRY
1774 from unittest.mock import MagicMock
1775 mock = MagicMock(spec=[])
1776 with patch.dict(_REGISTRY, {"broken2": mock}):
1777 result = runner.invoke(cli, ["domains", "validate", "broken2", "-j"])
1778 assert result.exit_code == 1
1779 data = json.loads(result.output.strip())
1780 assert data["ok"] is False
1781
1782 def test_broken_plugin_checks_have_failed_entries(self) -> None:
1783 from muse.plugins.registry import _REGISTRY
1784 from unittest.mock import MagicMock
1785 mock = MagicMock(spec=[])
1786 with patch.dict(_REGISTRY, {"broken3": mock}):
1787 result = runner.invoke(cli, ["domains", "validate", "broken3", "-j"])
1788 data = json.loads(result.output.strip())
1789 failed = [c for c in data["checks"] if not c["ok"]]
1790 assert len(failed) >= 1
1791
1792 def test_multi_domain_json_is_list(self) -> None:
1793 """When validating all (no name, no repo) with >=2 domains, output is a list."""
1794 from muse.plugins.registry import _REGISTRY
1795 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1796 result = runner.invoke(cli, ["domains", "validate", "-j"])
1797 assert result.exit_code == 0
1798 if len(_REGISTRY) >= 2:
1799 data = json.loads(result.output.strip())
1800 if isinstance(data, dict) and "results" in data:
1801 data = data["results"]
1802 assert isinstance(data, list)
1803
1804 def test_active_domain_not_in_registry_falls_back_to_all(self, tmp_path: pathlib.Path) -> None:
1805 """Active domain removed from registry → falls back to validating all."""
1806 _init_repo(tmp_path, domain="ghost")
1807 with patch("muse.cli.commands.domains._find_repo_root", return_value=tmp_path):
1808 result = runner.invoke(cli, ["domains", "validate", "-j"])
1809 assert result.exit_code == 0
1810 # should validate registry domains, not error on missing ghost
1811
1812 def test_help_shows_agent_quickstart(self) -> None:
1813 result = runner.invoke(cli, ["domains", "validate", "--help"])
1814 assert result.exit_code == 0
1815 assert "Agent quickstart" in result.output
1816
1817 def test_help_shows_json_schema(self) -> None:
1818 result = runner.invoke(cli, ["domains", "validate", "--help"])
1819 assert "JSON output schema" in result.output
1820
1821 def test_help_shows_exit_codes(self) -> None:
1822 result = runner.invoke(cli, ["domains", "validate", "--help"])
1823 assert "Exit codes" in result.output
1824
1825
1826 # ---------------------------------------------------------------------------
1827 # Security — muse domains validate
1828 # ---------------------------------------------------------------------------
1829
1830
1831 class TestDomainsValidateSecurity:
1832 def test_ansi_in_domain_name_stripped_in_error(self) -> None:
1833 """ANSI in unknown domain name must not bleed into stderr."""
1834 result = runner.invoke(cli, ["domains", "validate", "\x1b[31mevil\x1b[0m"])
1835 assert result.exit_code == 1
1836 assert "\x1b" not in result.output
1837
1838 def test_unregistered_domain_never_checked(self) -> None:
1839 """An unregistered name exits before any check logic runs."""
1840 result = runner.invoke(cli, ["domains", "validate", "notareal_domain_xyz"])
1841 assert result.exit_code == 1
1842 assert "not registered" in result.output or "not registered" in (result.output + "")
1843
1844 def test_json_output_only_on_stdout(self, capsys: pytest.CaptureFixture) -> None:
1845 """JSON is emitted to stdout; errors go to stderr only."""
1846 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1847 result = runner.invoke(cli, ["domains", "validate", "code", "-j"])
1848 assert result.exit_code == 0
1849 json.loads(result.output.strip()) # must parse cleanly
1850
1851 def test_ansi_in_check_detail_sanitized_text_mode(self) -> None:
1852 """Plugin with no schema attr exercises AttributeError detail path; output has no ANSI."""
1853 from muse.plugins.registry import _REGISTRY
1854 from unittest.mock import MagicMock
1855 # spec omits schema → _run_validate_plugin catches AttributeError, writes detail string
1856 mock = MagicMock(spec=["snapshot", "diff", "merge", "drift", "apply"])
1857 with patch.dict(_REGISTRY, {"ansi_test": mock}):
1858 result = runner.invoke(cli, ["domains", "validate", "ansi_test"])
1859 assert "\x1b" not in result.output
1860
1861 def test_no_traceback_on_unknown_domain(self) -> None:
1862 result = runner.invoke(cli, ["domains", "validate", "totally_unknown_domain"])
1863 assert "Traceback" not in result.output
1864
1865 def test_multi_domain_validate_all_domains_present_in_output(self) -> None:
1866 """When validating all, every registered domain name appears in output."""
1867 from muse.plugins.registry import _REGISTRY
1868 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1869 result = runner.invoke(cli, ["domains", "validate", "-j"])
1870 assert result.exit_code == 0
1871 output = result.output
1872 for domain_name in _REGISTRY:
1873 assert domain_name in output, f"Missing domain {domain_name!r} in output"
1874
1875
1876 # ---------------------------------------------------------------------------
1877 # Stress — muse domains validate
1878 # ---------------------------------------------------------------------------
1879
1880
1881 class TestDomainsValidateStress:
1882 def test_50_sequential_validate_calls(self) -> None:
1883 """50 sequential validate calls all succeed."""
1884 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1885 for i in range(50):
1886 result = runner.invoke(cli, ["domains", "validate", "code", "-j"])
1887 assert result.exit_code == 0, f"Call {i} failed: {result.output}"
1888
1889 def test_alternate_code_scaffold_100_times(self) -> None:
1890 """Alternate between validating code and scaffold 100 times."""
1891 domains = ["code", "scaffold"]
1892 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1893 for i in range(100):
1894 target = domains[i % 2]
1895 result = runner.invoke(cli, ["domains", "validate", target, "-j"])
1896 assert result.exit_code == 0, f"Step {i}: {result.output}"
1897 data = json.loads(result.output.strip())
1898 assert data["domain"] == target
1899
1900 def test_concurrent_validate_8_threads(self) -> None:
1901 """8 threads each validate code concurrently using core function directly."""
1902 import argparse
1903 import threading
1904
1905 from muse.cli.commands.domains import run_validate
1906
1907 errors: list[str] = []
1908
1909 def worker(idx: int) -> None:
1910 args = argparse.Namespace(validate_name="code", json_out=True)
1911 try:
1912 with patch("muse.cli.commands.domains._find_repo_root", return_value=None):
1913 run_validate(args)
1914 except SystemExit as exc:
1915 if exc.code != 0:
1916 errors.append(f"Thread {idx}: exit {exc.code}")
1917 except Exception as exc:
1918 errors.append(f"Thread {idx}: {exc}")
1919
1920 threads = [threading.Thread(target=worker, args=(i,)) for i in range(8)]
1921 for t in threads:
1922 t.start()
1923 for t in threads:
1924 t.join()
1925 assert not errors, f"Concurrent failures: {errors}"
1926
1927
1928 # ---------------------------------------------------------------------------
1929 # TestRegisterFlags — --json / -j normalized on every domains subparser
1930 # ---------------------------------------------------------------------------
1931
1932
1933 class TestRegisterFlags:
1934 """Every domains subparser must register --json with -j shorthand."""
1935
1936 def _make_parser(self) -> argparse.ArgumentParser:
1937 from muse.cli.commands.domains import register
1938 root = argparse.ArgumentParser()
1939 subs = root.add_subparsers()
1940 register(subs)
1941 return root
1942
1943 def test_domains_j_alias_exits_zero(self) -> None:
1944 result = runner.invoke(cli, ["domains", "-j"])
1945 assert result.exit_code == 0, result.output
1946
1947 def test_domains_j_alias_valid_json(self) -> None:
1948 result = runner.invoke(cli, ["domains", "-j"])
1949 json.loads(result.output) # must not raise
1950
1951 def test_domains_j_alias_has_domains_key(self) -> None:
1952 result = runner.invoke(cli, ["domains", "-j"])
1953 assert "domains" in json.loads(result.output)
1954
1955 def test_domains_json_out_default_false(self) -> None:
1956 p = self._make_parser()
1957 ns = p.parse_args(["domains"])
1958 assert ns.json_out is False
1959
1960 def test_domains_json_out_true_with_json_flag(self) -> None:
1961 p = self._make_parser()
1962 ns = p.parse_args(["domains", "--json"])
1963 assert ns.json_out is True
1964
1965 def test_domains_json_out_true_with_j_flag(self) -> None:
1966 p = self._make_parser()
1967 ns = p.parse_args(["domains", "-j"])
1968 assert ns.json_out is True
1969
1970 def test_validate_j_alias_exits_zero(self) -> None:
1971 result = runner.invoke(cli, ["domains", "validate", "-j"])
1972 assert result.exit_code == 0, result.output
1973
1974 def test_validate_j_alias_valid_json(self) -> None:
1975 result = runner.invoke(cli, ["domains", "validate", "-j"])
1976 json.loads(result.output) # must not raise
1977
1978 def test_info_j_alias_exits_zero(self) -> None:
1979 result = runner.invoke(cli, ["domains", "info", "code", "-j"])
1980 assert result.exit_code == 0, result.output
1981
1982 def test_info_j_alias_valid_json(self) -> None:
1983 result = runner.invoke(cli, ["domains", "info", "code", "-j"])
1984 json.loads(result.output) # must not raise
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 141 days ago