gabriel / muse public
test_cmd_cat_object.py python
646 lines 25.1 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Comprehensive tests for ``muse cat-object``.
2
3 Coverage tiers
4 --------------
5 - Unit: _CHUNK constant, _FORMAT_CHOICES
6 - Integration: raw/info formats, --json alias, missing/invalid object_id,
7 duration_ms in JSON output, --inline base64 content embedding
8 - Batch: --batch happy path, missing OIDs, mixed, binary, --batch-check,
9 sha256:-prefixed OIDs from stdin, invalid OIDs handled as missing,
10 empty lines skipped, large objects
11 - Security: ANSI in object_id error, path traversal object_id
12 - Stress: 10 MiB object streaming, 200 sequential reads
13 """
14 from __future__ import annotations
15
16 import json
17 import pathlib
18
19 from muse.core._types import blob_id, fake_id, long_id
20 from muse.core.errors import ExitCode
21 from muse.core.object_store import write_object
22 from tests.cli_test_helper import CliRunner, InvokeResult
23
24 runner = CliRunner()
25
26
27 # ---------------------------------------------------------------------------
28 # Helpers
29 # ---------------------------------------------------------------------------
30
31 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
32 """Minimal .muse/ structure."""
33 repo = tmp_path / "repo"
34 muse = repo / ".muse"
35 for sub in ("objects", "commits", "snapshots", "refs/heads"):
36 (muse / sub).mkdir(parents=True)
37 (muse / "HEAD").write_text("ref: refs/heads/main")
38 (muse / "repo.json").write_text(json.dumps({"repo_id": "test", "domain": "code"}))
39 return repo
40
41
42 def _store(repo: pathlib.Path, content: bytes) -> str:
43 """Write content to the object store and return its canonical object_id (sha256: prefix)."""
44 oid = blob_id(content)
45 write_object(repo, oid, content)
46 return oid
47
48
49 def _cat(repo: pathlib.Path, *args: str, stdin: str | bytes | None = None) -> InvokeResult:
50 from muse.cli.app import main as cli
51 return runner.invoke(
52 cli,
53 ["cat-object", *args],
54 env={"MUSE_REPO_ROOT": str(repo)},
55 input=stdin,
56 )
57
58
59 # ---------------------------------------------------------------------------
60 # Unit — module constants
61 # ---------------------------------------------------------------------------
62
63
64 class TestConstants:
65 def test_chunk_size_is_64kib(self) -> None:
66 from muse.cli.commands.cat_object import _CHUNK
67 assert _CHUNK == 65536
68
69 def test_format_choices_correct(self) -> None:
70 from muse.cli.commands.cat_object import _FORMAT_CHOICES
71 assert "raw" in _FORMAT_CHOICES
72 assert "info" in _FORMAT_CHOICES
73 assert "json" not in _FORMAT_CHOICES
74
75
76 # ---------------------------------------------------------------------------
77 # Integration — raw format (single-object mode)
78 # ---------------------------------------------------------------------------
79
80
81 class TestRawFormat:
82 def test_raw_bytes_match_stored_content(self, tmp_path: pathlib.Path) -> None:
83 repo = _make_repo(tmp_path)
84 content = b"hello object store"
85 oid = _store(repo, content)
86 result = _cat(repo, oid)
87 assert result.exit_code == 0
88 assert result.stdout_bytes == content
89
90 def test_raw_is_default_format(self, tmp_path: pathlib.Path) -> None:
91 repo = _make_repo(tmp_path)
92 content = b"default format"
93 oid = _store(repo, content)
94 result = _cat(repo, oid)
95 assert result.exit_code == 0
96 assert result.stdout_bytes == content
97
98 def test_raw_binary_content_preserved(self, tmp_path: pathlib.Path) -> None:
99 repo = _make_repo(tmp_path)
100 content = bytes(range(256))
101 oid = _store(repo, content)
102 result = _cat(repo, oid)
103 assert result.exit_code == 0
104 assert result.stdout_bytes == content
105
106 def test_raw_empty_object(self, tmp_path: pathlib.Path) -> None:
107 repo = _make_repo(tmp_path)
108 content = b""
109 oid = _store(repo, content)
110 result = _cat(repo, oid)
111 assert result.exit_code == 0
112 assert result.stdout_bytes == content
113
114 def test_explicit_format_raw(self, tmp_path: pathlib.Path) -> None:
115 repo = _make_repo(tmp_path)
116 content = b"explicit raw"
117 oid = _store(repo, content)
118 result = _cat(repo, oid)
119 assert result.exit_code == 0
120 assert result.stdout_bytes == content
121
122
123 # ---------------------------------------------------------------------------
124 # Integration — info / --json format (single-object mode)
125 # ---------------------------------------------------------------------------
126
127
128 class TestInfoFormat:
129 def test_info_format_shape(self, tmp_path: pathlib.Path) -> None:
130 repo = _make_repo(tmp_path)
131 content = b"info content"
132 oid = _store(repo, content)
133 result = _cat(repo, "--json", oid)
134 assert result.exit_code == 0
135 data = json.loads(result.output)
136 assert data["object_id"] == oid
137 assert data["present"] is True
138 assert data["size_bytes"] == len(content)
139
140 def test_json_flag_is_alias_for_info(self, tmp_path: pathlib.Path) -> None:
141 repo = _make_repo(tmp_path)
142 content = b"json alias test"
143 oid = _store(repo, content)
144 result = _cat(repo, "--json", oid)
145 assert result.exit_code == 0, f"--json failed: {result.output}"
146 data = json.loads(result.output)
147 assert data["object_id"] == oid
148 assert data["present"] is True
149 assert data["size_bytes"] == len(content)
150
151 def test_info_does_not_emit_content(self, tmp_path: pathlib.Path) -> None:
152 repo = _make_repo(tmp_path)
153 content = b"secret bytes"
154 oid = _store(repo, content)
155 result = _cat(repo, "--json", oid)
156 assert result.exit_code == 0
157 data = json.loads(result.output)
158 assert "object_id" in data
159 assert content not in result.output.encode()
160
161 def test_info_size_matches_actual_file(self, tmp_path: pathlib.Path) -> None:
162 repo = _make_repo(tmp_path)
163 content = b"size check " * 100
164 oid = _store(repo, content)
165 result = _cat(repo, "--json", oid)
166 data = json.loads(result.output)
167 assert data["size_bytes"] == len(content)
168
169 def test_missing_object_info_has_present_false(self, tmp_path: pathlib.Path) -> None:
170 repo = _make_repo(tmp_path)
171 oid = fake_id("missing-info-a")
172 result = _cat(repo, "--json", oid)
173 assert result.exit_code == ExitCode.USER_ERROR
174 data = json.loads(result.output)
175 assert data["present"] is False
176 assert data["size_bytes"] == 0
177
178 def test_json_flag_missing_object_has_present_false(self, tmp_path: pathlib.Path) -> None:
179 repo = _make_repo(tmp_path)
180 oid = fake_id("missing-json-b")
181 result = _cat(repo, "--json", oid)
182 assert result.exit_code == ExitCode.USER_ERROR
183 data = json.loads(result.output)
184 assert data["present"] is False
185
186 def test_json_output_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
187 repo = _make_repo(tmp_path)
188 content = b"elapsed timing test"
189 oid = _store(repo, content)
190 result = _cat(repo, "--json", oid)
191 assert result.exit_code == 0
192 data = json.loads(result.output)
193 assert "duration_ms" in data
194 assert isinstance(data["duration_ms"], float)
195 assert data["duration_ms"] >= 0.0
196
197 def test_json_duration_ms_present_for_missing_object(self, tmp_path: pathlib.Path) -> None:
198 repo = _make_repo(tmp_path)
199 oid = fake_id("missing-duration-c")
200 result = _cat(repo, "--json", oid)
201 assert result.exit_code == ExitCode.USER_ERROR
202 data = json.loads(result.output)
203 assert "duration_ms" in data
204
205
206 # ---------------------------------------------------------------------------
207 # Integration — error paths (single-object mode)
208 # ---------------------------------------------------------------------------
209
210
211 class TestErrorPaths:
212 def test_missing_object_raw_errors(self, tmp_path: pathlib.Path) -> None:
213 repo = _make_repo(tmp_path)
214 result = _cat(repo, fake_id("missing-raw-c"))
215 assert result.exit_code == ExitCode.USER_ERROR
216
217 def test_invalid_object_id_bare_hex_rejected(self, tmp_path: pathlib.Path) -> None:
218 """Bare hex without sha256: prefix is rejected — use sha256:<hex> form."""
219 repo = _make_repo(tmp_path)
220 result = _cat(repo, "a" * 64)
221 assert result.exit_code == ExitCode.USER_ERROR
222
223 def test_invalid_object_id_too_short(self, tmp_path: pathlib.Path) -> None:
224 repo = _make_repo(tmp_path)
225 result = _cat(repo, "abc123")
226 assert result.exit_code == ExitCode.USER_ERROR
227
228 def test_invalid_object_id_uppercase_content(self, tmp_path: pathlib.Path) -> None:
229 repo = _make_repo(tmp_path)
230 result = _cat(repo, long_id("A" * 64))
231 assert result.exit_code == ExitCode.USER_ERROR
232
233 def test_invalid_object_id_non_hex_content(self, tmp_path: pathlib.Path) -> None:
234 repo = _make_repo(tmp_path)
235 result = _cat(repo, long_id("z" * 64))
236 assert result.exit_code == ExitCode.USER_ERROR
237
238 def test_unrecognized_flag_errors(self, tmp_path: pathlib.Path) -> None:
239 repo = _make_repo(tmp_path)
240 result = _cat(repo, "--no-such-flag", fake_id("bad-flag-a"))
241 assert result.exit_code != 0
242
243 def test_no_object_id_without_batch_flag_errors(self, tmp_path: pathlib.Path) -> None:
244 repo = _make_repo(tmp_path)
245 result = _cat(repo)
246 assert result.exit_code == ExitCode.USER_ERROR
247
248 def test_no_repo_errors(self, tmp_path: pathlib.Path) -> None:
249 from muse.cli.app import main as cli
250 result = runner.invoke(
251 cli,
252 ["cat-object", fake_id("no-repo-a")],
253 env={"MUSE_REPO_ROOT": str(tmp_path / "no_repo")},
254 )
255 assert result.exit_code != 0
256
257
258 # ---------------------------------------------------------------------------
259 # Batch mode — --batch
260 # ---------------------------------------------------------------------------
261
262
263 class TestBatchMode:
264 def test_batch_single_object_emits_header_and_content(self, tmp_path: pathlib.Path) -> None:
265 repo = _make_repo(tmp_path)
266 content = b"batch content"
267 oid = _store(repo, content)
268 result = _cat(repo, "--batch", stdin=f"{oid}\n")
269 assert result.exit_code == 0
270 raw = result.stdout_bytes
271 # Header: "<oid> blob <size>\n"
272 header_line = f"{oid} blob {len(content)}\n".encode()
273 assert raw.startswith(header_line)
274 # Content follows header, then a trailing newline
275 body = raw[len(header_line):]
276 assert body == content + b"\n"
277
278 def test_batch_missing_oid_emits_missing(self, tmp_path: pathlib.Path) -> None:
279 repo = _make_repo(tmp_path)
280 oid = fake_id("missing-batch-d")
281 result = _cat(repo, "--batch", stdin=f"{oid}\n")
282 assert result.exit_code == 0
283 assert result.stdout_bytes == f"{oid} missing\n".encode()
284
285 def test_batch_invalid_oid_emits_missing(self, tmp_path: pathlib.Path) -> None:
286 """Invalid OIDs should produce a 'missing' line, not an error exit."""
287 repo = _make_repo(tmp_path)
288 result = _cat(repo, "--batch", stdin="not-a-valid-oid\n")
289 assert result.exit_code == 0
290 assert b"missing" in result.stdout_bytes
291
292 def test_batch_mixed_present_and_missing(self, tmp_path: pathlib.Path) -> None:
293 repo = _make_repo(tmp_path)
294 c1 = b"first"
295 c2 = b"second"
296 oid1 = _store(repo, c1)
297 oid2 = _store(repo, c2)
298 missing = fake_id("missing-mixed-e")
299 stdin = f"{oid1}\n{missing}\n{oid2}\n"
300 result = _cat(repo, "--batch", stdin=stdin)
301 assert result.exit_code == 0
302 raw = result.stdout_bytes
303
304 # oid1 present
305 assert f"{oid1} blob {len(c1)}\n".encode() in raw
306 assert c1 in raw
307 # missing
308 assert f"{missing} missing\n".encode() in raw
309 # oid2 present
310 assert f"{oid2} blob {len(c2)}\n".encode() in raw
311 assert c2 in raw
312
313 def test_batch_empty_lines_skipped(self, tmp_path: pathlib.Path) -> None:
314 repo = _make_repo(tmp_path)
315 content = b"hello"
316 oid = _store(repo, content)
317 # stdin has empty lines before and after
318 result = _cat(repo, "--batch", stdin=f"\n\n{oid}\n\n")
319 assert result.exit_code == 0
320 assert f"{oid} blob {len(content)}\n".encode() in result.stdout_bytes
321
322 def test_batch_binary_content_round_trips(self, tmp_path: pathlib.Path) -> None:
323 repo = _make_repo(tmp_path)
324 content = bytes(range(256))
325 oid = _store(repo, content)
326 result = _cat(repo, "--batch", stdin=f"{oid}\n")
327 assert result.exit_code == 0
328 raw = result.stdout_bytes
329 header = f"{oid} blob {len(content)}\n".encode()
330 body = raw[len(header):-1] # strip trailing newline
331 assert body == content
332
333 def test_batch_empty_stdin_produces_no_output(self, tmp_path: pathlib.Path) -> None:
334 repo = _make_repo(tmp_path)
335 result = _cat(repo, "--batch", stdin="")
336 assert result.exit_code == 0
337 assert result.stdout_bytes == b""
338
339 def test_batch_multiple_objects_in_order(self, tmp_path: pathlib.Path) -> None:
340 repo = _make_repo(tmp_path)
341 objects = [(b"alpha", ), (b"beta",), (b"gamma",)]
342 oids = [_store(repo, c[0]) for c in objects]
343 stdin = "\n".join(oids) + "\n"
344 result = _cat(repo, "--batch", stdin=stdin)
345 assert result.exit_code == 0
346 raw = result.stdout_bytes
347 pos = 0
348 for oid, (content,) in zip(oids, objects):
349 header = f"{oid} blob {len(content)}\n".encode()
350 assert raw[pos:pos + len(header)] == header
351 pos += len(header)
352 assert raw[pos:pos + len(content)] == content
353 pos += len(content) + 1 # +1 for trailing '\n'
354
355 def test_batch_mutually_exclusive_with_batch_check(self, tmp_path: pathlib.Path) -> None:
356 repo = _make_repo(tmp_path)
357 result = _cat(repo, "--batch", "--batch-check", stdin="")
358 assert result.exit_code != 0
359
360
361 # ---------------------------------------------------------------------------
362 # Batch-check mode — --batch-check
363 # ---------------------------------------------------------------------------
364
365
366 class TestBatchCheckMode:
367 def test_batch_check_emits_header_only_no_content(self, tmp_path: pathlib.Path) -> None:
368 repo = _make_repo(tmp_path)
369 content = b"check only"
370 oid = _store(repo, content)
371 result = _cat(repo, "--batch-check", stdin=f"{oid}\n")
372 assert result.exit_code == 0
373 raw = result.stdout_bytes
374 expected = f"{oid} blob {len(content)}\n".encode()
375 assert raw == expected
376 # Content bytes must NOT appear
377 assert content not in raw
378
379 def test_batch_check_missing_emits_missing(self, tmp_path: pathlib.Path) -> None:
380 repo = _make_repo(tmp_path)
381 oid = fake_id("missing-check-f")
382 result = _cat(repo, "--batch-check", stdin=f"{oid}\n")
383 assert result.exit_code == 0
384 assert result.stdout_bytes == f"{oid} missing\n".encode()
385
386 def test_batch_check_invalid_oid_emits_missing(self, tmp_path: pathlib.Path) -> None:
387 repo = _make_repo(tmp_path)
388 result = _cat(repo, "--batch-check", stdin="bad\n")
389 assert result.exit_code == 0
390 assert b"missing" in result.stdout_bytes
391
392 def test_batch_check_mixed(self, tmp_path: pathlib.Path) -> None:
393 repo = _make_repo(tmp_path)
394 c1 = b"present"
395 oid1 = _store(repo, c1)
396 missing = fake_id("missing-check-0")
397 result = _cat(repo, "--batch-check", stdin=f"{oid1}\n{missing}\n")
398 assert result.exit_code == 0
399 raw = result.stdout_bytes
400 assert f"{oid1} blob {len(c1)}\n".encode() in raw
401 assert f"{missing} missing\n".encode() in raw
402 # No content bytes
403 assert c1 not in raw
404
405 def test_batch_check_size_accurate(self, tmp_path: pathlib.Path) -> None:
406 repo = _make_repo(tmp_path)
407 content = b"x" * 1000
408 oid = _store(repo, content)
409 result = _cat(repo, "--batch-check", stdin=f"{oid}\n")
410 assert result.exit_code == 0
411 line = result.stdout_bytes.decode()
412 parts = line.strip().split()
413 assert parts[0] == oid
414 assert parts[1] == "blob"
415 assert int(parts[2]) == len(content)
416
417
418 # ---------------------------------------------------------------------------
419 # Security
420 # ---------------------------------------------------------------------------
421
422
423 class TestSecurity:
424 def test_ansi_in_invalid_id_not_in_output(self, tmp_path: pathlib.Path) -> None:
425 repo = _make_repo(tmp_path)
426 evil = "\x1b[31m" + "a" * 60
427 result = _cat(repo, evil)
428 assert result.exit_code == ExitCode.USER_ERROR
429 assert "\x1b" not in result.output
430
431 def test_path_traversal_in_object_id_rejected(self, tmp_path: pathlib.Path) -> None:
432 repo = _make_repo(tmp_path)
433 result = _cat(repo, "../../../etc/passwd")
434 assert result.exit_code == ExitCode.USER_ERROR
435
436 def test_null_byte_in_object_id_rejected(self, tmp_path: pathlib.Path) -> None:
437 repo = _make_repo(tmp_path)
438 result = _cat(repo, "a" * 32 + "\x00" + "b" * 31)
439 assert result.exit_code == ExitCode.USER_ERROR
440
441 def test_no_traceback_on_invalid_id(self, tmp_path: pathlib.Path) -> None:
442 repo = _make_repo(tmp_path)
443 result = _cat(repo, "not-a-valid-id")
444 assert "Traceback" not in result.output
445
446 def test_batch_path_traversal_treated_as_missing(self, tmp_path: pathlib.Path) -> None:
447 """In batch mode, bad OIDs are not errors — they are reported as missing."""
448 repo = _make_repo(tmp_path)
449 result = _cat(repo, "--batch", stdin="../../../etc/passwd\n")
450 assert result.exit_code == 0
451 assert b"missing" in result.stdout_bytes
452
453
454 # ---------------------------------------------------------------------------
455 # Stress
456 # ---------------------------------------------------------------------------
457
458
459 class TestStress:
460 def test_large_object_streams_without_oom(self, tmp_path: pathlib.Path) -> None:
461 repo = _make_repo(tmp_path)
462 content = b"Z" * (10 * 1024 * 1024) # 10 MiB
463 oid = _store(repo, content)
464 result = _cat(repo, oid)
465 assert result.exit_code == 0
466 assert len(result.stdout_bytes) == len(content)
467 assert result.stdout_bytes == content
468
469 def test_large_object_info_is_fast(self, tmp_path: pathlib.Path) -> None:
470 repo = _make_repo(tmp_path)
471 content = b"Y" * (10 * 1024 * 1024)
472 oid = _store(repo, content)
473 result = _cat(repo, "--json", oid)
474 assert result.exit_code == 0
475 data = json.loads(result.output)
476 assert data["size_bytes"] == len(content)
477
478 def test_200_sequential_reads(self, tmp_path: pathlib.Path) -> None:
479 repo = _make_repo(tmp_path)
480 content = b"repeated read"
481 oid = _store(repo, content)
482 for i in range(200):
483 result = _cat(repo, oid)
484 assert result.exit_code == 0, f"failed at iteration {i}"
485 assert result.stdout_bytes == content
486
487 def test_batch_50_objects(self, tmp_path: pathlib.Path) -> None:
488 """50 objects through a single --batch invocation."""
489 repo = _make_repo(tmp_path)
490 pairs: list[tuple[str, bytes]] = []
491 for i in range(50):
492 content = f"object-{i:03d}".encode()
493 oid = _store(repo, content)
494 pairs.append((oid, content))
495 stdin = "\n".join(oid for oid, _ in pairs) + "\n"
496 result = _cat(repo, "--batch", stdin=stdin)
497 assert result.exit_code == 0
498 raw = result.stdout_bytes
499 for oid, content in pairs:
500 assert f"{oid} blob {len(content)}\n".encode() in raw
501 assert content in raw
502
503 def test_batch_check_100_objects(self, tmp_path: pathlib.Path) -> None:
504 """100 objects through --batch-check — no content read."""
505 repo = _make_repo(tmp_path)
506 oids = []
507 sizes = []
508 for i in range(100):
509 content = b"x" * (i + 1)
510 oid = _store(repo, content)
511 oids.append(oid)
512 sizes.append(len(content))
513 stdin = "\n".join(oids) + "\n"
514 result = _cat(repo, "--batch-check", stdin=stdin)
515 assert result.exit_code == 0
516 lines = result.stdout_bytes.decode().strip().splitlines()
517 assert len(lines) == 100
518 for line, oid, size in zip(lines, oids, sizes):
519 parts = line.split()
520 assert parts[0] == oid
521 assert parts[1] == "blob"
522 assert int(parts[2]) == size
523
524
525 # ---------------------------------------------------------------------------
526 # --inline — base64 content embedding in JSON (agent round-trip saver)
527 # ---------------------------------------------------------------------------
528
529
530 class TestInline:
531 """--inline embeds base64-encoded content in the --json output.
532
533 Agents that need both metadata and content for small objects can get both
534 in a single invocation instead of two (--json for metadata, raw for bytes).
535 """
536
537 def test_inline_embeds_content_b64(self, tmp_path: pathlib.Path) -> None:
538 import base64
539 repo = _make_repo(tmp_path)
540 content = b"hello inline"
541 oid = _store(repo, content)
542 result = _cat(repo, "--json", "--inline", oid)
543 assert result.exit_code == 0
544 data = json.loads(result.output)
545 assert "content_b64" in data
546 assert base64.b64decode(data["content_b64"]) == content
547
548 def test_inline_requires_json_flag(self, tmp_path: pathlib.Path) -> None:
549 """--inline without --json is a user error."""
550 repo = _make_repo(tmp_path)
551 content = b"inline needs json"
552 oid = _store(repo, content)
553 result = _cat(repo, "--inline", oid)
554 assert result.exit_code == ExitCode.USER_ERROR
555
556 def test_inline_binary_content_round_trips(self, tmp_path: pathlib.Path) -> None:
557 import base64
558 repo = _make_repo(tmp_path)
559 content = bytes(range(256))
560 oid = _store(repo, content)
561 result = _cat(repo, "--json", "--inline", oid)
562 assert result.exit_code == 0
563 data = json.loads(result.output)
564 assert base64.b64decode(data["content_b64"]) == content
565
566 def test_inline_missing_object_no_content_b64(self, tmp_path: pathlib.Path) -> None:
567 repo = _make_repo(tmp_path)
568 oid = fake_id("missing-inline-a")
569 result = _cat(repo, "--json", "--inline", oid)
570 assert result.exit_code == ExitCode.USER_ERROR
571 data = json.loads(result.output)
572 assert data["present"] is False
573 assert "content_b64" not in data
574
575 def test_inline_json_still_has_standard_fields(self, tmp_path: pathlib.Path) -> None:
576 import base64
577 repo = _make_repo(tmp_path)
578 content = b"standard fields check"
579 oid = _store(repo, content)
580 result = _cat(repo, "--json", "--inline", oid)
581 assert result.exit_code == 0
582 data = json.loads(result.output)
583 assert data["object_id"] == oid
584 assert data["present"] is True
585 assert data["size_bytes"] == len(content)
586 assert "duration_ms" in data
587 assert isinstance(base64.b64decode(data["content_b64"]), bytes)
588
589 def test_inline_empty_object(self, tmp_path: pathlib.Path) -> None:
590 import base64
591 repo = _make_repo(tmp_path)
592 content = b""
593 oid = _store(repo, content)
594 result = _cat(repo, "--json", "--inline", oid)
595 assert result.exit_code == 0
596 data = json.loads(result.output)
597 assert data["content_b64"] == base64.b64encode(b"").decode()
598
599 def test_no_inline_flag_has_no_content_b64(self, tmp_path: pathlib.Path) -> None:
600 """Without --inline the JSON output must NOT include content_b64."""
601 repo = _make_repo(tmp_path)
602 content = b"no inline here"
603 oid = _store(repo, content)
604 result = _cat(repo, "--json", oid)
605 assert result.exit_code == 0
606 data = json.loads(result.output)
607 assert "content_b64" not in data
608
609
610 # ---------------------------------------------------------------------------
611 # Flag registration tests
612 # ---------------------------------------------------------------------------
613
614 import argparse as _argparse
615 from muse.cli.commands.cat_object import register as _register_cat_object
616
617
618 def _parse_co(*args: str) -> _argparse.Namespace:
619 """Build an argument parser via register() and parse args."""
620 root_p = _argparse.ArgumentParser()
621 subs = root_p.add_subparsers(dest="cmd")
622 _register_cat_object(subs)
623 return root_p.parse_args(["cat-object", *args])
624
625
626 class TestRegisterFlags:
627 def test_default_json_out_is_false(self) -> None:
628 ns = _parse_co(fake_id("a"))
629 assert ns.json_out is False
630
631 def test_json_flag_sets_json_out(self) -> None:
632 ns = _parse_co(fake_id("a"), "--json")
633 assert ns.json_out is True
634
635 def test_j_shorthand_sets_json_out(self) -> None:
636 ns = _parse_co(fake_id("a"), "-j")
637 assert ns.json_out is True
638
639 def test_inline_flag(self) -> None:
640 ns = _parse_co(fake_id("a"), "--json", "--inline")
641 assert ns.inline is True
642
643 def test_format_flag_no_longer_exists(self) -> None:
644 import pytest
645 with pytest.raises(SystemExit):
646 _parse_co(fake_id("a"), "--format", "info")
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago