diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cfbea1d6..b3b8152b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,17 +45,25 @@ repos: build| dist""" - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.0 + - repo: local hooks: - id: mypy - package: agent_assembly,test - # exclude: ^test/unit_test.{1,64}.py + name: mypy + # Run mypy inside the uv-managed project env so it resolves the SDK's + # runtime + optional framework deps (pydantic, httpx, pydantic-ai, …). + # The upstream mirrors-mypy hook installs into an isolated venv that lacks + # these, which made every BaseModel/framework subclass resolve to `Any`. + # Type-check the whole project via mypy.ini's `packages =` discovery (no + # file paths) so the generated proto .py/.pyi pairs don't trip a + # "Duplicate module" error and the hook stays identical to the configured + # `mypy` invocation. + entry: uv run mypy + language: system + types: [python] + pass_filenames: false args: - # - --strict - --ignore-missing-imports - --show-traceback - additional_dependencies: [types-PyYAML>=6.0.12.9] - repo: https://github.com/astral-sh/uv-pre-commit # uv version. diff --git a/agent_assembly/__init__.py b/agent_assembly/__init__.py index 68c66e5a..b02f92f4 100644 --- a/agent_assembly/__init__.py +++ b/agent_assembly/__init__.py @@ -95,22 +95,26 @@ def __dir__() -> list[str]: if TYPE_CHECKING: - from agent_assembly.adapters import FrameworkAdapter, GovernanceInterceptor - from agent_assembly.core import AssemblyContext, init_assembly + from agent_assembly.adapters import FrameworkAdapter as FrameworkAdapter + from agent_assembly.adapters import GovernanceInterceptor as GovernanceInterceptor + from agent_assembly.core import AssemblyContext as AssemblyContext + from agent_assembly.core import init_assembly as init_assembly from agent_assembly.exceptions import ( - AdapterValidationError, - AgentError, - AssemblyError, - ConfigurationError, - GatewayError, - MCPToolBlockedError, - PolicyError, - ToolExecutionBlockedError, + AdapterValidationError as AdapterValidationError, ) - from agent_assembly.types import AuditEvent, CallStackNode, CallStackNodeKind + from agent_assembly.exceptions import AgentError as AgentError + from agent_assembly.exceptions import AssemblyError as AssemblyError + from agent_assembly.exceptions import ConfigurationError as ConfigurationError + from agent_assembly.exceptions import GatewayError as GatewayError + from agent_assembly.exceptions import MCPToolBlockedError as MCPToolBlockedError + from agent_assembly.exceptions import PolicyError as PolicyError + from agent_assembly.exceptions import ( + ToolExecutionBlockedError as ToolExecutionBlockedError, + ) + from agent_assembly.types import AuditEvent as AuditEvent + from agent_assembly.types import CallStackNode as CallStackNode + from agent_assembly.types import CallStackNodeKind as CallStackNodeKind with contextlib.suppress(ImportError): - from agent_assembly._core import ( - GovernanceEvent, - RuntimeClient, - ) + from agent_assembly._core import GovernanceEvent as GovernanceEvent + from agent_assembly._core import RuntimeClient as RuntimeClient diff --git a/agent_assembly/adapters/crewai/patch.py b/agent_assembly/adapters/crewai/patch.py index 773d2299..6b4526ee 100644 --- a/agent_assembly/adapters/crewai/patch.py +++ b/agent_assembly/adapters/crewai/patch.py @@ -2,7 +2,7 @@ from __future__ import annotations -import importlib +import importlib as importlib from collections.abc import Mapping from dataclasses import dataclass from functools import wraps diff --git a/agent_assembly/adapters/google_adk/patch.py b/agent_assembly/adapters/google_adk/patch.py index 81b34954..c20eb765 100644 --- a/agent_assembly/adapters/google_adk/patch.py +++ b/agent_assembly/adapters/google_adk/patch.py @@ -2,7 +2,7 @@ from __future__ import annotations -import importlib +import importlib as importlib import inspect from collections.abc import Mapping from dataclasses import dataclass diff --git a/agent_assembly/adapters/mcp/patch.py b/agent_assembly/adapters/mcp/patch.py index a51ca1eb..29b48a7f 100644 --- a/agent_assembly/adapters/mcp/patch.py +++ b/agent_assembly/adapters/mcp/patch.py @@ -2,7 +2,7 @@ from __future__ import annotations -import importlib +import importlib as importlib import importlib.util import inspect from dataclasses import dataclass diff --git a/agent_assembly/adapters/openai_agents/patch.py b/agent_assembly/adapters/openai_agents/patch.py index 65851938..b3398d54 100644 --- a/agent_assembly/adapters/openai_agents/patch.py +++ b/agent_assembly/adapters/openai_agents/patch.py @@ -2,7 +2,7 @@ from __future__ import annotations -import importlib +import importlib as importlib import importlib.util import inspect from dataclasses import dataclass, field diff --git a/agent_assembly/adapters/pydantic_ai/patch.py b/agent_assembly/adapters/pydantic_ai/patch.py index e7fb9a8d..138c5f9b 100644 --- a/agent_assembly/adapters/pydantic_ai/patch.py +++ b/agent_assembly/adapters/pydantic_ai/patch.py @@ -2,7 +2,7 @@ from __future__ import annotations -import importlib +import importlib as importlib import inspect from collections.abc import Mapping from dataclasses import dataclass diff --git a/agent_assembly/client/emitter.py b/agent_assembly/client/emitter.py index b5efd916..118dfc9e 100644 --- a/agent_assembly/client/emitter.py +++ b/agent_assembly/client/emitter.py @@ -4,6 +4,7 @@ import logging import threading +from typing import Any from agent_assembly.client.gateway import GatewayClient @@ -21,7 +22,7 @@ def emit( source_agent_id: str, target_agent_id: str, edge_type: str, - metadata: dict | None = None, + metadata: dict[str, Any] | None = None, ) -> None: """Schedule a fire-and-forget edge report on a daemon thread.""" t = threading.Thread( @@ -36,7 +37,7 @@ def _send( source_agent_id: str, target_agent_id: str, edge_type: str, - metadata: dict | None, + metadata: dict[str, Any] | None, ) -> None: try: self._client.report_edge(source_agent_id, target_agent_id, edge_type, metadata) diff --git a/agent_assembly/client/gateway.py b/agent_assembly/client/gateway.py index 21bd79fe..ee79d96c 100644 --- a/agent_assembly/client/gateway.py +++ b/agent_assembly/client/gateway.py @@ -96,7 +96,7 @@ def __exit__(self, *args: object) -> None: """Context manager exit.""" self.close() - async def register_agent(self) -> dict: + async def register_agent(self) -> dict[str, Any]: """ Register the agent with the governance gateway. @@ -125,11 +125,12 @@ async def register_agent(self) -> dict: json=body if body else None, ) response.raise_for_status() - return response.json() + data: dict[str, Any] = response.json() + return data except httpx.HTTPError as e: raise GatewayError(f"Failed to register agent: {e}") from e - async def check_policy_compliance(self, action: str) -> dict: + async def check_policy_compliance(self, action: str) -> dict[str, Any]: """ Check if an action complies with governance policies. @@ -148,7 +149,8 @@ async def check_policy_compliance(self, action: str) -> dict: json={"action": action}, ) response.raise_for_status() - return response.json() + data: dict[str, Any] = response.json() + return data except httpx.HTTPError as e: raise GatewayError(f"Failed to check policy compliance: {e}") from e @@ -157,8 +159,8 @@ def report_edge( source_agent_id: str, target_agent_id: str, edge_type: str, - metadata: dict | None = None, - ) -> dict: + metadata: dict[str, Any] | None = None, + ) -> dict[str, Any]: """ Report a directed edge between two agents to the topology store. @@ -176,7 +178,7 @@ def report_edge( """ import json as _json - body: dict = { + body: dict[str, str] = { "source_agent_id": source_agent_id, "target_agent_id": target_agent_id, "edge_type": edge_type, @@ -186,7 +188,8 @@ def report_edge( try: response = self.client.post("/topology/edges", json=body) response.raise_for_status() - return response.json() + data: dict[str, Any] = response.json() + return data except httpx.HTTPError as e: raise GatewayError(f"Failed to report edge: {e}") from e diff --git a/agent_assembly/core/assembly.py b/agent_assembly/core/assembly.py index 695b9f1a..86183298 100644 --- a/agent_assembly/core/assembly.py +++ b/agent_assembly/core/assembly.py @@ -78,7 +78,7 @@ class AssemblyContext: def __enter__(self) -> AssemblyContext: return self - def __exit__(self, exc_type: object, exc: object, tb: object) -> bool: + def __exit__(self, exc_type: object, exc: object, tb: object) -> Literal[False]: del exc_type, exc, tb self.shutdown() return False diff --git a/agent_assembly/models/agent.py b/agent_assembly/models/agent.py index 8cfbbc8c..2a96bed5 100644 --- a/agent_assembly/models/agent.py +++ b/agent_assembly/models/agent.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime -from typing import Optional +from typing import Any, Optional from pydantic import BaseModel, Field @@ -25,7 +25,7 @@ class AgentState(BaseModel): agent_id: str = Field(..., description="Unique identifier for the agent") status: str = Field(default="idle", description="Current status of the agent") last_activity: Optional[datetime] = Field(None, description="Last activity timestamp") - metadata: dict = Field(default_factory=dict, description="Additional state metadata") + metadata: dict[str, Any] = Field(default_factory=dict, description="Additional state metadata") class PolicyEvaluation(BaseModel): diff --git a/agent_assembly/types.py b/agent_assembly/types.py index eabde1f7..31804d6d 100644 --- a/agent_assembly/types.py +++ b/agent_assembly/types.py @@ -110,7 +110,7 @@ def to_wire_bytes(self) -> bytes: wheel (pure-Python mode). """ try: - from agent_assembly._core import audit_event_to_wire_bytes # type: ignore[import-not-found] + from agent_assembly._core import audit_event_to_wire_bytes except ImportError as exc: raise ImportError( "AuditEvent.to_wire_bytes() requires the native " diff --git a/mypy.ini b/mypy.ini index a2f11403..b57cd779 100644 --- a/mypy.ini +++ b/mypy.ini @@ -21,6 +21,13 @@ warn_unused_ignores = True strict_equality = True strict_concatenate = True +# Generated gRPC/protobuf stubs (scripts/gen_proto.py). These are emitted by +# protoc + grpcio-tools and committed verbatim; a CI drift check regenerates +# them and asserts no diff, so they must not be hand-edited to satisfy mypy. +# Skip type-checking the generated output rather than annotate code we do not own. +[mypy-agent_assembly.proto.*] +ignore_errors = True + [mypy-agent_assembly.adapters.base] strict = True diff --git a/test/integration/test_assembly_integration.py b/test/integration/test_assembly_integration.py index 4461e596..5d64d52e 100644 --- a/test/integration/test_assembly_integration.py +++ b/test/integration/test_assembly_integration.py @@ -12,7 +12,7 @@ @pytest.mark.integration -def test_init_assembly_with_valid_config(): +def test_init_assembly_with_valid_config() -> None: """Test that assembly initialization works with valid configuration.""" # This test requires a running gateway context = init_assembly( @@ -26,7 +26,7 @@ def test_init_assembly_with_valid_config(): @pytest.mark.integration -def test_init_assembly_with_invalid_config(): +def test_init_assembly_with_invalid_config() -> None: """Test that assembly initialization fails with an unknown runtime mode.""" with pytest.raises(ConfigurationError): init_assembly( @@ -37,7 +37,7 @@ def test_init_assembly_with_invalid_config(): @pytest.mark.integration -def test_gateway_client_context_manager(): +def test_gateway_client_context_manager() -> None: """Test that the gateway client works as a context manager.""" context = init_assembly( gateway_url="http://localhost:8080", diff --git a/test/integration/test_crewai_interception_integration.py b/test/integration/test_crewai_interception_integration.py index b3a297a8..d7218aec 100644 --- a/test/integration/test_crewai_interception_integration.py +++ b/test/integration/test_crewai_interception_integration.py @@ -86,7 +86,7 @@ def test_crewai_real_task_and_tool_classes_flow_when_available( Agent = crewai.Agent Crew = crewai.Crew - class BlockedTool(BaseTool): + class BlockedTool(BaseTool): # type: ignore[misc,valid-type] # base is a runtime-imported framework class name: str = "blocked_tool" description: str = "Tool that should be blocked by governance." @@ -94,7 +94,7 @@ def _run(self, **kwargs: object) -> str: del kwargs return "should-not-run" - class SafeTool(BaseTool): + class SafeTool(BaseTool): # type: ignore[misc,valid-type] # base is a runtime-imported framework class name: str = "safe_tool" description: str = "Tool that should remain allowed." diff --git a/test/integration/test_native_core_runtime.py b/test/integration/test_native_core_runtime.py index 9523294d..de4b9f72 100644 --- a/test/integration/test_native_core_runtime.py +++ b/test/integration/test_native_core_runtime.py @@ -9,6 +9,7 @@ import time import tracemalloc from pathlib import Path +from typing import Any import pytest @@ -150,14 +151,14 @@ def make_audit_entry_payload(index: int, *, worker_id: int = 0) -> str: @pytest.fixture() -def native_core(): +def native_core() -> Any: if os.getenv("AAASM_RUN_NATIVE_CORE_TESTS") != "1": pytest.skip("Set AAASM_RUN_NATIVE_CORE_TESTS=1 to run native core runtime tests.") return pytest.importorskip("agent_assembly._core") @pytest.mark.integration -def test_send_event_is_non_blocking(native_core) -> None: +def test_send_event_is_non_blocking(native_core: Any) -> None: server = MockRuntimeServer() server.start() @@ -175,7 +176,7 @@ def test_send_event_is_non_blocking(native_core) -> None: @pytest.mark.integration -def test_runtime_client_has_no_thread_deadlock(native_core) -> None: +def test_runtime_client_has_no_thread_deadlock(native_core: Any) -> None: server = MockRuntimeServer() server.start() @@ -204,7 +205,7 @@ def worker(worker_id: int) -> None: @pytest.mark.integration -def test_runtime_client_tracemalloc_leak_guard(native_core) -> None: +def test_runtime_client_tracemalloc_leak_guard(native_core: Any) -> None: server = MockRuntimeServer() server.start() diff --git a/test/integration/test_spawn_lineage_integration.py b/test/integration/test_spawn_lineage_integration.py index fa8afcc9..6d84f2c8 100644 --- a/test/integration/test_spawn_lineage_integration.py +++ b/test/integration/test_spawn_lineage_integration.py @@ -375,7 +375,7 @@ def check_tool_start(self, **_kw: object) -> dict[str, str]: result = await tool._run(_Ctx(), {}) finally: _revert_tool_run_patch(FakeTool) - FakeTool._run = original_run + FakeTool._run = original_run # type: ignore[method-assign] # reassign fake method to install/restore stub assert result == "search-result" assert len(captured) == 1 @@ -435,7 +435,7 @@ def check_tool_start(self, **_kw: object) -> dict[str, str]: result = await FakeTool()._run(_Ctx(), {}) finally: _revert_tool_run_patch(FakeTool) - FakeTool._run = original_run + FakeTool._run = original_run # type: ignore[method-assign] # reassign fake method to install/restore stub assert result == "delegated" diff --git a/test/unit/adapters/crewai/test_crewai_spawn_context.py b/test/unit/adapters/crewai/test_crewai_spawn_context.py index 299428f4..932366de 100644 --- a/test/unit/adapters/crewai/test_crewai_spawn_context.py +++ b/test/unit/adapters/crewai/test_crewai_spawn_context.py @@ -20,22 +20,22 @@ class TestExtractWorkerAgentId: - def test_returns_agent_id_from_task_agent_id(self): + def test_returns_agent_id_from_task_agent_id(self) -> None: task = MagicMock() task.agent.id = "worker-001" assert _extract_worker_agent_id(task) == "worker-001" - def test_returns_agent_id_from_task_agent_agent_id_fallback(self): + def test_returns_agent_id_from_task_agent_agent_id_fallback(self) -> None: task = MagicMock(spec=["agent"]) task.agent = MagicMock(spec=["agent_id"]) task.agent.agent_id = "worker-002" assert _extract_worker_agent_id(task) == "worker-002" - def test_returns_none_when_no_agent(self): + def test_returns_none_when_no_agent(self) -> None: task = MagicMock(spec=[]) assert _extract_worker_agent_id(task) is None - def test_returns_none_when_agent_has_no_id_attrs(self): + def test_returns_none_when_agent_has_no_id_attrs(self) -> None: task = MagicMock(spec=["agent"]) task.agent = MagicMock(spec=[]) assert _extract_worker_agent_id(task) is None @@ -52,27 +52,27 @@ def __init__(self, agent_id: str | None = "worker-x"): else: self.agent = MagicMock(spec=[]) - def execute_sync(self, *_args: object, **_kwargs: object) -> str: + def execute_sync(self: object, *_args: object, **_kwargs: object) -> str: return "task-done" class TestTaskExecuteSyncSpawnContext: - def setup_method(self): + def setup_method(self) -> None: for attr in (_TASK_PATCHED_FLAG, "_agent_assembly_original_crewai_task_execute_sync"): if hasattr(FakeTask, attr): delattr(FakeTask, attr) - def teardown_method(self): + def teardown_method(self) -> None: _revert_task_execute_sync_patch(FakeTask) - def test_spawn_ctx_set_during_execute_sync(self): + def test_spawn_ctx_set_during_execute_sync(self) -> None: captured: list[SpawnContext | None] = [] - def capturing_execute(self, *args, **kwargs): + def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute + FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id="worker-x") task.execute_sync() @@ -81,17 +81,17 @@ def capturing_execute(self, *args, **kwargs): assert captured[0].parent_agent_id == "worker-x" assert captured[0].spawned_by_tool == "crewai_task" - def test_spawn_ctx_reset_after_execute_sync(self): + def test_spawn_ctx_reset_after_execute_sync(self) -> None: _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id="worker-y") task.execute_sync() assert _SPAWN_CTX.get() is None - def test_spawn_ctx_reset_on_exception(self): - def failing_execute(self, *args, **kwargs): + def test_spawn_ctx_reset_on_exception(self) -> None: + def failing_execute(self: object, *args: object, **kwargs: object) -> str: raise RuntimeError("task failed") - FakeTask.execute_sync = failing_execute + FakeTask.execute_sync = failing_execute # type: ignore[method-assign] # fake method swap _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id="worker-z") @@ -99,28 +99,28 @@ def failing_execute(self, *args, **kwargs): task.execute_sync() assert _SPAWN_CTX.get() is None - def test_no_spawn_ctx_when_no_agent_id(self): + def test_no_spawn_ctx_when_no_agent_id(self) -> None: captured: list[SpawnContext | None] = [] - def capturing_execute(self, *args, **kwargs): + def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute + FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id=None) task.execute_sync() assert captured[0] is None - def test_team_id_extracted_from_agent_crew(self): + def test_team_id_extracted_from_agent_crew(self) -> None: captured: list[SpawnContext | None] = [] - def capturing_execute(self, *args, **kwargs): + def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute + FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id="worker-a") crew = MagicMock() @@ -131,14 +131,14 @@ def capturing_execute(self, *args, **kwargs): assert captured[0] is not None assert captured[0].team_id == "crew-uuid-123" - def test_team_id_none_when_no_crew(self): + def test_team_id_none_when_no_crew(self) -> None: captured: list[SpawnContext | None] = [] - def capturing_execute(self, *args, **kwargs): + def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute + FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id="worker-b") task.agent = MagicMock(spec=["id"]) @@ -148,14 +148,14 @@ def capturing_execute(self, *args, **kwargs): assert captured[0] is not None assert captured[0].team_id is None - def test_delegation_reason_from_task_description(self): + def test_delegation_reason_from_task_description(self) -> None: captured: list[SpawnContext | None] = [] - def capturing_execute(self, *args, **kwargs): + def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute + FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id="worker-c") task.description = "Analyze quarterly reports" @@ -164,14 +164,14 @@ def capturing_execute(self, *args, **kwargs): assert captured[0] is not None assert captured[0].delegation_reason == "Analyze quarterly reports" - def test_delegation_reason_truncated_to_256_chars(self): + def test_delegation_reason_truncated_to_256_chars(self) -> None: captured: list[SpawnContext | None] = [] - def capturing_execute(self, *args, **kwargs): + def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute + FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id="worker-d") task.description = "x" * 300 @@ -180,14 +180,14 @@ def capturing_execute(self, *args, **kwargs): assert captured[0] is not None assert len(captured[0].delegation_reason) == 256 # type: ignore[arg-type] - def test_delegation_reason_none_when_description_empty(self): + def test_delegation_reason_none_when_description_empty(self) -> None: captured: list[SpawnContext | None] = [] - def capturing_execute(self, *args, **kwargs): + def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute + FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id="worker-e") task.description = "" @@ -198,35 +198,35 @@ def capturing_execute(self, *args, **kwargs): class TestExtractCrewTeamId: - def test_returns_str_of_crew_id(self): + def test_returns_str_of_crew_id(self) -> None: crew = MagicMock() crew.id = "abc-123" assert _extract_crew_team_id(crew) == "abc-123" - def test_returns_none_when_crew_is_none(self): + def test_returns_none_when_crew_is_none(self) -> None: assert _extract_crew_team_id(None) is None - def test_returns_none_when_crew_has_no_id(self): + def test_returns_none_when_crew_has_no_id(self) -> None: crew = MagicMock(spec=[]) assert _extract_crew_team_id(crew) is None - def test_stringifies_non_str_id(self): + def test_stringifies_non_str_id(self) -> None: crew = MagicMock() crew.id = 42 assert _extract_crew_team_id(crew) == "42" class TestExtractManagerAgentId: - def test_returns_manager_id(self): + def test_returns_manager_id(self) -> None: crew = MagicMock() crew.manager_agent.id = "manager-001" assert _extract_manager_agent_id(crew) == "manager-001" - def test_returns_none_when_no_manager_agent(self): + def test_returns_none_when_no_manager_agent(self) -> None: crew = MagicMock(spec=[]) assert _extract_manager_agent_id(crew) is None - def test_returns_none_when_manager_has_no_id(self): + def test_returns_none_when_manager_has_no_id(self) -> None: crew = MagicMock() crew.manager_agent = MagicMock(spec=[]) assert _extract_manager_agent_id(crew) is None @@ -242,7 +242,7 @@ def __init__(self, *, hierarchical: bool = False, manager_id: str = "mgr-1", cre self._hierarchical = hierarchical self.process = "hierarchical" if hierarchical else "sequential" - def kickoff(self, *_args: object, **_kwargs: object) -> str: + def kickoff(self: object, *_args: object, **_kwargs: object) -> str: return "crew-result" @@ -252,14 +252,14 @@ def kickoff(self, *_args: object, **_kwargs: object) -> str: class TestIsHierarchicalProcess: - def test_returns_false_for_sequential(self): + def test_returns_false_for_sequential(self) -> None: crew = FakeCrew(hierarchical=False) # _is_hierarchical_process checks crew.process == Process.hierarchical; # without real crewai installed it resolves Process to None → returns False result = _is_hierarchical_process(crew) assert isinstance(result, bool) - def test_returns_false_when_crewai_not_installed(self): + def test_returns_false_when_crewai_not_installed(self) -> None: from unittest.mock import patch with patch("importlib.import_module", side_effect=ImportError): @@ -272,7 +272,7 @@ def setup_method(self) -> None: for attr in (_CREW_KICKOFF_PATCHED_FLAG, _ORIGINAL_CREW_KICKOFF_ATTR): if hasattr(FakeCrew, attr): delattr(FakeCrew, attr) - FakeCrew.kickoff = _FAKE_CREW_ORIGINAL_KICKOFF + FakeCrew.kickoff = _FAKE_CREW_ORIGINAL_KICKOFF # type: ignore[method-assign] # fake method swap def teardown_method(self) -> None: _revert_crew_kickoff_patch(FakeCrew) @@ -280,17 +280,17 @@ def teardown_method(self) -> None: if hasattr(FakeCrew, attr): delattr(FakeCrew, attr) - def test_non_hierarchical_kickoff_bypasses_spawn_ctx(self): + def test_non_hierarchical_kickoff_bypasses_spawn_ctx(self) -> None: from unittest.mock import patch captured: list[SpawnContext | None] = [] original = FakeCrew.kickoff - def capturing_kickoff(self, *_args, **_kwargs): + def capturing_kickoff(self: object, *_args: object, **_kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeCrew.kickoff = capturing_kickoff + FakeCrew.kickoff = capturing_kickoff # type: ignore[method-assign] # fake method swap _apply_crew_kickoff_patch(FakeCrew) crew = FakeCrew(hierarchical=False) @@ -301,19 +301,19 @@ def capturing_kickoff(self, *_args, **_kwargs): crew.kickoff() assert captured[0] is None - FakeCrew.kickoff = original + FakeCrew.kickoff = original # type: ignore[method-assign] # reassign fake method to install/restore stub - def test_hierarchical_kickoff_sets_spawn_ctx(self): + def test_hierarchical_kickoff_sets_spawn_ctx(self) -> None: from unittest.mock import patch captured: list[SpawnContext | None] = [] original = FakeCrew.kickoff - def capturing_kickoff(self, *_args, **_kwargs): + def capturing_kickoff(self: object, *_args: object, **_kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeCrew.kickoff = capturing_kickoff + FakeCrew.kickoff = capturing_kickoff # type: ignore[method-assign] # fake method swap _apply_crew_kickoff_patch(FakeCrew) crew = FakeCrew(hierarchical=True, manager_id="mgr-42", crew_id="crew-99") @@ -329,9 +329,9 @@ def capturing_kickoff(self, *_args, **_kwargs): assert sc.team_id == "crew-99" assert sc.spawned_by_tool == "crewai_kickoff_hierarchical" assert sc.depth == 1 - FakeCrew.kickoff = original + FakeCrew.kickoff = original # type: ignore[method-assign] # reassign fake method to install/restore stub - def test_spawn_ctx_reset_after_hierarchical_kickoff(self): + def test_spawn_ctx_reset_after_hierarchical_kickoff(self) -> None: from unittest.mock import patch _apply_crew_kickoff_patch(FakeCrew) @@ -343,13 +343,13 @@ def test_spawn_ctx_reset_after_hierarchical_kickoff(self): crew.kickoff() assert _SPAWN_CTX.get() is None - def test_idempotent_apply(self): + def test_idempotent_apply(self) -> None: _apply_crew_kickoff_patch(FakeCrew) original = getattr(FakeCrew, _ORIGINAL_CREW_KICKOFF_ATTR, None) _apply_crew_kickoff_patch(FakeCrew) assert getattr(FakeCrew, _ORIGINAL_CREW_KICKOFF_ATTR, None) is original - def test_revert_restores_original(self): + def test_revert_restores_original(self) -> None: _apply_crew_kickoff_patch(FakeCrew) _revert_crew_kickoff_patch(FakeCrew) assert not hasattr(FakeCrew, _CREW_KICKOFF_PATCHED_FLAG) diff --git a/test/unit/adapters/crewai/test_patch.py b/test/unit/adapters/crewai/test_patch.py index 812aacef..d6c4cc29 100644 --- a/test/unit/adapters/crewai/test_patch.py +++ b/test/unit/adapters/crewai/test_patch.py @@ -416,4 +416,4 @@ def check_tool_start(self, **kwargs: object) -> dict[str, str]: assert future_a.result() == {"args": (), "kwargs": {}} assert future_b.result() == {"args": (), "kwargs": {}} - assert sorted(observed_agent_ids) == ["agent-A", "agent-B"] + assert sorted(observed_agent_ids) == ["agent-A", "agent-B"] # type: ignore[type-var] # data is str | None statically but never None here diff --git a/test/unit/adapters/langchain/test_langgraph_spawn_patch.py b/test/unit/adapters/langchain/test_langgraph_spawn_patch.py index 87f6139a..ed437578 100644 --- a/test/unit/adapters/langchain/test_langgraph_spawn_patch.py +++ b/test/unit/adapters/langchain/test_langgraph_spawn_patch.py @@ -1,5 +1,6 @@ from __future__ import annotations +from typing import Any from unittest.mock import MagicMock import pytest @@ -16,25 +17,25 @@ class TestIsCompiledSubgraph: - def test_returns_true_for_object_with_nodes_and_invoke(self): + def test_returns_true_for_object_with_nodes_and_invoke(self) -> None: obj = MagicMock() obj.nodes = {"a": MagicMock()} obj.invoke = MagicMock() assert _is_compiled_subgraph(obj) is True - def test_returns_false_for_callable_without_nodes(self): + def test_returns_false_for_callable_without_nodes(self) -> None: assert _is_compiled_subgraph(lambda x: x) is False - def test_returns_false_for_object_missing_invoke(self): + def test_returns_false_for_object_missing_invoke(self) -> None: obj = MagicMock(spec=["nodes"]) obj.nodes = {} assert _is_compiled_subgraph(obj) is False class TestMakeSubgraphSpawnWrapper: - def test_sync_wrapper_sets_spawn_ctx_then_resets(self): + def test_sync_wrapper_sets_spawn_ctx_then_resets(self) -> None: captured: list[SpawnContext | None] = [] - original_invoke = MagicMock(side_effect=lambda *_args, **_kwargs: captured.append(_SPAWN_CTX.get()) or "result") + original_invoke = MagicMock(side_effect=lambda *_args, **_kwargs: captured.append(_SPAWN_CTX.get()) or "result") # type: ignore[func-returns-value] subgraph = MagicMock() subgraph.invoke = original_invoke @@ -53,13 +54,15 @@ def test_sync_wrapper_sets_spawn_ctx_then_resets(self): assert sc.delegation_reason is None assert _SPAWN_CTX.get() is None - def test_delegation_reason_passed_through(self): + def test_delegation_reason_passed_through(self) -> None: captured: list[SpawnContext | None] = [] subgraph = MagicMock() - subgraph.invoke = MagicMock(side_effect=lambda *_args, **_kwargs: captured.append(_SPAWN_CTX.get()) or "r") + subgraph.invoke = MagicMock(side_effect=lambda *_args, **_kwargs: captured.append(_SPAWN_CTX.get()) or "r") # type: ignore[func-returns-value] wrapper = _make_subgraph_spawn_wrapper( - "mynode", subgraph, "parent", + "mynode", + subgraph, + "parent", delegation_reason="langgraph_node:mynode", ) wrapper({}) @@ -69,13 +72,15 @@ def test_delegation_reason_passed_through(self): assert sc.delegation_reason == "langgraph_node:mynode" assert sc.spawned_by_tool is None - def test_spawned_by_tool_passed_through_for_tool_node(self): + def test_spawned_by_tool_passed_through_for_tool_node(self) -> None: captured: list[SpawnContext | None] = [] subgraph = MagicMock() - subgraph.invoke = MagicMock(side_effect=lambda *_args, **_kwargs: captured.append(_SPAWN_CTX.get()) or "r") + subgraph.invoke = MagicMock(side_effect=lambda *_args, **_kwargs: captured.append(_SPAWN_CTX.get()) or "r") # type: ignore[func-returns-value] wrapper = _make_subgraph_spawn_wrapper( - "tool_x", subgraph, "parent", + "tool_x", + subgraph, + "parent", spawned_by_tool="tool_x", delegation_reason="langgraph_tool:tool_x", ) @@ -87,10 +92,10 @@ def test_spawned_by_tool_passed_through_for_tool_node(self): assert sc.delegation_reason == "langgraph_tool:tool_x" @pytest.mark.asyncio - async def test_async_wrapper_sets_spawn_ctx(self): + async def test_async_wrapper_sets_spawn_ctx(self) -> None: captured: list[SpawnContext | None] = [] - async def fake_ainvoke(*args, **kwargs): + async def fake_ainvoke(*args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "async-result" @@ -102,13 +107,15 @@ async def fake_ainvoke(*args, **kwargs): result = await wrapper({"input": "y"}) assert result == "async-result" - assert captured[0].parent_agent_id == "parent-async" - assert captured[0].depth == 1 + sc = captured[0] + assert sc is not None + assert sc.parent_agent_id == "parent-async" + assert sc.depth == 1 assert _SPAWN_CTX.get() is None - def test_spawn_ctx_depth_increments_when_already_in_ctx(self): + def test_spawn_ctx_depth_increments_when_already_in_ctx(self) -> None: captured: list[SpawnContext | None] = [] - original_invoke = MagicMock(side_effect=lambda *_args, **_kwargs: captured.append(_SPAWN_CTX.get()) or "r") + original_invoke = MagicMock(side_effect=lambda *_args, **_kwargs: captured.append(_SPAWN_CTX.get()) or "r") # type: ignore[func-returns-value] subgraph = MagicMock() subgraph.invoke = original_invoke @@ -120,9 +127,11 @@ def test_spawn_ctx_depth_increments_when_already_in_ctx(self): finally: _SPAWN_CTX.reset(token) - assert captured[0].depth == 2 + sc = captured[0] + assert sc is not None + assert sc.depth == 2 - def test_wrapper_is_pass_through_on_exception(self): + def test_wrapper_is_pass_through_on_exception(self) -> None: subgraph = MagicMock() subgraph.invoke = MagicMock(side_effect=RuntimeError("graph error")) @@ -144,7 +153,7 @@ def __init__(self, *, with_ainvoke: bool = False) -> None: class TestWrapNodeMapSubgraph: - def test_wrap_node_map_wraps_compiled_subgraph_sync(self): + def test_wrap_node_map_wraps_compiled_subgraph_sync(self) -> None: """_wrap_node_map replaces a compiled subgraph node with a sync spawn wrapper.""" subgraph = _FakeSubgraph(with_ainvoke=False) original_subgraph = subgraph @@ -155,7 +164,7 @@ def test_wrap_node_map_wraps_compiled_subgraph_sync(self): assert result is True assert node_map["subnode"] is not original_subgraph - def test_wrap_node_map_wraps_compiled_subgraph_with_ainvoke(self): + def test_wrap_node_map_wraps_compiled_subgraph_with_ainvoke(self) -> None: """_wrap_node_map patches both sync and async paths on a compiled subgraph.""" subgraph = _FakeSubgraph(with_ainvoke=True) original_ainvoke = subgraph.ainvoke @@ -173,48 +182,48 @@ def test_wrap_node_map_wraps_compiled_subgraph_with_ainvoke(self): class TestExtractAgentIdFromRunnableConfig: - def test_reads_aaasm_agent_id_from_configurable(self): + def test_reads_aaasm_agent_id_from_configurable(self) -> None: config = {"configurable": {"aaasm_agent_id": "parent-123"}} assert _extract_agent_id_from_runnable_config(config) == "parent-123" - def test_returns_none_when_key_absent(self): + def test_returns_none_when_key_absent(self) -> None: config = {"configurable": {"other_key": "value"}} assert _extract_agent_id_from_runnable_config(config) is None - def test_returns_none_for_empty_string(self): + def test_returns_none_for_empty_string(self) -> None: config = {"configurable": {"aaasm_agent_id": ""}} assert _extract_agent_id_from_runnable_config(config) is None - def test_returns_none_when_configurable_missing(self): + def test_returns_none_when_configurable_missing(self) -> None: config = {"metadata": {"aaasm_agent_id": "parent-xyz"}} assert _extract_agent_id_from_runnable_config(config) is None - def test_returns_none_for_non_dict_config(self): + def test_returns_none_for_non_dict_config(self) -> None: assert _extract_agent_id_from_runnable_config(None) is None assert _extract_agent_id_from_runnable_config("string") is None class TestIsToolNode: - def test_detects_tool_node_by_duck_typing(self): + def test_detects_tool_node_by_duck_typing(self) -> None: fake_tool_node = MagicMock(spec=["tools_by_name"]) fake_tool_node.tools_by_name = {"tool_a": MagicMock()} assert _is_tool_node(fake_tool_node) is True - def test_returns_false_for_compiled_subgraph(self): + def test_returns_false_for_compiled_subgraph(self) -> None: subgraph = _FakeSubgraph() assert _is_tool_node(subgraph) is False - def test_returns_false_for_plain_callable(self): + def test_returns_false_for_plain_callable(self) -> None: assert _is_tool_node(lambda x: x) is False - def test_returns_false_when_tools_by_name_not_dict(self): + def test_returns_false_when_tools_by_name_not_dict(self) -> None: obj = MagicMock() obj.tools_by_name = "not-a-dict" assert _is_tool_node(obj) is False class TestWrapToolNodeSubgraphs: - def test_wraps_compiled_subgraph_tool_inside_tool_node(self): + def test_wraps_compiled_subgraph_tool_inside_tool_node(self) -> None: subgraph = _FakeSubgraph() tool = MagicMock() tool.func = subgraph @@ -227,7 +236,7 @@ def test_wraps_compiled_subgraph_tool_inside_tool_node(self): assert result is True assert tool.func is not subgraph - def test_returns_false_when_no_compiled_subgraph_tools(self): + def test_returns_false_when_no_compiled_subgraph_tools(self) -> None: plain_func = lambda x: x # noqa: E731 — real function, not a compiled subgraph tool = MagicMock() @@ -240,10 +249,10 @@ def test_returns_false_when_no_compiled_subgraph_tools(self): assert result is False - def test_spawned_by_tool_and_delegation_reason_set_for_tool_node_path(self): + def test_spawned_by_tool_and_delegation_reason_set_for_tool_node_path(self) -> None: captured: list[SpawnContext | None] = [] subgraph = _FakeSubgraph() - subgraph.invoke = MagicMock(side_effect=lambda *_args, **_kwargs: captured.append(_SPAWN_CTX.get()) or "r") + subgraph.invoke = MagicMock(side_effect=lambda *_args, **_kwargs: captured.append(_SPAWN_CTX.get()) or "r") # type: ignore[func-returns-value] tool = MagicMock() tool.func = subgraph @@ -261,11 +270,12 @@ def test_spawned_by_tool_and_delegation_reason_set_for_tool_node_path(self): class TestWrapNodeMapDelegationReason: - def test_plain_subgraph_node_sets_delegation_reason(self): + def test_plain_subgraph_node_sets_delegation_reason(self) -> None: captured: list[SpawnContext | None] = [] subgraph = _FakeSubgraph() - subgraph.invoke = MagicMock(side_effect=lambda *_args, **_kwargs: captured.append(_SPAWN_CTX.get()) or "r") - node_map = {"my_subgraph": subgraph} + subgraph.invoke = MagicMock(side_effect=lambda *_args, **_kwargs: captured.append(_SPAWN_CTX.get()) or "r") # type: ignore[func-returns-value] + # _wrap_node_map replaces the value in place with a callable wrapper. + node_map: dict[str, Any] = {"my_subgraph": subgraph} _wrap_node_map(node_map, callback_handler=MagicMock(), process_agent_id="p") node_map["my_subgraph"]({}) @@ -275,7 +285,7 @@ def test_plain_subgraph_node_sets_delegation_reason(self): assert sc.spawned_by_tool is None assert sc.delegation_reason == "langgraph_node:my_subgraph" - def test_tool_node_in_map_triggers_tool_node_handling(self): + def test_tool_node_in_map_triggers_tool_node_handling(self) -> None: subgraph = _FakeSubgraph() tool = MagicMock() tool.func = subgraph diff --git a/test/unit/adapters/langgraph/test_edge_emission.py b/test/unit/adapters/langgraph/test_edge_emission.py index a72702f5..b68cf79e 100644 --- a/test/unit/adapters/langgraph/test_edge_emission.py +++ b/test/unit/adapters/langgraph/test_edge_emission.py @@ -11,9 +11,9 @@ class RecordingEdgeEmitter: """Synchronous test double that records emitted edges.""" def __init__(self) -> None: - self.edges: list[tuple[str, str, str, dict | None]] = [] + self.edges: list[tuple[str, str, str, dict[str, Any] | None]] = [] - def emit(self, source: str, target: str, edge_type: str, metadata: dict | None = None) -> None: + def emit(self, source: str, target: str, edge_type: str, metadata: dict[str, Any] | None = None) -> None: self.edges.append((source, target, edge_type, metadata)) @@ -31,6 +31,7 @@ def _make_node_map(*node_names: str, handler: Any) -> dict[str, Any]: """Build a fake compiled-graph node map with simple callables.""" node_map: dict[str, Any] = {} for name in node_names: + def make_func(n: str) -> Any: def node_fn(state: Any) -> dict[str, Any]: return {"node": n, **state} diff --git a/test/unit/adapters/mcp/test_patch.py b/test/unit/adapters/mcp/test_patch.py index 31d3f0d5..a6634ea2 100644 --- a/test/unit/adapters/mcp/test_patch.py +++ b/test/unit/adapters/mcp/test_patch.py @@ -120,7 +120,7 @@ async def check_tool_start(self, **kwargs: object) -> dict[str, str]: assert patcher.apply() is True session = FakeClientSession() - session._server_name = "stdlib-server" # type: ignore[attr-defined] + session._server_name = "stdlib-server" with pytest.raises( MCPToolBlockedError, match="blocked by governance policy: policy block", @@ -158,7 +158,7 @@ async def record_result(self, **kwargs: object) -> None: assert patcher.apply() is True session = FakeClientSession() - session._server_url = "https://api.mcp.test" # type: ignore[attr-defined] + session._server_url = "https://api.mcp.test" result = await session.call_tool("search", _ArgsMapping({"q": "hello"})) assert result["name"] == "search" @@ -189,7 +189,7 @@ async def wait_for_tool_approval(self, **kwargs: object) -> dict[str, str]: assert patcher.apply() is True session = FakeClientSession() - session._ws_url = "wss://tools.mcp.test" # type: ignore[attr-defined] + session._ws_url = "wss://tools.mcp.test" with pytest.raises( MCPToolBlockedError, match="rejected during approval: approval rejected", diff --git a/test/unit/adapters/openai_agents/test_edge_emission.py b/test/unit/adapters/openai_agents/test_edge_emission.py index 74eec0a7..5ddf1911 100644 --- a/test/unit/adapters/openai_agents/test_edge_emission.py +++ b/test/unit/adapters/openai_agents/test_edge_emission.py @@ -21,9 +21,9 @@ class RecordingEdgeEmitter: """Synchronous test double that records emitted edges.""" def __init__(self) -> None: - self.edges: list[tuple[str, str, str, dict | None]] = [] + self.edges: list[tuple[str, str, str, dict[str, Any] | None]] = [] - def emit(self, source: str, target: str, edge_type: str, metadata: dict | None = None) -> None: + def emit(self, source: str, target: str, edge_type: str, metadata: dict[str, Any] | None = None) -> None: self.edges.append((source, target, edge_type, metadata)) @@ -103,7 +103,7 @@ class NameOnlyHandoff: async def __call__(self, *_args: Any, **_kwargs: Any) -> str: return "handoff-result" - _apply_handoff_call_patch(NameOnlyHandoff, "agent-a") # type: ignore[arg-type] + _apply_handoff_call_patch(NameOnlyHandoff, "agent-a") h = NameOnlyHandoff() await h() diff --git a/test/unit/adapters/openai_agents/test_patch.py b/test/unit/adapters/openai_agents/test_patch.py index a9ee57e6..151bfec1 100644 --- a/test/unit/adapters/openai_agents/test_patch.py +++ b/test/unit/adapters/openai_agents/test_patch.py @@ -314,7 +314,7 @@ def check_tool_start(self, **kwargs: object) -> dict[str, str]: agent_id=None, ctx=SimpleNamespace(), ) - assert fallback_wait["status"] == "deny" + assert fallback_wait["status"] == "deny" # type: ignore[index] # fallback branch returns a dict (helper typed object) class SyncWait: def wait_for_tool_approval(self, **kwargs: object) -> dict[str, str]: diff --git a/test/unit/adapters/openai_agents/test_runner_spawn_patch.py b/test/unit/adapters/openai_agents/test_runner_spawn_patch.py index 09807b5d..a569921a 100644 --- a/test/unit/adapters/openai_agents/test_runner_spawn_patch.py +++ b/test/unit/adapters/openai_agents/test_runner_spawn_patch.py @@ -23,7 +23,7 @@ class TestLoadRunnerClass: - def test_returns_none_when_openai_agents_not_installed(self): + def test_returns_none_when_openai_agents_not_installed(self) -> None: with patch("importlib.import_module", side_effect=ImportError): result = _load_openai_agents_runner_class() assert result is None @@ -41,15 +41,15 @@ async def run(cls, agent: object, **_kwargs: object) -> str: class TestApplyRunnerRunPatch: - def setup_method(self): + def setup_method(self) -> None: set_process_agent_id("process-agent-001") for attr in (_RUNNER_PATCHED_FLAG, _ORIGINAL_RUNNER_RUN): if hasattr(FakeRunner, attr): delattr(FakeRunner, attr) # Restore to the original classmethod captured at class-definition time - FakeRunner.run = _FAKE_RUNNER_ORIGINAL_RUN + FakeRunner.run = _FAKE_RUNNER_ORIGINAL_RUN # type: ignore[method-assign] # fake method swap - def teardown_method(self): + def teardown_method(self) -> None: _revert_runner_run_patch(FakeRunner) set_process_agent_id(None) for attr in (_RUNNER_PATCHED_FLAG, _ORIGINAL_RUNNER_RUN): @@ -57,14 +57,14 @@ def teardown_method(self): delattr(FakeRunner, attr) @pytest.mark.asyncio - async def test_patched_run_sets_spawn_ctx(self): + async def test_patched_run_sets_spawn_ctx(self) -> None: captured: list[SpawnContext | None] = [] - async def capturing_run(agent, *, input, **kwargs): + async def capturing_run(agent: object, *, input: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeRunner.run = classmethod(capturing_run) + FakeRunner.run = classmethod(capturing_run) # type: ignore[arg-type,assignment,method-assign] _apply_runner_run_patch(FakeRunner, "process-agent-001") await FakeRunner.run(MagicMock(), input="hello") @@ -77,35 +77,35 @@ async def capturing_run(agent, *, input, **kwargs): assert sc.spawned_by_tool == "openai_agents_runner" @pytest.mark.asyncio - async def test_spawn_ctx_is_reset_after_run(self): - async def passthrough_run(agent, *, input, **kwargs): + async def test_spawn_ctx_is_reset_after_run(self) -> None: + async def passthrough_run(agent: object, *, input: object, **kwargs: object) -> str: return "ok" - FakeRunner.run = classmethod(passthrough_run) + FakeRunner.run = classmethod(passthrough_run) # type: ignore[arg-type,assignment,method-assign] _apply_runner_run_patch(FakeRunner, "process-agent-001") await FakeRunner.run(MagicMock(), input="x") assert _SPAWN_CTX.get() is None @pytest.mark.asyncio - async def test_spawn_ctx_reset_on_exception(self): - async def failing_run(agent, *, input, **kwargs): + async def test_spawn_ctx_reset_on_exception(self) -> None: + async def failing_run(agent: object, *, input: object, **kwargs: object) -> str: raise RuntimeError("runner failed") - FakeRunner.run = classmethod(failing_run) + FakeRunner.run = classmethod(failing_run) # type: ignore[arg-type,assignment,method-assign] _apply_runner_run_patch(FakeRunner, "process-agent-001") with pytest.raises(RuntimeError): await FakeRunner.run(MagicMock(), input="x") assert _SPAWN_CTX.get() is None - def test_idempotent_apply(self): + def test_idempotent_apply(self) -> None: _apply_runner_run_patch(FakeRunner, "process-agent-001") original_run = getattr(FakeRunner, _ORIGINAL_RUNNER_RUN, None) _apply_runner_run_patch(FakeRunner, "process-agent-001") assert getattr(FakeRunner, _ORIGINAL_RUNNER_RUN, None) is original_run - def test_revert_restores_original(self): + def test_revert_restores_original(self) -> None: _apply_runner_run_patch(FakeRunner, "process-agent-001") _revert_runner_run_patch(FakeRunner) assert not hasattr(FakeRunner, _RUNNER_PATCHED_FLAG) @@ -117,38 +117,38 @@ def test_revert_restores_original(self): class TestLoadHandoffClass: - def test_returns_none_when_openai_agents_not_installed(self): + def test_returns_none_when_openai_agents_not_installed(self) -> None: with patch("importlib.import_module", side_effect=ImportError): result = _load_openai_agents_handoff_class() assert result is None class TestExtractHandoffDelegationReason: - def test_reads_tool_description(self): + def test_reads_tool_description(self) -> None: obj = MagicMock() obj.tool_description = "Transfer control to agent B" assert _extract_handoff_delegation_reason(obj) == "Transfer control to agent B" - def test_falls_back_to_description(self): + def test_falls_back_to_description(self) -> None: obj = MagicMock(spec=["description"]) obj.description = "Handoff description" assert _extract_handoff_delegation_reason(obj) == "Handoff description" - def test_falls_back_to_reason(self): + def test_falls_back_to_reason(self) -> None: obj = MagicMock(spec=["reason"]) obj.reason = "some reason" assert _extract_handoff_delegation_reason(obj) == "some reason" - def test_fallback_when_no_description_attrs(self): + def test_fallback_when_no_description_attrs(self) -> None: obj = MagicMock(spec=[]) assert _extract_handoff_delegation_reason(obj) == "handoff" - def test_fallback_when_tool_description_is_empty_string(self): + def test_fallback_when_tool_description_is_empty_string(self) -> None: obj = MagicMock(spec=["tool_description"]) obj.tool_description = " " assert _extract_handoff_delegation_reason(obj) == "handoff" - def test_truncates_to_256_chars(self): + def test_truncates_to_256_chars(self) -> None: obj = MagicMock() obj.tool_description = "x" * 300 result = _extract_handoff_delegation_reason(obj) @@ -180,7 +180,7 @@ def teardown_method(self) -> None: delattr(FakeHandoff, attr) @pytest.mark.asyncio - async def test_patched_handoff_sets_spawn_ctx(self): + async def test_patched_handoff_sets_spawn_ctx(self) -> None: captured: list[SpawnContext | None] = [] class CapturingHandoff(FakeHandoff): @@ -199,7 +199,7 @@ async def __call__(self, *_args: object, **_kwargs: object) -> str: assert sc.depth == 1 @pytest.mark.asyncio - async def test_spawned_by_tool_is_none_for_handoff(self): + async def test_spawned_by_tool_is_none_for_handoff(self) -> None: captured: list[SpawnContext | None] = [] class CapturingHandoff(FakeHandoff): @@ -214,7 +214,7 @@ async def __call__(self, *_args: object, **_kwargs: object) -> str: assert captured[0].spawned_by_tool is None @pytest.mark.asyncio - async def test_delegation_reason_from_tool_description(self): + async def test_delegation_reason_from_tool_description(self) -> None: captured: list[SpawnContext | None] = [] class CapturingHandoff(FakeHandoff): @@ -230,7 +230,7 @@ async def __call__(self, *_args: object, **_kwargs: object) -> str: assert captured[0].delegation_reason == "Transfer to agent B" @pytest.mark.asyncio - async def test_delegation_reason_fallback_when_no_description(self): + async def test_delegation_reason_fallback_when_no_description(self) -> None: captured: list[SpawnContext | None] = [] class CapturingHandoff(FakeHandoff): @@ -248,14 +248,14 @@ async def __call__(self, *_args: object, **_kwargs: object) -> str: assert captured[0].delegation_reason == "handoff" @pytest.mark.asyncio - async def test_spawn_ctx_reset_after_handoff(self): + async def test_spawn_ctx_reset_after_handoff(self) -> None: _apply_handoff_call_patch(FakeHandoff, "process-agent-001") h = FakeHandoff() await h() assert _SPAWN_CTX.get() is None @pytest.mark.asyncio - async def test_spawn_ctx_reset_on_exception(self): + async def test_spawn_ctx_reset_on_exception(self) -> None: class FailingHandoff(FakeHandoff): async def __call__(self, *_args: object, **_kwargs: object) -> str: raise RuntimeError("handoff failed") @@ -265,20 +265,20 @@ async def __call__(self, *_args: object, **_kwargs: object) -> str: await FailingHandoff()() assert _SPAWN_CTX.get() is None - def test_idempotent_apply(self): + def test_idempotent_apply(self) -> None: _apply_handoff_call_patch(FakeHandoff, "process-agent-001") original = getattr(FakeHandoff, _ORIGINAL_HANDOFF_CALL, None) _apply_handoff_call_patch(FakeHandoff, "process-agent-001") assert getattr(FakeHandoff, _ORIGINAL_HANDOFF_CALL, None) is original - def test_revert_restores_original(self): + def test_revert_restores_original(self) -> None: _apply_handoff_call_patch(FakeHandoff, "process-agent-001") _revert_handoff_call_patch(FakeHandoff) assert not hasattr(FakeHandoff, _HANDOFF_PATCHED_FLAG) assert not hasattr(FakeHandoff, _ORIGINAL_HANDOFF_CALL) @pytest.mark.asyncio - async def test_depth_increments_when_inside_existing_spawn_ctx(self): + async def test_depth_increments_when_inside_existing_spawn_ctx(self) -> None: from agent_assembly.core.spawn import spawn_context_scope captured: list[SpawnContext | None] = [] diff --git a/test/unit/adapters/pydantic_ai/test_pydantic_ai_spawn_patch.py b/test/unit/adapters/pydantic_ai/test_pydantic_ai_spawn_patch.py index 7b1b3460..f144c31a 100644 --- a/test/unit/adapters/pydantic_ai/test_pydantic_ai_spawn_patch.py +++ b/test/unit/adapters/pydantic_ai/test_pydantic_ai_spawn_patch.py @@ -28,10 +28,10 @@ class FakeAgent: model = "fake-model" - async def run(self, *_args: object, **_kwargs: object) -> str: + async def run(self: object, *_args: object, **_kwargs: object) -> str: return "agent-result" - def run_sync(self, *_args: object, **_kwargs: object) -> str: + def run_sync(self: object, *_args: object, **_kwargs: object) -> str: return "sync-result" @@ -41,38 +41,38 @@ def run_sync(self, *_args: object, **_kwargs: object) -> str: class TestLoadPydanticAiAgentClass: - def test_returns_none_when_not_installed(self): + def test_returns_none_when_not_installed(self) -> None: with patch("importlib.import_module", side_effect=ImportError): assert _load_pydantic_ai_agent_class() is None class TestApplyAgentRunPatch: - def setup_method(self): + def setup_method(self) -> None: set_process_agent_id("pydantic-parent") - FakeAgent.run = _FAKE_AGENT_ORIGINAL_RUN - FakeAgent.run_sync = _FAKE_AGENT_ORIGINAL_RUN_SYNC + FakeAgent.run = _FAKE_AGENT_ORIGINAL_RUN # type: ignore[assignment,method-assign] # fake method swap + FakeAgent.run_sync = _FAKE_AGENT_ORIGINAL_RUN_SYNC # type: ignore[assignment,method-assign] for attr in (_AGENT_PATCHED_FLAG, _ORIGINAL_AGENT_RUN, _ORIGINAL_AGENT_RUN_SYNC): if hasattr(FakeAgent, attr): delattr(FakeAgent, attr) - def teardown_method(self): + def teardown_method(self) -> None: _revert_agent_run_patch(FakeAgent) set_process_agent_id(None) - FakeAgent.run = _FAKE_AGENT_ORIGINAL_RUN - FakeAgent.run_sync = _FAKE_AGENT_ORIGINAL_RUN_SYNC + FakeAgent.run = _FAKE_AGENT_ORIGINAL_RUN # type: ignore[assignment,method-assign] # fake method swap + FakeAgent.run_sync = _FAKE_AGENT_ORIGINAL_RUN_SYNC # type: ignore[assignment,method-assign] for attr in (_AGENT_PATCHED_FLAG, _ORIGINAL_AGENT_RUN, _ORIGINAL_AGENT_RUN_SYNC): if hasattr(FakeAgent, attr): delattr(FakeAgent, attr) @pytest.mark.asyncio - async def test_async_run_sets_spawn_ctx(self): + async def test_async_run_sets_spawn_ctx(self) -> None: captured: list[SpawnContext | None] = [] - async def capturing_run(self, *args, **kwargs): + async def capturing_run(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "ok" - FakeAgent.run = capturing_run + FakeAgent.run = capturing_run # type: ignore[method-assign] # reassign fake method to install/restore stub _apply_agent_run_patch(FakeAgent, "pydantic-parent") agent = FakeAgent() @@ -83,14 +83,14 @@ async def capturing_run(self, *args, **kwargs): assert captured[0].depth == 1 assert captured[0].spawned_by_tool == "pydantic_ai_agent" - def test_sync_run_sets_spawn_ctx(self): + def test_sync_run_sets_spawn_ctx(self) -> None: captured: list[SpawnContext | None] = [] - def capturing_run_sync(self, *args, **kwargs): + def capturing_run_sync(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "sync-ok" - FakeAgent.run_sync = capturing_run_sync + FakeAgent.run_sync = capturing_run_sync # type: ignore[method-assign] # fake method swap _apply_agent_run_patch(FakeAgent, "pydantic-parent") agent = FakeAgent() @@ -100,35 +100,35 @@ def capturing_run_sync(self, *args, **kwargs): assert captured[0].parent_agent_id == "pydantic-parent" @pytest.mark.asyncio - async def test_spawn_ctx_reset_after_async_run(self): + async def test_spawn_ctx_reset_after_async_run(self) -> None: _apply_agent_run_patch(FakeAgent, "pydantic-parent") agent = FakeAgent() await agent.run("x") assert _SPAWN_CTX.get() is None - def test_spawn_ctx_reset_after_sync_run(self): + def test_spawn_ctx_reset_after_sync_run(self) -> None: _apply_agent_run_patch(FakeAgent, "pydantic-parent") agent = FakeAgent() agent.run_sync("x") assert _SPAWN_CTX.get() is None @pytest.mark.asyncio - async def test_spawn_ctx_reset_on_exception_async(self): - async def failing_run(self, *args, **kwargs): + async def test_spawn_ctx_reset_on_exception_async(self) -> None: + async def failing_run(self: object, *args: object, **kwargs: object) -> str: raise RuntimeError("agent error") - FakeAgent.run = failing_run + FakeAgent.run = failing_run # type: ignore[method-assign] # reassign fake method to install/restore stub _apply_agent_run_patch(FakeAgent, "pydantic-parent") with pytest.raises(RuntimeError): await FakeAgent().run("x") assert _SPAWN_CTX.get() is None - def test_spawn_ctx_reset_on_exception_sync(self): - def failing_run_sync(self, *args, **kwargs): + def test_spawn_ctx_reset_on_exception_sync(self) -> None: + def failing_run_sync(self: object, *args: object, **kwargs: object) -> str: raise RuntimeError("sync agent error") - FakeAgent.run_sync = failing_run_sync + FakeAgent.run_sync = failing_run_sync # type: ignore[method-assign] # fake method swap _apply_agent_run_patch(FakeAgent, "pydantic-parent") with pytest.raises(RuntimeError): @@ -136,14 +136,14 @@ def failing_run_sync(self, *args, **kwargs): assert _SPAWN_CTX.get() is None @pytest.mark.asyncio - async def test_nested_depth_propagation(self): + async def test_nested_depth_propagation(self) -> None: captured: list[SpawnContext | None] = [] - async def capturing_run(self, *args, **kwargs): + async def capturing_run(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "ok" - FakeAgent.run = capturing_run + FakeAgent.run = capturing_run # type: ignore[method-assign] # reassign fake method to install/restore stub _apply_agent_run_patch(FakeAgent, "process-agent") outer_ctx = SpawnContext(parent_agent_id="grandparent", depth=2, spawned_by_tool="outer") @@ -156,13 +156,13 @@ async def capturing_run(self, *args, **kwargs): assert captured[0] is not None assert captured[0].depth == 3 - def test_idempotent_apply(self): + def test_idempotent_apply(self) -> None: _apply_agent_run_patch(FakeAgent, "pydantic-parent") first_original = getattr(FakeAgent, _ORIGINAL_AGENT_RUN, None) _apply_agent_run_patch(FakeAgent, "pydantic-parent") assert getattr(FakeAgent, _ORIGINAL_AGENT_RUN, None) is first_original - def test_revert_restores_original(self): + def test_revert_restores_original(self) -> None: _apply_agent_run_patch(FakeAgent, "pydantic-parent") _revert_agent_run_patch(FakeAgent) assert not hasattr(FakeAgent, _AGENT_PATCHED_FLAG) @@ -213,14 +213,14 @@ async def _run(self, _ctx: object, _args: object, **_kwargs: object) -> str: class TestApplyToolRunPatch: def setup_method(self) -> None: - FakeTool._run = _FAKE_TOOL_ORIGINAL_RUN + FakeTool._run = _FAKE_TOOL_ORIGINAL_RUN # type: ignore[assignment,method-assign] # fake method swap for attr in (_TOOLS_PATCHED_FLAG, _ORIGINAL_TOOL_RUN): if hasattr(FakeTool, attr): delattr(FakeTool, attr) def teardown_method(self) -> None: _revert_tool_run_patch(FakeTool) - FakeTool._run = _FAKE_TOOL_ORIGINAL_RUN + FakeTool._run = _FAKE_TOOL_ORIGINAL_RUN # type: ignore[assignment,method-assign] # fake method swap for attr in (_TOOLS_PATCHED_FLAG, _ORIGINAL_TOOL_RUN): if hasattr(FakeTool, attr): delattr(FakeTool, attr) @@ -233,7 +233,7 @@ async def capturing_run(self: object, ctx: object, args: object, **kw: object) - captured.append(_SPAWN_CTX.get()) return "ok" - FakeTool._run = capturing_run + FakeTool._run = capturing_run # type: ignore[assignment,method-assign] # fake method swap _apply_tool_run_patch(FakeTool, _FakeAllowHandler()) await FakeTool()._run(_FakeCtx(), {}) @@ -249,7 +249,7 @@ async def capturing_run(self: object, ctx: object, args: object, **kw: object) - captured.append(_SPAWN_CTX.get()) return "ok" - FakeTool._run = capturing_run + FakeTool._run = capturing_run # type: ignore[assignment,method-assign] # fake method swap _apply_tool_run_patch(FakeTool, _FakeAllowHandler()) await FakeTool()._run(_FakeCtx(), {}) @@ -265,7 +265,7 @@ async def capturing_run(self: object, ctx: object, args: object, **kw: object) - captured.append(_SPAWN_CTX.get()) return "ok" - FakeTool._run = capturing_run + FakeTool._run = capturing_run # type: ignore[assignment,method-assign] # fake method swap _apply_tool_run_patch(FakeTool, _FakeAllowHandler()) await FakeTool()._run(_FakeCtx(), {}) @@ -284,7 +284,7 @@ async def test_spawn_ctx_reset_on_tool_exception(self) -> None: async def failing_run(self: object, ctx: object, args: object, **kw: object) -> str: raise RuntimeError("tool broke") - FakeTool._run = failing_run + FakeTool._run = failing_run # type: ignore[assignment,method-assign] # fake method swap _apply_tool_run_patch(FakeTool, _FakeAllowHandler()) with pytest.raises(RuntimeError): @@ -299,7 +299,7 @@ async def should_not_be_called(self: object, ctx: object, args: object, **kw: ob called.append(True) return "should-not-run" - FakeTool._run = should_not_be_called + FakeTool._run = should_not_be_called # type: ignore[assignment,method-assign] # fake method swap _apply_tool_run_patch(FakeTool, _FakeDenyHandler()) from agent_assembly.exceptions import PolicyViolationError @@ -318,7 +318,7 @@ async def capturing_run(self: object, ctx: object, args: object, **kw: object) - captured.append(_SPAWN_CTX.get()) return "ok" - FakeTool._run = capturing_run + FakeTool._run = capturing_run # type: ignore[assignment,method-assign] # fake method swap _apply_tool_run_patch(FakeTool, _FakeAllowHandler()) outer = SpawnContext(parent_agent_id="parent", depth=3, spawned_by_tool="outer") diff --git a/test/unit/adapters/test_base.py b/test/unit/adapters/test_base.py index 1da5d37d..7db10895 100644 --- a/test/unit/adapters/test_base.py +++ b/test/unit/adapters/test_base.py @@ -13,7 +13,7 @@ def get_framework_name(self) -> str: def test_framework_adapter_requires_all_abstract_methods() -> None: with pytest.raises(TypeError): - IncompleteAdapter() + IncompleteAdapter() # type: ignore[abstract] # deliberately instantiate abstract class to assert TypeError class AvailableFrameworkAdapter(FrameworkAdapter): diff --git a/test/unit/adapters/test_registry.py b/test/unit/adapters/test_registry.py index a54ea235..dc3edf46 100644 --- a/test/unit/adapters/test_registry.py +++ b/test/unit/adapters/test_registry.py @@ -86,7 +86,7 @@ def get_supported_versions(self) -> list[str]: return [">=1.0.0"] def register_hooks(self, interceptor: GovernanceInterceptor) -> None: - interceptor.record_event("adapter-registered") + interceptor.record_event("adapter-registered") # type: ignore[attr-defined] # attr not on the static stub; set/used dynamically self.hook_registered = True def unregister_hooks(self) -> None: diff --git a/test/unit/client/test_dispatch_tool.py b/test/unit/client/test_dispatch_tool.py index b8c16a38..204b3d0e 100644 --- a/test/unit/client/test_dispatch_tool.py +++ b/test/unit/client/test_dispatch_tool.py @@ -2,6 +2,8 @@ from __future__ import annotations +from contextlib import AbstractContextManager +from typing import Any from unittest.mock import MagicMock, patch import httpx @@ -11,7 +13,7 @@ from agent_assembly.exceptions import GatewayError -def _make_response(status_code: int = 200, payload: dict | None = None) -> MagicMock: +def _make_response(status_code: int = 200, payload: dict[str, Any] | None = None) -> MagicMock: resp = MagicMock() resp.status_code = status_code resp.json.return_value = payload or {} @@ -28,7 +30,7 @@ def _make_response(status_code: int = 200, payload: dict | None = None) -> Magic return resp -def _patched_client(client: GatewayClient, mock_post: MagicMock) -> object: +def _patched_client(client: GatewayClient, mock_post: MagicMock) -> AbstractContextManager[Any]: return patch.object( type(client), "client", diff --git a/test/unit/core/test_gateway_resolver.py b/test/unit/core/test_gateway_resolver.py index c4f6879d..4c2246d7 100644 --- a/test/unit/core/test_gateway_resolver.py +++ b/test/unit/core/test_gateway_resolver.py @@ -26,7 +26,7 @@ def test_returns_false_when_httpx_raises(self) -> None: with patch(f"{_RESOLVER_MOD}.httpx.get", side_effect=httpx.ConnectError("refused")): assert gateway_resolver._probe_healthz("http://localhost:7391") is False - @pytest.mark.parametrize("status", [400, 404, 500, 503]) # type: ignore[misc] + @pytest.mark.parametrize("status", [400, 404, 500, 503]) def test_returns_false_on_non_2xx(self, status: int) -> None: fake_response = MagicMock(status_code=status) with patch(f"{_RESOLVER_MOD}.httpx.get", return_value=fake_response): diff --git a/test/unit/core/test_spawn_context.py b/test/unit/core/test_spawn_context.py index 214e8efb..d576596f 100644 --- a/test/unit/core/test_spawn_context.py +++ b/test/unit/core/test_spawn_context.py @@ -1,44 +1,46 @@ from __future__ import annotations +from typing import Any + import pytest from agent_assembly.core.spawn import _SPAWN_CTX, SpawnContext, spawn_context_scope class TestSpawnContext: - def test_dataclass_fields(self): + def test_dataclass_fields(self) -> None: ctx = SpawnContext(parent_agent_id="parent-1", depth=2, spawned_by_tool="tool-x") assert ctx.parent_agent_id == "parent-1" assert ctx.depth == 2 assert ctx.spawned_by_tool == "tool-x" - def test_spawned_by_tool_defaults_to_none(self): + def test_spawned_by_tool_defaults_to_none(self) -> None: ctx = SpawnContext(parent_agent_id="parent-1", depth=1) assert ctx.spawned_by_tool is None - def test_delegation_reason_defaults_to_none(self): + def test_delegation_reason_defaults_to_none(self) -> None: ctx = SpawnContext(parent_agent_id="parent-1", depth=1) assert ctx.delegation_reason is None - def test_delegation_reason_stored(self): + def test_delegation_reason_stored(self) -> None: ctx = SpawnContext(parent_agent_id="p", depth=1, delegation_reason="langgraph_node:call_llm") assert ctx.delegation_reason == "langgraph_node:call_llm" - def test_spawn_context_scope_sets_and_resets(self): + def test_spawn_context_scope_sets_and_resets(self) -> None: assert _SPAWN_CTX.get() is None ctx = SpawnContext(parent_agent_id="p", depth=1) with spawn_context_scope(ctx): assert _SPAWN_CTX.get() is ctx assert _SPAWN_CTX.get() is None - def test_spawn_context_scope_resets_on_exception(self): + def test_spawn_context_scope_resets_on_exception(self) -> None: assert _SPAWN_CTX.get() is None ctx = SpawnContext(parent_agent_id="p", depth=1) with pytest.raises(RuntimeError), spawn_context_scope(ctx): raise RuntimeError("boom") assert _SPAWN_CTX.get() is None - def test_spawn_context_scope_nested(self): + def test_spawn_context_scope_nested(self) -> None: outer = SpawnContext(parent_agent_id="outer", depth=1) inner = SpawnContext(parent_agent_id="inner", depth=2) with spawn_context_scope(outer): @@ -51,7 +53,7 @@ def test_spawn_context_scope_nested(self): class TestInitAssemblySpawnContextAutoRead: """init_assembly() must auto-fill parent_agent_id/depth from _SPAWN_CTX.""" - def test_auto_reads_parent_agent_id_from_spawn_ctx(self, monkeypatch): + def test_auto_reads_parent_agent_id_from_spawn_ctx(self, monkeypatch: pytest.MonkeyPatch) -> None: from unittest.mock import MagicMock, patch from agent_assembly.core import assembly @@ -62,9 +64,9 @@ def test_auto_reads_parent_agent_id_from_spawn_ctx(self, monkeypatch): monkeypatch.setattr(assembly, "_ACTIVE_CONTEXT", None) - captured: dict = {} + captured: dict[str, Any] = {} - def fake_gateway_client(**kwargs): + def fake_gateway_client(**kwargs: Any) -> Any: captured.update(kwargs) m = MagicMock() m.gateway_url = "http://gw" @@ -77,9 +79,7 @@ def fake_gateway_client(**kwargs): with ( patch("agent_assembly.core.assembly.GatewayClient", side_effect=fake_gateway_client), patch("agent_assembly.core.assembly._register_adapters", return_value=[]), - patch( - "agent_assembly.core.assembly._start_network_layer", return_value=("sdk-only", lambda: None) - ), + patch("agent_assembly.core.assembly._start_network_layer", return_value=("sdk-only", lambda: None)), spawn_context_scope(spawn_ctx), ): ctx = assembly.init_assembly( @@ -94,16 +94,16 @@ def fake_gateway_client(**kwargs): assert captured["depth"] == 1 assert captured["spawned_by_tool"] == "runner" - def test_explicit_parent_agent_id_overrides_spawn_ctx(self, monkeypatch): + def test_explicit_parent_agent_id_overrides_spawn_ctx(self, monkeypatch: pytest.MonkeyPatch) -> None: from unittest.mock import MagicMock, patch from agent_assembly.core import assembly from agent_assembly.core.spawn import SpawnContext, spawn_context_scope monkeypatch.setattr(assembly, "_ACTIVE_CONTEXT", None) - captured: dict = {} + captured: dict[str, Any] = {} - def fake_gateway_client(**kwargs): + def fake_gateway_client(**kwargs: Any) -> Any: captured.update(kwargs) m = MagicMock() m.gateway_url = "http://gw" @@ -116,9 +116,7 @@ def fake_gateway_client(**kwargs): with ( patch("agent_assembly.core.assembly.GatewayClient", side_effect=fake_gateway_client), patch("agent_assembly.core.assembly._register_adapters", return_value=[]), - patch( - "agent_assembly.core.assembly._start_network_layer", return_value=("sdk-only", lambda: None) - ), + patch("agent_assembly.core.assembly._start_network_layer", return_value=("sdk-only", lambda: None)), spawn_context_scope(spawn_ctx), ): ctx = assembly.init_assembly( @@ -134,15 +132,15 @@ def fake_gateway_client(**kwargs): assert captured["depth"] == 2 # ctx-provided depth was still used assert captured.get("spawned_by_tool") is None # ctx had no spawned_by_tool - def test_no_spawn_ctx_leaves_parent_agent_id_none(self, monkeypatch): + def test_no_spawn_ctx_leaves_parent_agent_id_none(self, monkeypatch: pytest.MonkeyPatch) -> None: from unittest.mock import MagicMock, patch from agent_assembly.core import assembly monkeypatch.setattr(assembly, "_ACTIVE_CONTEXT", None) - captured: dict = {} + captured: dict[str, Any] = {} - def fake_gateway_client(**kwargs): + def fake_gateway_client(**kwargs: Any) -> Any: captured.update(kwargs) m = MagicMock() m.gateway_url = "http://gw" @@ -153,9 +151,7 @@ def fake_gateway_client(**kwargs): with ( patch("agent_assembly.core.assembly.GatewayClient", side_effect=fake_gateway_client), patch("agent_assembly.core.assembly._register_adapters", return_value=[]), - patch( - "agent_assembly.core.assembly._start_network_layer", return_value=("sdk-only", lambda: None) - ), + patch("agent_assembly.core.assembly._start_network_layer", return_value=("sdk-only", lambda: None)), ): ctx = assembly.init_assembly( gateway_url="http://gw", diff --git a/test/unit/test_assembly.py b/test/unit/test_assembly.py index e2063a77..beeafbc4 100644 --- a/test/unit/test_assembly.py +++ b/test/unit/test_assembly.py @@ -135,7 +135,7 @@ def _fail_auto_start(_url: str = "") -> None: def test_mode_sdk_only_skips_network_layer() -> None: - network_mode, shutdown = core_assembly._start_network_layer(client=object(), mode="sdk-only") + network_mode, shutdown = core_assembly._start_network_layer(client=object(), mode="sdk-only") # type: ignore[arg-type] # placeholder client assert network_mode == "sdk-only" assert callable(shutdown) @@ -146,7 +146,7 @@ def test_mode_auto_uses_proxy_when_ebpf_is_not_supported( monkeypatch.setattr(core_assembly, "_platform_supports_ebpf", lambda: False) monkeypatch.setattr(core_assembly, "_start_mitm_proxy", lambda client: lambda: None) - network_mode, shutdown = core_assembly._start_network_layer(client=object(), mode="auto") + network_mode, shutdown = core_assembly._start_network_layer(client=object(), mode="auto") # type: ignore[arg-type] # placeholder client assert network_mode == "proxy" assert callable(shutdown) @@ -158,7 +158,7 @@ def test_mode_auto_uses_ebpf_when_supported( monkeypatch.setattr(core_assembly, "_platform_supports_ebpf", lambda: True) monkeypatch.setattr(core_assembly, "_start_ebpf_probes", lambda client: lambda: None) - network_mode, shutdown = core_assembly._start_network_layer(client=object(), mode="auto") + network_mode, shutdown = core_assembly._start_network_layer(client=object(), mode="auto") # type: ignore[arg-type] # placeholder client assert network_mode == "ebpf" assert callable(shutdown) @@ -167,7 +167,7 @@ def test_mode_proxy_forces_proxy_path( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(core_assembly, "_start_mitm_proxy", lambda client: lambda: None) - network_mode, shutdown = core_assembly._start_network_layer(client=object(), mode="proxy") + network_mode, shutdown = core_assembly._start_network_layer(client=object(), mode="proxy") # type: ignore[arg-type] # placeholder client assert network_mode == "proxy" assert callable(shutdown) @@ -178,7 +178,7 @@ def test_mode_ebpf_raises_on_unsupported_platform( monkeypatch.setattr(core_assembly, "_platform_supports_ebpf", lambda: False) with pytest.raises(ConfigurationError): - core_assembly._start_network_layer(client=object(), mode="ebpf") + core_assembly._start_network_layer(client=object(), mode="ebpf") # type: ignore[arg-type] # placeholder client def test_context_manager_shutdown_calls_adapter_unregister_hooks( diff --git a/test/unit/test_audit_event_wire_roundtrip.py b/test/unit/test_audit_event_wire_roundtrip.py index 5021ca59..615ce476 100644 --- a/test/unit/test_audit_event_wire_roundtrip.py +++ b/test/unit/test_audit_event_wire_roundtrip.py @@ -107,5 +107,5 @@ def test_call_stack_node_kind_outside_literal_round_trips_unchanged() -> None: decoded = AuditEvent.from_wire_bytes(original.to_wire_bytes()) - assert decoded.call_stack[0].kind == "unknown" + assert decoded.call_stack[0].kind == "unknown" # type: ignore[comparison-overlap] # round-trip yields out-of-enum "unknown" wire fallback assert decoded == original diff --git a/test/unit/test_init_exports.py b/test/unit/test_init_exports.py index 79f4a96a..9131938e 100644 --- a/test/unit/test_init_exports.py +++ b/test/unit/test_init_exports.py @@ -12,8 +12,10 @@ class RuntimeClient: ... class GovernanceEvent: ... - fake_core.RuntimeClient = RuntimeClient - fake_core.GovernanceEvent = GovernanceEvent + # Attrs do not exist on the ModuleType stub; this fake mimics the native + # extension exporting these symbols. + fake_core.RuntimeClient = RuntimeClient # type: ignore[attr-defined] # attr not on the static stub; set/used dynamically + fake_core.GovernanceEvent = GovernanceEvent # type: ignore[attr-defined] # attr not on the static stub; set/used dynamically original_package = sys.modules.pop("agent_assembly", None) original_core = sys.modules.get("agent_assembly._core") diff --git a/test/unit/test_install.py b/test/unit/test_install.py index df8a3d56..6602ead0 100644 --- a/test/unit/test_install.py +++ b/test/unit/test_install.py @@ -10,7 +10,7 @@ @pytest.fixture -def isolate_runtime(monkeypatch, tmp_path: Path) -> Path: +def isolate_runtime(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: """Isolate ensure_runtime() from the host environment. Yields a context where: @@ -26,7 +26,7 @@ def isolate_runtime(monkeypatch, tmp_path: Path) -> Path: return fake_binary -def test_ensure_runtime_returns_path_match_first(monkeypatch, tmp_path: Path) -> None: +def test_ensure_runtime_returns_path_match_first(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: """When `aasm` is on PATH, ensure_runtime returns that resolved path.""" import stat diff --git a/test/unit/test_op_control.py b/test/unit/test_op_control.py index b92bcc49..d5a819ab 100644 --- a/test/unit/test_op_control.py +++ b/test/unit/test_op_control.py @@ -70,11 +70,18 @@ def _agent(name: str = "agent-7") -> common_pb2.AgentId: def _msg(op_id: str, signal: int, sequence: int = 0) -> policy_pb2.OpControlMessage: - return policy_pb2.OpControlMessage(op_id=op_id, signal=signal, sequence=sequence) + # The generated proto stub types `signal` as `OpControlSignal | str | None`, + # but the OP_CONTROL_SIGNAL_* constants are plain ints at runtime. + return policy_pb2.OpControlMessage(op_id=op_id, signal=signal, sequence=sequence) # type: ignore[arg-type] + + +# What the `subscriber` fixture yields: the subscriber under test plus the +# queue-backed stream and stub the test drives it through. +_Subscriber = tuple[OpControlSubscriber, "_QueueStream", "_FakeStub"] @pytest.fixture -def subscriber(): +def subscriber() -> Iterator[_Subscriber]: stream = _QueueStream() stub = _FakeStub(stream) sub = OpControlSubscriber(stub, _agent()) @@ -86,13 +93,13 @@ def subscriber(): sub.close() -def test_await_op_returns_immediately_for_unknown_op(subscriber): +def test_await_op_returns_immediately_for_unknown_op(subscriber: _Subscriber) -> None: sub, _, _ = subscriber # No signal ever arrived for this op_id; await_op should be a no-op. sub.await_op("never-seen", timeout=0.1) -def test_pause_blocks_until_resume(subscriber): +def test_pause_blocks_until_resume(subscriber: _Subscriber) -> None: sub, stream, _ = subscriber stream.push(_msg("op-1", policy_pb2.OP_CONTROL_SIGNAL_PAUSE)) # Give the reader a moment to dispatch the pause. @@ -120,7 +127,7 @@ def waiter() -> None: assert not sub.is_paused("op-1") -def test_terminate_raises_op_terminated_error(subscriber): +def test_terminate_raises_op_terminated_error(subscriber: _Subscriber) -> None: sub, stream, _ = subscriber stream.push(_msg("op-2", policy_pb2.OP_CONTROL_SIGNAL_TERMINATE)) for _ in range(50): @@ -134,7 +141,7 @@ def test_terminate_raises_op_terminated_error(subscriber): assert exc_info.value.op_id == "op-2" -def test_terminate_unblocks_waiter_and_raises(subscriber): +def test_terminate_unblocks_waiter_and_raises(subscriber: _Subscriber) -> None: sub, stream, _ = subscriber stream.push(_msg("op-3", policy_pb2.OP_CONTROL_SIGNAL_PAUSE)) for _ in range(50): @@ -163,7 +170,7 @@ def waiter() -> None: assert captured[0].op_id == "op-3" -def test_signal_for_unknown_op_is_buffered_until_first_await(subscriber): +def test_signal_for_unknown_op_is_buffered_until_first_await(subscriber: _Subscriber) -> None: sub, stream, _ = subscriber # Pause arrives before anyone is awaiting — must still be remembered. stream.push(_msg("op-4", policy_pb2.OP_CONTROL_SIGNAL_PAUSE)) @@ -188,7 +195,7 @@ def waiter() -> None: t.join(timeout=1.0) -def test_subscribe_request_carries_composite_agent_id(subscriber): +def test_subscribe_request_carries_composite_agent_id(subscriber: _Subscriber) -> None: _, _, stub = subscriber assert stub.last_request is not None assert stub.last_request.agent_id.org_id == "org" @@ -196,7 +203,7 @@ def test_subscribe_request_carries_composite_agent_id(subscriber): assert stub.last_request.agent_id.agent_id == "agent-7" -def test_close_marks_stream_dead_and_wakes_waiters(subscriber): +def test_close_marks_stream_dead_and_wakes_waiters(subscriber: _Subscriber) -> None: sub, stream, _ = subscriber stream.push(_msg("op-5", policy_pb2.OP_CONTROL_SIGNAL_PAUSE)) for _ in range(50):