diff --git a/.claude/skills/bug-hunting-via-example/scripts/runner.py b/.claude/skills/bug-hunting-via-example/scripts/runner.py index 6ca1c64be..43c01f133 100644 --- a/.claude/skills/bug-hunting-via-example/scripts/runner.py +++ b/.claude/skills/bug-hunting-via-example/scripts/runner.py @@ -421,7 +421,7 @@ def reply_capture_context( ) -> AbstractAsyncContextManager[ReplyCapture]: """Open a baseline reply capture; imported late, after ``sys.path`` is set.""" # pyrefly: ignore[missing-import] - from tests.e2e.baseline.toolkit.capture import reply_capture + from tests.e2e.baseline.toolkit.capture import reply_capture # noqa: PLC0415 return reply_capture( ws, room_id, user_ops=user_ops, settings=settings, deadline_s=deadline_s @@ -1072,16 +1072,16 @@ async def run_live(plan: Plan, repo: Path, keep: bool, results: list[Result]) -> """ sys.path.insert(0, str(repo)) # pyrefly: ignore[missing-import] - from tests.e2e.baseline.settings import BaselineSettings + from tests.e2e.baseline.settings import BaselineSettings # noqa: PLC0415 # pyrefly: ignore[missing-import] - from tests.e2e.baseline.toolkit.provisioning import ( + from tests.e2e.baseline.toolkit.provisioning import ( # noqa: PLC0415 ResourceManager, user_rest_client, ) # pyrefly: ignore[missing-import] - from tests.e2e.baseline.toolkit.ws import user_ws_observer + from tests.e2e.baseline.toolkit.ws import user_ws_observer # noqa: PLC0415 settings = BaselineSettings() if not settings.e2e_tests_enabled: diff --git a/band-bridge/bridge_core/__main__.py b/band-bridge/bridge_core/__main__.py index 79d91166c..d12fad6ff 100644 --- a/band-bridge/bridge_core/__main__.py +++ b/band-bridge/bridge_core/__main__.py @@ -10,10 +10,10 @@ import asyncio +from .bridge import main -def _main() -> None: - from .bridge import main +def _main() -> None: asyncio.run(main()) diff --git a/band-bridge/bridge_core/bridge.py b/band-bridge/bridge_core/bridge.py index 55db306bd..a0f700e41 100644 --- a/band-bridge/bridge_core/bridge.py +++ b/band-bridge/bridge_core/bridge.py @@ -25,6 +25,8 @@ from datetime import datetime, timezone from typing import Any +from dotenv import load_dotenv + from band.client.rest import DEFAULT_REQUEST_OPTIONS from band.client.streaming import MessageCreatedPayload from band.runtime.types import PlatformMessage @@ -749,8 +751,6 @@ async def main( asyncio.run(main()) """ - from dotenv import load_dotenv - load_dotenv() log_level = os.environ.get("LOG_LEVEL", "INFO").upper() diff --git a/band-bridge/bridge_core/forwarder.py b/band-bridge/bridge_core/forwarder.py index 7b6df50ba..eb04629c8 100644 --- a/band-bridge/bridge_core/forwarder.py +++ b/band-bridge/bridge_core/forwarder.py @@ -57,7 +57,7 @@ def __init__( def _get_client(self) -> httpx.AsyncClient: if self._client is None: try: - import httpx as _httpx + import httpx as _httpx # noqa: PLC0415 except ImportError: raise ImportError( "httpx is required for HTTPForwarder. " @@ -97,8 +97,8 @@ def __init__( def _get_client(self) -> Any: if self._client is None: try: - import boto3 - from botocore.config import Config + import boto3 # noqa: PLC0415 + from botocore.config import Config # noqa: PLC0415 except ImportError: raise ImportError( "boto3 is required for AgentCoreForwarder. " diff --git a/docker/claude_sdk/runner.py b/docker/claude_sdk/runner.py index d0aa518dd..f63e652d2 100644 --- a/docker/claude_sdk/runner.py +++ b/docker/claude_sdk/runner.py @@ -26,6 +26,8 @@ import yaml +from band import Agent +from band.adapters import ClaudeSDKAdapter from band.config.loader import load_agent_config from band.config.logs import LogSettings from band.core.types import Emit @@ -161,9 +163,6 @@ async def main() -> None: lock_timeout_s=lock_timeout_s, ) - from band import Agent - from band.adapters import ClaudeSDKAdapter - agent_id = config["agent_id"] api_key = config["api_key"] model = config.get("model") diff --git a/docker/codex/runner.py b/docker/codex/runner.py index 572fb02c6..9bee1dfc2 100644 --- a/docker/codex/runner.py +++ b/docker/codex/runner.py @@ -31,6 +31,9 @@ import yaml +from band import Agent +from band.adapters import CodexAdapter +from band.adapters.codex import CodexAdapterConfig from band.config.loader import load_agent_config from band.config.logs import LogSettings from band.core.types import Emit @@ -189,10 +192,6 @@ async def main() -> None: lock_timeout_s=lock_timeout_s, ) - from band import Agent - from band.adapters import CodexAdapter - from band.adapters.codex import CodexAdapterConfig - agent_id = config["agent_id"] api_key = config["api_key"] diff --git a/docker/letta/runner.py b/docker/letta/runner.py index c560e0b7a..47386f47e 100644 --- a/docker/letta/runner.py +++ b/docker/letta/runner.py @@ -47,6 +47,8 @@ "pyyaml is required for the Letta runner. Install with: pip install pyyaml" ) +from band import Agent +from band.adapters.letta import LettaAdapter, LettaAdapterConfig, LettaMCPConfig from band.config.loader import load_agent_config from band.config.logs import LogSettings from band.core.types import Emit @@ -158,9 +160,6 @@ async def main() -> None: ) config = load_config(settings.agent_config, settings.agent_key) - from band import Agent - from band.adapters.letta import LettaAdapter, LettaAdapterConfig, LettaMCPConfig - agent_id = config["agent_id"] api_key = config["api_key"] diff --git a/examples/20-questions-arena/prompts.py b/examples/20-questions-arena/prompts.py index fa7df51cb..91a3a3a8f 100644 --- a/examples/20-questions-arena/prompts.py +++ b/examples/20-questions-arena/prompts.py @@ -36,7 +36,7 @@ def create_llm() -> BaseChatModel: if settings.anthropic_api_key: try: - from langchain_anthropic import ChatAnthropic + from langchain_anthropic import ChatAnthropic # noqa: PLC0415 except ImportError: raise ValueError( "ANTHROPIC_API_KEY is set but langchain-anthropic is not installed. " @@ -45,7 +45,7 @@ def create_llm() -> BaseChatModel: return ChatAnthropic(model="claude-sonnet-4-5-20250929") elif settings.openai_api_key: - from langchain_openai import ChatOpenAI + from langchain_openai import ChatOpenAI # noqa: PLC0415 -- only load the model actually selected by which API key is configured return ChatOpenAI(model="gpt-5.5") else: @@ -73,7 +73,7 @@ def create_llm_by_name(model: str) -> BaseChatModel: if not settings.anthropic_api_key: raise ValueError(f"ANTHROPIC_API_KEY must be set to use model '{model}'") try: - from langchain_anthropic import ChatAnthropic + from langchain_anthropic import ChatAnthropic # noqa: PLC0415 except ImportError: raise ValueError( "langchain-anthropic is not installed. " @@ -83,7 +83,7 @@ def create_llm_by_name(model: str) -> BaseChatModel: else: if not settings.openai_api_key: raise ValueError(f"OPENAI_API_KEY must be set to use model '{model}'") - from langchain_openai import ChatOpenAI + from langchain_openai import ChatOpenAI # noqa: PLC0415 -- only load the model actually selected by which API key is configured return ChatOpenAI(model=model) diff --git a/examples/a2a_gateway/02_with_demo_agent.py b/examples/a2a_gateway/02_with_demo_agent.py index 87fb10dea..de1431acf 100644 --- a/examples/a2a_gateway/02_with_demo_agent.py +++ b/examples/a2a_gateway/02_with_demo_agent.py @@ -62,6 +62,7 @@ import asyncio import logging import sys +import threading from pathlib import Path # Add current directory to path for local imports @@ -265,8 +266,6 @@ async def main() -> None: # Run gateway in background, orchestrator in foreground # Note: uvicorn.run() is blocking, so we run orchestrator in a thread - import threading - # Start gateway in asyncio gateway_task = asyncio.create_task(run_gateway()) diff --git a/examples/agentcore/verify_deployment.py b/examples/agentcore/verify_deployment.py index 96007c103..a2631a263 100644 --- a/examples/agentcore/verify_deployment.py +++ b/examples/agentcore/verify_deployment.py @@ -61,6 +61,12 @@ from dotenv import load_dotenv from band import LogSettings +from band_rest import ChatMessageRequest +from band_rest.types import ChatMessageRequestMentionsItem as Mention +from band_rest import CreateMyChatRoomRequestChat +from band_rest.types import ParticipantRequest +from band_rest import AsyncRestClient +from band.client.streaming import WebSocketClient logger = logging.getLogger("verify_deployment") @@ -132,8 +138,6 @@ async def send_trigger_message( Sends with user credentials so the sender is the user (agents skip self-authored messages) and @mentions PA so the platform routes it. """ - from band_rest import ChatMessageRequest - from band_rest.types import ChatMessageRequestMentionsItem as Mention response = await client.human_api_messages.send_my_chat_message( room_id, @@ -220,8 +224,6 @@ async def wait() -> str: async def create_room_with_pa(user_client: object, pa_agent_id: str, label: str) -> str: """Create a fresh chat room and add @personal_assistant to it.""" - from band_rest import CreateMyChatRoomRequestChat - from band_rest.types import ParticipantRequest response = await user_client.human_api_chats.create_my_chat_room( chat=CreateMyChatRoomRequestChat(), @@ -363,9 +365,6 @@ async def verify_parallel_rooms( async def main() -> None: - from band_rest import AsyncRestClient - - from band.client.streaming import WebSocketClient rest_url = require_env("BAND_REST_URL", "the target platform's REST base URL") ws_url = require_env("BAND_WS_URL", "the target platform's WebSocket URL") diff --git a/examples/claude_sdk_docker/create_agents.py b/examples/claude_sdk_docker/create_agents.py index c3ae66c17..cd62601ef 100644 --- a/examples/claude_sdk_docker/create_agents.py +++ b/examples/claude_sdk_docker/create_agents.py @@ -18,6 +18,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from band import LoggingStyle, LogSettings +from band_rest import AsyncRestClient +from band_rest.types import AgentRegisterRequest # The bare message format only exists for the standard style, so the style is # pinned rather than read from BAND_LOG_CONSOLE_STYLE. @@ -44,9 +46,6 @@ class Settings(BaseSettings): async def main() -> None: settings = Settings() - from band_rest import AsyncRestClient - from band_rest.types import AgentRegisterRequest - client = AsyncRestClient( api_key=settings.band_api_key, base_url=settings.band_rest_url ) diff --git a/examples/claude_sdk_docker/runner.py b/examples/claude_sdk_docker/runner.py index 24f6b882a..2ae6b26f2 100644 --- a/examples/claude_sdk_docker/runner.py +++ b/examples/claude_sdk_docker/runner.py @@ -30,6 +30,8 @@ from band import LogSettings from band.core.types import Emit +from band import Agent +from band.adapters import ClaudeSDKAdapter class Settings(BaseSettings): @@ -174,8 +176,6 @@ async def main() -> None: config = load_config(config_path) # Import here to allow early config validation - from band import Agent - from band.adapters import ClaudeSDKAdapter # Extract config values agent_id = config["agent_id"] diff --git a/examples/claude_sdk_docker/test_communication.py b/examples/claude_sdk_docker/test_communication.py index b3d771652..206016811 100644 --- a/examples/claude_sdk_docker/test_communication.py +++ b/examples/claude_sdk_docker/test_communication.py @@ -20,6 +20,13 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from band import LoggingStyle, LogSettings +from band_rest import AsyncRestClient +from band_rest.types import ( + ChatMessageRequest, + ChatMessageRequestMentionsItem, + ChatRoomRequest, + ParticipantRequest, +) # The bare message format only exists for the standard style, so the style is # pinned rather than read from BAND_LOG_CONSOLE_STYLE. @@ -49,13 +56,6 @@ def load_agent_config(filename: str) -> dict: async def main() -> None: - from band_rest import AsyncRestClient - from band_rest.types import ( - ChatMessageRequest, - ChatMessageRequestMentionsItem, - ChatRoomRequest, - ParticipantRequest, - ) # Load agent configs planner = load_agent_config("planner.yaml") diff --git a/examples/coding_agents/create_agents.py b/examples/coding_agents/create_agents.py index c9e66efa3..53aadfee1 100644 --- a/examples/coding_agents/create_agents.py +++ b/examples/coding_agents/create_agents.py @@ -18,6 +18,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from band import LoggingStyle, LogSettings +from band_rest import AsyncRestClient +from band_rest.types import AgentRegisterRequest # The bare message format only exists for the standard style, so the style is # pinned rather than read from BAND_LOG_CONSOLE_STYLE. @@ -44,9 +46,6 @@ class Settings(BaseSettings): async def main() -> None: settings = Settings() - from band_rest import AsyncRestClient - from band_rest.types import AgentRegisterRequest - client = AsyncRestClient( api_key=settings.band_api_key, base_url=settings.band_rest_url ) diff --git a/examples/coding_agents/test_communication.py b/examples/coding_agents/test_communication.py index ae9af40da..96288b2f0 100644 --- a/examples/coding_agents/test_communication.py +++ b/examples/coding_agents/test_communication.py @@ -20,6 +20,13 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from band import LoggingStyle, LogSettings +from band_rest import AsyncRestClient +from band_rest.types import ( + ChatMessageRequest, + ChatMessageRequestMentionsItem, + ChatRoomRequest, + ParticipantRequest, +) # The bare message format only exists for the standard style, so the style is # pinned rather than read from BAND_LOG_CONSOLE_STYLE. @@ -49,13 +56,6 @@ def load_agent_config(filename: str) -> dict: async def main() -> None: - from band_rest import AsyncRestClient - from band_rest.types import ( - ChatMessageRequest, - ChatMessageRequestMentionsItem, - ChatRoomRequest, - ParticipantRequest, - ) # Load agent configs planner = load_agent_config("planner.yaml") diff --git a/examples/langgraph/standalone_sql_agent.py b/examples/langgraph/standalone_sql_agent.py index 3bc211b5d..f66404514 100644 --- a/examples/langgraph/standalone_sql_agent.py +++ b/examples/langgraph/standalone_sql_agent.py @@ -18,7 +18,10 @@ - Query validation before execution """ +import logging import os +import sqlite3 +import urllib.request from typing import Annotated, Literal from langchain_community.agent_toolkits import SQLDatabaseToolkit @@ -104,10 +107,6 @@ def should_continue(state: MessagesState) -> Literal["tools", END]: def download_chinook_db(): """Download the Chinook sample database if not present.""" - import logging - import os - import urllib.request - logger = logging.getLogger(__name__) db_path = "Chinook.db" @@ -126,9 +125,6 @@ def download_chinook_db(): logger.error("Error downloading database: %s", e) logger.info("Creating minimal test database instead...") - # Create minimal test database if download fails - import sqlite3 - conn = sqlite3.connect(db_path) cursor = conn.cursor() diff --git a/examples/run_agent.py b/examples/run_agent.py index a3cd1d39a..fb37065f0 100644 --- a/examples/run_agent.py +++ b/examples/run_agent.py @@ -225,10 +225,10 @@ async def run_langgraph_agent( logger: logging.Logger, ) -> None: """Run the LangGraph agent.""" - from langchain_openai import ChatOpenAI - from langgraph.checkpoint.memory import InMemorySaver + from langchain_openai import ChatOpenAI # noqa: PLC0415 -- only load the langgraph extra when this example is the one selected to run + from langgraph.checkpoint.memory import InMemorySaver # noqa: PLC0415 -- only load the langgraph extra when this example is the one selected to run - from band.adapters import LangGraphAdapter + from band.adapters import LangGraphAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run adapter = LangGraphAdapter( llm=ChatOpenAI(model="gpt-5.4-mini"), @@ -255,7 +255,7 @@ async def run_pydantic_ai_agent( logger: logging.Logger, ) -> None: """Run the Pydantic AI agent.""" - from band.adapters import PydanticAIAdapter + from band.adapters import PydanticAIAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run # Augment custom_section for contact modes section = custom_section @@ -306,7 +306,7 @@ async def run_anthropic_agent( logger: logging.Logger, ) -> None: """Run the Anthropic SDK agent.""" - from band.adapters import AnthropicAdapter + from band.adapters import AnthropicAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run adapter = AnthropicAdapter( model=model, @@ -345,7 +345,7 @@ async def run_claude_sdk_agent( logger: logging.Logger, ) -> None: """Run the Claude Agent SDK agent.""" - from band.adapters import ClaudeSDKAdapter + from band.adapters import ClaudeSDKAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run adapter = ClaudeSDKAdapter( model=model, @@ -386,9 +386,9 @@ async def run_parlant_agent( logger: logging.Logger, ) -> None: """Run the Parlant agent.""" - import parlant.sdk as p + import parlant.sdk as p # noqa: PLC0415 -- only load the parlant extra when this example is the one selected to run - from band.adapters import ParlantAdapter + from band.adapters import ParlantAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run # Parlant chooses its model via the NLP service, not a model string; # the OpenAI service reads OPENAI_API_KEY. Its adapter has no emit kinds @@ -421,7 +421,7 @@ async def run_crewai_agent( logger: logging.Logger, ) -> None: """Run the CrewAI agent.""" - from band.adapters import CrewAIAdapter + from band.adapters import CrewAIAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run adapter = CrewAIAdapter( model=model, @@ -458,8 +458,8 @@ async def run_codex_agent( logger: logging.Logger, ) -> None: """Run the Codex app-server adapter.""" - from band.adapters import CodexAdapter - from band.adapters.codex import CodexAdapterConfig + from band.adapters import CodexAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run + from band.adapters.codex import CodexAdapterConfig # noqa: PLC0415 -- only load the codex extra when this example is the one selected to run adapter = CodexAdapter( config=CodexAdapterConfig( @@ -510,7 +510,7 @@ async def run_pydantic_ai_contacts_agent( - "reject bob" - "add john as a contact" """ - from band.adapters import PydanticAIAdapter + from band.adapters import PydanticAIAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run adapter = PydanticAIAdapter( model=model, @@ -541,8 +541,7 @@ async def run_contacts_auto_agent( - Auto-approve logic for contact requests - broadcast_changes=True to notify all rooms of contact updates """ - from band.adapters import PydanticAIAdapter - from band.platform.event import ContactRequestReceivedEvent + from band.adapters import PydanticAIAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run async def auto_approve(event: "ContactEvent", tools: "ContactTools") -> None: """Auto-approve all contact requests.""" @@ -594,7 +593,7 @@ async def run_contacts_hub_agent( - Agent can reason about requests and respond using tools - broadcast_changes=True to notify all rooms of outcomes """ - from band.adapters import PydanticAIAdapter + from band.adapters import PydanticAIAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run config = ContactEventConfig( strategy=ContactEventStrategy.HUB_ROOM, @@ -647,7 +646,7 @@ async def run_contacts_broadcast_agent( - broadcast_changes=True for awareness in all rooms - User can manually manage contacts via chat commands """ - from band.adapters import PydanticAIAdapter + from band.adapters import PydanticAIAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run config = ContactEventConfig( strategy=ContactEventStrategy.DISABLED, # No auto-handling @@ -687,7 +686,7 @@ async def run_a2a_agent( logger: logging.Logger, ) -> None: """Run the A2A bridge agent.""" - from band.adapters import A2AAdapter + from band.adapters import A2AAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run # Enable debug logging for A2A adapter to trace context_id and rehydration if enable_debug: @@ -721,7 +720,7 @@ async def run_a2a_gateway_agent( as A2A endpoints. Remote A2A agents can call these peers via standard A2A protocol. """ - from band.adapters import A2AGatewayAdapter + from band.adapters import A2AGatewayAdapter # noqa: PLC0415 -- only load the adapters extra when this example is the one selected to run # Enable debug logging for gateway adapter if enable_debug: diff --git a/examples/slack/01_basic_bot.py b/examples/slack/01_basic_bot.py index 4112bc677..be16b9494 100644 --- a/examples/slack/01_basic_bot.py +++ b/examples/slack/01_basic_bot.py @@ -73,6 +73,7 @@ from band.adapters import AnthropicAdapter from band.config import load_agent_config from band.integrations.slack import SlackAdapter, SlackApp +from starlette.applications import Starlette configure_logging(logging.INFO, extra_loggers={"slack_sdk": logging.INFO}) logger = logging.getLogger(__name__) @@ -157,8 +158,7 @@ async def main() -> None: # In a real service you'd mount ``slack.router`` into your # existing FastAPI/Starlette app instead of running uvicorn # standalone like this. - import uvicorn - from starlette.applications import Starlette + import uvicorn # noqa: PLC0415 -- only needed for the (non-default) HTTP transport path web_app = Starlette() web_app.mount("/slack", slack.router) diff --git a/examples/slack/dev_bridge.py b/examples/slack/dev_bridge.py index c0b03c53e..dd5ae89b0 100644 --- a/examples/slack/dev_bridge.py +++ b/examples/slack/dev_bridge.py @@ -37,6 +37,8 @@ from band import Agent, LogSettings from band.adapters import AnthropicAdapter from band.integrations.slack import SlackAdapter, SlackApp +import uvicorn +from starlette.applications import Starlette # slack_sdk raised alongside band, not a bare LogSettings().configure(): this # driver exists to debug the bridge, and slack_sdk's own INFO diagnostics are @@ -111,8 +113,6 @@ async def main() -> None: if transport == "http": # Mount the Slack router into a tiny ASGI app and run uvicorn # alongside the Band WS agent loop. - import uvicorn - from starlette.applications import Starlette starlette_app = Starlette() starlette_app.mount("/slack", slack.router) diff --git a/pyproject.toml b/pyproject.toml index 6a151f8b1..32f8f3249 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -458,6 +458,11 @@ conflicts = [ ], ] +[tool.ruff.lint] +# extend-select adds to ruff's default rule set (E4, E7, E9, F) rather than +# replacing it, so enabling PLC0415 doesn't change any other lint behavior. +extend-select = ["PLC0415"] + [tool.pyrefly] project_includes = [ "src/**/*.py", # Only check source files diff --git a/src/band/adapters/anthropic.py b/src/band/adapters/anthropic.py index 499f080d3..d7576d032 100644 --- a/src/band/adapters/anthropic.py +++ b/src/band/adapters/anthropic.py @@ -12,7 +12,7 @@ from typing import Any, ClassVar, cast from anthropic import AsyncAnthropic -from anthropic.types import Message, MessageParam, ToolParam, ToolUseBlock +from anthropic.types import Message, MessageParam, TextBlock, ToolParam, ToolUseBlock from typing_extensions import Unpack from band.core.exceptions import BandConfigError @@ -382,8 +382,6 @@ def _usage_from_response(response: Message) -> TurnUsage: # --- Copied from BandAnthropicAgent._extract_text_content --- def _extract_text_content(self, content: list) -> str: """Extract text content from response content blocks.""" - from anthropic.types import TextBlock - texts = [] for block in content: if isinstance(block, TextBlock) and block.text: @@ -393,8 +391,6 @@ def _extract_text_content(self, content: list) -> str: # --- Copied from BandAnthropicAgent._serialize_content_blocks --- def _serialize_content_blocks(self, content: list) -> list[dict[str, Any]]: """Serialize content blocks to dict format for message history.""" - from anthropic.types import TextBlock - serialized = [] for block in content: if isinstance(block, ToolUseBlock): diff --git a/src/band/adapters/crewai.py b/src/band/adapters/crewai.py index b1b54dc93..0e733bad9 100644 --- a/src/band/adapters/crewai.py +++ b/src/band/adapters/crewai.py @@ -64,9 +64,9 @@ def _silence_lite_agent_error_panel() -> None: """ try: # event_listener is imported for its side effect: registering the handlers. - from crewai.events import crewai_event_bus - from crewai.events.event_listener import event_listener # noqa: F401 - from crewai.events.types.agent_events import LiteAgentExecutionErrorEvent + from crewai.events import crewai_event_bus # noqa: PLC0415 -- crewai extra, absent from the standard dev venv + from crewai.events.event_listener import event_listener # noqa: F401, PLC0415 -- crewai extra, absent from the standard dev venv + from crewai.events.types.agent_events import LiteAgentExecutionErrorEvent # noqa: PLC0415 -- crewai extra, absent from the standard dev venv handlers = crewai_event_bus._sync_handlers.get( LiteAgentExecutionErrorEvent, frozenset() @@ -178,8 +178,8 @@ def __init__( async def on_started(self, agent_name: str, agent_description: str) -> None: """Initialize CrewAI agent after metadata is fetched.""" try: - from crewai import Agent as CrewAIAgent - from crewai import LLM + from crewai import Agent as CrewAIAgent # noqa: PLC0415 -- crewai extra, absent from the standard dev venv + from crewai import LLM # noqa: PLC0415 -- crewai extra, absent from the standard dev venv except ImportError as e: raise ImportError( "crewai is required for CrewAI adapter.\n" diff --git a/src/band/adapters/crewai_flow.py b/src/band/adapters/crewai_flow.py index 651a9f425..749431deb 100644 --- a/src/band/adapters/crewai_flow.py +++ b/src/band/adapters/crewai_flow.py @@ -31,6 +31,7 @@ from band.converters.crewai_flow import ( CrewAIFlowAmbiguousIdentityError, + CrewAIFlowBufferedSynthesis, CrewAIFlowDelegationState, CrewAIFlowDelegationStatus, CrewAIFlowError, @@ -534,7 +535,7 @@ def __init__( tools: AgentToolsProtocol, features: AdapterFeatures, ) -> None: - from band.integrations.crewai import EmitToolCallsReporter + from band.integrations.crewai import EmitToolCallsReporter # noqa: PLC0415 -- crewai extra, absent from the standard dev venv self._custom_tools = custom_tools self._tools = tools @@ -758,7 +759,7 @@ def create_crewai_tools( to call platform tools. The returned tools enforce the adapter's reserve-send-confirm sequence for visible writes. """ - from band.integrations.crewai.tools import ( + from band.integrations.crewai.tools import ( # noqa: PLC0415 -- crewai extra, absent from the standard dev venv CrewAIToolContext, build_band_crewai_tools, ) @@ -1310,8 +1311,6 @@ async def record_buffered( ``buffered_syntheses`` entry. The converter merges entries by ``source_message_id``, so multiple turns accumulate into one list. """ - from band.converters.crewai_flow import CrewAIFlowBufferedSynthesis - envelope = self._envelope( status=CrewAIFlowRunStatus.WAITING, stage=CrewAIFlowStage.WAITING_FOR_REPLIES, @@ -2286,11 +2285,6 @@ def _match_reply_to_delegation( candidate set, ambiguous matches (which also record a ``reply_ambiguous`` event side-effect). """ - from band.converters.crewai_flow import ( - CrewAIFlowAmbiguousIdentityError, - normalize_participant_key, - ) - # Compute sender's normalized key against the participant snapshot. try: sender_key = normalize_participant_key( diff --git a/src/band/adapters/google_adk.py b/src/band/adapters/google_adk.py index 484803f62..13799b945 100644 --- a/src/band/adapters/google_adk.py +++ b/src/band/adapters/google_adk.py @@ -118,10 +118,10 @@ def _require_adk() -> tuple[type, type, type, Any]: ImportError: If google-adk is not installed. """ try: - from google.adk import Agent as ADKAgent - from google.adk.runners import InMemoryRunner - from google.adk.tools import BaseTool - from google.genai import types + from google.adk import Agent as ADKAgent # noqa: PLC0415 -- genuinely deferred; google_adk extra kept out of this module's unconditional import surface + from google.adk.runners import InMemoryRunner # noqa: PLC0415 -- genuinely deferred; google_adk extra kept out of this module's unconditional import surface + from google.adk.tools import BaseTool # noqa: PLC0415 -- genuinely deferred; google_adk extra kept out of this module's unconditional import surface + from google.genai import types # noqa: PLC0415 -- genuinely deferred; google_adk extra kept out of this module's unconditional import surface except ImportError as exc: raise ImportError( "google-adk is required for GoogleADKAdapter. " diff --git a/src/band/adapters/langgraph.py b/src/band/adapters/langgraph.py index 8ccca9de9..53079cf62 100644 --- a/src/band/adapters/langgraph.py +++ b/src/band/adapters/langgraph.py @@ -8,6 +8,7 @@ from collections import OrderedDict from typing import ClassVar, TYPE_CHECKING, Any, Callable +from langgraph.checkpoint.memory import InMemorySaver from langgraph.pregel import Pregel from typing_extensions import Unpack @@ -22,6 +23,7 @@ TurnUsage, ) from band.converters.langchain import LangChainHistoryConverter, LangChainMessages +from band.integrations.langgraph import langchain_tools from band.runtime.prompts import render_system_prompt from band.runtime.tools import ( BandTool, @@ -147,16 +149,14 @@ def __init__( # patterns get a uniform tool list, and a tool written once works across # adapters (LangChain would otherwise reject a bare tuple). if additional_tools: - from band.integrations.langgraph.langchain_tools import ( - custom_tool_defs_to_langchain, - ) - normalized: list[Any] = [] for item in additional_tools: if isinstance( item, tuple ): # a band CustomToolDef (InputModel, handler) - normalized.extend(custom_tool_defs_to_langchain([item])) + normalized.extend( + langchain_tools.custom_tool_defs_to_langchain([item]) + ) else: # already a LangChain tool / callable normalized.append(item) additional_tools = normalized @@ -170,8 +170,10 @@ def __init__( # ("system", ...) message on bootstrap and the checkpointer carries it # forward, matching the pattern used by every other Band adapter. if uses_simple_pattern: - from langchain.agents import create_agent - from langgraph.checkpoint.memory import InMemorySaver + # `langchain` (distinct from `langgraph`) is only needed for this + # pattern -- a caller who supplies graph_factory=/graph= directly + # never touches create_agent and shouldn't have to install it. + from langchain.agents import create_agent # noqa: PLC0415 -- only needed by the simple llm= pattern below if checkpointer is None: checkpointer = InMemorySaver() @@ -280,15 +282,11 @@ async def on_message( room_id: str, ) -> None: """Handle message with LangGraph.""" - from band.integrations.langgraph.langchain_tools import ( - agent_tools_to_langchain, - ) - logger.info("[HANDLE] Message %s in room %s", msg.id, room_id) # Get LangChain tools - langchain_tools = ( - agent_tools_to_langchain( + lc_tools = ( + langchain_tools.agent_tools_to_langchain( tools, features=self.features, ) @@ -297,7 +295,7 @@ async def on_message( # Build or get graph if self.graph_factory: - graph = self.graph_factory(langchain_tools) + graph = self.graph_factory(lc_tools) else: graph = self._static_graph diff --git a/src/band/adapters/letta.py b/src/band/adapters/letta.py index deee3fefe..67fca2055 100644 --- a/src/band/adapters/letta.py +++ b/src/band/adapters/letta.py @@ -186,7 +186,7 @@ async def on_started(self, agent_name: str, agent_description: str) -> None: ) try: - from letta_client import AsyncLetta # type: ignore[import-not-found] # optional dependency + from letta_client import AsyncLetta # type: ignore[import-not-found] # optional dependency # noqa: PLC0415 except ImportError: raise ImportError( "letta-client is required for LettaAdapter. " diff --git a/src/band/adapters/parlant.py b/src/band/adapters/parlant.py index e34c6e8e3..43dd6385a 100644 --- a/src/band/adapters/parlant.py +++ b/src/band/adapters/parlant.py @@ -353,7 +353,7 @@ async def _prepare_server( if self._configure is not None: await self._configure(server, agent) - from parlant.core.application import Application # type: ignore[missing-import] + from parlant.core.application import Application # type: ignore[missing-import] # noqa: PLC0415 -- genuinely deferred; parlant extra kept out of this module's unconditional import surface return agent, server.container[Application] @@ -415,8 +415,8 @@ async def on_message( ) try: - from parlant.core.app_modules.sessions import Moderation # type: ignore[missing-import] - from parlant.core.sessions import EventSource # type: ignore[missing-import] + from parlant.core.app_modules.sessions import Moderation # type: ignore[missing-import] # noqa: PLC0415 -- genuinely deferred; parlant extra kept out of this module's unconditional import surface + from parlant.core.sessions import EventSource # type: ignore[missing-import] # noqa: PLC0415 -- genuinely deferred; parlant extra kept out of this module's unconditional import surface # Create customer message event (triggers processing) logger.debug("Room %s: Creating customer message event...", room_id) @@ -528,8 +528,8 @@ async def _inject_history( return 0 app = self._app - from parlant.core.app_modules.sessions import Moderation # type: ignore[missing-import] - from parlant.core.sessions import EventKind, EventSource # type: ignore[missing-import] + from parlant.core.app_modules.sessions import Moderation # type: ignore[missing-import] # noqa: PLC0415 -- genuinely deferred; parlant extra kept out of this module's unconditional import surface + from parlant.core.sessions import EventKind, EventSource # type: ignore[missing-import] # noqa: PLC0415 -- genuinely deferred; parlant extra kept out of this module's unconditional import surface # First, filter to only complete exchanges # A user message is only injected if it has a following assistant response @@ -630,8 +630,8 @@ async def _process_agent_response( app = self._app session_id_str = str(session_id) - from parlant.core.async_utils import Timeout # type: ignore[missing-import] - from parlant.core.sessions import EventKind, EventSource # type: ignore[missing-import] + from parlant.core.async_utils import Timeout # type: ignore[missing-import] # noqa: PLC0415 -- genuinely deferred; parlant extra kept out of this module's unconditional import surface + from parlant.core.sessions import EventKind, EventSource # type: ignore[missing-import] # noqa: PLC0415 -- genuinely deferred; parlant extra kept out of this module's unconditional import surface current_offset = min_offset # Wait up to the total response budget, polling in shorter windows. An empty diff --git a/src/band/agent.py b/src/band/agent.py index 23344ac79..e68263672 100644 --- a/src/band/agent.py +++ b/src/band/agent.py @@ -148,7 +148,13 @@ def create( on_participant_removed: Optional callback for participant_removed events. preprocessor: Custom event preprocessor (default: DefaultPreprocessor) """ - from band.config.settings import PlatformSettings + # Deferred: importing band.config here at module top-level reorders + # this module's own band.core.* imports behind it, which reintroduces + # a real circular import (band.config -> band.logging_config -> + # band.core.exceptions -> band.core.simple_adapter, back into + # band.logging_config mid-init) -- verified by moving it and running + # `python -c "import band.agent"`. + from band.config.settings import PlatformSettings # noqa: PLC0415 settings = PlatformSettings() runtime = PlatformRuntime( @@ -193,7 +199,10 @@ def from_config( Returns: Configured Agent instance. """ - from band.config.loader import load_agent_config + # Deferred for the same reason as PlatformSettings above: a top-level + # band.config import here reorders this module's band.core.* imports + # behind it, reintroducing a real circular import. + from band.config.loader import load_agent_config # noqa: PLC0415 agent_id, api_key = load_agent_config(name, config_path=config_path) return cls.create( diff --git a/src/band/converters/a2a.py b/src/band/converters/a2a.py index 753a29f86..be2373864 100644 --- a/src/band/converters/a2a.py +++ b/src/band/converters/a2a.py @@ -3,12 +3,10 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any +from typing import Any from band.core.protocols import HistoryConverter - -if TYPE_CHECKING: - from band.integrations.a2a.types import A2ASessionState +from band.integrations.a2a.types import A2ASessionState logger = logging.getLogger(__name__) @@ -36,9 +34,6 @@ def convert(self, raw: list[dict[str, Any]]) -> A2ASessionState: Returns: A2ASessionState with extracted context_id, task_id, and task_state """ - # Import at runtime to avoid circular import - from band.integrations.a2a.types import A2ASessionState - context_id: str | None = None task_id: str | None = None task_state: str | None = None diff --git a/src/band/converters/a2a_gateway.py b/src/band/converters/a2a_gateway.py index da131089a..918caaeab 100644 --- a/src/band/converters/a2a_gateway.py +++ b/src/band/converters/a2a_gateway.py @@ -8,8 +8,6 @@ from band.core.protocols import HistoryConverter -# Use TYPE_CHECKING to avoid circular import: -# gateway/__init__.py -> adapter.py -> this module -> gateway/types.py -> gateway/__init__.py if TYPE_CHECKING: from band.integrations.a2a.gateway.types import GatewaySessionState @@ -44,8 +42,10 @@ def convert(self, raw: list[dict[str, Any]]) -> GatewaySessionState: GatewaySessionState with context_to_room and room_participants mappings extracted from the history. """ - # Runtime import to avoid circular import at module load time - from band.integrations.a2a.gateway.types import GatewaySessionState + # band.integrations.a2a.gateway.types imports the optional + # `a2a_gateway` extra (a2a-sdk) at module top level — deferred so + # this converter stays importable without it. + from band.integrations.a2a.gateway.types import GatewaySessionState # noqa: PLC0415 context_to_room: dict[str, str] = {} room_participants: dict[str, set[str]] = defaultdict(set) diff --git a/src/band/converters/acp_client.py b/src/band/converters/acp_client.py index fa2734a7c..eb85d7a9a 100644 --- a/src/band/converters/acp_client.py +++ b/src/band/converters/acp_client.py @@ -39,8 +39,10 @@ def convert(self, raw: list[dict[str, Any]]) -> ACPClientSessionState: ACPClientSessionState with room-to-session resume candidates and the room's replayable text transcript. """ - # Runtime import to avoid circular import at module load time - from band.integrations.acp.client_types import ACPClientSessionState + # band.integrations.acp.client_types imports client_runtime, which + # imports the optional `acp` extra (agent-client-protocol) at module + # top level — deferred so this converter stays importable without it. + from band.integrations.acp.client_types import ACPClientSessionState # noqa: PLC0415 room_to_session: dict[str, str] = {} diff --git a/src/band/converters/acp_server.py b/src/band/converters/acp_server.py index ce2da2590..19e2758ef 100644 --- a/src/band/converters/acp_server.py +++ b/src/band/converters/acp_server.py @@ -3,14 +3,10 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any +from typing import Any from band.core.protocols import HistoryConverter - -# Use TYPE_CHECKING to avoid circular import: -# acp/__init__.py -> server_adapter.py -> this module -> acp/types.py -> acp/__init__.py -if TYPE_CHECKING: - from band.integrations.acp.types import ACPSessionState +from band.integrations.acp.types import ACPSessionState logger = logging.getLogger(__name__) @@ -40,9 +36,6 @@ def convert(self, raw: list[dict[str, Any]]) -> ACPSessionState: ACPSessionState with session_to_room mapping extracted from the history. """ - # Runtime import to avoid circular import at module load time - from band.integrations.acp.types import ACPSessionState - session_to_room: dict[str, str] = {} session_cwd: dict[str, str] = {} session_mcp_servers: dict[str, list[dict[str, Any]]] = {} diff --git a/src/band/converters/slack.py b/src/band/converters/slack.py index 83a250be7..b0493f4fb 100644 --- a/src/band/converters/slack.py +++ b/src/band/converters/slack.py @@ -14,12 +14,10 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any +from typing import Any from band.core.protocols import HistoryConverter - -if TYPE_CHECKING: - from band.integrations.slack.types import SlackSessionState +from band.integrations.slack.types import SlackRoomBinding, SlackSessionState logger = logging.getLogger(__name__) @@ -45,11 +43,6 @@ def convert(self, raw: list[dict[str, Any]]) -> SlackSessionState: contains a Slack bootstrap task event, otherwise the empty default state. """ - from band.integrations.slack.types import ( - SlackRoomBinding, - SlackSessionState, - ) - binding: SlackRoomBinding | None = None for msg in raw: if msg.get("message_type") != "task": diff --git a/src/band/integrations/a2a/__init__.py b/src/band/integrations/a2a/__init__.py index 7c7c51242..e17b0194e 100644 --- a/src/band/integrations/a2a/__init__.py +++ b/src/band/integrations/a2a/__init__.py @@ -27,7 +27,21 @@ await agent.run() """ -from band.integrations.a2a.adapter import A2AAdapter -from band.integrations.a2a.types import A2AAuth, A2ASessionState +from __future__ import annotations -__all__ = ["A2AAdapter", "A2AAuth", "A2ASessionState"] +from typing import TYPE_CHECKING + +from band.exports import lazy_exports + +if TYPE_CHECKING: + from band.integrations.a2a.adapter import A2AAdapter as A2AAdapter + from band.integrations.a2a.types import ( + A2AAuth as A2AAuth, + A2ASessionState as A2ASessionState, + ) + +__all__, __getattr__ = lazy_exports( + __name__, + adapter=["A2AAdapter"], + types=["A2AAuth", "A2ASessionState"], +) diff --git a/src/band/integrations/a2a/gateway/__init__.py b/src/band/integrations/a2a/gateway/__init__.py index 0f0ddf89a..5df997f30 100644 --- a/src/band/integrations/a2a/gateway/__init__.py +++ b/src/band/integrations/a2a/gateway/__init__.py @@ -1,14 +1,28 @@ """A2A Gateway adapter for exposing Band peers as A2A endpoints.""" -from band.integrations.a2a.gateway.adapter import A2AGatewayAdapter -from band.integrations.a2a.gateway.config import A2AGatewayAdapterConfig -from band.integrations.a2a.gateway.server import GatewayServer -from band.integrations.a2a.gateway.types import GatewaySessionState, PendingA2ATask - -__all__ = [ - "A2AGatewayAdapter", - "A2AGatewayAdapterConfig", - "GatewayServer", - "GatewaySessionState", - "PendingA2ATask", -] +from __future__ import annotations + +from typing import TYPE_CHECKING + +from band.exports import lazy_exports + +if TYPE_CHECKING: + from band.integrations.a2a.gateway.adapter import ( + A2AGatewayAdapter as A2AGatewayAdapter, + ) + from band.integrations.a2a.gateway.config import ( + A2AGatewayAdapterConfig as A2AGatewayAdapterConfig, + ) + from band.integrations.a2a.gateway.server import GatewayServer as GatewayServer + from band.integrations.a2a.gateway.types import ( + GatewaySessionState as GatewaySessionState, + PendingA2ATask as PendingA2ATask, + ) + +__all__, __getattr__ = lazy_exports( + __name__, + adapter=["A2AGatewayAdapter"], + config=["A2AGatewayAdapterConfig"], + server=["GatewayServer"], + types=["GatewaySessionState", "PendingA2ATask"], +) diff --git a/src/band/integrations/a2a/gateway/server.py b/src/band/integrations/a2a/gateway/server.py index 6d0fa28ed..0af03bce8 100644 --- a/src/band/integrations/a2a/gateway/server.py +++ b/src/band/integrations/a2a/gateway/server.py @@ -238,7 +238,7 @@ async def _handle_list_peers(self, _request: Request) -> JSONResponse: return JSONResponse({"peers": peers, "count": len(peers)}) async def start(self) -> None: - import uvicorn + import uvicorn # noqa: PLC0415 -- a2a_gateway extra kept out of this module's unconditional import surface self._app = self._build_app() self._uvicorn = uvicorn.Server( diff --git a/src/band/integrations/acp/__init__.py b/src/band/integrations/acp/__init__.py index 431839ca9..9519a9f97 100644 --- a/src/band/integrations/acp/__init__.py +++ b/src/band/integrations/acp/__init__.py @@ -40,6 +40,7 @@ from __future__ import annotations +import importlib from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -99,8 +100,6 @@ def __getattr__(name: str) -> object: if name in _IMPORT_MAP: module_path, attr_name = _IMPORT_MAP[name] - import importlib - module = importlib.import_module(module_path) return getattr(module, attr_name) raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/band/integrations/acp/cli.py b/src/band/integrations/acp/cli.py index 754b6d470..13e39b1b6 100644 --- a/src/band/integrations/acp/cli.py +++ b/src/band/integrations/acp/cli.py @@ -8,6 +8,7 @@ import os import sys +from band import Agent from band.config.logs import LogSettings from band.logging_config import LogStream @@ -81,11 +82,12 @@ async def main(args: argparse.Namespace | None = None) -> None: if not args.api_key: raise ValueError("API key is required. Use --api-key or set BAND_API_KEY.") - # Lazy imports to avoid import errors when ACP deps are not installed - from band import Agent - from band.integrations.acp.push_handler import ACPPushHandler - from band.integrations.acp.server import ACPServer, run_acp_server - from band.integrations.acp.server_adapter import BandACPServerAdapter + # Lazy: band.integrations.acp.server imports the optional `acp` extra + # (agent-client-protocol) at its own top level, so importing it eagerly + # here would break every venv that doesn't install the `acp` extra. + from band.integrations.acp.push_handler import ACPPushHandler # noqa: PLC0415 + from band.integrations.acp.server import ACPServer, run_acp_server # noqa: PLC0415 + from band.integrations.acp.server_adapter import BandACPServerAdapter # noqa: PLC0415 adapter = BandACPServerAdapter() diff --git a/src/band/integrations/acp/client_adapter.py b/src/band/integrations/acp/client_adapter.py index 1dfd13a45..b2023233d 100644 --- a/src/band/integrations/acp/client_adapter.py +++ b/src/band/integrations/acp/client_adapter.py @@ -45,6 +45,7 @@ ) from band.integrations.acp.room_emitter import RoomTurnEmitter from band.integrations.acp.types import ACPToolCall +from band.runtime.prompts import render_system_prompt from band.runtime.custom_tools import CustomToolDef, get_custom_tool_name from band.runtime.formatters import messages_before from band.integrations.mcp.local_server import LocalMCPServer @@ -439,8 +440,6 @@ def _resolve_transport( return (host, port) if has_tcp else (None, None) def _build_system_context(self, room_id: str, msg: PlatformMessage) -> str: - from band.runtime.prompts import render_system_prompt - agent_name = self.agent_name or "Agent" agent_desc = self.agent_description or "An AI assistant" requester_name = msg.sender_name or msg.sender_id or "Unknown" diff --git a/src/band/integrations/acp/server.py b/src/band/integrations/acp/server.py index a4df07457..e825bb8f9 100644 --- a/src/band/integrations/acp/server.py +++ b/src/band/integrations/acp/server.py @@ -10,6 +10,7 @@ NewSessionResponse, PromptResponse, run_agent, + update_agent_message_text, ) from acp.schema import ( AgentCapabilities, @@ -417,8 +418,6 @@ async def ext_notification(self, method: str, params: dict[str, Any]) -> None: if session_id and self._adapter.has_session(session_id): acp_client = self._adapter.get_acp_client() if acp_client: - from acp import update_agent_message_text - # Forward as informational text update match method: case "cursor/update_todos": diff --git a/src/band/integrations/base.py b/src/band/integrations/base.py index cf52ea5f5..f255826b7 100644 --- a/src/band/integrations/base.py +++ b/src/band/integrations/base.py @@ -43,6 +43,8 @@ from typing import TYPE_CHECKING +from band.runtime.formatters import build_participants_message + if TYPE_CHECKING: from band.runtime.execution import ExecutionContext @@ -67,8 +69,6 @@ def check_and_format_participants(ctx: "ExecutionContext") -> str | None: Formatted participant message if changed, None otherwise. Automatically calls mark_participants_sent() if changed. """ - from band.runtime.formatters import build_participants_message - if not ctx.participants_changed(): return None diff --git a/src/band/integrations/claude_sdk/tools.py b/src/band/integrations/claude_sdk/tools.py index e064fadd4..af8267255 100644 --- a/src/band/integrations/claude_sdk/tools.py +++ b/src/band/integrations/claude_sdk/tools.py @@ -37,6 +37,7 @@ BASE_TOOL_NAMES, CHAT_ID_FIELD_NAME, CHAT_TOOL_NAMES, + AgentTools, BandTool, ToolDefinition, append_mention_handles_hint, @@ -324,7 +325,6 @@ def create_band_mcp_server(agent: Any) -> Any: The returned server uses room-scoped ``AgentTools`` instances resolved from the running agent state at tool-call time. """ - from band.runtime.tools import AgentTools def _execution_for(room_id: str) -> ExecutionContext | None: executions = agent.runtime.executions if agent.runtime else {} diff --git a/src/band/integrations/codex/websocket_client.py b/src/band/integrations/codex/websocket_client.py index d5f229a4e..9271d6046 100644 --- a/src/band/integrations/codex/websocket_client.py +++ b/src/band/integrations/codex/websocket_client.py @@ -40,7 +40,8 @@ async def connect(self) -> None: return try: - from websockets.asyncio.client import connect + # `websockets` ships only under the `codex` extra, not core deps. + from websockets.asyncio.client import connect # noqa: PLC0415 -- codex extra, absent from the standard dev venv except ImportError as exc: raise RuntimeError( "websockets package is required for CodexWebSocketClient" diff --git a/src/band/integrations/crewai/runtime.py b/src/band/integrations/crewai/runtime.py index fa2374ee3..26b94aee0 100644 --- a/src/band/integrations/crewai/runtime.py +++ b/src/band/integrations/crewai/runtime.py @@ -40,7 +40,7 @@ def _ensure_nest_asyncio() -> None: return try: - import nest_asyncio + import nest_asyncio # noqa: PLC0415 except ImportError as e: # pragma: no cover - same import guard as the adapter raise ImportError( "crewai is required for CrewAI adapter.\n" diff --git a/src/band/integrations/crewai/tools.py b/src/band/integrations/crewai/tools.py index 886545039..d3c0c10b6 100644 --- a/src/band/integrations/crewai/tools.py +++ b/src/band/integrations/crewai/tools.py @@ -158,7 +158,7 @@ def _platform_tool( fallback_loop: asyncio.AbstractEventLoop | None, ) -> BaseTool: """Wrap one ToolSpec as the CrewAI BaseTool instance the crew is handed.""" - from crewai.tools import BaseTool + from crewai.tools import BaseTool # noqa: PLC0415 -- crewai extra, absent from the standard dev venv class PlatformTool(BaseTool): # str(...): pydantic doesn't validate field defaults (no @@ -192,7 +192,7 @@ def _custom_tool( fallback_loop: asyncio.AbstractEventLoop | None, ) -> BaseTool: """Wrap one CustomToolDef as a CrewAI BaseTool instance.""" - from crewai.tools import BaseTool + from crewai.tools import BaseTool # noqa: PLC0415 -- crewai extra, absent from the standard dev venv input_model, handler = definition tool_name = get_custom_tool_name(input_model) diff --git a/src/band/integrations/desktop_app/cli.py b/src/band/integrations/desktop_app/cli.py index a6d48c4ce..fda7f39a7 100644 --- a/src/band/integrations/desktop_app/cli.py +++ b/src/band/integrations/desktop_app/cli.py @@ -20,6 +20,6 @@ def entry_point() -> None: "through fcntl file locks and Unix sockets, which Windows does " "not provide." ) - from band.integrations.desktop_app.server import entry_point + from band.integrations.desktop_app.server import entry_point # noqa: PLC0415 entry_point() diff --git a/src/band/integrations/mcp/backends.py b/src/band/integrations/mcp/backends.py index 766d6fcd5..b20d9e6c9 100644 --- a/src/band/integrations/mcp/backends.py +++ b/src/band/integrations/mcp/backends.py @@ -82,7 +82,7 @@ async def create_band_mcp_backend( allowed_tools = _build_allowed_tools(tool_definitions, resolved_tools) if kind == "sdk": - from band.integrations.claude_sdk.tools import ( + from band.integrations.claude_sdk.tools import ( # noqa: PLC0415 -- only load the claude_sdk extra when the sdk transport kind is selected build_band_sdk_tools, create_band_sdk_mcp_server, ) diff --git a/src/band/integrations/parlant/tools.py b/src/band/integrations/parlant/tools.py index ca54d2e1d..dc04f0df6 100644 --- a/src/band/integrations/parlant/tools.py +++ b/src/band/integrations/parlant/tools.py @@ -219,8 +219,8 @@ def create_parlant_tools(features: AdapterFeatures | None = None) -> list[Any]: List of Parlant ToolEntry objects """ try: - import parlant.sdk as p # type: ignore[missing-import] - from parlant.core.tools import ( # type: ignore[missing-import] + import parlant.sdk as p # type: ignore[missing-import] # noqa: PLC0415 -- parlant extra, absent from the standard dev venv + from parlant.core.tools import ( # type: ignore[missing-import] # noqa: PLC0415 -- parlant extra, absent from the standard dev venv ToolContext, ToolParameterOptions, ToolResult, diff --git a/src/band/integrations/slack/__init__.py b/src/band/integrations/slack/__init__.py index 8fb38af5e..6fcffa287 100644 --- a/src/band/integrations/slack/__init__.py +++ b/src/band/integrations/slack/__init__.py @@ -30,7 +30,21 @@ await agent.run() """ -from band.integrations.slack.adapter import SlackAdapter -from band.integrations.slack.types import SlackApp, SlackSessionState +from __future__ import annotations -__all__ = ["SlackAdapter", "SlackApp", "SlackSessionState"] +from typing import TYPE_CHECKING + +from band.exports import lazy_exports + +if TYPE_CHECKING: + from band.integrations.slack.adapter import SlackAdapter as SlackAdapter + from band.integrations.slack.types import ( + SlackApp as SlackApp, + SlackSessionState as SlackSessionState, + ) + +__all__, __getattr__ = lazy_exports( + __name__, + adapter=["SlackAdapter"], + types=["SlackApp", "SlackSessionState"], +) diff --git a/src/band/integrations/slack/adapter.py b/src/band/integrations/slack/adapter.py index c7cdac93b..d84ab893b 100644 --- a/src/band/integrations/slack/adapter.py +++ b/src/band/integrations/slack/adapter.py @@ -385,7 +385,7 @@ def __init__( by the chosen transport is missing. """ try: - import slack_sdk # noqa: F401 + import slack_sdk # noqa: F401, PLC0415 except ImportError as exc: raise ImportError( "slack-sdk is required for SlackAdapter. " @@ -600,7 +600,7 @@ async def on_started(self, agent_name: str, agent_description: str) -> None: if self._transport == "socket": # Lazy import so HTTP-only installs don't pay the aiohttp / # Socket Mode import cost. - from band.integrations.slack.socket import ( + from band.integrations.slack.socket import ( # noqa: PLC0415 start_socket_listeners, ) @@ -1277,7 +1277,9 @@ async def _set_status( @staticmethod def _default_web_client_factory(app: SlackApp) -> AsyncWebClient: - from slack_sdk.web.async_client import AsyncWebClient + # slack extra kept out of this module's unconditional import surface; + # TYPE_CHECKING above supplies the annotation without a runtime cost + from slack_sdk.web.async_client import AsyncWebClient # noqa: PLC0415 return AsyncWebClient(token=app.bot_token) diff --git a/src/band/integrations/slack/socket.py b/src/band/integrations/slack/socket.py index f0c6c8b1f..e6dd5b0e9 100644 --- a/src/band/integrations/slack/socket.py +++ b/src/band/integrations/slack/socket.py @@ -125,7 +125,7 @@ def _default_client_factory( app: SlackApp, web_client: AsyncWebClient ) -> SocketModeClient: try: - from slack_sdk.socket_mode.aiohttp import SocketModeClient + from slack_sdk.socket_mode.aiohttp import SocketModeClient # noqa: PLC0415 except ImportError as exc: # pragma: no cover — import-time guard raise ImportError( "Socket Mode requires aiohttp. Install with " @@ -149,7 +149,8 @@ def _make_request_handler( avoid retries. Redelivered events (same ``event_id``) are dropped via ``seen_events`` so a reconnect can't double-invoke the brain. """ - from slack_sdk.socket_mode.response import SocketModeResponse + # slack extra kept out of this module's unconditional import surface + from slack_sdk.socket_mode.response import SocketModeResponse # noqa: PLC0415 async def handle(client: SocketModeClient, req: Any) -> None: envelope_id = getattr(req, "envelope_id", None) diff --git a/src/band/logging_config.py b/src/band/logging_config.py index 704d50723..6e993fc47 100644 --- a/src/band/logging_config.py +++ b/src/band/logging_config.py @@ -916,7 +916,7 @@ def _build_json_formatter( # _TraceContextFilter always sets record.trace_context; without this, # JsonFormatter's default (any non-reserved attribute is a free "extra") # would leak it into output even when json_fields excludes it. - from pythonjsonlogger.core import RESERVED_ATTRS + from pythonjsonlogger.core import RESERVED_ATTRS # noqa: PLC0415 -- logging extra, guarded above fields = tuple(json_fields or _JSON_DEFAULT_FIELDS) json_formatter: LoggingConfig = { @@ -936,8 +936,10 @@ def _build_json_formatter( def _build_rich_handler(*, stream: LogStream, datefmt: str) -> logging.Handler: - from rich.console import Console - from rich.logging import RichHandler + # rich ships with the optional `logging` extra (see _require_optional_package's + # caller above); a top-level import would break every install that omits it. + from rich.console import Console # noqa: PLC0415 + from rich.logging import RichHandler # noqa: PLC0415 # Do not let Rich default to stderr when callers requested stdout. output = sys.stdout if stream == LogStream.STDOUT else sys.stderr diff --git a/src/band/runtime/execution.py b/src/band/runtime/execution.py index 151d26a8c..197028383 100644 --- a/src/band/runtime/execution.py +++ b/src/band/runtime/execution.py @@ -32,7 +32,12 @@ from band_sdk_core import ClaimRegistry, ParticipantRoster, RetryTracker from band.client.rest import DEFAULT_REQUEST_OPTIONS -from band.client.streaming import ControlMode, DeliveryStatus +from band.client.streaming import ( + ControlMode, + DeliveryStatus, + MessageCreatedPayload, + MessageMetadata, +) from band.logging_config import TRACE_CONTEXT from band.platform.event import ( MessageEvent, @@ -52,6 +57,7 @@ SYNTHETIC_CONTACT_EVENTS_SENDER_ID, ) from band.runtime.context_serialization import context_item_to_dict +from band.runtime.formatters import build_participants_message, format_history_for_llm from band.runtime.participants import log_roster_call, log_roster_error from band.runtime.working_state import WorkingStateReporter @@ -1006,9 +1012,6 @@ def get_history_for_llm( if not self._context_cache: return [] - # Import here to avoid circular dependency - from band.runtime.formatters import format_history_for_llm - return format_history_for_llm( self._context_cache.messages, exclude_id=exclude_message_id, @@ -1017,8 +1020,6 @@ def get_history_for_llm( def build_participants_message(self) -> str: """Build a system message with current participant list for LLM.""" - from band.runtime.formatters import build_participants_message - return build_participants_message(self._roster.list()) async def _notify_participant_added(self, event: ParticipantAddedEvent) -> None: @@ -1523,8 +1524,6 @@ async def _process_claimed_backlog_message( metadata["status"] = "sent" # Create event from message for handler - from band.client.streaming import MessageCreatedPayload, MessageMetadata - event = MessageEvent( room_id=self.room_id, payload=MessageCreatedPayload( diff --git a/src/band/runtime/tools.py b/src/band/runtime/tools.py index f02b1b205..8b63e58df 100644 --- a/src/band/runtime/tools.py +++ b/src/band/runtime/tools.py @@ -20,6 +20,11 @@ import band_sdk_core from async_lru import alru_cache +from band_rest import ( + AgentRegisterRequest, + CreateContactRequestRequestContactRequest, + CreateMyChatRoomRequestChat, +) from pydantic import ( AliasChoices, BaseModel, @@ -30,13 +35,19 @@ ) from band.client.rest import ( + AgentMemoryCreateRequest, + ChatEventRequest, + ChatMessageRequest, + ChatMessageRequestMentionsItem, ChatRoomRequest, DEFAULT_REQUEST_OPTIONS, NotFoundError, + ParticipantRequest, UnprocessableEntityError, ) from band.config.settings import RuntimeSettings from band.runtime.capabilities import with_hub_room_contacts +from band.runtime.context_serialization import context_item_to_dict from band.runtime.participants import log_roster_call, participant_snapshot from band.core.exceptions import BandToolError from band.core.memory_types import ( @@ -2039,11 +2050,6 @@ async def send_message( Raises: ValueError: If a mentioned handle is not found in participants """ - from band.client.rest import ( - ChatMessageRequest, - ChatMessageRequestMentionsItem, - ) - # Deprecation warning for dict-style mentions WITHOUT an id: those # lean on name/handle resolution, which list[str] does better. # Id-bearing dicts are adapter-supplied ground truth (the message's @@ -2105,8 +2111,6 @@ async def send_event( Fern ChatEvent model (Pydantic). Serialized to dict by execute_tool_call() at the adapter boundary. """ - from band.client.rest import ChatEventRequest - logger.debug("Sending %s event to room %s", message_type, self.room_id) if not content: @@ -2171,8 +2175,6 @@ async def fetch_room_context( first. Used by state-reconstruction adapters (e.g. CrewAI Flow) to rebuild durable run state from task events. """ - from band.runtime.context_serialization import context_item_to_dict - response = await self.rest.agent_api_context.get_agent_chat_context( chat_id=room_id, page=page, @@ -2218,8 +2220,6 @@ async def add_participant( Raises: ValueError: If participant not found """ - from band.client.rest import ParticipantRequest - logger.debug( "Adding participant '%s' with role '%s' to room %s", identifier, @@ -2676,8 +2676,6 @@ async def store_memory( Fern Memory model (Pydantic). Serialized to dict by execute_tool_call() at the adapter boundary. """ - from band.client.rest import AgentMemoryCreateRequest - band_sdk_core.validate_memory_type_for_system(system, type) validate_subject_scope(MemoryStoreScope(scope), subject_id) @@ -3427,8 +3425,6 @@ async def list_my_agents( async def register_my_agent(self, name: str, description: str) -> Any: """Register a new remote agent owned by the user.""" - from band_rest import AgentRegisterRequest - logger.debug("Registering my agent: name=%s", name) agent_request = AgentRegisterRequest(name=name, description=description) return await self.rest.human_api_agents.register_my_agent( @@ -3451,8 +3447,6 @@ async def list_my_chats( async def create_my_chat_room(self, task_id: str | None = None) -> Any: """Create a new chat room with the user as owner.""" - from band_rest import CreateMyChatRoomRequestChat - logger.debug("Creating my chat room: task_id=%s", task_id) chat_request = ( CreateMyChatRoomRequestChat(task_id=task_id) @@ -3486,8 +3480,6 @@ async def create_contact_request( self, recipient_handle: str, message: str | None = None ) -> Any: """Send a contact request to another user.""" - from band_rest import CreateContactRequestRequestContactRequest - logger.debug("Creating contact request to: %s", recipient_handle) kwargs: dict[str, Any] = {"recipient_handle": recipient_handle} if message is not None: @@ -3631,8 +3623,6 @@ async def send_my_chat_message( MCP handler output verbatim (no exception raised) so the observable tool-surface error shape is preserved. """ - from band_rest import ChatMessageRequest, ChatMessageRequestMentionsItem - recipient_names = [ name.strip().lower() for name in recipients.split(",") if name.strip() ] @@ -3715,8 +3705,6 @@ async def add_my_chat_participant( Returns ``f"Added participant: {participant_id}"`` (discards the Fern response body) to match today's MCP handler output verbatim. """ - from band_rest import ParticipantRequest - logger.debug( "Adding my chat participant: chat_id=%s, participant_id=%s, role=%s", chat_id, diff --git a/tests/adapters/copilot_sdk/test_reply.py b/tests/adapters/copilot_sdk/test_reply.py index 3a7ab7acf..76f4c03c8 100644 --- a/tests/adapters/copilot_sdk/test_reply.py +++ b/tests/adapters/copilot_sdk/test_reply.py @@ -5,7 +5,7 @@ import pytest from band.adapters.copilot_sdk import _COPILOT_SDK_AVAILABLE -from band.runtime.tools import CHAT_ID_FIELD_NAME +from band.runtime.tools import CHAT_ID_FIELD_NAME, ToolCallOutcome from tests.adapters.copilot_sdk.fakes import ( FakeCopilotClient, FakeCopilotSession, @@ -114,7 +114,6 @@ async def test_fallback_fires_when_band_send_message_fails(self): """A failed band_send_message (ok=False, no exception) must NOT mark the turn replied — the final-text fallback must still fire, else the user gets a silent turn.""" - from band.runtime.tools import ToolCallOutcome class SendFailsTools(ToolSchemaFakeTools): async def execute_tool_call_structured(self, tool_name, arguments): diff --git a/tests/adapters/copilot_sdk/test_tool_bridging.py b/tests/adapters/copilot_sdk/test_tool_bridging.py index 3cc2c78a8..82c6d327e 100644 --- a/tests/adapters/copilot_sdk/test_tool_bridging.py +++ b/tests/adapters/copilot_sdk/test_tool_bridging.py @@ -6,7 +6,7 @@ from typing import Any import pytest -from pydantic import BaseModel +from pydantic import BaseModel, model_validator from band.adapters.copilot_sdk import _COPILOT_SDK_AVAILABLE from band.core.types import Emit @@ -144,7 +144,6 @@ async def echo(params: EchoInput) -> str: @pytest.mark.asyncio async def test_model_level_validation_error_is_llm_readable(self): """Model-validator errors have loc=() and must not crash the handler.""" - from pydantic import model_validator class PairInput(BaseModel): a: int diff --git a/tests/adapters/langgraph/test_graph_patterns.py b/tests/adapters/langgraph/test_graph_patterns.py index d90c74d95..f74b3ada0 100644 --- a/tests/adapters/langgraph/test_graph_patterns.py +++ b/tests/adapters/langgraph/test_graph_patterns.py @@ -10,6 +10,7 @@ from band.core.types import PlatformMessage from .helpers import make_capture_graph +from langchain_core.tools import StructuredTool class TestStaticGraph: @@ -102,7 +103,6 @@ async def test_factory_receives_distinct_tools_per_room( right tools to the factory each time, so a correctly-written factory has access to the current room's wrappers. """ - from langchain_core.tools import StructuredTool # Two rooms, two distinct AgentToolsProtocol instances. Wrappers # dispatch through ``tools.execute_tool_call(name, kwargs)``, so we diff --git a/tests/adapters/langgraph/test_lifecycle.py b/tests/adapters/langgraph/test_lifecycle.py index 0443643cb..dd338ec26 100644 --- a/tests/adapters/langgraph/test_lifecycle.py +++ b/tests/adapters/langgraph/test_lifecycle.py @@ -7,8 +7,10 @@ import pytest from langchain_core.messages import HumanMessage, SystemMessage +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.graph import END, START, MessagesState, StateGraph -from band.adapters.langgraph import LangGraphAdapter +from band.adapters.langgraph import _BOOTSTRAP_TRACKING_WARN_THRESHOLD, LangGraphAdapter from band.core.types import PlatformMessage from .helpers import make_capture_graph @@ -124,8 +126,6 @@ async def test_warns_on_large_bootstrapped_rooms( self, sample_message, mock_tools, mock_llm, mock_checkpointer ): """Should log a warning when _bootstrapped_rooms reaches threshold.""" - from band.adapters.langgraph import _BOOTSTRAP_TRACKING_WARN_THRESHOLD - adapter = LangGraphAdapter( llm=mock_llm, checkpointer=mock_checkpointer, @@ -213,9 +213,6 @@ async def test_restart_with_existing_checkpointer_state_does_not_rehydrate_twice self, mock_tools ): """Persistent checkpointer state should suppress duplicate bootstrap history.""" - from langgraph.checkpoint.memory import InMemorySaver - from langgraph.graph import END, START, MessagesState, StateGraph - checkpointer = InMemorySaver() seen_contents: list[list[str]] = [] seen_system_counts: list[int] = [] @@ -284,8 +281,6 @@ def capture_messages(state: MessagesState) -> dict[str, list[Any]]: @pytest.mark.asyncio async def test_empty_checkpointer_state_still_allows_bootstrap_hydration(self): - from langgraph.checkpoint.memory import InMemorySaver - adapter = LangGraphAdapter(graph=MagicMock(), inject_system_prompt=True) assert ( diff --git a/tests/adapters/langgraph/test_message_input.py b/tests/adapters/langgraph/test_message_input.py index 7faa4d3f9..9b5fd8a49 100644 --- a/tests/adapters/langgraph/test_message_input.py +++ b/tests/adapters/langgraph/test_message_input.py @@ -11,6 +11,9 @@ from band.core.types import Capability, Emit, PlatformMessage from .helpers import make_capture_graph +from langchain_core.tools import tool +from langgraph.graph import END, START, MessagesState, StateGraph +from langgraph.prebuilt import ToolNode class TestOnMessage: @@ -288,9 +291,6 @@ async def test_feature_capabilities_control_tool_groups( async def test_real_compiled_graph_emits_tool_events( self, sample_message, mock_tools ): - from langchain_core.tools import tool - from langgraph.graph import END, START, MessagesState, StateGraph - from langgraph.prebuilt import ToolNode @tool async def record_value(value: str) -> str: @@ -348,7 +348,6 @@ def request_tool(state: MessagesState) -> dict[str, list[AIMessage]]: async def test_real_compiled_graph_can_opt_into_bootstrap_system_prompt( self, sample_message, mock_tools ): - from langgraph.graph import END, START, MessagesState, StateGraph seen_prompts: list[str] = [] diff --git a/tests/adapters/langgraph/test_system_prompt.py b/tests/adapters/langgraph/test_system_prompt.py index d7a99bb7d..57f669bb3 100644 --- a/tests/adapters/langgraph/test_system_prompt.py +++ b/tests/adapters/langgraph/test_system_prompt.py @@ -9,6 +9,8 @@ from band.adapters.langgraph import LangGraphAdapter from .helpers import make_capture_graph +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.graph import END, START, MessagesState, StateGraph class TestSystemPromptCrossTurn: @@ -95,8 +97,6 @@ async def test_real_checkpointer_carries_system_prompt_forward( checkpointer (not the adapter) is what keeps the system prompt present across turns. """ - from langgraph.checkpoint.memory import InMemorySaver - from langgraph.graph import END, START, MessagesState, StateGraph checkpointer = InMemorySaver() seen_system_prompts: list[list[str]] = [] diff --git a/tests/adapters/opencode/test_setup.py b/tests/adapters/opencode/test_setup.py index 17dc323fc..d7a457b57 100644 --- a/tests/adapters/opencode/test_setup.py +++ b/tests/adapters/opencode/test_setup.py @@ -27,6 +27,7 @@ make_platform_message, tools_protocol, ) +import httpx def test_no_leaked_adapter_config_env_vars( @@ -37,7 +38,6 @@ def test_no_leaked_adapter_config_env_vars( async def test_startup_fails_loudly_when_server_unreachable() -> None: """The default (real-server) path must fail at startup naming the fix.""" - import httpx adapter = OpencodeAdapter() with patch( diff --git a/tests/adapters/test_anthropic_adapter.py b/tests/adapters/test_anthropic_adapter.py index d8cd1b945..dd8d83c3d 100644 --- a/tests/adapters/test_anthropic_adapter.py +++ b/tests/adapters/test_anthropic_adapter.py @@ -7,15 +7,26 @@ message history management, tool execution, custom tools, and error handling. """ +import asyncio +import json +import logging from datetime import datetime, timezone from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch import pytest +from anthropic.types import TextBlock, ToolUseBlock from pydantic import BaseModel, Field from band.adapters.anthropic import AnthropicAdapter -from band.core.types import Emit, PlatformMessage, TurnUsage +from band.core.types import ( + USAGE_EVENT_TYPE, + USAGE_METADATA_KEY, + Emit, + PlatformMessage, + ToolEventKey, + TurnUsage, +) from tests.adapters.usage_events import sent_usage_payloads @@ -207,7 +218,6 @@ class TestHelperMethods: def test_extract_text_content(self): """Should extract text from TextBlock content.""" - from anthropic.types import TextBlock adapter = AnthropicAdapter() @@ -230,7 +240,6 @@ def test_extract_text_content_empty(self): def test_serialize_content_blocks(self): """Should serialize ToolUseBlock and TextBlock.""" - from anthropic.types import TextBlock, ToolUseBlock adapter = AnthropicAdapter() @@ -256,7 +265,6 @@ class TestToolExecution: @pytest.mark.asyncio async def test_reports_tool_calls_when_enabled(self, mock_tools): """Should send events when execution reporting is enabled.""" - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter(emit=Emit.TOOL_CALLS) @@ -284,7 +292,6 @@ async def test_read_room_file_image_result_passes_through_as_vision_content( """An image band_read_room_file result must reach the model as a real Anthropic image content block, not get json.dumps'd into text (which would send the model a giant base64 string it can't see).""" - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter(emit=()) @@ -324,11 +331,6 @@ async def test_read_room_file_image_result_reports_placeholder_not_raw_base64( report a bounded placeholder, not the raw base64 payload -- the LLM- facing content block (asserted above) is a separate path from what gets reported to the platform-visible event.""" - import json - - from anthropic.types import ToolUseBlock - - from band.core.types import ToolEventKey adapter = AnthropicAdapter(emit=Emit.TOOL_CALLS) @@ -359,11 +361,6 @@ async def test_send_room_file_reports_content_placeholder_not_raw_bytes( """The tool_call event for band_send_room_file must report a bounded placeholder for `content`, not the raw file text -- real file bytes (up to ~1MB) have no business in a platform-visible log event.""" - import json - - from anthropic.types import ToolUseBlock - - from band.core.types import ToolEventKey adapter = AnthropicAdapter(emit=Emit.TOOL_CALLS) @@ -393,7 +390,6 @@ async def test_read_room_file_non_image_result_stays_text(self, mock_tools): """A description-only (non-image) read_room_file result keeps the ordinary json.dumps'd text content -- the image branch only fires for the real MCP-content shape.""" - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter(emit=()) @@ -420,7 +416,6 @@ async def test_read_room_file_non_image_result_stays_text(self, mock_tools): @pytest.mark.asyncio async def test_send_event_403_does_not_crash_tool_execution(self, mock_tools): """send_event 403 should not prevent tool from executing.""" - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter(emit=Emit.TOOL_CALLS) @@ -449,9 +444,6 @@ async def test_send_event_403_does_not_crash_tool_execution(self, mock_tools): @pytest.mark.asyncio async def test_send_event_failure_logs_warning(self, mock_tools, caplog): """send_event failures should be logged as warnings.""" - import logging - - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter(emit=Emit.TOOL_CALLS) @@ -508,7 +500,6 @@ async def test_usage_from_response_maps_and_sums(self): @pytest.mark.asyncio async def test_emits_usage_event_when_enabled(self, mock_tools): """With Emit.USAGE on, a non-empty TurnUsage rides a task event's metadata.""" - from band.core.types import USAGE_EVENT_TYPE, USAGE_METADATA_KEY adapter = AnthropicAdapter(emit=Emit.USAGE) @@ -554,7 +545,6 @@ async def test_usage_emit_skipped_during_task_cancellation(self, mock_tools): """A cancelled turn must not fire usage I/O from its finally: teardown (shutdown, a turn timeout) would otherwise block on a REST call, and a CancelledError raised mid-send could skip later cleanup.""" - import asyncio adapter = AnthropicAdapter(emit=Emit.USAGE) started = asyncio.Event() @@ -587,7 +577,6 @@ async def test_emits_summed_usage_across_tool_loop( is the deterministic summing proof the live smoke can't give (it never sees the per-call intermediates). """ - from anthropic.types import TextBlock, ToolUseBlock adapter = AnthropicAdapter(emit=Emit.USAGE) @@ -644,7 +633,6 @@ async def test_emits_accumulated_usage_when_loop_fails_midway( """A tool loop that raises after a successful call still emits that call's usage: tokens spent before the failure were still spent. The exception still propagates (the turn is marked failed).""" - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter(emit=Emit.USAGE) @@ -688,7 +676,6 @@ async def test_emits_accumulated_usage_when_loop_fails_midway( @pytest.mark.asyncio async def test_handles_tool_error(self, mock_tools): """Should handle tool execution errors gracefully.""" - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter() @@ -843,7 +830,6 @@ async def capture_call(messages, tools): @pytest.mark.asyncio async def test_routes_to_custom_tool(self, mock_tools): """Tool call for custom tool should execute custom function.""" - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter( additional_tools=[(EchoInput, echo_message)], @@ -872,7 +858,6 @@ async def test_routes_to_custom_tool(self, mock_tools): @pytest.mark.asyncio async def test_routes_to_platform_tool(self, mock_tools): """Tool call for platform tool should use execute_tool_call.""" - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter( additional_tools=[(EchoInput, echo_message)], @@ -903,7 +888,6 @@ async def test_routes_to_platform_tool(self, mock_tools): @pytest.mark.asyncio async def test_custom_tool_error_sets_is_error(self, mock_tools): """Custom tool exception should result in is_error=True.""" - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter( additional_tools=[(EchoInput, failing_tool)], @@ -928,7 +912,6 @@ async def test_custom_tool_error_sets_is_error(self, mock_tools): @pytest.mark.asyncio async def test_preserves_tool_use_id_on_error(self, mock_tools): """tool_use_id should be preserved even when custom tool fails.""" - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter( additional_tools=[(EchoInput, failing_tool)], @@ -951,7 +934,6 @@ async def test_preserves_tool_use_id_on_error(self, mock_tools): @pytest.mark.asyncio async def test_multiple_custom_tools_execution(self, mock_tools): """Multiple custom tools should be callable.""" - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter( additional_tools=[ @@ -985,7 +967,6 @@ async def test_multiple_custom_tools_execution(self, mock_tools): @pytest.mark.asyncio async def test_custom_tool_validation_error(self, mock_tools): """Invalid args should result in validation error.""" - from anthropic.types import ToolUseBlock adapter = AnthropicAdapter( additional_tools=[(EchoInput, echo_message)], diff --git a/tests/adapters/test_claude_sdk_adapter.py b/tests/adapters/test_claude_sdk_adapter.py index b71083626..b03e5fb67 100644 --- a/tests/adapters/test_claude_sdk_adapter.py +++ b/tests/adapters/test_claude_sdk_adapter.py @@ -17,7 +17,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from pydantic import BaseModel +from pydantic import BaseModel, Field from band.adapters.claude_sdk import ( ClaudeSDKAdapter, @@ -32,13 +32,19 @@ BAND_MEMORY_TOOLS, ) from band.converters.claude_sdk import ClaudeSDKSessionState +from band.runtime.custom_tools import get_custom_tool_name from band.runtime.tools import ( ALL_TOOL_NAMES, FILE_TOOL_NAMES, MAX_INLINE_IMAGE_BYTES, missing_reply_error, + mcp_tool_names, ) from band.core.types import Capability, Emit, PlatformMessage, ToolEventKey +from claude_agent_sdk._errors import CLIConnectionError +from claude_agent_sdk.types import PermissionResultAllow, ToolPermissionContext +from claude_agent_sdk.types import PermissionResultDeny +from band.integrations.claude_sdk.dedup_tools import DedupingAgentTools pytestmark = pytest.mark.skipif( not _CLAUDE_SDK_AVAILABLE, @@ -357,9 +363,6 @@ async def test_initializes_history_on_bootstrap(self, sample_message, mock_tools # By default the adapter wraps tools with DedupingAgentTools so # MCP tool calls go through the dedup shim. The wrapped # instance is what gets stored and forwarded. - from band.integrations.claude_sdk.dedup_tools import ( - DedupingAgentTools, - ) stored_tools = adapter._room_tools["room-123"] assert isinstance(stored_tools, DedupingAgentTools) @@ -493,7 +496,6 @@ async def test_invalidates_session_on_cli_connection_error( self, sample_message, mock_tools ): """CLIConnectionError should invalidate the dead session and re-raise.""" - from claude_agent_sdk._errors import CLIConnectionError adapter = ClaudeSDKAdapter() mock_client = MagicMock() @@ -533,7 +535,6 @@ async def test_cli_connection_error_reports_error_event( self, sample_message, mock_tools ): """CLIConnectionError should report error event to the user.""" - from claude_agent_sdk._errors import CLIConnectionError adapter = ClaudeSDKAdapter() mock_client = MagicMock() @@ -572,7 +573,6 @@ async def test_clears_session_id_on_cli_connection_error( self, sample_message, mock_tools ): """CLIConnectionError should clear cached session ID so resume is not attempted.""" - from claude_agent_sdk._errors import CLIConnectionError adapter = ClaudeSDKAdapter() # Pre-populate a session ID @@ -760,7 +760,6 @@ def test_band_memory_tools_list(self): def test_band_all_tools_combines_base_and_memory(self): """BAND_ALL_TOOLS should combine base, memory, and file tools without duplicates.""" - from band.runtime.tools import mcp_tool_names assert set(BAND_ALL_TOOLS) == ( set(BAND_BASE_TOOLS) @@ -779,7 +778,6 @@ class TestCustomTools: def test_accepts_additional_tools_parameter(self): """Adapter accepts list of CustomToolDef tuples.""" - from pydantic import BaseModel, Field class EchoInput(BaseModel): """Echo the message.""" @@ -798,7 +796,6 @@ async def echo(args: EchoInput) -> str: def test_multiple_custom_tools(self): """Should accept multiple custom tools.""" - from pydantic import BaseModel class Tool1Input(BaseModel): """Tool 1.""" @@ -825,7 +822,6 @@ def tool2(args: Tool2Input) -> str: @pytest.mark.asyncio async def test_custom_tools_added_to_allowed_tools(self): """Custom tools should be added to allowed_tools list.""" - from pydantic import BaseModel class CalculatorInput(BaseModel): """Perform calculations.""" @@ -864,7 +860,6 @@ def calc(args: CalculatorInput) -> float: @pytest.mark.asyncio async def test_custom_tools_registered_in_mcp_server(self): """Custom tools should be registered in MCP server (memory tools disabled).""" - from pydantic import BaseModel class EchoInput(BaseModel): """Echo tool.""" @@ -907,7 +902,6 @@ async def echo(args: EchoInput) -> str: @pytest.mark.asyncio async def test_custom_tools_registered_with_memory_tools_enabled(self): """Custom tools should be registered in MCP server (memory tools enabled).""" - from pydantic import BaseModel class EchoInput(BaseModel): """Echo tool.""" @@ -948,8 +942,6 @@ async def echo(args: EchoInput) -> str: def test_tool_name_derived_from_input_model(self): """Tool name should be derived from Pydantic model class name.""" - from band.runtime.custom_tools import get_custom_tool_name - from pydantic import BaseModel class MyCustomToolInput(BaseModel): """A custom tool.""" @@ -2097,10 +2089,6 @@ class TestCanUseToolCallback: @pytest.mark.asyncio async def test_auto_accept_returns_allow(self, mock_tools): """auto_accept mode should return PermissionResultAllow.""" - from claude_agent_sdk.types import ( - PermissionResultAllow, - ToolPermissionContext, - ) adapter = ClaudeSDKAdapter(approval_mode="auto_accept") adapter._room_tools["room-1"] = mock_tools @@ -2114,7 +2102,6 @@ async def test_auto_accept_returns_allow(self, mock_tools): @pytest.mark.asyncio async def test_auto_accept_sends_notification(self, mock_tools): """auto_accept should send policy notification when enabled.""" - from claude_agent_sdk.types import ToolPermissionContext adapter = ClaudeSDKAdapter( approval_mode="auto_accept", approval_text_notifications=True @@ -2132,10 +2119,6 @@ async def test_auto_accept_sends_notification(self, mock_tools): @pytest.mark.asyncio async def test_auto_decline_returns_deny(self, mock_tools): """auto_decline mode should return PermissionResultDeny.""" - from claude_agent_sdk.types import ( - PermissionResultDeny, - ToolPermissionContext, - ) adapter = ClaudeSDKAdapter(approval_mode="auto_decline") adapter._room_tools["room-1"] = mock_tools @@ -2149,7 +2132,6 @@ async def test_auto_decline_returns_deny(self, mock_tools): @pytest.mark.asyncio async def test_auto_accept_no_notification_when_disabled(self, mock_tools): """Should not send notification when approval_text_notifications=False.""" - from claude_agent_sdk.types import ToolPermissionContext adapter = ClaudeSDKAdapter( approval_mode="auto_accept", approval_text_notifications=False @@ -2165,10 +2147,6 @@ async def test_auto_accept_no_notification_when_disabled(self, mock_tools): @pytest.mark.asyncio async def test_manual_mode_sends_approval_request(self, mock_tools): """Manual mode should send approval message and wait on future.""" - from claude_agent_sdk.types import ( - PermissionResultAllow, - ToolPermissionContext, - ) adapter = ClaudeSDKAdapter(approval_mode="manual", approval_wait_timeout_s=1.0) adapter._room_tools["room-1"] = mock_tools @@ -2195,10 +2173,6 @@ async def approve_soon(): @pytest.mark.asyncio async def test_manual_mode_timeout_declines(self, mock_tools): """Manual mode should decline on timeout when timeout_decision='decline'.""" - from claude_agent_sdk.types import ( - PermissionResultDeny, - ToolPermissionContext, - ) adapter = ClaudeSDKAdapter( approval_mode="manual", @@ -2216,10 +2190,6 @@ async def test_manual_mode_timeout_declines(self, mock_tools): @pytest.mark.asyncio async def test_manual_mode_timeout_accepts(self, mock_tools): """Manual mode should accept on timeout when timeout_decision='accept'.""" - from claude_agent_sdk.types import ( - PermissionResultAllow, - ToolPermissionContext, - ) adapter = ClaudeSDKAdapter( approval_mode="manual", @@ -2237,10 +2207,6 @@ async def test_manual_mode_timeout_accepts(self, mock_tools): @pytest.mark.asyncio async def test_manual_mode_notification_failure_declines(self, mock_tools): """If the approval notification can't be delivered, decline immediately.""" - from claude_agent_sdk.types import ( - PermissionResultDeny, - ToolPermissionContext, - ) adapter = ClaudeSDKAdapter( approval_mode="manual", @@ -2567,7 +2533,6 @@ class TestPendingApprovalEviction: @pytest.mark.asyncio async def test_evicts_oldest_when_capacity_reached(self, mock_tools): """Should evict oldest pending when max capacity is reached.""" - from claude_agent_sdk.types import ToolPermissionContext adapter = ClaudeSDKAdapter( approval_mode="manual", @@ -2634,7 +2599,6 @@ class TestSendMessageDedupWiring: @pytest.mark.asyncio async def test_wraps_tools_by_default(self, sample_message, mock_tools): """By default, on_message stores a DedupingAgentTools wrapper.""" - from band.integrations.claude_sdk.dedup_tools import DedupingAgentTools adapter = ClaudeSDKAdapter() mock_client = MagicMock() @@ -2669,7 +2633,6 @@ async def test_wraps_tools_by_default(self, sample_message, mock_tools): @pytest.mark.asyncio async def test_ttl_zero_disables_wrapping(self, sample_message, mock_tools): """ttl=0 keeps the raw tools — no shim — for operators who opt out.""" - from band.integrations.claude_sdk.dedup_tools import DedupingAgentTools adapter = ClaudeSDKAdapter(send_message_dedup_ttl_seconds=0) mock_client = MagicMock() @@ -2759,7 +2722,6 @@ async def test_wrapper_persists_across_on_message_calls(self, sample_message): and one after the second on_message — and assert the duplicate is suppressed across the turn boundary. """ - from band.integrations.claude_sdk.dedup_tools import DedupingAgentTools adapter = ClaudeSDKAdapter() mock_client = MagicMock() @@ -2879,7 +2841,6 @@ async def test_distinct_rooms_get_distinct_wrappers(self, sample_message): a per-session or singleton tools cache) cannot silently turn the dedup wrapper into a tenant-wide message suppressor. """ - from band.integrations.claude_sdk.dedup_tools import DedupingAgentTools adapter = ClaudeSDKAdapter() mock_client = MagicMock() @@ -2941,7 +2902,6 @@ async def test_update_inner_skipped_when_tools_identity_unchanged( """When the runtime hands the adapter the same tools object twice, ``update_inner`` is a no-op and must be skipped — otherwise we'd briefly contend on the wrapper's lock for no reason.""" - from band.integrations.claude_sdk.dedup_tools import DedupingAgentTools adapter = ClaudeSDKAdapter() mock_client = MagicMock() diff --git a/tests/adapters/test_claude_sdk_tool_names.py b/tests/adapters/test_claude_sdk_tool_names.py index 12982bc3b..65094349d 100644 --- a/tests/adapters/test_claude_sdk_tool_names.py +++ b/tests/adapters/test_claude_sdk_tool_names.py @@ -13,6 +13,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from claude_agent_sdk import AssistantMessage, ResultMessage, ToolUseBlock from band.adapters.claude_sdk import ClaudeSDKAdapter from band.converters.claude_sdk import ClaudeSDKSessionState @@ -31,8 +32,6 @@ def test_semantic_tool_name_strips_only_our_server_prefix() -> None: @pytest.mark.asyncio async def test_tool_call_event_uses_bare_name() -> None: - from claude_agent_sdk import AssistantMessage, ResultMessage, ToolUseBlock - adapter = ClaudeSDKAdapter(emit=Emit.TOOL_CALLS) message = PlatformMessage( diff --git a/tests/adapters/test_codex_adapter.py b/tests/adapters/test_codex_adapter.py index 32a93ff6b..b01b97dd4 100644 --- a/tests/adapters/test_codex_adapter.py +++ b/tests/adapters/test_codex_adapter.py @@ -15,10 +15,24 @@ from pydantic import BaseModel -from band.adapters.codex import CodexAdapter, CodexAdapterConfig +from band.adapters.codex import ( + _MAX_DIFF_METADATA_BYTES, + _THOUGHT_ITEM_TYPES, + _TOOL_ITEM_TYPES, + CodexAdapter, + CodexAdapterConfig, + PendingApproval, +) from band.core.types import AgentInput, Emit, HistoryProvider, PlatformMessage from band.integrations.codex import CodexJsonRpcError, RpcEvent -from band.integrations.codex.types import CodexSessionState +from band.integrations.codex.types import ( + _MAX_ERROR_DETAIL_CHARS, + CodexItemType, + CodexSessionState, + CodexTokenUsage, + build_structured_error_metadata, + parse_plan_steps, +) from band.runtime.custom_tools import CustomToolDef from band.runtime.tools import ToolCallOutcome from band.testing import FakeAgentTools @@ -448,7 +462,6 @@ async def test_fallback_text_not_suppressed_when_send_message_tool_fails( The failure is a non-raising ok=False (bad args / API error) — the case the plain execute_tool_call would misread as success and wrongly suppress. """ - from band.runtime.tools import ToolCallOutcome class SendMessageFailureTools(ToolSchemaFakeTools): async def execute_tool_call_structured( @@ -1478,7 +1491,6 @@ async def test_transport_closed_drains_token_usage_for_dead_threads( transport/closed; otherwise they leak past on_cleanup because the thread id is no longer reachable through ``_room_threads``. """ - from band.integrations.codex.types import CodexTokenUsage events = [ _event_notification( @@ -3496,7 +3508,6 @@ async def test_tool_call_validation_error_returns_friendly_message(self) -> None returns ok=False with a friendly message (it does not raise), so the adapter surfaces it via the ok=False path. """ - from band.runtime.tools import ToolCallOutcome class ValidationErrorTools(ToolSchemaFakeTools): async def execute_tool_call_structured( @@ -4639,7 +4650,6 @@ async def test_usage_command_shows_token_usage(self) -> None: class TestCodexTypes: def test_build_structured_error_metadata_known_type(self) -> None: - from band.integrations.codex.types import build_structured_error_metadata error_obj = { "message": "Context overflow", @@ -4659,7 +4669,6 @@ def test_build_structured_error_metadata_known_type(self) -> None: assert meta["codex_turn_id"] == "turn-1" def test_build_structured_error_metadata_unknown_type(self) -> None: - from band.integrations.codex.types import build_structured_error_metadata error_obj = { "message": "Something weird happened", @@ -4671,7 +4680,6 @@ def test_build_structured_error_metadata_unknown_type(self) -> None: assert meta["codex_suggested_action"] is None def test_parse_plan_steps(self) -> None: - from band.integrations.codex.types import parse_plan_steps params = { "plan": { @@ -4689,7 +4697,6 @@ def test_parse_plan_steps(self) -> None: assert steps[2].status == "pending" def test_parse_plan_steps_string_entries(self) -> None: - from band.integrations.codex.types import parse_plan_steps params = {"plan": {"steps": ["Read code", "Fix bug"]}} steps = parse_plan_steps(params) @@ -4698,7 +4705,6 @@ def test_parse_plan_steps_string_entries(self) -> None: assert steps[0].status == "pending" def test_codex_token_usage_update(self) -> None: - from band.integrations.codex.types import CodexTokenUsage usage = CodexTokenUsage() usage.update( @@ -4722,7 +4728,6 @@ def test_codex_token_usage_update(self) -> None: def test_codex_token_usage_update_current_schema(self) -> None: """The current app-server schema nests cumulative counters under ``tokenUsage.total`` and names reasoning ``reasoningOutputTokens``.""" - from band.integrations.codex.types import CodexTokenUsage usage = CodexTokenUsage() usage.update( @@ -4809,8 +4814,6 @@ def test_codex_item_type_fully_classified(self) -> None: event, no test failure. This test is the guard: it fails loudly the moment the partition stops being exhaustive. """ - from band.adapters.codex import _TOOL_ITEM_TYPES, _THOUGHT_ITEM_TYPES - from band.integrations.codex.types import CodexItemType message_types = {CodexItemType.USER_MESSAGE, CodexItemType.AGENT_MESSAGE} classified = _TOOL_ITEM_TYPES | _THOUGHT_ITEM_TYPES | message_types @@ -5130,7 +5133,6 @@ async def test_thread_archive_clears_raw_history(self) -> None: def test_token_usage_update_handles_zero_values(self) -> None: """CodexTokenUsage.update() correctly handles explicit zero values.""" - from band.integrations.codex.types import CodexTokenUsage usage = CodexTokenUsage() usage.update( @@ -5466,7 +5468,6 @@ async def test_context_compaction_ignored_when_disabled(self) -> None: class TestPerTurnTokenUsage: def test_token_usage_computes_per_turn_deltas(self) -> None: """Per-turn deltas are computed from consecutive cumulative updates.""" - from band.integrations.codex.types import CodexTokenUsage usage = CodexTokenUsage() @@ -5504,7 +5505,6 @@ def test_token_usage_computes_per_turn_deltas(self) -> None: def test_token_usage_metadata_includes_turn_deltas(self) -> None: """to_metadata() includes per-turn deltas when available.""" - from band.integrations.codex.types import CodexTokenUsage usage = CodexTokenUsage() usage.update( @@ -5522,7 +5522,6 @@ def test_token_usage_metadata_includes_turn_deltas(self) -> None: def test_token_usage_format_summary_includes_turn(self) -> None: """format_summary() shows per-turn breakdown when deltas > 0.""" - from band.integrations.codex.types import CodexTokenUsage usage = CodexTokenUsage() usage.update( @@ -5540,7 +5539,6 @@ def test_token_usage_format_summary_includes_turn(self) -> None: def test_reset_turn_deltas(self) -> None: """reset_turn_deltas() zeroes out per-turn counters.""" - from band.integrations.codex.types import CodexTokenUsage usage = CodexTokenUsage() usage.update( @@ -5563,7 +5561,6 @@ def test_multi_event_turn_delta_is_cumulative_from_anchor(self) -> None: turn reporting ``turn_input_tokens=30``. With the anchor, the final value is ``180 - 100 = 80`` — the whole-turn rise. """ - from band.integrations.codex.types import CodexTokenUsage usage = CodexTokenUsage() # End of previous turn: cumulative = 100. @@ -5594,7 +5591,6 @@ class TestPlanStepsRobustness: def test_parse_plan_steps_handles_non_dict_plan(self) -> None: """parse_plan_steps must not crash when `plan` is not a dict.""" - from band.integrations.codex.types import parse_plan_steps assert parse_plan_steps({"plan": "not-a-dict"}) == [] assert parse_plan_steps({"plan": ["also", "not", "a", "dict"]}) == [] @@ -5602,7 +5598,6 @@ def test_parse_plan_steps_handles_non_dict_plan(self) -> None: def test_parse_plan_steps_reads_top_level_when_plan_absent(self) -> None: """When there's no 'plan' key, parse_plan_steps looks at top-level steps.""" - from band.integrations.codex.types import parse_plan_steps steps = parse_plan_steps({"steps": [{"text": "A", "status": "pending"}]}) assert len(steps) == 1 @@ -5700,7 +5695,6 @@ async def test_token_usage_event_skipped_when_total_is_zero(self) -> None: instances are truthy) so an empty token_usage event could be emitted even before Codex sent any thread/tokenUsage/updated notification. """ - from band.integrations.codex.types import CodexTokenUsage fake_client = FakeCodexClient() adapter = CodexAdapter( @@ -5735,7 +5729,6 @@ def test_structured_error_with_string_error_obj(self) -> None: was dead code; this test asserts the normalization still works when the original error_obj is a string rather than a dict. """ - from band.integrations.codex.types import build_structured_error_metadata # Simulate the normalization the adapter performs: convert string to # {"message": } before passing to build_structured_error_metadata. @@ -5849,7 +5842,6 @@ def test_token_usage_warns_on_non_monotonic_counters( late event from the previous turn with a smaller cumulative must leave the turn deltas clamped to 0 rather than going negative. """ - from band.integrations.codex.types import CodexTokenUsage usage = CodexTokenUsage() usage.update({"usage": {"inputTokens": 100, "outputTokens": 100}}) @@ -5912,7 +5904,6 @@ class TestStructuredErrorMappings: def test_known_error_type_maps_to_remediation( self, error_type: str, expected_action: str, expected_phrase: str ) -> None: - from band.integrations.codex.types import build_structured_error_metadata content, meta = build_structured_error_metadata( {"codexErrorInfo": {"type": error_type, "retryable": True}} @@ -5923,7 +5914,6 @@ def test_known_error_type_maps_to_remediation( assert expected_phrase in content.lower() def test_non_dict_codex_error_info_is_tolerated(self) -> None: - from band.integrations.codex.types import build_structured_error_metadata content, meta = build_structured_error_metadata( {"message": "boom", "codexErrorInfo": "not-a-dict"} @@ -5932,7 +5922,6 @@ def test_non_dict_codex_error_info_is_tolerated(self) -> None: assert content == "boom" def test_missing_codex_error_info_falls_back_to_message(self) -> None: - from band.integrations.codex.types import build_structured_error_metadata content, meta = build_structured_error_metadata({"message": "network down"}) assert meta["codex_error_type"] is None @@ -5940,7 +5929,6 @@ def test_missing_codex_error_info_falls_back_to_message(self) -> None: assert content == "network down" def test_additional_details_preserved_in_metadata(self) -> None: - from band.integrations.codex.types import build_structured_error_metadata _, meta = build_structured_error_metadata( { @@ -6171,7 +6159,6 @@ async def test_pending_approvals_cleared_on_room_cleanup(self) -> None: # Simulate an active room with a pending approval. loop = asyncio.get_running_loop() approval_future: asyncio.Future[str] = loop.create_future() - from band.adapters.codex import PendingApproval adapter._room_threads["room-1"] = "thr-1" adapter._pending_approvals["room-1"] = { @@ -6241,7 +6228,6 @@ class TestTokenUsageCumulativeMonotonicity: """ def test_late_smaller_event_does_not_corrupt_next_delta(self) -> None: - from band.integrations.codex.types import CodexTokenUsage usage = CodexTokenUsage() # End of previous turn: cumulative = 100. @@ -6268,10 +6254,6 @@ class TestStructuredErrorDetailCap: """``additionalDetails`` is attacker-influenceable and must be capped.""" def test_long_additional_details_string_is_truncated(self) -> None: - from band.integrations.codex.types import ( - _MAX_ERROR_DETAIL_CHARS, - build_structured_error_metadata, - ) long_detail = "x" * (_MAX_ERROR_DETAIL_CHARS + 500) _, meta = build_structured_error_metadata( @@ -6287,7 +6269,6 @@ def test_long_additional_details_string_is_truncated(self) -> None: def test_structured_dict_additional_details_are_preserved(self) -> None: """Only string details are capped; dict/list payloads pass through.""" - from band.integrations.codex.types import build_structured_error_metadata payload = {"hint": "refresh token", "code": 401} _, meta = build_structured_error_metadata( @@ -6300,7 +6281,6 @@ def test_structured_dict_additional_details_are_preserved(self) -> None: def test_empty_additional_details_is_dropped(self) -> None: """Empty strings are not echoed into metadata.""" - from band.integrations.codex.types import build_structured_error_metadata _, meta = build_structured_error_metadata( { @@ -6320,10 +6300,6 @@ def test_oversized_dict_additional_details_is_replaced_with_marker( WebSocket frame. When the serialized form exceeds the cap we replace the whole payload with a truncated marker string. """ - from band.integrations.codex.types import ( - _MAX_ERROR_DETAIL_CHARS, - build_structured_error_metadata, - ) # Build a dict whose JSON serialization comfortably exceeds the cap. oversized_value = "x" * (_MAX_ERROR_DETAIL_CHARS + 500) @@ -6345,7 +6321,6 @@ def test_unserializable_additional_details_is_dropped(self) -> None: round-trip through ``default=str``; pathological unserializable objects (e.g. a circular reference) must be dropped rather than raising into the event-emission path.""" - from band.integrations.codex.types import build_structured_error_metadata circular: dict[str, Any] = {} circular["self"] = circular @@ -6366,7 +6341,6 @@ class TestDiffByteCap: async def test_multibyte_diff_respects_byte_budget(self) -> None: """A diff built from 4-byte codepoints is capped to the byte budget, not the character budget (which would be ~4× larger on the wire).""" - from band.adapters.codex import _MAX_DIFF_METADATA_BYTES # Each emoji is 4 UTF-8 bytes; use ~1.5× the byte budget worth. emoji = "\U0001f600" diff --git a/tests/adapters/test_crewai_adapter.py b/tests/adapters/test_crewai_adapter.py index f921ece4d..8ec46f513 100644 --- a/tests/adapters/test_crewai_adapter.py +++ b/tests/adapters/test_crewai_adapter.py @@ -9,8 +9,13 @@ from __future__ import annotations -import importlib import asyncio +import concurrent.futures +import contextlib +import importlib +import sys +import threading +import warnings import json from datetime import datetime, timezone from typing import TYPE_CHECKING, Any @@ -20,6 +25,7 @@ from pydantic import BaseModel, Field from band.core.types import Capability, Emit, PlatformMessage +from band.runtime.prompts import render_system_prompt if TYPE_CHECKING: from band.adapters.crewai import CrewAIAdapter as CrewAIAdapterType @@ -35,7 +41,6 @@ def __init__(self): @pytest.fixture def crewai_mocks(monkeypatch): - import sys mock_crewai_module = MagicMock() mock_crewai_tools_module = MagicMock() @@ -62,7 +67,6 @@ def crewai_mocks(monkeypatch): @pytest.fixture def CrewAIAdapter(crewai_mocks) -> type["CrewAIAdapterType"]: - import importlib module = importlib.import_module("band.adapters.crewai") return module.CrewAIAdapter @@ -174,8 +178,6 @@ def room_context(crewai_mocks, mock_tools): with room_context("room-123"): # tool execution code here """ - import contextlib - import importlib module = importlib.import_module("band.adapters.crewai") @@ -195,7 +197,6 @@ class TestCrewAISpecificInitialization: def test_system_prompt_deprecation_warning(self, CrewAIAdapter): """system_prompt parameter should emit DeprecationWarning.""" - import warnings with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") @@ -210,7 +211,6 @@ def test_system_prompt_deprecation_warning(self, CrewAIAdapter): def test_system_prompt_does_not_override_backstory(self, CrewAIAdapter): """If both system_prompt and backstory are provided, backstory takes precedence.""" - import warnings with warnings.catch_warnings(record=True): warnings.simplefilter("always") @@ -521,7 +521,6 @@ async def test_does_not_report_completion_error_after_reply( self, CrewAIAdapter, sample_message, mock_tools, mock_crewai_agent ): """A turn is not silent when band_send_message already replied.""" - import importlib module = importlib.import_module("band.adapters.crewai") @@ -564,7 +563,6 @@ async def test_suppresses_empty_final_answer_after_reply( adapter replies through the tool, that fired on essentially every turn, posting a spurious error event alongside each (successful) reply. """ - import importlib module = importlib.import_module("band.adapters.crewai") @@ -608,7 +606,6 @@ async def test_suppresses_empty_final_answer_after_tool_only_turn( a tool already executed successfully this turn, that empty answer is benign: no error event, no re-raise. """ - import importlib module = importlib.import_module("band.adapters.crewai") @@ -659,7 +656,6 @@ async def test_genuine_error_after_reply_still_reports_and_raises( even one raised after band_send_message already replied — must still post an error event and propagate, so real bugs stay visible. """ - import importlib module = importlib.import_module("band.adapters.crewai") @@ -979,7 +975,6 @@ def _make_adapter(self, CrewAIAdapter: type) -> Any: def test_list_contacts_tool_executes( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): - import asyncio adapter = self._make_adapter(CrewAIAdapter) asyncio.run(adapter.on_started("TestBot", "Test bot")) @@ -998,7 +993,6 @@ def test_list_contacts_tool_executes( def test_add_contact_tool_executes( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): - import asyncio adapter = self._make_adapter(CrewAIAdapter) asyncio.run(adapter.on_started("TestBot", "Test bot")) @@ -1018,7 +1012,6 @@ def test_add_contact_tool_executes( def test_remove_contact_tool_executes( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): - import asyncio adapter = self._make_adapter(CrewAIAdapter) asyncio.run(adapter.on_started("TestBot", "Test bot")) @@ -1037,7 +1030,6 @@ def test_remove_contact_tool_executes( def test_list_contact_requests_tool_executes( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): - import asyncio adapter = self._make_adapter(CrewAIAdapter) asyncio.run(adapter.on_started("TestBot", "Test bot")) @@ -1060,7 +1052,6 @@ def test_list_contact_requests_tool_executes( def test_respond_contact_request_tool_executes( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): - import asyncio adapter = self._make_adapter(CrewAIAdapter) asyncio.run(adapter.on_started("TestBot", "Test bot")) @@ -1092,7 +1083,6 @@ class TestMemoryToolExecution: def test_list_memories_tool_executes( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): - import asyncio adapter = CrewAIAdapter(capabilities=Capability.MEMORY) asyncio.run(adapter.on_started("TestBot", "Test bot")) @@ -1129,7 +1119,6 @@ def test_list_memories_tool_executes( def test_store_memory_tool_executes( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): - import asyncio adapter = CrewAIAdapter(capabilities=Capability.MEMORY) asyncio.run(adapter.on_started("TestBot", "Test bot")) @@ -1165,7 +1154,6 @@ def test_store_memory_tool_executes( def test_get_memory_tool_executes( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): - import asyncio adapter = CrewAIAdapter(capabilities=Capability.MEMORY) asyncio.run(adapter.on_started("TestBot", "Test bot")) @@ -1184,7 +1172,6 @@ def test_get_memory_tool_executes( def test_supersede_memory_tool_executes( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): - import asyncio adapter = CrewAIAdapter(capabilities=Capability.MEMORY) asyncio.run(adapter.on_started("TestBot", "Test bot")) @@ -1206,7 +1193,6 @@ def test_supersede_memory_tool_executes( def test_archive_memory_tool_executes( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): - import asyncio adapter = CrewAIAdapter(capabilities=Capability.MEMORY) asyncio.run(adapter.on_started("TestBot", "Test bot")) @@ -1227,7 +1213,6 @@ def test_archive_memory_tool_executes( class TestToolExecution: def test_tool_returns_error_without_room_context(self, CrewAIAdapter, crewai_mocks): """Tools return error when called outside message handling (no context set).""" - import asyncio crewai_mocks.Agent.reset_mock() @@ -1328,7 +1313,6 @@ def test_successful_tool_execution_with_room_context( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): """Tools work when context variable is set (simulates call during message handling).""" - import asyncio crewai_mocks.Agent.reset_mock() @@ -1423,7 +1407,6 @@ async def test_emit_kwarg_controls_tool_call_reporting( def test_reports_tool_call_when_enabled( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): - import asyncio crewai_mocks.Agent.reset_mock() @@ -1444,7 +1427,7 @@ async def test_report_tool_call_403_does_not_crash( self, CrewAIAdapter, crewai_mocks, mock_tools ): """send_event 403 in EmitToolCallsReporter.report_call should not propagate.""" - from band.integrations.crewai import EmitToolCallsReporter + from band.integrations.crewai import EmitToolCallsReporter # noqa: PLC0415 -- crewai extra, absent from the standard dev venv adapter = CrewAIAdapter(emit=Emit.TOOL_CALLS) reporter = EmitToolCallsReporter(adapter.features) @@ -1458,7 +1441,7 @@ async def test_report_tool_result_403_does_not_crash( self, CrewAIAdapter, crewai_mocks, mock_tools ): """send_event 403 in EmitToolCallsReporter.report_result should not propagate.""" - from band.integrations.crewai import EmitToolCallsReporter + from band.integrations.crewai import EmitToolCallsReporter # noqa: PLC0415 -- crewai extra, absent from the standard dev venv adapter = CrewAIAdapter(emit=Emit.TOOL_CALLS) reporter = EmitToolCallsReporter(adapter.features) @@ -1479,8 +1462,6 @@ def test_nest_asyncio_not_applied_on_import(self, crewai_mocks): pydantic (which looks annotations up through that name) can no longer build the tool models. """ - import importlib - import sys nest_mock = sys.modules["nest_asyncio"] nest_mock.reset_mock() @@ -1492,8 +1473,6 @@ def test_nest_asyncio_not_applied_on_import(self, crewai_mocks): def test_ensure_nest_asyncio_applies_once( self, CrewAIAdapter, crewai_mocks, monkeypatch ): - import importlib - import sys module = importlib.import_module("band.integrations.crewai.runtime") @@ -1510,8 +1489,6 @@ def test_ensure_nest_asyncio_applies_once( def test_nest_asyncio_lock_exists(self, CrewAIAdapter, crewai_mocks): """The integrations.crewai.runtime module owns the threading lock.""" - import importlib - import threading module = importlib.import_module("band.integrations.crewai.runtime") @@ -1520,9 +1497,6 @@ def test_nest_asyncio_lock_exists(self, CrewAIAdapter, crewai_mocks): def test_ensure_nest_asyncio_is_thread_safe(self, CrewAIAdapter, crewai_mocks): """Multiple threads calling _ensure_nest_asyncio should only apply patch once.""" - import concurrent.futures - import importlib - import sys module = importlib.import_module("band.integrations.crewai.runtime") @@ -1541,8 +1515,6 @@ def test_ensure_nest_asyncio_is_thread_safe(self, CrewAIAdapter, crewai_mocks): class TestRunAsync: def test_run_async_with_running_loop(self, crewai_mocks): - import importlib - import sys module = importlib.import_module("band.integrations.crewai.runtime") module._nest_asyncio_applied = False @@ -1559,8 +1531,6 @@ async def test_coro() -> str: nest_mock.apply.assert_called_once() def test_run_async_without_running_loop(self, crewai_mocks): - import importlib - import sys module = importlib.import_module("band.integrations.crewai.runtime") module._nest_asyncio_applied = True @@ -1610,7 +1580,6 @@ async def test_mentions_normalize_to_list(self, send_message_schema, raw, expect class TestPromptRendering: def test_backstory_uses_render_system_prompt(self, CrewAIAdapter): """CrewAI backstory is now built via render_system_prompt.""" - from band.runtime.prompts import render_system_prompt prompt = render_system_prompt( agent_name="TestAgent", @@ -1733,7 +1702,6 @@ def test_custom_tool_execution_async( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): """Async custom tool should execute correctly.""" - import asyncio crewai_mocks.Agent.reset_mock() @@ -1757,7 +1725,6 @@ def test_custom_tool_execution_sync( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): """Sync custom tool should execute correctly.""" - import asyncio crewai_mocks.Agent.reset_mock() @@ -1781,7 +1748,6 @@ def test_custom_tool_error_handling( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): """Custom tool exception should result in error response.""" - import asyncio crewai_mocks.Agent.reset_mock() @@ -1805,7 +1771,6 @@ def test_custom_tool_reports_execution_when_enabled( self, CrewAIAdapter, crewai_mocks, mock_tools, room_context ): """Custom tool should report tool_call and tool_result events when enabled.""" - import asyncio crewai_mocks.Agent.reset_mock() @@ -1827,7 +1792,6 @@ def test_custom_tool_reports_execution_when_enabled( def test_custom_tool_without_room_context(self, CrewAIAdapter, crewai_mocks): """Custom tool should return error when called without room context.""" - import asyncio crewai_mocks.Agent.reset_mock() diff --git a/tests/adapters/test_crewai_adapter_soak.py b/tests/adapters/test_crewai_adapter_soak.py index 961daa167..41343e06a 100644 --- a/tests/adapters/test_crewai_adapter_soak.py +++ b/tests/adapters/test_crewai_adapter_soak.py @@ -69,8 +69,6 @@ def _make_msg(idx: int, room_id: str) -> PlatformMessage: @pytest.mark.asyncio async def test_soak_100_turns_3_rooms(crewai_mocks): """Drive 100 on_message calls across 3 rooms; assert no leaks.""" - import importlib - module = importlib.import_module("band.adapters.crewai") CrewAIAdapter = module.CrewAIAdapter diff --git a/tests/adapters/test_crewai_flow_adapter.py b/tests/adapters/test_crewai_flow_adapter.py index 1659c8176..56fd3f9b5 100644 --- a/tests/adapters/test_crewai_flow_adapter.py +++ b/tests/adapters/test_crewai_flow_adapter.py @@ -488,8 +488,10 @@ async def kickoff_async(self, inputs: dict | None = None) -> dict: class TestPublicImportPath: def test_lazy_import_from_band_adapters(self) -> None: - # The example imports `from band.adapters import CrewAIFlowAdapter`. - from band.adapters import CrewAIFlowAdapter as Imported + # The example imports `from band.adapters import CrewAIFlowAdapter`; this + # import must stay local, since it's the band.adapters lazy loader itself + # under test, not just a way to reach the class. + from band.adapters import CrewAIFlowAdapter as Imported # noqa: PLC0415 assert Imported is CrewAIFlowAdapter diff --git a/tests/adapters/test_crewai_flow_phase3.py b/tests/adapters/test_crewai_flow_phase3.py index 07c00dfb2..e8d9006cc 100644 --- a/tests/adapters/test_crewai_flow_phase3.py +++ b/tests/adapters/test_crewai_flow_phase3.py @@ -319,18 +319,14 @@ def factory(): class TestNestAsyncioNotInvoked: + @requires_nest_asyncio @pytest.mark.asyncio async def test_direct_response_does_not_apply_nest_asyncio( self, monkeypatch: pytest.MonkeyPatch ) -> None: # Patch nest_asyncio.apply at the module level. - try: - import nest_asyncio # type: ignore - - apply_mock = MagicMock() - monkeypatch.setattr(nest_asyncio, "apply", apply_mock) - except ImportError: - pytest.skip("nest_asyncio not installed") + apply_mock = MagicMock() + monkeypatch.setattr(nest_asyncio, "apply", apply_mock) flow = _make_flow_returning( {"decision": "direct_response", "content": "hi", "mentions": []} diff --git a/tests/adapters/test_crewai_flow_phase5.py b/tests/adapters/test_crewai_flow_phase5.py index 474386a6d..c01d6b44a 100644 --- a/tests/adapters/test_crewai_flow_phase5.py +++ b/tests/adapters/test_crewai_flow_phase5.py @@ -557,7 +557,7 @@ def append_task_events_to_context(tools: FakeAgentTools, start: int) -> int: class TestIdentityNormalization: def test_uuid_handle_displayname_resolve_to_same_key(self) -> None: - from band.converters.crewai_flow import normalize_participant_key + from band.converters.crewai_flow import normalize_participant_key # noqa: PLC0415 -- crewai extra, absent from the standard dev venv participants = [ { diff --git a/tests/adapters/test_deprecation_shims.py b/tests/adapters/test_deprecation_shims.py index 5d74c2250..e42607836 100644 --- a/tests/adapters/test_deprecation_shims.py +++ b/tests/adapters/test_deprecation_shims.py @@ -13,8 +13,13 @@ from __future__ import annotations +from unittest.mock import patch + import pytest +from band.adapters.anthropic import AnthropicAdapter +from band.adapters.gemini import GeminiAdapter +from band.adapters.letta import LettaAdapterConfig from band.core.exceptions import BandConfigError @@ -22,15 +27,11 @@ class TestSelectiveRenameShims: """Anthropic and Gemini get the api_key/prompt selective renames.""" def test_anthropic_anthropic_api_key_warns(self) -> None: - from band.adapters.anthropic import AnthropicAdapter with pytest.warns(DeprecationWarning, match="anthropic_api_key"): AnthropicAdapter(anthropic_api_key="sk-test-key") def test_anthropic_anthropic_api_key_resolves_to_provider_key(self) -> None: - from unittest.mock import patch - - from band.adapters.anthropic import AnthropicAdapter with patch("band.adapters.anthropic.AsyncAnthropic") as mock_cls: with pytest.warns(DeprecationWarning, match="anthropic_api_key"): @@ -38,7 +39,6 @@ def test_anthropic_anthropic_api_key_resolves_to_provider_key(self) -> None: mock_cls.assert_called_once_with(api_key="sk-old-key") def test_anthropic_api_key_warns(self) -> None: - from band.adapters.anthropic import AnthropicAdapter with pytest.warns( DeprecationWarning, match="api_key.*deprecated.*provider_key" @@ -46,9 +46,6 @@ def test_anthropic_api_key_warns(self) -> None: AnthropicAdapter(api_key="sk-test-key") def test_anthropic_api_key_resolves_to_provider_key(self) -> None: - from unittest.mock import patch - - from band.adapters.anthropic import AnthropicAdapter with patch("band.adapters.anthropic.AsyncAnthropic") as mock_cls: with pytest.warns( @@ -58,44 +55,37 @@ def test_anthropic_api_key_resolves_to_provider_key(self) -> None: mock_cls.assert_called_once_with(api_key="sk-test-key") def test_anthropic_custom_section_warns(self) -> None: - from band.adapters.anthropic import AnthropicAdapter with pytest.warns(DeprecationWarning, match="custom_section"): AnthropicAdapter(custom_section="Be helpful.") def test_anthropic_provider_key_and_api_key_conflict(self) -> None: - from band.adapters.anthropic import AnthropicAdapter with pytest.raises(BandConfigError, match="Cannot pass both"): AnthropicAdapter(provider_key="sk-new", api_key="sk-old") def test_anthropic_anthropic_api_key_and_provider_key_conflict(self) -> None: - from band.adapters.anthropic import AnthropicAdapter with pytest.raises(BandConfigError, match="Cannot pass"): AnthropicAdapter(provider_key="sk-new", anthropic_api_key="sk-old") def test_anthropic_prompt_and_custom_section_conflict(self) -> None: - from band.adapters.anthropic import AnthropicAdapter with pytest.raises(BandConfigError, match="Cannot pass both"): AnthropicAdapter(prompt="new", custom_section="old") def test_gemini_gemini_api_key_warns(self) -> None: - from band.adapters.gemini import GeminiAdapter with pytest.warns(DeprecationWarning, match="gemini_api_key"): GeminiAdapter(gemini_api_key="AIza-test-key") def test_gemini_gemini_api_key_resolves_to_provider_key(self) -> None: - from band.adapters.gemini import GeminiAdapter with pytest.warns(DeprecationWarning, match="gemini_api_key"): adapter = GeminiAdapter(gemini_api_key="AIza-old-key") assert adapter._provider_key == "AIza-old-key" def test_gemini_api_key_warns(self) -> None: - from band.adapters.gemini import GeminiAdapter with pytest.warns( DeprecationWarning, match="api_key.*deprecated.*provider_key" @@ -103,7 +93,6 @@ def test_gemini_api_key_warns(self) -> None: GeminiAdapter(api_key="AIza-test-key") def test_gemini_api_key_resolves_to_provider_key(self) -> None: - from band.adapters.gemini import GeminiAdapter with pytest.warns( DeprecationWarning, match="api_key.*deprecated.*provider_key" @@ -112,25 +101,21 @@ def test_gemini_api_key_resolves_to_provider_key(self) -> None: assert adapter._provider_key == "AIza-test-key" def test_gemini_custom_section_warns(self) -> None: - from band.adapters.gemini import GeminiAdapter with pytest.warns(DeprecationWarning, match="custom_section"): GeminiAdapter(custom_section="Be concise.") def test_gemini_provider_key_and_api_key_conflict(self) -> None: - from band.adapters.gemini import GeminiAdapter with pytest.raises(BandConfigError, match="Cannot pass both"): GeminiAdapter(provider_key="AIza-new", api_key="AIza-old") def test_gemini_gemini_api_key_and_provider_key_conflict(self) -> None: - from band.adapters.gemini import GeminiAdapter with pytest.raises(BandConfigError, match="Cannot pass"): GeminiAdapter(provider_key="AIza-new", gemini_api_key="AIza-old") def test_gemini_prompt_and_custom_section_conflict(self) -> None: - from band.adapters.gemini import GeminiAdapter with pytest.raises(BandConfigError, match="Cannot pass both"): GeminiAdapter(prompt="new", custom_section="old") @@ -140,7 +125,6 @@ class TestLettaApiKeyShim: """LettaAdapterConfig.api_key must warn and resolve to provider_key.""" def test_letta_api_key_warns(self) -> None: - from band.adapters.letta import LettaAdapterConfig with pytest.warns( DeprecationWarning, match="api_key.*deprecated.*provider_key" @@ -152,7 +136,6 @@ def test_letta_api_key_warns(self) -> None: assert "api_key" not in LettaAdapterConfig.model_fields def test_letta_provider_key_and_api_key_conflict(self) -> None: - from band.adapters.letta import LettaAdapterConfig with pytest.raises(BandConfigError, match="Cannot pass both"): LettaAdapterConfig(provider_key="new-key", api_key="old-key") @@ -162,7 +145,6 @@ class TestLettaMCPKwargShim: """Legacy Letta MCP kwargs must populate the nested MCP config.""" def test_legacy_mcp_kwargs_warn_and_populate_external_config(self) -> None: - from band.adapters.letta import LettaAdapterConfig with pytest.warns( DeprecationWarning, diff --git a/tests/adapters/test_gemini_adapter.py b/tests/adapters/test_gemini_adapter.py index e413bee44..f6e16a189 100644 --- a/tests/adapters/test_gemini_adapter.py +++ b/tests/adapters/test_gemini_adapter.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock, patch @@ -9,10 +10,10 @@ import pytest from google.genai import types from google.genai.errors import ServerError -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ValidationError from band.adapters.gemini import GeminiAdapter -from band.core.types import Emit, PlatformMessage +from band.core.types import Emit, PlatformMessage, ToolEventKey @pytest.fixture @@ -359,9 +360,6 @@ async def test_read_room_file_image_result_reports_placeholder_not_raw_base64( facing inline_data content block (asserted in TestReadRoomFileImagePassthrough above) is a separate path from what gets reported to the platform-visible event.""" - import json - - from band.core.types import ToolEventKey mock_tools.execute_tool_call = AsyncMock( return_value={ @@ -391,9 +389,6 @@ async def test_send_room_file_reports_content_placeholder_not_raw_bytes( """The tool_call event for band_send_room_file must report a bounded placeholder for `args`, not the raw file text -- real file bytes (up to ~1MB) have no business in a platform-visible log event.""" - import json - - from band.core.types import ToolEventKey mock_tools.execute_tool_call = AsyncMock(return_value={"status": "success"}) adapter = GeminiAdapter(provider_key="test-key", emit=Emit.TOOL_CALLS) @@ -527,8 +522,6 @@ async def test_cleanup_before_any_messages(self): class TestValidationErrorHandling: @pytest.mark.asyncio async def test_validation_error_returns_friendly_message(self, mock_tools): - from pydantic import ValidationError - adapter = GeminiAdapter(provider_key="test-key") mock_tools.execute_tool_call = AsyncMock( diff --git a/tests/adapters/test_google_adk_adapter.py b/tests/adapters/test_google_adk_adapter.py index c07e931b8..2746fa0ac 100644 --- a/tests/adapters/test_google_adk_adapter.py +++ b/tests/adapters/test_google_adk_adapter.py @@ -17,7 +17,7 @@ import pytest from pydantic import BaseModel, Field -from band.core.types import ALL_CAPABILITIES, Emit, PlatformMessage +from band.core.types import ALL_CAPABILITIES, Capability, Emit, PlatformMessage from band.runtime.tools import AgentTools, BandTool pytest.importorskip("google.adk", reason="google-adk not installed") @@ -109,8 +109,6 @@ def test_execution_reporting_default(self): def test_memory_tools_default(self): """Should default memory tools to False.""" - from band.core.types import Capability - adapter = GoogleADKAdapter() assert Capability.MEMORY not in adapter.features.capabilities diff --git a/tests/adapters/test_letta_mcp.py b/tests/adapters/test_letta_mcp.py index 82c6c9e3f..d68027411 100644 --- a/tests/adapters/test_letta_mcp.py +++ b/tests/adapters/test_letta_mcp.py @@ -21,6 +21,11 @@ RoomContext, ) from band.converters.letta import LettaSessionState +from band.integrations.letta.prompts import ( + SEND_EVENT_TOOL_NAMES, + SEND_MESSAGE_TOOL_NAMES, +) +from band.runtime.tools import BandTool from band.testing import FakeAgentTools from tests.adapters.lettakit import ( make_assistant_message, @@ -46,12 +51,6 @@ def test_send_tool_names_use_band_tool_enum_member(): """SEND_MESSAGE_TOOL_NAMES/SEND_EVENT_TOOL_NAMES's canonical entry must be a BandTool member, not a hardcoded literal duplicate that could silently drift from BandTool if its value ever changed.""" - from band.integrations.letta.prompts import ( - SEND_EVENT_TOOL_NAMES, - SEND_MESSAGE_TOOL_NAMES, - ) - from band.runtime.tools import BandTool - assert isinstance(SEND_MESSAGE_TOOL_NAMES[0], BandTool) assert isinstance(SEND_EVENT_TOOL_NAMES[0], BandTool) diff --git a/tests/adapters/test_parlant_adapter.py b/tests/adapters/test_parlant_adapter.py index 9288d9dcc..921c4f332 100644 --- a/tests/adapters/test_parlant_adapter.py +++ b/tests/adapters/test_parlant_adapter.py @@ -14,7 +14,7 @@ import pytest -from band.adapters.parlant import ParlantAdapter +from band.adapters.parlant import PARLANT_PREAMBLE_TAG, ParlantAdapter from band.core.types import PlatformMessage @@ -991,8 +991,6 @@ async def test_preamble_only_times_out_without_forwarding_a_reply( """Parlant emits a preamble then stalls the final generation. A preamble is an acknowledgment, not an answer, so the adapter must NOT forward it as the reply — the turn is given up honestly (no send_message) rather than faking success.""" - from band.adapters.parlant import PARLANT_PREAMBLE_TAG - adapter = ParlantAdapter( server=mock_parlant_server, parlant_agent=mock_parlant_agent, diff --git a/tests/adapters/test_pydantic_ai_adapter.py b/tests/adapters/test_pydantic_ai_adapter.py index 07051545c..354990cdf 100644 --- a/tests/adapters/test_pydantic_ai_adapter.py +++ b/tests/adapters/test_pydantic_ai_adapter.py @@ -8,7 +8,7 @@ """ from collections.abc import AsyncIterator, Iterator -from contextlib import asynccontextmanager +from contextlib import asynccontextmanager, contextmanager from datetime import datetime, timezone from pathlib import Path from types import SimpleNamespace @@ -20,7 +20,7 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter -from pydantic import BaseModel +from pydantic import BaseModel, Field from pydantic_ai import ( Agent, AgentRunResultEvent, @@ -29,6 +29,7 @@ InstrumentationSettings, RunContext, UnexpectedModelBehavior, + _tool_execution, ) from pydantic_ai.capabilities import ProcessHistory from pydantic_ai.messages import ( @@ -49,13 +50,15 @@ from band.adapters.pydantic_ai import ( OUTPUT_RETRIES_EXHAUSTED, PydanticAIAdapter, + _custom_tool_def_to_callable, _drop_non_replayable_messages, _is_output_retries_exhausted, _is_replayable_history_message, ) from band.core.protocols import AgentToolsProtocol -from band.core.types import Capability, Emit, PlatformMessage +from band.core.types import Capability, Emit, PlatformMessage, TurnUsage from band.runtime.custom_tools import get_custom_tool_name +from tests.adapters.usage_events import sent_usage_payloads from band.runtime.tools import get_tool_description from tests.framework_configs.adapters import pydantic_ai_probe_tools @@ -182,7 +185,6 @@ def test_usage_from_result_reads_the_usage_property(self): Reading it as a method instead would raise, and the guarded read would then report zeros for every turn — silent, so this is the guard. """ - from band.core.types import TurnUsage result = SimpleNamespace( usage=SimpleNamespace( @@ -201,7 +203,6 @@ def test_usage_from_result_reads_the_usage_property(self): def test_usage_from_result_swallows_errors(self): """Usage that fails to read yields empty usage, never propagates.""" - from band.core.types import TurnUsage class Unreadable: @property @@ -216,7 +217,6 @@ def test_usage_from_messages_sums_model_responses(self): Covers the empty-final-response path (no AgentRunResultEvent fires) where the turn still spent tokens — each ModelResponse carries its own usage. """ - from band.core.types import TurnUsage messages = [ ModelRequest(parts=[]), # non-response: ignored @@ -229,9 +229,6 @@ def test_usage_from_messages_sums_model_responses(self): def test_usage_from_messages_empty_when_no_responses(self): """No ModelResponse in the captured messages → empty usage.""" - from pydantic_ai.messages import ModelRequest - - from band.core.types import TurnUsage assert ( PydanticAIAdapter._usage_from_messages([ModelRequest(parts=[])]) @@ -249,7 +246,6 @@ def test_new_run_messages_isolates_this_run_despite_history_merge(self): run — and combined with the ModelResponse-only sum, yields only this turn's usage. """ - from band.core.types import TurnUsage # Prior history: a real response, then two instruction-less requests that # pydantic-ai would merge into one on the next run. @@ -784,8 +780,6 @@ async def test_read_room_file_forwards_file_id(self, file_tools): @pytest.mark.asyncio async def test_read_room_file_image_result_becomes_binary_content(self, file_tools): - from pydantic_ai.messages import BinaryContent - file_tools.read_room_file = AsyncMock( return_value={ "content": [ @@ -1512,7 +1506,6 @@ def test_swallow_matches_the_wording_pydantic_ai_actually_raises(self) -> None: exactly what 2.x did to the 1.x phrasing ("Exceeded maximum retries (N) for output validation"). Read the real source so a future reword fails here. """ - from pydantic_ai import _tool_execution source = Path(_tool_execution.__file__).read_text(encoding="utf-8").lower() assert OUTPUT_RETRIES_EXHAUSTED in source @@ -1553,7 +1546,6 @@ async def test_empty_output_after_tool_is_benign( # Regression (fallback path): with the run mocked, capture_run_messages records # nothing, so the swallow falls back to preserving at least the user prompt so # the next same-session turn isn't amnesiac. - from pydantic_ai.messages import ModelRequest, UserPromptPart preserved = adapter._message_history["room-123"] assert preserved, "swallowed turn should still record the user message" @@ -1569,14 +1561,6 @@ async def test_empty_output_preserves_full_captured_turn( ): """The swallow persists the whole captured turn — not just the user prompt — so a later 'what did you just say?' has the agent's reply in context.""" - from contextlib import contextmanager - - from pydantic_ai.messages import ( - ModelRequest, - ModelResponse, - TextPart, - UserPromptPart, - ) adapter = PydanticAIAdapter(model="openai:gpt-5.4") with patch.object(adapter, "_create_agent", return_value=mock_pydantic_agent): @@ -1652,9 +1636,6 @@ async def test_failed_run_still_emits_captured_usage( Tokens spent before the failure were still spent: the finally-based emit falls back to summing this run's captured ModelResponses when no result event fired, so a hard mid-run failure doesn't silently drop usage.""" - from contextlib import contextmanager - - from tests.adapters.usage_events import sent_usage_payloads adapter = PydanticAIAdapter( model="openai:gpt-5.4", @@ -1936,7 +1917,6 @@ class TestPortableCustomToolDef: @pytest.mark.asyncio async def test_tuple_is_normalized_to_a_named_callable(self): - from pydantic import BaseModel class LookupInput(BaseModel): """look up a code.""" @@ -1959,7 +1939,6 @@ def lookup(args: LookupInput) -> str: async def test_async_handler_is_awaited(self): """An async portable handler must be awaited (not returned as a coroutine) — the same shared-executor path every other adapter uses.""" - from pydantic import BaseModel class LookupInput(BaseModel): key: str @@ -1973,7 +1952,6 @@ async def lookup(args: LookupInput) -> str: assert await adapter._custom_tools[0](LookupInput(key="beta")) == "code:beta" def test_tuple_terminal_marker_is_honored(self): - from pydantic import BaseModel class DeployInput(BaseModel): """deploy.""" @@ -1991,11 +1969,6 @@ def deploy(args: DeployInput) -> str: assert adapter._custom_terminal_names == frozenset({"deploy"}) def test_converted_tuple_flattens_in_pydantic_ai(self): - from pydantic import BaseModel - from pydantic_ai import Agent - from pydantic_ai.models.test import TestModel - - from band.adapters.pydantic_ai import _custom_tool_def_to_callable class LookupInput(BaseModel): """look up a code.""" @@ -2016,7 +1989,6 @@ def lookup(args: LookupInput) -> str: @staticmethod def _tool_return_contents(result) -> list: - from pydantic_ai.messages import ToolReturnPart return [ part.content @@ -2030,11 +2002,6 @@ async def test_async_handler_tuple_is_awaited_end_to_end(self): """An async CustomToolDef handler returns its awaited value through a real pydantic-ai run — not an unawaited coroutine (which the previous sync passthrough produced, failing serialization).""" - from pydantic import BaseModel - from pydantic_ai import Agent - from pydantic_ai.models.test import TestModel - - from band.adapters.pydantic_ai import _custom_tool_def_to_callable class LookupInput(BaseModel): """look up a code.""" @@ -2059,11 +2026,6 @@ async def test_zero_arg_handler_tuple_runs_end_to_end(self): """A zero-argument handler with an empty InputModel executes through a real pydantic-ai run — the previous sync passthrough called it with one positional arg and raised TypeError.""" - from pydantic import BaseModel - from pydantic_ai import Agent - from pydantic_ai.models.test import TestModel - - from band.adapters.pydantic_ai import _custom_tool_def_to_callable class PingInput(BaseModel): """ping.""" @@ -2084,11 +2046,6 @@ async def test_aliased_input_model_runs_end_to_end(self): """An InputModel using a field alias executes through a real pydantic-ai run — a dump/re-validate round-trip would emit field names and fail re-validation against the alias-only model.""" - from pydantic import BaseModel, Field - from pydantic_ai import Agent - from pydantic_ai.models.test import TestModel - - from band.adapters.pydantic_ai import _custom_tool_def_to_callable class AliasedInput(BaseModel): """look up a user.""" diff --git a/tests/adapters/test_strands_adapter.py b/tests/adapters/test_strands_adapter.py index 3cf5aaa32..975750efe 100644 --- a/tests/adapters/test_strands_adapter.py +++ b/tests/adapters/test_strands_adapter.py @@ -37,13 +37,16 @@ from band.converters.strands import StrandsHistoryConverter # noqa: E402 from band.core.protocols import AgentToolsProtocol # noqa: E402 from band.core.types import ( # noqa: E402 + USAGE_METADATA_KEY, AgentInput, Capability, Emit, HistoryProvider, PlatformMessage, TurnUsage, + is_usage_event, ) +from band.runtime.tools import get_tool_description # noqa: E402 from band.testing import ( # noqa: E402 ErrorTurn, FakeAgentTools, @@ -245,7 +248,6 @@ async def test_capability_gated_tools_registered(self): @pytest.mark.asyncio async def test_platform_tool_descriptions_from_registry(self): - from band.runtime.tools import get_tool_description adapter = StrandsAdapter(model="m") await adapter.on_started("Bot", "A bot") @@ -498,8 +500,6 @@ async def test_usage_emitted_once_per_turn(self, tools, scripted): await _run_message(adapter, tools) - from band.core.types import USAGE_METADATA_KEY, is_usage_event - usage_events = [e for e in tools.events_sent if is_usage_event(e["metadata"])] assert len(usage_events) == 1 # The tool turn and the closing text turn are two model calls, so the @@ -629,8 +629,6 @@ async def test_provider_failure_keeps_the_transcript_and_reports_usage( with pytest.raises(EventLoopException, match="provider down"): await _run_message(adapter, tools) - from band.core.types import USAGE_METADATA_KEY, is_usage_event - tools.assert_message_sent(content="hi", count=1) assert _tool_results(adapter) # the completed call is still in the transcript usage = [e for e in tools.events_sent if is_usage_event(e["metadata"])] diff --git a/tests/adapters/test_usage_mapping.py b/tests/adapters/test_usage_mapping.py index 17aa198b9..934790fdc 100644 --- a/tests/adapters/test_usage_mapping.py +++ b/tests/adapters/test_usage_mapping.py @@ -20,7 +20,7 @@ from band.adapters.gemini import GeminiAdapter from band.adapters.google_adk import GoogleADKAdapter from band.adapters.letta import LettaAdapter -from band.core.types import TurnUsage +from band.core.types import USAGE_METADATA_KEY, TurnUsage, is_usage_event from band.integrations.opencode import OpencodeMessageInfo @@ -73,13 +73,9 @@ class TestIsUsageEvent: """The shared discriminator task-event consumers use to skip usage records.""" def test_true_when_band_usage_present(self): - from band.core.types import USAGE_METADATA_KEY, is_usage_event - assert is_usage_event({USAGE_METADATA_KEY: {"input_tokens": 1}}) is True def test_false_for_lifecycle_task_or_non_mapping(self): - from band.core.types import is_usage_event - assert is_usage_event({"codex_thread_id": "x"}) is False assert is_usage_event(None) is False assert is_usage_event("nope") is False diff --git a/tests/baseline/harness.py b/tests/baseline/harness.py index da948d823..457585c79 100644 --- a/tests/baseline/harness.py +++ b/tests/baseline/harness.py @@ -10,6 +10,8 @@ import pytest +from anthropic.types import TextBlock, ToolUseBlock + from band.adapters.anthropic import AnthropicAdapter from band.core.types import AdapterFeatures, PlatformMessage from band.testing import feature_kwargs @@ -186,8 +188,6 @@ async def _call_anthropic(self, **request: Any) -> Any: if isinstance(decision, Exception): raise decision - from anthropic.types import TextBlock, ToolUseBlock - content: list[Any] = [] for index, call in enumerate(decision.tool_calls, start=1): content.append( diff --git a/tests/bridge/test_forwarder.py b/tests/bridge/test_forwarder.py index 661035eac..062cee1c4 100644 --- a/tests/bridge/test_forwarder.py +++ b/tests/bridge/test_forwarder.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import time from typing import Any from unittest import mock from unittest.mock import AsyncMock, MagicMock @@ -140,8 +141,6 @@ async def test_session_id_falls_back_to_agent_id_without_room( async def test_timeout_raises(self, agentcore_target: AgentCoreTarget) -> None: # invoke takes longer than the timeout def _slow(*_args: Any, **_kwargs: Any) -> dict[str, Any]: - import time - time.sleep(0.2) return { "response": MagicMock( diff --git a/tests/cli/test_trigger.py b/tests/cli/test_trigger.py index 481b4a9b7..421b9d423 100644 --- a/tests/cli/test_trigger.py +++ b/tests/cli/test_trigger.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import asyncio import logging from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch @@ -585,12 +586,10 @@ def test_exits_1_on_timeout(self, monkeypatch): "30", ], ) - import asyncio as _asyncio - with ( patch( "band.cli.trigger.asyncio.run", - side_effect=_fake_asyncio_run(side_effect=_asyncio.TimeoutError()), + side_effect=_fake_asyncio_run(side_effect=asyncio.TimeoutError()), ), pytest.raises(SystemExit) as exc_info, ): @@ -612,12 +611,10 @@ def test_timeout_error_message(self, monkeypatch, capsys): "45", ], ) - import asyncio as _asyncio - with ( patch( "band.cli.trigger.asyncio.run", - side_effect=_fake_asyncio_run(side_effect=_asyncio.TimeoutError()), + side_effect=_fake_asyncio_run(side_effect=asyncio.TimeoutError()), ), pytest.raises(SystemExit), ): diff --git a/tests/conftest.py b/tests/conftest.py index 82473653e..8c9270364 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,6 +56,7 @@ ContactRemovedEvent, ) from band.platform.link import BandLink +from band.runtime.single_instance import SingleInstanceGuard from band.runtime.types import PlatformMessage from tests.paths import ENV_TEST_FILE @@ -186,8 +187,6 @@ def isolated_single_instance_lock(request, tmp_path_factory, monkeypatch): yield return - from band.runtime.single_instance import SingleInstanceGuard - lock_dir: list = [] created: list[SingleInstanceGuard] = [] diff --git a/tests/e2e/baseline/fixtures/platform.py b/tests/e2e/baseline/fixtures/platform.py index 46d2546b9..c3a56aaed 100644 --- a/tests/e2e/baseline/fixtures/platform.py +++ b/tests/e2e/baseline/fixtures/platform.py @@ -10,6 +10,8 @@ import pytest from band_rest import AsyncRestClient +from band import agent as agent_module +from band.runtime import single_instance from tests.e2e.baseline.settings import BaselineSettings from tests.e2e.baseline.toolkit.provisioning import ( ResourceManager, @@ -130,9 +132,6 @@ async def reap_leaked_agents() -> AsyncGenerator[None, None]: its lock — removes the zombie so the next start is a true singleton. Reruns re-run function fixtures, so reaping here heals them too. """ - from band import agent as agent_module - from band.runtime import single_instance - yield for leaked_agent in agent_module.running_agents(): logger.warning( diff --git a/tests/e2e/baseline/guards/test_adapter_registry.py b/tests/e2e/baseline/guards/test_adapter_registry.py index f72c08bca..e8ec2abac 100644 --- a/tests/e2e/baseline/guards/test_adapter_registry.py +++ b/tests/e2e/baseline/guards/test_adapter_registry.py @@ -10,6 +10,7 @@ import pytest +from band.core.types import Capability from tests.e2e.baseline.requires import Dep from tests.e2e.baseline.settings import BaselineSettings from tests.e2e.baseline.toolkit.adapters import ( @@ -71,8 +72,6 @@ def test_ci_lanes_partition_is_complete_and_disjoint() -> None: def test_supports_filter_selects_memory_adapters() -> None: """The capability filter narrows the matrix (the 'memory matrix' use case).""" - from band.core.types import Capability - memory_ids = {spec.id for spec in specs(supports={Capability.MEMORY})} assert memory_ids <= registered_ids() assert "anthropic" in memory_ids # a known memory-tool-loop adapter diff --git a/tests/e2e/baseline/smoke/adapters/test_copilot_acp.py b/tests/e2e/baseline/smoke/adapters/test_copilot_acp.py index 42bc58f62..a5fd3fb3a 100644 --- a/tests/e2e/baseline/smoke/adapters/test_copilot_acp.py +++ b/tests/e2e/baseline/smoke/adapters/test_copilot_acp.py @@ -145,7 +145,7 @@ def hermetic_copilot_config( smoke's one billed turn uses a cheap, deterministic model instead of Copilot's ``auto`` picker. """ - from band.adapters.copilot_acp import CopilotACPAdapterConfig + from band.adapters.copilot_acp import CopilotACPAdapterConfig # noqa: PLC0415 -- copilot_acp imports the acp (agent-client-protocol) extra at its own top level; not installed in every lane's venv home = copilot_home_dir(str(work_dir)) hosted_env = { @@ -182,7 +182,7 @@ async def test_copilot_hosted_auth_replies( cheap turn keeps it proven. Skips (not fails) without a token: hosted auth is optional extra coverage, the BYOK cells are the lane's bar. """ - from band.adapters.copilot_acp import CopilotACPAdapter + from band.adapters.copilot_acp import CopilotACPAdapter # noqa: PLC0415 -- copilot_acp imports the acp (agent-client-protocol) extra at its own top level; not installed in every lane's venv if not baseline_settings.backends.github_token: pytest.skip("GITHUB_TOKEN unset — the Copilot-hosted auth smoke needs one") @@ -240,7 +240,7 @@ async def test_acp_recall_via_room_replay_when_session_load_misses( reply lines are its only possible source (the regression case for a replay that drops the agent's side of the transcript). """ - from band.adapters.copilot_acp import CopilotACPAdapter + from band.adapters.copilot_acp import CopilotACPAdapter # noqa: PLC0415 -- copilot_acp imports the acp (agent-client-protocol) extra at its own top level; not installed in every lane's venv tracking_marker = unique_marker("acp-replay") agent_fact = "blue" diff --git a/tests/e2e/baseline/smoke/adapters/test_copilot_sdk.py b/tests/e2e/baseline/smoke/adapters/test_copilot_sdk.py index 732a6a64a..840fa5aa0 100644 --- a/tests/e2e/baseline/smoke/adapters/test_copilot_sdk.py +++ b/tests/e2e/baseline/smoke/adapters/test_copilot_sdk.py @@ -29,7 +29,12 @@ import pytest -from band.adapters.copilot_sdk import ASK_USER_ROOM, _COPILOT_SDK_AVAILABLE +from band.adapters.copilot_sdk import ( + ASK_USER_ROOM, + _COPILOT_SDK_AVAILABLE, + CopilotSDKAdapter, + CopilotSDKAdapterConfig, +) from tests.e2e.baseline.flaky import flaky_infra @@ -57,9 +62,7 @@ def _copilot_config(settings: BaselineSettings, **overrides: Any) -> Any: bespoke tests don't re-derive it; ``overrides`` layers the one knob each test actually cares about (``ask_user=``, ``base_directory=``). """ - from copilot import ProviderConfig - - from band.adapters.copilot_sdk import CopilotSDKAdapterConfig + from copilot import ProviderConfig # noqa: PLC0415 -- copilot_sdk extra; file collects even when absent, skipped via _COPILOT_SDK_AVAILABLE at test time return CopilotSDKAdapterConfig( model=settings.llm_models.anthropic_model, @@ -99,7 +102,6 @@ async def test_copilot_ask_user_handler_round_trips_to_room_reply( forwarding (without either, the model cannot ask and the handler never fires). """ - from band.adapters.copilot_sdk import CopilotSDKAdapter operator_channel = f"channel-{uuid.uuid4().hex[:6]}" asked: list[dict[str, Any]] = [] @@ -167,7 +169,6 @@ async def test_copilot_ask_user_room_question_answered_by_next_message( reply containing it proves the answer flowed through the room round trip — not model invention. """ - from band.adapters.copilot_sdk import CopilotSDKAdapter secret_channel = f"channel-{uuid.uuid4().hex[:6]}" adapter = CopilotSDKAdapter( @@ -248,7 +249,6 @@ async def test_copilot_recall_via_injected_history_when_resume_misses( replies are its only possible source (the regression case for one-sided injected history). """ - from band.adapters.copilot_sdk import CopilotSDKAdapter tracking_marker = f"MARKER_{uuid.uuid4().hex[:6]}" agent_fact = "blue" @@ -331,9 +331,7 @@ async def test_copilot_shared_client_across_adapter_lifecycles( still-running client — the borrowed client must survive an adapter's full cleanup (``owns_client=False`` contract). """ - from copilot import CopilotClient - - from band.adapters.copilot_sdk import CopilotSDKAdapter + from copilot import CopilotClient # noqa: PLC0415 -- copilot_sdk extra; file collects even when absent, skipped via _COPILOT_SDK_AVAILABLE at test time identity = await resource_manager.provision_agent("copilot-shared-client") room_a = await resource_manager.provision_room( diff --git a/tests/e2e/baseline/smoke/adapters/test_opencode.py b/tests/e2e/baseline/smoke/adapters/test_opencode.py index 407d16005..0f4969f75 100644 --- a/tests/e2e/baseline/smoke/adapters/test_opencode.py +++ b/tests/e2e/baseline/smoke/adapters/test_opencode.py @@ -31,6 +31,7 @@ import pytest +from band.adapters.opencode import OpencodeAdapter, OpencodeAdapterConfig from band.adapters.opencode.approvals import ( APPROVAL_HANDLED_TEMPLATE, APPROVAL_REQUESTED_PREFIX, @@ -76,8 +77,6 @@ def _handled(messages: list[MessageCreatedPayload], request_id: str) -> bool: def _manual_opencode_adapter(settings: BaselineSettings): """The matrix builder's OpenCode config, but in manual approval mode.""" - from band.adapters.opencode import OpencodeAdapter, OpencodeAdapterConfig - return OpencodeAdapter( config=OpencodeAdapterConfig( base_url=settings.backends.opencode_base_url, diff --git a/tests/e2e/baseline/smoke/adapters/test_parlant.py b/tests/e2e/baseline/smoke/adapters/test_parlant.py index 379ce366d..629831ced 100644 --- a/tests/e2e/baseline/smoke/adapters/test_parlant.py +++ b/tests/e2e/baseline/smoke/adapters/test_parlant.py @@ -39,6 +39,10 @@ # structural skip (not a fail) is correct where it isn't importable. pytest.importorskip("parlant.sdk") +import parlant.sdk as p + +from band.adapters.parlant import ParlantAdapter + _SHORT = "You are a friendly assistant in a chat room. Reply in one short sentence." @@ -72,10 +76,6 @@ async def test_parlant_replies( shared toolkit provisions and runs it, and the delivery barrier proves the turn completed before we read the reply. """ - import parlant.sdk as p - - from band.adapters.parlant import ParlantAdapter - adapter = ParlantAdapter( name="E2E Showcase Agent", description="A test agent for baseline E2E validation. Keep replies short.", diff --git a/tests/e2e/baseline/toolkit/builders.py b/tests/e2e/baseline/toolkit/builders.py index 8a2e2cf94..6b3f36e3b 100644 --- a/tests/e2e/baseline/toolkit/builders.py +++ b/tests/e2e/baseline/toolkit/builders.py @@ -50,7 +50,7 @@ def _build_anthropic( features: AdapterFeatures | None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - from band.adapters.anthropic import AnthropicAdapter + from band.adapters.anthropic import AnthropicAdapter # noqa: PLC0415 -- isolates the anthropic extra from the other frameworks this file builds return AnthropicAdapter( model=s.llm_models.anthropic_model, @@ -69,7 +69,7 @@ def _build_claude_sdk( features: AdapterFeatures | None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - from band.adapters.claude_sdk import ClaudeSDKAdapter + from band.adapters.claude_sdk import ClaudeSDKAdapter # noqa: PLC0415 -- isolates the claude_sdk extra from the other frameworks this file builds return ClaudeSDKAdapter( model=s.llm_models.anthropic_model, @@ -96,9 +96,9 @@ def _build_copilot_sdk( # The generic matrix builder is BYOK-on-Anthropic, matching claude_sdk's model; # ask_user / base_directory / a shared client are bespoke knobs exercised by # tests/e2e/baseline/smoke/adapters/test_copilot_sdk.py, not by this builder. - from copilot import ProviderConfig + from copilot import ProviderConfig # noqa: PLC0415 -- isolates the copilot_sdk extra from the other frameworks this file builds - from band.adapters.copilot_sdk import CopilotSDKAdapter, CopilotSDKAdapterConfig + from band.adapters.copilot_sdk import CopilotSDKAdapter, CopilotSDKAdapterConfig # noqa: PLC0415 -- isolates the copilot_sdk extra from the other frameworks this file builds return CopilotSDKAdapter( CopilotSDKAdapterConfig( @@ -124,10 +124,10 @@ def _build_langgraph( features: AdapterFeatures | None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - from langchain_openai import ChatOpenAI - from langgraph.checkpoint.memory import MemorySaver + from langchain_openai import ChatOpenAI # noqa: PLC0415 -- isolates the langgraph extra from the other frameworks this file builds + from langgraph.checkpoint.memory import MemorySaver # noqa: PLC0415 -- isolates the langgraph extra from the other frameworks this file builds - from band.adapters.langgraph import LangGraphAdapter + from band.adapters.langgraph import LangGraphAdapter # noqa: PLC0415 -- isolates the langgraph extra from the other frameworks this file builds return LangGraphAdapter( llm=ChatOpenAI( @@ -155,9 +155,9 @@ def _build_pydantic_ai( features: AdapterFeatures | None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - from pydantic_ai import RunContext + from pydantic_ai import RunContext # noqa: PLC0415 -- isolates the pydantic_ai extra from the other frameworks this file builds - from band.adapters.pydantic_ai import PydanticAIAdapter + from band.adapters.pydantic_ai import PydanticAIAdapter # noqa: PLC0415 -- isolates the pydantic_ai extra from the other frameworks this file builds # pydantic-ai takes native callables with a RunContext-first signature. native = ( @@ -179,9 +179,9 @@ def _build_strands( features: AdapterFeatures | None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - from strands.models.openai import OpenAIModel + from strands.models.openai import OpenAIModel # noqa: PLC0415 -- isolates the strands extra from the other frameworks this file builds - from band.adapters.strands import StrandsAdapter + from band.adapters.strands import StrandsAdapter # noqa: PLC0415 -- isolates the strands extra from the other frameworks this file builds # Strands has no provider-prefix string shorthand (a bare string means a # Bedrock model id), so the OpenAI provider is constructed explicitly. @@ -205,7 +205,7 @@ def _build_gemini( features: AdapterFeatures | None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - from band.adapters.gemini import GeminiAdapter + from band.adapters.gemini import GeminiAdapter # noqa: PLC0415 -- isolates the gemini extra from the other frameworks this file builds return GeminiAdapter( model=s.llm_models.gemini_model, @@ -224,7 +224,7 @@ def _build_google_adk( features: AdapterFeatures | None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - from band.adapters.google_adk import GoogleADKAdapter + from band.adapters.google_adk import GoogleADKAdapter # noqa: PLC0415 -- isolates the google_adk extra from the other frameworks this file builds # google-adk reads the provider key / Vertex config from the environment. return GoogleADKAdapter( @@ -243,7 +243,7 @@ def _build_crewai( features: AdapterFeatures | None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - from band.adapters.crewai import CrewAIAdapter + from band.adapters.crewai import CrewAIAdapter # noqa: PLC0415 -- isolates the crewai extra from the other frameworks this file builds return CrewAIAdapter( model=s.llm_models.openai_model, @@ -267,10 +267,10 @@ def _build_agno( # Agno bridges a user-built agent, so steering goes into its instructions. # Use the Anthropic model: small models refuse the suite's crafted prompts as # injection, so the matrix relies on E2E_ANTHROPIC_MODEL being a capable model. - from agno.agent import Agent as AgnoAgent - from agno.models.anthropic import Claude + from agno.agent import Agent as AgnoAgent # noqa: PLC0415 -- isolates the agno extra from the other frameworks this file builds + from agno.models.anthropic import Claude # noqa: PLC0415 -- isolates the agno extra from the other frameworks this file builds - from band.adapters.agno import AgnoAdapter + from band.adapters.agno import AgnoAdapter # noqa: PLC0415 -- isolates the agno extra from the other frameworks this file builds # agno tools are plain callables on the agent; the band adapter captures them # and re-offers them alongside the platform tools each run. @@ -296,7 +296,7 @@ def _build_crewai_flow( # CrewAI Flow returns a terminal result rather than running the Band tool loop, # so it takes a flow_factory (not a model/prompt) and advertises no platform # capabilities. The minimal flow echoes back so the reply path is observable. - from band.adapters.crewai_flow import CrewAIFlowAdapter + from band.adapters.crewai_flow import CrewAIFlowAdapter # noqa: PLC0415 -- isolates the crewai_flow extra from the other frameworks this file builds class _E2EFlow: async def kickoff_async(self, inputs: dict[str, Any]) -> dict[str, Any]: @@ -355,7 +355,7 @@ def _build_codex( features: AdapterFeatures | None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - from band.adapters.codex import CodexAdapter, CodexAdapterConfig + from band.adapters.codex import CodexAdapter, CodexAdapterConfig # noqa: PLC0415 -- isolates the codex extra from the other frameworks this file builds return CodexAdapter( config=CodexAdapterConfig(**codex_config_kwargs(s, prompt=prompt)), @@ -377,7 +377,7 @@ def _build_opencode( features: AdapterFeatures | None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - from band.adapters.opencode import OpencodeAdapter, OpencodeAdapterConfig + from band.adapters.opencode import OpencodeAdapter, OpencodeAdapterConfig # noqa: PLC0415 -- isolates the opencode extra from the other frameworks this file builds return OpencodeAdapter( config=OpencodeAdapterConfig( @@ -444,7 +444,7 @@ def _build_copilot_acp( features: AdapterFeatures | None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - from band.adapters.copilot_acp import CopilotACPAdapter, CopilotACPAdapterConfig + from band.adapters.copilot_acp import CopilotACPAdapter, CopilotACPAdapterConfig # noqa: PLC0415 -- isolates the copilot_acp extra from the other frameworks this file builds # stdio spawn of `copilot --acp` co-located with the SDK, so Band tools reach # Copilot over the loopback MCP server (inject_band_tools default True). @@ -505,7 +505,7 @@ def _build_letta( features: AdapterFeatures | None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - from band.adapters.letta import LettaAdapter, LettaAdapterConfig, LettaMCPConfig + from band.adapters.letta import LettaAdapter, LettaAdapterConfig, LettaMCPConfig # noqa: PLC0415 -- isolates the letta extra from the other frameworks this file builds _reject_tools(Adapter.LETTA, tools) diff --git a/tests/e2e/baseline/toolkit/provisioning.py b/tests/e2e/baseline/toolkit/provisioning.py index b3a53c626..33cb02326 100644 --- a/tests/e2e/baseline/toolkit/provisioning.py +++ b/tests/e2e/baseline/toolkit/provisioning.py @@ -38,6 +38,7 @@ from band.core.simple_adapter import SimpleAdapter from tests.e2e.baseline.settings import BaselineSettings +from tests.e2e.baseline.toolkit.adapters import build_adapter from tests.e2e.baseline.toolkit.user_ops import UserOps if TYPE_CHECKING: @@ -581,15 +582,9 @@ def build( features: AdapterFeatures | None = None, tools: list[ToolSpec] | None = None, ) -> SimpleAdapter[Any]: - """Construct (do not run) this cell's adapter; arguments override cell defaults. - - ``build_adapter`` is imported lazily so this module never pulls the adapter - registry (and its optional framework deps) at import time. - """ + """Construct (do not run) this cell's adapter; arguments override cell defaults.""" # Overrides use None-means-"cell default" (not a sentinel): no test needs to # clear a default back to "no prompt", so the sentinel would be dead machinery. - from tests.e2e.baseline.toolkit.adapters import build_adapter - return build_adapter( self.adapter_id, self.settings, diff --git a/tests/example_agents/test_docker_demo_conductor.py b/tests/example_agents/test_docker_demo_conductor.py index b43d31bf7..34a07d084 100644 --- a/tests/example_agents/test_docker_demo_conductor.py +++ b/tests/example_agents/test_docker_demo_conductor.py @@ -7,6 +7,7 @@ from __future__ import annotations +import dataclasses import datetime as dt from band_rest.types import ChatMessage @@ -189,8 +190,6 @@ def test_conductor_caps_do_not_drift_from_breaker_defaults() -> None: # BreakerConfig defaults, so the two can never silently drift (300 vs 600 again). # `interactive` is intentionally different (conductor defaults to interactive, # the breaker to headless-safe), so normalize just that one mode flag. - import dataclasses - settings = conductor.ConductorSettings() normalized = dataclasses.replace(settings.breaker_config(), interactive=False) assert normalized == conductor.BreakerConfig(), ( diff --git a/tests/framework_configs/adapters.py b/tests/framework_configs/adapters.py index c53829807..952468310 100644 --- a/tests/framework_configs/adapters.py +++ b/tests/framework_configs/adapters.py @@ -9,13 +9,32 @@ import functools import inspect +import logging from dataclasses import dataclass, field from typing import Any, Awaitable, Callable from unittest.mock import AsyncMock, MagicMock from tests.framework_configs.sentinel import MISSING, STRICT_CI, MissingSentinel -from band.adapters.claude_sdk import _CLAUDE_SDK_AVAILABLE as _HAS_CLAUDE_SDK -from band.adapters.copilot_sdk import _COPILOT_SDK_AVAILABLE as _HAS_COPILOT_SDK +from band.adapters.claude_sdk import ( + _CLAUDE_SDK_AVAILABLE as _HAS_CLAUDE_SDK, + ClaudeSDKAdapter, +) + +# Both classes construct with crewai absent; do not fake the package via +# ``sys.modules`` instead — see ``tests/test_module_isolation.py``. +from band.adapters.crewai import CrewAIAdapter +from band.adapters.crewai_flow import CrewAIFlowAdapter +from band.core.types import ALL_CAPABILITIES, AdapterFeatures, Capability +from band.adapters.codex import CodexAdapter, CodexAdapterConfig +from band.adapters.copilot_sdk import ( + _COPILOT_SDK_AVAILABLE as _HAS_COPILOT_SDK, + CopilotSDKAdapter, + CopilotSDKAdapterConfig, +) +from band.adapters.google_adk import GoogleADKAdapter +from band.adapters.opencode import OpencodeAdapter, OpencodeAdapterConfig +from band.adapters.parlant import ParlantAdapter +from band.integrations.crewai.tools import NoopReporter, build_band_crewai_tools __all__ = [ "AdapterConfig", @@ -110,8 +129,6 @@ def _all_capabilities() -> Any: instead of silently sitting outside every probe until someone remembers to add it. """ - from band.core.types import ALL_CAPABILITIES, AdapterFeatures - return AdapterFeatures(capabilities=ALL_CAPABILITIES) @@ -121,8 +138,7 @@ async def pydantic_ai_probe_tools() -> dict[str, Any]: Kept here rather than inline in a test so the walk through pydantic-ai's internals lives in exactly one place. """ - from band.adapters.pydantic_ai import PydanticAIAdapter - from band.core.types import Capability + from band.adapters.pydantic_ai import PydanticAIAdapter # noqa: PLC0415 -- isolates the pydantic_ai extra from the other frameworks this file configures adapter = PydanticAIAdapter( model="test", @@ -157,8 +173,6 @@ async def _crewai_advertised_arg_text() -> dict[str, dict[str, str | None]]: text plus CrewAI-specific mentions leniency, so a field re-declared on that subclass would drift silently — this is the probe that catches it. """ - from band.integrations.crewai.tools import NoopReporter, build_band_crewai_tools - tools = build_band_crewai_tools( get_context=lambda: None, reporter=NoopReporter(), @@ -179,13 +193,13 @@ async def _crewai_advertised_arg_text() -> dict[str, dict[str, str | None]]: def _anthropic_factory(**kw: Any) -> Any: - from band.adapters.anthropic import AnthropicAdapter + from band.adapters.anthropic import AnthropicAdapter # noqa: PLC0415 -- isolates the anthropic extra from the other frameworks this file configures return AnthropicAdapter(**kw) def _langgraph_factory(**kw: Any) -> Any: - from band.adapters.langgraph import LangGraphAdapter + from band.adapters.langgraph import LangGraphAdapter # noqa: PLC0415 -- isolates the langgraph extra from the other frameworks this file configures if "llm" not in kw and "graph_factory" not in kw and "graph" not in kw: kw["llm"] = MagicMock() @@ -202,25 +216,12 @@ def _langgraph_factory(**kw: Any) -> Any: def _crewai_installed() -> bool: """Whether the real crewai package is importable (the dev-crewai venv).""" try: - import crewai # noqa: F401 + import crewai # noqa: F401, PLC0415 except ImportError: return False return True -def _get_crewai_adapter_cls() -> type: - """The CrewAIAdapter class for the conformance config. - - A plain import needs no crewai: every crewai import in the adapter is - TYPE_CHECKING-only or function-local, so the module loads and the class - constructs with the package absent. Do not fake crewai through ``sys.modules`` - to get here — see ``tests/test_module_isolation.py`` for what that costs. - """ - from band.adapters.crewai import CrewAIAdapter - - return CrewAIAdapter - - async def _crewai_conformance_guard(*_args: Any, **_kw: Any) -> None: raise RuntimeError( "CrewAI conformance instance is config-only — " @@ -229,8 +230,7 @@ async def _crewai_conformance_guard(*_args: Any, **_kw: Any) -> None: def _crewai_factory(**kw: Any) -> Any: - cls = _get_crewai_adapter_cls() - instance = cls(**kw) + instance = CrewAIAdapter(**kw) # Guard the runtime methods in both venvs: without crewai they would fail on its # function-local import, and with it they would build a Crew and call an LLM for # real. on_cleanup is never guarded (dict.pop + logging, no CrewAI interaction). @@ -241,13 +241,11 @@ def _crewai_factory(**kw: Any) -> Any: def _claude_sdk_factory(**kw: Any) -> Any: - from band.adapters.claude_sdk import ClaudeSDKAdapter - return ClaudeSDKAdapter(**kw) def _pydantic_ai_factory(**kw: Any) -> Any: - from band.adapters.pydantic_ai import PydanticAIAdapter + from band.adapters.pydantic_ai import PydanticAIAdapter # noqa: PLC0415 -- isolates the pydantic_ai extra from the other frameworks this file configures if "model" not in kw: kw["model"] = _PYDANTIC_AI_INJECTED_MODEL @@ -255,7 +253,7 @@ def _pydantic_ai_factory(**kw: Any) -> Any: def _strands_factory(**kw: Any) -> Any: - from band.adapters.strands import StrandsAdapter + from band.adapters.strands import StrandsAdapter # noqa: PLC0415 -- isolates the strands extra from the other frameworks this file configures if "model" not in kw: kw["model"] = _STRANDS_INJECTED_MODEL @@ -263,8 +261,6 @@ def _strands_factory(**kw: Any) -> Any: def _parlant_factory(**kw: Any) -> Any: - from band.adapters.parlant import ParlantAdapter - # A borrowed server with no parlant_agent: system_prompt/custom_section # (exercised via custom_kwargs) only apply to an adapter-created agent, # so the factory lets the adapter create one on the mocked server. @@ -280,20 +276,16 @@ def _parlant_factory(**kw: Any) -> Any: def _codex_factory(**kw: Any) -> Any: - from band.adapters.codex import CodexAdapter - return CodexAdapter(**kw) def _letta_factory(**kw: Any) -> Any: - from band.adapters.letta import LettaAdapter + from band.adapters.letta import LettaAdapter # noqa: PLC0415 -- isolates the letta extra from the other frameworks this file configures return LettaAdapter(**kw) def _opencode_factory(**kw: Any) -> Any: - from band.adapters.opencode import OpencodeAdapter - # Fake the server boundary so on_started's reachability preflight # (which only runs with the default client factory) stays offline. kw.setdefault("client_factory", lambda _config: MagicMock()) @@ -301,7 +293,7 @@ def _opencode_factory(**kw: Any) -> Any: def _agno_factory(**kw: Any) -> Any: - from band.adapters.agno import AgnoAdapter + from band.adapters.agno import AgnoAdapter # noqa: PLC0415 -- isolates the agno extra from the other frameworks this file configures # AgnoAdapter takes a developer-built Agno Agent; inject a stand-in so the # adapter can be constructed without a real model/API key. @@ -311,14 +303,12 @@ def _agno_factory(**kw: Any) -> Any: def _gemini_factory(**kw: Any) -> Any: - from band.adapters.gemini import GeminiAdapter + from band.adapters.gemini import GeminiAdapter # noqa: PLC0415 -- isolates the gemini extra from the other frameworks this file configures return GeminiAdapter(**kw) def _google_adk_factory(**kw: Any) -> Any: - from band.adapters.google_adk import GoogleADKAdapter - return GoogleADKAdapter(**kw) @@ -339,7 +329,7 @@ def _google_adk_factory(**kw: Any) -> Any: def _build_anthropic_config() -> AdapterConfig: - from band.adapters.anthropic import AnthropicAdapter + from band.adapters.anthropic import AnthropicAdapter # noqa: PLC0415 -- isolates the anthropic extra from the other frameworks this file configures return AdapterConfig( framework_id="anthropic", @@ -363,7 +353,7 @@ def _build_anthropic_config() -> AdapterConfig: def _build_langgraph_config() -> AdapterConfig: - from band.adapters.langgraph import LangGraphAdapter + from band.adapters.langgraph import LangGraphAdapter # noqa: PLC0415 -- isolates the langgraph extra from the other frameworks this file configures return AdapterConfig( framework_id="langgraph", @@ -386,7 +376,7 @@ def _build_langgraph_config() -> AdapterConfig: def _build_crewai_config() -> AdapterConfig: - crewai_cls = _get_crewai_adapter_cls() + crewai_cls = CrewAIAdapter _crewai_available = _crewai_installed() return AdapterConfig( @@ -433,22 +423,10 @@ def _build_crewai_config() -> AdapterConfig: ) -def _get_crewai_flow_adapter_cls() -> type: - """The CrewAIFlowAdapter class for the conformance config. - - Plain import, as for ``_get_crewai_adapter_cls``. The adapter no longer - imports ``Flow`` at module scope, so this remains safe when crewai is absent. - """ - from band.adapters.crewai_flow import CrewAIFlowAdapter - - return CrewAIFlowAdapter - - def _crewai_flow_factory(**kw: Any) -> Any: - cls = _get_crewai_flow_adapter_cls() if "flow_factory" not in kw: kw["flow_factory"] = lambda: MagicMock() - instance = cls(**kw) + instance = CrewAIFlowAdapter(**kw) async def _guard(*_a: Any, **_k: Any) -> None: raise RuntimeError( @@ -461,7 +439,7 @@ async def _guard(*_a: Any, **_k: Any) -> None: def _build_crewai_flow_config() -> AdapterConfig: - flow_cls = _get_crewai_flow_adapter_cls() + flow_cls = CrewAIFlowAdapter return AdapterConfig( framework_id="crewai_flow", @@ -483,18 +461,11 @@ def _build_crewai_flow_config() -> AdapterConfig: def _copilot_sdk_factory(**kw: Any) -> Any: - from band.adapters.copilot_sdk import CopilotSDKAdapter - return CopilotSDKAdapter(**kw) def _build_copilot_sdk_config() -> AdapterConfig | None: - from band.adapters.copilot_sdk import ( - _COPILOT_SDK_AVAILABLE, - CopilotSDKAdapterConfig, - ) - - if not _COPILOT_SDK_AVAILABLE: + if not _HAS_COPILOT_SDK: return None # optional dep not installed; skip in CI custom = CopilotSDKAdapterConfig( @@ -519,9 +490,7 @@ def _build_copilot_sdk_config() -> AdapterConfig | None: def _build_claude_sdk_config() -> AdapterConfig | None: - from band.adapters.claude_sdk import _CLAUDE_SDK_AVAILABLE, ClaudeSDKAdapter - - if not _CLAUDE_SDK_AVAILABLE: + if not _HAS_CLAUDE_SDK: return None # optional dep not installed; skip in CI return AdapterConfig( @@ -556,7 +525,7 @@ def _build_claude_sdk_config() -> AdapterConfig | None: def _build_pydantic_ai_config() -> AdapterConfig: - from band.adapters.pydantic_ai import PydanticAIAdapter + from band.adapters.pydantic_ai import PydanticAIAdapter # noqa: PLC0415 -- isolates the pydantic_ai extra from the other frameworks this file configures return AdapterConfig( framework_id="pydantic_ai", @@ -589,7 +558,7 @@ def _build_pydantic_ai_config() -> AdapterConfig: def _build_strands_config() -> AdapterConfig: - from band.adapters.strands import StrandsAdapter + from band.adapters.strands import StrandsAdapter # noqa: PLC0415 -- isolates the strands extra from the other frameworks this file configures return AdapterConfig( framework_id="strands", @@ -616,10 +585,8 @@ def _build_strands_config() -> AdapterConfig: def _build_parlant_config() -> AdapterConfig: - from band.adapters.parlant import ParlantAdapter - try: - import parlant.sdk # noqa: F401 + import parlant.sdk # noqa: F401, PLC0415 _parlant_available = True except ImportError: @@ -649,8 +616,6 @@ def _build_parlant_config() -> AdapterConfig: def _build_codex_config() -> AdapterConfig: - from band.adapters.codex import CodexAdapterConfig - return AdapterConfig( framework_id="codex", display_name="Codex", @@ -672,7 +637,7 @@ def _build_codex_config() -> AdapterConfig: def _build_letta_config() -> AdapterConfig: - from band.adapters.letta import LettaAdapterConfig, LettaMCPConfig + from band.adapters.letta import LettaAdapterConfig, LettaMCPConfig # noqa: PLC0415 -- isolates the letta extra from the other frameworks this file configures return AdapterConfig( framework_id="letta", @@ -701,8 +666,6 @@ def _build_letta_config() -> AdapterConfig: def _build_opencode_config() -> AdapterConfig: - from band.adapters.opencode import OpencodeAdapterConfig - return AdapterConfig( framework_id="opencode", display_name="OpenCode", @@ -754,7 +717,7 @@ def _build_agno_config() -> AdapterConfig: def _build_gemini_config() -> AdapterConfig: - from band.adapters.gemini import GeminiAdapter + from band.adapters.gemini import GeminiAdapter # noqa: PLC0415 -- isolates the gemini extra from the other frameworks this file configures return AdapterConfig( framework_id="gemini", @@ -802,8 +765,6 @@ def _build_gemini_config() -> AdapterConfig: def _build_google_adk_config() -> AdapterConfig: - from band.adapters.google_adk import GoogleADKAdapter - return AdapterConfig( framework_id="google_adk", display_name="GoogleADK", @@ -857,7 +818,6 @@ def _build_adapter_configs() -> list[AdapterConfig]: in one framework does not prevent the remaining frameworks from being tested. In CI, failures are raised immediately to surface broken configs. """ - import logging logger = logging.getLogger(__name__) configs: list[AdapterConfig] = [] diff --git a/tests/framework_configs/converters.py b/tests/framework_configs/converters.py index 5cafde25e..4a2060a01 100644 --- a/tests/framework_configs/converters.py +++ b/tests/framework_configs/converters.py @@ -8,6 +8,7 @@ from __future__ import annotations import functools +import logging from dataclasses import dataclass from enum import StrEnum from typing import TYPE_CHECKING, Any, Callable @@ -15,6 +16,27 @@ if TYPE_CHECKING: from tests.framework_configs.output_adapters import OutputAdapter +from band.converters.anthropic import AnthropicHistoryConverter +from band.converters.claude_sdk import ClaudeSDKHistoryConverter, ClaudeSDKSessionState +from band.converters.copilot_sdk import ( + CopilotSDKHistoryConverter, + CopilotSDKSessionState, +) +from band.converters.crewai import CrewAIHistoryConverter +from band.converters.google_adk import GoogleADKHistoryConverter +from band.converters.parlant import ParlantHistoryConverter +from tests.framework_configs.output_adapters import ( + AgnoOutputAdapter, + ClaudeSDKOutputAdapter, + CopilotSDKOutputAdapter, + DictListOutputAdapter, + GeminiOutputAdapter, + GoogleADKOutputAdapter, + LangChainOutputAdapter, + PydanticAIOutputAdapter, + SenderDictListAdapter, + StrandsOutputAdapter, +) from tests.framework_configs.sentinel import STRICT_CI __all__ = [ @@ -77,67 +99,55 @@ class ConverterConfig: def _anthropic_factory(**kw: Any) -> Any: - from band.converters.anthropic import AnthropicHistoryConverter - return AnthropicHistoryConverter(**kw) def _langchain_factory(**kw: Any) -> Any: - from band.converters.langchain import LangChainHistoryConverter + from band.converters.langchain import LangChainHistoryConverter # noqa: PLC0415 -- isolates the langgraph extra from the other frameworks this file configures return LangChainHistoryConverter(**kw) def _crewai_factory(**kw: Any) -> Any: - from band.converters.crewai import CrewAIHistoryConverter - return CrewAIHistoryConverter(**kw) def _claude_sdk_factory(**kw: Any) -> Any: - from band.converters.claude_sdk import ClaudeSDKHistoryConverter - return ClaudeSDKHistoryConverter(**kw) def _copilot_sdk_factory(**kw: Any) -> Any: - from band.converters.copilot_sdk import CopilotSDKHistoryConverter - return CopilotSDKHistoryConverter(**kw) def _pydantic_ai_factory(**kw: Any) -> Any: - from band.converters.pydantic_ai import PydanticAIHistoryConverter + from band.converters.pydantic_ai import PydanticAIHistoryConverter # noqa: PLC0415 -- isolates the pydantic_ai extra from the other frameworks this file configures return PydanticAIHistoryConverter(**kw) def _parlant_factory(**kw: Any) -> Any: - from band.converters.parlant import ParlantHistoryConverter - return ParlantHistoryConverter(**kw) def _agno_factory(**kw: Any) -> Any: - from band.converters.agno import AgnoHistoryConverter + from band.converters.agno import AgnoHistoryConverter # noqa: PLC0415 -- isolates the agno extra from the other frameworks this file configures return AgnoHistoryConverter(**kw) def _gemini_factory(**kw: Any) -> Any: - from band.converters.gemini import GeminiHistoryConverter + from band.converters.gemini import GeminiHistoryConverter # noqa: PLC0415 -- isolates the gemini extra from the other frameworks this file configures return GeminiHistoryConverter(**kw) def _google_adk_factory(**kw: Any) -> Any: - from band.converters.google_adk import GoogleADKHistoryConverter - return GoogleADKHistoryConverter(**kw) def _strands_factory(**kw: Any) -> Any: - from band.converters.strands import StrandsHistoryConverter + from band.converters.strands import StrandsHistoryConverter # noqa: PLC0415 -- isolates the strands extra from the other frameworks this file configures return StrandsHistoryConverter(**kw) @@ -148,8 +158,6 @@ def _strands_factory(**kw: Any) -> Any: def _build_anthropic_config() -> ConverterConfig: - from tests.framework_configs.output_adapters import DictListOutputAdapter - return ConverterConfig( framework_id="anthropic", display_name="Anthropic", @@ -162,8 +170,6 @@ def _build_anthropic_config() -> ConverterConfig: def _build_langchain_config() -> ConverterConfig: - from tests.framework_configs.output_adapters import LangChainOutputAdapter - return ConverterConfig( framework_id="langchain", display_name="LangChain", @@ -179,8 +185,6 @@ def _build_langchain_config() -> ConverterConfig: def _build_crewai_config() -> ConverterConfig: - from tests.framework_configs.output_adapters import SenderDictListAdapter - return ConverterConfig( framework_id="crewai", display_name="CrewAI", @@ -199,9 +203,6 @@ def _build_crewai_config() -> ConverterConfig: def _build_claude_sdk_config() -> ConverterConfig: - from band.converters.claude_sdk import ClaudeSDKSessionState - from tests.framework_configs.output_adapters import ClaudeSDKOutputAdapter - return ConverterConfig( framework_id="claude_sdk", display_name="ClaudeSDK", @@ -216,9 +217,6 @@ def _build_claude_sdk_config() -> ConverterConfig: def _build_copilot_sdk_config() -> ConverterConfig: - from band.converters.copilot_sdk import CopilotSDKSessionState - from tests.framework_configs.output_adapters import CopilotSDKOutputAdapter - return ConverterConfig( framework_id="copilot_sdk", display_name="CopilotSDK", @@ -236,8 +234,6 @@ def _build_copilot_sdk_config() -> ConverterConfig: def _build_pydantic_ai_config() -> ConverterConfig: - from tests.framework_configs.output_adapters import PydanticAIOutputAdapter - return ConverterConfig( framework_id="pydantic_ai", display_name="PydanticAI", @@ -251,8 +247,6 @@ def _build_pydantic_ai_config() -> ConverterConfig: def _build_parlant_config() -> ConverterConfig: - from tests.framework_configs.output_adapters import SenderDictListAdapter - return ConverterConfig( framework_id="parlant", display_name="Parlant", @@ -273,8 +267,6 @@ def _build_parlant_config() -> ConverterConfig: def _build_agno_config() -> ConverterConfig: - from tests.framework_configs.output_adapters import AgnoOutputAdapter - return ConverterConfig( framework_id="agno", display_name="Agno", @@ -291,8 +283,6 @@ def _build_agno_config() -> ConverterConfig: def _build_gemini_config() -> ConverterConfig: - from tests.framework_configs.output_adapters import GeminiOutputAdapter - return ConverterConfig( framework_id="gemini", display_name="Gemini", @@ -334,8 +324,6 @@ def _build_gemini_config() -> ConverterConfig: def _build_google_adk_config() -> ConverterConfig: - from tests.framework_configs.output_adapters import GoogleADKOutputAdapter - return ConverterConfig( framework_id="google_adk", display_name="GoogleADK", @@ -352,8 +340,6 @@ def _build_google_adk_config() -> ConverterConfig: def _build_strands_config() -> ConverterConfig: - from tests.framework_configs.output_adapters import StrandsOutputAdapter - return ConverterConfig( framework_id="strands", display_name="Strands", @@ -391,8 +377,6 @@ def _build_converter_configs() -> list[ConverterConfig]: in one framework does not prevent the remaining frameworks from being tested. In CI, failures are raised immediately to surface broken configs. """ - import logging - logger = logging.getLogger(__name__) configs: list[ConverterConfig] = [] for builder in _CONVERTER_CONFIG_BUILDERS: diff --git a/tests/framework_configs/output_adapters.py b/tests/framework_configs/output_adapters.py index ffdc172f9..4ceb42ff8 100644 --- a/tests/framework_configs/output_adapters.py +++ b/tests/framework_configs/output_adapters.py @@ -10,6 +10,9 @@ import threading from typing import Any, Protocol +from band.converters.claude_sdk import ClaudeSDKSessionState +from band.converters.copilot_sdk import CopilotSDKSessionState + __all__ = [ "OutputAdapter", "BaseDictListOutputAdapter", @@ -127,7 +130,7 @@ def get_content(self, result: list, index: int) -> str: return result[index].content def get_role(self, result: list, index: int) -> str: - from langchain_core.messages import AIMessage, HumanMessage, ToolMessage + from langchain_core.messages import AIMessage, HumanMessage, ToolMessage # noqa: PLC0415 -- isolates the langgraph extra from the other frameworks this file configures msg = result[index] if isinstance(msg, HumanMessage): @@ -155,7 +158,7 @@ def content_contains(self, result: list, substring: str) -> bool: return False def assert_element_type(self, result: list, index: int, expected_role: str) -> None: - from langchain_core.messages import AIMessage, HumanMessage, ToolMessage + from langchain_core.messages import AIMessage, HumanMessage, ToolMessage # noqa: PLC0415 -- isolates the langgraph extra from the other frameworks this file configures msg = result[index] type_map: dict[str, type] = { @@ -216,7 +219,7 @@ def content_contains(self, result: list, substring: str) -> bool: return False def assert_element_type(self, result: list, index: int, expected_role: str) -> None: - from agno.models.message import Message + from agno.models.message import Message # noqa: PLC0415 -- isolates the agno extra from the other frameworks this file configures msg = result[index] assert isinstance(msg, Message), ( @@ -252,7 +255,7 @@ def _get_message_types(cls) -> Any: with cls._message_types_lock: # Double-check after acquiring lock. if cls._message_types is None: - from pydantic_ai.messages import ( + from pydantic_ai.messages import ( # noqa: PLC0415 -- isolates the pydantic_ai extra from the other frameworks this file configures ModelRequest, ModelResponse, TextPart, @@ -579,8 +582,6 @@ def __init__(self) -> None: self._inner = StringOutputAdapter() def assert_result_type(self, result: Any) -> None: - from band.converters.claude_sdk import ClaudeSDKSessionState - assert isinstance(result, ClaudeSDKSessionState), ( f"Expected ClaudeSDKSessionState, got {type(result).__name__}" ) @@ -620,8 +621,6 @@ class CopilotSDKOutputAdapter(ClaudeSDKOutputAdapter): """ def assert_result_type(self, result: Any) -> None: - from band.converters.copilot_sdk import CopilotSDKSessionState - assert isinstance(result, CopilotSDKSessionState), ( f"Expected CopilotSDKSessionState, got {type(result).__name__}" ) diff --git a/tests/framework_conformance/test_adapter_conformance.py b/tests/framework_conformance/test_adapter_conformance.py index b344ec81b..8603be70b 100644 --- a/tests/framework_conformance/test_adapter_conformance.py +++ b/tests/framework_conformance/test_adapter_conformance.py @@ -7,9 +7,11 @@ from __future__ import annotations +import inspect + import pytest -from band.core.types import Capability +from band.core.types import Capability, Emit from tests.baseline.adapter import Adapter from tests.e2e.baseline.agents import ExcludedAdapter @@ -137,7 +139,6 @@ def test_on_message_is_callable(self, adapter_config): def test_on_message_is_coroutine_function(self, adapter_config): """on_message must be an async method.""" - import inspect adapter = adapter_config.adapter_factory() assert inspect.iscoroutinefunction(adapter.on_message) @@ -165,7 +166,6 @@ class TestAdapterFeaturesContract: def test_supported_emit_declared(self, adapter_config): """Every adapter class must define SUPPORTED_EMIT as a frozenset.""" - from band.core.types import Emit adapter = adapter_config.adapter_factory() cls = type(adapter) @@ -337,7 +337,10 @@ def test_e2e_matrix_list_matches_this_unit_level_set(self) -> None: (both share opencode's already-fixed MCP engine, so they're excluded from *this* unit-level set as having no probe of their own, but get a real E2E cell each).""" - from tests.e2e.baseline.smoke.matrix.test_capability_matrix import ( + # Real circular import: test_capability_matrix imports + # IMAGE_PASSTHROUGH_SUPPORTED_FRAMEWORK_IDS from this module at its own + # top level, so this side must defer to call time. + from tests.e2e.baseline.smoke.matrix.test_capability_matrix import ( # noqa: PLC0415 IMAGE_PASSTHROUGH_ADAPTERS, ) diff --git a/tests/framework_conformance/test_agent_wiring_rules.py b/tests/framework_conformance/test_agent_wiring_rules.py index 060483d9e..f74a07b33 100644 --- a/tests/framework_conformance/test_agent_wiring_rules.py +++ b/tests/framework_conformance/test_agent_wiring_rules.py @@ -13,12 +13,22 @@ from __future__ import annotations +from dataclasses import replace from types import SimpleNamespace +from unittest.mock import patch import pytest from tests.e2e.baseline.agent_wiring import assert_agent_fixtures_wired -from tests.e2e.baseline.agents import WITH_ADAPTERS_MARKER, PER_ADAPTER_MARKER +from tests.e2e.baseline.toolkit import adapters as adapters_module +from tests.e2e.baseline.toolkit.adapters import Adapter, spec_for, specs +from tests.e2e.baseline.agents import ( + WITH_ADAPTERS_MARKER, + PER_ADAPTER_MARKER, + PerAdapter, + WithAdapters, + per_adapter, +) class FakeItem: @@ -181,7 +191,6 @@ def test_decorator_that_provisions_nothing_is_rejected() -> None: def test_from_node_raises_when_the_decorator_is_missing() -> None: """A missing decorator fails loud with the caller's hint, not a downstream error.""" - from tests.e2e.baseline.agents import WithAdapters with pytest.raises(pytest.UsageError, match="requires @with_adapters"): WithAdapters.from_node(FakeItem(), hint="requires @with_adapters") @@ -190,7 +199,6 @@ def test_from_node_raises_when_the_decorator_is_missing() -> None: def test_from_node_raises_on_a_wrong_payload_type() -> None: """A marker whose arg is not the expected payload (e.g. a raw pytest.mark) is caught by the isinstance check — a clear UsageError, not an AttributeError deep in a fixture.""" - from tests.e2e.baseline.agents import PerAdapter with pytest.raises(pytest.UsageError): PerAdapter.from_node(FakeItem(each=True)) # FakeItem carries a SimpleNamespace @@ -216,12 +224,6 @@ def test_peer_must_be_a_live_adapter() -> None: Synthesizes the pending state by patching a live adapter's registry entry, so the guard stays testable when (as expected) no real adapter is pending. """ - from dataclasses import replace - from unittest.mock import patch - - from tests.e2e.baseline.agents import per_adapter - from tests.e2e.baseline.toolkit import adapters as adapters_module - from tests.e2e.baseline.toolkit.adapters import Adapter, spec_for pending_spec = replace( spec_for(Adapter.LANGGRAPH), e2e_pending="synthetic: backend not CI-wired" @@ -243,7 +245,6 @@ def test_peer_must_be_a_live_adapter() -> None: def test_pending_adapters_match_the_allowlist() -> None: """The e2e_pending set equals the explicit allowlist (empty today).""" - from tests.e2e.baseline.toolkit.adapters import specs pending = { str(spec.id): spec.e2e_pending diff --git a/tests/framework_conformance/test_crewai_job_coverage.py b/tests/framework_conformance/test_crewai_job_coverage.py index 9354c8c02..0da3b4ccd 100644 --- a/tests/framework_conformance/test_crewai_job_coverage.py +++ b/tests/framework_conformance/test_crewai_job_coverage.py @@ -10,6 +10,7 @@ import pytest from tests.framework_conformance import venv_job_coverage as vjc +from tests.framework_configs.sentinel import StrictnessSettings from tests.paths import REPO_ROOT # Import names of the distributions only `dev-crewai` installs. Kept as a map so @@ -90,8 +91,6 @@ def test_missing_framework_optout_is_parsed_as_a_boolean( monkeypatch.setenv("CI", "true") monkeypatch.setenv("BAND_ALLOW_MISSING_FRAMEWORKS", flag) - from tests.framework_configs.sentinel import StrictnessSettings - settings = StrictnessSettings() strict = settings.ci and not settings.band_allow_missing_frameworks assert strict is (flag != "1") diff --git a/tests/framework_conformance/test_files_image_passthrough_matrix.py b/tests/framework_conformance/test_files_image_passthrough_matrix.py index a3d48a960..7bb724dcb 100644 --- a/tests/framework_conformance/test_files_image_passthrough_matrix.py +++ b/tests/framework_conformance/test_files_image_passthrough_matrix.py @@ -24,10 +24,12 @@ from collections.abc import Awaitable, Callable, Iterable from types import SimpleNamespace from typing import Any +from unittest.mock import AsyncMock, MagicMock import pytest -from band.runtime.tools import BandTool, TOOL_DEFINITIONS +from band.core.types import AdapterFeatures, Capability +from band.runtime.tools import BandTool, TOOL_DEFINITIONS, ToolCallOutcome from tests.framework_conformance.test_adapter_conformance import ( IMAGE_PASSTHROUGH_SUPPORTED_FRAMEWORK_IDS, ) @@ -101,7 +103,7 @@ async def execute_tool_call( async def _probe_claude_sdk() -> bool: - from band.integrations.claude_sdk.tools import build_band_sdk_tools + from band.integrations.claude_sdk.tools import build_band_sdk_tools # noqa: PLC0415 -- claude_sdk extra, absent from the standard dev-crewai/dev-parlant lane venvs sdk_tools = build_band_sdk_tools( tool_definitions=[TOOL_DEFINITIONS[BandTool.READ_ROOM_FILE]], @@ -116,11 +118,10 @@ async def _probe_claude_sdk() -> bool: async def _probe_anthropic() -> bool: - from unittest.mock import AsyncMock, MagicMock - from anthropic.types import ToolUseBlock + from anthropic.types import ToolUseBlock # noqa: PLC0415 -- anthropic extra, absent from the standard dev-crewai/dev-parlant lane venvs - from band.adapters.anthropic import AnthropicAdapter + from band.adapters.anthropic import AnthropicAdapter # noqa: PLC0415 -- anthropic extra, absent from the standard dev-crewai/dev-parlant lane venvs adapter = AnthropicAdapter(emit=()) tools = MagicMock() @@ -147,9 +148,11 @@ async def _probe_anthropic() -> bool: async def _probe_opencode() -> bool: - from mcp.shared.memory import create_connected_server_and_client_session + from mcp.shared.memory import ( # noqa: PLC0415 -- opencode extra, absent from the standard dev-crewai/dev-parlant lane venvs + create_connected_server_and_client_session, + ) - from band.integrations.mcp.engine import ( + from band.integrations.mcp.engine import ( # noqa: PLC0415 -- opencode extra, absent from the standard dev-crewai/dev-parlant lane venvs EmbeddedResolver, EngineSpec, build_engine, @@ -179,11 +182,10 @@ async def _probe_opencode() -> bool: async def _probe_gemini() -> bool: - from unittest.mock import AsyncMock, MagicMock - from google.genai import types + from google.genai import types # noqa: PLC0415 -- gemini extra, absent from the standard dev-crewai/dev-parlant lane venvs - from band.adapters.gemini import GeminiAdapter + from band.adapters.gemini import GeminiAdapter # noqa: PLC0415 -- gemini extra, absent from the standard dev-crewai/dev-parlant lane venvs adapter = GeminiAdapter(provider_key="test-key") tools = MagicMock() @@ -206,10 +208,10 @@ async def _probe_gemini() -> bool: async def _probe_langgraph() -> bool: - from unittest.mock import AsyncMock, MagicMock - from band.core.types import AdapterFeatures, Capability - from band.integrations.langgraph.langchain_tools import agent_tools_to_langchain + from band.integrations.langgraph.langchain_tools import ( # noqa: PLC0415 -- langgraph extra, absent from the standard dev-crewai/dev-parlant lane venvs + agent_tools_to_langchain, + ) tools = MagicMock() tools.is_hub_room = False @@ -233,9 +235,12 @@ async def _probe_langgraph() -> bool: async def _probe_agno() -> bool: - from agno.tools.function import ToolResult + from agno.tools.function import ToolResult # noqa: PLC0415 -- agno extra, absent from the standard dev-crewai/dev-parlant lane venvs - from band.adapters.agno import _bind_room_tools, _make_band_entrypoint + from band.adapters.agno import ( # noqa: PLC0415 -- agno extra, absent from the standard dev-crewai/dev-parlant lane venvs + _bind_room_tools, + _make_band_entrypoint, + ) entry = _make_band_entrypoint(BandTool.READ_ROOM_FILE) with _bind_room_tools(_StubReadRoomFileTools()): @@ -250,7 +255,7 @@ async def _probe_agno() -> bool: async def _probe_strands() -> bool: - from band.adapters.strands import _tool_result + from band.adapters.strands import _tool_result # noqa: PLC0415 -- strands extra, absent from the standard dev-crewai/dev-parlant lane venvs tool_use = {"toolUseId": "t1", "name": BandTool.READ_ROOM_FILE, "input": {}} @@ -264,13 +269,10 @@ async def _probe_strands() -> bool: async def _probe_copilot_sdk() -> bool: - from types import SimpleNamespace - from unittest.mock import AsyncMock, MagicMock - from copilot import ToolInvocation + from copilot import ToolInvocation # noqa: PLC0415 -- copilot_sdk extra, absent from the standard dev-crewai/dev-parlant lane venvs - from band.adapters.copilot_sdk import CopilotSDKAdapter - from band.runtime.tools import ToolCallOutcome + from band.adapters.copilot_sdk import CopilotSDKAdapter # noqa: PLC0415 -- copilot_sdk extra, absent from the standard dev-crewai/dev-parlant lane venvs room_tools = MagicMock() room_tools.execute_tool_call_structured = AsyncMock( @@ -298,7 +300,7 @@ async def _probe_copilot_sdk() -> bool: async def _probe_codex() -> bool: - from band.adapters.codex import _image_content_items + from band.adapters.codex import _image_content_items # noqa: PLC0415 -- codex extra, absent from the standard dev-crewai/dev-parlant lane venvs content_items = _image_content_items(_IMAGE_RESULT) @@ -307,8 +309,7 @@ async def _probe_codex() -> bool: async def _probe_pydantic_ai() -> bool: - from band.adapters.pydantic_ai import PydanticAIAdapter - from band.core.types import Capability + from band.adapters.pydantic_ai import PydanticAIAdapter # noqa: PLC0415 -- pydantic_ai extra, absent from the standard dev-crewai/dev-parlant lane venvs adapter = PydanticAIAdapter(model="test", capabilities=Capability.FILES) await adapter.on_started(agent_name="Probe", agent_description="probe") @@ -325,13 +326,12 @@ async def _probe_pydantic_ai() -> bool: async def _probe_crewai() -> bool: - from band.integrations.crewai.tools import ( + from band.integrations.crewai.tools import ( # noqa: PLC0415 -- crewai extra, absent from the standard dev-crewai/dev-parlant lane venvs CrewAIToolContext, NoopReporter, build_band_crewai_tools, vision_sentinel, ) - from band.core.types import Capability context = CrewAIToolContext(room_id="room-1", tools=_StubReadRoomFileTools()) tools = build_band_crewai_tools( @@ -374,8 +374,7 @@ def test_crewai_platform_tool_name_is_plain_str() -> None: BandTool (StrEnum) default would leave tool.name a BandTool instance at runtime instead of the str the field is typed as -- str(spec.name) at the PlatformTool definition must keep it a plain str.""" - from band.core.types import Capability - from band.integrations.crewai.tools import ( + from band.integrations.crewai.tools import ( # noqa: PLC0415 -- crewai extra, absent from the standard dev-crewai/dev-parlant lane venvs CrewAIToolContext, NoopReporter, build_band_crewai_tools, diff --git a/tests/integration/test_google_adk_converter.py b/tests/integration/test_google_adk_converter.py index 49285737a..cfd139e18 100644 --- a/tests/integration/test_google_adk_converter.py +++ b/tests/integration/test_google_adk_converter.py @@ -18,6 +18,7 @@ from band_rest.types import ChatMessageRequestMentionsItem as Mention from tests.integration.conftest import requires_api +from band.converters.google_adk import GoogleADKHistoryConverter logger = logging.getLogger(__name__) @@ -74,8 +75,6 @@ async def test_converter_with_real_tool_history( if shared_room is None or shared_agent1_info is None: pytest.skip("shared_room or shared_agent1_info not available") - from band.converters.google_adk import GoogleADKHistoryConverter - chat_id = shared_room agent_name = shared_agent1_info.name tc_id = _unique_id("tc_adk_real") @@ -183,8 +182,6 @@ async def test_converter_batches_parallel_tool_calls( if shared_room is None or shared_agent1_info is None: pytest.skip("shared_room or shared_agent1_info not available") - from band.converters.google_adk import GoogleADKHistoryConverter - chat_id = shared_room agent_name = shared_agent1_info.name tc_id_1 = _unique_id("tc_adk_batch1") @@ -295,8 +292,6 @@ async def test_skips_thought_events(self, api_client, shared_room): if shared_room is None: pytest.skip("shared_room not available") - from band.converters.google_adk import GoogleADKHistoryConverter - chat_id = shared_room marker = uuid.uuid4().hex[:8] thought_content = f"Let me think about this {marker}..." @@ -331,8 +326,6 @@ async def test_skips_error_events(self, api_client, shared_room): if shared_room is None: pytest.skip("shared_room not available") - from band.converters.google_adk import GoogleADKHistoryConverter - chat_id = shared_room marker = uuid.uuid4().hex[:8] error_content = f"Error: API rate limit exceeded {marker}" @@ -369,8 +362,6 @@ async def test_error_tool_result_preserves_is_error_flag( if shared_room is None or shared_agent1_info is None: pytest.skip("shared_room or shared_agent1_info not available") - from band.converters.google_adk import GoogleADKHistoryConverter - chat_id = shared_room agent_name = shared_agent1_info.name tc_id = _unique_id("tc_adk_err") @@ -440,8 +431,6 @@ async def test_full_conversation_flow( if shared_room is None or shared_agent1_info is None: pytest.skip("shared_room or shared_agent1_info not available") - from band.converters.google_adk import GoogleADKHistoryConverter - chat_id = shared_room agent_name = shared_agent1_info.name diff --git a/tests/integration/test_history_converters.py b/tests/integration/test_history_converters.py index bcd23fbe8..a3bb3a0f4 100644 --- a/tests/integration/test_history_converters.py +++ b/tests/integration/test_history_converters.py @@ -26,6 +26,9 @@ from band_rest import ChatEventRequest, ChatMessageRequest from band_rest.types import ChatMessageRequestMentionsItem as Mention +from band.converters.anthropic import AnthropicHistoryConverter +from band.converters.pydantic_ai import PydanticAIHistoryConverter +from band.runtime.formatters import format_history_for_llm from tests.integration.conftest import fetch_all_context, requires_api logger = logging.getLogger(__name__) @@ -83,8 +86,6 @@ async def test_converter_with_real_tool_history( if shared_room is None or shared_agent1_info is None: pytest.skip("shared_room or shared_agent1_info not available") - from band.converters.anthropic import AnthropicHistoryConverter - chat_id = shared_room agent_name = shared_agent1_info.name @@ -178,8 +179,6 @@ async def test_converter_batches_parallel_tool_calls( if shared_room is None or shared_agent1_info is None: pytest.skip("shared_room or shared_agent1_info not available") - from band.converters.anthropic import AnthropicHistoryConverter - chat_id = shared_room agent_name = shared_agent1_info.name @@ -293,8 +292,6 @@ async def test_converter_with_real_tool_history( ModelRequest = pydantic_ai_messages.ModelRequest ModelResponse = pydantic_ai_messages.ModelResponse - from band.converters.pydantic_ai import PydanticAIHistoryConverter - chat_id = shared_room agent_name = shared_agent1_info.name tc_id = _unique_id("call_pai") @@ -381,8 +378,6 @@ async def test_full_conversation_flow( if shared_room is None or shared_agent1_info is None: pytest.skip("shared_room or shared_agent1_info not available") - from band.converters.anthropic import AnthropicHistoryConverter - chat_id = shared_room agent_name = shared_agent1_info.name @@ -504,8 +499,6 @@ async def test_handles_thought_events( if shared_room is None: pytest.skip("shared_room not available") - from band.converters.anthropic import AnthropicHistoryConverter - chat_id = shared_room marker = uuid.uuid4().hex[:8] thought_content = f"Let me think about this request {marker}..." @@ -560,8 +553,6 @@ async def test_handles_error_events( if shared_room is None: pytest.skip("shared_room not available") - from band.converters.anthropic import AnthropicHistoryConverter - chat_id = shared_room marker = uuid.uuid4().hex[:8] error_content = f"Error: API rate limit exceeded {marker}" @@ -601,8 +592,6 @@ async def test_replaces_uuid_mentions_with_handles( if shared_room is None or shared_user_peer is None: pytest.skip("shared_room or shared_user_peer not available") - from band.runtime.formatters import format_history_for_llm - chat_id = shared_room peer_id = shared_user_peer.id peer_name = shared_user_peer.name diff --git a/tests/integration/test_letta_live.py b/tests/integration/test_letta_live.py index ec183787d..b5b38f1d8 100644 --- a/tests/integration/test_letta_live.py +++ b/tests/integration/test_letta_live.py @@ -28,6 +28,8 @@ import pytest from pydantic_settings import BaseSettings, SettingsConfigDict +from letta_client import AsyncLetta +from band.adapters.letta import LettaAdapter, LettaAdapterConfig, LettaMCPConfig pytestmark = pytest.mark.requires_api @@ -53,7 +55,6 @@ class LettaLiveSettings(BaseSettings): def _make_client() -> object: - from letta_client import AsyncLetta client_kwargs: dict[str, str] = {"base_url": LETTA_BASE_URL} if LETTA_API_KEY: @@ -99,7 +100,6 @@ async def test_adapter_self_hosted_mcp_registration() -> None: that URL reports the band platform tools (i.e. Letta could actually reach the server — discovery is a live MCP round-trip, not a config echo). """ - from band.adapters.letta import LettaAdapter, LettaAdapterConfig, LettaMCPConfig loopback = LETTA_MCP_ADVERTISED_HOST in ("127.0.0.1", "localhost") adapter = LettaAdapter( diff --git a/tests/integration/test_trigger.py b/tests/integration/test_trigger.py index b798828bc..c7aec64a6 100644 --- a/tests/integration/test_trigger.py +++ b/tests/integration/test_trigger.py @@ -21,8 +21,9 @@ find_peer_by_handle, run, ) -from band_rest import AsyncRestClient +from band_rest import AsyncRestClient, ChatRoomRequest from band_rest.core.api_error import ApiError +from band_rest.types import ParticipantRequest from tests.conftest_integration import ( AgentInfo, @@ -171,16 +172,12 @@ async def test_real_api_error_format( """Trigger a real ApiError and verify _format_api_error extracts the message.""" # Use an invalid participant_id to trigger a real API error # First create a room we can use - from band_rest import ChatRoomRequest - chat_response = await api_client.agent_api_chats.create_agent_chat( chat=ChatRoomRequest() ) room_id = chat_response.data.id try: - from band_rest.types import ParticipantRequest - with pytest.raises(ApiError) as exc_info: await api_client.agent_api_participants.add_agent_chat_participant( chat_id=room_id, diff --git a/tests/integrations/acp/test_client_adapter_behavior.py b/tests/integrations/acp/test_client_adapter_behavior.py index b8f71801e..5004a71a2 100644 --- a/tests/integrations/acp/test_client_adapter_behavior.py +++ b/tests/integrations/acp/test_client_adapter_behavior.py @@ -29,6 +29,7 @@ from band.runtime.formatters import build_participants_message from tests.integrations.acp.acp_toolkit import FakeACPAgent, acp_adapter, live_line +from acp import RequestError # The header is a template ({marker} carries the per-turn nonce); its first # line is the stable sentinel tests can look for verbatim. @@ -784,7 +785,6 @@ async def test_replay_after_midrun_respawn() -> None: the next turn's freshly created session must be re-seeded from the room transcript (re-fetched, since the runtime only hands history to bootstrap turns), not start amnesiac.""" - from acp import RequestError outcomes = iter(["I noted your favorite color.", "boom", "Blue."]) agent = FakeACPAgent() diff --git a/tests/integrations/acp/test_e2e_codex_acp.py b/tests/integrations/acp/test_e2e_codex_acp.py index 4dfba740e..d3452b242 100644 --- a/tests/integrations/acp/test_e2e_codex_acp.py +++ b/tests/integrations/acp/test_e2e_codex_acp.py @@ -42,6 +42,10 @@ from band.integrations.mcp.local_server import LocalMCPServer from band.runtime.tools import AgentTools from tests.toolkit.timeouts import backstop_timeout +from acp import spawn_agent_process +from acp.schema import HttpMcpServer +from tests.runtime.conftest import make_participant +from acp.exceptions import RequestError logger = logging.getLogger(__name__) @@ -90,7 +94,6 @@ class EchoInput(BaseModel): def _spawn_codex_acp(acp_client: BandACPClient): """Spawn the installed codex-acp executable.""" - from acp import spawn_agent_process if _CODEX_ACP_COMMAND is None: pytest.skip("codex-acp not available") @@ -220,7 +223,6 @@ async def test_codex_acp_http_mcp_server_tool_call( acp_runtime: ACPRuntime, ) -> None: """Should connect to a local HTTP MCP server and execute a tool.""" - from acp.schema import HttpMcpServer assert acp_runtime.client is not None @@ -288,9 +290,6 @@ async def test_codex_acp_band_mcp_tool_call( acp_runtime: ACPRuntime, ) -> None: """Should discover and call a real Band MCP tool.""" - from acp.schema import HttpMcpServer - - from tests.runtime.conftest import make_participant assert acp_runtime.client is not None @@ -417,7 +416,6 @@ async def test_codex_acp_multiple_sessions(acp_runtime: ACPRuntime) -> None: @pytest.mark.asyncio async def test_codex_acp_list_sessions(acp_client: BandACPClient) -> None: """Should list created sessions (if supported by the agent).""" - from acp.exceptions import RequestError ctx = _spawn_codex_acp(acp_client) conn, _proc = await ctx.__aenter__() @@ -459,7 +457,6 @@ async def test_codex_acp_list_sessions(acp_client: BandACPClient) -> None: @pytest.mark.asyncio async def test_spawn_process_safety(acp_client: BandACPClient) -> None: """Should handle __aenter__ failure gracefully for bad command.""" - from acp import spawn_agent_process ctx = spawn_agent_process(acp_client, "nonexistent-acp-command-12345") with pytest.raises(Exception): diff --git a/tests/integrations/claude_sdk/test_session_manager.py b/tests/integrations/claude_sdk/test_session_manager.py index 7755ee755..6a8bc1bb9 100644 --- a/tests/integrations/claude_sdk/test_session_manager.py +++ b/tests/integrations/claude_sdk/test_session_manager.py @@ -7,6 +7,7 @@ import pytest from band.adapters.claude_sdk import _CLAUDE_SDK_AVAILABLE as _HAS_CLAUDE_SDK +from band.integrations.claude_sdk.session_manager import ClaudeSessionManager if _HAS_CLAUDE_SDK: from claude_agent_sdk import ClaudeAgentOptions @@ -49,9 +50,6 @@ async def test_invalidate_removes_session_without_disconnect( self, mock_options: ClaudeAgentOptions ) -> None: """invalidate_session should remove the client without calling disconnect().""" - from band.integrations.claude_sdk.session_manager import ( - ClaudeSessionManager, - ) manager = ClaudeSessionManager(mock_options) await manager.start() @@ -73,9 +71,6 @@ async def test_invalidate_nonexistent_room_is_noop( self, mock_options: ClaudeAgentOptions ) -> None: """invalidate_session on a room that doesn't exist should be a safe no-op.""" - from band.integrations.claude_sdk.session_manager import ( - ClaudeSessionManager, - ) manager = ClaudeSessionManager(mock_options) await manager.start() @@ -92,9 +87,6 @@ async def test_get_or_create_after_invalidate_creates_fresh_client( self, mock_options: ClaudeAgentOptions ) -> None: """After invalidation, get_or_create_session should create a new client.""" - from band.integrations.claude_sdk.session_manager import ( - ClaudeSessionManager, - ) manager = ClaudeSessionManager(mock_options) await manager.start() @@ -127,9 +119,6 @@ async def test_invalidate_when_not_started_is_noop( self, mock_options: ClaudeAgentOptions ) -> None: """invalidate_session before start() should return immediately.""" - from band.integrations.claude_sdk.session_manager import ( - ClaudeSessionManager, - ) manager = ClaudeSessionManager(mock_options) @@ -141,9 +130,6 @@ async def test_invalidate_does_not_affect_other_rooms( self, mock_options: ClaudeAgentOptions ) -> None: """Invalidating one room should leave other rooms' sessions intact.""" - from band.integrations.claude_sdk.session_manager import ( - ClaudeSessionManager, - ) manager = ClaudeSessionManager(mock_options) await manager.start() @@ -166,9 +152,6 @@ class TestBuildOptions: def test_preserves_all_base_fields(self, real_options: ClaudeAgentOptions) -> None: """_build_options should preserve all base_options fields.""" - from band.integrations.claude_sdk.session_manager import ( - ClaudeSessionManager, - ) manager = ClaudeSessionManager(real_options) result = manager._build_options("room-1") @@ -181,9 +164,6 @@ def test_preserves_all_base_fields(self, real_options: ClaudeAgentOptions) -> No def test_always_returns_copy(self, real_options: ClaudeAgentOptions) -> None: """_build_options should return a copy even with no overrides.""" - from band.integrations.claude_sdk.session_manager import ( - ClaudeSessionManager, - ) manager = ClaudeSessionManager(real_options) result = manager._build_options("room-1") @@ -192,9 +172,6 @@ def test_always_returns_copy(self, real_options: ClaudeAgentOptions) -> None: def test_applies_resume_override(self, real_options: ClaudeAgentOptions) -> None: """_build_options should set resume when session_id provided.""" - from band.integrations.claude_sdk.session_manager import ( - ClaudeSessionManager, - ) manager = ClaudeSessionManager(real_options) result = manager._build_options("room-1", resume_session_id="sess-abc") @@ -205,9 +182,6 @@ def test_applies_can_use_tool_factory( self, real_options: ClaudeAgentOptions ) -> None: """_build_options should bind can_use_tool from factory.""" - from band.integrations.claude_sdk.session_manager import ( - ClaudeSessionManager, - ) mock_callback = MagicMock() factory = MagicMock(return_value=mock_callback) @@ -222,9 +196,6 @@ def test_does_not_mutate_base_options( self, real_options: ClaudeAgentOptions ) -> None: """_build_options should not mutate the original base_options.""" - from band.integrations.claude_sdk.session_manager import ( - ClaudeSessionManager, - ) manager = ClaudeSessionManager(real_options) manager._build_options("room-1", resume_session_id="sess-abc") diff --git a/tests/integrations/mcp/test_local_server.py b/tests/integrations/mcp/test_local_server.py index 88daf8159..97f20ca4e 100644 --- a/tests/integrations/mcp/test_local_server.py +++ b/tests/integrations/mcp/test_local_server.py @@ -17,6 +17,7 @@ from pydantic import BaseModel from sse_starlette.sse import AppStatus +import band.integrations.mcp.local_server as local_server_mod from band.core.exceptions import BandToolError from band.integrations.mcp.engine import ( EngineSpec, @@ -380,7 +381,6 @@ async def test_start_forwards_real_host_to_build_engine( locks DNS-rebinding protection to 127.0.0.1/localhost only -- even for a server explicitly bound to a non-loopback host for a Docker callback (see LocalMCPServer's own class docstring).""" - import band.integrations.mcp.local_server as local_server_mod seen_hosts: list[str] = [] real_build_engine = local_server_mod.build_engine @@ -427,7 +427,6 @@ async def test_start_closes_socket_when_engine_construction_fails( """Regression: a failure between socket reservation and the uvicorn serve task starting (e.g. build_engine raising) must still close the reserved socket, not leak a bound-and-listening fd.""" - import band.integrations.mcp.local_server as local_server_mod server = LocalMCPServer( name="test-engine-failure", tool_registrations=[], port_min=0, port_max=0 diff --git a/tests/integrations/slack/test_blockkit.py b/tests/integrations/slack/test_blockkit.py index 414bf17c3..cf67f8511 100644 --- a/tests/integrations/slack/test_blockkit.py +++ b/tests/integrations/slack/test_blockkit.py @@ -25,7 +25,7 @@ render_plan_blocks, ) from band.integrations.slack.types import SlackRoomBinding -from band.runtime.tools import AgentTools +from band.runtime.tools import AgentTools, ToolCallOutcome # ── humanize_tool_name ────────────────────────────────────────────────────── @@ -249,8 +249,6 @@ def _patch_super_execute( ) -> tuple[Any, AsyncMock]: """Patch ``AgentTools.execute_tool_call_structured`` so super calls return controlled :class:`ToolCallOutcome` results.""" - from band.runtime.tools import ToolCallOutcome - mock = AsyncMock( return_value=ToolCallOutcome( value=return_value, ok=ok, error_message=error_message @@ -344,11 +342,12 @@ async def test_failed_tool_call_flips_task_to_error(): async def test_super_exception_marks_task_error_and_reraises(): tools, _, _ = _make_tools() err = RuntimeError("boom") - from band.runtime.tools import AgentTools as _AT # BandToolError (and anything else that propagates out of the # structured call) must mark the task ERROR and re-raise. - with patch.object(_AT, "execute_tool_call_structured", AsyncMock(side_effect=err)): + with patch.object( + AgentTools, "execute_tool_call_structured", AsyncMock(side_effect=err) + ): with pytest.raises(RuntimeError, match="boom"): await tools.execute_tool_call("band_lookup_peers", {}) diff --git a/tests/integrations/slack/test_retry_idempotency.py b/tests/integrations/slack/test_retry_idempotency.py index e520b8bcd..2cbc65939 100644 --- a/tests/integrations/slack/test_retry_idempotency.py +++ b/tests/integrations/slack/test_retry_idempotency.py @@ -17,8 +17,9 @@ import hmac import json import time +from types import SimpleNamespace from typing import Any -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, MagicMock import httpx import pytest @@ -26,6 +27,7 @@ from starlette.applications import Starlette from starlette.testclient import TestClient +from band.core.simple_adapter import SimpleAdapter from band.integrations.slack.server import ( DEFAULT_SEEN_EVENTS_CACHE_SIZE, SeenEvents, @@ -34,6 +36,7 @@ from band.integrations.slack.signature import SLACK_SIGNATURE_VERSION from band.integrations.slack.types import SlackApp from band.testing.platform import platform_connection_stub +from band.integrations.slack.adapter import SlackAdapter # ── Unit tests on SeenEvents ──────────────────────────────────────────────── @@ -287,11 +290,6 @@ async def test_full_pipeline_three_retries_one_brain_invocation(): """End-to-end via the full SlackAdapter wrapping shape: 3 retries must produce exactly one inner brain invocation and one Band room (not three).""" - from types import SimpleNamespace - from unittest.mock import MagicMock - - from band.core.simple_adapter import SimpleAdapter - from band.integrations.slack.adapter import SlackAdapter class _Brain(SimpleAdapter[Any]): def __init__(self) -> None: diff --git a/tests/integrations/slack/test_server.py b/tests/integrations/slack/test_server.py index d234456ec..fbaa984cd 100644 --- a/tests/integrations/slack/test_server.py +++ b/tests/integrations/slack/test_server.py @@ -6,14 +6,18 @@ import hmac import json import time +from typing import Any +from unittest.mock import MagicMock import pytest from starlette.applications import Starlette from starlette.testclient import TestClient +from band.core.simple_adapter import SimpleAdapter from band.integrations.slack.server import build_router from band.integrations.slack.signature import SLACK_SIGNATURE_VERSION from band.integrations.slack.types import SlackApp +from band.integrations.slack.adapter import SlackAdapter def _sign(secret: str, body: bytes, timestamp: str) -> str: @@ -247,11 +251,6 @@ async def boom(received_app, payload): def test_adapter_router_property_exposes_starlette_router(): - from typing import Any - from unittest.mock import MagicMock - - from band.core.simple_adapter import SimpleAdapter - from band.integrations.slack.adapter import SlackAdapter class _NoopInner(SimpleAdapter[Any]): async def on_message(self, *args: Any, **kwargs: Any) -> None: diff --git a/tests/integrations/slack/test_signature.py b/tests/integrations/slack/test_signature.py index a5a28022b..8875fc17b 100644 --- a/tests/integrations/slack/test_signature.py +++ b/tests/integrations/slack/test_signature.py @@ -4,6 +4,7 @@ import hashlib import hmac +import time import pytest @@ -201,8 +202,6 @@ def test_signature_uses_constant_time_compare(): def test_signature_uses_real_clock_when_now_not_passed(): # When no `now` is passed, the function reads time.time(); a fresh # signature with a current timestamp should verify. - import time - body = b"hello" timestamp = str(int(time.time())) signature = _sign(SIGNING_SECRET, body, timestamp) diff --git a/tests/integrations/slack/test_socket_transport.py b/tests/integrations/slack/test_socket_transport.py index 18ea8f33c..cfa10c7fb 100644 --- a/tests/integrations/slack/test_socket_transport.py +++ b/tests/integrations/slack/test_socket_transport.py @@ -25,6 +25,7 @@ from band.integrations.slack.dedup import SeenEvents from band.integrations.slack.socket import ( SlackSocketListener, + _make_request_handler, start_socket_listeners, ) from band.integrations.slack.types import SlackApp @@ -379,7 +380,6 @@ async def fake_start_socket_listeners( for app in apps: client = socket_clients[app.slug] # Build the real per-app handler. - from band.integrations.slack.socket import _make_request_handler client.socket_mode_request_listeners.append( _make_request_handler( @@ -416,7 +416,6 @@ async def test_socket_listener_drops_bot_events(monkeypatch): async def fake_start_socket_listeners( *, apps, web_client_factory, dispatcher, client_factory=None ): - from band.integrations.slack.socket import _make_request_handler for app in apps: fake.socket_mode_request_listeners.append( @@ -459,9 +458,6 @@ async def test_socket_listener_drops_duplicate_event_id(): Socket Mode can replay events across reconnects; like the HTTP route, the listener dedups on ``event_id`` so the brain isn't invoked twice. """ - from unittest.mock import AsyncMock - - from band.integrations.slack.socket import _make_request_handler dispatcher = AsyncMock() client = SimpleNamespace(send_socket_mode_response=AsyncMock()) diff --git a/tests/integrations/slack/test_wrapping.py b/tests/integrations/slack/test_wrapping.py index a5b3d559a..92a260db3 100644 --- a/tests/integrations/slack/test_wrapping.py +++ b/tests/integrations/slack/test_wrapping.py @@ -25,12 +25,13 @@ from datetime import datetime, timezone from types import SimpleNamespace from typing import Any -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest from httpx import ASGITransport +from band.core.exceptions import BandToolError from band.core.simple_adapter import SimpleAdapter from band.core.types import ( AdapterFeatures, @@ -48,7 +49,7 @@ ) from band.integrations.slack.signature import SLACK_SIGNATURE_VERSION from band.integrations.slack.types import SlackApp, SlackRoomBinding -from band.runtime.tools import AgentTools +from band.runtime.tools import AgentTools, ToolCallOutcome from band.testing.platform import platform_connection_stub @@ -961,9 +962,6 @@ async def test_execute_tool_call_delegates_non_slack_tools_to_super(): it delegates via ``execute_tool_call_structured`` and returns its ``value``. """ - from unittest.mock import patch - - from band.runtime.tools import ToolCallOutcome tools, _, _ = _make_tee_tools() super_mock = AsyncMock(return_value=ToolCallOutcome(value="ok", ok=True)) @@ -977,7 +975,6 @@ async def test_execute_tool_call_delegates_non_slack_tools_to_super(): @pytest.mark.asyncio async def test_send_message_no_longer_overridden_uses_real_band_path(): """The base AgentTools.send_message behavior is restored (mention required).""" - from band.core.exceptions import BandToolError tools, _, slack = _make_tee_tools() # Mentionless message hits the platform's "≥1 mention required" guard. diff --git a/tests/integrations/test_crewai_real_tools.py b/tests/integrations/test_crewai_real_tools.py index d520a5e28..37369ff4f 100644 --- a/tests/integrations/test_crewai_real_tools.py +++ b/tests/integrations/test_crewai_real_tools.py @@ -19,7 +19,7 @@ pytest.importorskip("crewai", reason="crewai not installed (band-sdk[crewai])") -from band.core.types import AdapterFeatures # noqa: E402 +from band.core.types import AdapterFeatures, Capability # noqa: E402 from band.integrations.crewai.tools import ( # noqa: E402 NoopReporter, build_band_crewai_tools, @@ -47,8 +47,6 @@ def test_platform_tools_build_against_the_real_base_tool() -> None: def test_file_tools_build_against_the_real_base_tool() -> None: """The three room-file tool models also resolve and instantiate for real -- same "not fully defined" failure mode this file exists to catch.""" - from band.core.types import Capability - tools = build_band_crewai_tools( get_context=lambda: None, reporter=NoopReporter(), diff --git a/tests/integrations/test_crewai_tools.py b/tests/integrations/test_crewai_tools.py index 73928e627..2657eaa52 100644 --- a/tests/integrations/test_crewai_tools.py +++ b/tests/integrations/test_crewai_tools.py @@ -14,6 +14,12 @@ from unittest.mock import AsyncMock, MagicMock import pytest +from pydantic import BaseModel, ValidationError + +from band.core.exceptions import BandToolError +from band.core.memory_types import memory_type_field_description +from band.core.types import AdapterFeatures, Capability, Emit +from band.runtime.tools import file_content_placeholder, image_block_placeholder class MockBaseTool: @@ -48,14 +54,12 @@ def crewai_mocks(monkeypatch): @pytest.fixture def builder_mod(crewai_mocks): - import importlib return importlib.import_module("band.integrations.crewai.tools") @pytest.fixture def runtime_mod(crewai_mocks): - import importlib return importlib.import_module("band.integrations.crewai.runtime") @@ -63,7 +67,6 @@ def runtime_mod(crewai_mocks): @pytest.fixture def platform_args_schemas(builder_mod): """Tool name -> the args schema CrewAI actually advertises to the LLM.""" - from band.core.types import Capability tools = builder_mod.build_band_crewai_tools( get_context=lambda: None, @@ -98,7 +101,6 @@ def test_base_tools_only(self, builder_mod): assert len(tools) == 7 def test_capability_contacts_adds_five(self, builder_mod): - from band.core.types import Capability tools = builder_mod.build_band_crewai_tools( get_context=lambda: None, @@ -117,7 +119,6 @@ def test_capability_contacts_adds_five(self, builder_mod): assert len(tools) == 12 def test_capability_memory_adds_five(self, builder_mod): - from band.core.types import Capability tools = builder_mod.build_band_crewai_tools( get_context=lambda: None, @@ -136,7 +137,6 @@ def test_capability_memory_adds_five(self, builder_mod): assert len(tools) == 12 def test_capability_files_adds_three(self, builder_mod): - from band.core.types import Capability tools = builder_mod.build_band_crewai_tools( get_context=lambda: None, @@ -153,7 +153,6 @@ def test_capability_files_adds_three(self, builder_mod): assert len(tools) == 10 def test_both_capabilities(self, builder_mod): - from band.core.types import Capability tools = builder_mod.build_band_crewai_tools( get_context=lambda: None, @@ -163,7 +162,6 @@ def test_both_capabilities(self, builder_mod): assert len(tools) == 17 # 7 base + 5 contacts + 5 memory def test_all_three_capabilities(self, builder_mod): - from band.core.types import Capability tools = builder_mod.build_band_crewai_tools( get_context=lambda: None, @@ -175,7 +173,6 @@ def test_all_three_capabilities(self, builder_mod): assert len(tools) == 20 # 7 base + 5 contacts + 5 memory + 3 files def test_custom_tools_appended(self, builder_mod): - from pydantic import BaseModel class MyInput(BaseModel): """My custom tool.""" @@ -195,7 +192,6 @@ async def my_handler(_: MyInput) -> str: assert len(tools) == 8 def test_adapter_feature_filters_apply_to_platform_tools(self, builder_mod): - from band.core.types import AdapterFeatures, Capability tools = builder_mod.build_band_crewai_tools( get_context=lambda: None, @@ -215,9 +211,6 @@ def test_adapter_feature_filters_apply_to_platform_tools(self, builder_mod): assert "band_archive_memory" not in names def test_adapter_feature_filters_only_apply_to_platform_tools(self, builder_mod): - from pydantic import BaseModel - - from band.core.types import AdapterFeatures class MyInput(BaseModel): value: str @@ -267,7 +260,6 @@ async def handler(_: BaseModel) -> str: def test_platform_tool_schemas_reject_invalid_values( self, platform_args_schemas, tool_name, payload ): - from pydantic import ValidationError with pytest.raises(ValidationError): platform_args_schemas[tool_name].model_validate(payload) @@ -320,7 +312,6 @@ def test_lookup_peers_reports_serialized_result_for_raw_model_return( json.dumps has no default=str, so an unserialized model previously raised inside report_result -- caught by its own try/except and only logged as a warning -- silently dropping the tool_result event.""" - from band.core.types import AdapterFeatures, Emit class FakePeersResponse: def __init__(self, data): @@ -448,7 +439,6 @@ def test_reply_tracker_not_marked_on_send_failure(self, builder_mod): def test_send_failure_appends_available_handles(self, builder_mod): """The real empty-mentions error already lists the room's handles, so the CrewAI enricher must surface them once — not append a second copy.""" - from band.core.exceptions import BandToolError tools_obj = MagicMock() tools_obj.agent_id = None @@ -487,7 +477,6 @@ def test_send_failure_appends_available_handles(self, builder_mod): def test_send_failure_excludes_agent_own_handle(self, builder_mod): """The agent's own handle is never offered as a retry option — an agent can't @mention itself, so listing it only misleads the LLM.""" - from band.core.exceptions import BandToolError tools_obj = MagicMock() tools_obj.agent_id = "self-2" @@ -521,7 +510,6 @@ def test_send_failure_excludes_agent_own_handle(self, builder_mod): class TestFileTools: def test_list_room_files_forwards_cursor(self, builder_mod): - from band.core.types import Capability tools_obj = MagicMock() tools_obj.list_room_files = AsyncMock( @@ -541,7 +529,6 @@ def test_list_room_files_forwards_cursor(self, builder_mod): tools_obj.list_room_files.assert_awaited_once_with("cursor-1") def test_list_room_files_default_cursor_is_none(self, builder_mod): - from band.core.types import Capability tools_obj = MagicMock() tools_obj.list_room_files = AsyncMock(return_value={"data": []}) @@ -558,7 +545,6 @@ def test_list_room_files_default_cursor_is_none(self, builder_mod): tools_obj.list_room_files.assert_awaited_once_with(None) def test_read_room_file_forwards_file_id(self, builder_mod): - from band.core.types import Capability tools_obj = MagicMock() tools_obj.read_room_file = AsyncMock( @@ -581,7 +567,6 @@ def test_read_room_file_image_result_becomes_vision_sentinel(self, builder_mod): """CrewAI's own StepExecutor rewrites a VISION_IMAGE:: tool-result string into a real image_url content block -- pin that band_read_room_file emits exactly that sentinel for an image result.""" - from band.core.types import Capability image_result = { "content": [{"type": "image", "data": "ZmFrZQ==", "mimeType": "image/png"}] @@ -605,8 +590,6 @@ def test_read_room_file_image_result_reports_placeholder_not_base64( ): """The full base64 sentinel must reach CrewAI's StepExecutor, but the platform tool_result event must not carry that same base64 blob.""" - from band.core.types import AdapterFeatures, Capability, Emit - from band.runtime.tools import image_block_placeholder image_result = { "content": [{"type": "image", "data": "ZmFrZQ==", "mimeType": "image/png"}] @@ -636,7 +619,6 @@ def test_read_room_file_image_result_reports_placeholder_not_base64( def test_send_room_file_forwards_args_in_protocol_order(self, builder_mod): """AgentToolsProtocol.send_room_file wants (content, filename, caption, mentions) positionally -- pin the reorder from the tool's own kwargs.""" - from band.core.types import Capability tools_obj = MagicMock() tools_obj.send_room_file = AsyncMock( @@ -667,7 +649,6 @@ def test_send_room_file_forwards_args_in_protocol_order(self, builder_mod): def test_send_room_file_mentions_accepts_lenient_string_shape(self, builder_mod): """Smaller models emit mentions as a JSON-string or bracketed string, same leniency need as band_send_message -- see normalize_mentions_lenient.""" - from band.core.types import Capability tools_obj = MagicMock() tools_obj.send_room_file = AsyncMock( @@ -694,8 +675,6 @@ def test_send_room_file_reports_content_placeholder_not_raw_bytes( ): """The full content must still reach send_room_file, but the tool_call event must not carry that same raw payload.""" - from band.core.types import AdapterFeatures, Capability, Emit - from band.runtime.tools import file_content_placeholder tools_obj = MagicMock() tools_obj.send_room_file = AsyncMock( @@ -730,7 +709,6 @@ def test_send_room_file_reports_content_placeholder_not_raw_bytes( assert content not in json.dumps(reported_args) def test_send_room_file_failure_returns_error_status(self, builder_mod): - from band.core.types import Capability tools_obj = MagicMock() tools_obj.send_room_file = AsyncMock(side_effect=RuntimeError("upload failed")) @@ -758,7 +736,6 @@ def test_send_room_file_failure_returns_error_status(self, builder_mod): class TestEmitToolCallsReporter: @pytest.mark.asyncio async def test_does_not_emit_when_tool_calls_unset(self, builder_mod): - from band.core.types import AdapterFeatures features = AdapterFeatures() # empty emit set reporter = builder_mod.EmitToolCallsReporter(features) @@ -772,7 +749,6 @@ async def test_does_not_emit_when_tool_calls_unset(self, builder_mod): @pytest.mark.asyncio async def test_emits_when_tool_calls_set(self, builder_mod): - from band.core.types import AdapterFeatures, Emit features = AdapterFeatures(emit=frozenset({Emit.TOOL_CALLS})) reporter = builder_mod.EmitToolCallsReporter(features) @@ -795,7 +771,6 @@ async def test_emits_canonical_event_schema(self, builder_mod): were silently dropped on read. Pin the schema here so a count-only assertion can't let that drift back in. """ - from band.core.types import AdapterFeatures, Emit features = AdapterFeatures(emit=frozenset({Emit.TOOL_CALLS})) reporter = builder_mod.EmitToolCallsReporter(features) @@ -822,7 +797,6 @@ async def test_emits_canonical_event_schema(self, builder_mod): @pytest.mark.asyncio async def test_error_result_sets_is_error(self, builder_mod): - from band.core.types import AdapterFeatures, Emit features = AdapterFeatures(emit=frozenset({Emit.TOOL_CALLS})) reporter = builder_mod.EmitToolCallsReporter(features) @@ -839,7 +813,6 @@ async def test_error_result_sets_is_error(self, builder_mod): @pytest.mark.asyncio async def test_send_event_failure_does_not_propagate(self, builder_mod): - from band.core.types import AdapterFeatures, Emit features = AdapterFeatures(emit=frozenset({Emit.TOOL_CALLS})) reporter = builder_mod.EmitToolCallsReporter(features) @@ -905,7 +878,6 @@ class TestStoreMemoryArgsSchema: """CrewAI advertises the master model, so master text and validators apply.""" def test_type_description_comes_from_master(self, platform_args_schemas) -> None: - from band.core.memory_types import memory_type_field_description schema = platform_args_schemas["band_store_memory"] assert ( @@ -915,7 +887,6 @@ def test_type_description_comes_from_master(self, platform_args_schemas) -> None def test_rejects_subject_scope_without_subject_id( self, platform_args_schemas ) -> None: - from pydantic import ValidationError with pytest.raises(ValidationError, match="requires a subject_id"): platform_args_schemas["band_store_memory"].model_validate( @@ -930,7 +901,6 @@ def test_rejects_subject_scope_without_subject_id( ) def test_rejects_type_for_wrong_system(self, platform_args_schemas) -> None: - from pydantic import ValidationError with pytest.raises( ValidationError, match="type `semantic` is not valid for system `sensory`" diff --git a/tests/markdown_docs/fixtures.py b/tests/markdown_docs/fixtures.py index 7f848cec1..4604ff162 100644 --- a/tests/markdown_docs/fixtures.py +++ b/tests/markdown_docs/fixtures.py @@ -10,6 +10,9 @@ import pytest +from band import Agent +from band.client.rest import AsyncRestClient +from band.config import loader from tests.markdown_docs.globals import ( MARKDOWN_AGENT_ID, MARKDOWN_API_KEY, @@ -77,8 +80,6 @@ def _seed_markdown_env(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.fixture def client(monkeypatch: pytest.MonkeyPatch): """Back `fixture:client` snippets with a generated client and fake HTTP.""" - from band.client.rest import AsyncRestClient - # Use the generated client so docs fail if Fern namespaces drift. rest_client = AsyncRestClient( api_key=MARKDOWN_API_KEY, @@ -120,8 +121,6 @@ def noop_run(coro: object) -> None: @pytest.fixture def agent_config_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: """Back `fixture:agent_config_path` snippets with temporary credentials.""" - from band import Agent - from band.config import loader async def run_noop(self: Agent) -> None: return None diff --git a/tests/markdown_docs/globals.py b/tests/markdown_docs/globals.py index 6ad8b1fd2..05bb5c65b 100644 --- a/tests/markdown_docs/globals.py +++ b/tests/markdown_docs/globals.py @@ -65,7 +65,7 @@ def create_calculator_graph() -> MarkdownCalculatorGraph: def _try_lazy_adapter(name: str) -> type | None: """Import an adapter via ``band.adapters`` lazy loader when extras are installed.""" try: - import band.adapters as adapters_mod + import band.adapters as adapters_mod # noqa: PLC0415 return getattr(adapters_mod, name) except (ImportError, ModuleNotFoundError, AttributeError): @@ -90,7 +90,7 @@ def _sdk_symbols() -> dict[str, object]: symbols[adapter_name] = adapter_cls try: - from band.adapters.codex import CodexAdapter, CodexAdapterConfig + from band.adapters.codex import CodexAdapter, CodexAdapterConfig # noqa: PLC0415 symbols["CodexAdapter"] = CodexAdapter symbols["CodexAdapterConfig"] = CodexAdapterConfig diff --git a/tests/mcp/conftest.py b/tests/mcp/conftest.py index cb4da53d1..f61f6091e 100644 --- a/tests/mcp/conftest.py +++ b/tests/mcp/conftest.py @@ -10,6 +10,7 @@ from typing import Any import pytest +from band_rest import RestClient from mcp import ClientSession @@ -22,8 +23,6 @@ def _assert_no_method_name_collisions() -> None: group ever share a method name, such a test would silently pass with the wrong assertion instead of failing loudly. """ - from band_rest import RestClient - try: client = RestClient(api_key="dummy", base_url="http://localhost") except Exception as exc: diff --git a/tests/mcp/test_engine.py b/tests/mcp/test_engine.py index 7230781c1..f4f46f7d6 100644 --- a/tests/mcp/test_engine.py +++ b/tests/mcp/test_engine.py @@ -38,6 +38,7 @@ from band_mcp.server import standalone_spec from band_mcp.shared import AGENT_TOOLS_CACHE_MAX_SIZE, StandaloneResolver from tests.mcp.conftest import FakeHumanTools +from mcp.server.transport_security import TransportSecuritySettings async def _list_tool(session: ClientSession, name: str) -> Any: @@ -98,7 +99,6 @@ def test_non_loopback_host_does_not_get_loopback_only_protection(self) -> None: assert mcp.settings.transport_security is None def test_explicit_transport_security_overrides_host_auto_detection(self) -> None: - from mcp.server.transport_security import TransportSecuritySettings explicit = TransportSecuritySettings( enable_dns_rebinding_protection=True, diff --git a/tests/platform/test_link_control.py b/tests/platform/test_link_control.py index df1ab9f78..3c5c7df0e 100644 --- a/tests/platform/test_link_control.py +++ b/tests/platform/test_link_control.py @@ -11,6 +11,7 @@ import pytest +import band.platform.link as link_mod from band.client.streaming import AgentControlPayload from band.platform.link import BandLink @@ -70,8 +71,6 @@ async def fake_factory(*args, **kwargs): return fake_ws # Patch WebSocketClient construction to return our fake. - import band.platform.link as link_mod - orig = link_mod.WebSocketClient link_mod.WebSocketClient = lambda *a, **k: fake_ws # type: ignore[assignment] try: diff --git a/tests/runtime/test_contact_handler.py b/tests/runtime/test_contact_handler.py index f3a7700a5..3ce4fa6cc 100644 --- a/tests/runtime/test_contact_handler.py +++ b/tests/runtime/test_contact_handler.py @@ -1,5 +1,6 @@ """Tests for ContactEventHandler.""" +import asyncio from unittest.mock import AsyncMock, MagicMock import pytest @@ -9,6 +10,7 @@ ContactRequestUpdatedEvent, ContactAddedEvent, ContactRemovedEvent, + MessageEvent, ) from band.client.streaming import ( ContactRequestReceivedPayload, @@ -16,7 +18,11 @@ ContactAddedPayload, ContactRemovedPayload, ) -from band.runtime.contact_handler import ContactEventHandler, MAX_DEDUP_CACHE_SIZE +from band.runtime.contact_handler import ( + ContactEventHandler, + HUB_ROOM_SYSTEM_PROMPT, + MAX_DEDUP_CACHE_SIZE, +) from band.runtime.contact_tools import ContactTools from band.runtime.types import ContactEventConfig, ContactEventStrategy @@ -497,7 +503,6 @@ async def test_hub_room_reuses_existing_room( async def test_hub_room_thread_safe(self, mock_hub_link, mock_hub_event_callback): """Concurrent events should reuse the same hub room.""" - import asyncio config = ContactEventConfig(strategy=ContactEventStrategy.HUB_ROOM) handler = ContactEventHandler( @@ -553,7 +558,6 @@ async def test_hub_room_injects_message_event( self, mock_hub_link, mock_hub_event_callback, sample_request_received_event ): """Events should be injected as MessageEvent with type 'text'.""" - from band.platform.event import MessageEvent config = ContactEventConfig(strategy=ContactEventStrategy.HUB_ROOM) handler = ContactEventHandler( @@ -758,7 +762,6 @@ async def test_hub_room_injects_system_prompt_on_first_event( self, mock_hub_link, mock_hub_event_callback, sample_request_received_event ): """System prompt should be injected on first event only.""" - from band.runtime.contact_handler import HUB_ROOM_SYSTEM_PROMPT mock_hub_init_callback = AsyncMock() @@ -792,7 +795,6 @@ async def test_hub_room_injects_system_prompt_on_first_event( async def test_hub_room_system_prompt_contains_instructions(self): """System prompt should contain contact management instructions.""" - from band.runtime.contact_handler import HUB_ROOM_SYSTEM_PROMPT # Verify key instructions are present assert "contact requests" in HUB_ROOM_SYSTEM_PROMPT.lower() diff --git a/tests/runtime/test_execution.py b/tests/runtime/test_execution.py index f77242bdf..f3970f2f1 100644 --- a/tests/runtime/test_execution.py +++ b/tests/runtime/test_execution.py @@ -10,6 +10,7 @@ from band_sdk_core import ClaimRegistry, RetryTracker +from band.client.streaming import MessageMetadata from band.logging_config import TRACE_CONTEXT, trace_context_scope from band.runtime.execution import ( Execution, @@ -18,7 +19,7 @@ BacklogProcessResult, _error_label, ) -from band.runtime.types import ConversationContext, SessionConfig +from band.runtime.types import ConversationContext, PlatformMessage, SessionConfig # Import test helpers from conftest from tests.conftest import ( @@ -689,8 +690,6 @@ async def test_sync_processes_backlog_messages( self, mock_link_with_next, mock_handler ): """Sync should process backlog messages from /next.""" - from datetime import datetime, timezone - from band.runtime.types import PlatformMessage # Setup get_next_message to return one backlog message, then None backlog_msg = PlatformMessage( @@ -731,8 +730,6 @@ async def test_sync_point_clears_marker_and_keeps_dedupe_cache( self, mock_link_with_next, mock_handler ): """When sync point is reached, marker is cleared and dedupe is preserved.""" - from datetime import datetime, timezone - from band.runtime.types import PlatformMessage # Setup: WS message arrives, then /next returns same message sync_msg = PlatformMessage( @@ -776,8 +773,6 @@ async def test_sync_removes_duplicate_from_ws_queue( self, mock_link_with_next, mock_handler ): """Sync should dedupe when non-message events are ahead of sync-point WS copy.""" - from datetime import datetime, timezone - from band.runtime.types import PlatformMessage sync_msg = PlatformMessage( id="msg-sync-001", @@ -851,8 +846,6 @@ async def test_ws_replay_with_processed_metadata_is_not_reopened( self, mock_link_with_next, mock_handler ): """Processed WebSocket replay should not call mark_processing or execute.""" - from band.client.streaming import MessageMetadata - ctx = ExecutionContext( "room-123", mock_link_with_next, @@ -923,7 +916,6 @@ async def test_pending_next_message_present_in_context_still_executes( self, mock_link_with_next, mock_handler ): """A pending /next message is work even when it appears in room context.""" - from band.runtime.types import PlatformMessage pending_msg = PlatformMessage( id="msg-pending-down", @@ -981,7 +973,6 @@ async def test_same_id_backlog_and_ws_paths_are_locally_inflight_deduped( self, mock_link_with_next, mock_handler ): """Only one path should execute when /next and WebSocket race on an id.""" - from band.runtime.types import PlatformMessage processing_started = asyncio.Event() release_processing = asyncio.Event() @@ -1036,7 +1027,6 @@ async def test_first_message_to_fresh_room_executes_once(self, mock_link_with_ne WebSocket copy arrives while that execution is still in flight. The second delivery must be deduplicated, not re-executed. """ - from band.runtime.types import PlatformMessage handler_started = asyncio.Event() release_handler = asyncio.Event() @@ -1186,7 +1176,6 @@ async def test_backlog_processed_ack_failure_is_not_remembered( self, mock_link_with_next, mock_handler ): """Local success without durable processed ack must not enter processed dedupe.""" - from band.runtime.types import PlatformMessage msg = PlatformMessage( id="msg-ack-fails", @@ -1246,7 +1235,6 @@ async def test_backlog_processed_ack_failure_retries_ack_without_handler_replay( self, mock_link_with_next, mock_handler ): """Redelivery after local success should retry only the processed ack.""" - from band.runtime.types import PlatformMessage msg = PlatformMessage( id="msg-backlog-ack-retry", @@ -1282,7 +1270,6 @@ async def test_processed_ack_retry_budget_exhaustion_keeps_local_completion( self, mock_link_with_next, mock_handler ): """Permanent processed ack failure should not deadlock or replay local side effects.""" - from band.runtime.types import PlatformMessage msg = PlatformMessage( id="msg-ack-budget", @@ -1423,7 +1410,6 @@ async def test_resync_retries_pending_ack_before_advancing_to_newer_backlog( before processing a newer /next backlog message -- normal resync cannot get past a stuck pending ACK to reach newer backlog. Once the ACK confirms, resync proceeds normally to the newer message.""" - from band.runtime.types import PlatformMessage newer_msg = PlatformMessage( id="msg-newer", @@ -1465,7 +1451,6 @@ async def test_sync_point_claim_failure_does_not_clear_marker( self, mock_link_with_next, mock_handler ): """A failed durable claim is not a completed sync point.""" - from band.runtime.types import PlatformMessage sync_msg = PlatformMessage( id="msg-sync-claim-fails", @@ -1503,7 +1488,6 @@ async def test_startup_backlog_claim_failure_does_not_spin( self, mock_link_with_next, mock_handler ): """Startup sync should stop after one unclaimable non-sync backlog message.""" - from band.runtime.types import PlatformMessage msg = PlatformMessage( id="msg-startup-claim-fails", @@ -1540,7 +1524,6 @@ async def test_startup_backlog_claim_failure_does_not_process_newer_ws_event( self, mock_link_with_next, mock_handler ): """Startup sync should not switch to WebSocket after an unclaimable backlog message.""" - from band.runtime.types import PlatformMessage backlog_msg = PlatformMessage( id="msg-older-claim-fails", @@ -1584,7 +1567,6 @@ async def test_resync_claim_failure_does_not_spin( self, mock_link_with_next, mock_handler ): """Resync should stop after one unclaimable /next message.""" - from band.runtime.types import PlatformMessage msg = PlatformMessage( id="msg-resync-claim-fails", @@ -1620,7 +1602,6 @@ async def test_resync_claim_failure_does_not_process_newer_ws_event( self, mock_link_with_next, mock_handler ): """Phase 2 resync should block queued WebSocket events behind older /next work.""" - from band.runtime.types import PlatformMessage msg = PlatformMessage( id="msg-resync-older-claim-fails", @@ -1665,8 +1646,6 @@ async def test_sync_skips_permanently_failed( self, mock_link_with_next, mock_handler ): """Sync should skip permanently failed messages.""" - from datetime import datetime, timezone - from band.runtime.types import PlatformMessage failed_msg = PlatformMessage( id="msg-failed-001", @@ -1702,8 +1681,6 @@ async def test_sync_skips_permanently_failed( async def test_retry_tracker_records_failures(self, mock_link_with_next): """Retry tracker should record failed processing attempts.""" - from datetime import datetime, timezone - from band.runtime.types import PlatformMessage # Handler that fails failing_handler = AsyncMock(side_effect=Exception("Processing failed")) @@ -1743,7 +1720,6 @@ async def test_retry_saturation_skips_handler_on_next_delivery( """Once a message's attempts exceed max_retries it becomes permanently failed, and a *subsequent* delivery of that same message must skip the handler entirely rather than invoke it again.""" - from band.runtime.types import PlatformMessage failing_handler = AsyncMock(side_effect=Exception("Processing failed")) msg = PlatformMessage( diff --git a/tests/runtime/test_hub_room_auto_enable.py b/tests/runtime/test_hub_room_auto_enable.py index 396fae84b..7fe0ee9ed 100644 --- a/tests/runtime/test_hub_room_auto_enable.py +++ b/tests/runtime/test_hub_room_auto_enable.py @@ -14,9 +14,12 @@ import pytest from band.core.types import Capability +from band.runtime.execution import ExecutionContext +from band.runtime.runtime import AgentRuntime from band.runtime.tools import ( AgentTools, CONTACT_TOOL_NAMES, + MEMORY_TOOL_NAMES, iter_tool_definitions, ) @@ -42,8 +45,6 @@ def test_memory_and_contacts_independent(self) -> None: names = {d.name for d in defs} assert names.isdisjoint(CONTACT_TOOL_NAMES) # Memory tools present - from band.runtime.tools import MEMORY_TOOL_NAMES - assert MEMORY_TOOL_NAMES.issubset(names) @@ -96,9 +97,6 @@ def test_default_include_contacts_true_in_normal_room(self, mock_rest) -> None: class TestRuntimeHubRoomWiring: def test_set_hub_room_id_propagates_to_new_executions(self) -> None: """AgentRuntime.set_hub_room_id is forwarded to ExecutionContext.""" - from band.runtime.execution import ExecutionContext - from band.runtime.runtime import AgentRuntime - link = MagicMock() link.rest = MagicMock() on_execute = AsyncMock() diff --git a/tests/runtime/test_human_tools.py b/tests/runtime/test_human_tools.py index 7f79e489b..eaad3dbbc 100644 --- a/tests/runtime/test_human_tools.py +++ b/tests/runtime/test_human_tools.py @@ -30,9 +30,19 @@ import httpx import pytest -from band_rest import AsyncRestClient - -from band.client.rest import DEFAULT_REQUEST_OPTIONS, ParsingError +from band_rest import ( + AgentRegisterRequest, + AsyncRestClient, + CreateContactRequestRequestContactRequest, + CreateMyChatRoomRequestChat, +) + +from band.client.rest import ( + DEFAULT_REQUEST_OPTIONS, + ChatMessageRequest, + ParsingError, + ParticipantRequest, +) from band.runtime.tools import HumanTools @@ -75,7 +85,6 @@ async def test_list_my_agents_forwards_page_args() -> None: @pytest.mark.asyncio async def test_register_my_agent_builds_request_object() -> None: - from band_rest import AgentRegisterRequest rest = _make_rest_fake() response = MagicMock() @@ -109,7 +118,6 @@ async def test_list_my_chats_forwards_pagination() -> None: @pytest.mark.asyncio async def test_create_my_chat_room_with_task_id() -> None: - from band_rest import CreateMyChatRoomRequestChat rest = _make_rest_fake() rest.human_api_chats.create_my_chat_room = AsyncMock(return_value=MagicMock()) @@ -123,7 +131,6 @@ async def test_create_my_chat_room_with_task_id() -> None: @pytest.mark.asyncio async def test_create_my_chat_room_without_task_id() -> None: - from band_rest import CreateMyChatRoomRequestChat rest = _make_rest_fake() rest.human_api_chats.create_my_chat_room = AsyncMock(return_value=MagicMock()) @@ -161,7 +168,6 @@ async def test_list_my_contacts_forwards_pagination() -> None: @pytest.mark.asyncio async def test_create_contact_request_without_message() -> None: - from band_rest import CreateContactRequestRequestContactRequest rest = _make_rest_fake() rest.human_api_contacts.create_contact_request = AsyncMock(return_value=MagicMock()) @@ -178,7 +184,6 @@ async def test_create_contact_request_without_message() -> None: @pytest.mark.asyncio async def test_create_contact_request_with_message() -> None: - from band_rest import CreateContactRequestRequestContactRequest rest = _make_rest_fake() rest.human_api_contacts.create_contact_request = AsyncMock(return_value=MagicMock()) @@ -413,7 +418,6 @@ def _mk_participant(**kwargs: Any) -> MagicMock: @pytest.mark.asyncio async def test_send_my_chat_message_resolves_recipients_by_name() -> None: - from band_rest import ChatMessageRequest rest = _make_rest_fake() alice = _mk_participant(id="u-1", name="Alice") @@ -494,7 +498,6 @@ async def test_list_my_chat_participants_forwards_filter() -> None: @pytest.mark.asyncio async def test_add_my_chat_participant_builds_request_with_default_role() -> None: - from band_rest import ParticipantRequest rest = _make_rest_fake() rest.human_api_participants.add_my_chat_participant = AsyncMock( diff --git a/tests/runtime/test_resync.py b/tests/runtime/test_resync.py index 25b2d53a9..cb42e697d 100644 --- a/tests/runtime/test_resync.py +++ b/tests/runtime/test_resync.py @@ -23,6 +23,7 @@ from band.runtime.presence import RoomPresence from band.runtime.runtime import AgentRuntime from band.runtime.types import PlatformMessage, SessionConfig +from tests.conftest import make_message_event from tests.runtime.conftest import admit_room @@ -189,8 +190,6 @@ async def test_idle_timeout_does_not_fire_when_events_arrive( self, mock_link, mock_handler ): """If events arrive before timeout, resync should not add extra /next calls.""" - from tests.conftest import make_message_event - config = SessionConfig(idle_resync_seconds=60) # very long timeout ctx = ExecutionContext("room-1", mock_link, mock_handler, config=config) await ctx.start() diff --git a/tests/runtime/test_runtime_control.py b/tests/runtime/test_runtime_control.py index b125325bc..122fb2ac9 100644 --- a/tests/runtime/test_runtime_control.py +++ b/tests/runtime/test_runtime_control.py @@ -7,7 +7,10 @@ import pytest from band.client.streaming import AgentControlPayload +from band.platform.event import ReconnectedEvent +from band.runtime.execution import ExecutionContext from band.runtime.runtime import AgentRuntime +from band.runtime.types import PlatformMessage def _control(mode: str, scope: str = "agent", **kw) -> AgentControlPayload: @@ -136,9 +139,6 @@ async def test_reconnect_while_stopped_does_not_invoke_adapter(self): Asserts the adapter is NOT invoked (covers both the no-persistence claim and the recovery-sweep guard), per architect's Step-4 should-fix. """ - from band.platform.event import ReconnectedEvent - from band.runtime.execution import ExecutionContext - link = MagicMock() link.agent_id = "agent-123" link.rest = MagicMock() @@ -157,8 +157,6 @@ async def test_reconnect_while_stopped_does_not_invoke_adapter(self): # is platform-authoritative. The LOCAL risk is the recovery sweep, which # fetches 'processing' messages DIRECTLY (bypassing /next): the stop path # leaves the interrupted message there. The _stopped guard must skip it. - from band.runtime.types import PlatformMessage - stuck = PlatformMessage( id="stuck", room_id="room-123", diff --git a/tests/runtime/test_tool_definitions.py b/tests/runtime/test_tool_definitions.py index f0bdb1f80..1529b1dfc 100644 --- a/tests/runtime/test_tool_definitions.py +++ b/tests/runtime/test_tool_definitions.py @@ -9,7 +9,7 @@ """ import pytest -from pydantic import ValidationError +from pydantic import BaseModel, Field, ValidationError, field_validator from band.runtime.tools import ( TOOL_MODELS, @@ -250,7 +250,6 @@ def test_whitespace_only_description_is_omitted(self, monkeypatch): an empty result, which would crash any adapter importing this tool's schema. """ - from pydantic import BaseModel, Field class ProbeInput(BaseModel): """Probe tool for a whitespace-only field description.""" @@ -280,8 +279,6 @@ def test_returns_master_model_unchanged_without_validators(self): assert platform_args_schema("band_send_message") is SendMessageInput def test_subclass_keeps_master_description_and_field_text(self): - from pydantic import field_validator - schema = platform_args_schema( "band_send_message", validators={ diff --git a/tests/runtime/test_tool_definitions_surface.py b/tests/runtime/test_tool_definitions_surface.py index 5c2d8f791..cd48d981e 100644 --- a/tests/runtime/test_tool_definitions_surface.py +++ b/tests/runtime/test_tool_definitions_surface.py @@ -17,6 +17,7 @@ from __future__ import annotations +import logging from unittest.mock import MagicMock import pytest @@ -332,8 +333,6 @@ def test_build_registrations_filters_non_agent_definitions( entries (they'd ``AttributeError`` on ``AgentTools`` at call time) and logs a warning per dropped entry. """ - import logging - agent_tools = MagicMock(spec=AgentTools) mixed = [ TOOL_DEFINITIONS["band_send_message"], @@ -367,7 +366,6 @@ def test_build_resolved_registrations_filters_non_agent_definitions( ) -> None: """Resolved variant applies the same defense-in-depth filter. This is the path the opencode/claude_sdk/acp adapters exercise.""" - import logging def _resolver(_room_id: str): return None diff --git a/tests/runtime/test_tools.py b/tests/runtime/test_tools.py index 46b22aa2c..94894cec0 100644 --- a/tests/runtime/test_tools.py +++ b/tests/runtime/test_tools.py @@ -42,6 +42,7 @@ SendEventInput, StoreMemoryInput, AddParticipantInput, + RemoveParticipantInput, LookupPeersInput, GetParticipantsInput, CreateChatroomInput, @@ -1249,7 +1250,6 @@ async def test_add_participant_persists_across_recreated_tools( Uses real ExecutionContext — its ``participants`` property returns a copy, so without the _ctx backref the mutation would be lost. """ - from band.runtime.execution import ExecutionContext ctx = ExecutionContext( room_id="room-789", @@ -1283,7 +1283,6 @@ async def test_remove_participant_persists_across_recreated_tools( Uses real ExecutionContext — its ``participants`` property returns a copy, so without the _ctx backref the removal would be lost. """ - from band.runtime.execution import ExecutionContext participant = { "id": "user-1", @@ -1368,7 +1367,6 @@ async def test_send_message_empty_mentions_excludes_self( ): """The empty-mentions error lists other participants but not the agent itself — an agent can't @mention itself.""" - from band.core.exceptions import BandToolError tools = AgentTools( "room-123", mock_rest_client, participants, agent_id="user-2" @@ -1753,7 +1751,6 @@ async def test_get_participants_syncs_roster_to_ctx(self, mock_rest_client): """get_participants() must make the ctx roster follow the REST list — stale entries drop (even ctx-only ones this AgentTools never saw) and new ones appear — so the refresh survives turn boundaries.""" - from band.runtime.execution import ExecutionContext ctx = ExecutionContext( room_id="room-123", @@ -2057,7 +2054,6 @@ async def test_raises_error_with_participant_names( self, mock_rest_client, participants ): """Should raise BandToolError listing available participants when mentions empty.""" - from band.core.exceptions import BandToolError tools = AgentTools("room-123", mock_rest_client, participants) @@ -2075,7 +2071,6 @@ async def test_raises_error_when_mentions_none( self, mock_rest_client, participants ): """Should raise BandToolError when mentions is None.""" - from band.core.exceptions import BandToolError tools = AgentTools("room-123", mock_rest_client, participants) @@ -2084,7 +2079,6 @@ async def test_raises_error_when_mentions_none( async def test_uses_handle_when_available(self, mock_rest_client): """Should prefer handle over name in error message.""" - from band.core.exceptions import BandToolError participants = [ {"id": "user-1", "name": "User One", "type": "User", "handle": "@user-one"}, @@ -2096,7 +2090,6 @@ async def test_uses_handle_when_available(self, mock_rest_client): async def test_omits_participant_without_handle(self, mock_rest_client): """Should omit handle-less participants — they can't be @mentioned.""" - from band.core.exceptions import BandToolError participants = [ {"id": "user-1", "name": "User One", "type": "User"}, @@ -2130,7 +2123,6 @@ async def test_execute_tool_call_raises_band_tool_error( self, mock_rest_client, participants ): """execute_tool_call lets BandToolError propagate for wrapper translation.""" - from band.core.exceptions import BandToolError tools = AgentTools("room-123", mock_rest_client, participants) @@ -2322,7 +2314,6 @@ def test_add_participant_input_accepts_legacy_name_field(self): def test_remove_participant_input_accepts_legacy_name_field(self): """RemoveParticipantInput should accept 'name' as alias for backward compat.""" - from band.runtime.tools import RemoveParticipantInput model = RemoveParticipantInput.model_validate({"name": "User One"}) assert model.identifier == "User One" diff --git a/tests/skills/bughunting/test_runner.py b/tests/skills/bughunting/test_runner.py index 099661792..9b89b8db4 100644 --- a/tests/skills/bughunting/test_runner.py +++ b/tests/skills/bughunting/test_runner.py @@ -29,6 +29,7 @@ from band.client.streaming import MessageCreatedPayload +import tests.e2e.baseline.toolkit.capture as capture_module from tests.e2e.baseline.toolkit.observations.replies import Replies from tests.e2e.baseline.toolkit.observations.tool_calls import ToolCall, ToolCalls from tests.paths import REPO_ROOT @@ -183,7 +184,6 @@ def running_example( def install_capture(monkeypatch: pytest.MonkeyPatch, capture: Capture) -> None: """Make the runner's late-imported ``reply_capture`` yield ``capture``.""" - import tests.e2e.baseline.toolkit.capture as capture_module @asynccontextmanager async def factory(*args: Any, **kwargs: Any) -> AsyncIterator[Capture]: diff --git a/tests/test_agent.py b/tests/test_agent.py index 2f4c3a641..bc4e17c65 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -1,14 +1,22 @@ """Tests for Agent compositor.""" +from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock, patch import pytest from band.agent import Agent, DEFAULT_SHUTDOWN_TIMEOUT +from band.client.streaming import ( + MessageCreatedPayload, + MessageMetadata, + ParticipantAddedPayload, + RoomAddedPayload, +) from band.core.simple_adapter import SimpleAdapter from band.core.types import AdapterFeatures, AgentInput, Capability +from band.platform.event import MessageEvent, ParticipantAddedEvent, RoomAddedEvent from band.runtime.capabilities import FeatureFlag -from band.runtime.types import AgentConfig, SessionConfig +from band.runtime.types import AgentConfig, ConversationContext, SessionConfig from band.preprocessing.default import DefaultPreprocessor from band.testing.platform import platform_connection_stub @@ -477,8 +485,6 @@ async def test_default_preprocessor_filters_non_message_events( agent = Agent(runtime=mock_runtime, adapter=mock_adapter) # Create a non-MessageEvent (e.g., RoomAddedEvent) - from band.platform.event import RoomAddedEvent - from band.client.streaming import RoomAddedPayload mock_ctx = MagicMock() mock_event = RoomAddedEvent( @@ -503,9 +509,6 @@ async def test_default_preprocessor_filters_participant_events( """Participant events should not become adapter execution turns.""" agent = Agent(runtime=mock_runtime, adapter=mock_adapter) - from band.client.streaming import ParticipantAddedPayload - from band.platform.event import ParticipantAddedEvent - mock_ctx = MagicMock() mock_event = ParticipantAddedEvent( room_id="room-123", @@ -531,10 +534,6 @@ class TestStartupRaceCondition: @pytest.mark.asyncio async def test_adapter_on_started_before_first_message(self): """System prompt must be set before any message processing.""" - from band.client.streaming import MessageCreatedPayload, MessageMetadata - from band.platform.event import MessageEvent - from band.runtime.types import ConversationContext - from datetime import datetime, timezone # Track the order of calls call_order = [] diff --git a/tests/test_band_import.py b/tests/test_band_import.py index 6be3bdbfd..23ddcd341 100644 --- a/tests/test_band_import.py +++ b/tests/test_band_import.py @@ -4,7 +4,7 @@ def test_band_import_surface_exposes_agent_and_link() -> None: - from band import ( + from band import ( # noqa: PLC0415 -- pins the exact import path this test exercises Agent, BandLink, LogLevel, @@ -36,18 +36,18 @@ def test_legacy_root_package_is_not_available() -> None: def test_band_submodule_imports_use_band_modules() -> None: - import band.adapters - import band.integrations.acp + import band.adapters # noqa: PLC0415 -- pins the exact import path this test exercises + import band.integrations.acp # noqa: PLC0415 -- pins the exact import path this test exercises assert band.adapters.__name__ == "band.adapters" assert band.integrations.acp.__name__ == "band.integrations.acp" def test_acp_facades_expose_band_names_only() -> None: - import band.adapters as adapters - import band.integrations.acp as acp - from band.adapters import BandACPServerAdapter as BandAdapterFacade - from band.integrations.acp import BandACPClient, BandACPServerAdapter + import band.adapters as adapters # noqa: PLC0415 -- pins the exact import path this test exercises + import band.integrations.acp as acp # noqa: PLC0415 -- pins the exact import path this test exercises + from band.adapters import BandACPServerAdapter as BandAdapterFacade # noqa: PLC0415 -- pins the exact import path this test exercises + from band.integrations.acp import BandACPClient, BandACPServerAdapter # noqa: PLC0415 -- pins the exact import path this test exercises legacy_prefix = "Then" + "voi" @@ -59,8 +59,8 @@ def test_acp_facades_expose_band_names_only() -> None: def test_mcp_facade_exposes_band_backend_names_only() -> None: - import band.integrations.mcp as mcp - from band.integrations.mcp import BandMCPBackend, BandMCPBackendKind + import band.integrations.mcp as mcp # noqa: PLC0415 -- pins the exact import path this test exercises + from band.integrations.mcp import BandMCPBackend, BandMCPBackendKind # noqa: PLC0415 -- pins the exact import path this test exercises legacy_prefix = "Then" + "voi" diff --git a/tests/test_capability_gating_e2e.py b/tests/test_capability_gating_e2e.py index afdd18f6b..e042a6b54 100644 --- a/tests/test_capability_gating_e2e.py +++ b/tests/test_capability_gating_e2e.py @@ -13,6 +13,9 @@ from __future__ import annotations +import os +from unittest.mock import MagicMock, patch + import pytest from band.adapters.claude_sdk import _CLAUDE_SDK_AVAILABLE as _HAS_CLAUDE_SDK @@ -29,7 +32,7 @@ @pytest.mark.asyncio class TestCapabilityGatingEndToEnd: async def test_anthropic_adapter_renders_memory_section_when_enabled(self) -> None: - from band.adapters.anthropic import AnthropicAdapter + from band.adapters.anthropic import AnthropicAdapter # noqa: PLC0415 -- isolates the anthropic extra from the other frameworks this file tests adapter = AnthropicAdapter(capabilities={Capability.MEMORY}) await adapter.on_started("test-agent", "A test agent") @@ -38,7 +41,7 @@ async def test_anthropic_adapter_renders_memory_section_when_enabled(self) -> No assert "band_store_memory" in adapter._system_prompt async def test_anthropic_adapter_omits_memory_section_when_disabled(self) -> None: - from band.adapters.anthropic import AnthropicAdapter + from band.adapters.anthropic import AnthropicAdapter # noqa: PLC0415 -- isolates the anthropic extra from the other frameworks this file tests adapter = AnthropicAdapter() await adapter.on_started("test-agent", "A test agent") @@ -48,7 +51,7 @@ async def test_anthropic_adapter_omits_memory_section_when_disabled(self) -> Non async def test_anthropic_adapter_renders_contacts_section_when_enabled( self, ) -> None: - from band.adapters.anthropic import AnthropicAdapter + from band.adapters.anthropic import AnthropicAdapter # noqa: PLC0415 -- isolates the anthropic extra from the other frameworks this file tests adapter = AnthropicAdapter(capabilities={Capability.CONTACTS}) await adapter.on_started("test-agent", "A test agent") @@ -58,7 +61,7 @@ async def test_anthropic_adapter_renders_contacts_section_when_enabled( async def test_anthropic_adapter_renders_both_sections_when_both_enabled( self, ) -> None: - from band.adapters.anthropic import AnthropicAdapter + from band.adapters.anthropic import AnthropicAdapter # noqa: PLC0415 -- isolates the anthropic extra from the other frameworks this file tests adapter = AnthropicAdapter( capabilities={Capability.MEMORY, Capability.CONTACTS} @@ -69,7 +72,7 @@ async def test_anthropic_adapter_renders_both_sections_when_both_enabled( assert "## Contact Management Tools" in adapter._system_prompt async def test_gemini_adapter_renders_memory_section_when_enabled(self) -> None: - from band.adapters.gemini import GeminiAdapter + from band.adapters.gemini import GeminiAdapter # noqa: PLC0415 -- isolates the gemini extra from the other frameworks this file tests adapter = GeminiAdapter(capabilities={Capability.MEMORY}) await adapter.on_started("test-agent", "A test agent") @@ -77,9 +80,8 @@ async def test_gemini_adapter_renders_memory_section_when_enabled(self) -> None: assert "## Memory Tools" in adapter._system_prompt async def test_langgraph_adapter_renders_memory_section_when_enabled(self) -> None: - from unittest.mock import MagicMock - from band.adapters.langgraph import LangGraphAdapter + from band.adapters.langgraph import LangGraphAdapter # noqa: PLC0415 -- isolates the langgraph extra from the other frameworks this file tests adapter = LangGraphAdapter( llm=MagicMock(), @@ -97,12 +99,11 @@ async def test_pydantic_ai_adapter_renders_memory_section_when_enabled( We still cover the contract via Anthropic + Gemini + LangGraph above. """ - import os if not os.environ.get("OPENAI_API_KEY"): pytest.skip("PydanticAIAdapter requires OPENAI_API_KEY to start") - from band.adapters.pydantic_ai import PydanticAIAdapter + from band.adapters.pydantic_ai import PydanticAIAdapter # noqa: PLC0415 -- isolates the pydantic_ai extra from the other frameworks this file tests adapter = PydanticAIAdapter( model="openai:gpt-5.4", @@ -119,7 +120,7 @@ async def test_pydantic_ai_adapter_renders_memory_section_when_enabled( async def test_anthropic_adapter_with_no_features_omits_capability_sections( self, ) -> None: - from band.adapters.anthropic import AnthropicAdapter + from band.adapters.anthropic import AnthropicAdapter # noqa: PLC0415 -- isolates the anthropic extra from the other frameworks this file tests adapter = AnthropicAdapter() await adapter.on_started("test-agent", "A test agent") @@ -137,7 +138,7 @@ async def test_claude_sdk_adapter_renders_memory_section_when_enabled( self, ) -> None: """Claude SDK prompt should include memory tools section when MEMORY capability is set.""" - from band.integrations.claude_sdk.prompts import ( + from band.integrations.claude_sdk.prompts import ( # noqa: PLC0415 -- isolates the claude_sdk extra from the other frameworks this file tests generate_claude_sdk_agent_prompt, ) @@ -156,7 +157,7 @@ async def test_claude_sdk_adapter_renders_memory_section_when_enabled( async def test_claude_sdk_adapter_omits_memory_section_when_disabled( self, ) -> None: - from band.integrations.claude_sdk.prompts import ( + from band.integrations.claude_sdk.prompts import ( # noqa: PLC0415 -- isolates the claude_sdk extra from the other frameworks this file tests generate_claude_sdk_agent_prompt, ) @@ -173,7 +174,7 @@ async def test_claude_sdk_adapter_omits_memory_section_when_disabled( async def test_claude_sdk_adapter_renders_contacts_section_when_enabled( self, ) -> None: - from band.integrations.claude_sdk.prompts import ( + from band.integrations.claude_sdk.prompts import ( # noqa: PLC0415 -- isolates the claude_sdk extra from the other frameworks this file tests generate_claude_sdk_agent_prompt, ) @@ -187,14 +188,13 @@ async def test_claude_sdk_adapter_renders_contacts_section_when_enabled( @pytest.mark.skipif(not _HAS_CREWAI, reason="crewai not installed") async def test_crewai_adapter_renders_memory_section_when_enabled(self) -> None: """CrewAI backstory should contain memory instructions when MEMORY capability is set.""" - from unittest.mock import MagicMock, patch with ( patch("crewai.Agent") as mock_agent_cls, patch("crewai.LLM"), ): mock_agent_cls.return_value = MagicMock() - from band.adapters.crewai import CrewAIAdapter + from band.adapters.crewai import CrewAIAdapter # noqa: PLC0415 -- isolates the crewai extra from the other frameworks this file tests adapter = CrewAIAdapter(capabilities={Capability.MEMORY}) await adapter.on_started("test-agent", "A test agent") @@ -204,14 +204,13 @@ async def test_crewai_adapter_renders_memory_section_when_enabled(self) -> None: @pytest.mark.skipif(not _HAS_CREWAI, reason="crewai not installed") async def test_crewai_adapter_omits_memory_section_when_disabled(self) -> None: - from unittest.mock import MagicMock, patch with ( patch("crewai.Agent") as mock_agent_cls, patch("crewai.LLM"), ): mock_agent_cls.return_value = MagicMock() - from band.adapters.crewai import CrewAIAdapter + from band.adapters.crewai import CrewAIAdapter # noqa: PLC0415 -- isolates the crewai extra from the other frameworks this file tests adapter = CrewAIAdapter() await adapter.on_started("test-agent", "A test agent") @@ -221,7 +220,7 @@ async def test_crewai_adapter_omits_memory_section_when_disabled(self) -> None: async def test_anthropic_include_base_instructions_false_drops_base(self) -> None: """include_base_instructions=False renders identity without base instructions.""" - from band.adapters.anthropic import AnthropicAdapter + from band.adapters.anthropic import AnthropicAdapter # noqa: PLC0415 -- isolates the anthropic extra from the other frameworks this file tests adapter = AnthropicAdapter( prompt="Focus on Python.", @@ -241,7 +240,7 @@ async def test_anthropic_include_base_instructions_false_still_renders_capabilit self, ) -> None: """Capability sections render independently of include_base_instructions.""" - from band.adapters.anthropic import AnthropicAdapter + from band.adapters.anthropic import AnthropicAdapter # noqa: PLC0415 -- isolates the anthropic extra from the other frameworks this file tests adapter = AnthropicAdapter( include_base_instructions=False, @@ -257,7 +256,7 @@ async def test_anthropic_include_base_instructions_false_still_renders_capabilit async def test_gemini_include_base_instructions_false_drops_base(self) -> None: """GeminiAdapter honors include_base_instructions=False end-to-end.""" - from band.adapters.gemini import GeminiAdapter + from band.adapters.gemini import GeminiAdapter # noqa: PLC0415 -- isolates the gemini extra from the other frameworks this file tests adapter = GeminiAdapter( prompt="Focus on Python.", diff --git a/tests/test_integrations_base.py b/tests/test_integrations_base.py index 9ead3dfcb..3a2d27ef8 100644 --- a/tests/test_integrations_base.py +++ b/tests/test_integrations_base.py @@ -7,14 +7,14 @@ from unittest.mock import MagicMock +from band.integrations.base import check_and_format_participants + class TestCheckAndFormatParticipants: """Test check_and_format_participants() helper.""" def test_returns_none_when_no_change(self): """Should return None when participants haven't changed.""" - from band.integrations.base import check_and_format_participants - ctx = MagicMock() ctx.participants_changed.return_value = False @@ -25,7 +25,6 @@ def test_returns_none_when_no_change(self): def test_returns_message_when_changed(self): """Should return formatted message when participants changed.""" - from band.integrations.base import check_and_format_participants ctx = MagicMock() ctx.participants_changed.return_value = True @@ -43,7 +42,6 @@ def test_returns_message_when_changed(self): def test_marks_participants_sent_automatically(self): """Should automatically call mark_participants_sent() when returning message.""" - from band.integrations.base import check_and_format_participants ctx = MagicMock() ctx.participants_changed.return_value = True @@ -59,12 +57,12 @@ class TestIntegrationsImport: def test_can_import_from_integrations(self): """Should be able to import check_and_format_participants from integrations.""" - from band.integrations import check_and_format_participants + from band.integrations import check_and_format_participants # noqa: PLC0415 -- pins the exact import path this test exercises assert check_and_format_participants is not None def test_check_and_format_participants_in_all(self): """Should be listed in __all__.""" - from band import integrations + from band import integrations # noqa: PLC0415 -- pins the exact import path this test exercises assert "check_and_format_participants" in integrations.__all__ diff --git a/tests/test_lazy_exports.py b/tests/test_lazy_exports.py index 655bf57c8..c8d28ccd2 100644 --- a/tests/test_lazy_exports.py +++ b/tests/test_lazy_exports.py @@ -83,7 +83,7 @@ def test_first_access_binds_the_name_into_the_package() -> None: A resolved export that never lands in the namespace re-enters importlib on every single read. """ - import band.testing + import band.testing # noqa: PLC0415 -- pins the exact import path this test exercises vars(band.testing).pop("FakeAgentTools", None) @@ -93,7 +93,7 @@ def test_first_access_binds_the_name_into_the_package() -> None: def test_unknown_attribute_raises_attribute_error() -> None: - import band.adapters + import band.adapters # noqa: PLC0415 -- pins the exact import path this test exercises with pytest.raises(AttributeError, match="NoSuchAdapter"): band.adapters.NoSuchAdapter diff --git a/tests/test_readme_snippets.py b/tests/test_readme_snippets.py index 0a980b7c9..d73b56fde 100644 --- a/tests/test_readme_snippets.py +++ b/tests/test_readme_snippets.py @@ -44,34 +44,34 @@ class TestTopLevelImports: """README shows `from band import Agent` and similar.""" def test_agent_import(self) -> None: - from band import Agent, build_logging_config, configure_logging + from band import Agent, build_logging_config, configure_logging # noqa: PLC0415 -- pins the exact import path this test exercises assert Agent is not None assert build_logging_config is not None assert configure_logging is not None def test_adapter_features_and_capability_import(self) -> None: - from band.core.types import AdapterFeatures, Capability + from band.core.types import AdapterFeatures, Capability # noqa: PLC0415 -- pins the exact import path this test exercises assert AdapterFeatures is not None assert Capability is not None def test_adapter_features_shorthand_import(self) -> None: """README uses `from band import AdapterFeatures, Emit`.""" - from band import AdapterFeatures, Emit + from band import AdapterFeatures, Emit # noqa: PLC0415 -- pins the exact import path this test exercises assert AdapterFeatures is not None assert Emit is not None def test_capability_shorthand_import(self) -> None: """README uses `from band import Capability, Emit`.""" - from band import Capability, Emit + from band import Capability, Emit # noqa: PLC0415 -- pins the exact import path this test exercises assert Capability is not None assert Emit is not None def test_exception_imports(self) -> None: - from band import ( + from band import ( # noqa: PLC0415 -- pins the exact import path this test exercises BandConfigError, BandConnectionError, BandError, @@ -92,7 +92,7 @@ class TestQuickstartLangGraph: """README quickstart shows LangGraphAdapter(llm=..., checkpointer=...).""" def test_adapter_import(self) -> None: - from band.adapters import LangGraphAdapter + from band.adapters import LangGraphAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert LangGraphAdapter is not None @@ -104,8 +104,8 @@ def test_adapter_import(self) -> None: }, ) def test_quickstart_instantiation(self) -> None: - from band import Agent - from band.adapters import LangGraphAdapter + from band import Agent # noqa: PLC0415 -- pins the exact import path this test exercises + from band.adapters import LangGraphAdapter # noqa: PLC0415 -- pins the exact import path this test exercises llm = MagicMock() checkpointer = MagicMock() @@ -130,20 +130,20 @@ class TestAdapterSwapSnippets: """README shows short adapter-swap snippets for Anthropic, PydanticAI, Gemini.""" def test_anthropic_adapter_import_and_init(self) -> None: - from band.adapters import AnthropicAdapter + from band.adapters import AnthropicAdapter # noqa: PLC0415 -- pins the exact import path this test exercises adapter = AnthropicAdapter(model="claude-sonnet-4-5") assert adapter is not None @skip_no_pydantic_ai def test_pydantic_ai_adapter_import_and_init(self) -> None: - from band.adapters import PydanticAIAdapter + from band.adapters import PydanticAIAdapter # noqa: PLC0415 -- pins the exact import path this test exercises adapter = PydanticAIAdapter(model="openai:gpt-5.4-mini") assert adapter is not None def test_gemini_adapter_import_and_init(self) -> None: - from band.adapters import GeminiAdapter + from band.adapters import GeminiAdapter # noqa: PLC0415 -- pins the exact import path this test exercises adapter = GeminiAdapter(model="gemini-2.5-flash") assert adapter is not None @@ -158,80 +158,80 @@ class TestSupportedAdaptersTable: """README table lists every adapter with its import path.""" def test_langgraph_adapter(self) -> None: - from band.adapters import LangGraphAdapter + from band.adapters import LangGraphAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert LangGraphAdapter is not None @skip_no_pydantic_ai def test_pydantic_ai_adapter(self) -> None: - from band.adapters import PydanticAIAdapter + from band.adapters import PydanticAIAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert PydanticAIAdapter is not None def test_anthropic_adapter(self) -> None: - from band.adapters import AnthropicAdapter + from band.adapters import AnthropicAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert AnthropicAdapter is not None @skip_no_claude_sdk def test_claude_sdk_adapter(self) -> None: - from band.adapters import ClaudeSDKAdapter + from band.adapters import ClaudeSDKAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert ClaudeSDKAdapter is not None def test_crewai_adapter(self) -> None: - from band.adapters import CrewAIAdapter + from band.adapters import CrewAIAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert CrewAIAdapter is not None def test_crewai_flow_adapter(self) -> None: - from band.adapters import CrewAIFlowAdapter + from band.adapters import CrewAIFlowAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert CrewAIFlowAdapter is not None def test_gemini_adapter(self) -> None: - from band.adapters import GeminiAdapter + from band.adapters import GeminiAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert GeminiAdapter is not None def test_google_adk_adapter(self) -> None: - from band.adapters import GoogleADKAdapter + from band.adapters import GoogleADKAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert GoogleADKAdapter is not None def test_parlant_adapter(self) -> None: - from band.adapters import ParlantAdapter + from band.adapters import ParlantAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert ParlantAdapter is not None def test_letta_adapter(self) -> None: - from band.adapters import LettaAdapter + from band.adapters import LettaAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert LettaAdapter is not None def test_codex_adapter(self) -> None: - from band.adapters import CodexAdapter + from band.adapters import CodexAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert CodexAdapter is not None def test_opencode_adapter(self) -> None: - from band.adapters import OpencodeAdapter + from band.adapters import OpencodeAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert OpencodeAdapter is not None def test_a2a_adapter(self) -> None: - from band.adapters.a2a import A2AAdapter, A2AAuth + from band.adapters.a2a import A2AAdapter, A2AAuth # noqa: PLC0415 -- pins the exact import path this test exercises assert A2AAdapter is not None assert A2AAuth is not None def test_a2a_gateway_adapter(self) -> None: - from band.adapters.a2a_gateway import A2AGatewayAdapter + from band.adapters.a2a_gateway import A2AGatewayAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert A2AGatewayAdapter is not None def test_acp_client_adapter(self) -> None: - from band.adapters.acp import ACPClientAdapter + from band.adapters.acp import ACPClientAdapter # noqa: PLC0415 -- pins the exact import path this test exercises assert ACPClientAdapter is not None @@ -245,7 +245,7 @@ class TestPlatformToolsSnippets: """README shows AdapterFeatures with Capability and Emit.""" def test_capability_set_creation(self) -> None: - from band.core.types import AdapterFeatures, Capability + from band.core.types import AdapterFeatures, Capability # noqa: PLC0415 -- pins the exact import path this test exercises features = AdapterFeatures( capabilities={Capability.CONTACTS, Capability.MEMORY}, @@ -256,8 +256,8 @@ def test_capability_set_creation(self) -> None: def test_adapter_with_features(self) -> None: """README snippet: AnthropicAdapter with capabilities.""" - from band.adapters import AnthropicAdapter - from band.core.types import Capability + from band.adapters import AnthropicAdapter # noqa: PLC0415 -- pins the exact import path this test exercises + from band.core.types import Capability # noqa: PLC0415 -- pins the exact import path this test exercises adapter = AnthropicAdapter( model="claude-sonnet-4-5", @@ -277,7 +277,7 @@ class TestEmitOptionsSnippets: """README shows emit configuration on adapters.""" def test_emit_enum_values(self) -> None: - from band import Emit + from band import Emit # noqa: PLC0415 -- pins the exact import path this test exercises assert hasattr(Emit, "TOOL_CALLS") assert hasattr(Emit, "THOUGHTS") @@ -285,8 +285,8 @@ def test_emit_enum_values(self) -> None: def test_anthropic_with_emit(self) -> None: """README snippet: emit=Emit.TOOL_CALLS.""" - from band import Emit - from band.adapters import AnthropicAdapter + from band import Emit # noqa: PLC0415 -- pins the exact import path this test exercises + from band.adapters import AnthropicAdapter # noqa: PLC0415 -- pins the exact import path this test exercises adapter = AnthropicAdapter( model="claude-sonnet-4-5", @@ -298,8 +298,8 @@ def test_anthropic_with_emit(self) -> None: @skip_no_claude_sdk def test_claude_sdk_with_emit_and_capability(self) -> None: """README snippet: capabilities + emit combined.""" - from band import Capability, Emit - from band.adapters import ClaudeSDKAdapter + from band import Capability, Emit # noqa: PLC0415 -- pins the exact import path this test exercises + from band.adapters import ClaudeSDKAdapter # noqa: PLC0415 -- pins the exact import path this test exercises adapter = ClaudeSDKAdapter( model="sonnet", @@ -313,8 +313,8 @@ def test_claude_sdk_with_emit_and_capability(self) -> None: def test_codex_all_emits(self) -> None: """README snippet: all three emit options on CodexAdapter.""" - from band import Emit - from band.adapters import CodexAdapter + from band import Emit # noqa: PLC0415 -- pins the exact import path this test exercises + from band.adapters import CodexAdapter # noqa: PLC0415 -- pins the exact import path this test exercises adapter = CodexAdapter( emit=Emit.TOOL_CALLS | Emit.THOUGHTS | Emit.TASK_EVENTS, @@ -334,7 +334,7 @@ class TestCustomInstructionsSnippets: """README shows custom_section and prompt params.""" def test_langgraph_custom_section(self) -> None: - from band.adapters import LangGraphAdapter + from band.adapters import LangGraphAdapter # noqa: PLC0415 -- pins the exact import path this test exercises llm = MagicMock() checkpointer = MagicMock() @@ -351,7 +351,7 @@ def test_langgraph_custom_section(self) -> None: assert "support triage" in adapter.custom_section def test_anthropic_prompt(self) -> None: - from band.adapters import AnthropicAdapter + from band.adapters import AnthropicAdapter # noqa: PLC0415 -- pins the exact import path this test exercises adapter = AnthropicAdapter( model="claude-sonnet-4-5", @@ -370,7 +370,7 @@ class TestCustomToolsSnippets: """README shows Pydantic model + callable for custom tools.""" def test_anthropic_custom_tools(self) -> None: - from band.adapters import AnthropicAdapter + from band.adapters import AnthropicAdapter # noqa: PLC0415 -- pins the exact import path this test exercises class WeatherInput(BaseModel): """Get current weather for a city.""" @@ -397,7 +397,7 @@ class TestBYOASnippet: """README shows graph_factory pattern for LangGraph.""" def test_graph_factory_pattern(self) -> None: - from band.adapters import LangGraphAdapter + from band.adapters import LangGraphAdapter # noqa: PLC0415 -- pins the exact import path this test exercises _llm = MagicMock() _checkpointer = MagicMock() @@ -421,7 +421,7 @@ class TestContactManagementSnippets: """README shows ContactEventConfig with HUB_ROOM and CALLBACK strategies.""" def test_contact_event_imports(self) -> None: - from band.runtime.types import ContactEventStrategy + from band.runtime.types import ContactEventStrategy # noqa: PLC0415 -- pins the exact import path this test exercises assert ContactEventStrategy.DISABLED is not None assert ContactEventStrategy.HUB_ROOM is not None @@ -436,8 +436,8 @@ def test_contact_event_imports(self) -> None: ) def test_hub_room_config(self) -> None: """README snippet: Agent.create with HUB_ROOM strategy.""" - from band import Agent - from band.runtime.types import ContactEventConfig, ContactEventStrategy + from band import Agent # noqa: PLC0415 -- pins the exact import path this test exercises + from band.runtime.types import ContactEventConfig, ContactEventStrategy # noqa: PLC0415 -- pins the exact import path this test exercises adapter = MagicMock() @@ -461,9 +461,9 @@ def test_hub_room_config(self) -> None: ) def test_callback_config(self) -> None: """README snippet: Agent.create with CALLBACK strategy + handler.""" - from band import Agent - from band.platform.event import ContactRequestReceivedEvent - from band.runtime.types import ContactEventConfig, ContactEventStrategy + from band import Agent # noqa: PLC0415 -- pins the exact import path this test exercises + from band.platform.event import ContactRequestReceivedEvent # noqa: PLC0415 -- pins the exact import path this test exercises + from band.runtime.types import ContactEventConfig, ContactEventStrategy # noqa: PLC0415 -- pins the exact import path this test exercises TRUSTED_HANDLES = {"@teammate"} @@ -491,7 +491,7 @@ async def handle_contact(event, tools) -> None: def test_contact_request_payload_fields(self) -> None: """Verify payload has from_handle and id fields.""" - from band.client.streaming import ContactRequestReceivedPayload + from band.client.streaming import ContactRequestReceivedPayload # noqa: PLC0415 -- pins the exact import path this test exercises payload = ContactRequestReceivedPayload( id="req-1", @@ -514,7 +514,7 @@ class TestA2ABridgeSnippet: """README snippet: A2AAdapter(remote_url=..., auth=...).""" def test_a2a_adapter_instantiation(self) -> None: - from band.adapters.a2a import A2AAdapter, A2AAuth + from band.adapters.a2a import A2AAdapter, A2AAuth # noqa: PLC0415 -- pins the exact import path this test exercises adapter = A2AAdapter( remote_url="http://localhost:10000", @@ -540,8 +540,8 @@ class TestA2AGatewaySnippet: }, ) def test_gateway_full_snippet(self) -> None: - from band import Agent - from band.adapters.a2a_gateway import A2AGatewayAdapter + from band import Agent # noqa: PLC0415 -- pins the exact import path this test exercises + from band.adapters.a2a_gateway import A2AGatewayAdapter # noqa: PLC0415 -- pins the exact import path this test exercises gateway_port = int(os.getenv("GATEWAY_PORT", "10000")) gateway_url = os.getenv("GATEWAY_URL", f"http://localhost:{gateway_port}") @@ -569,7 +569,7 @@ class TestExceptionHierarchy: """README states BandError is the base for the other three.""" def test_hierarchy(self) -> None: - from band import ( + from band import ( # noqa: PLC0415 -- pins the exact import path this test exercises BandConfigError, BandConnectionError, BandError, @@ -581,7 +581,7 @@ def test_hierarchy(self) -> None: assert issubclass(BandToolError, BandError) def test_exceptions_are_raiseable(self) -> None: - from band import BandConfigError, BandConnectionError, BandToolError + from band import BandConfigError, BandConnectionError, BandToolError # noqa: PLC0415 -- pins the exact import path this test exercises with pytest.raises(BandConfigError): raise BandConfigError("bad config") @@ -609,7 +609,7 @@ class TestQuickReferenceSnippets: }, ) def test_agent_create_and_run_signature(self) -> None: - from band import Agent + from band import Agent # noqa: PLC0415 -- pins the exact import path this test exercises adapter = MagicMock() diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 5238a06ce..480bca24c 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -65,7 +65,7 @@ def test_adapter_features_constructible(): def test_can_import_letta_adapter_via_lazy_loader(): """LettaAdapter resolves through the adapters lazy loader.""" - from band.adapters import LettaAdapter, LettaAdapterConfig + from band.adapters import LettaAdapter, LettaAdapterConfig # noqa: PLC0415 -- pins the exact import path this test exercises assert LettaAdapter is not None assert LettaAdapterConfig is not None @@ -73,7 +73,7 @@ def test_can_import_letta_adapter_via_lazy_loader(): def test_can_import_langgraph_integrations(): """Verify we can import LangGraph integration utilities.""" - from band.integrations.langgraph import ( + from band.integrations.langgraph import ( # noqa: PLC0415 -- pins the exact import path this test exercises agent_tools_to_langchain, graph_as_tool, )