test_domains_publish_supported_commands.py
python
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be
Merge branch 'fix/hub-user-read-update-path' into dev
Human
9 days ago
| 1 | """Tests for auto-derived `supported_commands` in `muse domains publish` — |
| 2 | muse#74 Phase 3 (SCR_08-12). |
| 3 | |
| 4 | Before this fix, `run_publish`'s auto-derive path (no `--capabilities`) |
| 5 | hardcoded `supported_commands=["commit", "diff", "merge", "log", "status"]` |
| 6 | for every domain, regardless of what it actually supports. This file pins |
| 7 | the fix: the auto-derive path (and the explicit-`--capabilities`-with- |
| 8 | omitted-key path, publish only) now call |
| 9 | `domain_command_registry.get_supported_commands(active_domain_name)` |
| 10 | instead. |
| 11 | |
| 12 | `run_update` is deliberately excluded from this fallback (see the plan's |
| 13 | Design section) — SCR_12 pins that its omitted-key behavior stays `[]`, |
| 14 | unchanged. |
| 15 | """ |
| 16 | from __future__ import annotations |
| 17 | |
| 18 | import json |
| 19 | import pathlib |
| 20 | import unittest.mock |
| 21 | import urllib.request |
| 22 | from typing import TYPE_CHECKING |
| 23 | |
| 24 | from tests.cli_test_helper import CliRunner |
| 25 | |
| 26 | from muse._version import __version__ |
| 27 | from muse.core.paths import muse_dir |
| 28 | |
| 29 | if TYPE_CHECKING: |
| 30 | from muse.core.transport import SigningIdentity |
| 31 | |
| 32 | cli = None # argparse migration — CliRunner ignores this arg |
| 33 | |
| 34 | runner = CliRunner() |
| 35 | |
| 36 | |
| 37 | def _make_signing() -> "SigningIdentity": |
| 38 | from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey |
| 39 | from muse.core.transport import SigningIdentity |
| 40 | return SigningIdentity(handle="testuser", private_key=Ed25519PrivateKey.generate()) |
| 41 | |
| 42 | |
| 43 | def _make_repo(tmp_path: pathlib.Path, monkeypatch, domain: str) -> pathlib.Path: |
| 44 | dot_muse = muse_dir(tmp_path) |
| 45 | (dot_muse / "refs" / "heads").mkdir(parents=True) |
| 46 | (dot_muse / "objects").mkdir() |
| 47 | (dot_muse / "commits").mkdir() |
| 48 | (dot_muse / "snapshots").mkdir() |
| 49 | (dot_muse / "repo.json").write_text( |
| 50 | json.dumps({"repo_id": "test-repo", "schema_version": __version__, "domain": domain}) |
| 51 | ) |
| 52 | (dot_muse / "HEAD").write_text("ref: refs/heads/main\n") |
| 53 | monkeypatch.chdir(tmp_path) |
| 54 | monkeypatch.setattr("muse.cli.commands.domains.get_signing_identity", lambda *a, **kw: _make_signing()) |
| 55 | return tmp_path |
| 56 | |
| 57 | |
| 58 | def _mock_urlopen_capture() -> tuple[list[urllib.request.Request], object]: |
| 59 | captured: list[urllib.request.Request] = [] |
| 60 | |
| 61 | def fake_urlopen( |
| 62 | req: urllib.request.Request, timeout: float | None = None, context: object = None |
| 63 | ) -> unittest.mock.MagicMock: |
| 64 | captured.append(req) |
| 65 | resp = unittest.mock.MagicMock() |
| 66 | resp.read.return_value = json.dumps( |
| 67 | {"domain_id": "d1", "scoped_id": "@testuser/genomics", "manifest_hash": "sha256:abc"} |
| 68 | ).encode() |
| 69 | resp.__enter__ = unittest.mock.MagicMock(return_value=resp) |
| 70 | resp.__exit__ = unittest.mock.MagicMock(return_value=False) |
| 71 | return resp |
| 72 | |
| 73 | return captured, fake_urlopen |
| 74 | |
| 75 | |
| 76 | _BASE_ARGS = [ |
| 77 | "domains", "publish", |
| 78 | "--author", "testuser", |
| 79 | "--slug", "genomics", |
| 80 | "--name", "Genomics", |
| 81 | "--description", "Version DNA sequences", |
| 82 | "--viewer-type", "genome", |
| 83 | "--hub", "https://hub.test", |
| 84 | ] |
| 85 | |
| 86 | |
| 87 | def _sent_supported_commands(captured: list[urllib.request.Request]) -> list[str]: |
| 88 | assert len(captured) == 1, "expected exactly one HTTP request" |
| 89 | body = json.loads(captured[0].data.decode()) |
| 90 | return list(body["capabilities"]["supported_commands"]) |
| 91 | |
| 92 | |
| 93 | class TestAutoDeriveFromRealCliNamespace: |
| 94 | def test_scr08_code_domain_gets_real_commands_not_hardcoded_five( |
| 95 | self, tmp_path: pathlib.Path, monkeypatch: "pytest.MonkeyPatch" # noqa: F821 |
| 96 | ) -> None: |
| 97 | _make_repo(tmp_path, monkeypatch, domain="code") |
| 98 | captured, fake_urlopen = _mock_urlopen_capture() |
| 99 | |
| 100 | with unittest.mock.patch("urllib.request.urlopen", fake_urlopen): |
| 101 | result = runner.invoke(cli, _BASE_ARGS) |
| 102 | |
| 103 | assert result.exit_code == 0, result.output |
| 104 | commands = _sent_supported_commands(captured) |
| 105 | assert commands != ["commit", "diff", "merge", "log", "status"], ( |
| 106 | "still sending the old hardcoded universal list" |
| 107 | ) |
| 108 | for anchor in ("grep", "impact", "symbols"): |
| 109 | assert anchor in commands, f"{anchor!r} missing from published code capabilities" |
| 110 | assert len(commands) > 30 |
| 111 | |
| 112 | def test_scr09_identity_domain_gets_empty_list( |
| 113 | self, tmp_path: pathlib.Path, monkeypatch: "pytest.MonkeyPatch" # noqa: F821 |
| 114 | ) -> None: |
| 115 | _make_repo(tmp_path, monkeypatch, domain="identity") |
| 116 | captured, fake_urlopen = _mock_urlopen_capture() |
| 117 | |
| 118 | with unittest.mock.patch("urllib.request.urlopen", fake_urlopen): |
| 119 | result = runner.invoke(cli, _BASE_ARGS) |
| 120 | |
| 121 | assert result.exit_code == 0, result.output |
| 122 | assert _sent_supported_commands(captured) == [] |
| 123 | |
| 124 | |
| 125 | class TestExplicitCapabilitiesOverride: |
| 126 | def test_scr10_explicit_supported_commands_key_overrides_auto_derive( |
| 127 | self, tmp_path: pathlib.Path, monkeypatch: "pytest.MonkeyPatch" # noqa: F821 |
| 128 | ) -> None: |
| 129 | _make_repo(tmp_path, monkeypatch, domain="code") |
| 130 | captured, fake_urlopen = _mock_urlopen_capture() |
| 131 | caps = json.dumps({ |
| 132 | "dimensions": [{"name": "x", "description": "y"}], |
| 133 | "merge_semantics": "three_way", |
| 134 | "supported_commands": ["custom-one", "custom-two"], |
| 135 | }) |
| 136 | |
| 137 | with unittest.mock.patch("urllib.request.urlopen", fake_urlopen): |
| 138 | result = runner.invoke(cli, [*_BASE_ARGS, "--capabilities", caps]) |
| 139 | |
| 140 | assert result.exit_code == 0, result.output |
| 141 | assert _sent_supported_commands(captured) == ["custom-one", "custom-two"] |
| 142 | |
| 143 | def test_scr11_explicit_capabilities_without_supported_commands_key_falls_back_to_auto_derive( |
| 144 | self, tmp_path: pathlib.Path, monkeypatch: "pytest.MonkeyPatch" # noqa: F821 |
| 145 | ) -> None: |
| 146 | _make_repo(tmp_path, monkeypatch, domain="code") |
| 147 | captured, fake_urlopen = _mock_urlopen_capture() |
| 148 | caps = json.dumps({ |
| 149 | "dimensions": [{"name": "x", "description": "y"}], |
| 150 | "merge_semantics": "three_way", |
| 151 | }) |
| 152 | |
| 153 | with unittest.mock.patch("urllib.request.urlopen", fake_urlopen): |
| 154 | result = runner.invoke(cli, [*_BASE_ARGS, "--capabilities", caps]) |
| 155 | |
| 156 | assert result.exit_code == 0, result.output |
| 157 | commands = _sent_supported_commands(captured) |
| 158 | assert commands != [] |
| 159 | assert "grep" in commands |
| 160 | |
| 161 | |
| 162 | class TestUpdateBehaviorDeliberatelyUnchanged: |
| 163 | def test_scr12_update_with_capabilities_and_no_supported_commands_key_stays_empty( |
| 164 | self, tmp_path: pathlib.Path, monkeypatch: "pytest.MonkeyPatch" # noqa: F821 |
| 165 | ) -> None: |
| 166 | _make_repo(tmp_path, monkeypatch, domain="code") |
| 167 | captured, fake_urlopen = _mock_urlopen_capture() |
| 168 | caps = json.dumps({ |
| 169 | "dimensions": [{"name": "x", "description": "y"}], |
| 170 | "merge_semantics": "three_way", |
| 171 | }) |
| 172 | update_args = [ |
| 173 | "domains", "update", |
| 174 | "--author", "testuser", |
| 175 | "--slug", "genomics", |
| 176 | "--capabilities", caps, |
| 177 | "--hub", "https://hub.test", |
| 178 | ] |
| 179 | |
| 180 | with unittest.mock.patch("urllib.request.urlopen", fake_urlopen): |
| 181 | result = runner.invoke(cli, update_args) |
| 182 | |
| 183 | assert result.exit_code == 0, result.output |
| 184 | assert len(captured) == 1 |
| 185 | body = json.loads(captured[0].data.decode()) |
| 186 | # `update` sends the whole capabilities object only when provided; |
| 187 | # this pins that it is NOT auto-derived — omitted key stays []. |
| 188 | assert body["capabilities"]["supported_commands"] == [] |
File History
2 commits
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be
Merge branch 'fix/hub-user-read-update-path' into dev
Human
9 days ago
sha256:b7be56ec091919a612cffe7f3c8b600209d5155517443fdac0e16954c8c7d81f
Merge 'task/git-export-ignored-file-deletion' into 'dev' — …
Human
12 days ago