diff --git a/.github/workflows/deploy-mcp.yaml b/.github/workflows/deploy-mcp.yaml index b9570ef2..2d2dc00a 100644 --- a/.github/workflows/deploy-mcp.yaml +++ b/.github/workflows/deploy-mcp.yaml @@ -75,6 +75,8 @@ jobs: run: uv sync --directory everyrow-mcp - name: Run ruff run: uv run --directory everyrow-mcp ruff check + - name: Run basedpyright + run: uv run --directory everyrow-mcp basedpyright --project . - name: Run tests run: uv run --directory everyrow-mcp pytest tests diff --git a/everyrow-mcp/pyproject.toml b/everyrow-mcp/pyproject.toml index 8d37510c..cae06735 100644 --- a/everyrow-mcp/pyproject.toml +++ b/everyrow-mcp/pyproject.toml @@ -36,7 +36,7 @@ dev = [ ] [tool.basedpyright] -venvPath = "." +venvPath = ".." venv = ".venv" include = ["src", "tests"] typeCheckingMode = "standard" diff --git a/everyrow-mcp/src/everyrow_mcp/app.py b/everyrow-mcp/src/everyrow_mcp/app.py index e30e8906..7b83a7f1 100644 --- a/everyrow-mcp/src/everyrow_mcp/app.py +++ b/everyrow-mcp/src/everyrow_mcp/app.py @@ -50,7 +50,7 @@ async def http_lifespan(_server: FastMCP): across sessions. Process exit handles cleanup. """ redis_client = get_redis_client() - await redis_client.ping() + await redis_client.ping() # pyright: ignore[reportGeneralTypeIssues] def _http_client_factory() -> AuthenticatedClient: access_token = get_access_token() @@ -73,7 +73,7 @@ def _http_client_factory() -> AuthenticatedClient: async def no_auth_http_lifespan(_server: FastMCP): """HTTP no-auth mode: singleton client from API key, verify Redis.""" redis_client = get_redis_client() - await redis_client.ping() + await redis_client.ping() # pyright: ignore[reportGeneralTypeIssues] with _create_sdk_client() as client: response = await get_billing(client=client) diff --git a/everyrow-mcp/src/everyrow_mcp/auth.py b/everyrow-mcp/src/everyrow_mcp/auth.py index b94bdf67..6985ea53 100644 --- a/everyrow-mcp/src/everyrow_mcp/auth.py +++ b/everyrow-mcp/src/everyrow_mcp/auth.py @@ -346,6 +346,7 @@ async def authorize( state = secrets.token_urlsafe(32) supabase_verifier = secrets.token_urlsafe(32) + assert client.client_id is not None pending = PendingAuth( client_id=client.client_id, params=params, @@ -364,10 +365,11 @@ async def handle_start(self, request: Request) -> RedirectResponse: request, "start", request.path_params.get("state") ) + state = request.path_params.get("state", "") response = RedirectResponse(url=pending.supabase_redirect_url, status_code=302) response.set_cookie( key="mcp_auth_state", - value=request.path_params.get("state"), + value=state, max_age=settings.pending_auth_ttl, httponly=True, samesite="lax", @@ -472,6 +474,7 @@ async def exchange_authorization_code( client: OAuthClientInformationFull, authorization_code: EveryRowAuthorizationCode, ) -> OAuthToken: + assert client.client_id is not None return await self._issue_token_response( access_token=authorization_code.supabase_access_token, client_id=client.client_id, @@ -519,6 +522,7 @@ async def exchange_refresh_token( value=refresh_token.model_dump_json(), ) raise + assert client.client_id is not None return await self._issue_token_response( access_token=supa_tokens.access_token, client_id=client.client_id, diff --git a/everyrow-mcp/src/everyrow_mcp/http_config.py b/everyrow-mcp/src/everyrow_mcp/http_config.py index 478b8009..a72cf339 100644 --- a/everyrow-mcp/src/everyrow_mcp/http_config.py +++ b/everyrow-mcp/src/everyrow_mcp/http_config.py @@ -49,6 +49,7 @@ def configure_http_mode( ) redis_client = get_redis_client() + auth_provider: EveryRowAuthProvider | None = None if no_auth: lifespan = no_auth_http_lifespan else: @@ -65,7 +66,7 @@ def configure_http_mode( mcp.settings.port = port _register_widgets(mcp, mcp_server_url) - _register_routes(mcp, redis_client, auth_provider if not no_auth else None) + _register_routes(mcp, redis_client, auth_provider) _add_middleware(mcp, redis_client, rate_limit=not no_auth) @@ -103,7 +104,7 @@ def _register_routes( async def _health(_request: Request) -> Response: try: - await redis.ping() + await redis.ping() # pyright: ignore[reportGeneralTypeIssues] except Exception: return JSONResponse( {"status": "unhealthy", "redis": "unreachable"}, status_code=503 diff --git a/everyrow-mcp/src/everyrow_mcp/result_store.py b/everyrow-mcp/src/everyrow_mcp/result_store.py index 4dced821..72bc3a97 100644 --- a/everyrow-mcp/src/everyrow_mcp/result_store.py +++ b/everyrow-mcp/src/everyrow_mcp/result_store.py @@ -16,6 +16,7 @@ import json import logging import math +from typing import Any import pandas as pd from mcp.types import TextContent @@ -26,7 +27,7 @@ logger = logging.getLogger(__name__) -def _sanitize_records(records: list[dict]) -> list[dict]: +def _sanitize_records(records: list[dict[str, Any]]) -> list[dict[str, Any]]: """Replace NaN/Inf float values with None for valid JSON serialization. pandas ``to_dict(orient="records")`` preserves ``float('nan')`` which @@ -54,9 +55,9 @@ def _estimate_tokens(text: str) -> int: def clamp_page_to_budget( - preview_records: list[dict], + preview_records: list[dict[str, Any]], page_size: int, -) -> tuple[list[dict], int]: +) -> tuple[list[dict[str, Any]], int]: estimated = _estimate_tokens(json.dumps(preview_records)) if estimated <= settings.token_budget: return preview_records, page_size @@ -86,7 +87,7 @@ def clamp_page_to_budget( def _build_result_response( task_id: str, csv_url: str, - preview_records: list[dict], + preview_records: list[dict[str, Any]], total: int, columns: list[str], offset: int, @@ -242,7 +243,7 @@ async def try_store_result( columns = list(df.columns) # Store base metadata - meta = {"total": total, "columns": columns} + meta: dict[str, Any] = {"total": total, "columns": columns} if session_url: meta["session_url"] = session_url await redis_store.store_result_meta(task_id, json.dumps(meta)) diff --git a/everyrow-mcp/src/everyrow_mcp/tool_helpers.py b/everyrow-mcp/src/everyrow_mcp/tool_helpers.py index b0206dfa..e5912d66 100644 --- a/everyrow-mcp/src/everyrow_mcp/tool_helpers.py +++ b/everyrow-mcp/src/everyrow_mcp/tool_helpers.py @@ -132,9 +132,7 @@ async def create_tool_response( return [main_content] -_UI_EXCLUDE = frozenset( - {"is_terminal", "is_screen", "task_type", "error", "started_at"} -) +_UI_EXCLUDE: set[str] = {"is_terminal", "is_screen", "task_type", "error", "started_at"} class TaskState(BaseModel): diff --git a/everyrow-mcp/src/everyrow_mcp/tools.py b/everyrow-mcp/src/everyrow_mcp/tools.py index e5b60318..01b19407 100644 --- a/everyrow-mcp/src/everyrow_mcp/tools.py +++ b/everyrow-mcp/src/everyrow_mcp/tools.py @@ -679,7 +679,7 @@ async def everyrow_results_http( # ── Fetch from API ──────────────────────────────────────────── try: df, session_id = await _fetch_task_result(client, task_id) - session_url = get_session_url(session_id) if session_id else "" + session_url = get_session_url(UUID(session_id)) if session_id else "" except TaskNotReady as e: return [ TextContent( diff --git a/everyrow-mcp/tests/conftest.py b/everyrow-mcp/tests/conftest.py index 745826e3..57596e8c 100644 --- a/everyrow-mcp/tests/conftest.py +++ b/everyrow-mcp/tests/conftest.py @@ -4,6 +4,7 @@ # Set env vars for HttpSettings before any everyrow imports import os +from collections.abc import AsyncGenerator os.environ.setdefault("EVERYROW_API_KEY", "test-api-key") os.environ.setdefault("SUPABASE_URL", "https://test.supabase.co") @@ -64,7 +65,7 @@ def _redis_server(): @pytest.fixture -async def fake_redis(_redis_server) -> aioredis.Redis: +async def fake_redis(_redis_server) -> AsyncGenerator[aioredis.Redis, None]: """A real Redis client, flushed after each test.""" r = aioredis.Redis(host="localhost", port=_REDIS_PORT, decode_responses=True) await r.flushdb() @@ -102,7 +103,7 @@ def override_settings(**overrides): @pytest.fixture -async def everyrow_client(): +async def everyrow_client() -> AsyncGenerator[object, None]: """Provide a real everyrow SDK client for integration tests.""" with create_client() as client: yield client diff --git a/everyrow-mcp/tests/test_auth.py b/everyrow-mcp/tests/test_auth.py index fd515ea2..8b817c91 100644 --- a/everyrow-mcp/tests/test_auth.py +++ b/everyrow-mcp/tests/test_auth.py @@ -15,6 +15,7 @@ ) from mcp.server.auth.provider import AccessToken, AuthorizationParams from mcp.shared.auth import OAuthClientInformationFull +from pydantic import AnyUrl from everyrow_mcp.auth import ( EveryRowAuthorizationCode, @@ -53,6 +54,7 @@ def mock_redis(): async def _setex(*args, name=None, time=None, value=None): # noqa: ARG001 key = name if name is not None else args[0] val = value if value is not None else args[2] if len(args) > 2 else None + assert val is not None store[key] = val async def _exists(key): @@ -89,7 +91,7 @@ def verifier(rsa_keypair, mock_redis): def _make_jwt( private_key, - claims: dict | None = None, + claims: dict[str, str | int] | None = None, *, remove_claims: list[str] | None = None, ) -> str: @@ -362,6 +364,7 @@ async def _set(key, value): async def _setex(*args, name=None, time=None, value=None): # noqa: ARG001 key = name if name is not None else args[0] val = value if value is not None else args[2] if len(args) > 2 else None + assert val is not None store[key] = val async def _get(key): @@ -414,7 +417,7 @@ def test_client(): """A minimal OAuthClientInformationFull for tests.""" return OAuthClientInformationFull( client_id="test-client-id", - redirect_uris=["https://example.com/callback"], + redirect_uris=[AnyUrl("https://example.com/callback")], ) @@ -427,7 +430,7 @@ async def test_auth_code_consumed_atomically(self, provider, test_client): auth_code_obj = EveryRowAuthorizationCode( code=auth_code_str, client_id="test-client-id", - redirect_uri="https://example.com/callback", + redirect_uri=AnyUrl("https://example.com/callback"), redirect_uri_provided_explicitly=True, code_challenge="test-challenge", scopes=["read"], @@ -563,7 +566,7 @@ async def test_redirect_uri_mismatch_rejected(self, provider, test_client): params = AuthorizationParams( state="s1", scopes=["read"], - redirect_uri="https://evil.example.com/callback", + redirect_uri=AnyUrl("https://evil.example.com/callback"), code_challenge="challenge", redirect_uri_provided_explicitly=True, ) @@ -576,7 +579,7 @@ async def test_matching_redirect_uri_accepted(self, provider, test_client): params = AuthorizationParams( state="s1", scopes=["read"], - redirect_uri="https://example.com/callback", + redirect_uri=AnyUrl("https://example.com/callback"), code_challenge="challenge", redirect_uri_provided_explicitly=True, ) @@ -653,7 +656,7 @@ async def test_auth_code_client_id_mismatch(self, provider): auth_code_obj = EveryRowAuthorizationCode( code=auth_code_str, client_id="other-client-id", - redirect_uri="https://example.com/callback", + redirect_uri=AnyUrl("https://example.com/callback"), redirect_uri_provided_explicitly=True, code_challenge="test-challenge", scopes=["read"], @@ -669,7 +672,7 @@ async def test_auth_code_client_id_mismatch(self, provider): wrong_client = OAuthClientInformationFull( client_id="wrong-client-id", - redirect_uris=["https://example.com/callback"], + redirect_uris=[AnyUrl("https://example.com/callback")], ) result = await provider.load_authorization_code(wrong_client, auth_code_str) assert result is None @@ -692,7 +695,7 @@ async def test_refresh_token_client_id_mismatch(self, provider): wrong_client = OAuthClientInformationFull( client_id="wrong-client-id", - redirect_uris=["https://example.com/callback"], + redirect_uris=[AnyUrl("https://example.com/callback")], ) result = await provider.load_refresh_token(wrong_client, "rt-mismatch") assert result is None @@ -731,7 +734,7 @@ async def test_auth_code_expired_rejected(self, provider, test_client): auth_code_obj = EveryRowAuthorizationCode( code=auth_code_str, client_id="test-client-id", - redirect_uri="https://example.com/callback", + redirect_uri=AnyUrl("https://example.com/callback"), redirect_uri_provided_explicitly=True, code_challenge="test-challenge", scopes=["read"], diff --git a/everyrow-mcp/tests/test_http_transport.py b/everyrow-mcp/tests/test_http_transport.py index 97d61b14..c87516c2 100644 --- a/everyrow-mcp/tests/test_http_transport.py +++ b/everyrow-mcp/tests/test_http_transport.py @@ -28,13 +28,16 @@ import subprocess import sys import time +from collections.abc import Generator from contextlib import asynccontextmanager +from typing import Any import httpx import pytest import redis from mcp.client.session import ClientSession from mcp.client.streamable_http import streamablehttp_client +from mcp.types import TextContent # Skip all tests unless opted in pytestmark = pytest.mark.skipif( @@ -77,7 +80,7 @@ def _flush_redis_db(db: int = REDIS_TEST_DB) -> None: @pytest.fixture(scope="session") -def mcp_server() -> str: +def mcp_server() -> Generator[str, None, None]: """Start the MCP server subprocess on a random port with Redis DB 15. Yields the base URL (e.g. http://127.0.0.1:PORT). @@ -166,7 +169,7 @@ async def poll_via_mcp( result = await session.call_tool( "everyrow_progress", {"params": {"task_id": task_id}} ) - texts = [b.text for b in result.content if hasattr(b, "text")] + texts = [b.text for b in result.content if isinstance(b, TextContent)] human_text = texts[-1] if texts else "" print(f" Poll {i + 1}: {human_text.splitlines()[0]}") @@ -180,10 +183,10 @@ async def poll_via_mcp( raise TimeoutError(f"Task {task_id} did not complete within {max_polls} polls") -def parse_widget_json(content_blocks) -> dict: +def parse_widget_json(content_blocks) -> dict[str, Any]: """Parse the first TextContent block as JSON (the widget data).""" for block in content_blocks: - if hasattr(block, "text"): + if isinstance(block, TextContent): try: return json.loads(block.text) except json.JSONDecodeError: @@ -256,7 +259,10 @@ async def test_agent_submit_poll_results( ) # Fail fast on tool errors - first_text = submit_result.content[0].text if submit_result.content else "" + first_block = submit_result.content[0] if submit_result.content else None + first_text = ( + first_block.text if isinstance(first_block, TextContent) else "" + ) assert not first_text.startswith("Error"), f"Tool error: {first_text}" # Parse widget JSON from the first content block @@ -276,7 +282,9 @@ async def test_agent_submit_poll_results( "everyrow_results", {"params": {"task_id": task_id, "page_size": 1}}, ) - results_texts = [b.text for b in results_resp.content if hasattr(b, "text")] + results_texts = [ + b.text for b in results_resp.content if isinstance(b, TextContent) + ] results_widget = parse_widget_json(results_resp.content) print(f"\nResults page 1 widget: {json.dumps(results_widget, indent=2)}") @@ -306,7 +314,7 @@ async def test_agent_submit_poll_results( ) results_widget2 = parse_widget_json(results_resp2.content) results_texts2 = [ - b.text for b in results_resp2.content if hasattr(b, "text") + b.text for b in results_resp2.content if isinstance(b, TextContent) ] print(f"\nResults page 2 widget: {json.dumps(results_widget2, indent=2)}") @@ -328,6 +336,7 @@ async def test_agent_submit_poll_results( reader = csv.DictReader(io.StringIO(csv_response.text)) rows = list(reader) assert len(rows) == 2, f"Expected 2 CSV rows, got {len(rows)}" + assert reader.fieldnames is not None assert "headquarters" in reader.fieldnames, ( f"Expected 'headquarters' column, got columns: {reader.fieldnames}" ) diff --git a/everyrow-mcp/tests/test_mcp_e2e.py b/everyrow-mcp/tests/test_mcp_e2e.py index 795e0028..7a934ef1 100644 --- a/everyrow-mcp/tests/test_mcp_e2e.py +++ b/everyrow-mcp/tests/test_mcp_e2e.py @@ -16,7 +16,7 @@ from contextlib import asynccontextmanager from datetime import UTC, datetime from unittest.mock import AsyncMock, MagicMock, patch -from uuid import uuid4 +from uuid import UUID, uuid4 import pandas as pd import pytest @@ -27,6 +27,7 @@ from everyrow.generated.models.task_status_response import TaskStatusResponse from mcp.server.fastmcp.server import lifespan_wrapper from mcp.shared.memory import create_connected_server_and_client_session +from mcp.types import TextContent # Import tools module to trigger @mcp.tool() registration on the FastMCP instance import everyrow_mcp.tools # noqa: F401 @@ -104,7 +105,7 @@ def _mock_status_response( running: int = 2, ) -> TaskStatusResponse: return TaskStatusResponse( - task_id=task_id or uuid4(), + task_id=UUID(task_id) if task_id else uuid4(), session_id=uuid4(), status=TaskStatus(status), task_type=PublicTaskType.AGENT, @@ -189,10 +190,12 @@ async def test_call_screen_tool(self, _http_state, tmp_path): assert not result.isError # HTTP mode returns 2 content items: widget JSON + human text assert len(result.content) == 2 + assert isinstance(result.content[0], TextContent) widget = json.loads(result.content[0].text) assert widget["task_id"] == task_id assert widget["status"] == "submitted" assert "progress_url" in widget + assert isinstance(result.content[1], TextContent) assert task_id in result.content[1].text @pytest.mark.asyncio @@ -226,6 +229,7 @@ async def test_call_progress_tool(self, _http_state): ) assert not result.isError + assert isinstance(result.content[-1], TextContent) human_text = result.content[-1].text assert "5/10" in human_text assert "running" in human_text.lower() or "Running" in human_text @@ -288,6 +292,7 @@ async def test_call_agent_tool(self, _http_state, tmp_path): assert not result.isError assert len(result.content) == 2 + assert isinstance(result.content[0], TextContent) widget = json.loads(result.content[0].text) assert widget["task_id"] == task_id @@ -323,6 +328,7 @@ async def test_completed_progress_suggests_results(self, _http_state): ) assert not result.isError + assert isinstance(result.content[-1], TextContent) human_text = result.content[-1].text assert "everyrow_results" in human_text @@ -369,6 +375,7 @@ async def test_screen_pipeline(self, _real_client, jobs_csv): ) assert not submit_result.isError + assert isinstance(submit_result.content[0], TextContent) task_id = _extract_task_id(submit_result.content[0].text) print(f"\nSubmitted screen task: {task_id}") @@ -384,6 +391,7 @@ async def test_screen_pipeline(self, _real_client, jobs_csv): ) assert not progress_result.isError + assert isinstance(progress_result.content[-1], TextContent) text = progress_result.content[-1].text print(f" Progress: {text.splitlines()[0]}") @@ -417,6 +425,7 @@ async def test_screen_pipeline(self, _real_client, jobs_csv): ) assert not results.isError + assert isinstance(results.content[-1], TextContent) print(f" Results: {results.content[-1].text}") @pytest.mark.asyncio @@ -451,6 +460,7 @@ async def test_agent_pipeline(self, _real_client, tmp_path): ) assert not submit_result.isError + assert isinstance(submit_result.content[0], TextContent) task_id = _extract_task_id(submit_result.content[0].text) print(f"\nSubmitted agent task: {task_id}") @@ -466,6 +476,7 @@ async def test_agent_pipeline(self, _real_client, tmp_path): ) assert not progress_result.isError + assert isinstance(progress_result.content[-1], TextContent) text = progress_result.content[-1].text print(f" Progress: {text.splitlines()[0]}") @@ -499,4 +510,5 @@ async def test_agent_pipeline(self, _real_client, tmp_path): ) assert not results.isError + assert isinstance(results.content[-1], TextContent) print(f" Results: {results.content[-1].text}") diff --git a/everyrow-mcp/tests/test_result_store.py b/everyrow-mcp/tests/test_result_store.py index 1175c97b..3335d7c1 100644 --- a/everyrow-mcp/tests/test_result_store.py +++ b/everyrow-mcp/tests/test_result_store.py @@ -258,6 +258,7 @@ async def test_preserves_session_url_from_meta(self, _http_state): result = await try_cached_result(task_id, 0, 10, mcp_server_url=FAKE_SERVER_URL) + assert result is not None widget = json.loads(result[0].text) assert widget["session_url"] == "https://everyrow.io/sessions/xyz" @@ -334,6 +335,7 @@ async def test_includes_session_url_in_meta(self, sample_df, _http_state): ) meta_raw = await redis_store.get_result_meta(task_id) + assert meta_raw is not None meta = json.loads(meta_raw) assert meta["session_url"] == "https://everyrow.io/sessions/abc" diff --git a/everyrow-mcp/tests/test_routes.py b/everyrow-mcp/tests/test_routes.py index e8416743..4470a255 100644 --- a/everyrow-mcp/tests/test_routes.py +++ b/everyrow-mcp/tests/test_routes.py @@ -82,7 +82,7 @@ class TestApiProgress: @pytest.mark.asyncio async def test_options_returns_204(self): req = FakeRequest(method="OPTIONS", path_params={"task_id": "abc"}) - resp = await api_progress(req) + resp = await api_progress(req) # pyright: ignore[reportArgumentType] assert resp.status_code == 204 assert resp.headers["Access-Control-Allow-Origin"] == settings.mcp_server_url @@ -96,9 +96,9 @@ async def test_invalid_poll_token_returns_403(self): path_params={"task_id": task_id}, query_params={"token": "wrong-token"}, ) - resp = await api_progress(req) + resp = await api_progress(req) # pyright: ignore[reportArgumentType] assert resp.status_code == 403 - body = json.loads(resp.body.decode()) + body = json.loads(bytes(resp.body).decode()) assert body["error"] == "Unauthorized" @pytest.mark.asyncio @@ -109,7 +109,7 @@ async def test_missing_poll_token_returns_403(self): path_params={"task_id": task_id}, query_params={}, ) - resp = await api_progress(req) + resp = await api_progress(req) # pyright: ignore[reportArgumentType] assert resp.status_code == 403 @pytest.mark.asyncio @@ -123,9 +123,9 @@ async def test_missing_task_token_returns_404(self): path_params={"task_id": task_id}, query_params={"token": poll_token}, ) - resp = await api_progress(req) + resp = await api_progress(req) # pyright: ignore[reportArgumentType] assert resp.status_code == 404 - body = json.loads(resp.body.decode()) + body = json.loads(bytes(resp.body).decode()) assert body["error"] == "Unknown task" @pytest.mark.asyncio @@ -149,10 +149,10 @@ async def test_valid_progress_returns_status(self): new_callable=AsyncMock, return_value=status_resp, ): - resp = await api_progress(req) + resp = await api_progress(req) # pyright: ignore[reportArgumentType] assert resp.status_code == 200 - body = json.loads(resp.body.decode()) + body = json.loads(bytes(resp.body).decode()) assert body["status"] == "running" assert body["completed"] == 3 assert body["total"] == 10 @@ -181,10 +181,10 @@ async def test_completed_task_pops_tokens(self): new_callable=AsyncMock, return_value=status_resp, ): - resp = await api_progress(req) + resp = await api_progress(req) # pyright: ignore[reportArgumentType] assert resp.status_code == 200 - body = json.loads(resp.body.decode()) + body = json.loads(bytes(resp.body).decode()) assert body["status"] == "completed" # Task token cleaned up; poll token kept for CSV download @@ -208,8 +208,8 @@ async def test_api_error_returns_500(self): new_callable=AsyncMock, side_effect=RuntimeError("API down"), ): - resp = await api_progress(req) + resp = await api_progress(req) # pyright: ignore[reportArgumentType] assert resp.status_code == 500 - body = json.loads(resp.body.decode()) + body = json.loads(bytes(resp.body).decode()) assert body["error"] == "Internal server error" diff --git a/everyrow-mcp/tests/test_server.py b/everyrow-mcp/tests/test_server.py index 1db29097..d30545bf 100644 --- a/everyrow-mcp/tests/test_server.py +++ b/everyrow-mcp/tests/test_server.py @@ -1124,7 +1124,7 @@ class TestResultsInputValidation: def test_stdio_requires_output_path(self): """Test that StdioResultsInput requires output_path.""" with pytest.raises(ValidationError): - StdioResultsInput(task_id="00000000-0000-0000-0000-000000000000") + StdioResultsInput(task_id="00000000-0000-0000-0000-000000000000") # pyright: ignore[reportCallIssue] def test_stdio_output_path_validated(self, tmp_path: Path): """Test that output_path is validated when provided.""" diff --git a/everyrow-mcp/tests/test_state_redis.py b/everyrow-mcp/tests/test_state_redis.py index cf0b5815..ac1ee471 100644 --- a/everyrow-mcp/tests/test_state_redis.py +++ b/everyrow-mcp/tests/test_state_redis.py @@ -98,12 +98,13 @@ async def test_different_offsets_are_independent(self): await redis_store.store_result_page("task-multi", 0, 10, page0) await redis_store.store_result_page("task-multi", 10, 10, page10) - assert json.loads(await redis_store.get_result_page("task-multi", 0, 10)) == [ - {"row": 0} - ] - assert json.loads(await redis_store.get_result_page("task-multi", 10, 10)) == [ - {"row": 10} - ] + raw_page0 = await redis_store.get_result_page("task-multi", 0, 10) + assert raw_page0 is not None + assert json.loads(raw_page0) == [{"row": 0}] + + raw_page10 = await redis_store.get_result_page("task-multi", 10, 10) + assert raw_page10 is not None + assert json.loads(raw_page10) == [{"row": 10}] @pytest.mark.asyncio async def test_get_missing_returns_none(self): diff --git a/everyrow-mcp/tests/test_stdio_content.py b/everyrow-mcp/tests/test_stdio_content.py index 8bb60a0e..8f092ea2 100644 --- a/everyrow-mcp/tests/test_stdio_content.py +++ b/everyrow-mcp/tests/test_stdio_content.py @@ -756,6 +756,7 @@ async def test_screen_pipeline_stdio_clean( assert len(submit.content) == 1, ( f"Stdio submit should return 1 item, got {len(submit.content)}" ) + assert isinstance(submit.content[0], TextContent) task_id = _extract_task_id(submit.content[0].text) print(f"\n Submitted screen: {task_id}") @@ -772,6 +773,7 @@ async def test_screen_pipeline_stdio_clean( ) assert len(progress.content) == 1 + assert isinstance(progress.content[0], TextContent) text = progress.content[0].text print(f" Progress: {text.splitlines()[0]}") @@ -797,6 +799,7 @@ async def test_screen_pipeline_stdio_clean( assert not results.isError _assert_mcp_result_clean(results, tool_name="screen results") assert len(results.content) == 1 + assert isinstance(results.content[0], TextContent) assert "Saved" in results.content[0].text assert output_file.exists() print(f" Results: {results.content[0].text}") @@ -837,6 +840,7 @@ async def test_agent_pipeline_stdio_clean(self, _real_stdio_client, tmp_path): assert not submit.isError _assert_mcp_result_clean(submit, tool_name="agent submit") assert len(submit.content) == 1 + assert isinstance(submit.content[0], TextContent) task_id = _extract_task_id(submit.content[0].text) print(f"\n Submitted agent: {task_id}") @@ -853,6 +857,7 @@ async def test_agent_pipeline_stdio_clean(self, _real_stdio_client, tmp_path): ) assert len(progress.content) == 1 + assert isinstance(progress.content[0], TextContent) text = progress.content[0].text print(f" Progress: {text.splitlines()[0]}") @@ -878,6 +883,7 @@ async def test_agent_pipeline_stdio_clean(self, _real_stdio_client, tmp_path): assert not results.isError _assert_mcp_result_clean(results, tool_name="agent results") assert len(results.content) == 1 + assert isinstance(results.content[0], TextContent) assert "Saved" in results.content[0].text assert output_file.exists() @@ -905,6 +911,7 @@ async def test_single_agent_pipeline_stdio_clean( assert not submit.isError _assert_mcp_result_clean(submit, tool_name="single_agent submit") assert len(submit.content) == 1 + assert isinstance(submit.content[0], TextContent) task_id = _extract_task_id(submit.content[0].text) print(f"\n Submitted single_agent: {task_id}") @@ -921,6 +928,7 @@ async def test_single_agent_pipeline_stdio_clean( ) assert len(progress.content) == 1 + assert isinstance(progress.content[0], TextContent) text = progress.content[0].text print(f" Progress: {text.splitlines()[0]}") @@ -946,5 +954,6 @@ async def test_single_agent_pipeline_stdio_clean( assert not results.isError _assert_mcp_result_clean(results, tool_name="single_agent results") assert len(results.content) == 1 + assert isinstance(results.content[0], TextContent) assert output_file.exists() print(f" Results: {results.content[0].text}")