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).
# ---------------------------------------------------------------------------
class TaskRequest(BaseModel):
task: str
transport: str = "claude_code"
@ -89,6 +90,7 @@ class OrchestratorInvokeResponse(BaseModel):
# Token helper.
# ---------------------------------------------------------------------------
def _get_token() -> str:
"""Read the API bearer token from the environment.
@ -139,6 +141,7 @@ def _build_coordinator(db_path: str | Path | None = None) -> Any:
# App factory.
# ---------------------------------------------------------------------------
def make_app(
*,
coordinator: Any = None,
@ -179,7 +182,9 @@ def make_app(
) -> None:
expected = _get_token()
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
# inject a pre-built coordinator never trigger the DB path.
@ -194,7 +199,9 @@ def make_app(
_state["ready"] = True
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:
"""Intake a new agent-team task and run it to the first human gate."""
coord = _get_coordinator()
@ -233,7 +240,9 @@ def make_app(
response_model=OrchestratorInvokeResponse,
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.
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).
# ---------------------------------------------------------------------------
def serve(
*,
host: str = _DEFAULT_HOST,

View file

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

View file

@ -177,7 +177,9 @@ def make_fast_coder_invoker() -> "BuildCallable":
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``.
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)
assert "make_fast_coder_invoker" in src
from agent_team.nodes.builders_llm import subprocess_build
subprocess_src = inspect.getsource(subprocess_build)
assert "subprocess.run" in subprocess_src

View file

@ -12,6 +12,7 @@ Covers:
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
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)
try:
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")
finally:
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
# ---------------------------------------------------------------------------
# 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()
def api_token(monkeypatch: Any) -> str:
@ -249,6 +261,7 @@ def _make_stub_coordinator(thread_id: str = "t-test-123") -> MagicMock:
return coord
@requires_fastapi
def test_api_401_without_token(api_token: str) -> None:
from fastapi.testclient import TestClient
@ -260,6 +273,7 @@ def test_api_401_without_token(api_token: str) -> None:
assert resp.status_code == 401
@requires_fastapi
def test_api_401_wrong_token(api_token: str) -> None:
from fastapi.testclient import TestClient
@ -275,6 +289,7 @@ def test_api_401_wrong_token(api_token: str) -> None:
assert resp.status_code == 401
@requires_fastapi
def test_api_post_tasks_200(api_token: str) -> None:
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()
@requires_fastapi
def test_api_get_task_404_no_state(api_token: str) -> None:
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
@requires_fastapi
def test_api_get_task_with_graph_state(api_token: str) -> None:
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"
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 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"]
@requires_fastapi
def test_api_no_token_env_raises_on_request(monkeypatch: Any) -> None:
from fastapi.testclient import TestClient