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