gabriel / muse public
test_cat_supercharge.py python
632 lines 24.0 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 20 days ago
1 """Supercharged tests for ``muse code cat`` (symbol-level).
2
3 New features under TDD:
4 --limit N cap --all results; truncated + total_symbols in JSON
5 total_symbols always present in --all --json output
6 redirected_from JSON field when global-fallback fires
7 fmt bug fix no NameError when --json + no addresses
8
9 7-tier coverage
10 ---------------
11 Unit _resolve_symbol edge cases
12 Integration --limit, total_symbols, redirected_from, fmt-bug
13 E2E --at historical ref; --limit round-trip
14 Security (file-level security covered by test_cmd_core_cat.py)
15 Stress --limit 10 of 200 symbols fast
16 Data integrity source matches actual bytes; --at gives different content than HEAD
17 Performance --limit faster than full --all
18 """
19
20 from __future__ import annotations
21 from collections.abc import Mapping
22
23 import json
24 import pathlib
25 import textwrap
26 import time
27
28 import pytest
29
30 from tests.cli_test_helper import CliRunner
31 from muse.core.object_store import write_object
32 from muse.core.ids import hash_commit, hash_snapshot
33 from muse.core.commits import (
34 CommitRecord,
35 write_commit,
36 )
37 from muse.core.snapshots import (
38 SnapshotRecord,
39 write_snapshot,
40 )
41 import datetime
42 from muse.core.types import blob_id, long_id
43 from muse.core.paths import muse_dir, ref_path
44
45 cli = None
46 runner = CliRunner()
47
48 _REPO_ID = "cat-sc-test"
49 _counter = 0
50
51
52 # ---------------------------------------------------------------------------
53 # Helpers
54 # ---------------------------------------------------------------------------
55
56
57 def _init_repo(path: pathlib.Path, repo_id: str = _REPO_ID) -> pathlib.Path:
58 dot_muse = muse_dir(path)
59 for d in ("commits", "snapshots", "objects", "refs/heads"):
60 (dot_muse / d).mkdir(parents=True, exist_ok=True)
61 (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
62 (dot_muse / "repo.json").write_text(
63 json.dumps({"repo_id": repo_id, "domain": "code"}), encoding="utf-8"
64 )
65 return path
66
67
68 def _env(repo: pathlib.Path) -> Mapping[str, str]:
69 return {"MUSE_REPO_ROOT": str(repo)}
70
71
72 def _add_file(repo: pathlib.Path, rel_path: str, content: bytes) -> str:
73 """Write a file to disk and return its object_id."""
74 obj_id = blob_id(content)
75 write_object(repo, obj_id, content)
76 full_path = repo / rel_path
77 full_path.parent.mkdir(parents=True, exist_ok=True)
78 full_path.write_bytes(content)
79 return obj_id
80
81
82 def _make_commit(
83 repo: pathlib.Path,
84 files: dict[str, bytes],
85 message: str = "commit",
86 parent_id: str | None = None,
87 branch: str = "main",
88 ) -> str:
89 global _counter
90 _counter += 1
91 manifest: dict[str, str] = {}
92 for rel_path, content in files.items():
93 obj_id = _add_file(repo, rel_path, content)
94 manifest[rel_path] = obj_id
95 snap_id = hash_snapshot(manifest)
96 write_snapshot(repo, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
97 committed_at = datetime.datetime.now(datetime.timezone.utc)
98 parent_ids = [parent_id] if parent_id else []
99 commit_id = hash_commit(
100 parent_ids=parent_ids,
101 snapshot_id=snap_id,
102 message=message,
103 committed_at_iso=committed_at.isoformat(),
104 )
105 write_commit(repo, CommitRecord(
106 commit_id=commit_id,
107 branch=branch,
108 snapshot_id=snap_id,
109 message=message,
110 committed_at=committed_at,
111 parent_commit_id=parent_id,
112 ))
113 (ref_path(repo, branch)).write_text(commit_id, encoding="utf-8")
114 return commit_id
115
116
117 _SIMPLE_PY = textwrap.dedent("""\
118 def hello():
119 return "hello"
120
121 def world():
122 return "world"
123
124 class Greeter:
125 def greet(self):
126 return "hi"
127 """)
128
129 _UPDATED_PY = textwrap.dedent("""\
130 def hello():
131 return "hello updated"
132
133 def world():
134 return "world"
135
136 class Greeter:
137 def greet(self):
138 return "hi"
139 """)
140
141
142 # ---------------------------------------------------------------------------
143 # Fixtures
144 # ---------------------------------------------------------------------------
145
146
147 @pytest.fixture
148 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
149 _init_repo(tmp_path)
150 _make_commit(tmp_path, {"mod.py": _SIMPLE_PY.encode()}, message="initial")
151 return tmp_path
152
153
154 @pytest.fixture
155 def two_commit_repo(tmp_path: pathlib.Path) -> pathlib.Path:
156 """Repo with two commits — mod.py changes between them."""
157 _init_repo(tmp_path)
158 cid1 = _make_commit(tmp_path, {"mod.py": _SIMPLE_PY.encode()}, message="v1")
159 _make_commit(
160 tmp_path, {"mod.py": _UPDATED_PY.encode()}, message="v2", parent_id=cid1
161 )
162 return tmp_path
163
164
165 # ---------------------------------------------------------------------------
166 # Bug fix: fmt NameError when --json and no addresses
167 # ---------------------------------------------------------------------------
168
169
170 class TestFmtBugFix:
171 """fmt NameError fix — targets muse code cat (symbol-level)."""
172
173 def test_no_address_json_flag_no_unbound_error(self, repo: pathlib.Path) -> None:
174 """Passing --json with no address must not raise UnboundLocalError."""
175 result = runner.invoke(cli, ["code", "cat", "--json"], env=_env(repo))
176 assert result.exit_code != 0
177 assert "UnboundLocalError" not in result.output
178 assert "Traceback" not in result.output
179
180 def test_no_address_text_mode_no_crash(self, repo: pathlib.Path) -> None:
181 result = runner.invoke(cli, ["code", "cat"], env=_env(repo))
182 assert result.exit_code != 0
183 assert "UnboundLocalError" not in result.output
184
185 def test_no_address_json_contains_error_key(self, repo: pathlib.Path) -> None:
186 result = runner.invoke(cli, ["code", "cat", "--json"], env=_env(repo))
187 assert result.exit_code != 0
188 data = json.loads(result.output)
189 assert "error" in data
190
191
192 # ---------------------------------------------------------------------------
193 # Integration: total_symbols in --all --json
194 # ---------------------------------------------------------------------------
195
196
197 class TestAllJsonTotalSymbols:
198 def test_all_json_has_total_symbols(self, repo: pathlib.Path) -> None:
199 result = runner.invoke(
200 cli, ["code", "cat", "mod.py", "--all", "--json"], env=_env(repo)
201 )
202 assert result.exit_code == 0
203 data = json.loads(result.output)
204 assert "total_symbols" in data
205
206 def test_all_json_total_symbols_count(self, repo: pathlib.Path) -> None:
207 """total_symbols == len(results) when no kind filter."""
208 result = runner.invoke(
209 cli, ["code", "cat", "mod.py", "--all", "--json"], env=_env(repo)
210 )
211 data = json.loads(result.output)
212 assert data["total_symbols"] == len(data["results"])
213
214 def test_all_json_kind_filter_shows_unfiltered_total(
215 self, repo: pathlib.Path
216 ) -> None:
217 """When --kind filters, total_symbols reflects pre-filter count."""
218 all_result = runner.invoke(
219 cli, ["code", "cat", "mod.py", "--all", "--json"], env=_env(repo)
220 )
221 total = json.loads(all_result.output)["total_symbols"]
222
223 func_result = runner.invoke(
224 cli, ["code", "cat", "mod.py", "--all", "--kind", "function", "--json"],
225 env=_env(repo),
226 )
227 func_data = json.loads(func_result.output)
228 # total_symbols should be the pre-filter total, not just functions
229 assert func_data["total_symbols"] == total
230 # But results should only have functions
231 assert all(r["kind"] == "function" for r in func_data["results"])
232
233 def test_all_json_total_symbols_stable_across_filters(
234 self, repo: pathlib.Path
235 ) -> None:
236 """total_symbols is the same regardless of --kind filter."""
237 base = json.loads(
238 runner.invoke(cli, ["code", "cat", "mod.py", "--all", "--json"], env=_env(repo)).output
239 )["total_symbols"]
240 for kind in ("function", "method", "class"):
241 data = json.loads(
242 runner.invoke(
243 cli, ["code", "cat", "mod.py", "--all", "--kind", kind, "--json"],
244 env=_env(repo),
245 ).output
246 )
247 assert data["total_symbols"] == base
248
249
250 # ---------------------------------------------------------------------------
251 # Integration: --limit N for --all mode
252 # ---------------------------------------------------------------------------
253
254
255 class TestAllLimit:
256 def _big_repo(self, tmp_path: pathlib.Path) -> pathlib.Path:
257 _init_repo(tmp_path)
258 funcs = "\n\n".join(f"def func_{i}():\n pass" for i in range(30))
259 _make_commit(tmp_path, {"big.py": funcs.encode()}, message="big")
260 return tmp_path
261
262 def test_limit_caps_results(self, tmp_path: pathlib.Path) -> None:
263 repo = self._big_repo(tmp_path)
264 result = runner.invoke(
265 cli, ["code", "cat", "big.py", "--all", "--limit", "5", "--json"], env=_env(repo)
266 )
267 assert result.exit_code == 0
268 data = json.loads(result.output)
269 assert len(data["results"]) == 5
270
271 def test_limit_sets_truncated_true(self, tmp_path: pathlib.Path) -> None:
272 repo = self._big_repo(tmp_path)
273 result = runner.invoke(
274 cli, ["code", "cat", "big.py", "--all", "--limit", "5", "--json"], env=_env(repo)
275 )
276 data = json.loads(result.output)
277 assert data["truncated"] is True
278
279 def test_no_limit_truncated_false(self, tmp_path: pathlib.Path) -> None:
280 repo = self._big_repo(tmp_path)
281 result = runner.invoke(
282 cli, ["code", "cat", "big.py", "--all", "--json"], env=_env(repo)
283 )
284 data = json.loads(result.output)
285 assert data.get("truncated") is False
286
287 def test_limit_larger_than_results_not_truncated(
288 self, tmp_path: pathlib.Path
289 ) -> None:
290 repo = self._big_repo(tmp_path)
291 result = runner.invoke(
292 cli, ["code", "cat", "big.py", "--all", "--limit", "999", "--json"], env=_env(repo)
293 )
294 data = json.loads(result.output)
295 assert data.get("truncated") is False
296 assert len(data["results"]) == 30
297
298 def test_limit_zero_shows_zero_results(self, tmp_path: pathlib.Path) -> None:
299 repo = self._big_repo(tmp_path)
300 result = runner.invoke(
301 cli, ["code", "cat", "big.py", "--all", "--limit", "0", "--json"], env=_env(repo)
302 )
303 assert result.exit_code == 0
304 data = json.loads(result.output)
305 assert len(data["results"]) == 0
306 assert data["truncated"] is True
307
308 def test_limit_text_mode_respects_cap(self, tmp_path: pathlib.Path) -> None:
309 repo = self._big_repo(tmp_path)
310 result = runner.invoke(
311 cli, ["code", "cat", "big.py", "--all", "--limit", "3"], env=_env(repo)
312 )
313 assert result.exit_code == 0
314 # Only 3 symbols printed — count '# big.py::' headers
315 headers = [line for line in result.output.splitlines() if line.startswith("# big.py::")]
316 assert len(headers) == 3
317
318 def test_limit_without_all_is_ignored(self, repo: pathlib.Path) -> None:
319 """--limit without --all should be silently accepted (operates on results list)."""
320 result = runner.invoke(
321 cli, ["code", "cat", "mod.py::hello", "--limit", "5", "--json"], env=_env(repo)
322 )
323 # Should work normally (limit doesn't apply in address mode)
324 assert result.exit_code == 0
325
326
327 # ---------------------------------------------------------------------------
328 # Integration: redirected_from in JSON for global fallback
329 # ---------------------------------------------------------------------------
330
331
332 class TestRedirectedFrom:
333 def test_json_global_fallback_has_redirected_from(
334 self, tmp_path: pathlib.Path
335 ) -> None:
336 """When symbol is found in a different file via fallback, JSON has redirected_from."""
337 _init_repo(tmp_path)
338 _make_commit(
339 tmp_path,
340 {
341 # wrong.py has some symbols but NOT my_func — triggers global fallback
342 "wrong.py": b"def other_func():\n pass\n",
343 "right.py": b"def my_func():\n pass\n",
344 },
345 message="two files",
346 )
347 result = runner.invoke(
348 cli,
349 ["code", "cat", "wrong.py::my_func", "--json"],
350 env=_env(tmp_path),
351 )
352 assert result.exit_code == 0
353 data = json.loads(result.output)
354 assert len(data["results"]) == 1
355 r = data["results"][0]
356 assert "redirected_from" in r
357 assert "wrong.py" in r["redirected_from"]
358
359 def test_text_fallback_still_prints_note(self, tmp_path: pathlib.Path) -> None:
360 _init_repo(tmp_path)
361 _make_commit(
362 tmp_path,
363 {
364 # wrong.py has some symbols but NOT my_func
365 "wrong.py": b"def other_func():\n pass\n",
366 "right.py": b"def my_func():\n pass\n",
367 },
368 message="two files",
369 )
370 result = runner.invoke(
371 cli, ["code", "cat", "wrong.py::my_func"], env=_env(tmp_path)
372 )
373 assert result.exit_code == 0
374 assert "note" in result.output.lower() or "found in" in result.output.lower()
375
376
377 # ---------------------------------------------------------------------------
378 # Data integrity
379 # ---------------------------------------------------------------------------
380
381
382 class TestDataIntegrity:
383 def test_symbol_source_matches_file_bytes(self, repo: pathlib.Path) -> None:
384 """Source extracted by cat must appear verbatim in the actual file."""
385 result = runner.invoke(
386 cli, ["code", "cat", "mod.py::hello", "--json"], env=_env(repo)
387 )
388 data = json.loads(result.output)
389 source = data["results"][0]["source"]
390 disk_content = (repo / "mod.py").read_text()
391 assert source in disk_content
392
393 def test_at_ref_gives_different_content_than_head(
394 self, two_commit_repo: pathlib.Path
395 ) -> None:
396 log = runner.invoke(cli, ["log", "--json"], env=_env(two_commit_repo))
397 old_cid = json.loads(log.output)["commits"][-1]["commit_id"]
398
399 head = runner.invoke(
400 cli, ["code", "cat", "mod.py::hello", "--json"], env=_env(two_commit_repo)
401 )
402 old = runner.invoke(
403 cli,
404 ["code", "cat", "mod.py::hello", "--at", old_cid, "--json"],
405 env=_env(two_commit_repo),
406 )
407 head_src = json.loads(head.output)["results"][0]["source"]
408 old_src = json.loads(old.output)["results"][0]["source"]
409 assert head_src != old_src
410 assert "updated" in head_src
411 assert "updated" not in old_src
412
413 def test_all_symbols_cover_all_defs(self, repo: pathlib.Path) -> None:
414 """--all must return entries for every def/class in the file."""
415 result = runner.invoke(
416 cli, ["code", "cat", "mod.py", "--all", "--json"], env=_env(repo)
417 )
418 data = json.loads(result.output)
419 names = {r["symbol"] for r in data["results"]}
420 assert "hello" in names
421 assert "world" in names
422 # Greeter class or Greeter.greet method
423 assert any("Greeter" in n or "greet" in n for n in names)
424
425 def test_limit_preserves_lineno_order(self, tmp_path: pathlib.Path) -> None:
426 """With --limit, returned symbols should be the first N in line order."""
427 _init_repo(tmp_path)
428 funcs = "\n\n".join(f"def func_{i}():\n pass" for i in range(10))
429 _make_commit(tmp_path, {"ordered.py": funcs.encode()}, message="ordered")
430 result = runner.invoke(
431 cli, ["code", "cat", "ordered.py", "--all", "--limit", "3", "--json"],
432 env=_env(tmp_path),
433 )
434 data = json.loads(result.output)
435 linenos = [r["lineno"] for r in data["results"]]
436 assert linenos == sorted(linenos)
437 # First 3 should be func_0, func_1, func_2
438 symbols = [r["symbol"] for r in data["results"]]
439 assert symbols == ["func_0", "func_1", "func_2"]
440
441
442 # ---------------------------------------------------------------------------
443 # Performance
444 # ---------------------------------------------------------------------------
445
446
447 class TestPerformance:
448 @pytest.fixture
449 def large_repo(self, tmp_path: pathlib.Path) -> pathlib.Path:
450 _init_repo(tmp_path)
451 funcs = "\n\n".join(f"def func_{i}():\n return {i}" for i in range(200))
452 _make_commit(tmp_path, {"large.py": funcs.encode()}, message="large")
453 return tmp_path
454
455 def test_limit_10_faster_than_all(self, large_repo: pathlib.Path) -> None:
456 """--limit 10 should complete in under 3s on 200-symbol file."""
457 t0 = time.monotonic()
458 result = runner.invoke(
459 cli, ["code", "cat", "large.py", "--all", "--limit", "10", "--json"],
460 env=_env(large_repo),
461 )
462 elapsed = time.monotonic() - t0
463 assert result.exit_code == 0
464 data = json.loads(result.output)
465 assert len(data["results"]) == 10
466 assert elapsed < 3.0
467
468
469 # ---------------------------------------------------------------------------
470 # TestJsonAlias — -j works identically to --json
471 # ---------------------------------------------------------------------------
472
473
474 class TestJsonAlias:
475 """-j shorthand must behave identically to --json."""
476
477 def test_j_alias_exits_zero(self, repo: pathlib.Path) -> None:
478 r = runner.invoke(cli, ["code", "cat", "mod.py::hello", "-j"], env=_env(repo))
479 assert r.exit_code == 0, r.output
480
481 def test_j_alias_valid_json(self, repo: pathlib.Path) -> None:
482 r = runner.invoke(cli, ["code", "cat", "mod.py::hello", "-j"], env=_env(repo))
483 json.loads(r.output) # must not raise
484
485 def test_j_alias_has_results_key(self, repo: pathlib.Path) -> None:
486 r = runner.invoke(cli, ["code", "cat", "mod.py::hello", "-j"], env=_env(repo))
487 data = json.loads(r.output)
488 assert "results" in data
489
490 def test_j_alias_has_errors_key(self, repo: pathlib.Path) -> None:
491 r = runner.invoke(cli, ["code", "cat", "mod.py::hello", "-j"], env=_env(repo))
492 data = json.loads(r.output)
493 assert "errors" in data
494
495 def test_j_alias_same_top_level_keys_as_json_flag(self, repo: pathlib.Path) -> None:
496 r1 = runner.invoke(cli, ["code", "cat", "mod.py::hello", "--json"], env=_env(repo))
497 r2 = runner.invoke(cli, ["code", "cat", "mod.py::hello", "-j"], env=_env(repo))
498 d1 = json.loads(r1.output)
499 d2 = json.loads(r2.output)
500 d1.pop("duration_ms", None)
501 d2.pop("duration_ms", None)
502 assert set(d1.keys()) == set(d2.keys())
503
504 def test_j_alias_result_address_matches(self, repo: pathlib.Path) -> None:
505 r1 = runner.invoke(cli, ["code", "cat", "mod.py::hello", "--json"], env=_env(repo))
506 r2 = runner.invoke(cli, ["code", "cat", "mod.py::hello", "-j"], env=_env(repo))
507 assert json.loads(r1.output)["results"][0]["address"] == \
508 json.loads(r2.output)["results"][0]["address"]
509
510
511 # ---------------------------------------------------------------------------
512 # TestExitCode — JSON output must include exit_code
513 # ---------------------------------------------------------------------------
514
515
516 class TestExitCode:
517 """JSON envelope must carry exit_code mirroring the process exit."""
518
519 def test_json_has_exit_code(self, repo: pathlib.Path) -> None:
520 r = runner.invoke(cli, ["code", "cat", "mod.py::hello", "--json"], env=_env(repo))
521 data = json.loads(r.output)
522 assert "exit_code" in data
523
524 def test_json_exit_code_zero_on_success(self, repo: pathlib.Path) -> None:
525 r = runner.invoke(cli, ["code", "cat", "mod.py::hello", "--json"], env=_env(repo))
526 assert r.exit_code == 0
527 data = json.loads(r.output)
528 assert data["exit_code"] == 0
529
530 def test_json_exit_code_is_int(self, repo: pathlib.Path) -> None:
531 r = runner.invoke(cli, ["code", "cat", "mod.py::hello", "--json"], env=_env(repo))
532 data = json.loads(r.output)
533 assert isinstance(data["exit_code"], int)
534
535 def test_j_alias_exit_code_present(self, repo: pathlib.Path) -> None:
536 r = runner.invoke(cli, ["code", "cat", "mod.py::hello", "-j"], env=_env(repo))
537 data = json.loads(r.output)
538 assert "exit_code" in data
539
540 def test_exit_code_mirrors_process_exit_on_success(self, repo: pathlib.Path) -> None:
541 r = runner.invoke(cli, ["code", "cat", "mod.py::hello", "--json"], env=_env(repo))
542 data = json.loads(r.output)
543 assert data["exit_code"] == r.exit_code
544
545 def test_exit_code_nonzero_on_symbol_not_found(self, repo: pathlib.Path) -> None:
546 r = runner.invoke(cli, ["code", "cat", "mod.py::nonexistent_fn", "--json"], env=_env(repo))
547 assert r.exit_code != 0
548 data = json.loads(r.output)
549 assert data["exit_code"] != 0
550
551 def test_exit_code_mirrors_process_exit_on_error(self, repo: pathlib.Path) -> None:
552 r = runner.invoke(cli, ["code", "cat", "mod.py::nonexistent_fn", "--json"], env=_env(repo))
553 data = json.loads(r.output)
554 assert data["exit_code"] == r.exit_code
555
556 def test_exit_code_zero_with_all_flag(self, repo: pathlib.Path) -> None:
557 r = runner.invoke(cli, ["code", "cat", "mod.py", "--all", "--json"], env=_env(repo))
558 assert r.exit_code == 0
559 data = json.loads(r.output)
560 assert data["exit_code"] == 0
561
562
563 # ---------------------------------------------------------------------------
564 # TestTypedDicts — _CatOutputJson carries the envelope fields
565 # ---------------------------------------------------------------------------
566
567
568 class TestTypedDicts:
569 """_CatOutputJson must carry source_ref, results, errors, exit_code, duration_ms."""
570
571 def test_cat_output_json_exists(self) -> None:
572 from muse.cli.commands.cat import _CatOutputJson # noqa: F401
573
574 def test_cat_output_json_has_exit_code_annotation(self) -> None:
575 from muse.cli.commands.cat import _CatOutputJson
576 assert "exit_code" in _CatOutputJson.__annotations__
577
578 def test_cat_output_json_has_duration_ms_annotation(self) -> None:
579 from muse.cli.commands.cat import _CatOutputJson
580 assert "duration_ms" in _CatOutputJson.__annotations__
581
582 def test_cat_output_json_has_results_annotation(self) -> None:
583 from muse.cli.commands.cat import _CatOutputJson
584 assert "results" in _CatOutputJson.__annotations__
585
586 def test_cat_output_json_has_errors_annotation(self) -> None:
587 from muse.cli.commands.cat import _CatOutputJson
588 assert "errors" in _CatOutputJson.__annotations__
589
590 def test_cat_output_json_has_source_ref_annotation(self) -> None:
591 from muse.cli.commands.cat import _CatOutputJson
592 assert "source_ref" in _CatOutputJson.__annotations__
593
594 def test_cat_result_exists(self) -> None:
595 from muse.cli.commands.cat import CatResult # noqa: F401
596
597 def test_cat_error_exists(self) -> None:
598 from muse.cli.commands.cat import CatError # noqa: F401
599
600
601 # ---------------------------------------------------------------------------
602 # TestDocstrings — run() docstring documents new fields
603 # ---------------------------------------------------------------------------
604
605
606 class TestDocstrings:
607 """run() must document exit_code in the JSON output section."""
608
609 def test_run_docstring_documents_fields(self) -> None:
610 from muse.cli.commands.cat import run
611 assert "exit_code" in run.__doc__
612
613
614 # ---------------------------------------------------------------------------
615 # TestAnsiSanitization — no escape codes in JSON output
616 # ---------------------------------------------------------------------------
617
618
619 class TestAnsiSanitization:
620 """No ANSI escape sequences anywhere in the JSON output."""
621
622 def test_json_output_no_ansi(self, repo: pathlib.Path) -> None:
623 r = runner.invoke(cli, ["code", "cat", "mod.py::hello", "--json"], env=_env(repo))
624 assert "\x1b" not in r.output
625
626 def test_j_alias_output_no_ansi(self, repo: pathlib.Path) -> None:
627 r = runner.invoke(cli, ["code", "cat", "mod.py::hello", "-j"], env=_env(repo))
628 assert "\x1b" not in r.output
629
630 def test_error_path_json_no_ansi(self, repo: pathlib.Path) -> None:
631 r = runner.invoke(cli, ["code", "cat", "mod.py::no_such_fn", "--json"], env=_env(repo))
632 assert "\x1b" not in r.output
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 20 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 28 days ago