"""TDD supercharge tests for ``muse code predict``. Gaps being closed ----------------- - ``-j`` alias for ``--json`` - ``exit_code`` and ``duration_ms`` in JSON envelope - ``--explain --json`` structured output for agents (new ``_ExplainJson``) - Zero existing test coverage — unit + integration + security added Unit tests ---------- - ``_sanitise`` — truncation, control chars, ANSI stripping, unicode - ``_module_key`` — depth variants, single-component path, nested path - ``_pct_bar`` — 0.0, 0.5, 1.0, clamping outside [0, 1] - ``_confidence_label`` — boundary values at 0.70 and 0.45 - ``_iter_symbol_ops`` — None, empty ops, patch children, non-:: filtered - ``_build_predictions`` — empty commits, single op, scoring in [0, 1] Integration tests ----------------- - predict --json schema (all required keys present) - -j alias works - exit_code + duration_ms in JSON - --top limits output - --min-confidence filter - --file filter - --explain on existing symbol (human output) - --explain ADDRESS --json → _ExplainJson structured output - --explain missing address exits 1 - empty repo exits non-zero Security tests -------------- - --explain without ``::`` exits 1 - --min-confidence out of range exits 1 """ from __future__ import annotations from collections.abc import Mapping import datetime import json import pathlib import textwrap import typing import pytest from muse.core.types import fake_id, long_id from muse.cli.commands.predict import ( _build_predictions, _confidence_label, _iter_symbol_ops, _module_key, _pct_bar, _sanitise, ) from muse.core.store import CommitRecord from tests.cli_test_helper import CliRunner cli = None runner = CliRunner() # --------------------------------------------------------------------------- # Helpers for building fake CommitRecords # --------------------------------------------------------------------------- def _fake_commit( *, commit_id: str = "sha256:aaa", ops: list[dict] | None = None, seconds_ago: int = 0, ) -> CommitRecord: """Build a minimal CommitRecord for unit testing _build_predictions.""" structured_delta = None if ops is not None: structured_delta = { "domain": "code", "ops": ops, "summary": "", "sem_ver_bump": "none", "breaking_changes": [], } return CommitRecord( commit_id=commit_id, branch="dev", snapshot_id="snap-1", message="test commit", committed_at=datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(seconds=seconds_ago), structured_delta=structured_delta, ) def _sym_op(address: str, op: str = "modify", new_summary: str = "") -> Mapping[str, object]: """Build a minimal symbol-level op dict (non-patch, has ::).""" return {"op": op, "address": address, "new_summary": new_summary} def _patch_op(file_addr: str, children: list[dict]) -> Mapping[str, object]: """Build a PatchOp with symbol-level child ops.""" return { "op": "patch", "address": file_addr, "child_ops": children, "child_domain": "code", "child_summary": "", "from_address": None, "file_change": None, } # --------------------------------------------------------------------------- # Unit — _sanitise # --------------------------------------------------------------------------- class TestSanitise: def test_passthrough_normal_string(self) -> None: assert _sanitise("hello world") == "hello world" def test_strips_control_chars(self) -> None: assert _sanitise("hello\x00world") == "helloworld" def test_strips_ansi_escape(self) -> None: result = _sanitise("hello\x1b[31mworld\x1b[0m") assert "\x1b" not in result def test_truncates_at_max_len(self) -> None: long_str = "a" * 100 result = _sanitise(long_str, max_len=10) assert len(result) <= 10 assert result.endswith("…") def test_exact_length_no_truncation(self) -> None: s = "a" * 80 result = _sanitise(s, max_len=80) assert result == s def test_strips_leading_trailing_whitespace(self) -> None: assert _sanitise(" hello ") == "hello" def test_unicode_passthrough(self) -> None: assert _sanitise("café résumé") == "café résumé" def test_empty_string(self) -> None: assert _sanitise("") == "" # --------------------------------------------------------------------------- # Unit — _module_key # --------------------------------------------------------------------------- class TestModuleKey: def test_depth_1_single_dir(self) -> None: key = _module_key("muse/core/store.py::foo", depth=1) assert key == "muse/" def test_depth_2_two_dirs(self) -> None: key = _module_key("muse/core/store.py::foo", depth=2) assert key == "muse/core/" def test_depth_3_deep(self) -> None: key = _module_key("a/b/c/d.py::foo", depth=3) assert key == "a/b/c/" def test_single_component_no_dir(self) -> None: key = _module_key("billing.py::compute", depth=2) assert key == "billing.py" def test_depth_beyond_path_length(self) -> None: # Should not crash when depth exceeds actual path depth. key = _module_key("a/b.py::foo", depth=10) assert "/" in key or key # just must not raise def test_strips_symbol_part(self) -> None: key1 = _module_key("muse/core/store.py::func_a", depth=2) key2 = _module_key("muse/core/store.py::func_b", depth=2) assert key1 == key2 # same module, different symbols # --------------------------------------------------------------------------- # Unit — _pct_bar # --------------------------------------------------------------------------- class TestPctBar: def test_zero_all_empty(self) -> None: bar = _pct_bar(0.0, width=10) assert bar == "░" * 10 def test_one_all_filled(self) -> None: bar = _pct_bar(1.0, width=10) assert bar == "█" * 10 def test_half_half(self) -> None: bar = _pct_bar(0.5, width=10) assert bar.count("█") == 5 assert bar.count("░") == 5 def test_below_zero_clamps(self) -> None: bar = _pct_bar(-0.5, width=10) assert bar == "░" * 10 def test_above_one_clamps(self) -> None: bar = _pct_bar(1.5, width=10) assert bar == "█" * 10 def test_width_respected(self) -> None: for w in (5, 10, 20, 40): bar = _pct_bar(0.7, width=w) assert len(bar) == w # --------------------------------------------------------------------------- # Unit — _confidence_label # --------------------------------------------------------------------------- class TestConfidenceLabel: def test_high_at_threshold(self) -> None: assert _confidence_label(0.70) == "high" def test_high_above_threshold(self) -> None: assert _confidence_label(0.99) == "high" def test_medium_just_below_high(self) -> None: assert _confidence_label(0.69) == "medium" def test_medium_at_threshold(self) -> None: assert _confidence_label(0.45) == "medium" def test_low_just_below_medium(self) -> None: assert _confidence_label(0.44) == "low" def test_low_at_zero(self) -> None: assert _confidence_label(0.0) == "low" # --------------------------------------------------------------------------- # Unit — _iter_symbol_ops # --------------------------------------------------------------------------- class TestIterSymbolOps: def test_none_delta_yields_nothing(self) -> None: assert list(_iter_symbol_ops(None)) == [] def test_empty_ops_yields_nothing(self) -> None: delta = {"domain": "code", "ops": [], "summary": "", "sem_ver_bump": "none", "breaking_changes": []} assert list(_iter_symbol_ops(delta)) == [] def test_non_patch_op_with_symbol_address_yielded(self) -> None: op = _sym_op("billing.py::compute") delta = {"domain": "code", "ops": [op], "summary": "", "sem_ver_bump": "none", "breaking_changes": []} result = list(_iter_symbol_ops(delta)) assert len(result) == 1 assert result[0]["address"] == "billing.py::compute" def test_non_symbol_op_filtered_out(self) -> None: op = {"op": "modify", "address": "billing.py"} # no :: delta = {"domain": "code", "ops": [op], "summary": "", "sem_ver_bump": "none", "breaking_changes": []} assert list(_iter_symbol_ops(delta)) == [] def test_patch_op_yields_symbol_children(self) -> None: child = _sym_op("billing.py::compute") op = _patch_op("billing.py", [child]) delta = {"domain": "code", "ops": [op], "summary": "", "sem_ver_bump": "none", "breaking_changes": []} result = list(_iter_symbol_ops(delta)) assert len(result) == 1 assert result[0]["address"] == "billing.py::compute" def test_patch_op_filters_non_symbol_children(self) -> None: file_child = {"op": "modify", "address": "billing.py", "new_summary": ""} sym_child = _sym_op("billing.py::compute") op = _patch_op("billing.py", [file_child, sym_child]) delta = {"domain": "code", "ops": [op], "summary": "", "sem_ver_bump": "none", "breaking_changes": []} result = list(_iter_symbol_ops(delta)) assert len(result) == 1 def test_multiple_ops_all_yielded(self) -> None: ops = [_sym_op(f"billing.py::func_{i}") for i in range(5)] delta = {"domain": "code", "ops": ops, "summary": "", "sem_ver_bump": "none", "breaking_changes": []} result = list(_iter_symbol_ops(delta)) assert len(result) == 5 # --------------------------------------------------------------------------- # Unit — _build_predictions # --------------------------------------------------------------------------- class TestBuildPredictions: def test_empty_commits_returns_empty(self) -> None: result = _build_predictions([], horizon=10, module_depth=2) assert result == [] def test_commits_with_no_ops_returns_empty(self) -> None: commits = [_fake_commit(ops=None), _fake_commit(ops=[])] result = _build_predictions(commits, horizon=10, module_depth=2) assert result == [] def test_single_symbol_appears_in_predictions(self) -> None: op = _sym_op("billing.py::compute") commits = [_fake_commit(ops=[op])] result = _build_predictions(commits, horizon=10, module_depth=2) addresses = [r["address"] for r in result] assert "billing.py::compute" in addresses def test_score_in_zero_one(self) -> None: op = _sym_op("billing.py::compute") commits = [_fake_commit(ops=[op])] result = _build_predictions(commits, horizon=10, module_depth=2) for r in result: assert 0.0 <= r["score"] <= 1.0, f"score out of range: {r['score']}" def test_sorted_by_score_descending(self) -> None: ops = [_sym_op(f"billing.py::func_{i}") for i in range(10)] # All commits different symbols — scores may vary. commits = [_fake_commit(commit_id=fake_id(str(i)), ops=[op]) for i, op in enumerate(ops)] result = _build_predictions(commits, horizon=10, module_depth=2) scores = [r["score"] for r in result] assert scores == sorted(scores, reverse=True) def test_confidence_label_consistent_with_score(self) -> None: op = _sym_op("billing.py::compute") commits = [_fake_commit(ops=[op])] * 5 result = _build_predictions(commits, horizon=10, module_depth=2) for r in result: if r["score"] >= 0.70: assert r["confidence"] == "high" elif r["score"] >= 0.45: assert r["confidence"] == "medium" else: assert r["confidence"] == "low" def test_reasons_list_non_empty(self) -> None: op = _sym_op("billing.py::compute") commits = [_fake_commit(ops=[op])] result = _build_predictions(commits, horizon=10, module_depth=2) for r in result: assert len(r["reasons"]) >= 1 def test_frequent_symbol_scores_higher(self) -> None: """A symbol touched many times in the horizon window scores higher.""" freq_op = _sym_op("billing.py::hot") rare_op = _sym_op("billing.py::cold") # hot appears in 10 commits, cold in 1 commits = ( [_fake_commit(commit_id=fake_id(str(i)), ops=[freq_op]) for i in range(10)] + [_fake_commit(commit_id=long_id(f"{10:064}"), ops=[rare_op])] ) result = _build_predictions(commits, horizon=15, module_depth=2) by_addr = {r["address"]: r for r in result} assert "billing.py::hot" in by_addr assert "billing.py::cold" in by_addr assert by_addr["billing.py::hot"]["score"] > by_addr["billing.py::cold"]["score"] def test_horizon_window_controls_frequency_signal(self) -> None: """Commits outside the horizon don't boost frequency signal.""" op = _sym_op("billing.py::compute") # 5 commits all beyond horizon=3 commits = [ _fake_commit(commit_id=fake_id(str(i)), ops=[op]) for i in range(10) ] result_wide = _build_predictions(commits, horizon=10, module_depth=2) result_narrow = _build_predictions(commits, horizon=2, module_depth=2) # Wide horizon → more frequency signal → equal or higher score by_addr_w = {r["address"]: r for r in result_wide} by_addr_n = {r["address"]: r for r in result_narrow} if "billing.py::compute" in by_addr_w and "billing.py::compute" in by_addr_n: assert ( by_addr_w["billing.py::compute"]["signals"]["frequency"] >= by_addr_n["billing.py::compute"]["signals"]["frequency"] ) def test_co_change_partners_populated(self) -> None: """Symbols that always co-occur get co_change signal and partners.""" op_a = _sym_op("billing.py::func_a") op_b = _sym_op("billing.py::func_b") commits = [ _fake_commit(commit_id=fake_id(str(i)), ops=[op_a, op_b]) for i in range(5) ] result = _build_predictions(commits, horizon=10, module_depth=2) by_addr = {r["address"]: r for r in result} for addr in ("billing.py::func_a", "billing.py::func_b"): assert addr in by_addr assert by_addr[addr]["signals"]["co_change"] > 0 def test_required_fields_present(self) -> None: op = _sym_op("billing.py::compute") commits = [_fake_commit(ops=[op])] result = _build_predictions(commits, horizon=10, module_depth=2) required = { "address", "name", "kind", "file", "score", "confidence", "reasons", "signals", "last_changed_commit", "last_changed_date", "top_partners", } for r in result: assert required <= set(r.keys()), f"Missing keys: {required - set(r.keys())}" def test_signal_keys_present(self) -> None: op = _sym_op("billing.py::compute") commits = [_fake_commit(ops=[op])] result = _build_predictions(commits, horizon=10, module_depth=2) signal_keys = {"recency", "frequency", "co_change", "sig_instability", "module_velocity"} for r in result: assert signal_keys <= set(r["signals"].keys()) # --------------------------------------------------------------------------- # Integration fixtures # --------------------------------------------------------------------------- @pytest.fixture def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: """Repo with 3 commits touching billing.py::compute repeatedly.""" monkeypatch.chdir(tmp_path) monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) runner.invoke(cli, ["init", "--domain", "code"]) src = tmp_path / "billing.py" # Commit 1 — initial version src.write_text("def compute(x: int) -> int:\n return x * 2\n\ndef helper() -> int:\n return 0\n") runner.invoke(cli, ["commit", "-m", "initial"]) # Commit 2 — modify compute src.write_text("def compute(x: int) -> int:\n return x * 3\n\ndef helper() -> int:\n return 0\n") runner.invoke(cli, ["commit", "-m", "tweak compute"]) # Commit 3 — modify compute again src.write_text("def compute(x: int) -> int:\n return x * 4\n\ndef helper() -> int:\n return 0\n") runner.invoke(cli, ["commit", "-m", "tweak compute again"]) return tmp_path # --------------------------------------------------------------------------- # Integration — basic invocation # --------------------------------------------------------------------------- class TestPredictBasic: def test_exits_zero(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict"]) assert result.exit_code == 0, result.output def test_emits_some_output(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict"]) assert result.exit_code == 0 assert len(result.output) > 0 def test_empty_repo_exits_nonzero( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.chdir(tmp_path) monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) runner.invoke(cli, ["init", "--domain", "code"]) # No commits → HEAD is None result = runner.invoke(cli, ["code", "predict"]) assert result.exit_code != 0 # --------------------------------------------------------------------------- # Integration — --json schema # --------------------------------------------------------------------------- class TestPredictJsonSchema: def test_json_exits_zero(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json"]) assert result.exit_code == 0, result.output def test_json_is_valid(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(result.output.strip()) assert isinstance(data, dict) def test_json_has_generated_at(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(result.output) assert "generated_at" in data def test_json_has_horizon_commits(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(result.output) assert "horizon_commits" in data assert isinstance(data["horizon_commits"], int) def test_json_has_commits_analysed(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(result.output) assert "commits_analysed" in data def test_json_has_truncated(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(result.output) assert "truncated" in data assert isinstance(data["truncated"], bool) def test_json_has_predictions_list(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(result.output) assert "predictions" in data assert isinstance(data["predictions"], list) def test_json_has_exit_code(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(result.output) assert "exit_code" in data def test_json_exit_code_is_zero(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(result.output) assert data["exit_code"] == 0 def test_json_has_duration_ms(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(result.output) assert "duration_ms" in data assert isinstance(data["duration_ms"], float) assert data["duration_ms"] > 0 def test_prediction_record_schema(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(result.output) if not data["predictions"]: pytest.skip("no predictions in this repo") pred = data["predictions"][0] required = { "address", "name", "kind", "file", "score", "confidence", "reasons", "signals", "last_changed_commit", "last_changed_date", "top_partners", } assert required <= set(pred.keys()) def test_signal_set_schema(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(result.output) if not data["predictions"]: pytest.skip("no predictions in this repo") signals = data["predictions"][0]["signals"] assert set(signals.keys()) == { "recency", "frequency", "co_change", "sig_instability", "module_velocity" } # --------------------------------------------------------------------------- # Integration — -j alias # --------------------------------------------------------------------------- class TestJsonAlias: def test_j_alias_exits_zero(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "-j"]) assert result.exit_code == 0, result.output def test_j_alias_emits_valid_json(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "-j"]) data = json.loads(result.output.strip()) assert isinstance(data, dict) def test_j_alias_matches_json_flag(self, repo: pathlib.Path) -> None: r1 = runner.invoke(cli, ["code", "predict", "-j"]) r2 = runner.invoke(cli, ["code", "predict", "--json"]) d1 = json.loads(r1.output) d2 = json.loads(r2.output) # Dynamic fields differ; structural shape must match. d1.pop("generated_at", None) d2.pop("generated_at", None) d1.pop("duration_ms", None) d2.pop("duration_ms", None) assert set(d1.keys()) == set(d2.keys()) # --------------------------------------------------------------------------- # Integration — filters # --------------------------------------------------------------------------- class TestPredictFilters: def test_top_limits_predictions(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json", "--top", "1"]) data = json.loads(result.output) assert len(data["predictions"]) <= 1 def test_top_zero_shows_all(self, repo: pathlib.Path) -> None: r_all = runner.invoke(cli, ["code", "predict", "--json", "--top", "0"]) r_one = runner.invoke(cli, ["code", "predict", "--json", "--top", "1"]) d_all = json.loads(r_all.output) d_one = json.loads(r_one.output) assert len(d_all["predictions"]) >= len(d_one["predictions"]) def test_min_confidence_filters(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "code", "predict", "--json", "--min-confidence", "0.99", ]) data = json.loads(result.output) for pred in data["predictions"]: assert pred["score"] >= 0.99 def test_file_filter(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "code", "predict", "--json", "--file", "nonexistent_file_xyz.py", ]) data = json.loads(result.output) assert data["predictions"] == [] def test_horizon_reflected_in_json(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, ["code", "predict", "--json", "--horizon", "5"]) data = json.loads(result.output) assert data["horizon_commits"] == 5 # --------------------------------------------------------------------------- # Integration — --explain # --------------------------------------------------------------------------- class TestPredictExplain: def test_explain_missing_separator_exits_one(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "code", "predict", "--explain", "billing_compute", ]) assert result.exit_code == 1 def test_explain_unknown_address_exits_one(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "code", "predict", "--explain", "billing.py::zzz_nonexistent_xyz", ]) assert result.exit_code == 1 def test_explain_human_output_for_known_symbol(self, repo: pathlib.Path) -> None: # First get a prediction to know a valid address. r = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(r.output) if not data["predictions"]: pytest.skip("no predictions") addr = data["predictions"][0]["address"] result = runner.invoke(cli, ["code", "predict", "--explain", addr]) assert result.exit_code == 0 assert "signal breakdown" in result.output.lower() or "score" in result.output.lower() # --------------------------------------------------------------------------- # Integration — --explain --json (_ExplainJson) # --------------------------------------------------------------------------- class TestPredictExplainJson: def test_explain_json_exits_zero(self, repo: pathlib.Path) -> None: r = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(r.output) if not data["predictions"]: pytest.skip("no predictions") addr = data["predictions"][0]["address"] result = runner.invoke(cli, [ "code", "predict", "--explain", addr, "--json", ]) assert result.exit_code == 0, result.output def test_explain_json_valid_json(self, repo: pathlib.Path) -> None: r = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(r.output) if not data["predictions"]: pytest.skip("no predictions") addr = data["predictions"][0]["address"] result = runner.invoke(cli, [ "code", "predict", "--explain", addr, "-j", ]) explain = json.loads(result.output.strip()) assert isinstance(explain, dict) def test_explain_json_has_address(self, repo: pathlib.Path) -> None: r = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(r.output) if not data["predictions"]: pytest.skip("no predictions") addr = data["predictions"][0]["address"] result = runner.invoke(cli, ["code", "predict", "--explain", addr, "-j"]) explain = json.loads(result.output) assert explain["address"] == addr def test_explain_json_has_score(self, repo: pathlib.Path) -> None: r = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(r.output) if not data["predictions"]: pytest.skip("no predictions") addr = data["predictions"][0]["address"] result = runner.invoke(cli, ["code", "predict", "--explain", addr, "-j"]) explain = json.loads(result.output) assert "score" in explain assert isinstance(explain["score"], float) def test_explain_json_has_signals(self, repo: pathlib.Path) -> None: r = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(r.output) if not data["predictions"]: pytest.skip("no predictions") addr = data["predictions"][0]["address"] result = runner.invoke(cli, ["code", "predict", "--explain", addr, "-j"]) explain = json.loads(result.output) assert "signals" in explain assert set(explain["signals"].keys()) == { "recency", "frequency", "co_change", "sig_instability", "module_velocity" } def test_explain_json_has_reasons(self, repo: pathlib.Path) -> None: r = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(r.output) if not data["predictions"]: pytest.skip("no predictions") addr = data["predictions"][0]["address"] result = runner.invoke(cli, ["code", "predict", "--explain", addr, "-j"]) explain = json.loads(result.output) assert "reasons" in explain assert isinstance(explain["reasons"], list) def test_explain_json_has_top_partners(self, repo: pathlib.Path) -> None: r = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(r.output) if not data["predictions"]: pytest.skip("no predictions") addr = data["predictions"][0]["address"] result = runner.invoke(cli, ["code", "predict", "--explain", addr, "-j"]) explain = json.loads(result.output) assert "top_partners" in explain assert isinstance(explain["top_partners"], list) def test_explain_json_has_exit_code(self, repo: pathlib.Path) -> None: r = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(r.output) if not data["predictions"]: pytest.skip("no predictions") addr = data["predictions"][0]["address"] result = runner.invoke(cli, ["code", "predict", "--explain", addr, "-j"]) explain = json.loads(result.output) assert "exit_code" in explain assert explain["exit_code"] == 0 def test_explain_json_has_duration_ms(self, repo: pathlib.Path) -> None: r = runner.invoke(cli, ["code", "predict", "--json"]) data = json.loads(r.output) if not data["predictions"]: pytest.skip("no predictions") addr = data["predictions"][0]["address"] result = runner.invoke(cli, ["code", "predict", "--explain", addr, "-j"]) explain = json.loads(result.output) assert "duration_ms" in explain assert explain["duration_ms"] >= 0 def test_explain_json_importable_typeddict(self) -> None: from muse.cli.commands.predict import _ExplainJson hints = typing.get_type_hints(_ExplainJson) assert "address" in hints assert "score" in hints assert "signals" in hints assert "reasons" in hints assert "top_partners" in hints assert "exit_code" in hints assert "duration_ms" in hints # --------------------------------------------------------------------------- # Integration — security # --------------------------------------------------------------------------- class TestPredictSecurity: def test_min_confidence_above_one_exits_one(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "code", "predict", "--min-confidence", "1.5", ]) assert result.exit_code == 1 def test_min_confidence_below_zero_exits_one(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "code", "predict", "--min-confidence", "-0.1", ]) assert result.exit_code == 1 def test_explain_without_double_colon_exits_one(self, repo: pathlib.Path) -> None: result = runner.invoke(cli, [ "code", "predict", "--explain", "no_separator_here", ]) assert result.exit_code == 1 # --------------------------------------------------------------------------- # TypedDict coverage # --------------------------------------------------------------------------- class TestTypedDicts: def test_predict_json_typeddict_importable(self) -> None: from muse.cli.commands.predict import _PredictJson hints = typing.get_type_hints(_PredictJson) assert "predictions" in hints assert "exit_code" in hints assert "duration_ms" in hints def test_explain_json_typeddict_importable(self) -> None: from muse.cli.commands.predict import _ExplainJson assert _ExplainJson is not None class TestRegisterFlags: def _parse(self, *args: str) -> "argparse.Namespace": import argparse from muse.cli.commands.predict import register p = argparse.ArgumentParser() top_sub = p.add_subparsers() code_p = top_sub.add_parser("code") code_sub = code_p.add_subparsers() register(code_sub) return p.parse_args(["code", "predict", *args]) def test_json_short_flag(self) -> None: args = self._parse("-j") assert args.json_out is True def test_json_long_flag(self) -> None: args = self._parse("--json") assert args.json_out is True def test_default_no_json(self) -> None: args = self._parse() assert args.json_out is False