gabriel / muse public
test_push_force_flag.py python
270 lines 9.5 KB
Raw
sha256:79ffe87f5fe2ec146e35f05521218bbf54dffdb0440c07f970bad05f16efb89f chore: merge main — carry all urllib/typing/test fixes from dev Sonnet 4.6 minor ⚠ breaking 20 days ago
1 """TDD — --force flag must appear in the unpack-mpack request body.
2
3 Gap 2: _run_mpack_path() never includes "force" in the unpack body. The
4 server receives the unpack request with no force field and has no way to
5 distinguish a force push from a normal push.
6
7 Test plan
8 ---------
9 F1 (RED) _push_mpack with force=True sends {"force": True} in the
10 unpack-mpack POST body.
11 F2 (RED) _push_mpack with force=False sends {"force": False} (or omits it
12 and server defaults to False).
13 F3 _push_mpack without --force does NOT set force=True by accident.
14 F4 The "force" key is present in the unpack body regardless of the
15 force value (protocol stability).
16 """
17 from __future__ import annotations
18
19 import asyncio
20 import datetime
21 import json
22 import pathlib
23 from unittest.mock import MagicMock, patch
24
25 import msgpack
26 import pytest
27
28 from muse._version import __version__
29 from muse.core.mpack import PushResult, RemoteInfo
30 from muse.core.object_store import write_object
31 from muse.core.paths import heads_dir, muse_dir
32
33 _Headers = dict[str, str] # HTTP header map
34 _JsonDict = dict[str, str | int | float | bool | None | list[str]] # JSON object
35 from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id
36 from muse.core.store import (
37 CommitRecord,
38 SnapshotRecord,
39 write_commit,
40 write_snapshot,
41 )
42 from muse.core.types import Manifest, blob_id
43
44
45 # ---------------------------------------------------------------------------
46 # Helpers
47 # ---------------------------------------------------------------------------
48
49 def _bare_repo(tmp_path: pathlib.Path) -> pathlib.Path:
50 muse = muse_dir(tmp_path)
51 for d in ("commits", "snapshots", "objects", "refs/heads", "remotes"):
52 (muse / d).mkdir(parents=True, exist_ok=True)
53 (muse / "HEAD").write_text("ref: refs/heads/main\n")
54 (muse / "repo.json").write_text(
55 json.dumps({"repo_id": "test-repo", "schema_version": __version__, "domain": "code"})
56 )
57 (muse / "config.toml").write_text('[remotes.origin]\nurl = "https://hub.example.com/r"\n')
58 return tmp_path
59
60
61 def _make_commit(
62 root: pathlib.Path,
63 label: str,
64 parent_id: str | None = None,
65 content: bytes | None = None,
66 ) -> CommitRecord:
67 raw = content if content is not None else f"content-{label}".encode()
68 oid = blob_id(raw)
69 write_object(root, oid, raw)
70 manifest: Manifest = {"file.txt": oid}
71 snap_id = compute_snapshot_id(manifest)
72 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
73 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
74 parent_ids = [parent_id] if parent_id else []
75 cid = compute_commit_id(
76 parent_ids=parent_ids,
77 snapshot_id=snap_id,
78 message=f"commit {label}",
79 committed_at_iso=committed_at.isoformat(),
80 )
81 write_commit(root, CommitRecord(
82 commit_id=cid,
83 branch="main",
84 snapshot_id=snap_id,
85 message=f"commit {label}",
86 committed_at=committed_at,
87 parent_commit_id=parent_id,
88 ))
89 return CommitRecord(
90 commit_id=cid,
91 branch="main",
92 snapshot_id=snap_id,
93 message=f"commit {label}",
94 committed_at=committed_at,
95 parent_commit_id=parent_id,
96 )
97
98
99 def _fake_resp(body: bytes, status: int = 200) -> MagicMock:
100 r = MagicMock()
101 r.status_code = status
102 r.content = body
103 r.headers = {"content-type": "application/x-msgpack"}
104 r.text = ""
105 return r
106
107
108 class _FakeHttpxClient:
109 """Captures all HTTP requests made by _run_mpack_path."""
110
111 def __init__(self, upload_url: str = "https://minio.example.com/put?sig=x") -> None:
112 self.posts: list[tuple[str, bytes]] = [] # (url, body)
113 self.puts: list[tuple[str, bytes]] = [] # (url, body)
114 self._upload_url = upload_url
115
116 async def __aenter__(self) -> "_FakeHttpxClient":
117 return self
118
119 async def __aexit__(self, *_: object) -> None:
120 pass
121
122 async def post(self, url: str, *, content: bytes, headers: _Headers) -> MagicMock:
123 self.posts.append((url, content))
124 if "mpack-presign" in url:
125 return _fake_resp(msgpack.packb(
126 {"upload_url": self._upload_url},
127 use_bin_type=True,
128 ))
129 # unpack-mpack
130 return _fake_resp(msgpack.packb(
131 {"job_id": "job-test", "head": "", "branch": "main",
132 "objects_in_mpack": 0, "commits_in_mpack": 0},
133 use_bin_type=True,
134 ))
135
136 async def put(self, url: str, *, content: bytes) -> MagicMock:
137 self.puts.append((url, content))
138 return _fake_resp(b"", 200)
139
140
141 def _run_push_mpack(
142 root: pathlib.Path,
143 local_head: str,
144 remote_head: str | None,
145 force: bool,
146 ) -> "_FakeHttpxClient":
147 """Run _push_mpack with a fake transport and capture the HTTP requests."""
148 from muse.cli.commands.push import _push_mpack
149
150 transport = MagicMock()
151 transport.fetch_remote_info.return_value = RemoteInfo(
152 domain="code",
153 default_branch="main",
154 branch_heads={"main": remote_head} if remote_head else {},
155 )
156 # _build_request is called to build auth headers — return a minimal mock
157 mock_req = MagicMock()
158 mock_req.headers = {"Authorization": "MSign stub", "Content-Type": "application/x-msgpack"}
159 transport._build_request.return_value = mock_req
160
161 fake_client = _FakeHttpxClient()
162
163 with (
164 patch("muse.cli.commands.push._httpx.AsyncClient", return_value=fake_client),
165 patch("muse.cli.commands.push._make_r2_client", return_value=fake_client),
166 ):
167 _push_mpack(
168 transport,
169 "https://hub.example.com/repos/r1",
170 None,
171 root,
172 local_head,
173 [],
174 "main",
175 force,
176 )
177
178 return fake_client
179
180
181 def _unpack_body(body: bytes) -> _JsonDict:
182 """Decode the unpack-mpack POST body."""
183 import json
184 return json.loads(body)
185
186
187 # ---------------------------------------------------------------------------
188 # F1 — force=True appears in unpack body (currently RED)
189 # ---------------------------------------------------------------------------
190
191 def test_f1_force_true_sent_in_unpack_body(
192 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
193 ) -> None:
194 """_push_mpack(force=True) must send {"force": True} in the unpack body.
195
196 This is currently RED: _run_mpack_path does not include "force" in the
197 unpack payload.
198 """
199 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
200 monkeypatch.chdir(tmp_path)
201 root = _bare_repo(tmp_path)
202 commit = _make_commit(root, "f1", content=b"force flag test")
203 (heads_dir(root) / "main").write_text(commit.commit_id)
204
205 fake = _run_push_mpack(root, commit.commit_id, remote_head=None, force=True)
206
207 unpack_posts = [(url, body) for url, body in fake.posts if "unpack-mpack" in url]
208 assert unpack_posts, "No POST to unpack-mpack was made"
209
210 body = _unpack_body(unpack_posts[0][1])
211 assert "force" in body, (
212 f"'force' key missing from unpack body. Got keys: {list(body.keys())}. "
213 "Fix: add 'force': force to the unpack_body dict in _run_mpack_path."
214 )
215 assert body["force"] is True, (
216 f"Expected force=True in unpack body, got {body['force']!r}"
217 )
218
219
220 # ---------------------------------------------------------------------------
221 # F2 — force=False is also sent explicitly (currently RED)
222 # ---------------------------------------------------------------------------
223
224 def test_f2_force_false_sent_in_unpack_body(
225 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
226 ) -> None:
227 """_push_mpack(force=False) must send {"force": False} in the unpack body."""
228 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
229 monkeypatch.chdir(tmp_path)
230 root = _bare_repo(tmp_path)
231 commit = _make_commit(root, "f2", content=b"no force test")
232 (heads_dir(root) / "main").write_text(commit.commit_id)
233
234 fake = _run_push_mpack(root, commit.commit_id, remote_head=None, force=False)
235
236 unpack_posts = [(url, body) for url, body in fake.posts if "unpack-mpack" in url]
237 assert unpack_posts, "No POST to unpack-mpack was made"
238
239 body = _unpack_body(unpack_posts[0][1])
240 assert "force" in body, (
241 f"'force' key missing from unpack body. Got keys: {list(body.keys())}."
242 )
243 assert body["force"] is False, (
244 f"Expected force=False in unpack body, got {body['force']!r}"
245 )
246
247
248 # ---------------------------------------------------------------------------
249 # F3 — "force" key is always present regardless of value
250 # ---------------------------------------------------------------------------
251
252 def test_f3_force_key_present_in_all_pushes(
253 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
254 ) -> None:
255 """The 'force' key must always be in the unpack body (protocol stability)."""
256 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
257 monkeypatch.chdir(tmp_path)
258 root = _bare_repo(tmp_path)
259 commit = _make_commit(root, "f3", content=b"always force key")
260 (heads_dir(root) / "main").write_text(commit.commit_id)
261
262 for force_value in (True, False):
263 fake = _run_push_mpack(root, commit.commit_id, remote_head=None, force=force_value)
264 unpack_posts = [(url, body) for url, body in fake.posts if "unpack-mpack" in url]
265 assert unpack_posts
266 body = _unpack_body(unpack_posts[0][1])
267 assert "force" in body, f"force={force_value}: 'force' missing from unpack body"
268 assert isinstance(body["force"], bool), (
269 f"force={force_value}: body['force'] must be bool, got {type(body['force'])}"
270 )
File History 2 commits
sha256:79ffe87f5fe2ec146e35f05521218bbf54dffdb0440c07f970bad05f16efb89f chore: merge main — carry all urllib/typing/test fixes from dev Sonnet 4.6 minor 20 days ago
sha256:0bea7600d1eee83e87950be49933b1006fa9dc2c71e7c4ee748d324f61138156 chore: bump version to 0.2.0rc11; fix typing audit violatio… Sonnet 4.6 minor 20 days ago