mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-04-28 03:20:01 +00:00
## Summary - add an opt-in local `smoke/` pytest suite for API, auth, providers, CLI, IDE-shaped requests, messaging, voice, tools, and thinking stream contracts - keep smoke tests out of normal CI collection with `testpaths = ["tests"]` - write sanitized smoke artifacts under `.smoke-results/` ## Verification - `uv run ruff format` - `uv run ruff check` - `uv run ty check` - `uv run ty check smoke` - `FCC_LIVE_SMOKE=1 FCC_SMOKE_TARGETS=all FCC_SMOKE_RUN_VOICE=1 uv run pytest smoke -n 0 -m live -s --tb=short` -> 17 passed, 9 skipped - `uv run pytest` -> 904 passed ## Notes - Skipped live checks require local credentials/tools/services, such as provider models, Telegram/Discord targets, voice backend, or Claude CLI. - `claude-pick` smoke was intentionally removed.
126 lines
3.2 KiB
Python
126 lines
3.2 KiB
Python
"""Subprocess lifecycle helpers for local smoke servers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import time
|
|
from collections.abc import Iterator
|
|
from contextlib import contextmanager, suppress
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
|
|
from .config import SmokeConfig, redacted
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class RunningServer:
|
|
base_url: str
|
|
port: int
|
|
log_path: Path
|
|
process: subprocess.Popen[bytes]
|
|
|
|
|
|
def find_free_port() -> int:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.bind(("127.0.0.1", 0))
|
|
return int(sock.getsockname()[1])
|
|
|
|
|
|
@contextmanager
|
|
def start_server(
|
|
config: SmokeConfig,
|
|
*,
|
|
env_overrides: dict[str, str] | None = None,
|
|
command: list[str] | None = None,
|
|
name: str = "server",
|
|
) -> Iterator[RunningServer]:
|
|
port = find_free_port()
|
|
config.results_dir.mkdir(parents=True, exist_ok=True)
|
|
log_path = config.results_dir / f"{name}-{config.worker_id}-{port}.log"
|
|
|
|
env = os.environ.copy()
|
|
env.update(
|
|
{
|
|
"HOST": "127.0.0.1",
|
|
"PORT": str(port),
|
|
"LOG_FILE": str(log_path),
|
|
"MESSAGING_PLATFORM": "none",
|
|
"PYTHONUNBUFFERED": "1",
|
|
}
|
|
)
|
|
if env_overrides:
|
|
env.update(env_overrides)
|
|
|
|
cmd = command or [
|
|
"uv",
|
|
"run",
|
|
"uvicorn",
|
|
"server:app",
|
|
"--host",
|
|
"127.0.0.1",
|
|
"--port",
|
|
str(port),
|
|
"--timeout-graceful-shutdown",
|
|
"5",
|
|
]
|
|
|
|
with log_path.open("ab") as log_file:
|
|
process = subprocess.Popen(
|
|
cmd,
|
|
cwd=config.root,
|
|
env=env,
|
|
stdout=log_file,
|
|
stderr=subprocess.STDOUT,
|
|
)
|
|
running = RunningServer(
|
|
base_url=f"http://127.0.0.1:{port}",
|
|
port=port,
|
|
log_path=log_path,
|
|
process=process,
|
|
)
|
|
try:
|
|
_wait_for_health(running, timeout_s=config.timeout_s)
|
|
yield running
|
|
finally:
|
|
_stop_process(process)
|
|
|
|
|
|
def _wait_for_health(server: RunningServer, *, timeout_s: float) -> None:
|
|
deadline = time.monotonic() + timeout_s
|
|
last_error = ""
|
|
while time.monotonic() < deadline:
|
|
if server.process.poll() is not None:
|
|
break
|
|
try:
|
|
response = httpx.get(f"{server.base_url}/health", timeout=2.0)
|
|
if response.status_code == 200:
|
|
return
|
|
last_error = f"HTTP {response.status_code}: {response.text[:200]}"
|
|
except Exception as exc:
|
|
last_error = f"{type(exc).__name__}: {exc}"
|
|
time.sleep(0.25)
|
|
|
|
log_excerpt = ""
|
|
with suppress(OSError):
|
|
log_excerpt = server.log_path.read_text(encoding="utf-8", errors="replace")[
|
|
-2000:
|
|
]
|
|
raise AssertionError(
|
|
"Smoke server did not become healthy. "
|
|
f"last_error={last_error!r} log={redacted(log_excerpt)!r}"
|
|
)
|
|
|
|
|
|
def _stop_process(process: subprocess.Popen[bytes]) -> None:
|
|
if process.poll() is not None:
|
|
return
|
|
process.terminate()
|
|
try:
|
|
process.wait(timeout=8)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait(timeout=5)
|