"""Supercharge tests for muse remote — agent-first JSON envelope. All seven subcommands (list, add, remove, rename, get-url, set-url, status) must emit ``duration_ms`` and ``exit_code`` in every JSON response — success and error alike. Agents poll these to measure latency and confirm outcomes without parsing exit codes separately. Coverage tiers -------------- I Unit — TypedDict field presence II Integration — every subcommand JSON success path carries the envelope III Integration — every subcommand JSON error path carries the envelope IV End-to-end — exit_code in JSON matches the process exit code V Data integrity — duration_ms is a non-negative integer; exit_code is int VI Security — envelope present even on validation-rejected inputs VII Performance — local subcommands complete within 200 ms """ from __future__ import annotations import json import time import pathlib import threading import pytest from tests.cli_test_helper import CliRunner from muse.core.paths import muse_dir runner = CliRunner() # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture() def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: """Minimal Muse repo with .muse/config.toml wired up.""" dot_muse = muse_dir(tmp_path) dot_muse.mkdir() (dot_muse / "config.toml").write_text('[repo]\nname = "test"\n') monkeypatch.chdir(tmp_path) return tmp_path @pytest.fixture() def repo_with_origin(repo: pathlib.Path) -> pathlib.Path: """Repo pre-loaded with a single remote named 'origin'.""" runner.invoke(None, ["remote", "add", "origin", "https://musehub.ai/gabriel/test"]) return repo # --------------------------------------------------------------------------- # I Unit — TypedDict field presence # --------------------------------------------------------------------------- class TestTypedDictFields: def test_I1_remote_list_json_has_duration_ms(self) -> None: """_RemoteListJson TypedDict must declare duration_ms.""" from muse.cli.commands.remote import _RemoteListJson import typing hints = typing.get_type_hints(_RemoteListJson) assert "duration_ms" in hints, "_RemoteListJson missing duration_ms field" def test_I2_remote_list_json_has_exit_code(self) -> None: """_RemoteListJson TypedDict must declare exit_code.""" from muse.cli.commands.remote import _RemoteListJson import typing hints = typing.get_type_hints(_RemoteListJson) assert "exit_code" in hints, "_RemoteListJson missing exit_code field" def test_I3_mutation_json_has_duration_ms(self) -> None: """_RemoteMutationJson TypedDict must declare duration_ms.""" from muse.cli.commands.remote import _RemoteMutationJson import typing hints = typing.get_type_hints(_RemoteMutationJson) assert "duration_ms" in hints, "_RemoteMutationJson missing duration_ms field" def test_I4_mutation_json_has_exit_code(self) -> None: """_RemoteMutationJson TypedDict must declare exit_code.""" from muse.cli.commands.remote import _RemoteMutationJson import typing hints = typing.get_type_hints(_RemoteMutationJson) assert "exit_code" in hints, "_RemoteMutationJson missing exit_code field" def test_I5_get_url_json_has_duration_ms(self) -> None: """_RemoteGetUrlJson TypedDict must declare duration_ms.""" from muse.cli.commands.remote import _RemoteGetUrlJson import typing hints = typing.get_type_hints(_RemoteGetUrlJson) assert "duration_ms" in hints, "_RemoteGetUrlJson missing duration_ms field" def test_I6_get_url_json_has_exit_code(self) -> None: """_RemoteGetUrlJson TypedDict must declare exit_code.""" from muse.cli.commands.remote import _RemoteGetUrlJson import typing hints = typing.get_type_hints(_RemoteGetUrlJson) assert "exit_code" in hints, "_RemoteGetUrlJson missing exit_code field" def test_I7_status_json_has_duration_ms(self) -> None: """_RemoteStatusJson TypedDict must declare duration_ms.""" from muse.cli.commands.remote import _RemoteStatusJson import typing hints = typing.get_type_hints(_RemoteStatusJson) assert "duration_ms" in hints, "_RemoteStatusJson missing duration_ms field" def test_I8_status_json_has_exit_code(self) -> None: """_RemoteStatusJson TypedDict must declare exit_code.""" from muse.cli.commands.remote import _RemoteStatusJson import typing hints = typing.get_type_hints(_RemoteStatusJson) assert "exit_code" in hints, "_RemoteStatusJson missing exit_code field" # --------------------------------------------------------------------------- # II Integration — success JSON carries envelope # --------------------------------------------------------------------------- class TestSuccessEnvelope: def test_II1_list_json_has_envelope(self, repo: pathlib.Path) -> None: """muse remote --json must include duration_ms and exit_code.""" r = runner.invoke(None, ["remote", "--json"]) data = json.loads(r.output) assert "duration_ms" in data, "list JSON missing duration_ms" assert "exit_code" in data, "list JSON missing exit_code" def test_II2_add_json_has_envelope(self, repo: pathlib.Path) -> None: """muse remote add --json must include duration_ms and exit_code.""" r = runner.invoke(None, ["remote", "add", "origin", "https://musehub.ai/gabriel/test", "--json"]) data = json.loads(r.output) assert "duration_ms" in data, "add JSON missing duration_ms" assert "exit_code" in data, "add JSON missing exit_code" def test_II3_remove_json_has_envelope(self, repo_with_origin: pathlib.Path) -> None: """muse remote remove --json must include duration_ms and exit_code.""" r = runner.invoke(None, ["remote", "remove", "origin", "--json"]) data = json.loads(r.output) assert "duration_ms" in data, "remove JSON missing duration_ms" assert "exit_code" in data, "remove JSON missing exit_code" def test_II4_rename_json_has_envelope(self, repo_with_origin: pathlib.Path) -> None: """muse remote rename --json must include duration_ms and exit_code.""" r = runner.invoke(None, ["remote", "rename", "origin", "upstream", "--json"]) data = json.loads(r.output) assert "duration_ms" in data, "rename JSON missing duration_ms" assert "exit_code" in data, "rename JSON missing exit_code" def test_II5_get_url_json_has_envelope(self, repo_with_origin: pathlib.Path) -> None: """muse remote get-url --json must include duration_ms and exit_code.""" r = runner.invoke(None, ["remote", "get-url", "origin", "--json"]) data = json.loads(r.output) assert "duration_ms" in data, "get-url JSON missing duration_ms" assert "exit_code" in data, "get-url JSON missing exit_code" def test_II6_set_url_json_has_envelope(self, repo_with_origin: pathlib.Path) -> None: """muse remote set-url --json must include duration_ms and exit_code.""" r = runner.invoke(None, ["remote", "set-url", "origin", "https://musehub.ai/gabriel/new", "--json"]) data = json.loads(r.output) assert "duration_ms" in data, "set-url JSON missing duration_ms" assert "exit_code" in data, "set-url JSON missing exit_code" # --------------------------------------------------------------------------- # III Integration — error JSON carries envelope # --------------------------------------------------------------------------- class TestErrorEnvelope: def test_III1_add_duplicate_error_has_envelope(self, repo_with_origin: pathlib.Path) -> None: """muse remote add duplicate --json error must include duration_ms and exit_code.""" r = runner.invoke(None, ["remote", "add", "origin", "https://musehub.ai/x/y", "--json"]) data = json.loads(r.output) assert "duration_ms" in data, "add-duplicate error JSON missing duration_ms" assert "exit_code" in data, "add-duplicate error JSON missing exit_code" def test_III2_add_invalid_name_error_has_envelope(self, repo: pathlib.Path) -> None: """muse remote add invalid-name --json error must include envelope.""" r = runner.invoke(None, ["remote", "add", "bad name!", "https://musehub.ai/x/y", "--json"]) data = json.loads(r.output) assert "duration_ms" in data assert "exit_code" in data def test_III3_add_bad_scheme_error_has_envelope(self, repo: pathlib.Path) -> None: """muse remote add ftp:// --json error must include envelope.""" r = runner.invoke(None, ["remote", "add", "origin", "ftp://example.com/repo", "--json"]) data = json.loads(r.output) assert "duration_ms" in data assert "exit_code" in data def test_III4_remove_not_found_error_has_envelope(self, repo: pathlib.Path) -> None: """muse remote remove missing --json error must include envelope.""" r = runner.invoke(None, ["remote", "remove", "ghost", "--json"]) data = json.loads(r.output) assert "duration_ms" in data assert "exit_code" in data def test_III5_rename_not_found_error_has_envelope(self, repo: pathlib.Path) -> None: """muse remote rename missing --json error must include envelope.""" r = runner.invoke(None, ["remote", "rename", "ghost", "phantom", "--json"]) data = json.loads(r.output) assert "duration_ms" in data assert "exit_code" in data def test_III6_get_url_not_found_error_has_envelope(self, repo: pathlib.Path) -> None: """muse remote get-url missing --json error must include envelope.""" r = runner.invoke(None, ["remote", "get-url", "ghost", "--json"]) data = json.loads(r.output) assert "duration_ms" in data assert "exit_code" in data def test_III7_set_url_not_found_error_has_envelope(self, repo: pathlib.Path) -> None: """muse remote set-url missing --json error must include envelope.""" r = runner.invoke(None, ["remote", "set-url", "ghost", "https://musehub.ai/x/y", "--json"]) data = json.loads(r.output) assert "duration_ms" in data assert "exit_code" in data # --------------------------------------------------------------------------- # IV End-to-end — exit_code in JSON matches process exit code # --------------------------------------------------------------------------- class TestExitCodeAccuracy: def test_IV1_add_success_exit_code_is_0(self, repo: pathlib.Path) -> None: """exit_code: 0 in JSON on successful add.""" r = runner.invoke(None, ["remote", "add", "origin", "https://musehub.ai/gabriel/test", "--json"]) assert r.exit_code == 0 assert json.loads(r.output)["exit_code"] == 0 def test_IV2_add_error_exit_code_is_1(self, repo_with_origin: pathlib.Path) -> None: """exit_code: 1 in JSON when add fails (duplicate).""" r = runner.invoke(None, ["remote", "add", "origin", "https://musehub.ai/x/y", "--json"]) assert r.exit_code == 1 assert json.loads(r.output)["exit_code"] == 1 def test_IV3_remove_success_exit_code_is_0(self, repo_with_origin: pathlib.Path) -> None: """exit_code: 0 in JSON on successful remove.""" r = runner.invoke(None, ["remote", "remove", "origin", "--json"]) assert r.exit_code == 0 assert json.loads(r.output)["exit_code"] == 0 def test_IV4_remove_error_exit_code_is_1(self, repo: pathlib.Path) -> None: """exit_code: 1 in JSON when remove fails (not found).""" r = runner.invoke(None, ["remote", "remove", "ghost", "--json"]) assert r.exit_code == 1 assert json.loads(r.output)["exit_code"] == 1 def test_IV5_rename_success_exit_code_is_0(self, repo_with_origin: pathlib.Path) -> None: """exit_code: 0 in JSON on successful rename.""" r = runner.invoke(None, ["remote", "rename", "origin", "upstream", "--json"]) assert r.exit_code == 0 assert json.loads(r.output)["exit_code"] == 0 def test_IV6_get_url_success_exit_code_is_0(self, repo_with_origin: pathlib.Path) -> None: """exit_code: 0 in JSON on successful get-url.""" r = runner.invoke(None, ["remote", "get-url", "origin", "--json"]) assert r.exit_code == 0 assert json.loads(r.output)["exit_code"] == 0 def test_IV7_set_url_success_exit_code_is_0(self, repo_with_origin: pathlib.Path) -> None: """exit_code: 0 in JSON on successful set-url.""" r = runner.invoke(None, ["remote", "set-url", "origin", "https://musehub.ai/gabriel/new", "--json"]) assert r.exit_code == 0 assert json.loads(r.output)["exit_code"] == 0 def test_IV8_list_success_exit_code_is_0(self, repo: pathlib.Path) -> None: """exit_code: 0 in JSON on successful list (even empty).""" r = runner.invoke(None, ["remote", "--json"]) assert r.exit_code == 0 assert json.loads(r.output)["exit_code"] == 0 # --------------------------------------------------------------------------- # V Data integrity — field types and values # --------------------------------------------------------------------------- class TestEnvelopeTypes: def test_V1_duration_ms_is_non_negative_int_on_add(self, repo: pathlib.Path) -> None: """duration_ms must be a non-negative integer.""" r = runner.invoke(None, ["remote", "add", "origin", "https://musehub.ai/gabriel/test", "--json"]) data = json.loads(r.output) assert isinstance(data["duration_ms"], (int, float)), "duration_ms must be numeric" assert data["duration_ms"] >= 0, "duration_ms must be non-negative" def test_V2_duration_ms_is_non_negative_int_on_list(self, repo: pathlib.Path) -> None: """duration_ms on list is a non-negative numeric.""" r = runner.invoke(None, ["remote", "--json"]) data = json.loads(r.output) assert isinstance(data["duration_ms"], (int, float)) assert data["duration_ms"] >= 0 def test_V3_duration_ms_is_non_negative_int_on_error( self, repo: pathlib.Path ) -> None: """duration_ms on error path is a non-negative int.""" r = runner.invoke(None, ["remote", "remove", "ghost", "--json"]) data = json.loads(r.output) assert isinstance(data["duration_ms"], (int, float)) assert data["duration_ms"] >= 0 def test_V4_exit_code_is_int(self, repo: pathlib.Path) -> None: """exit_code must be a plain int in JSON.""" r = runner.invoke(None, ["remote", "--json"]) data = json.loads(r.output) assert isinstance(data["exit_code"], int) def test_V5_invalid_name_exit_code_matches(self, repo: pathlib.Path) -> None: """exit_code in JSON matches actual exit code for invalid-name errors.""" r = runner.invoke(None, ["remote", "add", "bad/name", "https://musehub.ai/x/y", "--json"]) data = json.loads(r.output) assert data["exit_code"] == r.exit_code def test_V6_all_success_fields_present_add(self, repo: pathlib.Path) -> None: """add success JSON has all documented fields including envelope.""" r = runner.invoke(None, ["remote", "add", "origin", "https://musehub.ai/gabriel/test", "--json"]) data = json.loads(r.output) for field in ("status", "name", "url", "old_url", "old_name", "new_name", "duration_ms", "exit_code"): assert field in data, f"add JSON missing field: {field}" def test_V7_all_success_fields_present_get_url( self, repo_with_origin: pathlib.Path ) -> None: """get-url success JSON has all documented fields including envelope.""" r = runner.invoke(None, ["remote", "get-url", "origin", "--json"]) data = json.loads(r.output) for field in ("name", "url", "duration_ms", "exit_code"): assert field in data, f"get-url JSON missing field: {field}" def test_V8_all_success_fields_present_list(self, repo: pathlib.Path) -> None: """list success JSON has all documented fields including envelope.""" r = runner.invoke(None, ["remote", "--json"]) data = json.loads(r.output) for field in ("remotes", "duration_ms", "exit_code"): assert field in data, f"list JSON missing field: {field}" # --------------------------------------------------------------------------- # VI Security — envelope present even on adversarial inputs # --------------------------------------------------------------------------- class TestSecurityEnvelope: def test_VI1_ansi_in_name_error_has_envelope(self, repo: pathlib.Path) -> None: """ANSI-injected remote name error JSON has envelope.""" r = runner.invoke(None, ["remote", "add", "\x1b[31mmalicious", "https://musehub.ai/x/y", "--json"]) data = json.loads(r.output) assert "duration_ms" in data assert "exit_code" in data assert data["exit_code"] != 0 def test_VI2_file_scheme_error_has_envelope(self, repo: pathlib.Path) -> None: """file:// URL scheme rejection carries envelope.""" r = runner.invoke(None, ["remote", "add", "malicious", "file:///etc/passwd", "--json"]) data = json.loads(r.output) assert "duration_ms" in data assert "exit_code" in data assert data["exit_code"] != 0 def test_VI3_oversized_name_error_has_envelope(self, repo: pathlib.Path) -> None: """Oversized remote name error JSON has envelope.""" long_name = "a" * 101 r = runner.invoke(None, ["remote", "add", long_name, "https://musehub.ai/x/y", "--json"]) data = json.loads(r.output) assert "duration_ms" in data assert "exit_code" in data def test_VI4_oversized_url_error_has_envelope(self, repo: pathlib.Path) -> None: """Oversized URL error JSON has envelope.""" long_url = f"https://musehub.ai/{'a' * 2048}" r = runner.invoke(None, ["remote", "add", "origin", long_url, "--json"]) data = json.loads(r.output) assert "duration_ms" in data assert "exit_code" in data def test_VI5_rename_new_name_invalid_error_has_envelope( self, repo_with_origin: pathlib.Path ) -> None: """Invalid new name in rename error JSON has envelope.""" r = runner.invoke(None, ["remote", "rename", "origin", "bad name!", "--json"]) data = json.loads(r.output) assert "duration_ms" in data assert "exit_code" in data # --------------------------------------------------------------------------- # VII Performance — local subcommands complete quickly # --------------------------------------------------------------------------- class TestPerformance: _LIMIT_MS = 200 def test_VII1_add_completes_within_limit(self, repo: pathlib.Path) -> None: """muse remote add --json completes within 200 ms.""" r = runner.invoke(None, ["remote", "add", "origin", "https://musehub.ai/gabriel/test", "--json"]) data = json.loads(r.output) assert data["duration_ms"] <= self._LIMIT_MS, ( f"add took {data['duration_ms']} ms — exceeds {self._LIMIT_MS} ms limit" ) def test_VII2_list_completes_within_limit(self, repo: pathlib.Path) -> None: """muse remote --json completes within 200 ms.""" r = runner.invoke(None, ["remote", "--json"]) data = json.loads(r.output) assert data["duration_ms"] <= self._LIMIT_MS, ( f"list took {data['duration_ms']} ms — exceeds {self._LIMIT_MS} ms limit" ) def test_VII3_remove_completes_within_limit( self, repo_with_origin: pathlib.Path ) -> None: """muse remote remove --json completes within 200 ms.""" r = runner.invoke(None, ["remote", "remove", "origin", "--json"]) data = json.loads(r.output) assert data["duration_ms"] <= self._LIMIT_MS def test_VII4_get_url_completes_within_limit( self, repo_with_origin: pathlib.Path ) -> None: """muse remote get-url --json completes within 200 ms.""" r = runner.invoke(None, ["remote", "get-url", "origin", "--json"]) data = json.loads(r.output) assert data["duration_ms"] <= self._LIMIT_MS def test_VII5_set_url_completes_within_limit( self, repo_with_origin: pathlib.Path ) -> None: """muse remote set-url --json completes within 200 ms.""" r = runner.invoke(None, ["remote", "set-url", "origin", "https://musehub.ai/gabriel/new", "--json"]) data = json.loads(r.output) assert data["duration_ms"] <= self._LIMIT_MS def test_VII6_rename_completes_within_limit( self, repo_with_origin: pathlib.Path ) -> None: """muse remote rename --json completes within 200 ms.""" r = runner.invoke(None, ["remote", "rename", "origin", "upstream", "--json"]) data = json.loads(r.output) assert data["duration_ms"] <= self._LIMIT_MS class TestRegisterFlags: def test_default_json_out_is_false(self) -> None: import argparse from muse.cli.commands.remote import register p = argparse.ArgumentParser() subs = p.add_subparsers() register(subs) args = p.parse_args(["remote"]) assert args.json_out is False def test_json_flag_sets_json_out(self) -> None: import argparse from muse.cli.commands.remote import register p = argparse.ArgumentParser() subs = p.add_subparsers() register(subs) args = p.parse_args(["remote", "--json"]) assert args.json_out is True def test_j_shorthand_sets_json_out(self) -> None: import argparse from muse.cli.commands.remote import register p = argparse.ArgumentParser() subs = p.add_subparsers() register(subs) args = p.parse_args(["remote", "-j"]) assert args.json_out is True