test_core_object_availability.py
python
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3
docs: docstring sprint contract→find-symbol — idiomatic run…
Sonnet 4.6
patch
140 days ago
| 1 | """Tests for muse/core/object_availability.py — ObjectState model. |
| 2 | |
| 3 | Every object in a Muse repo is in one of three states: |
| 4 | PRESENT — bytes exist in the local .muse/objects/ store |
| 5 | PROMISED — not local but a promisor remote is configured; can be fetched |
| 6 | MISSING — not local AND no promisor remote; genuine data loss risk |
| 7 | |
| 8 | Coverage: |
| 9 | Unit: object_state() for all three states |
| 10 | load_promisor_remotes() reads config correctly |
| 11 | Edge cases: no remotes configured, promisor=false explicit opt-out, |
| 12 | mixed promisor and non-promisor remotes |
| 13 | Invariants: PRESENT when file exists regardless of remotes |
| 14 | MISSING only when no promisor remote AND file absent |
| 15 | """ |
| 16 | |
| 17 | from __future__ import annotations |
| 18 | |
| 19 | import json |
| 20 | import pathlib |
| 21 | |
| 22 | import pytest |
| 23 | |
| 24 | import hashlib |
| 25 | |
| 26 | from muse.core._types import long_id |
| 27 | from muse.core.object_availability import ObjectState, load_promisor_remotes, object_state |
| 28 | from muse.core.object_store import write_object |
| 29 | |
| 30 | |
| 31 | # --------------------------------------------------------------------------- |
| 32 | # Helpers |
| 33 | # --------------------------------------------------------------------------- |
| 34 | |
| 35 | def _sha(data: bytes) -> str: |
| 36 | return long_id(hashlib.sha256(data).hexdigest()) |
| 37 | |
| 38 | |
| 39 | def _init_repo(path: pathlib.Path, remotes: dict | None = None) -> pathlib.Path: |
| 40 | muse = path / ".muse" |
| 41 | for d in ("commits", "snapshots", "objects", "refs/heads"): |
| 42 | (muse / d).mkdir(parents=True, exist_ok=True) |
| 43 | (muse / "HEAD").write_text("ref: refs/heads/main") |
| 44 | (muse / "repo.json").write_text(json.dumps({"repo_id": "avail-test", "domain": "code"})) |
| 45 | if remotes: |
| 46 | _write_remotes(path, remotes) |
| 47 | return path |
| 48 | |
| 49 | |
| 50 | def _write_remotes(repo: pathlib.Path, remotes: dict) -> None: |
| 51 | """Write a minimal config.toml with the given remotes. |
| 52 | |
| 53 | remotes format: {"name": {"url": "...", "promisor": True/False}} |
| 54 | """ |
| 55 | lines = [] |
| 56 | for name, cfg in remotes.items(): |
| 57 | lines.append(f'[remotes.{name}]') |
| 58 | lines.append(f'url = "{cfg["url"]}"') |
| 59 | if "promisor" in cfg: |
| 60 | val = "true" if cfg["promisor"] else "false" |
| 61 | lines.append(f'promisor = {val}') |
| 62 | (repo / ".muse" / "config.toml").write_text("\n".join(lines) + "\n") |
| 63 | |
| 64 | |
| 65 | _OBJ_CONTENT = b"test object content for availability" |
| 66 | _OBJ_ID = _sha(_OBJ_CONTENT) |
| 67 | |
| 68 | |
| 69 | # --------------------------------------------------------------------------- |
| 70 | # ObjectState enum |
| 71 | # --------------------------------------------------------------------------- |
| 72 | |
| 73 | class TestObjectStateEnum: |
| 74 | def test_has_present(self) -> None: |
| 75 | assert ObjectState.PRESENT == "present" |
| 76 | |
| 77 | def test_has_promised(self) -> None: |
| 78 | assert ObjectState.PROMISED == "promised" |
| 79 | |
| 80 | def test_has_missing(self) -> None: |
| 81 | assert ObjectState.MISSING == "missing" |
| 82 | |
| 83 | def test_is_string_enum(self) -> None: |
| 84 | assert isinstance(ObjectState.PRESENT, str) |
| 85 | |
| 86 | |
| 87 | # --------------------------------------------------------------------------- |
| 88 | # object_state() — PRESENT |
| 89 | # --------------------------------------------------------------------------- |
| 90 | |
| 91 | class TestObjectStatePresent: |
| 92 | def test_present_when_file_exists_no_remotes(self, tmp_path: pathlib.Path) -> None: |
| 93 | repo = _init_repo(tmp_path) |
| 94 | content = b"content-no-remotes" |
| 95 | oid = _sha(content) |
| 96 | write_object(repo, oid, content) |
| 97 | assert object_state(repo, oid, []) == ObjectState.PRESENT |
| 98 | |
| 99 | def test_present_when_file_exists_with_promisor(self, tmp_path: pathlib.Path) -> None: |
| 100 | repo = _init_repo(tmp_path) |
| 101 | content = b"content-with-promisor" |
| 102 | oid = _sha(content) |
| 103 | write_object(repo, oid, content) |
| 104 | # Even with promisors, PRESENT wins — file is local |
| 105 | assert object_state(repo, oid, ["local"]) == ObjectState.PRESENT |
| 106 | |
| 107 | def test_present_takes_priority_over_promisor(self, tmp_path: pathlib.Path) -> None: |
| 108 | repo = _init_repo(tmp_path) |
| 109 | content = b"present content bytes" |
| 110 | obj_id = _sha(content) |
| 111 | write_object(repo, obj_id, content) |
| 112 | state = object_state(repo, obj_id, ["remote-a", "remote-b"]) |
| 113 | assert state == ObjectState.PRESENT |
| 114 | |
| 115 | |
| 116 | # --------------------------------------------------------------------------- |
| 117 | # object_state() — PROMISED |
| 118 | # --------------------------------------------------------------------------- |
| 119 | |
| 120 | class TestObjectStatePromised: |
| 121 | def test_promised_when_absent_with_one_promisor(self, tmp_path: pathlib.Path) -> None: |
| 122 | repo = _init_repo(tmp_path) |
| 123 | state = object_state(repo, _OBJ_ID, ["local"]) |
| 124 | assert state == ObjectState.PROMISED |
| 125 | |
| 126 | def test_promised_when_absent_with_multiple_promisors(self, tmp_path: pathlib.Path) -> None: |
| 127 | repo = _init_repo(tmp_path) |
| 128 | state = object_state(repo, _OBJ_ID, ["origin", "backup"]) |
| 129 | assert state == ObjectState.PROMISED |
| 130 | |
| 131 | def test_promised_not_present(self, tmp_path: pathlib.Path) -> None: |
| 132 | repo = _init_repo(tmp_path) |
| 133 | state = object_state(repo, _OBJ_ID, ["local"]) |
| 134 | assert state != ObjectState.PRESENT |
| 135 | |
| 136 | |
| 137 | # --------------------------------------------------------------------------- |
| 138 | # object_state() — MISSING |
| 139 | # --------------------------------------------------------------------------- |
| 140 | |
| 141 | class TestObjectStateMissing: |
| 142 | def test_missing_when_absent_no_remotes(self, tmp_path: pathlib.Path) -> None: |
| 143 | repo = _init_repo(tmp_path) |
| 144 | state = object_state(repo, _OBJ_ID, []) |
| 145 | assert state == ObjectState.MISSING |
| 146 | |
| 147 | def test_missing_not_promised(self, tmp_path: pathlib.Path) -> None: |
| 148 | repo = _init_repo(tmp_path) |
| 149 | state = object_state(repo, _OBJ_ID, []) |
| 150 | assert state != ObjectState.PROMISED |
| 151 | |
| 152 | |
| 153 | # --------------------------------------------------------------------------- |
| 154 | # load_promisor_remotes() — reads config |
| 155 | # --------------------------------------------------------------------------- |
| 156 | |
| 157 | class TestLoadPromisorRemotes: |
| 158 | def test_empty_when_no_config(self, tmp_path: pathlib.Path) -> None: |
| 159 | repo = _init_repo(tmp_path) |
| 160 | result = load_promisor_remotes(repo) |
| 161 | assert result == [] |
| 162 | |
| 163 | def test_all_remotes_are_promisors_by_default(self, tmp_path: pathlib.Path) -> None: |
| 164 | repo = _init_repo(tmp_path, remotes={ |
| 165 | "local": {"url": "http://localhost:10003/gabriel/muse"}, |
| 166 | "staging": {"url": "https://staging.musehub.ai/gabriel/muse"}, |
| 167 | }) |
| 168 | result = load_promisor_remotes(repo) |
| 169 | assert "local" in result |
| 170 | assert "staging" in result |
| 171 | |
| 172 | def test_explicit_promisor_true_included(self, tmp_path: pathlib.Path) -> None: |
| 173 | repo = _init_repo(tmp_path, remotes={ |
| 174 | "origin": {"url": "http://localhost:10003/gabriel/muse", "promisor": True}, |
| 175 | }) |
| 176 | assert "origin" in load_promisor_remotes(repo) |
| 177 | |
| 178 | def test_explicit_promisor_false_excluded(self, tmp_path: pathlib.Path) -> None: |
| 179 | repo = _init_repo(tmp_path, remotes={ |
| 180 | "mirror": {"url": "http://mirror.example.com/muse", "promisor": False}, |
| 181 | }) |
| 182 | assert "mirror" not in load_promisor_remotes(repo) |
| 183 | |
| 184 | def test_mixed_promisors(self, tmp_path: pathlib.Path) -> None: |
| 185 | repo = _init_repo(tmp_path, remotes={ |
| 186 | "local": {"url": "http://localhost:10003/g/muse"}, # default → promisor |
| 187 | "mirror": {"url": "http://mirror.example.com/muse", "promisor": False}, |
| 188 | }) |
| 189 | result = load_promisor_remotes(repo) |
| 190 | assert "local" in result |
| 191 | assert "mirror" not in result |
| 192 | |
| 193 | def test_returns_list_of_strings(self, tmp_path: pathlib.Path) -> None: |
| 194 | repo = _init_repo(tmp_path, remotes={ |
| 195 | "local": {"url": "http://localhost:10003/g/muse"}, |
| 196 | }) |
| 197 | result = load_promisor_remotes(repo) |
| 198 | assert isinstance(result, list) |
| 199 | assert all(isinstance(r, str) for r in result) |
| 200 | |
| 201 | def test_no_promisor_when_all_opted_out(self, tmp_path: pathlib.Path) -> None: |
| 202 | repo = _init_repo(tmp_path, remotes={ |
| 203 | "mirror": {"url": "http://mirror.example.com/muse", "promisor": False}, |
| 204 | }) |
| 205 | assert load_promisor_remotes(repo) == [] |
File History
1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3
docs: docstring sprint contract→find-symbol — idiomatic run…
Sonnet 4.6
patch
140 days ago