| 1 | import io |
| 2 | import unittest |
| 3 | from contextlib import redirect_stderr |
| 4 | from unittest import mock |
| 5 | |
| 6 | from lib import ui |
| 7 | |
| 8 | |
| 9 | class PromoMessageTests(unittest.TestCase): |
| 10 | def test_x_promo_mentions_browser_support_and_fallbacks(self): |
| 11 | msg = ui.PROMO_SINGLE_KEY["x"] |
| 12 | self.assertIn("Firefox", msg, "Firefox should be listed as supported") |
| 13 | self.assertIn("Windows", msg, "Windows limitation should be mentioned") |
| 14 | self.assertIn("FROM_BROWSER=auto", msg, "Chrome opt-in should be mentioned") |
| 15 | self.assertIn("AUTH_TOKEN", msg, "AUTH_TOKEN/CT0 fallback should be listed") |
| 16 | self.assertIn("XAI_API_KEY", msg, "XAI_API_KEY fallback should be listed") |
| 17 | |
| 18 | def test_x_promo_does_not_say_firefox_or_safari_without_qualification(self): |
| 19 | msg = ui.PROMO_SINGLE_KEY["x"] |
| 20 | self.assertNotIn( |
| 21 | "Firefox or Safari", |
| 22 | msg, |
| 23 | "Promo should not say 'Firefox or Safari' without qualification", |
| 24 | ) |
| 25 | self.assertNotIn( |
| 26 | "Chrome/Safari", |
| 27 | msg, |
| 28 | "Promo should not list Chrome alongside Safari as if both are default", |
| 29 | ) |
| 30 | |
| 31 | |
| 32 | class UiV3Tests(unittest.TestCase): |
| 33 | def test_show_diagnostic_banner_uses_v3_source_model(self): |
| 34 | diag = { |
| 35 | "available_sources": ["grounding", "youtube"], |
| 36 | "providers": {"google": True, "openai": False, "xai": False}, |
| 37 | "x_backend": None, |
| 38 | "bird_installed": True, |
| 39 | "bird_authenticated": False, |
| 40 | "bird_username": None, |
| 41 | "native_web_backend": "brave", |
| 42 | } |
| 43 | with mock.patch.object(ui, "IS_TTY", False): |
| 44 | stderr = io.StringIO() |
| 45 | with redirect_stderr(stderr): |
| 46 | ui.show_diagnostic_banner(diag) |
| 47 | output = stderr.getvalue() |
| 48 | self.assertIn("Reddit", output) |
| 49 | self.assertIn("unavailable", output) |
| 50 | self.assertIn("Add AUTH_TOKEN/CT0 or XAI_API_KEY", output) |
| 51 | self.assertIn("brave API available", output) |
| 52 | |
| 53 | def test_build_nux_message_mentions_v3_unlock_paths(self): |
| 54 | text = ui._build_nux_message( |
| 55 | {"available_sources": ["reddit", "youtube", "grounding"]} |
| 56 | ) |
| 57 | self.assertIn("Reddit ✓, X ✗, YouTube ✓, Web ✓", text) |
| 58 | self.assertIn("works fine as-is", text) |
| 59 | self.assertIn("all free", text) |
| 60 | |
| 61 | def test_show_complete_uses_actual_sources_for_source_restricted_runs(self): |
| 62 | with mock.patch.object(ui, "IS_TTY", False): |
| 63 | stderr = io.StringIO() |
| 64 | with redirect_stderr(stderr): |
| 65 | progress = ui.ProgressDisplay("test topic", show_banner=False) |
| 66 | progress.show_complete( |
| 67 | source_counts={"grounding": 2}, |
| 68 | display_sources=["grounding"], |
| 69 | ) |
| 70 | output = stderr.getvalue() |
| 71 | self.assertIn("Web: 2 results", output) |
| 72 | self.assertNotIn("Reddit:", output) |
| 73 | self.assertNotIn("X:", output) |
| 74 | |
| 75 | def test_show_complete_supports_newer_sources(self): |
| 76 | with mock.patch.object(ui, "IS_TTY", False): |
| 77 | stderr = io.StringIO() |
| 78 | with redirect_stderr(stderr): |
| 79 | progress = ui.ProgressDisplay("test topic", show_banner=False) |
| 80 | progress.show_complete( |
| 81 | source_counts={ |
| 82 | "bluesky": 3, |
| 83 | "truthsocial": 1, |
| 84 | "xiaohongshu": 4, |
| 85 | }, |
| 86 | display_sources=["bluesky", "truthsocial", "xiaohongshu"], |
| 87 | ) |
| 88 | output = stderr.getvalue() |
| 89 | self.assertIn("Bluesky: 3 posts", output) |
| 90 | self.assertIn("Truth Social: 1 post", output) |
| 91 | self.assertIn("Xiaohongshu: 4 posts", output) |
| 92 | |
| 93 | if __name__ == "__main__": |
| 94 | unittest.main() |
| 95 |