返回 last30days-skill
test_youtube_backfill.py
根目录 / tests / test_youtube_backfill.py
1 """Tests for backfill_transcripts - the post-selection transcript pass (#542).
2
3 search_and_transcribe() fetches transcripts for each search's top-by-views
4 candidates, but the pipeline's final selection ranks by relevance. When the
5 two sets are disjoint, every surviving video ships transcript-less. These
6 tests pin the second-pass backfill that runs from _finalize_items_by_source().
7 """
8
9 import unittest
10 from unittest import mock
11
12 from lib import schema, youtube_yt
13
14
15 def _item(item_id, **metadata):
16 return schema.SourceItem(
17 item_id=item_id,
18 source="youtube",
19 title=f"Video {item_id}",
20 body=f"Video {item_id}",
21 url=f"https://www.youtube.com/watch?v={item_id}",
22 metadata=dict(metadata),
23 )
24
25
26 TRANSCRIPT = (
27 "Claude Code skills are directories containing a SKILL md file. "
28 "This video covers the fastest growing skills on GitHub this month. "
29 "Superpowers enforces test driven development across every phase."
30 )
31
32
33 class TestBackfillTranscripts(unittest.TestCase):
34 def setUp(self):
35 patcher = mock.patch.object(youtube_yt, "is_ytdlp_installed", return_value=True)
36 patcher.start()
37 self.addCleanup(patcher.stop)
38
39 def test_backfills_survivors_without_transcripts(self):
40 """The #542 shape: finalized videos disjoint from the fetched set."""
41 items = [_item("vidA"), _item("vidB"), _item("vidC"), _item("vidD")]
42 with mock.patch.object(
43 youtube_yt, "fetch_transcripts_parallel",
44 return_value={"vidA": TRANSCRIPT, "vidB": TRANSCRIPT},
45 ) as fetch:
46 youtube_yt.backfill_transcripts(items, topic="claude code skills", depth="default")
47
48 # default budget is 2, attempts capped at need*3
49 fetched_ids = fetch.call_args.args[0]
50 self.assertEqual(fetched_ids, ["vidA", "vidB", "vidC", "vidD"])
51 self.assertEqual(items[0].metadata["transcript_snippet"], TRANSCRIPT)
52 self.assertEqual(items[1].metadata["transcript_snippet"], TRANSCRIPT)
53 self.assertNotIn("transcript_snippet", items[2].metadata)
54 self.assertTrue(items[0].metadata.get("transcript_highlights"))
55 self.assertTrue(items[0].snippet)
56
57 def test_noop_when_budget_already_met(self):
58 """Survivors that already carry >= limit transcripts trigger no fetch."""
59 items = [
60 _item("vidA", transcript_snippet=TRANSCRIPT),
61 _item("vidB", transcript_highlights=["quote"]),
62 _item("vidC"),
63 ]
64 with mock.patch.object(youtube_yt, "fetch_transcripts_parallel") as fetch:
65 youtube_yt.backfill_transcripts(items, depth="default")
66 fetch.assert_not_called()
67
68 def test_partial_budget_fetches_only_the_gap(self):
69 """One transcript present at default depth (limit 2) -> need is 1."""
70 items = [
71 _item("vidA", transcript_snippet=TRANSCRIPT),
72 _item("vidB"),
73 _item("vidC"),
74 _item("vidD"),
75 _item("vidE"),
76 ]
77 with mock.patch.object(
78 youtube_yt, "fetch_transcripts_parallel", return_value={"vidB": TRANSCRIPT},
79 ) as fetch:
80 youtube_yt.backfill_transcripts(items, depth="default")
81 # need=1, attempts capped at need*3=3
82 self.assertEqual(fetch.call_args.args[0], ["vidB", "vidC", "vidD"])
83
84 def test_noop_at_quick_depth(self):
85 """quick depth has a transcript budget of 0 - never fetch."""
86 items = [_item("vidA")]
87 with mock.patch.object(youtube_yt, "fetch_transcripts_parallel") as fetch:
88 youtube_yt.backfill_transcripts(items, depth="quick")
89 fetch.assert_not_called()
90
91 def test_noop_when_ytdlp_missing(self):
92 items = [_item("vidA")]
93 with mock.patch.object(youtube_yt, "is_ytdlp_installed", return_value=False):
94 with mock.patch.object(youtube_yt, "fetch_transcripts_parallel") as fetch:
95 youtube_yt.backfill_transcripts(items, depth="default")
96 fetch.assert_not_called()
97
98 def test_skips_captions_disabled_items(self):
99 """Uploader-disabled captions are not retried and stay marked."""
100 items = [_item("vidA", captions_disabled=True), _item("vidB")]
101 with mock.patch.object(
102 youtube_yt, "fetch_transcripts_parallel", return_value={"vidB": TRANSCRIPT},
103 ) as fetch:
104 youtube_yt.backfill_transcripts(items, depth="default")
105 self.assertEqual(fetch.call_args.args[0], ["vidB"])
106
107 def test_marks_newly_discovered_captions_disabled(self):
108 """A backfill attempt that reveals no caption tracks feeds quality_nudge."""
109 def fake_fetch(video_ids, out_captions_disabled=None, token=None):
110 if out_captions_disabled is not None:
111 out_captions_disabled.add("vidA")
112 return {"vidA": None, "vidB": TRANSCRIPT}
113
114 items = [_item("vidA"), _item("vidB")]
115 with mock.patch.object(youtube_yt, "fetch_transcripts_parallel", side_effect=fake_fetch):
116 youtube_yt.backfill_transcripts(items, depth="default")
117 self.assertTrue(items[0].metadata.get("captions_disabled"))
118 self.assertNotIn("transcript_snippet", items[0].metadata)
119 self.assertEqual(items[1].metadata["transcript_snippet"], TRANSCRIPT)
120
121 def test_does_not_overwrite_existing_snippet(self):
122 items = [_item("vidA")]
123 items[0].snippet = "ranker-chosen snippet"
124 with mock.patch.object(
125 youtube_yt, "fetch_transcripts_parallel", return_value={"vidA": TRANSCRIPT},
126 ):
127 youtube_yt.backfill_transcripts(items, depth="default")
128 self.assertEqual(items[0].snippet, "ranker-chosen snippet")
129 self.assertEqual(items[0].metadata["transcript_snippet"], TRANSCRIPT)
130
131
132 class TestFinalizeWiring(unittest.TestCase):
133 def test_finalize_calls_backfill_for_youtube_survivors(self):
134 from lib import pipeline
135
136 items = [_item("vidA")]
137 with mock.patch.object(youtube_yt, "backfill_transcripts") as backfill:
138 pipeline._finalize_items_by_source(
139 {"youtube": items}, topic="claude code skills", depth="deep",
140 )
141 backfill.assert_called_once()
142 self.assertEqual(backfill.call_args.kwargs.get("depth"), "deep")
143 self.assertEqual(backfill.call_args.kwargs.get("topic"), "claude code skills")
144
145 def test_finalize_skips_backfill_in_mock_mode(self):
146 from lib import pipeline
147
148 items = [_item("vidA")]
149 with mock.patch.object(youtube_yt, "backfill_transcripts") as backfill:
150 pipeline._finalize_items_by_source(
151 {"youtube": items}, topic="claude code skills", depth="default", mock=True,
152 )
153 backfill.assert_not_called()
154
155
156 if __name__ == "__main__":
157 unittest.main()
158
158 lines PYTHON