gabriel / muse public
test_narrative_supercharge.py python
629 lines 23.5 KB
Raw
sha256:74b5023693ac2ab80e3b89fddc66e0d60d7d931a1266d3f9294f645c3102fe76 tests/test_lineage_supercharge.py, tests/test_narrative_sup… Human 3 days ago
1 """Supercharge tests for ``muse code narrative``.
2
3 Coverage gaps addressed
4 -----------------------
5 - ``-j`` alias for ``--json``
6 - ``exit_code`` field in JSON envelope
7 - ``duration_ms`` field in JSON envelope
8 - ``sig_changes`` / ``renames`` counts verified in JSON
9 - ``truncated`` is False for small repos
10 - ``kind`` is correct in JSON
11 - ``last_impl_date`` / ``last_impl_commit`` present and non-empty when impl exists
12 - JSON is a single line (machine-parseable)
13 - TypedDict exports: ``_NarrativeJson`` and ``_EventRecord`` importable, match output
14 - Unit: ``_classify_op`` all branches
15 - Unit: ``_sanitise_msg`` truncation and control-char stripping
16 - Unit: ``_format_date`` / ``_format_date_long``
17 - Unit: ``_days_ago`` buckets
18 - Unit: ``_relative_to`` buckets
19 - Unit: ``_extract_rename`` patterns
20 - Unit: ``_event_detail``
21 - ``--show-source`` does not crash with ``--json`` mode
22 """
23
24 from __future__ import annotations
25 from collections.abc import Mapping
26
27 import datetime
28 import json
29 import pathlib
30 import textwrap
31
32 import pytest
33 from tests.cli_test_helper import CliRunner
34
35 cli = None # CliRunner ignores this argument; see cli_test_helper.py
36 runner = CliRunner()
37
38
39 # ---------------------------------------------------------------------------
40 # Base repo fixture
41 # ---------------------------------------------------------------------------
42
43
44 @pytest.fixture()
45 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
46 monkeypatch.chdir(tmp_path)
47 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
48 r = runner.invoke(cli, ["init", "--domain", "code"])
49 assert r.exit_code == 0, r.output
50 return tmp_path
51
52
53 # ---------------------------------------------------------------------------
54 # Shared fixture — minimal repo with a symbol that has a rich history
55 # ---------------------------------------------------------------------------
56
57
58 @pytest.fixture()
59 def narrative_repo(repo: pathlib.Path) -> pathlib.Path:
60 """Repo with billing.py::compute_total across four commits.
61
62 Commit 1 — seed (readme only)
63 Commit 2 — create billing.py with compute_total
64 Commit 3 — body rewrite (impl)
65 Commit 4 — signature change
66 """
67 (repo / "readme.txt").write_text("seed\n")
68 runner.invoke(cli, ["code", "add", "readme.txt"])
69 r = runner.invoke(cli, ["commit", "-m", "chore: seed"])
70 assert r.exit_code == 0, r.output
71
72 (repo / "billing.py").write_text(textwrap.dedent("""\
73 def compute_total(items):
74 total = 0
75 for item in items:
76 total += item["price"]
77 return total
78 """))
79 runner.invoke(cli, ["code", "add", "billing.py"])
80 r = runner.invoke(cli, ["commit", "-m", "feat: add compute_total"])
81 assert r.exit_code == 0, r.output
82
83 (repo / "billing.py").write_text(textwrap.dedent("""\
84 def compute_total(items):
85 return sum(i["price"] for i in items)
86 """))
87 runner.invoke(cli, ["code", "add", "billing.py"])
88 r = runner.invoke(cli, ["commit", "-m", "perf: vectorise compute_total body implementation"])
89 assert r.exit_code == 0, r.output
90
91 (repo / "billing.py").write_text(textwrap.dedent("""\
92 def compute_total(items, currency="USD"):
93 return sum(i["price"] for i in items)
94 """))
95 runner.invoke(cli, ["code", "add", "billing.py"])
96 r = runner.invoke(cli, ["commit", "-m", "feat: compute_total signature add currency"])
97 assert r.exit_code == 0, r.output
98
99 return repo
100
101
102 CMD = ["code", "narrative"]
103 ADDR = "billing.py::compute_total"
104
105
106 # ---------------------------------------------------------------------------
107 # -j alias
108 # ---------------------------------------------------------------------------
109
110
111 class TestJsonAlias:
112 def test_j_alias_exits_zero(self, narrative_repo: pathlib.Path) -> None:
113 r = runner.invoke(cli, CMD + [ADDR, "-j"])
114 assert r.exit_code == 0, r.output
115
116 def test_j_alias_emits_valid_json(self, narrative_repo: pathlib.Path) -> None:
117 r = runner.invoke(cli, CMD + [ADDR, "-j"])
118 data = json.loads(r.output)
119 assert isinstance(data, dict)
120
121 def test_j_alias_same_as_json_flag(self, narrative_repo: pathlib.Path) -> None:
122 r1 = runner.invoke(cli, CMD + [ADDR, "--json"])
123 r2 = runner.invoke(cli, CMD + [ADDR, "-j"])
124 d1 = json.loads(r1.output)
125 d2 = json.loads(r2.output)
126 # Ignore duration_ms which may differ; compare structural keys.
127 for key in ("address", "name", "kind", "status", "impl_changes", "sig_changes"):
128 assert d1[key] == d2[key], f"mismatch on {key!r}: {d1[key]!r} vs {d2[key]!r}"
129
130
131 # ---------------------------------------------------------------------------
132 # exit_code in JSON envelope
133 # ---------------------------------------------------------------------------
134
135
136 class TestJsonExitCode:
137 def test_exit_code_present_in_json(self, narrative_repo: pathlib.Path) -> None:
138 r = runner.invoke(cli, CMD + [ADDR, "--json"])
139 data = json.loads(r.output)
140 assert "exit_code" in data
141
142 def test_exit_code_is_zero_on_success(self, narrative_repo: pathlib.Path) -> None:
143 r = runner.invoke(cli, CMD + [ADDR, "--json"])
144 data = json.loads(r.output)
145 assert data["exit_code"] == 0
146
147 def test_exit_code_is_int(self, narrative_repo: pathlib.Path) -> None:
148 r = runner.invoke(cli, CMD + [ADDR, "--json"])
149 data = json.loads(r.output)
150 assert isinstance(data["exit_code"], int)
151
152
153 # ---------------------------------------------------------------------------
154 # duration_ms in JSON envelope
155 # ---------------------------------------------------------------------------
156
157
158 class TestJsonDurationMs:
159 def test_duration_ms_present_in_json(self, narrative_repo: pathlib.Path) -> None:
160 r = runner.invoke(cli, CMD + [ADDR, "--json"])
161 data = json.loads(r.output)
162 assert "duration_ms" in data
163
164 def test_duration_ms_is_positive(self, narrative_repo: pathlib.Path) -> None:
165 r = runner.invoke(cli, CMD + [ADDR, "--json"])
166 data = json.loads(r.output)
167 assert data["duration_ms"] > 0
168
169 def test_duration_ms_is_float_or_int(self, narrative_repo: pathlib.Path) -> None:
170 r = runner.invoke(cli, CMD + [ADDR, "--json"])
171 data = json.loads(r.output)
172 assert isinstance(data["duration_ms"], (int, float))
173
174
175 # ---------------------------------------------------------------------------
176 # Extra JSON context fields
177 # ---------------------------------------------------------------------------
178
179
180 class TestJsonContextFields:
181 def _json(self, narrative_repo: pathlib.Path) -> Mapping[str, object]:
182 r = runner.invoke(cli, CMD + [ADDR, "--json"])
183 assert r.exit_code == 0, r.output
184 return json.loads(r.output)
185
186 def test_kind_is_function(self, narrative_repo: pathlib.Path) -> None:
187 data = self._json(narrative_repo)
188 assert data["kind"] == "function"
189
190 def test_sig_changes_gte_one(self, narrative_repo: pathlib.Path) -> None:
191 data = self._json(narrative_repo)
192 # We made at least one signature change commit.
193 assert data["sig_changes"] >= 1
194
195 def test_renames_is_int(self, narrative_repo: pathlib.Path) -> None:
196 data = self._json(narrative_repo)
197 assert isinstance(data["renames"], int)
198 assert data["renames"] >= 0
199
200 def test_truncated_false_for_small_repo(self, narrative_repo: pathlib.Path) -> None:
201 data = self._json(narrative_repo)
202 assert data["truncated"] is False
203
204 def test_last_impl_commit_nonempty_when_impl_exists(
205 self, narrative_repo: pathlib.Path
206 ) -> None:
207 data = self._json(narrative_repo)
208 assert data["impl_changes"] >= 1
209 assert data["last_impl_commit"] != ""
210
211 def test_last_impl_date_is_date_format(self, narrative_repo: pathlib.Path) -> None:
212 import re
213 data = self._json(narrative_repo)
214 if data["impl_changes"] >= 1:
215 assert re.match(r"\d{4}-\d{2}-\d{2}", data["last_impl_date"]), (
216 f"Expected YYYY-MM-DD but got {data['last_impl_date']!r}"
217 )
218
219
220 # ---------------------------------------------------------------------------
221 # JSON is single-line
222 # ---------------------------------------------------------------------------
223
224
225 class TestJsonSingleLine:
226 def test_json_output_is_single_line(self, narrative_repo: pathlib.Path) -> None:
227 r = runner.invoke(cli, CMD + [ADDR, "--json"])
228 lines = [l for l in r.output.splitlines() if l.strip()]
229 assert len(lines) == 1, f"Expected one JSON line, got {len(lines)}: {r.output[:200]}"
230
231 def test_json_output_parseable_without_strip(
232 self, narrative_repo: pathlib.Path
233 ) -> None:
234 r = runner.invoke(cli, CMD + [ADDR, "--json"])
235 # Should parse even with trailing newline.
236 data = json.loads(r.output)
237 assert data["address"] == ADDR
238
239
240 # ---------------------------------------------------------------------------
241 # TypedDict exports
242 # ---------------------------------------------------------------------------
243
244
245 class TestTypedDictExport:
246 def test_narrative_json_typeddict_importable(self) -> None:
247 from muse.cli.commands.narrative import _NarrativeJson
248 import typing
249 hints = typing.get_type_hints(_NarrativeJson)
250 assert "address" in hints
251 assert "exit_code" in hints
252 assert "duration_ms" in hints
253
254 def test_event_record_typeddict_importable(self) -> None:
255 from muse.cli.commands.narrative import _EventRecord
256 import typing
257 hints = typing.get_type_hints(_EventRecord)
258 for key in ("date", "commit_id", "commit_msg", "event_type", "sem_ver_bump", "detail"):
259 assert key in hints, f"_EventRecord missing key: {key!r}"
260
261 def test_narrative_json_typeddict_matches_output(
262 self, narrative_repo: pathlib.Path
263 ) -> None:
264 """Every key in the TypedDict must appear in actual JSON output."""
265 from muse.cli.commands.narrative import _NarrativeJson
266 import typing
267 r = runner.invoke(cli, CMD + [ADDR, "--json"])
268 data = json.loads(r.output)
269 hints = typing.get_type_hints(_NarrativeJson)
270 for key in hints:
271 assert key in data, f"JSON output missing TypedDict key: {key!r}"
272
273
274 # ---------------------------------------------------------------------------
275 # Unit: _classify_op
276 # ---------------------------------------------------------------------------
277
278
279 class TestClassifyOp:
280 def _op(self, **kwargs: str) -> Mapping[str, object]:
281 base = {"op": "replace", "new_summary": "", "old_summary": "", "address": "x.py::f"}
282 base.update(kwargs)
283 return base
284
285 def test_insert_is_create(self) -> None:
286 from muse.cli.commands.narrative import _classify_op
287 assert _classify_op({"op": "insert"}) == "create"
288
289 def test_delete_is_delete(self) -> None:
290 from muse.cli.commands.narrative import _classify_op
291 assert _classify_op({"op": "delete"}) == "delete"
292
293 def test_rename_keyword_in_new_summary(self) -> None:
294 from muse.cli.commands.narrative import _classify_op
295 op = self._op(new_summary="renamed foo to bar")
296 assert _classify_op(op) == "rename"
297
298 def test_moved_keyword_in_new_summary(self) -> None:
299 from muse.cli.commands.narrative import _classify_op
300 op = self._op(new_summary="moved module to package")
301 assert _classify_op(op) == "rename"
302
303 def test_signature_keyword_in_new_summary(self) -> None:
304 from muse.cli.commands.narrative import _classify_op
305 op = self._op(new_summary="signature change detected")
306 assert _classify_op(op) == "sig"
307
308 def test_implementation_keyword_in_new_summary(self) -> None:
309 from muse.cli.commands.narrative import _classify_op
310 op = self._op(new_summary="implementation rewritten")
311 assert _classify_op(op) == "impl"
312
313 def test_body_keyword_in_old_summary_is_impl(self) -> None:
314 from muse.cli.commands.narrative import _classify_op
315 op = self._op(new_summary="", old_summary="body changed completely")
316 assert _classify_op(op) == "impl"
317
318 def test_unknown_replace_defaults_to_impl(self) -> None:
319 from muse.cli.commands.narrative import _classify_op
320 op = self._op(new_summary="some unrecognized text")
321 assert _classify_op(op) == "impl"
322
323 def test_other_op_kind_returns_other(self) -> None:
324 from muse.cli.commands.narrative import _classify_op
325 assert _classify_op({"op": "unknown_op"}) == "other"
326
327
328 # ---------------------------------------------------------------------------
329 # Unit: _sanitise_msg
330 # ---------------------------------------------------------------------------
331
332
333 class TestSanitiseMsg:
334 def test_strips_control_chars(self) -> None:
335 from muse.cli.commands.narrative import _sanitise_msg
336 # ESC + some control chars
337 result = _sanitise_msg("\x1b[31mred\x1b[0m")
338 assert "\x1b" not in result
339 assert "red" in result
340
341 def test_truncates_at_72(self) -> None:
342 from muse.cli.commands.narrative import _sanitise_msg
343 long_msg = "x" * 100
344 result = _sanitise_msg(long_msg)
345 assert len(result) <= 72
346
347 def test_short_message_unchanged(self) -> None:
348 from muse.cli.commands.narrative import _sanitise_msg
349 msg = "feat: add compute_total"
350 assert _sanitise_msg(msg) == msg
351
352 def test_trailing_ellipsis_on_truncation(self) -> None:
353 from muse.cli.commands.narrative import _sanitise_msg
354 result = _sanitise_msg("a" * 100)
355 assert result.endswith("…")
356
357 def test_null_byte_stripped(self) -> None:
358 from muse.cli.commands.narrative import _sanitise_msg
359 result = _sanitise_msg("hello\x00world")
360 assert "\x00" not in result
361
362
363 # ---------------------------------------------------------------------------
364 # Unit: _format_date / _format_date_long
365 # ---------------------------------------------------------------------------
366
367
368 class TestFormatDate:
369 def _dt(self, year: int = 2026, month: int = 1, day: int = 12) -> datetime.datetime:
370 return datetime.datetime(year, month, day, tzinfo=datetime.timezone.utc)
371
372 def test_format_date_basic(self) -> None:
373 from muse.cli.commands.narrative import _format_date
374 result = _format_date(self._dt(2026, 1, 12))
375 assert "Jan" in result
376 assert "12" in result
377 assert "2026" in result
378
379 def test_format_date_no_double_space(self) -> None:
380 from muse.cli.commands.narrative import _format_date
381 # Day 1 could produce double space; must be collapsed.
382 result = _format_date(self._dt(2026, 3, 1))
383 assert " " not in result
384
385 def test_format_date_long_st_suffix(self) -> None:
386 from muse.cli.commands.narrative import _format_date_long
387 result = _format_date_long(self._dt(2026, 1, 1))
388 assert "1st" in result
389
390 def test_format_date_long_nd_suffix(self) -> None:
391 from muse.cli.commands.narrative import _format_date_long
392 result = _format_date_long(self._dt(2026, 1, 2))
393 assert "2nd" in result
394
395 def test_format_date_long_rd_suffix(self) -> None:
396 from muse.cli.commands.narrative import _format_date_long
397 result = _format_date_long(self._dt(2026, 1, 3))
398 assert "3rd" in result
399
400 def test_format_date_long_th_suffix(self) -> None:
401 from muse.cli.commands.narrative import _format_date_long
402 result = _format_date_long(self._dt(2026, 1, 4))
403 assert "4th" in result
404
405 def test_format_date_long_11th_exception(self) -> None:
406 from muse.cli.commands.narrative import _format_date_long
407 # 11th should be 'th' not 'st'
408 result = _format_date_long(self._dt(2026, 1, 11))
409 assert "11th" in result
410
411 def test_format_date_long_12th_exception(self) -> None:
412 from muse.cli.commands.narrative import _format_date_long
413 result = _format_date_long(self._dt(2026, 1, 12))
414 assert "12th" in result
415
416 def test_format_date_long_13th_exception(self) -> None:
417 from muse.cli.commands.narrative import _format_date_long
418 result = _format_date_long(self._dt(2026, 1, 13))
419 assert "13th" in result
420
421
422 # ---------------------------------------------------------------------------
423 # Unit: _days_ago
424 # ---------------------------------------------------------------------------
425
426
427 class TestDaysAgo:
428 def _dt(self, days_ago: int) -> datetime.datetime:
429 now = datetime.datetime.now(tz=datetime.timezone.utc)
430 return now - datetime.timedelta(days=days_ago)
431
432 def test_today(self) -> None:
433 from muse.cli.commands.narrative import _days_ago
434 assert _days_ago(self._dt(0)) == "today"
435
436 def test_yesterday(self) -> None:
437 from muse.cli.commands.narrative import _days_ago
438 assert _days_ago(self._dt(1)) == "1 day ago"
439
440 def test_few_days(self) -> None:
441 from muse.cli.commands.narrative import _days_ago
442 result = _days_ago(self._dt(5))
443 assert "days ago" in result
444
445 def test_weeks(self) -> None:
446 from muse.cli.commands.narrative import _days_ago
447 result = _days_ago(self._dt(14))
448 assert "wk ago" in result
449
450 def test_months(self) -> None:
451 from muse.cli.commands.narrative import _days_ago
452 result = _days_ago(self._dt(60))
453 assert "mo ago" in result
454
455 def test_years(self) -> None:
456 from muse.cli.commands.narrative import _days_ago
457 result = _days_ago(self._dt(400))
458 assert "yr" in result
459
460 def test_none_returns_unknown(self) -> None:
461 from muse.cli.commands.narrative import _days_ago
462 assert _days_ago(None) == "unknown"
463
464
465 # ---------------------------------------------------------------------------
466 # Unit: _relative_to
467 # ---------------------------------------------------------------------------
468
469
470 class TestRelativeTo:
471 def _dt(self, year: int, month: int, day: int) -> datetime.datetime:
472 return datetime.datetime(year, month, day)
473
474 def test_same_day(self) -> None:
475 from muse.cli.commands.narrative import _relative_to
476 d = self._dt(2026, 1, 12)
477 assert _relative_to(d, d) == "the same day"
478
479 def test_one_day_later(self) -> None:
480 from muse.cli.commands.narrative import _relative_to
481 d1 = self._dt(2026, 1, 12)
482 d2 = self._dt(2026, 1, 13)
483 assert _relative_to(d1, d2) == "1 day later"
484
485 def test_days_later(self) -> None:
486 from muse.cli.commands.narrative import _relative_to
487 d1 = self._dt(2026, 1, 12)
488 d2 = self._dt(2026, 1, 17)
489 result = _relative_to(d1, d2)
490 assert "days later" in result
491
492 def test_weeks_later(self) -> None:
493 from muse.cli.commands.narrative import _relative_to
494 d1 = self._dt(2026, 1, 1)
495 d2 = self._dt(2026, 1, 15) # 14 days = 2 weeks
496 result = _relative_to(d1, d2)
497 assert "week" in result and "later" in result
498
499 def test_months_later(self) -> None:
500 from muse.cli.commands.narrative import _relative_to
501 d1 = self._dt(2026, 1, 1)
502 d2 = self._dt(2026, 4, 1) # ~90 days
503 result = _relative_to(d1, d2)
504 assert "month" in result and "later" in result
505
506 def test_years_later(self) -> None:
507 from muse.cli.commands.narrative import _relative_to
508 d1 = self._dt(2024, 1, 1)
509 d2 = self._dt(2026, 1, 1)
510 result = _relative_to(d1, d2)
511 assert "year" in result and "later" in result
512
513
514 # ---------------------------------------------------------------------------
515 # Unit: _extract_rename
516 # ---------------------------------------------------------------------------
517
518
519 class TestExtractRename:
520 def test_renamed_x_to_y_pattern(self) -> None:
521 from muse.cli.commands.narrative import _extract_rename
522 old, new = _extract_rename("renamed foo to bar", "")
523 assert old == "foo"
524 assert new == "bar"
525
526 def test_moved_x_to_y_pattern(self) -> None:
527 from muse.cli.commands.narrative import _extract_rename
528 old, new = _extract_rename("moved old_name to new_name", "")
529 assert old == "old_name"
530 assert new == "new_name"
531
532 def test_fallback_from_colons(self) -> None:
533 from muse.cli.commands.narrative import _extract_rename
534 old, new = _extract_rename("billing.py::new_func", "billing.py::old_func")
535 assert old == "old_func"
536 assert new == "new_func"
537
538 def test_empty_summaries_return_empty(self) -> None:
539 from muse.cli.commands.narrative import _extract_rename
540 old, new = _extract_rename("", "")
541 assert old == ""
542 assert new == ""
543
544 def test_case_insensitive_match(self) -> None:
545 from muse.cli.commands.narrative import _extract_rename
546 old, new = _extract_rename("Renamed Alpha to Beta", "")
547 assert old == "Alpha"
548 assert new == "Beta"
549
550
551 # ---------------------------------------------------------------------------
552 # Unit: _event_detail
553 # ---------------------------------------------------------------------------
554
555
556 class TestEventDetail:
557 def _raw_event(self, event_type: str, new_sum: str = "", old_sum: str = "") -> "_RawEvent":
558 from muse.cli.commands.narrative import _RawEvent
559 import datetime
560 return _RawEvent(
561 ts=datetime.datetime(2026, 1, 12),
562 commit_id="abc1234",
563 commit_msg="feat: something",
564 sem_ver_bump="minor",
565 event_type=event_type,
566 op_new_summary=new_sum,
567 op_old_summary=old_sum,
568 )
569
570 def test_rename_event_extracts_arrow(self) -> None:
571 from muse.cli.commands.narrative import _event_detail
572 ev = self._raw_event("rename", "renamed foo to bar", "")
573 result = _event_detail(ev)
574 assert "foo" in result and "bar" in result
575
576 def test_create_event_returns_summary(self) -> None:
577 from muse.cli.commands.narrative import _event_detail
578 ev = self._raw_event("create", "Created as a function taking 2 params.")
579 result = _event_detail(ev)
580 assert "Created" in result
581
582 def test_impl_event_returns_empty(self) -> None:
583 from muse.cli.commands.narrative import _event_detail
584 ev = self._raw_event("impl", "")
585 # For impl events with no special content, detail is empty.
586 result = _event_detail(ev)
587 assert isinstance(result, str)
588
589
590 # ---------------------------------------------------------------------------
591 # --show-source with --json does not crash
592 # ---------------------------------------------------------------------------
593
594
595 class TestShowSourceWithJson:
596 def test_show_source_json_combination_does_not_crash(
597 self, narrative_repo: pathlib.Path
598 ) -> None:
599 """--show-source is ignored in JSON mode; command must not crash."""
600 r = runner.invoke(cli, CMD + [ADDR, "--json", "--show-source"])
601 # Should exit zero even if --show-source is silently ignored in JSON mode.
602 assert r.exit_code == 0, r.output
603 data = json.loads(r.output)
604 assert "address" in data
605
606
607 class TestRegisterFlags:
608 def _parse(self, *args: str) -> "argparse.Namespace":
609 import argparse
610 from muse.cli.commands.narrative import register
611 p = argparse.ArgumentParser()
612 top_sub = p.add_subparsers()
613 code_p = top_sub.add_parser("code")
614 code_sub = code_p.add_subparsers()
615 register(code_sub)
616 return p.parse_args(["code", "narrative", "dummy.py::fn", *args])
617
618 def test_json_short_flag(self) -> None:
619 args = self._parse("-j")
620 assert args.json_out is True
621
622 def test_json_long_flag(self) -> None:
623 args = self._parse("--json")
624 assert args.json_out is True
625
626 def test_default_no_json(self) -> None:
627 args = self._parse()
628 assert args.json_out is False
629
File History 1 commit
sha256:74b5023693ac2ab80e3b89fddc66e0d60d7d931a1266d3f9294f645c3102fe76 tests/test_lineage_supercharge.py, tests/test_narrative_sup… Human 3 days ago