diff --git a/agent_assembly/adapters/openai_agents/patch.py b/agent_assembly/adapters/openai_agents/patch.py index a0decd48..0e6c260e 100644 --- a/agent_assembly/adapters/openai_agents/patch.py +++ b/agent_assembly/adapters/openai_agents/patch.py @@ -510,6 +510,87 @@ def patched_init(self: Any, *args: Any, **kwargs: Any) -> None: return None +async def _run_async_tool_pre_execution_check( + callback_handler: Any, + *, + tool_name: str, + tool_input: Any, + agent_id: Any, + ctx: Any, + enforce: bool, +) -> tuple[bool, Any, bool]: + """Run the pre-execution governance check for a tool invocation. + + Returns ``(return_now, return_value, governance_failed)``. When + ``return_now`` is True the caller must immediately return ``return_value`` + (a deny/blocked result); otherwise the tool call may proceed. + """ + try: + decision = await _invoke_async_tool_check( + callback_handler, + tool_name=tool_name, + tool_input=tool_input, + agent_id=agent_id, + ctx=ctx, + ) + status, reason = _normalize_decision(decision, enforce=enforce) + is_pending_flow = False + if status == "pending": + is_pending_flow = True + timeout_seconds = _get_pending_tool_approval_timeout_seconds(callback_handler) + final_decision = await _wait_for_async_tool_approval( + callback_handler, + tool_name=tool_name, + timeout_seconds=timeout_seconds, + tool_input=tool_input, + agent_id=agent_id, + ctx=ctx, + ) + status, reason = _normalize_decision(final_decision, enforce=enforce) + + # Fail closed: only an explicit "allow" may proceed. A terminal + # "pending" (approval timed out or the resolver returned pending + # again) is a non-decision, not a grant — blocking it here stops it + # from falling through and running the tool, matching LangChain. + if status != "allow": + blocked_result = _build_tool_deny_error( + tool_name=tool_name, + reason=reason, + is_pending_rejection=is_pending_flow, + ) + # Guard the audit so its failure cannot re-enter this handler and + # downgrade a decided deny to an allow (AAASM-4782). + await _record_denied_tool_result( + callback_handler, + tool_name=tool_name, + tool_input=tool_input, + result=blocked_result, + agent_id=agent_id, + ctx=ctx, + ) + return True, blocked_result, False + except Exception as error: + governance_failed = _is_governance_error(error) + if not governance_failed: + raise + # A governance-layer fault during the pre-execution check. Under + # enforce the SDK is a security control: fail closed by denying the + # call rather than running the tool ungoverned — matching every other + # adapter, which never swallows a governance error into an allow. + # Under observe/disabled fall through to run the tool (fail-open by + # design), preserving the dry-run/hermetic posture (AAASM-4782). + if enforce: + deny = _build_tool_deny_error( + tool_name=tool_name, + reason=_GOVERNANCE_FAULT_DENY_REASON, + is_pending_rejection=False, + ) + return True, deny, True + return False, None, True + + return False, None, False + + def _wrap_on_invoke_tool(tool_obj: Any, callback_handler: Any) -> None: """Wrap a single tool instance's ``on_invoke_tool`` coroutine with governance.""" original_invoke = getattr(tool_obj, "on_invoke_tool", None) @@ -525,67 +606,16 @@ async def governed_invoke(ctx: Any, tool_input: Any) -> Any: tool_name = str(getattr(tool_obj, "name", tool_obj.__class__.__name__)) agent_id = _resolve_agent_id(ctx) - governance_failed = False - try: - decision = await _invoke_async_tool_check( - callback_handler, - tool_name=tool_name, - tool_input=tool_input, - agent_id=agent_id, - ctx=ctx, - ) - status, reason = _normalize_decision(decision, enforce=enforce) - is_pending_flow = False - if status == "pending": - is_pending_flow = True - timeout_seconds = _get_pending_tool_approval_timeout_seconds(callback_handler) - final_decision = await _wait_for_async_tool_approval( - callback_handler, - tool_name=tool_name, - timeout_seconds=timeout_seconds, - tool_input=tool_input, - agent_id=agent_id, - ctx=ctx, - ) - status, reason = _normalize_decision(final_decision, enforce=enforce) - - # Fail closed: only an explicit "allow" may proceed. A terminal - # "pending" (approval timed out or the resolver returned pending - # again) is a non-decision, not a grant — blocking it here stops it - # from falling through and running the tool, matching LangChain. - if status != "allow": - blocked_result = _build_tool_deny_error( - tool_name=tool_name, - reason=reason, - is_pending_rejection=is_pending_flow, - ) - # Guard the audit so its failure cannot re-enter this handler and - # downgrade a decided deny to an allow (AAASM-4782). - await _record_denied_tool_result( - callback_handler, - tool_name=tool_name, - tool_input=tool_input, - result=blocked_result, - agent_id=agent_id, - ctx=ctx, - ) - return blocked_result - except Exception as error: - governance_failed = _is_governance_error(error) - if not governance_failed: - raise - # A governance-layer fault during the pre-execution check. Under - # enforce the SDK is a security control: fail closed by denying the - # call rather than running the tool ungoverned — matching every other - # adapter, which never swallows a governance error into an allow. - # Under observe/disabled fall through to run the tool (fail-open by - # design), preserving the dry-run/hermetic posture (AAASM-4782). - if enforce: - return _build_tool_deny_error( - tool_name=tool_name, - reason=_GOVERNANCE_FAULT_DENY_REASON, - is_pending_rejection=False, - ) + return_now, return_value, governance_failed = await _run_async_tool_pre_execution_check( + callback_handler, + tool_name=tool_name, + tool_input=tool_input, + agent_id=agent_id, + ctx=ctx, + enforce=enforce, + ) + if return_now: + return return_value result = original_invoke(ctx, tool_input) if inspect.isawaitable(result): diff --git a/test/bench/conftest.py b/test/bench/conftest.py index 5b17c758..7f29d7c3 100644 --- a/test/bench/conftest.py +++ b/test/bench/conftest.py @@ -12,7 +12,7 @@ MAX_DETECTION_NS = 50_000_000 # <50ms detection overhead (AAASM-47) -@pytest.fixture() +@pytest.fixture def mock_gateway_client() -> MagicMock: """Return a MagicMock that satisfies GatewayClient interface.""" client = MagicMock() @@ -23,7 +23,7 @@ def mock_gateway_client() -> MagicMock: return client -@pytest.fixture() +@pytest.fixture def noop_interceptor() -> _NoopInterceptor: """Return a no-op governance interceptor for benchmarking hooks.""" return _NoopInterceptor() diff --git a/test/bench/test_latency_contracts.py b/test/bench/test_latency_contracts.py index 3b9adbce..9792f987 100644 --- a/test/bench/test_latency_contracts.py +++ b/test/bench/test_latency_contracts.py @@ -328,12 +328,12 @@ def test_detection_latency_under_50ms() -> None: # --------------------------------------------------------------------------- -def test_init_assembly_coldstart_latency() -> None: +def test_init_assembly_coldstart_latency(monkeypatch: pytest.MonkeyPatch) -> None: """Measure init_assembly() cold-start P50/P95/P99.""" samples: list[int] = [] for _ in range(_ITERATIONS): - assembly_mod._ACTIVE_CONTEXT = None + monkeypatch.setattr(assembly_mod, "_ACTIVE_CONTEXT", None) start = time.perf_counter_ns() ctx = init_assembly( diff --git a/test/integration/test_native_core_runtime.py b/test/integration/test_native_core_runtime.py index ae2e1ec5..103e4cd2 100644 --- a/test/integration/test_native_core_runtime.py +++ b/test/integration/test_native_core_runtime.py @@ -148,7 +148,7 @@ def make_audit_entry_payload(index: int, *, worker_id: int = 0) -> str: ) -@pytest.fixture() +@pytest.fixture 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.") diff --git a/test/unit/adapters/crewai/test_crewai_spawn_context.py b/test/unit/adapters/crewai/test_crewai_spawn_context.py index 932366de..20170f3d 100644 --- a/test/unit/adapters/crewai/test_crewai_spawn_context.py +++ b/test/unit/adapters/crewai/test_crewai_spawn_context.py @@ -65,14 +65,14 @@ def setup_method(self) -> None: def teardown_method(self) -> None: _revert_task_execute_sync_patch(FakeTask) - def test_spawn_ctx_set_during_execute_sync(self) -> None: + def test_spawn_ctx_set_during_execute_sync(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap + monkeypatch.setattr(FakeTask, "execute_sync", capturing_execute) _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id="worker-x") task.execute_sync() @@ -87,11 +87,11 @@ def test_spawn_ctx_reset_after_execute_sync(self) -> None: task.execute_sync() assert _SPAWN_CTX.get() is None - def test_spawn_ctx_reset_on_exception(self) -> None: + def test_spawn_ctx_reset_on_exception(self, monkeypatch: pytest.MonkeyPatch) -> None: def failing_execute(self: object, *args: object, **kwargs: object) -> str: raise RuntimeError("task failed") - FakeTask.execute_sync = failing_execute # type: ignore[method-assign] # fake method swap + monkeypatch.setattr(FakeTask, "execute_sync", failing_execute) _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id="worker-z") @@ -99,28 +99,28 @@ def failing_execute(self: object, *args: object, **kwargs: object) -> str: task.execute_sync() assert _SPAWN_CTX.get() is None - def test_no_spawn_ctx_when_no_agent_id(self) -> None: + def test_no_spawn_ctx_when_no_agent_id(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap + monkeypatch.setattr(FakeTask, "execute_sync", capturing_execute) _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) -> None: + def test_team_id_extracted_from_agent_crew(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap + monkeypatch.setattr(FakeTask, "execute_sync", capturing_execute) _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id="worker-a") crew = MagicMock() @@ -131,14 +131,14 @@ def capturing_execute(self: object, *args: object, **kwargs: object) -> str: assert captured[0] is not None assert captured[0].team_id == "crew-uuid-123" - def test_team_id_none_when_no_crew(self) -> None: + def test_team_id_none_when_no_crew(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap + monkeypatch.setattr(FakeTask, "execute_sync", capturing_execute) _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: object, *args: object, **kwargs: object) -> str: assert captured[0] is not None assert captured[0].team_id is None - def test_delegation_reason_from_task_description(self) -> None: + def test_delegation_reason_from_task_description(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap + monkeypatch.setattr(FakeTask, "execute_sync", capturing_execute) _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: object, *args: object, **kwargs: object) -> str: assert captured[0] is not None assert captured[0].delegation_reason == "Analyze quarterly reports" - def test_delegation_reason_truncated_to_256_chars(self) -> None: + def test_delegation_reason_truncated_to_256_chars(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap + monkeypatch.setattr(FakeTask, "execute_sync", capturing_execute) _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: object, *args: object, **kwargs: object) -> str: 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) -> None: + def test_delegation_reason_none_when_description_empty(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] def capturing_execute(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeTask.execute_sync = capturing_execute # type: ignore[method-assign] # fake method swap + monkeypatch.setattr(FakeTask, "execute_sync", capturing_execute) _apply_task_execute_sync_patch(FakeTask, MagicMock()) task = FakeTask(agent_id="worker-e") task.description = "" @@ -280,17 +280,16 @@ def teardown_method(self) -> None: if hasattr(FakeCrew, attr): delattr(FakeCrew, attr) - def test_non_hierarchical_kickoff_bypasses_spawn_ctx(self) -> None: + def test_non_hierarchical_kickoff_bypasses_spawn_ctx(self, monkeypatch: pytest.MonkeyPatch) -> None: from unittest.mock import patch captured: list[SpawnContext | None] = [] - original = FakeCrew.kickoff def capturing_kickoff(self: object, *_args: object, **_kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeCrew.kickoff = capturing_kickoff # type: ignore[method-assign] # fake method swap + monkeypatch.setattr(FakeCrew, "kickoff", capturing_kickoff) _apply_crew_kickoff_patch(FakeCrew) crew = FakeCrew(hierarchical=False) @@ -301,19 +300,17 @@ def capturing_kickoff(self: object, *_args: object, **_kwargs: object) -> str: crew.kickoff() assert captured[0] is None - FakeCrew.kickoff = original # type: ignore[method-assign] # reassign fake method to install/restore stub - def test_hierarchical_kickoff_sets_spawn_ctx(self) -> None: + def test_hierarchical_kickoff_sets_spawn_ctx(self, monkeypatch: pytest.MonkeyPatch) -> None: from unittest.mock import patch captured: list[SpawnContext | None] = [] - original = FakeCrew.kickoff def capturing_kickoff(self: object, *_args: object, **_kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeCrew.kickoff = capturing_kickoff # type: ignore[method-assign] # fake method swap + monkeypatch.setattr(FakeCrew, "kickoff", capturing_kickoff) _apply_crew_kickoff_patch(FakeCrew) crew = FakeCrew(hierarchical=True, manager_id="mgr-42", crew_id="crew-99") @@ -329,7 +326,6 @@ def capturing_kickoff(self: object, *_args: object, **_kwargs: object) -> str: assert sc.team_id == "crew-99" assert sc.spawned_by_tool == "crewai_kickoff_hierarchical" assert sc.depth == 1 - FakeCrew.kickoff = original # type: ignore[method-assign] # reassign fake method to install/restore stub def test_spawn_ctx_reset_after_hierarchical_kickoff(self) -> None: from unittest.mock import patch diff --git a/test/unit/adapters/langgraph/test_edge_emission.py b/test/unit/adapters/langgraph/test_edge_emission.py index b68cf79e..4d1ab047 100644 --- a/test/unit/adapters/langgraph/test_edge_emission.py +++ b/test/unit/adapters/langgraph/test_edge_emission.py @@ -4,6 +4,8 @@ from typing import Any +import pytest + from agent_assembly.adapters.langgraph import patch as lg_patch @@ -88,51 +90,43 @@ def test_transition_input_keys_contains_state_keys() -> None: assert set(meta["transition_input_keys"]) >= {"msg", "count"} -def test_no_edge_emitted_for_first_node_in_graph() -> None: +def test_no_edge_emitted_for_first_node_in_graph(monkeypatch: pytest.MonkeyPatch) -> None: """The very first node has no predecessor so no edge should be emitted.""" emitter = RecordingEdgeEmitter() - lg_patch.set_edge_emitter(emitter) - try: - handler = _NullCallbackHandler() - node_map = _make_node_map("only_node", handler=handler) - lg_patch._wrap_node_map(node_map, handler) - node_map["only_node"]({}) - finally: - lg_patch.set_edge_emitter(None) - lg_patch._NODE_TRANSITION.name = None + monkeypatch.setattr(lg_patch, "_EDGE_EMITTER", emitter) + monkeypatch.setattr(lg_patch._NODE_TRANSITION, "name", None, raising=False) + handler = _NullCallbackHandler() + node_map = _make_node_map("only_node", handler=handler) + lg_patch._wrap_node_map(node_map, handler) + node_map["only_node"]({}) assert emitter.edges == [] -def test_three_node_graph_emits_two_edges() -> None: +def test_three_node_graph_emits_two_edges(monkeypatch: pytest.MonkeyPatch) -> None: emitter = RecordingEdgeEmitter() - lg_patch.set_edge_emitter(emitter) - try: - handler = _NullCallbackHandler() - node_map = _make_node_map("a", "b", "c", handler=handler) - lg_patch._wrap_node_map(node_map, handler) - state: dict[str, Any] = {} - state = node_map["a"](state) - state = node_map["b"](state) - node_map["c"](state) - finally: - lg_patch.set_edge_emitter(None) - lg_patch._NODE_TRANSITION.name = None + monkeypatch.setattr(lg_patch, "_EDGE_EMITTER", emitter) + monkeypatch.setattr(lg_patch._NODE_TRANSITION, "name", None, raising=False) + handler = _NullCallbackHandler() + node_map = _make_node_map("a", "b", "c", handler=handler) + lg_patch._wrap_node_map(node_map, handler) + state: dict[str, Any] = {} + state = node_map["a"](state) + state = node_map["b"](state) + node_map["c"](state) assert len(emitter.edges) == 2 assert emitter.edges[0][:3] == ("a", "b", "messages") assert emitter.edges[1][:3] == ("b", "c", "messages") -def test_no_edge_emitted_when_emitter_is_none() -> None: +def test_no_edge_emitted_when_emitter_is_none(monkeypatch: pytest.MonkeyPatch) -> None: """When no emitter is registered, transitions must not raise.""" - lg_patch.set_edge_emitter(None) - try: - handler = _NullCallbackHandler() - node_map = _make_node_map("x", "y", handler=handler) - lg_patch._wrap_node_map(node_map, handler) - state: dict[str, Any] = {} - state = node_map["x"](state) - node_map["y"](state) # Should not raise - finally: - lg_patch._NODE_TRANSITION.name = None + monkeypatch.setattr(lg_patch, "_EDGE_EMITTER", None) + monkeypatch.setattr(lg_patch._NODE_TRANSITION, "name", None, raising=False) + handler = _NullCallbackHandler() + node_map = _make_node_map("x", "y", handler=handler) + lg_patch._wrap_node_map(node_map, handler) + state: dict[str, Any] = {} + state = node_map["x"](state) + node_map["y"](state) # Should not raise 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 6ef4afb1..e166b526 100644 --- a/test/unit/adapters/openai_agents/test_runner_spawn_patch.py +++ b/test/unit/adapters/openai_agents/test_runner_spawn_patch.py @@ -58,14 +58,14 @@ def teardown_method(self) -> None: delattr(FakeRunner, attr) @pytest.mark.asyncio - async def test_patched_run_sets_spawn_ctx(self) -> None: + async def test_patched_run_sets_spawn_ctx(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] async def capturing_run(agent: object, *, input: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "done" - FakeRunner.run = classmethod(capturing_run) # type: ignore[arg-type,assignment,method-assign] + monkeypatch.setattr(FakeRunner, "run", classmethod(capturing_run)) _apply_runner_run_patch(FakeRunner, "process-agent-001") await FakeRunner.run(MagicMock(), input="hello") @@ -78,22 +78,22 @@ async def capturing_run(agent: object, *, input: object, **kwargs: object) -> st assert sc.spawned_by_tool == "openai_agents_runner" @pytest.mark.asyncio - async def test_spawn_ctx_is_reset_after_run(self) -> None: + async def test_spawn_ctx_is_reset_after_run(self, monkeypatch: pytest.MonkeyPatch) -> None: async def passthrough_run(agent: object, *, input: object, **kwargs: object) -> str: return "ok" - FakeRunner.run = classmethod(passthrough_run) # type: ignore[arg-type,assignment,method-assign] + monkeypatch.setattr(FakeRunner, "run", classmethod(passthrough_run)) _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) -> None: + async def test_spawn_ctx_reset_on_exception(self, monkeypatch: pytest.MonkeyPatch) -> None: async def failing_run(agent: object, *, input: object, **kwargs: object) -> str: raise RuntimeError("runner failed") - FakeRunner.run = classmethod(failing_run) # type: ignore[arg-type,assignment,method-assign] + monkeypatch.setattr(FakeRunner, "run", classmethod(failing_run)) _apply_runner_run_patch(FakeRunner, "process-agent-001") agent = MagicMock() @@ -115,7 +115,8 @@ def test_revert_restores_original(self) -> None: import asyncio as _asyncio result = _asyncio.run(FakeRunner.run(MagicMock(), input="x")) - assert isinstance(result, str) and result.startswith("ran:") + assert isinstance(result, str) + assert result.startswith("ran:") class TestLoadHandoffClass: diff --git a/test/unit/adapters/pydantic_ai/test_pydantic_ai_patch.py b/test/unit/adapters/pydantic_ai/test_pydantic_ai_patch.py index 8653d45c..43534a09 100644 --- a/test/unit/adapters/pydantic_ai/test_pydantic_ai_patch.py +++ b/test/unit/adapters/pydantic_ai/test_pydantic_ai_patch.py @@ -647,5 +647,6 @@ async def test_denies_under_enforce( tool = FakeTool() ctx = SimpleNamespace(deps=SimpleNamespace(assembly_agent_id="agent-a"), run_id="run-1") + args = _ArgsModel({"topic": "finance"}) with pytest.raises(PolicyViolationError): - await tool._run(ctx, _ArgsModel({"topic": "finance"})) + await tool._run(ctx, args) 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 9cac4e8e..4d0fe8ab 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 @@ -65,14 +65,14 @@ def teardown_method(self) -> None: delattr(FakeAgent, attr) @pytest.mark.asyncio - async def test_async_run_sets_spawn_ctx(self) -> None: + async def test_async_run_sets_spawn_ctx(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] async def capturing_run(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "ok" - FakeAgent.run = capturing_run # type: ignore[method-assign] # reassign fake method to install/restore stub + monkeypatch.setattr(FakeAgent, "run", capturing_run) _apply_agent_run_patch(FakeAgent, "pydantic-parent") agent = FakeAgent() @@ -83,14 +83,14 @@ async def capturing_run(self: object, *args: object, **kwargs: object) -> str: assert captured[0].depth == 1 assert captured[0].spawned_by_tool == "pydantic_ai_agent" - def test_sync_run_sets_spawn_ctx(self) -> None: + def test_sync_run_sets_spawn_ctx(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] 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 # type: ignore[method-assign] # fake method swap + monkeypatch.setattr(FakeAgent, "run_sync", capturing_run_sync) _apply_agent_run_patch(FakeAgent, "pydantic-parent") agent = FakeAgent() @@ -113,11 +113,11 @@ def test_spawn_ctx_reset_after_sync_run(self) -> None: assert _SPAWN_CTX.get() is None @pytest.mark.asyncio - async def test_spawn_ctx_reset_on_exception_async(self) -> None: + async def test_spawn_ctx_reset_on_exception_async(self, monkeypatch: pytest.MonkeyPatch) -> None: async def failing_run(self: object, *args: object, **kwargs: object) -> str: raise RuntimeError("agent error") - FakeAgent.run = failing_run # type: ignore[method-assign] # reassign fake method to install/restore stub + monkeypatch.setattr(FakeAgent, "run", failing_run) _apply_agent_run_patch(FakeAgent, "pydantic-parent") agent = FakeAgent() @@ -125,11 +125,11 @@ async def failing_run(self: object, *args: object, **kwargs: object) -> str: await agent.run("x") assert _SPAWN_CTX.get() is None - def test_spawn_ctx_reset_on_exception_sync(self) -> None: + def test_spawn_ctx_reset_on_exception_sync(self, monkeypatch: pytest.MonkeyPatch) -> None: def failing_run_sync(self: object, *args: object, **kwargs: object) -> str: raise RuntimeError("sync agent error") - FakeAgent.run_sync = failing_run_sync # type: ignore[method-assign] # fake method swap + monkeypatch.setattr(FakeAgent, "run_sync", failing_run_sync) _apply_agent_run_patch(FakeAgent, "pydantic-parent") agent = FakeAgent() @@ -138,14 +138,14 @@ def failing_run_sync(self: object, *args: object, **kwargs: object) -> str: assert _SPAWN_CTX.get() is None @pytest.mark.asyncio - async def test_nested_depth_propagation(self) -> None: + async def test_nested_depth_propagation(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] async def capturing_run(self: object, *args: object, **kwargs: object) -> str: captured.append(_SPAWN_CTX.get()) return "ok" - FakeAgent.run = capturing_run # type: ignore[method-assign] # reassign fake method to install/restore stub + monkeypatch.setattr(FakeAgent, "run", capturing_run) _apply_agent_run_patch(FakeAgent, "process-agent") outer_ctx = SpawnContext(parent_agent_id="grandparent", depth=2, spawned_by_tool="outer") @@ -228,14 +228,14 @@ def teardown_method(self) -> None: delattr(FakeTool, attr) @pytest.mark.asyncio - async def test_tool_run_sets_spawned_by_tool(self) -> None: + async def test_tool_run_sets_spawned_by_tool(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] async def capturing_run(self: object, ctx: object, args: object, **kw: object) -> str: captured.append(_SPAWN_CTX.get()) return "ok" - FakeTool._run = capturing_run # type: ignore[assignment,method-assign] # fake method swap + monkeypatch.setattr(FakeTool, "_run", capturing_run) _apply_tool_run_patch(FakeTool, _FakeAllowHandler()) await FakeTool()._run(_FakeCtx(), {}) @@ -244,14 +244,14 @@ async def capturing_run(self: object, ctx: object, args: object, **kw: object) - assert captured[0].spawned_by_tool == "search" @pytest.mark.asyncio - async def test_tool_run_sets_delegation_reason(self) -> None: + async def test_tool_run_sets_delegation_reason(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] async def capturing_run(self: object, ctx: object, args: object, **kw: object) -> str: captured.append(_SPAWN_CTX.get()) return "ok" - FakeTool._run = capturing_run # type: ignore[assignment,method-assign] # fake method swap + monkeypatch.setattr(FakeTool, "_run", capturing_run) _apply_tool_run_patch(FakeTool, _FakeAllowHandler()) await FakeTool()._run(_FakeCtx(), {}) @@ -260,14 +260,14 @@ async def capturing_run(self: object, ctx: object, args: object, **kw: object) - assert captured[0].delegation_reason == "tool:search" @pytest.mark.asyncio - async def test_tool_run_sets_parent_agent_id_from_ctx(self) -> None: + async def test_tool_run_sets_parent_agent_id_from_ctx(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] async def capturing_run(self: object, ctx: object, args: object, **kw: object) -> str: captured.append(_SPAWN_CTX.get()) return "ok" - FakeTool._run = capturing_run # type: ignore[assignment,method-assign] # fake method swap + monkeypatch.setattr(FakeTool, "_run", capturing_run) _apply_tool_run_patch(FakeTool, _FakeAllowHandler()) await FakeTool()._run(_FakeCtx(), {}) @@ -282,11 +282,11 @@ async def test_spawn_ctx_reset_after_tool_run(self) -> None: assert _SPAWN_CTX.get() is None @pytest.mark.asyncio - async def test_spawn_ctx_reset_on_tool_exception(self) -> None: + async def test_spawn_ctx_reset_on_tool_exception(self, monkeypatch: pytest.MonkeyPatch) -> None: async def failing_run(self: object, ctx: object, args: object, **kw: object) -> str: raise RuntimeError("tool broke") - FakeTool._run = failing_run # type: ignore[assignment,method-assign] # fake method swap + monkeypatch.setattr(FakeTool, "_run", failing_run) _apply_tool_run_patch(FakeTool, _FakeAllowHandler()) tool = FakeTool() @@ -296,14 +296,14 @@ async def failing_run(self: object, ctx: object, args: object, **kw: object) -> assert _SPAWN_CTX.get() is None @pytest.mark.asyncio - async def test_denied_tool_does_not_set_spawn_ctx(self) -> None: + async def test_denied_tool_does_not_set_spawn_ctx(self, monkeypatch: pytest.MonkeyPatch) -> None: called = [] async def should_not_be_called(self: object, ctx: object, args: object, **kw: object) -> str: called.append(True) return "should-not-run" - FakeTool._run = should_not_be_called # type: ignore[assignment,method-assign] # fake method swap + monkeypatch.setattr(FakeTool, "_run", should_not_be_called) _apply_tool_run_patch(FakeTool, _FakeDenyHandler()) from agent_assembly.exceptions import PolicyViolationError @@ -317,14 +317,14 @@ async def should_not_be_called(self: object, ctx: object, args: object, **kw: ob assert _SPAWN_CTX.get() is None @pytest.mark.asyncio - async def test_tool_run_depth_increments_with_outer_ctx(self) -> None: + async def test_tool_run_depth_increments_with_outer_ctx(self, monkeypatch: pytest.MonkeyPatch) -> None: captured: list[SpawnContext | None] = [] async def capturing_run(self: object, ctx: object, args: object, **kw: object) -> str: captured.append(_SPAWN_CTX.get()) return "ok" - FakeTool._run = capturing_run # type: ignore[assignment,method-assign] # fake method swap + monkeypatch.setattr(FakeTool, "_run", capturing_run) _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_registry.py b/test/unit/adapters/test_registry.py index 02e7202e..c3ce2703 100644 --- a/test/unit/adapters/test_registry.py +++ b/test/unit/adapters/test_registry.py @@ -218,7 +218,7 @@ def test_auto_detect_is_idempotent_for_entry_point_adapters( monkeypatch: pytest.MonkeyPatch, ) -> None: registry = AdapterRegistry() - CountingEntryPointAdapter.register_calls = 0 + monkeypatch.setattr(CountingEntryPointAdapter, "register_calls", 0) class FakeEntryPoints(list[FakeEntryPoint]): def select(self, *, group: str) -> list[FakeEntryPoint]: diff --git a/test/unit/cli/conftest.py b/test/unit/cli/conftest.py index 7660b78a..63398e09 100644 --- a/test/unit/cli/conftest.py +++ b/test/unit/cli/conftest.py @@ -80,26 +80,26 @@ class NotAnAdapter: """A class that does not inherit from FrameworkAdapter.""" -@pytest.fixture() +@pytest.fixture def valid_adapter_cls() -> type: return ValidAdapter -@pytest.fixture() +@pytest.fixture def empty_name_adapter_cls() -> type: return EmptyNameAdapter -@pytest.fixture() +@pytest.fixture def empty_versions_adapter_cls() -> type: return EmptyVersionsAdapter -@pytest.fixture() +@pytest.fixture def non_idempotent_adapter_cls() -> type: return NonIdempotentAdapter -@pytest.fixture() +@pytest.fixture def not_an_adapter_cls() -> type: return NotAnAdapter diff --git a/test/unit/test_check_readme_version.py b/test/unit/test_check_readme_version.py index c1d50ef5..543c19b2 100644 --- a/test/unit/test_check_readme_version.py +++ b/test/unit/test_check_readme_version.py @@ -19,7 +19,8 @@ def _load_module() -> ModuleType: spec = importlib.util.spec_from_file_location("check_readme_version", _SCRIPT) - assert spec is not None and spec.loader is not None + assert spec is not None + assert spec.loader is not None module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module diff --git a/test/unit/test_op_control.py b/test/unit/test_op_control.py index 60cacd09..e1d7358b 100644 --- a/test/unit/test_op_control.py +++ b/test/unit/test_op_control.py @@ -167,7 +167,8 @@ def waiter() -> None: stream.push(_msg("op-3", policy_pb2.OP_CONTROL_SIGNAL_TERMINATE, sequence=1)) assert done.wait(timeout=2.0) t.join(timeout=1.0) - assert captured and isinstance(captured[0], OpTerminatedError) + assert captured + assert isinstance(captured[0], OpTerminatedError) assert captured[0].op_id == "op-3"