gabriel / muse public
test_symbol_log_supercharge.py python
695 lines 30.6 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Seven-tier tests for ``muse/cli/commands/symbol_log.py``.
2
3 Tiers
4 -----
5 Unit — TypedDict fields, SymbolEvent constructor/to_dict, _flat_ops,
6 _find_events_in_commit for each EventKind, _print_human branches.
7 Integration — -j alias parity, JSON envelope fields, rename-tracking,
8 all EventKind paths end-to-end through _find_events_in_commit.
9 End-to-end — CLI invocation: valid symbol, missing symbol, bad address,
10 --from, --max truncation, invalid ref.
11 Stress — 1 000 SymbolEvent constructions; concurrent reads.
12 Data integrity — to_dict round-trip; chronological ordering; counts accurate.
13 Security — ANSI in address/message/detail; hostile strings in address.
14 Performance — 1 000 to_dict calls under 0.5 s; duration_ms < 30 000ms.
15 """
16
17 from __future__ import annotations
18 from collections.abc import Mapping
19
20 import datetime
21 import json
22 import os
23 import pathlib
24 import textwrap
25 import threading
26 import time
27 from typing import get_type_hints
28
29 import pytest
30
31 from muse.core._types import fake_id
32 from tests.cli_test_helper import CliRunner, InvokeResult
33
34 runner = CliRunner()
35
36
37 # ──────────────────────────────────────────────────────────────────────────────
38 # Fixtures
39 # ──────────────────────────────────────────────────────────────────────────────
40
41
42 def _commit(repo: pathlib.Path, files: Mapping[str, str], message: str) -> None:
43 for name, content in files.items():
44 path = repo / name
45 path.parent.mkdir(parents=True, exist_ok=True)
46 path.write_text(content, encoding="utf-8")
47 saved = os.getcwd()
48 try:
49 os.chdir(repo)
50 runner.invoke(None, ["code", "add", "."])
51 runner.invoke(None, ["commit", "-m", message])
52 finally:
53 os.chdir(saved)
54
55
56 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
57 saved = os.getcwd()
58 try:
59 os.chdir(repo)
60 return runner.invoke(None, args)
61 finally:
62 os.chdir(saved)
63
64
65 def _symlog(repo: pathlib.Path, *args: str) -> InvokeResult:
66 return _invoke(repo, ["code", "symbol-log", *args])
67
68
69 @pytest.fixture()
70 def sym_repo(tmp_path: pathlib.Path) -> pathlib.Path:
71 """Repo with two commits so symbol-log has real history to walk.
72
73 Commit 1: billing.py with class Invoice + function process_invoice.
74 Commit 2: billing.py with Invoice body modified (new method).
75 """
76 saved = os.getcwd()
77 try:
78 os.chdir(tmp_path)
79 runner.invoke(None, ["init"])
80 finally:
81 os.chdir(saved)
82
83 _commit(tmp_path, {
84 "billing.py": textwrap.dedent("""\
85 class Invoice:
86 def __init__(self, amount):
87 self.amount = amount
88
89 def process_invoice(inv):
90 return inv.amount * 1.1
91 """),
92 }, "feat: add Invoice and process_invoice")
93
94 _commit(tmp_path, {
95 "billing.py": textwrap.dedent("""\
96 class Invoice:
97 def __init__(self, amount):
98 self.amount = amount
99
100 def total(self):
101 return self.amount * 1.1
102
103 def process_invoice(inv):
104 return inv.total()
105 """),
106 }, "feat: add Invoice.total method")
107
108 return tmp_path
109
110
111 # ──────────────────────────────────────────────────────────────────────────────
112 # Shared commit/delta helpers
113 # ──────────────────────────────────────────────────────────────────────────────
114
115
116 def _make_commit(
117 *,
118 commit_id: str = fake_id("aa"),
119 message: str = "feat: hello",
120 committed_at: datetime.datetime | None = None,
121 structured_delta: Mapping[str, object] | None = None,
122 ):
123 from muse.core.store import CommitRecord
124 return CommitRecord(
125 commit_id=commit_id,
126 repo_id="repo-1",
127 created_on_branch="dev",
128 parent_commit_id=None,
129 snapshot_id="snap",
130 message=message,
131 committed_at=committed_at or datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
132 structured_delta=structured_delta,
133 )
134
135
136 def _insert_delta(address: str, summary: str = "created") -> Mapping[str, object]:
137 return {"ops": [{"op": "insert", "address": address, "content_summary": summary}]}
138
139
140 def _delete_delta(address: str, summary: str = "deleted") -> Mapping[str, object]:
141 return {"ops": [{"op": "delete", "address": address, "content_summary": summary}]}
142
143
144 def _replace_delta(address: str, new_summary: str = "implementation changed") -> Mapping[str, object]:
145 return {"ops": [{"op": "replace", "address": address, "new_summary": new_summary}]}
146
147
148 def _patch_delta(address: str, new_summary: str = "implementation changed") -> Mapping[str, object]:
149 """replace wrapped in a patch parent — tests _flat_ops flattening."""
150 return {
151 "ops": [
152 {
153 "op": "patch",
154 "address": address,
155 "child_ops": [
156 {"op": "replace", "address": address, "new_summary": new_summary}
157 ],
158 }
159 ]
160 }
161
162
163 # ──────────────────────────────────────────────────────────────────────────────
164 # Unit — TypedDict
165 # ──────────────────────────────────────────────────────────────────────────────
166
167
168 class TestTypedDict:
169 def test_symbol_log_json_exists(self) -> None:
170 from muse.cli.commands.symbol_log import _SymbolLogJson # noqa: F401
171
172 def test_has_schema_version(self) -> None:
173 from muse.cli.commands.symbol_log import _SymbolLogJson
174 assert "schema" in get_type_hints(_SymbolLogJson)
175
176 def test_has_exit_code(self) -> None:
177 from muse.cli.commands.symbol_log import _SymbolLogJson
178 assert "exit_code" in get_type_hints(_SymbolLogJson)
179
180 def test_has_duration_ms(self) -> None:
181 from muse.cli.commands.symbol_log import _SymbolLogJson
182 assert "duration_ms" in get_type_hints(_SymbolLogJson)
183
184 def test_has_core_fields(self) -> None:
185 from muse.cli.commands.symbol_log import _SymbolLogJson
186 hints = get_type_hints(_SymbolLogJson)
187 for field in ("address", "start_ref", "total_commits_scanned", "truncated", "events"):
188 assert field in hints, f"missing field: {field}"
189
190
191 # ──────────────────────────────────────────────────────────────────────────────
192 # Unit — SymbolEvent
193 # ──────────────────────────────────────────────────────────────────────────────
194
195
196 class TestSymbolEvent:
197 def test_constructor_stores_kind(self) -> None:
198 from muse.cli.commands.symbol_log import SymbolEvent
199 ev = SymbolEvent("created", _make_commit(), "f.py::fn", "created")
200 assert ev.kind == "created"
201
202 def test_constructor_stores_address(self) -> None:
203 from muse.cli.commands.symbol_log import SymbolEvent
204 ev = SymbolEvent("modified", _make_commit(), "f.py::fn", "impl changed")
205 assert ev.address == "f.py::fn"
206
207 def test_constructor_stores_detail(self) -> None:
208 from muse.cli.commands.symbol_log import SymbolEvent
209 ev = SymbolEvent("deleted", _make_commit(), "f.py::fn", "removed")
210 assert ev.detail == "removed"
211
212 def test_constructor_default_new_address_is_none(self) -> None:
213 from muse.cli.commands.symbol_log import SymbolEvent
214 ev = SymbolEvent("created", _make_commit(), "f.py::fn", "x")
215 assert ev.new_address is None
216
217 def test_constructor_stores_new_address(self) -> None:
218 from muse.cli.commands.symbol_log import SymbolEvent
219 ev = SymbolEvent("renamed", _make_commit(), "f.py::old", "old → new", "f.py::new")
220 assert ev.new_address == "f.py::new"
221
222 def test_to_dict_has_all_fields(self) -> None:
223 from muse.cli.commands.symbol_log import SymbolEvent
224 commit = _make_commit(commit_id=fake_id("bb"), message="msg")
225 ev = SymbolEvent("modified", commit, "f.py::fn", "detail")
226 d = ev.to_dict()
227 for key in ("event", "commit_id", "message", "committed_at", "address", "detail", "new_address"):
228 assert key in d, f"missing key: {key}"
229
230 def test_to_dict_event_matches_kind(self) -> None:
231 from muse.cli.commands.symbol_log import SymbolEvent
232 ev = SymbolEvent("signature", _make_commit(), "f.py::fn", "sig changed")
233 assert ev.to_dict()["event"] == "signature"
234
235 def test_to_dict_committed_at_is_isoformat(self) -> None:
236 from muse.cli.commands.symbol_log import SymbolEvent
237 dt = datetime.datetime(2026, 3, 14, 12, 0, tzinfo=datetime.timezone.utc)
238 ev = SymbolEvent("created", _make_commit(committed_at=dt), "f.py::fn", "x")
239 iso = ev.to_dict()["committed_at"]
240 assert "2026-03-14" in iso
241 assert "T" in iso
242
243 def test_to_dict_new_address_none_when_not_set(self) -> None:
244 from muse.cli.commands.symbol_log import SymbolEvent
245 ev = SymbolEvent("modified", _make_commit(), "f.py::fn", "x")
246 assert ev.to_dict()["new_address"] is None
247
248
249 # ──────────────────────────────────────────────────────────────────────────────
250 # Unit — _flat_ops
251 # ──────────────────────────────────────────────────────────────────────────────
252
253
254 class TestFlatOps:
255 def test_passthrough_non_patch(self) -> None:
256 from muse.cli.commands.symbol_log import _flat_ops
257 ops = [{"op": "insert", "address": "f.py::fn"}]
258 assert _flat_ops(ops) == ops
259
260 def test_flattens_patch_children(self) -> None:
261 from muse.cli.commands.symbol_log import _flat_ops
262 child = {"op": "replace", "address": "f.py::fn", "new_summary": "x"}
263 ops = [{"op": "patch", "address": "f.py::fn", "child_ops": [child]}]
264 result = _flat_ops(ops)
265 assert result == [child]
266
267 def test_mixed_ops_preserved_in_order(self) -> None:
268 from muse.cli.commands.symbol_log import _flat_ops
269 insert = {"op": "insert", "address": "f.py::a"}
270 child = {"op": "replace", "address": "f.py::b", "new_summary": "y"}
271 patch = {"op": "patch", "address": "f.py::b", "child_ops": [child]}
272 result = _flat_ops([insert, patch])
273 assert result == [insert, child]
274
275 def test_empty_ops_returns_empty(self) -> None:
276 from muse.cli.commands.symbol_log import _flat_ops
277 assert _flat_ops([]) == []
278
279
280 # ──────────────────────────────────────────────────────────────────────────────
281 # Unit — _find_events_in_commit (each EventKind)
282 # ──────────────────────────────────────────────────────────────────────────────
283
284
285 class TestFindEventsInCommit:
286 def test_no_delta_returns_empty(self) -> None:
287 from muse.cli.commands.symbol_log import _find_events_in_commit
288 commit = _make_commit(structured_delta=None)
289 evs, addr = _find_events_in_commit(commit, "f.py::fn")
290 assert evs == []
291 assert addr == "f.py::fn"
292
293 def test_insert_produces_created(self) -> None:
294 from muse.cli.commands.symbol_log import _find_events_in_commit
295 commit = _make_commit(structured_delta=_insert_delta("f.py::fn"))
296 evs, addr = _find_events_in_commit(commit, "f.py::fn")
297 assert len(evs) == 1
298 assert evs[0].kind == "created"
299 assert addr == "f.py::fn"
300
301 def test_delete_produces_deleted(self) -> None:
302 from muse.cli.commands.symbol_log import _find_events_in_commit
303 commit = _make_commit(structured_delta=_delete_delta("f.py::fn"))
304 evs, addr = _find_events_in_commit(commit, "f.py::fn")
305 assert len(evs) == 1
306 assert evs[0].kind == "deleted"
307
308 def test_delete_moved_to_produces_moved(self) -> None:
309 from muse.cli.commands.symbol_log import _find_events_in_commit
310 commit = _make_commit(structured_delta=_delete_delta("f.py::fn", "moved to g.py::fn"))
311 evs, _ = _find_events_in_commit(commit, "f.py::fn")
312 assert evs[0].kind == "moved"
313
314 def test_replace_produces_modified(self) -> None:
315 from muse.cli.commands.symbol_log import _find_events_in_commit
316 commit = _make_commit(structured_delta=_replace_delta("f.py::fn"))
317 evs, addr = _find_events_in_commit(commit, "f.py::fn")
318 assert len(evs) == 1
319 assert evs[0].kind == "modified"
320
321 def test_replace_renamed_to_updates_address(self) -> None:
322 from muse.cli.commands.symbol_log import _find_events_in_commit
323 commit = _make_commit(structured_delta=_replace_delta("f.py::old", "renamed to new"))
324 evs, addr = _find_events_in_commit(commit, "f.py::old")
325 assert evs[0].kind == "renamed"
326 assert addr == "f.py::new"
327 assert evs[0].new_address == "f.py::new"
328
329 def test_replace_moved_to_produces_moved(self) -> None:
330 from muse.cli.commands.symbol_log import _find_events_in_commit
331 commit = _make_commit(structured_delta=_replace_delta("f.py::fn", "moved to g.py::fn"))
332 evs, _ = _find_events_in_commit(commit, "f.py::fn")
333 assert evs[0].kind == "moved"
334
335 def test_replace_signature_produces_signature(self) -> None:
336 from muse.cli.commands.symbol_log import _find_events_in_commit
337 commit = _make_commit(structured_delta=_replace_delta("f.py::fn", "signature changed"))
338 evs, _ = _find_events_in_commit(commit, "f.py::fn")
339 assert evs[0].kind == "signature"
340
341 def test_patch_wrapper_is_flattened(self) -> None:
342 from muse.cli.commands.symbol_log import _find_events_in_commit
343 commit = _make_commit(structured_delta=_patch_delta("f.py::fn"))
344 evs, _ = _find_events_in_commit(commit, "f.py::fn")
345 assert len(evs) == 1
346 assert evs[0].kind == "modified"
347
348 def test_unrelated_address_produces_no_events(self) -> None:
349 from muse.cli.commands.symbol_log import _find_events_in_commit
350 commit = _make_commit(structured_delta=_insert_delta("other.py::other"))
351 evs, addr = _find_events_in_commit(commit, "f.py::fn")
352 assert evs == []
353 assert addr == "f.py::fn"
354
355
356 # ──────────────────────────────────────────────────────────────────────────────
357 # Integration — alias, docstrings, envelope
358 # ──────────────────────────────────────────────────────────────────────────────
359
360
361 class TestAliasRegistration:
362 def test_j_alias_registered(self) -> None:
363 from muse.cli.commands.symbol_log import register
364 import argparse
365 p = argparse.ArgumentParser()
366 sub = p.add_subparsers()
367 register(sub)
368 ns = p.parse_args(["symbol-log", "f.py::fn", "-j"])
369 assert ns.json_out is True
370
371 def test_json_flag_sets_as_json_true(self) -> None:
372 from muse.cli.commands.symbol_log import register
373 import argparse
374 p = argparse.ArgumentParser()
375 sub = p.add_subparsers()
376 register(sub)
377 ns = p.parse_args(["symbol-log", "f.py::fn", "--json"])
378 assert ns.json_out is True
379
380
381 class TestDocstrings:
382 def test_register_mentions_json_alias(self) -> None:
383 from muse.cli.commands.symbol_log import register
384 doc = register.__doc__ or ""
385 assert "--json" in doc or "-j" in doc
386
387 def test_run_mentions_exit_code(self) -> None:
388 from muse.cli.commands.symbol_log import run
389 assert "exit_code" in (run.__doc__ or "")
390
391 def test_run_mentions_duration_ms(self) -> None:
392 from muse.cli.commands.symbol_log import run
393 assert "duration_ms" in (run.__doc__ or "")
394
395 def test_run_mentions_schema_version(self) -> None:
396 from muse.cli.commands.symbol_log import run
397 assert "schema" in (run.__doc__ or "")
398
399
400 class TestJsonEnvelope:
401 def test_schema_version_present(self, sym_repo) -> None:
402 r = _symlog(sym_repo, "billing.py::Invoice", "-j")
403 assert r.exit_code == 0
404 assert "schema" in json.loads(r.output)
405
406 def test_exit_code_zero(self, sym_repo) -> None:
407 r = _symlog(sym_repo, "billing.py::Invoice", "-j")
408 assert r.exit_code == 0
409 d = json.loads(r.output)
410 assert d["exit_code"] == 0
411
412 def test_duration_ms_present_and_float(self, sym_repo) -> None:
413 r = _symlog(sym_repo, "billing.py::Invoice", "-j")
414 assert r.exit_code == 0
415 d = json.loads(r.output)
416 assert "duration_ms" in d
417 assert isinstance(d["duration_ms"], float)
418
419 def test_schema_version_nonempty_string(self, sym_repo) -> None:
420 r = _symlog(sym_repo, "billing.py::Invoice", "-j")
421 assert r.exit_code == 0
422 d = json.loads(r.output)
423 assert isinstance(d["schema"], int) and d["schema"] > 0
424
425
426 class TestJsonAlias:
427 def test_j_parity_with_json_flag(self, sym_repo) -> None:
428 r1 = _symlog(sym_repo, "billing.py::Invoice", "--json")
429 r2 = _symlog(sym_repo, "billing.py::Invoice", "-j")
430 assert r1.exit_code == 0
431 assert r2.exit_code == 0
432 d1, d2 = json.loads(r1.output), json.loads(r2.output)
433 assert d1["address"] == d2["address"]
434 assert d1["events"] == d2["events"]
435 assert d1["schema"] == d2["schema"]
436 assert d1["exit_code"] == d2["exit_code"]
437
438
439 # ──────────────────────────────────────────────────────────────────────────────
440 # End-to-end
441 # ──────────────────────────────────────────────────────────────────────────────
442
443
444 class TestEndToEnd:
445 def test_valid_symbol_exits_zero(self, sym_repo) -> None:
446 r = _symlog(sym_repo, "billing.py::Invoice")
447 assert r.exit_code == 0
448
449 def test_unknown_symbol_exits_zero_with_no_events(self, sym_repo) -> None:
450 r = _symlog(sym_repo, "billing.py::DoesNotExistXXX")
451 assert r.exit_code == 0
452 assert "no events found" in r.output
453
454 def test_bad_address_no_double_colon_exits_nonzero(self, sym_repo) -> None:
455 r = _symlog(sym_repo, "billing.py")
456 assert r.exit_code != 0
457
458 def test_max_1_sets_truncated_true_in_json(self, sym_repo) -> None:
459 r = _symlog(sym_repo, "billing.py::Invoice", "--max", "1", "-j")
460 assert r.exit_code == 0
461 d = json.loads(r.output)
462 assert d["truncated"] is True
463 assert d["total_commits_scanned"] == 1
464
465 def test_max_1_shows_truncation_warning_in_human(self, sym_repo) -> None:
466 r = _symlog(sym_repo, "billing.py::Invoice", "--max", "1")
467 assert r.exit_code == 0
468 assert "incomplete" in r.output or "limit" in r.output
469
470 def test_max_zero_exits_nonzero(self, sym_repo) -> None:
471 r = _symlog(sym_repo, "billing.py::Invoice", "--max", "0")
472 assert r.exit_code != 0
473
474 def test_invalid_from_ref_exits_nonzero(self, sym_repo) -> None:
475 r = _symlog(sym_repo, "billing.py::Invoice", "--from", "deadbeefdeadbeef")
476 assert r.exit_code != 0
477
478 def test_json_address_field_matches_input(self, sym_repo) -> None:
479 r = _symlog(sym_repo, "billing.py::Invoice", "-j")
480 assert r.exit_code == 0
481 assert json.loads(r.output)["address"] == "billing.py::Invoice"
482
483 def test_json_events_is_list(self, sym_repo) -> None:
484 r = _symlog(sym_repo, "billing.py::Invoice", "-j")
485 assert r.exit_code == 0
486 assert isinstance(json.loads(r.output)["events"], list)
487
488 def test_start_ref_is_head_by_default(self, sym_repo) -> None:
489 r = _symlog(sym_repo, "billing.py::Invoice", "-j")
490 assert r.exit_code == 0
491 assert json.loads(r.output)["start_ref"] == "HEAD"
492
493 def test_human_output_shows_symbol_header(self, sym_repo) -> None:
494 r = _symlog(sym_repo, "billing.py::Invoice")
495 assert r.exit_code == 0
496 assert "billing.py::Invoice" in r.output
497
498
499 # ──────────────────────────────────────────────────────────────────────────────
500 # Stress
501 # ──────────────────────────────────────────────────────────────────────────────
502
503
504 class TestStress:
505 def test_1000_symbol_event_constructions(self) -> None:
506 from muse.cli.commands.symbol_log import SymbolEvent
507 commit = _make_commit()
508 for i in range(1_000):
509 ev = SymbolEvent("modified", commit, f"f{i}.py::fn", f"detail {i}")
510 assert ev.kind == "modified"
511
512 def test_1000_to_dict_calls(self) -> None:
513 from muse.cli.commands.symbol_log import SymbolEvent
514 commit = _make_commit()
515 ev = SymbolEvent("created", commit, "f.py::fn", "x")
516 for _ in range(1_000):
517 d = ev.to_dict()
518 assert "event" in d
519
520 def test_flat_ops_10000_calls(self) -> None:
521 from muse.cli.commands.symbol_log import _flat_ops
522 ops = [{"op": "insert", "address": f"f{i}.py::fn"} for i in range(10)]
523 for _ in range(10_000):
524 result = _flat_ops(ops)
525 assert len(result) == 10
526
527 def test_concurrent_find_events(self) -> None:
528 from muse.cli.commands.symbol_log import _find_events_in_commit
529 commit = _make_commit(structured_delta=_insert_delta("f.py::fn"))
530 results: list[int] = []
531 lock = threading.Lock()
532
533 def _run() -> None:
534 evs, _ = _find_events_in_commit(commit, "f.py::fn")
535 with lock:
536 results.append(len(evs))
537
538 threads = [threading.Thread(target=_run) for _ in range(50)]
539 for t in threads: t.start()
540 for t in threads: t.join()
541 assert all(n == 1 for n in results)
542 assert len(results) == 50
543
544
545 # ──────────────────────────────────────────────────────────────────────────────
546 # Data integrity
547 # ──────────────────────────────────────────────────────────────────────────────
548
549
550 class TestDataIntegrity:
551 def test_to_dict_preserves_all_fields(self) -> None:
552 from muse.cli.commands.symbol_log import SymbolEvent
553 commit = _make_commit(
554 commit_id=fake_id("cc"),
555 message="fix: something",
556 committed_at=datetime.datetime(2026, 5, 1, tzinfo=datetime.timezone.utc),
557 )
558 ev = SymbolEvent("renamed", commit, "a.py::old", "old → new", "a.py::new")
559 d = ev.to_dict()
560 assert d["event"] == "renamed"
561 assert d["commit_id"] == fake_id("cc")
562 assert d["message"] == "fix: something"
563 assert d["address"] == "a.py::old"
564 assert d["detail"] == "old → new"
565 assert d["new_address"] == "a.py::new"
566 assert "2026-05-01" in d["committed_at"]
567
568 def test_events_in_json_are_chronological(self, sym_repo) -> None:
569 r = _symlog(sym_repo, "billing.py::Invoice", "-j")
570 assert r.exit_code == 0
571 events = json.loads(r.output)["events"]
572 if len(events) >= 2:
573 times = [datetime.datetime.fromisoformat(e["committed_at"]) for e in events]
574 assert times == sorted(times), "events not in chronological order"
575
576 def test_total_commits_scanned_is_int(self, sym_repo) -> None:
577 r = _symlog(sym_repo, "billing.py::Invoice", "-j")
578 assert r.exit_code == 0
579 assert isinstance(json.loads(r.output)["total_commits_scanned"], int)
580
581 def test_truncated_is_bool(self, sym_repo) -> None:
582 r = _symlog(sym_repo, "billing.py::Invoice", "-j")
583 assert r.exit_code == 0
584 assert isinstance(json.loads(r.output)["truncated"], bool)
585
586 def test_rename_tracking_continues_with_new_address(self) -> None:
587 from muse.cli.commands.symbol_log import _find_events_in_commit
588 rename_commit = _make_commit(structured_delta=_replace_delta("f.py::old", "renamed to new"))
589 _, next_addr = _find_events_in_commit(rename_commit, "f.py::old")
590 assert next_addr == "f.py::new"
591 insert_commit = _make_commit(structured_delta=_insert_delta("f.py::new"))
592 evs, _ = _find_events_in_commit(insert_commit, next_addr)
593 assert len(evs) == 1
594 assert evs[0].kind == "created"
595
596 def test_new_address_none_for_modified(self) -> None:
597 from muse.cli.commands.symbol_log import _find_events_in_commit
598 commit = _make_commit(structured_delta=_replace_delta("f.py::fn"))
599 evs, _ = _find_events_in_commit(commit, "f.py::fn")
600 assert evs[0].new_address is None
601
602 def test_json_serialisable(self, sym_repo) -> None:
603 r = _symlog(sym_repo, "billing.py::Invoice", "-j")
604 assert r.exit_code == 0
605 json.loads(r.output) # must not raise
606
607
608 # ──────────────────────────────────────────────────────────────────────────────
609 # Security
610 # ──────────────────────────────────────────────────────────────────────────────
611
612
613 class TestSecurity:
614 def test_ansi_in_address_does_not_crash(self, sym_repo) -> None:
615 ansi_addr = "\x1b[31mbad\x1b[0m.py::fn"
616 r = _symlog(sym_repo, ansi_addr)
617 assert r.exit_code in (0, 1, 2)
618
619 def test_ansi_in_commit_message_survives_to_dict(self) -> None:
620 from muse.cli.commands.symbol_log import SymbolEvent
621 evil_msg = "\x1b[31mevil\x1b[0m"
622 ev = SymbolEvent("modified", _make_commit(message=evil_msg), "f.py::fn", "x")
623 d = ev.to_dict()
624 assert d["message"] == evil_msg
625
626 def test_very_long_address_does_not_crash(self, sym_repo) -> None:
627 long_addr = "f.py::" + "x" * 10_000
628 r = _symlog(sym_repo, long_addr)
629 assert r.exit_code in (0, 1, 2)
630
631 def test_unicode_in_address_does_not_crash(self, sym_repo) -> None:
632 r = _symlog(sym_repo, "音符.py::関数")
633 assert r.exit_code in (0, 1, 2)
634
635 def test_hostile_detail_survives_json_serialisation(self) -> None:
636 from muse.cli.commands.symbol_log import SymbolEvent
637 evil = '"; DROP TABLE commits; --'
638 ev = SymbolEvent("modified", _make_commit(), "f.py::fn", evil)
639 d = ev.to_dict()
640 assert json.loads(json.dumps(d))["detail"] == evil
641
642 def test_very_long_message_in_commit_does_not_crash(self) -> None:
643 from muse.cli.commands.symbol_log import SymbolEvent
644 ev = SymbolEvent("modified", _make_commit(message="x" * 100_000), "f.py::fn", "x")
645 d = ev.to_dict()
646 assert len(d["message"]) == 100_000
647
648
649 # ──────────────────────────────────────────────────────────────────────────────
650 # Performance
651 # ──────────────────────────────────────────────────────────────────────────────
652
653
654 class TestPerformance:
655 def test_1000_to_dict_under_500ms(self) -> None:
656 from muse.cli.commands.symbol_log import SymbolEvent
657 ev = SymbolEvent("modified", _make_commit(), "f.py::fn", "impl changed")
658 start = time.perf_counter()
659 for _ in range(1_000):
660 ev.to_dict()
661 elapsed = time.perf_counter() - start
662 assert elapsed < 0.5, f"1 000 to_dict calls took {elapsed:.2f}s"
663
664 def test_duration_ms_present_and_reasonable(self, sym_repo) -> None:
665 r = _symlog(sym_repo, "billing.py::Invoice", "-j")
666 assert r.exit_code == 0
667 d = json.loads(r.output)
668 assert "duration_ms" in d
669 assert 0 <= d["duration_ms"] < 30_000
670
671
672 # Flag registration
673 # ──────────────────────────────────────────────────────────────────────────────
674
675
676 class TestRegisterFlags:
677 def _parse(self, *args: str):
678 import argparse
679 from muse.cli.commands.symbol_log import register
680 p = argparse.ArgumentParser()
681 sub = p.add_subparsers()
682 register(sub)
683 return p.parse_args(["symbol-log", *args])
684
685 def test_default_json_out_is_false(self) -> None:
686 ns = self._parse("src/utils.py::fn")
687 assert ns.json_out is False
688
689 def test_json_flag_sets_json_out(self) -> None:
690 ns = self._parse("--json", "src/utils.py::fn")
691 assert ns.json_out is True
692
693 def test_j_shorthand_sets_json_out(self) -> None:
694 ns = self._parse("-j", "src/utils.py::fn")
695 assert ns.json_out is True
File History 2 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