test_mist_cli.py
python
sha256:f1f585ee9ca4e1ada936668c1b14f42f961a1fa78a2c033b643595f9c1bf9ac7
fixes for proposal flow
Human
patch
1 day ago
| 1 | """Tests for the ``muse mist`` CLI command — Phase 2. |
| 2 | |
| 3 | Test tiers covered |
| 4 | ------------------ |
| 5 | Tier 1 — Shape / API surface |
| 6 | ``muse mist`` is registered; all 7 subcommands present in --help; |
| 7 | all run_* functions importable; docstrings present. |
| 8 | |
| 9 | Tier 2 — Round-trip (local, no MuseHub) |
| 10 | ``muse mist create`` reads a file, computes mist_id, returns JSON; |
| 11 | round-trip with multiple artifact types (Python, MIDI magic bytes, JSON). |
| 12 | |
| 13 | Tier 3 — Edge cases |
| 14 | Empty file; file at exactly 10 MiB limit; unknown extension. |
| 15 | create with all optional flags set. |
| 16 | |
| 17 | Tier 5 — Data integrity |
| 18 | mist_id in create output matches compute_mist_id of file content; |
| 19 | artifact_type matches detect_artifact_type; |
| 20 | symbol_anchors non-empty for Python, empty for binary; |
| 21 | validate_tag rejects invalid tags. |
| 22 | |
| 23 | Tier 6 — Performance |
| 24 | create on a 1 MiB file under 200 ms. |
| 25 | |
| 26 | Tier 7 — Security |
| 27 | create rejects filenames with path traversal, null bytes, ANSI escapes; |
| 28 | create rejects content > 10 MiB; |
| 29 | create rejects invalid visibility; |
| 30 | create rejects > 10 tags; |
| 31 | _validate_tag rejects XSS, null bytes, HTML specials. |
| 32 | |
| 33 | Tier 8 — Docstrings |
| 34 | All public symbols in mist.py carry non-empty docstrings. |
| 35 | """ |
| 36 | |
| 37 | from __future__ import annotations |
| 38 | |
| 39 | import json |
| 40 | import os |
| 41 | import pathlib |
| 42 | import sys |
| 43 | import time |
| 44 | import types |
| 45 | |
| 46 | import pytest |
| 47 | |
| 48 | |
| 49 | # --------------------------------------------------------------------------- |
| 50 | # Helpers |
| 51 | # --------------------------------------------------------------------------- |
| 52 | |
| 53 | |
| 54 | def invoke_mist(args: list[str]) -> tuple[int, str, str]: |
| 55 | """Run ``muse mist <args>`` in-process and capture stdout/stderr. |
| 56 | |
| 57 | Returns: |
| 58 | A tuple of (exit_code, stdout, stderr). |
| 59 | """ |
| 60 | from io import StringIO |
| 61 | |
| 62 | old_stdout, old_stderr = sys.stdout, sys.stderr |
| 63 | sys.stdout = out = StringIO() |
| 64 | sys.stderr = err = StringIO() |
| 65 | exit_code = 0 |
| 66 | try: |
| 67 | from muse.cli.app import main |
| 68 | |
| 69 | main(["mist"] + args) |
| 70 | except SystemExit as exc: |
| 71 | exit_code = int(exc.code) if exc.code is not None else 0 |
| 72 | finally: |
| 73 | sys.stdout = old_stdout |
| 74 | sys.stderr = old_stderr |
| 75 | return exit_code, out.getvalue(), err.getvalue() |
| 76 | |
| 77 | |
| 78 | @pytest.fixture() |
| 79 | def python_file(tmp_path: pathlib.Path) -> pathlib.Path: |
| 80 | """A valid Python source file for mist create tests.""" |
| 81 | f = tmp_path / "compute.py" |
| 82 | f.write_bytes( |
| 83 | b"def compute(x: int) -> int:\n" |
| 84 | b' """Double the input."""\n' |
| 85 | b" return x * 2\n" |
| 86 | ) |
| 87 | return f |
| 88 | |
| 89 | |
| 90 | @pytest.fixture() |
| 91 | def midi_file(tmp_path: pathlib.Path) -> pathlib.Path: |
| 92 | """A minimal MIDI file (MThd magic bytes) for mist create tests.""" |
| 93 | f = tmp_path / "motif.mid" |
| 94 | # Minimal MIDI header: MThd + header length (6) + format (1) + tracks (1) + division |
| 95 | f.write_bytes(b"MThd\x00\x00\x00\x06\x00\x01\x00\x01\x01\xe0") |
| 96 | return f |
| 97 | |
| 98 | |
| 99 | @pytest.fixture() |
| 100 | def empty_file(tmp_path: pathlib.Path) -> pathlib.Path: |
| 101 | """An empty file.""" |
| 102 | f = tmp_path / "empty.txt" |
| 103 | f.write_bytes(b"") |
| 104 | return f |
| 105 | |
| 106 | |
| 107 | @pytest.fixture() |
| 108 | def large_file(tmp_path: pathlib.Path) -> pathlib.Path: |
| 109 | """A 1 MiB file for performance tests.""" |
| 110 | f = tmp_path / "large.py" |
| 111 | f.write_bytes(b"# comment\n" * 104858) # ~1 MiB |
| 112 | return f |
| 113 | |
| 114 | |
| 115 | # --------------------------------------------------------------------------- |
| 116 | # Tier 1 — Shape / API surface |
| 117 | # --------------------------------------------------------------------------- |
| 118 | |
| 119 | |
| 120 | class TestMistCliShape: |
| 121 | """Verify muse mist is registered and all subcommands are present.""" |
| 122 | |
| 123 | SUBCOMMANDS = ("create", "list", "read", "fork", "push", "embed", "delete") |
| 124 | RUN_FUNCS = ("run_create", "run_list", "run_read", "run_fork", "run_push", "run_embed", "run_delete") |
| 125 | |
| 126 | def test_mist_help_exits_0(self) -> None: |
| 127 | code, out, _ = invoke_mist(["--help"]) |
| 128 | assert code == 0 |
| 129 | assert "mist" in out.lower() |
| 130 | |
| 131 | def test_all_subcommands_in_help(self) -> None: |
| 132 | code, out, _ = invoke_mist(["--help"]) |
| 133 | assert code == 0 |
| 134 | for sub in self.SUBCOMMANDS: |
| 135 | assert sub in out, f"Subcommand {sub!r} missing from muse mist --help" |
| 136 | |
| 137 | @pytest.mark.parametrize("sub", SUBCOMMANDS) |
| 138 | def test_subcommand_help_exits_0(self, sub: str) -> None: |
| 139 | code, out, _ = invoke_mist([sub, "--help"]) |
| 140 | assert code == 0 |
| 141 | assert sub in out.lower() |
| 142 | |
| 143 | @pytest.mark.parametrize("func_name", RUN_FUNCS) |
| 144 | def test_run_functions_importable(self, func_name: str) -> None: |
| 145 | import muse.cli.commands.mist as mod |
| 146 | |
| 147 | assert hasattr(mod, func_name), f"{func_name} missing from muse.cli.commands.mist" |
| 148 | assert callable(getattr(mod, func_name)) |
| 149 | |
| 150 | def test_register_function_importable(self) -> None: |
| 151 | from muse.cli.commands.mist import register |
| 152 | |
| 153 | assert callable(register) |
| 154 | |
| 155 | def test_validate_tag_importable(self) -> None: |
| 156 | from muse.cli.commands.mist import _validate_tag |
| 157 | |
| 158 | assert callable(_validate_tag) |
| 159 | |
| 160 | def test_mist_registered_in_app(self) -> None: |
| 161 | """muse mist appears in the top-level command list.""" |
| 162 | code, out, _ = invoke_mist(["--help"]) |
| 163 | # Just verify we can invoke the mist namespace (exit 0 from --help) |
| 164 | assert code == 0 |
| 165 | |
| 166 | def test_create_json_flag_present(self) -> None: |
| 167 | code, out, _ = invoke_mist(["create", "--help"]) |
| 168 | assert "--json" in out |
| 169 | |
| 170 | def test_create_push_flag_present(self) -> None: |
| 171 | code, out, _ = invoke_mist(["create", "--help"]) |
| 172 | assert "--push" in out |
| 173 | |
| 174 | def test_create_sign_flag_present(self) -> None: |
| 175 | code, out, _ = invoke_mist(["create", "--help"]) |
| 176 | assert "--sign" in out |
| 177 | |
| 178 | def test_create_visibility_flag_present(self) -> None: |
| 179 | code, out, _ = invoke_mist(["create", "--help"]) |
| 180 | assert "--visibility" in out |
| 181 | |
| 182 | |
| 183 | # --------------------------------------------------------------------------- |
| 184 | # Tier 2 — Round-trip (local, no MuseHub) |
| 185 | # --------------------------------------------------------------------------- |
| 186 | |
| 187 | |
| 188 | class TestMistCreateRoundTrip: |
| 189 | """End-to-end round-trip for muse mist create (no MuseHub required).""" |
| 190 | |
| 191 | def test_create_python_file_exits_0(self, python_file: pathlib.Path) -> None: |
| 192 | code, out, err = invoke_mist(["create", str(python_file), "--json"]) |
| 193 | assert code == 0, f"Unexpected exit {code}: stderr={err}" |
| 194 | |
| 195 | def test_create_python_file_returns_json(self, python_file: pathlib.Path) -> None: |
| 196 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 197 | data = json.loads(out) |
| 198 | assert "mist_id" in data |
| 199 | assert "artifact_type" in data |
| 200 | assert "filename" in data |
| 201 | assert "size_bytes" in data |
| 202 | |
| 203 | def test_create_python_mist_id_is_12_chars(self, python_file: pathlib.Path) -> None: |
| 204 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 205 | data = json.loads(out) |
| 206 | assert len(data["mist_id"]) == 12 |
| 207 | |
| 208 | def test_create_python_artifact_type_is_code(self, python_file: pathlib.Path) -> None: |
| 209 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 210 | data = json.loads(out) |
| 211 | assert data["artifact_type"] == "code" |
| 212 | assert data["language"] == "python" |
| 213 | |
| 214 | def test_create_python_symbol_anchors_non_empty(self, python_file: pathlib.Path) -> None: |
| 215 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 216 | data = json.loads(out) |
| 217 | assert isinstance(data["symbol_anchors"], list) |
| 218 | assert len(data["symbol_anchors"]) > 0 |
| 219 | |
| 220 | def test_create_midi_file_exits_0(self, midi_file: pathlib.Path) -> None: |
| 221 | code, out, err = invoke_mist(["create", str(midi_file), "--json"]) |
| 222 | assert code == 0, f"Unexpected exit {code}: stderr={err}" |
| 223 | |
| 224 | def test_create_midi_artifact_type_is_midi(self, midi_file: pathlib.Path) -> None: |
| 225 | _, out, _ = invoke_mist(["create", str(midi_file), "--json"]) |
| 226 | data = json.loads(out) |
| 227 | assert data["artifact_type"] == "midi" |
| 228 | |
| 229 | def test_create_midi_symbol_anchors_empty(self, midi_file: pathlib.Path) -> None: |
| 230 | _, out, _ = invoke_mist(["create", str(midi_file), "--json"]) |
| 231 | data = json.loads(out) |
| 232 | assert data["symbol_anchors"] == [] |
| 233 | |
| 234 | def test_create_size_bytes_correct(self, python_file: pathlib.Path) -> None: |
| 235 | expected = python_file.read_bytes() |
| 236 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 237 | data = json.loads(out) |
| 238 | assert data["size_bytes"] == len(expected) |
| 239 | |
| 240 | def test_create_filename_correct(self, python_file: pathlib.Path) -> None: |
| 241 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 242 | data = json.loads(out) |
| 243 | assert data["filename"] == python_file.name |
| 244 | |
| 245 | def test_create_no_url_without_push(self, python_file: pathlib.Path) -> None: |
| 246 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 247 | data = json.loads(out) |
| 248 | assert data["url"] == "" |
| 249 | |
| 250 | def test_create_human_readable_output(self, python_file: pathlib.Path) -> None: |
| 251 | code, out, _ = invoke_mist(["create", str(python_file)]) |
| 252 | assert code == 0 |
| 253 | assert "✅" in out |
| 254 | assert "Mist created" in out |
| 255 | |
| 256 | def test_create_with_title_flag(self, python_file: pathlib.Path) -> None: |
| 257 | """--title flag is accepted and does not crash.""" |
| 258 | code, _, _ = invoke_mist(["create", str(python_file), "--title", "My test mist"]) |
| 259 | assert code == 0 |
| 260 | |
| 261 | def test_create_with_description_flag(self, python_file: pathlib.Path) -> None: |
| 262 | code, _, _ = invoke_mist(["create", str(python_file), "--description", "A test."]) |
| 263 | assert code == 0 |
| 264 | |
| 265 | def test_create_with_tag_flag(self, python_file: pathlib.Path) -> None: |
| 266 | code, _, _ = invoke_mist(["create", str(python_file), "--tag", "security", "--tag", "utils"]) |
| 267 | assert code == 0 |
| 268 | |
| 269 | def test_create_with_agent_flags(self, python_file: pathlib.Path) -> None: |
| 270 | code, out, _ = invoke_mist([ |
| 271 | "create", str(python_file), |
| 272 | "--agent-id", "cccode-v3", |
| 273 | "--model-id", "claude-sonnet-4-6", |
| 274 | "--json", |
| 275 | ]) |
| 276 | assert code == 0 |
| 277 | data = json.loads(out) |
| 278 | assert data["agent_id"] == "cccode-v3" |
| 279 | assert data["model_id"] == "claude-sonnet-4-6" |
| 280 | |
| 281 | def test_create_with_secret_visibility(self, python_file: pathlib.Path) -> None: |
| 282 | code, _, _ = invoke_mist(["create", str(python_file), "--visibility", "secret"]) |
| 283 | assert code == 0 |
| 284 | |
| 285 | |
| 286 | # --------------------------------------------------------------------------- |
| 287 | # Tier 3 — Edge cases |
| 288 | # --------------------------------------------------------------------------- |
| 289 | |
| 290 | |
| 291 | class TestMistCreateEdgeCases: |
| 292 | """Boundary and unusual-input tests for muse mist create.""" |
| 293 | |
| 294 | def test_create_empty_file_exits_0(self, empty_file: pathlib.Path) -> None: |
| 295 | code, out, err = invoke_mist(["create", str(empty_file), "--json"]) |
| 296 | assert code == 0, f"stderr: {err}" |
| 297 | data = json.loads(out) |
| 298 | assert data["size_bytes"] == 0 |
| 299 | assert len(data["mist_id"]) == 12 |
| 300 | |
| 301 | def test_create_empty_file_symbol_anchors_empty(self, empty_file: pathlib.Path) -> None: |
| 302 | _, out, _ = invoke_mist(["create", str(empty_file), "--json"]) |
| 303 | data = json.loads(out) |
| 304 | assert data["symbol_anchors"] == [] |
| 305 | |
| 306 | def test_create_unknown_extension(self, tmp_path: pathlib.Path) -> None: |
| 307 | f = tmp_path / "data.xyzzy" |
| 308 | f.write_bytes(b"\xde\xad\xbe\xef" * 10) |
| 309 | code, out, _ = invoke_mist(["create", str(f), "--json"]) |
| 310 | assert code == 0 |
| 311 | data = json.loads(out) |
| 312 | assert data["artifact_type"] == "unknown" |
| 313 | |
| 314 | def test_create_json_file_abi(self, tmp_path: pathlib.Path) -> None: |
| 315 | f = tmp_path / "contract.json" |
| 316 | f.write_bytes(json.dumps([{"type": "function", "name": "transfer"}]).encode()) |
| 317 | _, out, _ = invoke_mist(["create", str(f), "--json"]) |
| 318 | data = json.loads(out) |
| 319 | assert data["artifact_type"] == "abi" |
| 320 | |
| 321 | def test_create_json_file_schema(self, tmp_path: pathlib.Path) -> None: |
| 322 | f = tmp_path / "schema.json" |
| 323 | f.write_bytes(json.dumps({"$schema": "http://json-schema.org/draft-07/schema#"}).encode()) |
| 324 | _, out, _ = invoke_mist(["create", str(f), "--json"]) |
| 325 | data = json.loads(out) |
| 326 | assert data["artifact_type"] == "json_schema" |
| 327 | |
| 328 | def test_create_markdown_file(self, tmp_path: pathlib.Path) -> None: |
| 329 | f = tmp_path / "README.md" |
| 330 | f.write_bytes(b"# Hello\n\nWorld\n") |
| 331 | _, out, _ = invoke_mist(["create", str(f), "--json"]) |
| 332 | data = json.loads(out) |
| 333 | assert data["artifact_type"] == "code" |
| 334 | assert data["language"] == "markdown" |
| 335 | |
| 336 | def test_create_solidity_file(self, tmp_path: pathlib.Path) -> None: |
| 337 | f = tmp_path / "Token.sol" |
| 338 | f.write_bytes(b"// SPDX-License-Identifier: MIT\ncontract Token {}\n") |
| 339 | _, out, _ = invoke_mist(["create", str(f), "--json"]) |
| 340 | data = json.loads(out) |
| 341 | assert data["artifact_type"] == "code" |
| 342 | assert data["language"] == "solidity" |
| 343 | |
| 344 | def test_create_ten_tags_accepted(self, python_file: pathlib.Path) -> None: |
| 345 | tags = [f"--tag tag{i}" for i in range(10)] |
| 346 | flat: list[str] = ["create", str(python_file)] |
| 347 | for i in range(10): |
| 348 | flat += ["--tag", f"tag{i}"] |
| 349 | code, _, _ = invoke_mist(flat) |
| 350 | assert code == 0 |
| 351 | |
| 352 | def test_create_different_content_different_mist_id(self, tmp_path: pathlib.Path) -> None: |
| 353 | f1 = tmp_path / "a.py" |
| 354 | f1.write_bytes(b"x = 1") |
| 355 | f2 = tmp_path / "b.py" |
| 356 | f2.write_bytes(b"x = 2") |
| 357 | _, out1, _ = invoke_mist(["create", str(f1), "--json"]) |
| 358 | _, out2, _ = invoke_mist(["create", str(f2), "--json"]) |
| 359 | id1 = json.loads(out1)["mist_id"] |
| 360 | id2 = json.loads(out2)["mist_id"] |
| 361 | assert id1 != id2 |
| 362 | |
| 363 | def test_create_same_content_same_mist_id(self, tmp_path: pathlib.Path) -> None: |
| 364 | """Content-addressed: same bytes always yield the same mist_id.""" |
| 365 | content = b"def stable(): return True\n" |
| 366 | f1 = tmp_path / "f1.py" |
| 367 | f1.write_bytes(content) |
| 368 | f2 = tmp_path / "f2.py" |
| 369 | f2.write_bytes(content) |
| 370 | _, out1, _ = invoke_mist(["create", str(f1), "--json"]) |
| 371 | _, out2, _ = invoke_mist(["create", str(f2), "--json"]) |
| 372 | assert json.loads(out1)["mist_id"] == json.loads(out2)["mist_id"] |
| 373 | |
| 374 | |
| 375 | # --------------------------------------------------------------------------- |
| 376 | # Tier 5 — Data integrity |
| 377 | # --------------------------------------------------------------------------- |
| 378 | |
| 379 | |
| 380 | class TestMistCliDataIntegrity: |
| 381 | """Verify correctness of computed fields in CLI output.""" |
| 382 | |
| 383 | def test_mist_id_matches_compute_mist_id(self, python_file: pathlib.Path) -> None: |
| 384 | from muse.plugins.mist.plugin import compute_mist_id |
| 385 | |
| 386 | content = python_file.read_bytes() |
| 387 | expected_id = compute_mist_id(content) |
| 388 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 389 | data = json.loads(out) |
| 390 | assert data["mist_id"] == expected_id |
| 391 | |
| 392 | def test_artifact_type_matches_detect(self, python_file: pathlib.Path) -> None: |
| 393 | from muse.plugins.mist.plugin import detect_artifact_type |
| 394 | |
| 395 | content = python_file.read_bytes() |
| 396 | expected = detect_artifact_type(python_file.name, content) |
| 397 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 398 | data = json.loads(out) |
| 399 | assert data["artifact_type"] == expected["artifact_type"] |
| 400 | assert data["language"] == expected["language"] |
| 401 | |
| 402 | def test_size_bytes_matches_actual(self, python_file: pathlib.Path) -> None: |
| 403 | content = python_file.read_bytes() |
| 404 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 405 | data = json.loads(out) |
| 406 | assert data["size_bytes"] == len(content) |
| 407 | |
| 408 | def test_signed_false_without_sign_flag(self, python_file: pathlib.Path) -> None: |
| 409 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 410 | data = json.loads(out) |
| 411 | assert data["signed"] is False |
| 412 | |
| 413 | def test_symbol_anchors_list_type(self, python_file: pathlib.Path) -> None: |
| 414 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 415 | data = json.loads(out) |
| 416 | assert isinstance(data["symbol_anchors"], list) |
| 417 | |
| 418 | def test_symbol_anchors_contain_function_name(self, python_file: pathlib.Path) -> None: |
| 419 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 420 | data = json.loads(out) |
| 421 | anchors = data["symbol_anchors"] |
| 422 | assert any("compute" in a for a in anchors), f"No 'compute' in {anchors}" |
| 423 | |
| 424 | def test_mist_id_base58_only(self, python_file: pathlib.Path) -> None: |
| 425 | from muse.plugins.mist.plugin import _BASE58_ALPHABET |
| 426 | |
| 427 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 428 | mist_id = json.loads(out)["mist_id"] |
| 429 | for ch in mist_id: |
| 430 | assert ch in _BASE58_ALPHABET, f"Non-base58 char {ch!r} in mist_id" |
| 431 | |
| 432 | def test_validate_tag_rejects_xss(self) -> None: |
| 433 | from muse.cli.commands.mist import _validate_tag |
| 434 | |
| 435 | with pytest.raises(ValueError, match="HTML"): |
| 436 | _validate_tag("<script>alert(1)</script>") |
| 437 | |
| 438 | def test_validate_tag_rejects_null_byte(self) -> None: |
| 439 | from muse.cli.commands.mist import _validate_tag |
| 440 | |
| 441 | with pytest.raises(ValueError, match="null byte"): |
| 442 | _validate_tag("tag\x00") |
| 443 | |
| 444 | def test_validate_tag_rejects_too_long(self) -> None: |
| 445 | from muse.cli.commands.mist import _validate_tag |
| 446 | |
| 447 | with pytest.raises(ValueError, match="limit"): |
| 448 | _validate_tag("t" * 65) |
| 449 | |
| 450 | def test_validate_tag_rejects_control_char(self) -> None: |
| 451 | from muse.cli.commands.mist import _validate_tag |
| 452 | |
| 453 | with pytest.raises(ValueError, match="control char"): |
| 454 | _validate_tag("tag\x01") |
| 455 | |
| 456 | def test_validate_tag_accepts_normal_tag(self) -> None: |
| 457 | from muse.cli.commands.mist import _validate_tag |
| 458 | |
| 459 | _validate_tag("security") # must not raise |
| 460 | _validate_tag("erc-8004") |
| 461 | _validate_tag("midi-motif") |
| 462 | |
| 463 | |
| 464 | # --------------------------------------------------------------------------- |
| 465 | # Tier 6 — Performance |
| 466 | # --------------------------------------------------------------------------- |
| 467 | |
| 468 | |
| 469 | class TestMistCliPerformance: |
| 470 | """Timing constraints for the CLI's hot paths.""" |
| 471 | |
| 472 | def test_create_1mb_file_under_200ms(self, large_file: pathlib.Path) -> None: |
| 473 | start = time.perf_counter() |
| 474 | code, out, err = invoke_mist(["create", str(large_file), "--json"]) |
| 475 | elapsed = time.perf_counter() - start |
| 476 | assert code == 0, f"stderr: {err}" |
| 477 | assert elapsed < 0.200, f"create took {elapsed:.3f}s on 1 MiB file" |
| 478 | |
| 479 | def test_create_deterministic_ids_10_calls_under_500ms(self, python_file: pathlib.Path) -> None: |
| 480 | start = time.perf_counter() |
| 481 | ids = set() |
| 482 | for _ in range(10): |
| 483 | _, out, _ = invoke_mist(["create", str(python_file), "--json"]) |
| 484 | ids.add(json.loads(out)["mist_id"]) |
| 485 | elapsed = time.perf_counter() - start |
| 486 | assert len(ids) == 1, "Same file should always yield same mist_id" |
| 487 | assert elapsed < 0.500, f"10 create calls took {elapsed:.3f}s" |
| 488 | |
| 489 | |
| 490 | # --------------------------------------------------------------------------- |
| 491 | # Tier 7 — Security |
| 492 | # --------------------------------------------------------------------------- |
| 493 | |
| 494 | |
| 495 | class TestMistCliSecurity: |
| 496 | """Input sanitisation and rejection of malicious inputs.""" |
| 497 | |
| 498 | @pytest.mark.parametrize("filename,expected_exit", [ |
| 499 | ("../../../etc/passwd", 1), |
| 500 | ("file\x00hidden.py", 1), |
| 501 | ("..", 1), |
| 502 | ("a" * 256, 1), |
| 503 | ]) |
| 504 | def test_invalid_filename_rejected( |
| 505 | self, |
| 506 | tmp_path: pathlib.Path, |
| 507 | filename: str, |
| 508 | expected_exit: int, |
| 509 | ) -> None: |
| 510 | """validate_mist_filename rejects attack vectors before creating.""" |
| 511 | # We can't easily test invalid filenames via CLI since the filesystem |
| 512 | # won't let us create files with these names. Test the validator directly. |
| 513 | from muse.plugins.mist.plugin import validate_mist_filename |
| 514 | |
| 515 | with pytest.raises(ValueError): |
| 516 | validate_mist_filename(filename) |
| 517 | |
| 518 | def test_content_over_10mb_rejected(self, tmp_path: pathlib.Path) -> None: |
| 519 | f = tmp_path / "huge.py" |
| 520 | f.write_bytes(b"x" * (10 * 1024 * 1024 + 1)) |
| 521 | code, _, err = invoke_mist(["create", str(f), "--json"]) |
| 522 | assert code != 0 |
| 523 | assert "10 MiB" in err or "limit" in err.lower() |
| 524 | |
| 525 | def test_invalid_visibility_rejected(self, python_file: pathlib.Path) -> None: |
| 526 | code, _, err = invoke_mist(["create", str(python_file), "--visibility", "everyone"]) |
| 527 | assert code != 0 |
| 528 | assert "visibility" in err.lower() or "invalid" in err.lower() |
| 529 | |
| 530 | def test_too_many_tags_rejected(self, python_file: pathlib.Path) -> None: |
| 531 | flat: list[str] = ["create", str(python_file)] |
| 532 | for i in range(11): |
| 533 | flat += ["--tag", f"tag{i}"] |
| 534 | code, _, err = invoke_mist(flat) |
| 535 | assert code != 0 |
| 536 | assert "tag" in err.lower() |
| 537 | |
| 538 | def test_file_not_found_exits_nonzero(self) -> None: |
| 539 | code, _, err = invoke_mist(["create", "/nonexistent/path/file.py", "--json"]) |
| 540 | assert code != 0 |
| 541 | assert "not found" in err.lower() or "no such" in err.lower() |
| 542 | |
| 543 | def test_mist_id_not_sequential_for_sequential_content(self) -> None: |
| 544 | from muse.plugins.mist.plugin import compute_mist_id |
| 545 | |
| 546 | ids = [compute_mist_id(bytes([i])) for i in range(10)] |
| 547 | # IDs should not be lexicographically sequential |
| 548 | assert ids != sorted(ids), "Mist IDs should not be predictably ordered" |
| 549 | |
| 550 | def test_xss_tag_rejected_via_validate_tag(self) -> None: |
| 551 | from muse.cli.commands.mist import _validate_tag |
| 552 | |
| 553 | for payload in ("<img onerror=alert(1)>", "<script>", '"inject"', "' OR 1=1"): |
| 554 | if any(c in payload for c in "<>\"'&"): |
| 555 | with pytest.raises(ValueError): |
| 556 | _validate_tag(payload) |
| 557 | |
| 558 | def test_mist_id_collision_resistance_1000_payloads(self) -> None: |
| 559 | from muse.plugins.mist.plugin import compute_mist_id |
| 560 | |
| 561 | ids = {compute_mist_id(f"payload-{i:04d}".encode()) for i in range(1000)} |
| 562 | assert len(ids) == 1000, "Expected no collisions across 1000 distinct payloads" |
| 563 | |
| 564 | |
| 565 | # --------------------------------------------------------------------------- |
| 566 | # Tier 8 — Docstrings |
| 567 | # --------------------------------------------------------------------------- |
| 568 | |
| 569 | |
| 570 | class TestMistCliDocstrings: |
| 571 | """Every public symbol in mist.py must carry a non-empty docstring.""" |
| 572 | |
| 573 | PUBLIC_FUNCS = ( |
| 574 | "run_create", |
| 575 | "run_list", |
| 576 | "run_read", |
| 577 | "run_fork", |
| 578 | "run_push", |
| 579 | "run_embed", |
| 580 | "run_delete", |
| 581 | "register", |
| 582 | "_validate_tag", |
| 583 | "_require_hub", |
| 584 | "_hub_api", |
| 585 | "_get_hub_url", |
| 586 | ) |
| 587 | |
| 588 | def test_module_docstring(self) -> None: |
| 589 | import muse.cli.commands.mist as mod |
| 590 | |
| 591 | assert mod.__doc__ and len(mod.__doc__.strip()) > 20 |
| 592 | |
| 593 | @pytest.mark.parametrize("func_name", PUBLIC_FUNCS) |
| 594 | def test_function_has_docstring(self, func_name: str) -> None: |
| 595 | import muse.cli.commands.mist as mod |
| 596 | |
| 597 | fn = getattr(mod, func_name, None) |
| 598 | assert fn is not None, f"{func_name} not found in module" |
| 599 | assert fn.__doc__ and len(fn.__doc__.strip()) > 0, ( |
| 600 | f"{func_name} is missing a docstring" |
| 601 | ) |
| 602 | |
| 603 | def test_register_docstring_lists_all_subcommands(self) -> None: |
| 604 | from muse.cli.commands.mist import register |
| 605 | |
| 606 | doc = register.__doc__ or "" |
| 607 | for sub in ("create", "list", "read", "fork", "push", "embed", "delete"): |
| 608 | assert sub in doc, f"register() docstring missing subcommand: {sub}" |
| 609 | |
| 610 | |
| 611 | # --------------------------------------------------------------------------- |
| 612 | # PEM-load site migration — TDD tests (M1–M3) |
| 613 | # |
| 614 | # These tests confirm that the three PEM-load sites in mist.py have been |
| 615 | # migrated away from the deleted `load_signing_identity` function to the |
| 616 | # current `get_signing_identity` + `build_msign_header` path. |
| 617 | # |
| 618 | # Before the fix: ImportError — "cannot import name 'load_signing_identity' |
| 619 | # from 'muse.core.transport'" crashes all three call sites. |
| 620 | # After the fix: no ImportError; signing identity resolved via keychain. |
| 621 | # --------------------------------------------------------------------------- |
| 622 | |
| 623 | |
| 624 | class TestMistPemLoadSites: |
| 625 | """M-series: PEM-load site migration tests for mist.py.""" |
| 626 | |
| 627 | # M1 — _hub_api signing path |
| 628 | def test_M1_hub_api_no_import_error_when_signing_identity_present( |
| 629 | self, |
| 630 | tmp_path: pathlib.Path, |
| 631 | monkeypatch: pytest.MonkeyPatch, |
| 632 | ) -> None: |
| 633 | """_hub_api must not crash with ImportError from the deleted load_signing_identity. |
| 634 | |
| 635 | The fix: replace the key_path/load_signing_identity block with |
| 636 | get_signing_identity(remote_url=server_root) + build_msign_header. |
| 637 | """ |
| 638 | import io |
| 639 | import json as _json |
| 640 | import unittest.mock as mock |
| 641 | from muse.core.transport import SigningIdentity |
| 642 | from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey |
| 643 | |
| 644 | fake_key = Ed25519PrivateKey.generate() |
| 645 | fake_signing = SigningIdentity(handle="alice", private_key=fake_key) |
| 646 | |
| 647 | # Fake JSON response via _urllib_do (transport now uses urllib) |
| 648 | fake_response_data = _json.dumps({"ok": True}).encode() |
| 649 | |
| 650 | from muse.core.identity import IdentityEntry |
| 651 | identity: IdentityEntry = {"type": "human", "handle": "alice"} |
| 652 | |
| 653 | with ( |
| 654 | mock.patch("muse.cli.config.get_signing_identity", return_value=fake_signing), |
| 655 | mock.patch("muse.core.transport._urllib_do", return_value=fake_response_data), |
| 656 | mock.patch("muse.core.hub_trust.check_and_pin"), |
| 657 | ): |
| 658 | from muse.cli.commands.mist import _hub_api |
| 659 | result = _hub_api( |
| 660 | "https://musehub.ai", |
| 661 | identity, |
| 662 | "GET", |
| 663 | "/api/test", |
| 664 | ) |
| 665 | assert result == {"ok": True} |
| 666 | |
| 667 | # M2 — run_create --sign path |
| 668 | def test_M2_run_create_sign_no_import_error( |
| 669 | self, |
| 670 | tmp_path: pathlib.Path, |
| 671 | monkeypatch: pytest.MonkeyPatch, |
| 672 | ) -> None: |
| 673 | """run_create with --sign must not crash with ImportError or TypeError. |
| 674 | |
| 675 | Bugs before fix: |
| 676 | - load_identity() called without hub_url (TypeError) |
| 677 | - load_signing_identity imported from transport (ImportError — deleted) |
| 678 | |
| 679 | The fix: replace the key_path/load_signing_identity block with |
| 680 | get_signing_identity() + sign_bytes(signing.private_key, content). |
| 681 | """ |
| 682 | import unittest.mock as mock |
| 683 | from muse.core.transport import SigningIdentity |
| 684 | from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey |
| 685 | |
| 686 | # Create a real file to sign |
| 687 | src = tmp_path / "song.py" |
| 688 | src.write_bytes(b"def play(): pass\n") |
| 689 | |
| 690 | fake_key = Ed25519PrivateKey.generate() |
| 691 | fake_signing = SigningIdentity(handle="alice", private_key=fake_key) |
| 692 | |
| 693 | with ( |
| 694 | mock.patch("muse.cli.commands.mist.get_signing_identity", return_value=fake_signing, create=True), |
| 695 | ): |
| 696 | code, out, err = invoke_mist(["create", str(src), "--sign", "--json"]) |
| 697 | |
| 698 | # Must not crash; signed field may be True or False but no internal exception. |
| 699 | assert code == 0, f"Expected exit 0, got {code}:\n{err}" |
| 700 | data = json.loads(out) |
| 701 | assert "signed" in data |
| 702 | |
| 703 | # M3 — run_raw signing path |
| 704 | def test_M3_run_raw_no_import_error_when_identity_present( |
| 705 | self, |
| 706 | tmp_path: pathlib.Path, |
| 707 | monkeypatch: pytest.MonkeyPatch, |
| 708 | ) -> None: |
| 709 | """run_raw must not crash with ImportError from the deleted load_signing_identity. |
| 710 | |
| 711 | Bugs before fix: |
| 712 | - load_identity() called without hub_url inside _require_hub (TypeError) |
| 713 | - load_signing_identity imported from transport (ImportError — deleted) |
| 714 | |
| 715 | The fix: replace the key_path/load_signing_identity block with |
| 716 | get_signing_identity(remote_url=server_root) + build_msign_header. |
| 717 | """ |
| 718 | import argparse |
| 719 | import unittest.mock as mock |
| 720 | from muse.core.transport import SigningIdentity |
| 721 | from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey |
| 722 | |
| 723 | fake_key = Ed25519PrivateKey.generate() |
| 724 | fake_signing = SigningIdentity(handle="alice", private_key=fake_key) |
| 725 | fake_identity = {"type": "human", "handle": "alice", "hd_path": "m/0'"} |
| 726 | |
| 727 | # Fake raw bytes response (e.g. MIDI content) |
| 728 | fake_bytes = b"MThd\x00\x00\x00\x06" |
| 729 | fake_http_resp = mock.MagicMock() |
| 730 | fake_http_resp.__enter__ = lambda s: s |
| 731 | fake_http_resp.__exit__ = mock.MagicMock(return_value=False) |
| 732 | fake_http_resp.read.return_value = fake_bytes |
| 733 | fake_http_resp.headers = {"Content-Type": "audio/midi"} |
| 734 | |
| 735 | with ( |
| 736 | mock.patch("muse.cli.commands.mist.get_signing_identity", return_value=fake_signing, create=True), |
| 737 | mock.patch("muse.cli.commands.mist._require_hub", return_value=("https://musehub.ai", fake_identity)), |
| 738 | mock.patch("urllib.request.urlopen", return_value=fake_http_resp), |
| 739 | ): |
| 740 | from muse.cli.commands.mist import run_raw |
| 741 | args = argparse.Namespace(mist_id="alice/abc123", output=None, hub="https://musehub.ai") |
| 742 | try: |
| 743 | run_raw(args) |
| 744 | except SystemExit: |
| 745 | pass |
| 746 | except ImportError as e: |
| 747 | raise AssertionError(f"run_raw raised ImportError: {e}") from e |
File History
3 commits
sha256:f1f585ee9ca4e1ada936668c1b14f42f961a1fa78a2c033b643595f9c1bf9ac7
fixes for proposal flow
Human
patch
1 day ago
sha256:79ffe87f5fe2ec146e35f05521218bbf54dffdb0440c07f970bad05f16efb89f
chore: merge main — carry all urllib/typing/test fixes from dev
Sonnet 4.6
minor
⚠
8 days ago
sha256:0bea7600d1eee83e87950be49933b1006fa9dc2c71e7c4ee748d324f61138156
chore: bump version to 0.2.0rc11; fix typing audit violatio…
Sonnet 4.6
minor
⚠
8 days ago