"""8-tier tests for hub.py ergonomics: --body-file and --assignee. Covers: Tier 1 — Shape / Schema Tier 2 — Round-Trip / Integration Tier 3 — Edge Cases Tier 4 — Stress Tier 5 — Data Integrity Tier 6 — Performance Tier 7 — Security (extra vigilant — all inputs are user-supplied) Tier 8 — Docstrings / API Contract """ from __future__ import annotations from collections.abc import Mapping import io import json import pathlib import textwrap import time import unittest.mock import pytest from tests.cli_test_helper import CliRunner from muse._version import __version__ from muse.core.paths import commits_dir, heads_dir, muse_dir, objects_dir, snapshots_dir from muse.cli.commands.hub.connection import ( _MAX_HANDLE_LEN, _HANDLE_RE, _resolve_body, _validate_assignee, ) from muse.cli.config import set_hub_url from muse.core.types import MsgpackDict from muse.core.identity import IdentityEntry type _HubBody = Mapping[str, str | bool | list[str] | None] | None type _HubResponse = MsgpackDict from muse.core.errors import ExitCode from muse.core.identity import IdentityEntry, save_identity cli = None runner = CliRunner() # --------------------------------------------------------------------------- # Shared fixtures # --------------------------------------------------------------------------- @pytest.fixture() def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: """Minimal .muse/ repo with identity wired up.""" heads_dir(tmp_path).mkdir(parents=True, exist_ok=True) objects_dir(tmp_path).mkdir(parents=True, exist_ok=True) commits_dir(tmp_path).mkdir(parents=True, exist_ok=True) snapshots_dir(tmp_path).mkdir(parents=True, exist_ok=True) (muse_dir(tmp_path) / "repo.json").write_text( json.dumps({ "repo_id": "test-repo", "schema_version": __version__, "domain": "midi", }) ) (muse_dir(tmp_path) / "HEAD").write_text("ref: refs/heads/main\n") monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) monkeypatch.chdir(tmp_path) return tmp_path def _setup_auth(repo: pathlib.Path, hub_url: str = "https://localhost:1337/gabriel/muse") -> None: set_hub_url(hub_url, repo) identity = IdentityEntry( type="human", handle="gabriel", key_path=str(repo / "fake_home" / ".muse" / "keys" / "key.pem"), algorithm="ed25519", fingerprint="deadbeef", ) save_identity(hub_url, identity) def _hub_patches(calls: list[tuple], response: _HubResponse | None = None) -> unittest.mock._patch: """Context manager that mocks hub network helpers and records calls.""" mock_resp = response or {"issueId": "abc-123", "number": 1, "title": "t"} def _fake_api(hub_url: str, identity: IdentityEntry, method: str, path: str, **kw: str) -> _HubResponse: calls.append((method, path, kw)) return mock_resp return unittest.mock.patch.multiple( "muse.cli.commands.hub", _hub_api=unittest.mock.MagicMock(side_effect=_fake_api), _get_hub_and_identity=unittest.mock.MagicMock( return_value=("https://localhost:1337/gabriel/muse", unittest.mock.MagicMock()) ), _resolve_repo_id=unittest.mock.MagicMock(return_value="test-repo-id"), ) # =========================================================================== # Tier 1 — Shape / Schema # =========================================================================== class TestShape: """Parser flags exist; helpers are importable with correct signatures.""" def test_resolve_body_importable(self) -> None: """_resolve_body must be importable from hub.""" from muse.cli.commands.hub import _resolve_body assert callable(_resolve_body) def test_validate_assignee_importable(self) -> None: """_validate_assignee must be importable from hub.""" from muse.cli.commands.hub import _validate_assignee assert callable(_validate_assignee) def test_handle_re_is_compiled_pattern(self) -> None: """_HANDLE_RE must be a compiled regex.""" import re assert isinstance(_HANDLE_RE, re.Pattern) def test_max_handle_len_positive_int(self) -> None: """_MAX_HANDLE_LEN must be a positive integer.""" assert isinstance(_MAX_HANDLE_LEN, int) assert _MAX_HANDLE_LEN > 0 def test_assignee_flag_present_on_issue_create(self, repo: pathlib.Path) -> None: """--assignee must appear in the issue create help text.""" _setup_auth(repo) result = runner.invoke(cli, ["hub", "issue", "create", "--help"]) assert "--assignee" in result.output, ( f"--assignee flag missing from 'hub issue create --help': {result.output}" ) def test_body_file_flag_present_on_issue_create(self, repo: pathlib.Path) -> None: """--body-file must appear in the issue create help text.""" _setup_auth(repo) result = runner.invoke(cli, ["hub", "issue", "create", "--help"]) assert "--body-file" in result.output def test_body_file_flag_present_on_issue_update(self, repo: pathlib.Path) -> None: """--body-file must appear in the issue update help text.""" _setup_auth(repo) result = runner.invoke(cli, ["hub", "issue", "update", "--help"]) assert "--body-file" in result.output def test_body_file_flag_present_on_issue_comment(self, repo: pathlib.Path) -> None: """--body-file must appear in the issue comment help text.""" _setup_auth(repo) result = runner.invoke(cli, ["hub", "issue", "comment", "--help"]) assert "--body-file" in result.output def test_handle_re_matches_valid_handles(self) -> None: """_HANDLE_RE must match well-formed handles.""" valid = [ "gabriel", "aaronrene", "mix-engine-7", "studio_9", "A", "a1", "z" * _MAX_HANDLE_LEN, ] for h in valid: if len(h) <= _MAX_HANDLE_LEN: assert _HANDLE_RE.match(h), f"_HANDLE_RE should match {h!r}" def test_handle_re_rejects_leading_hyphen(self) -> None: assert not _HANDLE_RE.match("-gabriel") def test_handle_re_rejects_spaces(self) -> None: assert not _HANDLE_RE.match("gab riel") def test_handle_re_rejects_at_symbol(self) -> None: assert not _HANDLE_RE.match("@gabriel") def test_handle_re_rejects_slash(self) -> None: assert not _HANDLE_RE.match("gabriel/muse") # =========================================================================== # Tier 2 — Round-Trip / Integration # =========================================================================== class TestRoundTrip: """CLI invocations produce correct sequences of API calls.""" def test_issue_create_with_assignee_calls_create_then_assign( self, repo: pathlib.Path ) -> None: """create + --assignee must POST to /issues then POST to /assign.""" _setup_auth(repo) calls: list[tuple] = [] with _hub_patches(calls): result = runner.invoke( cli, ["hub", "issue", "create", "--title", "Test", "--assignee", "aaronrene"], ) assert result.exit_code == 0, result.output methods_and_paths = [(m, p) for m, p, _ in calls] assert any("/issues" in p and m == "POST" for m, p in methods_and_paths), ( f"Expected POST /issues; got {methods_and_paths}" ) assert any("/assign" in p for _, p in methods_and_paths), ( f"Expected /assign call; got {methods_and_paths}" ) def test_issue_create_without_assignee_does_not_call_assign( self, repo: pathlib.Path ) -> None: """create without --assignee must NOT dispatch an /assign call.""" _setup_auth(repo) calls: list[tuple] = [] with _hub_patches(calls): result = runner.invoke( cli, ["hub", "issue", "create", "--title", "No assignee"] ) assert result.exit_code == 0, result.output paths = [p for _, p, _ in calls] assert not any("/assign" in p for p in paths), ( f"/assign was called even though no --assignee was given: {paths}" ) def test_issue_create_with_body_file_sends_correct_body( self, tmp_path: pathlib.Path, repo: pathlib.Path ) -> None: """Body read from --body-file must appear verbatim in the API payload.""" _setup_auth(repo) body_text = "# My Issue\n\nHello `world`\n" body_file = tmp_path / "body.md" body_file.write_text(body_text, encoding="utf-8") payloads: list[dict] = [] def _capturing_api(hub_url: str, identity: IdentityEntry, method: str, path: str, *, body: _HubBody = None, **kw: str) -> _HubResponse: if body: payloads.append(body) return {"issueId": "x", "number": 1, "title": "t"} with unittest.mock.patch.multiple( "muse.cli.commands.hub", _hub_api=unittest.mock.MagicMock(side_effect=_capturing_api), _get_hub_and_identity=unittest.mock.MagicMock( return_value=("https://localhost:1337/gabriel/muse", unittest.mock.MagicMock()) ), _resolve_repo_id=unittest.mock.MagicMock(return_value="test-repo-id"), ): result = runner.invoke( cli, ["hub", "issue", "create", "--title", "t", "--body-file", str(body_file)], ) assert result.exit_code == 0, result.output assert payloads, "No API payload captured" assert payloads[0]["body"] == body_text def test_issue_update_with_body_file_sends_correct_body( self, tmp_path: pathlib.Path, repo: pathlib.Path ) -> None: """issue update --body-file must PATCH with the file contents.""" _setup_auth(repo) body_text = "Updated body `with backticks`" body_file = tmp_path / "update.md" body_file.write_text(body_text, encoding="utf-8") payloads: list[dict] = [] def _capturing_api(hub_url: str, identity: IdentityEntry, method: str, path: str, *, body: _HubBody = None, **kw: str) -> _HubResponse: if body: payloads.append(body) return {"number": 1, "title": "x", "state": "open"} with unittest.mock.patch.multiple( "muse.cli.commands.hub", _hub_api=unittest.mock.MagicMock(side_effect=_capturing_api), _get_hub_and_identity=unittest.mock.MagicMock( return_value=("https://localhost:1337/gabriel/muse", unittest.mock.MagicMock()) ), _resolve_repo_id=unittest.mock.MagicMock(return_value="test-repo-id"), ): result = runner.invoke( cli, ["hub", "issue", "update", "1", "--body-file", str(body_file)], ) assert result.exit_code == 0, result.output assert payloads and payloads[0]["body"] == body_text def test_issue_assign_validates_handle_before_api_call( self, repo: pathlib.Path ) -> None: """issue assign with a bad handle must fail before any network I/O.""" _setup_auth(repo) calls: list[tuple] = [] with _hub_patches(calls): result = runner.invoke( cli, ["hub", "issue", "assign", "1", "--assignee", "bad handle!"], ) assert result.exit_code != 0 assert not calls, "API was called even though the handle was invalid" # =========================================================================== # Tier 3 — Edge Cases # =========================================================================== class TestEdgeCases: """Boundary conditions and unusual-but-valid inputs.""" def test_body_file_wins_over_body_when_both_given( self, tmp_path: pathlib.Path, repo: pathlib.Path ) -> None: """When both --body and --body-file are given, --body-file wins.""" _setup_auth(repo) file_text = "from file" body_file = tmp_path / "b.txt" body_file.write_text(file_text, encoding="utf-8") payloads: list[dict] = [] def _cap(hub_url: str, identity: IdentityEntry, method: str, path: str, *, body: _HubBody = None, **kw: str) -> _HubResponse: if body: payloads.append(body) return {"issueId": "x", "number": 1, "title": "t"} with unittest.mock.patch.multiple( "muse.cli.commands.hub", _hub_api=unittest.mock.MagicMock(side_effect=_cap), _get_hub_and_identity=unittest.mock.MagicMock( return_value=("https://localhost:1337/gabriel/muse", unittest.mock.MagicMock()) ), _resolve_repo_id=unittest.mock.MagicMock(return_value="test-repo-id"), ): result = runner.invoke( cli, ["hub", "issue", "create", "--title", "t", "--body", "from inline", "--body-file", str(body_file)], ) assert result.exit_code == 0, result.output assert payloads[0]["body"] == file_text, ( f"Expected file content, got {payloads[0]['body']!r}" ) def test_body_file_missing_exits_with_user_error( self, tmp_path: pathlib.Path, repo: pathlib.Path ) -> None: """--body-file pointing at a non-existent file must exit USER_ERROR.""" _setup_auth(repo) result = runner.invoke( cli, ["hub", "issue", "create", "--title", "t", "--body-file", str(tmp_path / "does_not_exist.md")], ) assert result.exit_code == ExitCode.USER_ERROR def test_validate_assignee_empty_allow_empty_true_ok(self) -> None: """An empty handle is valid when allow_empty=True (unassign path).""" _validate_assignee("", allow_empty=True) # must not raise def test_validate_assignee_empty_allow_empty_false_raises(self) -> None: """An empty handle is invalid when allow_empty=False (create path).""" with pytest.raises(SystemExit) as exc_info: _validate_assignee("", allow_empty=False) assert exc_info.value.code == ExitCode.USER_ERROR def test_validate_assignee_single_char_valid(self) -> None: """A single alphanumeric char is a valid handle.""" _validate_assignee("a") def test_issue_create_assignee_shown_in_success_output( self, repo: pathlib.Path ) -> None: """After creation with --assignee, the assignee name should appear in output.""" _setup_auth(repo) calls: list[tuple] = [] with _hub_patches(calls): result = runner.invoke( cli, ["hub", "issue", "create", "--title", "t", "--assignee", "aaronrene"], ) assert result.exit_code == 0, result.output assert "aaronrene" in result.stderr def test_resolve_body_neither_body_nor_body_file_returns_empty(self) -> None: """When args has neither body nor body_file, _resolve_body returns ''.""" import argparse args = argparse.Namespace(body=None, body_file=None) assert _resolve_body(args) == "" def test_resolve_body_body_only_returns_body(self) -> None: import argparse args = argparse.Namespace(body="hello", body_file=None) assert _resolve_body(args) == "hello" def test_resolve_body_body_file_none_returns_body_string(self) -> None: import argparse args = argparse.Namespace(body="hello", body_file=None) assert _resolve_body(args) == "hello" def test_resolve_body_body_file_path_returns_file_contents( self, tmp_path: pathlib.Path ) -> None: import argparse f = tmp_path / "b.txt" f.write_text("file body", encoding="utf-8") args = argparse.Namespace(body="", body_file=str(f)) assert _resolve_body(args) == "file body" def test_resolve_body_stdin_sentinel(self, monkeypatch: pytest.MonkeyPatch) -> None: """Passing '-' for body_file must read from stdin.""" import argparse import io monkeypatch.setattr("sys.stdin", io.StringIO("stdin body")) args = argparse.Namespace(body="", body_file="-") assert _resolve_body(args) == "stdin body" # =========================================================================== # Tier 4 — Stress # =========================================================================== class TestStress: """Max-length inputs and bulk operations work without error.""" def test_max_length_valid_handle_accepted(self) -> None: """A handle exactly _MAX_HANDLE_LEN chars long must be accepted.""" handle = "a" * _MAX_HANDLE_LEN _validate_assignee(handle) # must not raise def test_handle_one_over_max_rejected(self) -> None: """A handle one char over _MAX_HANDLE_LEN must be rejected.""" handle = "a" * (_MAX_HANDLE_LEN + 1) with pytest.raises(SystemExit) as exc_info: _validate_assignee(handle) assert exc_info.value.code == ExitCode.USER_ERROR def test_large_body_file_read_correctly(self, tmp_path: pathlib.Path) -> None: """A 200KB body file must be read in full without truncation.""" import argparse large_body = "# heading\n\n" + ("line of content\n" * 12_000) f = tmp_path / "large.md" f.write_text(large_body, encoding="utf-8") args = argparse.Namespace(body="", body_file=str(f)) result = _resolve_body(args) assert result == large_body def test_validate_assignee_called_1000_times_fast(self) -> None: """_validate_assignee on a valid handle 1000× must complete quickly.""" start = time.monotonic() for _ in range(1000): _validate_assignee("gabriel") elapsed = time.monotonic() - start assert elapsed < 0.5, f"1000 validations took {elapsed:.3f}s — too slow" def test_issue_create_with_many_labels_and_assignee( self, repo: pathlib.Path ) -> None: """Multiple labels combined with --assignee must succeed.""" _setup_auth(repo) calls: list[tuple] = [] with _hub_patches(calls): result = runner.invoke( cli, [ "hub", "issue", "create", "--title", "Big issue", "--label", "bug", "--label", "enhancement", "--label", "phase-1", "--assignee", "aaronrene", ], ) assert result.exit_code == 0, result.output assert any("/assign" in p for _, p, _ in calls) # =========================================================================== # Tier 5 — Data Integrity # =========================================================================== class TestDataIntegrity: """Payloads sent to the API match exactly what the user supplied.""" def test_assignee_payload_matches_flag_value(self, repo: pathlib.Path) -> None: """The handle POSTed to /assign must be exactly what --assignee received.""" _setup_auth(repo) captured_assign_body: list[dict] = [] def _cap(hub_url: str, identity: IdentityEntry, method: str, path: str, *, body: _HubBody = None, **kw: str) -> _HubResponse: if "/assign" in path and body: captured_assign_body.append(body) return {"issueId": "x", "number": 1, "title": "t"} with unittest.mock.patch.multiple( "muse.cli.commands.hub", _hub_api=unittest.mock.MagicMock(side_effect=_cap), _get_hub_and_identity=unittest.mock.MagicMock( return_value=("https://localhost:1337/gabriel/muse", unittest.mock.MagicMock()) ), _resolve_repo_id=unittest.mock.MagicMock(return_value="test-repo-id"), ): runner.invoke( cli, ["hub", "issue", "create", "--title", "t", "--assignee", "aaronrene"], ) assert captured_assign_body, "No assign payload captured" assert captured_assign_body[0]["assignee"] == "aaronrene" def test_body_file_bytes_match_payload_exactly( self, tmp_path: pathlib.Path, repo: pathlib.Path ) -> None: """Non-ASCII in body file (UTF-8 encoded) must round-trip unchanged.""" _setup_auth(repo) body_text = "Header\n\n```python\nprint('hello')\n```\n\n— em-dash ✓" body_file = tmp_path / "body.md" body_file.write_text(body_text, encoding="utf-8") payloads: list[dict] = [] def _cap(hub_url: str, identity: IdentityEntry, method: str, path: str, *, body: _HubBody = None, **kw: str) -> _HubResponse: if body: payloads.append(body) return {"issueId": "x", "number": 1, "title": "t"} with unittest.mock.patch.multiple( "muse.cli.commands.hub", _hub_api=unittest.mock.MagicMock(side_effect=_cap), _get_hub_and_identity=unittest.mock.MagicMock( return_value=("https://localhost:1337/gabriel/muse", unittest.mock.MagicMock()) ), _resolve_repo_id=unittest.mock.MagicMock(return_value="test-repo-id"), ): runner.invoke( cli, ["hub", "issue", "create", "--title", "t", "--body-file", str(body_file)], ) assert payloads and payloads[0]["body"] == body_text def test_assign_called_with_correct_issue_number( self, repo: pathlib.Path ) -> None: """The /assign path must contain the issue number returned by the create endpoint.""" _setup_auth(repo) assign_paths: list[str] = [] def _cap(hub_url: str, identity: IdentityEntry, method: str, path: str, *, body: _HubBody = None, **kw: str) -> _HubResponse: if "/assign" in path: assign_paths.append(path) return {"issueId": "x", "number": 42, "title": "t"} with unittest.mock.patch.multiple( "muse.cli.commands.hub", _hub_api=unittest.mock.MagicMock(side_effect=_cap), _get_hub_and_identity=unittest.mock.MagicMock( return_value=("https://localhost:1337/gabriel/muse", unittest.mock.MagicMock()) ), _resolve_repo_id=unittest.mock.MagicMock(return_value="test-repo-id"), ): runner.invoke( cli, ["hub", "issue", "create", "--title", "t", "--assignee", "gabriel"], ) assert assign_paths, "No /assign call found" assert "42" in assign_paths[0], ( f"/assign path {assign_paths[0]!r} should contain issue number 42" ) # =========================================================================== # Tier 6 — Performance # =========================================================================== class TestPerformance: """_resolve_body and _validate_assignee run in sub-millisecond time.""" def test_resolve_body_from_file_single_read( self, tmp_path: pathlib.Path ) -> None: """_resolve_body must read the file exactly once (no double reads).""" import argparse f = tmp_path / "b.txt" f.write_text("content", encoding="utf-8") read_count = 0 real_open = open def _counting_open(path: pathlib.Path | str, *a: str, **kw: str) -> "io.IOBase": nonlocal read_count if str(path) == str(f): read_count += 1 return real_open(path, *a, **kw) with unittest.mock.patch("builtins.open", side_effect=_counting_open): args = argparse.Namespace(body="", body_file=str(f)) _resolve_body(args) assert read_count == 1, f"File was read {read_count} times; expected exactly 1" def test_validate_assignee_valid_under_1ms(self) -> None: """Single _validate_assignee call on a valid handle must finish under 1 ms.""" start = time.monotonic() _validate_assignee("gabriel") elapsed = time.monotonic() - start assert elapsed < 0.001, f"took {elapsed*1000:.2f} ms" def test_validate_assignee_invalid_under_1ms(self) -> None: """Rejection path must also complete under 1 ms.""" start = time.monotonic() with pytest.raises(SystemExit): _validate_assignee("bad handle!") elapsed = time.monotonic() - start assert elapsed < 0.001, f"took {elapsed*1000:.2f} ms" # =========================================================================== # Tier 7 — Security # =========================================================================== class TestSecurity: """All user-supplied handle input is rigorously rejected for malformed values. Threat model (see _validate_assignee docstring): * Terminal injection via ANSI/control sequences * Null-byte truncation attacks * Newline injection — makes error messages look like success * Unicode confusable characters impersonating ASCII handles * Oversized input for DoS * Shell metacharacters (defense-in-depth; args go into JSON anyway) """ @pytest.mark.parametrize("bad_handle", [ "gab\x00riel", # null byte "gab\nriel", # newline "gab\rriel", # carriage return "gab\triel", # tab "\x01gabriel", # SOH control char "gabriel\x1b[31mred", # ANSI escape — terminal injection "gabriel\x7f", # DEL "\x0cgabriel", # form feed "gabriel\x0b", # vertical tab "gabriel\x08extra", # backspace — visual overwrite attack ]) def test_control_characters_rejected(self, bad_handle: str) -> None: """Any handle with a control character must be rejected.""" with pytest.raises(SystemExit) as exc_info: _validate_assignee(bad_handle) assert exc_info.value.code == ExitCode.USER_ERROR, ( f"Expected USER_ERROR for handle {bad_handle!r}" ) @pytest.mark.parametrize("bad_handle", [ "gаbriel", # Cyrillic 'а' (U+0430) — looks like 'a' "aaronrenё", # Cyrillic 'ё' (U+0451) "gábríel", # Latin accented chars "gabriel™", # trademark symbol "gabriel→muse", # arrow "𝕘𝕒𝕓𝕣𝕚𝕖𝕝", # mathematical double-struck ]) def test_non_ascii_unicode_rejected(self, bad_handle: str) -> None: """Non-ASCII Unicode handles must be rejected (confusable / RTL risk).""" with pytest.raises(SystemExit) as exc_info: _validate_assignee(bad_handle) assert exc_info.value.code == ExitCode.USER_ERROR, ( f"Expected USER_ERROR for handle {bad_handle!r}" ) @pytest.mark.parametrize("bad_handle", [ "gabriel muse", # space "gabriel@muse", # at sign "gabriel/muse", # slash — path traversal appearance "gabriel.muse", # dot "gabriel!", # bang "gabriel;rm -rf /", # shell injection attempt "gabriel$(whoami)", # command substitution "gabriel`id`", # backtick injection "gabriel|cat /etc/passwd", # pipe "gabriel&&echo pwned", # logical AND "gabriel>>/etc/crontab", # redirection ]) def test_shell_metacharacters_rejected(self, bad_handle: str) -> None: """Shell metacharacters and non-alphanumeric symbols must be rejected.""" with pytest.raises(SystemExit) as exc_info: _validate_assignee(bad_handle) assert exc_info.value.code == ExitCode.USER_ERROR def test_extremely_long_handle_rejected(self) -> None: """A handle of 1000 chars must be rejected — DoS guard.""" with pytest.raises(SystemExit) as exc_info: _validate_assignee("a" * 1000) assert exc_info.value.code == ExitCode.USER_ERROR def test_empty_handle_not_allowed_on_create( self, repo: pathlib.Path ) -> None: """issue create --assignee '' must fail before any API call.""" _setup_auth(repo) calls: list[tuple] = [] with _hub_patches(calls): result = runner.invoke( cli, ["hub", "issue", "create", "--title", "t", "--assignee", ""], ) assert result.exit_code != 0 assert not any("/issues" in p for _, p, _ in calls), ( "API was called even though the assignee was empty" ) def test_invalid_handle_rejects_before_network_io( self, repo: pathlib.Path ) -> None: """An invalid handle must fail before ANY network call is made.""" _setup_auth(repo) calls: list[tuple] = [] with _hub_patches(calls): result = runner.invoke( cli, ["hub", "issue", "create", "--title", "t", "--assignee", "bad handle!"], ) assert result.exit_code != 0 assert not calls, f"Network was called despite invalid handle: {calls}" def test_body_file_path_not_leaked_on_error( self, tmp_path: pathlib.Path, repo: pathlib.Path ) -> None: """Error message for missing --body-file must contain the path for actionability, but must not expose sensitive filesystem layout beyond what was explicitly given.""" _setup_auth(repo) # Use a path that doesn't exist missing = str(tmp_path / "secret_dir" / "body.md") result = runner.invoke( cli, ["hub", "issue", "create", "--title", "t", "--body-file", missing], ) assert result.exit_code == ExitCode.USER_ERROR # The path given by the user may appear in the error (for debugging), # but no other paths from the filesystem should be exposed. assert "secret_dir" in result.stderr or "body.md" in result.stderr, ( "Error message should mention the problematic path for debuggability" ) def test_issue_assign_invalid_handle_rejected( self, repo: pathlib.Path ) -> None: """issue assign with control-char handle must fail before any API call.""" _setup_auth(repo) calls: list[tuple] = [] with _hub_patches(calls): result = runner.invoke( cli, ["hub", "issue", "assign", "1", "--assignee", "bad\x00handle"], ) assert result.exit_code != 0 assert not calls def test_issue_update_invalid_assign_rejected_before_network( self, repo: pathlib.Path ) -> None: """issue update --assign with bad handle must fail before API call.""" _setup_auth(repo) calls: list[tuple] = [] with _hub_patches(calls): result = runner.invoke( cli, ["hub", "issue", "update", "1", "--assign", "bad handle!"], ) assert result.exit_code != 0 assign_calls = [p for _, p, _ in calls if "/assign" in p] assert not assign_calls # =========================================================================== # Tier 8 — Docstrings / API Contract # =========================================================================== class TestDocstrings: """Key symbols have complete, accurate docstrings.""" def test_resolve_body_has_docstring(self) -> None: """_resolve_body must have a non-empty docstring.""" assert _resolve_body.__doc__, "_resolve_body has no docstring" def test_resolve_body_docstring_mentions_body_file(self) -> None: assert "body_file" in _resolve_body.__doc__ or "body-file" in _resolve_body.__doc__ def test_resolve_body_docstring_mentions_stdin(self) -> None: assert "-" in _resolve_body.__doc__, ( "Docstring should mention '-' as the stdin sentinel" ) def test_resolve_body_docstring_has_args_section(self) -> None: assert "Args:" in _resolve_body.__doc__ def test_resolve_body_docstring_has_returns_section(self) -> None: assert "Returns:" in _resolve_body.__doc__ def test_resolve_body_docstring_has_raises_section(self) -> None: assert "Raises:" in _resolve_body.__doc__ def test_validate_assignee_has_docstring(self) -> None: assert _validate_assignee.__doc__, "_validate_assignee has no docstring" def test_validate_assignee_docstring_mentions_allow_empty(self) -> None: assert "allow_empty" in _validate_assignee.__doc__ def test_validate_assignee_docstring_has_threat_model(self) -> None: doc = _validate_assignee.__doc__ assert any( kw in doc for kw in ("terminal", "injection", "null", "control", "Threat") ), "Docstring should describe the threat model" def test_validate_assignee_docstring_has_raises_section(self) -> None: assert "Raises:" in _validate_assignee.__doc__ def test_validate_assignee_docstring_mentions_user_error(self) -> None: assert "USER_ERROR" in _validate_assignee.__doc__ def test_handle_re_constant_is_named_consistently(self) -> None: """_HANDLE_RE must follow the module constant naming convention.""" from muse.cli.commands import hub assert hasattr(hub, "_HANDLE_RE"), "_HANDLE_RE not found in hub module" def test_max_handle_len_constant_documented_in_code(self) -> None: """_MAX_HANDLE_LEN must exist as a module-level constant.""" from muse.cli.commands import hub assert hasattr(hub, "_MAX_HANDLE_LEN") assert isinstance(hub._MAX_HANDLE_LEN, int)