| 1 | """grok must never be a dead end: every failure falls through to the next backend. |
| 2 | |
| 3 | Grok is opt-in only (not in the auto chain), but when pinned via |
| 4 | LAST30DAYS_X_BACKEND=grok, any way grok can come back empty or error must |
| 5 | surface properly so the run reports the failure honestly rather than silently |
| 6 | leaving X uncovered. |
| 7 | """ |
| 8 | |
| 9 | from unittest import mock |
| 10 | |
| 11 | from lib import grok_x, pipeline |
| 12 | |
| 13 | |
| 14 | def _stub_grok(monkeypatch, stdout="", returncode=0, raises=None): |
| 15 | monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok") |
| 16 | |
| 17 | def fake_run(cmd, **kwargs): |
| 18 | if raises: |
| 19 | raise raises |
| 20 | import subprocess |
| 21 | return subprocess.CompletedProcess(cmd, returncode, stdout, "") |
| 22 | |
| 23 | monkeypatch.setattr(grok_x.subprocess, "run", fake_run) |
| 24 | |
| 25 | |
| 26 | def test_grok_cli_missing_yields_an_error_for_failover(monkeypatch): |
| 27 | monkeypatch.setattr(grok_x, "binary_path", lambda: None) |
| 28 | result = grok_x.search_x("topic", "2026-07-14", "2026-08-13") |
| 29 | assert result["items"] == [] |
| 30 | assert result["error"], "a missing CLI must surface an error so the chain logs it" |
| 31 | |
| 32 | |
| 33 | def test_grok_timeout_yields_an_error_for_failover(monkeypatch): |
| 34 | import subprocess |
| 35 | _stub_grok(monkeypatch, raises=subprocess.TimeoutExpired("grok", 1)) |
| 36 | result = grok_x.search_x("topic", "2026-07-14", "2026-08-13") |
| 37 | assert result["items"] == [] |
| 38 | assert "timed out" in result["error"] |
| 39 | |
| 40 | |
| 41 | def test_grok_nonzero_exit_yields_an_error_for_failover(monkeypatch): |
| 42 | _stub_grok(monkeypatch, stdout="boom", returncode=1) |
| 43 | result = grok_x.search_x("topic", "2026-07-14", "2026-08-13") |
| 44 | assert result["items"] == [] |
| 45 | assert result["error"] |
| 46 | |
| 47 | |
| 48 | def test_grok_fabricated_results_end_up_empty_not_fabricated(monkeypatch): |
| 49 | """Provenance rejection must yield no items, so the chain moves to bird |
| 50 | rather than the run reporting invented posts.""" |
| 51 | block = ( |
| 52 | "id: 1956158892141441450\nhandle: steipete\n" |
| 53 | "created_at: Fri, 15 Aug 2025 00:59:58 GMT\nlikes: 100\n" |
| 54 | "text: recalled from training data\n" |
| 55 | ) |
| 56 | _stub_grok(monkeypatch, stdout=block) |
| 57 | result = grok_x.search_x("steipete", "2026-07-14", "2026-08-13") |
| 58 | assert result["items"] == [] |
| 59 | |
| 60 | |
| 61 | def test_clean_empty_result_carries_no_error(monkeypatch): |
| 62 | """A quiet window is not a broken backend; the chain still tries the next.""" |
| 63 | _stub_grok(monkeypatch, stdout="The search returned no posts.") |
| 64 | result = grok_x.search_x("topic", "2026-07-14", "2026-08-13") |
| 65 | assert result["items"] == [] |
| 66 | assert "error" not in result |
| 67 | |
| 68 | |
| 69 | # --- lane budget and selective retry --------------------------------------- |
| 70 | |
| 71 | def test_lane_budget_stops_issuing_queries(monkeypatch): |
| 72 | """Three lanes over three handles is up to 14 sequential LLM subprocess |
| 73 | calls, bounded only by per-call timeouts without a shared budget.""" |
| 74 | import time as _time |
| 75 | calls = {"n": 0} |
| 76 | monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok") |
| 77 | |
| 78 | def fake_run(cmd, **kwargs): |
| 79 | import subprocess |
| 80 | calls["n"] += 1 |
| 81 | return subprocess.CompletedProcess(cmd, 0, "no posts", "") |
| 82 | |
| 83 | monkeypatch.setattr(grok_x.subprocess, "run", fake_run) |
| 84 | past = _time.monotonic() - 1 |
| 85 | items, revoked = grok_x.search_handles( |
| 86 | ["a", "b", "c"], "topic", "2026-07-14", "2026-08-13", deadline=past |
| 87 | ) |
| 88 | assert items == [] |
| 89 | assert revoked is False |
| 90 | assert calls["n"] == 0, "an expired budget must stop the lane before any call" |
| 91 | |
| 92 | |
| 93 | def test_lane_budget_constant_is_bounded(): |
| 94 | assert 0 < grok_x.LANE_BUDGET_SECONDS <= 300 |
| 95 | |
| 96 | |
| 97 | def test_clean_empty_is_not_retried(monkeypatch): |
| 98 | """Retrying a byte-identical prompt cannot turn an empty window into posts; |
| 99 | it only doubles latency and Grok-plan spend on the common case.""" |
| 100 | calls = {"n": 0} |
| 101 | monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok") |
| 102 | |
| 103 | def fake_run(cmd, **kwargs): |
| 104 | import subprocess |
| 105 | calls["n"] += 1 |
| 106 | return subprocess.CompletedProcess(cmd, 0, "The search returned no posts.", "") |
| 107 | |
| 108 | monkeypatch.setattr(grok_x.subprocess, "run", fake_run) |
| 109 | items, error, auth_revoked = grok_x._run_query("topic", "2026-07-14", "2026-08-13") |
| 110 | assert items == [] and error == "" |
| 111 | assert calls["n"] == 1, "a clean empty result must not be retried" |
| 112 | assert not auth_revoked |
| 113 | |
| 114 | |
| 115 | def test_fabricated_response_is_still_retried(monkeypatch): |
| 116 | """The retry must survive for the case it exists to fix.""" |
| 117 | calls = {"n": 0} |
| 118 | monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok") |
| 119 | fabricated = ( |
| 120 | "id: 1956158892141441450\nhandle: steipete\n" |
| 121 | "created_at: Fri, 15 Aug 2025 00:59:58 GMT\nlikes: 10\ntext: recalled\n" |
| 122 | ) |
| 123 | |
| 124 | def fake_run(cmd, **kwargs): |
| 125 | import subprocess |
| 126 | calls["n"] += 1 |
| 127 | return subprocess.CompletedProcess(cmd, 0, fabricated, "") |
| 128 | |
| 129 | monkeypatch.setattr(grok_x.subprocess, "run", fake_run) |
| 130 | grok_x._run_query("steipete", "2026-07-14", "2026-08-13") |
| 131 | assert calls["n"] == 2, "a provenance rejection is exactly what retrying can fix" |
| 132 | |
| 133 | |
| 134 | def test_lanes_use_a_single_attempt(monkeypatch): |
| 135 | """Lane queries pass attempts=1: an empty lane is a legitimate outcome and |
| 136 | the shared budget is better spent on breadth than on repeats.""" |
| 137 | import inspect |
| 138 | for fn in (grok_x.search_handles, grok_x.search_mentions, grok_x.search_name): |
| 139 | assert "attempts=1" in inspect.getsource(fn), ( |
| 140 | f"{fn.__name__} should not retry lane queries" |
| 141 | ) |
| 142 | |
| 143 | |
| 144 | def test_every_empty_path_leaves_the_chain_free_to_continue(monkeypatch): |
| 145 | """The chain advances on an empty item list, so each way grok can come back |
| 146 | empty must actually return an empty list rather than raising or hanging. |
| 147 | |
| 148 | Behavioral on purpose: an earlier version of this test asserted on the |
| 149 | loop's source text and broke when the loop moved, while proving nothing |
| 150 | about what grok returns. |
| 151 | """ |
| 152 | import subprocess as sp |
| 153 | cases = { |
| 154 | "missing binary": (lambda: None, None), |
| 155 | "fabricated": (lambda: "/usr/bin/grok", |
| 156 | "id: 1956158892141441450\nhandle: steipete\n" |
| 157 | "created_at: Fri, 15 Aug 2025 00:59:58 GMT\nlikes: 1\ntext: x\n"), |
| 158 | "clean empty": (lambda: "/usr/bin/grok", "no posts found"), |
| 159 | "nonzero exit": (lambda: "/usr/bin/grok", None), |
| 160 | } |
| 161 | for label, (binary, stdout) in cases.items(): |
| 162 | monkeypatch.setattr(grok_x, "binary_path", binary) |
| 163 | if stdout is not None: |
| 164 | monkeypatch.setattr( |
| 165 | grok_x.subprocess, "run", |
| 166 | lambda cmd, **kw: sp.CompletedProcess(cmd, 0, stdout, ""), |
| 167 | ) |
| 168 | elif binary() is not None: |
| 169 | monkeypatch.setattr( |
| 170 | grok_x.subprocess, "run", |
| 171 | lambda cmd, **kw: sp.CompletedProcess(cmd, 1, "", "boom"), |
| 172 | ) |
| 173 | result = grok_x.search_x("topic", "2026-07-14", "2026-08-13") |
| 174 | assert result["items"] == [], f"{label} must yield no items so the chain advances" |
| 175 | |
| 176 | |
| 177 | def test_a_call_is_not_started_when_it_cannot_finish_in_budget(monkeypatch): |
| 178 | """The lanes run synchronously with no outer timeout, so a per-call floor |
| 179 | would let the last call overrun the documented shared ceiling.""" |
| 180 | import time as _time |
| 181 | calls = {"n": 0} |
| 182 | monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok") |
| 183 | |
| 184 | def fake_run(cmd, **kwargs): |
| 185 | import subprocess |
| 186 | calls["n"] += 1 |
| 187 | return subprocess.CompletedProcess(cmd, 0, "no posts", "") |
| 188 | |
| 189 | monkeypatch.setattr(grok_x.subprocess, "run", fake_run) |
| 190 | # 5s left: below the minimum useful call, so nothing should start. |
| 191 | near = _time.monotonic() + 5 |
| 192 | items, error, auth_revoked = grok_x._run_query("topic", "2026-07-14", "2026-08-13", deadline=near) |
| 193 | assert calls["n"] == 0 |
| 194 | assert "budget exhausted" in error |
| 195 | assert not auth_revoked |
| 196 | |
| 197 | |
| 198 | def test_timeout_never_exceeds_the_remaining_budget(monkeypatch): |
| 199 | import time as _time |
| 200 | seen = {} |
| 201 | monkeypatch.setattr(grok_x, "binary_path", lambda: "/usr/bin/grok") |
| 202 | |
| 203 | def fake_run(cmd, **kwargs): |
| 204 | import subprocess |
| 205 | seen["timeout"] = kwargs.get("timeout") |
| 206 | return subprocess.CompletedProcess(cmd, 0, "no posts", "") |
| 207 | |
| 208 | monkeypatch.setattr(grok_x.subprocess, "run", fake_run) |
| 209 | remaining = 40 |
| 210 | grok_x._run_query( |
| 211 | "topic", "2026-07-14", "2026-08-13", |
| 212 | deadline=_time.monotonic() + remaining, |
| 213 | ) |
| 214 | assert seen["timeout"] <= remaining |
| 215 |