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:
parent
e5a8949d0d
commit
366d07a84e
5 changed files with 41 additions and 8 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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]()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Reference in a new issue