fix(ws1): skip fastapi TestClient tests when fastapi absent + ruff format

CI has no fastapi (box-only dependency); api.py imports it lazily. Guard the
7 TestClient smoke tests with skipif(find_spec('fastapi') is None) so they
skip in CI instead of failing collection, leaving the 14 invoker_multi tests
running. Also apply ruff format to the 5 WS1 files CI flagged.
This commit is contained in:
Adam Moussa 2026-06-23 11:40:46 -04:00
parent e5a8949d0d
commit 366d07a84e
5 changed files with 41 additions and 8 deletions

View file

@ -63,6 +63,7 @@ _DEFAULT_PORT = 8765
# Pydantic request/response models (module-level so FastAPI resolves them). # Pydantic request/response models (module-level so FastAPI resolves them).
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TaskRequest(BaseModel): class TaskRequest(BaseModel):
task: str task: str
transport: str = "claude_code" transport: str = "claude_code"
@ -89,6 +90,7 @@ class OrchestratorInvokeResponse(BaseModel):
# Token helper. # Token helper.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _get_token() -> str: def _get_token() -> str:
"""Read the API bearer token from the environment. """Read the API bearer token from the environment.
@ -139,6 +141,7 @@ def _build_coordinator(db_path: str | Path | None = None) -> Any:
# App factory. # App factory.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def make_app( def make_app(
*, *,
coordinator: Any = None, coordinator: Any = None,
@ -179,7 +182,9 @@ def make_app(
) -> None: ) -> None:
expected = _get_token() expected = _get_token()
if creds is None or not hmac.compare_digest(creds.credentials, expected): if creds is None or not hmac.compare_digest(creds.credentials, expected):
raise HTTPException(status_code=401, detail="invalid or missing bearer token") raise HTTPException(
status_code=401, detail="invalid or missing bearer token"
)
# Coordinator is built lazily and cached on first use so test clients that # Coordinator is built lazily and cached on first use so test clients that
# inject a pre-built coordinator never trigger the DB path. # inject a pre-built coordinator never trigger the DB path.
@ -194,7 +199,9 @@ def make_app(
_state["ready"] = True _state["ready"] = True
return coord return coord
@app.post("/tasks", response_model=TaskResponse, dependencies=[Depends(_check_token)]) @app.post(
"/tasks", response_model=TaskResponse, dependencies=[Depends(_check_token)]
)
def create_task(request: TaskRequest) -> TaskResponse: def create_task(request: TaskRequest) -> TaskResponse:
"""Intake a new agent-team task and run it to the first human gate.""" """Intake a new agent-team task and run it to the first human gate."""
coord = _get_coordinator() coord = _get_coordinator()
@ -233,7 +240,9 @@ def make_app(
response_model=OrchestratorInvokeResponse, response_model=OrchestratorInvokeResponse,
dependencies=[Depends(_check_token)], dependencies=[Depends(_check_token)],
) )
def orchestrator_invoke(request: OrchestratorInvokeRequest) -> OrchestratorInvokeResponse: def orchestrator_invoke(
request: OrchestratorInvokeRequest,
) -> OrchestratorInvokeResponse:
"""Route a one-shot prompt through the root orchestrator run.py. """Route a one-shot prompt through the root orchestrator run.py.
Shells out to the orchestrator root's ``run.py`` with the prompt text. Shells out to the orchestrator root's ``run.py`` with the prompt text.
@ -272,6 +281,7 @@ def make_app(
# Attended deploy helper (do NOT call in tests or CI). # Attended deploy helper (do NOT call in tests or CI).
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def serve( def serve(
*, *,
host: str = _DEFAULT_HOST, host: str = _DEFAULT_HOST,

View file

@ -96,8 +96,7 @@ def _load_model(key: str) -> Any:
} }
if key not in factories: if key not in factories:
raise ValueError( raise ValueError(
f"unknown model key {key!r}; must be one of: " f"unknown model key {key!r}; must be one of: " + ", ".join(sorted(MODELS))
+ ", ".join(sorted(MODELS))
) )
return factories[key]() return factories[key]()

View file

@ -177,7 +177,9 @@ def make_fast_coder_invoker() -> "BuildCallable":
return _invoke return _invoke
def subprocess_build(instruction: str, *, route: _OrchestratorRoute | None = None) -> str: def subprocess_build(
instruction: str, *, route: _OrchestratorRoute | None = None
) -> str:
"""Opt-in fallback :data:`BuildCallable`: shell out to ``run.py fast_coder``. """Opt-in fallback :data:`BuildCallable`: shell out to ``run.py fast_coder``.
The original subprocess-based build path, retained as an opt-in alternative The original subprocess-based build path, retained as an opt-in alternative

View file

@ -337,6 +337,7 @@ def test_default_build_is_the_injection_default() -> None:
src = inspect.getsource(default_build) src = inspect.getsource(default_build)
assert "make_fast_coder_invoker" in src assert "make_fast_coder_invoker" in src
from agent_team.nodes.builders_llm import subprocess_build from agent_team.nodes.builders_llm import subprocess_build
subprocess_src = inspect.getsource(subprocess_build) subprocess_src = inspect.getsource(subprocess_build)
assert "subprocess.run" in subprocess_src assert "subprocess.run" in subprocess_src

View file

@ -12,6 +12,7 @@ Covers:
from __future__ import annotations from __future__ import annotations
import importlib.util
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -180,7 +181,9 @@ def test_review_loop_llm_default_is_cross_reviewer_invoker() -> None:
saved = sys.modules.pop("models", None) saved = sys.modules.pop("models", None)
try: try:
with patch.dict(sys.modules, {"models": None}): # type: ignore[dict-item] with patch.dict(sys.modules, {"models": None}): # type: ignore[dict-item]
with pytest.raises((RuntimeError, AttributeError, TypeError, ModuleNotFoundError)): with pytest.raises(
(RuntimeError, AttributeError, TypeError, ModuleNotFoundError)
):
fn("any prompt") fn("any prompt")
finally: finally:
if saved is not None: if saved is not None:
@ -232,6 +235,15 @@ def test_builders_llm_default_build_uses_in_process() -> None:
# api.py: bearer-token auth + endpoint smoke tests via FastAPI TestClient # api.py: bearer-token auth + endpoint smoke tests via FastAPI TestClient
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# fastapi is a box-only dependency (installed on the R720 during attended
# deploy, never in CI or the root requirements). api.py imports it lazily, so
# these TestClient-based smoke tests are the only thing that hard-requires it.
# Skip them when it is absent rather than failing collection in CI.
requires_fastapi = pytest.mark.skipif(
importlib.util.find_spec("fastapi") is None,
reason="fastapi not installed (box-only dependency)",
)
@pytest.fixture() @pytest.fixture()
def api_token(monkeypatch: Any) -> str: def api_token(monkeypatch: Any) -> str:
@ -249,6 +261,7 @@ def _make_stub_coordinator(thread_id: str = "t-test-123") -> MagicMock:
return coord return coord
@requires_fastapi
def test_api_401_without_token(api_token: str) -> None: def test_api_401_without_token(api_token: str) -> None:
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@ -260,6 +273,7 @@ def test_api_401_without_token(api_token: str) -> None:
assert resp.status_code == 401 assert resp.status_code == 401
@requires_fastapi
def test_api_401_wrong_token(api_token: str) -> None: def test_api_401_wrong_token(api_token: str) -> None:
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@ -275,6 +289,7 @@ def test_api_401_wrong_token(api_token: str) -> None:
assert resp.status_code == 401 assert resp.status_code == 401
@requires_fastapi
def test_api_post_tasks_200(api_token: str) -> None: def test_api_post_tasks_200(api_token: str) -> None:
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@ -293,6 +308,7 @@ def test_api_post_tasks_200(api_token: str) -> None:
stub.start_task.assert_called_once() stub.start_task.assert_called_once()
@requires_fastapi
def test_api_get_task_404_no_state(api_token: str) -> None: def test_api_get_task_404_no_state(api_token: str) -> None:
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@ -309,6 +325,7 @@ def test_api_get_task_404_no_state(api_token: str) -> None:
assert resp.status_code == 503 assert resp.status_code == 503
@requires_fastapi
def test_api_get_task_with_graph_state(api_token: str) -> None: def test_api_get_task_with_graph_state(api_token: str) -> None:
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@ -332,7 +349,10 @@ def test_api_get_task_with_graph_state(api_token: str) -> None:
assert data["state"]["task"] == "add login" assert data["state"]["task"] == "add login"
def test_api_orchestrator_invoke_calls_subprocess(api_token: str, tmp_path: Any) -> None: @requires_fastapi
def test_api_orchestrator_invoke_calls_subprocess(
api_token: str, tmp_path: Any
) -> None:
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from agent_team.api import make_app from agent_team.api import make_app
@ -357,6 +377,7 @@ def test_api_orchestrator_invoke_calls_subprocess(api_token: str, tmp_path: Any)
assert "hello world" in resp.json()["text"] assert "hello world" in resp.json()["text"]
@requires_fastapi
def test_api_no_token_env_raises_on_request(monkeypatch: Any) -> None: def test_api_no_token_env_raises_on_request(monkeypatch: Any) -> None:
from fastapi.testclient import TestClient from fastapi.testclient import TestClient