"""TDD — deploy/repair_commit_signatures.py's pure payload-projection logic. Root cause (muse#108 wire-protocol investigation, continued): the rc10 object-store migration stamped a spurious signer_public_key *and signature* onto historical commit rows without recomputing commit_id (confirmed via wire_repair_commit's own docstring in musehub_wire_push.py). The repair mechanism (wire_repair_commit) already exists and already works correctly (MWP-8 shipped its cache-invalidation); what never happened is actually running it against the full set of affected rows -- only two commits in gabriel/muse were ever manually repaired. This covers the testable core: projecting a `muse read --json` record (which carries envelope fields like muse_version/exit_code/duration_ms mixed in with the real commit data) down to exactly the WireCommit-shaped subset needed for a repair-commit request body. Not covered here: the network/subprocess orchestration wrapper, which is thin plumbing with no branching logic worth a unit test (same convention as backfill_structured_delta.py). """ from __future__ import annotations import pytest from deploy.repair_commit_signatures import project_to_wire_commit_fields from musehub.models.wire import WireCommit _ENVELOPE_KEYS = { "muse_version", "schema", "exit_code", "duration_ms", "timestamp", "warnings", } _SAMPLE_MUSE_READ_RECORD = { "muse_version": "0.2.1rc10", "schema": 1, "exit_code": 0, "duration_ms": 44.66, "timestamp": "2026-10-21T15:49:41.186Z", "warnings": [], "commit_id": "sha256:" + "c" * 64, "branch": "dev", "snapshot_id": "sha256:" + "d" * 64, "message": "fix: something", "committed_at": "2026-05-20T14:43:25.290457+00:00", "parent_commit_id": "sha256:" + "e" * 64, "author": "gabriel", "structured_delta": {"domain": "code", "ops": []}, "sem_ver_bump": "patch", "breaking_changes": [], "agent_id": "claude-code", "model_id": "claude-sonnet-4-6", "toolchain_id": None, "prompt_hash": None, "signature": "ed25519:" + "f" * 86, "signer_public_key": "ed25519:" + "g" * 43, "signer_key_id": "sha256:" + "h" * 64, "status": None, "files_added": [], "files_removed": [], "files_modified": ["musehub/services/musehub_wire_push.py"], "dirs_added": [], "dirs_removed": [], "total_changes": 1, } class TestProjectToWireCommitFields: def test_envelope_keys_stripped(self) -> None: result = project_to_wire_commit_fields(_SAMPLE_MUSE_READ_RECORD) assert not (_ENVELOPE_KEYS & result.keys()) def test_read_only_diff_summary_keys_stripped(self) -> None: result = project_to_wire_commit_fields(_SAMPLE_MUSE_READ_RECORD) for k in ("status", "files_added", "files_removed", "files_modified", "dirs_added", "dirs_removed", "total_changes"): assert k not in result def test_real_fields_preserved_exactly(self) -> None: result = project_to_wire_commit_fields(_SAMPLE_MUSE_READ_RECORD) assert result["commit_id"] == _SAMPLE_MUSE_READ_RECORD["commit_id"] assert result["signature"] == _SAMPLE_MUSE_READ_RECORD["signature"] assert result["signer_public_key"] == _SAMPLE_MUSE_READ_RECORD["signer_public_key"] assert result["structured_delta"] == _SAMPLE_MUSE_READ_RECORD["structured_delta"] assert result["committed_at"] == _SAMPLE_MUSE_READ_RECORD["committed_at"] def test_none_toolchain_and_prompt_hash_coerced_to_empty_string(self) -> None: """muse read reports unset optional provenance fields as null, but WireCommit's schema default is "" -- None would fail validation under a stricter model, so normalize proactively rather than rely on WireCommit's current leniency.""" result = project_to_wire_commit_fields(_SAMPLE_MUSE_READ_RECORD) assert result["toolchain_id"] == "" assert result["prompt_hash"] == "" def test_none_agent_id_and_model_id_coerced_to_empty_string(self) -> None: """Real bug found running the repair batch against staging: human commits report agent_id/model_id as null (not just toolchain_id/ prompt_hash), and WireCommit's validator rejects None outright -- 'Input should be a valid string' -- causing a 422 on every such commit instead of a successful repair.""" record = dict(_SAMPLE_MUSE_READ_RECORD) record["agent_id"] = None record["model_id"] = None result = project_to_wire_commit_fields(record) assert result["agent_id"] == "" assert result["model_id"] == "" # Must actually validate -- this is the exact failure mode seen live. WireCommit(**result) def test_result_validates_as_a_real_wire_commit(self) -> None: """The actual integration contract: the projected dict must be accepted by musehub's own WireCommit model unmodified.""" result = project_to_wire_commit_fields(_SAMPLE_MUSE_READ_RECORD) wire = WireCommit(**result) assert wire.commit_id == _SAMPLE_MUSE_READ_RECORD["commit_id"] assert wire.signature == _SAMPLE_MUSE_READ_RECORD["signature"] def test_missing_parent2_commit_id_defaults_to_none(self) -> None: record = dict(_SAMPLE_MUSE_READ_RECORD) assert "parent2_commit_id" not in record result = project_to_wire_commit_fields(record) wire = WireCommit(**result) assert wire.parent2_commit_id is None def test_merge_commit_parent2_preserved(self) -> None: record = dict(_SAMPLE_MUSE_READ_RECORD) record["parent2_commit_id"] = "sha256:" + "9" * 64 result = project_to_wire_commit_fields(record) assert result["parent2_commit_id"] == "sha256:" + "9" * 64 def test_nullable_string_fields_list_is_exhaustive(self) -> None: """Regression guard for the exact bug this file documents: the null-coercion list must cover every WireCommit field typed as plain `str` (not `str | None`). A future field added to WireCommit without updating this list would reproduce the live 422 failure silently.""" from deploy.repair_commit_signatures import _NULLABLE_STRING_FIELDS plain_str_fields = { name for name, field in WireCommit.model_fields.items() if field.annotation is str } # commit_id is required (no default) and never null by construction # (muse read always returns it) -- not a candidate for this coercion. plain_str_fields.discard("commit_id") assert set(_NULLABLE_STRING_FIELDS) == plain_str_fields