gabriel / muse public
test_core_object_availability.py python
205 lines 7.9 KB
Raw
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