diff --git a/agent-team/agent_team/api.py b/agent-team/agent_team/api.py index 8ba955c..730d0c8 100644 --- a/agent-team/agent_team/api.py +++ b/agent-team/agent_team/api.py @@ -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, diff --git a/agent-team/agent_team/invoker_multi.py b/agent-team/agent_team/invoker_multi.py index a34ffd8..384c085 100644 --- a/agent-team/agent_team/invoker_multi.py +++ b/agent-team/agent_team/invoker_multi.py @@ -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]() diff --git a/agent-team/agent_team/nodes/builders_llm.py b/agent-team/agent_team/nodes/builders_llm.py index 5440e11..7f3ad78 100644 --- a/agent-team/agent_team/nodes/builders_llm.py +++ b/agent-team/agent_team/nodes/builders_llm.py @@ -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 diff --git a/agent-team/tests/test_builders_llm.py b/agent-team/tests/test_builders_llm.py index cacfc46..535fd5d 100644 --- a/agent-team/tests/test_builders_llm.py +++ b/agent-team/tests/test_builders_llm.py @@ -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 diff --git a/agent-team/tests/test_ws1_invoker_multi_api.py b/agent-team/tests/test_ws1_invoker_multi_api.py index f6c7786..775c869 100644 --- a/agent-team/tests/test_ws1_invoker_multi_api.py +++ b/agent-team/tests/test_ws1_invoker_multi_api.py @@ -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