"""muse#20 Phase 4 — cross-host signing identity regression guard. Every command that talks to a remote must resolve the signing identity against the ACTUAL target hostname, not the repo's default [hub] url. muse#20's background bug (the ls-remote HTTP 401) and the domains.py bug fixed in Phase 1 are both instances of the same mistake: computing the target URL, then calling `get_signing_identity(root)` without forwarding it, so the lookup silently falls back to the wrong host's key. This is a single integration-style test (mocked transport/network — no live hub required, unlike tests/test_wire_localhost.py) that configures a repo whose default [hub] url and a named remote deliberately point at DIFFERENT hostnames, then drives `ls-remote`, `fetch`, and `domains publish` against the named remote — the three callers the ticket's Phase 4 specifically names — asserting each one resolves identity against the remote's own hostname. Intended to catch this exact regression class immediately whenever a new remote-talking command is added without forwarding the resolved URL. """ from __future__ import annotations import json import pathlib from unittest.mock import patch from tests.cli_test_helper import CliRunner from muse.core.paths import muse_dir cli = None runner = CliRunner() _DEFAULT_HUB = "https://localhost:1337" _TARGET_REMOTE_URL = "https://staging.musehub.ai/gabriel/muse" _TARGET_HOST = "staging.musehub.ai" def _env(root: pathlib.Path) -> dict[str, str]: return {"MUSE_REPO_ROOT": str(root)} def _init_repo(tmp_path: pathlib.Path) -> pathlib.Path: """Repo whose default [hub] url and the "staging" remote point at deliberately different hosts, so a wrong-hostname identity lookup is observable.""" dot_muse = muse_dir(tmp_path) for sub in ("commits", "snapshots", "objects", "refs/heads"): (dot_muse / sub).mkdir(parents=True, exist_ok=True) (dot_muse / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8") (dot_muse / "repo.json").write_text( json.dumps({"repo_id": "test-repo", "domain": "code"}), encoding="utf-8" ) (dot_muse / "config.toml").write_text( f'[hub]\nurl = "{_DEFAULT_HUB}"\n\n' f'[remotes.staging]\nurl = "{_TARGET_REMOTE_URL}"\n', encoding="utf-8", ) return tmp_path def _make_remote_info() -> dict: return { "repo_id": "test-repo", "domain": "code", "default_branch": "main", "branch_heads": {"main": "sha256:" + "a" * 64}, } class TestCrossHostSigningIdentity: def test_ls_remote_uses_target_host_not_default_hub(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) with patch("muse.cli.commands.ls_remote.HttpTransport") as mt: with patch("muse.cli.commands.ls_remote.get_signing_identity") as mock_gsi: mock_gsi.return_value = None mt.return_value.fetch_remote_info.return_value = _make_remote_info() r = runner.invoke(cli, ["ls-remote", "staging", "--json"], env=_env(repo)) assert r.exit_code == 0, r.output mock_gsi.assert_called_once() _, kwargs = mock_gsi.call_args assert kwargs.get("remote_url") == _TARGET_REMOTE_URL assert _TARGET_HOST in kwargs["remote_url"] assert "localhost" not in kwargs["remote_url"] def test_fetch_uses_target_host_not_default_hub(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) with patch("muse.cli.commands.fetch.get_signing_identity") as mock_gsi: mock_gsi.return_value = None with patch("muse.cli.commands.fetch.make_transport") as mt: mt.return_value.fetch_remote_info.return_value = _make_remote_info() r = runner.invoke(cli, ["fetch", "staging", "--json"], env=_env(repo)) assert r.exit_code == 0, r.output mock_gsi.assert_called_once() _, kwargs = mock_gsi.call_args assert kwargs.get("remote_url") == _TARGET_REMOTE_URL assert _TARGET_HOST in kwargs["remote_url"] assert "localhost" not in kwargs["remote_url"] def test_domains_publish_uses_target_host_not_default_hub(self, tmp_path: pathlib.Path) -> None: repo = _init_repo(tmp_path) with patch("muse.cli.commands.domains.get_signing_identity") as mock_gsi: mock_gsi.return_value = None runner.invoke( cli, [ "domains", "publish", "--author", "gabriel", "--slug", "genomics", "--name", "Genomics", "--description", "desc", "--viewer-type", "generic", "--hub", _TARGET_REMOTE_URL, ], env=_env(repo), ) mock_gsi.assert_called_once() _, kwargs = mock_gsi.call_args assert kwargs.get("remote_url") == _TARGET_REMOTE_URL assert _TARGET_HOST in kwargs["remote_url"] assert "localhost" not in kwargs["remote_url"] def test_no_caller_ever_falls_back_to_default_hub_host(self, tmp_path: pathlib.Path) -> None: """Belt-and-suspenders: across all three callers, none resolves identity against the repo's default [hub] url host once a different target is explicit (named remote or --hub).""" repo = _init_repo(tmp_path) seen_hosts: list[str] = [] with patch("muse.cli.commands.ls_remote.HttpTransport") as mt1: with patch("muse.cli.commands.ls_remote.get_signing_identity") as gsi1: gsi1.return_value = None mt1.return_value.fetch_remote_info.return_value = _make_remote_info() runner.invoke(cli, ["ls-remote", "staging", "--json"], env=_env(repo)) seen_hosts.append(gsi1.call_args.kwargs.get("remote_url", "")) with patch("muse.cli.commands.fetch.get_signing_identity") as gsi2: gsi2.return_value = None with patch("muse.cli.commands.fetch.make_transport") as mt2: mt2.return_value.fetch_remote_info.return_value = _make_remote_info() runner.invoke(cli, ["fetch", "staging", "--json"], env=_env(repo)) seen_hosts.append(gsi2.call_args.kwargs.get("remote_url", "")) with patch("muse.cli.commands.domains.get_signing_identity") as gsi3: gsi3.return_value = None runner.invoke( cli, [ "domains", "publish", "--author", "gabriel", "--slug", "genomics", "--name", "Genomics", "--description", "desc", "--viewer-type", "generic", "--hub", _TARGET_REMOTE_URL, ], env=_env(repo), ) seen_hosts.append(gsi3.call_args.kwargs.get("remote_url", "")) assert all(_TARGET_HOST in h for h in seen_hosts), seen_hosts assert not any("localhost" in h for h in seen_hosts), seen_hosts