test_update_ref_supercharge.py
python
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d
feat(pack): delta-encode snapshots in MPackBundle wire format
Sonnet 4.6
minor
⚠ breaking
121 days ago
| 1 | """SUPERCHARGE tests for ``muse update-ref``. |
| 2 | |
| 3 | Coverage tiers |
| 4 | -------------- |
| 5 | - U (Unit): duration_ms / exit_code in every JSON success path |
| 6 | - E (Error): JSON errors → stdout when --json; stderr in text mode |
| 7 | - S (Schema): error payload has {error, message, duration_ms, exit_code} |
| 8 | - D (Data): exit_code semantics, previous/deleted field accuracy |
| 9 | - CAS: compare-and-swap timing and error payloads |
| 10 | - P (Perf): duration_ms stays under a sane ceiling |
| 11 | - Sec (Security): no traceback on any error path; path traversal rejected |
| 12 | - C (Concurrency): independent branches updated safely in parallel threads |
| 13 | |
| 14 | Utilities used |
| 15 | -------------- |
| 16 | - ``long_id(hex)`` — ``sha256:<64-hex>`` from a bare hex string |
| 17 | - ``short_id(id)`` — ``sha256:<12-hex>`` abbreviated form |
| 18 | - ``blob_id(data)`` — ``sha256:<hex>`` of arbitrary bytes (unique IDs) |
| 19 | """ |
| 20 | from __future__ import annotations |
| 21 | from collections.abc import Mapping |
| 22 | |
| 23 | import datetime |
| 24 | import json |
| 25 | import pathlib |
| 26 | import threading |
| 27 | from unittest import mock |
| 28 | |
| 29 | import pytest |
| 30 | |
| 31 | from muse.core.errors import ExitCode |
| 32 | from muse.core.paths import muse_dir, ref_path |
| 33 | from muse.core.snapshot import compute_commit_id, compute_snapshot_id |
| 34 | from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot |
| 35 | from muse.core.types import blob_id, long_id, short_id |
| 36 | from tests.cli_test_helper import CliRunner, InvokeResult |
| 37 | |
| 38 | runner = CliRunner() |
| 39 | |
| 40 | _SNAP_ID: str = compute_snapshot_id({}) |
| 41 | _COMMITTED_AT: datetime.datetime = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc) |
| 42 | |
| 43 | |
| 44 | # --------------------------------------------------------------------------- |
| 45 | # Helpers |
| 46 | # --------------------------------------------------------------------------- |
| 47 | |
| 48 | |
| 49 | def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: |
| 50 | repo = tmp_path / "repo" |
| 51 | dot_muse = muse_dir(repo) |
| 52 | for sub in ("objects", "commits", "snapshots", "refs/heads"): |
| 53 | (dot_muse / sub).mkdir(parents=True) |
| 54 | (dot_muse / "HEAD").write_text("ref: refs/heads/main") |
| 55 | (dot_muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": "code"})) |
| 56 | return repo |
| 57 | |
| 58 | |
| 59 | def _snap(repo: pathlib.Path) -> str: |
| 60 | write_snapshot(repo, SnapshotRecord( |
| 61 | snapshot_id=_SNAP_ID, |
| 62 | manifest={}, |
| 63 | created_at=_COMMITTED_AT, |
| 64 | )) |
| 65 | return _SNAP_ID |
| 66 | |
| 67 | |
| 68 | def _commit(repo: pathlib.Path, message: str = "test") -> str: |
| 69 | snap_id = _snap(repo) |
| 70 | commit_id = compute_commit_id( |
| 71 | parent_ids=[], |
| 72 | snapshot_id=snap_id, |
| 73 | message=message, |
| 74 | committed_at_iso=_COMMITTED_AT.isoformat(), |
| 75 | ) |
| 76 | write_commit(repo, CommitRecord( |
| 77 | commit_id=commit_id, |
| 78 | repo_id="test-repo", |
| 79 | branch="main", |
| 80 | snapshot_id=snap_id, |
| 81 | message=message, |
| 82 | committed_at=_COMMITTED_AT, |
| 83 | )) |
| 84 | return commit_id |
| 85 | |
| 86 | |
| 87 | def _write_ref(repo: pathlib.Path, branch: str, commit_id: str) -> None: |
| 88 | branch_ref = ref_path(repo, branch) |
| 89 | branch_ref.parent.mkdir(parents=True, exist_ok=True) |
| 90 | branch_ref.write_text(commit_id) |
| 91 | |
| 92 | |
| 93 | def _ur(repo: pathlib.Path, *args: str) -> InvokeResult: |
| 94 | from muse.cli.app import main as cli |
| 95 | return runner.invoke( |
| 96 | cli, |
| 97 | ["update-ref", *args], |
| 98 | env={"MUSE_REPO_ROOT": str(repo)}, |
| 99 | ) |
| 100 | |
| 101 | |
| 102 | # --------------------------------------------------------------------------- |
| 103 | # U — Unit: duration_ms and exit_code in every success JSON path |
| 104 | # --------------------------------------------------------------------------- |
| 105 | |
| 106 | |
| 107 | class TestElapsedMsExitCode: |
| 108 | """U1–U8: every successful JSON response carries duration_ms and exit_code.""" |
| 109 | |
| 110 | def test_u1_create_ref_has_duration_ms(self, tmp_path: pathlib.Path) -> None: |
| 111 | """U1: creating a new ref emits duration_ms.""" |
| 112 | repo = _make_repo(tmp_path) |
| 113 | cid = _commit(repo) |
| 114 | r = _ur(repo, "feat/alpha", cid, "--json") |
| 115 | assert r.exit_code == 0 |
| 116 | data = json.loads(r.output) |
| 117 | assert "duration_ms" in data, "duration_ms missing from create-ref JSON" |
| 118 | |
| 119 | def test_u2_create_ref_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None: |
| 120 | """U2: creating a new ref has exit_code 0.""" |
| 121 | repo = _make_repo(tmp_path) |
| 122 | cid = _commit(repo) |
| 123 | r = _ur(repo, "feat/beta", cid, "--json") |
| 124 | assert r.exit_code == 0 |
| 125 | data = json.loads(r.output) |
| 126 | assert data["exit_code"] == 0 |
| 127 | |
| 128 | def test_u3_update_ref_has_duration_ms(self, tmp_path: pathlib.Path) -> None: |
| 129 | """U3: updating an existing ref emits duration_ms.""" |
| 130 | repo = _make_repo(tmp_path) |
| 131 | old = _commit(repo, "old") |
| 132 | new = _commit(repo, "new") |
| 133 | _write_ref(repo, "main", old) |
| 134 | r = _ur(repo, "main", new, "--json") |
| 135 | assert r.exit_code == 0 |
| 136 | data = json.loads(r.output) |
| 137 | assert "duration_ms" in data |
| 138 | |
| 139 | def test_u4_update_ref_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None: |
| 140 | """U4: updating an existing ref has exit_code 0.""" |
| 141 | repo = _make_repo(tmp_path) |
| 142 | old = _commit(repo, "old") |
| 143 | new = _commit(repo, "new") |
| 144 | _write_ref(repo, "main", old) |
| 145 | r = _ur(repo, "main", new, "--json") |
| 146 | assert r.exit_code == 0 |
| 147 | assert json.loads(r.output)["exit_code"] == 0 |
| 148 | |
| 149 | def test_u5_delete_ref_has_duration_ms(self, tmp_path: pathlib.Path) -> None: |
| 150 | """U5: deleting a ref emits duration_ms.""" |
| 151 | repo = _make_repo(tmp_path) |
| 152 | cid = long_id("a" * 64) |
| 153 | _write_ref(repo, "to-del", cid) |
| 154 | r = _ur(repo, "--delete", "to-del", "--json") |
| 155 | assert r.exit_code == 0 |
| 156 | data = json.loads(r.output) |
| 157 | assert "duration_ms" in data, "duration_ms missing from delete-ref JSON" |
| 158 | |
| 159 | def test_u6_delete_ref_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None: |
| 160 | """U6: deleting a ref has exit_code 0.""" |
| 161 | repo = _make_repo(tmp_path) |
| 162 | cid = long_id("b" * 64) |
| 163 | _write_ref(repo, "to-del2", cid) |
| 164 | r = _ur(repo, "--delete", "to-del2", "--json") |
| 165 | assert r.exit_code == 0 |
| 166 | assert json.loads(r.output)["exit_code"] == 0 |
| 167 | |
| 168 | def test_u7_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None: |
| 169 | """U7: duration_ms is a float (not int, not string).""" |
| 170 | repo = _make_repo(tmp_path) |
| 171 | cid = _commit(repo) |
| 172 | r = _ur(repo, "timing-branch", cid, "--json") |
| 173 | assert r.exit_code == 0 |
| 174 | val = json.loads(r.output)["duration_ms"] |
| 175 | assert isinstance(val, float), f"expected float, got {type(val)}" |
| 176 | |
| 177 | def test_u8_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None: |
| 178 | """U8: duration_ms >= 0.""" |
| 179 | repo = _make_repo(tmp_path) |
| 180 | cid = _commit(repo) |
| 181 | r = _ur(repo, "timing-branch2", cid, "--json") |
| 182 | assert r.exit_code == 0 |
| 183 | assert json.loads(r.output)["duration_ms"] >= 0.0 |
| 184 | |
| 185 | def test_u9_no_verify_success_has_duration_ms(self, tmp_path: pathlib.Path) -> None: |
| 186 | """U9: --no-verify path also emits duration_ms.""" |
| 187 | repo = _make_repo(tmp_path) |
| 188 | cid = long_id("f" * 64) # not in store — valid format, skip verification |
| 189 | r = _ur(repo, "--no-verify", "staging", cid, "--json") |
| 190 | assert r.exit_code == 0 |
| 191 | data = json.loads(r.output) |
| 192 | assert "duration_ms" in data |
| 193 | |
| 194 | def test_u10_short_id_in_output_is_not_expected(self, tmp_path: pathlib.Path) -> None: |
| 195 | """U10: commit_id in JSON output is the full long_id (not short_id).""" |
| 196 | repo = _make_repo(tmp_path) |
| 197 | cid = _commit(repo) |
| 198 | r = _ur(repo, "full-id-branch", cid, "--json") |
| 199 | assert r.exit_code == 0 |
| 200 | data = json.loads(r.output) |
| 201 | # Full id must be present — short_id (12 hex chars) would be truncated |
| 202 | assert data["commit_id"] == cid |
| 203 | assert len(data["commit_id"]) == 71 # sha256: + 64 hex |
| 204 | |
| 205 | |
| 206 | # --------------------------------------------------------------------------- |
| 207 | # E — Error routing: JSON errors → stdout when --json, stderr in text mode |
| 208 | # --------------------------------------------------------------------------- |
| 209 | |
| 210 | |
| 211 | class TestJsonErrorsToStdout: |
| 212 | """E1–E7: all error paths emit JSON to stdout when --json is active.""" |
| 213 | |
| 214 | def test_e1_invalid_branch_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None: |
| 215 | """E1: invalid branch name → JSON error on stdout, stderr empty.""" |
| 216 | repo = _make_repo(tmp_path) |
| 217 | r = _ur(repo, "branch\x00null", long_id("a" * 64), "--json") |
| 218 | assert r.exit_code != 0 |
| 219 | assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}" |
| 220 | data = json.loads(r.output) |
| 221 | assert "error" in data |
| 222 | |
| 223 | def test_e2_commit_not_found_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None: |
| 224 | """E2: commit not in store → JSON error on stdout, stderr empty.""" |
| 225 | repo = _make_repo(tmp_path) |
| 226 | cid = long_id("9" * 64) # not in store, valid format |
| 227 | r = _ur(repo, "main", cid, "--json") |
| 228 | assert r.exit_code != 0 |
| 229 | assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}" |
| 230 | data = json.loads(r.output) |
| 231 | assert "error" in data |
| 232 | |
| 233 | def test_e3_invalid_commit_id_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None: |
| 234 | """E3: malformed commit ID → JSON error on stdout, stderr empty.""" |
| 235 | repo = _make_repo(tmp_path) |
| 236 | r = _ur(repo, "main", "not-a-valid-id", "--json") |
| 237 | assert r.exit_code != 0 |
| 238 | assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}" |
| 239 | data = json.loads(r.output) |
| 240 | assert "error" in data |
| 241 | |
| 242 | def test_e4_delete_nonexistent_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None: |
| 243 | """E4: --delete on nonexistent ref → JSON error on stdout.""" |
| 244 | repo = _make_repo(tmp_path) |
| 245 | r = _ur(repo, "--delete", "ghost-branch", "--json") |
| 246 | assert r.exit_code != 0 |
| 247 | assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}" |
| 248 | data = json.loads(r.output) |
| 249 | assert "error" in data |
| 250 | |
| 251 | def test_e5_cas_mismatch_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None: |
| 252 | """E5: CAS mismatch → JSON error on stdout with current/expected fields.""" |
| 253 | repo = _make_repo(tmp_path) |
| 254 | actual = _commit(repo, "actual") |
| 255 | new_id = _commit(repo, "new") |
| 256 | other = blob_id(b"other-id") # valid ID, not the actual value |
| 257 | _write_ref(repo, "main", actual) |
| 258 | r = _ur(repo, "--old-value", other, "main", new_id, "--json") |
| 259 | assert r.exit_code != 0 |
| 260 | assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}" |
| 261 | data = json.loads(r.output) |
| 262 | assert "error" in data |
| 263 | |
| 264 | def test_e6_no_commit_id_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None: |
| 265 | """E6: missing commit_id (no --delete) → JSON error on stdout.""" |
| 266 | repo = _make_repo(tmp_path) |
| 267 | r = _ur(repo, "main", "--json") |
| 268 | assert r.exit_code != 0 |
| 269 | assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}" |
| 270 | data = json.loads(r.output) |
| 271 | assert "error" in data |
| 272 | |
| 273 | def test_e7_text_mode_errors_on_stderr(self, tmp_path: pathlib.Path) -> None: |
| 274 | """E7: text mode errors go to stderr (stdout_bytes empty).""" |
| 275 | repo = _make_repo(tmp_path) |
| 276 | r = _ur(repo, "branch\x00bad", long_id("a" * 64), "--format", "text") |
| 277 | assert r.exit_code != 0 |
| 278 | assert r.stdout_bytes == b"", f"stdout_bytes should be empty in text mode: {r.stdout_bytes!r}" |
| 279 | assert r.stderr.strip() != "", "stderr should have error text in text mode" |
| 280 | |
| 281 | def test_e8_write_failure_json_error_on_stdout(self, tmp_path: pathlib.Path) -> None: |
| 282 | """E8: OSError from write_branch_ref → exit 3, JSON error on stdout.""" |
| 283 | repo = _make_repo(tmp_path) |
| 284 | cid = _commit(repo) |
| 285 | with mock.patch( |
| 286 | "muse.cli.commands.update_ref.write_branch_ref", |
| 287 | side_effect=OSError("disk full"), |
| 288 | ): |
| 289 | r = _ur(repo, "main", cid, "--json") |
| 290 | assert r.exit_code == ExitCode.INTERNAL_ERROR |
| 291 | assert r.stderr.strip() == "", f"stderr should be empty: {r.stderr!r}" |
| 292 | data = json.loads(r.output) |
| 293 | assert "error" in data |
| 294 | assert "disk full" in data.get("message", "") |
| 295 | |
| 296 | |
| 297 | # --------------------------------------------------------------------------- |
| 298 | # S — Schema completeness for error payloads |
| 299 | # --------------------------------------------------------------------------- |
| 300 | |
| 301 | |
| 302 | class TestErrorJsonSchema: |
| 303 | """S1–S5: every JSON error payload has {error, message, duration_ms, exit_code}.""" |
| 304 | |
| 305 | def _parse_error(self, r: InvokeResult) -> Mapping[str, object]: |
| 306 | return json.loads(r.output) |
| 307 | |
| 308 | def _required_keys(self) -> set[str]: |
| 309 | return {"error", "message", "duration_ms", "exit_code"} |
| 310 | |
| 311 | def test_s1_invalid_branch_error_schema(self, tmp_path: pathlib.Path) -> None: |
| 312 | """S1: invalid branch name error has all required keys.""" |
| 313 | repo = _make_repo(tmp_path) |
| 314 | r = _ur(repo, "bad\x00branch", long_id("a" * 64), "--json") |
| 315 | data = self._parse_error(r) |
| 316 | missing = self._required_keys() - data.keys() |
| 317 | assert not missing, f"missing keys: {missing}" |
| 318 | |
| 319 | def test_s2_commit_not_found_error_schema(self, tmp_path: pathlib.Path) -> None: |
| 320 | """S2: commit-not-found error has all required keys.""" |
| 321 | repo = _make_repo(tmp_path) |
| 322 | r = _ur(repo, "main", long_id("e" * 64), "--json") |
| 323 | data = self._parse_error(r) |
| 324 | missing = self._required_keys() - data.keys() |
| 325 | assert not missing, f"missing keys: {missing}" |
| 326 | |
| 327 | def test_s3_cas_mismatch_error_schema(self, tmp_path: pathlib.Path) -> None: |
| 328 | """S3: CAS mismatch error has all required keys.""" |
| 329 | repo = _make_repo(tmp_path) |
| 330 | actual = _commit(repo, "actual") |
| 331 | new_id = _commit(repo, "new") |
| 332 | wrong = blob_id(b"wrong-old-value") |
| 333 | _write_ref(repo, "main", actual) |
| 334 | r = _ur(repo, "--old-value", wrong, "main", new_id, "--json") |
| 335 | data = self._parse_error(r) |
| 336 | missing = self._required_keys() - data.keys() |
| 337 | assert not missing, f"missing keys: {missing}" |
| 338 | |
| 339 | def test_s4_write_failure_error_schema(self, tmp_path: pathlib.Path) -> None: |
| 340 | """S4: write-failure error has all required keys.""" |
| 341 | repo = _make_repo(tmp_path) |
| 342 | cid = _commit(repo) |
| 343 | with mock.patch( |
| 344 | "muse.cli.commands.update_ref.write_branch_ref", |
| 345 | side_effect=OSError("ENOSPC"), |
| 346 | ): |
| 347 | r = _ur(repo, "main", cid, "--json") |
| 348 | data = self._parse_error(r) |
| 349 | missing = self._required_keys() - data.keys() |
| 350 | assert not missing, f"missing keys: {missing}" |
| 351 | |
| 352 | def test_s5_error_duration_ms_is_float_non_negative(self, tmp_path: pathlib.Path) -> None: |
| 353 | """S5: duration_ms in error JSON is a float >= 0.""" |
| 354 | repo = _make_repo(tmp_path) |
| 355 | r = _ur(repo, "bad\x00name", long_id("a" * 64), "--json") |
| 356 | data = self._parse_error(r) |
| 357 | assert isinstance(data["duration_ms"], float) |
| 358 | assert data["duration_ms"] >= 0.0 |
| 359 | |
| 360 | def test_s6_error_exit_code_matches_process_exit(self, tmp_path: pathlib.Path) -> None: |
| 361 | """S6: exit_code in JSON matches actual process exit code.""" |
| 362 | repo = _make_repo(tmp_path) |
| 363 | r = _ur(repo, "bad\x00name", long_id("a" * 64), "--json") |
| 364 | data = self._parse_error(r) |
| 365 | assert data["exit_code"] == r.exit_code |
| 366 | |
| 367 | def test_s7_success_json_all_fields_present(self, tmp_path: pathlib.Path) -> None: |
| 368 | """S7: create-ref success JSON has branch, commit_id, previous, duration_ms, exit_code.""" |
| 369 | repo = _make_repo(tmp_path) |
| 370 | cid = _commit(repo) |
| 371 | r = _ur(repo, "schema-check", cid, "--json") |
| 372 | assert r.exit_code == 0 |
| 373 | data = json.loads(r.output) |
| 374 | for key in ("branch", "commit_id", "previous", "duration_ms", "exit_code"): |
| 375 | assert key in data, f"missing key {key!r} in success JSON" |
| 376 | |
| 377 | def test_s8_delete_json_all_fields_present(self, tmp_path: pathlib.Path) -> None: |
| 378 | """S8: delete-ref success JSON has branch, deleted, duration_ms, exit_code.""" |
| 379 | repo = _make_repo(tmp_path) |
| 380 | cid = long_id("d" * 64) |
| 381 | _write_ref(repo, "del-schema", cid) |
| 382 | r = _ur(repo, "--delete", "del-schema", "--json") |
| 383 | assert r.exit_code == 0 |
| 384 | data = json.loads(r.output) |
| 385 | for key in ("branch", "deleted", "duration_ms", "exit_code"): |
| 386 | assert key in data, f"missing key {key!r} in delete JSON" |
| 387 | |
| 388 | |
| 389 | # --------------------------------------------------------------------------- |
| 390 | # D — Data integrity |
| 391 | # --------------------------------------------------------------------------- |
| 392 | |
| 393 | |
| 394 | class TestDataIntegrity: |
| 395 | """D1–D8: output values are semantically correct.""" |
| 396 | |
| 397 | def test_d1_previous_is_none_for_new_ref(self, tmp_path: pathlib.Path) -> None: |
| 398 | """D1: previous is null when no ref existed before.""" |
| 399 | repo = _make_repo(tmp_path) |
| 400 | cid = _commit(repo) |
| 401 | data = json.loads(_ur(repo, "fresh-branch", cid, "--json").output) |
| 402 | assert data["previous"] is None |
| 403 | |
| 404 | def test_d2_previous_matches_old_commit(self, tmp_path: pathlib.Path) -> None: |
| 405 | """D2: previous matches the commit_id that was there before.""" |
| 406 | repo = _make_repo(tmp_path) |
| 407 | old = _commit(repo, "old commit") |
| 408 | new = _commit(repo, "new commit") |
| 409 | _write_ref(repo, "main", old) |
| 410 | data = json.loads(_ur(repo, "main", new, "--json").output) |
| 411 | assert data["previous"] == old |
| 412 | assert data["commit_id"] == new |
| 413 | |
| 414 | def test_d3_branch_field_matches_arg(self, tmp_path: pathlib.Path) -> None: |
| 415 | """D3: branch field in JSON matches the branch argument.""" |
| 416 | repo = _make_repo(tmp_path) |
| 417 | cid = _commit(repo) |
| 418 | data = json.loads(_ur(repo, "my-feature", cid, "--json").output) |
| 419 | assert data["branch"] == "my-feature" |
| 420 | |
| 421 | def test_d4_deleted_true_on_delete(self, tmp_path: pathlib.Path) -> None: |
| 422 | """D4: deleted field is boolean true on successful delete.""" |
| 423 | repo = _make_repo(tmp_path) |
| 424 | _write_ref(repo, "ephemeral", long_id("e" * 64)) |
| 425 | data = json.loads(_ur(repo, "--delete", "ephemeral", "--json").output) |
| 426 | assert data["deleted"] is True |
| 427 | |
| 428 | def test_d5_exit_code_1_for_user_errors(self, tmp_path: pathlib.Path) -> None: |
| 429 | """D5: user-visible errors (bad branch, not-found commit) use exit_code 1.""" |
| 430 | repo = _make_repo(tmp_path) |
| 431 | r = _ur(repo, "main", long_id("9" * 64), "--json") # not in store |
| 432 | data = json.loads(r.output) |
| 433 | assert data["exit_code"] == ExitCode.USER_ERROR |
| 434 | |
| 435 | def test_d6_exit_code_3_for_write_failure(self, tmp_path: pathlib.Path) -> None: |
| 436 | """D6: write failure uses exit_code 3 (internal error).""" |
| 437 | repo = _make_repo(tmp_path) |
| 438 | cid = _commit(repo) |
| 439 | with mock.patch( |
| 440 | "muse.cli.commands.update_ref.write_branch_ref", |
| 441 | side_effect=OSError("ENOSPC"), |
| 442 | ): |
| 443 | r = _ur(repo, "main", cid, "--json") |
| 444 | data = json.loads(r.output) |
| 445 | assert data["exit_code"] == ExitCode.INTERNAL_ERROR |
| 446 | |
| 447 | def test_d7_cas_mismatch_includes_current_and_expected(self, tmp_path: pathlib.Path) -> None: |
| 448 | """D7: CAS error JSON includes current ref value and what was expected.""" |
| 449 | repo = _make_repo(tmp_path) |
| 450 | actual = _commit(repo, "actual-commit") |
| 451 | new_id = _commit(repo, "new-commit") |
| 452 | wrong_old = blob_id(b"wrong-expected-value") |
| 453 | _write_ref(repo, "main", actual) |
| 454 | r = _ur(repo, "--old-value", wrong_old, "main", new_id, "--json") |
| 455 | assert r.exit_code == ExitCode.USER_ERROR |
| 456 | data = json.loads(r.output) |
| 457 | # current and expected give agents enough context to retry correctly |
| 458 | assert "current" in data, "CAS error must include current ref value" |
| 459 | assert "expected" in data or wrong_old in str(data), "CAS error must include expected value" |
| 460 | |
| 461 | def test_d8_blob_id_produces_unique_valid_ids(self, tmp_path: pathlib.Path) -> None: |
| 462 | """D8: blob_id() always produces distinct valid sha256-prefixed IDs.""" |
| 463 | ids = {blob_id(__import__("os").urandom(32)) for _ in range(20)} |
| 464 | assert len(ids) == 20, "blob_id must produce unique IDs" |
| 465 | for bid in ids: |
| 466 | assert bid.startswith("sha256:") |
| 467 | assert len(bid) == 71 |
| 468 | |
| 469 | def test_d9_long_id_produces_correct_prefix(self, tmp_path: pathlib.Path) -> None: |
| 470 | """D9: long_id() round-trips correctly through update-ref.""" |
| 471 | repo = _make_repo(tmp_path) |
| 472 | bare = "c" * 64 |
| 473 | cid = long_id(bare) |
| 474 | assert cid == f"sha256:{bare}" |
| 475 | # Use it as a ref value (bypassing store check) |
| 476 | r = _ur(repo, "--no-verify", "long-id-test", cid, "--json") |
| 477 | assert r.exit_code == 0 |
| 478 | data = json.loads(r.output) |
| 479 | assert data["commit_id"] == cid |
| 480 | |
| 481 | def test_d10_short_id_is_prefix_of_long_id(self) -> None: |
| 482 | """D10: short_id is the first 19 chars of long_id (sha256: + 12 hex).""" |
| 483 | cid = long_id("abcdef1234567890" * 4) |
| 484 | sid = short_id(cid) |
| 485 | assert cid.startswith(sid) |
| 486 | assert sid.startswith("sha256:") |
| 487 | assert len(sid) == 19 # "sha256:" (7) + 12 hex chars |
| 488 | |
| 489 | |
| 490 | # --------------------------------------------------------------------------- |
| 491 | # CAS — compare-and-swap error payloads and timing |
| 492 | # --------------------------------------------------------------------------- |
| 493 | |
| 494 | |
| 495 | class TestCASSchema: |
| 496 | """CAS-specific: error routing and schema when CAS fires.""" |
| 497 | |
| 498 | def test_cas1_null_guard_mismatch_has_duration_ms(self, tmp_path: pathlib.Path) -> None: |
| 499 | """CAS1: --old-value null mismatch (ref exists) error has duration_ms.""" |
| 500 | repo = _make_repo(tmp_path) |
| 501 | existing = _commit(repo, "existing") |
| 502 | new_id = _commit(repo, "new") |
| 503 | _write_ref(repo, "contested", existing) |
| 504 | r = _ur(repo, "--old-value", "null", "contested", new_id, "--json") |
| 505 | assert r.exit_code != 0 |
| 506 | data = json.loads(r.output) |
| 507 | assert "duration_ms" in data |
| 508 | |
| 509 | def test_cas2_mismatch_error_to_stdout_not_stderr(self, tmp_path: pathlib.Path) -> None: |
| 510 | """CAS2: CAS mismatch with --json → error on stdout, stderr empty.""" |
| 511 | repo = _make_repo(tmp_path) |
| 512 | actual = _commit(repo, "actual") |
| 513 | new_id = _commit(repo, "new") |
| 514 | wrong = blob_id(b"wrong-cas-value") |
| 515 | _write_ref(repo, "main", actual) |
| 516 | r = _ur(repo, "--old-value", wrong, "main", new_id, "--json") |
| 517 | assert r.exit_code != 0 |
| 518 | assert r.stderr.strip() == "" |
| 519 | data = json.loads(r.output) |
| 520 | assert "error" in data |
| 521 | assert "duration_ms" in data |
| 522 | assert "exit_code" in data |
| 523 | |
| 524 | def test_cas3_success_has_duration_ms(self, tmp_path: pathlib.Path) -> None: |
| 525 | """CAS3: successful CAS also emits duration_ms.""" |
| 526 | repo = _make_repo(tmp_path) |
| 527 | old = _commit(repo, "old") |
| 528 | new = _commit(repo, "new") |
| 529 | _write_ref(repo, "main", old) |
| 530 | r = _ur(repo, "--old-value", old, "main", new, "--json") |
| 531 | assert r.exit_code == 0 |
| 532 | data = json.loads(r.output) |
| 533 | assert "duration_ms" in data |
| 534 | |
| 535 | def test_cas4_invalid_old_value_format_error_on_stdout(self, tmp_path: pathlib.Path) -> None: |
| 536 | """CAS4: bare hex (missing sha256: prefix) in --old-value → JSON error on stdout.""" |
| 537 | repo = _make_repo(tmp_path) |
| 538 | cid = _commit(repo) |
| 539 | bare_hex = "a" * 64 # missing sha256: prefix |
| 540 | r = _ur(repo, "--old-value", bare_hex, "main", cid, "--json") |
| 541 | assert r.exit_code != 0 |
| 542 | assert r.stderr.strip() == "" |
| 543 | data = json.loads(r.output) |
| 544 | assert "error" in data |
| 545 | |
| 546 | |
| 547 | # --------------------------------------------------------------------------- |
| 548 | # P — Performance |
| 549 | # --------------------------------------------------------------------------- |
| 550 | |
| 551 | |
| 552 | class TestPerformance: |
| 553 | """P1–P3: duration_ms is a realistic duration.""" |
| 554 | |
| 555 | def test_p1_single_update_under_2000ms(self, tmp_path: pathlib.Path) -> None: |
| 556 | """P1: a single ref update finishes in < 2 seconds.""" |
| 557 | repo = _make_repo(tmp_path) |
| 558 | cid = _commit(repo) |
| 559 | r = _ur(repo, "perf-branch", cid, "--json") |
| 560 | assert r.exit_code == 0 |
| 561 | assert json.loads(r.output)["duration_ms"] < 2000.0 |
| 562 | |
| 563 | def test_p2_delete_under_2000ms(self, tmp_path: pathlib.Path) -> None: |
| 564 | """P2: a ref delete finishes in < 2 seconds.""" |
| 565 | repo = _make_repo(tmp_path) |
| 566 | _write_ref(repo, "perf-del", long_id("f" * 64)) |
| 567 | r = _ur(repo, "--delete", "perf-del", "--json") |
| 568 | assert r.exit_code == 0 |
| 569 | assert json.loads(r.output)["duration_ms"] < 2000.0 |
| 570 | |
| 571 | def test_p3_200_sequential_updates_all_have_duration_ms(self, tmp_path: pathlib.Path) -> None: |
| 572 | """P3: 200 sequential updates all emit duration_ms.""" |
| 573 | repo = _make_repo(tmp_path) |
| 574 | cid = _commit(repo) |
| 575 | for i in range(200): |
| 576 | r = _ur(repo, "perf-stress", cid, "--json") |
| 577 | assert r.exit_code == 0, f"failed at iteration {i}" |
| 578 | assert "duration_ms" in json.loads(r.output), f"missing duration_ms at iteration {i}" |
| 579 | |
| 580 | |
| 581 | # --------------------------------------------------------------------------- |
| 582 | # Sec — Security: no traceback on any error path |
| 583 | # --------------------------------------------------------------------------- |
| 584 | |
| 585 | |
| 586 | class TestSecurity: |
| 587 | """Sec1–Sec5: error paths never produce raw Python tracebacks.""" |
| 588 | |
| 589 | def test_sec1_no_traceback_invalid_branch_json_mode(self, tmp_path: pathlib.Path) -> None: |
| 590 | """Sec1: invalid branch name with --json → no Traceback.""" |
| 591 | repo = _make_repo(tmp_path) |
| 592 | r = _ur(repo, "bad\x00branch", long_id("a" * 64), "--json") |
| 593 | assert r.exit_code != 0 |
| 594 | assert "Traceback" not in r.output |
| 595 | assert "Traceback" not in r.stderr |
| 596 | |
| 597 | def test_sec2_no_traceback_write_failure_json_mode(self, tmp_path: pathlib.Path) -> None: |
| 598 | """Sec2: mocked write failure with --json → no Traceback.""" |
| 599 | repo = _make_repo(tmp_path) |
| 600 | cid = _commit(repo) |
| 601 | with mock.patch( |
| 602 | "muse.cli.commands.update_ref.write_branch_ref", |
| 603 | side_effect=OSError("permission denied"), |
| 604 | ): |
| 605 | r = _ur(repo, "main", cid, "--json") |
| 606 | assert r.exit_code != 0 |
| 607 | assert "Traceback" not in r.output |
| 608 | assert "Traceback" not in r.stderr |
| 609 | |
| 610 | def test_sec3_no_traceback_write_failure_text_mode(self, tmp_path: pathlib.Path) -> None: |
| 611 | """Sec3: mocked write failure in text mode → no Traceback.""" |
| 612 | repo = _make_repo(tmp_path) |
| 613 | cid = _commit(repo) |
| 614 | with mock.patch( |
| 615 | "muse.cli.commands.update_ref.write_branch_ref", |
| 616 | side_effect=OSError("permission denied"), |
| 617 | ): |
| 618 | r = _ur(repo, "main", cid, "--format", "text") |
| 619 | assert r.exit_code != 0 |
| 620 | assert "Traceback" not in r.output |
| 621 | assert "Traceback" not in r.stderr |
| 622 | |
| 623 | def test_sec4_path_traversal_in_branch_rejected(self, tmp_path: pathlib.Path) -> None: |
| 624 | """Sec4: branch names with ../ path traversal are rejected.""" |
| 625 | repo = _make_repo(tmp_path) |
| 626 | cid = _commit(repo) |
| 627 | r = _ur(repo, "../../../etc/cron.d/malicious", cid, "--json") |
| 628 | assert r.exit_code != 0 |
| 629 | assert r.stderr.strip() == "" # error in stdout (json mode) |
| 630 | data = json.loads(r.output) |
| 631 | assert "error" in data |
| 632 | |
| 633 | def test_sec5_ansi_in_branch_rejected_json_mode(self, tmp_path: pathlib.Path) -> None: |
| 634 | """Sec5: ANSI escape codes in branch name are rejected; error to stdout.""" |
| 635 | repo = _make_repo(tmp_path) |
| 636 | r = _ur(repo, "\x1b[31mbranch", long_id("a" * 64), "--json") |
| 637 | assert r.exit_code != 0 |
| 638 | assert r.stderr.strip() == "" |
| 639 | data = json.loads(r.output) |
| 640 | assert "error" in data |
| 641 | |
| 642 | |
| 643 | # --------------------------------------------------------------------------- |
| 644 | # C — Concurrency: parallel updates to independent branches |
| 645 | # --------------------------------------------------------------------------- |
| 646 | |
| 647 | |
| 648 | class TestConcurrency: |
| 649 | """C1: N threads each update a distinct branch — no cross-contamination.""" |
| 650 | |
| 651 | def test_c1_parallel_independent_branch_updates(self, tmp_path: pathlib.Path) -> None: |
| 652 | """C1: 16 threads each write to their own branch — all succeed.""" |
| 653 | repo = _make_repo(tmp_path) |
| 654 | cid = _commit(repo, "shared commit") |
| 655 | N = 16 |
| 656 | results: list[InvokeResult | None] = [None] * N |
| 657 | errors: list[str] = [] |
| 658 | |
| 659 | def _worker(idx: int) -> None: |
| 660 | branch = f"concurrent-branch-{idx}" |
| 661 | r = _ur(repo, "--no-verify", branch, cid, "--json") |
| 662 | results[idx] = r |
| 663 | if r.exit_code != 0: |
| 664 | errors.append(f"thread {idx} failed: exit_code={r.exit_code}") |
| 665 | |
| 666 | threads = [threading.Thread(target=_worker, args=(i,)) for i in range(N)] |
| 667 | for t in threads: |
| 668 | t.start() |
| 669 | for t in threads: |
| 670 | t.join() |
| 671 | |
| 672 | assert not errors, "\n".join(errors) |
| 673 | for i, r in enumerate(results): |
| 674 | assert r is not None |
| 675 | assert r.exit_code == 0, f"thread {i} non-zero exit" |
| 676 | data = json.loads(r.output) |
| 677 | assert data["branch"] == f"concurrent-branch-{i}" |
| 678 | assert "duration_ms" in data |
File History
1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d
feat(pack): delta-encode snapshots in MPackBundle wire format
Sonnet 4.6
minor
⚠
121 days ago