| 1 | """Retrieval-floor exemption for posts authored by a handle the run is searching. |
| 2 | |
| 3 | The production failure this pins: a mixed batch where the mention lane clears |
| 4 | the relevance floor and the from lane does not. Because |
| 5 | `prune_low_relevance` ends with `return filtered or items`, the all-fail rescue |
| 6 | only fires when *everything* fails. A mixed batch is therefore the exact shape |
| 7 | that silently loses the subject's own posts, and no prior test exercised it -- |
| 8 | existing supplement-lane tests use single-item batches that trip the rescue. |
| 9 | """ |
| 10 | |
| 11 | from lib import schema, signals |
| 12 | |
| 13 | |
| 14 | def _x_item(item_id: str, author: str, relevance: float, engagement: dict | None = None): |
| 15 | item = schema.SourceItem( |
| 16 | item_id=item_id, |
| 17 | source="x", |
| 18 | title="", |
| 19 | body="post body", |
| 20 | url=f"https://x.com/{author}/status/{item_id}", |
| 21 | author=author, |
| 22 | engagement=engagement or {}, |
| 23 | ) |
| 24 | item.local_relevance = relevance |
| 25 | return item |
| 26 | |
| 27 | |
| 28 | def _mixed_batch(): |
| 29 | """From-lane items score 0.0 (a post rarely names its own author); |
| 30 | mention-lane items clear the floor because they contain the handle.""" |
| 31 | return [ |
| 32 | _x_item("1", "steipete", 0.0, {"likes": 7773, "reposts": 391}), |
| 33 | _x_item("2", "steipete", 0.0, {"likes": 3466, "reposts": 128}), |
| 34 | _x_item("3", "someone_else", 0.32, {"likes": 78, "reposts": 8}), |
| 35 | _x_item("4", "another_acct", 0.23, {"likes": 8, "reposts": 1}), |
| 36 | ] |
| 37 | |
| 38 | |
| 39 | def _annotate(items): |
| 40 | """Populate engagement_score the way the real pipeline does.""" |
| 41 | scores = signals.normalize([signals.engagement_raw(i) for i in items]) |
| 42 | for item, score in zip(items, scores, strict=True): |
| 43 | item.engagement_score = score |
| 44 | return items |
| 45 | |
| 46 | |
| 47 | def test_mixed_batch_keeps_first_party_and_drops_off_topic(): |
| 48 | items = _annotate(_mixed_batch()) |
| 49 | kept = signals.prune_low_relevance(items, first_party_handles={"steipete"}) |
| 50 | authors = sorted(i.author for i in kept) |
| 51 | assert "steipete" in authors, ( |
| 52 | "first-party posts were pruned from a mixed batch; this is the measured " |
| 53 | "defect where 8 subject-authored posts never reached the report" |
| 54 | ) |
| 55 | assert len([a for a in authors if a == "steipete"]) == 2 |
| 56 | |
| 57 | |
| 58 | def test_mixed_batch_without_exemption_still_loses_first_party(): |
| 59 | """Characterizes the defect: without the exemption the batch drops them.""" |
| 60 | items = _annotate(_mixed_batch()) |
| 61 | kept = signals.prune_low_relevance(items) |
| 62 | assert all(i.author != "steipete" for i in kept), ( |
| 63 | "expected the unexempted path to still drop zero-relevance first-party " |
| 64 | "posts; if this now passes, the floor changed and the exemption's " |
| 65 | "justification needs rechecking" |
| 66 | ) |
| 67 | |
| 68 | |
| 69 | def test_non_first_party_below_floor_is_still_pruned(): |
| 70 | """The exemption must be scoped, not a blanket floor removal.""" |
| 71 | items = _annotate(_mixed_batch() + [_x_item("5", "spam_acct", 0.02, {"likes": 0})]) |
| 72 | kept = signals.prune_low_relevance(items, first_party_handles={"steipete"}) |
| 73 | assert all(i.author != "spam_acct" for i in kept) |
| 74 | |
| 75 | |
| 76 | def test_batch_minimum_is_not_treated_as_zero_engagement(): |
| 77 | """`normalize` maps the batch minimum to exactly 0, so an item with real |
| 78 | engagement was being given the stricter 1.5x social threshold purely for |
| 79 | being the least-engaged item present.""" |
| 80 | items = _annotate([ |
| 81 | _x_item("1", "acct_a", 0.20, {"likes": 500, "reposts": 40}), |
| 82 | _x_item("2", "acct_b", 0.20, {"likes": 5000, "reposts": 400}), |
| 83 | ]) |
| 84 | least = next(i for i in items if i.author == "acct_a") |
| 85 | assert least.engagement_score == 0, "precondition: min-max maps batch min to 0" |
| 86 | kept = signals.prune_low_relevance(items) |
| 87 | assert any(i.author == "acct_a" for i in kept), ( |
| 88 | "an item with 500 likes was pruned by the zero-engagement gate purely " |
| 89 | "because it was the batch minimum" |
| 90 | ) |
| 91 | |
| 92 | |
| 93 | def test_genuinely_zero_engagement_still_gets_stricter_threshold(): |
| 94 | """The stricter gate must survive for real zero-engagement social noise.""" |
| 95 | items = _annotate([ |
| 96 | _x_item("1", "acct_a", 0.20, {"likes": 0, "reposts": 0}), |
| 97 | _x_item("2", "acct_b", 0.90, {"likes": 5000, "reposts": 400}), |
| 98 | ]) |
| 99 | kept = signals.prune_low_relevance(items) |
| 100 | assert all(i.author != "acct_a" for i in kept), ( |
| 101 | "a genuinely zero-engagement item at 0.20 should fail the 0.225 gate" |
| 102 | ) |
| 103 | |
| 104 | |
| 105 | def test_all_fail_rescue_is_unchanged(): |
| 106 | items = _annotate([_x_item("1", "acct_a", 0.01), _x_item("2", "acct_b", 0.02)]) |
| 107 | kept = signals.prune_low_relevance(items) |
| 108 | assert len(kept) == 2, "the all-fail rescue must still return the batch" |
| 109 | |
| 110 | |
| 111 | def test_batch_with_no_first_party_behaves_as_before(): |
| 112 | items = _annotate(_mixed_batch()) |
| 113 | assert signals.prune_low_relevance(items, first_party_handles=frozenset()) == \ |
| 114 | signals.prune_low_relevance(items) |
| 115 | |
| 116 | |
| 117 | def test_handles_are_matched_case_insensitively(): |
| 118 | items = _annotate(_mixed_batch()) |
| 119 | kept = signals.prune_low_relevance(items, first_party_handles={"SteiPete"}) |
| 120 | assert any(i.author == "steipete" for i in kept) |
| 121 | |
| 122 | |
| 123 | # --- KTD8: one owner for the entity-miss predicate ------------------------- |
| 124 | |
| 125 | def _candidate(explanation: str, final_score: float, author: str = "someone"): |
| 126 | url = f"https://x.com/{author}/status/1" |
| 127 | cand = schema.Candidate( |
| 128 | candidate_id="c1", |
| 129 | item_id="i1", |
| 130 | source="x", |
| 131 | title="t", |
| 132 | url=url, |
| 133 | snippet="s", |
| 134 | subquery_labels=["primary"], |
| 135 | native_ranks={"primary:x": 1}, |
| 136 | local_relevance=0.0, |
| 137 | freshness=80, |
| 138 | engagement=50, |
| 139 | source_quality=0.68, |
| 140 | rrf_score=0.02, |
| 141 | ) |
| 142 | cand.source_items = [ |
| 143 | schema.SourceItem( |
| 144 | item_id="i1", source="x", title="t", body="b", url=url, author=author, |
| 145 | ) |
| 146 | ] |
| 147 | cand.explanation = explanation |
| 148 | cand.final_score = final_score |
| 149 | return cand |
| 150 | |
| 151 | |
| 152 | def test_render_delegates_to_shared_predicate(): |
| 153 | """render must not carry its own copy of the entity-miss test.""" |
| 154 | from lib import render, rerank |
| 155 | cand = _candidate("fallback-local-score (entity-miss demotion)", 40.0) |
| 156 | assert render._best_take_relevance_ok(cand) is rerank.candidate_relevance_ok(cand) |
| 157 | ok = _candidate("llm-scored", 40.0) |
| 158 | assert render._best_take_relevance_ok(ok) is rerank.candidate_relevance_ok(ok) |
| 159 | |
| 160 | |
| 161 | def test_shared_predicate_rejects_entity_miss_and_zero_score(): |
| 162 | from lib import rerank |
| 163 | assert not rerank.candidate_relevance_ok( |
| 164 | _candidate("fallback-local-score (entity-miss demotion)", 40.0) |
| 165 | ) |
| 166 | assert not rerank.candidate_relevance_ok(_candidate("llm-scored", 0.0)) |
| 167 | assert rerank.candidate_relevance_ok(_candidate("llm-scored", 40.0)) |
| 168 | |
| 169 | |
| 170 | def test_first_party_carveout_reaches_render_side_gate(): |
| 171 | """The measured KTD8 failure: a first-party post demoted on the LLM path |
| 172 | was floored by rerank but still discarded at render because the render-side |
| 173 | copy re-tested the explanation string.""" |
| 174 | from lib import render, rerank |
| 175 | cand = _candidate("fallback-local-score (entity-miss demotion)", 0.0, author="steipete") |
| 176 | assert not render._best_take_relevance_ok(cand), "precondition: demoted before the floor runs" |
| 177 | rerank._apply_first_party_floor([cand], resolved_handles={"steipete"}) |
| 178 | assert cand.final_score >= rerank.FIRST_PARTY_FLOOR |
| 179 | assert render._best_take_relevance_ok(cand), ( |
| 180 | "first-party carve-out applied in rerank did not propagate to the " |
| 181 | "render-side relevance gate" |
| 182 | ) |
| 183 | |
| 184 | |
| 185 | def test_non_first_party_demotion_survives_the_floor_pass(): |
| 186 | from lib import render, rerank |
| 187 | cand = _candidate("fallback-local-score (entity-miss demotion)", 0.0, author="rando") |
| 188 | rerank._apply_first_party_floor([cand], resolved_handles={"steipete"}) |
| 189 | assert not render._best_take_relevance_ok(cand), ( |
| 190 | "an off-topic collision post must stay buried" |
| 191 | ) |
| 192 | |
| 193 | |
| 194 | # --- Phase 1 / quick-depth wiring (Greptile) -------------------------------- |
| 195 | |
| 196 | def test_phase_one_normalize_receives_the_explicit_handles(): |
| 197 | """Quick runs skip Phase 2 entirely, so an exemption reaching only the |
| 198 | supplement path leaves quick-depth reports discarding the subject's posts.""" |
| 199 | import inspect |
| 200 | from lib import pipeline |
| 201 | src = inspect.getsource(pipeline.run) |
| 202 | assert "explicit_first_party = {" in src, ( |
| 203 | "the user-named handles must be resolved before retrieval, not after" |
| 204 | ) |
| 205 | assert "first_party_handles=explicit_first_party," in src, ( |
| 206 | "the Phase 1 per-source normalize must receive the exemption" |
| 207 | ) |
| 208 | |
| 209 | |
| 210 | def test_explicit_handles_are_available_before_any_retrieval(): |
| 211 | """The entity-extracted set does not exist until Phase 2; the explicit one |
| 212 | must be built from run()'s own arguments so Phase 1 can use it.""" |
| 213 | import inspect |
| 214 | from lib import pipeline |
| 215 | src = inspect.getsource(pipeline.run) |
| 216 | build_at = src.index("explicit_first_party = {") |
| 217 | first_use = src.index("first_party_handles=explicit_first_party,") |
| 218 | assert build_at < first_use |
| 219 | |
| 220 | |
| 221 | def test_related_handles_lane_gets_the_exemption(): |
| 222 | import inspect |
| 223 | from lib import pipeline |
| 224 | src = inspect.getsource(pipeline._run_supplemental_searches) |
| 225 | assert "first_party_handles=related_handles," in src |
| 226 | |
| 227 | |
| 228 | def test_quick_run_without_an_explicit_handle_still_protects_the_subject(): |
| 229 | """The gap in the first fix: covering only user-typed handles does nothing |
| 230 | for a quick run, where nobody typed one and Phase 2's automatic resolution |
| 231 | never executes.""" |
| 232 | from lib import pipeline |
| 233 | candidates = pipeline._topic_first_party_candidates("Peter Steinberger steipete") |
| 234 | assert "steipete" in candidates |
| 235 | |
| 236 | items = _annotate(_mixed_batch()) |
| 237 | kept = signals.prune_low_relevance(items, first_party_handles=candidates) |
| 238 | assert any(i.author == "steipete" for i in kept), ( |
| 239 | "a quick search naming the subject must not discard what they wrote" |
| 240 | ) |
| 241 | |
| 242 | |
| 243 | def test_topic_candidates_include_explicit_mentions(): |
| 244 | from lib import pipeline |
| 245 | assert "getenergy_" in pipeline._topic_first_party_candidates("@GetEnergy_ launch") |
| 246 | |
| 247 | |
| 248 | def test_topic_candidates_exclude_stopwords(): |
| 249 | from lib import pipeline |
| 250 | got = pipeline._topic_first_party_candidates("the best of the year") |
| 251 | assert "the" not in got and "of" not in got |
| 252 | |
| 253 | |
| 254 | def test_topic_candidates_do_not_exempt_unrelated_authors(): |
| 255 | """Candidates only matter when a post's author matches one, so ordinary |
| 256 | words cost nothing -- no account is named 'lunch'.""" |
| 257 | from lib import pipeline |
| 258 | candidates = pipeline._topic_first_party_candidates("bentgo lunch boxes") |
| 259 | items = _annotate(_mixed_batch() + [_x_item("9", "spam_acct", 0.01, {"likes": 0})]) |
| 260 | kept = signals.prune_low_relevance(items, first_party_handles=candidates) |
| 261 | assert all(i.author != "spam_acct" for i in kept) |
| 262 | |
| 263 | |
| 264 | def test_topic_candidates_are_unioned_into_the_explicit_set(): |
| 265 | import inspect |
| 266 | from lib import pipeline |
| 267 | src = inspect.getsource(pipeline.run) |
| 268 | assert "_topic_first_party_candidates(topic)" in src |
| 269 | build = src.index("explicit_first_party") |
| 270 | use = src.index("first_party_handles=explicit_first_party,") |
| 271 | assert build < use |
| 272 | |
| 273 | |
| 274 | def test_name_only_topic_resolves_the_subject_from_mentions(): |
| 275 | """The hard case: search "Peter Steinberger" with no handle anywhere. His |
| 276 | handle is @steipete, which matches no topic token, and Phase 2's resolution |
| 277 | has not run. Posts *about* him mention him, which is the signal the engine |
| 278 | already uses -- just later than the prune.""" |
| 279 | from lib import pipeline |
| 280 | raw = [ |
| 281 | {"text": "Great thread from @steipete on agent loops"}, |
| 282 | {"text": "@steipete nailed this one"}, |
| 283 | {"text": "watching @steipete build in public is wild"}, |
| 284 | {"text": "unrelated chatter with no mention"}, |
| 285 | ] |
| 286 | assert "steipete" in pipeline._batch_subject_handles(raw) |
| 287 | |
| 288 | |
| 289 | def test_batch_subject_keys_on_mentions_not_authors(): |
| 290 | """A prolific commentator inflates author counts; being mentioned by other |
| 291 | accounts is what identifies the subject.""" |
| 292 | from lib import pipeline |
| 293 | raw = [ |
| 294 | {"author_handle": "spam_acct", "text": "buy now"}, |
| 295 | {"author_handle": "spam_acct", "text": "buy now again"}, |
| 296 | {"author_handle": "spam_acct", "text": "and again"}, |
| 297 | {"author_handle": "someone", "text": "actually useful thread by @realsubject"}, |
| 298 | ] |
| 299 | got = pipeline._batch_subject_handles(raw) |
| 300 | assert "spam_acct" not in got |
| 301 | assert "realsubject" in got |
| 302 | |
| 303 | |
| 304 | def test_batch_subject_is_capped(): |
| 305 | from lib import pipeline |
| 306 | raw = [{"text": f"@acct{i} said something"} for i in range(10)] |
| 307 | assert len(pipeline._batch_subject_handles(raw)) <= 2 |
| 308 | |
| 309 | |
| 310 | def test_batch_subject_is_empty_without_mentions(): |
| 311 | from lib import pipeline |
| 312 | assert pipeline._batch_subject_handles([{"text": "no mentions here"}]) == set() |
| 313 | assert pipeline._batch_subject_handles([]) == set() |
| 314 | |
| 315 | |
| 316 | def test_batch_inference_is_unioned_not_gated(): |
| 317 | """Regression: gating this on "no handles supplied" made it dead code. |
| 318 | |
| 319 | The caller's set is derived partly from topic tokens, so it is non-empty |
| 320 | for essentially every real topic -- a fallback would never fire and the |
| 321 | name-only case would stay broken while looking fixed. |
| 322 | """ |
| 323 | import inspect |
| 324 | from lib import pipeline |
| 325 | src = inspect.getsource(pipeline._normalize_score_dedupe) |
| 326 | assert "floor_handles |= _batch_subject_handles(raw_items)" in src |
| 327 | assert "not floor_handles" not in src, ( |
| 328 | "batch inference must union, never gate on an empty supplied set" |
| 329 | ) |
| 330 | |
| 331 | |
| 332 | def test_name_only_topic_keeps_subject_posts_end_to_end(): |
| 333 | """The full path: topic names a person, handle appears nowhere in it, and |
| 334 | the subject's own zero-relevance posts still survive the floor.""" |
| 335 | from lib import pipeline |
| 336 | supplied = pipeline._topic_first_party_candidates("Peter Steinberger") |
| 337 | assert "steipete" not in supplied, "precondition: the handle is not in the topic" |
| 338 | raw = [ |
| 339 | {"text": "Great thread from @steipete on agent loops"}, |
| 340 | {"text": "@steipete nailed this"}, |
| 341 | {"text": "more praise for @steipete"}, |
| 342 | ] |
| 343 | inferred = pipeline._batch_subject_handles(raw) |
| 344 | items = _annotate(_mixed_batch()) |
| 345 | kept = signals.prune_low_relevance( |
| 346 | items, first_party_handles=supplied | inferred |
| 347 | ) |
| 348 | assert any(i.author == "steipete" for i in kept) |
| 349 | |
| 350 | |
| 351 | # --- ordering fix: prune X after resolution, not before --------------------- |
| 352 | |
| 353 | def test_x_defers_its_relevance_floor_past_resolution(): |
| 354 | """The root ordering bug: X was pruned before the run knew who the subject |
| 355 | was, so the floor could not exempt an author nobody had identified yet. No |
| 356 | amount of guessing at prune time substitutes for knowing.""" |
| 357 | import inspect |
| 358 | from lib import pipeline |
| 359 | src = inspect.getsource(pipeline.run) |
| 360 | assert 'defer_relevance_prune=(source == "x")' in src |
| 361 | |
| 362 | |
| 363 | def test_deferred_prune_runs_after_resolution_and_before_fusion(): |
| 364 | import inspect |
| 365 | from lib import pipeline |
| 366 | src = inspect.getsource(pipeline.run) |
| 367 | resolve_at = src.index("resolved_handles = explicit_first_party") |
| 368 | prune_at = src.index("Deferred X relevance floor") |
| 369 | fuse_at = src.index("candidates = weighted_rrf(") |
| 370 | assert resolve_at < prune_at < fuse_at, ( |
| 371 | "the deferred floor must see resolved handles and still run before fusion" |
| 372 | ) |
| 373 | |
| 374 | |
| 375 | def test_non_x_sources_still_prune_in_place(): |
| 376 | """Only X defers; everything else keeps its existing behavior.""" |
| 377 | import inspect |
| 378 | from lib import pipeline |
| 379 | src = inspect.getsource(pipeline._normalize_score_dedupe) |
| 380 | assert 'if source != "jobs" and not defer_relevance_prune:' in src |
| 381 | |
| 382 | |
| 383 | def test_deferred_prune_still_drops_off_topic_posts(): |
| 384 | """Deferring must not mean skipping.""" |
| 385 | items = _annotate(_mixed_batch() + [_x_item("9", "spam_acct", 0.01, {"likes": 0})]) |
| 386 | kept = signals.prune_low_relevance(items, first_party_handles={"steipete"}) |
| 387 | assert any(i.author == "steipete" for i in kept) |
| 388 | assert all(i.author != "spam_acct" for i in kept) |
| 389 | |
| 390 | |
| 391 | # --- unresolved subject policy: skip X floor when no real handle identified --- |
| 392 | |
| 393 | def test_topic_handle_mentions_extracts_only_at_mentions(): |
| 394 | """@mentions in the topic are real handles; regular words are not.""" |
| 395 | from lib import pipeline |
| 396 | assert pipeline._topic_handle_mentions("Peter Steinberger @steipete") == {"steipete"} |
| 397 | assert pipeline._topic_handle_mentions("Peter Steinberger") == set() |
| 398 | assert pipeline._topic_handle_mentions("@GetEnergy_ launch") == {"getenergy_"} |
| 399 | |
| 400 | |
| 401 | def test_topic_handle_mentions_is_case_insensitive(): |
| 402 | from lib import pipeline |
| 403 | assert pipeline._topic_handle_mentions("@SteiPete") == {"steipete"} |
| 404 | |
| 405 | |
| 406 | def test_entity_topic_no_handle_no_discovery_skips_x_floor(): |
| 407 | """Policy: when the subject cannot be identified, skip the X floor entirely. |
| 408 | |
| 409 | Entity-shaped topic, no --x-handle, auto-resolve/Phase 2 return nothing. |
| 410 | Retrieved X items include a zero-relevance post whose author is not a |
| 411 | topic token. That post survives prune because the floor is not applied. |
| 412 | A companion on-topic post is in the batch so the all-fail rescue cannot |
| 413 | hide an incorrectly applied floor. |
| 414 | """ |
| 415 | from unittest.mock import patch |
| 416 | from lib import pipeline |
| 417 | |
| 418 | topic = "Peter Steinberger" |
| 419 | assert pipeline._topic_handle_mentions(topic) == set() |
| 420 | topic_tokens = pipeline._topic_first_party_candidates(topic) |
| 421 | assert "peter" in topic_tokens and "steinberger" in topic_tokens |
| 422 | assert "rando_acct" not in topic_tokens |
| 423 | |
| 424 | raw_items = [ |
| 425 | { |
| 426 | "id": "1", |
| 427 | "text": "shipping a new agent loop tonight", |
| 428 | "url": "https://x.com/rando_acct/status/1", |
| 429 | "author_handle": "rando_acct", |
| 430 | "date": "2026-08-01", |
| 431 | "engagement": {"likes": 0, "reposts": 0, "replies": 0}, |
| 432 | }, |
| 433 | { |
| 434 | "id": "2", |
| 435 | "text": "Peter Steinberger just shipped another agent demo", |
| 436 | "url": "https://x.com/third_acct/status/2", |
| 437 | "author_handle": "third_acct", |
| 438 | "date": "2026-08-01", |
| 439 | "engagement": {"likes": 50, "reposts": 5, "replies": 2}, |
| 440 | }, |
| 441 | ] |
| 442 | plan = { |
| 443 | "intent": "person", |
| 444 | "freshness_mode": "balanced_recent", |
| 445 | "cluster_mode": "topic", |
| 446 | "subqueries": [{ |
| 447 | "label": "primary", |
| 448 | "search_query": topic, |
| 449 | "ranking_query": topic, |
| 450 | "sources": ["x"], |
| 451 | }], |
| 452 | "source_weights": {"x": 1.0}, |
| 453 | } |
| 454 | |
| 455 | def fake_retrieve(**kwargs): |
| 456 | if kwargs.get("source") == "x": |
| 457 | return raw_items, {} |
| 458 | return [], {} |
| 459 | |
| 460 | with patch("lib.pipeline._retrieve_stream", side_effect=lambda **kw: fake_retrieve(**kw)): |
| 461 | report = pipeline.run( |
| 462 | topic=topic, |
| 463 | config={"LAST30DAYS_REASONING_PROVIDER": "gemini"}, |
| 464 | depth="quick", |
| 465 | requested_sources=["x"], |
| 466 | mock=True, |
| 467 | external_plan=plan, |
| 468 | as_of_date="2026-08-14", |
| 469 | ) |
| 470 | |
| 471 | x_items = report.items_by_source.get("x") or [] |
| 472 | authors = {item.author for item in x_items} |
| 473 | assert "rando_acct" in authors, ( |
| 474 | "unresolved subject policy: skip X floor when no real handle identified; " |
| 475 | "the zero-relevance post whose author is not a topic token must survive" |
| 476 | ) |
| 477 | |
| 478 | |
| 479 | def test_entity_topic_with_at_mention_applies_x_floor(): |
| 480 | """When the topic DOES include @mentions, the X floor applies normally.""" |
| 481 | from lib import pipeline |
| 482 | |
| 483 | topic = "Peter Steinberger @steipete" |
| 484 | explicit_x_handles = pipeline._topic_handle_mentions(topic) |
| 485 | supplemental_handles = [] |
| 486 | |
| 487 | assert "steipete" in explicit_x_handles, ( |
| 488 | "precondition: @mention is a real handle" |
| 489 | ) |
| 490 | |
| 491 | real_x_handles = explicit_x_handles | { |
| 492 | h.lstrip("@").strip().lower() for h in supplemental_handles if h and h.strip() |
| 493 | } |
| 494 | assert real_x_handles, "real handles were resolved" |
| 495 | |
| 496 | topic_candidates = pipeline._topic_first_party_candidates(topic) |
| 497 | items = _annotate([ |
| 498 | _x_item("1", "steipete", 0.0, {"likes": 100, "reposts": 10}), |
| 499 | _x_item("2", "rando_acct", 0.0, {"likes": 0, "reposts": 0}), |
| 500 | _x_item("3", "third_acct", 0.18, {"likes": 20, "reposts": 2}), |
| 501 | ]) |
| 502 | |
| 503 | kept = signals.prune_low_relevance(items, first_party_handles=topic_candidates | real_x_handles) |
| 504 | |
| 505 | assert "steipete" in {i.author for i in kept}, ( |
| 506 | "first-party posts survive the floor" |
| 507 | ) |
| 508 | assert "rando_acct" not in {i.author for i in kept}, ( |
| 509 | "when real handles exist, off-topic zero-relevance posts are pruned" |
| 510 | ) |
| 511 | |
| 512 | |
| 513 | def test_explicit_x_handle_applies_x_floor(): |
| 514 | """--x-handle triggers the floor even with entity-only topic.""" |
| 515 | from lib import pipeline |
| 516 | |
| 517 | topic = "Peter Steinberger" |
| 518 | x_handle = "steipete" |
| 519 | explicit_x_handles = {x_handle.lstrip("@").strip().lower()} |
| 520 | |
| 521 | assert pipeline._topic_handle_mentions(topic) == set(), ( |
| 522 | "precondition: topic has no @mentions" |
| 523 | ) |
| 524 | assert explicit_x_handles == {"steipete"}, ( |
| 525 | "but we have an explicit --x-handle" |
| 526 | ) |
| 527 | |
| 528 | topic_candidates = pipeline._topic_first_party_candidates(topic) |
| 529 | resolved_handles = topic_candidates | explicit_x_handles |
| 530 | |
| 531 | items = _annotate([ |
| 532 | _x_item("1", "steipete", 0.0, {"likes": 100, "reposts": 10}), |
| 533 | _x_item("2", "rando_acct", 0.0, {"likes": 0, "reposts": 0}), |
| 534 | ]) |
| 535 | |
| 536 | kept = signals.prune_low_relevance(items, first_party_handles=resolved_handles) |
| 537 | |
| 538 | assert "steipete" in {i.author for i in kept} |
| 539 | assert "rando_acct" not in {i.author for i in kept}, ( |
| 540 | "--x-handle triggers the floor, pruning off-topic posts" |
| 541 | ) |
| 542 | |
| 543 | |
| 544 | # --- thin-source retry must not prune X before handle resolution ------------ |
| 545 | |
| 546 | def test_thin_retry_keeps_zero_relevance_subject_authored_x_post(): |
| 547 | """Phase 1 defers the X floor; the simplified-query retry must too. |
| 548 | |
| 549 | A default/deep run with fewer than three X items retries with a simpler |
| 550 | query. If that retry returns a subject-authored post that does not repeat |
| 551 | the subject's name, applying the relevance floor here (with no resolved |
| 552 | handles) discards it before it enters the bundle. The later |
| 553 | resolved-handle floor cannot recover a post that never arrived. |
| 554 | """ |
| 555 | import threading |
| 556 | from unittest.mock import patch |
| 557 | from lib import pipeline |
| 558 | |
| 559 | topic = "Peter Steinberger" |
| 560 | raw_items = [ |
| 561 | { |
| 562 | "id": "1", |
| 563 | "text": "shipping a new agent loop tonight", |
| 564 | "url": "https://x.com/steipete/status/1", |
| 565 | "author_handle": "steipete", |
| 566 | "date": "2026-08-01", |
| 567 | "engagement": {"likes": 3466, "reposts": 128, "replies": 40}, |
| 568 | }, |
| 569 | { |
| 570 | "id": "2", |
| 571 | "text": "Peter Steinberger just shipped another agent demo", |
| 572 | "url": "https://x.com/third_acct/status/2", |
| 573 | "author_handle": "third_acct", |
| 574 | "date": "2026-08-01", |
| 575 | "engagement": {"likes": 50, "reposts": 5, "replies": 2}, |
| 576 | }, |
| 577 | ] |
| 578 | plan = schema.QueryPlan( |
| 579 | intent="person", |
| 580 | freshness_mode="balanced_recent", |
| 581 | cluster_mode="topic", |
| 582 | raw_topic=topic, |
| 583 | subqueries=[ |
| 584 | schema.SubQuery( |
| 585 | label="primary", |
| 586 | search_query=topic, |
| 587 | ranking_query=topic, |
| 588 | sources=["x"], |
| 589 | ) |
| 590 | ], |
| 591 | source_weights={"x": 1.0}, |
| 592 | ) |
| 593 | bundle = schema.RetrievalBundle() |
| 594 | |
| 595 | with patch("lib.pipeline._retrieve_stream", return_value=(raw_items, {})): |
| 596 | pipeline._retry_thin_sources( |
| 597 | topic=topic, |
| 598 | bundle=bundle, |
| 599 | plan=plan, |
| 600 | config={}, |
| 601 | depth="default", |
| 602 | date_range=("2026-07-15", "2026-08-14"), |
| 603 | runtime=schema.ProviderRuntime( |
| 604 | reasoning_provider="mock", |
| 605 | planner_model="mock", |
| 606 | rerank_model="mock", |
| 607 | ), |
| 608 | mock=False, |
| 609 | rate_limited_sources=set(), |
| 610 | rate_limit_lock=threading.Lock(), |
| 611 | settings=pipeline.DEPTH_SETTINGS["default"], |
| 612 | ) |
| 613 | |
| 614 | x_items = bundle.items_by_source.get("x") or [] |
| 615 | authors = {item.author for item in x_items} |
| 616 | assert "steipete" in authors, ( |
| 617 | "thin-retry X path must defer the relevance floor the way Phase 1 does; " |
| 618 | "a zero-relevance subject-authored post must survive into the bundle" |
| 619 | ) |
| 620 | assert "third_acct" in authors, ( |
| 621 | "precondition: the companion on-topic post cleared the floor, so the " |
| 622 | "all-fail rescue cannot hide an incorrectly applied prune" |
| 623 | ) |
| 624 |