| 1 | """grok must be visible as an opt-in backup, not as a default. |
| 2 | |
| 3 | Grok is demoted to opt-in only: a leftover ~/.grok/auth.json must never steal |
| 4 | the X lane. The default auto chain is bird → xai → xurl → xquik. Pin |
| 5 | LAST30DAYS_X_BACKEND=grok to enable grok explicitly. |
| 6 | """ |
| 7 | |
| 8 | import inspect |
| 9 | from pathlib import Path |
| 10 | from unittest import mock |
| 11 | |
| 12 | from lib import backends, doctor, health, quality_nudge |
| 13 | |
| 14 | REPO = Path(__file__).resolve().parent.parent |
| 15 | |
| 16 | |
| 17 | def test_doctor_does_not_auto_select_grok_unpinned(): |
| 18 | """Doctor must NOT report 'will use: grok' for unpinned runs. |
| 19 | Grok is opt-in only; a leftover auth.json must never steal the X lane.""" |
| 20 | src = inspect.getsource(doctor._x_record) |
| 21 | # The old code would check grok_x.has_stored_auth() and set status="ok" |
| 22 | # with record["will_use"]="grok" when grok was available unpinned. That |
| 23 | # promotion block is removed: no "has_stored_auth()" call that sets |
| 24 | # will_use to grok for unpinned runs. |
| 25 | # |
| 26 | # Comments may mention "will use: grok" to explain what we DON'T do, so |
| 27 | # check for the old logic pattern: has_stored_auth -> grok promotion. |
| 28 | assert "has_stored_auth" not in src |
| 29 | |
| 30 | |
| 31 | def test_doctor_mentions_grok_as_opt_in(): |
| 32 | """Doctor comments explain that grok is opt-in only.""" |
| 33 | src = inspect.getsource(doctor._x_record) |
| 34 | assert "opt-in" in src.lower() |
| 35 | |
| 36 | |
| 37 | def test_quality_nudge_does_not_turn_optional_x_into_a_grok_prompt(): |
| 38 | src = inspect.getsource(quality_nudge) |
| 39 | assert "grok_cli_missing" not in src |
| 40 | # Unconfigured/declined X is an optional omission, never a setup nudge... |
| 41 | assert 'optional_omitted.append("x")' in src |
| 42 | assert '"cookies_missing"' not in src |
| 43 | # ...but a configured X that errored still surfaces its repair. |
| 44 | assert '"cookies_expired"' in src |
| 45 | |
| 46 | |
| 47 | def test_configuration_documents_the_grok_path_as_opt_in(): |
| 48 | text = (REPO / "CONFIGURATION.md").read_text() |
| 49 | assert "Grok CLI (opt-in backup)" in text |
| 50 | assert "grok login" in text |
| 51 | # Document that grok requires a pin. |
| 52 | assert "LAST30DAYS_X_BACKEND=grok" in text |
| 53 | |
| 54 | |
| 55 | def test_configuration_pin_row_lists_opt_ins_last(): |
| 56 | """Pin row shows all backends with the opt-ins (grok, xapi) last.""" |
| 57 | text = (REPO / "CONFIGURATION.md").read_text() |
| 58 | # Auto-chain order first, then the opt-in backends. |
| 59 | assert "`bird` / `xai` / `xurl` / `xquik` / `grok` / `xapi`" in text |
| 60 | |
| 61 | |
| 62 | def test_configuration_does_not_claim_grok_is_free(): |
| 63 | text = (REPO / "CONFIGURATION.md").read_text() |
| 64 | section = text[text.index("Grok CLI (opt-in backup)"):][:1200] |
| 65 | assert "draws on your Grok plan" in section or "draw on your Grok plan" in section |
| 66 | |
| 67 | |
| 68 | def test_configuration_documents_bird_first_chain(): |
| 69 | """Auto chain is bird first: cookies beat XAI_API_KEY.""" |
| 70 | text = (REPO / "CONFIGURATION.md").read_text() |
| 71 | assert "bird first" in text.lower() or "bird (browser cookies) → xai" in text.lower() |
| 72 | |
| 73 | |
| 74 | def test_configuration_chain_paragraph_keeps_bird_first_and_adds_grok_bot_exception(): |
| 75 | """Ordinary hosts keep bird first; the Grok Bot exception is the official chain.""" |
| 76 | text = (REPO / "CONFIGURATION.md").read_text() |
| 77 | start = text.index("**X backend priority (bird first).**") |
| 78 | paragraph = text[start:text.index("\n", start)] |
| 79 | assert "bird (browser cookies) → xai (API key) → xurl (OAuth2 CLI) → xquik (API key)" in paragraph |
| 80 | assert "Grok Bot" in paragraph |
| 81 | assert "xapi" in paragraph and "xai" in paragraph and "xurl" in paragraph |
| 82 | assert "X connector" in paragraph |
| 83 | # Grok CLI stays an opt-in backup on every host. |
| 84 | assert "leftover grok login never steals the X lane" in paragraph |
| 85 | |
| 86 | |
| 87 | def test_changelog_fragments_exist_and_changelog_is_untouched(): |
| 88 | frags = list((REPO / "changelog.d").glob("*grok*")) + \ |
| 89 | list((REPO / "changelog.d").glob("*bird*")) |
| 90 | if not frags: |
| 91 | # Release PRs consume fragments into CHANGELOG.md via towncrier. |
| 92 | changelog = (REPO / "CHANGELOG.md").read_text() |
| 93 | # Old text or new text about X backend changes. |
| 94 | assert "X search now works with no X credential" in changelog or "bird first" in changelog.lower() |
| 95 | return |
| 96 | assert frags, "feature PRs add a changelog.d fragment" |
| 97 | |
| 98 | |
| 99 | def test_official_x_api_fragments_exist_and_changelog_is_untouched(): |
| 100 | """U8: two orphan fragments carry the change; CHANGELOG.md is release-owned.""" |
| 101 | changed = REPO / "changelog.d" / "+grok-bot-official-x-api.changed.md" |
| 102 | added = REPO / "changelog.d" / "+x-api-backend-and-host-lane.added.md" |
| 103 | changelog = (REPO / "CHANGELOG.md").read_text() |
| 104 | if not changed.exists() and not added.exists(): |
| 105 | # A release consumed the fragments into CHANGELOG.md via towncrier. |
| 106 | assert "X connector" in changelog and "xapi" in changelog |
| 107 | return |
| 108 | assert changed.exists() and added.exists(), "both U8 fragments must ship together" |
| 109 | changed_text = changed.read_text().strip() |
| 110 | added_text = added.read_text().strip() |
| 111 | assert changed_text and added_text |
| 112 | # Fragment bodies are not pasted into CHANGELOG.md by hand. |
| 113 | assert changed_text.splitlines()[0] not in changelog |
| 114 | assert added_text.splitlines()[0] not in changelog |
| 115 | for text in (changed_text, added_text): |
| 116 | lowered = text.lower() |
| 117 | # R20/R21: the fragments never name the legacy backends, cookies, |
| 118 | # a pin as an override path, or why the feature exists. |
| 119 | for banned in ("bird", "xquik", "grok cli", "cookie", "override", "xai's request", "marketplace"): |
| 120 | assert banned not in lowered, f"fragment names {banned!r}" |
| 121 | |
| 122 | |
| 123 | # --- SKILL.md unlock surfaces --------------------------------------------- |
| 124 | |
| 125 | def _skill_md(): |
| 126 | return (REPO / "skills" / "last30days" / "SKILL.md").read_text() |
| 127 | |
| 128 | |
| 129 | def test_skill_md_does_not_check_grok_first(): |
| 130 | """Grok is opt-in only: SKILL.md should NOT check for grok before cookies.""" |
| 131 | text = _skill_md() |
| 132 | # The old "Check for a Grok path before asking for cookies" should be removed. |
| 133 | assert "Check for a Grok path before asking for cookies" not in text |
| 134 | |
| 135 | |
| 136 | def test_skill_md_presents_grok_as_opt_in_backup(): |
| 137 | """SKILL.md presents grok as an opt-in backup, not a primary option.""" |
| 138 | text = _skill_md() |
| 139 | assert "Grok CLI is an opt-in backup" in text |
| 140 | # Should mention the pin requirement. |
| 141 | assert "LAST30DAYS_X_BACKEND=grok" in text |
| 142 | |
| 143 | |
| 144 | def test_skill_md_replaces_just_in_time_unlock_with_optional_omission(): |
| 145 | """A useful report ends without a second X consent or key prompt.""" |
| 146 | text = _skill_md() |
| 147 | assert "Just-in-time X unlock" not in text |
| 148 | section = text[text.index("Optional X omission"):][:1200] |
| 149 | assert "finish the useful findings first" in section |
| 150 | assert "Do not open a modal" in section |
| 151 | |
| 152 | |
| 153 | def test_skill_md_keeps_grok_paid_caveat_in_explicit_setup_path(): |
| 154 | text = _skill_md() |
| 155 | section = text[text.index("Grok CLI is an opt-in backup"):][:1200] |
| 156 | assert "Do not call it free" in section |
| 157 | |
| 158 | |
| 159 | # --- Doctor grok-only unpinned behavior (R3/R8) ----------------------------- |
| 160 | |
| 161 | |
| 162 | def test_doctor_grok_only_unpinned_is_not_tier_error(): |
| 163 | """Unpinned grok-only is unconfigured (tier off), NOT broken (tier error). |
| 164 | |
| 165 | R3: unpinned grok is 'available, unused — pin LAST30DAYS_X_BACKEND=grok' |
| 166 | R8: grok-only unpinned = X unconfigured / skipped, not auth-failed or broken |
| 167 | """ |
| 168 | from lib import grok_x |
| 169 | |
| 170 | config = {} # No pin, no auto-chain credentials |
| 171 | bird_status = { |
| 172 | "installed": False, |
| 173 | "authenticated": False, |
| 174 | "username": "", |
| 175 | "can_install": False, |
| 176 | } |
| 177 | # Grok is the only backend with OK status; all auto-chain backends are MISSING. |
| 178 | with ( |
| 179 | mock.patch.object(grok_x, "binary_path", return_value="/usr/bin/grok"), |
| 180 | mock.patch.object(grok_x, "has_stored_auth", return_value=True), |
| 181 | mock.patch.object(grok_x, "stored_auth_status", return_value=(grok_x.AUTH_OK, "", None)), |
| 182 | mock.patch("lib.backends.which", return_value="/usr/bin/grok"), |
| 183 | mock.patch("lib.bird_x.get_bird_status", return_value=bird_status), |
| 184 | mock.patch("lib.bird_x.is_bird_installed", return_value=False), |
| 185 | mock.patch("lib.xurl_x.has_stored_auth", return_value=False), |
| 186 | ): |
| 187 | record = doctor._x_record(config) |
| 188 | # Must NOT be tier error / NOT WORKING. |
| 189 | assert record["tier"] != "error", "grok-only unpinned must not be tier error" |
| 190 | # Should be unconfigured (tier off). |
| 191 | assert record["status"] == "unconfigured" |
| 192 | assert record["tier"] == "off" |
| 193 | # Note should mention grok is available but requires a pin. |
| 194 | assert "grok" in record["note"].lower() |
| 195 | assert "pin" in record["note"].lower() or "LAST30DAYS_X_BACKEND" in record["note"] |
| 196 | |
| 197 | |
| 198 | def test_doctor_grok_error_unpinned_is_not_tier_error(): |
| 199 | """Unpinned grok ERROR (broken store) is unconfigured, NOT tier error. |
| 200 | |
| 201 | When grok's auth store is unreadable/corrupt, doctor should NOT report X |
| 202 | as NOT WORKING with a `grok login` prescription. An unused opt-in backend |
| 203 | with a broken store is still unused — tier off, not tier error. |
| 204 | """ |
| 205 | from lib import grok_x |
| 206 | |
| 207 | config = {} # No pin, no auto-chain credentials |
| 208 | bird_status = { |
| 209 | "installed": False, |
| 210 | "authenticated": False, |
| 211 | "username": "", |
| 212 | "can_install": False, |
| 213 | } |
| 214 | # Grok has ERROR status (unreadable store); all auto-chain backends MISSING. |
| 215 | with ( |
| 216 | mock.patch.object(grok_x, "binary_path", return_value="/usr/bin/grok"), |
| 217 | mock.patch.object(grok_x, "has_stored_auth", return_value=False), |
| 218 | mock.patch.object( |
| 219 | grok_x, "stored_auth_status", |
| 220 | return_value=(grok_x.AUTH_ERROR, "store unreadable", None), |
| 221 | ), |
| 222 | mock.patch("lib.backends.which", return_value="/usr/bin/grok"), |
| 223 | mock.patch("lib.bird_x.get_bird_status", return_value=bird_status), |
| 224 | mock.patch("lib.bird_x.is_bird_installed", return_value=False), |
| 225 | mock.patch("lib.xurl_x.has_stored_auth", return_value=False), |
| 226 | ): |
| 227 | record = doctor._x_record(config) |
| 228 | # Must NOT be tier error / NOT WORKING. |
| 229 | assert record["tier"] != "error", "grok ERROR unpinned must not be tier error" |
| 230 | # Should be unconfigured (tier off). |
| 231 | assert record["status"] == "unconfigured" |
| 232 | assert record["tier"] == "off" |
| 233 | # Note should mention grok is unused/opt-in; should NOT prescribe grok login. |
| 234 | assert "grok" in record["note"].lower() |
| 235 | assert "opt-in" in record["note"].lower() or "unused" in record["note"].lower() |
| 236 | # Fix should be empty (no grok login prescription for unused opt-in). |
| 237 | assert record["fix"] == "" |
| 238 | |
| 239 | |
| 240 | def test_doctor_grok_store_with_pending_bird_predicts_bird(): |
| 241 | """Unpinned + grok store + pending browser auth -> doctor predicts bird. |
| 242 | |
| 243 | When bird is installed and browser-cookie extraction is configured, |
| 244 | doctor should predict bird (pending-cookie usable), NOT report X as |
| 245 | unconfigured due to an unused grok store. Pending bird takes precedence. |
| 246 | """ |
| 247 | from lib import env, grok_x |
| 248 | |
| 249 | config = {} # No pin, no static AUTH_TOKEN/CT0 |
| 250 | bird_status = { |
| 251 | "installed": True, # Bird IS installed |
| 252 | "authenticated": False, # No static cookies in config |
| 253 | "username": "", |
| 254 | "can_install": True, |
| 255 | } |
| 256 | # Grok has OK status; bird is pending (installed, FROM_BROWSER configured). |
| 257 | with ( |
| 258 | mock.patch.object(grok_x, "binary_path", return_value="/usr/bin/grok"), |
| 259 | mock.patch.object(grok_x, "has_stored_auth", return_value=True), |
| 260 | mock.patch.object(grok_x, "stored_auth_status", return_value=(grok_x.AUTH_OK, "", None)), |
| 261 | mock.patch("lib.backends.which", return_value="/usr/bin/grok"), |
| 262 | mock.patch("lib.bird_x.get_bird_status", return_value=bird_status), |
| 263 | mock.patch("lib.bird_x.is_bird_installed", return_value=True), |
| 264 | mock.patch("lib.xurl_x.has_stored_auth", return_value=False), |
| 265 | # Simulate x_pending_browser_auth returning True (bird pending) |
| 266 | mock.patch.object(env, "x_pending_browser_auth", return_value=True), |
| 267 | ): |
| 268 | record = doctor._x_record(config) |
| 269 | # Doctor should predict bird (pending), NOT report X as unconfigured. |
| 270 | assert record["tier"] != "off", "pending bird should not be tier off" |
| 271 | assert record["status"] != "unconfigured", "pending bird should not be unconfigured" |
| 272 | # Should be OK tier with bird prediction. |
| 273 | assert record["tier"] == "ok" |
| 274 | assert record["status"] == health.OK |
| 275 | assert "bird" in record["note"].lower() |
| 276 | assert "browser cookies" in record["note"].lower() or "cookie" in record["note"].lower() |
| 277 | |
| 278 | |
| 279 | def test_doctor_grok_does_not_hide_xurl_error(): |
| 280 | """Unpinned + grok store + xurl ERROR -> doctor keeps xurl error, not unconfigured. |
| 281 | |
| 282 | When an auto-chain backend (xurl) is configured but broken, doctor must |
| 283 | report that error with its repair guidance. Unused grok must NOT swallow |
| 284 | the genuine auto-chain failure. |
| 285 | """ |
| 286 | from lib import backends as _backends |
| 287 | from lib import grok_x |
| 288 | |
| 289 | config = {} # No pin |
| 290 | bird_status = { |
| 291 | "installed": False, |
| 292 | "authenticated": False, |
| 293 | "username": "", |
| 294 | "can_install": False, |
| 295 | } |
| 296 | |
| 297 | # Mock xurl probe to return ERROR status |
| 298 | def mock_probe_xurl(config): |
| 299 | return _backends.BackendFinding( |
| 300 | name="xurl", |
| 301 | status=health.ERROR, |
| 302 | detail="store unreadable", |
| 303 | prescription="xurl auth oauth2 login", |
| 304 | requires="xurl CLI installed + OAuth2 login", |
| 305 | ) |
| 306 | |
| 307 | # Intercept _run_probe to inject xurl ERROR |
| 308 | original_run_probe = _backends._run_probe |
| 309 | |
| 310 | def patched_run_probe(spec, config): |
| 311 | if spec.name == "xurl": |
| 312 | return mock_probe_xurl(config) |
| 313 | return original_run_probe(spec, config) |
| 314 | |
| 315 | # Grok has OK status; xurl has ERROR (unreadable store). |
| 316 | # All other auto-chain backends are MISSING. |
| 317 | with ( |
| 318 | mock.patch.object(grok_x, "binary_path", return_value="/usr/bin/grok"), |
| 319 | mock.patch.object(grok_x, "has_stored_auth", return_value=True), |
| 320 | mock.patch.object(grok_x, "stored_auth_status", return_value=(grok_x.AUTH_OK, "", None)), |
| 321 | mock.patch("lib.backends.which", side_effect=lambda cmd: "/usr/bin/grok" if cmd == "grok" else None), |
| 322 | mock.patch("lib.bird_x.get_bird_status", return_value=bird_status), |
| 323 | mock.patch("lib.bird_x.is_bird_installed", return_value=False), |
| 324 | mock.patch.object(_backends, "_run_probe", patched_run_probe), |
| 325 | ): |
| 326 | record = doctor._x_record(config) |
| 327 | |
| 328 | # Doctor should NOT report X as unconfigured. |
| 329 | assert record["tier"] != "off", "xurl error must not be hidden by unused grok" |
| 330 | assert record["status"] != "unconfigured", "xurl error must not become unconfigured" |
| 331 | # Should keep the error tier and xurl repair guidance. |
| 332 | assert record["tier"] == "error" |
| 333 | # The fix should contain xurl repair guidance, not be empty. |
| 334 | assert record["fix"], "xurl repair guidance must not be cleared" |
| 335 | assert "xurl" in record["fix"].lower() or "oauth" in record["fix"].lower() |
| 336 | |
| 337 | |
| 338 | def test_doctor_pending_bird_does_not_hide_xurl_error(): |
| 339 | """Unpinned + FROM_BROWSER + bird installed + xurl ERROR -> keeps xurl error. |
| 340 | |
| 341 | Pending bird must NOT replace a record with a configured auto-chain backend |
| 342 | in ERROR. Doctor should report the xurl error with its repair guidance, |
| 343 | NOT report X as OK via pending bird. |
| 344 | """ |
| 345 | from lib import backends as _backends |
| 346 | from lib import env, grok_x |
| 347 | |
| 348 | config = {} # No pin, no static AUTH_TOKEN/CT0 |
| 349 | bird_status = { |
| 350 | "installed": True, # Bird IS installed (for pending bird) |
| 351 | "authenticated": False, # No static cookies |
| 352 | "username": "", |
| 353 | "can_install": True, |
| 354 | } |
| 355 | |
| 356 | # Mock xurl probe to return ERROR status |
| 357 | def mock_probe_xurl(config): |
| 358 | return _backends.BackendFinding( |
| 359 | name="xurl", |
| 360 | status=health.ERROR, |
| 361 | detail="store unreadable", |
| 362 | prescription="xurl auth oauth2 login", |
| 363 | requires="xurl CLI installed + OAuth2 login", |
| 364 | ) |
| 365 | |
| 366 | original_run_probe = _backends._run_probe |
| 367 | |
| 368 | def patched_run_probe(spec, config): |
| 369 | if spec.name == "xurl": |
| 370 | return mock_probe_xurl(config) |
| 371 | return original_run_probe(spec, config) |
| 372 | |
| 373 | # x_pending_browser_auth returns True (bird pending), but xurl has ERROR. |
| 374 | with ( |
| 375 | mock.patch.object(grok_x, "binary_path", return_value=None), |
| 376 | mock.patch.object(grok_x, "has_stored_auth", return_value=False), |
| 377 | mock.patch("lib.backends.which", return_value=None), |
| 378 | mock.patch("lib.bird_x.get_bird_status", return_value=bird_status), |
| 379 | mock.patch("lib.bird_x.is_bird_installed", return_value=True), |
| 380 | mock.patch.object(env, "x_pending_browser_auth", return_value=True), |
| 381 | mock.patch.object(_backends, "_run_probe", patched_run_probe), |
| 382 | ): |
| 383 | record = doctor._x_record(config) |
| 384 | |
| 385 | # Doctor should NOT report X as OK via pending bird. |
| 386 | assert record["tier"] != "ok", "xurl error must not be hidden by pending bird" |
| 387 | assert record["status"] != health.OK, "xurl error must not become OK" |
| 388 | # Should keep the error tier and xurl repair guidance. |
| 389 | assert record["tier"] == "error" |
| 390 | assert record["fix"], "xurl repair guidance must not be cleared" |
| 391 | assert "xurl" in record["fix"].lower() or "oauth" in record["fix"].lower() |
| 392 |