gabriel / muse public

test_stress_domains_publish.py file-level

at sha256:d · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:4 Merge branch 'dev' into main · gabriel · Jun 17, 2026
1 """Stress and integration tests for ``muse domains publish``.
2
3 Covers:
4 Stress:
5 - 500 sequential publish calls with mocked HTTP (throughput baseline)
6 - Concurrent mock publishes via threading (thread-safety of CliRunner)
7 - Large capabilities JSON (100 dimensions, 50 artifact types)
8 - Rapid successive 409 conflicts do not leak state
9
10 Integration (end-to-end CLI):
11 - Full workflow: scaffold β†’ register β†’ publish (all mock layers wired)
12 - Round-trip: publish β†’ parse --json β†’ assert all fields present
13 - Author/slug with hyphens and underscores (URL-safe validation)
14 - Unicode description does not corrupt JSON encoding
15 - Very long description (4000 chars) is transmitted verbatim
16 - Hub URL override propagates to HTTP request
17 """
18 from __future__ import annotations
19
20 import concurrent.futures
21 import http.client
22 import io
23 import json
24 import pathlib
25 import threading
26 import time
27 import urllib.error
28 import urllib.request
29 import unittest.mock
30 from typing import Generator
31
32 import pytest
33 from tests.cli_test_helper import CliRunner
34
35 from muse._version import __version__
36 from muse.core.paths import muse_dir
37 cli = None # argparse migration β€” CliRunner ignores this arg
38 from muse.cli.commands.domains import _post_json, _PublishPayload, _Capabilities, _DimensionDef
39
40 runner = CliRunner()
41
42
43 def _make_signing() -> "SigningIdentity":
44 """Generate a fresh Ed25519 SigningIdentity for tests."""
45 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
46 from muse.core.transport import SigningIdentity
47 return SigningIdentity(handle="testuser", private_key=Ed25519PrivateKey.generate())
48
49
50 # ---------------------------------------------------------------------------
51 # Fixtures
52 # ---------------------------------------------------------------------------
53
54 _REQUIRED_ARGS = [
55 "domains", "publish",
56 "--author", "alice",
57 "--slug", "genomics",
58 "--name", "Genomics",
59 "--description", "Version DNA sequences",
60 "--viewer-type", "genome",
61 "--capabilities", json.dumps({
62 "dimensions": [{"name": "sequence", "description": "DNA base pairs"}],
63 "artifact_types": ["fasta"],
64 "merge_semantics": "three_way",
65 "supported_commands": ["commit", "diff"],
66 }),
67 "--hub", "https://hub.test",
68 ]
69
70 _SUCCESS_BODY = json.dumps({
71 "domain_id": "dom-001",
72 "scoped_id": "@alice/genomics",
73 "manifest_hash": "sha256:abc123",
74 })
75
76
77 @pytest.fixture()
78 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
79 dot_muse = muse_dir(tmp_path)
80 (dot_muse / "refs" / "heads").mkdir(parents=True)
81 (dot_muse / "objects").mkdir()
82 (dot_muse / "commits").mkdir()
83 (dot_muse / "snapshots").mkdir()
84 (dot_muse / "repo.json").write_text(
85 json.dumps({"repo_id": "test-repo", "schema_version": __version__, "domain": "code"})
86 )
87 (dot_muse / "HEAD").write_text("ref: refs/heads/main\n")
88 monkeypatch.chdir(tmp_path)
89 monkeypatch.setattr("muse.cli.commands.domains.get_signing_identity", lambda *a, **kw: _make_signing())
90 return tmp_path
91
92
93 def _mock_ok() -> unittest.mock.MagicMock:
94 """Return a context-manager mock that yields a 200 response."""
95 mock_resp = unittest.mock.MagicMock()
96 mock_resp.read.return_value = _SUCCESS_BODY.encode()
97 mock_resp.__enter__ = unittest.mock.MagicMock(return_value=mock_resp)
98 mock_resp.__exit__ = unittest.mock.MagicMock(return_value=False)
99 return mock_resp
100
101
102 # ---------------------------------------------------------------------------
103 # _post_json unit stress
104 # ---------------------------------------------------------------------------
105
106
107 def test_post_json_sequential_throughput() -> None:
108 """500 sequential _post_json calls complete in under 2 seconds (mock)."""
109 payload = _PublishPayload(
110 author_slug="alice",
111 slug="bench",
112 display_name="Bench",
113 description="Benchmark domain",
114 capabilities=_Capabilities(
115 dimensions=[_DimensionDef(name="x", description="x axis")],
116 merge_semantics="three_way",
117 ),
118 viewer_type="spatial",
119 version="0.1.0",
120 )
121
122 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
123 start = time.monotonic()
124 for _ in range(500):
125 result = _post_json("https://hub.test/api/v1/domains", payload, _make_signing())
126 assert result["scoped_id"] == "@alice/genomics"
127 elapsed = time.monotonic() - start
128
129 assert elapsed < 2.0, f"500 iterations took {elapsed:.2f}s β€” too slow"
130
131
132 def test_post_json_large_capabilities() -> None:
133 """_post_json handles 100-dimension, 50-artifact-type payloads without truncation."""
134 dims = [_DimensionDef(name=f"dim_{i}", description=f"Dimension {i} " * 10) for i in range(100)]
135 artifacts = [f"type_{i:03}" for i in range(50)]
136 payload = _PublishPayload(
137 author_slug="alice",
138 slug="large",
139 display_name="Large Domain",
140 description="A domain with many dimensions",
141 capabilities=_Capabilities(
142 dimensions=dims,
143 artifact_types=artifacts,
144 merge_semantics="three_way",
145 supported_commands=["commit", "diff", "merge", "log", "status"],
146 ),
147 viewer_type="generic",
148 version="1.0.0",
149 )
150
151 captured_bodies: list[bytes] = []
152
153 def _capture(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
154 raw = req.data
155 captured_bodies.append(raw if raw is not None else b"")
156 return _mock_ok()
157
158 with unittest.mock.patch("urllib.request.urlopen", side_effect=_capture):
159 result = _post_json("https://hub.test/api/v1/domains", payload, _make_signing())
160
161 body = json.loads(captured_bodies[0])
162 assert len(body["capabilities"]["dimensions"]) == 100
163 assert len(body["capabilities"]["artifact_types"]) == 50
164 assert result["scoped_id"] == "@alice/genomics"
165
166
167 def test_post_json_unicode_description() -> None:
168 """Unicode characters in description survive JSON round-trip correctly."""
169 unicode_desc = "Version 🎡 sΓ©quences d'ADN β€” supports ζΌ’ε­— and Γ‘oΓ±o input"
170 payload = _PublishPayload(
171 author_slug="alice",
172 slug="unicode",
173 display_name="Unicode Domain",
174 description=unicode_desc,
175 capabilities=_Capabilities(),
176 viewer_type="generic",
177 version="0.1.0",
178 )
179
180 captured_bodies: list[bytes] = []
181
182 def _capture(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
183 raw = req.data
184 captured_bodies.append(raw if raw is not None else b"")
185 return _mock_ok()
186
187 with unittest.mock.patch("urllib.request.urlopen", side_effect=_capture):
188 _post_json("https://hub.test/api/v1/domains", payload, _make_signing())
189
190 body = json.loads(captured_bodies[0].decode("utf-8"))
191 assert body["description"] == unicode_desc
192
193
194 def test_post_json_409_does_not_modify_state() -> None:
195 """Multiple 409 errors in a row do not corrupt any shared state."""
196 payload = _PublishPayload(
197 author_slug="alice",
198 slug="conflict",
199 display_name="X",
200 description="Y",
201 capabilities=_Capabilities(),
202 viewer_type="v",
203 version="0.1.0",
204 )
205 err = urllib.error.HTTPError(
206 url="https://hub.test/api/v1/domains",
207 code=409,
208 msg="Conflict",
209 hdrs=http.client.HTTPMessage(),
210 fp=io.BytesIO(b'{"error": "already_exists"}'),
211 )
212 with unittest.mock.patch("urllib.request.urlopen", side_effect=err):
213 for _ in range(50):
214 with pytest.raises(urllib.error.HTTPError) as exc_info:
215 _post_json("https://hub.test/api/v1/domains", payload, _make_signing())
216 assert exc_info.value.code == 409
217
218
219 # ---------------------------------------------------------------------------
220 # CLI stress tests
221 # ---------------------------------------------------------------------------
222
223
224 def test_cli_publish_large_description(repo: pathlib.Path) -> None:
225 """CLI accepts --description up to 4000 characters and transmits verbatim."""
226 long_desc = "A" * 4000
227 large_args = [
228 "domains", "publish",
229 "--author", "alice",
230 "--slug", "largdesc",
231 "--name", "Large",
232 "--description", long_desc,
233 "--viewer-type", "genome",
234 "--capabilities", '{"merge_semantics": "three_way"}',
235 "--hub", "https://hub.test",
236 ]
237 captured: list[bytes] = []
238
239 def _capture(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
240 raw = req.data
241 captured.append(raw if raw is not None else b"")
242 return _mock_ok()
243
244 with unittest.mock.patch("urllib.request.urlopen", side_effect=_capture):
245 result = runner.invoke(cli, large_args)
246
247 assert result.exit_code == 0, result.output
248 body = json.loads(captured[0])
249 assert body["description"] == long_desc
250
251
252 def test_cli_publish_slug_with_hyphens(repo: pathlib.Path) -> None:
253 """--slug with hyphens (e.g. 'spatial-3d') is transmitted as-is."""
254 args = [
255 "domains", "publish",
256 "--author", "alice",
257 "--slug", "spatial-3d",
258 "--name", "Spatial 3D",
259 "--description", "Version 3-D scenes",
260 "--viewer-type", "spatial",
261 "--capabilities", '{"merge_semantics": "three_way"}',
262 "--hub", "https://hub.test",
263 ]
264 captured: list[bytes] = []
265
266 def _capture(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
267 raw = req.data
268 captured.append(raw if raw is not None else b"")
269 return _mock_ok()
270
271 with unittest.mock.patch("urllib.request.urlopen", side_effect=_capture):
272 result = runner.invoke(cli, args)
273
274 assert result.exit_code == 0, result.output
275 body = json.loads(captured[0])
276 assert body["slug"] == "spatial-3d"
277
278
279 def test_cli_publish_hub_url_propagated(repo: pathlib.Path) -> None:
280 """--hub URL override is used as the request endpoint."""
281 custom_hub = "https://custom.musehub.example.com"
282 captured_urls: list[str] = []
283
284 def _capture(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
285 captured_urls.append(req.full_url)
286 return _mock_ok()
287
288 with unittest.mock.patch("urllib.request.urlopen", side_effect=_capture):
289 result = runner.invoke(cli, _REQUIRED_ARGS[:-2] + ["--hub", custom_hub])
290
291 assert result.exit_code == 0, result.output
292 assert captured_urls[0].startswith(custom_hub)
293
294
295 def test_cli_publish_json_roundtrip(repo: pathlib.Path) -> None:
296 """--json output is valid JSON with all expected keys."""
297 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
298 result = runner.invoke(cli, _REQUIRED_ARGS + ["--json"])
299
300 assert result.exit_code == 0, result.output
301 data = json.loads(result.output.strip())
302 assert "scoped_id" in data
303 assert "manifest_hash" in data
304
305
306 def test_cli_publish_version_semver(repo: pathlib.Path) -> None:
307 """--version is passed through without modification."""
308 captured: list[bytes] = []
309
310 def _capture(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
311 raw = req.data
312 captured.append(raw if raw is not None else b"")
313 return _mock_ok()
314
315 with unittest.mock.patch("urllib.request.urlopen", side_effect=_capture):
316 result = runner.invoke(cli, _REQUIRED_ARGS + ["--version", "2.14.0-beta.1"])
317
318 assert result.exit_code == 0, result.output
319 assert json.loads(captured[0])["version"] == "2.14.0-beta.1"
320
321
322 # ---------------------------------------------------------------------------
323 # Thread safety
324 # ---------------------------------------------------------------------------
325
326
327 def test_post_json_concurrent_thread_safety() -> None:
328 """10 concurrent threads invoking _post_json do not race on mock state."""
329 # CliRunner is not thread-safe (StringIO), so we test the lower-level
330 # _post_json helper directly β€” this is what the CLI delegates to.
331 counter: list[int] = [0]
332 lock = threading.Lock()
333
334 def _count_and_ok(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
335 with lock:
336 counter[0] += 1
337 return _mock_ok()
338
339 payload = _PublishPayload(
340 author_slug="alice",
341 slug="genomics",
342 display_name="Genomics",
343 description="Version DNA",
344 capabilities=_Capabilities(merge_semantics="three_way"),
345 viewer_type="genome",
346 version="0.1.0",
347 )
348
349 with unittest.mock.patch("urllib.request.urlopen", side_effect=_count_and_ok):
350 with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool:
351 futures = [
352 pool.submit(_post_json, "https://hub.test/api/v1/domains", payload, _make_signing())
353 for _ in range(10)
354 ]
355 results = [f.result() for f in futures]
356
357 assert len(results) == 10
358 assert all(r["scoped_id"] == "@alice/genomics" for r in results)
359 assert counter[0] == 10
360
361
362 # ---------------------------------------------------------------------------
363 # End-to-end integration
364 # ---------------------------------------------------------------------------
365
366
367 def test_e2e_publish_complete_payload_structure(repo: pathlib.Path) -> None:
368 """E2E: full publish sends author_slug, slug, display_name, description, capabilities, viewer_type, version."""
369 captured: list[bytes] = []
370
371 def _capture(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
372 raw = req.data
373 captured.append(raw if raw is not None else b"")
374 return _mock_ok()
375
376 with unittest.mock.patch("urllib.request.urlopen", side_effect=_capture):
377 result = runner.invoke(cli, _REQUIRED_ARGS + ["--version", "1.0.0"])
378
379 assert result.exit_code == 0, result.output
380 body = json.loads(captured[0])
381
382 # All required fields present
383 assert body["author_slug"] == "alice"
384 assert body["slug"] == "genomics"
385 assert body["display_name"] == "Genomics"
386 assert body["description"] == "Version DNA sequences"
387 assert body["viewer_type"] == "genome"
388 assert body["version"] == "1.0.0"
389
390 # Capabilities structure
391 caps = body["capabilities"]
392 assert isinstance(caps, dict)
393 assert "dimensions" in caps
394 assert isinstance(caps["dimensions"], list)
395
396
397 def test_e2e_publish_capabilities_auto_from_code_plugin(repo: pathlib.Path) -> None:
398 """E2E: capabilities auto-derived from code plugin contain correct dimensions."""
399 no_caps_args = [a for a in _REQUIRED_ARGS if a not in ("--capabilities",)]
400 # Remove the JSON value immediately after --capabilities
401 filtered: list[str] = []
402 skip_next = False
403 for arg in _REQUIRED_ARGS:
404 if skip_next:
405 skip_next = False
406 continue
407 if arg == "--capabilities":
408 skip_next = True
409 continue
410 filtered.append(arg)
411
412 captured: list[bytes] = []
413
414 def _capture(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
415 raw = req.data
416 captured.append(raw if raw is not None else b"")
417 return _mock_ok()
418
419 with unittest.mock.patch("urllib.request.urlopen", side_effect=_capture):
420 result = runner.invoke(cli, filtered)
421
422 assert result.exit_code == 0, result.output
423 body = json.loads(captured[0])
424 dims = body["capabilities"]["dimensions"]
425 # Code plugin has 5 dimensions β€” must include "symbols" and "imports"
426 names = [d["name"] for d in dims]
427 assert "symbols" in names
428 assert "imports" in names
429 assert len(dims) >= 3
430
431
432 def test_e2e_publish_400_sequential_calls_stable(repo: pathlib.Path) -> None:
433 """E2E stress: 400 sequential publish invocations all succeed.
434
435 The wall-clock budget is intentionally generous (120s) to accommodate
436 GitHub Actions' shared runners, which can be 3-4Γ— slower than a
437 developer laptop. The assertion guards against catastrophic regressions
438 (infinite loops, exponential backoff bugs) rather than raw throughput.
439 """
440 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
441 start = time.monotonic()
442 for i in range(400):
443 result = runner.invoke(cli, _REQUIRED_ARGS)
444 assert result.exit_code == 0, f"Run {i} failed: {result.output}"
445 elapsed = time.monotonic() - start
446
447 assert elapsed < 120.0, f"400 CLI invocations took {elapsed:.1f}s"
448
449
450 # ---------------------------------------------------------------------------
451 # Extended β€” muse domains publish (deeper coverage)
452 # ---------------------------------------------------------------------------
453
454
455 class TestPublishExtended:
456 def test_j_alias_works(self, repo: pathlib.Path) -> None:
457 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
458 result = runner.invoke(cli, _REQUIRED_ARGS + ["-j"])
459 assert result.exit_code == 0, result.output
460 data = json.loads(result.output.strip())
461 assert "scoped_id" in data
462
463 def test_help_mentions_json_flag(self, repo: pathlib.Path) -> None:
464 result = runner.invoke(cli, ["domains", "publish", "--help"])
465 assert "--json" in result.output or "-j" in result.output
466
467 def test_help_shows_exit_codes(self, repo: pathlib.Path) -> None:
468 result = runner.invoke(cli, ["domains", "publish", "--help"])
469 assert "Exit code" in result.output or "exit code" in result.output
470
471 def test_json_is_compact_single_line(self, repo: pathlib.Path) -> None:
472 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
473 result = runner.invoke(cli, _REQUIRED_ARGS + ["--json"])
474 assert result.exit_code == 0, result.output
475 lines = [l for l in result.output.splitlines() if l.strip()]
476 assert len(lines) == 1, f"Expected 1 non-empty line, got {len(lines)}"
477
478 def test_json_has_scoped_id(self, repo: pathlib.Path) -> None:
479 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
480 result = runner.invoke(cli, _REQUIRED_ARGS + ["--json"])
481 data = json.loads(result.output.strip())
482 assert data["scoped_id"] == "@alice/genomics"
483
484 def test_json_has_manifest_hash(self, repo: pathlib.Path) -> None:
485 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
486 result = runner.invoke(cli, _REQUIRED_ARGS + ["--json"])
487 data = json.loads(result.output.strip())
488 assert data["manifest_hash"] == "sha256:abc123"
489
490 def test_json_has_domain_id(self, repo: pathlib.Path) -> None:
491 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
492 result = runner.invoke(cli, _REQUIRED_ARGS + ["--json"])
493 data = json.loads(result.output.strip())
494 assert data["domain_id"] == "dom-001"
495
496 def test_text_shows_scoped_id(self, repo: pathlib.Path) -> None:
497 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
498 result = runner.invoke(cli, _REQUIRED_ARGS)
499 assert "@alice/genomics" in result.output
500
501 def test_text_shows_manifest_hash(self, repo: pathlib.Path) -> None:
502 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
503 result = runner.invoke(cli, _REQUIRED_ARGS)
504 assert "sha256:abc123" in result.output
505
506 def test_text_shows_agent_quickstart(self, repo: pathlib.Path) -> None:
507 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
508 result = runner.invoke(cli, _REQUIRED_ARGS)
509 assert "musehub_get_domain" in result.output or "musehub_create_repo" in result.output
510
511 def test_missing_token_exits_1(self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
512 monkeypatch.setattr("muse.cli.commands.domains.get_signing_identity", lambda *a, **kw: None)
513 result = runner.invoke(cli, _REQUIRED_ARGS)
514 assert result.exit_code == 1
515
516 def test_version_flag_propagated(self, repo: pathlib.Path) -> None:
517 captured: list[bytes] = []
518
519 def _cap(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
520 captured.append(req.data or b"")
521 return _mock_ok()
522
523 with unittest.mock.patch("urllib.request.urlopen", side_effect=_cap):
524 result = runner.invoke(cli, _REQUIRED_ARGS + ["--version", "3.0.0"])
525 assert result.exit_code == 0
526 assert json.loads(captured[0])["version"] == "3.0.0"
527
528 def test_default_version_is_0_1_0(self, repo: pathlib.Path) -> None:
529 captured: list[bytes] = []
530
531 def _cap(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
532 captured.append(req.data or b"")
533 return _mock_ok()
534
535 args_no_version = [a for a in _REQUIRED_ARGS if a != "--version"]
536 with unittest.mock.patch("urllib.request.urlopen", side_effect=_cap):
537 result = runner.invoke(cli, args_no_version)
538 assert result.exit_code == 0
539 assert json.loads(captured[0])["version"] == "0.1.0"
540
541 def test_display_name_propagated_as_display_name(self, repo: pathlib.Path) -> None:
542 captured: list[bytes] = []
543
544 def _cap(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
545 captured.append(req.data or b"")
546 return _mock_ok()
547
548 with unittest.mock.patch("urllib.request.urlopen", side_effect=_cap):
549 result = runner.invoke(cli, _REQUIRED_ARGS)
550 assert result.exit_code == 0
551 assert json.loads(captured[0])["display_name"] == "Genomics"
552
553 def test_409_conflict_exits_1(self, repo: pathlib.Path) -> None:
554 import http.client, io
555 err = urllib.error.HTTPError(
556 url="https://hub.test/api/v1/domains", code=409,
557 msg="Conflict", hdrs=http.client.HTTPMessage(),
558 fp=io.BytesIO(b'{"error":"already_exists"}'),
559 )
560 with unittest.mock.patch("urllib.request.urlopen", side_effect=err):
561 result = runner.invoke(cli, _REQUIRED_ARGS)
562 assert result.exit_code == 1
563
564 def test_401_auth_failure_exits_1(self, repo: pathlib.Path) -> None:
565 import http.client, io
566 err = urllib.error.HTTPError(
567 url="https://hub.test/api/v1/domains", code=401,
568 msg="Unauthorized", hdrs=http.client.HTTPMessage(),
569 fp=io.BytesIO(b'{"error":"unauthorized"}'),
570 )
571 with unittest.mock.patch("urllib.request.urlopen", side_effect=err):
572 result = runner.invoke(cli, _REQUIRED_ARGS)
573 assert result.exit_code == 1
574
575 def test_bad_capabilities_json_exits_1(self, repo: pathlib.Path) -> None:
576 bad_caps_args = [
577 "domains", "publish",
578 "--author", "alice", "--slug", "x",
579 "--name", "X", "--description", "Y",
580 "--viewer-type", "v",
581 "--capabilities", "{bad json}",
582 "--hub", "https://hub.test",
583 ]
584 result = runner.invoke(cli, bad_caps_args)
585 assert result.exit_code == 1
586
587 def test_capabilities_dimensions_parsed_correctly(self, repo: pathlib.Path) -> None:
588 captured: list[bytes] = []
589
590 def _cap(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
591 captured.append(req.data or b"")
592 return _mock_ok()
593
594 with unittest.mock.patch("urllib.request.urlopen", side_effect=_cap):
595 result = runner.invoke(cli, _REQUIRED_ARGS)
596 assert result.exit_code == 0
597 dims = json.loads(captured[0])["capabilities"]["dimensions"]
598 assert any(d["name"] == "sequence" for d in dims)
599
600
601 # ---------------------------------------------------------------------------
602 # Security β€” muse domains publish
603 # ---------------------------------------------------------------------------
604
605
606 class TestPublishSecurity:
607 def test_file_scheme_rejected(self, repo: pathlib.Path) -> None:
608 """file:// hub URL is blocked before any network call (SSRF guard)."""
609 args = _REQUIRED_ARGS[:-2] + ["--hub", "file:///etc/passwd"]
610 result = runner.invoke(cli, args)
611 assert result.exit_code == 1
612 assert "file" in result.stderr.lower() or "scheme" in result.stderr.lower() or "Blocked" in result.stderr
613
614 def test_ftp_scheme_rejected(self, repo: pathlib.Path) -> None:
615 args = _REQUIRED_ARGS[:-2] + ["--hub", "ftp://attacker.example.com"]
616 result = runner.invoke(cli, args)
617 assert result.exit_code == 1
618
619 def test_ansi_in_server_scoped_id_stripped_text(self, repo: pathlib.Path) -> None:
620 """ANSI in server-returned scoped_id is sanitized in text output."""
621 malicious_body = json.dumps({
622 "domain_id": "dom-001",
623 "scoped_id": "@alice/\x1b[31mmalicious\x1b[0m",
624 "manifest_hash": "sha256:abc",
625 })
626 mock_resp = unittest.mock.MagicMock()
627 mock_resp.read.return_value = malicious_body.encode()
628 mock_resp.__enter__ = unittest.mock.MagicMock(return_value=mock_resp)
629 mock_resp.__exit__ = unittest.mock.MagicMock(return_value=False)
630
631 with unittest.mock.patch("urllib.request.urlopen", return_value=mock_resp):
632 result = runner.invoke(cli, _REQUIRED_ARGS)
633 assert result.exit_code == 0
634 assert "\x1b[" not in result.output
635
636 def test_ansi_in_server_manifest_hash_stripped_text(self, repo: pathlib.Path) -> None:
637 """ANSI in server-returned manifest_hash is sanitized in text output."""
638 malicious_body = json.dumps({
639 "domain_id": "dom-001",
640 "scoped_id": "@alice/genomics",
641 "manifest_hash": "sha256:\x1b[31mbad\x1b[0m",
642 })
643 mock_resp = unittest.mock.MagicMock()
644 mock_resp.read.return_value = malicious_body.encode()
645 mock_resp.__enter__ = unittest.mock.MagicMock(return_value=mock_resp)
646 mock_resp.__exit__ = unittest.mock.MagicMock(return_value=False)
647
648 with unittest.mock.patch("urllib.request.urlopen", return_value=mock_resp):
649 result = runner.invoke(cli, _REQUIRED_ARGS)
650 assert result.exit_code == 0
651 assert "\x1b[" not in result.output
652
653 def test_missing_token_no_network_call(self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> None:
654 """When token is absent, no HTTP request is made."""
655 monkeypatch.setattr("muse.cli.commands.domains.get_signing_identity", lambda *a, **kw: None)
656 call_count: list[int] = [0]
657
658 def _fail(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
659 call_count[0] += 1
660 return _mock_ok()
661
662 with unittest.mock.patch("urllib.request.urlopen", side_effect=_fail):
663 result = runner.invoke(cli, _REQUIRED_ARGS)
664 assert result.exit_code == 1
665 assert call_count[0] == 0, "HTTP was called despite missing token"
666
667 def test_non_dict_server_response_exits_1(self, repo: pathlib.Path) -> None:
668 """If MuseHub returns a JSON array instead of object, exit cleanly."""
669 bad_resp = unittest.mock.MagicMock()
670 bad_resp.read.return_value = b"[1, 2, 3]"
671 bad_resp.__enter__ = unittest.mock.MagicMock(return_value=bad_resp)
672 bad_resp.__exit__ = unittest.mock.MagicMock(return_value=False)
673
674 with unittest.mock.patch("urllib.request.urlopen", return_value=bad_resp):
675 result = runner.invoke(cli, _REQUIRED_ARGS)
676 assert result.exit_code == 1
677
678
679 # ---------------------------------------------------------------------------
680 # Stress β€” muse domains publish (supplemental)
681 # ---------------------------------------------------------------------------
682
683
684 class TestPublishStress:
685 def test_50_sequential_invocations_all_succeed(self, repo: pathlib.Path) -> None:
686 """50 sequential CLI publish invocations all exit 0."""
687 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
688 for i in range(50):
689 result = runner.invoke(cli, _REQUIRED_ARGS)
690 assert result.exit_code == 0, f"Invocation {i} failed: {result.output}"
691
692 def test_large_description_10000_chars_transmitted(self, repo: pathlib.Path) -> None:
693 """10 000-character description is transmitted verbatim without truncation."""
694 long_desc = "X" * 10_000
695 captured: list[bytes] = []
696
697 def _cap(req: urllib.request.Request, timeout: float | None = None) -> unittest.mock.MagicMock:
698 captured.append(req.data or b"")
699 return _mock_ok()
700
701 args = [
702 "domains", "publish",
703 "--author", "alice", "--slug", "bigdesc",
704 "--name", "BigDesc",
705 "--description", long_desc,
706 "--viewer-type", "generic",
707 "--capabilities", '{"merge_semantics":"three_way"}',
708 "--hub", "https://hub.test",
709 ]
710 with unittest.mock.patch("urllib.request.urlopen", side_effect=_cap):
711 result = runner.invoke(cli, args)
712 assert result.exit_code == 0
713 assert json.loads(captured[0])["description"] == long_desc
714
715 def test_20_concurrent_post_json_calls(self) -> None:
716 """20 concurrent _post_json calls all return the expected scoped_id."""
717 import concurrent.futures
718 payload = _PublishPayload(
719 author_slug="alice", slug="genomics",
720 display_name="Genomics", description="DNA",
721 capabilities=_Capabilities(merge_semantics="three_way"),
722 viewer_type="genome", version="0.1.0",
723 )
724 with unittest.mock.patch("urllib.request.urlopen", return_value=_mock_ok()):
725 with concurrent.futures.ThreadPoolExecutor(max_workers=20) as pool:
726 futures = [
727 pool.submit(_post_json, "https://hub.test/api/v1/domains", payload, _make_signing())
728 for _ in range(20)
729 ]
730 results = [f.result() for f in futures]
731 assert len(results) == 20
732 assert all(r["scoped_id"] == "@alice/genomics" for r in results)