Spaces:
Running
Running
eliezer avihail
Fix smoke test harness: gradio_client token kwarg and empty question (#42)
500fff2 unverified | """Post-deploy smoke test for the Hugging Face Space. | |
| Runs in GitHub Actions (which can reach both the Space and OpenRouter) after | |
| the sync-to-hf workflow finishes β a live health check the local dev sandbox | |
| can't perform because its egress is locked down. Steps: | |
| 1. poll the HF runtime API until the Space finishes (re)building and is RUNNING | |
| 2. ask it a real question through the Gradio API (api_name="/respond") | |
| 3. fail the job if the answer is an LLM/transport error, so a broken deploy | |
| shows up as a red check instead of silently serving errors | |
| Env: | |
| SPACE_ID repo id of the Space (default eliezeravihail/torchdocs-agent) | |
| HF_TOKEN token for the runtime API / private space (optional for public) | |
| SMOKE_QUESTION question to ask (has a sensible default) | |
| BUILD_TIMEOUT seconds to wait for RUNNING (default 900) | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| import time | |
| import requests | |
| SPACE_ID = os.environ.get("SPACE_ID", "eliezeravihail/torchdocs-agent") | |
| HF_TOKEN = os.environ.get("HF_TOKEN") or None | |
| # `or` (not a get default): the workflow always sets SMOKE_QUESTION, but it's | |
| # empty on the workflow_run trigger β an empty string must fall back too | |
| QUESTION = os.environ.get("SMOKE_QUESTION") or "How do I use torch.optim.SGD with momentum?" | |
| BUILD_TIMEOUT = int(os.environ.get("BUILD_TIMEOUT", "900")) | |
| POLL_EVERY = 15 | |
| RUNTIME_URL = f"https://huggingface.co/api/spaces/{SPACE_ID}/runtime" | |
| # markers that mean generation/transport actually broke β hard fail | |
| LLM_ERROR_MARKERS = ( | |
| "went wrong", | |
| "connection error", | |
| "llm call failed", | |
| "all providers failed", | |
| "is not set", | |
| "must be a full http", | |
| ) | |
| # a working app that simply has no index yet β warn, don't fail (different subsystem) | |
| EMPTY_INDEX_MARKER = "could not find anything" | |
| def _headers() -> dict: | |
| return {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {} | |
| def wait_for_running() -> bool: | |
| """Block until the Space reports RUNNING, or a failure/timeout.""" | |
| deadline = time.time() + BUILD_TIMEOUT | |
| # small grace so we poll the *new* build, not the old RUNNING instance that | |
| # is still up for the moment right after the push | |
| time.sleep(30) | |
| last = None | |
| while time.time() < deadline: | |
| try: | |
| resp = requests.get(RUNTIME_URL, headers=_headers(), timeout=30) | |
| stage = resp.json().get("stage") | |
| except Exception as exc: # noqa: BLE001 β transient during rebuild | |
| stage = f"unreachable ({exc})" | |
| if stage != last: | |
| print(f"[smoke] space stage: {stage}", flush=True) | |
| last = stage | |
| if stage == "RUNNING": | |
| return True | |
| if stage in ("BUILD_ERROR", "RUNTIME_ERROR", "CONFIG_ERROR"): | |
| print(f"[smoke] space entered failure stage: {stage}", flush=True) | |
| return False | |
| time.sleep(POLL_EVERY) | |
| print(f"[smoke] timed out after {BUILD_TIMEOUT}s waiting for RUNNING", flush=True) | |
| return False | |
| def ask_space() -> str: | |
| """Call the Space's /respond endpoint and return the answer text.""" | |
| from gradio_client import Client | |
| # the token kwarg was renamed across gradio_client versions (hf_token β token) | |
| # and the Space is public anyway, so try the variants and fall back tokenless | |
| client = None | |
| for kw in ("token", "hf_token"): | |
| try: | |
| client = Client(SPACE_ID, **{kw: HF_TOKEN}) | |
| break | |
| except TypeError: | |
| continue | |
| if client is None: | |
| client = Client(SPACE_ID) | |
| result = client.predict(QUESTION, api_name="/respond") | |
| return result if isinstance(result, str) else str(result) | |
| def main() -> int: | |
| print(f"[smoke] space={SPACE_ID} question={QUESTION!r}", flush=True) | |
| if not wait_for_running(): | |
| return 1 | |
| # the server just came up; give _warm_up (embedding model load) a moment | |
| time.sleep(10) | |
| try: | |
| answer = ask_space() | |
| except Exception as exc: # noqa: BLE001 β the call itself failing is a fail | |
| print(f"[smoke] FAIL: calling the Space raised {type(exc).__name__}: {exc}", flush=True) | |
| return 1 | |
| print("[smoke] ---------------- answer ----------------", flush=True) | |
| print(answer[:2000], flush=True) | |
| print("[smoke] ------------------------------------------", flush=True) | |
| low = answer.lower() | |
| hit = next((m for m in LLM_ERROR_MARKERS if m in low), None) | |
| if hit: | |
| print(f"[smoke] FAIL: answer contains an error marker: {hit!r}", flush=True) | |
| return 1 | |
| if not answer.strip(): | |
| print("[smoke] FAIL: empty answer", flush=True) | |
| return 1 | |
| if EMPTY_INDEX_MARKER in low: | |
| # generation worked; the corpus/index is just empty β surface loudly | |
| # but don't red the deploy, that's a separate (indexing) concern | |
| print("[smoke] WARNING: LLM ok but the docs index looks empty", flush=True) | |
| print("[smoke] PASS: Space answered without an LLM/transport error", flush=True) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |