"""Seven-tier tests for ``muse/cli/commands/symbol_log.py``. Tiers ----- Unit — TypedDict fields, SymbolEvent constructor/to_dict, _flat_ops, _find_events_in_commit for each EventKind, _print_human branches. Integration — -j alias parity, JSON envelope fields, rename-tracking, all EventKind paths end-to-end through _find_events_in_commit. End-to-end — CLI invocation: valid symbol, missing symbol, bad address, --from, --max truncation, invalid ref. Stress — 1 000 SymbolEvent constructions; concurrent reads. Data integrity — to_dict round-trip; chronological ordering; counts accurate. Security — ANSI in address/message/detail; hostile strings in address. Performance — 1 000 to_dict calls under 0.5 s; duration_ms < 30 000ms. """ from __future__ import annotations from collections.abc import Mapping import datetime import json import os import pathlib import textwrap import threading import time from typing import TYPE_CHECKING, get_type_hints import pytest from muse.core.types import fake_id from tests.cli_test_helper import CliRunner, InvokeResult if TYPE_CHECKING: from muse.core.commits import CommitRecord runner = CliRunner() # ────────────────────────────────────────────────────────────────────────────── # Fixtures # ────────────────────────────────────────────────────────────────────────────── def _commit(repo: pathlib.Path, files: Mapping[str, str], message: str) -> None: for name, content in files.items(): path = repo / name path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") saved = os.getcwd() try: os.chdir(repo) runner.invoke(None, ["code", "add", "."]) runner.invoke(None, ["commit", "-m", message]) finally: os.chdir(saved) def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult: saved = os.getcwd() try: os.chdir(repo) return runner.invoke(None, args) finally: os.chdir(saved) def _symlog(repo: pathlib.Path, *args: str) -> InvokeResult: return _invoke(repo, ["code", "symbol-log", *args]) @pytest.fixture() def sym_repo(tmp_path: pathlib.Path) -> pathlib.Path: """Repo with two commits so symbol-log has real history to walk. Commit 1: billing.py with class Invoice + function process_invoice. Commit 2: billing.py with Invoice body modified (new method). """ saved = os.getcwd() try: os.chdir(tmp_path) runner.invoke(None, ["init"]) finally: os.chdir(saved) _commit(tmp_path, { "billing.py": textwrap.dedent("""\ class Invoice: def __init__(self, amount): self.amount = amount def process_invoice(inv): return inv.amount * 1.1 """), }, "feat: add Invoice and process_invoice") _commit(tmp_path, { "billing.py": textwrap.dedent("""\ class Invoice: def __init__(self, amount): self.amount = amount def total(self): return self.amount * 1.1 def process_invoice(inv): return inv.total() """), }, "feat: add Invoice.total method") return tmp_path # ────────────────────────────────────────────────────────────────────────────── # Shared commit/delta helpers # ────────────────────────────────────────────────────────────────────────────── def _make_commit( *, commit_id: str = fake_id("aa"), message: str = "feat: hello", committed_at: datetime.datetime | None = None, structured_delta: Mapping[str, object] | None = None, ) -> "CommitRecord": from muse.core.commits import CommitRecord return CommitRecord( commit_id=commit_id, branch="dev", parent_commit_id=None, snapshot_id="snap", message=message, committed_at=committed_at or datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc), structured_delta=structured_delta, ) def _insert_delta(address: str, summary: str = "created") -> Mapping[str, object]: return {"ops": [{"op": "insert", "address": address, "content_summary": summary}]} def _delete_delta(address: str, summary: str = "deleted") -> Mapping[str, object]: return {"ops": [{"op": "delete", "address": address, "content_summary": summary}]} def _replace_delta(address: str, new_summary: str = "implementation changed") -> Mapping[str, object]: return {"ops": [{"op": "replace", "address": address, "new_summary": new_summary}]} def _patch_delta(address: str, new_summary: str = "implementation changed") -> Mapping[str, object]: """replace wrapped in a patch parent — tests _flat_ops flattening.""" return { "ops": [ { "op": "patch", "address": address, "child_ops": [ {"op": "replace", "address": address, "new_summary": new_summary} ], } ] } # ────────────────────────────────────────────────────────────────────────────── # Unit — TypedDict # ────────────────────────────────────────────────────────────────────────────── class TestTypedDict: def test_symbol_log_json_exists(self) -> None: from muse.cli.commands.symbol_log import _SymbolLogJson # noqa: F401 def test_has_schema_version(self) -> None: from muse.cli.commands.symbol_log import _SymbolLogJson assert "schema" in get_type_hints(_SymbolLogJson) def test_has_exit_code(self) -> None: from muse.cli.commands.symbol_log import _SymbolLogJson assert "exit_code" in get_type_hints(_SymbolLogJson) def test_has_duration_ms(self) -> None: from muse.cli.commands.symbol_log import _SymbolLogJson assert "duration_ms" in get_type_hints(_SymbolLogJson) def test_has_core_fields(self) -> None: from muse.cli.commands.symbol_log import _SymbolLogJson hints = get_type_hints(_SymbolLogJson) for field in ("address", "start_ref", "total_commits_scanned", "truncated", "events"): assert field in hints, f"missing field: {field}" # ────────────────────────────────────────────────────────────────────────────── # Unit — SymbolEvent # ────────────────────────────────────────────────────────────────────────────── class TestSymbolEvent: def test_constructor_stores_kind(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent ev = SymbolEvent("created", _make_commit(), "f.py::fn", "created") assert ev.kind == "created" def test_constructor_stores_address(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent ev = SymbolEvent("modified", _make_commit(), "f.py::fn", "impl changed") assert ev.address == "f.py::fn" def test_constructor_stores_detail(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent ev = SymbolEvent("deleted", _make_commit(), "f.py::fn", "removed") assert ev.detail == "removed" def test_constructor_default_new_address_is_none(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent ev = SymbolEvent("created", _make_commit(), "f.py::fn", "x") assert ev.new_address is None def test_constructor_stores_new_address(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent ev = SymbolEvent("renamed", _make_commit(), "f.py::old", "old → new", "f.py::new") assert ev.new_address == "f.py::new" def test_to_dict_has_all_fields(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent commit = _make_commit(commit_id=fake_id("bb"), message="msg") ev = SymbolEvent("modified", commit, "f.py::fn", "detail") d = ev.to_dict() for key in ("event", "commit_id", "message", "committed_at", "address", "detail", "new_address"): assert key in d, f"missing key: {key}" def test_to_dict_event_matches_kind(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent ev = SymbolEvent("signature", _make_commit(), "f.py::fn", "sig changed") assert ev.to_dict()["event"] == "signature" def test_to_dict_committed_at_is_isoformat(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent dt = datetime.datetime(2026, 3, 14, 12, 0, tzinfo=datetime.timezone.utc) ev = SymbolEvent("created", _make_commit(committed_at=dt), "f.py::fn", "x") iso = ev.to_dict()["committed_at"] assert "2026-03-14" in iso assert "T" in iso def test_to_dict_new_address_none_when_not_set(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent ev = SymbolEvent("modified", _make_commit(), "f.py::fn", "x") assert ev.to_dict()["new_address"] is None # ────────────────────────────────────────────────────────────────────────────── # Unit — _flat_ops # ────────────────────────────────────────────────────────────────────────────── class TestFlatOps: def test_passthrough_non_patch(self) -> None: from muse.cli.commands.symbol_log import _flat_ops ops = [{"op": "insert", "address": "f.py::fn"}] assert _flat_ops(ops) == ops def test_flattens_patch_children(self) -> None: from muse.cli.commands.symbol_log import _flat_ops child = {"op": "replace", "address": "f.py::fn", "new_summary": "x"} ops = [{"op": "patch", "address": "f.py::fn", "child_ops": [child]}] result = _flat_ops(ops) assert result == [child] def test_mixed_ops_preserved_in_order(self) -> None: from muse.cli.commands.symbol_log import _flat_ops insert = {"op": "insert", "address": "f.py::a"} child = {"op": "replace", "address": "f.py::b", "new_summary": "y"} patch = {"op": "patch", "address": "f.py::b", "child_ops": [child]} result = _flat_ops([insert, patch]) assert result == [insert, child] def test_empty_ops_returns_empty(self) -> None: from muse.cli.commands.symbol_log import _flat_ops assert _flat_ops([]) == [] # ────────────────────────────────────────────────────────────────────────────── # Unit — _find_events_in_commit (each EventKind) # ────────────────────────────────────────────────────────────────────────────── class TestFindEventsInCommit: def test_no_delta_returns_empty(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit commit = _make_commit(structured_delta=None) evs, addr = _find_events_in_commit(commit, "f.py::fn") assert evs == [] assert addr == "f.py::fn" def test_insert_produces_created(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit commit = _make_commit(structured_delta=_insert_delta("f.py::fn")) evs, addr = _find_events_in_commit(commit, "f.py::fn") assert len(evs) == 1 assert evs[0].kind == "created" assert addr == "f.py::fn" def test_delete_produces_deleted(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit commit = _make_commit(structured_delta=_delete_delta("f.py::fn")) evs, addr = _find_events_in_commit(commit, "f.py::fn") assert len(evs) == 1 assert evs[0].kind == "deleted" def test_delete_moved_to_produces_moved(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit commit = _make_commit(structured_delta=_delete_delta("f.py::fn", "moved to g.py::fn")) evs, _ = _find_events_in_commit(commit, "f.py::fn") assert evs[0].kind == "moved" def test_replace_produces_modified(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit commit = _make_commit(structured_delta=_replace_delta("f.py::fn")) evs, addr = _find_events_in_commit(commit, "f.py::fn") assert len(evs) == 1 assert evs[0].kind == "modified" def test_replace_renamed_to_updates_address(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit commit = _make_commit(structured_delta=_replace_delta("f.py::old", "renamed to new")) evs, addr = _find_events_in_commit(commit, "f.py::old") assert evs[0].kind == "renamed" assert addr == "f.py::new" assert evs[0].new_address == "f.py::new" def test_replace_moved_to_produces_moved(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit commit = _make_commit(structured_delta=_replace_delta("f.py::fn", "moved to g.py::fn")) evs, _ = _find_events_in_commit(commit, "f.py::fn") assert evs[0].kind == "moved" def test_replace_signature_produces_signature(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit commit = _make_commit(structured_delta=_replace_delta("f.py::fn", "signature changed")) evs, _ = _find_events_in_commit(commit, "f.py::fn") assert evs[0].kind == "signature" def test_patch_wrapper_is_flattened(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit commit = _make_commit(structured_delta=_patch_delta("f.py::fn")) evs, _ = _find_events_in_commit(commit, "f.py::fn") assert len(evs) == 1 assert evs[0].kind == "modified" def test_unrelated_address_produces_no_events(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit commit = _make_commit(structured_delta=_insert_delta("other.py::other")) evs, addr = _find_events_in_commit(commit, "f.py::fn") assert evs == [] assert addr == "f.py::fn" # ────────────────────────────────────────────────────────────────────────────── # Integration — alias, docstrings, envelope # ────────────────────────────────────────────────────────────────────────────── class TestAliasRegistration: def test_j_alias_registered(self) -> None: from muse.cli.commands.symbol_log import register import argparse p = argparse.ArgumentParser() sub = p.add_subparsers() register(sub) ns = p.parse_args(["symbol-log", "f.py::fn", "-j"]) assert ns.json_out is True def test_json_flag_sets_as_json_true(self) -> None: from muse.cli.commands.symbol_log import register import argparse p = argparse.ArgumentParser() sub = p.add_subparsers() register(sub) ns = p.parse_args(["symbol-log", "f.py::fn", "--json"]) assert ns.json_out is True class TestDocstrings: def test_register_mentions_json_alias(self) -> None: from muse.cli.commands.symbol_log import register doc = register.__doc__ or "" assert "--json" in doc or "-j" in doc def test_run_mentions_exit_code(self) -> None: from muse.cli.commands.symbol_log import run assert "exit_code" in (run.__doc__ or "") def test_run_mentions_duration_ms(self) -> None: from muse.cli.commands.symbol_log import run assert "duration_ms" in (run.__doc__ or "") def test_run_mentions_schema_version(self) -> None: from muse.cli.commands.symbol_log import run assert "schema" in (run.__doc__ or "") class TestJsonEnvelope: def test_schema_version_present(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r.exit_code == 0 assert "schema" in json.loads(r.output) def test_exit_code_zero(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r.exit_code == 0 d = json.loads(r.output) assert d["exit_code"] == 0 def test_duration_ms_present_and_float(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r.exit_code == 0 d = json.loads(r.output) assert "duration_ms" in d assert isinstance(d["duration_ms"], float) def test_schema_version_nonempty_string(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r.exit_code == 0 d = json.loads(r.output) assert isinstance(d["schema"], int) and d["schema"] > 0 class TestJsonAlias: def test_j_parity_with_json_flag(self, sym_repo: pathlib.Path) -> None: r1 = _symlog(sym_repo, "billing.py::Invoice", "--json") r2 = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r1.exit_code == 0 assert r2.exit_code == 0 d1, d2 = json.loads(r1.output), json.loads(r2.output) assert d1["address"] == d2["address"] assert d1["events"] == d2["events"] assert d1["schema"] == d2["schema"] assert d1["exit_code"] == d2["exit_code"] # ────────────────────────────────────────────────────────────────────────────── # End-to-end # ────────────────────────────────────────────────────────────────────────────── class TestEndToEnd: def test_valid_symbol_exits_zero(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice") assert r.exit_code == 0 def test_unknown_symbol_exits_zero_with_no_events(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::DoesNotExistXXX") assert r.exit_code == 0 assert "no events found" in r.output def test_bad_address_no_double_colon_exits_nonzero(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py") assert r.exit_code != 0 def test_max_1_sets_truncated_true_in_json(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "--max", "1", "-j") assert r.exit_code == 0 d = json.loads(r.output) assert d["truncated"] is True assert d["total_commits_scanned"] == 1 def test_max_1_shows_truncation_warning_in_human(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "--max", "1") assert r.exit_code == 0 assert "incomplete" in r.output or "limit" in r.output def test_max_zero_exits_nonzero(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "--max", "0") assert r.exit_code != 0 def test_invalid_from_ref_exits_nonzero(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "--from", "deadbeefdeadbeef") assert r.exit_code != 0 def test_json_address_field_matches_input(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r.exit_code == 0 assert json.loads(r.output)["address"] == "billing.py::Invoice" def test_json_events_is_list(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r.exit_code == 0 assert isinstance(json.loads(r.output)["events"], list) def test_start_ref_is_head_by_default(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r.exit_code == 0 assert json.loads(r.output)["start_ref"] == "HEAD" def test_human_output_shows_symbol_header(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice") assert r.exit_code == 0 assert "billing.py::Invoice" in r.output # ────────────────────────────────────────────────────────────────────────────── # Stress # ────────────────────────────────────────────────────────────────────────────── class TestStress: def test_1000_symbol_event_constructions(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent commit = _make_commit() for i in range(1_000): ev = SymbolEvent("modified", commit, f"f{i}.py::fn", f"detail {i}") assert ev.kind == "modified" def test_1000_to_dict_calls(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent commit = _make_commit() ev = SymbolEvent("created", commit, "f.py::fn", "x") for _ in range(1_000): d = ev.to_dict() assert "event" in d def test_flat_ops_10000_calls(self) -> None: from muse.cli.commands.symbol_log import _flat_ops ops = [{"op": "insert", "address": f"f{i}.py::fn"} for i in range(10)] for _ in range(10_000): result = _flat_ops(ops) assert len(result) == 10 def test_concurrent_find_events(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit commit = _make_commit(structured_delta=_insert_delta("f.py::fn")) results: list[int] = [] lock = threading.Lock() def _run() -> None: evs, _ = _find_events_in_commit(commit, "f.py::fn") with lock: results.append(len(evs)) threads = [threading.Thread(target=_run) for _ in range(50)] for t in threads: t.start() for t in threads: t.join() assert all(n == 1 for n in results) assert len(results) == 50 # ────────────────────────────────────────────────────────────────────────────── # Data integrity # ────────────────────────────────────────────────────────────────────────────── class TestDataIntegrity: def test_to_dict_preserves_all_fields(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent commit = _make_commit( commit_id=fake_id("cc"), message="fix: something", committed_at=datetime.datetime(2026, 5, 1, tzinfo=datetime.timezone.utc), ) ev = SymbolEvent("renamed", commit, "a.py::old", "old → new", "a.py::new") d = ev.to_dict() assert d["event"] == "renamed" assert d["commit_id"] == fake_id("cc") assert d["message"] == "fix: something" assert d["address"] == "a.py::old" assert d["detail"] == "old → new" assert d["new_address"] == "a.py::new" assert "2026-05-01" in d["committed_at"] def test_events_in_json_are_chronological(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r.exit_code == 0 events = json.loads(r.output)["events"] if len(events) >= 2: times = [datetime.datetime.fromisoformat(e["committed_at"]) for e in events] assert times == sorted(times), "events not in chronological order" def test_total_commits_scanned_is_int(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r.exit_code == 0 assert isinstance(json.loads(r.output)["total_commits_scanned"], int) def test_truncated_is_bool(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r.exit_code == 0 assert isinstance(json.loads(r.output)["truncated"], bool) def test_rename_tracking_continues_with_new_address(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit rename_commit = _make_commit(structured_delta=_replace_delta("f.py::old", "renamed to new")) _, next_addr = _find_events_in_commit(rename_commit, "f.py::old") assert next_addr == "f.py::new" insert_commit = _make_commit(structured_delta=_insert_delta("f.py::new")) evs, _ = _find_events_in_commit(insert_commit, next_addr) assert len(evs) == 1 assert evs[0].kind == "created" def test_new_address_none_for_modified(self) -> None: from muse.cli.commands.symbol_log import _find_events_in_commit commit = _make_commit(structured_delta=_replace_delta("f.py::fn")) evs, _ = _find_events_in_commit(commit, "f.py::fn") assert evs[0].new_address is None def test_json_serialisable(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r.exit_code == 0 json.loads(r.output) # must not raise # ────────────────────────────────────────────────────────────────────────────── # Security # ────────────────────────────────────────────────────────────────────────────── class TestSecurity: def test_ansi_in_address_does_not_crash(self, sym_repo: pathlib.Path) -> None: ansi_addr = "\x1b[31mbad\x1b[0m.py::fn" r = _symlog(sym_repo, ansi_addr) assert r.exit_code in (0, 1, 2) def test_ansi_in_commit_message_survives_to_dict(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent malicious_msg = "\x1b[31mmalicious\x1b[0m" ev = SymbolEvent("modified", _make_commit(message=malicious_msg), "f.py::fn", "x") d = ev.to_dict() assert d["message"] == malicious_msg def test_very_long_address_does_not_crash(self, sym_repo: pathlib.Path) -> None: long_addr = f"f.py::{'x' * 10_000}" r = _symlog(sym_repo, long_addr) assert r.exit_code in (0, 1, 2) def test_unicode_in_address_does_not_crash(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "音符.py::関数") assert r.exit_code in (0, 1, 2) def test_hostile_detail_survives_json_serialisation(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent malicious = '"; DROP TABLE commits; --' ev = SymbolEvent("modified", _make_commit(), "f.py::fn", malicious) d = ev.to_dict() assert json.loads(json.dumps(d))["detail"] == malicious def test_very_long_message_in_commit_does_not_crash(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent ev = SymbolEvent("modified", _make_commit(message="x" * 100_000), "f.py::fn", "x") d = ev.to_dict() assert len(d["message"]) == 100_000 # ────────────────────────────────────────────────────────────────────────────── # Performance # ────────────────────────────────────────────────────────────────────────────── class TestPerformance: def test_1000_to_dict_under_500ms(self) -> None: from muse.cli.commands.symbol_log import SymbolEvent ev = SymbolEvent("modified", _make_commit(), "f.py::fn", "impl changed") start = time.perf_counter() for _ in range(1_000): ev.to_dict() elapsed = time.perf_counter() - start assert elapsed < 0.5, f"1 000 to_dict calls took {elapsed:.2f}s" def test_duration_ms_present_and_reasonable(self, sym_repo: pathlib.Path) -> None: r = _symlog(sym_repo, "billing.py::Invoice", "-j") assert r.exit_code == 0 d = json.loads(r.output) assert "duration_ms" in d assert 0 <= d["duration_ms"] < 30_000 # Flag registration # ────────────────────────────────────────────────────────────────────────────── class TestRegisterFlags: def _parse(self, *args: str) -> "argparse.Namespace": import argparse from muse.cli.commands.symbol_log import register p = argparse.ArgumentParser() sub = p.add_subparsers() register(sub) return p.parse_args(["symbol-log", *args]) def test_default_json_out_is_false(self) -> None: ns = self._parse("src/utils.py::fn") assert ns.json_out is False def test_json_flag_sets_json_out(self) -> None: ns = self._parse("--json", "src/utils.py::fn") assert ns.json_out is True def test_j_shorthand_sets_json_out(self) -> None: ns = self._parse("-j", "src/utils.py::fn") assert ns.json_out is True