gabriel / musehub public
test_repair_commit_signatures.py python
142 lines 6.5 KB
Raw
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