返回 last30days-skill
test_x_chain_failover.py
根目录 / tests / test_x_chain_failover.py
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
215 lines PYTHON