"""Tests for Phase 4.2 — ``muse commit --meta`` and ``--event-type`` flags. Coverage tiers (Rule #0) ------------------------ 1. **Unit** — ``_validate_meta_payload``, ``_validate_event_type``, ``_has_sensitive_keys``, new flag parsing, constants. 2. **Integration** — Commits written to a real repo include ``metadata["meta"]`` and ``metadata["event_type"]`` and survive a round-trip through ``read_commit``. 3. **End-to-end** — CLI invocations using the full argparse + run() path. 4. **Stress** — 500 sequential commits each carrying ``--meta`` payloads complete in < 60 s. 5. **Data-integrity** — ``--meta`` content is preserved byte-for-byte through ``write_commit`` → ``read_commit`` → ``CommitRecord``. 6. **Performance** — 1 000 calls to ``_validate_meta_payload`` and ``_validate_event_type`` complete in < 500 ms combined. 7. **Security** — Sensitive keys (password, api_key, TOKEN, bearer…), oversized payloads, non-dict JSON, deeply-nested sensitive keys, and control-char injection are all rejected; errors are clear and never leak raw secret values into messages. """ from __future__ import annotations import json import os import pathlib import time from typing import Any import pytest from tests.cli_test_helper import CliRunner, InvokeResult from muse.core.store import read_commit, get_head_commit_id, read_current_branch runner = CliRunner() # ───────────────────────────────────────────────────────────────────────────── # Helpers # ───────────────────────────────────────────────────────────────────────────── def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult: """Run a muse command in *repo* and return the result.""" saved = os.getcwd() try: os.chdir(repo) return runner.invoke(None, args) finally: os.chdir(saved) def _commit(repo: pathlib.Path, *extra: str) -> InvokeResult: return _invoke(repo, ["commit", "-m", "test", *extra]) def _init_and_stage(tmp_path: pathlib.Path, filename: str = "note.md") -> pathlib.Path: """Init a repo, create and stage a file, return the repo path.""" tmp_path.mkdir(parents=True, exist_ok=True) _invoke(tmp_path, ["init"]) (tmp_path / filename).write_text("# Test\n\ncontent\n") return tmp_path # ───────────────────────────────────────────────────────────────────────────── # Fixtures # ───────────────────────────────────────────────────────────────────────────── @pytest.fixture() def repo(tmp_path: pathlib.Path) -> pathlib.Path: """Initialised repo with one tracked file ready to commit.""" return _init_and_stage(tmp_path) # ============================================================================= # Tier 1 — Unit tests # ============================================================================= class TestHasSensitiveKeys: """Unit tests for ``_has_sensitive_keys``.""" def _fn(self, obj: Any, depth: int = 0) -> bool: from muse.cli.commands.commit import _has_sensitive_keys return _has_sensitive_keys(obj, depth) def test_none_is_safe(self) -> None: assert self._fn(None) is False def test_empty_dict_is_safe(self) -> None: assert self._fn({}) is False def test_password_key_detected(self) -> None: assert self._fn({"password": "x"}) is True def test_api_key_underscore(self) -> None: assert self._fn({"api_key": "x"}) is True def test_api_key_hyphen(self) -> None: assert self._fn({"api-key": "x"}) is True def test_token_uppercase(self) -> None: assert self._fn({"TOKEN": "x"}) is True def test_secret_key(self) -> None: assert self._fn({"secret": "x"}) is True def test_bearer_key(self) -> None: assert self._fn({"bearer": "x"}) is True def test_private_key_underscore(self) -> None: assert self._fn({"private_key": "x"}) is True def test_private_key_hyphen(self) -> None: assert self._fn({"private-key": "x"}) is True def test_credential_key(self) -> None: assert self._fn({"credential": "x"}) is True def test_authorization_key(self) -> None: assert self._fn({"authorization": "Bearer abc"}) is True def test_safe_key(self) -> None: assert self._fn({"topic": "agents", "confidence": 0.9}) is False def test_nested_sensitive_key_depth_1(self) -> None: assert self._fn({"outer": {"password": "x"}}) is True def test_nested_sensitive_key_depth_8(self) -> None: """Sensitive key at exactly depth 8 must be detected.""" d: Any = {"password": "x"} for _ in range(8): d = {"nested": d} assert self._fn(d) is True def test_nested_sensitive_key_depth_9_not_scanned(self) -> None: """Sensitive key at depth 9 (beyond limit) must NOT be detected.""" d: Any = {"password": "x"} for _ in range(9): d = {"safe": d} assert self._fn(d) is False def test_sensitive_key_in_list(self) -> None: assert self._fn([{"password": "x"}]) is True def test_safe_list(self) -> None: assert self._fn([{"topic": "x"}, {"tag": "y"}]) is False def test_integer_scalar_safe(self) -> None: assert self._fn(42) is False def test_string_scalar_safe(self) -> None: assert self._fn("password") is False # values don't matter, only keys class TestValidateMetaPayload: """Unit tests for ``_validate_meta_payload``.""" def _fn(self, raw: str) -> str: from muse.cli.commands.commit import _validate_meta_payload return _validate_meta_payload(raw) def test_valid_simple_dict(self) -> None: result = self._fn('{"topic": "agents"}') assert json.loads(result) == {"topic": "agents"} def test_returns_canonical_json(self) -> None: """Keys must be sorted in the canonical output.""" result = self._fn('{"z": 1, "a": 2}') parsed = json.loads(result) assert list(parsed.keys()) == sorted(parsed.keys()) def test_invalid_json_raises(self) -> None: with pytest.raises(ValueError, match="valid JSON"): self._fn("{not valid}") def test_json_array_raises(self) -> None: with pytest.raises(ValueError, match="JSON object"): self._fn("[1, 2, 3]") def test_json_string_raises(self) -> None: with pytest.raises(ValueError, match="JSON object"): self._fn('"hello"') def test_json_number_raises(self) -> None: with pytest.raises(ValueError, match="JSON object"): self._fn("42") def test_json_null_raises(self) -> None: with pytest.raises(ValueError, match="JSON object"): self._fn("null") def test_sensitive_key_password_raises(self) -> None: with pytest.raises(ValueError, match="sensitive-data pattern"): self._fn('{"password": "x"}') def test_sensitive_key_api_key_raises(self) -> None: with pytest.raises(ValueError, match="sensitive-data pattern"): self._fn('{"api_key": "123"}') def test_sensitive_nested_key_raises(self) -> None: with pytest.raises(ValueError, match="sensitive-data pattern"): self._fn('{"outer": {"token": "abc"}}') def test_oversized_payload_raises(self) -> None: from muse.cli.commands.commit import _MAX_META_BYTES # Build a payload that serialises to more than _MAX_META_BYTES bytes. big = {"k" + str(i): "v" * 100 for i in range(_MAX_META_BYTES // 100 + 1)} with pytest.raises(ValueError, match="size limit"): self._fn(json.dumps(big)) def test_exactly_at_limit_accepted(self) -> None: """A payload at exactly _MAX_META_BYTES bytes (after encoding) must pass.""" from muse.cli.commands.commit import _MAX_META_BYTES # Build the smallest dict whose canonical form is ≤ the limit. payload = {"x": "y"} assert len(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()) <= _MAX_META_BYTES self._fn(json.dumps(payload)) # must not raise def test_empty_dict_accepted(self) -> None: result = self._fn("{}") assert result == "{}" def test_nested_values_preserved(self) -> None: raw = '{"context": {"agent": "claude", "confidence": 0.95}}' result = self._fn(raw) assert json.loads(result)["context"]["confidence"] == 0.95 def test_reserved_top_level_meta_raises(self) -> None: """Top-level 'meta' key conflicts with the CLI-controlled metadata slot.""" with pytest.raises(ValueError, match="reserved top-level key"): self._fn('{"meta": {"x": 1}}') def test_reserved_top_level_event_type_raises(self) -> None: """Top-level 'event_type' key conflicts with the --event-type slot.""" with pytest.raises(ValueError, match="reserved top-level key"): self._fn('{"event_type": "search"}') def test_reserved_top_level_both_raises(self) -> None: """Both reserved keys present together must still raise.""" with pytest.raises(ValueError, match="reserved top-level key"): self._fn('{"meta": {}, "event_type": "x"}') def test_reserved_key_only_at_top_level(self) -> None: """Nested 'meta' / 'event_type' (not at top level) is allowed.""" result = self._fn('{"context": {"meta": "ok", "event_type": "ok"}}') parsed = json.loads(result) assert parsed["context"]["meta"] == "ok" assert parsed["context"]["event_type"] == "ok" def test_nan_value_rejected(self) -> None: """NaN is not valid JSON; must be rejected at parse time.""" with pytest.raises(ValueError, match="NaN"): self._fn('{"x": NaN}') def test_infinity_value_rejected(self) -> None: """Infinity is not valid JSON; must be rejected at parse time.""" with pytest.raises(ValueError, match="Infinity"): self._fn('{"x": Infinity}') def test_negative_infinity_value_rejected(self) -> None: """-Infinity is not valid JSON; must be rejected at parse time.""" with pytest.raises(ValueError, match="Infinity"): self._fn('{"x": -Infinity}') def test_oversized_raw_input_rejected_pre_parse(self) -> None: """Raw input above the pre-parse cap is rejected before json.loads runs.""" from muse.cli.commands.commit import _MAX_META_RAW_BYTES oversized = "{" + ('"k":' + ('"' + "x" * 100 + '",') * 2_000) + '"end":1}' assert len(oversized) > _MAX_META_RAW_BYTES with pytest.raises(ValueError, match="pre-parse cap"): self._fn(oversized) def test_unicode_values_preserved(self) -> None: result = self._fn('{"note": "こんにちは"}') assert "こんにちは" in result def test_integer_values_preserved(self) -> None: result = self._fn('{"count": 42}') assert json.loads(result)["count"] == 42 def test_boolean_values_preserved(self) -> None: result = self._fn('{"active": true}') assert json.loads(result)["active"] is True class TestValidateEventType: """Unit tests for ``_validate_event_type``.""" def _fn(self, kind: str) -> str: from muse.cli.commands.commit import _validate_event_type return _validate_event_type(kind) def test_all_15_kinds_accepted(self) -> None: from muse.cli.commands.commit import _FALLBACK_EVENT_KINDS for kind in _FALLBACK_EVENT_KINDS: assert self._fn(kind) == kind def test_invalid_kind_raises(self) -> None: with pytest.raises(ValueError, match="not a valid"): self._fn("recall") def test_another_invalid_kind(self) -> None: with pytest.raises(ValueError, match="not a valid"): self._fn("note_read") def test_empty_string_raises(self) -> None: with pytest.raises(ValueError, match="not a valid"): self._fn("") def test_case_sensitive_uppercase_raises(self) -> None: with pytest.raises(ValueError, match="not a valid"): self._fn("WRITE") def test_valid_kinds_list_in_error_message(self) -> None: """Error message must list valid kinds so users can self-correct.""" with pytest.raises(ValueError) as exc_info: self._fn("nonexistent") assert "search" in str(exc_info.value) def test_returns_unchanged_valid_kind(self) -> None: assert self._fn("write") == "write" def test_10k_char_injection_raises_cleanly(self) -> None: """Injecting a 10 000-char string must not crash; must raise ValueError.""" with pytest.raises(ValueError): self._fn("x" * 10_000) class TestCommitConstants: """Unit tests for the constants introduced by Phase 4.2.""" def test_max_meta_bytes_is_64k(self) -> None: from muse.cli.commands.commit import _MAX_META_BYTES assert _MAX_META_BYTES == 65_536 def test_max_meta_raw_bytes_is_twice_canonical(self) -> None: """Pre-parse raw cap must be exactly 2× canonical cap (documented contract).""" from muse.cli.commands.commit import _MAX_META_BYTES, _MAX_META_RAW_BYTES assert _MAX_META_RAW_BYTES == _MAX_META_BYTES * 2 def test_reserved_meta_keys_contains_expected(self) -> None: from muse.cli.commands.commit import _RESERVED_META_KEYS assert "event_type" in _RESERVED_META_KEYS assert "meta" in _RESERVED_META_KEYS def test_fallback_event_kinds_has_15_entries(self) -> None: from muse.cli.commands.commit import _FALLBACK_EVENT_KINDS assert len(_FALLBACK_EVENT_KINDS) == 15 def test_fallback_event_kinds_contains_all_expected(self) -> None: from muse.cli.commands.commit import _FALLBACK_EVENT_KINDS expected = { "search", "export", "write", "import", "index", "propose", "agent_interaction", "capture", "error", "session_summary", "user", "consolidation", "consolidation_pass", "maintenance", "insight", } assert _FALLBACK_EVENT_KINDS == expected class TestNewFlags: """Unit tests: new --event-type and --meta flags are registered correctly.""" def _parse(self, *args: str) -> object: import argparse from muse.cli.commands.commit import register p = argparse.ArgumentParser() sub = p.add_subparsers() register(sub) return p.parse_args(["commit", *args]) def test_event_type_flag_default_is_none(self) -> None: ns = self._parse("-m", "x") assert ns.event_type is None def test_event_type_flag_accepts_value(self) -> None: ns = self._parse("-m", "x", "--event-type", "write") assert ns.event_type == "write" def test_meta_flag_default_is_none(self) -> None: ns = self._parse("-m", "x") assert ns.meta is None def test_meta_flag_accepts_json_string(self) -> None: ns = self._parse("-m", "x", "--meta", '{"k": "v"}') assert ns.meta == '{"k": "v"}' # ============================================================================= # Tier 2 — Integration tests # ============================================================================= class TestMetaIntegration: """Integration: commits with --meta and --event-type persist in the store.""" def test_event_type_persisted_in_metadata(self, repo: pathlib.Path) -> None: result = _commit(repo, "--event-type", "write") assert result.exit_code == 0 branch = read_current_branch(repo) cid = get_head_commit_id(repo, branch) assert cid is not None rec = read_commit(repo, cid) assert rec is not None assert rec.metadata.get("event_type") == "write" def test_meta_payload_persisted_in_metadata(self, repo: pathlib.Path) -> None: payload = '{"topic": "agents", "confidence": 0.9}' result = _commit(repo, "--meta", payload) assert result.exit_code == 0 branch = read_current_branch(repo) cid = get_head_commit_id(repo, branch) rec = read_commit(repo, cid) assert rec is not None stored = rec.metadata.get("meta") assert stored is not None parsed = json.loads(stored) assert parsed["topic"] == "agents" assert parsed["confidence"] == pytest.approx(0.9) def test_both_flags_together(self, repo: pathlib.Path) -> None: payload = '{"phase": "4.2"}' result = _commit(repo, "--event-type", "consolidation", "--meta", payload) assert result.exit_code == 0 branch = read_current_branch(repo) cid = get_head_commit_id(repo, branch) rec = read_commit(repo, cid) assert rec is not None assert rec.metadata["event_type"] == "consolidation" assert json.loads(rec.metadata["meta"])["phase"] == "4.2" def test_meta_not_present_when_flag_omitted(self, repo: pathlib.Path) -> None: result = _commit(repo) assert result.exit_code == 0 branch = read_current_branch(repo) cid = get_head_commit_id(repo, branch) rec = read_commit(repo, cid) assert rec is not None assert "meta" not in rec.metadata assert "event_type" not in rec.metadata def test_existing_metadata_keys_preserved(self, repo: pathlib.Path) -> None: """--section, --track, --emotion coexist with --meta and --event-type.""" result = _commit( repo, "--section", "verse", "--event-type", "write", "--meta", '{"note": "test"}', ) assert result.exit_code == 0 branch = read_current_branch(repo) cid = get_head_commit_id(repo, branch) rec = read_commit(repo, cid) assert rec is not None assert rec.metadata["section"] == "verse" assert rec.metadata["event_type"] == "write" assert json.loads(rec.metadata["meta"])["note"] == "test" def test_meta_canonical_json_is_sorted(self, repo: pathlib.Path) -> None: """Keys in stored --meta must be in sorted order (canonical form).""" result = _commit(repo, "--meta", '{"z": 1, "a": 2, "m": 3}') assert result.exit_code == 0 branch = read_current_branch(repo) cid = get_head_commit_id(repo, branch) rec = read_commit(repo, cid) assert rec is not None stored = json.loads(rec.metadata["meta"]) assert list(stored.keys()) == sorted(stored.keys()) # ============================================================================= # Tier 3 — End-to-end CLI tests # ============================================================================= class TestMetaEndToEnd: """End-to-end: full CLI path including error output.""" def test_invalid_event_type_exits_nonzero(self, repo: pathlib.Path) -> None: result = _commit(repo, "--event-type", "recall") assert result.exit_code != 0 def test_invalid_event_type_prints_error(self, repo: pathlib.Path) -> None: result = _commit(repo, "--event-type", "not_a_kind") combined = (result.stdout or "") + (result.stderr or "") assert "not a valid" in combined.lower() or "valid" in combined.lower() def test_invalid_json_meta_exits_nonzero(self, repo: pathlib.Path) -> None: result = _commit(repo, "--meta", "{broken json") assert result.exit_code != 0 def test_invalid_json_meta_prints_error(self, repo: pathlib.Path) -> None: result = _commit(repo, "--meta", "{broken}") combined = (result.stdout or "") + (result.stderr or "") assert "json" in combined.lower() or "meta" in combined.lower() def test_meta_with_sensitive_key_exits_nonzero(self, repo: pathlib.Path) -> None: result = _commit(repo, "--meta", '{"password": "secret123"}') assert result.exit_code != 0 def test_meta_with_array_exits_nonzero(self, repo: pathlib.Path) -> None: result = _commit(repo, "--meta", "[1, 2, 3]") assert result.exit_code != 0 def test_json_output_with_meta_flag(self, repo: pathlib.Path) -> None: result = _commit(repo, "--meta", '{"x": 1}', "--json") assert result.exit_code == 0 # The JSON output should include commit_id at minimum. data = json.loads(result.stdout or "{}") assert "commit_id" in data def test_valid_meta_succeeds(self, repo: pathlib.Path) -> None: result = _commit(repo, "--meta", '{"topic": "vault"}') assert result.exit_code == 0 def test_valid_event_type_succeeds(self, repo: pathlib.Path) -> None: result = _commit(repo, "--event-type", "index") assert result.exit_code == 0 # ============================================================================= # Tier 4 — Stress tests # ============================================================================= class TestMetaStress: """Stress: 100 commits with --meta complete without errors.""" def test_100_commits_with_meta(self, tmp_path: pathlib.Path) -> None: repo = _init_and_stage(tmp_path) start = time.perf_counter() for i in range(100): (repo / "note.md").write_text(f"# Note {i}\n\ncontent {i}\n") result = _commit(repo, "--meta", f'{{"iteration": {i}}}') assert result.exit_code == 0, f"Commit {i} failed: {result.stdout}{result.stderr}" elapsed = time.perf_counter() - start assert elapsed < 60.0, f"100 commits took {elapsed:.1f}s (limit 60s)" # ============================================================================= # Tier 5 — Data-integrity tests # ============================================================================= class TestMetaDataIntegrity: """Data-integrity: --meta payload is preserved byte-for-byte through the store.""" def test_complex_payload_round_trips(self, repo: pathlib.Path) -> None: """A complex nested payload must survive write → read unchanged.""" payload = { "agent": "claude-opus", "confidence": 0.987654321, "tags": ["vault", "memory", "phase4"], "nested": {"depth": 1, "values": [1, 2, 3]}, "unicode": "こんにちは世界", } raw = json.dumps(payload) result = _commit(repo, "--meta", raw) assert result.exit_code == 0 branch = read_current_branch(repo) cid = get_head_commit_id(repo, branch) rec = read_commit(repo, cid) assert rec is not None stored = json.loads(rec.metadata["meta"]) assert stored["agent"] == "claude-opus" assert stored["confidence"] == pytest.approx(0.987654321) assert sorted(stored["tags"]) == ["memory", "phase4", "vault"] # order preserved, check content assert stored["nested"]["depth"] == 1 assert stored["unicode"] == "こんにちは世界" def test_event_type_preserved_exactly(self, repo: pathlib.Path) -> None: for kind in ["write", "consolidation", "agent_interaction"]: (repo / "note.md").write_text(f"# {kind}\n") result = _commit(repo, "--event-type", kind) assert result.exit_code == 0 branch = read_current_branch(repo) cid = get_head_commit_id(repo, branch) rec = read_commit(repo, cid) assert rec is not None assert rec.metadata["event_type"] == kind def test_meta_keys_sorted_canonically(self, repo: pathlib.Path) -> None: """Keys must be stored in sorted order regardless of input order.""" _commit(repo, "--meta", '{"zz": 1, "aa": 2, "mm": 3}') branch = read_current_branch(repo) cid = get_head_commit_id(repo, branch) rec = read_commit(repo, cid) assert rec is not None keys = list(json.loads(rec.metadata["meta"]).keys()) assert keys == sorted(keys) # ============================================================================= # Tier 6 — Performance tests # ============================================================================= class TestMetaPerformance: """Performance: validation helpers must be fast.""" def test_1000_meta_validations_under_500ms(self) -> None: from muse.cli.commands.commit import _validate_meta_payload payload = json.dumps({"topic": "vault", "phase": "4.2", "count": 0}) start = time.perf_counter() for i in range(1000): _validate_meta_payload(payload.replace('"count": 0', f'"count": {i}')) elapsed = time.perf_counter() - start assert elapsed < 0.5, f"1000 validations took {elapsed * 1000:.0f}ms (limit 500ms)" def test_1000_event_type_validations_under_200ms(self) -> None: from muse.cli.commands.commit import _validate_event_type kinds = ["write", "search", "consolidation", "agent_interaction", "index"] start = time.perf_counter() for i in range(1000): _validate_event_type(kinds[i % len(kinds)]) elapsed = time.perf_counter() - start assert elapsed < 0.2, f"1000 event-type validations took {elapsed * 1000:.0f}ms (limit 200ms)" def test_sensitive_scan_large_safe_dict_under_500ms(self) -> None: """Scanning a 10 000-key safe dict must complete quickly.""" from muse.cli.commands.commit import _has_sensitive_keys big_safe = {f"key_{i}": f"value_{i}" for i in range(10_000)} start = time.perf_counter() result = _has_sensitive_keys(big_safe) elapsed = time.perf_counter() - start assert result is False assert elapsed < 0.5, f"Scan of 10k-key dict took {elapsed * 1000:.0f}ms (limit 500ms)" # ============================================================================= # Tier 7 — Security tests # ============================================================================= class TestMetaSecurity: """Security: adversarial and injection inputs are rejected cleanly.""" def _validate(self, raw: str) -> str: from muse.cli.commands.commit import _validate_meta_payload return _validate_meta_payload(raw) def test_password_key_rejected(self) -> None: with pytest.raises(ValueError): self._validate('{"password": "hunter2"}') def test_api_key_rejected(self) -> None: with pytest.raises(ValueError): self._validate('{"api_key": "sk-abc123"}') def test_api_key_hyphen_rejected(self) -> None: with pytest.raises(ValueError): self._validate('{"api-key": "x"}') def test_token_rejected(self) -> None: with pytest.raises(ValueError): self._validate('{"token": "ghp_..."}') def test_credential_rejected(self) -> None: with pytest.raises(ValueError): self._validate('{"credential": "x"}') def test_authorization_rejected(self) -> None: with pytest.raises(ValueError): self._validate('{"authorization": "Bearer abc"}') def test_bearer_rejected(self) -> None: with pytest.raises(ValueError): self._validate('{"bearer": "abc"}') def test_private_key_rejected(self) -> None: with pytest.raises(ValueError): self._validate('{"private_key": "-----BEGIN RSA"}') def test_private_key_hyphen_rejected(self) -> None: with pytest.raises(ValueError): self._validate('{"private-key": "x"}') def test_nested_secret_at_depth_1_rejected(self) -> None: with pytest.raises(ValueError): self._validate('{"outer": {"password": "x"}}') def test_nested_secret_at_depth_8_rejected(self) -> None: """Depth 8 is still within the scan limit.""" d: Any = {"password": "x"} for _ in range(8): d = {"wrapper": d} with pytest.raises(ValueError): self._validate(json.dumps(d)) def test_secret_at_depth_9_not_scanned(self) -> None: """Depth > 8 is beyond scan limit and must NOT be rejected (mirrors JS).""" d: Any = {"password": "x"} for _ in range(9): d = {"safe_wrapper": d} result = self._validate(json.dumps(d)) assert result # no error raised def test_error_message_does_not_leak_secret_value(self) -> None: """The error message must not echo the secret value back.""" try: self._validate('{"password": "super_secret_value_123"}') except ValueError as exc: assert "super_secret_value_123" not in str(exc) def test_json_injection_via_nested_string_values_accepted(self) -> None: """Nested JSON string values are safe — they're values not keys.""" result = self._validate('{"payload": "{\\"key\\": \\"value\\"}"}') assert result # must not raise def test_oversized_payload_rejected_cleanly(self) -> None: from muse.cli.commands.commit import _MAX_META_BYTES big = {"k": "x" * (_MAX_META_BYTES + 10)} with pytest.raises(ValueError, match="size limit"): self._validate(json.dumps(big)) def test_non_dict_json_types_rejected(self) -> None: for raw in ['["a", "b"]', '"string"', "42", "true", "null"]: with pytest.raises(ValueError): self._validate(raw) def test_control_chars_in_values_accepted_stored_safely(self) -> None: """Control characters in VALUES are not our concern at this layer (they're stored as-is in JSON; the display layer uses sanitize_display). The validator must not raise on control chars in values.""" result = self._validate('{"note": "line\\nbreak"}') assert result # must not raise; \\n in JSON is a valid escape def test_very_long_invalid_kind_raises_cleanly(self) -> None: from muse.cli.commands.commit import _validate_event_type with pytest.raises(ValueError): _validate_event_type("x" * 10_000) def test_event_type_valid_kinds_do_not_expose_secrets(self) -> None: """Valid kind validation must not reference any secret or external resource.""" from muse.cli.commands.commit import _validate_event_type result = _validate_event_type("write") assert result == "write" # pure in-memory check def test_reserved_keys_fail_fast_no_data_divergence(self) -> None: """Reserved top-level keys are a hard error — prevents permanent commit-graph divergence between metadata['event_type'] and json.loads(metadata['meta'])['event_type']. """ with pytest.raises(ValueError, match="reserved top-level key"): self._validate('{"event_type": "search"}') with pytest.raises(ValueError, match="reserved top-level key"): self._validate('{"meta": {}}') def test_nan_infinity_rejected_for_spec_compliance(self) -> None: """NaN / Infinity / -Infinity must be rejected so the v1 stored form is always spec-compliant JSON readable by strict parsers (e.g. the future Rust port and MuseHub UI). """ for raw in ('{"x": NaN}', '{"x": Infinity}', '{"x": -Infinity}'): with pytest.raises(ValueError): self._validate(raw) def test_oversized_raw_pre_parse_cap_bounds_dos(self) -> None: """The pre-parse raw cap bounds json.loads / sensitive-key scan cost on adversarial input, defending future --meta @file or stdin paths. """ from muse.cli.commands.commit import _MAX_META_RAW_BYTES with pytest.raises(ValueError, match="pre-parse cap"): self._validate("{" + '"x"' + ":" + '"' + "y" * (_MAX_META_RAW_BYTES + 100) + '"' + "}")