| 1 | """Tests for the --diagnose / --preflight browser-auth pending signal. |
| 2 | |
| 3 | `--diagnose` and `--preflight` load config in `plan_only` mode, which skips |
| 4 | browser-cookie extraction (no Keychain popup). Before this fix that made X drop |
| 5 | out of `available_sources` whenever auth came from FROM_BROWSER, even though a |
| 6 | real run authenticates X fine — a false-negative that sent a debugging session |
| 7 | down a 30-minute wrong path. `env.x_pending_browser_auth` reports the |
| 8 | "available pending browser auth" state without reading any cookie, and |
| 9 | `pipeline.diagnose` / `pipeline.available_sources` surface it. |
| 10 | """ |
| 11 | |
| 12 | from unittest import mock |
| 13 | |
| 14 | from lib import env, pipeline |
| 15 | |
| 16 | |
| 17 | def _cfg(**over): |
| 18 | cfg = {"_BROWSER_COOKIE_MODE": "plan_only"} |
| 19 | cfg.update(over) |
| 20 | return cfg |
| 21 | |
| 22 | |
| 23 | class TestXPendingBrowserAuth: |
| 24 | """The no-read predicate in env.py.""" |
| 25 | |
| 26 | def test_pending_true_with_from_browser_and_bird(self): |
| 27 | cfg = _cfg(FROM_BROWSER="chrome") |
| 28 | with mock.patch("lib.env.get_x_source", return_value=None), \ |
| 29 | mock.patch("lib.bird_x.is_bird_installed", return_value=True): |
| 30 | assert env.x_pending_browser_auth(cfg) is True |
| 31 | |
| 32 | def test_false_when_no_browser_resolved(self): |
| 33 | # FROM_BROWSER=off -> cookie_extraction_browsers() returns []. |
| 34 | cfg = _cfg(FROM_BROWSER="off") |
| 35 | with mock.patch("lib.env.get_x_source", return_value=None), \ |
| 36 | mock.patch("lib.bird_x.is_bird_installed", return_value=True): |
| 37 | assert env.x_pending_browser_auth(cfg) is False |
| 38 | |
| 39 | def test_false_when_from_browser_unset(self): |
| 40 | cfg = _cfg() |
| 41 | with mock.patch("lib.env.get_x_source", return_value=None), \ |
| 42 | mock.patch("lib.bird_x.is_bird_installed", return_value=True): |
| 43 | assert env.x_pending_browser_auth(cfg) is False |
| 44 | |
| 45 | def test_false_when_x_available_outright(self): |
| 46 | # Static bird creds / xurl / xquik -> get_x_source truthy -> not pending. |
| 47 | cfg = _cfg(FROM_BROWSER="chrome") |
| 48 | with mock.patch("lib.env.get_x_source", return_value="bird"): |
| 49 | assert env.x_pending_browser_auth(cfg) is False |
| 50 | |
| 51 | def test_false_when_xai_key_present(self): |
| 52 | # Real get_x_source path: an xAI key makes X available outright. |
| 53 | cfg = _cfg(FROM_BROWSER="chrome", XAI_API_KEY="dummy-not-real") |
| 54 | assert env.x_pending_browser_auth(cfg) is False |
| 55 | |
| 56 | def test_false_when_bird_not_installed(self): |
| 57 | cfg = _cfg(FROM_BROWSER="chrome") |
| 58 | with mock.patch("lib.env.get_x_source", return_value=None), \ |
| 59 | mock.patch("lib.bird_x.is_bird_installed", return_value=False): |
| 60 | assert env.x_pending_browser_auth(cfg) is False |
| 61 | |
| 62 | def test_false_in_read_mode(self): |
| 63 | # A real run (mode=read) has already attempted extraction; its status |
| 64 | # must be unchanged, never "pending". |
| 65 | cfg = _cfg(FROM_BROWSER="chrome", _BROWSER_COOKIE_MODE="read") |
| 66 | with mock.patch("lib.env.get_x_source", return_value=None), \ |
| 67 | mock.patch("lib.bird_x.is_bird_installed", return_value=True): |
| 68 | assert env.x_pending_browser_auth(cfg) is False |
| 69 | |
| 70 | def test_reads_no_cookies(self): |
| 71 | # R3: the predicate must never read cookie values or hit Keychain. |
| 72 | cfg = _cfg(FROM_BROWSER="chrome") |
| 73 | with mock.patch("lib.env.get_x_source", return_value=None), \ |
| 74 | mock.patch("lib.bird_x.is_bird_installed", return_value=True), \ |
| 75 | mock.patch("lib.env.extract_browser_credentials", |
| 76 | side_effect=AssertionError("must not read cookies")), \ |
| 77 | mock.patch("lib.cookie_extract.extract_cookies", |
| 78 | side_effect=AssertionError("must not read cookies")): |
| 79 | assert env.x_pending_browser_auth(cfg) is True |
| 80 | |
| 81 | |
| 82 | class TestDiagnoseSurfacesPending: |
| 83 | """available_sources + diagnose consume the predicate.""" |
| 84 | |
| 85 | def test_diagnose_includes_pending_x_and_flag(self): |
| 86 | cfg = _cfg(FROM_BROWSER="chrome") |
| 87 | with mock.patch("lib.env.get_x_source", return_value=None), \ |
| 88 | mock.patch("lib.bird_x.is_bird_installed", return_value=True): |
| 89 | diag = pipeline.diagnose(cfg, safe=True) |
| 90 | assert "x" in diag["available_sources"] |
| 91 | assert diag["x_pending_browser_auth"] is True |
| 92 | # The safe contract is preserved: no cookie values read. |
| 93 | assert diag["browser_cookies"]["reads_values"] is False |
| 94 | |
| 95 | def test_diagnose_excludes_x_when_browser_off(self): |
| 96 | cfg = _cfg(FROM_BROWSER="off") |
| 97 | with mock.patch("lib.env.get_x_source", return_value=None), \ |
| 98 | mock.patch("lib.bird_x.is_bird_installed", return_value=True): |
| 99 | diag = pipeline.diagnose(cfg, safe=True) |
| 100 | assert "x" not in diag["available_sources"] |
| 101 | assert diag["x_pending_browser_auth"] is False |
| 102 | |
| 103 | def test_diagnose_x_outright_not_pending(self): |
| 104 | cfg = _cfg(FROM_BROWSER="chrome") |
| 105 | with mock.patch("lib.env.get_x_source", return_value="bird"): |
| 106 | diag = pipeline.diagnose(cfg, safe=True) |
| 107 | assert "x" in diag["available_sources"] |
| 108 | assert diag["x_pending_browser_auth"] is False |
| 109 | |
| 110 | def test_available_sources_no_double_x(self): |
| 111 | # When X is available outright, the else branch must not also append a 2nd "x". |
| 112 | cfg = _cfg(FROM_BROWSER="chrome") |
| 113 | with mock.patch("lib.env.get_x_source", return_value="bird"): |
| 114 | sources = pipeline.available_sources(cfg) |
| 115 | assert sources.count("x") == 1 |
| 116 | |
| 117 | def test_available_sources_uses_precomputed_x_pending(self): |
| 118 | # diagnose() passes x_pending in to avoid a second predicate evaluation; |
| 119 | # when provided, available_sources must not call the predicate itself. |
| 120 | cfg = _cfg(FROM_BROWSER="chrome") |
| 121 | with mock.patch("lib.env.get_x_source", return_value=None), \ |
| 122 | mock.patch("lib.env.x_pending_browser_auth", |
| 123 | side_effect=AssertionError("must use precomputed x_pending")): |
| 124 | assert "x" in pipeline.available_sources(cfg, x_pending=True) |
| 125 | assert "x" not in pipeline.available_sources(cfg, x_pending=False) |
| 126 | |
| 127 | def test_diagnose_evaluates_predicate_once(self): |
| 128 | # The tidy: x_pending_browser_auth runs exactly once per diagnose() call. |
| 129 | cfg = _cfg(FROM_BROWSER="chrome") |
| 130 | with mock.patch("lib.env.get_x_source", return_value=None), \ |
| 131 | mock.patch("lib.bird_x.is_bird_installed", return_value=True), \ |
| 132 | mock.patch("lib.env.x_pending_browser_auth", wraps=env.x_pending_browser_auth) as spy: |
| 133 | diag = pipeline.diagnose(cfg, safe=True) |
| 134 | assert spy.call_count == 1 |
| 135 | assert "x" in diag["available_sources"] |
| 136 | assert diag["x_pending_browser_auth"] is True |
| 137 |