test_mist_read_api_path.py
python
sha256:c287f599c5429903a139eadf3c5db5d930520e57cb0c3c575d9570e953c3b2d6
chore: bump version to 0.2.0.dev2 — nightly.2
Sonnet 4.6
patch
12 days ago
| 1 | """Regression tests for muse mist read #44. |
| 2 | |
| 3 | ``run_read`` built the wrong API path for the ``owner/ID`` input form — |
| 4 | ``/api/{owner}/mists/{id}`` instead of the real route ``/api/mists/{id}`` |
| 5 | (owner is not part of the path; every other mist subcommand already |
| 6 | discards it correctly). The wrong path hits no matching route, and the |
| 7 | resulting non-JSON response surfaced as a misleading |
| 8 | "Server returned invalid JSON" error instead of the actual HTTP status. |
| 9 | """ |
| 10 | |
| 11 | from __future__ import annotations |
| 12 | |
| 13 | import argparse |
| 14 | import pathlib |
| 15 | import unittest.mock as mock |
| 16 | |
| 17 | import pytest |
| 18 | |
| 19 | from muse.core.identity import IdentityEntry |
| 20 | from muse.core.transport import TransportError |
| 21 | |
| 22 | |
| 23 | def _args(mist_id: str, *, json_output: bool = True) -> argparse.Namespace: |
| 24 | return argparse.Namespace(mist_id=mist_id, json_output=json_output, hub="https://musehub.ai") |
| 25 | |
| 26 | |
| 27 | def _fake_identity() -> IdentityEntry: |
| 28 | return {"type": "human", "handle": "alice"} |
| 29 | |
| 30 | |
| 31 | class TestRunReadApiPath: |
| 32 | def test_owner_qualified_id_uses_mists_path_not_owner_path( |
| 33 | self, capsys: pytest.CaptureFixture[str] |
| 34 | ) -> None: |
| 35 | """owner/ID form must call /api/mists/{id} — MR_03.""" |
| 36 | from muse.cli.commands.mist import run_read |
| 37 | |
| 38 | with ( |
| 39 | mock.patch( |
| 40 | "muse.cli.commands.mist._require_hub", |
| 41 | return_value=("https://musehub.ai", _fake_identity()), |
| 42 | ), |
| 43 | mock.patch( |
| 44 | "muse.cli.commands.mist._hub_api", return_value={"mist_id": "H7XFVEzeDfqk"} |
| 45 | ) as mocked, |
| 46 | ): |
| 47 | run_read(_args("gabriel/H7XFVEzeDfqk")) |
| 48 | |
| 49 | called_path = mocked.call_args.args[3] |
| 50 | assert called_path == "/api/mists/H7XFVEzeDfqk" |
| 51 | |
| 52 | def test_bare_id_uses_mists_path(self, capsys: pytest.CaptureFixture[str]) -> None: |
| 53 | """Bare ID form must call /api/mists/{id} — MR_04.""" |
| 54 | from muse.cli.commands.mist import run_read |
| 55 | |
| 56 | with ( |
| 57 | mock.patch( |
| 58 | "muse.cli.commands.mist._require_hub", |
| 59 | return_value=("https://musehub.ai", _fake_identity()), |
| 60 | ), |
| 61 | mock.patch( |
| 62 | "muse.cli.commands.mist._hub_api", return_value={"mist_id": "H7XFVEzeDfqk"} |
| 63 | ) as mocked, |
| 64 | ): |
| 65 | run_read(_args("H7XFVEzeDfqk")) |
| 66 | |
| 67 | called_path = mocked.call_args.args[3] |
| 68 | assert called_path == "/api/mists/H7XFVEzeDfqk" |
| 69 | |
| 70 | def test_owner_is_discarded_not_embedded_in_path( |
| 71 | self, capsys: pytest.CaptureFixture[str] |
| 72 | ) -> None: |
| 73 | """The owner segment is advisory only — never present in the API path — MR_05.""" |
| 74 | from muse.cli.commands.mist import run_read |
| 75 | |
| 76 | with ( |
| 77 | mock.patch( |
| 78 | "muse.cli.commands.mist._require_hub", |
| 79 | return_value=("https://musehub.ai", _fake_identity()), |
| 80 | ), |
| 81 | mock.patch( |
| 82 | "muse.cli.commands.mist._hub_api", return_value={"mist_id": "abc123"} |
| 83 | ) as mocked, |
| 84 | ): |
| 85 | run_read(_args("someOwner/abc123")) |
| 86 | |
| 87 | called_path = mocked.call_args.args[3] |
| 88 | assert "someOwner" not in called_path |
| 89 | assert called_path == "/api/mists/abc123" |
| 90 | |
| 91 | def test_bare_id_and_owner_qualified_id_produce_identical_output( |
| 92 | self, capsys: pytest.CaptureFixture[str] |
| 93 | ) -> None: |
| 94 | """Both input forms must be equivalent — acceptance criterion 1.""" |
| 95 | from muse.cli.commands.mist import run_read |
| 96 | |
| 97 | with ( |
| 98 | mock.patch( |
| 99 | "muse.cli.commands.mist._require_hub", |
| 100 | return_value=("https://musehub.ai", _fake_identity()), |
| 101 | ), |
| 102 | mock.patch( |
| 103 | "muse.cli.commands.mist._hub_api", return_value={"mist_id": "H7XFVEzeDfqk"} |
| 104 | ), |
| 105 | ): |
| 106 | run_read(_args("gabriel/H7XFVEzeDfqk")) |
| 107 | out_qualified = capsys.readouterr().out |
| 108 | |
| 109 | run_read(_args("H7XFVEzeDfqk")) |
| 110 | out_bare = capsys.readouterr().out |
| 111 | |
| 112 | assert out_qualified == out_bare |
| 113 | |
| 114 | |
| 115 | class TestHubApi404Message: |
| 116 | def test_404_surfaces_http_status_not_json_parse_error( |
| 117 | self, capsys: pytest.CaptureFixture[str] |
| 118 | ) -> None: |
| 119 | """A 404 with a valid JSON body must report the HTTP status — MR_02/MR_06.""" |
| 120 | from muse.cli.commands.mist import _hub_api |
| 121 | |
| 122 | err = TransportError('{"detail": "Not Found"}', 404) |
| 123 | |
| 124 | with ( |
| 125 | mock.patch("muse.cli.config.get_signing_identity", return_value=None), |
| 126 | mock.patch( |
| 127 | "muse.core.transport.HttpTransport.hub_json", side_effect=err |
| 128 | ), |
| 129 | ): |
| 130 | with pytest.raises(SystemExit): |
| 131 | _hub_api("https://musehub.ai", _fake_identity(), "GET", "/api/mists/missing") |
| 132 | |
| 133 | captured = capsys.readouterr() |
| 134 | assert "404" in captured.err |
| 135 | assert "invalid JSON" not in captured.err |
File History
1 commit
sha256:c287f599c5429903a139eadf3c5db5d930520e57cb0c3c575d9570e953c3b2d6
chore: bump version to 0.2.0.dev2 — nightly.2
Sonnet 4.6
patch
12 days ago