test_repair_commit_signatures.py
python
sha256:195787044debd61f93cc31b92754ee0f78e1b6f66757ee07ea5058ed5ce5197d
fix: dedup fetch.mpack.prebuild against running jobs, not j…
Sonnet 5
minor
⚠ breaking
23 hours ago
| 1 | """TDD — deploy/repair_commit_signatures.py's pure payload-projection logic. |
| 2 | |
| 3 | Root cause (muse#108 wire-protocol investigation, continued): the rc10 |
| 4 | object-store migration stamped a spurious signer_public_key *and signature* |
| 5 | onto historical commit rows without recomputing commit_id (confirmed via |
| 6 | wire_repair_commit's own docstring in musehub_wire_push.py). The repair |
| 7 | mechanism (wire_repair_commit) already exists and already works correctly |
| 8 | (MWP-8 shipped its cache-invalidation); what never happened is actually |
| 9 | running it against the full set of affected rows -- only two commits in |
| 10 | gabriel/muse were ever manually repaired. |
| 11 | |
| 12 | This covers the testable core: projecting a `muse read --json` record (which |
| 13 | carries envelope fields like muse_version/exit_code/duration_ms mixed in with |
| 14 | the real commit data) down to exactly the WireCommit-shaped subset needed for |
| 15 | a repair-commit request body. Not covered here: the network/subprocess |
| 16 | orchestration wrapper, which is thin plumbing with no branching logic worth a |
| 17 | unit test (same convention as backfill_structured_delta.py). |
| 18 | """ |
| 19 | from __future__ import annotations |
| 20 | |
| 21 | import pytest |
| 22 | |
| 23 | from deploy.repair_commit_signatures import project_to_wire_commit_fields |
| 24 | from musehub.models.wire import WireCommit |
| 25 | |
| 26 | _ENVELOPE_KEYS = { |
| 27 | "muse_version", "schema", "exit_code", "duration_ms", "timestamp", "warnings", |
| 28 | } |
| 29 | |
| 30 | _SAMPLE_MUSE_READ_RECORD = { |
| 31 | "muse_version": "0.2.1rc10", |
| 32 | "schema": 1, |
| 33 | "exit_code": 0, |
| 34 | "duration_ms": 44.66, |
| 35 | "timestamp": "2026-10-21T15:49:41.186Z", |
| 36 | "warnings": [], |
| 37 | "commit_id": "sha256:" + "c" * 64, |
| 38 | "branch": "dev", |
| 39 | "snapshot_id": "sha256:" + "d" * 64, |
| 40 | "message": "fix: something", |
| 41 | "committed_at": "2026-05-20T14:43:25.290457+00:00", |
| 42 | "parent_commit_id": "sha256:" + "e" * 64, |
| 43 | "author": "gabriel", |
| 44 | "structured_delta": {"domain": "code", "ops": []}, |
| 45 | "sem_ver_bump": "patch", |
| 46 | "breaking_changes": [], |
| 47 | "agent_id": "claude-code", |
| 48 | "model_id": "claude-sonnet-4-6", |
| 49 | "toolchain_id": None, |
| 50 | "prompt_hash": None, |
| 51 | "signature": "ed25519:" + "f" * 86, |
| 52 | "signer_public_key": "ed25519:" + "g" * 43, |
| 53 | "signer_key_id": "sha256:" + "h" * 64, |
| 54 | "status": None, |
| 55 | "files_added": [], |
| 56 | "files_removed": [], |
| 57 | "files_modified": ["musehub/services/musehub_wire_push.py"], |
| 58 | "dirs_added": [], |
| 59 | "dirs_removed": [], |
| 60 | "total_changes": 1, |
| 61 | } |
| 62 | |
| 63 | |
| 64 | class TestProjectToWireCommitFields: |
| 65 | def test_envelope_keys_stripped(self) -> None: |
| 66 | result = project_to_wire_commit_fields(_SAMPLE_MUSE_READ_RECORD) |
| 67 | assert not (_ENVELOPE_KEYS & result.keys()) |
| 68 | |
| 69 | def test_read_only_diff_summary_keys_stripped(self) -> None: |
| 70 | result = project_to_wire_commit_fields(_SAMPLE_MUSE_READ_RECORD) |
| 71 | for k in ("status", "files_added", "files_removed", "files_modified", |
| 72 | "dirs_added", "dirs_removed", "total_changes"): |
| 73 | assert k not in result |
| 74 | |
| 75 | def test_real_fields_preserved_exactly(self) -> None: |
| 76 | result = project_to_wire_commit_fields(_SAMPLE_MUSE_READ_RECORD) |
| 77 | assert result["commit_id"] == _SAMPLE_MUSE_READ_RECORD["commit_id"] |
| 78 | assert result["signature"] == _SAMPLE_MUSE_READ_RECORD["signature"] |
| 79 | assert result["signer_public_key"] == _SAMPLE_MUSE_READ_RECORD["signer_public_key"] |
| 80 | assert result["structured_delta"] == _SAMPLE_MUSE_READ_RECORD["structured_delta"] |
| 81 | assert result["committed_at"] == _SAMPLE_MUSE_READ_RECORD["committed_at"] |
| 82 | |
| 83 | def test_none_toolchain_and_prompt_hash_coerced_to_empty_string(self) -> None: |
| 84 | """muse read reports unset optional provenance fields as null, but |
| 85 | WireCommit's schema default is "" -- None would fail validation |
| 86 | under a stricter model, so normalize proactively rather than rely |
| 87 | on WireCommit's current leniency.""" |
| 88 | result = project_to_wire_commit_fields(_SAMPLE_MUSE_READ_RECORD) |
| 89 | assert result["toolchain_id"] == "" |
| 90 | assert result["prompt_hash"] == "" |
| 91 | |
| 92 | def test_none_agent_id_and_model_id_coerced_to_empty_string(self) -> None: |
| 93 | """Real bug found running the repair batch against staging: human |
| 94 | commits report agent_id/model_id as null (not just toolchain_id/ |
| 95 | prompt_hash), and WireCommit's validator rejects None outright -- |
| 96 | 'Input should be a valid string' -- causing a 422 on every such |
| 97 | commit instead of a successful repair.""" |
| 98 | record = dict(_SAMPLE_MUSE_READ_RECORD) |
| 99 | record["agent_id"] = None |
| 100 | record["model_id"] = None |
| 101 | result = project_to_wire_commit_fields(record) |
| 102 | assert result["agent_id"] == "" |
| 103 | assert result["model_id"] == "" |
| 104 | # Must actually validate -- this is the exact failure mode seen live. |
| 105 | WireCommit(**result) |
| 106 | |
| 107 | def test_result_validates_as_a_real_wire_commit(self) -> None: |
| 108 | """The actual integration contract: the projected dict must be |
| 109 | accepted by musehub's own WireCommit model unmodified.""" |
| 110 | result = project_to_wire_commit_fields(_SAMPLE_MUSE_READ_RECORD) |
| 111 | wire = WireCommit(**result) |
| 112 | assert wire.commit_id == _SAMPLE_MUSE_READ_RECORD["commit_id"] |
| 113 | assert wire.signature == _SAMPLE_MUSE_READ_RECORD["signature"] |
| 114 | |
| 115 | def test_missing_parent2_commit_id_defaults_to_none(self) -> None: |
| 116 | record = dict(_SAMPLE_MUSE_READ_RECORD) |
| 117 | assert "parent2_commit_id" not in record |
| 118 | result = project_to_wire_commit_fields(record) |
| 119 | wire = WireCommit(**result) |
| 120 | assert wire.parent2_commit_id is None |
| 121 | |
| 122 | def test_merge_commit_parent2_preserved(self) -> None: |
| 123 | record = dict(_SAMPLE_MUSE_READ_RECORD) |
| 124 | record["parent2_commit_id"] = "sha256:" + "9" * 64 |
| 125 | result = project_to_wire_commit_fields(record) |
| 126 | assert result["parent2_commit_id"] == "sha256:" + "9" * 64 |
| 127 | |
| 128 | def test_nullable_string_fields_list_is_exhaustive(self) -> None: |
| 129 | """Regression guard for the exact bug this file documents: the |
| 130 | null-coercion list must cover every WireCommit field typed as plain |
| 131 | `str` (not `str | None`). A future field added to WireCommit without |
| 132 | updating this list would reproduce the live 422 failure silently.""" |
| 133 | from deploy.repair_commit_signatures import _NULLABLE_STRING_FIELDS |
| 134 | |
| 135 | plain_str_fields = { |
| 136 | name for name, field in WireCommit.model_fields.items() |
| 137 | if field.annotation is str |
| 138 | } |
| 139 | # commit_id is required (no default) and never null by construction |
| 140 | # (muse read always returns it) -- not a candidate for this coercion. |
| 141 | plain_str_fields.discard("commit_id") |
| 142 | assert set(_NULLABLE_STRING_FIELDS) == plain_str_fields |
File History
1 commit
sha256:195787044debd61f93cc31b92754ee0f78e1b6f66757ee07ea5058ed5ce5197d
fix: dedup fetch.mpack.prebuild against running jobs, not j…
Sonnet 5
minor
⚠
23 hours ago