feat(x): bird first, grok CLI opt-in only (#1005)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
+4
-2
@@ -167,7 +167,9 @@ python3 skills/last30days/scripts/last30days.py "MCP servers" \
|
|||||||
|
|
||||||
**YouTube transcript tuning.** `LAST30DAYS_YT_SUB_LANGS` controls the comma-separated caption-language priority passed to yt-dlp and defaults to `en,es,pt`. When `SCRAPECREATORS_API_KEY` is available, yt-dlp uses one fast attempt before the paid fallback; set `LAST30DAYS_YT_TRANSCRIPT_FAST_TIMEOUT` to the number of seconds allowed for that attempt when a throttled host needs longer than the 12-second default. A VTT completed before the timeout is reused rather than discarded. `LAST30DAYS_YT_SEARCH_TIMEOUT` sets the per-search yt-dlp deadline (default 120s). Comparison-mode fan-out also caps concurrent yt-dlp processes process-wide and caches identical searches within a run so redundant `ytsearch` calls do not self-throttle the same IP.
|
**YouTube transcript tuning.** `LAST30DAYS_YT_SUB_LANGS` controls the comma-separated caption-language priority passed to yt-dlp and defaults to `en,es,pt`. When `SCRAPECREATORS_API_KEY` is available, yt-dlp uses one fast attempt before the paid fallback; set `LAST30DAYS_YT_TRANSCRIPT_FAST_TIMEOUT` to the number of seconds allowed for that attempt when a throttled host needs longer than the 12-second default. A VTT completed before the timeout is reused rather than discarded. `LAST30DAYS_YT_SEARCH_TIMEOUT` sets the per-search yt-dlp deadline (default 120s). Comparison-mode fan-out also caps concurrent yt-dlp processes process-wide and caches identical searches within a run so redundant `ytsearch` calls do not self-throttle the same IP.
|
||||||
|
|
||||||
**X with no X credential (Grok CLI).** Install the Grok CLI (`curl -fsSL https://x.ai/cli/install.sh | bash`) and run `grok login`, and X works with no X account, no browser cookies, and no `XAI_API_KEY`. It sits ahead of the cookie path in the backend chain; pin `LAST30DAYS_X_BACKEND=bird` to prefer cookies. It is not "free" in the way the cookie path is: calls draw on your Grok plan, and depth costs several calls per run because the underlying tool caps each search at 10 posts. Results are validated before use — every returned post's ID is decoded to confirm it falls inside the requested date range, because the retrieval is performed by a language model and can otherwise return confident, well-formed posts that were never searched for.
|
**X backend priority (bird first).** The default X backend chain is bird (browser cookies) → xai (API key) → xurl (OAuth2 CLI) → xquik (API key). Cookies beat `XAI_API_KEY` when both are present. A leftover grok login never steals the X lane; see below.
|
||||||
|
|
||||||
|
**Grok CLI (opt-in backup).** Install the Grok CLI (`curl -fsSL https://x.ai/cli/install.sh | bash`) and run `grok login`, and X can work with no X account, no browser cookies, and no `XAI_API_KEY`. However, grok is **opt-in only**: a leftover `~/.grok/auth.json` must never steal the X lane. Pin `LAST30DAYS_X_BACKEND=grok` to enable it. It is not "free" in the way the cookie path is: calls draw on your Grok plan, and depth costs several calls per run because the underlying tool caps each search at 10 posts. Results are validated before use — every returned post's ID is decoded to confirm it falls inside the requested date range, because the retrieval is performed by a language model and can otherwise return confident, well-formed posts that were never searched for.
|
||||||
|
|
||||||
**X on cookie-less hosts.** Bird (the free X source) scrapes X using your logged-in browser cookies (`AUTH_TOKEN`/`CT0`), which agent hosts like OpenClaw, CI, or headless runs often can't supply — and scraping carries some account risk. On those, set `XQUIK_API_KEY` (or `XAI_API_KEY`) for full, ranked X coverage from a single API key: the same engagement-based ranking, first-party authorship, and handle (from/mentions) lanes the native X source gets. `--diagnose` reports whether the key is working (and flags an unpaid key).
|
**X on cookie-less hosts.** Bird (the free X source) scrapes X using your logged-in browser cookies (`AUTH_TOKEN`/`CT0`), which agent hosts like OpenClaw, CI, or headless runs often can't supply — and scraping carries some account risk. On those, set `XQUIK_API_KEY` (or `XAI_API_KEY`) for full, ranked X coverage from a single API key: the same engagement-based ranking, first-party authorship, and handle (from/mentions) lanes the native X source gets. `--diagnose` reports whether the key is working (and flags an unpaid key).
|
||||||
|
|
||||||
@@ -421,7 +423,7 @@ Every live run writes its JSON result to `~/.config/last30days/doctor-cache.json
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `LAST30DAYS_DOCTOR_TTL` | Freshness window for `doctor --cached`, in **seconds**. Defaults to `900` (15 minutes). `0` makes every `--cached` call run live. |
|
| `LAST30DAYS_DOCTOR_TTL` | Freshness window for `doctor --cached`, in **seconds**. Defaults to `900` (15 minutes). `0` makes every `--cached` call run live. |
|
||||||
| `LAST30DAYS_DOCTOR_PROBE_TIMEOUT` | Per-source deadline (**seconds**) for `doctor --probe` live checks. Defaults to `10`. Caps each concurrent probe so a slow source cannot hang the command. |
|
| `LAST30DAYS_DOCTOR_PROBE_TIMEOUT` | Per-source deadline (**seconds**) for `doctor --probe` live checks. Defaults to `10`. Caps each concurrent probe so a slow source cannot hang the command. |
|
||||||
| `LAST30DAYS_X_BACKEND` | Pins the X backend (`xai` / `grok` / `bird` / `xurl` / `xquik`); doctor renders the pin and predicts "will use" accordingly. Pin `bird` to keep the cookie path when a signed-in `grok` CLI is present, since `grok` otherwise wins by default. |
|
| `LAST30DAYS_X_BACKEND` | Pins the X backend (`bird` / `xai` / `xurl` / `xquik` / `grok`); doctor renders the pin and predicts "will use" accordingly. The unpinned auto chain is bird → xai → xurl → xquik (grok is opt-in only). Pin `grok` to enable it; a leftover `~/.grok/auth.json` is never auto-selected. |
|
||||||
| `LAST30DAYS_REDDIT_BACKEND` | `scrapecreators` makes ScrapeCreators the primary Reddit backend; doctor renders Reddit's conditional routing with the pin applied. |
|
| `LAST30DAYS_REDDIT_BACKEND` | `scrapecreators` makes ScrapeCreators the primary Reddit backend; doctor renders Reddit's conditional routing with the pin applied. |
|
||||||
| `LAST30DAYS_REDDIT_SC_MIN_ITEMS` | Integer thinness floor for ScrapeCreators Reddit **search** backfill. Default `0` = empty-only (free path keeps any non-empty result; no credit spend). Set above `0` to backfill when free yield is below that count; merged results dedupe by post id. Requires `SCRAPECREATORS_API_KEY`. Ignored when `LAST30DAYS_REDDIT_BACKEND=scrapecreators` (SC is already primary). |
|
| `LAST30DAYS_REDDIT_SC_MIN_ITEMS` | Integer thinness floor for ScrapeCreators Reddit **search** backfill. Default `0` = empty-only (free path keeps any non-empty result; no credit spend). Set above `0` to backfill when free yield is below that count; merged results dedupe by post id. Requires `SCRAPECREATORS_API_KEY`. Ignored when `LAST30DAYS_REDDIT_BACKEND=scrapecreators` (SC is already primary). |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
X backend priority changed: bird (browser cookies) is now first in the auto chain, ahead of xai/xurl/xquik. Cookies beat XAI_API_KEY when both are present. Grok CLI is demoted to opt-in only: a leftover `~/.grok/auth.json` no longer steals the X lane. Pin `LAST30DAYS_X_BACKEND=grok` to enable it explicitly.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# feat(x): demote Grok CLI to opt-in backup
|
||||||
|
|
||||||
|
Stop using the Grok CLI as the default X backend. A leftover `~/.grok/auth.json` must never steal the X lane. Grok stays as a pin-only backup: off unless LAST30DAYS_X_BACKEND=grok or --x-backend grok.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- R1. Unpinned auto chain is bird → xai → xurl → xquik. Bird is first. Grok is not a member. Presence of ~/.grok/auth.json (ok, expired, or error) must not change which backend an unpinned run uses.
|
||||||
|
- R2. Grok remains a valid explicit selection: LAST30DAYS_X_BACKEND=grok and --x-backend grok. A pin forces grok with no failover. If grok is unusable, X is unconfigured and doctor/footer say so with the existing login hint.
|
||||||
|
- R3. Doctor "will use: grok" only when grok is pinned and the probe is OK or DEGRADED. Unpinned, grok may appear as unused opt-in ("available, unused — pin LAST30DAYS_X_BACKEND=grok"), never as the predicted winner.
|
||||||
|
- R4. get_x_source_status and get_x_source_with_method must prefer bird over xai/xurl/xquik when cookies are present. Grok wins only when the pin is grok.
|
||||||
|
- R5. Host docs stop presenting grok as the default keyless X path. Document it as opt-in backup. Default story is bird first, then xai / xurl / xquik.
|
||||||
|
- R6. Setup / first-run / prescriptions do not nag grok login as the fix for missing X. Cookie consent and paid keys remain the default prescriptions. Grok login is mentioned only as an optional pin.
|
||||||
|
- R7. Do not delete scripts/lib/grok_x.py, retrieve-judge-retry, or expires_at honesty. Pinned grok still uses them.
|
||||||
|
- R8. A machine with only a grok login (no cookies, no XAI/XQUIK, no xurl) has X unconfigured until the user pins grok. Footer: X skipped-unconfigured, not auth-failed-from-grok.
|
||||||
|
- R9. Tests cover the cases above; docs/changelog updated.
|
||||||
|
|
||||||
|
### Implementation units
|
||||||
|
U1 env.py: _X_BACKEND_ORDER = ("bird", "xai", "xurl", "xquik"); X_BACKEND_OPT_IN = ("grok",); X_BACKEND_KNOWN = ORDER + OPT_IN; pin uses KNOWN; unpinned walks ORDER only; get_x_source_status bird first, grok only if pinned; get_x_source_with_method bird before xai.
|
||||||
|
|
||||||
|
U2 backends.py / doctor.py / prescriptions.py: descriptor is auto ORDER then grok opt-in; unpinned collect-then-pick ignores opt-in; do not change _probe_grok honesty.
|
||||||
|
|
||||||
|
U3 SKILL.md, CONFIGURATION.md, README.md, README.pt-BR.md if needed, changelog.d: remove "sits ahead of the cookie path"; document bird → xai → xurl → xquik; pin grok to enable it.
|
||||||
|
|
||||||
|
U4 tests: unpinned grok-only empty; unpinned grok+bird → bird; unpinned bird+xai → bird; pin grok+store → ["grok"]; pin grok no store empty; doctor unpinned never predicts grok; descriptor parity treats grok as trailing opt-in.
|
||||||
|
|
||||||
|
### Tests T1–T7
|
||||||
|
T1 unpinned grok AUTH_OK, no other creds → X unconfigured
|
||||||
|
T2 unpinned grok AUTH_EXPIRED, no other creds → X unconfigured (not will-use grok)
|
||||||
|
T3 unpinned grok AUTH_OK + cookies → bird
|
||||||
|
T4 unpinned XAI_API_KEY + grok store, no cookies → xai
|
||||||
|
T4b unpinned XAI_API_KEY + cookies → bird
|
||||||
|
T5 pin grok AUTH_OK → grok no failover
|
||||||
|
T6 pin grok no store → error / grok login prescription
|
||||||
|
T7 docs match R5
|
||||||
|
|
||||||
|
### Keep-the-door-open (KTD)
|
||||||
|
1. grok_x.py stays untouched
|
||||||
|
2. x_judge.py stays untouched
|
||||||
|
3. expires_at honesty stays untouched
|
||||||
|
4. auth.x.ai is never called
|
||||||
|
5. bird cookie extraction is unchanged
|
||||||
@@ -577,8 +577,6 @@ Options:
|
|||||||
|
|
||||||
**If the user picks Auto setup:**
|
**If the user picks Auto setup:**
|
||||||
|
|
||||||
**Check for a Grok path before asking for cookies.** Run `command -v grok`. If it resolves, X can be unlocked with no X credential at all, so lead with that instead of a cookie read: tell the user `grok login` is enough and only offer the cookie options if they decline. Do not call it free — it needs a Grok plan.
|
|
||||||
|
|
||||||
Get cookie consent first. Check if `BROWSER_CONSENT=true` already exists in `~/.config/last30days/.env`; if so, skip the consent prompt and run `setup --allow-browser-cookies` directly. Otherwise **call AskUserQuestion:**
|
Get cookie consent first. Check if `BROWSER_CONSENT=true` already exists in `~/.config/last30days/.env`; if so, skip the consent prompt and run `setup --allow-browser-cookies` directly. Otherwise **call AskUserQuestion:**
|
||||||
Question: "Auto setup installs the free CLIs either way - yt-dlp (YouTube), Digg, arXiv, and Techmeme. The only thing that needs your OK is reading your browser's x.com cookies to authenticate X/Twitter search: I check Chrome first (a one-time macOS Keychain prompt may appear; click Always Allow), then Firefox and Safari. Cookies are read live, never saved to disk. Include X?"
|
Question: "Auto setup installs the free CLIs either way - yt-dlp (YouTube), Digg, arXiv, and Techmeme. The only thing that needs your OK is reading your browser's x.com cookies to authenticate X/Twitter search: I check Chrome first (a one-time macOS Keychain prompt may appear; click Always Allow), then Firefox and Safari. Cookies are read live, never saved to disk. Include X?"
|
||||||
Options (give each option the description shown):
|
Options (give each option the description shown):
|
||||||
@@ -586,7 +584,7 @@ Options (give each option the description shown):
|
|||||||
- "Skip X - just the CLIs" - description: "No cookie reads. Still installs yt-dlp (YouTube), Digg, arXiv, and Techmeme." Run `FROM_BROWSER=off "${LAST30DAYS_PYTHON:-python3}" skills/last30days/scripts/last30days.py setup`.
|
- "Skip X - just the CLIs" - description: "No cookie reads. Still installs yt-dlp (YouTube), Digg, arXiv, and Techmeme." Run `FROM_BROWSER=off "${LAST30DAYS_PYTHON:-python3}" skills/last30days/scripts/last30days.py setup`.
|
||||||
- "xAI API key for X instead" - description: "Use an api.x.ai key for X search (no cookie read), plus install yt-dlp (YouTube), Digg, arXiv, and Techmeme." Ask them to paste it, write `XAI_API_KEY` to `.env`, then run `FROM_BROWSER=off "${LAST30DAYS_PYTHON:-python3}" skills/last30days/scripts/last30days.py setup`.
|
- "xAI API key for X instead" - description: "Use an api.x.ai key for X search (no cookie read), plus install yt-dlp (YouTube), Digg, arXiv, and Techmeme." Ask them to paste it, write `XAI_API_KEY` to `.env`, then run `FROM_BROWSER=off "${LAST30DAYS_PYTHON:-python3}" skills/last30days/scripts/last30days.py setup`.
|
||||||
|
|
||||||
When `command -v grok` resolved, replace the "xAI API key for X instead" option with: "Sign in to Grok instead" - description: "X search with no X account, cookies, or API key. Run `grok login` once." Nothing is written to `.env`; still install the free CLIs with `FROM_BROWSER=off "${LAST30DAYS_PYTHON:-python3}" skills/last30days/scripts/last30days.py setup`.
|
**Grok CLI is an opt-in backup, not a setup-time recommendation.** Do NOT check for grok first or offer it as a primary option during setup. A leftover `~/.grok/auth.json` must never steal the X lane. If the user mentions having a Grok account, tell them: "You can use the Grok CLI by pinning `LAST30DAYS_X_BACKEND=grok` in your `.env` after running `grok login`. This is opt-in because a leftover grok login should not take over X automatically." Do not call it free — it needs a Grok plan.
|
||||||
|
|
||||||
The consented `setup --allow-browser-cookies` run extracts cookies (Chrome/Chromium family first via the Keychain with no Full Disk Access, then Firefox and Safari as fallbacks; the winning browser is pinned for future runs only when it is Firefox or Safari, so Chrome never re-triggers the Keychain prompt on later runs) and best-effort installs yt-dlp (YouTube), the free keyless Digg CLI (`digg-pp-cli` via `@mvanhorn/printing-press-library install digg --cli-only`; Digg activates only when the binary is on the **agent subprocess PATH**, typically `$HOME/.local/bin`; setup reports honestly if installed off-PATH; recommend-only if `npx` is unavailable), plus the free keyless arXiv and Techmeme CLIs. Show the user what was found and installed - including whether Digg landed on PATH (active) or off-PATH (installed but not yet active).
|
The consented `setup --allow-browser-cookies` run extracts cookies (Chrome/Chromium family first via the Keychain with no Full Disk Access, then Firefox and Safari as fallbacks; the winning browser is pinned for future runs only when it is Firefox or Safari, so Chrome never re-triggers the Keychain prompt on later runs) and best-effort installs yt-dlp (YouTube), the free keyless Digg CLI (`digg-pp-cli` via `@mvanhorn/printing-press-library install digg --cli-only`; Digg activates only when the binary is on the **agent subprocess PATH**, typically `$HOME/.local/bin`; setup reports honestly if installed off-PATH; recommend-only if `npx` is unavailable), plus the free keyless arXiv and Techmeme CLIs. Show the user what was found and installed - including whether Digg landed on PATH (active) or off-PATH (installed but not yet active).
|
||||||
|
|
||||||
@@ -2010,23 +2008,15 @@ If the research output contains a `**🔍 Research Coverage:**` block, render it
|
|||||||
|
|
||||||
**Just-in-time X unlock:** If X returned 0 results because no X auth is configured (no AUTH_TOKEN/CT0, no XAI_API_KEY, no FROM_BROWSER), offer to set it up right there.
|
**Just-in-time X unlock:** If X returned 0 results because no X auth is configured (no AUTH_TOKEN/CT0, no XAI_API_KEY, no FROM_BROWSER), offer to set it up right there.
|
||||||
|
|
||||||
**First check whether a Grok path is already present.** Run `command -v grok`. If it resolves, the user needs no X credential at all — say so and offer `grok login` as the first option, because it is one command and unlocks X with no X account, no cookies, and no API key.
|
|
||||||
|
|
||||||
**Call AskUserQuestion.** Question: "X/Twitter wasn't searched. Want to unlock it?"
|
**Call AskUserQuestion.** Question: "X/Twitter wasn't searched. Want to unlock it?"
|
||||||
|
|
||||||
Options when `grok` IS on PATH (or the user has said they have a Grok account):
|
Default options (always presented first — cookie consent and paid keys are the primary X fix):
|
||||||
- "Sign in to Grok (no X account needed)" - Have them run `grok login`; nothing is written to .env and no X credential is involved
|
|
||||||
- "Scan my browser cookies (free)" - Get consent, run cookie scan, write BROWSER_CONSENT=true + FROM_BROWSER=auto to .env
|
|
||||||
- "I have AUTH_TOKEN and CT0 from my browser" - Ask them to paste each value, then write AUTH_TOKEN=<value>\nCT0=<value> to .env
|
|
||||||
- "Skip for now"
|
|
||||||
|
|
||||||
Options when `grok` is NOT on PATH — offer today's options unchanged, and mention the Grok path only as a closing one-liner rather than a menu entry. A user with no Grok account should not have their actionable choices pushed down the list by one they cannot take:
|
|
||||||
- "Scan my browser cookies (free)" - Get consent, run cookie scan, write BROWSER_CONSENT=true + FROM_BROWSER=auto to .env
|
- "Scan my browser cookies (free)" - Get consent, run cookie scan, write BROWSER_CONSENT=true + FROM_BROWSER=auto to .env
|
||||||
- "I have AUTH_TOKEN and CT0 from my browser" - Ask them to paste each value, then write AUTH_TOKEN=<value>\nCT0=<value> to .env
|
- "I have AUTH_TOKEN and CT0 from my browser" - Ask them to paste each value, then write AUTH_TOKEN=<value>\nCT0=<value> to .env
|
||||||
- "I have an xAI API key" - Ask them to paste it, write XAI_API_KEY to .env
|
- "I have an xAI API key" - Ask them to paste it, write XAI_API_KEY to .env
|
||||||
- "Skip for now"
|
- "Skip for now"
|
||||||
|
|
||||||
After the modal, if `grok` was absent, add one line: "If you have a Grok account, installing the Grok CLI (`curl -fsSL https://x.ai/cli/install.sh | bash`) unlocks X with no X credential at all." Do not describe the Grok path as free — it needs a Grok plan.
|
**Grok CLI is an opt-in backup, not a default prescription.** After showing the modal, add one line: "If you have a Grok account and prefer to use it: install the Grok CLI (`curl -fsSL https://x.ai/cli/install.sh | bash`), run `grok login`, then set `LAST30DAYS_X_BACKEND=grok` to enable it." Do not describe the Grok path as free — it needs a Grok plan. Do not put grok first or as a primary recommendation; a leftover `~/.grok/auth.json` must never steal the X lane.
|
||||||
|
|
||||||
**THEN - Engine footer pass-through (right before invitation):**
|
**THEN - Engine footer pass-through (right before invitation):**
|
||||||
|
|
||||||
|
|||||||
@@ -107,12 +107,15 @@ class BackendSpec:
|
|||||||
|
|
||||||
``probe`` must be side-effect-free. When ``paid`` is True the probe is
|
``probe`` must be side-effect-free. When ``paid`` is True the probe is
|
||||||
key-presence only: no subprocess, no network, no credential spend.
|
key-presence only: no subprocess, no network, no credential spend.
|
||||||
|
``opt_in`` marks backends that are never auto-selected and require an
|
||||||
|
explicit pin (grok).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
requires: str
|
requires: str
|
||||||
probe: Callable[[Dict[str, Any]], "BackendFinding"]
|
probe: Callable[[Dict[str, Any]], "BackendFinding"]
|
||||||
paid: bool = False
|
paid: bool = False
|
||||||
|
opt_in: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -427,6 +430,8 @@ _X_PROBES: Dict[str, Callable[[Dict[str, Any]], BackendFinding]] = {
|
|||||||
"xquik": _key_probe("xquik", "XQUIK_API_KEY", "XQUIK_API_KEY (xquik.com)"),
|
"xquik": _key_probe("xquik", "XQUIK_API_KEY", "XQUIK_API_KEY (xquik.com)"),
|
||||||
}
|
}
|
||||||
_X_PAID = {"xai", "xquik"}
|
_X_PAID = {"xai", "xquik"}
|
||||||
|
# Opt-in backends: never auto-selected; require explicit pin.
|
||||||
|
_X_OPT_IN = set(env.X_BACKEND_OPT_IN)
|
||||||
|
|
||||||
_WEB_PROBES: Dict[str, Callable[[Dict[str, Any]], BackendFinding]] = {
|
_WEB_PROBES: Dict[str, Callable[[Dict[str, Any]], BackendFinding]] = {
|
||||||
"brave": _key_probe("brave", "BRAVE_API_KEY", "BRAVE_API_KEY"),
|
"brave": _key_probe("brave", "BRAVE_API_KEY", "BRAVE_API_KEY"),
|
||||||
@@ -447,25 +452,32 @@ _SC_SPEC = BackendSpec(
|
|||||||
paid=True,
|
paid=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# X backend requirements, keyed by name.
|
||||||
|
_X_REQUIRES: Dict[str, str] = {
|
||||||
|
"xai": "XAI_API_KEY (xAI/Grok live search)",
|
||||||
|
"grok": "grok CLI installed + signed in (opt-in only; pin to enable)",
|
||||||
|
"bird": "X browser cookies (AUTH_TOKEN/CT0) + node",
|
||||||
|
"xurl": "xurl CLI installed + OAuth2 login",
|
||||||
|
"xquik": "XQUIK_API_KEY (xquik.com)",
|
||||||
|
}
|
||||||
|
|
||||||
DESCRIPTORS: Dict[str, ChainDescriptor] = {
|
DESCRIPTORS: Dict[str, ChainDescriptor] = {
|
||||||
# X: chain order and pin var imported from env.py (single source of truth).
|
# X: chain order and pin var imported from env.py (single source of truth).
|
||||||
|
# Backends include the auto chain (X_BACKEND_ORDER) plus opt-in entries
|
||||||
|
# (X_BACKEND_OPT_IN) for doctor visibility. Opt-in backends like grok
|
||||||
|
# appear in findings but are never auto-selected; pin to enable.
|
||||||
"x": ChainDescriptor(
|
"x": ChainDescriptor(
|
||||||
source="x",
|
source="x",
|
||||||
mode=MODE_ALTERNATIVE,
|
mode=MODE_ALTERNATIVE,
|
||||||
backends=tuple(
|
backends=tuple(
|
||||||
BackendSpec(
|
BackendSpec(
|
||||||
name=name,
|
name=name,
|
||||||
requires={
|
requires=_X_REQUIRES[name],
|
||||||
"xai": "XAI_API_KEY (xAI/Grok live search)",
|
|
||||||
"grok": "grok CLI installed + signed in (no X credential)",
|
|
||||||
"bird": "X browser cookies (AUTH_TOKEN/CT0) + node",
|
|
||||||
"xurl": "xurl CLI installed + OAuth2 login",
|
|
||||||
"xquik": "XQUIK_API_KEY (xquik.com)",
|
|
||||||
}[name],
|
|
||||||
probe=_X_PROBES[name],
|
probe=_X_PROBES[name],
|
||||||
paid=name in _X_PAID,
|
paid=name in _X_PAID,
|
||||||
|
opt_in=name in _X_OPT_IN,
|
||||||
)
|
)
|
||||||
for name in env.X_BACKEND_ORDER
|
for name in env.X_BACKEND_ORDER + env.X_BACKEND_OPT_IN
|
||||||
),
|
),
|
||||||
pin_var=env.X_BACKEND_PIN_VAR,
|
pin_var=env.X_BACKEND_PIN_VAR,
|
||||||
),
|
),
|
||||||
@@ -575,6 +587,8 @@ def _resolve_alternative(
|
|||||||
) -> BackendResolution:
|
) -> BackendResolution:
|
||||||
names = [spec.name for spec in descriptor.backends]
|
names = [spec.name for spec in descriptor.backends]
|
||||||
by_name = {f.name: f for f in findings}
|
by_name = {f.name: f for f in findings}
|
||||||
|
# Track which backends are opt-in (never auto-selected).
|
||||||
|
opt_in_names = {spec.name for spec in descriptor.backends if spec.opt_in}
|
||||||
res = BackendResolution(
|
res = BackendResolution(
|
||||||
source=descriptor.source,
|
source=descriptor.source,
|
||||||
mode=MODE_ALTERNATIVE,
|
mode=MODE_ALTERNATIVE,
|
||||||
@@ -611,18 +625,21 @@ def _resolve_alternative(
|
|||||||
|
|
||||||
# Collect-then-pick: first fully-usable wins; else best degraded; else
|
# Collect-then-pick: first fully-usable wins; else best degraded; else
|
||||||
# error carrying the highest-priority backend's prescription.
|
# error carrying the highest-priority backend's prescription.
|
||||||
for finding in findings:
|
# Opt-in backends are NEVER auto-selected; skip them entirely.
|
||||||
|
auto_findings = [f for f in findings if f.name not in opt_in_names]
|
||||||
|
for finding in auto_findings:
|
||||||
if finding.status == health.OK:
|
if finding.status == health.OK:
|
||||||
res.active_backend = finding.name
|
res.active_backend = finding.name
|
||||||
res.tier = TIER_OK
|
res.tier = TIER_OK
|
||||||
return res
|
return res
|
||||||
for finding in findings:
|
for finding in auto_findings:
|
||||||
if finding.status == health.DEGRADED:
|
if finding.status == health.DEGRADED:
|
||||||
res.active_backend = finding.name
|
res.active_backend = finding.name
|
||||||
res.tier = TIER_WARN
|
res.tier = TIER_WARN
|
||||||
return res
|
return res
|
||||||
res.tier = TIER_ERROR
|
res.tier = TIER_ERROR
|
||||||
res.prescription = findings[0].prescription if findings else ""
|
# Prescription comes from the first auto-chain backend, not opt-in.
|
||||||
|
res.prescription = auto_findings[0].prescription if auto_findings else ""
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -417,20 +417,6 @@ def _reddit_record(config):
|
|||||||
|
|
||||||
def _x_record(config):
|
def _x_record(config):
|
||||||
record = _chained_record("x", config)
|
record = _chained_record("x", config)
|
||||||
# A usable grok CLI covers X with no X credential at all, so it must not be
|
|
||||||
# reported as unconfigured. Checked before the browser-auth override below
|
|
||||||
# because it outranks bird in the chain and, unlike a cookie session, its
|
|
||||||
# availability is verifiable locally rather than "not verified until a run".
|
|
||||||
from . import grok_x
|
|
||||||
if record["status"] == "unconfigured" and grok_x.has_stored_auth():
|
|
||||||
record["status"] = health.OK
|
|
||||||
record["tier"] = TIER_BY_STATUS[health.OK]
|
|
||||||
record["note"] = (
|
|
||||||
"will use: grok (grok CLI, signed in; no X account, cookies, or "
|
|
||||||
"API key needed)"
|
|
||||||
)
|
|
||||||
record["fix"] = ""
|
|
||||||
return record
|
|
||||||
# Diagnose/doctor load config in plan_only mode, so browser cookies are not
|
# Diagnose/doctor load config in plan_only mode, so browser cookies are not
|
||||||
# extracted and every X backend reads as statically missing -> unconfigured.
|
# extracted and every X backend reads as statically missing -> unconfigured.
|
||||||
# But if bird is installed and FROM_BROWSER will authenticate X at run time,
|
# But if bird is installed and FROM_BROWSER will authenticate X at run time,
|
||||||
@@ -439,16 +425,91 @@ def _x_record(config):
|
|||||||
# diagnose cannot drift. It reads no cookie *values*, so it confirms a run
|
# diagnose cannot drift. It reads no cookie *values*, so it confirms a run
|
||||||
# will *attempt* browser auth, not that the session is currently valid -
|
# will *attempt* browser auth, not that the session is currently valid -
|
||||||
# keep the note honest and point at the verified key-backed path.
|
# keep the note honest and point at the verified key-backed path.
|
||||||
if record["status"] == "unconfigured" and env.x_pending_browser_auth(
|
#
|
||||||
config, local_only=True
|
# This check MUST come before grok normalization: a pending bird path takes
|
||||||
):
|
# precedence over marking X as unconfigured due to an unused grok store.
|
||||||
record["status"] = health.OK
|
# Handle both "unconfigured" (all backends missing) and "error" (grok present
|
||||||
record["tier"] = TIER_BY_STATUS[health.OK]
|
# but opt-in, no auto-chain backend usable) when pending bird applies.
|
||||||
record["note"] = (
|
#
|
||||||
"will use: bird (browser cookies; session not verified until a run "
|
# HOWEVER: pending bird must NOT replace a record that has a configured
|
||||||
"- add XAI_API_KEY for a verified, cookie-free path)"
|
# auto-chain backend in ERROR/DEGRADED/BROKEN/TIMEOUT. Same rule as the
|
||||||
|
# grok normalizer: only upgrade when no auto backend is configured-but-broken.
|
||||||
|
pending_bird = env.x_pending_browser_auth(config, local_only=True)
|
||||||
|
if pending_bird and record["status"] in ("unconfigured", health.ERROR):
|
||||||
|
backends_list = record.get("backends", [])
|
||||||
|
auto_chain_names = {"bird", "xai", "xurl", "xquik"}
|
||||||
|
auto_backends = [b for b in backends_list if b.get("name") in auto_chain_names]
|
||||||
|
# Only apply pending-bird upgrade if ALL auto-chain backends are MISSING.
|
||||||
|
# If any auto backend is configured but broken, keep that error.
|
||||||
|
all_auto_missing = all(
|
||||||
|
b.get("status") == health.MISSING for b in auto_backends
|
||||||
)
|
)
|
||||||
record["fix"] = ""
|
if all_auto_missing:
|
||||||
|
record["status"] = health.OK
|
||||||
|
record["tier"] = TIER_BY_STATUS[health.OK]
|
||||||
|
record["note"] = (
|
||||||
|
"will use: bird (browser cookies; session not verified until a run "
|
||||||
|
"- add XAI_API_KEY for a verified, cookie-free path)"
|
||||||
|
)
|
||||||
|
record["fix"] = ""
|
||||||
|
return record
|
||||||
|
#
|
||||||
|
# Grok is opt-in only: a leftover ~/.grok/auth.json must never steal the X
|
||||||
|
# lane. The grok backend appears in the chain findings (for visibility) but
|
||||||
|
# is never auto-selected. Doctor reports it as "available, unused - pin
|
||||||
|
# LAST30DAYS_X_BACKEND=grok to enable" rather than "will use: grok".
|
||||||
|
#
|
||||||
|
# R3/R8: When no auto-chain backend is CONFIGURED (all MISSING) but grok has
|
||||||
|
# any non-MISSING status, X is unconfigured/skipped - NOT broken/auth-failed.
|
||||||
|
# The tier must be "off" (unconfigured), not "error" (NOT WORKING).
|
||||||
|
#
|
||||||
|
# HOWEVER: if an auto-chain backend IS configured but broken (ERROR/DEGRADED),
|
||||||
|
# do NOT normalize to unconfigured. Keep that backend's error and repair
|
||||||
|
# guidance. Unused grok must not swallow a genuine auto-chain failure.
|
||||||
|
#
|
||||||
|
# Do NOT apply this normalization when pending browser auth would make bird
|
||||||
|
# usable — check pending_bird first (handled above via early return).
|
||||||
|
if (
|
||||||
|
record["tier"] == TIER_ERROR
|
||||||
|
and not record.get("pinned")
|
||||||
|
and record.get("active_backend") is None
|
||||||
|
and not pending_bird
|
||||||
|
):
|
||||||
|
backends_list = record.get("backends", [])
|
||||||
|
auto_chain_names = {"bird", "xai", "xurl", "xquik"}
|
||||||
|
auto_backends = [b for b in backends_list if b.get("name") in auto_chain_names]
|
||||||
|
# Only normalize if ALL auto-chain backends are MISSING (not configured).
|
||||||
|
# If any auto backend is ERROR/DEGRADED/BROKEN/TIMEOUT, keep that error.
|
||||||
|
all_auto_missing = all(
|
||||||
|
b.get("status") == health.MISSING for b in auto_backends
|
||||||
|
)
|
||||||
|
if not all_auto_missing:
|
||||||
|
# An auto-chain backend is configured but broken — do NOT normalize.
|
||||||
|
# Keep the original error and its repair guidance.
|
||||||
|
return record
|
||||||
|
grok_finding = next(
|
||||||
|
(b for b in backends_list if b.get("name") == "grok"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if grok_finding and grok_finding.get("status") in (
|
||||||
|
health.OK,
|
||||||
|
health.DEGRADED,
|
||||||
|
health.ERROR,
|
||||||
|
):
|
||||||
|
record["status"] = "unconfigured"
|
||||||
|
record["tier"] = TIER_OFF
|
||||||
|
if grok_finding.get("status") == health.ERROR:
|
||||||
|
record["note"] = (
|
||||||
|
"X unconfigured; grok CLI store is broken but unused (opt-in only) — "
|
||||||
|
"pin LAST30DAYS_X_BACKEND=grok to enable, then fix the store"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
record["note"] = (
|
||||||
|
"X unconfigured; grok CLI available but opt-in only — "
|
||||||
|
"pin LAST30DAYS_X_BACKEND=grok to enable"
|
||||||
|
)
|
||||||
|
record["fix"] = ""
|
||||||
|
return record
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -751,12 +751,18 @@ def extract_browser_credentials(config: dict[str, Any]) -> dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def get_x_source_with_method(config: dict[str, Any]) -> tuple[str | None, str]:
|
def get_x_source_with_method(config: dict[str, Any]) -> tuple[str | None, str]:
|
||||||
"""Return (source, method) for X search, where method describes the auth origin."""
|
"""Return (source, method) for X search, where method describes the auth origin.
|
||||||
if config.get("XAI_API_KEY"):
|
|
||||||
return "xai", "xai"
|
Order mirrors _X_BACKEND_ORDER: bird first (cookies beat XAI_API_KEY when
|
||||||
|
both are present), then xai, then xurl. Grok is opt-in only and is never
|
||||||
|
auto-selected here.
|
||||||
|
"""
|
||||||
|
# Bird first: cookies beat XAI_API_KEY when both are present.
|
||||||
if config.get("AUTH_TOKEN") and config.get("CT0"):
|
if config.get("AUTH_TOKEN") and config.get("CT0"):
|
||||||
method = config.get("_AUTH_TOKEN_SOURCE", "env")
|
method = config.get("_AUTH_TOKEN_SOURCE", "env")
|
||||||
return "bird", method
|
return "bird", method
|
||||||
|
if config.get("XAI_API_KEY"):
|
||||||
|
return "xai", "xai"
|
||||||
# Fall back to xurl CLI (official X API v2, OAuth2, free developer app)
|
# Fall back to xurl CLI (official X API v2, OAuth2, free developer app)
|
||||||
from . import xurl_x
|
from . import xurl_x
|
||||||
if xurl_x.is_available():
|
if xurl_x.is_available():
|
||||||
@@ -789,17 +795,27 @@ def get_reddit_source(config: dict[str, Any]) -> str | None:
|
|||||||
# source; the rest are ordered failover backups, tried only if the one before
|
# source; the rest are ordered failover backups, tried only if the one before
|
||||||
# returns nothing or errors. There is one X source ("x"); these are its
|
# returns nothing or errors. There is one X source ("x"); these are its
|
||||||
# interchangeable backends, never run in parallel.
|
# interchangeable backends, never run in parallel.
|
||||||
# xai — xAI/Grok live search (XAI_API_KEY)
|
|
||||||
# bird — X GraphQL scrape via the user's browser cookies (AUTH_TOKEN/CT0)
|
# bird — X GraphQL scrape via the user's browser cookies (AUTH_TOKEN/CT0)
|
||||||
|
# xai — xAI/Grok live search (XAI_API_KEY)
|
||||||
# xurl — official X API v2 (xurl CLI, OAuth2)
|
# xurl — official X API v2 (xurl CLI, OAuth2)
|
||||||
# xquik — key-based REST X search (XQUIK_API_KEY); keyless of browser cookies
|
# xquik — key-based REST X search (XQUIK_API_KEY)
|
||||||
_X_BACKEND_ORDER = ("xai", "grok", "bird", "xurl", "xquik")
|
_X_BACKEND_ORDER = ("bird", "xai", "xurl", "xquik")
|
||||||
|
|
||||||
|
# Opt-in backends: never in the unpinned auto chain; require explicit pin.
|
||||||
|
# grok is here because a leftover ~/.grok/auth.json must never steal the X
|
||||||
|
# lane. Pin LAST30DAYS_X_BACKEND=grok to enable it.
|
||||||
|
_X_BACKEND_OPT_IN = ("grok",)
|
||||||
|
|
||||||
|
# All known backends (auto chain + opt-in): valid values for the pin var.
|
||||||
|
_X_BACKEND_KNOWN = _X_BACKEND_ORDER + _X_BACKEND_OPT_IN
|
||||||
|
|
||||||
# Public routing definitions for the doctor/backend-descriptor layer
|
# Public routing definitions for the doctor/backend-descriptor layer
|
||||||
# (lib/backends.py). These are aliases for knowledge this module already
|
# (lib/backends.py). These are aliases for knowledge this module already
|
||||||
# owns — the declared X chain order and the pin/floor env var names — so
|
# owns — the declared X chain order and the pin/floor env var names — so
|
||||||
# descriptors import one source of truth instead of restating it.
|
# descriptors import one source of truth instead of restating it.
|
||||||
X_BACKEND_ORDER = _X_BACKEND_ORDER
|
X_BACKEND_ORDER = _X_BACKEND_ORDER
|
||||||
|
X_BACKEND_OPT_IN = _X_BACKEND_OPT_IN
|
||||||
|
X_BACKEND_KNOWN = _X_BACKEND_KNOWN
|
||||||
X_BACKEND_PIN_VAR = 'LAST30DAYS_X_BACKEND'
|
X_BACKEND_PIN_VAR = 'LAST30DAYS_X_BACKEND'
|
||||||
REDDIT_BACKEND_PIN_VAR = 'LAST30DAYS_REDDIT_BACKEND'
|
REDDIT_BACKEND_PIN_VAR = 'LAST30DAYS_REDDIT_BACKEND'
|
||||||
REDDIT_SC_MIN_ITEMS_VAR = 'LAST30DAYS_REDDIT_SC_MIN_ITEMS'
|
REDDIT_SC_MIN_ITEMS_VAR = 'LAST30DAYS_REDDIT_SC_MIN_ITEMS'
|
||||||
@@ -842,9 +858,14 @@ def x_backend_chain(config: dict[str, Any], local_only: bool = False) -> list[st
|
|||||||
exactly one X source — these are its backends, never fetched in parallel.
|
exactly one X source — these are its backends, never fetched in parallel.
|
||||||
|
|
||||||
A ``LAST30DAYS_X_BACKEND`` pin forces a single backend (no failover): the
|
A ``LAST30DAYS_X_BACKEND`` pin forces a single backend (no failover): the
|
||||||
user explicitly chose it. Browser-cookie probing is intentionally avoided
|
user explicitly chose it. Valid pin values are in ``_X_BACKEND_KNOWN``
|
||||||
(automatic Keychain access causes popups); bird counts as available only
|
(the auto chain plus opt-in backends like grok). Browser-cookie probing
|
||||||
when AUTH_TOKEN and CT0 are present explicitly.
|
is intentionally avoided (automatic Keychain access causes popups); bird
|
||||||
|
counts as available only when AUTH_TOKEN and CT0 are present explicitly.
|
||||||
|
|
||||||
|
Unpinned runs walk only ``_X_BACKEND_ORDER``: opt-in backends like grok
|
||||||
|
are never auto-selected. A leftover ~/.grok/auth.json must not steal the
|
||||||
|
X lane; pin ``LAST30DAYS_X_BACKEND=grok`` to enable it explicitly.
|
||||||
|
|
||||||
``local_only=True`` is the doctor/safe-diagnose flavor: availability is
|
``local_only=True`` is the doctor/safe-diagnose flavor: availability is
|
||||||
answered from local evidence only (no subprocess spawns that reach the
|
answered from local evidence only (no subprocess spawns that reach the
|
||||||
@@ -857,11 +878,14 @@ def x_backend_chain(config: dict[str, Any], local_only: bool = False) -> list[st
|
|||||||
bird_x.set_credentials(config.get('AUTH_TOKEN'), config.get('CT0'))
|
bird_x.set_credentials(config.get('AUTH_TOKEN'), config.get('CT0'))
|
||||||
|
|
||||||
preferred = (config.get(X_BACKEND_PIN_VAR) or '').lower()
|
preferred = (config.get(X_BACKEND_PIN_VAR) or '').lower()
|
||||||
if preferred in _X_BACKEND_ORDER:
|
# Pin accepted from _X_BACKEND_KNOWN (auto chain + opt-in like grok).
|
||||||
|
if preferred in _X_BACKEND_KNOWN:
|
||||||
if _x_backend_available(preferred, config, has_bird_creds, local_only):
|
if _x_backend_available(preferred, config, has_bird_creds, local_only):
|
||||||
return [preferred]
|
return [preferred]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Unpinned: walk only _X_BACKEND_ORDER (bird -> xai -> xurl -> xquik).
|
||||||
|
# Opt-in backends like grok are never auto-selected.
|
||||||
return [
|
return [
|
||||||
b for b in _X_BACKEND_ORDER
|
b for b in _X_BACKEND_ORDER
|
||||||
if _x_backend_available(b, config, has_bird_creds, local_only)
|
if _x_backend_available(b, config, has_bird_creds, local_only)
|
||||||
@@ -1252,27 +1276,43 @@ def get_x_source_status(config: dict[str, Any], probe: bool = False) -> dict[str
|
|||||||
|
|
||||||
# Grok availability is filesystem-only on both paths (PATH lookup plus the
|
# Grok availability is filesystem-only on both paths (PATH lookup plus the
|
||||||
# credential store), so it is safe to compute here regardless of `probe`.
|
# credential store), so it is safe to compute here regardless of `probe`.
|
||||||
|
# Grok is opt-in only: it appears in grok_available but never wins the
|
||||||
|
# unpinned source selection.
|
||||||
from . import grok_x as _grok_x
|
from . import grok_x as _grok_x
|
||||||
grok_available = _grok_x.has_stored_auth()
|
grok_available = _grok_x.has_stored_auth()
|
||||||
|
|
||||||
# Determine active source. bird (browser cookies) and xAI win when present;
|
# Determine active source. A pin forces a single backend (R4): ANY known
|
||||||
# when neither is available, xquik is the active X source. A probe that
|
# pin is exclusive, mirroring x_backend_chain's [] semantics. Pinned
|
||||||
# clearly failed (False) means xquik is not actually usable.
|
# backend available → that source. Pinned backend unavailable → None.
|
||||||
if xai_available:
|
# Otherwise, order mirrors _X_BACKEND_ORDER: bird first (cookies beat
|
||||||
source = 'xai'
|
# XAI_API_KEY when both are present), then xai, then xurl, then xquik.
|
||||||
elif grok_available:
|
# Grok is opt-in only and never auto-selected; a leftover ~/.grok/auth.json
|
||||||
# Ahead of bird per the chain order: grok needs no X credential at all,
|
# must not steal the X lane.
|
||||||
# while a cookie session is one expiry away from silently degrading.
|
pin = (config.get(X_BACKEND_PIN_VAR) or '').lower()
|
||||||
source = 'grok'
|
if pin and pin in _X_BACKEND_KNOWN:
|
||||||
elif bird_status["authenticated"]:
|
# Pin is exclusive: pinned backend if available, else None (no fallback).
|
||||||
source = 'bird'
|
if pin == 'bird':
|
||||||
else:
|
source = 'bird' if bird_status["authenticated"] else None
|
||||||
if xurl_available:
|
elif pin == 'xai':
|
||||||
source = 'xurl'
|
source = 'xai' if xai_available else None
|
||||||
elif xquik_available and xquik_working is not False:
|
elif pin == 'xurl':
|
||||||
source = 'xquik'
|
source = 'xurl' if xurl_available else None
|
||||||
|
elif pin == 'xquik':
|
||||||
|
source = 'xquik' if (xquik_available and xquik_working is not False) else None
|
||||||
|
elif pin == 'grok':
|
||||||
|
source = 'grok' if grok_available else None
|
||||||
else:
|
else:
|
||||||
source = None
|
source = None
|
||||||
|
elif bird_status["authenticated"]:
|
||||||
|
source = 'bird'
|
||||||
|
elif xai_available:
|
||||||
|
source = 'xai'
|
||||||
|
elif xurl_available:
|
||||||
|
source = 'xurl'
|
||||||
|
elif xquik_available and xquik_working is not False:
|
||||||
|
source = 'xquik'
|
||||||
|
else:
|
||||||
|
source = None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"source": source,
|
"source": source,
|
||||||
|
|||||||
@@ -315,9 +315,12 @@ def resolve_runtime(config: dict[str, Any], depth: str) -> tuple[schema.Provider
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_x_backend(config: dict[str, Any]) -> str | None:
|
def _resolve_x_backend(config: dict[str, Any]) -> str | None:
|
||||||
preferred = (config.get(env.X_BACKEND_PIN_VAR) or "").lower()
|
"""Resolve the X backend for runtime fetch.
|
||||||
if preferred in env.X_BACKEND_ORDER:
|
|
||||||
return preferred
|
Delegates to env.get_x_source which handles:
|
||||||
|
- Any known pin (X_BACKEND_KNOWN) exclusively: returns pin if available, None otherwise
|
||||||
|
- Unpinned: walks auto-chain (X_BACKEND_ORDER) only, never auto-selects opt-in backends
|
||||||
|
"""
|
||||||
return env.get_x_source(config)
|
return env.get_x_source(config)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -153,8 +153,21 @@ class TestDescriptorRegistry:
|
|||||||
def test_x_chain_comes_from_env_definitions(self):
|
def test_x_chain_comes_from_env_definitions(self):
|
||||||
d = backends.get_descriptor("x")
|
d = backends.get_descriptor("x")
|
||||||
assert d.mode == backends.MODE_ALTERNATIVE
|
assert d.mode == backends.MODE_ALTERNATIVE
|
||||||
assert tuple(s.name for s in d.backends) == env.X_BACKEND_ORDER
|
# Auto chain order: bird first, grok excluded (opt-in only).
|
||||||
assert env.X_BACKEND_ORDER == ("xai", "grok", "bird", "xurl", "xquik")
|
assert env.X_BACKEND_ORDER == ("bird", "xai", "xurl", "xquik")
|
||||||
|
# Grok is opt-in only, not in the auto chain.
|
||||||
|
assert env.X_BACKEND_OPT_IN == ("grok",)
|
||||||
|
# All known backends (auto + opt-in) for pin validation.
|
||||||
|
assert env.X_BACKEND_KNOWN == ("bird", "xai", "xurl", "xquik", "grok")
|
||||||
|
# Descriptor includes all backends (auto + opt-in) for doctor visibility.
|
||||||
|
assert tuple(s.name for s in d.backends) == env.X_BACKEND_ORDER + env.X_BACKEND_OPT_IN
|
||||||
|
# Grok is marked opt-in in the descriptor.
|
||||||
|
grok_spec = next(s for s in d.backends if s.name == "grok")
|
||||||
|
assert grok_spec.opt_in is True
|
||||||
|
# Auto chain backends are NOT marked opt-in.
|
||||||
|
for name in env.X_BACKEND_ORDER:
|
||||||
|
spec = next(s for s in d.backends if s.name == name)
|
||||||
|
assert spec.opt_in is False
|
||||||
assert d.pin_var == env.X_BACKEND_PIN_VAR == "LAST30DAYS_X_BACKEND"
|
assert d.pin_var == env.X_BACKEND_PIN_VAR == "LAST30DAYS_X_BACKEND"
|
||||||
|
|
||||||
def test_env_exposes_reddit_pin_constants(self):
|
def test_env_exposes_reddit_pin_constants(self):
|
||||||
@@ -197,11 +210,49 @@ class TestXPrediction:
|
|||||||
assert res.active_backend == "bird"
|
assert res.active_backend == "bird"
|
||||||
assert res.tier == backends.TIER_OK
|
assert res.tier == backends.TIER_OK
|
||||||
assert res.pinned is False
|
assert res.pinned is False
|
||||||
# Chain rendered in declared order regardless of availability.
|
# Chain includes all backends (auto + opt-in) for doctor visibility.
|
||||||
assert res.chain == list(env.X_BACKEND_ORDER)
|
expected_chain = list(env.X_BACKEND_ORDER + env.X_BACKEND_OPT_IN)
|
||||||
assert [f.name for f in res.findings] == list(env.X_BACKEND_ORDER)
|
assert res.chain == expected_chain
|
||||||
|
assert [f.name for f in res.findings] == expected_chain
|
||||||
assert "will use: bird" in res.summary
|
assert "will use: bird" in res.summary
|
||||||
|
|
||||||
|
def test_bird_predicted_even_when_xai_key_present(self):
|
||||||
|
"""Cookies beat XAI_API_KEY when both are present (bird-first chain)."""
|
||||||
|
config = {"AUTH_TOKEN": "dummy-token", "CT0": "dummy-ct0", "XAI_API_KEY": "dummy-key"}
|
||||||
|
res = _resolve_x(config, bird_installed=True)
|
||||||
|
assert res.active_backend == "bird"
|
||||||
|
assert res.tier == backends.TIER_OK
|
||||||
|
assert "will use: bird" in res.summary
|
||||||
|
|
||||||
|
def test_grok_is_never_auto_selected_unpinned(self):
|
||||||
|
"""Grok is opt-in only: even if grok is the only configured backend, X is unconfigured unpinned."""
|
||||||
|
config = {}
|
||||||
|
res = _resolve_x(config, grok_installed=True, grok_authed=True)
|
||||||
|
# Grok is available but opt-in - should NOT be auto-selected.
|
||||||
|
grok = next(f for f in res.findings if f.name == "grok")
|
||||||
|
assert grok.status == health.OK
|
||||||
|
# But it should not be the active backend.
|
||||||
|
assert res.active_backend is None
|
||||||
|
assert res.tier == backends.TIER_ERROR
|
||||||
|
|
||||||
|
def test_grok_selected_when_pinned(self):
|
||||||
|
"""Pin grok to enable it explicitly."""
|
||||||
|
config = {"LAST30DAYS_X_BACKEND": "grok"}
|
||||||
|
res = _resolve_x(config, grok_installed=True, grok_authed=True)
|
||||||
|
assert res.active_backend == "grok"
|
||||||
|
assert res.pinned is True
|
||||||
|
assert res.pin == "grok"
|
||||||
|
assert res.tier == backends.TIER_OK
|
||||||
|
|
||||||
|
def test_grok_pin_with_no_store_is_error(self):
|
||||||
|
"""Pin grok without a valid store -> error with grok login prescription."""
|
||||||
|
config = {"LAST30DAYS_X_BACKEND": "grok"}
|
||||||
|
res = _resolve_x(config, grok_installed=True, grok_authed=False)
|
||||||
|
assert res.active_backend is None
|
||||||
|
assert res.pinned is True
|
||||||
|
assert res.tier == backends.TIER_ERROR
|
||||||
|
assert "grok login" in res.prescription.lower()
|
||||||
|
|
||||||
# Scenario 2: pin var set to a later backend -> honored + marked pinned.
|
# Scenario 2: pin var set to a later backend -> honored + marked pinned.
|
||||||
def test_pin_to_later_backend_honored_and_marked(self):
|
def test_pin_to_later_backend_honored_and_marked(self):
|
||||||
config = {
|
config = {
|
||||||
@@ -233,7 +284,9 @@ class TestXPrediction:
|
|||||||
res = _resolve_x({})
|
res = _resolve_x({})
|
||||||
assert res.active_backend is None
|
assert res.active_backend is None
|
||||||
assert res.tier == backends.TIER_ERROR
|
assert res.tier == backends.TIER_ERROR
|
||||||
assert "XAI_API_KEY" in res.prescription
|
# bird (cookies) is first in the chain, so the prescription is about
|
||||||
|
# browser cookies, not XAI_API_KEY.
|
||||||
|
assert "browser-cookie" in res.prescription or "cookies" in res.prescription.lower()
|
||||||
|
|
||||||
def test_pinned_but_unusable_backend_is_error_with_its_prescription(self):
|
def test_pinned_but_unusable_backend_is_error_with_its_prescription(self):
|
||||||
# Pin bird without cookies: env.x_backend_chain returns [] (pipeline
|
# Pin bird without cookies: env.x_backend_chain returns [] (pipeline
|
||||||
@@ -286,21 +339,32 @@ class TestXPrediction:
|
|||||||
|
|
||||||
def test_grok_expired_is_degraded_not_ok(self):
|
def test_grok_expired_is_degraded_not_ok(self):
|
||||||
"""Expired grok session -> DEGRADED tier (warn), not OK."""
|
"""Expired grok session -> DEGRADED tier (warn), not OK."""
|
||||||
res = _resolve_x({}, grok_installed=True, grok_expired=True)
|
# Grok is opt-in: needs explicit pin to be selected.
|
||||||
|
config = {"LAST30DAYS_X_BACKEND": "grok"}
|
||||||
|
res = _resolve_x(config, grok_installed=True, grok_expired=True)
|
||||||
grok = next(f for f in res.findings if f.name == "grok")
|
grok = next(f for f in res.findings if f.name == "grok")
|
||||||
assert grok.status == health.DEGRADED
|
assert grok.status == health.DEGRADED
|
||||||
assert grok.usable # DEGRADED is still usable (refresh may work)
|
assert grok.usable # DEGRADED is still usable (refresh may work)
|
||||||
assert "expired" in grok.detail.lower()
|
assert "expired" in grok.detail.lower()
|
||||||
assert "grok login" in grok.prescription.lower()
|
assert "grok login" in grok.prescription.lower()
|
||||||
# The chain resolution should still pick grok (degraded is usable).
|
# With pin, grok is selected (degraded is usable).
|
||||||
assert res.active_backend == "grok"
|
assert res.active_backend == "grok"
|
||||||
assert res.tier == backends.TIER_WARN
|
assert res.tier == backends.TIER_WARN
|
||||||
|
|
||||||
|
def test_grok_expired_unpinned_not_selected(self):
|
||||||
|
"""Expired grok without pin: X unconfigured, grok not auto-selected."""
|
||||||
|
res = _resolve_x({}, grok_installed=True, grok_expired=True)
|
||||||
|
grok = next(f for f in res.findings if f.name == "grok")
|
||||||
|
assert grok.status == health.DEGRADED
|
||||||
|
# Grok is opt-in, so even though it's usable (degraded), it's not selected.
|
||||||
|
assert res.active_backend is None
|
||||||
|
assert res.tier == backends.TIER_ERROR
|
||||||
|
|
||||||
def test_grok_expired_with_fallback_picks_fallback(self):
|
def test_grok_expired_with_fallback_picks_fallback(self):
|
||||||
"""When grok is expired AND a better backend is OK, pick the OK one."""
|
"""When grok is expired AND a better auto-chain backend is OK, pick the OK one."""
|
||||||
config = {"AUTH_TOKEN": "dummy-token", "CT0": "dummy-ct0"}
|
config = {"AUTH_TOKEN": "dummy-token", "CT0": "dummy-ct0"}
|
||||||
res = _resolve_x(config, grok_installed=True, grok_expired=True, bird_installed=True)
|
res = _resolve_x(config, grok_installed=True, grok_expired=True, bird_installed=True)
|
||||||
# Bird is OK, grok is DEGRADED. OK wins over DEGRADED.
|
# Bird is OK and in the auto chain. Grok is not considered (opt-in).
|
||||||
assert res.active_backend == "bird"
|
assert res.active_backend == "bird"
|
||||||
assert res.tier == backends.TIER_OK
|
assert res.tier == backends.TIER_OK
|
||||||
|
|
||||||
@@ -313,14 +377,17 @@ class TestXPrediction:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Grok session expiry: three states
|
# Grok session expiry: three states (grok is opt-in only)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class TestGrokExpiryStates:
|
class TestGrokExpiryStates:
|
||||||
"""Test the three grok auth states from the plan:
|
"""Test the three grok auth states from the plan:
|
||||||
1. No grok CLI -> silent fallback
|
1. No grok CLI -> silent fallback (opt-in only)
|
||||||
2. CLI installed, never logged in -> silent fallback
|
2. CLI installed, never logged in -> silent fallback (opt-in only)
|
||||||
3. CLI installed, WAS logged in, session dead -> DEGRADED with expiry info
|
3. CLI installed, WAS logged in, session dead -> DEGRADED with expiry info (opt-in only)
|
||||||
|
|
||||||
|
Note: Grok is opt-in only. These tests verify the finding status, but grok
|
||||||
|
is never auto-selected unpinned.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_no_grok_cli_is_missing(self):
|
def test_no_grok_cli_is_missing(self):
|
||||||
@@ -329,6 +396,9 @@ class TestGrokExpiryStates:
|
|||||||
grok = next(f for f in res.findings if f.name == "grok")
|
grok = next(f for f in res.findings if f.name == "grok")
|
||||||
assert grok.status == health.MISSING
|
assert grok.status == health.MISSING
|
||||||
assert "not found on PATH" in grok.detail
|
assert "not found on PATH" in grok.detail
|
||||||
|
# Grok is opt-in, so even MISSING doesn't affect the resolution.
|
||||||
|
# X is unconfigured (no auto-chain backends available).
|
||||||
|
assert res.active_backend is None
|
||||||
|
|
||||||
def test_grok_installed_never_logged_in_is_missing(self):
|
def test_grok_installed_never_logged_in_is_missing(self):
|
||||||
"""CLI installed but never logged in -> MISSING with login hint."""
|
"""CLI installed but never logged in -> MISSING with login hint."""
|
||||||
@@ -337,6 +407,8 @@ class TestGrokExpiryStates:
|
|||||||
assert grok.status == health.MISSING
|
assert grok.status == health.MISSING
|
||||||
assert "not signed in" in grok.detail
|
assert "not signed in" in grok.detail
|
||||||
assert "grok login" in grok.prescription
|
assert "grok login" in grok.prescription
|
||||||
|
# Grok is opt-in, so X is unconfigured.
|
||||||
|
assert res.active_backend is None
|
||||||
|
|
||||||
def test_grok_session_expired_is_degraded_with_expiry(self):
|
def test_grok_session_expired_is_degraded_with_expiry(self):
|
||||||
"""Session expired -> DEGRADED with timestamp and refresh hint."""
|
"""Session expired -> DEGRADED with timestamp and refresh hint."""
|
||||||
@@ -346,12 +418,16 @@ class TestGrokExpiryStates:
|
|||||||
assert "expired" in grok.detail.lower()
|
assert "expired" in grok.detail.lower()
|
||||||
# The detail should include the expiry timestamp and hint
|
# The detail should include the expiry timestamp and hint
|
||||||
assert "refresh" in grok.detail.lower() or "login" in grok.prescription.lower()
|
assert "refresh" in grok.detail.lower() or "login" in grok.prescription.lower()
|
||||||
|
# Grok is opt-in, so X is unconfigured even with degraded grok.
|
||||||
|
assert res.active_backend is None
|
||||||
|
|
||||||
def test_grok_healthy_session_is_ok(self):
|
def test_grok_healthy_session_is_ok(self):
|
||||||
"""Non-expired credentials -> OK."""
|
"""Non-expired credentials -> OK, but still opt-in only."""
|
||||||
res = _resolve_x({}, grok_installed=True, grok_authed=True)
|
res = _resolve_x({}, grok_installed=True, grok_authed=True)
|
||||||
grok = next(f for f in res.findings if f.name == "grok")
|
grok = next(f for f in res.findings if f.name == "grok")
|
||||||
assert grok.status == health.OK
|
assert grok.status == health.OK
|
||||||
|
# Grok is opt-in, so X is unconfigured unpinned.
|
||||||
|
assert res.active_backend is None
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -605,6 +681,233 @@ class TestXParityWithPipeline:
|
|||||||
def test_parity_nothing_configured(self):
|
def test_parity_nothing_configured(self):
|
||||||
self._assert_parity({})
|
self._assert_parity({})
|
||||||
|
|
||||||
|
def test_parity_grok_only_unpinned_is_unconfigured(self):
|
||||||
|
"""Grok-only with no pin: X unconfigured (parity with env.x_backend_chain)."""
|
||||||
|
self._assert_parity({}, grok_installed=True, grok_authed=True)
|
||||||
|
|
||||||
|
def test_parity_grok_pinned(self):
|
||||||
|
"""Grok pinned: grok is selected (parity with env.x_backend_chain)."""
|
||||||
|
self._assert_parity(
|
||||||
|
{"LAST30DAYS_X_BACKEND": "grok"},
|
||||||
|
grok_installed=True,
|
||||||
|
grok_authed=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_parity_cookies_beat_xai_key(self):
|
||||||
|
"""Cookies beat XAI_API_KEY when both present (bird-first chain)."""
|
||||||
|
self._assert_parity(
|
||||||
|
{"AUTH_TOKEN": "dummy-token", "CT0": "dummy-ct0", "XAI_API_KEY": "dummy-key"},
|
||||||
|
bird_installed=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# get_x_source_status pin semantics (R4): grok pin forces grok source
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestGetXSourceStatusGrokPin:
|
||||||
|
"""get_x_source_status must respect LAST30DAYS_X_BACKEND=grok pin."""
|
||||||
|
|
||||||
|
def test_pin_grok_with_store_returns_grok_source(self):
|
||||||
|
"""Pin grok + valid store -> get_x_source_status source is 'grok'."""
|
||||||
|
config = {"LAST30DAYS_X_BACKEND": "grok"}
|
||||||
|
bird_status = {
|
||||||
|
"installed": False,
|
||||||
|
"authenticated": False,
|
||||||
|
"username": "",
|
||||||
|
"can_install": False,
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
mock.patch("lib.grok_x.has_stored_auth", return_value=True),
|
||||||
|
mock.patch("lib.bird_x.get_bird_status", return_value=bird_status),
|
||||||
|
):
|
||||||
|
status = env.get_x_source_status(config, probe=False)
|
||||||
|
assert status["source"] == "grok"
|
||||||
|
assert status["grok_available"] is True
|
||||||
|
|
||||||
|
def test_unpinned_with_store_does_not_return_grok_source(self):
|
||||||
|
"""Unpinned + valid grok store -> source is NOT 'grok' (opt-in only)."""
|
||||||
|
config = {} # No pin
|
||||||
|
bird_status = {
|
||||||
|
"installed": False,
|
||||||
|
"authenticated": False,
|
||||||
|
"username": "",
|
||||||
|
"can_install": False,
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
mock.patch("lib.grok_x.has_stored_auth", return_value=True),
|
||||||
|
mock.patch("lib.bird_x.get_bird_status", return_value=bird_status),
|
||||||
|
):
|
||||||
|
status = env.get_x_source_status(config, probe=False)
|
||||||
|
# Grok is available but NOT the source (opt-in only)
|
||||||
|
assert status["source"] != "grok"
|
||||||
|
assert status["source"] is None # No other backend configured
|
||||||
|
assert status["grok_available"] is True
|
||||||
|
|
||||||
|
def test_pin_grok_with_cookies_still_returns_grok(self):
|
||||||
|
"""Pin grok with cookies present -> grok (pin forces single backend)."""
|
||||||
|
config = {
|
||||||
|
"LAST30DAYS_X_BACKEND": "grok",
|
||||||
|
"AUTH_TOKEN": "dummy-token",
|
||||||
|
"CT0": "dummy-ct0",
|
||||||
|
}
|
||||||
|
bird_status = {
|
||||||
|
"installed": True,
|
||||||
|
"authenticated": True,
|
||||||
|
"username": "test",
|
||||||
|
"can_install": True,
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
mock.patch("lib.grok_x.has_stored_auth", return_value=True),
|
||||||
|
mock.patch("lib.bird_x.get_bird_status", return_value=bird_status),
|
||||||
|
):
|
||||||
|
status = env.get_x_source_status(config, probe=False)
|
||||||
|
# Pin forces grok even when bird is available
|
||||||
|
assert status["source"] == "grok"
|
||||||
|
|
||||||
|
def test_pin_grok_no_store_with_other_creds_returns_none(self):
|
||||||
|
"""Pin grok + no store + other creds present -> source is None (exclusive pin)."""
|
||||||
|
config = {
|
||||||
|
"LAST30DAYS_X_BACKEND": "grok",
|
||||||
|
"AUTH_TOKEN": "dummy-token",
|
||||||
|
"CT0": "dummy-ct0",
|
||||||
|
"XAI_API_KEY": "dummy-key",
|
||||||
|
}
|
||||||
|
bird_status = {
|
||||||
|
"installed": True,
|
||||||
|
"authenticated": True,
|
||||||
|
"username": "test",
|
||||||
|
"can_install": True,
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
mock.patch("lib.grok_x.has_stored_auth", return_value=False),
|
||||||
|
mock.patch("lib.bird_x.get_bird_status", return_value=bird_status),
|
||||||
|
):
|
||||||
|
status = env.get_x_source_status(config, probe=False)
|
||||||
|
# Pin is exclusive: grok unavailable -> None, NOT fallback to bird/xai
|
||||||
|
assert status["source"] is None
|
||||||
|
assert status["grok_available"] is False
|
||||||
|
# Other backends ARE available, but pin blocks fallback
|
||||||
|
assert status["bird_authenticated"] is True
|
||||||
|
assert status["xai_available"] is True
|
||||||
|
|
||||||
|
def test_pin_xai_with_cookies_returns_xai(self):
|
||||||
|
"""Pin xai + cookies + XAI_API_KEY -> source is xai, not bird."""
|
||||||
|
config = {
|
||||||
|
"LAST30DAYS_X_BACKEND": "xai",
|
||||||
|
"AUTH_TOKEN": "dummy-token",
|
||||||
|
"CT0": "dummy-ct0",
|
||||||
|
"XAI_API_KEY": "dummy-key",
|
||||||
|
}
|
||||||
|
bird_status = {
|
||||||
|
"installed": True,
|
||||||
|
"authenticated": True,
|
||||||
|
"username": "test",
|
||||||
|
"can_install": True,
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
mock.patch("lib.grok_x.has_stored_auth", return_value=False),
|
||||||
|
mock.patch("lib.bird_x.get_bird_status", return_value=bird_status),
|
||||||
|
):
|
||||||
|
status = env.get_x_source_status(config, probe=False)
|
||||||
|
# Pin is exclusive: xai pinned + available -> xai (not bird)
|
||||||
|
assert status["source"] == "xai"
|
||||||
|
# Bird is also available, but pin forces xai
|
||||||
|
assert status["bird_authenticated"] is True
|
||||||
|
|
||||||
|
def test_pin_xai_no_key_with_cookies_returns_none(self):
|
||||||
|
"""Pin xai + no XAI_API_KEY + cookies -> source is None (exclusive pin)."""
|
||||||
|
config = {
|
||||||
|
"LAST30DAYS_X_BACKEND": "xai",
|
||||||
|
"AUTH_TOKEN": "dummy-token",
|
||||||
|
"CT0": "dummy-ct0",
|
||||||
|
# No XAI_API_KEY
|
||||||
|
}
|
||||||
|
bird_status = {
|
||||||
|
"installed": True,
|
||||||
|
"authenticated": True,
|
||||||
|
"username": "test",
|
||||||
|
"can_install": True,
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
mock.patch("lib.grok_x.has_stored_auth", return_value=False),
|
||||||
|
mock.patch("lib.bird_x.get_bird_status", return_value=bird_status),
|
||||||
|
):
|
||||||
|
status = env.get_x_source_status(config, probe=False)
|
||||||
|
# Pin is exclusive: xai pinned but unavailable -> None (no fallback)
|
||||||
|
assert status["source"] is None
|
||||||
|
assert status["xai_available"] is False
|
||||||
|
# Bird is available but pin blocks fallback
|
||||||
|
assert status["bird_authenticated"] is True
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Runtime X backend pin (x_backend_chain / _resolve_x_backend)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestRuntimeXBackendPin:
|
||||||
|
"""Runtime fetch path must honor any known pin exclusively, including grok."""
|
||||||
|
|
||||||
|
def test_pin_grok_with_store_and_cookies_returns_grok(self):
|
||||||
|
"""Pin grok + grok store + cookies -> runtime returns grok, not bird."""
|
||||||
|
from lib import grok_x, providers
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"LAST30DAYS_X_BACKEND": "grok",
|
||||||
|
"AUTH_TOKEN": "dummy-token",
|
||||||
|
"CT0": "dummy-ct0",
|
||||||
|
"XAI_API_KEY": "dummy-key",
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
mock.patch.object(grok_x, "has_stored_auth", return_value=True),
|
||||||
|
mock.patch("lib.bird_x.is_bird_installed", return_value=True),
|
||||||
|
):
|
||||||
|
# x_backend_chain is the authoritative runtime path
|
||||||
|
chain = env.x_backend_chain(config)
|
||||||
|
# _resolve_x_backend delegates to get_x_source (wraps x_backend_chain)
|
||||||
|
resolved = providers._resolve_x_backend(config)
|
||||||
|
# Pin grok + available -> grok (not bird/xai)
|
||||||
|
assert chain == ["grok"]
|
||||||
|
assert resolved == "grok"
|
||||||
|
|
||||||
|
def test_pin_grok_no_store_with_cookies_returns_none(self):
|
||||||
|
"""Pin grok + no store + cookies -> runtime returns None (exclusive pin)."""
|
||||||
|
from lib import grok_x, providers
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"LAST30DAYS_X_BACKEND": "grok",
|
||||||
|
"AUTH_TOKEN": "dummy-token",
|
||||||
|
"CT0": "dummy-ct0",
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
mock.patch.object(grok_x, "has_stored_auth", return_value=False),
|
||||||
|
mock.patch("lib.bird_x.is_bird_installed", return_value=True),
|
||||||
|
):
|
||||||
|
chain = env.x_backend_chain(config)
|
||||||
|
resolved = providers._resolve_x_backend(config)
|
||||||
|
# Pin grok + unavailable -> [] / None (no fallthrough to bird)
|
||||||
|
assert chain == []
|
||||||
|
assert resolved is None
|
||||||
|
|
||||||
|
def test_unpinned_with_grok_store_and_cookies_returns_bird(self):
|
||||||
|
"""Unpinned + grok store + cookies -> runtime returns bird, never grok."""
|
||||||
|
from lib import grok_x, providers
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"AUTH_TOKEN": "dummy-token",
|
||||||
|
"CT0": "dummy-ct0",
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
mock.patch.object(grok_x, "has_stored_auth", return_value=True),
|
||||||
|
mock.patch("lib.bird_x.is_bird_installed", return_value=True),
|
||||||
|
):
|
||||||
|
chain = env.x_backend_chain(config)
|
||||||
|
resolved = providers._resolve_x_backend(config)
|
||||||
|
# Unpinned -> auto-chain (bird first), grok never auto-selected
|
||||||
|
assert chain[0] == "bird"
|
||||||
|
assert "grok" not in chain
|
||||||
|
assert resolved == "bird"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# YouTube chain: yt-dlp -> ScrapeCreators
|
# YouTube chain: yt-dlp -> ScrapeCreators
|
||||||
|
|||||||
+291
-42
@@ -1,28 +1,37 @@
|
|||||||
"""grok must be visible to a user, not just functional.
|
"""grok must be visible as an opt-in backup, not as a default.
|
||||||
|
|
||||||
A backend nobody is told about is a backend nobody uses, and the whole point
|
Grok is demoted to opt-in only: a leftover ~/.grok/auth.json must never steal
|
||||||
of this path is removing a credential wall the user currently hits.
|
the X lane. The default auto chain is bird → xai → xurl → xquik. Pin
|
||||||
|
LAST30DAYS_X_BACKEND=grok to enable grok explicitly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from lib import doctor, quality_nudge
|
from lib import backends, doctor, health, quality_nudge
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
def test_doctor_reports_x_covered_by_a_signed_in_grok():
|
def test_doctor_does_not_auto_select_grok_unpinned():
|
||||||
|
"""Doctor must NOT report 'will use: grok' for unpinned runs.
|
||||||
|
Grok is opt-in only; a leftover auth.json must never steal the X lane."""
|
||||||
src = inspect.getsource(doctor._x_record)
|
src = inspect.getsource(doctor._x_record)
|
||||||
assert "grok_x.has_stored_auth()" in src
|
# The old code would check grok_x.has_stored_auth() and set status="ok"
|
||||||
assert "will use: grok" in src
|
# with record["will_use"]="grok" when grok was available unpinned. That
|
||||||
|
# promotion block is removed: no "has_stored_auth()" call that sets
|
||||||
|
# will_use to grok for unpinned runs.
|
||||||
|
#
|
||||||
|
# Comments may mention "will use: grok" to explain what we DON'T do, so
|
||||||
|
# check for the old logic pattern: has_stored_auth -> grok promotion.
|
||||||
|
assert "has_stored_auth" not in src
|
||||||
|
|
||||||
|
|
||||||
def test_doctor_prefers_grok_over_the_unverified_cookie_note():
|
def test_doctor_mentions_grok_as_opt_in():
|
||||||
"""grok outranks bird in the chain, and unlike a cookie session its
|
"""Doctor comments explain that grok is opt-in only."""
|
||||||
availability is locally verifiable rather than 'not verified until a run'."""
|
|
||||||
src = inspect.getsource(doctor._x_record)
|
src = inspect.getsource(doctor._x_record)
|
||||||
assert src.index("has_stored_auth") < src.index("x_pending_browser_auth")
|
assert "opt-in" in src.lower()
|
||||||
|
|
||||||
|
|
||||||
def test_quality_nudge_offers_grok_but_does_not_call_it_free():
|
def test_quality_nudge_offers_grok_but_does_not_call_it_free():
|
||||||
@@ -34,33 +43,41 @@ def test_quality_nudge_offers_grok_but_does_not_call_it_free():
|
|||||||
assert "if you have a Grok" in src
|
assert "if you have a Grok" in src
|
||||||
|
|
||||||
|
|
||||||
def test_configuration_documents_the_grok_path():
|
def test_configuration_documents_the_grok_path_as_opt_in():
|
||||||
text = (REPO / "CONFIGURATION.md").read_text()
|
text = (REPO / "CONFIGURATION.md").read_text()
|
||||||
assert "X with no X credential (Grok CLI)" in text
|
assert "Grok CLI (opt-in backup)" in text
|
||||||
assert "grok login" in text
|
assert "grok login" in text
|
||||||
assert "LAST30DAYS_X_BACKEND=bird" in text, (
|
# Document that grok requires a pin.
|
||||||
"users need the documented escape hatch back to the cookie path"
|
assert "LAST30DAYS_X_BACKEND=grok" in text
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_configuration_pin_row_lists_grok():
|
def test_configuration_pin_row_lists_grok_last():
|
||||||
|
"""Pin row shows all backends with grok last (opt-in)."""
|
||||||
text = (REPO / "CONFIGURATION.md").read_text()
|
text = (REPO / "CONFIGURATION.md").read_text()
|
||||||
assert "`xai` / `grok` / `bird` / `xurl` / `xquik`" in text
|
# New order: bird first, grok last (opt-in).
|
||||||
|
assert "`bird` / `xai` / `xurl` / `xquik` / `grok`" in text
|
||||||
|
|
||||||
|
|
||||||
def test_configuration_does_not_claim_grok_is_free():
|
def test_configuration_does_not_claim_grok_is_free():
|
||||||
text = (REPO / "CONFIGURATION.md").read_text()
|
text = (REPO / "CONFIGURATION.md").read_text()
|
||||||
section = text[text.index("X with no X credential (Grok CLI)"):][:1200]
|
section = text[text.index("Grok CLI (opt-in backup)"):][:1200]
|
||||||
assert "draws on your Grok plan" in section or "draw on your Grok plan" in section
|
assert "draws on your Grok plan" in section or "draw on your Grok plan" in section
|
||||||
|
|
||||||
|
|
||||||
|
def test_configuration_documents_bird_first_chain():
|
||||||
|
"""Auto chain is bird first: cookies beat XAI_API_KEY."""
|
||||||
|
text = (REPO / "CONFIGURATION.md").read_text()
|
||||||
|
assert "bird first" in text.lower() or "bird (browser cookies) → xai" in text.lower()
|
||||||
|
|
||||||
|
|
||||||
def test_changelog_fragments_exist_and_changelog_is_untouched():
|
def test_changelog_fragments_exist_and_changelog_is_untouched():
|
||||||
frags = list((REPO / "changelog.d").glob("*grok*")) + \
|
frags = list((REPO / "changelog.d").glob("*grok*")) + \
|
||||||
list((REPO / "changelog.d").glob("*first-party*"))
|
list((REPO / "changelog.d").glob("*bird*"))
|
||||||
if not frags:
|
if not frags:
|
||||||
# Release PRs consume fragments into CHANGELOG.md via towncrier.
|
# Release PRs consume fragments into CHANGELOG.md via towncrier.
|
||||||
changelog = (REPO / "CHANGELOG.md").read_text()
|
changelog = (REPO / "CHANGELOG.md").read_text()
|
||||||
assert "X search now works with no X credential" in changelog
|
# Old text or new text about X backend changes.
|
||||||
|
assert "X search now works with no X credential" in changelog or "bird first" in changelog.lower()
|
||||||
return
|
return
|
||||||
assert frags, "feature PRs add a changelog.d fragment"
|
assert frags, "feature PRs add a changelog.d fragment"
|
||||||
|
|
||||||
@@ -71,34 +88,266 @@ def _skill_md():
|
|||||||
return (REPO / "skills" / "last30days" / "SKILL.md").read_text()
|
return (REPO / "skills" / "last30days" / "SKILL.md").read_text()
|
||||||
|
|
||||||
|
|
||||||
def test_all_unlock_surfaces_mention_the_grok_path():
|
def test_skill_md_does_not_check_grok_first():
|
||||||
|
"""Grok is opt-in only: SKILL.md should NOT check for grok before cookies."""
|
||||||
text = _skill_md()
|
text = _skill_md()
|
||||||
for marker in (
|
# The old "Check for a Grok path before asking for cookies" should be removed.
|
||||||
"Just-in-time X unlock",
|
assert "Check for a Grok path before asking for cookies" not in text
|
||||||
"Check for a Grok path before asking for cookies",
|
|
||||||
"Grok CLI (no X credential)",
|
|
||||||
):
|
|
||||||
assert marker in text, f"missing grok surfacing at: {marker}"
|
|
||||||
|
|
||||||
|
|
||||||
def test_grok_ordering_is_conditional_on_a_detected_path():
|
def test_skill_md_presents_grok_as_opt_in_backup():
|
||||||
"""A user with no Grok account must not have their actionable options
|
"""SKILL.md presents grok as an opt-in backup, not a primary option."""
|
||||||
pushed down the list by one they cannot take."""
|
text = _skill_md()
|
||||||
|
assert "Grok CLI is an opt-in backup" in text
|
||||||
|
# Should mention the pin requirement.
|
||||||
|
assert "LAST30DAYS_X_BACKEND=grok" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_md_just_in_time_unlock_defaults():
|
||||||
|
"""Just-in-time X unlock presents cookies and keys first."""
|
||||||
text = _skill_md()
|
text = _skill_md()
|
||||||
section = text[text.index("Just-in-time X unlock"):][:3000]
|
section = text[text.index("Just-in-time X unlock"):][:3000]
|
||||||
assert "command -v grok" in section
|
# Default options should be cookies and keys, not grok.
|
||||||
assert "Options when `grok` IS on PATH" in section
|
assert "Scan my browser cookies" in section
|
||||||
assert "Options when `grok` is NOT on PATH" in section
|
assert "xAI API key" in section
|
||||||
|
|
||||||
|
|
||||||
def test_no_grok_branch_keeps_todays_options_unchanged():
|
|
||||||
text = _skill_md()
|
|
||||||
section = text[text.index("Options when `grok` is NOT on PATH"):][:900]
|
|
||||||
for option in ("Scan my browser cookies", "AUTH_TOKEN and CT0", "xAI API key", "Skip for now"):
|
|
||||||
assert option in section
|
|
||||||
|
|
||||||
|
|
||||||
def test_skill_md_does_not_call_the_grok_path_free():
|
def test_skill_md_does_not_call_the_grok_path_free():
|
||||||
text = _skill_md()
|
text = _skill_md()
|
||||||
section = text[text.index("Just-in-time X unlock"):][:3000]
|
section = text[text.index("Just-in-time X unlock"):][:3000]
|
||||||
assert "Do not describe the Grok path as free" in section
|
assert "Do not describe the Grok path as free" in section or "Do not call it free" in section
|
||||||
|
|
||||||
|
|
||||||
|
# --- Doctor grok-only unpinned behavior (R3/R8) -----------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_doctor_grok_only_unpinned_is_not_tier_error():
|
||||||
|
"""Unpinned grok-only is unconfigured (tier off), NOT broken (tier error).
|
||||||
|
|
||||||
|
R3: unpinned grok is 'available, unused — pin LAST30DAYS_X_BACKEND=grok'
|
||||||
|
R8: grok-only unpinned = X unconfigured / skipped, not auth-failed or broken
|
||||||
|
"""
|
||||||
|
from lib import grok_x
|
||||||
|
|
||||||
|
config = {} # No pin, no auto-chain credentials
|
||||||
|
bird_status = {
|
||||||
|
"installed": False,
|
||||||
|
"authenticated": False,
|
||||||
|
"username": "",
|
||||||
|
"can_install": False,
|
||||||
|
}
|
||||||
|
# Grok is the only backend with OK status; all auto-chain backends are MISSING.
|
||||||
|
with (
|
||||||
|
mock.patch.object(grok_x, "binary_path", return_value="/usr/bin/grok"),
|
||||||
|
mock.patch.object(grok_x, "has_stored_auth", return_value=True),
|
||||||
|
mock.patch.object(grok_x, "stored_auth_status", return_value=(grok_x.AUTH_OK, "", None)),
|
||||||
|
mock.patch("lib.backends.which", return_value="/usr/bin/grok"),
|
||||||
|
mock.patch("lib.bird_x.get_bird_status", return_value=bird_status),
|
||||||
|
mock.patch("lib.bird_x.is_bird_installed", return_value=False),
|
||||||
|
mock.patch("lib.xurl_x.has_stored_auth", return_value=False),
|
||||||
|
):
|
||||||
|
record = doctor._x_record(config)
|
||||||
|
# Must NOT be tier error / NOT WORKING.
|
||||||
|
assert record["tier"] != "error", "grok-only unpinned must not be tier error"
|
||||||
|
# Should be unconfigured (tier off).
|
||||||
|
assert record["status"] == "unconfigured"
|
||||||
|
assert record["tier"] == "off"
|
||||||
|
# Note should mention grok is available but requires a pin.
|
||||||
|
assert "grok" in record["note"].lower()
|
||||||
|
assert "pin" in record["note"].lower() or "LAST30DAYS_X_BACKEND" in record["note"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_doctor_grok_error_unpinned_is_not_tier_error():
|
||||||
|
"""Unpinned grok ERROR (broken store) is unconfigured, NOT tier error.
|
||||||
|
|
||||||
|
When grok's auth store is unreadable/corrupt, doctor should NOT report X
|
||||||
|
as NOT WORKING with a `grok login` prescription. An unused opt-in backend
|
||||||
|
with a broken store is still unused — tier off, not tier error.
|
||||||
|
"""
|
||||||
|
from lib import grok_x
|
||||||
|
|
||||||
|
config = {} # No pin, no auto-chain credentials
|
||||||
|
bird_status = {
|
||||||
|
"installed": False,
|
||||||
|
"authenticated": False,
|
||||||
|
"username": "",
|
||||||
|
"can_install": False,
|
||||||
|
}
|
||||||
|
# Grok has ERROR status (unreadable store); all auto-chain backends MISSING.
|
||||||
|
with (
|
||||||
|
mock.patch.object(grok_x, "binary_path", return_value="/usr/bin/grok"),
|
||||||
|
mock.patch.object(grok_x, "has_stored_auth", return_value=False),
|
||||||
|
mock.patch.object(
|
||||||
|
grok_x, "stored_auth_status",
|
||||||
|
return_value=(grok_x.AUTH_ERROR, "store unreadable", None),
|
||||||
|
),
|
||||||
|
mock.patch("lib.backends.which", return_value="/usr/bin/grok"),
|
||||||
|
mock.patch("lib.bird_x.get_bird_status", return_value=bird_status),
|
||||||
|
mock.patch("lib.bird_x.is_bird_installed", return_value=False),
|
||||||
|
mock.patch("lib.xurl_x.has_stored_auth", return_value=False),
|
||||||
|
):
|
||||||
|
record = doctor._x_record(config)
|
||||||
|
# Must NOT be tier error / NOT WORKING.
|
||||||
|
assert record["tier"] != "error", "grok ERROR unpinned must not be tier error"
|
||||||
|
# Should be unconfigured (tier off).
|
||||||
|
assert record["status"] == "unconfigured"
|
||||||
|
assert record["tier"] == "off"
|
||||||
|
# Note should mention grok is unused/opt-in; should NOT prescribe grok login.
|
||||||
|
assert "grok" in record["note"].lower()
|
||||||
|
assert "opt-in" in record["note"].lower() or "unused" in record["note"].lower()
|
||||||
|
# Fix should be empty (no grok login prescription for unused opt-in).
|
||||||
|
assert record["fix"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_doctor_grok_store_with_pending_bird_predicts_bird():
|
||||||
|
"""Unpinned + grok store + pending browser auth -> doctor predicts bird.
|
||||||
|
|
||||||
|
When bird is installed and browser-cookie extraction is configured,
|
||||||
|
doctor should predict bird (pending-cookie usable), NOT report X as
|
||||||
|
unconfigured due to an unused grok store. Pending bird takes precedence.
|
||||||
|
"""
|
||||||
|
from lib import env, grok_x
|
||||||
|
|
||||||
|
config = {} # No pin, no static AUTH_TOKEN/CT0
|
||||||
|
bird_status = {
|
||||||
|
"installed": True, # Bird IS installed
|
||||||
|
"authenticated": False, # No static cookies in config
|
||||||
|
"username": "",
|
||||||
|
"can_install": True,
|
||||||
|
}
|
||||||
|
# Grok has OK status; bird is pending (installed, FROM_BROWSER configured).
|
||||||
|
with (
|
||||||
|
mock.patch.object(grok_x, "binary_path", return_value="/usr/bin/grok"),
|
||||||
|
mock.patch.object(grok_x, "has_stored_auth", return_value=True),
|
||||||
|
mock.patch.object(grok_x, "stored_auth_status", return_value=(grok_x.AUTH_OK, "", None)),
|
||||||
|
mock.patch("lib.backends.which", return_value="/usr/bin/grok"),
|
||||||
|
mock.patch("lib.bird_x.get_bird_status", return_value=bird_status),
|
||||||
|
mock.patch("lib.bird_x.is_bird_installed", return_value=True),
|
||||||
|
mock.patch("lib.xurl_x.has_stored_auth", return_value=False),
|
||||||
|
# Simulate x_pending_browser_auth returning True (bird pending)
|
||||||
|
mock.patch.object(env, "x_pending_browser_auth", return_value=True),
|
||||||
|
):
|
||||||
|
record = doctor._x_record(config)
|
||||||
|
# Doctor should predict bird (pending), NOT report X as unconfigured.
|
||||||
|
assert record["tier"] != "off", "pending bird should not be tier off"
|
||||||
|
assert record["status"] != "unconfigured", "pending bird should not be unconfigured"
|
||||||
|
# Should be OK tier with bird prediction.
|
||||||
|
assert record["tier"] == "ok"
|
||||||
|
assert record["status"] == health.OK
|
||||||
|
assert "bird" in record["note"].lower()
|
||||||
|
assert "browser cookies" in record["note"].lower() or "cookie" in record["note"].lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_doctor_grok_does_not_hide_xurl_error():
|
||||||
|
"""Unpinned + grok store + xurl ERROR -> doctor keeps xurl error, not unconfigured.
|
||||||
|
|
||||||
|
When an auto-chain backend (xurl) is configured but broken, doctor must
|
||||||
|
report that error with its repair guidance. Unused grok must NOT swallow
|
||||||
|
the genuine auto-chain failure.
|
||||||
|
"""
|
||||||
|
from lib import backends as _backends
|
||||||
|
from lib import grok_x
|
||||||
|
|
||||||
|
config = {} # No pin
|
||||||
|
bird_status = {
|
||||||
|
"installed": False,
|
||||||
|
"authenticated": False,
|
||||||
|
"username": "",
|
||||||
|
"can_install": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock xurl probe to return ERROR status
|
||||||
|
def mock_probe_xurl(config):
|
||||||
|
return _backends.BackendFinding(
|
||||||
|
name="xurl",
|
||||||
|
status=health.ERROR,
|
||||||
|
detail="store unreadable",
|
||||||
|
prescription="xurl auth oauth2 login",
|
||||||
|
requires="xurl CLI installed + OAuth2 login",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Intercept _run_probe to inject xurl ERROR
|
||||||
|
original_run_probe = _backends._run_probe
|
||||||
|
|
||||||
|
def patched_run_probe(spec, config):
|
||||||
|
if spec.name == "xurl":
|
||||||
|
return mock_probe_xurl(config)
|
||||||
|
return original_run_probe(spec, config)
|
||||||
|
|
||||||
|
# Grok has OK status; xurl has ERROR (unreadable store).
|
||||||
|
# All other auto-chain backends are MISSING.
|
||||||
|
with (
|
||||||
|
mock.patch.object(grok_x, "binary_path", return_value="/usr/bin/grok"),
|
||||||
|
mock.patch.object(grok_x, "has_stored_auth", return_value=True),
|
||||||
|
mock.patch.object(grok_x, "stored_auth_status", return_value=(grok_x.AUTH_OK, "", None)),
|
||||||
|
mock.patch("lib.backends.which", side_effect=lambda cmd: "/usr/bin/grok" if cmd == "grok" else None),
|
||||||
|
mock.patch("lib.bird_x.get_bird_status", return_value=bird_status),
|
||||||
|
mock.patch("lib.bird_x.is_bird_installed", return_value=False),
|
||||||
|
mock.patch.object(_backends, "_run_probe", patched_run_probe),
|
||||||
|
):
|
||||||
|
record = doctor._x_record(config)
|
||||||
|
|
||||||
|
# Doctor should NOT report X as unconfigured.
|
||||||
|
assert record["tier"] != "off", "xurl error must not be hidden by unused grok"
|
||||||
|
assert record["status"] != "unconfigured", "xurl error must not become unconfigured"
|
||||||
|
# Should keep the error tier and xurl repair guidance.
|
||||||
|
assert record["tier"] == "error"
|
||||||
|
# The fix should contain xurl repair guidance, not be empty.
|
||||||
|
assert record["fix"], "xurl repair guidance must not be cleared"
|
||||||
|
assert "xurl" in record["fix"].lower() or "oauth" in record["fix"].lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_doctor_pending_bird_does_not_hide_xurl_error():
|
||||||
|
"""Unpinned + FROM_BROWSER + bird installed + xurl ERROR -> keeps xurl error.
|
||||||
|
|
||||||
|
Pending bird must NOT replace a record with a configured auto-chain backend
|
||||||
|
in ERROR. Doctor should report the xurl error with its repair guidance,
|
||||||
|
NOT report X as OK via pending bird.
|
||||||
|
"""
|
||||||
|
from lib import backends as _backends
|
||||||
|
from lib import env, grok_x
|
||||||
|
|
||||||
|
config = {} # No pin, no static AUTH_TOKEN/CT0
|
||||||
|
bird_status = {
|
||||||
|
"installed": True, # Bird IS installed (for pending bird)
|
||||||
|
"authenticated": False, # No static cookies
|
||||||
|
"username": "",
|
||||||
|
"can_install": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock xurl probe to return ERROR status
|
||||||
|
def mock_probe_xurl(config):
|
||||||
|
return _backends.BackendFinding(
|
||||||
|
name="xurl",
|
||||||
|
status=health.ERROR,
|
||||||
|
detail="store unreadable",
|
||||||
|
prescription="xurl auth oauth2 login",
|
||||||
|
requires="xurl CLI installed + OAuth2 login",
|
||||||
|
)
|
||||||
|
|
||||||
|
original_run_probe = _backends._run_probe
|
||||||
|
|
||||||
|
def patched_run_probe(spec, config):
|
||||||
|
if spec.name == "xurl":
|
||||||
|
return mock_probe_xurl(config)
|
||||||
|
return original_run_probe(spec, config)
|
||||||
|
|
||||||
|
# x_pending_browser_auth returns True (bird pending), but xurl has ERROR.
|
||||||
|
with (
|
||||||
|
mock.patch.object(grok_x, "binary_path", return_value=None),
|
||||||
|
mock.patch.object(grok_x, "has_stored_auth", return_value=False),
|
||||||
|
mock.patch("lib.backends.which", return_value=None),
|
||||||
|
mock.patch("lib.bird_x.get_bird_status", return_value=bird_status),
|
||||||
|
mock.patch("lib.bird_x.is_bird_installed", return_value=True),
|
||||||
|
mock.patch.object(env, "x_pending_browser_auth", return_value=True),
|
||||||
|
mock.patch.object(_backends, "_run_probe", patched_run_probe),
|
||||||
|
):
|
||||||
|
record = doctor._x_record(config)
|
||||||
|
|
||||||
|
# Doctor should NOT report X as OK via pending bird.
|
||||||
|
assert record["tier"] != "ok", "xurl error must not be hidden by pending bird"
|
||||||
|
assert record["status"] != health.OK, "xurl error must not become OK"
|
||||||
|
# Should keep the error tier and xurl repair guidance.
|
||||||
|
assert record["tier"] == "error"
|
||||||
|
assert record["fix"], "xurl repair guidance must not be cleared"
|
||||||
|
assert "xurl" in record["fix"].lower() or "oauth" in record["fix"].lower()
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""grok must never be a dead end: every failure falls through to the next backend.
|
"""grok must never be a dead end: every failure falls through to the next backend.
|
||||||
|
|
||||||
grok sits ahead of bird in the chain, so any way it can come back empty has to
|
Grok is opt-in only (not in the auto chain), but when pinned via
|
||||||
hand off rather than leave the run with no X coverage.
|
LAST30DAYS_X_BACKEND=grok, any way grok can come back empty or error must
|
||||||
|
surface properly so the run reports the failure honestly rather than silently
|
||||||
|
leaving X uncovered.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|||||||
Reference in New Issue
Block a user