| 1 | """Unit tests for untested internal functions across rerank, render, planner, and signals. |
| 2 | |
| 3 | These pin the correct behavior of core building blocks that higher-level |
| 4 | tests exercise transitively but don't assert on directly. A regression in |
| 5 | any of these functions would silently degrade output quality. |
| 6 | """ |
| 7 | |
| 8 | import unittest |
| 9 | |
| 10 | from lib import planner, rerank, render, signals, schema |
| 11 | |
| 12 | |
| 13 | def _item(source: str = "reddit", **kwargs) -> schema.SourceItem: |
| 14 | defaults = dict( |
| 15 | item_id="t1", source=source, title="Test Title", body="Test body", |
| 16 | url="https://example.com", engagement={}, metadata={}, |
| 17 | ) |
| 18 | defaults.update(kwargs) |
| 19 | return schema.SourceItem(**defaults) |
| 20 | |
| 21 | |
| 22 | def _candidate(source: str = "reddit", **kwargs) -> schema.Candidate: |
| 23 | defaults = dict( |
| 24 | candidate_id="c1", item_id="t1", source=source, title="Test", |
| 25 | url="https://example.com", snippet="snippet", subquery_labels=["primary"], |
| 26 | native_ranks={"primary": 1}, local_relevance=0.5, freshness=50, |
| 27 | engagement=50, source_quality=0.7, rrf_score=0.01, sources=[source], |
| 28 | source_items=[], |
| 29 | ) |
| 30 | defaults.update(kwargs) |
| 31 | return schema.Candidate(**defaults) |
| 32 | |
| 33 | # --------------------------------------------------------------------------- |
| 34 | # rerank._fallback_tuple |
| 35 | # --------------------------------------------------------------------------- |
| 36 | |
| 37 | |
| 38 | class TestFallbackTuple(unittest.TestCase): |
| 39 | |
| 40 | def test_returns_score_and_explanation(self): |
| 41 | c = _candidate(local_relevance=0.8, freshness=80, source_quality=0.7) |
| 42 | score, explanation = rerank._fallback_tuple(c) |
| 43 | self.assertIsInstance(score, float) |
| 44 | self.assertEqual(explanation, "fallback-local-score") |
| 45 | |
| 46 | def test_score_clamped_to_0_100(self): |
| 47 | c = _candidate(local_relevance=2.0, freshness=200, source_quality=2.0) |
| 48 | score, _ = rerank._fallback_tuple(c) |
| 49 | self.assertLessEqual(score, 100.0) |
| 50 | self.assertGreaterEqual(score, 0.0) |
| 51 | |
| 52 | def test_higher_relevance_gives_higher_score(self): |
| 53 | high = _candidate(local_relevance=0.9, freshness=50, source_quality=0.7) |
| 54 | low = _candidate(local_relevance=0.1, freshness=50, source_quality=0.7) |
| 55 | self.assertGreater(rerank._fallback_tuple(high)[0], rerank._fallback_tuple(low)[0]) |
| 56 | |
| 57 | # --------------------------------------------------------------------------- |
| 58 | # rerank._normalized_rrf |
| 59 | # --------------------------------------------------------------------------- |
| 60 | |
| 61 | |
| 62 | class TestNormalizedRrf(unittest.TestCase): |
| 63 | |
| 64 | def test_zero_input(self): |
| 65 | self.assertAlmostEqual(rerank._normalized_rrf(0.0), 0.0) |
| 66 | |
| 67 | def test_positive_input(self): |
| 68 | result = rerank._normalized_rrf(0.04) |
| 69 | self.assertGreater(result, 0.0) |
| 70 | self.assertLessEqual(result, 100.0) |
| 71 | |
| 72 | def test_clamped_at_100(self): |
| 73 | result = rerank._normalized_rrf(1.0) |
| 74 | self.assertLessEqual(result, 100.0) |
| 75 | |
| 76 | # --------------------------------------------------------------------------- |
| 77 | # render._assess_data_freshness |
| 78 | # --------------------------------------------------------------------------- |
| 79 | |
| 80 | |
| 81 | class TestAssessDataFreshness(unittest.TestCase): |
| 82 | |
| 83 | def _report(self, items_by_source: dict) -> schema.Report: |
| 84 | return schema.Report( |
| 85 | topic="test", range_from="2026-02-15", range_to="2026-03-17", |
| 86 | generated_at="2026-03-17T00:00:00Z", |
| 87 | provider_runtime=schema.ProviderRuntime( |
| 88 | reasoning_provider="test", planner_model="test", rerank_model="test", |
| 89 | ), |
| 90 | query_plan=schema.QueryPlan( |
| 91 | intent="comparison", freshness_mode="balanced_recent", |
| 92 | cluster_mode="debate", raw_topic="test", subqueries=[], |
| 93 | source_weights={}, |
| 94 | ), |
| 95 | clusters=[], ranked_candidates=[], |
| 96 | items_by_source=items_by_source, errors_by_source={}, |
| 97 | ) |
| 98 | |
| 99 | def test_no_items_returns_warning(self): |
| 100 | report = self._report({}) |
| 101 | result = render._assess_data_freshness(report) |
| 102 | self.assertIsNotNone(result) |
| 103 | self.assertIn("Limited", result) |
| 104 | |
| 105 | def test_all_old_items_returns_warning(self): |
| 106 | items = [_item(published_at="2026-01-01") for _ in range(10)] |
| 107 | report = self._report({"reddit": items}) |
| 108 | result = render._assess_data_freshness(report) |
| 109 | self.assertIsNotNone(result) |
| 110 | |
| 111 | def test_many_recent_items_returns_none(self): |
| 112 | from datetime import date |
| 113 | today = date.today().isoformat() |
| 114 | items = [_item(published_at=today) for _ in range(10)] |
| 115 | report = self._report({"reddit": items}) |
| 116 | result = render._assess_data_freshness(report) |
| 117 | self.assertIsNone(result) |
| 118 | |
| 119 | # --------------------------------------------------------------------------- |
| 120 | # render._format_date |
| 121 | # --------------------------------------------------------------------------- |
| 122 | |
| 123 | |
| 124 | class TestFormatDate(unittest.TestCase): |
| 125 | |
| 126 | def test_high_confidence_clean(self): |
| 127 | item = _item(published_at="2026-03-10", date_confidence="high") |
| 128 | self.assertEqual(render._format_date(item), "2026-03-10") |
| 129 | |
| 130 | def test_low_confidence_tagged(self): |
| 131 | item = _item(published_at="2026-03-10", date_confidence="low") |
| 132 | self.assertIn("date:low", render._format_date(item)) |
| 133 | |
| 134 | def test_none_item(self): |
| 135 | self.assertIn("unknown", render._format_date(None).lower()) |
| 136 | |
| 137 | # --------------------------------------------------------------------------- |
| 138 | # render._format_actor |
| 139 | # --------------------------------------------------------------------------- |
| 140 | |
| 141 | |
| 142 | class TestFormatActor(unittest.TestCase): |
| 143 | |
| 144 | def test_reddit_subreddit(self): |
| 145 | item = _item(source="reddit", container="python") |
| 146 | self.assertEqual(render._format_actor(item), "r/python") |
| 147 | |
| 148 | def test_x_handle(self): |
| 149 | item = _item(source="x", author="karpathy") |
| 150 | self.assertEqual(render._format_actor(item), "@karpathy") |
| 151 | |
| 152 | def test_youtube_channel(self): |
| 153 | item = _item(source="youtube", author="Fireship") |
| 154 | self.assertEqual(render._format_actor(item), "Fireship") |
| 155 | |
| 156 | # --------------------------------------------------------------------------- |
| 157 | # render._format_engagement |
| 158 | # --------------------------------------------------------------------------- |
| 159 | |
| 160 | |
| 161 | class TestFormatEngagement(unittest.TestCase): |
| 162 | |
| 163 | def test_reddit_format(self): |
| 164 | item = _item(engagement={"score": 344, "num_comments": 119}) |
| 165 | result = render._format_engagement(item) |
| 166 | self.assertIn("344", result) |
| 167 | self.assertIn("pts", result) |
| 168 | |
| 169 | def test_empty_engagement(self): |
| 170 | item = _item(engagement={}) |
| 171 | self.assertIsNone(render._format_engagement(item)) |
| 172 | |
| 173 | # --------------------------------------------------------------------------- |
| 174 | # render._format_corroboration |
| 175 | # --------------------------------------------------------------------------- |
| 176 | |
| 177 | |
| 178 | class TestFormatCorroboration(unittest.TestCase): |
| 179 | |
| 180 | def test_multi_source(self): |
| 181 | c = _candidate(sources=["reddit", "x", "hackernews"]) |
| 182 | result = render._format_corroboration(c) |
| 183 | self.assertIn("Also on", result) |
| 184 | self.assertIn("X", result) |
| 185 | |
| 186 | def test_single_source_none(self): |
| 187 | c = _candidate(sources=["reddit"]) |
| 188 | self.assertIsNone(render._format_corroboration(c)) |
| 189 | |
| 190 | # --------------------------------------------------------------------------- |
| 191 | # render._format_explanation |
| 192 | # --------------------------------------------------------------------------- |
| 193 | |
| 194 | |
| 195 | class TestFormatExplanation(unittest.TestCase): |
| 196 | |
| 197 | def test_hides_fallback_sentinel(self): |
| 198 | c = _candidate(explanation="fallback-local-score") |
| 199 | self.assertIsNone(render._format_explanation(c)) |
| 200 | |
| 201 | def test_shows_real_explanation(self): |
| 202 | c = _candidate(explanation="Directly compares frameworks") |
| 203 | self.assertEqual(render._format_explanation(c), "Directly compares frameworks") |
| 204 | |
| 205 | # --------------------------------------------------------------------------- |
| 206 | # render._fmt_pairs and _format_number |
| 207 | # --------------------------------------------------------------------------- |
| 208 | |
| 209 | |
| 210 | class TestFmtPairs(unittest.TestCase): |
| 211 | |
| 212 | def test_basic(self): |
| 213 | self.assertEqual(render._fmt_pairs([(120, "pts"), (48, "cmt")]), "120pts, 48cmt") |
| 214 | |
| 215 | def test_skips_none_and_zero(self): |
| 216 | self.assertEqual(render._fmt_pairs([(None, "pts"), (0, "cmt"), (5, "re")]), "5re") |
| 217 | |
| 218 | def test_large_numbers(self): |
| 219 | self.assertIn("94,200", render._fmt_pairs([(94200, "views")])) |
| 220 | |
| 221 | |
| 222 | class TestFormatNumber(unittest.TestCase): |
| 223 | |
| 224 | def test_comma_thousands(self): |
| 225 | self.assertEqual(render._format_number(94200), "94,200") |
| 226 | |
| 227 | def test_small_integer(self): |
| 228 | self.assertEqual(render._format_number(42), "42") |
| 229 | |
| 230 | # --------------------------------------------------------------------------- |
| 231 | # render._truncate |
| 232 | # --------------------------------------------------------------------------- |
| 233 | |
| 234 | |
| 235 | class TestTruncate(unittest.TestCase): |
| 236 | |
| 237 | def test_short_text(self): |
| 238 | self.assertEqual(render._truncate("hello", 100), "hello") |
| 239 | |
| 240 | def test_long_text_has_ellipsis(self): |
| 241 | result = render._truncate("a" * 200, 50) |
| 242 | self.assertTrue(result.endswith("...")) |
| 243 | self.assertEqual(len(result), 50) |
| 244 | |
| 245 | # --------------------------------------------------------------------------- |
| 246 | # planner._normalize_subquery_weights |
| 247 | # --------------------------------------------------------------------------- |
| 248 | |
| 249 | |
| 250 | class TestNormalizeSubqueryWeights(unittest.TestCase): |
| 251 | |
| 252 | def test_sums_to_one(self): |
| 253 | sqs = [ |
| 254 | schema.SubQuery(label="a", search_query="a", ranking_query="a?", sources=["r"], weight=3.0), |
| 255 | schema.SubQuery(label="b", search_query="b", ranking_query="b?", sources=["r"], weight=1.0), |
| 256 | ] |
| 257 | normed = planner._normalize_subquery_weights(sqs) |
| 258 | total = sum(sq.weight for sq in normed) |
| 259 | self.assertAlmostEqual(total, 1.0) |
| 260 | |
| 261 | def test_preserves_ratio(self): |
| 262 | sqs = [ |
| 263 | schema.SubQuery(label="a", search_query="a", ranking_query="a?", sources=["r"], weight=4.0), |
| 264 | schema.SubQuery(label="b", search_query="b", ranking_query="b?", sources=["r"], weight=1.0), |
| 265 | ] |
| 266 | normed = planner._normalize_subquery_weights(sqs) |
| 267 | self.assertAlmostEqual(normed[0].weight / normed[1].weight, 4.0) |
| 268 | |
| 269 | # --------------------------------------------------------------------------- |
| 270 | # planner._normalize_weights |
| 271 | # --------------------------------------------------------------------------- |
| 272 | |
| 273 | |
| 274 | class TestNormalizeWeights(unittest.TestCase): |
| 275 | |
| 276 | def test_sums_to_one(self): |
| 277 | result = planner._normalize_weights({"a": 3.0, "b": 1.0}) |
| 278 | self.assertAlmostEqual(sum(result.values()), 1.0) |
| 279 | |
| 280 | def test_negative_clamped_to_zero(self): |
| 281 | result = planner._normalize_weights({"a": 2.0, "b": -1.0}) |
| 282 | self.assertAlmostEqual(result["b"], 0.0) |
| 283 | |
| 284 | # --------------------------------------------------------------------------- |
| 285 | # planner._trim_subqueries_for_depth |
| 286 | # --------------------------------------------------------------------------- |
| 287 | |
| 288 | |
| 289 | class TestTrimSubqueriesForDepth(unittest.TestCase): |
| 290 | |
| 291 | def _sq(self, label: str = "primary", sources: list[str] = None) -> schema.SubQuery: |
| 292 | return schema.SubQuery( |
| 293 | label=label, search_query="test", ranking_query="test?", |
| 294 | sources=sources or ["reddit", "x", "grounding", "youtube", "hackernews", "polymarket"], |
| 295 | weight=1.0, |
| 296 | ) |
| 297 | |
| 298 | def test_quick_limits_sources(self): |
| 299 | sqs = [self._sq()] |
| 300 | result = planner._trim_subqueries_for_depth(sqs, "comparison", "quick", ["reddit", "x", "grounding"]) |
| 301 | self.assertLessEqual(len(result[0].sources), 2) |
| 302 | |
| 303 | def test_default_comparison_expands_via_capabilities(self): |
| 304 | available = ["reddit", "x", "grounding", "youtube", "hackernews", "tiktok", "instagram"] |
| 305 | sqs = [self._sq(sources=available)] |
| 306 | result = planner._trim_subqueries_for_depth(sqs, "comparison", "default", available) |
| 307 | # Comparison should use all capability-matched sources, not top-3 |
| 308 | self.assertGreater(len(result[0].sources), 3) |
| 309 | |
| 310 | def test_deep_expands_via_capabilities(self): |
| 311 | available = ["reddit", "x", "youtube", "hackernews", "polymarket"] |
| 312 | sqs = [self._sq(sources=available)] |
| 313 | result = planner._trim_subqueries_for_depth(sqs, "comparison", "deep", available) |
| 314 | # Deep comparison should also use capability expansion, not trim |
| 315 | self.assertGreaterEqual(len(result[0].sources), 4) |
| 316 | |
| 317 | # --------------------------------------------------------------------------- |
| 318 | # signals.annotate_stream |
| 319 | # --------------------------------------------------------------------------- |
| 320 | |
| 321 | |
| 322 | class TestAnnotateStream(unittest.TestCase): |
| 323 | |
| 324 | def test_attaches_metadata(self): |
| 325 | items = [ |
| 326 | _item(engagement={"score": 100, "num_comments": 50, "upvote_ratio": 0.9}), |
| 327 | ] |
| 328 | annotated = signals.annotate_stream(items, "test query", "balanced_recent") |
| 329 | item = annotated[0] |
| 330 | self.assertIsNotNone(item.local_relevance) |
| 331 | self.assertIsNotNone(item.freshness) |
| 332 | self.assertIsNotNone(item.engagement_score) |
| 333 | self.assertIsNotNone(item.source_quality) |
| 334 | self.assertIsNotNone(item.local_rank_score) |
| 335 | |
| 336 | def test_sorted_by_local_rank_score(self): |
| 337 | items = [ |
| 338 | _item(item_id="low", title="irrelevant stuff", engagement={}), |
| 339 | _item(item_id="high", title="test query exact match test query", engagement={"score": 500, "num_comments": 200}), |
| 340 | ] |
| 341 | annotated = signals.annotate_stream(items, "test query", "balanced_recent") |
| 342 | self.assertEqual(annotated[0].item_id, "high") |
| 343 | |
| 344 | # --------------------------------------------------------------------------- |
| 345 | # signals.prune_low_relevance |
| 346 | # --------------------------------------------------------------------------- |
| 347 | |
| 348 | |
| 349 | class TestPruneLowRelevance(unittest.TestCase): |
| 350 | |
| 351 | def test_removes_low_relevance_items(self): |
| 352 | items = [ |
| 353 | _item(item_id="good"), |
| 354 | _item(item_id="bad"), |
| 355 | ] |
| 356 | items[0].local_relevance = 0.8 |
| 357 | items[1].local_relevance = 0.01 |
| 358 | result = signals.prune_low_relevance(items, minimum=0.1) |
| 359 | self.assertEqual(len(result), 1) |
| 360 | self.assertEqual(result[0].item_id, "good") |
| 361 | |
| 362 | def test_keeps_all_if_all_below_minimum(self): |
| 363 | items = [_item(item_id="only")] |
| 364 | items[0].local_relevance = 0.05 |
| 365 | result = signals.prune_low_relevance(items, minimum=0.1) |
| 366 | self.assertEqual(len(result), 1) # fallback keeps all |
| 367 | |
| 368 | # --------------------------------------------------------------------------- |
| 369 | # Bug fixes found by PR review agents |
| 370 | # --------------------------------------------------------------------------- |
| 371 | |
| 372 | |
| 373 | class TestDaysAgoZeroFalsy(unittest.TestCase): |
| 374 | """render._assess_data_freshness must not treat days_ago=0 as falsy.""" |
| 375 | |
| 376 | def _report_with_items(self, dates_list: list[str]) -> schema.Report: |
| 377 | items = [_item(published_at=d) for d in dates_list] |
| 378 | return schema.Report( |
| 379 | topic="test", range_from="2026-02-15", range_to="2026-03-17", |
| 380 | generated_at="2026-03-17T00:00:00Z", |
| 381 | provider_runtime=schema.ProviderRuntime( |
| 382 | reasoning_provider="test", planner_model="test", rerank_model="test", |
| 383 | ), |
| 384 | query_plan=schema.QueryPlan( |
| 385 | intent="comparison", freshness_mode="balanced_recent", |
| 386 | cluster_mode="debate", raw_topic="test", subqueries=[], |
| 387 | source_weights={}, |
| 388 | ), |
| 389 | clusters=[], ranked_candidates=[], |
| 390 | items_by_source={"reddit": items}, errors_by_source={}, |
| 391 | ) |
| 392 | |
| 393 | def test_items_from_today_count_as_recent(self): |
| 394 | from datetime import date |
| 395 | today = date.today().isoformat() |
| 396 | report = self._report_with_items([today] * 5) |
| 397 | warning = render._assess_data_freshness(report) |
| 398 | self.assertIsNone(warning, f"Items from today should be recent, got warning: {warning}") |
| 399 | |
| 400 | |
| 401 | class TestRerankBoundary(unittest.TestCase): |
| 402 | """Rerank demotion must have a clean boundary at exactly 20.0.""" |
| 403 | |
| 404 | def test_score_at_exactly_20_is_not_demoted(self): |
| 405 | c = _candidate() |
| 406 | c.rerank_score = 20.0 |
| 407 | score_at_20 = rerank._final_score(c) |
| 408 | c.rerank_score = 50.0 |
| 409 | score_at_50 = rerank._final_score(c) |
| 410 | self.assertGreater(score_at_20 / score_at_50, 0.3, |
| 411 | "Score at 20.0 should not be demoted") |
| 412 | |
| 413 | def test_score_at_19_99_is_demoted(self): |
| 414 | c = _candidate() |
| 415 | c.rerank_score = 19.99 |
| 416 | score_demoted = rerank._final_score(c) |
| 417 | c.rerank_score = 20.0 |
| 418 | score_not_demoted = rerank._final_score(c) |
| 419 | self.assertLess(score_demoted, score_not_demoted * 0.5, |
| 420 | "Score at 19.99 should be heavily demoted vs 20.0") |
| 421 | |
| 422 | |
| 423 | class TestSlashFalsePositives(unittest.TestCase): |
| 424 | """Slash regex must not misclassify compound terms as comparisons.""" |
| 425 | |
| 426 | def test_ci_cd_is_not_comparison(self): |
| 427 | self.assertNotEqual(planner._infer_intent("CI/CD pipeline setup"), "comparison") |
| 428 | |
| 429 | def test_tcp_ip_is_not_comparison(self): |
| 430 | self.assertNotEqual(planner._infer_intent("TCP/IP networking guide"), "comparison") |
| 431 | |
| 432 | def test_io_is_not_comparison(self): |
| 433 | self.assertNotEqual(planner._infer_intent("I/O performance tuning"), "comparison") |
| 434 | |
| 435 | def test_os_kernel_is_not_comparison(self): |
| 436 | self.assertNotEqual(planner._infer_intent("input/output buffering"), "comparison") |
| 437 | |
| 438 | def test_proper_noun_slash_still_works(self): |
| 439 | self.assertEqual(planner._infer_intent("React/Vue/Svelte"), "comparison") |
| 440 | |
| 441 | |
| 442 | class TestGenericEngagementFormatter(unittest.TestCase): |
| 443 | """Generic formatter must not garble output for unknown sources.""" |
| 444 | |
| 445 | def test_xiaohongshu_engagement_not_garbled(self): |
| 446 | item = _item(source="xiaohongshu", engagement={"likes": 500, "views": 10000}) |
| 447 | result = render._format_engagement(item) |
| 448 | if result is not None: |
| 449 | self.assertNotIn("likes500", result, "Key used as value prefix") |
| 450 | self.assertNotIn("views10000", result, "Key used as value prefix") |
| 451 | # Should contain numeric values, not dict keys as numbers |
| 452 | self.assertIn("500", result) |
| 453 | |
| 454 | if __name__ == "__main__": |
| 455 | unittest.main() |
| 456 | |
| 457 | |
| 458 | class TestDefaultDepthDoesNotCapSources(unittest.TestCase): |
| 459 | """Default depth must not aggressively limit sources for any intent. |
| 460 | |
| 461 | E2E testing showed factual/opinion/prediction/concept queries getting |
| 462 | 0-1 sources because SOURCE_LIMITS["default"] capped them at 2-3, |
| 463 | and those 2-3 sources returned empty. v2.9.5 searched all available |
| 464 | sources and let scoring handle quality. |
| 465 | """ |
| 466 | |
| 467 | ALL_SOURCES = ["reddit", "x", "grounding", "youtube", "hackernews", |
| 468 | "tiktok", "instagram", "polymarket"] |
| 469 | |
| 470 | def _plan_sources(self, topic: str) -> list[str]: |
| 471 | plan = planner.plan_query( |
| 472 | topic=topic, |
| 473 | available_sources=self.ALL_SOURCES, |
| 474 | requested_sources=None, |
| 475 | depth="default", |
| 476 | provider=None, |
| 477 | model=None, |
| 478 | ) |
| 479 | return plan.subqueries[0].sources |
| 480 | |
| 481 | def test_factual_gets_more_than_2_sources(self): |
| 482 | sources = self._plan_sources("what is quantum computing") |
| 483 | self.assertGreater(len(sources), 2, |
| 484 | f"Factual query capped at {len(sources)} sources: {sources}") |
| 485 | |
| 486 | def test_opinion_gets_more_than_3_sources(self): |
| 487 | sources = self._plan_sources("thoughts on Rust") |
| 488 | self.assertGreater(len(sources), 3, |
| 489 | f"Opinion query capped at {len(sources)} sources: {sources}") |
| 490 | |
| 491 | def test_prediction_gets_more_than_3_sources(self): |
| 492 | sources = self._plan_sources("odds of recession") |
| 493 | self.assertGreater(len(sources), 3, |
| 494 | f"Prediction query capped at {len(sources)} sources: {sources}") |
| 495 | |
| 496 | def test_breaking_news_gets_more_than_4_sources(self): |
| 497 | sources = self._plan_sources("kanye west") |
| 498 | self.assertGreater(len(sources), 4, |
| 499 | f"Breaking news capped at {len(sources)} sources: {sources}") |
| 500 | |
| 501 | def test_concept_gets_more_than_3_sources(self): |
| 502 | sources = self._plan_sources("explain transformer architecture") |
| 503 | self.assertGreater(len(sources), 3, |
| 504 | f"Concept query capped at {len(sources)} sources: {sources}") |
| 505 | |
| 506 | def test_quick_mode_still_limited(self): |
| 507 | """Quick mode should remain tight for latency.""" |
| 508 | plan = planner.plan_query( |
| 509 | topic="what is quantum computing", |
| 510 | available_sources=self.ALL_SOURCES, |
| 511 | requested_sources=None, |
| 512 | depth="quick", |
| 513 | provider=None, |
| 514 | model=None, |
| 515 | ) |
| 516 | self.assertLessEqual(len(plan.subqueries[0].sources), 3) |
| 517 | |
| 518 | |
| 519 | class TestRerankWeightBalance(unittest.TestCase): |
| 520 | """Reranker weight must dominate over RRF when candidates have divergent quality.""" |
| 521 | |
| 522 | def test_rerank_gap_dominates_with_identical_rrf(self): |
| 523 | """Two candidates with identical RRF but rerank_scores of 80 and 40 should have a meaningful final_score gap (rerank still dominates).""" |
| 524 | high = _candidate(rrf_score=0.03, freshness=50, source_quality=0.7) |
| 525 | high.rerank_score = 80.0 |
| 526 | high.final_score = rerank._final_score(high) |
| 527 | |
| 528 | low = _candidate(rrf_score=0.03, freshness=50, source_quality=0.7) |
| 529 | low.rerank_score = 40.0 |
| 530 | low.final_score = rerank._final_score(low) |
| 531 | |
| 532 | gap = high.final_score - low.final_score |
| 533 | # Rerank weight is 0.60, so gap = 0.60 * 40 = 24 points. |
| 534 | # Engagement boost may add a small delta but rerank remains dominant. |
| 535 | self.assertGreaterEqual(gap, 23.0, |
| 536 | f"Rerank gap should be >= 23 points, got {gap:.1f}") |
| 537 | |
| 538 | |
| 539 | class TestXaiModelDefault(unittest.TestCase): |
| 540 | """XAI_DEFAULT must be a model that xAI's API actually accepts.""" |
| 541 | |
| 542 | def test_default_is_not_grok_3(self): |
| 543 | from lib import providers |
| 544 | self.assertNotEqual(providers.XAI_DEFAULT, "grok-3-fast", |
| 545 | "grok-3-fast returns HTTP 400 from xAI API") |
| 546 | self.assertNotEqual(providers.XAI_DEFAULT, "grok-3-mini-fast", |
| 547 | "grok-3-mini-fast returns HTTP 400 from xAI API") |
| 548 | |
| 549 | def test_default_is_grok_4_generation(self): |
| 550 | from lib import providers |
| 551 | self.assertIn("grok-4", providers.XAI_DEFAULT, |
| 552 | f"XAI_DEFAULT should be a grok-4 model, got: {providers.XAI_DEFAULT}") |
| 553 | |
| 554 |