gabriel / muse public
test_read_snapshot_supercharge.py python
435 lines 17.2 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 136 days ago
1 """Supercharge tests for ``muse read-snapshot``.
2
3 Coverage tiers
4 --------------
5 - Unit: _short_id helper — prefix preservation, hex length
6 - Integration: duration_ms + exit_code in JSON; text short-ID format
7 - Flag interaction: --no-manifest + --path-prefix together
8 - Data integrity: sha256: on snapshot_id; valid JSON; unicode paths
9 - Path prefix edge cases: empty prefix, no trailing slash, exact filename
10 - Performance: single read and 1000-file manifest under threshold
11 """
12 from __future__ import annotations
13 from collections.abc import Mapping
14
15 import datetime
16 import json
17 import pathlib
18 import re
19 import time
20
21 from muse.core.errors import ExitCode
22 from muse.core.snapshot import compute_snapshot_id
23 from muse.core.store import SnapshotRecord, write_snapshot
24 from tests.cli_test_helper import CliRunner, InvokeResult
25 from muse.core._types import fake_id, long_id, split_id
26
27 runner = CliRunner()
28
29 _CREATED_AT = datetime.datetime(2026, 3, 18, 12, 0, tzinfo=datetime.timezone.utc)
30
31 _SHA256_FULL = re.compile(r"^sha256:[0-9a-f]{64}$")
32 _SHA256_SHORT_19 = re.compile(r"^sha256:[0-9a-f]{12}$")
33
34
35 # ---------------------------------------------------------------------------
36 # Helpers
37 # ---------------------------------------------------------------------------
38
39
40 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
41 repo = tmp_path / "repo"
42 muse = repo / ".muse"
43 for sub in ("objects", "commits", "snapshots", "refs/heads"):
44 (muse / sub).mkdir(parents=True)
45 (muse / "HEAD").write_text("ref: refs/heads/main")
46 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": "code"}))
47 return repo
48
49
50 def _snap(repo: pathlib.Path, manifest: Mapping[str, object] | None = None) -> str:
51 m = manifest or {}
52 sid = compute_snapshot_id(m)
53 write_snapshot(repo, SnapshotRecord(snapshot_id=sid, manifest=m, created_at=_CREATED_AT))
54 return sid
55
56
57 def _rs(repo: pathlib.Path, *args: str) -> InvokeResult:
58 from muse.cli.app import main as cli
59 return runner.invoke(cli, ["read-snapshot", *args], env={"MUSE_REPO_ROOT": str(repo)})
60
61
62 def _rsj(repo: pathlib.Path, *args: str) -> InvokeResult:
63 """Like _rs but always passes --json for JSON-output tests."""
64 return _rs(repo, "--json", *args)
65
66
67 def _oid(n: int) -> str:
68 """Canonical sha256:-prefixed object ID for test manifests."""
69 return long_id(format(n, "064x"))
70
71
72 # ---------------------------------------------------------------------------
73 # Unit — _short_id
74 # ---------------------------------------------------------------------------
75
76
77 class TestShortId:
78 """_short_id must keep sha256: prefix and truncate to exactly 12 hex chars."""
79
80 def test_short_id_keeps_sha256_prefix(self) -> None:
81 from muse.cli.commands.read_snapshot import _short_id
82 sid = long_id("a" * 64)
83 assert _short_id(sid).startswith("sha256:")
84
85 def test_short_id_12_hex_chars_after_prefix(self) -> None:
86 from muse.cli.commands.read_snapshot import _short_id
87 sid = long_id("deadbeef" * 8)
88 result = _short_id(sid)
89 assert result == long_id("deadbeef" + "dead")# 7 + 12 = 19
90
91 def test_short_id_total_length_is_19(self) -> None:
92 from muse.cli.commands.read_snapshot import _short_id
93 sid = long_id("0" * 64)
94 assert len(_short_id(sid)) == 19
95
96 def test_short_id_bare_hex_fallback(self) -> None:
97 from muse.cli.commands.read_snapshot import _short_id
98 bare = "a" * 64
99 assert len(_short_id(bare)) == 12
100
101 def test_short_id_matches_regex(self) -> None:
102 from muse.cli.commands.read_snapshot import _short_id
103 sid = long_id("abcdef01" * 8)
104 assert _SHA256_SHORT_19.match(_short_id(sid))
105
106
107 # ---------------------------------------------------------------------------
108 # Integration — duration_ms and exit_code
109 # ---------------------------------------------------------------------------
110
111
112 class TestDurationAndExitCode:
113 def test_duration_ms_present_on_success(self, tmp_path: pathlib.Path) -> None:
114 repo = _make_repo(tmp_path)
115 sid = _snap(repo)
116 data = json.loads(_rsj(repo, sid).output)
117 assert "duration_ms" in data, "duration_ms must be present in JSON output"
118
119 def test_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
120 repo = _make_repo(tmp_path)
121 sid = _snap(repo)
122 data = json.loads(_rsj(repo, sid).output)
123 assert data["exit_code"] == 0
124
125 def test_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None:
126 repo = _make_repo(tmp_path)
127 sid = _snap(repo)
128 data = json.loads(_rsj(repo, sid).output)
129 assert isinstance(data["duration_ms"], float)
130
131 def test_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
132 repo = _make_repo(tmp_path)
133 sid = _snap(repo)
134 data = json.loads(_rsj(repo, sid).output)
135 assert data["duration_ms"] >= 0.0
136
137 def test_duration_ms_3dp_precision(self, tmp_path: pathlib.Path) -> None:
138 repo = _make_repo(tmp_path)
139 sid = _snap(repo)
140 ms = json.loads(_rsj(repo, sid).output)["duration_ms"]
141 assert round(ms, 3) == ms
142
143 def test_duration_ms_present_with_no_manifest(self, tmp_path: pathlib.Path) -> None:
144 repo = _make_repo(tmp_path)
145 sid = _snap(repo, {"a.py": _oid(1)})
146 data = json.loads(_rsj(repo, "--no-manifest", sid).output)
147 assert "duration_ms" in data
148 assert "exit_code" in data
149
150 def test_duration_ms_present_with_path_prefix(self, tmp_path: pathlib.Path) -> None:
151 repo = _make_repo(tmp_path)
152 sid = _snap(repo, {"src/a.py": _oid(1), "tests/b.py": _oid(2)})
153 data = json.loads(_rsj(repo, "--path-prefix", "src/", sid).output)
154 assert "duration_ms" in data
155 assert data["exit_code"] == 0
156
157
158 # ---------------------------------------------------------------------------
159 # Integration — text format short ID
160 # ---------------------------------------------------------------------------
161
162
163 class TestTextFormatShortId:
164 """Text format must emit sha256:<12-hex> (19 chars), not the old 12-char bare slice."""
165
166 def _short_token(self, line: str) -> str | None:
167 for tok in line.split():
168 if _SHA256_SHORT_19.match(tok):
169 return tok
170 return None
171
172 def test_text_short_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
173 repo = _make_repo(tmp_path)
174 sid = _snap(repo, {"f.py": _oid(1)})
175 result = _rs(repo, sid)
176 assert result.exit_code == 0
177 tok = self._short_token(result.output.strip())
178 assert tok is not None, f"no sha256:<12-hex> token in: {result.output!r}"
179 assert tok.startswith("sha256:")
180
181 def test_text_short_id_has_12_hex_chars(self, tmp_path: pathlib.Path) -> None:
182 repo = _make_repo(tmp_path)
183 sid = _snap(repo)
184 result = _rs(repo, sid)
185 tok = self._short_token(result.output.strip())
186 assert tok is not None
187 assert tok.startswith("sha256:")
188 assert len(tok[len("sha256:"):]) == 12
189
190 def test_text_short_id_total_length_is_19(self, tmp_path: pathlib.Path) -> None:
191 repo = _make_repo(tmp_path)
192 sid = _snap(repo)
193 result = _rs(repo, sid)
194 tok = self._short_token(result.output.strip())
195 assert tok is not None
196 assert len(tok) == 19
197
198 def test_text_short_id_is_prefix_of_full_id(self, tmp_path: pathlib.Path) -> None:
199 repo = _make_repo(tmp_path)
200 sid = _snap(repo, {"x.py": _oid(9)})
201 result = _rs(repo, sid)
202 tok = self._short_token(result.output.strip())
203 assert tok is not None
204 assert sid.startswith(tok), f"{tok!r} is not a prefix of {sid!r}"
205
206
207 # ---------------------------------------------------------------------------
208 # Flag interaction — --no-manifest + --path-prefix together
209 # ---------------------------------------------------------------------------
210
211
212 class TestFlagInteraction:
213 """--no-manifest and --path-prefix may be combined.
214
215 Use case: "how many files are under src/ without downloading any OIDs?"
216 The file_count reflects the filtered count; manifest is omitted.
217 """
218
219 def test_no_manifest_plus_path_prefix_succeeds(self, tmp_path: pathlib.Path) -> None:
220 repo = _make_repo(tmp_path)
221 sid = _snap(repo, {
222 "src/a.py": _oid(1),
223 "src/b.py": _oid(2),
224 "tests/c.py": _oid(3),
225 })
226 result = _rsj(repo, "--no-manifest", "--path-prefix", "src/", sid)
227 assert result.exit_code == 0, result.output
228
229 def test_no_manifest_plus_path_prefix_file_count_is_filtered(self, tmp_path: pathlib.Path) -> None:
230 repo = _make_repo(tmp_path)
231 sid = _snap(repo, {
232 "src/a.py": _oid(1),
233 "src/b.py": _oid(2),
234 "tests/c.py": _oid(3),
235 })
236 data = json.loads(_rsj(repo, "--no-manifest", "--path-prefix", "src/", sid).output)
237 assert data["file_count"] == 2, "file_count must reflect the prefix-filtered count"
238
239 def test_no_manifest_plus_path_prefix_manifest_absent(self, tmp_path: pathlib.Path) -> None:
240 repo = _make_repo(tmp_path)
241 sid = _snap(repo, {"src/a.py": _oid(1)})
242 data = json.loads(_rsj(repo, "--no-manifest", "--path-prefix", "src/", sid).output)
243 assert "manifest" not in data
244
245 def test_no_manifest_plus_path_prefix_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
246 repo = _make_repo(tmp_path)
247 sid = _snap(repo, {"src/a.py": _oid(1)})
248 data = json.loads(_rsj(repo, "--no-manifest", "--path-prefix", "src/", sid).output)
249 assert "duration_ms" in data
250 assert data["exit_code"] == 0
251
252
253 # ---------------------------------------------------------------------------
254 # Data integrity
255 # ---------------------------------------------------------------------------
256
257
258 class TestDataIntegrity:
259 def test_snapshot_id_has_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
260 repo = _make_repo(tmp_path)
261 sid = _snap(repo)
262 data = json.loads(_rsj(repo, sid).output)
263 assert _SHA256_FULL.match(data["snapshot_id"]), \
264 f"snapshot_id must be sha256:<64hex>, got {data['snapshot_id']!r}"
265
266 def test_json_output_is_valid_json(self, tmp_path: pathlib.Path) -> None:
267 repo = _make_repo(tmp_path)
268 sid = _snap(repo, {"a.py": _oid(1)})
269 result = _rsj(repo, sid)
270 assert result.exit_code == 0
271 data = json.loads(result.output)
272 assert isinstance(data, dict)
273
274 def test_manifest_values_are_strings(self, tmp_path: pathlib.Path) -> None:
275 """Manifest object IDs are strings — no type coercion."""
276 repo = _make_repo(tmp_path)
277 sid = _snap(repo, {"a.py": _oid(1), "b.py": _oid(2)})
278 data = json.loads(_rsj(repo, sid).output)
279 for path, oid in data["manifest"].items():
280 assert isinstance(oid, str), f"manifest[{path!r}] must be a string, got {type(oid)}"
281
282 def test_unicode_paths_in_manifest(self, tmp_path: pathlib.Path) -> None:
283 """Unicode file paths round-trip through JSON without corruption."""
284 repo = _make_repo(tmp_path)
285 paths = {
286 "src/音楽.py": _oid(1),
287 "tracks/café/main.mid": _oid(2),
288 "docs/naïve_approach.md": _oid(3),
289 }
290 sid = _snap(repo, paths)
291 data = json.loads(_rsj(repo, sid).output)
292 assert data["file_count"] == 3
293 for p in paths:
294 assert p in data["manifest"], f"unicode path {p!r} missing from manifest"
295
296 def test_created_at_iso8601_with_timezone(self, tmp_path: pathlib.Path) -> None:
297 repo = _make_repo(tmp_path)
298 sid = _snap(repo)
299 data = json.loads(_rsj(repo, sid).output)
300 dt = datetime.datetime.fromisoformat(data["created_at"])
301 assert dt.tzinfo is not None, "created_at must include timezone"
302
303 def test_file_count_matches_manifest_length(self, tmp_path: pathlib.Path) -> None:
304 """file_count must equal len(manifest) in the response."""
305 repo = _make_repo(tmp_path)
306 n = 17
307 sid = _snap(repo, {f"f{i}.py": _oid(i) for i in range(n)})
308 data = json.loads(_rsj(repo, sid).output)
309 assert data["file_count"] == n
310 assert len(data["manifest"]) == n
311
312
313 # ---------------------------------------------------------------------------
314 # Path prefix edge cases
315 # ---------------------------------------------------------------------------
316
317
318 class TestPathPrefixEdgeCases:
319 def test_empty_prefix_matches_all(self, tmp_path: pathlib.Path) -> None:
320 """Empty --path-prefix matches every path (prefix of every string)."""
321 repo = _make_repo(tmp_path)
322 sid = _snap(repo, {"src/a.py": _oid(1), "tests/b.py": _oid(2)})
323 data = json.loads(_rsj(repo, "--path-prefix", "", sid).output)
324 assert data["file_count"] == 2
325
326 def test_prefix_without_trailing_slash(self, tmp_path: pathlib.Path) -> None:
327 """Prefix 'src' (no slash) matches 'src/a.py' and also 'src_util.py'."""
328 repo = _make_repo(tmp_path)
329 sid = _snap(repo, {
330 "src/a.py": _oid(1),
331 "src_util.py": _oid(2),
332 "tests/b.py": _oid(3),
333 })
334 data = json.loads(_rsj(repo, "--path-prefix", "src", sid).output)
335 assert "src/a.py" in data["manifest"]
336 assert "src_util.py" in data["manifest"]
337 assert "tests/b.py" not in data["manifest"]
338
339 def test_prefix_exact_filename_match(self, tmp_path: pathlib.Path) -> None:
340 """A prefix equal to an exact filename matches only that file."""
341 repo = _make_repo(tmp_path)
342 sid = _snap(repo, {"README.md": _oid(1), "README.md.bak": _oid(2)})
343 data = json.loads(_rsj(repo, "--path-prefix", "README.md", sid).output)
344 assert "README.md" in data["manifest"]
345 assert "README.md.bak" in data["manifest"] # startswith matches both
346
347 def test_prefix_no_match_empty_manifest_with_duration(self, tmp_path: pathlib.Path) -> None:
348 """No-match prefix returns empty manifest with duration_ms."""
349 repo = _make_repo(tmp_path)
350 sid = _snap(repo, {"src/a.py": _oid(1)})
351 data = json.loads(_rsj(repo, "--path-prefix", "nonexistent/", sid).output)
352 assert data["file_count"] == 0
353 assert data["manifest"] == {}
354 assert "duration_ms" in data
355
356
357 # ---------------------------------------------------------------------------
358 # Security
359 # ---------------------------------------------------------------------------
360
361
362 class TestSecuritySupercharge:
363 def test_path_prefix_with_traversal_attempt(self, tmp_path: pathlib.Path) -> None:
364 """Path prefix with '../' traversal must not escape manifest keys."""
365 repo = _make_repo(tmp_path)
366 sid = _snap(repo, {"src/a.py": _oid(1), "../etc/passwd": _oid(2)})
367 # The manifest key itself is literally '../etc/passwd' — filter should match it
368 # only if the prefix is '../', not silently escape the repo root
369 data = json.loads(_rsj(repo, "--path-prefix", "src/", sid).output)
370 # Only src/a.py should match src/ prefix
371 assert "src/a.py" in data["manifest"]
372 assert "../etc/passwd" not in data["manifest"]
373
374 def test_no_traceback_on_sha256_prefixed_missing_id(self, tmp_path: pathlib.Path) -> None:
375 """Valid sha256: format but non-existent ID — no traceback, clean error."""
376 repo = _make_repo(tmp_path)
377 missing = long_id("dead" * 16)
378 result = _rs(repo, missing)
379 assert result.exit_code == ExitCode.USER_ERROR
380 assert "Traceback" not in result.output
381
382
383 # ---------------------------------------------------------------------------
384 # Performance
385 # ---------------------------------------------------------------------------
386
387
388 class TestPerformanceSupercharge:
389 def test_single_read_under_500ms(self, tmp_path: pathlib.Path) -> None:
390 repo = _make_repo(tmp_path)
391 sid = _snap(repo, {"a.py": _oid(0)})
392 t0 = time.monotonic()
393 result = _rs(repo, sid)
394 duration_ms = (time.monotonic() - t0) * 1000
395 assert result.exit_code == 0
396 assert duration_ms < 500
397
398 def test_1000_file_manifest_under_1000ms(self, tmp_path: pathlib.Path) -> None:
399 repo = _make_repo(tmp_path)
400 manifest = {f"src/module{i:04d}.py": _oid(i) for i in range(1000)}
401 sid = _snap(repo, manifest)
402 t0 = time.monotonic()
403 result = _rs(repo, sid)
404 duration_ms = (time.monotonic() - t0) * 1000
405 assert result.exit_code == 0
406 assert duration_ms < 1000
407
408 def test_duration_ms_plausible(self, tmp_path: pathlib.Path) -> None:
409 """duration_ms from the output itself must be < 500ms for a warm read."""
410 repo = _make_repo(tmp_path)
411 sid = _snap(repo, {"a.py": _oid(0)})
412 data = json.loads(_rsj(repo, sid).output)
413 assert data["duration_ms"] < 500
414
415
416 class TestRegisterFlags:
417 def _parse(self, *args: str) -> "argparse.Namespace":
418 import argparse
419 from muse.cli.commands.read_snapshot import register
420 p = argparse.ArgumentParser()
421 subs = p.add_subparsers()
422 register(subs)
423 return p.parse_args(["read-snapshot", fake_id("a"), *args])
424
425 def test_json_short_flag(self):
426 args = self._parse("-j")
427 assert args.json_out is True
428
429 def test_json_long_flag(self):
430 args = self._parse("--json")
431 assert args.json_out is True
432
433 def test_default_no_json(self):
434 args = self._parse()
435 assert args.json_out is False
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 136 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 142 days ago